fix(ci): unbreak the pre-push gate — strict-mode leak and apm-owned settings drift

Two pre-existing failures, both red at HEAD before ADR-0020 work began,
both invisible in an ordinary local run.

RUN_TESTS_STRICT leaked from the environment into test-run-tests.sh's
fixture children. The meta-test is itself a suite the runner discovers,
so under the gate's own invocation the variable propagated outer runner
-> batch_run -> the fixture's copy of run-tests.sh, flipping it strict.
Case 10c (a deliberate control asserting a skip is tolerated WITHOUT
strict) then failed. Six further cases were silently running strict too
and asserting against the wrong stream — case 9 was matching the stderr
strict block rather than the stdout skip list it was written to check.
run_fake now spawns via 'env -u RUN_TESTS_STRICT', so fixture strictness
is a property of the case, never of how the file was launched. No
assertion weakened; run-tests.sh itself is untouched.

pretty-format-json --autofix was re-sorting apm's output on the way into
every commit. .claude/settings.json is apm-owned (ADR-0018/0019) and its
exclude list named fifteen generated manifests but not this file, so
since 2e395a4 it has been committed in a key order apm would never write
— permanent drift on a file with an empty git diff. Content was always
byte-identical; only JSON key order differed. The exclude ships in the
same commit as the corrected file because otherwise the hook re-breaks
it during staging.

apm.lock.yaml: generated_at churn, plus lint's exec_status corrected from
'deployed' to 'gated_pending_approval' — executables.allow grants only
kyberforge#1.5.0, so lint's hooks/bin are genuinely gated.

New coverage: an ambient RUN_TESTS_STRICT must not reach a fixture that
did not ask for it, and under --strict the skip report goes to stderr
only with the stdout list suppressed. Neither was pinned.
This commit is contained in:
2026-08-14 21:54:00 +00:00
parent 36ba7a18f8
commit 76075223c7
5 changed files with 122 additions and 8 deletions

View File

@@ -203,3 +203,24 @@ this skill to manage label definitions`). That is the `Kyberforge.VagueQualifier
Fix: keep the opener rule opener-anchored and give mid-description prose its own rule with its own
token list. A rule's scope anchor is part of its contract, not an implementation detail to relax when
a new case does not fit.
## 2026-08-14 — A formatter in the commit path manufactures drift on a file with a clean git diff
`apm audit --ci` failed for weeks on `.claude/settings.json` while `git diff` on that file was empty —
the worst possible pairing of signals, because the file matched HEAD exactly and every instinct says
"nothing changed here". The content was identical to apm's output to the byte; only the JSON key
order differed. `pretty-format-json --autofix` sorts object keys unless `--no-sort-keys` is passed,
and its `exclude:` listed fifteen generated manifests but not this file, so from the commit that
first wrote a hook entry there (`2e395a4`) onward, apm's insertion-ordered output was silently
re-sorted on the way in. apm then replayed the install, produced its own order, and reported drift
against a file no human had touched.
Two general points. First, a tool-owned generated file that passes through an autofixing formatter is
drifted by construction, and the diff that would reveal it never appears in `git diff` — it only
exists between the formatter's input and its output, which nothing stores. Second, the fix is
self-undoing unless the exclude lands in the same commit: correcting the file alone means the hook
re-breaks it as it is staged. Fix: when a tool declares ownership of a path, add that path to every
autofixing hook's `exclude` at the moment ownership is declared, not when the drift is noticed. This
repo gates marketplace-mirror, plugin-content and vale-style drift deterministically and has no
equivalent gate asserting tool-owned paths stay out of formatter scope — `.claude/settings.json` was
the sixteenth exclude and nothing prevents a seventeenth.