Uh oh!
There was an error while loading. Please reload this page.
feat(scripts): pin documented interface blocks against the shipped type, both ways - #7306
Merged
Merged
Conversation
…type, both ways
Tier 2 of objectui#5043, the half the card scoped as "bidirectional pins for
documented `interface` blocks". A README that writes out a type it also exports
is making a claim the tier-1 name check cannot reach: a standalone
`interface GanttTask { ... }` in a fenced block is a LOCAL declaration, so it
compiles green no matter what it says.
`scripts/check-readme-exports.mjs` now compares, for every fenced block that
declares `interface X { ... }` or `type X = { ... }` where `X` is an export of
that README's own package, the documented property names against the shipped
declaration's, in both directions:
fabricated-key a documented key the shipped type does not have, judged
against the FULL property set so an inherited key is not
reported as invented;
stale-omission a shipped key the block never mentions, judged against the
interface's OWN declared members so an excerpt of a type
with a large base is not stale for leaving the base alone.
One direction pins only half, and a RENAME is visible only as the pair -- the
new spelling as `fabricated-key`, the old one as `stale-omission` on the same
declaration. The header states the bound the card recorded: the typed-example
half cannot carry that class, because a property-level type error
short-circuits the missing-property detail.
Two ways to declare a deliberate excerpt, with different meanings: a
`PARTIAL_MARKER` HTML comment in the README (grammar deliberately in
`check-doc-snippet-types.mjs`'s `FRAGMENT_MARKER` family), and a
`PARTIAL_EXCERPTS` ledger here for drift owed to a content card. Both are
shrink-only and both suppress omissions ONLY -- neither can hide a fabricated
key. An unbuilt package is a FAILURE on this side too, never a serene
`local-declaration`, and three new floors let the pin walk collapse and name
itself independently of the import walk.
No new compiler: `check-doc-snippet-types.mjs` already collects every
`packages/<name>/README.md` and compiles the covered ones (39 in its surface, 8
compiled, 31 on its shrink-only ledger). Measured and written into the header.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b`type-check:scripts` reads the JSDoc as the declaration for this module, so the new option existed at runtime and not in the contract — every test passing it was a TS2353 while the suite itself was green. The producer's declaration was the stale half; fixed there rather than loosened at the five call sites. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
The ledger is a debt list, and a debt with no card is one nobody picks up. Each entry now names objectui#7302 (the README fixes) beside objectui#6214 (the gate that recorded it), and the kanban pair notes that objectui#6155 can move its omission set — that set is read from what the package exports, not copied into the entry. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
This was referenced Sep 2, 2026
…compared `Test (shard 2/4)` went red on the interface pin's first CI run. Not a flake: the test shards run `pnpm install` then `pnpm test` and NEVER build, so on CI every documented type comes back `unjudgeable-type` — and the three real `PARTIAL_EXCERPTS` entries then suppressed no omission, so the shrink-only sweep reported all three as `stale-excerpt-entry`. That is the wrong verdict, and wrong in the direction that costs most: it tells a reader to DELETE a ledger entry that is still true. "This entry suppressed nothing" has two causes that look identical from outside — the README caught up (stale), or the declaration could not be judged at all (still true) — and they must not share a verdict. So a declaration that resolves to `unjudgeable-type` now SUSPENDS the rule for its ledger entry and its marker rather than satisfying it. Both are counted as `not judged` in the census, and the run still FAILS — under `unjudgeable-type`, which names the real problem. The counter increments only where an excerpt was actually declared, so the census means "claims this run could not check" and not "declarations it could not judge", which `typesUnjudgeable` already says. Tests: four fixture legs, on the fixture tree so they hold in both build states — the ledger case, the marker case, the no-excerpt case pinning the counter's meaning, and a MUST-FAIL CONTROL asserting the same entry on a built tree is still reported stale. Without that control all three would also pass if the suspension had swallowed the rule outright, which is the opposite defect and the more expensive one: a ledger that can never shrink again. Tier 1's `repo state` test filtered `!== 'unjudgeable'` to mean "could not judge". That class has two verdicts now, so the filter names both — the assertion that the unbuilt tree must still FAIL is unchanged — and its unbuilt branch now asserts zero stale-excerpt and zero stale-marker rows by name, so this regression cannot come back silently. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
…dme-interface-pins-wt
yinlianghui
marked this pull request as ready for review
September 2, 2026 06:12
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Sep 2, 2026
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 freeto 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.
Fixes#6214
Tier 2 of #5043, and not the shape the card assumed. The card asked for two things — compile the README code blocks, and add bidirectional pins for documented
interfaceblocks. The two measurements the unlock comment left owed were taken first, and the first of them says the compile half already has a gate. So this PR is the pin, and the compile half is reported rather than rebuilt.Both measurements are below with their numbers, because both change what got built.
Measurement (a) — the compile half is already
check:doc-snippets' jobscripts/check-doc-snippet-types.mjsdoes not scancontent/docsonly. Its collector walks that tree and everypackages/PKG/README.md(thelistDocumentswalk, and the scan surface is stated in its header in the same breath as the coverage rule). Coverage there is declared, never assumed: a document is compiled unless it is named inUNGATED_DOCSwith a reason, and that ledger is shrink-only — an entry naming a file that does not exist, or that holds no ts/tsx block, fails as stale.Measured on this branch's base:
cli,components,create-plugin,plugin-grid,plugin-timeline,runner,test-support,vscode-extension)UNGATED_DOCSdebt ledger, each with a reason.mdxpages)#5174's batches are burning that ledger down — its header records batch 1 clearing 90 diagnostics, batch 2 eighty-nine, batch 3 eighty-one, batch 4 one hundred and four, and batch 5 is live on
content/docsright now. So a second README compiler here would duplicate a working gate and split its ledger in two, and every README it brought under itself would then have two places to declare a fragment. Not built, deliberately; the reasoning is in this gate's header under "Compiling the blocks" so the next reader does not re-derive it.One honest gap that measurement exposes, recorded and not fixed here: the doc-snippets collector reads
packages/NAME/README.mdat the package root only, so the four nested READMEs this gate walks (packages/types/src/zod/README.mdand friends) are outside its surface entirely — neither covered nor on its ledger. That is #5174's own class of finding, one directory deeper, and it belongs to that gate.Measurement (b) — the tier-2 baseline, re-taken on the merged ref
The card was blocked on this: "Running all 15 compilable plugin-gantt blocks under
strict, after #5012 landed, still exitsrc=2: twoTS7006."#5057 has since reachedcompletedstate, by way of merged PR #5900 and the README moved again in231d1b93c, so the figure was re-taken with the existing instrument —check-doc-snippet-types.mjs's ownanalyze+compileSnippets, withplugin-ganttlifted out ofUNGATED_DOCS— rather than a new compiler:It does not compile clean, and the shape of the red moved. The filed
rc=2 / two TS7006is now oneTS7006plus twoTS7031(the same unannotated-callback class, redistributed), and the bulk is a different family: sevenTS2304from blocks that are deliberately-not-valid property sketches ({ type, tasks: … }, baredependencies: [ … ]), which is precisely the caseFRAGMENT_MARKERexists for over there. Two blocks do not even parse. One diagnostic is real drift a compile tier would catch —TS2322: Type '"gantt"' is not assignable to type '"object-gantt"'.Under the card's clause — "decide explicitly whether to list it as excluded or add a class, don't let it fall out silently" — this is listed, not inherited:
packages/plugin-gantt/README.mdstays onUNGATED_DOCS(untouched by this PR, it is #5174's file) and the number above is on the record. No shrink-only exclusion entry is added here for it, because this PR adds no compile pass for it to be excluded from.What this PR builds: the bidirectional interface pin
A README that writes out a type it also exports makes a claim tier 1's name check cannot reach. The card records why, and the pin is built on it:
So for every fenced block that declares
interface X { … }ortype X = { … }whereXis an export of that README's own package, the documented property names are compared against the shipped declaration's, in both directions:fabricated-keystale-omissionThe asymmetry is the design, not a shortcut. Documenting a key that arrives through
extendsis correct, so the doc side is judged leniently; an excerpt of a type with a large base is not stale for leaving the base's keys to the base's own page, so the shipped side is judged narrowly. Measured on the real tree: judging the shipped side against all properties instead would report 36 omissions onDetailViewSchemaalone, most of them inheritedBaseSchemamembers.One direction pins only half, which is why both are here — and a rename is visible only as the pair, the new spelling as
fabricated-keyand the old one asstale-omissionon the same declaration and the same line. No single direction reports it.The bound, stated in the script header because it is narrow
The card's other measurement is written into the header verbatim in substance: the typed-example half cannot carry the key-rename class, because a property-level type error short-circuits the missing-property detail —
const task: GanttTask = { name: …, start: '2024-01-01' }reports twoTS2322and never thattitleis gone. This pin is what covers renames oninterfaceblocks, and the header says so and promises nothing more.Where it deliberately stops (first cut, all four in the header)
packages/types/README.md'sDataSourceis entirely method signatures, so it resolves and is compared over zero keys — and the census says1 compared over ZERO documented keysrather than letting it read as a verified declaration.Xis resolved against the README's own package only. Widening to any workspace package pulls inpackages/plugin-detail/README.md'sDetailViewSchema(owned by@object-ui/types) at the 36 omissions above. That widening needs its own inheritance policy and its own card.Declaring a deliberate excerpt — two homes, two different claims
Mirroring the pair
check-doc-snippet-types.mjsalready runs (UNGATED_DOCS+FRAGMENT_MARKER), and for the same reason: they say different things.PARTIAL_MARKER— an HTML comment in the README, above the fence, whose body readsreadme-exports: partial GanttTask — reason. (Spelled with a placeholder here on purpose: an HTML comment written literally into a GitHub body does not survive to the reader. The exact literal is published asPARTIAL_MARKER_EXAMPLEin the script, a test asserts the regex accepts it, and the gate prints it in its own failure message — so the one place a reader meets it is the one place it is guaranteed correct.) It says this excerpt is deliberate. The grammar is deliberately the same family asFRAGMENT_MARKER(marker word, an em dash / double hyphen / colon, then a reason of at least 12 characters), so a reader who knows one knows the other. The verb differs because the claim does —doc-snippet: fragmentsays a block cannot compile, and that must not double as permission to omit a key. Only the HTML spelling exists here: these areREADME.mdfiles, not MDX.It names the interface, which
FRAGMENT_MARKERhas no need to do:packages/plugin-kanban/README.mddeclares two types in one block, and a marker that silenced a whole block would silence the neighbour nobody looked at. Pinned as a test.PARTIAL_EXCERPTS— a ledger in the script. It says this is drift, owed to a content card.Both suppress
stale-omissiononly. Neither can hide afabricated-key— an excerpt may leave a key out, it may not invent one, and there is no reading of "partial" under which a ledger should be able to hide that. Both are shrink-only and enforced as such: a marker or entry that suppressed nothing fails, so the lists cannot quietly stop shrinking.An unbuilt package is a FAILURE on this side too
Tier 1's rule, kept, and it needed its own branch: with no export surface on disk every documented type resolves to nothing and would read as a serene
local-declaration, so the pin would report green over blocks it never judged. Scoped exactly as the import side is — it fails only where the missing surface would have changed a verdict, i.e. where a block declares a type. Verdictunjudgeable-type, in the census, pinned by a test.Non-vacuity
Three new
FLOORScounters —typeDeclarations,typesResolved,keysCompared— so the pin walk can collapse and name itself instead of hiding behind a healthy import walk. Pinned by a three-leg independence test on its own fixture tree (a control leg proving nothing breaches while both walks are healthy, an exact-equality leg on the three pin counters alone, and a leg pinning that the package-side counters are unchanged) — the same shape, and the same exactness argument, as tier 1's.Its own fixture tree, deliberately: tier 1's independence test asserts
exportSymbolsexactly (toBe(4)), and adding exports toalphato give this half something to compare would have turned that assertion into a number nobody chose.The census, before and after
Before, at
eb33a8d4c(this branch's base):After, at
15a06cff3(the head of this branch, after the merge ofmain) — the same line with the pin's half appended:Green at rest, with the counts that say what was skipped travelling beside the ones that say what was judged.
The exclusion ledger this opens with — three entries, two READMEs
Well under the ten-README ceiling the order set, so this ships rather than escalating. Every one is the
stale-omissiondirection; zero fabricated keys anywhere in the tree.packages/plugin-gantt/README.md::GanttTaskfields,hasOwnDatesObjectGanttitself. It wants the in-README marker, not this ledger.packages/plugin-kanban/README.md::KanbanColumncollapsedpackages/plugin-kanban/README.md::KanbanCardcardSubtitle,cardFieldCells,coverImageNo README content is edited by this PR, including the marker
GanttTaskdeserves: the order scoped this card to the gate, so all three are recorded here with the card number and filed as a content follow-up, #7302 (unassigned,finding). The kanban entry notes that #6155 — four disagreeing declarations of that pair — can move its omission set, because that set is derived from what the package exports, not copied into the entry.scantakes anexcerptsoption so the ledger can be switched off, and a test asserts that with it off the repo reds with exactly these three and that every red is astale-omission— so the ledger is provably not covering a fabricated key or anything else. That test is written to hold built and unbuilt, like tier 1'srepo stateblock: on an unbuilt tree the same declarations areunjudgeable-type, which is the failure the rule requires and not a skip.Ablation — seven legs, no tracked file ever mutated
Every leg runs against a scratch copy handed to the gate through tier 1's
--readme REAL=SCRATCHoverride. Each plant is proved on disk by an anchored count before and after, never by an editor's exit code, and the runner carries atrap … EXIT INT TERMthat deletes the scratch files and re-verifies the tracked README's blob.The plant proof has its own negative control, run first: an anchor that is deliberately never written must be rejected. It was (
PLANT NOT ON DISK … its reading is VOID, exit 9) — so the passes below mean something.interface GanttMarkerEXIT=0, verdictmatchesiconEXIT=1—interface GanttMarker documents 'icon', not on the shipped typelabeltocaptionEXIT=1—documents 'caption'andomits 'label', both at:718colorEXIT=1—interface GanttMarker omits 'color'EXIT=0, census1 excerpt(s) declared by markerEXIT=1— the marker does not cover fabricationEXIT=1—declares an excerpt that omits nothing any moreEvery leg names the README, the interface and the key. Re-run in full on the merged head
e32824dae; the only commit after it (15a06cff3) edits three reason strings insidePARTIAL_EXCERPTS, none of which any leg above reads — every leg plants and judgesGanttMarker, which the ledger does not name. The restore proof closes it:git status --porcelainwas empty after the run.Verification
All at the branch head
15a06cff3unless a line says otherwise, gate verdicts quoted from the line each gate printed, every exit code captured by redirect before any pipe.Root vitest only (#3378), never package-scoped. The suite grew 47 → 83 cases.
Authored by Claude Code in session
session_01BGMDbrVa8JjZcCQ7DWYH1b(https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b) — written into the prose here because a footer does not survive a later edit of this body.One gate went red and it was mine to fix.
type-check:scriptsreported fiveTS2353onexcerpts— the option existed at runtime and not inscan's JSDoc, which is what that gate reads as the declaration. Contract-first: the producer's declaration was the stale half, fixed there rather than loosened at the five call sites. The vitest suite was green through all of it, which is why the type gate is a separate reading and not a duplicate of it.No changeset, and the gate says so in its own words:"2 file(s) changed, 0 of them published source of a package the release covers … no changeset is owed." Everything in this diff is under
scripts/. Same disposition as #6212, which shipped scripts-only the same way; theskip-changesetlabel is deliberately not applied — in this repository that label is read by no workflow and exempts nothing, and the gate's verdict line is the authority.Test scope, derived from disk not assumed.
git grep -l 'check-readme-exports'across*.test.tsreturns exactly one file:scripts/__tests__/check-readme-exports.test.ts. That is the whole set of suites that read anything this diff changes, and it was run in full on the final head.Coverage of the type gate, measured rather than claimed.
tsc -p tsconfig.scripts.json --listFilesreports 1 hit each forscripts/check-readme-exports.mjsandscripts/__tests__/check-readme-exports.test.ts— so "type-check is clean" is a statement about both edited files and not a true sentence about a population that excludes them.Lint scope, stated rather than assumed — and this is the full run for this diff, not a narrowing.
pnpm lint:rootiseslint .with four ignore patterns; read from eslint's own config via--format json, its population is 226 files, 0 errors, 29 warnings, and both edited files are in it with zero messages. No type-aware linting is configured (noprojectService, noparserOptions.project), so this diff cannot move the verdict of any untouched file. Every code file in the diff is underscripts/, i.e. entirely inside that population.Declared narrowing
Two, both stated so a reader can price them:
pnpm lint(turbo run lint, per package) was not run. Zero package source changed, and the three-part evidence above — population read from eslint's own config, file counts from--format json, and no type-aware linting — is what makes that a measurement rather than a skip.scripts/__tests__was not run whole, only the one suite. Derived from disk, as above: no other test file reads any changed path. CI runs the directory regardless.The authoritative reading is the CI job conclusions on this PR.
Merge, never a rebase
One merge of
origin/main(eb33a8d4c→ec0a7b846), no conflicts.git diff --name-onlyacross that range touches five files, all underscripts/, and zero underpackages/— so the builtdist/this gate reads is still exactly the merged tree's, and the census above is a reading about the tree being shipped rather than a stale one.Follow-ups
interfaceblocks are behind their shipped types — thePARTIAL_EXCERPTSledger #6214 opened with #7302 (filed here, unassigned) — the three README content fixes the ledger records, with theGanttTaskone spelled out as a marker rather than a widened block.check:doc-snippets. Its README debt is the 31-entryUNGATED_DOCSledger; check-doc-snippet-types.mjs collects only.mdxunder content/docs — 40.mdguides are uncovered with no UNGATED_DOCS entry, contradicting its own "covered by default" rule #5174's batches own the direction of travel, andpackages/plugin-gantt/README.md's 13 diagnostics are measured above for whoever takes it..mdxunder content/docs — 40.mdguides are uncovered with no UNGATED_DOCS entry, contradicting its own "covered by default" rule #5174's own class of finding, one directory deeper.DetailViewSchema) is measured and deliberately not built: it needs an inheritance policy first.BaseSchemacarries an index signature and its Zod mirror is.passthrough().Generated by Claude Code