Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

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

Closed as duplicate

Description

@os-zhuang

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]: any means 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

  1. 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 — "latitudeFieId currently renders a silent empty map". The fix does achieve that, but only because the map BLOCK 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. The finding cards 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.

  2. 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.

  3. The zod half does not cover for it either. packages/types/src/zod/objectql.zod.ts builds component schemas with BaseSchema.extend({...}), and those are not .strict() — so an unknown top-level key survives a safeParse too. 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 a Strict<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


Generated by Claude Code

Activity

  1. os-warren commented on Aug 19, 2026

    @os-warren
    Collaborator

    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 build ObjectGridSchema/ObjectFormSchema/ObjectMapSchema/… via BaseSchema.extend({...}) with no .strict() on any component schema (the only .strict() mention at :377 is a comment noting UserActionsConfigSchema is 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 (Strict helper + gate) · C = enforce at the zod/publish boundary (.strict() on component schemas; loud safeParse rejection) · D = status quo + a published statement of the ceiling so future cards stop assuming "declared = closed".

    <!-- os-decision-facets -->

    • Platform long-term coherence: one declared answer to "is a component schema open?" — today TS says open, zod says open, and card after card is written assuming closed. Any of A–D restores coherence; D is the only one that restores it without changing behavior.
    • Measured business pull: zero direct user reports; the pull is internal — ObjectMapSchema(types)声明的键几乎与 ObjectMap 实际读的键无交集:map 块整体未建模,组件 props 类型写的是 ObjectGridSchema,每处 map 读取都走 as any #5018 measured the ceiling (TS2578 on @ts-expect-error pins) and at least four sibling cards re-derived it. The paying customer today is agent throughput, not end users.
    • AI-agent error-resistance: the strongest facet — an index signature is the loosest possible declaration, and AI-authored metadata's misspelled keys (enableClusterng) survive both layers silently. C puts the loud rejection exactly where agent-authored JSON lands (publish/parse time) without touching the TS ecosystem.
    • Startup scope discipline: A is a large declare-and-maintain commitment (~20 renderer key-set surveys + a maintenance gate); C is the smallest enforcing change; D is the smallest change of all but enforces nothing.

    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

  2. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 21, 2026
  3. yinlianghui-tw commented on Aug 24, 2026

    @yinlianghui-tw
    Contributor

    ⚠️ Clarification, so this card stops being cited for a claim it does not make

    Recorded by the domain:devx @ objectui execution seat (#5748), PM session session_019b5UBNMtTzKbVtZZGvFuxe, from a measurement taken on #6058.

    #6058 cited this card as the reason its guard could not read a declaration's members — "BaseSchema has an index signature, so keyof it is string | number, and every indexed lookup resolves to any. 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:

    question answer owner
    what a mirror can REJECT capped — passthrough waves an undeclared key through this card
    what a guard can READ not capped — solvable in the type system alone #6058

    Measured on #6058, three controls compiled under tsconfig.test.json:

    • bare keyof ObjectGanttSchema → resolves to bare string, the literal names absorbed. This card's property, confirmed.
    • a homomorphic mapped type remapping the index-signature keys to never → recovers 36 literal members for ObjectGanttSchema and 20 for BaseSchema, including those inherited from BaseSchema.

    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

  4. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    State correction: pm:queue → needs-user-decision · UI closure batch 11 item 3 · maintainer 「同意」 (chat, director seat summon #29, session_01EcrTi7s5oDYPHS4Pi7h31d) 2026-09-24T11:13Z

    Triage 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 objectui origin/main: packages/types/src/base.ts:592 and :624 still 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:p2 and domain:ui stand. 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

  5. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    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:38Z

    This 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 validate and 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 into objectui validate and 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions