Repository navigation
feat(spec): object-map, object-gantt and object-tree get ComponentPropsMap rows, derived from the renderers' read points (objectui#8348 Q1-C) - #18403
Conversation
…psMap rows, derived from the renderers' read points WIP: the three rows plus their flat-config guidance sets. Key sets measured from objectui's plugin-map / plugin-gantt / plugin-tree at the .objectui-sha pin 53ded82b, per the #7751 derivation method. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
…rescriptions and door lists Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
… docs and strictness ledger; add the changeset Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d20154227410f6a7b1d37f07ae61e0cce799fa02 && git checkout d20154227410f6a7b1d37f07ae61e0cce799fa02
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b6fe311a9ab7bebdfb22631030ac19ebeb87c6a5 c431c22e058f0f74be179cd2388911634d64c67f && git checkout -B drift-repro b6fe311a9ab7bebdfb22631030ac19ebeb87c6a5 && git merge --no-ff c431c22e058f0f74be179cd2388911634d64c67f
node scripts/docs-audit/affected-docs.mjs --json b6fe311a9ab7bebdfb22631030ac19ebeb87c6a5
|
Contract reviewServed-tier: ① Derived judgmentsThe method was tested against the renderers themselves at the
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
…tree states the real reason its objectName is optional; two citations corrected Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
…describes Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
Contract review126/126 at-tier stamps (control: an ① Derived judgmentsMethod: every read point re-read from objectui at the pin Accept-set and public-surface changes, one by one:
FALSE claims — text only; no key, value shape, row membership or level moves:
② Semver levelChangeset ③ Boundary flags
Implemented-by: VERDICT: FAIL Generated by Claude Code |
…ree describes The `objectName` / `data` / `staticData` describes on `object-gantt` and `object-tree` named `getDataConfig` as the reader that resolves the record source. Measured at the `.objectui-sha` pin `53ded82b`: `ObjectGantt.tsx` and `ObjectTree.tsx` contain zero occurrences of that name (control: `ObjectMap.tsx` has 8 — it keeps a local wrapper at :135, so the map describes stay as they are). Both renderers reach the shared three-rung ladder `resolveRecordSourceConfig` (`@object-ui/core` `record-source.ts` :151 / :155 / :162) — gantt at :593, tree at :359. The ladder ORDER those sentences describe was already true; only the function they named was wrong, and all six ship into the public reference. Also: the tree `data` JSDoc cited two read points where the schema header already rules that `:496` is not a second site for the object arm, and two source citations were off by a line (`safeParse` is at :500, not :501; the "Not authorable" marker is at :482, not :477). Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
…rce describes Six rows follow the corrected describes: `object-gantt` and `object-tree` now name `resolveRecordSourceConfig`. The three `object-map` rows keep `getDataConfig` because that renderer really does keep a local wrapper of that name above the shared ladder. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
Contract review119/119 at-tier stamps (control: an ① Derived judgmentsMethod: every read point re-read from objectui at the pin Accept-set and public-surface changes, one by one:
Every sentence asserting runtime behaviour, re-read at the pin — all 32 describes on the three new schemas, on BOTH sides (32 of 32 byte-exact in
Citations: every Non-blocking imprecisions, source or docs text, none a false attribution and none moving a key, value shape, row or level — the seat may fold them into the next round that touches these lines, with
② Semver levelChangeset ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Provenance — at-tier PASS at the current head, carriers cleared, pre-check clear
Landing pre-check, all three, ⛔ every one re-read AFTER the carrier strip: ① the PASS above names this head. ②
|
… key its five item-titled siblings already carry (objectstack-ai#18561) Fixes objectstack-ai#16894 Clause-②: yes (widening) `KanbanConfigSchema` now declares `titleField` as optional `z.string()`, executing the director seat's decision batch objectstack-ai#87 (recorded at objectstack-ai/objectui#8367 comment `5582071618`, confirmed by the maintainer verbatim 「批 objectstack-ai#87 同意」). The direction was settled there; this PR is the implementation only. ## What moved | file | change | |---|---| | `packages/spec/src/ui/view.zod.ts` | `titleField: z.string().optional()` on `KanbanConfigSchema`, plus the docblock that records why it is optional | | `packages/spec/src/ui/view.test.ts` | the card's four-leg probe (both controls firing on the same call shape) and the optionality leg | | `packages/spec/authorable-surface/ui.json` | regenerated — one added row, `ui/KanbanConfig:titleField` | | `content/docs/references/ui/view.mdx`, `content/docs/references/api/protocol.mdx`, `content/docs/references/data/object.mdx` | regenerated by `gen:docs` | | `.changeset/16894-kanban-config-titlefield.md` | `@objectstack/spec: minor` | Regenerated, never hand-edited: `check:authorable-surface` wrote the first and `pnpm --filter @objectstack/spec gen:docs` the rest. `check:generated` reports all 15 artifacts up to date. ## Premise re-derivation — the card's sibling table does NOT match the tree Re-derived on this worktree at `origin/main` `79a046f8c` (the dispatch's own derivation ran one commit behind, at `582d3e5`). `git grep -n 'titleField' -- packages/spec/src/ui/view.zod.ts` returns six carriers; mapping each to the schema whose member list encloses it: | line (pre-change) | owning schema | arity | |---|---|---| | `:1057` | `GalleryConfigSchema` (declared `:1050`) | optional | | `:1071` | `TimelineConfigSchema` (declared `:1065`) | **required** | | `:1407` | `CalendarConfigSchema` (declared `:1401`) | optional — the ADR-0079 fallback docblock the ruling names | | `:1490` | `GanttConfigSchema` (declared `:1484`) | required | | `:1638` | `ListMapConfigSchema` (declared `:1631`) | optional | | — | `KanbanConfigSchema` (declared `:1346`) | **absent — the defect** | Two corrections to the card body, neither of which disturbs the ruling: - The card's table calls **Timeline optional**. It is **required** on the tree. - The card's table lists **five** schemas. There are **six** carriers: `ListMapConfigSchema` also declares `titleField`, optional, and the card does not mention it. The ruling is unaffected: it prescribes the arity directly (`optional z.string()`) and names `CalendarConfigSchema`'s docblock as the reference, which is optional at `:1407`. So the shape landed here is the ruled one, and the card's optional/required split was simply not re-measured when it was written. The defect itself re-derives exactly as filed: `KanbanConfigSchema` is a `strictObject` and refused `titleField` by name. ## Evidence | leg | command | verdict | |---|---|---| | build | `pnpm --filter @objectstack/spec build` | `VERDICT command-exit 0` | | ② typecheck | `pnpm --filter @objectstack/spec typecheck` | `TYPECHECK_EXIT=0` — `check:test-typecheck: OK` | | ② tests | `pnpm --filter @objectstack/spec test` | `TEST_EXIT=0` — `Test Files 483 passed (483)` / `Tests 13775 passed (13775)` | | targeted | `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/ui/view.test.ts` | `Test Files 1 passed (1)` / `Tests 376 passed (376)` | | artifacts | `pnpm --filter @objectstack/spec check:generated` | 15 of 15 up to date | ① is empty by construction: `packages/spec` has no workspace dependencies, so `--filter '@objectstack/spec^...' build` has an empty closure. The package itself was built before any gate that reads `dist/`. ③ Gate families were derived on this worktree, not inherited: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` over the real 7-path change set yields **108 families**. All 108 were run with each exit code landed to disk before being read, and reconciled with `--ran`: ``` dispatch-gates --ran: 108 derived family(ies) accounted for — 105 run, 3 NOT-MEASURED (3 DERIVED from a recorded exit 3) ``` **103 green.** The five non-zero results, none of them a finding against this diff: - `pnpm check:dual-build-cjs-loads`, `pnpm check:lean-entry-closure`, `pnpm check:type-check-debt` — **exit 3, PREREQUISITE NOT MET**, all three refusing because the whole monorepo is not built on this worktree. NOT MEASURED, declared to CI, which checks out and builds fresh. - `node scripts/check-plugin-teardown-shape.mjs --self-test` — **exit 1, but a prerequisite refusal in its own words**: "cannot read the positive control at `621a487607881c66b2899b7e3477115229a156b4` … Deepen the clone". This container's checkout is shallow. NOT MEASURED. The gate itself (without `--self-test`) ran **green**. - `pnpm check:cross-package-test-inputs` — **exit 1, pre-existing on the base tree**, proven by control rather than asserted. With all seven changed paths restored to `79a046f8c` (and the changeset removed), the gate fails identically; the finding it prints names `packages/cli/test/init-created-files-summary.e2e.test.ts` descending `packages/spec/dist/`, and this diff touches neither that test, nor `turbo.json`, nor any declaration table. Restore was proven by `git diff HEAD` empty plus a `git hash-object` match against every HEAD blob. **Lint, as a proven narrowing rather than a repo-wide sweep.** `pnpm exec eslint --no-inline-config --format json` over the two changed TypeScript files: **exit 0, 2 files, 0 errors, 0 warnings**, the file count read from the JSON output's own length. The population it narrows from is **6798** files — computed from ESLint's own resolved config via `ESLint#isPathIgnored` over `git ls-files`, not estimated. The narrowing excludes nothing, and that is a property of this repository's config rather than a hope: `eslint.config.mjs` states it in its own comment — this repo "runs one `eslint.config.mjs`, which never enables type-aware linting (no `parserOptions.project`, no typed `@typescript-eslint` rules) for ANY file, test or not." With no rule reading types across a file boundary, a diff confined to these two files cannot move the verdict on any of the other 6796. The repo-wide `pnpm lint` run remains CI's. Every reading above was taken at `6d01b4b`, this branch's final commit, on a tree whose `git status --porcelain` is empty. `origin/main` had not moved from the branch point (`79a046f8c`) when the PR was opened, so no merge was owed. ## Acceptance notes - **`packages/lint` does not field-check the key this PR declares.** `POSITIONS.kanban` in `packages/lint/src/validate-list-view-field-refs.ts` lists `groupByField` (error), `summarizeField` (warning) and `columns` (warning) — and no `titleField`. Every sibling face lists it: `calendar.titleField` warns, `gantt.titleField` and `timeline.titleField` error. So from this release a stale or misspelled `kanban.titleField` names nothing and is reported by nothing, while the identical typo one block away is caught. Read-only on this card by dispatch, so it is reported and not edited; nothing in this PR turns it red, and no gate reconciles that table against the spec's member lists. Worth its own card. - **The node-level half (objectstack-ai/objectui#7742) has no home here, and the card's conditional resolves to "no".** `ObjectKanbanPropsSchema` in `packages/spec/src/ui/component.zod.ts` **already declares** `titleField: z.string().optional()`, described as "Legacy fallback for `cardTitle` (the board reads `cardTitle || titleField`). Prefer `cardTitle`". That is a different semantic on a different schema in a different file — a deprecated alias for `cardTitle`, not a view-config field binding — so one declaration cannot serve both faces and this card is not widened. That file is also held by objectstack-ai#18305 / PR objectstack-ai#18403; it was read, never touched. - Noted, not filed: `KanbanConfigSchema` carries no object-level `.describe()`, where `GalleryConfigSchema`, `TimelineConfigSchema`, `CalendarConfigSchema` and `ListMapConfigSchema` all do, so its nested-shape heading in the generated reference renders with no description sentence. Cosmetic, generated-docs only, no accept set involved — left for whichever card next edits this schema. - The issue body was checked for sanitizer truncation: none found. It is 6732 characters through the REST read and terminates on its own `Refs:` line. ## File surface Two files beyond the dispatch's declared surface, both **generated by the mandated `gen:docs` run** and neither hand-edited: `content/docs/references/api/protocol.mdx` and `content/docs/references/data/object.mdx`. The kanban config shape is inlined on those two reference pages as well as on `ui/view.mdx`, so each picks up `titleField?: string` in its `kanban` row. Omitting them would leave `check:docs` red. Every hunk in all three files is this one key and nothing else. --- _Generated by [Claude Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…t-map.map` back at it (objectstack-ai#18904) Fixes objectstack-ai#18406 Clause-②: yes (widening) — the published accept set grows by exactly one key, `ui/ListMapConfig:style`. Changeset: `@objectstack/spec` minor. Director decision batch objectstack-ai#153 item 4, letter 1 (ruling comment 5724940537 on objectstack-ai#18406, maintainer 「其他同意」). ⛔ Not `mapStyle` (letter 2 declined: "a second spelling the renderer would have to learn"). ⛔ No alias (letter 3 declined: "an alias is a permanent obligation for a key nobody has written yet"). ## The defect, measured `ListMapConfigSchema` is a `strictObject` and `style` was the one member of the map renderer's own documented config surface it omitted, so `ListMapConfigSchema.safeParse({ style: 'https://tiles.example/style.json' })` answered `success: false` and a map style could not be declared through the spec's list-view face at all. Measured at the `.objectui-sha` pin `53ded82bf7a494f54e344e19099dbf00854b8694`: | Reading | Where | | :--- | :--- | | `ObjectMapConfigSchema` declares eight keys, `style` among them | `packages/types/src/zod/objectql.zod.ts:562`, the `style` row at `:570` | | `getMapConfig` reads `schema.mapStyle || schema.map?.style` | `packages/plugin-map/src/ObjectMap.tsx:365` | | the authored block is validated against that schema | `packages/plugin-map/src/ObjectMap.tsx:373` | | objectui's own docs publish `style` inside the block | `content/docs/plugins/plugin-map.mdx:131` (that path is objectui's, and the spec docblock's parenthetical is scoped to objectui — verified present at the pin, so the citation is sound) | | `FLAT_MAP_CONFIG_KEYS` is that shape MINUS `style` | `packages/plugin-list/src/ListView.tsx:40-67` | So the divergence was against the documented surface the spec docblock itself cites, not merely against code.⚠️ Line numbers were re-derived from the tree. The card and the ruling both cite `view.zod.ts:1631` for `ListMapConfigSchema`; on this branch's base it is at **`:1754`**. ## The change, file by file **`packages/spec/src/ui/view.zod.ts`** - `ListMapConfigSchema` (`:1754`) gains `style`, an optional string, after `center`. The describe names it the map style URL, records that the component-level `mapStyle` is read FIRST and wins when both are present, and says it is not the inline CSS `style` record a component node carries. - The docblock's strictness paragraph (`:1746-1752`) gains a purely ADDITIVE correction. `strictObject` stays and the rationale for closing the block is untouched — the ruling does not fault strictness. What is recorded is that the paragraph's parity claim was one key SHORT, which key it was, and the pin it was measured at. **`packages/spec/src/ui/component.zod.ts`** - `map` (`:3272`) stops being `z.unknown()` and becomes `ListMapConfigSchema.optional()`, spelled exactly as the sibling `gantt` door is. `ListMapConfigSchema` joins the `./view.zod` import on line 4. The registration `'object-map': ObjectMapPropsSchema` (`:3735`) is unchanged. - The "VALUE posture for `map`" paragraph is rewritten: it was the record of WHY the door had to stay open, and that reason is now discharged. - The flat-guidance docblock gains "minus that same `style`" plus the reason `style` is subtracted on both sides — flattened to the top level it collides with `BaseSchema.style`, the node's inline CSS record, which is why the renderer stopped reading a top-level `style` as a map style at all (objectui#5017) and why the prescription routes it to `mapStyle`. - `mapStyle` is untouched. It is the component-level spelling, read first, and is not a member of the config block. **`packages/spec/src/ui/component.test.ts` — PR objectstack-ai#18403's negative pins, inverted (`:3534-3540`)** - `ListMapConfigSchema.safeParse({ style: ... }).success` flips `false` → `true`. - The door's acceptance of a `map` block carrying `style` stays `true`, now through the spec's own schema rather than through an open value. - ⭐ A third assertion is added, because the two above would pass just as well against the `z.unknown()` they replaced: an authored `map` block carrying `styl` is refused, and the refusal is asserted to land AT `map` with `styl` echoed in the issue's `keys`. - The flat-key-set equality (`:3507-3509`) now derives by SUBTRACTION — the config block's shape minus `style` — rather than by hand-listing, so a newly declared config key still lands in that assertion. **`packages/spec/src/ui/view.test.ts`** - The documented-surface test carries eight keys instead of seven, and asserts the parse OUTPUT equals the input, so a declaration that silently dropped a member could not satisfy it. - A new pin takes the card's repro as its subject: `style` accepted alone, the node-level object form of `style` refused, and both `styl` and `mapStyle` still refused — `style` did not open the block. **Generated (regenerated, never hand-edited)** - `packages/spec/authorable-surface/ui.json` gains exactly one line, `ui/ListMapConfig:style`. Written by `pnpm --filter @objectstack/spec build`. - `content/docs/references/ui/view.mdx` and `content/docs/references/ui/component.mdx` via `gen:docs`. In the component reference the `object-map.map` row changes from `any` to the block's real shape and a nested-shape table for it appears — the retraction of `z.unknown()` made visible in the published reference. - `authorable-surface.base.json` is deliberately NOT touched; only `gen:authorable-surface-base` writes it. Of the 15 generated artefacts, `check:generated` proved exactly one stale (`content/docs/references/**`) and `--fix` regenerated only that one. After the `origin/main` merge and a full rebuild, all 15 report up to date with a clean tree. ## Reverse verification **Ablation (PLANT mode, `scripts/ablation-replace.mjs`).** The `style` row was renamed to `styleAblated18406`, so the key is no longer declared under the name the pins assert. On-disk evidence: anchor count 1 → 0, planted marker 0 → 1, blob `1c83516951e3` → `d256ada1f791`. Restore proved by blob equal to HEAD, `git diff HEAD` empty, and a WHOLE-TREE `git status --porcelain` empty. - **Source leg — RED, as predicted.** 4 of 782 tests fail, and they are exactly the four this PR authors or modifies; 778 pass. So the new pins are discriminating, not vacuously true. - **Build leg — an independent third witness.** `gen:schema`'s authorable-key ratchet also reds under the ablation and names `ui/ListMapConfig:style` as having "disappeared from the contract" while the committed baseline still records it. That is mechanical confirmation that the key really landed in the tracked contract. - **Dist leg — NOT MEASURED, and declared so.** Because the ablated build failed at `gen:schema`, no ablated `dist/` was ever produced. The temporary type probe consequently read the PRE-mutation artefact and exited 0 — precisely the false green `scripts/ablation-dist-preflight.mjs` exists to catch, and it caught it, reporting the marker ABSENT from `dist/`. That green is discarded, not recorded as a pass. **Cross-package type reverse verification**, supplied separately because the dist leg above could not supply it. A temporary probe inside `packages/lint` resolves `@objectstack/spec/ui` through the package `exports`, i.e. the BUILT declarations, so its verdict is a function of build state: - Positive leg, exit 0 — `style` is assignable on the config block, AND a `ts-expect-error` directive on an undeclared member inside `map` is USED, which is only true if the door really narrowed away from `unknown`. - Negative leg, exit 2 — both halves fail as required, and each would have COMPILED against the pre-change declarations. The compiler names `bogus` as not existing in the block type and prints that type inline with its `style` member, and it reports the stale expectation-of-refusal as an unused directive. A cached declaration file cannot produce either error. Both probe files were deleted; the tree is clean. ## Gates `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived **107** command families from the real change set (8 paths, including the generated artefacts and tests). All 107 were run, each exit code captured BEFORE any pipe and written to a file, and `--ran FILE` reconciles to **107 derived / 107 run / 0 NOT-MEASURED / 0 UNRUN** — a DERIVED zero, since every line carries its code and none of them is 3. Eight families first returned a non-zero that was NOT a finding and are green after their prerequisite was met: five reported `PREREQUISITE NOT MET` for build state (cleared by a full `pnpm build`), one needed the `MANIFEST` value CI supplies and was then run in CI's exact spelling, and one — `check-plugin-teardown-shape.mjs --self-test` — refused because its fixture commit is unreachable in a shallow clone, cleared by a targeted `git fetch origin 621a487...` rather than an unshallow. ⛔ Not read as clearance: 13 of the roster families run only the checker's own `--self-test`, 49 declare a tracked-file roster whose silence is a fact about a list, 11 declare a population too wide to place, and 6 CI jobs scheduled by these paths run steps no local command covers. Those are CI's. ## Lint and tests Both readings taken at `0af4dfc76`, the final commit. - **`pnpm lint`** = `eslint . --no-inline-config`, run repo-wide rather than narrowed: exit 0, **6853 files inspected** (counted from `--format json`), 0 errors, 0 warnings, 75s. For the record, the config never enables type-aware linting for any file — no `parserOptions.project` and no typed rules — which `eslint.config.mjs:326-336` states with its own positive control, so this diff could not move an untouched file's verdict either way. - **`pnpm --filter @objectstack/spec test`** — 488 files, 14128 tests, all passing. - **`pnpm --filter @objectstack/spec typecheck`** — green, test layer included. - **`check:generated`** — all 15 artefacts up to date. Verify-lock readings, for the lane's baseline: acquisitions waited 0s / 5s / 0s / 4m05s / 1m18s / 0s, and the full `pnpm build` held the lock 4m56s. ## Acceptance notes Observed, in scope of nothing here, and ⛔ not filed by this seat. 1. **The docblock clause "so an extra key here would be dropped there" is measurably false at the pin**, and it is the clause immediately beside the one this PR corrects. `ObjectMapConfigSchema` is a plain `z.object`, not strict, so a `safeParse` of a block carrying an extra key SUCCEEDS; `getMapConfig` uses that result only to decide whether to emit a `console.warn`; and `ObjectMap.tsx:378` returns a spread of the authored block, so the extra key is carried through rather than dropped. An undeclared key inside `map` is therefore neither flagged nor dropped by the renderer — the opposite of what the clause asserts. Left untouched deliberately: the ruling's facts reach the `style` key and objectui's read of it, and they do not reach this mechanism claim, so correcting it would widen the diff past what the ruling authorises. Passed to the seat for filing-class judgement rather than filed. 2. The retraction did **not** need to reach into PR objectstack-ai#18638's hunk regions. Its `view.zod.ts` hunks start at old lines 53 / 2241 / 2771 / 2978 and its `component.zod.ts` hunks at 6 / 738 / 1591 / 1601; this PR's regions are `view.zod.ts:1727-1766` and `component.zod.ts:4, 3163-3175, 3210-3218, 3272-3273`. The `origin/main` merge brought only `content/docs/deployment/validating-metadata.mdx` and `scripts/gen-sdui-manifest-node.mjs`, neither of which overlaps. ## 维护者速读(草稿) ### 改了什么 地图视图配置块 `ListMapConfigSchema` 新增一个可选键 `style` —— 地图底图样式的 URL。同时把组件契约里 `object-map` 的 `map` 属性重新指回这个块(上一个 PR 因为这个键缺失,被迫把它放成了"任意值")。就一个键,没有别名,没有第二种拼写。 ### 为什么改 渲染器一直在读这个键,规格却一直不声明它。后果是:作者在平台的权威声明面上**根本无法声明地图样式** —— 写 `style` 被严格校验拒掉,写 `mapStyle` 规格也不认。代价已经付过一次:为了绕开这个拒收,上一个 PR 把一个组件属性的值契约整个放开成了"任意值",于是那个位置的拼写错误不再有人拦。这次两件事一起收回。 键名取 `style` 而不是 `mapStyle`,因为 Mapbox 与 MapLibre 这个行业本来就叫 `style`,也是渲染器在配置块里实际读的名字。 ### 风险与代价(含回滚) - **接受集只扩大,不缩小**:原先能通过的元数据,现在全部照旧通过。 - **一处收紧**:`object-map` 的 `map` 值从"任意值"变回真正的块契约,所以块内的拼写错误现在会被明确拒收并指名。这正是本次要恢复的保护,但它确实是一个行为变化 —— 如果线上有依赖"任意值"往 `map` 里塞非契约键的写法,会在这里被拒。仓内扫描未发现这类写法。 - **不涉及 objectui 仓**:objectui 那边教人写 `mapStyle` 的开发警告,由另一张 `domain:ui` 卡单独修,本 PR 不碰。 - **回滚**:单次 revert 即可。没有数据迁移,没有退役键,没有 ADR-0087 转换,没有墓碑。 ### 席位意见 ### 你要做的 本 PR 是 `Clause-②: yes`(公开接受集扩大),按流程需要合约层评审后才可落地;评审席位与载体标签不由本席位挂摘。除评审外无需人工动作 —— 本地门禁已全绿,生成产物已按机制重生成。 --- _Generated by [Claude Code](https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh)_ Co-authored-by: Claude <noreply@anthropic.com>
… NavigationModeSchema modes (objectstack-ai#19675) Part of objectstack-ai#18459 (item N2) >⚠️ **Closing keyword corrected by the seat from `Fixes` to `Part of`.** Card objectstack-ai#18459 carries FOUR items. N1 is measured DEAD on main (PR objectstack-ai#18904 replaced the enumerating describe with a pointer at `ListViewSchema.map`, whose schema declares every key `.optional()`), and **N2 is what this PR lands**. ⛔ **N3 and N4 are NOT fixed** — both were re-measured at the live pin and deliberately left on the card, N3 with fresh numbers that supersede the card's own stale proposals. A `Fixes` would have closed the card on merge and destroyed both. The seat releases the card back to the queue on landing with a `Release:` naming what landed and what remains. Clause-②: no ## Summary The card carried **two shipped imprecisions**. Re-measured at the LIVE objectui pin `87af769e9a3ee28ace099fdd653d3ebd79fe82e2` before anything was changed, **one is dead and one survives** — and the surviving one is wider than the card scoped it. This PR fixes the surviving one. The falsification of the other is recorded below rather than acted on. ## Zone 2 — what the re-measurement found ### N1 (lat/lng pair marked required) — FALSIFIED, already fixed. No change here. At PR objectstack-ai#18403 the `object-map` `map` row read: `'Map field config, the author face: { latitudeField, longitudeField, locationField?, titleField?, descriptionField?, zoom?, center?, style? }. Taken WHOLE when present ...'` — the enumeration the card objected to. Commit `1aa5026e30` (PR objectstack-ai#18904, "declare `style` on `ListMapConfigSchema` and point `object-map.map` back at it") **deleted that enumeration**. On `main` today the row reads `'Map field config, the author face — the same block ListViewSchema.map declares ...'`, and the generated reference renders the TYPE from `ListMapConfigSchema` itself: `| **map** | { latitudeField?: string; longitudeField?: string; locationField?: string; titleField?: string; … } | optional | ...` Every key optional, no required pair claimed. The statement the card called false no longer exists in the source or in the shipped reference. Reproduce: `git log -S "Map field config, the author face: { latitudeField" -- packages/spec/src/ui/component.zod.ts` returns exactly two commits, `a3d4c59d69` (added, PR objectstack-ai#18403) and `1aa5026e30` (removed, PR objectstack-ai#18904). ### N2 (navigation list omits `new_window`) — CONFIRMED, and it is four rows, not three. At the live pin, `object-grid`, `object-map`, `object-gantt` and `object-tree` each hand `schema.navigation` straight into the shared `useNavigationOverlay` hook (`ObjectGrid.tsx:2321`, `ObjectMap.tsx:1041`, `ObjectGantt.tsx:1680`, `ObjectTree.tsx:891`). That hook binds its own mode union to this package's `NavigationModeSchema` — its docblock says so in as many words, *"the seven modes this hook switches on are exactly the seven the exported union publishes"*, held by a parity test on that side — and its click router carries a `new_window` branch that delegates to `onNavigate` and otherwise falls through to a `window.open`. `object-gantt` implements that action itself as well. The four `.describe()` strings listed six of the seven (no `new_window`), except `object-grid` — the row the other three were copied from — which listed five (no `popover` either). All four doors are `z.unknown()`, so that sentence is the entire published account of the shape, and the reference page renders the type beside it as `any`. ### N3 (two comment citations in `component.test.ts`) — NOT fixed here, and the card's own correction is stale. Both citations still exist (`component.test.ts:3649` `label (:1849)`, `:3750` `ObjectGantt.tsx:501`). But they are HISTORICAL citations anchored to the retired pin `53ded82b`, and the corrections the card proposes (`:1871`, `:500`) were measured at that same retired pin. At the live pin the true positions are **`:503`** for the `GanttConfigSchema.safeParse` and **`:2181`** for the label resolution — so applying the card's numbers would replace one unverifiable pair with another. `component.test.ts` is also outside this dispatch's declared file surface. Left for the card. ### N4 (historical pin-citation spelling) — unchanged, measured. `check:objectui-pin-citations --list` on this head: **48 historical / 29 asserting / 7 assertion** entries. **30 historical citations still name the retired `53ded82b`**, ten of them in `component.zod.ts`, including the three headers this card is about (`component.zod.ts:3561` map, `:3715` gantt, `:3906` tree). The gate permits the historical form by design, so nothing is violated and nothing reds — those anchors simply are not re-checked on a pin bump, which is exactly what the card recorded. N3's re-measurement above shows the drift is real and not hypothetical. Converting them is a re-measurement job, not a spelling change, and is left for the card. ## The change Four `.describe()` strings, plus the reference page regenerated from them with `pnpm --filter @objectstack/spec check:generated --fix` (never by hand): - `ObjectGridPropsSchema.navigation` - `ObjectMapPropsSchema.navigation` - `ObjectGanttPropsSchema.navigation` - `ObjectTreePropsSchema.navigation` Each now lists `page | drawer | modal | split | popover | new_window | none` and names `NavigationModeSchema` as the source of that list, so the next member added to the enum has a named edge into these rows instead of four independently drifting lists. **Why `object-grid` is in scope.** The card names it as the precedent the other three copied and says "fixing it means fixing the family, not just these three rows". It is the same defect class, in the same file, in the same gate family, adds no verification surface, and understates by two rather than one. Correcting three while leaving the source of the pattern intact would leave the family in the state that produced the defect. No accept set moves: the doors stay `z.unknown()`. ## Changeset — measured, not pattern-matched `patch` on `@objectstack/spec`. The changed bytes ARE published bytes: - `packages/spec/package.json` `files[]` contains `src/**/*.zod.ts`, so `src/ui/component.zod.ts` ships verbatim in the tarball. - The compiled strings ship in `dist/ui/index.js` and `dist/ui/index.mjs`. - `npm pack --dry-run --json` in `packages/spec` lists all three of those paths among its 2030 entries. `@objectstack/spec` is `private: false` at `17.4.0` — a released package. - Controls: a positive control (an untouched describe from the same rows) is found in the same dist files; a negative control (a string present nowhere) is found in none. So `skip-changeset` would have been wrong here, and no changeset would have been wrong too. ## Verification Head `55dd769453`. `origin/main` merged in at `f2101d0492` via `scripts/pm/os-regen-merge.sh` before the final measurements; all 15 spec generated artifacts re-checked clean after it. - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived **102 families** from the actual changed paths. All 102 run, exit codes captured to a file BEFORE any pipe, reconciled with `--ran`: **102 run, 0 NOT MEASURED, 0 UNRUN**. - The single non-zero is `pnpm --filter @objectstack/spec run check:react-declaration-parity` in its bare spelling, which exits 1 by design with no `MANIFEST`. Run as CI runs it — `MANIFEST="$PWD/sdui.manifest.json" ... --baseline react-declaration-parity.baseline.json --strict` — it exits **0**, with no new declaration divergence vs the accepted baseline. - `pnpm --filter @objectstack/spec test` — **512 files, 14953 passed, 1 todo**. - `pnpm --filter @objectstack/spec typecheck` — exit 0 (`tsc --noEmit`, `check:scripts-typecheck`, `check:test-typecheck`). - `pnpm lint` (`eslint . --no-inline-config`, the whole repo, not narrowed) — exit 0. - `pnpm --filter @objectstack/spec check:generated` — all 15 artifacts up to date. - Every heavy run went through `scripts/pm/os-verify-lock.sh`; verdicts read from its `VERDICT command-exit` line. ## Acceptance notes Observed while measuring, deliberately NOT changed here and not filed: 1. **These four describes account for `mode` only.** `useNavigationOverlay` also reads `size`, `openNewTab` and `preventNavigation`, and the TYPED sibling rows (`object-kanban`, `object-calendar`, the list-view entry) name all four members. On a `z.unknown()` door the describe is the whole account, so naming one member of four understates the block — a separate imprecision from the one this card carries. Carrier: the next PR touching these rows, or a decision to point these four doors at `NavigationConfigSchema` the way their typed siblings already do. 2. **A stale comment in the sibling repo.** `plugin-tree/src/ObjectTree.tsx` at the live pin states "measured on the installed spec: `navigation` is declared on exactly one `ComponentPropsMap` entry, `object-grid`". It is now declared on several. A comment in another repository with no contract behind it. Carrier: objectui. 3. N3 and N4 above remain open on the card, with fresh measurements recorded there. --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #18305
Executes the objectui#8348 ruling 「8348 以协议为准」 (decision batch #83, 2026-09-08) and batch #136 item 3 (Q1-C, maintainer 「同意」):
ComponentPropsMapgainsobject-map,object-ganttandobject-tree, each row's key set derived the #7751 way — from the objectui renderer's own read points, at the.objectui-shapin53ded82b.Not derived from
@object-ui/types' mirror. That mirror standing in as the authority for map and gantt is the defect this card closes, and forobject-treethe mirror is measurably wrong about four keys (below).The #7751 derivation table, extended by three rows
Every citation is a line in the objectui checkout at
53ded82b. The shared record-source ladder isresolveRecordSourceConfigincore/src/utils/record-source.ts— rung 1data(:151, returned VERBATIM as aViewData), rung 2staticData(:155, wrapped into{ provider: 'value' }), rung 3objectName(:162).object-map—plugin-map/src/ObjectMap.tsx, registry shellplugin-map/src/index.tsxobjectName:793(fetch dep),:890(navigation)z.string().optional()data:169array-shorthand head, thenresolveRecordSourceConfigat:174ViewDataSchemastaticDataz.array(z.unknown())filter:742—$filter: schema.filterz.array(ViewFilterRuleSchema)sort:743—$orderby: convertSortToQueryParams(schema.sort)z.array(SortItemSchema)map:370—getMapConfigbranch 1, the author face; the registration's declared{ name: 'map', type: 'object' }inputz.unknown()(see note)mapStyle:365—schema.mapStyle || schema.map?.stylez.string()navigation:889—useNavigationOverlayz.unknown()enableClustering:905—enableClustering ?? (schema.enableClustering || …)z.boolean()Measured and deliberately NOT declared: the flat
map-config spellingsgetMapConfigbranch 2 reads at:382-396(locationField,latitudeField,longitudeField,titleField,descriptionField,zoom,center) — the ObjectView/ListView flatten product, ruled an internal transport form, not a second authoring surface (maintainer ruling objectui#5018, 2026-08-17);style, which:283reads ONLY to say it is not consumed as a map style (objectui#5017) and which is the node's own inline CSS record; and the React props ofObjectMapProps(clusterRadius, thedataARRAY prop, the four callbacks,className,dataSource).object-gantt—plugin-gantt/src/ObjectGantt.tsx, registry shellplugin-gantt/src/index.tsxobjectName:1359(layout key),:1491(navigation),:1873(export name)z.string().optional()dataresolveRecordSourceConfigat:593ViewDataSchemastaticDataz.array(z.unknown())filter:738—$filter: schema.filterz.array(ViewFilterRuleSchema)sort:739—$orderby: convertSortToQueryParams(schema.sort)z.array(SortItemSchema)gantt:499-501—getGanttConfigbranch 1; the registration's{ name: 'gantt', type: 'object' }input, and:501validates it against this repo's OWNGanttConfigSchemaGanttConfigSchemanavigation:1487—schema.navigation ?? { mode: 'drawer' }z.unknown()label:1871—resolveInlineI18nLabel(schema.label, displayLocale)in the export-file-name chain (:1849is the comment ABOVE that chain, not a read)I18nLabelSchemaskipWeekends:1205—!!swinto the working calendarz.boolean()holidays:1206—new Set(hol)for the duration mathz.array(z.string())persistLayout:1357—schema.persistLayout === falsez.boolean()viewName:1359— theobjectName:viewNamestorage keyz.string()markers:1826—markers={schema.markers}z.array(z.unknown())criticalPath:1829—criticalPathDefault={!!schema.criticalPath}z.boolean()showBaselines:1832—schema.showBaselines !== falsez.boolean()readOnly:1833,:1916—!!schema.readOnlyz.boolean()mobileReadOnly:1834—schema.mobileReadOnly !== falsez.boolean()Measured and deliberately NOT declared: the 30 flat
GanttConfigspellings branch 2 reads at:513-543(objectui#6469 inherited the objectui#5018 ruling for this block);title, which this renderer never reads; and a row cap — the reload's$topis the platform ceilingNON_GRID_ROW_CEILING_TOP, which the renderer's own comment marks 「⛔ Not authorable」.object-tree—plugin-tree/src/ObjectTree.tsx, registry shellplugin-tree/src/index.tsxobjectName:534,:570,:605z.string().optional()dataresolveRecordSourceConfigat:359— rung 1, returned VERBATIM as aViewData. That ONE site is the whole support for this armViewDataSchemastaticDataz.array(z.unknown())filter:474—$filter: schema.filterz.array(ViewFilterRuleSchema)tree:108— the nested blockgetTreeConfigreads; the registration's{ name: 'tree', type: 'object' }inputTreeConfigSchemanavigation:591—useNavigationOverlay({ navigation: (schema as any).navigation })z.unknown()dataIS on the row, and that is the measurement, not family symmetry. The card left this conditional — 「if the renderer's authored surface has nodata, the row declares none」 — and the condition is FALSE here: the renderer reachesschema.datathrough the shared ladder at:359, which returns it verbatim as aViewData. ⛔:496is not a second site for that arm, and an earlier revision of this body over-claimed it as one:(rest as any).data ?? (schema as any).datais gated byArray.isArray(passed)on the very next line, so it honours only the bare-ARRAY shorthand this row refuses — the same shorthandobject-mapmeasures and declines above. One ladder site is sufficient and:359is it, so the row stands on a corrected citation. What is absent is the DECLARATION, on both published faces:ObjectTreeSchemaon@object-ui/typesat this pin declares nodata, nostaticData, nofilterand nonavigationat all, and makesobjectNamerequired — four keys the renderer reads. That is precisely the mirror-is-wrong shape the ruling puts the protocol in front of, so the row follows the read points and the mirror is the face that has to follow.No
sort.ObjectTree.tsx:473-484issues$filter,$topand$expandand no$orderby, and nothing else in the file reads an order. Asortdoor here would publish a key with no read site — the exact defect #7751 exists to remove, in the other direction.Three derivation decisions, each pinned rather than left to review
ObjectView/ListViewbuild these nodes by spreadingoptions.map/options.gantt/options.tree's CONTENTS at the top level, carrying no block key at all; that is an internal transport form (objectui#5018, inherited by objectui#6469), and the map and gantt renderers name every flat key beside a present block as IGNORED in a dev warning. Writing one now gets a wrong-layer prescription naming the config block — the channelobject-calendaralready uses. Each set is held equal to the config block it points at (ListMapConfigSchema.shape;GanttConfigSchema.shapeplus the legacy singulardependencyField;TreeConfigSchema.shapeplustitleField, whichgetTreeConfig:117reads only aslabelField's last fallback), so it cannot drift silently.filterandsortare the family's one orthography from birth —ViewFilterRule[](ui#6206-B reaching the family: finding(spec): fourobject-*filterdoors inComponentPropsMapdeclarez.unknown()— no orthography at all — so they still accept the MongoDB-style record ui#6206-B retired, silently; the card that closed the lastFilterConditionSchemadoor assumed they declared the array #15449, batch Refactor: Separate authentication configuration from identity data models #55 option A) andSortItem[](objectui#8221, batch 🔗 Broken links detected in documentation #77 option B). A new door on a family-wide ruling has no "measured before the ruling" arm. The whole-map census pins incomponent.test.tsalready demanded it: everyfilterdoor must refuse the record form, and onlyrecord:related_listmay take a stringsort.gantttakesGanttConfigSchemabecauseObjectGantt.tsx:501literally validates the authored block against that schema, imported from@objectstack/spec/ui.treetakesTreeConfigSchema, which spec:GanttConfigSchemaisstrictObject(...).passthrough(), so a mistyped gantt key is silently accepted — the only one of the three view config schemas that does not refuse it #15469 shut against unknown keys on this very measurement, re-measured unchanged here.mapstaysz.unknown()— and that is measured, not conservative-by-default: the spec's ownListMapConfigSchemais strict and declares nostyle, the keygetMapConfig:365reads atschema.map?.style, so pointing this door at it would refuse a value the renderer honours today. The pin records the reason so the later value ratchet has one.Measured file face
13 files, all additive to a published surface; nothing retired, nothing narrowed.
No corpus moves: a whole-tree grep for an authored
object-map/object-gantt/object-treepage node finds zero outsidesdui.manifest.json(a registry dump, not authored metadata), against a positive control that findsobject-gridauthored twice inexamples/app-showcase.PageComponentSchema.typealready accepted all three through its open string arm and still does; what changes is that an authored props bag on one of them is judged instead of skipped.Verification
All readings below are at
e7967ce543(the branch tip) unless a row says otherwise, exit codes captured by redirect-then-$?, never through a pipe. The spec suite, typecheck and the generated-artifact checks were re-run after the patch round below.pnpm --filter @objectstack/spec buildpnpm --filter @objectstack/spec testpnpm --filter @objectstack/spec typechecktsc --noEmit+check:scripts-typecheck+check:test-typecheck)pnpm --filter @objectstack/lint testComponentPropsMapconsumer; its closure built first)pnpm --filter @objectstack/lint typecheckpnpm --filter @objectstack/spec check:generated--fixregenerated the 5 it proved staleMANIFEST=… check:react-declaration-parity --baseline … --strictobject-map2 both / 7 spec-only / 0 registry-only,object-gantt2 both / 14 spec-only / 0 registry-only; baseline ratchet: no new divergencepnpm exec eslint . --no-inline-config --format jsonscripts/pm/dispatch-gates.mjs --commands)packages/spechas no workspace dependencies, so the dependency-closure build is an empty run by construction.CI on this head is not enumerated here and is not claimed green — the verdicts above are local runs only.
Patch round — the at-tier review's two text findings, and the citations
The contract review returned PASS; these are the three things its scope did not settle, all of them text or citation, with no key, no value shape and no row membership moved. Verified after the round rather than asserted: the three key sets read back identical,
ComponentPropsMapstill carries 48 rows,object-treestill accepts theViewDataobject arm and still refuses the bare array, the gantt block is still strict,object-chartis still absent.object-gantt's describes said "chart". Every sibling names its own noun, andobject-chartis the row this section deliberately leaves absent, so "chart" pointed an author at the one block with no row. Three describes now name gantt (objectName,label,readOnly). Proof on the shipping artifact —content/docs/references/ui/component.mdx, regenerated with the repo's owngen:docs, never by hand:Object this chart binds to→ 0;Object this gantt binds to→ 1, at:398; controlObject this NOUN binds to→ 6, unchanged, so all six blocks name their own noun exactly once.object-tree'sobjectNamegave a reason that is false for tree. It said the component-leveldataSourcebinding can supply the object. Measured at the pin53ded82b:plugin-tree/src/index.tsxhas 0 hits for theElementDataSource*wiring, against 7 each inplugin-map,plugin-gantt,plugin-gridandplugin-calendar— four controls, so the zero discriminates. The key stays optional; the reason is now the true one, the ladder's first two rungs (datacan name the object itself,staticDataneeds none), plus the explicit negative. In the generated doc the false reason reads 0 hits and the new line 1, at:768.labelat:1871(:1849is the comment), and treedataon:359alone.check:docswas red one3a256daa3— source fixed, shipping artifact not yet regenerated — and is green one7967ce543:check:docsexit 0, "223 generated files in sync with packages/spec";check:generatedexit 0, "All 15 generated artifacts are up to date";check:authorable-surfaceexit 0, which is whygen:schemahad nothing to write andgen:docswas the only stale artifact.pnpm --filter @objectstack/spec testexit 0, 482 files / 13727 tests, andtypecheckexit 0, both re-run after the round.git diff --name-only origin/main...HEAD -- content/docs/releases/→ 0 paths, against a control of 2 undercontent/docs/references/.On this head
Type Check · source gatesis completed / success — the job the red had truncated now reaches its end. The rest of CI is still running and is not claimed green.Patch round 2 — the at-tier review's FAIL: F1 blocking, F2 and F3 in passing
Review of record: comment
5696414075on this PR,VERDICT: FAILat heade7967ce543. That review re-derived the accept set, the public surface, all three key sets, every value posture and theminorlevel and judged them RIGHT — nothing in this round moves a key, a value shape, a row membership or the level. Text, plus the one regeneration it forces.F1 (blocking) — six describes named a function that does not exist in the code they describe. The
objectName/data/staticDatadescribes onobject-ganttandobject-treesaid the record source is resolved by, or "read FIRST/SECOND by",getDataConfig. Re-measured at the.objectui-shapin53ded82b, each file fetched withgit showin the same command block as its count:53ded82bgetDataConfigresolveRecordSourceConfigplugin-gantt/src/ObjectGantt.tsx:64, call:593plugin-tree/src/ObjectTree.tsx:41, call:359, comment:422plugin-map/src/ObjectMap.tsx(control)The control discriminates, and it also settles what the map keeps:
ObjectMap.tsxreally does hold a localgetDataConfigat:135— the array-shorthand head, delegating to the shared ladder at:174— so the three map describes are TRUE and keep the name. Gantt and tree reach the shared three-rung ladder directly:resolveRecordSourceConfigin@object-ui/coreutils/record-source.ts:146, rungs at:151(data, returned verbatim),:155(staticData, wrapped as{ provider: 'value', items }) and:162(objectName, folded to{ provider: 'object', object }). So the ladder ORDER those six sentences describe was already true and only the name was wrong; the six now nameresolveRecordSourceConfig. Neither renderer has any reader above its ladder call, so "read FIRST" is literal for both — unlike the map, whose array head runs first, which is the second reason its describes keep the wrapper's name.Both sides counted, because this trap already bit this PR once. An earlier round fixed a false describe in the source alone and
check:docscaught the generated doc still carrying the old text. Aftergen:docs, atc431c22e05:getDataConfigresolveRecordSourceConfigpackages/spec/src/ui/component.zod.ts:3226:3242:3244content/docs/references/ui/component.mdx:638-640:398-400, tree:768-770Both sides read 9 and 0 before the round. The generated diff is exactly six table rows; no map row moved.
F2 —
component.zod.ts:3545, the treedatafield JSDoc, cited two read points where the schema header at:3487already rules that:496is not a second site for the object arm. It cites:359alone now. Source comment; no artifact follows it.F3 — two citations off by one line, both re-read at the pin:
getGanttConfig'ssafeParseisObjectGantt.tsx:500(:501is theif (!result.success)beneath it), and the tree's "Not authorable" marker isObjectTree.tsx:482, not:477.Verification for this round
pnpm --filter @objectstack/spec build, thencheck:generated— exit 0, all 15 artifacts up to date includingcheck:docs, withcontent/docs/references/ui/component.mdxactually regenerated bygen:docsrather than hand-edited. Before the regeneration the same gate named that one artifact, and only it, stale.pnpm --filter @objectstack/spec test— exit 0, 482 files / 13727 tests.pnpm --filter @objectstack/spec typecheck— exit 0.check:objectui-pin-citations— exit 0, 26 asserting pin citations match53ded82bf. It verifies the sha, not the line numbers, and reports 0 anchor content assertions here: F3 was measured by hand at the pin, not caught by this gate.dispatch-gatesderives for this change set were run locally, every one exit 0; the remaining 75 are the repo-wide farm CI owns. A declared narrowing, not a silent skip.pnpm lintnarrowed to the diff, with the three readings that make the narrowing a measurement: the population is eslint's own**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}and eslint itself answers the regenerated.mdxwith "File ignored because no matching configuration was supplied", so the linted population of this diff is one file;--format jsonreports 1 file, 0 errors, 0 warnings; and no type-aware linting is configured anywhere ineslint.config.mjs(noprojectService, noparserOptions.project), so a comment-and-describe edit in one file cannot move any untouched file's verdict.Acceptance notes
Docs Drift Check, re-verified rather than waved through
The drift check flagged 6 hand-written pages. Each was re-read; none needs an edit, and the readings are here rather than implied by their absence from the diff.
content/docs/protocol/objectui/layout-dsl.mdxdoes not ride this PR — measured, not assumed. It is flagged via the symbolComponentPropsMap, which it names exactly once, at:721, and as a MECHANISM: "For the platform's own types the authoring rules dispatchComponentPropsMapand reject a misspelled prop; acustom.*type has no entry there." That sentence is still exactly true after this change — three more types now have entries, and it enumerates none. The page carries no register of the object-block family at all: no table, no list, and its only numeric completeness claim is about breakpoints (:579). The discriminating control:object-calendarandobject-formeach get 0 hits on that page, and both have carriedComponentPropsMaprows since A typo'd key inside a page component'spropertiesis accepted in silence — the props bag is a passthrough record and the SDUI props gate has no schema to dispatch forobject-*blocks #7751 landed on 2026-08-12 — so the page already omits 2 of the 6 pre-existing rows, against a positive control ofobject-metric(3),object-master-detail-form(3),object-kanban(2),object-grid(1). A page that has never tracked the row set is not a list this change makes incomplete; its### Metric Widget/### Master-Detail Forms/### Kanban Boardsections are chosen worked examples. Writing three new hand-written renderer sections would be new documentation of three renderers, which is a card, not a rider on "add three rows".content/docs/ui/views.mdx,concepts/architecture.mdx,getting-started/common-patterns.mdx,protocol/kernel/i18n-standard.mdxandui/field-grouping-and-order.mdxeach get 0 hits forComponentPropsMapand 0 forobject-map/object-gantt/object-tree. They are reached only through string literals that this PR's newOBJECT_GANTT_FLAT_CONFIG_GUIDANCEkey array happens to contain —groupByField,timeZone,colorField,startDateFieldand friends.views.mdxdocuments the LIST-VIEWganttblock (type: 'gantt'withgantt: { startDateField, … },:340-352), whose schema isListViewSchema.gantt/GanttConfigSchemainview.zod.ts— a file this diff does not touch at all, so that page's vocabulary is unmoved.content/docs/releases/; they are read-only.git diff --name-only origin/main...HEAD -- content/docs/releases/returns 0 paths, against a control of 2 paths undercontent/docs/references/in the same diff, so the zero is a reading and not an empty filter.component-reference-rail.test.tsneeds no change — the reading, not the omissionThe card names it among "the vocabulary tests that pin the row set". Measured on this branch: the file is 164 lines, mentions
ComponentPropsMap4 times, and every one of them is the single subscript['record:reference_rail']or prose about that one row (:3,:21,:33,:35).Object.keys(ComponentPropsMap)→ 0 hits; anyobject-type literal → 0 hits. Control, the same two probes oncomponent-type-vocabulary.test.ts, which genuinely does pin the row set: 6 and 8 ⇒ the zeros discriminate. All twelve of itsdescribe/itsubjects arerecord:reference_railbehaviour. It reads no property of the row SET, so nothing in it can move when threeobject-*rows land — and it passed unchanged inside the 482 green spec files above.The row-set pins that DO move with the rows are
component.test.ts(six ruled blocks to nine, plus the filter / sort / plural-filters/ optional-objectNamedoor lists, plus a new describe) andcomponent-type-vocabulary.test.ts. The reference rail that regenerates is the docs one,content/docs/references/ui/component.mdx.Out of scope for this PR, noted rather than fixed
Each with the measurement that found it:
ListMapConfigSchemadeclares no map style at all. The spec's list-view map block is strict and has neitherstylenormapStyle, whileObjectMap.tsx:365readsschema.map?.styleand objectui'sObjectMapConfigSchemadeclaresstyle.ListMapConfigSchema.safeParse({ style: '…' })is refused — pinned as a negative in this PR's own tests, because it is the reasonobject-map.mapstaysz.unknown(). An author cannot declare a map style through the spec's list-view face today. Carrier: this PR'smapvalue ratchet, whenever it is taken.object-treeis absent from the trackedsdui.manifest.json.check:react-declaration-parityprintsobject-tree: NO component in the manifest — not registered or not public, althoughplugin-tree/src/index.tsxregisters it under bothobject-treeandtree. The baseline ratchet is unaffected (a block with no baseline row cannot regress), so this PR is green either way, but the new row gets no parity comparison. Carrier: the nextsdui:manifestregeneration.pnpm check:cross-package-test-inputsis sensitive to local build state, not to this diff. It exits 1 in a worktree wherepackages/spec/dist/exists and 0 where it does not — measured here as a one-variable control, with the diff held constant and the directory restored byte-identically (216 files,dist/ui/index.mjshash unchanged). It namespackages/cli/test/init-created-files-summary.e2e.test.ts, which symlinkspackages/spec/distat:115; this PR touches neitherpackages/clinorturbo.json. TheLint & Repo Gatesjob runs it on an unbuilt tree, which is why CI is green on it.objectui#9239has moved since the card was written. The card describes it as an open, separate calendar-mirror contradiction; it isclosed completed. Nothing in this PR depends on it, andobject-calendaris untouched.53ded82b,packages/types/src/zod/objectql.zod.tsdocblock:726statesgetDataConfigis inplugin-gantt/src/ObjectGantt.tsx, and threeObjectGanttSchemadescribes repeat the name (:738,:739,:838) — while that file has 0 occurrences of it (control:ObjectMap.tsx8, and the map's own mirror describes at:641-643are correspondingly TRUE). Separately,core/src/utils/record-source.tsquotes our describes as the ruled contract at:28,:92,:95and:97and attributes the text to the map and gantt zod twins, so after this PR that quotation matches the map twin and no longer matches the gantt one. Both sites are objectui's, in objectui's repo; nothing here can fix them and the pin does not move. Handed to the seat to file against objectui rather than fixed in passing.Not touched, deliberately: the
object-chartdeliberate-absence note incomponent.zod.ts, pinned as still absent in the new tests.objectui#8348 carries
pm:blockedon this card; its remaining slice judges map / gantt / tree against these rows once this lands. Nothing here changes any objectui file.Authored by the
domain:specexecution seat, sessionsession_01KB5PFtxuy1x3dcR5gxudx6.Generated by Claude Code