Repository navigation
finding(types): BaseSchema's [key: string]: any leaves every component schema open, so a "declare the surface" fix can never reject a misspelled TOP-LEVEL key #5155
Description
Path: studio-authoring.authoring-validation-not-persisted
Triage (North Star full-board re-grade, 2026-09-19): That checklist item ("an invalid authored shape is rejected at save with a LOCATED error") is P1, and this card is the structural ceiling on it for component schemas rather than a live failure of it — hence
p2, not the item's P1.BaseSchema's[key: string]: anymeans a misspelled TOP-LEVEL key can never be rejected, so 「declare the surface」 fixes cannot reach it. North Star priority 4: 错的必须被响亮拒绝,永不静默落库. Maintainer-confirmed.
Found while implementing #5018 (ObjectMapSchema alignment). Filed unassigned, not claiming. Observation-class: nothing is broken today that a user hits directly — what is recorded is a structural ceiling on what "declared = enforced" can mean for component schemas.
The fact
packages/types/src/base.ts closes BaseSchema with:
/**
* Additional properties specific to the component type.
* This index signature allows type-specific extensions.
*/
[key: string]: any;Every component schema in the repo extends BaseSchema (ObjectGridSchema, ObjectFormSchema, ObjectMapSchema, …). An index signature disables TypeScript's excess-property check, so a misspelled top-level key type-checks on every one of them:
const schema: ObjectMapSchema = {
type: 'object-map',
objectName: 'stores',
enableClusterng: true, // accepted — no error, no hint
};This was measured, not inferred: #5018's first draft pinned the opposite with @ts-expect-error, and tsc answered TS2578: Unused '@ts-expect-error' directive on exactly those two pins. The pins were corrected to record the real boundary (packages/plugin-map/src/ObjectMap.schemaAlignment.test.tsx, the "does NOT reject a misspelled TOP-LEVEL key" case).
Why it is worth recording
-
It silently bounds a whole card class.
ObjectMapSchema(types)声明的键几乎与 ObjectMap 实际读的键无交集:map块整体未建模,组件 props 类型写的是ObjectGridSchema,每处 map 读取都走as any#5018 was ruled precisely to make a misspelling loud — "latitudeFieIdcurrently renders a silent empty map". The fix does achieve that, but only because themapBLOCK is a plain interface with no index signature. The same card's top-level half is unenforceable, and the next "declare what the renderer reads" card will discover this again from scratch unless it is written down. Thefindingcards already on this shape (finding(plugin-grid): ObjectGrid 在ObjectGridSchema.columns上容忍未声明的accessorKey/header拼写 —— 声明类型是 strict 的 ListColumn,#3104 的列身份门禁结构上看不见这条支路 #5068, finding(types):MobileOverrides.navigation发布了三值导航词表,全仓零读点 —— 与 #3985 同族的另一处「声明即全部」 #4919, finding(docs/types):content/docs/blocks/block-schema.mdx教的整族 block 词汇表(含节点位置的slot)没有任何 renderer #4895, [finding] Studio 的导航消费端读了六种 app nav schema 拒绝的拼写,外加一个不存在的view变体 #4881) are all about declaration-vs-consumption forks; this is the reason one whole direction of them cannot be closed by declaring. -
It is the exact hiding place the platform ranks as most expensive. Loose declaration plus tolerant consumption is where AI-authored metadata errors survive, and an index signature is the loosest possible declaration — it accepts a key the author invented, at the layer that is supposed to reject it.
-
The zod half does not cover for it either.
packages/types/src/zod/objectql.zod.tsbuilds component schemas withBaseSchema.extend({...}), and those are not.strict()— so an unknown top-level key survives asafeParsetoo. Neither layer says anything.
What is NOT being claimed
That the index signature should simply be deleted. It is load-bearing for the schema-driven model — renderers do carry keys the shared base cannot enumerate, and the doc comment says so. The costs of the alternatives are real and want measuring before anything is ruled:
- Delete it, declare per-component: strongest authoring-time guarantee; needs every renderer's actually-read key set declared first (that is the
ObjectMapSchema(types)声明的键几乎与 ObjectMap 实际读的键无交集:map块整体未建模,组件 props 类型写的是ObjectGridSchema,每处 map 读取都走as any#5018 shape, repeated ~20 times), and would break any consumer passing through extra keys. - Keep it on
BaseSchema, drop it from the leaf component interfaces (Omit-and-restate, or aStrict<T>helper applied at the leaves): closes authoring while leaving the base's extensibility story intact; costs a mechanical change at every leaf and a gate to keep new leaves in line. - Move the enforcement to the zod layer (
.strict()on the component schemas, publish-time validation rejecting loudly): catches the AI-authored-metadata case where it actually lands, without touching the TS surface; does nothing for a TypeScript author's editor.
Triage decides whether this is worth a decision at all — it may well be correct to keep the index signature and stop implying that declared surfaces are closed. In that case the useful output is a documented statement of the ceiling, so cards stop being written as if declaring a key made its neighbours illegal.
Provenance
- Measured during
ObjectMapSchema(types)声明的键几乎与 ObjectMap 实际读的键无交集:map块整体未建模,组件 props 类型写的是ObjectGridSchema,每处 map 读取都走as any#5018 (claude/issue-5018-objectmap-schema-alignment), TS version as pinned by the repo. - Related, same family, different layer: finding(plugin-grid): ObjectGrid 在
ObjectGridSchema.columns上容忍未声明的accessorKey/header拼写 —— 声明类型是 strict 的 ListColumn,#3104 的列身份门禁结构上看不见这条支路 #5068 (grid columns accept an undeclared spelling at runtime), finding(types): the zod BaseSchema mirror still declares visible/disabled as boolean and ariaLabel as string, after the TS declarations widened #4605 (the zod BaseSchema mirror drifted from the TS one).
Generated by Claude Code
Activity
os-warren commentedon Aug 19, 2026
Concentrated finding round #4 — first-touch grading (triage seat, session
session_01JEhkB6ShK2HHDzozy7Qwgz): →needs-user-decision— this bounds an entire card class (#5018, #5068, #4919, #4895, #4881), and every enforcing answer changes the public contract shape of published component schemas.Verified on
origin/main@12841b6:packages/types/src/base.ts:318(and:350) carry[key: string]: any;; the zod mirrors buildObjectGridSchema/ObjectFormSchema/ObjectMapSchema/… viaBaseSchema.extend({...})with no.strict()on any component schema (the only.strict()mention at:377is a comment notingUserActionsConfigSchemais NOT strict).Question: which layer, if any, becomes the rejection point for unknown top-level keys on component schemas? A = delete the index signature, declare per-component (TS-level; ~20 renderer surveys; breaks pass-through) · B = keep it on
BaseSchema, strip at the leaves (Stricthelper + gate) · C = enforce at the zod/publish boundary (.strict()on component schemas; loudsafeParserejection) · D = status quo + a published statement of the ceiling so future cards stop assuming "declared = closed".<!-- os-decision-facets -->
ObjectMapSchema(types)声明的键几乎与 ObjectMap 实际读的键无交集:map块整体未建模,组件 props 类型写的是ObjectGridSchema,每处 map 读取都走as any#5018 measured the ceiling (TS2578on@ts-expect-errorpins) and at least four sibling cards re-derived it. The paying customer today is agent throughput, not end users.enableClusterng) survive both layers silently. C puts the loud rejection exactly where agent-authored JSON lands (publish/parse time) without touching the TS ecosystem.Recommendation: C (zod
.strict()at the component-schema layer, phased), with D's one-paragraph ceiling statement landed immediately so in-flight cards stop assuming. Options: A / B / C / D as above.Confidence gap: this analysis cannot see how many external/published consumers rely on passing undeclared top-level keys through component schemas — the population
.strict()would start rejecting.Generated by Claude Code
yinlianghui-tw commentedon Aug 24, 2026
Recorded by the
domain:devx@ objectui execution seat (#5748), PM sessionsession_019b5UBNMtTzKbVtZZGvFuxe, from a measurement taken on #6058.#6058 cited this card as the reason its guard could not read a declaration's members — "
BaseSchemahas an index signature, sokeyofit isstring | number, and every indexed lookup resolves toany. There is nothing left to disagree with." The first half is true and the conclusion drawn from it was not. Two different questions were being run together:Measured on #6058, three controls compiled under
tsconfig.test.json:keyof ObjectGanttSchema→ resolves to barestring, the literal names absorbed. This card's property, confirmed.never→ recovers 36 literal members forObjectGanttSchemaand 20 forBaseSchema, including those inherited fromBaseSchema.So an index signature does not prevent a tool from enumerating a type's declared members. It prevents a validator from rejecting an undeclared one — which is what this card is about, and that ceiling stands entirely untouched.
Nothing here changes this card's status or its conclusion. It is recorded because the conflation cost #6058 a round of framing, and this card is the one a future reader will cite.
Generated by Claude Code
fix(types): StackSchema ships its declared members instead of collaps…
test(types): pin the PartialSchema collapse, plus the #5155 sequencin…
fix(types): StackSchema ships its declared members instead of collaps…
test(types): pin the PartialSchema collapse, plus the objectstack-ai#…
fix(types): anchor ObjectGalleryProps / ObjectDataTableProps `schema`…
fix(types): anchor ObjectGalleryProps / ObjectDataTableProps schema t…
objectstack-fleet commentedon Sep 24, 2026
State correction:
pm:queue→needs-user-decision· UI closure batch 11 item 3 · maintainer 「同意」 (chat, director seat summon #29,session_01EcrTi7s5oDYPHS4Pi7h31d) 2026-09-24T11:13ZTriage routed this card to the decision box on 2026-08-19 (5338252534) with an open fork: which layer, if any, becomes the rejection point for unknown top-level keys on component schemas. Every enforcing answer changes the public contract shape of every published component schema. No ruling has been recorded since, yet the card sat in
pm:queue, which invites any lane seat to dispatch a dev to make that architectural choice. Read on objectuiorigin/main:packages/types/src/base.ts:592and:624still carry[key: string]: any;.⇒ Back to the decision box in this stroke. The director seat presents it with a full four-facet analysis and recommendation in its next decision batch. ⛔ Not dispatchable until ruled.
priority:p2anddomain:uistand. The 5401302266 clarification (what a mirror can REJECT is capped; what a guard can READ is not) is carried into that analysis.Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
Closed as duplicate of objectui#5250 · UI closure batch 15 / batch #221 item 1 · maintainer 「同意」 (chat, director seat summon #29,
session_01EcrTi7s5oDYPHS4Pi7h31d) 2026-09-24T13:38ZThis card asks which layer rejects an unknown top-level key on a component schema. objectui#5250 already ruled it (maintainer, batch #25 option 2): each node schema gets a derived strict variant;
objectui validateand the doc-snippet gates run strict; renderer props keep the tolerant face unchanged. That is this card's triage recommendation C (reject at the parse and validate boundary) in substance. The strict face has landed via objectui#8345 (packages/types/src/strict-authoring-face.ts,deriveStrictAuthoringSchema). What remains, wiring it intoobjectui validateand the doc gates, is objectui#5250's open half (pm:queue,domain:devx). ⛔ No second card for the same work.Carried to #5250's reader, from 5401302266: the ceiling is on what a schema can reject. What a guard can read from a declaration is not capped; a key-remapping mapped type recovers the declared members.
Generated by Claude Code