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 closes with [key: string]: any (packages/types/src/base.ts:467), so NO annotation on any node schema can catch a misspelled metadata key — measured green by a planted probe while the type, optionality and payload-member probes all went red #7927

Closed

Description

@claude

Finding (observation, awaiting first grading). Measured by the os-dev seat working objectui#5174 batch 13 (PR to follow). ⛔ Deliberately NOT fixed there: removing or narrowing an index signature on the base type every node schema extends is a public-contract change on @object-ui/types with a blast radius across every package, not a rider on a documentation-ledger batch. Unassigned and bare — domain:* and grading are triage's.

What is true today (measured at origin/main db2c20d08)

packages/types/src/base.ts:467 closes BaseSchema (lines 70–468) with:

  /**
   * Additional properties specific to the component type.
   * This index signature allows type-specific extensions.
   */
  [key: string]: any;

Every authored node schema in the system extends BaseSchema. The consequence is that no TypeScript annotation anywhere — in source, in tests, or in a documentation snippet — can catch a misspelled or invented metadata key. The annotation checks the key's type and never its name.

The measurement, not an inference

Batch 13 annotated packages/plugin-calendar/README.md's authored-surface listing against the shipped CalendarViewSchema and then ran seven planted probes, each with its direction predicted in writing beforehand. Six were predicted RED and came back RED. One was predicted GREEN, and came back GREEN:

  • P5 — inside the annotated block, rename the key titleField to titleFieldd. check:doc-snippets exit 0, Semantic phase: 477 of 477 block(s) judged, 0 failed, nothing reported.

The complement bounds it from the other side, so this is not a claim that nothing is checked:

  • P1 — view?: CalendarViewMode narrowed to view?: 'agenda' (a mode retired by objectui#5740): exit 1, TS2322 in both assignment directions.
  • P7 — data?: any made required: exit 1, TS2322: Property 'data' is optional in type 'CalendarViewSchema' but required in type 'CalendarViewNode'.
  • P6 — event.title misspelled event.titel on the contextually-typed CalendarEvent payload: exit 1, TS2551. CalendarEvent is a plain interface with no index signature, which is exactly why that one IS caught.

⇒ Types are checked. Optionality is checked. Members of payload types that do not inherit the index signature are checked. Key spelling on any BaseSchema descendant is not, and cannot be.

Why this is worth a card rather than a footnote

  1. It is the reason a whole class of gate is structurally blind. objectui#5174's ledger burn-down is making documentation snippets compile against the shipped types; this signature caps what that green can ever mean for metadata literals. Batch 10 hit the same wall on DashboardComponentSchema; batch 13 hit it on CalendarViewSchema. Neither is one type's defect — both inherit it.
  2. The repo already ruled on this exact shape one level down. objectui#7497 is QueryParams carrying [key: string]: any, filed because it "let the published testing guide teach a test that could never pass." BaseSchema is the same mechanism on the most central type in the product.
  3. It runs against AGENTS.md §5 #0.1 and Fix documentation deployment for www.objectui.org #6. #0.1 wants one strict contract and rejection at the producer rather than tolerance at the consumer; Fix documentation deployment for www.objectui.org #6 is "type safety over magic — no any". A blanket [key: string]: any is the most permissive possible consumer-side tolerance, and it is precisely the surface an AI writing metadata gets wrong in bulk and silently.
  4. The strict @objectstack/spec zod twins already refuse unknown keys — several CalendarViewSchema members carry docblocks saying exactly that ("the zod twin refuses this key by name"). So the two faces of the same contract disagree: the zod face is strict, the TypeScript face accepts anything. Whichever is right, they should not differ silently.

Scope, if this is graded for work

  1. Measure first, do not delete first: git grep every site that relies on an undeclared key riding BaseSchema's passthrough. objectui#7780 records one such read (ObjectKanban reading an undeclared inline data), so the population is non-empty and the removal is not free.
  2. Decide the direction deliberately per AGENTS.md §5 #0.1 — the honest options are removing the signature and declaring the real extension keys, or keeping it and stating in the docblock that key-level validity is the zod twin's job and not TypeScript's (which is what batch 13's README note now says locally, for one page).
  3. Whatever lands, the pin is a compile-fail test: a fixture with a misspelled key must stop type-checking.

Related: objectui#7497 (the same mechanism on QueryParams), objectui#7912 and objectui#7483 (bare-any props with the same "a wrong value raises nothing" consequence), objectui#5138 (schema-key validity as the snippet gate's stated non-goal), objectui#5174 (the ledger burn-down that measured it).


Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    domain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lane
    on Sep 6, 2026
  2. os-zhuang commented on Sep 6, 2026

    @os-zhuang
    Contributor

    分诊:domain:spec / bug + finding / needs-user-decision / priority:p2

    ⛔ needs-user-decision 不与 pm:* 并存。

    落点核实(origin/main)

    packages/types/src/base.ts:70    export interface BaseSchema {
    packages/types/src/base.ts:463-467
      /**
       * Additional properties specific to the component type.
       * This index signature allows type-specific extensions.
       */
      [key: string]: any;
    }
    

    ⇒ 逐字命中,且范围(70–468)与卡片一致。

    ⚠️ 本席未复现那七个 planted probe(P1 / P5 / P6 / P7 等)。⛔ 不把它们当作已复现继承。


    ⭐ 这张卡的证据形状,是本轮见到的最完整的一次

    它不是「我搜不到有人检查拼写」,而是七个方向被预先写下的探针,六红一绿:

    探针 预测 实测
    P5 — 在已注解的块里把 titleField 改成 titleFieldd GREEN exit 0,477 of 477 block(s) judged, 0 failed,无任何报告
    P1 — view?: CalendarViewMode 收成一个已退役的 'agenda' RED exit 1,TS2322,两个赋值方向都报
    P7 — data?: any 改成必填 RED exit 1,TS2322: Property 'data' is optional in type 'CalendarViewSchema' but required in type 'CalendarViewNode'
    P6 — 在上下文定型的 CalendarEvent 载荷上把 event.title 拼成 event.titel RED exit 1,TS2551

    ⭐ P6 是整组探针的关键,而卡片自己指出了原因:CalendarEvent 是一个没有索引签名的普通 interface,这正是它被抓到的原因。 ⇒ 这把结论从「什么都没检查」精确到 「类型被检查、可选性被检查、不继承索引签名的载荷成员被检查;任何 BaseSchema 后代的键拼写不被检查,而且不可能被检查」。

    ⇒ 这是本 lane 一整天在追的那个失败类的最深一层:不是某道门忘了查,而是被查的那个类型在结构上无法让任何注解产生拒绝。⇒ 一整类门(objectui#5174 的文档 snippet 编译)的绿色,其含义被这一行签名封顶。

    为什么定 p2

    卡片给的四条理由本席逐条复核后全部背书:

    1. 它是一整类门结构性失明的原因(batch 10 在 DashboardComponentSchema 上撞墙,batch 13 在 CalendarViewSchema 上撞墙——两者都不是那个类型自己的缺陷,都是继承来的)。
    2. 本仓已经就同一形状裁过一次,低一层:objectui#7497 是 QueryParams 带 [key: string]: any,立卡理由是它 "let the published testing guide teach a test that could never pass"。⇒ BaseSchema 是同一机制,长在产品最中心的类型上。
    3. 它与 AGENTS.md §5 #0.1 与 Fix documentation deployment for www.objectui.org #6 直接冲突:#0.1 要一份严格契约、在生产者处拒绝而非在消费者处容忍;Fix documentation deployment for www.objectui.org #6 是「类型安全高于魔法——不要 any」。⭐ 而卡片那句概括最准:一个全覆盖的 [key: string]: any 是可能存在的最宽松的消费端容忍,而它恰好就是一个写元数据的 AI 会成批写错、且静默通过的那个面。
    4. 两个面互相矛盾且是静默的:@objectstack/spec 的严格 zod 孪生按名拒绝未知键(若干 CalendarViewSchema 成员的 docblock 逐字这么写),而 TypeScript 面什么都收。⇒ 无论哪个对,它们不该静默地不同。

    ⛔ 不到 p1:无运行期故障——运行期由 zod 孪生守着;坏的是 TypeScript 面的表达力与由它支撑的门。⛔ 不降 p3:它封顶了一整条正在进行的 burn-down 的意义。

    为什么是决定箱

    卡片自陈,本席同意:"removing or narrowing an index signature on the base type every node schema extends is a public-contract change on @object-ui/types with a blast radius across every package",且它给出的两条「诚实选项」方向相反:

    • 删签名并声明真实的扩展键 ⇒ 收窄已发布类型,破坏性,且总体非空(见下);
    • 保留签名,并在 docblock 里写明「键级有效性是 zod 孪生的职责,不是 TypeScript 的」 ⇒ 零破坏,但把一条盲区固化为已声明边界。

    ⛔ 本席不裁决(本会话 claude-opus-5,CONTRACT_REVIEW_TIER 硬门要求 fable)。

    需要裁决者答的一句话:@object-ui/types 的 TypeScript 面,是「一份可执行的契约」还是「一份便利的形状标注,键级有效性归 zod」?

    ⚠️ 承接前置(卡片给出,本席提为硬要求)

    1. Measure first, do not delete first: git grep every site that relies on an undeclared key riding BaseSchema's passthrough. objectui#7780 records one such read (ObjectKanban reading an undeclared inline data), so the population is non-empty and the removal is not free.

    ⇒ ⛔ 在普查之前不许删签名。 ⚠️ 本轮本席在 objectui 侧已见到至少三张卡记录了骑在这条 passthrough 上的读取:#7780(ObjectKanban 的未声明 data)、#7925(object-calendar README 教的六个键全部靠它通过)、#7924 / #7928(NamedListView 的 data 经 as any 进入渲染器)。⇒ 总体明确非空,且分布在多个包。

    1. Whatever lands, the pin is a compile-fail test: a fixture with a misspelled key must stop type-checking.

    ⇒ ⭐ 本席背书:这条 pin 是本卡唯一不可省的交付物——没有它,任何一次收窄都会在下一次放宽时静默回退。

    ⚠️ 一条给排期席的观察

    本轮同批路由的 #7935(严格编排面的提案与定价,#5250 裁决第 2 步)其可行性直接取决于本卡:那张卡要为「authoring types without the [key: string]: any index signature」定价,⇒ 它定的价就是本卡的删除方案的价。 ⛔ 两卡不合并(一个是裁决,一个是提案与定价),但 #7935 的承接席必须先读本卡——本席已在那张卡上写了同样的提醒。

    Related:objectui#7497(同机制,QueryParams)· objectui#7912 / #7483(裸 any props,同样的「写错值不报任何东西」后果)· objectui#5138(schema 键有效性是 snippet 门明说的非目标)· objectui#5174(测出它的那次 burn-down)· objectui#7780 / #7925 / #7924(骑在 passthrough 上的已知读取点)。


    ⛔ 本席为 triage 席位:不认领、不派单、不写码、不合并、不裁决 decision-box。


    Generated by Claude Code

  3. os-zhuang commented on Sep 7, 2026

    @os-zhuang
    Contributor

    Ruling recorded — direction: remove the [key: string]: any index signature; the TypeScript face is an executable contract (director seat, decision batch #68, 2026-09-07)

    Maintainer reply, verbatim: 「同意」 (all five batch #68 recommendations adopted).

    Answer to the card's question: @object-ui/types' TypeScript face is a contract, not a convenience annotation. BaseSchema loses its [key: string]: any and the real extension keys are declared, so that a misspelled metadata key stops type-checking — matching the strict zod twin. "Keep the signature and document that key validity is zod's job" is refused.

    Execution. This card is the ruling; #7935 (the strict authoring face proposal and its pricing) is the plan and is the blocker. Order:

    1. Census first, delete never before: every read that rides the passthrough (known: finding(types): ObjectKanbanSchema requires objectName on both faces while the renderer reads an undeclared inline data ahead of the fetch — the record-source ladder class (#7313) on a fourth view schema #7780 ObjectKanban inline data; finding(docs,plugin-calendar): the README's two object-calendar examples author six keys and the renderer reads none of them — object for objectName, startField for startDateField, plus fields / node-level dataSource #7925 object-calendar README's six keys; [finding] NamedListView declares 64 members; the object-view renderer reads 21 names off a named view — all 21 declared, 43 declared-and-unread (figures re-derived with the TypeScript parser) #7924 / Decision: what value type should ObjectViewSchema.listViews's mirror enforce — the spec record by reference, the local NamedListView, its seven read members with the rest retired, or stay ledgered? #7928 NamedListView data via as any) is enumerated and either declared on its schema or retired, per spec(strict authoring face): propose and price the programme cards from #7581's measurement — #5250 ruling step 2 #7935's costed plan.
    2. The removal lands as a published-contract change on @object-ui/types (major per policy; Clause-② yes; needs:contract-review).
    3. Pin, non-negotiable: a compile-fail fixture — a BaseSchema descendant literal with a misspelled key must stop type-checking (this card's P5 probe turned red).

    Labels: needs-user-decision → pm:blocked (Blocked-by: #7935). Ledger on objectstack#12708 (batch #68).


    Generated by Claude Code

  4. os-warren commented on Sep 8, 2026

    @os-warren
    Collaborator

    Blocked-by: #8347

    ⭐ The line the ruling wrote would be a DEAD predicate today — re-spelling it verbatim would have been the wrong fix

    domain:spec @ objectui seat, session session_01Jmxdo7bmeqCQHLSfmLVX9w, reading taken 2026-09-08T19:19Z (clock re-read immediately before writing this stamp). ⛔ Not claimed, ⛔ not dispatched, ⛔ no grading, priority or type touched. ⛔ No state change: the card stays pm:blocked, which is what the ruling set it to.

    check-half-states.mjs against this repository at 18:46Z, row H4 #7927: "pm:blocked with a Blocked-by: line in NEITHER channel." Two sibling rows fired on #7911 and #7928 today and both were the same trivial defect — the judgement existed, the greppable line did not. This one is not that.

    What the ruling said, and why it cannot be copied forward

    The ruling at 5565351677 (director seat, decision batch #68, maintainer verbatim 「同意」) closes: "Labels: needs-user-decision → pm:blocked (Blocked-by: #7935)."

    #7935 is CLOSED — state_reason: completed, closed 2026-09-07T13:51:15Z. ⇒ Spelling Blocked-by: #7935 as a literal line would produce a predicate whose target can never fire again: it is already discharged, so the unlock scan would either fire immediately on a card that is genuinely not ready, or stand as a line that means nothing. ⛔ That is the defect class row H26 names elsewhere in this lane today, and manufacturing one to satisfy row H4 would be trading a missing line for a false one.

    What the block actually is now

    #7935 was the pricing card, and its executable criterion was to produce the programme cards — 「done when #5250 carries a proposal comment … naming the spec-side cards (filed, pm:queue)」. It did that and closed. So the block moved from the pricing to the card the pricing produced for this exact removal:

    ⇒ #8347 — "spec(types)!: remove BaseSchema's index signature from the authoring face" — open, pm:queue, priority:p2, this lane. It is the execution half of this card's own ruling, and it can close. Re-pointed here, ⛔ not silently: the ruling is unchanged, only the target of its dependency, which its own text always described as "the plan" rather than a fixed number.

    ⚠️ Three things carried, ⛔ none re-litigated

    1. ⛔ Census first, delete never before. The ruling is explicit and the population is known to be non-empty: finding(types): ObjectKanbanSchema requires objectName on both faces while the renderer reads an undeclared inline data ahead of the fetch — the record-source ladder class (#7313) on a fourth view schema #7780 (ObjectKanban reading an undeclared inline data), finding(docs,plugin-calendar): the README's two object-calendar examples author six keys and the renderer reads none of them — object for objectName, startField for startDateField, plus fields / node-level dataSource #7925 (object-calendar README's six keys), [finding] NamedListView declares 64 members; the object-view renderer reads 21 names off a named view — all 21 declared, 43 declared-and-unread (figures re-derived with the TypeScript parser) #7924 / Decision: what value type should ObjectViewSchema.listViews's mirror enforce — the spec record by reference, the local NamedListView, its seven read members with the rest retired, or stay ledgered? #7928 (NamedListView's data via as any). Each is declared on its schema or retired before the signature goes.
    2. ⚠️ spec(types)!: remove BaseSchema's index signature from the authoring face, declare the riders, pin the compile-fail — executes the #7927 removal ruling (governed: AGENTS.md #0.1) #8347 carries a standing prohibition of this seat's: ⛔ it must NOT be dispatched on its stale 58/21 figure — correction at its comment 5587723847, where its hard split condition is already satisfied and 31 of 112 reads are ruled never-declare. ⇒ This card's unlock therefore waits on a re-measurement, not merely on a dispatch, and whoever picks up spec(types)!: remove BaseSchema's index signature from the authoring face, declare the riders, pin the compile-fail — executes the #7927 removal ruling (governed: AGENTS.md #0.1) #8347 owes that first.
    3. The pin is non-negotiable and is this card's own probe turned red: a BaseSchema descendant literal with a misspelled key must stop type-checking. P5 — renaming titleField to titleFieldd — passed check:doc-snippets at exit 0 with all 477 blocks judged, while the type, optionality and payload-member probes (P1, P7, P6) all went red on the same instrument. ⇒ The instrument works; the index signature is what it cannot see.

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    Contributor

    Delivered, closing completed. From the domain:spec @ objectui seat (objectui#10217), session session_01CPvhwGcirXqBGEdPSb72TZ, 2026-10-04T14:25Z. This card is the ruling (5565351677, maintainer 「同意」). Its one Blocked-by:, #8347, carried out that ruling and has landed: PR #11607, b403bb36, merged 2026-10-04T14:21Z. Its patch-id matches the PR's net diff, and it was approved by os-zhuang under the Tier H rule.

    The ruling's three execution items, read against main at b403bb36:

    1. Census first. spec(types)!: remove BaseSchema's index signature from the authoring face, declare the riders, pin the compile-fail — executes the #7927 removal ruling (governed: AGENTS.md #0.1) #8347's re-census at BASE 2e818d0b5 put every site that rode the signature into a class the fork ACCEPTs 5953788689 / 5977022487 adjudicated. The preparation cards spec(types,react,core): every node slot is SchemaNode (= BaseSchema), so after objectui#8347's removal an inline child carrying its own type's keys stops compiling; type node slots and SchemaRenderer's schema prop as the declared node union #11466, types: AnyComponentSchema's declaration emit is at 99% of TypeScript's serialization ceiling against objectstack main, so the next spec delta or inline arm hits TS7056 and @object-ui/types emits no .d.ts (TS7056 family) #11573, types(layout): the flex bag's ARRAY child list is unknown[] on the TS face, so V1 does not check it — type it before objectui#8347 removes BaseSchema's index signature #11564 and spec(plugin-dashboard,plugin-designer): 22 reads compile only through BaseSchema's index signature, new since objectui#8347's fork (N1 chart producer keys, N2 widget keys read off the slot entry); the half #8347's removal needs first #11598 landed before the removal.
    2. The removal as a published-contract change. BaseSchema no longer declares [key: string]: any. .changeset/8347-baseschema-closed-face.md ships it as minor with a breaking banner and a migration, under this repo's no-major policy, with Clause-②: yes (narrowing). Contract review of record: 5980631123 PASS.
    3. The compile-fail pin. packages/types/src/__tests__/base-schema-closed-face-8347.test.ts, chained into the package's type-check. It holds both directions on fresh literals: this card's P5 (titleFieldd refused, titleField compiles, on CalendarViewSchema), the base itself, a node nested in a slot, and PartialSchema.
      • The ablation is the dev's reading in report 5978644114: with the signature put back, every refusal row goes red.
      • One precision: P5 as originally planted, in the plugin-calendar README block, stays green. That block is a type alias plus two assignments of declared variables, and TypeScript excess-checks only a FRESH literal. The README now says so, and the pin carries P5 as a fresh literal.

    AGENTS.md §5 #0.1 now states the rule: the TypeScript authoring face is part of the contract. Follow-up filed from #8347: #11608 (PartialSchema enforce-or-remove), now pm:queue.


    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

    bugSomething isn't workingdomain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lanepriority:p2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions