os-sdlc: test taxonomy classifier (mutation testing increment 2) #1634
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#1634
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
Mutation-based removal applies only to BEHAVIOR tests. Other test kinds follow their own keep or delete rules. The pipeline cannot sort a test into its kind without an agent. The plan rules deterministic signals for PROMPT-TEXT, LINT-AS-TEST, and STRUCTURE, an owned allowlist for CONTRACT, BEHAVIOR by elimination, and a MIXED bucket for judgment.
Reproduce
n/a: feature ticket.
Approach
Source:
docs/research/mutation-testing/plan-v1-2026-11-08.md, table "Increments, in order", row 2. The section "Rulings" is binding; do not re-decide it. Row 2 says "MISS bucket"; the Rulings say MIXED; use MIXED. Parent: #1638.The classifier gives each test file one kind. It reads the file with Prism, so text inside a heredoc never counts as a signal (cop tests hold Ruby source in heredocs, for example
tests/cops/effect_in_model_test.rb:27). The classifier is a Ruby object for #1636 and #1637. It has no CLI and no gate or map wiring.test_fileis relative to the target directory, the same form asMutationTest::Scope#testsand the Mutineer report.target_diris the directory thatImplementation#mutation_testalready builds (lib/os_sdlc/implementation.rb:85).Kinds:
BEHAVIOR,CONTRACT,STRUCTURE,PROMPT-TEXT,LINT-AS-TEST,MIXED.What a test method reaches: a test method is a
def test_*anywhere in the file. It reaches its own body, thesetupbody, the bodies of same-file methods it calls by bare name (followed transitively), and the values of same-file constants it reads by name.Signals, over the nodes one test method reaches:
File.read,File.readlines,File.foreach,File.expand_path,File.join,Dir.globorDir.[]whose string-literal arguments, joined with/, match%r{(\A|/)(agents|skills)/(.*/)?[^/]*\.md\z}.Dir.glob,Dir.[]orFind.findcall whose arguments hold__dir__,__FILE__or a constant read, in a method that also has anassert_empty, anassert_operator, or anassert_equalwhose first argument is an Integer or Array literal.assert*orrefute*call whose argument subtree holdsexist?onFile,DirorFileTest,const_defined?, orrespond_to?; or any call namedeager_loadoreager_load_all.Method kind: no signal is BEHAVIOR (by elimination); one signal kind is that kind; two or more signal kinds is MIXED.
File kind, first rule that applies:
requireorrequire_relativecalls (a pure require pointer): STRUCTURE.test_*method: MIXED (unclassified).CONTRACT allowlist:
<target dir>/<test_path>/contract_seams.yml; for os-sdlc,plugins/os-sdlc/tests/contract_seams.yml. A missing file is an empty allowlist. The format maps a seam to an owning side, and the side to one test file, so the format enforces "at most one test per seam per owning side". A side whose value is not one String makes the build raiseYamlFileError.Seed seams:
tracker—Tracker.forpicks Forgejo or Backlog at run time (lib/os_sdlc/tracker.rb:5-9); three sides (lib/os_sdlc/tracker/null.rb:5,lib/os_sdlc/tracker/backlog.rb:31,lib/os_sdlc/tracker/forgejo.rb:22) sharetests/support/tracker_contract_tests.rb:3(lens T6).procman-server-type—Daemon.connecthandsProcmanSpecto the separate procman gem (lib/os_sdlc/daemon.rb:37,lib/os_sdlc/daemon/procman_spec.rb:8,Gemfile:20).Rejected: file-level signals only (a file with one lint test and many behavior tests,
tests/cops/inline_dataset_query_test.rb:65, would hide its behavior tests); grep over file text (heredoc fixtures give false STRUCTURE signals); the allowlist under.sdlc/(.sdlc/.gitignoreignores all butproject.yaml, and a target in a subdirectory has no.sdlc/).Required work:
OsSdlc::MutationTest::TaxonomywithTaxonomy.for(root, test_path:),#kind(test_file)(the four file rules above) and#behavior?(test_file)— check: presentdef behavior?inplugins/os-sdlc/lib/os_sdlc/mutation_test/taxonomy.rbMutationTest::Taxonomy::TestFile, built from source text and parsed with Prism; it answersrequire_pointer?and gives the nodes eachtest_*method reaches — check: presentPrism.parseinplugins/os-sdlc/lib/os_sdlc/mutation_test/taxonomy/test_file.rbMutationTest::Taxonomy::Signals, the owner of the three signal rules, returning the set of kinds found in a set of nodes — check: presentLINT-AS-TESTinplugins/os-sdlc/lib/os_sdlc/mutation_test/taxonomy/signals.rbMutationTest::Taxonomy::Seams, a value built from the parsed YAML mapping that answersinclude?(test_file)and raisesYamlFileErrornaming the seam and side when a side's value is not one String — check: presentYamlFileErrorinplugins/os-sdlc/lib/os_sdlc/mutation_test/taxonomy/seams.rbtests/tracker/null_contract_test.rbinplugins/os-sdlc/tests/contract_seams.ymlMutationTest::Scope.fromkeep only test paths that end in_test.rb. TodayChanges#test_pathsholds every changed path under the test directory, andMutineerRunner::Command(lib/os_sdlc/mutation_test/mutineer_runner/command.rb:23) passes each one to Mutineer as--test; this ticket's owncontract_seams.yml, and any changed file undertests/fixtures/, would reach Mutineer as a test file — check: present_test.rbinplugins/os-sdlc/lib/os_sdlc/mutation_test/scope.rbAcceptance criteria
agents/*.mdpath held in a constant, when it is classified, then its kind is PROMPT-TEXT.setupreads askills/**/SKILL.mdpath, when it is classified, then its kind is PROMPT-TEXT.Dir.globbuilt from a constant path, when it is classified, then its kind is LINT-AS-TEST.Object.const_defined?, when it is classified, then its kind is STRUCTURE.Zeitwerk::Loader.eager_load_all, when it is classified, then its kind is STRUCTURE.File.exist?only as a guard outside an assertion, when it is classified, then its kind is BEHAVIOR.assert File.exist?(...), when it is classified, then its kind is BEHAVIOR.respond_to?, when it is classified, then its kind is CONTRACT.YamlFileErrornaming the seam and the side.behavior?is asked for each, then it is true for the first and false for the second._test.rbfile and one.ymlfile, when a mutation scope is built from them, then its tests hold only the_test.rbfile.Owner sketch
MutationTest::TaxonomyTaxonomy.for(root, test_path:),#kind(test_file),#behavior?(test_file)MutationTest::Taxonomy::TestFile#require_pointer?,#test_methodsMutationTest::Taxonomy::Signals#kinds(nodes)MutationTest::Taxonomy::SeamsSeams.new(mapping),#include?(test_file)MutationTest::Scope(existing)Scope.from(changes, since, within:),#testsFailing test at
plugins/os-sdlc/tests/mutation_test/taxonomy_test.rb; every test is red today withNameError: uninitialized constant OsSdlc::MutationTest::Taxonomy.Failing test at
plugins/os-sdlc/tests/mutation_test/scope_test.rb; red today becausescope.testsalso holdstests/contract_seams.yml.Out of scope
Boot::Commands::TABLE.tests/system_model_viewer_procman_spec_contract_test.rbin the seed allowlist; its tests at lines 12 to 30 check behavior, so CONTRACT would hide them.Origin
docs/research/mutation-testing/plan-v1-2026-11-08.md(commitb90d8253), ruled by Jared 2026-11-07 and 2026-11-08docs/research/mutation-testing/plan-v1-2026-11-08.md)VERDICT: CREATE
os-sdlc: per-ticket mutation survivor gate (mutation testing increment 1)to os-sdlc: test taxonomy classifier (mutation testing increment 2)Decompose routing (ticket-decompose, Phase Decide):
a. One change: a test-kind classifier that assigns each test file one kind (PROMPT-TEXT, LINT-AS-TEST, STRUCTURE, CONTRACT, BEHAVIOR, MIXED) from deterministic signals (plan Rulings line 36).
b. Owner: no existing object owns test kinds (no TestKind/taxonomy code in lib, bin, maps, hooks, tests, eval). New object; secret: the signals that decide a test file's kind.
c. Routed to /os-sdlc:ticket-takedown: step b names a new object. Takedown writes the Owner sketch.
Facts for takedown:
Takedown relevance: not relevant
Harm: A test-removal pass that runs without a test-kind classifier could delete or keep tests by mutation survival alone, including PROMPT-TEXT, LINT-AS-TEST, and STRUCTURE tests that the plan says follow their own keep rules. No such pass runs today, so no one is harmed now.
Protects: The plan's ruled rule that mutation-based removal applies only to BEHAVIOR tests, so the suite is not thinned by a gate that cannot tell test kinds apart.
Harm path: A future whole-codebase redundant-test pass (plan increment 4) would call the classifier before removing a test. Today the only mutation path is the per-ticket survivor gate in mutation_test/, which reports survivors and removes nothing, so no current command, hook, or skill reaches the missing classifier. (live: false)
Evidence:
1596f6ba: Most recent mutation commit in history fixes target root paths in the mutation test; it adds no classifier and deletes no tests.Close candidate for Jared's batch ruling. The ticket stays open.
Work started: /os-sdlc:implement on branch ticket-1634 (worktree .claude/worktrees/ticket-1634), map poodr-ticket-implementation, target os-sdlc. First live run of the survivor-check and survivor-tests nodes.
Resolution
Done: MutationTest::Taxonomy gives each target-relative test file one kind (BEHAVIOR, CONTRACT, STRUCTURE, PROMPT-TEXT, LINT-AS-TEST, MIXED) from Prism signals per test method, with the CONTRACT allowlist tests/contract_seams.yml; Scope.from passes only _test.rb paths to Mutineer. All 15 acceptance criteria verified by behavior-verifier.
Evidence: Implementation 266 on branch ticket-1634: commit
875b5785(Runner commit, 11 files), history row9deaf01a. Suite pass 54.0 s; survivor-check found survivors, survivor-tests killed them, survivor-check pass (3.6 s); contract-auditor pass. First live run of the survivor nodes.Follow-ups: #1642 takedown relevance judge rejects ruled feature tickets (captured, skeptic CREATE). #1643 programmer-repair gate omits required-work (captured, skeptic CREATE).