Repository navigation
Description
Objective
Derive a cross-consumer parity test from the role tables that runs beside the hand-written parity suites, and use it to make #1458's sibling sweeps a mechanical diff for the roles. Retire a hand-written row only after a perturbation shows the derived test discriminates the same defect.
Umbrella: R00: #1690 · Planning ID: R10: #1700
Completion prerequisites: R05: #1695, R06: #1696, R07: #1697, R08: #1698, R09: #1699
Why this exists, and what it cannot do
tests/parity/* are exhaustive matches on LANG over fixtures someone chose (#1281 made them exhaustive after Ruby, Lua, Perl and Go were found missing from families they can express). They catch two languages disagreeing on one construct the author thought of. slot_pays_tests.rs is better — cyclomatic as an oracle ABC did not write — but a construct absent from its template is unmeasured (#1535, #1536).
A derived test takes its inputs from the rows: for every claimed (language, role, kind) it locates occurrences in the corpus and asserts each consumer's verdict. What that adds is breadth across kinds nobody fixtured and a sweep that is a test run rather than an issue-body table. What it cannot do — and this package must say so in its module doc — is detect an omitted row, because inputs and expected verdicts come from the same declaration; nor cover a kind the corpus never emits. Hand-written fixtures with independently specified expected values, including negative contexts, stay for that (.claude/rules/testing.md, "Take the expected value from outside the fix").
Implementation steps
-
roles::paritytest module (landed asroles/derived_parity_tests.rs+src/metrics/derived_parity_tests.rs; the per-spaceabc.conditions == cyclomatic() - 1half was measured and left out — 36 fixture functions differ by Rule 5 design — see the retirement comment): per language table, per row, locate every occurrence of each claimed kind in that language's integration corpus (reuse the corpus walk) and assertowns_*/pays_*per occurrence; per space on the shared slot fixtures, assertabc.conditions == cyclomatic() - 1where the language's policy expressions say they must agree, and the documented difference otherwise. - A census-driven variant of
cyclomatic_cross_language_parity.rs(the role set is the family list of the R-layer relations and the F-layer forwards; anabsentrow prints its reason): the family list is the role set; a language declaringabsentis skipped with its reason printed. - Report, per (language, role, kind), "no occurrence in corpus" (
make role-coverage-claimed; the Rust census prints its own) as a named row in the census output so uncovered claims are visible, not silent. - Retire a
slot_pays_tests.rsrow only when (none retired — measured, recorded below) perturbing the production line it guards fails the derived test; record each retirement with its perturbation. Rows pinning a documented policy difference (else/?Rule-5 rows) stay. - Retirement candidate from R07 (retired after both perturbations, recorded below) (comment):
elixir_consumer_word_policy_agrees_with_the_control_flow_rows(src/metrics/elixir_spelling_parity_tests.rs) — retire only when perturbing the Elixir row gate and the consumer arm each fail the derived test; the derived locator must find Elixir occurrences by word, not kind, and read thewith/trypolicy exceptions from a declared place. -
Bring R07's 21 per-language control-flow fixtures and 5 Tcl/Elixir C5b fixtures into the tree as the corpus rows for the six control-flow roles, landing with the derived assertions that read them; recovery-shape fixtures keep their parity-not-contract wording.Split (2026-10-31):- (a) Preserve the fixtures — done by chore(utils): tier-1 homes for the role-series drift drivers and the R07 fixtures #1744 (branch
fix/issue-1744, pending merge:tests/corpus/control_flow_test.rsover the submodule'scontrol_flow/<lang>/, submodule c51ca7fa), which copies the 25 distinct files intotests/repositories/big-code-analysis-output/with per-function snapshots captured atfdd12ad4. The cache holds 25, not 21 + 5:fixtures-c5b/r07.{ex,tcl,irule}are byte-identical to theirfixtures-r07/twins, andfixtures-0e/sw.{groovy,java}were omitted from the count. None is a recovery-shape fixture — every one dumps with zeroERROR/MISSINGnodes at the pin. The "three recovery-shape fixtures (C# strandedcatch, Perlwhen, Bash heredoc-in-compound)" named in the R07 comment below do not exist in the cache; if this package wants that coverage it writes them. - (b) Attach the derived parity assertions to those files — this package's, unchanged in scope.
- (a) Preserve the fixtures — done by chore(utils): tier-1 homes for the role-series drift drivers and the R07 fixtures #1744 (branch
- Gate the derived tests on the union of the language features whose rows they read, with the non-vacuity assertion inside (
.claude/rules/testing.md, "Gate a feature-gated fixture table on the union of its rows").
Tests and edge cases
- Perturb a consumer or a projection (make Java's
operator_fieldprojection returnNone; make the ABC token arm skip its projection) while the row and the independent hand-written fixture stay fixed; the derived test and the hand-written one both fail. Dropping a row entry is not a valid perturbation here — it deletes the generated assertion with it, which is the stated limitation. -
-p big-code-analysis --no-default-features --features go: the derived tests run their Go rows and assert non-zero;--no-default-featureswith no language: absent, not vacuous. Confirm the filtered-out counts differ across sets (fix(abc/objc):@availablescores no condition #1457).
Acceptance criteria
- A new
role_table!with one row and no fixture produces at least one derived assertion per claimed kind on the corpus or a named "no occurrence" row. - Retirements listed with perturbation evidence; no hand-written row retired without it.
-
check-test-lang-gatesand--compareclean.
Verification and completion evidence
- Perturbation result; feature-isolation run counts;
BCA_GATE:line (completion comment).
Activity
dekobon commentedon Oct 23, 2026
Tracking is ready
The body now links the master issue and the work packages it references. Native parent and blocked-by relationships match the listed completion prerequisites. Implementation and acceptance checkboxes remain open; prerequisite closure and the R04: #1694 go decision govern execution order.
dekobon commentedon Oct 31, 2026
Inputs and a retirement candidate from R07 (#1697)
Two things from R07: #1697's close-out belong to this package's derived parity test.
1. Retirement candidate: the Elixir rows-vs-consumer test
R07's Elixir cyclomatic and cognitive bodies dispatch once on the cached
ElixirCallTargetword rather than reading theBranch/Switch/Looprows (one text read percallinstead of five; the rows and the consumer arms are therefore two spellings of one set). The alignment review called that the "two encodings" class the epic counts, and R07 closed it with a hand-written test:elixir_consumer_word_policy_agrees_with_the_control_flow_rows(src/metrics/elixir_spelling_parity_tests.rs:577) iteratesElixirCallTarget::ALL(madepubfor it), scoresdef f(x) do\n <word> x do\n _ -> x\n end\nendagainst the same body under a plainfoo, and asserts the container decides / opens a level exactly when a row owns the word — naming the two per-word policies the consumers carry (withpays no container decision, #1535;tryopens no level, #416). It fails whenWhileis dropped from either the row gate or the cyclomatic arm, and panics on a variant added to the enum without a spelling.This is exactly one hand-written instance of what this package derives per (language, role, kind). It is the retirement candidate under the rule above — retire it only when perturbing the row gate and perturbing the consumer arm each fail the derived test, with both perturbations recorded. Note the derived form has to reach the same thing the hand-written one does: Elixir's rows are text-gated, so the derived locator must find occurrences by word, not by kind (every control construct is
Call), and the per-word policy exceptions have to be declared where the derived test can read them rather than re-typed in a second table.The two Tcl/Elixir read-budget tests (
src/observation_tests.rs:313,351) are not candidates: they pin a cache invariant on the real walk, which no row-derived assertion expresses.2. Corpus rows: the R07 fixtures
R07 wrote one control-flow fixture per language exercising every
Branch/Switch/Loop/Catch/ElseIf/Exitspelling the grammar has, plus the dialect-specific shapes — 21 files (r07.{c,cpp,cs,ex,go,groovy,irule,java,js,kt,lua,m,php,pl,py,rb,rs,sh,tcl,ts,tsx}) and 5 C5b fixtures (c5b.{ex,tcl}for the keyword-section andswitch-arm shapes;r07.{ex,irule,tcl}recovery variants). They were the fixture roots of the zero-drift oracle runs recorded on #1697 and today live only in the tier-2 working cache (.claude/rules/temp-files.md), which this package should not cite.switchexpression, a Perlwhen, a Tclswitchbrace list — as unlocated, since the integration corpora do not contain them all). Each fixture pins nothing itself; the derived assertions are what give it a reason to exist, so they land together.catch, Perlwhen, Bash heredoc-in-compound) pin parity between consumers on an error-recovery tree, not a language contract (grammar-dispatch §6); keep that wording on the derived rows that read them.Added by the R07 close-out; #1697's completion comment lists these as its deferred items.
dekobon commentedon Nov 1, 2026
Fixture step split in the body: (a) preservation is #1744's (25 distinct files — the three
c5b/r07.*were byte-identical copies of ther07set, andsw.{groovy,java}were uncounted — landing in the output submodule with snapshots captured atfdd12ad4); (b) the derived assertions stay here. Correction: none of the cached fixtures is a recovery shape (all parse clean at the pin), so the three recovery fixtures the R07 comment names must be written by this package if wanted.dekobon commentedon Nov 1, 2026
Pointer update: #1744 landed the step this issue carries for it on branch fix/issue-1744 (pending merge; commits 2783e686 and 603e902e, output submodule c51ca7fa). The checklist line above now says so; ir_check.sh and snapdiff.py are recorded there as one-off and not landed.
dekobon commentedon Nov 1, 2026
Follow-up noted by the whole-branch review of fix/batch-2026-10-31 (which landed #1744's fixtures): check-role-coverage.py samples only CORPORA = (DeepSpeech, pdf.js, serde), so make role-coverage still reports 15 languages as not sampled by the corpus — and the branch just preserved one hand-written control-flow file per language under big-code-analysis-output/control_flow// that would sample every one of them. drift-oracle.py's corpus_skip likewise skips the submodule unless it is passed as a --root. Adding the submodule to CORPORA changes .role-coverage-observed.txt provenance and deserves its own review; it fits this package's "report no occurrence in corpus as a named row" step. Deliberately not done on the batch.
dekobon commentedon Nov 1, 2026
From R09 (#1699): rows and fixtures the derived parity can read
R09 landed the seven documentation rows in every language and three cross-language pins in
big-code-analysis-ast/src/roles/doc_tests/parity.rsthat this issue's derived tests should subsume or sit beside:doc_targets_outside_the_space_tree_are_each_named— declared-list parityowns_DocTarget ⊆ owns_FuncOpener ∪ owns_Container ∪ owns_Memberper language, every exception named in a test-side(language, kind, reason)table, both directions (an unnamed target fails; a stale name fails). The C-family entries resolve names per dialect with adialect_dependentescape for kinds the space tree owns only in C++.doc_targets_the_space_tree_rejects_by_gate_are_each_named— the same on nodes with the real chain and gates applied, with a non-vacuity target count per fixture; C#'s bodied property / indexer are the named gated-out shapes.doc_comment_implies_comment_and_docstring_implies_neither—is_doc_comment ⇒ is_commentas the Checker answers it per language, and Python'sDocstringowners reaching neither predicate.When the census-driven variant lands, these are the hand-written rows to retire by the perturbation rule in this issue's steps. The 26 per-language doc fixtures (one file per language spelling every row owner and both sides of every gate, plus six Doxygen fixtures: entries per dialect, an Objective-C header read as C / C++ / Mozcpp, error recovery, a sole conditional child of an error) are the corpus rows for the seven roles, as R07's control-flow fixtures are for its six; they live in the per-language
rows_answer_the_tabletests indoc_tests/mod.rstoday.dekobon commentedon Nov 3, 2026
Two scope decisions before implementation (R10 plan, revision 2)
Recorded here so neither is slipped in. Both deviate from the literal text of the steps above; the reason for each is below.
1.
slot_pays_tests.rs: expect zero retirements. The derived form that can read the corpus is a per-function-spaceabc.conditions == cyclomatic() - 1 + deltaovercontrol_flow/<lang>/*with a test-sidedeltatable naming each Rule-5 / outer-negation entry. A difference check passes when both metrics regress by the same amount; the hand-written rows assert exact(conditions, decisions)pairs on minimal templates, which catch that. The derived assertion therefore adds corpus breadth beside the rows rather than replacing any. Each attempted retirement is still recorded with its perturbation and result, so the step is satisfied by evidence, not deletions.2. The three integration corpora (DeepSpeech / pdf.js / serde) are opt-in, pending a cost measurement. The default derived test walks
control_flow/plus the inline R09 doc fixtures — every one of the 25 production tables, deterministic, clean-parse. The full-corpus walk is a separate#[ignore]test wired to a newmake role-parity-corpusrecipe (whichrole-coverage-observewill depend on), becausemake role-coverage-observeruns no Rust test today and an ignored test with no recipe runs nowhere. If the measured default walk stays under ~5 s under nextest, the corpora are promoted into it and the recipe is dropped.Also corrected from the earlier framing: the Elixir candidate is not deferred to #1701.
Cyclomatic::computeis a per-node trait reachable from a root-crate test, so the derived form runs the metric arm directly; the two mandated perturbations decide retirement.Candidate selection in the derived census is by the declared kind lists (
OWNERS ∪ PAYERS ∪ LEAF_KINDS), never by a gated verdict, so gate negatives are recorded rather than discarded; Elixir occurrences are keyed byElixirCallTargetword.4 remaining items
test(metrics): derive the metric-arm parity from the role tables in t…
docs(lessons): a fallback that rebuilds a withheld input answers as i…
feat(utils): sample the control-flow fixtures in role-coverage and re…
test(ast): derive cross-consumer parity from the role tables beside t…
test(metrics): derive the metric-arm parity from the role tables in t…
docs(lessons): a fallback that rebuilds a withheld input answers as i…
feat(utils): sample the control-flow fixtures in role-coverage and re…
test(ast): derive cross-consumer parity from the role tables beside t…
test(metrics): derive the metric-arm parity from the role tables in t…
docs(lessons): a fallback that rebuilds a withheld input answers as i…
test(ast): derive cross-consumer parity from the role tables beside t…
test(metrics): derive the metric-arm parity from the role tables in t…
docs(lessons): a fallback that rebuilds a withheld input answers as i…
dekobon commentedon Nov 4, 2026
Landed on
mainatd96b6fcavia PR #1790 (four signed commits, fast-forwarded).The last CI round turned up one more thing worth recording: the
features (*)andchain auditjobs run the lib tests withsubmodules: false, and the derived-parity tests readcontrol_flow/from the output submodule. The whole-corpus obligations (a positive per owner kind / per relation, the stale direction of each exception table, Elixir's per-word coverage, the rootrows_own!premise) are nowcorpus_-prefixed tests that assert the submodule is present; those legs filter the prefix by name (not test(/::corpus_/)/--skip ::corpus_) so the skip shows in the test count rather than as a pass. The structural assertions and the contradiction direction of every exception table still run on the documentation fixtures alone on every leg.