Filed unassigned, not claiming. Follow-up to #5043, whose triage comment explicitly held this out of that card: "Tier 2 (extract-blocks-and-compile + bidirectional pins) is explicitly OUT of this card — it needs a baseline decision first (known pre-existing reds, see the entry-price note); file a follow-up if wanted." This is that follow-up.
Tier 1 shipped in PR #6212 (scripts/check-readme-exports.mjs, pnpm check:readme-exports, README Export Check, zero repo-wide).
What tier 1 provably cannot see
Tier 1 answers exactly one question: does the imported name exist. #5043's own measurements record the class it misses — a README where every name is a real export and the types are wrong:
The two design points already measured, so nobody re-derives them
A documented interface block compiles green no matter what it says. A standalone interface GanttTask { … } in a README is a local declaration unrelated to the real type; feeding it to tsc as-is proves nothing. It needs bidirectional pins appended to the block:
declareconstreal: RealGanttTask;exportconstdocFromReal: GanttTask=real;// real -> documenteddeclareconstdoc: GanttTask;exportconstrealFromDoc: RealGanttTask=doc;// documented -> real
Measured on the old plugin-gantt README, predictions written first, both directions red as predicted: TS2741 Property 'name' is missing in type '…GanttTask' but required in type 'GanttTask' and TS2741 Property 'title' is missing …. One direction pins only half: real→doc alone misses optional keys the doc block invents; doc→real alone misses required keys the doc block drops.
And the typed-example half cannot carry it. Predicted that the old const task: GanttTask = { name: …, start: '2024-01-01' } would report name/title; measured, it reported only two TS2322: Type 'string' is not assignable to type 'Date' — a property-level assignment error short-circuits the missing-property detail. So the key-rename fact is carried only by the pin block. A tier-2 implementation that extracts typed examples without adding pins misses that whole class.
Two engineering details from the same measurement:
- Multiple blocks in one README each write
const schema = { … }, and with no imports each is a global script, so one tsc invocation reports duplicate identifiers. One export {}; per block makes it a module — scope only, no effect on the check. - Some blocks in this family are deliberately not valid (bare
{ type, tasks: …, … } property sketches, bare dependencies: [ … ] fragments). They must be skipped explicitly and listed with the reason, never dropped silently. Classify by a content signature (a regex on the block's first line), not by block index — the same machinery then runs against both the current README and a pre-fix one, whose block indices differ.
Why it is blocked on a baseline decision, not on effort
Running all 15 compilable plugin-gantt blocks under strict, after#5012 landed, still exits rc=2: two TS7006 from onTaskClick: (task) => … written inside an unannotated const schema = { … }. Those are not a missed fix — they are part of the fabricated-schema-key surface filed as #5057.
So tier 1 could require zero repo-wide (and does). Tier 2 cannot on day one: it arrives with a batch of existing reds entangled with the invented-key surface, and needs triage plus a baseline ruling (fix-then-require-zero, or land with a shrink-only ledger like #5867's) before it can gate anything.
A stated bound both tiers share
Neither tier sees authorable-JSON key surfaces. BaseSchema carries [key: string]: any (packages/types/src/base.ts:318) and its Zod mirror is .passthrough() (packages/types/src/zod/base.zod.ts:154), so TS2353 is structurally impossible on ObjectXxxSchema — measured on gantt at 82a94170c: annotate a block containing only keys adjudicated fabricated, exit=0, zero errors. Tier 2 pins only three things on that family: the type literal, missing required keys, and literals of runtime types that do not inherit BaseSchema. That third one is the real capability and it is genuine — it is what caught CalendarEvent — but the authorable key surface needs a third instrument (#5057 / #5074 are its user-visible halves).
Also worth knowing when reading tier-2 output: when one literal has both an excess property and a property-level type error, TS reports only the latter and skips the freshness check, so a naive "compile once, read all errors" pass systematically under-reports excess keys.
Grading
For PM triage. Labelled finding: it is prevention work, and its user-visible instances are filed separately. The first task on it is the baseline ruling, not code.
Filed unassigned, not claiming. Follow-up to #5043, whose triage comment explicitly held this out of that card: "Tier 2 (extract-blocks-and-compile + bidirectional pins) is explicitly OUT of this card — it needs a baseline decision first (known pre-existing reds, see the entry-price note); file a follow-up if wanted." This is that follow-up.
Tier 1 shipped in PR #6212 (
scripts/check-readme-exports.mjs,pnpm check:readme-exports,README Export Check, zero repo-wide).What tier 1 provably cannot see
Tier 1 answers exactly one question: does the imported name exist. #5043's own measurements record the class it misses — a README where every name is a real export and the types are wrong:
packages/plugin-calendar/README.md(plugin-calendar README 教的calendarComponents手动注册 API 不存在 —— 照抄即抛 #5010): after the import path was corrected, the example still did not compile.CalendarEventwas a real export, but the runtime one (id: string | number,start: Date), not the authorable-JSON oneCalendarViewSchema.eventswants (id: string,start: string | Date). ThreeTS2322. Name-set checking is green on all of it.packages/plugin-gantt/README.md(plugin-gantt README 教的ganttComponents手动注册与GanttSchema类型都不存在 #5012):GanttTaskis a real export, and the "Task Structure" reference block writes it asname+ ISO string dates when the real type istitle+Date.The two design points already measured, so nobody re-derives them
A documented
interfaceblock compiles green no matter what it says. A standaloneinterface GanttTask { … }in a README is a local declaration unrelated to the real type; feeding it totscas-is proves nothing. It needs bidirectional pins appended to the block:Measured on the old plugin-gantt README, predictions written first, both directions red as predicted:
TS2741 Property 'name' is missing in type '…GanttTask' but required in type 'GanttTask'andTS2741 Property 'title' is missing …. One direction pins only half: real→doc alone misses optional keys the doc block invents; doc→real alone misses required keys the doc block drops.And the typed-example half cannot carry it. Predicted that the old
const task: GanttTask = { name: …, start: '2024-01-01' }would reportname/title; measured, it reported only twoTS2322: Type 'string' is not assignable to type 'Date'— a property-level assignment error short-circuits the missing-property detail. So the key-rename fact is carried only by the pin block. A tier-2 implementation that extracts typed examples without adding pins misses that whole class.Two engineering details from the same measurement:
const schema = { … }, and with no imports each is a global script, so onetscinvocation reports duplicate identifiers. Oneexport {};per block makes it a module — scope only, no effect on the check.{ type, tasks: …, … }property sketches, baredependencies: [ … ]fragments). They must be skipped explicitly and listed with the reason, never dropped silently. Classify by a content signature (a regex on the block's first line), not by block index — the same machinery then runs against both the current README and a pre-fix one, whose block indices differ.Why it is blocked on a baseline decision, not on effort
Running all 15 compilable plugin-gantt blocks under
strict, after#5012 landed, still exitsrc=2: twoTS7006fromonTaskClick: (task) => …written inside an unannotatedconst schema = { … }. Those are not a missed fix — they are part of the fabricated-schema-key surface filed as #5057.So tier 1 could require zero repo-wide (and does). Tier 2 cannot on day one: it arrives with a batch of existing reds entangled with the invented-key surface, and needs triage plus a baseline ruling (fix-then-require-zero, or land with a shrink-only ledger like #5867's) before it can gate anything.
A stated bound both tiers share
Neither tier sees authorable-JSON key surfaces.
BaseSchemacarries[key: string]: any(packages/types/src/base.ts:318) and its Zod mirror is.passthrough()(packages/types/src/zod/base.zod.ts:154), soTS2353is structurally impossible onObjectXxxSchema— measured on gantt at82a94170c: annotate a block containing only keys adjudicated fabricated,exit=0, zero errors. Tier 2 pins only three things on that family: thetypeliteral, missing required keys, and literals of runtime types that do not inheritBaseSchema. That third one is the real capability and it is genuine — it is what caughtCalendarEvent— but the authorable key surface needs a third instrument (#5057 / #5074 are its user-visible halves).Also worth knowing when reading tier-2 output: when one literal has both an excess property and a property-level type error, TS reports only the latter and skips the freshness check, so a naive "compile once, read all errors" pass systematically under-reports excess keys.
Grading
For PM triage. Labelled
finding: it is prevention work, and its user-visible instances are filed separately. The first task on it is the baseline ruling, not code.