Account for every conformance fixture in both SPEC rosters, and gate them - #740
Conversation
Sensitive Change Detection (shadow mode)This PR modifies control-plane files:
|
There was a problem hiding this comment.
Pull request overview
Adds automated gates to keep conformance fixture documentation complete and ensure every mock case remains executable by at least one SDK runner.
Changes:
- Gates fixture rosters in SPEC.md against tracked fixtures.
- Adds runner skip extraction and fixture-execution validation.
- Adds self-tests, CI wiring, and contributor guidance.
Tip
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
spec/doc-constants.json |
Registers fixture roster markers. |
SPEC.md |
Completes and documents fixture rosters. |
scripts/test-doc-constants.rb |
Tests roster drift detection. |
scripts/test-check-fixture-execution.rb |
Tests execution-gate failure modes. |
scripts/sync-doc-constants.rb |
Implements fixture roster checks. |
scripts/conformance_skips.rb |
Extracts runner exclusions. |
scripts/check-fixture-execution |
Detects universally skipped cases. |
Makefile |
Adds execution gate targets. |
CONTRIBUTING.md |
Documents fixture roster requirements. |
.github/workflows/test.yml |
Runs new gates in CI. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1fbfb340dc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (2)
Makefile:1209
- This count is inaccurate:
conformance_skips.rbreads six name-keyed literal tables, one for each runner, plus the Kotlin and Swift tag branches.
# it stayed green in both. This reads the six runners' skip mechanisms — five
# literal tables plus Kotlin's and Swift's whole-case `link-header` tag branch,
# which the other four use to suppress one ASSERTION rather than the case — and
scripts/check-fixture-execution:209
- The roster comparison accepts stale skip keys. If a fixture case is renamed or deleted while its table entry and SPEC bullet remain, both sets still agree even though a default run can never report that skip; this recreates the “wrong extraction and stale roster agree” false-green described above. Validate every primary skip-table key against the loaded mock-case names (and add a self-test for an unknown key) before comparing the roster.
errors = check_companion_tables(tables) + check_roster(ROOT, tables) +
check_modes(cases) + check_execution(cases, exclusions)
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (5)
scripts/sync-doc-constants.rb:765
- This only requires the category and filename cells, so a row with no owning section still satisfies the new bijection and lets
doc-constants-checkvouch for a fixture that no spec section owns. Require a present, non-empty third cell (and add that malformed-row case to the self-test).
if cells.length < 2
errors << "#{span.file}:#{line_no}: category row has #{cells.length} cell(s); the shape is " \
"| Category | `file.json` | Owning Spec Section(s) |"
next
end
scripts/conformance_skips.rb:250
- Ending at the first newline after any balanced brackets can silently parse a valid multiline Swift declaration as empty. For example,
let temporarySkips: [String: String] =followed by the dictionary on the next line setsenteredfrom the type annotation, reaches depth zero before the initializer, and returns no keys. That can hide a newly added skip. Start delimiter tracking at the initializer expression (or otherwise distinguish annotation brackets), and cover this Swift form in the self-test.
if char == "\n"
return [strings, :closed] if entered && depth.zero?
scripts/sync-doc-constants.rb:827
- The Appendix D coverage check accepts any row whose first cell is a fixture filename; even
|alpha.json|counts as mapping that fixture to a primary section. Validate the non-empty summary and primary-section cells before adding the file tocovered, otherwise the gate can pass while the documented mapping is absent.
if cells.empty?
errors << "#{span.file}:#{line_no}: mapping row has no cells; the shape is " \
"| `file.json` | Test name | Primary section |"
next
end
scripts/check-fixture-execution:119
- Using
||treats an explicitly malformed JSON value such as"mode": falseornullas if the field were omitted, so this validator accepts it asmockinstead of reporting an unrecognized mode. Default only when the key is absent. Also, the comment's"mdoe": "mock"example is not an all-runner exclusion: becausemodeis absent, every runner defaults it tomock.
def check_modes(cases)
cases.filter_map do |test_case|
mode = test_case["mode"] || ConformanceSkips::DEFAULT_MODE
next if ConformanceSkips::KNOWN_MODES.include?(mode)
scripts/conformance_skips.rb:451
- This
|| []bypasses the non-array guard for falsey JSON values:"tags": falseornullis silently treated as no tags, despite the stated fail-closed behavior. Default only when the member is absent so every explicitly malformed value reaches the type check; add a false/null case alongside the existing object case.
cases.each do |test_case|
case_tags = test_case["tags"] || []
# A non-array `tags` would answer include? in whatever way its own
# class defines — a Hash by key, a String by substring — and the wrong
# answer here is silent under-exclusion, which reads as a case some
# runner still executes. conformance-fixtures-check validates the
# fixture schema, but a gate that credits malformed input because
# another gate usually catches it is crediting what it did not read.
unless case_tags.is_a?(Array)
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (2)
scripts/conformance_skips.rb:476
- This audit only runs for runners that already have a registered tag branch. Because Go, Python, Ruby, and TypeScript have empty
tag_branches, reintroducing #573's whole-caselink-headerchecks in those runners would not be observed; the gate would still think those four execute the case and would miss the all-six exclusion it is intended to prevent. The self-test also models those four exclusions as name-table entries rather than actual tag branches. Please inventory/classify tag inspections for all six runners and add a mutation that introduces a whole-case branch in one of these four runners.
runner.tag_branches.group_by(&:file).each do |file, branches|
accessor = branches.first.accessor
mentions = read(root, file).lines.count { |line| line.include?(accessor) }
next if mentions == branches.length
scripts/check-fixture-execution:154
- Using
||treats an explicitly presentfalseornullmode as if the field were omitted. Those are not recognized modes, but this validator reclassifies them asmock, allowing this gate to pass unless a separate schema check happens to run. Distinguish an absent key from a present invalid value, and add this case to the self-test.
mode = test_case["mode"] || ConformanceSkips::DEFAULT_MODE
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 02d6b4ebe0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (2)
scripts/conformance_skips.rb:217
- The anchor guard is not fail-closed when the declaration keeps its name but stops being a standalone literal. For example,
SKIPS = SOME_SETleavesenteredfalse, soscancontinues into the following declaration (often the companion reason map) and can return those keys;SKIPS = [] + EXTRA_SKIPSreturns only the literal portion. Either reshape can under-report actual exclusions and let an all-runner skip pass. Bound parsing to the single assignment and reject aliases/composed expressions, with a mutation test for this case.
offset = lines[0, hits.first].sum(&:length) + assignment
strings, state = scan(source, offset, table.comment)
scripts/sync-doc-constants.rb:693
- This splits escaped Markdown pipes too. A valid Appendix row whose Test name is
A \| Band whose Primary section is blank is parsed as ifBwere the third cell, so the checker accepts coverage without an actual section mapping. Split only on unescaped pipes; a regression case with an escaped pipe in the Test name would pin the behavior.
parts = line.split("|")
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 046b133b8c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
scripts/conformance_skips.rb:510
- This regex cannot parse a rostered case name containing an escaped quote:
- "a \"quoted\" case" — ...is truncated toa \. The new escaped-quote self-test actually expectsskipped but not rostered, so it confirms extraction from the runner while leaving such a legitimate skip impossible to roster; the gate can never become green unless the fixture is renamed. Parse the quoted literal with escape awareness and make that self-test pass end-to-end.
end
body = lines[(hits.first + 1)..].take_while { |line| !line.start_with?("## ", "---") }.join
split = body.split(/^\*\*([A-Za-z]+)\*\*/)[1..].to_a.each_slice(2).to_a
blocks = split.to_h
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5ee9b4f5dd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
SPEC §19's Test Categories table and Appendix D each claim to account for every fixture under conformance/tests/, and both had drifted. The last three fixture-adding commits missed the convention in three different ways: dee221c (#601) added documents_write.json and updated neither table; b238e5e (#683) added uploads_write.json with four Appendix D rows and no §19 row; #726 added search.json's §19 row and missed Appendix D. Two half-applications in opposite directions is not carelessness — CONTRIBUTING.md tells contributors to add conformance tests and mentions neither table. Each cell is derived from the fixture's own description citations, which is the convention the existing rows follow. documents_write.json cites "SPEC 18 body compaction" and "SPEC 18 rule 6", and §5's Documents subsection already back-references it; uploads_write.json cites "SPEC §18", "SPEC.md §5" and "SPEC §6 step 11", the same four attributions its Appendix D rows already spell out. Also moves the search row into its sorted position, where #726 misfiled it between retry and schedule-entries-write. Claude-Session: https://claude.ai/code/session_01VyZUi8zkRfhatnBYoS4eyn
`sync-doc-constants.rb` already does table-completeness checking:
@assertion-types wraps SPEC §19's assertion-type table in a block marker
and set-compares it against conformance/schema.json. The two fixture
rosters are the same shape one level out — a table that claims to
account for every fixture under conformance/tests/, with nothing
checking it — so they become two more block kinds rather than a new
script and a new CI step. `make doc-constants-check` already runs the
gate and its self-test, and is already in check-targets and spec-gates.
The source is `git ls-files conformance/tests/*.json`, not Dir.glob, for
tracked_markdown's reason: an untracked scratch fixture must not fail a
developer's build. Direct children only — git's pathspec `*` matches
across `/`, and a nested fixture is discovered by no runner, so
demanding a roster row for it would be documenting a claim that is not
true. That scope is also how SPEC §23's carve-out is honored:
conformance/oauth/, oauth-token/ and event-feed*/ are documented at
their own section and directory.
The two invariants differ because the artifacts differ. §19's table is a
bijection, so all of it is asserted: one row per fixture, both
directions, and category slug == basename with `_` as `-` (verified
across all 22 rows). Appendix D's rows are curated summaries that
deliberately bundle several cases — uploads_write.json legitimately has
four — so it gets coverage only, and a self-test case pins that
difference by asserting several rows for one fixture still passes.
Both tables also reject a row whose attribution cell is blank, and a
`§N` reference that resolves to no `## §N.` heading — the latter catching
a reference that resolved when written and stopped resolving when a
section was renumbered, which a reviewer of the same PR cannot see. A row
with no section reference at all is still accepted, because rejecting it
needs a carve-out for live-my-surface.json's external-governance
attribution and the carve-out list is the part that grows.
Neither table is writable: --write only ever touched line spans, and a
row here carries an owning-section attribution or a case summary only
the fixture's author can make.
Both checks reject SPEC.md as it stood before the preceding commit:
SPEC.md:2110-2131: conformance/tests holds 22 tracked fixture(s), the
table categorises 20; missing: `documents_write.json`,
`uploads_write.json`.
SPEC.md:3379-3458: no row maps these tracked fixtures to a primary
section: `documents_write.json`, `search.json`.
Claude-Session: https://claude.ai/code/session_01VyZUi8zkRfhatnBYoS4eyn
…lugs Two review findings on the roster gate, both real interactions rather than Markdown-spelling edge cases. Block span bodies were dropped from the prose pool along with line spans, but the two are not alike. The writer rewrites line spans only, and the block checkers read nothing but the `|` rows, so an ordinary sentence parked inside a roster or assertion-types block survives both untouched. Excluding the whole body let "verified against <current pin>" sit there with no marker and no grant — invisible to check_unmarked_pin and silently stale at the next repin, which is the exact claim class this gate exists to catch, hidden by the gate's own span bookkeeping. Only line spans leave the pool now. The §19 categories table tallied FILES, which catches one fixture on two rows but not two fixtures deriving one category. `_` and `-` collapse to the same slug, so `foo_bar.json` and `foo-bar.json` each satisfy the per-row slug rule while the table stops being the bijection its heading asserts. Now tallied on the DERIVED slug — a row whose category cell is simply wrong is already reported and still reaches that tally, so grouping by the declared cell would both miss real collisions and invent false ones. Appendix D is unaffected: it has no category column and deliberately allows many rows per fixture. Both self-test cases were shown to fail against the un-fixed gate first — reverting each fix in turn leaves exactly its own case reporting "expected FAILURE, gate exited 0".
c52d1dc to
865bacc
Compare
|
Rebased onto main now that #742 has merged (the census half of this PR's original split), and took the two findings in the last round that describe real interactions. Fixed in
Both self-test cases were shown to fail against the un-fixed gate first. Declined, deliberately: the two Markdown-spelling findings (three-hyphen separator cells, backslash parity before a pipe). Both are true. But this is the eleventh comment on
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
Suppressed comments (4)
scripts/sync-doc-constants.rb:932
- Require exactly three cells here as well. For example, an unescaped pipe in a test summary shifts the real Primary section into a fourth cell; this code treats the preceding fragment as the section, ignores the actual
§N, and still counts the fixture as covered. Rejecting extra columns makes malformed rows fail closed.
if cells.length < 3
SPEC.md:2195
- This now documents a gate that is not present: the PR deliberately split
check-fixture-executionout, the repository has no such script or Make target, and #602 remains open. The following roster-validation paragraph is likewise describing the removed gate. Restore the pending-gap wording so SPEC does not promise coverage CI cannot provide.
layer down. `make check-fixture-execution` (#602) is what detects it now: it
reads the six runners' skip mechanisms and fails when a mock case is excluded
everywhere. Maximum overlap today is 2 of 6, so that gate is green on arrival
and its self-test — which crafts the all-six state — is what proves it can say
no.
scripts/sync-doc-constants.rb:827
- Require exactly three cells, not merely at least three. With a raw pipe in this attribution, a four-cell row is accepted and only
cells[2]is validated; a later invalid reference such as§99is silently ignored even though the gate claims to validate every section reference. Escaped pipes still remain valid cell content.
This issue also appears on line 932 of the same file.
if cells.length < 3
scripts/sync-doc-constants.rb:795
- This explanation is incorrect for the both-empty case covered by the new self-test: when both the tracked-fixture set and table rows are empty, the bidirectional set comparison passes, so this guard does add coverage rather than only improving the message. State that distinction explicitly; otherwise a maintainer could remove the guard based on this comment and reintroduce the vacuous pass.
# Both directions already fail loudly on their own — an empty table makes every
# fixture `missing`, an empty fixture list makes every row `extra` — so this
# buys no coverage. It buys the message: a regex that stopped matching would
# otherwise be reported as 22 individually plausible drift findings instead of
# the one thing that is actually wrong. House standard since
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 865bacccb6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…iting deletion Codex raised the first as a P1 and Copilot as a suppressed comment; both were right, and it is the defect class this PR family exists to prevent — prose claiming coverage CI cannot provide. SPEC §19 said `make check-fixture-execution` (#602) "is what detects it now". That gate was the source-text parser split out of this PR to conformance-skips-parser-archive; the prose describing it stayed behind. There is no such script and no such target — `make -n check-fixture-execution` exits "No rule to make target" — so the paragraph promised all-six detection that does not exist, while #602 is still open. Replaced with what is actually true: each runner's case census (#742) catches a case executed by no runner for a MECHANICAL reason, and explicitly does not catch the deliberate all-six exclusion this section describes, because each census counts its own skip and stays green. The roster below it is restated rather than derived, and nothing checks it (#736) — which is why #736 waits for #602's cross-runner manifest instead of being fixed on its own. Separately, roster_vacuity's comment claimed the guard "buys no coverage". That is true only when ONE side is empty. When BOTH are, `missing` and `extra` are both empty, the comparison is trivially satisfied, and this guard is the only thing refusing the vacuous pass — a committed self-test case covers exactly that. The comment as written invited deleting a live guard on the strength of reasoning that applies to a different case.
|
Round two at Codex P1 — SPEC promised a gate that does not exist. Copilot suppressed — Declined: roster tables inside code fences. Fenced rows are still parsed and still checked, so it cannot let a wrong roster pass — it produces a roster that renders as a code block, a display defect any reader sees. That makes it the third Markdown-spelling finding on this file after two were declined this round, which is the signal rather than the task. Reasoning is in the thread.
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
Suppressed comments (2)
CONTRIBUTING.md:238
- This says the convention “is the only place” it was documented, but before this PR the checklist mentioned neither roster, while after this PR the convention is also stated in SPEC.md and the gate policy. Rephrase this as a historical absence so the contributor guidance matches the PR’s rationale.
`make doc-constants-check` fails until both exist — it is the only place
this convention was written down, which is why three fixtures in a row
landed with one row, the other, or neither.
scripts/sync-doc-constants.rb:949
- Rows with more than three cells are accepted. An unescaped pipe in a test summary (for example,
|alpha.json| supports A | B | §1 |) shiftsBintocells[2]and the real section into an ignored fourth cell; because non-§attributions are intentionally allowed, the gate then passes even though the rendered Primary section is not the claimed section. Require exactly three parsed cells so malformed rows fail closed.
if cells.length < 3
errors << "#{span.file}:#{line_no}: mapping row has #{cells.length} cell(s); the shape is " \
"| `file.json` | Test name | Primary section |"
next
|
@codex review |
…ying claim Copilot raised the cell count three times across two rounds; taking it, because it asks for something different from the Markdown-spelling findings declined alongside it. Those asked the splitter to UNDERSTAND more Markdown — separator widths, backslash parity. This asks it to REFUSE what it does not understand, which is the direction this file already argues for: "a row the parser cannot see is a row it silently vouches for." And it closes the pipe class as a class rather than one spelling at a time — however a stray pipe was written, the cell count is wrong and the row fails loudly instead of being mis-parsed quietly. The consequence was real, not cosmetic. A raw pipe in an attribution shifts the real section into a fourth cell and leaves the fragment before it in cells[2], where non-`§` attributions are legitimately allowed — so the gate validated the wrong cell and a `§99` in the actual section position was never checked, on a gate whose whole claim is that it validates every section reference. Both tables had it; Appendix D's free-form summaries are the likeliest place for someone to write `supports A | B`. Self-test cases added for both tables and shown to fail against `< 3` first. CONTRIBUTING.md separately claimed the checklist item "is the only place this convention was written down" — false the moment this PR also stated it in SPEC §19 and the gate. Rephrased as the historical absence it describes.
|
Round three at Required exactly three cells per roster row. Copilot raised this three times across two rounds and I had been treating it as part of the declined Markdown-spelling class. That was the wrong call, and the distinction is worth stating: the declined findings ask the splitter to understand more Markdown (separator widths, backslash parity); this one asks it to refuse what it does not understand. That is the direction this file already argues for — "a row the parser cannot see is a row it silently vouches for" — and it closes the pipe class as a class rather than one spelling at a time. The consequence was real, not cosmetic: a raw pipe in an attribution shifts the real section into a fourth cell and leaves the fragment before it in Worth noting this also subsumes the backslash-parity finding I declined earlier: a row whose pipes split unexpectedly now fails on its cell count instead of being silently mis-parsed. Loud-wrong beats quiet-wrong, which was the actual objection. CONTRIBUTING.md claimed the checklist item "is the only place this convention was written down" — self-falsifying, since this PR also states it in SPEC §19 and the gate. Rephrased as the historical absence it describes.
|
|
@codex review |
|
Codex Review: Didn't find any major issues. Can't wait for the next one! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
Suppressed comments (2)
scripts/sync-doc-constants.rb:741
- Valid Markdown table rows do not require a leading pipe, but this filter silently drops them. For example, adding
gamma |gamma.json| §1to this marked table renders as a roster row while the gate still passes, so an untracked or duplicate entry can evade the bidirectional check. Parse optional-leading-pipe rows, or explicitly reject any table row shape the gate does not support instead of filtering it out.
numbered = span.lines.each_with_index
.map { |line, index| [span.line_no + index, line.strip] }
.select { |_, line| line.start_with?("|") }
scripts/sync-doc-constants.rb:391
- This changes
proseto retain block-span bodies, butscan_file's return-contract comment above still saysprose_linesexcludes content inside any marked span. Update that contract to distinguish line spans from block spans so it describes the data callers now receive.
spans.each do |s|
next if s.block
(s.line_no...(s.line_no + s.lines.length)).each { |n| covered << n }
…743) * Detect a fixture case that every runner excludes (#602) Each runner's case census (#742) answers "did THIS runner account for every case". It cannot answer the question #602 actually asks — is any case executed by NO runner — because a case every runner excludes leaves all six censuses green: each one counted its own skip. Only a comparison across runners sees it. Every runner now writes the cases it did not execute, with reasons, to conformance/manifests/<runner>.json, and scripts/check-fixture-execution.rb fails when a case appears in all six. Manifests rather than parsed output because TypeScript prints no `SKIP:` line — a skip there is `it.skip`, reported in vitest's own format — so a gate scraping stdout would be blind to exactly one runner, in the silent direction: TypeScript would contribute an empty exclusion set and no case could reach all-six. Each manifest also records `executed`, asserted against the census total by both writer and reader: without it a case a runner silently dropped is simply absent from its exclusion set, and absent reads identically to "ran fine". THE ABSENCE RULE IS THE DESIGN. FULL mode requires all six manifests and fails if any is missing — a missing manifest must never read as "that runner executed everything", which is exactly what makes an all-six case invisible. Swift's runner is macOS-only, so a Linux run produces five and uses PARTIAL mode: an exclusion shared by every VISIBLE runner is a warning, never a failure, because five-of-six is not the all-six claim and a warning cannot false-fail. Both modes fail on zero manifests. CI resolves it properly rather than living with the partial answer: the six language jobs each upload their manifest and the existing fan-in job runs FULL mode over all six. That step is ordered AFTER the results check, because the job is `if: always()` — a language job that died before its upload would otherwise be reported as a missing manifest, burying the real cause. The local target depends on `conformance` rather than trusting whatever manifests are on disk, which would let it validate last week's exclusion sets. Maximum overlap today is 2 of 6 (#596 narrowed it), so the gate is green on arrival and a live run only proves it can say yes. scripts/test-check-fixture-execution.rb crafts the all-six state and every absence, integrity and disagreement case; it already found one defect (a non-object manifest crashed with a backtrace instead of naming the file). Verified end-to-end too: adding one shared exclusion to all six real manifests makes the gate fail and name every runner's reason. SPEC §19 now describes the gate that exists, closing the gap #740's P1 found — where the prose promised `make check-fixture-execution` after the source-text parser it referred to had been withdrawn. * Gate the Ruby and Python manifest uploads to the leg that runs conformance Both jobs are matrixed and run their conformance suite on ONE version — `matrix.ruby == '3.3'`, `matrix.python == '3.13'`. The upload steps went in unconditionally, so on every other leg the conformance step was skipped, no manifest was written, and `if-no-files-found: error` failed the job. Seven red checks, all of them this. Mirroring the condition is the fix rather than relaxing if-no-files-found: a leg that DOES run conformance and produces no manifest is a real defect, and that is exactly what the fan-in gate's absence rule depends on being loud. Go, TypeScript, Kotlin and Swift have no matrix, so their uploads stay unconditional and each artifact name remains unique. * Keep --partial from softening a complete manifest set, and wire the target in Two review findings from Copilot, both correct. `--partial` describes the INPUT — "this run cannot produce all six" — not a licence to soften the verdict. When every expected runner reported anyway (a macOS developer passing the flag out of habit, or a CI step keeping it for safety), "excluded by all present runners" IS the all-six claim, and the warning path let the one state this gate exists to reject exit 0. Partial handling now applies only when a manifest is genuinely absent. The self-test case for it was shown to fail against the un-narrowed code first. `check-fixture-execution` was also absent from `check-targets`: the edit that was supposed to add it matched the first occurrence of its anchor, which was the .PHONY line, so the target existed and `make check` never ran it. Now in the list, verified by reading the check-targets line itself rather than grepping the file. * Give exclusions a real identity, own a fresh run, and self-test in CI Four review findings, all real, two of them defects in the gate's core claim. CASE IDENTITY IS [file, name], NOT name. Codex is right that fixture case names are not unique: verified, "replace-omission-clears: sparse replace sends the request verbatim with no GET" appears in THREE fixtures and the non-idempotent POST retry name in two, while names are unique within a file. Keyed on name alone, a runner excluding one of those collapsed two entries into one — its own `executed + excluded` integrity check would then fail spuriously, and worse, a name excluded by three runners in one file and three in another would read as excluded by all six. A false failure on cases that all run. Every runner now records the fixture file, and the gate keys on the pair; it also rejects the same case excluded twice by one runner, which would otherwise add up while the comparison saw a single entry. STALE MANIFESTS. Both bots found this, and it was created by the --partial narrowing in the previous commit. On Linux `conformance-swift` is a no-op, so a swift.json left by an earlier macOS run over the same checkout survives while the other five refresh; the gate then sees six manifests, stops treating the run as partial, and compares five current exclusion sets against a stale sixth. Silent-wrong, in a gate whose whole claim is about what the runners actually did. The target now wipes conformance/manifests and runs the suite itself, so "six manifests present" means "six runners reported in this run". The reset is in the recipe, not a prerequisite: prerequisite order is not guaranteed under `make -j`, and a reset racing the runners would delete the output it exists to protect. check-targets therefore lists this target INSTEAD of `conformance`, so the suite still runs exactly once per `make check`. SELF-TEST IN CI. The fan-in job ran only the live gate, whose inputs are built to pass; the self-test was reachable only through the Make target the workflow deliberately bypasses. A regression making the gate accept a missing manifest or an all-six exclusion would have left CI green — the check-targets-is-not-CI -coverage shape, in the PR that adds the gate. It now runs as its own step. New self-test cases: one name in two files is two cases and must NOT fire; the same name in one file excluded everywhere still must; an exclusion without its fixture file is rejected; one case excluded twice in a manifest is rejected. * Reset manifests via an order-only prerequisite, not a sub-make The previous commit had check-fixture-execution run `$(MAKE) conformance` itself so it could wipe the manifest directory first. That worked, and cost something I did not price: with `conformance` no longer listed in check-targets, `conformance-kotlin` stopped being reachable from check-targets in the graph check-gradle-serialization walks — so that gate could no longer see one of the two Gradle invocations it exists to keep off each other. Its self-test caught it exactly as designed ("conformance-kotlin edge removed: expected the gate to FAIL, it passed"), which is a mutation case earning its keep on a change nobody wrote it for. Restructured so both properties hold. `conformance` is back in check-targets and check-fixture-execution depends on it, restoring reachability. The reset is now a phony target that all six language targets take as an ORDER-ONLY prerequisite (`|`), which is what makes it correct under `make -j`: make builds a prerequisite to completion before any dependent starts, and builds the phony target once per invocation, so the reset cannot race the runners it protects. A plain prerequisite of the aggregate target would not do that — siblings may run concurrently. It fires for a single-language run too, deliberately: `make conformance-go` clears the directory, so it holds only what that invocation produced and the gate goes partial rather than silently mixing runs across machines. Verified both directions: a planted stale swift.json is wiped and regenerated, and `make conformance-go` alone leaves only go.json. * Let a re-run replace its own manifest artifact Copilot flagged this on all six uploads. Re-running a job creates a new ATTEMPT within the same run, and upload-artifact v4+ artifacts are immutable per run — so the second attempt fails on a name conflict with the first attempt's manifest, before the fan-in gate can run. Reachable exactly when someone is re-running to get a red PR green, which is the worst time for the gate to become unreachable. `overwrite: true` is also right on the merits, not just as conflict avoidance: the collecting gate must read THIS attempt's exclusion set. An artifact left by a previous attempt is the CI-side version of the stale-manifest bug fixed two commits ago, and the same answer applies — six manifests present must mean six runners reported in this run.
Copilot's suppressed comment, and the third silent-pass hole this round: a
stale roster entry written as an indented `-`, or with `*`, `+`, or `1.`, was
skipped by `start_with?("- ")`. Skipped means absent from the roster set, which
means it contradicts no manifest, which means the gate passes. A false green —
the one outcome this extractor may not produce, and the invariant the PR argues
from.
Three holes of one shape in a single round is evidence about the instrument,
not a queue of three patches, so this is not a third selector. The default is
inverted instead: a line that looks like a list item in ANY form must be the
canonical `- "case name"`, or it is an error. One predicate covers every
spelling, including ones nobody has written yet — the same "refuse what you do
not understand" move that was right for the roster tables' cell count in #740,
rather than teaching the parser one more shape.
Prose is untouched because it is not list-shaped: the roster's headings wrap
onto continuation lines and Python's section is a sentence, and verified that
every list-like line in the real roster is already canonical.
Four self-test cases, one per marker form, all shown to fail against the
permissive skip.
…736) (#744) * Check SPEC's Zero-Skip roster against what the runners reported (#736) SPEC section 19's roster promises "every skip a default (mock-mode) conformance run reports, one line per runner x test, verbatim from the runners' skip mechanisms", plus the maintenance rule "a PR that closes a gap deletes exactly its own lines". Both were enforced by nothing. It was already wrong. Kotlin and Swift each exclude "List operation returns first page with Link header" wholesale through their `link-header` tag branch, and the roster described that in prose instead of enumerating it — two of six runners misstated, in a roster long enough that nobody re-derives it by hand. Those lines are added here, which is what makes the set equality hold. The execution manifests (#602) made the ENUMERATION derivable, so check-fixture-execution now compares it for set equality in both directions: a runner skip with no roster line fails, and a roster line for a skip that no longer exists fails. The CLASSIFICATION and reasoning on each line stay judgement and nothing asserts them, which is why the section keeps `[manual]`. On the parser, since #740 declined to keep teaching sync-doc-constants more GFM: a mis-parse there is SILENT — it validates the wrong cell and reports success. This extraction fails LOUD in both directions. A bullet it cannot read is a name missing from the roster set; a name it invents is an extra. Neither produces a passing comparison, so the failure mode is a false alarm the author fixes, never a false green. A self-test case pins that property. The delimiters are deliberately not @-markers: sync-doc-constants owns those and runs in spec-gates, where no conformance run has happened and there are no manifests to compare against. Registering a kind there whose real enforcement lives here would split one check across two gates. Five self-test cases, each shown to fail against a gate with the roster check removed: skip missing from the roster, roster line for a closed gap, a runner with no section, SPEC with no roster block, and a bullet without a quoted name. * Check the roster in partial mode, and refuse duplicates that pass silently Three review findings, both bots converging on two classes. Two of them are silent passes, which is the one failure mode this extractor is not allowed to have — the whole argument for reading prose here is that every misreading surfaces as a set mismatch. ROSTER DRIFT WENT UNCHECKED ON LINUX. The roster comparison sat after the partial-mode early return, and the Linux `make check-fixture-execution` path always passes --partial because Swift's manifest is macOS-only. So the normal local path never checked the roster at all: a stale Go or Ruby line passed and only the CI fan-in could catch it. The check now runs before the branch, over whatever manifests exist — "does Ruby's roster line match what Ruby reported" needs Ruby's manifest and nothing else. Partial input relaxes exactly one thing, the all-six overlap verdict, because that is the only claim needing every runner. A SECOND ROSTER BLOCK WAS IGNORED. parse_roster took the first begin and the first end, so a duplicate complete block was never compared and a stale line inside it passed unnoticed. Now exactly one of each delimiter is required, with "no roster at all" kept as its own distinct failure so the message names what happened. A CASE LISTED TWICE UNDER ONE RUNNER WAS INVISIBLE. Array#- removes every matching occurrence, so `actual - stated` and `stated - actual` both came back empty and the duplicate passed — carrying two possibly conflicting classifications for one case, against a section promising one line per runner x test. Duplicate bullets and duplicate runner headings are both rejected now. Four self-test cases, each shown to fail against the un-fixed code first: roster drift caught in partial mode, two roster blocks, one case listed twice, two sections for one runner. * Fail closed on any list-shaped roster line, closing the class Copilot's suppressed comment, and the third silent-pass hole this round: a stale roster entry written as an indented `-`, or with `*`, `+`, or `1.`, was skipped by `start_with?("- ")`. Skipped means absent from the roster set, which means it contradicts no manifest, which means the gate passes. A false green — the one outcome this extractor may not produce, and the invariant the PR argues from. Three holes of one shape in a single round is evidence about the instrument, not a queue of three patches, so this is not a third selector. The default is inverted instead: a line that looks like a list item in ANY form must be the canonical `- "case name"`, or it is an error. One predicate covers every spelling, including ones nobody has written yet — the same "refuse what you do not understand" move that was right for the roster tables' cell count in #740, rather than teaching the parser one more shape. Prose is untouched because it is not list-shaped: the roster's headings wrap onto continuation lines and Python's section is a sentence, and verified that every list-like line in the real roster is already canonical. Four self-test cases, one per marker form, all shown to fail against the permissive skip.
Three rosters in this repo claim to account for every conformance fixture. Two of them had drifted, and nothing checked either.
origin/mainbefore this PRdocuments_write.json,uploads_write.jsondocuments_write.json,search.jsonThe convention was missed by the last three fixture-adding commits, in three different ways.
dee221c85(#601) addeddocuments_write.jsonand updated neither table.b238e5ed4(#683) addeduploads_write.jsonwith four Appendix D rows and no §19 row. #726 addedsearch.json's §19 row and missed Appendix D. Two half-applications in opposite directions is much stronger evidence for a gate than either gap alone: it is not a person being careless, it is a rule nobody can see.CONTRIBUTING.mdtold contributors to add conformance tests and mentioned neither table — this PR fixes that too.No adversary here, and asking who the attacker is would mislead: this is the accident-and-regression class, which is what a cheap merge-blocking check is legitimately for.
It reuses existing machinery
sync-doc-constants.rbalready does table-completeness checking —@assertion-typeswraps §19's assertion-type table in a block marker and set-compares it againstconformance/schema.json. The two fixture rosters are the same shape one level out, so they become two more block kinds: no new script, no new Makefile target, no new CI step.make doc-constants-checkalready runs the gate and its self-test and is already incheck-targetsandspec-gates.The two invariants differ because the artifacts differ. §19's table is a bijection, so all of it is asserted — one row per fixture, both directions, and category slug == basename with
_as-(verified across all 22 rows, zero violations). Appendix D's rows are curated summaries that deliberately bundle several cases (uploads_write.jsonlegitimately has four), so it gets coverage only, and a self-test case pins that difference by asserting several rows for one fixture still passes.Both tables also reject a row whose attribution cell is blank, and a
§Nthat resolves to no## §N.heading — the latter catching a reference that resolved when written and stopped resolving when a section was renumbered, which a reviewer of the same PR cannot see. A row with no section reference is still accepted: rejecting it needs a carve-out forlive-my-surface.json's external-governance attribution, and the carve-out list is the part that grows.Neither table is writable.
--writeonly ever touched line spans, and a row here carries an attribution only the fixture's author can make.Verification
origin/mainas it stood, and pass only after the first commit's rows land (REAL_EXIT=1, output quoted in that commit).make doc-constants-checkgreen underLC_ALL=C.Scope: #602 is deliberately not here
This PR originally also carried a source-text parser for the six conformance runners, to answer #602 ("is any fixture executed by nothing?") and to hold SPEC §19's Zero-Skip roster to it. That half has been split out. Its rule count grew across nine commits and four review rounds while the call sites it covers never moved — the state
AGENTS.mddescribes as "reassess the instrument, not write a sixth selector".The question is right; the instrument was wrong. Ground truth for "what does this runner skip" is runtime, not source text, and every runner already reports
Passed/Failed/Skipped/Total— sopassed + failed + skipped == mock case count, per runner is a single arithmetic identity that subsumes most of what the parser did by hand: nested fixtures,modetypos, empty fixtures, dropped cases,result.skipped, derived tables, spreads and percent-strings alike.The parser work is preserved on
conformance-skips-parser-archiveand #602 stays open. Note for whoever picks it up: five runners printSKIP: <name>; TypeScript does not — it callsit.skip(), so that lane needs vitest's JSON reporter rather than a line match.Closes nothing on its own; #602 remains open.