Skip to content

S2.6.4 — The two-surface mutation recorder #321

Closed

Description

@dev-pmallapp

Parent: #293

Build target 5. Anchors: CLAIM-293.8. Cases: 4, 10.

Blocked by: #318

There are TWO mutation surfaces, and an argv-only recorder covers half the skills

Surface Who mutates it Detected by
the forge all four skills — milestone, issues, sentinel comments mutating-argv classifier
the git worktree story-design writes a Design; story-test-plan writes a plan; both commit worktree tree hash

An argv-only recorder scores a Design rewritten with identical bytes as zero mutations. That is wrong twice: it is an action that can race, and it means the "zero mutations" denominator quietly covers two of four skills.

The verdict definition

(mutating-argv set is empty) AND (tree hash unchanged) AND (no write syscall to a tracked path occurred) — the third clause because writing identical bytes leaves the hash equal while still having been a write.

The semantic model already exists in the tree

GhPlan = readonly Argv[] treats an empty command list as a successful outcome, and planLabelTransition already returns "nothing to do" as success rather than as a failed create. That is the zero-mutation result modelled as a first-class value — mirror it rather than inventing a second vocabulary.

Neither surface may be silently unwired

Mutation N2's shape: "skills" was left in SCOPE_DIRS but never walked, and four of five assertions still passed. Only a run-time equality between the printed denominator and the files on disk caught it.

So each surface needs at least one scenario that trips ONLY it — a comment-only scenario and a file-only scenario.

Done when

  • Argv mutations and worktree tree-hash changes recorded and reported as two separate counts (case 4)
  • An identical-bytes rewrite is reported as a mutation (case 10)
  • Each surface has a scenario tripping only it, so neither can be silently unwired
  • At least one scenario per file-writing skill satisfies run1.tree_hash != pre.tree_hash — proving the worktree surface was armed
  • Attempted mutating argv is recorded, not only successful — required by S2.6.3 — The stateful fake forge: able to lie, and non-enforcing #320's non-enforcing fake

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 9, 2026

    @dev-pmallapp
    OwnerAuthor

    iai-evidence

    Verdict: PASS. Artifact: docs/evidence/321-20260909T125257Z.md, pinned at dc5e8ee4fe60cca36654fed19de4a20b775c7ac5. PR #332.

    Build target 5 of docs/design/stories/293.md. Case 4 PASS, case 10 PASS. CLAIM-293.8 is met for the recorder; the per-skill half of its threshold closes at #323.

    Chain at 761bb1d

    987 tests / 973 pass / 0 fail / 14 skip / 3513 expect() · lint 0 across 109 files · typecheck clean · skill-lint 4 + 12, 0 errors · claim-lint 117 files, 0 errors, 1 warning (the expected docs/design/stories/9.md one). CI on PR #332 is 7/7.

    The six-scenario matrix

    Scenario attemptedMutations writes HEAD tree dirty
    N control — reads only 0 0 = = false
    F forge only — gh issue comment 1 0 = = false
    W worktree only — differing bytes, uncommitted 0 1 identical: false = = true
    I case 10 — byte-identical rewrite 0 1 identical: true = = false
    C differing bytes, committed 0 1 ≠ ≠ false
    E identical rewrite + git commit --allow-empty 0 1 identical: true ≠ = false

    The zeros are asserted, not implied. The non-zeros prove each surface is wired; only the zeros prove the two are independent. One counter fed by both inputs passes every non-zero assertion and reports "two surfaces" while having one — mutation N2's shape.

    N is where this task's non-vacuity comes from, not from an exit code: there is no runner, and #322 owns it. E is why there are three worktree observables — git commit --allow-empty after an identical rewrite moves HEAD while leaving HEAD^{tree} byte-identical.

    Mutation testing — 14 injected, 12 killed, 2 survivors by design

    # Mutation Result
    M1 attemptedMutations forced empty KILLED
    M2 the write record never pushed KILLED
    M3 identical-bytes writes skipped KILLED — I and E only
    M4 identical computed as existedBefore KILLED — W and C only
    M5 test-only: toEqual weakened to a length check SURVIVED — the control half
    M6 M5 plus a forked argv classifier KILLED, by a different test
    M7 after sampled at begin() KILLED
    M8 after.head frozen to before.head KILLED
    M9 the recorder's probes routed through its own wrapper KILLED
    M10 the bypass clause deleted KILLED
    M11 a new unsanctioned writer under packages/harness/src KILLED, and it names the file
    M12 the filter gains && exitCode === 0 KILLED, by one test only
    S1 a shipped ## Re-entry table reordered SURVIVED, by design
    S2 the fake forge made to enforce a duplicate SURVIVED, by design

    M3 and M4 kill disjoint halves. A suite carrying only the differing-bytes scenarios ships M3 green — which is exactly the defect case 10 exists to prevent.

    M5/M6 is #320's M9/M9b one layer up. The weakening survives alone; the paired defect is killed by a different test, over a deliberately mixed argv roster. A weakened assertion is not detectable by the test it weakens — third occurrence of that class, after #319's M3 and #320's M9.

    M12's asymmetry. An exit-code filter is killed only by a lie that refuses. The silent-no-op lie exits 0, so the forge said yes and did nothing, and the filter survives it. The property "records the attempt, not the success" is observable only through a refusal — #320's finding 1, measured from the recorder's side.

    S2 and M12 are the same property from opposite sides. The recorder must not care what the double decided, and must not be able to: attemptedMutations is typed GhPlan, an argv list with no verdict field.

    Findings raised

    1. Every effect-sampling alternative to the write seam is blind by construction. The third clause of the verdict is an attempt clause; git status, git hash-object, a --stat diff and an mtime comparison all sample effect, and the defining property of an identical-bytes rewrite is that it leaves none.
    2. The seam has a run-time bypass detector, and its blind spot is the one the seam exists to cover — an identical-bytes bypass leaves HEAD, tree and dirty status unchanged. Stated in the module, the test and the artifact so it is never read as complete. The static half is a closed sanctioned-writer list over a flat read of packages/harness/src, so S2.6.5 — The anti-vacuity assertion set, asserted by the runner #322's and S2.6.6 — The seam rung and its CI job #323's modules join the population automatically.
    3. Tracked-ness is recorded, never filtered. A Design is untracked at the moment it is created; filtering on it would score story-design as zero mutations — the argv-only defect reproduced inside its own fix.
    4. M8's prediction was wrong, and is recorded as wrong. It was predicted to kill scenario E only; it kills C as well. The conclusion — HEAD is load-bearing — held; the stated mechanism did not.

    What this does NOT establish

  3. dev-pmallapp commented on Sep 10, 2026

    @dev-pmallapp
    OwnerAuthor

    Closed on merge. PR #332 merged into story/293-s2-6-skill-execution-harness at 27ac716c81309e8f7e57042a94f247a4fe22462b, merged by a human — iAI never merges (CONTRIBUTING.md:416).

    Verdict stands at PASS for cases 4 and 10. CLAIM-293.8 is met for the recorder only — its per-skill threshold, at least one scenario per file-writing skill with run1.tree_hash != pre.tree_hash, needs a transcription and a runner and closes at #323, which must not read this as having closed it. Evidence: docs/evidence/321-20260909T125257Z.md.

    Carried forward to #322: there is still no runner (bun run skill-harness exists in no package.json), the anti-vacuity must be asserted by the runner's exit code and not only by a test file, and the sanctioned-writer list is a flat read of packages/harness/src — so #322's new modules join that population automatically and must go through recorder.writeFile or add themselves to the list in review. Still unpaid: #330. Still untouched: #331.

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