Skip to content

fix(ca): the coverage gate could not run, in any tree (#507)#513

Merged
SUaDtL merged 6 commits into
mainfrom
feat/507-coverage-tooling
Jul 26, 2026
Merged

fix(ca): the coverage gate could not run, in any tree (#507)#513
SUaDtL merged 6 commits into
mainfrom
feat/507-coverage-tooling

Conversation

@SUaDtL

@SUaDtL SUaDtL commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Closes #507. Step 3 of the chain; stacked on #510 (base is chore/vitest-4-ca-tools, so the diff stays clean — GitHub retargets to main when #510 merges).

The defect

tdd Phase 5 and refactor Phase 2/6 instruct "run the coverage command from tech-stack.md", and both carry a hard rule forbidding guessing one. No coverage command existed — no provider in any of the three TypeScript trees, no script, and tech-stack.md contained the word "coverage" zero times.

So every run reached the phase, found nothing to run, and passed through on a gap. A BLOCK gate that cannot execute is worse than an absent one — it reads as satisfied in every lane. Same defect class as #501 (five suites running on nothing) and #506 (an assertion that restated the bug).

refactor was the worse casualty. Phase 2 is "Behavioral parity coverage proof" — the gate that justifies the entire lane by proving pre-existing tests can detect a behaviour change before production code is touched. It rested wholly on a command that did not exist, leaving the lane running on Phase 5's "zero pre-existing tests modified" alone.

What lands

Tooling@vitest/coverage-v8@4.1.9 in all three trees, npm run coverage in each, scope/provider/reporters in each vitest.config.ts so the invocation is argument-free and platform-identical. ca/tools gets its first vitest.config.ts; coverage output is gitignored.

The metric, named. The threshold table said only "minimum coverage". That omission was load-bearing, not cosmetic — a report gives four numbers that disagree:

tree lines branches statements functions
plugins/ca/tools 67.22% 59.46% 63.91% 56.77%
plugins/ca-pi/tools 85.37% 78.73% 80.12% 85.31%
plugins/ca-sandbox/tools 86.13% 79.96% 85.49% 81.59%

ca-pi is compliant at maturity 3 on lines and non-compliant on branches. "≥ 85%" with no column named is not a threshold anyone can be held to.

Now lines and branches, both binding. Lines catches code no test reaches — which is exactly what #504 turned out to be, a catch with zero executions inside a 750-test suite. Branches catches the untaken half of a condition a test does reach, which lines alone reports as covered. Statements duplicates lines; functions is noisiest at small counts.

A guard against recurrence. GateCommandTest asserts that any command a gate reads from tech-stack.md is actually defined there. Verified in both directions rather than assumed — run against the pre-fix file it fires, and it correctly identifies both demanders (tdd, refactor).

Deliberate non-choices

  • Threshold not encoded in the vitest configs. The skills apply it from CONTEXT.md stage: against maturity-coverage.md. Copying the number into three package configs forks the source of truth, and the copies drift the first time the stage moves.
  • No CI job enforces coverage. ca/tools is below the stage-2 floor, so wiring it into required CI would block every merge in the repo on an unrelated backfill. It is an orchestrator gate, which is what the skills already assume.
  • stage: stays at 2. Raising to 3 today blocks all three trees — ca-pi fails branches at 78.73, ca-sandbox at 79.96. A gate nothing can pass gets overridden, and an overridden gate teaches readers it is noise.
  • The ca/tools shortfall is plugins/ca/tools is below the stage-2 coverage floor on lines and branches #511, not this PR. Folding it in would turn tdd Phase 5 cannot run: no coverage command exists for any plugin #507 into "write several hundred statements of tests before anything ships".
  • Python hooks stay unmeasured. Both skills now say so explicitly, so the phase is recorded as having no numeric floor rather than silently skipped.

Versioning

The surface renders into three hosts, so this is a payload change for ca, ca-codex and ca-pi — not just a tools/ edit.

  • ca-pi 0.1.34 → 0.1.35 + changelog + regenerated root manifest. Its guard demands a strict advance on any payload change regardless of tag: Pi payload, version, changelog, and root metadata advanced together: 0.1.34 -> 0.1.35.
  • ca (2.9.1) and ca-codex (0.3.0) are unpublished (no v2.9.1 / ca-codex-v0.3.0 tag), and their gates permit a payload change on an unpublished version. Entry added under root ## [Unreleased].

Verification

  • npm run coverage green in all three trees; ca/tools still discovers all 324 tests after gaining a config
  • 23 python suites + unittest discover over plugins/ca/hooks/tests + check-plugin-refs.py
  • sync-core.py --check, build-surface.py --check, build-host-packages.py --check — all in sync
  • test_pi_package.py's exact workspace contract updated for the new script and dependency
  • tsc --noEmit clean in all three trees

build-surface --check caught me editing rendered files instead of core/surface/ templates; the edits were moved to source and re-rendered.

https://claude.ai/code/session_01JgdLb6n8mUdkFiKDTA37ML

SUaDtL added 3 commits July 26, 2026 15:15
`plugins/ca/tools` was the last tree on vitest 3. ca-pi pins 4.1.9 exactly and
ca-sandbox resolves to it, so one sibling ran a different test-runner major
than the other two - divergent behavior, divergent docs, and a migration debt
that only grows. It also blocked #507: `@vitest/coverage-v8` peer-pins vitest
exactly, so coverage tooling could never reach this tree, and the v3-compatible
fallback carries 5 HIGH advisories that fail the dev-inclusive audit gate.

Pinned exact rather than caret, matching ca-pi, because the coverage-v8 peer is
an exact pin and a drifting specifier ERESOLVEs on any single-side bump.

The lock now carries rolldown, lightningcss, @emnapi and @napi-rs/wasm-runtime,
which vitest 4 uses for its test transform. None of it reaches the artifact:
`farm.js` is built by esbuild invoked directly, not by vitest's pipeline. That
is verified rather than argued - the built file is byte-identical, and the
absence check required by security-controls.md:266-268 finds no rolldown,
lightningcss, emnapi, napi-rs, `.node`, `.wasm`, or vite reference in it, with
zero non-`node:` requires.

Licenses need no new decision. security-controls.md already extended MPL-2.0
and 0BSD to development-only tooling under `plugins/*/tools` on 2026-07-14,
naming the ca-pi Vitest 4.1.9 lock gate as the case it resolved. This puts
ca/tools into that same approved posture: its lock is now a superset of the
already-approved ca-pi lock by exactly one package, `tsx`, which it already
declared.

No test changed. 323 passed, 1 skipped before and after.

CHANGELOG: none - development tooling only, no shipped payload change.
Ref: #507
Claude-Session: https://claude.ai/code/session_01JgdLb6n8mUdkFiKDTA37ML
`tdd` Phase 5 and `refactor` Phase 2/6 instruct "run the coverage command from
`tech-stack.md`", and both forbid guessing one. No coverage command existed -
no provider installed in any of the three TypeScript trees, no script, and the
word "coverage" appeared in tech-stack.md zero times. Every run reached the
phase, found nothing to run, and passed through on a gap.

A BLOCK gate that cannot execute is worse than an absent one: it reads as
satisfied in every lane. Same defect class as #501's five suites that ran on
nothing and #506's assertion that restated the bug.

`refactor` was the worse casualty. Phase 2 is "Behavioral parity coverage
proof" - the gate that justifies the entire lane by showing the pre-existing
tests can detect a behavior change BEFORE production code is touched - and it
rested wholly on a command that did not exist. The lane was running on Phase
5's "zero pre-existing tests modified" alone.

The threshold table never named a metric, and that omission was load-bearing
rather than cosmetic: a report gives four numbers that disagree, so "at least
70%" with no column named is not something anyone can be held to. ca-pi
measured 85.37% lines against 78.73% branches - compliant at maturity 3, or
not, depending purely on which column the reader picked. It is now lines AND
branches, both binding. Lines catches code no test reaches at all, which is
what #504 turned out to be: a `catch` with zero executions inside a 750-test
suite. Branches catches the untaken half of a condition a test does reach.

The threshold is deliberately NOT encoded in the three vitest configs. The
skills apply it from CONTEXT.md `stage:` against maturity-coverage.md; copying
the number into three package configs would fork the source of truth and the
copies would drift the first time the stage moves.

No CI job enforces coverage, also deliberately. `plugins/ca/tools` is below the
stage-2 floor (67.22% lines, 59.46% branches), so wiring it into required CI
would block every merge in the repo on an unrelated backfill. That backfill is
tracked as its own issue rather than absorbed here, which would have turned
#507 into "write several hundred statements of tests".

A contract test now asserts that any command a gate reads from tech-stack.md is
actually defined there. Verified against the pre-fix file rather than assumed:
it fires.

CHANGELOG: tdd Phase 5 and refactor Phase 2/6 can run their coverage gate; the
maturity threshold now specifies lines and branches, both binding.
Closes #507
Refs #511
Claude-Session: https://claude.ai/code/session_01JgdLb6n8mUdkFiKDTA37ML
# Conflicts:
#	.codearbiter/gate-events.log
#	plugins/ca/tools/package-lock.json
@SUaDtL

SUaDtL commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

Replaces #512, which GitHub auto-closed when --delete-branch on #510 removed its base branch. A closed PR cannot be retargeted or reopened, so this is the same commit (2681e57) plus a merge of main.

Merge resolution, for the record:

  • .codearbiter/gate-events.log — resolved as an append-only union (1009 rows from main, 16 branch-only rows appended). --ours and --theirs each discard real audit rows.
  • plugins/ca/tools/package-lock.json — took this branch's superset (vitest 4 + coverage-v8), then regenerated with npm install --package-lock-only so it is npm's own output rather than a hand-merged lock. Verified: declared and locked versions agree at 4.1.9 for both packages.

Re-verified after the merge, not just before it:

  • clean npm ci --ignore-scripts + npm run buildfarm.js unchanged
  • npm run coverage in ca/tools → 324 tests, 67.22% lines / 59.46% branches (unchanged, as expected)
  • 21 python suites + hooks unittest + all four generator --checks → pass
  • ca-pi release guard against the new main: 0.1.34 -> 0.1.35

#509's --ignore-scripts guard and this PR's GateCommandTest now coexist in test_ci_impact.py; the suite passes with both.

SUaDtL added 2 commits July 26, 2026 16:34
…esolve

Adversarial review of this PR returned BLOCK, and both blockers were #507's own
defect class reproduced inside the fix for #507. That is the worst possible
outcome for this change, so they are fixed here rather than deferred.

BLOCK-1 - the guard never ran for the files it guards. GateCommandTest lives in
test_ci_impact.py, which CI invokes only in the path-scoped `ci-impact` job.
Neither `.codearbiter/tech-stack.md` nor `plugins/ca/skills/**` was in the
`impact` filter, and tech-stack.md was in no push trigger either. So a PR whose
only change was deleting the coverage section - precisely the regression the
guard exists to stop - skipped the one job that runs it. It was green on its own
introducing PR only because that PR also edited test_ci_impact.py, which is
self-referential and proves nothing about any later change.

This is the repo's own established rule, violated: #384 for the parity fixture,
#403/#404 for gitleaks and docs.yml, #416 for the decisions directory. Both
subjects are now registered, with a reachability test mirroring
test_secret_scan_config_edits_reach_the_guard_that_constrains_them.

BLOCK-2 - the guard checked the prose, not the command. It asserted a regex
matched inside a markdown file and stopped there. Deleting the `coverage` script
from two of the three package.json files left tech-stack.md naming a command
that no longer existed, with every assertion still green. The guard now resolves
`npm --prefix <dir> run <script>` and asserts the script exists in that manifest.

The DEFINITIONS map is also checked in BOTH directions now. The first cut carried
a `typecheck` entry matching no prose - asserting nothing while looking like
protection - and omitted `test`, which is genuinely demanded. Running the
bidirectional check immediately surfaced a third live demander neither I nor the
map knew about: dependency-reviewer reads an `audit` command. A one-way map had
no way to notice. Scope also widened from skills to skills AND agents.

Also fixed, from the same review:

- coverage-auditor carried a hardcoded copy of the threshold table. The copies
  diverged the moment the canonical one gained the lines-and-branches rule,
  leaving the agent the gate DISPATCHES applying a one-number test the gate no
  longer used. Handed ca-pi's real report at stage 3, the skill says BLOCK and
  the auditor's own instructions permitted PASS. The duplicate is deleted; it
  reads maturity-coverage.md.
- refactor Phase 6 lacked the no-tooling clause Phase 2 gained, so a Python
  surface cleared Phase 2 under the documented exemption and then hit an
  unclearable Phase 6. Same skill, two answers about the same surface.
- the documented ca/tools baseline did not reproduce. tech-stack.md said 67.22%
  lines; a clean checkout gives 67.31%. The gap is `plugins/ca/tools/.farm/`,
  dev-tree state absent from a fresh clone. Corrected to the clean-checkout
  figure, which is what CI and every other contributor measure. #511 updated.

CHANGELOG: the coverage-gate guard now runs for the files it guards and verifies
the command resolves; coverage-auditor reads the shared threshold table.
Refs #507
Claude-Session: https://claude.ai/code/session_01JgdLb6n8mUdkFiKDTA37ML
Re-review returned PASS WITH CONCERNS. Both blockers were confirmed fixed by
mutation, but two MEDIUMs remained - and both were the same fail-open shape the
fix had just corrected, sitting one layer further down. Leaving those in the fix
for a fail-open gate is not a trade worth making, so they are closed here.

MEDIUM-9 - the resolution check scanned, then verified whatever it happened to
parse. Zero matches produced an empty finding list and a green test. Its regex
matched only `npm --prefix X run Y`, so two working invocations evaded it
entirely:

    npm run coverage --prefix plugins/ca/tools
    npm --prefix=plugins/ca/tools run coverage

Both were confirmed green by the reviewer with the `coverage` script deleted
from all three manifests. Fixed three ways: the parser now reads prefix and
script independently of argument order and accepts `--prefix=`; the scan asserts
it is non-vacuous; and a new completeness check derives the expected set from
the `plugins/*/tools` manifests rather than from the prose, so a tree cannot be
added - or documented in a phrasing the parser misses - without being caught.

MEDIUM-10 - the `test` entry proved only that a `## Test` heading was followed
by a non-empty fence. Replacing the body with a bare `:` deleted every real
invocation and stayed green.

The first attempt at that fix did not work, and my own mutation harness is what
caught it: `.*?` under DOTALL scans PAST the section, so the pattern matched a
later ```sh fence from `## Coverage` and reported the gutted section as fine. It
is now tempered with `(?:(?!^## )[\s\S])*?` so it cannot leave the section.
This is worth recording because the untempered version LOOKED strictly tighter
than what it replaced and was not - a green assertion agreeing with the defect,
which is the failure mode this whole issue is about.

The DEFINITIONS comment now records honestly that `coverage` is the only entry
backed by resolution and that `test` and `audit` are text patterns, rather than
claiming uniform strength the map does not have.

Mutation-verified, six cases plus a control:

    baseline (unmutated)                            PASS
    `npm run coverage --prefix X` + scripts deleted  FIRES
    `npm --prefix=X run coverage` + scripts deleted  FIRES
    no npm invocation at all (vacuity)               FIRES
    a tree's coverage script undocumented            FIRES
    `## Test` body gutted to a bare `:`              FIRES
    coverage section deleted entirely                FIRES

The harness itself needed fixing first: its control was red because the temp
tree omitted `tools/ci-impact.py`, which test_ci_impact.py exec_module()s at
import. Every case reported a uniform meaningless red, which proves nothing -
a mutation test whose control fails is worthless.

LOW-11 - root CHANGELOG updated; the entry described only the original text
check and understated what the guard now does.

CHANGELOG: the gate-command guard resolves commands in any npm argument order,
refuses to pass on an empty scan, and derives its expected set from the
manifests.
Refs #507
Claude-Session: https://claude.ai/code/session_01JgdLb6n8mUdkFiKDTA37ML
Third adversarial pass. Two more MEDIUMs, both leaving a tree where the gate
cannot run - #507's own definition - so both are closed rather than filed.

MEDIUM-12 - the completeness check derived its obligation from the thing it was
enforcing. It skipped any tree without a `coverage` script, so deleting the
script deleted the requirement to document it. Two coordinated edits - drop the
script, drop the doc line - and a tree leaves the coverage gate with every
assertion green and no audit entry.

The incentive gradient made that worse than the arithmetic suggests.
`plugins/ca/tools` is the one tree below the floor, so a change there hits an
unclearable BLOCK today. The ways out are backfill (#511), `/ca:override`
(logged), or quietly drop the coverage command - and the guard closed the
alternatives while leaving the silent door open, for the tree under the most
pressure to use it.

It is now keyed off `test`, which states the actual rule - a tree that runs
tests owes a coverage command - and cannot be satisfied by deleting the subject.

MEDIUM-13 - `site/` is a fourth tested TypeScript tree: real vitest suites under
`site/test/`, a `test` script, and no coverage command. The completeness check
globbed `plugins/*/tools`, so it structurally could not see it. My own commit
message claimed "complete by construction ... a new tree cannot be added
undocumented", which was true within `plugins/*/tools` and not true of the repo.

The glob is now repo-wide. `site/` is held in an explicit `COVERAGE_EXEMPT` set
citing #514, with a companion test asserting every exemption still names a real
tested tree - a stale exemption fails rather than silently widening. The gap is
visible and reviewed instead of structural. It is not closed; #514 owns that,
along with `site/` still being on vitest 3.

LOW-14 - the non-vacuity assert was global, satisfied by the unrelated `build`
and `typecheck` invocations in the same file. Scoped to `coverage`, so it means
what its message says.

LOW-15 and the over-built question - the reviewer was asked whether any of this
should be deleted rather than kept, and answered yes, in one place: the `test`
entry had grown a 90-character regex with three false-failure modes (`python3`,
a backtick inside a comment, a ```bash fence) to protect the one token that
needs it least, since a missing test command is loud. All three would have fired
on a legitimate edit, and a guard that cries wolf teaches people to weaken it.
Replaced with a short structural check in Python, and the asymmetry is now a
named design decision: `coverage` is resolution-backed, `test` and `audit` are
structural.

Mutation-verified, twelve cases including three that must NOT fire:

    baseline (unmutated)                              green
    drop ca/tools coverage script AND its doc line    FIRES
    a tested tree undocumented and unexempt           FIRES
    stale exemption (site/ removed)                   FIRES
    all coverage lines rewritten to `-C` form         FIRES
    `npm run coverage --prefix X` + scripts deleted   FIRES
    `## Test` gutted to a bare `:`                    FIRES
    `## Test` reduced to comments only                FIRES
    coverage section deleted entirely                 FIRES
    python3 in the Test fence                         green
    a backtick inside a Test comment                  green
    fence tagged ```bash                              green

CHANGELOG: the coverage completeness check is keyed off tested trees repo-wide,
with site/ held as a named, asserted exemption rather than an invisible gap.
Refs #507, #514
Claude-Session: https://claude.ai/code/session_01JgdLb6n8mUdkFiKDTA37ML
@SUaDtL
SUaDtL merged commit c9c5cbc into main Jul 26, 2026
43 checks passed
@SUaDtL
SUaDtL deleted the feat/507-coverage-tooling branch July 26, 2026 21:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tdd Phase 5 cannot run: no coverage command exists for any plugin

1 participant