Repository navigation
feat(types)!: BaseSchema loses its index signature, so a typed node literal refuses an undeclared key; the compile-fail pin, both directions (objectui#8347) - #11607
Conversation
…eir readers take off the node, where those readers live `ObjectKanban` reads the gate-written `sort` (the binding's `dataSource.sort`, objectui#10068) through a non-exported read type, `GateBoundKanbanSchema`, and `TimelineRenderSchema` declares the `onItemClick` slot `ObjectTimeline` composes. Neither key joins an authoring face: `sort` stays undeclared on `ObjectKanbanSchema` (objectui#8174) and `onItemClick` stays off `TimelineSchema` (the objectui#6170 docblock). Both reads compiled only as `any` through `BaseSchema`'s index signature; this compiles with or without it. Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ Co-authored-by: Claude <noreply@anthropic.com>
…mpiling without `BaseSchema`'s index signature Two schema-to-record reads convert through `unknown`, and the gantt and map `propertiesBag-10859` fixtures keep `operator` a literal and type the bag node as its block node. Each compiles with the signature present too. Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ Co-authored-by: Claude <noreply@anthropic.com>
…put, by reference, on both faces
The TypeScript face is `@objectstack/spec`'s `EvaluatedExpressionInput`; the
zod twin is the same accept set (a predicate string, or the spec's own
`EvaluatedExpressionSchema` envelope) with `EvaluatedExpressionInputSchema`'s
verdict and without its string transform. Both read `string` before, so the
`{ dialect, source }` envelope a spec parse writes into this key was refused
(objectui#8347, ruled Q6 = B). `visible-when-spec-input-8347.test.ts` pins the
type identity, the verdict parity on a probe set read against the spec in the
same run, and that a string parses to itself.
Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ
Co-authored-by: Claude <noreply@anthropic.com>
…l pin, both directions, and the adjudicated flips `BaseSchema` declares no `[key: string]: any`, so a node literal annotated with its node type refuses a key no declaration names (objectui#8347, executing the objectui#7927 ruling). `ComponentRendererProps` keeps its own signature. - `base-schema-closed-face-8347.test.ts`: #7927's P5 turned red on a fresh literal, the correct spelling compiling, a nested node, the non-fresh bound, and `PartialSchema`'s working reading. - The adjudicated probe and ceiling rows flip: each "an unknown key still compiles" row becomes an `@ts-expect-error`, and each `IsAny` fall-through control becomes a membership reading with a lit `IsAny<any>` control. - The `props` alias rows flip (option A, the maintainer's open choice). - The valueless `metric-card` in the legacy envelope is pinned refused on the TS face (Q3 = A); `envelopeStray` and its `complex.ts` note go, per the constant's own removal condition. - `object-view-slot-key-lists` tripwires flip; `partial-schema-collapse-pin` is deleted per its header and `PartialSchema`'s docblock describes the alias as it now works. - The mirror-parity ledger records five interface-to-passthrough entries the removal surfaces, measured as an artifact of the assignability test. - Prose in these files that described the signature as live is corrected. Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ Co-authored-by: Claude <noreply@anthropic.com>
The listed doc blocks now compile against the closed face: architecture.md's button writes `label`; objectos-integration.mdx's page block authors `regions`, and its object-view nodes drop the adapter written as `dataSource`; the types README's card writes `children`; plugin-calendar.mdx and plugin-map.mdx pass their click handlers as component props instead of node keys. Prose that described `BaseSchema`'s signature as live is corrected (schema-reference, schema-renderer, ci-cd-pipeline, plugin-form, the wrapperClass callouts, and the plugin-form, plugin-dashboard, plugin-calendar and types READMEs), the `examples/dashboard.ts` comment too, and the pending flex changeset gets a dated note (objectui#8347). Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ Co-authored-by: Claude <noreply@anthropic.com>
…he contract; §4 drops `props` Describes what shipped with objectui#8347: `BaseSchema` declares no index signature, a refused key is declared by reference or fixed at the producer, never cast past, and the fresh-literal bound is stated with the pin that measures it. §4's abridged listing drops `props`, which `BaseSchema` does not declare, and #4's `events` sentence names the face that still keeps one. Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Inputs: card #8347 (body and all 26 comments, rulings and earlier records included), PR #11607 (body, 79-file list, net diff against ① Derived judgments
② Semver level
③ Boundary flagsDev deviations (report
The open choice: Acceptance notes (read, not filed): Carried items, checked against the diff: Gate reading: every check-run on the head is Implemented-by: VERDICT: FAIL One finding, ② only: the Generated by Claude Code |
… plugin-timeline is `minor` The arm is the closed pair `(widening)` / `(narrowing)`, and `(mixed)` read as no arm at all; this repository spells the widen-one-surface, narrow-another shape `yes (narrowing)`. The plugin-timeline bullet now says what ships: an optional `onItemClick` on `TimelineRenderSchema`, the prop type of the exported `TimelineRenderer`, so a one-member widening of an exported type and `minor` (contract review `5978769372`, ② and ① item 6). Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Inputs: card #8347 (body and all 27 comments: the three P1 fork reports, the fork ACCEPTs, the carried items, the two done reports What moved between the two heads: one commit, one file, ① Derived judgments
② Semver level
③ Boundary flagsDev deviations (report
Patch-round report The open choice, Owed, and not evidenced in the inputs as filed: the enforce-or-remove card for Acceptance notes (read, not filed), and one correction:
Carried items, checked against the diff: Gate reading (the head's check-runs, which are the gate verdicts), read three times: at 2026-10-04T10:05Z 42 runs with 13 Landing: this PR is Tier H ( Implemented-by: VERDICT: PASS Generated by Claude Code |
…gnature now gone) Comment and prose bytes only. Every sentence that said `BaseSchema` (or a node type through it) carries, closes with or ends in `[key: string]: any`, every future-tense "objectui#8347 removes", and every TS2578 note that promised a directive goes unused when its member is deleted, now says what is true at this head: the signature is gone, a deletion is refused on a fresh literal and still rides a widened value, and the zod face keeps its `.passthrough()` reasoning, named as the zod face. AGENTS.md section 5 #0.1 gains the package-local hand-off-type route for a host-written runtime key (objectui#6356). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…ignature Patch round 3 on objectui#8347 works on the merged tree: the six PRs main took since the merge base touch no file this branch edits, so the merge is textual-conflict-free; the post-merge type-check of the packages main moved is the check that the merge is also semantically clean. Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ Co-authored-by: Claude <noreply@anthropic.com>
…ipt face (objectui#8347) The architecture guide's "JSON Protocol" section and the protocol rule's "Component Schema Structure" section taught that the published `BaseSchema` declares its members "plus an index signature", and that `props` / `events` (and `properties`) reach a node through `[key: string]: any`. objectui#8347 removed that signature, so both became false on the TypeScript face. Both warning paragraphs now give the two verdicts a key no member declares gets, one per face: a compile error in a fresh typed literal on the TypeScript authoring face (with the fresh-literal bound stated as AGENTS.md section 5 #0.1 states it), and kept-then-judged-by-the-named-reader on the tolerant zod / JSON face. The teaching fences drop `props`, `events` and `properties`, which the published type never declared, and end on the same no-index-signature line AGENTS.md section 4 uses; they stay SHAPE copies, not member lists, and the member count in prose is gone (section 5 #9). `props` keeps one sentence of its own, stating option A as shipped (not declared on the TypeScript face, read by the runtime), so a flip to B or C is a one-sentence edit. The "Actions Are Node Types" rule's `events` paragraph no longer points at a fence entry that is gone. Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ Co-authored-by: Claude <noreply@anthropic.com>
…d, and retitle the four pins that described the signature as live While `BaseSchema` carried `[key: string]: any`, an `@ts-expect-error` over a tombstone or a typed member doubled as a deletion guard: delete the member, the key widened to `any`, the directive went unused, TS2578. Since the removal the deleted key is refused as undeclared (or the indexed access itself errors), the directive stays used, and the deletion goes unnoticed. Each of the five files gains a type-level row that asserts the member EXISTS with its declared or tombstone type: an indexed access on a member that does not exist is a compile error, and `Equal` reddens on a re-typing. `object-kanban-card-title-9606` pins `cardTitle` (and the `titleField` alias the ruling forbids retiring); `object-grid-default-sort-tombstone-5861` pins `defaultSort`; `object-grid-show-filters-retired-11068` pins `showFilters` (and that the list-view and object-view spellings stay live); `object-grid-declared-keys-11068` pins `rowSpecActions` and `bulkSpecActions`, the two tombstones `BaseSchema` does not declare; `gantt-flat-config-declared-keys` pins `staticData`, `filter` and `sort`, the three query keys its derived invariant cannot see. Every existing assertion stays. The header comments name the real guard. Four strings that described the signature as live are rewritten, the assertion logic untouched: two it-titles (action-callback-retired-7068, timeline-dead-keys-retired-6170), one it.each title (stack-schema-emitted-members) and one assertion message (schema-reference-named-list-view-keys-7923). No baseline, snapshot, allowlist or script references those titles (tracked-files grep, zero hits outside the four files). Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ Co-authored-by: Claude <noreply@anthropic.com>
…tion exposed The first ablation pass on the restored guards showed that a deleted member also reddens the `zod-mirror-parity` mirrored-undeclared ledger (the zod twin still declares it), the kanban file's two fresh literals, and, for `defaultSort`, the `ObjectGridSlotKey` constraint in `objectql.ts`. The new rows' docblocks say so, and the kanban file's older claim that the ratchet is blind to this deletion is dated to when it was measured, with the re-measurement beside it. Comment bytes only. Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ Co-authored-by: Claude <noreply@anthropic.com>
…y they restate `any` The `_StaticDataIsDeclared` and `_FilterIsDeclared` rows pin members `objectql.ts` declares as `any[]`; `Equal` is strict, so the row must spell the same type. A per-line disable with that reason replaces the two `no-explicit-any` warnings the lint run reported. Comment bytes only. Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Inputs: card #8347 (body and all 31 comments: the three P1 fork reports and their ACCEPTs, the carried items, the done report What moved since the PASS record's head ① Derived judgments
② Semver level
③ Boundary flagsDev deviations, each answered. Round 1 (
Round 2 (
Round 3 (
The open choice, A precision on the prose face, for the seat, not a finding: the rewritten The Carried items, checked against the diff: Gate reading (the head's check-runs, which are the gate verdicts), read three times: at 2026-10-04T12:42:34Z, 42 runs with 5 Landing: this PR is Tier H on two rows ( Implemented-by: VERDICT: PASS Generated by Claude Code |
…hrough SchemaRendererProvider; the widget-slot ledger entry names who keeps the fallback (objectui#8347 round 4)
Patch round 4, doc and comment bytes only (seat ruling 5980160589 on contract review 5980141759 ③).
content/docs/guide/objectos-integration.mdx: the App block this PR rewrote handed the adapter as a `dataSource` prop on `SchemaRenderer`. The renderer reads its data source from `SchemaRendererContext` (the `useContext(SchemaRendererContext)` read in SchemaRenderer.tsx), and an explicit prop is only forwarded to the root component, so the nested `object-view` never got the adapter. The block now wraps the renderer in `SchemaRendererProvider dataSource={dataSource}` from `@object-ui/react`, imported the way the other guides import it, with a comment saying why; the "System Objects Integration" comment names only the provider. No other line this PR added hands an adapter to `SchemaRenderer` as a prop (the two plugin-calendar blocks pass `dataSource` to `ObjectCalendar`, which declares that prop itself); the occurrences already on main are left as they are.
packages/types/src/__tests__/zod-mirror-parity.test.ts, the `complex.zod.ts#DashboardWidgetSchema` `component` entry: its exit condition read as a reservation for objectui#8347 to retire the mirror slot's `BaseSchema` fallback. The fallback stays by objectui#8344's decision; objectui#8347's Q3 = A (ACCEPT 5977022487) put the zod face out of that card's scope; no open card retires the arm, and narrowing it is a mirror change. The comment now says exactly that, and that this entry and the `widgets` one above are deleted when a card does retire it. The TypeScript printer with removeComments prints HEAD and this tree identically for the file.
Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ
Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Inputs: card #8347 (body and all 33 comments: the three P1 fork reports and their ACCEPTs, the carried items, the done report What moved since the PASS record's head The comment-versus-code split over the whole net diff: of the 39 shipped (non-test) source files in the file list, exactly four carry a non-comment change ( ① Derived judgments
② Semver level
③ Boundary flagsDev deviations, each answered. Round 1 (
Patch round 1 ( Round 2 (
Round 3 (
Round 4 (
The open choice, Seat ruling Acceptance notes (read, not filed):
Carried items, checked against the diff: Gate reading (the head's check-runs, which are the gate verdicts), read three times: at 2026-10-04T13:26:51Z, 42 runs, 29 Landing: this PR is Tier H on two rows ( Implemented-by: VERDICT: PASS Generated by Claude Code |
维护者速读改了什么:
为什么改:落地 objectui#7927 的裁决(TS 面是可执行的契约),也就是 #5250 严格化里 TS 的那一半。此前签名让任何键都能编译,AI 写错元数据时被静默放过。为此已先落地准备卡 #11466、#11573、#11564、#11598。 风险与代价(含回滚):
席位意见:建议批准。
你要做的:审阅后在本 PR 上提交 Approve( Generated by Claude Code |
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…ys; the snake_case spellings are refused by name (objectui#11610) (objectstack-ai#11614) Fixes objectstack-ai#11610 Clause-②: yes (narrowing) The `grid` widget's eight field-level keys become camelCase in one move across the published type, its zod mirror, the form-field face and every reader. The snake_case spellings retire at once and are refused by name on all three faces, each refusal naming the camelCase key. Ground: objectstack-ai/objectstack#21704 fork 2, ruled B (record `5978663135`); claim `5979906990` (seat ruling: refuse by name, no load-time conversion, premised on no stored producer). Dispatch session `session_01CPvhwGcirXqBGEdPSb72TZ`, `mode:subagent`. ## Premise (measured before any edit): no stored producer outside objectui's own fixtures - **objectstack `origin/main` `316be321`** (read through `git grep` on a private ref, nothing edited): the eight snake_case words occur only in comments, test strings and one changeset line. `packages/spec` `component.zod.ts` names them in three comments (the line-items guidance block, the `customFields` fork note, the master-detail `sortField` retirement note); the retirement test for `details.sortField` carries one as a must-not-match judge string; `conversions/registry.ts`, `migrations/registry.ts` and the retired-key entry for `details.sortField` carry it in comments. No schema declares or emits any of the eight as a field's metadata, and the spec's `FieldType` enum has no `grid` member at all, so the spec cannot store a `grid` field. The seat's reading is confirmed. - **objectui at base `b508ac50`** (every tracked file, `git grep` on that commit), each hit classified: - grid field metadata, READERS: `GridField.tsx` (the eight reads). - grid field metadata, PRODUCERS (in-repo, render-time, not stored): `MasterDetailForm` and `LineItemsPanel` (both hand the grid an object cast through `any`), and `apps/console` `DevLookup` (a dev harness). - grid field metadata, DECLARATIONS: `GridFieldMetadata`, the form-field face (`FormField`), its zod mirror (`FormFieldSchema`). - grid field metadata, STORED fixture: `examples/schema-catalog` `fields-grid/line-items-grid.json` (the only stored document). - docs: `content/docs/fields/grid.mdx`; the clipboard-paste RFC (states `max_rows` / `allow_add` as GridFieldMetadata's API in its proposed wiring); ADR 0001 (reported below, not edited). - tests: seven `GridField` test files in `packages/fields` (the triage counted five), the master-detail members test and the line-items total test in `packages/plugin-form`, three `packages/types` tests (strict face, form-field coverage, mirror parity comment), the console registry pin's prose. - SAME NAME, DIFFERENT READER (not renamed): the `## MAX_ROWS vs MIN_ROWS` heading-anchor string in `plugin-markdown`'s toc test; the en locale comment (a comment about the grid's label, updated); every `CHANGELOG.md` (history, untouched). The master-detail `details.sortField` the spec retired is a different key on a different object and stays untouched. - Existing facts not re-measured: the objectstack#21464 census (0 `customFields` entries) and the triage's hotcrm / cloud reading (0 hits). ## Rename map | FROM | TO | | --- | --- | | `min_rows` | `minRows` | | `max_rows` | `maxRows` | | `allow_add` | `allowAdd` | | `allow_delete` | `allowDelete` | | `allow_reorder` | `allowReorder` | | `total_field` | `totalField` | | `add_label` | `addLabel` | | `sort_field` | `sortField` | One list holds it: `GRID_FIELD_RETIRED_KEYS` (new export of `@object-ui/types`, with its key type `GridFieldRetiredKey`). The zod arms and the widget both read it; a type-level pin holds its keys equal to the `never` tombstones and its values equal to the live camelCase members. ## The retirement form, per face (refused by name, never stripped) - **TypeScript.** `GridFieldMetadata` and `FormField` each declare the eight snake_case keys as `never` tombstones with a `@deprecated` note naming the replacement. `{ type: 'grid', name: 'lines', min_rows: 1 }` typed `GridFieldMetadata` does not compile; on the docs face the gate's compile of `grid.mdx` reports `TS2322: Type 'number' is not assignable to type 'undefined'` for the same key. - **zod (`@object-ui/types/zod`).** `FormFieldSchema` declares each snake_case key as an `aliasKeyRefusal` arm. On the mirror, the tolerant face (`safeValidateSchema`, the `objectui validate` door) and the strict authoring face, `fields[0].min_rows: 1` answers exactly one issue, `invalid_type` at `fields.0.min_rows`, whose message leads ``Unrecognized key(s) on this form field: `min_rows`. Did you mean `min_rows` → `minRows`?`` and goes on to say the value stays the same. Any value is refused (the refusal is by name). The camelCase key with the same value parses and is kept on all three doors. - **The widget (`@object-ui/fields`).** `GridField` is now a thin gate over the grid body: a field whose metadata carries any snake_case key (with a defined value) is drawn as `RetiredGridFieldKeys` instead of the grid, an inline `role="alert"` (`data-testid="grid-field-retired-keys"`) reading for example ``[object-ui] Grid field `lines` carries retired snake_case key(s): `min_rows` → `minRows`. …``, plus the same text on `console.error` once per message. This is `RetiredFieldTombstone`'s settled shape in this package: nothing thrown, nothing silently substituted, rows untouched (`onChange` is never called). Producers move with it: both plugin-form adapters write the camelCase keys, and their grid object is now `satisfies` Partial of `GridFieldMetadata` instead of `as any`, so a snake_case key written there no longer compiles. ## Pins and guards, with ablation readings All ablations ran through the anchored mutation tool (anchor must hit; blob change and restore proven on disk; restore leg `git checkout HEAD`, blob after restore equal to the HEAD blob, `git diff HEAD` empty), from committed state. 1. **The eight camelCase keys draw**, each through the real `GridField` with a lit control without the key (`GridField.retiredSnakeKeys-11610.test.tsx`): `minRows` disables every Remove at the minimum; `maxRows` disables Add and every Duplicate at the maximum; `allowAdd: false` removes Add and Duplicate; `allowDelete: false` removes Remove; `allowReorder: false` removes the drag handles; `totalField` draws the footer total (30); `addLabel` labels Add; `sortField` stamps the new indices after a drag. Through the real form path too: `fields-grid-keys-camelcase-11610.test.tsx` renders the catalog fixture through `SchemaRenderer` and the form renderer (label `Add line item`, total `109.97`, two drag handles / Duplicate / Remove, all enabled), with the snake_case respelling as its control. 2. **A snake_case key is answered by the refusal, never dropped**: per key, the alert names the key and its replacement, the grid is not drawn, `console.error` is called once with the alert's text; edges pinned: several keys in one alert in map order, a snake key beside its camelCase twin is still refused, an `undefined` value is not refused (as on the TS and zod faces), a nameless field reads "This grid field", a second mount logs nothing new. - ABLATION (widget gate disabled): 12 refusal tests red, the 10 others (the eight draws, the lit control, the `undefined` row) green. Restored. 3. **zod refusal by name** (`grid-field-keys-camelcase-11610.test.ts`, three doors × eight keys, plus lit controls and an any-value row). - ABLATION (delete the `min_rows` arm): 8 tests red (the `min_rows` row and the any-value row on each of the three doors, and both `form-field-zod-coverage` rows). Restored. 4. **TS tombstones, each with a deletion guard that does not lean on an index signature** (same file, read by `tsc -p tsconfig.test.json`): an `Equal` row per member on both faces, a `keyof` membership row on `FormField`, and the map-equals-tombstones rows. - ABLATION A (delete all eight `GridFieldMetadata` tombstones): the eight `Equal` rows red, `TS2339` naming each key; the two map rows red; the strict-face lit-control row red; the map's `satisfies` red (`TS2561`).⚠️ The eight `@ts-expect-error` rows over fresh `GridFieldMetadata` literals stayed SILENT: with the member gone the key is still an excess property and the directive swallows it. That is the objectui#8347 lesson, reproduced; the `Equal` rows are what catch the deletion. - ABLATION A1 (delete `total_field` alone): exactly its `Equal` row (`TS2339`), the map row and the `satisfies` red. - ABLATION B (delete all eight `FormField` tombstones): the eight `Equal` rows and the `keyof` row red, four `TS2578` unused `@ts-expect-error` (the index signature now accepts the key), the strict-face by-reference rows red, and the mirror-parity ratchet red on `FormFieldSchema`. - ABLATION B1 (delete `sort_field` alone): its `Equal` row, the `keyof` row, its `TS2578`, the strict-face rows and the parity ratchet red, naming `sort_field`. - Every leg restored with blob equal to HEAD; the green `tsc -p tsconfig.test.json` runs are in the gates below. 5. **Docs face**: respelling one key of `grid.mdx`'s snippet snake_case turns `pnpm check:doc-snippets` red at that line (`TS2322`), restored green. Repo-wide pin sweep: every assertion that read a grid key by its snake_case spelling now asserts the camelCase key's substance (the master-detail grid object's exact key set and values, the strict face's accept and wrong-type rows, the coverage key set, the widget draws). The rejection rows for genuinely wrong values (a string `minRows`, a numeric `addLabel`, …) stay, respelled; the guarded surface grows by the eight refusals. ## Files and landing point The claim's surface, plus two adjacent files the census named: `apps/console` `DevLookup.tsx` (a producer) and its registry pin's prose; `packages/i18n` en locale (one comment naming `add_label`); the RFC. The landing point is the claim's list; no producer was found in another package. ## ADR and RFC - `docs/adr/0001-master-detail-subform.md` (governed, ⛔ not edited) names the keys in three sentences: "Honors `min_rows` / `max_rows` / `allow_add` / `allow_delete` from the …", "Drag reorder of lines (`allow_reorder`), grouping/subtotal rows." and "… **drag-to-reorder** (a `sort_field` …". Reported for whoever owns that record. - `content/docs/rfcs/0001-clipboard-paste.md` (status Draft) stated `max_rows` "from GridFieldMetadata" and wired `field.allow_add` / `field.max_rows` as the current API, so it is edited to the camelCase keys. ## Serial and overlap The claim's serial step is done. objectui#11605's PR objectstack-ai#11612 (`fd060f07`) and objectui#8347's PR objectstack-ai#11607 (`b403bb36`) both landed. `main` at `b403bb36` is merged into this branch by merge commit `8b56ef26`. Its combined diff is empty, so no hunk was hand-resolved. The four files this branch shares with them carry `main`'s hunks outside this branch's hunks: `packages/i18n/src/locales/en.ts`, `packages/types/src/form.ts`, `packages/types/src/index.ts` and `zod-mirror-parity.test.ts`. The contract review on the merged head (`5981193754`) reads every hunk of this branch as byte-identical to the reviewed head. The gate verdict on the merged head is its own check-runs. ## Gates (the dev's local readings, tree `82b400f9`; the current head's verdict is its check-runs) - type-check, package and test program: `@object-ui/types`, `@object-ui/fields`, `@object-ui/plugin-form`, `@object-ui/i18n`, `@object-ui/example-schema-catalog`, `@object-ui/console` — all exit 0 (the console run after building `@object-ui/plugin-tree`, which its import graph needs). The fields and plugin-form test programs list the touched test files (`--listFilesOnly`). - vitest, repo-root form: `packages/types/` 353 files / 9482 tests passed; `packages/fields/` 233 passed + 1 skipped / 3647 passed + 7 skipped; `packages/plugin-form/` 163 / 1889 passed + 1 skipped; `examples/schema-catalog/` + the console registry pin + `packages/i18n/` 121 / 3807 passed + 13 skipped. The `types` / `fields` / `plugin-form` runs measured a working tree byte-identical to `82b400f9` (its two later-committed files, the changeset and the catalog test, were already on disk); the rest ran at `82b400f9`. - lint (`eslint` with the root config, as each package's `lint` runs it) over the 25 touched TS/TSX files, counted from `--format json`: 0 errors; the one finding on a touched line is the pre-existing `as any` warning in `GridField.test.tsx`'s fixture. Narrowing statement: the population is every file under the touched packages, read by the one root `eslint.config.js`; it enables no type-aware linting and no custom rule reads the filesystem, so this diff cannot move a verdict on an untouched file. `pnpm lint` itself is CI's. - `check:doc-snippets` (777 blocks judged, 0 failed), `check:doc-examples`, `check:readme-exports`, `check:new-line-citations` (0 new), `check:control-bytes`, `check:spec-symbols`, `check:designer-field-key-parity`, `check:prompt-keys`, `check:handler-key-reads`, `check:unreferenced-sources`, `check:phantom-deps`, `check:self-import`, `check:test-path-roots`, `check:doc-types`, `check:doc-example-ids`, `check:doc-fences`, `check:doc-example-readers`, `check:i18n-drift`, `check:i18n-keys`, `check:esm-specifiers`, `check:side-effects-array`: all exit 0. - changeset checkers: presence, no-major, fixed, overwrite, claims, pending-literals: all exit 0. `.changeset/11610-grid-keys-camelcase.md`: `@object-ui/types` and `@object-ui/fields` `minor`, breaking banner, FROM → TO for all eight, `Clause-②: yes (narrowing)`. - Governed surface: `node scripts/check-governed-queue-guard.mjs --test` over the diff answers NOT GOVERNED. ## Acceptance notes -⚠️ Same name, different meaning: the grid's `totalField` names the CHILD column summed (the value a spec `amountField` carries), while the spec's own `totalField` on a master-detail subform or `record:line_items` names the PARENT field the sum is saved to. The mechanical camelCase of `total_field` lands on that homonym. Every declaration, the doc page and the changeset say so; the open question in the report asks the seat whether the spec's runtime form field should carry it under this name. - When the spec seat adds the camelCase keys to the runtime form field (objectstack-ai/objectstack#21704), the three objectstack comments that still name the snake_case keys can move with it. Carrier: that stage. - `DevLookup.tsx` keeps its `as any` field (a dev harness; only the key moved). The dispatching session is `https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ`. --- _Generated by [Claude Code](https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ)_ --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Leehom <pm@objectstack.ai>
Fixes #8347
Clause-②: yes (narrowing)
BaseSchemano longer declares[key: string]: any. A node literal annotated with its node type now refuses a key that no declaration names, a misspelled key included, and the correct spelling still compiles. This executes the objectui#7927 ruling ("the TypeScript face is an executable contract"), with the compile-fail pin that ruling made non-negotiable.ComponentRendererPropskeeps its own signature, and a row in the pin notices if it is ever removed. The zod faces and every runtime path are unchanged, exceptvisibleWhen(below).Governed: this diff touches
AGENTS.mdand two published-skill files (skills/objectui/guides/architecture.md,skills/objectui/rules/protocol.md), so the PR stays a draft until an authorized approval is on it (node scripts/check-governed-queue-guard.mjs --testanswers GOVERNED for each of them). Session:https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ(PM loop round 5, claim5977679107, amended for patch round 3 by5979679543).The maintainer's one open choice: the
propsalias on the TypeScript faceThis PR writes option A:
propsis not declared on the TypeScript authoring face. The renderer still reads it at runtime (the maintainer's 2026-08-18 ruling binds the runtime and leaves the TS face open). So the twobadgeliterals incomponents'alias-precedence-cross-channel.test.tsxthat authorpropsnow carry an@ts-expect-errornaming this choice, andAGENTS.md§4's abridged listing drops thepropsmember thatBaseSchemanever declared.props, and the published skill teaches it as wrong.props?: unknownonBaseSchema.AuthoringNodederives from the zodBaseSchemashape, so B also needs a zod mirror row and a parity entry.AuthoringNodeonly.To flip it, remove the two directives and add the member. Nothing else in this PR depends on the choice.
What changed, class by class (one commit each)
plugin-kanban:ObjectKanbanreads the gate-writtensort(the binding'sdataSource.sort) throughGateBoundKanbanSchema, a read type private to the file;ObjectKanbanSchemastill declares nosort(objectui#8174).plugin-timeline:TimelineRenderSchema, the private hand-off type, declares theonItemClickslotObjectTimelinecomposes;TimelineSchemastill declares none. Both compile with the signature present or absent.unknown; the gantt and mappropertiesBag-10859fixtures keepoperatora literal and type the bag node as its block node.BaseSchema.visibleWhen, Q6 = B. The TS face is the spec'sEvaluatedExpressionInput, by reference. The zod twin is the same accept set (a string, or the spec's ownEvaluatedExpressionSchemaenvelope) withEvaluatedExpressionInputSchema's verdict and without its string transform.propsflips; the valueless envelopemetric-cardpinned refused on the TS face (Q3 = A);envelopeStrayand itscomplex.tsnote deleted per the constant's own removal condition; the twoobject-view-slot-key-liststripwires flipped;partial-schema-collapse-pin.test.tsdeleted per its header, withPartialSchema's docblock describing the alias as it now works; five mirror-parity ledger entries (below); and the prose in those files that described the signature as live.objectos-integration.mdxpage block (nowregions, and since round 4 rendered underSchemaRendererProvider); the published passages that described the signature as live; the plugin-calendar README note; the plugin-dashboard README widget-key passage; theexamples/dashboard.tscomment; a dated note on the pending11564changeset, whose "until objectui#8347 removes it" sentence this release makes false.AGENTS.md. §5 #0.1 gains the rule as shipped: the TS authoring face is part of the contract, a refused key is declared by reference or fixed at the producer and never cast past, and the fresh-literal bound is named with the pin that measures it. §4 dropsprops; Add default props to all components to prevent collapse in designer #4'seventssentence names the face that still keeps one.minorfor@object-ui/typeswith the breaking note;minorforplugin-timeline(TimelineRenderSchema, theschemaprop type of the exportedTimelineRenderer, gains one optional member);patchforplugin-kanban.mainatb508ac50. The two published-skill sections (H3 below); the five restored deletion guards and the reworded test strings (Acceptance notes);mainmerged by a merge commit, with the type-check of every packagemainmoved since the merge base green afterwards.SchemaRendererProvider dataSource(the renderer reads it fromSchemaRendererContext; adataSourceprop onSchemaRendererreaches only the root component), and its system-objects comment names only the provider. The parity ledger'scomplex.zod.ts#DashboardWidgetSchemacomponententry says the mirror slot'sBaseSchemafallback stays by objectui#8344's decision, with no open card retiring it, instead of reserving it for this card.The compile-fail pin, both directions
packages/types/src/__tests__/base-schema-closed-face-8347.test.ts, read bytsc -p tsconfig.test.json:titleFielddon aCalendarViewSchemaliteral is an@ts-expect-error, andtitleFieldcompiles. The same onBaseSchemaitself (classNmaeagainstclassName), on a node nested in a slot (labellon abuttonchild), and onPartialSchema.BaseSchemacloses 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 ran it, on today's plugin-calendar README block, stays green with or without the signature (measured on this card's census): that block is a type alias plus two assignments of declared variables, and tsc never excess-checks those. So the pin uses fresh literals, and the bound itself is pinned as a directive-free row, so nobody reads the file as claiming more.ablation-replace.mjs(anchor hit 1 to 0, on-disk blob moved, trap-restored to blob equal to HEAD,git diff HEADempty): with[key: string]: anyput back onBaseSchema, the types test program exits 2 with 52 errors, and every refusal row of the pin turns red (TS2578 on the four misspelling rows, TS2344 on the two no-index rows). At HEAD the same program exits 0.The measurements the dispatch named
H1, the re-census at BASE
2e818d0b5, before any kept edit. Instrument: every package built (turbo, forced: 40 of 40, 0 cached); the sixBaseSchemalines deleted and nothing else; types rebuilt (dist keeps one signature,ComponentRendererProps'); then every tsc program the workspace type-check scripts name, one job each (90 jobs, apps/site's generators included, plus the four root programs).e0a9af67, removal keptThe 81 fall exactly into the predicted classes: src 3 (
ObjectKanbansorttwice, timelineonItemClick); tests 67 adjudicated probe and ceiling flips (types 49; chartsxAxisKytwice andobjectNme; dashboardbogusKeytwice andzzUndeclared7353four times; gridbogusKeyRefusedNowhereand twoExpectwrappers; listvisibleWhntwice; mapenableClusterng; treezzplTreeAbsentControl8655,objectNaem; componentscontentonlabel), 2propsflips, 3 mechanical rows (two schema-to-record casts, one mirror-parity ledger mismatch), and the 6 gantt and map fixture literals. No site needed a contract decision.check:doc-snippetsunder the removal at BASE: 14 of 777 blocks failed, the 13 listed plus theobjectos-integration.mdxpage block (BASE control: 0 failed); doc-examples, doc-types and skill-examples stayed green.The parity ledger. The mismatch was five pairs whose member is typed with an
interface(AppSchemaRendererNodeSchema.schema; thereportspairs'report,sections,chart) against a.passthrough()mirror whosez.inputcarries a string index signature. TypeScript grants an implicit index signature to anonymous and mapped types and never to an interface. Measured: the declared type is refused, and a one-level anonymous copy of it fits the same mirror input. They are ledgered as an artifact of the assignability test, with that reading in each entry's docblock, and the header's derived figures moved with them.H2, the objectui#11573 headroom pin.
AnyComponentSchema's declaration on the compiler's counter: 12260 before, 12974 after (thevisibleWhentwin), limit 900000. Read through a trap-deleted copy of the pin that writes its reading.H3, skill fences.
check:skill-examplesexits 0 under the removal at BASE and on every head since. Patch round 3 edits the twoskills/**files whose prose the removal made false: the architecture guide's "JSON Protocol" section and the protocol rule's "Component Schema Structure" section said the publishedBaseSchemadeclares its members plus an index signature, and thatprops/events/propertiesreach a node through it. Both now state the two verdicts a key no member declares gets, one per face (a compile error in a fresh typed literal on the TypeScript face, with the fresh-literal bound said as AGENTS.md §5 #0.1 says it; kept and judged by the named reader on the tolerant zod face), and their teaching fences drop the three keysBaseSchemanever declared and end on the same no-index-signature line as AGENTS.md §4.propskeeps one sentence stating option A, so a flip to B or C is a one-sentence edit. Neither fence is a marked example;check:skill-examples,check:skill-eval-tokensandcheck:skills-pathsstay green.H4. The flex rows' header and
examples/dashboard.tsno longer say "until objectui#8347"; the flex test gains the row that removal promised (atextentry misspelled inside the list is refused, and the well-formed list'stextentry compiles).H5,⚠️ This is a narrowing as well as a widening on the tolerant face: a blank
visibleWhen.visible-when-spec-input-8347.test.ts: the TS member isEqualto the spec'sEvaluatedExpressionInput; on 13 probe values the mirror's verdict equalsEvaluatedExpressionInputSchema's andPageComponentSchema.visibleWhen's, read in the same run (the probe set holds both verdicts); a blank string is refused with the spec's own sentence; a string parses to the same string, while the spec rewrites it (the control); an envelope parses to itself; and the envelope a spec parse writes is accepted by both component faces, a dialect-less one refused.visibleWhenstring used to parse and is refused now, as the spec refuses it.Gates, on head
e0a9af67The dev's local readings on that head. The gate verdict on the current head is its own check-runs.
pnpm --filter @object-ui/types build: exit 0.pnpm --filter @object-ui/types test: exit 0,Test Files 353 passed (353),Tests 9419 passed (9419).Test Files 819 passed | 1 skipped (820).components: see the report.check:doc-snippets777 of 777 block(s) judged, 0 failed.;check:doc-examples35 compile, 87 fail, 87 of those declared;check:doc-types,check:skill-examples,check:readme-exports,check:spec-symbols,check:changeset-claims,check:pending-changeset-literals,check:control-bytes,changeset:check,type-check:coverage: exit 0.check:new-line-citations:0 new citation(s).check-governed-queue-guard.mjs --testover the changed paths: GOVERNED (AGENTS.md; since patch round 3 also the twoskills/**files).Overlap
PR #11069 (draft) edits
content/docs/guide/architecture.md's paragraph on a button's text key. This PR edits the same file's Type Safety block (the button literal writeslabel). Region-disjoint; whichever lands second mergesmain.Acceptance notes (read, not filed)
ObjectChartBlock's pre-gateschemaprop isBaseSchema(ruling5810232641). A fresh literal at that prop is now excess-checked againstBaseSchemaitself, so a correctly spelledobjectNameis refused there as much as theobjectNmeprobe; in-tree callers pass typed nodes. The flipped probe's comment says so.RemoveIndexSignatureinObjectGrid.tsxstrips nothing now, as its own test predicted; retiring it is cleanup left out of this PR.object-view-slot-key-lists'formPicklist may move back toOmit, per its header's removal condition; not done here.PartialSchemahas no consumer outside its declaration (a tracked-filesgit grepfor the name, withSchemaByTypeas the positive control). The enforce-or-remove card the deleted pin's header asks for is filed: objectui#11608 (Blocked-by: #8347).git grep -n -i "index signature",git grep -n -F '[key: string]: any'andgit grep -n 'objectui#8347 remove'overpackages/,apps/,examples/,content/docs/andskills/, CHANGELOGs and release notes excluded as append-only. What they still find is true at the head: other types' own signatures (ComponentRendererProps,AuthUser,FormField, a widget's own props type, the field-widgetdata-attribute signature,QueryParams' statements), the tolerant zod face's.passthrough(), and past-tense history.@ts-expect-errorover a tombstone or a typed member also failed (TS2578) when its member was deleted; since the removal the undeclared key satisfies the directive.object-kanban-card-title-9606,object-grid-default-sort-tombstone-5861,object-grid-show-filters-retired-11068,object-grid-declared-keys-11068andgantt-flat-config-declared-keyseach gain a type-level row that is a compile error when its member is deleted and pins the declared or tombstone type; each was shown red by a trap-restored deletion of its member and green at HEAD (readings on the card). Three test titles and one assertion message that described the signature as live are reworded, assertion logic untouched.维护者速读(草稿)
改了什么:
@object-ui/types的BaseSchema去掉了[key: string]: any。从此用 TypeScript 写节点时,拼错或未声明的键会直接编译报错,正确拼写照常通过;新增的钉子测试两个方向都钉住,并用"把签名加回去、钉子变红"的消融证明它真能失败。顺带:visibleWhen改为直接引用 spec 的输入类型(字符串或{ dialect, source }信封);看板与时间轴两处运行期键改为在各自包内私有类型上声明;一批文档示例与说明改成在新约束下成立;AGENTS.md §5 #0.1 写明"TS 编写面也是契约"。已发布技能包(skills/objectui)里两处讲BaseSchema形状的教学文案同步改写——它们原先说该类型带索引签名、props/events由签名承接,去掉签名后已不成立;因此本 PR 的受管面除 AGENTS.md 外还含skills/**,同一份批准覆盖两者。为什么改:这是 objectui#7927 的裁决(TS 面是可执行契约)的落地,也是本卡的全部范围。此前签名让任何拼写都能编译,AI 批量写错元数据时被静默放过。
风险与代价(含回滚):对 TS 使用者是破坏性收窄(按仓规以
minor发布并在 changeset 写明);运行期与 zod 宽容面不变,唯一例外是visibleWhen现在拒绝空字符串、接受 spec 的信封。已知边界:TS 只对"新鲜对象字面量"做多余键检查,经变量转手的值不会再查,已在 AGENTS.md 与钉子里写明。回滚:还原本 PR 即可,无数据迁移。席位意见:
你要做的:1)裁决
props别名在 TS 面上的去留:本 PR 按方案 A(不声明)写,B 是在BaseSchema上声明props?: unknown,C 是只在AuthoringNode上声明;改选只需删两条@ts-expect-error并加成员。2)批准后由认领席落地(受管面,草稿态等你的批准)。Generated by Claude Code