Skip to content

S2.6.5 — The anti-vacuity assertion set, asserted by the runner #322

Closed

Description

@dev-pmallapp

Parent: #293

Build target 6. Anchors: CLAIM-293.3, NEVER-293.4. Cases: 3, 11, 5, 6, 7, 21, 22.

Blocked by: #321

The single most important rule in this Story

Every == 0 assertion must be paired with a > 0 assertion on the same counter, measured one run earlier.

"Run 2 made zero mutations" is meaningless alone. "Run 1 made more than zero and run 2 made zero" is the property. This generalises the house denominator rule from corpus size to effect count.

The scanning denominators stop the harness reporting success over zero skills. Only run1.mutations > 0 stops it reporting success over zero work. A harness with the first and without the second is skill-lint-green-over-zero-files with extra steps: it scans four skills, executes nothing meaningful, and reports a perfect idempotency result.

The assertion set

Run-level:

  • scenarios_executed > 0
  • skills_covered == |skills/*/SKILL.md|, both sides read at run time — mutation J2: delete a verb directory and every "zero violations over the real corpus" loop stays green
  • skills_covered == PINNED_SKILL_COUNT, a hardcoded literal citing the plan — J2 and this fail for different reasons and neither substitutes; a disk-read on both sides passes a corpus of three verbs and a deleted one
  • scenarios == SCENARIO_ROSTER.length
  • every skill has ≥ 1 scenario, asserted per-skill — an aggregate N > 0 is satisfied by four scenarios all on one verb

Per-scenario, and the ordering matters:

  • run1.mutations > 0 evaluated before run2.mutations == 0 is considered
  • run2.reads >= that skill's ## Re-entry row count, read from its SKILL.md at run time — this distinguishes "decided to do nothing" from "did nothing". A run 2 with zero mutations and zero reads did not run. The least obvious assertion here and the one most likely to be omitted
  • run2.mutating_argv == [] reported as the list, not the count

Verdict mechanics — non-negotiable

The verdict is the exit code plus a JSON artifact. The runner never greps its own stdout, and nothing greps the runner's.

Finding 37: a mutation harness decided on "0 fail" not in stdout, and the corpus under test contained the literal "63 pass, 0 fail" — it reported SURVIVED for a mutation that killed two tests. Its recorded owner is "any future harness". This is it. docs/evidence/ is full of the phrases this runner will print.

The runner exits non-zero on an empty roster, and a test invokes it against one. skill-lint exits 0 over zero files today and its non-vacuity lives only in a test file — do not repeat that.

Done when

  • All the above, asserted by the runner's exit code, not only by a test file (cases 3, 11)
  • Three mutations killed: empty roster, deleted verb directory, run 1 mutating nothing — each naming the scenario (cases 5, 6, 7)
  • A fixture repository seeded with the runner's own success phrases still yields the correct verdict (cases 21, 22)

Refs #293

Activity

  1. added
    type:taskOne unit of work within a Story
    domain:devSoftware development pack
    iaiCreated or managed by iAI
    on Sep 7, 2026
  2. dev-pmallapp commented on Sep 10, 2026

    @dev-pmallapp
    OwnerAuthor

    iai-evidence

    Verdict: PASS. Artifact: docs/evidence/322-20260910T042657Z.md, pinned at 95c0af46749e5b9d912c7419beaa64c695309e8d. PR #333.

    Build target 6 of docs/design/stories/293.md. Cases 3, 5, 6, 7, 11, 21, 22 PASS. CLAIM-293.3 and NEVER-293.4 are met for the harness's transcription of each skill, which is a different population from the skill.

    bun run skill-harness exists for the first time. The command named in seven test-plan cases was in no package.json in this tree until now.

    Chain

    1038 tests / 1024 pass / 0 fail / 14 skip / 3777 expect() · lint 0 across 121 files · typecheck clean · skill-lint 4 + 12, 0 errors · claim-lint 120 files, 0 errors, 1 warning (the expected stories/9.md one) · bun run skill-harness exit 0 · both verifier scripts PASS, required contexts still six. CI on PR #333 is 7/7.

    The measurement

    Scenario run 1 run 2 Re-entry rows
    goal-create/absent-milestone mutations 1, reads 3 0, reads 3 3
    story-create/two-unstoried-rows mutations 2, reads 4 0, reads 4 4
    story-design/design-and-sentinel mutations 4, reads 4 0, reads 4 4
    story-test-plan/plan-and-sentinel mutations 4, reads 5 0, reads 5 5
    story-design/sentinel-only forge only — 1, reads 4 0, reads 4 4
    story-test-plan/file-only worktree only — 3, reads 5 0, reads 5 5

    Every threshold in the right-hand column is read off skills/*/SKILL.md at run time. run2.reads >= rows is what separates "decided to do nothing" from "did nothing" — a run 2 with zero mutations and zero reads did not run.

    The last two scenarios are the surface-separation pair, which is why the mutation counter is the union of both surfaces: a forge-only count scores story-test-plan/file-only as a vacuous run 1, and mutation M8 measures exactly that.

    Mutation testing — 16 injected, 13 killed, 3 survivors

    # Mutation Result
    M1 the roster emptied KILLED — the CLI, on the empty-roster message specifically
    M2 a verb directory renamed away KILLED — coverage mismatch naming the verb, plus the pinned-count failure
    M3 goal-create's create step disabled KILLED — run-1-made-no-mutation; artifact shows run2: null
    M4 run 2 executed before run 1's > 0 check KILLED by the unit test only
    M5 countReads becomes the subtraction KILLED by the unit test only
    M6 the reads threshold hardcoded to 4 KILLED — by goal-create, not the skill predicted
    M7 per-skill coverage replaced by an aggregate KILLED by the unit test only
    M8 mutations counts the forge only KILLED — by the worktree-only scenario
    M9 the run-2 tree-hash check deleted SURVIVED — entailed by the evidence being empty
    M10 a fresh fake forge before run 2 KILLED, after a fix
    M11 the verdict re-derived from rendered text KILLED, after a fix — statically
    M12 renderArtifact forces verdict: "pass" KILLED, after a fix
    M13 the wrapper hardcodes process.exit(0) KILLED
    M14 test-only weakening SURVIVED — the control half
    M14b M14 plus a real defect KILLED, by a different test
    S1 two ## Re-entry rows reordered SURVIVED, by design

    Three survived their first run, and each was a real gap now closed and re-killed:

    1. M10 crashed instead of reporting. A fresh forge before run 2 made a transcription throw and the runner died with an unhandled rejection — no verdict, no artifact, no scenario named, exactly when the artifact is most wanted. Now a scenario-threw failure, one failure per cause.
    2. M11 was behaviourally equivalent, so no dynamic test could ever kill it. renderReport(...).includes("verdict: pass") returns the identical answer for every fixture in the corpus. The kill had to be static: decideVerdict's own source may not contain renderReport, renderArtifact, SUCCESS_PHRASES, .includes(" or stdout, with the extracted body asserted non-empty first. This is finding 37, whose recorded owner was "any future harness".
    3. M12 exposed an unasserted field. renderArtifact could emit verdict: "pass" on a failing run with every test green, because the only agreement assertion compared exit codes and the real roster never fails. S2.6.6 — The seam rung and its CI job #323's CI job may read that artifact — the disagreement would have surfaced as a green job over a red run.

    M4, M5 and M7 leave bun run skill-harness exit 0 and unchanged, and die only to synthetic scenarios built to trip them:

    A green run of the real roster is not evidence that the runner's checks work. It is evidence that the roster passes them. A task that shipped only the real roster would have shipped all three undetected.

    M9 survived everywhere and is kept anyway — the tree-hash check is entailed by run 2's mutation evidence being empty over this roster, retained because the issue mandates it and M10 is its one independent killer. Recorded as redundancy rather than hidden. S1 survives by design and must not be "fixed": reordering a ## Re-entry row changes not one assertion, which is NEVER-293.5 working.

    Findings raised

    1. S2.6.4 — The two-surface mutation recorder #321's write-seam lint was RED AGAINST CORRECT CODE, and the class was already written down. It matched the bare word writeFile anywhere in a module; runner.ts exposes a context method that only delegates to recorder.writeFile and imports no fs at all. packages/exec/test/port.test.ts had already rewritten its launcher detector from a call shape to an import for exactly this reason, and the standing rule is that for an assertion — unlike a ban — a false positive must be fixed. Knowing the class did not prevent reproducing it one task later. The detector now reads the capability off the import list, with a regression test asserting a delegating module is a non-writer.
    2. A read may not be computed by subtraction. isMutatingGhArgv returns false for every git argv by construction, so calls.length - attemptedMutations.length scores git add and git commit as reads and a run 2 that committed four times would satisfy reads >= 4. M5 is the measurement: the subtraction passes the real roster and dies only to a deliberately mixed one.
    3. The fake forge models neither a milestone PATCH nor --milestone on gh issue edit. Two ## Re-entry branches are dead in this roster for a correct reason. They were left implemented and unexercised rather than forced: a scenario built to reach them would be built around the fake's gaps. Owner: S2.6.6 — The seam rung and its CI job #323's audit and S2.6.7 — The live rung, never required #324's live rung.

    What this does NOT establish

  3. dev-pmallapp commented on Sep 10, 2026

    @dev-pmallapp
    OwnerAuthor

    Closed by the merge of PR #333 into story/293-s2-6-skill-execution-harness at 6e05328.

    A task PR targets the story branch, so GitHub's closing keywords never fire (CONTRIBUTING.md, Pull requests); this closure is explicit and cites the merge commit.

    Verified PASS before merge — evidence docs/evidence/322-20260910T042657Z.md, ## iai-evidence sentinel posted, CI 7/7 on the PR head 95c0af4.

    Carried forward to #323, deliberately not closed here:

    • CLAIM-293.11 / case 18 — the four transcriptions are unaudited; ruling on them here would have been self-attestation.
    • CLAIM-293.6 half (ii) — --inject is deliberately absent from the CLI; case 9 needs the truncated-list lie armed per scenario.
    • Build target 7 — the seam rung, its job-list edit and the seventh required context.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:devSoftware development packiaiCreated or managed by iAIstatus:resolvedWork complete, awaiting closuretype:taskOne unit of work within a Story

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions