os-sdlc: MutationTest objects and os-sdlc-survivors command (#1638 part B) #1640
Labels
No labels
P0
P1
P2
P3
bug
category/code-change
category/cross-repo
category/eval-run
category/investigation
category/no-test-target
category/owner-ruling
category/prose-artifact
category/recurring-chore
category/structure
category/umbrella
crc-refactor
create
delete
enhancement
filed-by/agent
filed-by/user
frozen
lint-rule
needs-info
needs-triage
next
plugin/cc-architect
plugin/os
plugin/os-adr
plugin/os-aidd-lint
plugin/os-backlog
plugin/os-context
plugin/os-doc-hygiene
plugin/os-sdlc
plugin/os-vault
project/cc-os
ready-for-agent
ready-for-human
recurring
review
rule-proposal
update
waiting
wayfinder:grilling
wayfinder:map
wayfinder:map
wayfinder:research
wayfinder:task
wayfinder:task
wontfix
worklist/deviations
worklist/lint-rule
worklist/new-implement-build
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
jared/cc-os#1640
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
os-sdlc cannot tell which mutants on a ticket's changed lines survive the ticket's tests. A surviving mutant shows a missing test or dead code.
Approach
Design:
docs/system-design-format/proof/2026-11-08-os-sdlc-mutation-survivors.md, sections 2 to 6, build slices 3 to 5. Parent: #1638. The design's "Rules to keep" table is binding.MutationTestdepends on two roles: a runner (survivors(scope)returnsMutationTest::Survivorsor raisesMutationTest::RunError) and a switch (enabled?). It never names Mutineer, a shell, or a file.MutationTest::MutineerRunneris the only object that knows the Mutineer argv, JSON schema, and operator names. Tests ofMutationTestuse a fake runner.Required work:
MutationTestwithoutcome— check: presentdef outcomeinplugins/os-sdlc/lib/os_sdlc/mutation_test.rbMutationTest::Scope,MutationTest::Outcome(to_jsonandself.parseown the step contract),MutationTest::Survivors,MutationTest::Mutant,MutationTest::RunError(aLoud::Error), andMutationTest::Config— check: presentdef self.parseinplugins/os-sdlc/lib/os_sdlc/mutation_test/outcome.rbMutationTest::MutineerRunner. It runsbundle exec mutineer run <sources> --test <t>... --since <ref> --no-matrix --format json --output .mutineer/gate-<id>.jsonfrom the target root, reads the JSON file, and folds eachboolean_literalrow into astatement_removalrow with the same file, subject, and line whose token contains it — check: present--no-matrixinplugins/os-sdlc/lib/os_sdlc/mutation_test/**/*.rbImplementation#mutation_test(root). It asksProject::BaseBranch#merge_baseandProject::Changes#since, and injects the Mutineer runner and the config switch — check: presentdef mutation_testinplugins/os-sdlc/lib/os_sdlc/**/*.rbMutationTest::Check, the Boot table entrysurvivors(parse the id withInputs.implementation_check("survivors"), as themutation-checkentry does), andbin/os-sdlc-survivors(the one-line entry script shape from CLAUDE.md "Hook contract").Checkreturns exit 1 itself for survivors; it never raises to signal survivors. Only a raised error reachesProcessIO#guardedand exit 3 — check: present"survivors"inplugins/os-sdlc/lib/os_sdlc/boot/commands.rbAcceptance criteria
.mutineer.yml, when the outcome is asked, then it is skipped with reasonno_config, the runner is not asked, andos-sdlc-survivorsexits 0.no_changed_codeand the runner is not asked.no_tests, it lists the untested files, and the runner is not asked.os-sdlc-survivorsruns, then it prints the outcome JSON and exits 0.os-sdlc-survivorsruns, then it prints one row per survivor with id, file, line, kind, subject, and diff, and exits 1.MutationTest::RunError, whenos-sdlc-survivorsruns, then it writes oneos-sdlc-stderr line that carries the runner's error text and exits 3.boolean_literaland astatement_removalsurvivor on the same file, subject, and line where the statement token contains the literal token, when the Mutineer runner reads it, then only thestatement_removalrow is returned.MutationTest::RunErrorcarrying Mutineer's stderr..mutineer.ymlwhose bundle has nomutineergem, when the Mutineer runner runs, then it raisesMutationTest::RunError; it does not skip.to_json, whenMutationTest::Outcome.parsereads it, then it returns an equal outcome.Owner sketch
Notes from blast radius
ProjectShellruns with the unbundled environment, sobundle exec mutineeruses the target's own Gemfile. Only a target whose Gemfile hasmutineercan run the gate.Implementation#mutation_testandImplementation::MutationCheckare different checks. Keep the names apart in tests and messages.Blocking edges
Blocked by #1639.
Origin
docs/system-design-format/proof/2026-11-08-os-sdlc-mutation-survivors.md(commitf0e172b7) for parent #1638docs/system-design-format/proof/2026-11-08-os-sdlc-mutation-survivors.md)VERDICT: CREATE
Blocked on #1639 (Project::BaseBranch#merge_base and Project::Changes#since).
Unblocked: #1639 merged (Project::BaseBranch#merge_base, Project::Changes#since).
Work started on branch ticket-1640 (worktree .claude/worktrees/ticket-1640) through /os-sdlc:implement.
Design-audit note (skeptic DROP, need-to-exist): MutationTest::Scope.from makes test paths relative to the target, and Target#relative passes an outside path through unchanged. A target with a sibling test_path (path: lib, test_path: tests) would give Mutineer wrong test paths. No project uses that configuration, so no ticket. File it when such a target is onboarded.
Resolution
Done: MutationTest (switch, runner, scope roles), MutineerRunner (argv, JSON report, boolean_literal fold), Implementation#mutation_test wired to Project::BaseBranch and Project::Changes#since, os-sdlc-survivors CLI. Post-run fixes: Mutineer runs in the target directory with target-relative paths (
1596f6ba); Project::Target#directory owns the target's absolute directory (bd83e03e).Evidence: Implementation 264 implementation_complete (21 dispatches, 25 min 20 s); commits
09ad8f39,1596f6ba,bd83e03eon ticket-1640. Full suite 3528 runs, 0 failures (61.9 s). Rubocop clean. Design audit of1596f6ba: 4 findings, 1 fixed, 1 removed by that fix, 2 answered. Real run on plugins/os-sdlc: survivors abfd2fb825c1 (return_nil) and 559f08c32fd1 (statement_removal) at lib/os_sdlc/offense_directive.rb:28.Follow-ups: #1641 (survivors gate step) is unblocked. Sibling test_path note: dropped by ticket-skeptic, recorded as a comment on this ticket.