feat(baton): per-type → endpoint keying for generated type files (closes #313)#320
Merged
Merged
Conversation
#313) Key a contract's shape by call-site binding (api.get<T>('/route')) in addition to nearest endpoint anchor, so a generated types.gen.ts whose interfaces carry no per-type anchor no longer collapses onto one coarse contract. C2/C3 now fire for the correct contract (proven on a new anchorless fixture). Also resolves the union an interface references to the same contract (for C3), skips /api/ inside module specifiers (import '@/api/...' was a bogus `types.gen` contract), and makes normalize_endpoint strip query strings + ${id} template params. Sentinel dogfood (read-only): consumers now spread across services.health / audit.records / incidents / incidents.reopen instead of one `services` blob; run stays 0 blocking, correctly (the file was remediated to match the backend). The symmetric producer-side gap (huma route-registration separated from output structs) is filed as #319. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Closes #313. The coherence engine keyed a contract's shape (fields, enum variants) by the nearest
/api/...anchor at or above a declaration. On a generated types file (web/src/api/types.gen.ts) holding all API types in one file with sparse endpoint comments, every interface collapsed onto a few coarseContractIds (on Sentinel: all ontoservices), so the #304 field/enum mismatch (C2/C3) could never isolate on the right contract — the dogfood's "0 blocking by design".This adds a higher-priority keying source — call-site binding — keying a type to its route from the usage site, where the association actually lives:
It is the issue's second proposed option, and more general than parsing OpenAPI (Sentinel does not even commit an OpenAPI file — it is gated).
Changes
codescan::collect_bindings) — minesIDENT<Type>('/route')repo-wide intoType → ContractId. Conservative (R6): ambiguous (two routes) → dropped; only a quoted first arg starting with/counts, souseState<T>('loading'),useQuery<T>({…}), JSX<Table<Row>never bind.codescan::extract_file) —binding(type) ?? nearest_anchor(line); a bound declaration keys even when its file has no anchor.type X = 'A'|'B'its fields reference to the same contract (needed for C3).scan::scan_endpoints) —/api/glued to an alias/relative path (import … from '@/api/types.gen') is skipped; it was producing a bogustypes.gencontract.normalize_endpoint— strips query strings (?…) and template params (${id}).Verification
cargo test -p straymark-baton✓ — 47 tests (+6: 3 unit, 3 integration);clippyclean.tests/fixtures/generated-types-project/: an anchorlesstypes.gen.tswhose two interfaces bind to two routes via call sites → C2 + C3 fire onservices.health, the matchingservicessibling stays silent. Proves the "Done when" (per-contract isolation).git statusunchanged): consumers now spread acrossservices.health/audit.records/incidents/incidents.reopeninstead of oneservicesblob; bogustypes.gencontract gone. Run stays 0 blocking — correctly: Sentinel'stypes.gen.tswas remediated to match the backend, so no drift, no false positives (R6 holds). The Cross-spec decision propagation: a post-MVP backend decision silently diverged a later consumer spec's contract — the system view should link them #304 C4 finding still fires.Full write-up:
experiment-baton/AILOG-2026-06-26-001.Scope boundary
Consumer-side (generated type) keying was #313's chartered scope and is done. The dogfood surfaced the symmetric producer-side gap —
humaregisters routes in one block while response structs sit far below, so the Go producer mis-keys (services.healthshowsproducer=None). It does not change the Sentinel result (remediated → 0 blocking either way) and the capability is already proven on the fixture. Filed as #319.🤖 Generated with Claude Code