Add a mutation gate that proves a green guard test goes red on a broken patch #875

Closed
opened 2026-10-01 15:26:43 +00:00 by jared · 2 comments
Owner

Problem

A guard test proves behavior that already exists. ADR-0258 (decision 12) proves such a test with a mutation, not red-first: the test must go red when the guarded code is broken. No gate does this. Gate::Name::ALL (lib/os_sdlc/model/gate/name.rb:12) has no mutation name, and Gate::Commands#to_h (lib/os_sdlc/query/gate/commands.rb:13) has no command for it. cc-plugins #79 (commit 84bac12) is the motivating case: a tier1 parity test was green on day one, the pipeline escalated unexpected_pass, and the work was not merged.

Reproduce

n/a: design change, nothing to reproduce.

Approach

Add a mutation gate. The test-writer handoff carries mutation: { path:, patch: }: path is a production file under the target path, and patch is a unified diff that breaks the guarded code.

Gates on a node run inside the agent turn, before the handoff exists (CompletionIntake#gate_verdict_outcome, command/completion_intake.rb:99, reads the verdict the in-turn gate recorded). So the mutation gate cannot run on the test-writer node. It runs on the next node of the guard map, a gate-only node whose agent runs os-sdlc-gate once (the guard-map ticket adds that node). The check reads the patch from Implementation#latest_handoff_from("test-writer") (model/implementation.rb:74).

  1. Add MUTATION = "mutation" to Gate::Name and to ALL.
  2. Add Name::MUTATION => "os-sdlc-mutation-check #{implementation.id}" to Gate::Commands#to_h.
  3. Add bin/os-sdlc-mutation-check IMPLEMENTATION_ID and the Boot entry mutation-check (controller/boot/commands.rb), the same shape as required-work-check.
  4. Add a mutation reader to HandoffContent (parser/handoff_content.rb). It returns nil when the field is absent.
  5. The check applies the patch to path, runs the diff-test command (Gate::Commands#plan), and restores the file in ensure. It exits 0 only when the test run is red (executed and failed). It exits nonzero with a reason when the field is missing, the patch does not apply, path is outside the target path, or the test stays green ("mutation survived").

Required work:

  1. Add the mutation gate name — check: present MUTATION = "mutation" in plugins/os-sdlc/lib/os_sdlc/model/gate/name.rb
  2. Map the gate to its check command — check: present Name::MUTATION in plugins/os-sdlc/lib/os_sdlc/query/gate/commands.rb
  3. Add the mutation-check CLI — check: present mutation-check in plugins/os-sdlc/bin/os-sdlc-mutation-check
  4. Read the mutation field from the handoff — check: present def mutation in plugins/os-sdlc/lib/os_sdlc/parser/handoff_content.rb

Acceptance criteria

  1. Given a test-writer handoff whose mutation patch breaks the code the new test guards, when os-sdlc-mutation-check runs, then it exits 0 and the production file is byte-identical to its content before the run.
  2. Given a test-writer handoff whose mutation patch leaves the new test green, when the check runs, then it exits nonzero, the output says the mutation survived, and the production file is restored.
  3. Given a test-writer handoff with no mutation field, when the check runs, then it exits nonzero and names the missing field.
  4. Given a mutation patch that does not apply to path, when the check runs, then it exits nonzero, names the path, and leaves the file unchanged.
  5. Given a node with gates: [mutation], when the gate commands are built, then the mutation command is os-sdlc-mutation-check <implementation id>.

Owner sketch

class OsSdlc::Gate::Name
  MUTATION = "mutation"
end

class OsSdlc::Mutation::Check
  def initialize(root:, implementation:)
  def report
end

class OsSdlc::HandoffContent
  def mutation
end

Origin

  • Trigger: user ruling on the map reroute design, 2026-10-01 (ADR-0258 decision 12; ADR-0259)
  • Improvised this session: none
  • Chain: guard tests escalate as unexpected_pass ← red-first is the only proof a test-writer round can give ← DESIGN (docs/adr/0258)
  • Root candidate: #859
  • Where: plugins/os-sdlc/lib/os_sdlc/query/gate/commands.rb
  • Improvised this session: none
  • Root candidate: #859
  • Session: 55a56279-83ee-47d5-a21e-ee67f9e28abd
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/55a56279-83ee-47d5-a21e-ee67f9e28abd.jsonl

Parent: #859

## Problem A guard test proves behavior that already exists. ADR-0258 (decision 12) proves such a test with a mutation, not red-first: the test must go red when the guarded code is broken. No gate does this. `Gate::Name::ALL` (lib/os_sdlc/model/gate/name.rb:12) has no `mutation` name, and `Gate::Commands#to_h` (lib/os_sdlc/query/gate/commands.rb:13) has no command for it. cc-plugins #79 (commit 84bac12) is the motivating case: a tier1 parity test was green on day one, the pipeline escalated `unexpected_pass`, and the work was not merged. ## Reproduce n/a: design change, nothing to reproduce. ## Approach Add a `mutation` gate. The test-writer handoff carries `mutation: { path:, patch: }`: `path` is a production file under the target path, and `patch` is a unified diff that breaks the guarded code. Gates on a node run inside the agent turn, before the handoff exists (`CompletionIntake#gate_verdict_outcome`, command/completion_intake.rb:99, reads the verdict the in-turn gate recorded). So the mutation gate cannot run on the test-writer node. It runs on the next node of the guard map, a gate-only node whose agent runs `os-sdlc-gate` once (the guard-map ticket adds that node). The check reads the patch from `Implementation#latest_handoff_from("test-writer")` (model/implementation.rb:74). 1. Add `MUTATION = "mutation"` to `Gate::Name` and to `ALL`. 2. Add `Name::MUTATION => "os-sdlc-mutation-check #{implementation.id}"` to `Gate::Commands#to_h`. 3. Add `bin/os-sdlc-mutation-check IMPLEMENTATION_ID` and the Boot entry `mutation-check` (controller/boot/commands.rb), the same shape as `required-work-check`. 4. Add a `mutation` reader to `HandoffContent` (parser/handoff_content.rb). It returns nil when the field is absent. 5. The check applies the patch to `path`, runs the diff-test command (`Gate::Commands#plan`), and restores the file in `ensure`. It exits 0 only when the test run is red (executed and failed). It exits nonzero with a reason when the field is missing, the patch does not apply, `path` is outside the target path, or the test stays green ("mutation survived"). Required work: 1. Add the mutation gate name — check: present `MUTATION = "mutation"` in `plugins/os-sdlc/lib/os_sdlc/model/gate/name.rb` 2. Map the gate to its check command — check: present `Name::MUTATION` in `plugins/os-sdlc/lib/os_sdlc/query/gate/commands.rb` 3. Add the mutation-check CLI — check: present `mutation-check` in `plugins/os-sdlc/bin/os-sdlc-mutation-check` 4. Read the mutation field from the handoff — check: present `def mutation` in `plugins/os-sdlc/lib/os_sdlc/parser/handoff_content.rb` ## Acceptance criteria 1. Given a test-writer handoff whose `mutation` patch breaks the code the new test guards, when `os-sdlc-mutation-check` runs, then it exits 0 and the production file is byte-identical to its content before the run. 2. Given a test-writer handoff whose `mutation` patch leaves the new test green, when the check runs, then it exits nonzero, the output says the mutation survived, and the production file is restored. 3. Given a test-writer handoff with no `mutation` field, when the check runs, then it exits nonzero and names the missing field. 4. Given a `mutation` patch that does not apply to `path`, when the check runs, then it exits nonzero, names the path, and leaves the file unchanged. 5. Given a node with `gates: [mutation]`, when the gate commands are built, then the `mutation` command is `os-sdlc-mutation-check <implementation id>`. ## Owner sketch ```ruby class OsSdlc::Gate::Name MUTATION = "mutation" end class OsSdlc::Mutation::Check def initialize(root:, implementation:) def report end class OsSdlc::HandoffContent def mutation end ``` ## Origin - Trigger: user ruling on the map reroute design, 2026-10-01 (ADR-0258 decision 12; ADR-0259) - Improvised this session: none - Chain: guard tests escalate as unexpected_pass ← red-first is the only proof a test-writer round can give ← DESIGN (docs/adr/0258) - Root candidate: #859 - Where: plugins/os-sdlc/lib/os_sdlc/query/gate/commands.rb - Improvised this session: none - Root candidate: #859 - Session: 55a56279-83ee-47d5-a21e-ee67f9e28abd - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/55a56279-83ee-47d5-a21e-ee67f9e28abd.jsonl Parent: #859
Author
Owner

Work started: os-sdlc pipeline on branch ticket-875 (worktree .claude/worktrees/ticket-875).

Work started: os-sdlc pipeline on branch ticket-875 (worktree .claude/worktrees/ticket-875).
Author
Owner

Resolution

Done: Added the mutation gate: Gate::Name::MUTATION, Gate::Commands maps it to os-sdlc-mutation-check , HandoffContent#mutation reads mutation: {path:, patch:}, Mutation::Check applies the patch, runs the diff-test plan, restores the file, and passes only on a red run.

Evidence: Pipeline implementation 8 completed with no rejections (8 dispatches, 9 min 49 s); commit 461ff366 merged to main; History row 5b49309e; bin/test 9 suites passed on main.

Follow-ups: #893 (mutation gate counts a load failure as a kill; ticket-skeptic VERDICT: CREATE)

## Resolution **Done:** Added the mutation gate: Gate::Name::MUTATION, Gate::Commands maps it to os-sdlc-mutation-check <id>, HandoffContent#mutation reads mutation: {path:, patch:}, Mutation::Check applies the patch, runs the diff-test plan, restores the file, and passes only on a red run. **Evidence:** Pipeline implementation 8 completed with no rejections (8 dispatches, 9 min 49 s); commit 461ff366 merged to main; History row 5b49309e; bin/test 9 suites passed on main. **Follow-ups:** #893 (mutation gate counts a load failure as a kill; ticket-skeptic VERDICT: CREATE)
jared closed this issue 2026-10-01 20:49:04 +00:00
jared referenced this issue from a commit 2026-10-02 17:43:17 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
jared/cc-os#875
No description provided.