Skip to content

Derive SPEC §5's service roster from the generated accessors (#600) - #745

Merged
jeremy merged 13 commits into
mainfrom
spec-roster-derived
Aug 17, 2026
Merged

Derive SPEC §5's service roster from the generated accessors (#600)#745
jeremy merged 13 commits into
mainfrom
spec-roster-derived

Conversation

@jeremy

@jeremy jeremy commented Aug 14, 2026

Copy link
Copy Markdown
Member

Closes #600.

Rebuilt on main after #746 (2026-08-15). This branch was based on the pre-#746 main and carried three commits that hardened scripts/check-fixture-execution.rb's prose roster reader. #746 deleted that reader, moved SPEC §19's Zero-Skip roster to spec/zero-skip-roster.yml, and made SPEC's block generated and byte-compared. Those three commits hardened something that no longer exists, so they are dropped rather than rebased — see the comment below. What remains is only the #600 service-roster work: four commits, and this PR no longer touches check-fixture-execution.rb at all.

The one non-parser hunk in the dropped range — a python/README.md fix documenting that a generated service without an accessor is constructed directly (GaugesService(account)) — is preserved, folded into the first commit alongside the Appendix F accessor-gap work it belongs with.

SPEC.md §5 listed 46 account-scoped services. There are 53.

#600 reported five missing. It is seven: cloudFiles and googleDocuments are also absent, and were missed by the issue itself because they arrive through SERVICE_SPLITS["Files"] rather than a new top-level tag — no PR ever "added a service" a reader could enumerate, and SPEC has never mentioned either (zero occurrences of "cloud file" or "google doc" in the repo). I corrected the issue body rather than quietly fixing past it, since #600's own argument is that a half-fix is worse than none.

kotlin generated accessors: 53      swift generated accessors: 53      identical: YES
SPEC §5 roster:             46
missing: bookmarks calendars cloudFiles drafts folders googleDocuments myNotes
extra:   (none)

Source of truth

The two generated accessor files, which must agree with each other:

  • kotlin/…/generated/ServiceAccessors.ktval AccountClient.<name>
  • swift/…/Generated/AccountClient+Services.swiftpublic var <name>: <X>Service

They exist solely to hang services off AccountClient, so they encode account-scoping directly rather than leaving it to be inferred. A disagreement between them is reported as a generator bug, never resolved by preferring one.

Rejected, with reasons worth keeping. openapi.json plus each generator's TAG_TO_SERVICE/SERVICE_SPLITS is the true root, but deriving it in Ruby means a sixth hand-copy of those tables — they already exist five times, once per language, and no gate compares them to each other (check-*-service-drift.sh compares operationIds, never service names). The derivation would become the drift surface this PR removes. Per-SDK client wiring can't answer it either: it drifts by design.

The gate

Two kinds in sync-doc-constants.rb: @account-scoped-services (block, holds the roster) and @service-count (line, sharing @operation-count's ticked-int checker).

These land beside #746's @zero-skip-roster, in the seams it opened rather than in place of them:

  • rewrite_line takes a hash of kind => already-computed Integer. Generate SPEC §19's Zero-Skip roster from YAML, and delete the prose reader #746 made its arguments values rather than thunks so the write loop cannot raise after earlier files are on disk; keying that by kind applies the same rule to a set of kinds, so a third ticked-int kind is an entry at the call site and still cannot reintroduce a deferred computation.
  • Generate SPEC §19's Zero-Skip roster from YAML, and delete the prose reader #746's write preflight — which forces every deferred input before a byte is written — gains the fourth entry it was written to accommodate. @service-count reads two generated SDK files, so it is forced only when a span actually claims it, by the same marked-only rule as the rest.
  • @account-scoped-services is a block kind and stays out of WRITABLE_BLOCK_KINDS. Since Generate SPEC §19's Zero-Skip roster from YAML, and delete the prose reader #746 made one block kind writable, that is now a choice rather than a consequence, and both places that stated the old blanket "no block kind is writable" now say which reason applies: every character of this roster is derivable, so the writer could author it — but only by fixing an order the check deliberately does not assert. The roster's claim is a set; the file is alphabetical as a courtesy to readers, not as a rule this gate holds.

The roster stays a comma list. table_rows is table-only, so reusing it means 53 rows of exactly three cells — and the only derivable column is the name. The other two would be new hand-maintained content, i.e. new drift surface, in a change whose entire point is removing drift surface.

So the parser is bespoke, and its shape is pinned tight: exactly one non-empty line, every entry lowerCamelCase, anything else an error. #736 asserted "every misreading surfaces as a mismatch, never a silent pass" in a comment and had it breached three times. Here that property is committed as cases: bullets, a wrapped line, an emptied block, a trailing comma, a duplicate entry, a second block, both accessor-file failure modes, and an extractor that matches nothing.

Every new case was shown to fail against a gate with its own guard removed — 13 mutations, each restored by cp + diff -q. Two of them initially hit the same guard (" dupes = …" is a substring of the 4-space-indented per-file check), so the roster's own duplicate guard was untested until the anchors were fixed; both now kill at distinct sites.

Every stale number, not just the roster

Marked and corrected: §1's component table, §5's heading, total-surface sentence and derivation rule, Appendix B, two Appendix F rows, and the Kotlin/Swift README claims (11 @service-count spans, all gated).

Reworded to restate nothing rather than marked — each earned nothing by naming a constant: §1's topology tree (43 more services) (already self-inconsistent: 2 shown + 43 ≠ 46), §5's = 47 services total and "12 coarse tags" (openapi.json now has 22), the Ruby and Python README counts, and spec/api-gaps/event-feed.md's "47 → 48".

Appendix F's per-SDK table was 46 across the board and is now measured:

SDK before now
Swift, Kotlin, Ruby, TypeScript 46 53
Go 44 51 — folds automation→Lineup and clientVisibility→Recordings, spells timesheets singular. Capability 53/53; surface is not
Python 46 51 — a genuine wiring gap: gauges.py and my_notifications.py exist and neither client references them

§5's claim that the five generator tables "produce identical service sets across SDKs" was false; it now says what actually holds.

Verification

All under LC_ALL=C, real exit codes:

command exit
ruby scripts/sync-doc-constants.rb --check 0
ruby scripts/test-doc-constants.rb 0
ruby scripts/test-check-fixture-execution.rb 0
ruby scripts/check-fixture-execution.rb 0
make doc-constants-check 0
make sync-api-version 0

The live gate reports 53 account-scoped services (Kotlin and Swift accessors agree) and 16 skips across 6 runners, i.e. #746's Zero-Skip roster still renders byte-identically beside the new kinds.

--write is a no-op on the clean tree — prints already in sync and leaves git status clean. This is the one that mattered for the rebuild, since #746 rewrote the writer and this PR's whole point is that the writer keeps the counts current.

The reconciliation is load-bearing, not decorative. Mutating the write preflight to resolve only operation-count (the shape #746 left behind — i.e. "forgot to extend it to the new kind") kills 18 self-test cases with KeyError: "service-count" from inside the write loop. Single matching site, verified changed on disk, restored by cp + diff -q, green again after restore.

Not run: full make check, blocked in this worktree by a local Go toolchain mismatch (go1.26.5 vs go1.26.2 compiling stdlib) unrelated to this change.

Follow-up worth its own issue

Those five TAG_TO_SERVICE/SERVICE_SPLITS copies have no gate comparing them to each other. Nothing here changes that — this PR checks a downstream rendering, which is why Go's and Python's divergences show up as documentation rather than as a failure.


Summary by cubic

Derives SPEC §5’s account-scoped service roster and count from the generated Kotlin/Swift AccountClient accessors and writes both during generation, removing drift. Old: a hand-maintained list and manual counts (46). New: roster + @service-count derive from accessors (53) and are written together, so a green generate cannot contradict itself.

  • Source of truth and gates: kotlin/.../generated/ServiceAccessors.kt and swift/.../Generated/AccountClient+Services.swift must agree; duplicate accessors fail; missing file or empty extraction is fatal. @service-count and @operation-count reject a zero-derived value. @api-version and @bc3-pin dates must be ISO format.
  • Roster marker and writer: adds @account-scoped-services and makes it writable; the writer emits accessor order (alphabetical today) while the check compares sets. The checker refuses malformed entries (non‑lowerCamelCase), repeated names, extra/missing services, empty or multi‑line blocks, and indented roster lines (to avoid silent reformatting).
  • Docs and SDK notes: materializes the 53‑service roster in §5 and Appendix B; updates Appendix F per‑SDK counts (Swift/Kotlin/TypeScript/Ruby 53; Go 51 with automation→Lineup and clientVisibility→Recordings; Python 51 with two unwired services, linked to Python's Client exposes 51 of 53 generated services — gauges and my_notifications are unreachable #732). Fixes Swift/Kotlin README counts and clarifies Kotlin’s handwritten composites without changing the accessor set. Updates Python README to show constructing missing-accessor services for both sync and async clients. Tweaks the event‑feed note.
  • Build/CI: make generate runs sync-api-version again after all generators so roster and counts reflect fresh accessors; contributor docs now point to make generate (remove stale per-target lists). CI enforces the updated doc‑constants gate and marker counts.

Written for commit e27db46. Summary will update on new commits.

Review in cubic

Copilot AI balanced review requested due to automatic review settings August 14, 2026 06:40
@github-actions github-actions Bot added github-actions Pull requests that update GitHub Actions ruby Pull requests that update the Ruby SDK kotlin swift spec Changes to the Smithy spec or OpenAPI python Pull requests that update the Python SDK labels Aug 14, 2026
@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.

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

Derives documented service counts and rosters from generated Kotlin/Swift accessors while adding drift checks for service and conformance skip documentation.

Changes:

  • Updates the canonical account-scoped service roster from 46 to 53.
  • Adds service-count and roster validation with negative-case tests.
  • Checks SPEC §19’s skip roster against execution manifests.

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 12 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
.github/workflows/test.yml Updates gate documentation.
SPEC.md Corrects service rosters/counts and skip documentation.
kotlin/README.md Updates service counts.
python/README.md Removes stale counts.
ruby/README.md References the canonical service set.
scripts/check-fixture-execution.rb Validates the documented skip roster.
scripts/sync-doc-constants.rb Derives and checks service documentation.
scripts/test-check-fixture-execution.rb Tests skip-roster failures.
scripts/test-doc-constants.rb Tests service-roster/count failures.
spec/api-gaps/event-feed.md Removes a stale numeric delta.
spec/doc-constants.json Registers new marker kinds and counts.
swift/README.md Updates service counts.
Suppressed comments (1)

python/README.md:458

  • Not all generated services are currently accessible through AccountClient: Appendix F now says gauges and myNotifications exist but neither client references them. This wording and the “full set” link obscure that user-visible gap; distinguish the wired client surface from the generated modules.
All services are accessed through an `AccountClient`, obtained via `client.for_account(account_id)`. The table below covers the common ones; see `basecamp/generated/services/` for the full set.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread scripts/check-fixture-execution.rb Outdated
Comment thread scripts/check-fixture-execution.rb
Comment thread python/README.md
Copilot AI review requested due to automatic review settings August 14, 2026 07:15
@jeremy

jeremy commented Aug 14, 2026

Copy link
Copy Markdown
Member Author

Round one at 8eae4e75. Three findings from Copilot, all correct; one was a defect in the gate's core claim, one was a documentation claim this PR itself falsified, and one is declined with reasoning in the thread.

The gate defect. The roster reader's fail-closed rule keyed on the list marker, and a stale entry does not need a marker to read as one. > - "case" — reason renders as a list item; a table row and a plain sentence state a case just as plainly; so does an HTML comment. All of them were skipped, never contradicted a manifest, and passed green — the same false green the marker rule was added to close, one level up. Verified by running each shape against the un-fixed reader first: exit 0 on all six.

The fix is not a wider marker regex. Teaching it about > would have answered exactly one spelling and left the rest, which is the tell that the marker was never the right thing to look at. The rule now keys on the payload: a roster line's claim is the quoted case name, so any line inside the block carrying a " must be the canonical - "case name" — reason bullet or it is an error. One predicate, closing blockquotes, tables, comments and spellings nobody has written.

The boundary is now stated rather than overclaimed. Quote-free, marker-free prose still passes, and the comment says so: an entry too vague to parse is too vague to be a roster line, the failure it could hide (under-listing) is caught from the other side by a runner exclusion the roster does not list, and only a stale entry can hide in a skipped line — which has to name its case to be one.

Six negative cases added, plus a positive one pinning the quote-free continuation prose the live roster relies on, since the payload rule can only be this strict because no heading or continuation line in the real block quotes a case name.

The documentation claim. python/README.md told users that every service in generated/services/ answers to account.<name>. Appendix F, in this same change, records that gauges and myNotifications do not. A user types account.gauges, gets an AttributeError, and cannot tell whether the SDK supports gauges at all. The Services section now says a generated service without an accessor is constructed from the account client directly, and points at Appendix F for which — written by reference, so it needs no edit when the gap closes. Verified against the built package: both services construct from an AccountClient and neither accessor exists.

Wiring those two accessors is the real fix and belongs in its own change, with its own sync/async tests, moving the Appendix F row this PR just established.

Declined: projecting [file, name] to name in the roster comparison. True that it loses information, but the roster identifies a case by name alone by design, so the claim being compared is a set of names and it survives the move described. The moment the ambiguity becomes real — one runner excluding a name in two fixtures — the duplicate guard immediately below refuses rather than comparing an ambiguous set, and says to add fixture qualifiers then. Reasoning is in the thread, left unresolved.

@jeremy

jeremy commented Aug 14, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: 8eae4e755d

ℹ️ 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 12 out of 12 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

SPEC.md:310

  • The Files split example remains incomplete: all generator mappings also split this tag into CloudFiles and GoogleDocuments (for example, typescript/scripts/generate-services.ts:214-215). As written, this sentence repeats the exact omission the PR is correcting and still documents only four of the six file services.
The OpenAPI spec groups operations under coarse tags (e.g., `Automation`, `Todos`, `Files`). The service generators split those tags into the `53` fine-grained services above <!-- @service-count --> using a two-table mapping: `TAG_TO_SERVICE` (tag → default service name) and `SERVICE_SPLITS` (tag → {service → [operationIds]}). For example, the `Todos` tag splits into `Todos`, `Todolists`, `Todosets`, `TodolistGroups`; the `Files` tag splits into `Attachments`, `Uploads`, `Vaults`, `Documents`. These mappings are defined in each language's generator script. They are five hand-maintained copies of one table with no gate comparing them, so they are expected to produce identical service sets and are not guaranteed to: Appendix F records where they currently do not.

Comment thread scripts/check-fixture-execution.rb Outdated
Copilot AI review requested due to automatic review settings August 14, 2026 07:19
@jeremy

jeremy commented Aug 14, 2026

Copy link
Copy Markdown
Member Author

Follow-up to the parser fix, at b412dfc2 — found by attacking the fix rather than by review, and it was a live hole in what I had just pushed.

The payload rule closed every line the reader falls through to. A runner heading is the one line it does not: **Go** (x) — also skips "ghost": is recognised as Go's section, consumed, and the claim riding on it is never compared. Same false green, in the last place left to hide one. Confirmed green against 8eae4e75 before moving anything.

The rule now runs ahead of every other branch, which is what makes "any line carrying a quoted case name" mean any line. It can be that strict because no heading or continuation line in the live roster quotes a case name — checked against SPEC.md, not assumed.

That reordering then un-pinned the marker rule, which is the more interesting part. All four of its cases carried quoted names, so the payload rule refuses them first and they would have stayed green with the marker rule deleted outright — a suite that looks thorough while testing one guard twice. Each marker now runs twice and the quote-free half is reachable by nothing else.

Kill matrix, restored by cp and verified byte-identical: deleting rule 1 kills 6 cases at line 290; deleting rule 2 kills 6 different cases at line 307. Disjoint sets, distinct lines.

ruby scripts/test-check-fixture-execution.rb, ruby scripts/check-fixture-execution.rb --partial and make doc-constants-check all exit 0 locally under LC_ALL=C; deferring to CI for the numbers.

@jeremy

jeremy commented Aug 14, 2026

Copy link
Copy Markdown
Member Author

@codex review

@jeremy

jeremy commented Aug 14, 2026

Copy link
Copy Markdown
Member Author

Stopping the patch loop on the roster reader — this needs a call, not a fifth round

6d773c32 is comment-only. Here is why I stopped writing fixes.

I probed the reader I had just hardened, and the payload rule has a character-class tail:

> - "straight stale" — x.                  exit=1 refused
> - “curly stale” — x.                     exit=0 <== SILENT PASS
The Go runner also skips “curly prose stale”.  exit=0 <== SILENT PASS
| Go | “curly table” | x |                 exit=0 <== SILENT PASS
> - `backtick stale` — x.                  exit=0 <== SILENT PASS

include?('"') is ASCII-only. A curly-quoted or backticked name in a shape the marker rule does not see is still a stale entry that passes green. Typographic quotes are not exotic here — they are what a paste from anything with autocorrect produces.

That is round four on one predicate: marker widening (4ce947b6), payload rule (8eae4e75), payload hoisted above the heading branch (b412dfc2), and now this. Each round was individually correct and individually small, which is exactly the accretion pattern that should stop rather than continue. Adding [“”\]` answers this one and invites the next, and I do not think the fifth selector is the right thing to write.

The terminating instrument, and it already exists in this PR

check_account_scoped_services has no such tail, and the reason is structural: it demands exactly one line and refuses anything else. It is a whitelist. Nothing is skipped, so nothing can hide, and no character class ever needs enumerating.

The same move here: every non-blank line inside the roster markers must be a runner heading or a canonical - "case name" — reason bullet. No third category. Blockquotes, tables, HTML comments, curly quotes, backticks and the spellings nobody has written are all refused by the absence of a fallthrough, not by recognition.

The cost is documentation formatting, which is why I am not just doing it. Five of the six sections currently rely on a fallthrough for prose:

  • Go, Kotlin, Swift, TypeScript — headings wrapped across two lines
  • Python — heading plus a two-line sentence ("none. The link-header fixture above runs; only its requestCount assertion is suppressed.")

Under the whitelist each of those has to be reflowed onto its heading line, or moved outside the markers. SPEC.md already carries much longer lines than that would produce, so it is feasible — but it is a readability call on a section people actually read, and it is yours.

The three options as I see them

  1. Whitelist. Terminates the class outright. Costs the reflow above, and future prose has to live on the heading line or outside the markers.
  2. Widen the payload class to ", , , backtick. Two-character fix, closes today's probe, does not terminate — the next reviewer brings «» or an unquoted name in a table cell.
  3. Accept the residue. It needs a conjunction to bite: a shape the marker rule does not see and a payload the quote rule does not see. Neither alone is enough, and under-listing is still caught from the other side by a runner exclusion the roster does not list.

I have taken option 3's honesty half regardless, since it is free and its absence is the specific failure this section's lineage repeated three times: the comment now states both residues explicitly instead of implying the class is shut. If you want 1 or 2, say which and I will do it.

Everything else on the PR is addressed — the two Copilot findings I fixed, the one I declined with reasoning in its thread, plus the heading hole I found by attacking my own fix. Both gates are wired into CI (test-check-fixture-execution.rb at test.yml:978, the full-mode live check at :996), so this reader is genuinely gated either way.

@jeremy

jeremy commented Aug 14, 2026

Copy link
Copy Markdown
Member Author

@codex review

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 12 out of 12 changed files in this pull request and generated no new comments.

Suppressed comments (2)

SPEC.md:310

  • The Files split is still documented as only four services, but every generator mapping includes CloudFiles and GoogleDocuments too (for example, ruby/scripts/generate-services.rb:66-72). This preserves the exact omission this PR is correcting; include both services in the example.
The OpenAPI spec groups operations under coarse tags (e.g., `Automation`, `Todos`, `Files`). The service generators split those tags into the `53` fine-grained services above <!-- @service-count --> using a two-table mapping: `TAG_TO_SERVICE` (tag → default service name) and `SERVICE_SPLITS` (tag → {service → [operationIds]}). For example, the `Todos` tag splits into `Todos`, `Todolists`, `Todosets`, `TodolistGroups`; the `Files` tag splits into `Attachments`, `Uploads`, `Vaults`, `Documents`. These mappings are defined in each language's generator script. They are five hand-maintained copies of one table with no gate comparing them, so they are expected to produce identical service sets and are not guaranteed to: Appendix F records where they currently do not.

SPEC.md:3649

  • The unbackticked 52 is another current-value claim derived from the service roster, but @service-count validates and rewrites only the single backticked integer. On the next service addition the writer will update 53 while leaving 52 stale, contrary to the derived-claim rule in spec/doc-constants.json:5. Avoid restating the arithmetic here.
| Kotlin | `53` — full canonical set (`ServiceAccessors.kt`, generated; §5's other source). `todos` exposes a handwritten composite subclassing the generated `TodosService`; the other 52 are the generated classes directly <!-- @service-count --> |

Copilot AI review requested due to automatic review settings August 14, 2026 07:24
@jeremy
jeremy force-pushed the spec-roster-derived branch from 6d773c3 to 5a82e2d Compare August 14, 2026 07:27

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 12 out of 12 changed files in this pull request and generated 2 comments.

Comment thread scripts/sync-doc-constants.rb
Comment thread scripts/check-fixture-execution.rb Outdated
Copilot AI review requested due to automatic review settings August 14, 2026 07:28
@jeremy
jeremy force-pushed the spec-roster-derived branch from 5a82e2d to f5d344d Compare August 14, 2026 07: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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector

Copy link
Copy Markdown

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

Reviewed commit: e055f81a39

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

@jeremy

jeremy commented Aug 16, 2026

Copy link
Copy Markdown
Member Author

Review status: converged on everything raised, but four commits have had no bot review

Stating this plainly rather than letting a green tick imply more than it should.

Copilot is quota-limited. Its last four "reviews" — 5815d3cea, 7553dc630, ab700ebf0, e055f81a3 — are all Copilot was unable to review this pull request because the user who requested the review has reached their quota limit. That is also the one failing check run on this PR; there are zero non-bot failures (45 success).

Codex has not reviewed since ef796f748, across three explicit @codex review requests. It never auto-reviews new commits, so its silence is not a verdict.

So the last four commits are unreviewed by either bot. Since that is exactly when a PR accumulates unexamined work, I reviewed them adversarially myself instead of waiting, and it was not idle: it found three real bugs, all in --write, all of the same shape — --write returns before the per-kind checkers run, so a guard living in a checker protects --check and leaves the writer free to act.

commit what --write did before the fix
ef796f748 both accessor extractions empty → agree at zero → all 11 @service-count spans rewritten to 0, exit 0
7553dc630 .paths: {} is a legitimate empty sum → all 6 @operation-count spans across 3 files rewritten to 0, exit 0
e055f81a3 "version": "" → dates deleted from every @api-version and @bc3-pin span, exit 0

I also probed the block side rather than assuming it was fine: #746's writable roster refuses every vacuous source in --write (runners: {}, a blank file, a missing runner section — exit 2 on all three, with precise messages). That concern was unfounded, and it is worth saying so rather than only reporting the hits.

Threads: 6 of 6 answered inline, including the one decline, which is argued on layering rather than scheduling and filed as #747.

I am not treating unavailable reviewers as approval. If either bot comes back with a finding on the merged commit, it gets a follow-up PR.

@chatgpt-codex-connector

Copy link
Copy Markdown

Review Result

No actionable findings on the four commits called out in the trigger.

The changes consistently move validation to the shared source paths reached by both --check and --write:

No repository changes were necessary, so I did not create a commit or pull request.

Checks

  • ⚠️ ruby scripts/test-doc-constants.rb — could not run because the configured mise Ruby installation is unavailable and mise’s network lookups were blocked.
  • ⚠️ ruby scripts/sync-doc-constants.rb --check — blocked by the same unavailable mise Ruby installation.
  • git diff --check
  • git status --short --branch — working tree remains clean.

View task →

The row records that `gauges` and `myNotifications` are generated but unwired,
which this PR discovered while measuring the per-SDK surfaces. It read as a
standing fact rather than an open defect, and #732 already tracks fixing it.

A documented gap nobody can navigate from is how a gap becomes permanent. Go's
row deliberately gets no such reference: folding `automation` and
`clientVisibility` is a design decision, not a defect awaiting a fix.
Copilot AI review requested due to automatic review settings August 16, 2026 23:37

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Found by an independent review of this branch, and it is the sharpest finding on
the PR: the fix in a2468c0 made `make generate` sync @service-count after the
accessor generators, and left the roster block out of the writer entirely.

The count and the roster derive from ONE source. Leaving one writable and the
other not does not preserve a choice — it splits a single claim across two
mechanisms that update at different times. Reproduced: append an accessor to
both generated files, run --write, and it rewrites all eleven @service-count
spans to `54`, exits 0, and leaves both marked blocks still enumerating 53. A
tracked spec contradicting itself, produced by a green generation run, with only
a later `make check` to notice. That is the failure the `generate` target's own
comment argues against, one level further down.

I had excluded the roster deliberately, arguing that writing it would fix an
order the checker does not assert and so convert a courtesy to readers into an
enforced syntax. The argument was wrong in its weighting, and the remaining half
of it costs nothing: the writer emits the accessors' order, which is the
alphabetical order the roster already uses, and --check still compares SETS — a
hand-reordered roster passes and is normalised on the next write, never
rejected. There is a case pinning exactly that.

The self-test case that asserted the old behaviour is flipped rather than
supplemented: it applied drift to the roster alone and required the writer to
leave it. It now drifts BOTH and requires both back, because restoring the count
alone is precisely the bug. Verified against the un-fixed code by returning the
roster to non-writable, which fires "expected the roster restored".

The three table kinds stay unwritable for the reason they always were: each
carries a column only a person can write.
Copilot AI review requested due to automatic review settings August 16, 2026 23:52
@jeremy

jeremy commented Aug 16, 2026

Copy link
Copy Markdown
Member Author

Independent review, standing in for the unavailable bots

With Copilot quota-limited and Codex silent across four requests, I ran an independent adversarial review of the branch rather than let five commits ship unreviewed. It confirmed the derived facts against the actual artifacts — both accessor extractions at 53, Appendix F's six per-SDK numbers, Go's two folds and singular Timesheet, Kotlin's six handwritten composites against HAND_WRITTEN_SERVICES, both split examples against SERVICE_SPLITS, and that GaugesService(account) in the Python README actually constructs — and found two things.

Accepted, and it is the sharpest finding on this PR — fixed in 5c1c8c027

@service-count was writable and @account-scoped-services was not. Since a2468c08f made make generate sync the counts after the accessor generators, that split the claim across two update mechanisms:

$ # append one accessor to BOTH generated files, then:
$ ruby scripts/sync-doc-constants.rb --write
WRITE_EXIT=0
count spans:    `54` account-scoped
roster entries: 53          <- both marked blocks still enumerate the old set
$ ruby scripts/sync-doc-constants.rb --check
1

A tracked spec contradicting itself, produced by a green generation run — the failure the generate target's own comment argues against, one level further down.

I had excluded the roster deliberately, arguing that writing it would fix an order the checker does not assert. That weighting was wrong, and the surviving half costs nothing: the writer emits the accessors' order, which is the alphabetical order already there, and --check still compares sets — a reordered roster passes and is normalised, never rejected. There is a case pinning exactly that.

The self-test that asserted the old behaviour is flipped, not supplemented: it drifted the roster alone and required the writer to leave it. It now drifts both and requires both back, because restoring the count alone is the bug.

Post-fix, the same probe leaves counts and both rosters at 54 with widgets present, and the subsequent --check exits 0.

Declined, because the remedy would make the claim vacuous rather than checked

The second finding: Appendix F's Ruby and TypeScript rows say "53 — full canonical set", are derivable today, so <!-- @service-count --> on those two "would cost nothing and would fail loudly the day either SDK falls behind".

It would do the opposite. @service-count asserts one thing: that the backticked integer equals the count derived from the Kotlin and Swift accessor files. The gate contains zero references to ruby/lib or typescript/src. So if Ruby fell to 52 while canonical stayed 53, the writer would keep that row reading 53 and the checker would compare 53 == 53 and pass — a row that looks gated while asserting nothing whatsoever about Ruby, and a writer actively maintaining the wrong number. Today's honest hand-verified-and-dated row would have been replaced by a false green.

The instrument that genuinely closes it is comparing all five SDKs' generated inventories, which is #747 — filed with the verified numbers showing all five already agree on the 53.

(The "Verified 2026-08-13" stamp stays unmarked for the same reason: it is an as-of fact bound to a fixed observation, which is the class AGENTS.md says never gets a marker.)

@jeremy

jeremy commented Aug 16, 2026

Copy link
Copy Markdown
Member Author

@codex review

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@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: 5c1c8c027b

ℹ️ 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/sync-doc-constants.rb
Comment thread scripts/sync-doc-constants.rb
Comment thread python/README.md Outdated
Three findings from Codex on the commit that made the roster writable. All
correct.

**The contract said the opposite in three places.** Making a block writable is a
change to what contributors are told to do by hand, and I changed the behaviour
without changing any of the documents that describe it: this script's --write
header still said exactly one block kind was writable, spec/doc-constants.json
still called the roster "deliberately NOT writable" and argued for it, and
AGENTS.md still told contributors Zero-Skip was the sole generated block. A
maintainer following those would have hand-edited a roster that `make generate`
now overwrites. The doc-constants.json entry keeps the old argument and says why
it was wrong, since the reasoning is the useful part.

**An indented roster would have been silently unindented.** scan_file accepts a
block marker up to three spaces in, and this checker strips before splitting, so
a roster nested in a list passed --check — while the renderer emits at column
zero, so the next `make generate` would lift it out of its list and report
success. Refused rather than preserved: teaching the renderer to reproduce an
indentation nothing depends on would make --write maintain a shape no longer the
author's to choose. This block is generated output now, and saying so is the
smaller change.

**The Python workaround was wrong for async users.** The README told anyone
without an accessor to construct `GaugesService(account)`. `AsyncClient.for_account`
returns an `AsyncAccountClient`, and the sync class calls that client's methods
without awaiting them, so the documented escape hatch hands back coroutines
instead of responses for the SDK's advertised async surface. Both classes exist
and are exported; the README now pairs them and says why the pairing matters.

The indentation case is verified against the un-fixed gate; the other two are
documentation, where the check is that they now match the code.
Copilot AI review requested due to automatic review settings August 17, 2026 00:10
@jeremy

jeremy commented Aug 17, 2026

Copy link
Copy Markdown
Member Author

@codex review

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: e27db46ae5

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

@jeremy
jeremy merged commit 49f18e4 into main Aug 17, 2026
48 of 49 checks passed
@jeremy
jeremy deleted the spec-roster-derived branch August 17, 2026 01:36
jeremy added a commit that referenced this pull request Aug 17, 2026
The service split is one mapping hand-transcribed into five generator
configs, and nothing compared them. The per-SDK `check-*-service-drift`
scripts compare operationIds, never service names, so a service added to
three tables and forgotten in the other two produced five different SDK
surfaces and a fully green `make`.

#745 made SPEC section 5's roster derived, but from the generated Kotlin
and Swift accessors only, because those two encode account-scoping
directly. Codex named the residue: those are two independent
transcriptions, not two renderings of one root, so a service omitted from
BOTH leaves them agreeing and certifies the old roster and the old count.
Agreement between two transcriptions is not confirmation against a root.

`scripts/check-service-inventory-parity` reads what the generators
ALREADY EMITTED and asserts the emissions agree — five generated service
directories, the two accessor files, and Go's hand-written accessors.
Reading output rather than reimplementing the mapping is what keeps it
from being the sixth hand-copy #745 rejected. Committed generated files
only, so it needs no toolchain and lands in the spec-gates job beside
kt-check-drift. All eight agree on 53 today.

It also checks each accessor file against its own services directory,
which is the other half: section 5's roster reads accessors, so a
generator that emitted a service file and no accessor would leave the
roster short with every directory agreeing the service exists.

Normalization is one rule per source, expressed as fields on a SOURCES
entry rather than a per-SDK branch: files to drop, one suffix to strip,
one identifier casing. Go's three divergences are data too, and are
checked for STALENESS rather than merely applied — a fold that closes or
a spelling that changes fails the gate instead of quietly ceasing to
mean anything.

The self-test drives the real checker over synthetic trees built by
inverting that normalization, so the builder round-tripping 53 names is
itself a positive control. Ten negative cases, and the guard-to-case
mapping in its header is measured, not reasoned: each guard was neutered
in a copy and the red set recorded. Neutering the parity comparison
turns 1, 2, 3 and 9 red; the duplicate check 4; the floor 5; the
existence check 6; the two staleness checks 7 and 8; the both-spellings
check 10. Neutering either carve-out APPLICATION turns both positive
controls red, which is the only way a rule that prevents false positives
can be pinned.

SPEC.md's derivation rule said no gate compares the tables. It does now,
and the sentence says so; the `@service-count` marker beside it is
untouched. Appendix F's preamble and sync-doc-constants.rb's note that
the residue was open are updated for the same reason.

Closes #747
jeremy added a commit that referenced this pull request Aug 17, 2026
The service split is one mapping hand-transcribed into five generator
configs, and nothing compared them. The per-SDK `check-*-service-drift`
scripts compare operationIds, never service names, so a service added to
three tables and forgotten in the other two produced five different SDK
surfaces and a fully green `make`.

#745 made SPEC section 5's roster derived, but from the generated Kotlin
and Swift accessors only, because those two encode account-scoping
directly. Codex named the residue: those are two independent
transcriptions, not two renderings of one root, so a service omitted from
BOTH leaves them agreeing and certifies the old roster and the old count.
Agreement between two transcriptions is not confirmation against a root.

`scripts/check-service-inventory-parity` reads what the generators
ALREADY EMITTED and asserts the emissions agree — five generated service
directories, the two accessor files, and Go's hand-written accessors.
Reading output rather than reimplementing the mapping is what keeps it
from being the sixth hand-copy #745 rejected. Committed generated files
only, so it needs no toolchain and lands in the spec-gates job beside
kt-check-drift. All eight agree on 53 today.

It also checks each accessor file against its own services directory,
which is the other half: section 5's roster reads accessors, so a
generator that emitted a service file and no accessor would leave the
roster short with every directory agreeing the service exists.

Normalization is one rule per source, expressed as fields on a SOURCES
entry rather than a per-SDK branch: files to drop, one suffix to strip,
one identifier casing. Go's three divergences are data too, and are
checked for STALENESS rather than merely applied — a fold that closes or
a spelling that changes fails the gate instead of quietly ceasing to
mean anything.

The self-test drives the real checker over synthetic trees built by
inverting that normalization, so the builder round-tripping 53 names is
itself a positive control. Ten negative cases, and the guard-to-case
mapping in its header is measured, not reasoned: each guard was neutered
in a copy and the red set recorded. Neutering the parity comparison
turns 1, 2, 3 and 9 red; the duplicate check 4; the floor 5; the
existence check 6; the two staleness checks 7 and 8; the both-spellings
check 10. Neutering either carve-out APPLICATION turns both positive
controls red, which is the only way a rule that prevents false positives
can be pinned.

SPEC.md's derivation rule said no gate compares the tables. It does now,
and the sentence says so; the `@service-count` marker beside it is
untouched. Appendix F's preamble and sync-doc-constants.rb's note that
the residue was open are updated for the same reason.

Closes #747
jeremy added a commit that referenced this pull request Aug 17, 2026
#752)

* Compare what the six generators emitted, not what two of them agree on

The service split is one mapping hand-transcribed into five generator
configs, and nothing compared them. The per-SDK `check-*-service-drift`
scripts compare operationIds, never service names, so a service added to
three tables and forgotten in the other two produced five different SDK
surfaces and a fully green `make`.

#745 made SPEC section 5's roster derived, but from the generated Kotlin
and Swift accessors only, because those two encode account-scoping
directly. Codex named the residue: those are two independent
transcriptions, not two renderings of one root, so a service omitted from
BOTH leaves them agreeing and certifies the old roster and the old count.
Agreement between two transcriptions is not confirmation against a root.

`scripts/check-service-inventory-parity` reads what the generators
ALREADY EMITTED and asserts the emissions agree — five generated service
directories, the two accessor files, and Go's hand-written accessors.
Reading output rather than reimplementing the mapping is what keeps it
from being the sixth hand-copy #745 rejected. Committed generated files
only, so it needs no toolchain and lands in the spec-gates job beside
kt-check-drift. All eight agree on 53 today.

It also checks each accessor file against its own services directory,
which is the other half: section 5's roster reads accessors, so a
generator that emitted a service file and no accessor would leave the
roster short with every directory agreeing the service exists.

Normalization is one rule per source, expressed as fields on a SOURCES
entry rather than a per-SDK branch: files to drop, one suffix to strip,
one identifier casing. Go's three divergences are data too, and are
checked for STALENESS rather than merely applied — a fold that closes or
a spelling that changes fails the gate instead of quietly ceasing to
mean anything.

The self-test drives the real checker over synthetic trees built by
inverting that normalization, so the builder round-tripping 53 names is
itself a positive control. Ten negative cases, and the guard-to-case
mapping in its header is measured, not reasoned: each guard was neutered
in a copy and the red set recorded. Neutering the parity comparison
turns 1, 2, 3 and 9 red; the duplicate check 4; the floor 5; the
existence check 6; the two staleness checks 7 and 8; the both-spellings
check 10. Neutering either carve-out APPLICATION turns both positive
controls red, which is the only way a rule that prevents false positives
can be pinned.

SPEC.md's derivation rule said no gate compares the tables. It does now,
and the sentence says so; the `@service-count` marker beside it is
untouched. Appendix F's preamble and sync-doc-constants.rb's note that
the residue was open are updated for the same reason.

Closes #747

* Read Python from its barrel, and state the motivation accurately

Two review findings, both correct.

CODEX: a stale Python module counted as still emitted. Of the five
generators, four sweep outputs the current mapping no longer produces
(generate-services.ts:1736, generate-services.rb:344, Main.kt:65, and
Swift regenerates whole); `python/scripts/generate_services.py` does
not. So dropping a Python service leaves its module on disk, and a
directory listing counts the corpse — the exact divergence this gate
exists to catch, passing it.

It reaches CI, which I had assumed it did not. `check-python-service-
drift.sh` regenerates to a tmpdir and `diff -rq`s, so it reports
`Only in <committed>: fanfares.py` and fails — verified. But the Python
CI job does not run that script. It regenerates IN PLACE and tests
`git status --porcelain`, which is blind to a stale file that is already
committed: nothing rewrites it, nothing deletes it, so there is no diff.
Verified in a throwaway repo: 0 porcelain lines, corpse still present.

Python is now read from its generated `__init__.py`, which is rewritten
whole every run and so cannot name a service that is no longer emitted.
It also does not import `_base.py`/`_async_base.py`, so the hand-written
infrastructure drops out of the reading instead of being filtered from
it. The generator defect itself is #757; this only stops one gate
believing a corpse.

Two cases, both red-proven against the pre-fix checker, which fails 11
with "`fanfares` is emitted by python but NOT by ..." and passes 12:
  11. stale module on disk, absent from the barrel  -> must PASS
  12. barrel short a service the directory still has -> must FAIL
Case 11 is pass-shaped on purpose: nothing else can pin a fix whose
whole effect is to stop a false failure. Reverting the source to a
directory listing turns 11 and 12 red and nothing else, measured.

COPILOT: the motivation prose was false. It said the per-SDK drift
scripts compare operationIds and never service names. TypeScript, Ruby,
Python and Swift regenerate-and-diff whole trees, so they do see service
filenames; only Kotlin's fast check is operationId-only. The real gap is
the AXIS, not the granularity: each validates one SDK against its own
generator and can see no other SDK. Reworded in the script header, the
Makefile and the workflow, and SPEC's derivation rule now names the
barrel rather than claiming five directories.

A gate whose stated motivation is false is one nobody can size later,
which is the whole reason this is worth a commit rather than a
footnote.

Refs #757

* Mirror Python's rename exactly, instead of stripping any _service suffix

Codex, correctly. Python's entry borrowed Ruby's `strip_suffix: "_service"`,
and the comment said "Ruby suffixes every file, so one rule covers both".
Sharing the rule is exactly the bug: it is right for Ruby and wrong for
Python.

`generate_services.py`'s `service_filename` renames one module and tests
for it by name:

    snake = to_snake_case(name)
    if snake == "webhooks": return "webhooks_service.py"
    return f"{snake}.py"

Every other service keeps its snake name verbatim — including one whose
canonical name legitimately ends in `_service`. A blind suffix strip took
such a name apart, so Python reported a service the other seven did not
have and the gate failed on a mapping where every generator agreed.

That direction matters. This was a FALSE POSITIVE, not a miss: a red
build blocking a correct change, and an invitation to "fix" a mapping
that was never wrong. Ruby keeps `strip_suffix:` because suffixing every
file genuinely is its rule; Python gets `rename:`, an exact-match map
applied before any suffix logic. Two instruments, because the two
generators do two different things.

Case 13 adds a service whose canonical name is `notification_service`
across all eight renderings and requires a PASS. The roster still holds
`webhooks`, so one case pins both halves: the module that IS renamed and
one that merely looks like it.

Red-proven against the pre-fix checker, which fails it with both sides of
the split it invents:

    `notification` is emitted by python but NOT by typescript, ruby, ...
    `notification_service` is emitted by typescript, ruby, ... but NOT by python

Kill matrix re-measured. Respelling the rename as `strip_suffix` turns 13
red and nothing else, which is what makes it a pin rather than a smoke
alarm. Deleting the rename outright, or never applying it, turns both
positive controls plus 11 and 13 red — 11 and 13 are pass-shaped and go
red on any global breakage, and the header now says so rather than
implying they are specific.

* Say what disagreed and where to look; stop naming a cause

Four Copilot findings, all correct, and one class: the gate was precise
about what it MEASURES and loose about what it CLAIMS.

Three of the four arrived as SUPPRESSED comments inside the review body,
which never become threads and so never move the unresolved count. Two
sweeps read clean past them. The fourth appeared only in the newest
review, since the block regenerates each time.

The per-failure message asserted "the split tables disagree about whether
this service exists". Self-test case 3 is a standing counterexample: it
constructs a Kotlin service file with no accessor beside it, where every
split table agrees and a generator disagrees with ITSELF. And go-accessors
is hand-written, so a Go-only absence is missing wiring, not a mapping
difference. It now reports which renderings lack the service and where to
look, and asserts nothing about why.

The failure footer had the same defect at summary level: it printed for
EVERY failure class — missing input, duplicate extraction, collapsed
extraction, stale carve-out — and told all of them a tag mapping had
drifted. Five of this suite's own cases would have sent a maintainer to
the wrong file. Now neutral.

sync-doc-constants.rb said this gate compares "the GENERATED service
inventories of all six SDKs". Go has no generated per-service files, so
that was wrong in the one place the argument turns on it: "not a sixth
hand-copy, because it reads generator OUTPUT" is weaker if a rendering is
hand-written. Seven of eight are generated; Go is the exception, which is
exactly why it carries three carve-outs and now the type check below. The
same over-claim is corrected in the script header and SPEC. Its earlier
paragraph also still said "no gate compares them to each other", which
this PR falsifies; it now points at the gate without weakening the
argument for not writing a sixth copy.

Separately, an open thread: the Go rendering recorded only accessor NAMES,
so `Gauges() *ReportsService` contributed `gauges` and the gate reported
parity while Go handed callers the wrong service. Both types exist, so the
compiler is silent, and Go's accessors ARE its inventory here — nothing
else would catch it. Now checked as a pair. All 51 satisfy name == type
stem today. A backreference in the pattern would have made a mismatch fail
to EXTRACT, reported as "go-accessors is missing gauges" — true, and a bad
description of a line spelling `gauges` right there.

Three new cases, all red-proven against the pre-fix checker:

  14. Go accessor returning another service type -> pre-fix: PASSED,
      "53 services agree across 8 renderings", the miss exactly
  15. stale carve-out failure must not say "tag mapping" -> pre-fix:
      "failed correctly but MISDIAGNOSED"
   3. now also asserts the parity message does NOT blame the split tables

15 and 3 use a new `expect_fail_without`: the only way to hold a
diagnostic honest is to assert what it must NOT say. Kill matrix
re-measured — the type check pins 14 alone, the footer pins 15 alone, the
parity message pins 3 alone.
jeremy added a commit that referenced this pull request Aug 18, 2026
* Check that an operation lands on the same service in every SDK

`SERVICE_SPLITS` maps tag -> {service -> [operationIds]} and is transcribed by
hand into five generator configs. If ONE of them routes an operation to a
different but ALREADY-EXISTING service, nothing in `make` notices: the
service-name union is unchanged so check-service-inventory-parity sees nothing,
the global operationId set is unchanged so every per-SDK check-*-service-drift
sees nothing, and the SDK compiles with its tests exercising the method wherever
it now lives. Five SDKs expose `account.cardTables.get`, one exposes
`account.cards.get`, and both are "present".

A new sibling gate rather than a second question bolted onto
check-service-inventory-parity, whose own header forbids that shape and whose
success line states exactly one quantity. None of its eight SOURCES entries is
reusable here: different inputs (method bodies, not filenames), different key
space (250, not 53), different source count.

Keyed on `(method, path)`, not operationId, because Ruby carries `operation:` on
reads only -- 125 of 250. And on a multiset-shaped key rather than the
`{service -> path set}` #756 words it as, because paths collide verb-agnostically
inside one service (`boosts` has GET and DELETE on /boosts/{}).

Named from the CLASS declaration rather than the filename, which is what keeps
the gate free of a per-SDK naming table: all five generators name the class
`#{service}Service`, so one universal rule serves where reading filenames would
mean transcribing the sibling gate's drop/strip/rename/spelling table a second
time. Name-aware rather than a grouping comparison, because a wholesale swap of
two classes' contents leaves both the blocks and the name set unchanged.

openapi.json appears for two things only: the roster each SDK must be shown to
emit, and the operationId used to label a failure. It is never consulted for
which service an operation belongs on -- that is #756's "expensive, total"
option 2 and the sixth-hand-copy shape #745 rejected. The comparison is SDK
against SDK. Because that roster is exact, there is no MIN_ floor on the per-SDK
reads: a request helper the patterns do not recognize is reported by name rather
than as a plausible-looking count.

Go is excluded, and the reason belongs on the page or it reads as a hole.
`grep -rn "SERVICE_SPLITS\|TAG_TO_SERVICE" go/` returns nothing -- Go's service
layer is hand-authored wrappers over one flat oapi-codegen client with no
service grouping, so it has no table that could disagree. It is also assigned
differently on purpose: TrashRecording is re-exposed on ten services, and
SetClientVisibility, ListLineupMarkers and the progress reports sit elsewhere.
27 operationIds would need exempting, an order of magnitude more than
GO_CARVE_OUTS, to encode intent rather than drift.

The self-test drives synthetic five-SDK trees through the real checker. Its
roster is scraped from openapi.json and the real Swift services rather than
written down, and it keys on operationId -- which the checker never extracts
from any SDK -- so the builder and the thing it drives share no reader. The
builder rotates through every request helper the checker declares a pattern
for, so a dead rule fails the positive control.

The guard-to-case mapping in its header is measured, not reasoned about. One
mutation survives and is recorded as surviving: changing the service-suffix
strip from `sub` to `gsub` turns no case red, because all five generators spell
the class identically and the strip is cosmetic.

Its own CI step, not just `make check` membership: no CI job runs the full
`make check`.

Closes #756

* Say what the key actually is, and drop a branch that cannot be reached

Two corrections found while measuring the self-test's kill matrix.

"A multiset-shaped key" was the wrong description and would have sent the next
reader looking for counting the gate does not do. The key is `(method, path)`,
asserted unique against openapi.json, so the assignment is a plain map. What the
paragraph is actually about is why the METHOD has to be in the key: a
`{service -> path set}` merges `boosts`'s GET and DELETE on /boosts/{}, and a
reassignment of one of that pair would leave the set unchanged.

`label`'s not-found branch was unreachable — it is only ever called with a key
that came out of `expected` — so it is a `fetch` now. An unreachable fallback in
a diagnostic is worse than none: it would render a plausible-looking message for
a state that cannot arise, so nobody would learn anything from seeing it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

github-actions Pull requests that update GitHub Actions kotlin python Pull requests that update the Python SDK ruby Pull requests that update the Ruby SDK spec Changes to the Smithy spec or OpenAPI swift

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SPEC §5's account-scoped service roster is seven short: 53, not 46

2 participants