Skip to content

Account for every conformance fixture in both SPEC rosters, and gate them - #740

Merged
jeremy merged 5 commits into
mainfrom
fixture-roster-gates
Aug 13, 2026
Merged

Account for every conformance fixture in both SPEC rosters, and gate them#740
jeremy merged 5 commits into
mainfrom
fixture-roster-gates

Conversation

@jeremy

@jeremy jeremy commented Aug 13, 2026

Copy link
Copy Markdown
Member

Three rosters in this repo claim to account for every conformance fixture. Two of them had drifted, and nothing checked either.

claim where state on origin/main before this PR
every fixture has an owning spec section SPEC §19 Test Categories table missing documents_write.json, uploads_write.json
every fixture maps to a primary section SPEC Appendix D missing documents_write.json, search.json

The convention was missed by the last three fixture-adding commits, in three different ways. dee221c85 (#601) added documents_write.json and updated neither table. b238e5ed4 (#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 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.md told 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.rb already does table-completeness checking — @assertion-types wraps §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, so they become two more block kinds: no new script, no new Makefile target, no 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 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.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 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 is still accepted: 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 attribution only the fixture's author can make.

Verification

  • Both checks reject origin/main as it stood, and pass only after the first commit's rows land (REAL_EXIT=1, output quoted in that commit).
  • Every guard was shown to fail its self-test when removed — one mutation per guard, restored by copy.
  • Vacuity guarded on both sides, and it earns it: a gutted table is caught by the set comparison alone, but both sides empty at once exits 0 without the explicit guards. That is why they are stated rather than left to emerge, and there is a case for it.
  • Each commit is green standalone (the markers ship with the gate that reads them, so the backfill commit does not reference a kind the inventory has not yet declared).
  • make doc-constants-check green under LC_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.md describes 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 — so passed + failed + skipped == mock case count, per runner is a single arithmetic identity that subsumes most of what the parser did by hand: nested fixtures, mode typos, empty fixtures, dropped cases, result.skipped, derived tables, spreads and percent-strings alike.

The parser work is preserved on conformance-skips-parser-archive and #602 stays open. Note for whoever picks it up: five runners print SKIP: <name>; TypeScript does not — it calls it.skip(), so that lane needs vitest's JSON reporter rather than a line match.

Closes nothing on its own; #602 remains open.

Copilot AI balanced review requested due to automatic review settings August 13, 2026 02:47
@github-actions

Copy link
Copy Markdown
Contributor

Sensitive Change Detection (shadow mode)

This PR modifies control-plane files:

  • .github/workflows/test.yml

Shadow mode — this check is informational only. When activated, changes to these paths will require approval from a maintainer.

@github-actions github-actions Bot added github-actions Pull requests that update GitHub Actions spec Changes to the Smithy spec or OpenAPI labels Aug 13, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread scripts/conformance_skips.rb Outdated
Comment thread scripts/check-fixture-execution Outdated
Comment thread scripts/sync-doc-constants.rb
Comment thread scripts/sync-doc-constants.rb Outdated
Comment thread Makefile Outdated
Copilot AI review requested due to automatic review settings August 13, 2026 02:52

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread scripts/conformance_skips.rb Outdated
Comment thread scripts/sync-doc-constants.rb Outdated
Comment thread scripts/sync-doc-constants.rb Outdated
Comment thread scripts/conformance_skips.rb Outdated
Comment thread scripts/check-fixture-execution Outdated
Comment thread scripts/check-fixture-execution Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.rb reads 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)

Copilot AI review requested due to automatic review settings August 13, 2026 02:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-check vouch 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 sets entered from 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 to covered, 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": false or null as if the field were omitted, so this validator accepts it as mock instead 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: because mode is absent, every runner defaults it to mock.
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": false or null is 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)

Copilot AI review requested due to automatic review settings August 13, 2026 03:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-case link-header checks 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 present false or null mode as if the field were omitted. Those are not recognized modes, but this validator reclassifies them as mock, 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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread scripts/check-fixture-execution Outdated
Comment thread scripts/conformance_skips.rb Outdated
Comment thread scripts/check-fixture-execution Outdated
Comment thread scripts/conformance_skips.rb Outdated
Comment thread scripts/sync-doc-constants.rb Outdated
Comment thread scripts/check-fixture-execution Outdated
Copilot AI review requested due to automatic review settings August 13, 2026 03:13

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_SET leaves entered false, so scan continues into the following declaration (often the companion reason map) and can return those keys; SKIPS = [] + EXTRA_SKIPS returns 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 \| B and whose Primary section is blank is parsed as if B were 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("|")

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread scripts/conformance_skips.rb Outdated
Comment thread scripts/conformance_skips.rb Outdated
Comment thread scripts/conformance_skips.rb Outdated
Copilot AI review requested due to automatic review settings August 13, 2026 03:29

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings August 13, 2026 03:34

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 to a \. The new escaped-quote self-test actually expects skipped 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

Comment thread scripts/conformance_skips.rb Outdated
Copilot AI review requested due to automatic review settings August 13, 2026 03:38

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread scripts/conformance_skips.rb Outdated
Comment thread scripts/conformance_skips.rb Outdated
Comment thread scripts/conformance_skips.rb Outdated
jeremy added 3 commits August 13, 2026 13:08
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".
Copilot AI review requested due to automatic review settings August 13, 2026 20:13
@jeremy
jeremy force-pushed the fixture-roster-gates branch from c52d1dc to 865bacc Compare August 13, 2026 20:13
@jeremy

jeremy commented Aug 13, 2026

Copy link
Copy Markdown
Member Author

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 865bacc:

  • Block bodies were dropped from the pin scan along with line spans. The two are not alike: the writer rewrites line spans only, and the block checkers read nothing but the | rows, so a sentence parked inside a roster block survives both. That let an unmarked current-pin claim hide inside a table — the gate's own span bookkeeping concealing the claim class it exists to catch.
  • Colliding category slugs. The table tallied files, so foo_bar.json and foo-bar.json both derive foo-bar, both rows pass the per-row slug rule, and the table quietly stops being the bijection its heading asserts. Now tallied on the derived slug.

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 sync-doc-constants.rb, and that shape — "another Markdown spelling the parser does not recognise" — is what got the source-text parser withdrawn from this same PR after nine commits of rules covered exactly the call sites it started with. The failure mode is a colleague mistyping a roster row; the consequence is a table that renders wrong in SPEC.md, which a human reviewing that diff sees immediately. If one ever does slip through unnoticed, the answer is to stop hand-parsing GFM in Ruby, not to widen the regex again. Reasoning is in each thread so it does not get re-litigated next round.

make doc-constants-check green (13 marked spans, 22 fixtures). Full make check running on Linux.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-execution out, 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 §99 is 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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread SPEC.md Outdated
Comment thread scripts/sync-doc-constants.rb
…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.
Copilot AI review requested due to automatic review settings August 13, 2026 20:21
@jeremy

jeremy commented Aug 13, 2026

Copy link
Copy Markdown
Member Author

Round two at 9d70731. One of these was a genuine miss on my part.

Codex P1 — SPEC promised a gate that does not exist. make check-fixture-execution was the source-text parser split out of this PR to conformance-skips-parser-archive; the prose describing it stayed behind. Verified: make -n check-fixture-execution exits "No rule to make target". That is precisely the defect class this PR family exists to prevent, sitting in the PR that adds the gates. Copilot flagged it in its suppressed block too. SPEC §19 now states what is true — the merged case census (#742) catches a case executed by no runner for a mechanical reason, and explicitly does not catch the deliberate all-six exclusion, because each census counts its own skip and stays green. #602 stays open with the reason recorded, and #736 now points at the same cross-runner manifest.

Copilot suppressed — roster_vacuity's comment invited deleting a live guard. It claimed the guard "buys no coverage". True when one side is empty; false when both are, where 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 covers exactly that case. Comment corrected to name the distinction and say which test dies if the guard goes.

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.

make doc-constants-check green; make check passed on Linux at 865bacc and is re-running for this head.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 |) shifts B into cells[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

@jeremy

jeremy commented Aug 13, 2026

Copy link
Copy Markdown
Member Author

@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.
Copilot AI review requested due to automatic review settings August 13, 2026 20:30
@jeremy

jeremy commented Aug 13, 2026

Copy link
Copy Markdown
Member Author

Round three at 46bff5c, taking both of Copilot's suppressed comments.

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 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 claim is that it validates every section reference. Both tables had it. Self-test cases added for each, shown to fail against < 3 first.

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.

make doc-constants-check green; make check passed on Linux at 9d70731 and CI was green there (26 runs, 0 failures). Re-running both for this head.

@jeremy

jeremy commented Aug 13, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 46bff5c37b

ℹ️ 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".

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | §1 to 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 prose to retain block-span bodies, but scan_file's return-contract comment above still says prose_lines excludes 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 }

@jeremy
jeremy merged commit c7416bc into main Aug 13, 2026
45 checks passed
@jeremy
jeremy deleted the fixture-roster-gates branch August 13, 2026 20:45
jeremy added a commit that referenced this pull request Aug 14, 2026
…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.
jeremy added a commit that referenced this pull request Aug 14, 2026
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.
jeremy added a commit that referenced this pull request Aug 14, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

spec Changes to the Smithy spec or OpenAPI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants