Repository navigation
feat(plugin-tree,plugin-map,types): object-tree and object-map are judged and publish the keys their renderers honour (objectui#11168 slice 3) - #11335
Conversation
…s its renderer honours, and ObjectTreeSchema mirrors the 17.5.0 row (objectui#11168 slice 3) The repo-wide registry parity guard now loads `@object-ui/plugin-tree`, so `object-tree` leaves `UNJUDGED_SPEC_BLOCKS` (where the 17.5.0 bump booked it OWED) and is judged in both directions. Every key was measured through the real SchemaRenderer and this package's registration first (objectui#11111 decision 3 = B): - `data`, `staticData`, `filter` and `navigation` are honoured, so the registration declares them on both tags; `objectName` is no longer required (a tree on inline rows never reads it, and the page validator raised missing-required-prop on a tree that draws). - `ObjectTreeSchema` (TS and zod) gains `data`, `staticData`, `tree` and `navigation` (the last two the spec's own schemas, by reference), and `objectName` goes optional behind the record-source refinement. The pointer's measurement: `objectui validate` refused a staticData-only tree with invalid_type at objectName. The binding does not count on this block: its registration is not gate-wrapped. - Member pins for tree / data / staticData / filter / navigation in one file, registered in MEMBER_PINS. unjudgedBlocks 4 -> 3, owner 8 -> 7, census judged 35 -> 36 / ledgered 13 -> 12. Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT Co-authored-by: Claude <noreply@anthropic.com>
…bleClustering are published, and every structured key is member-pinned (objectui#11168 slice 3) The registry parity guard now loads `@object-ui/plugin-map`, so `object-map` leaves `UNJUDGED_SPEC_BLOCKS` and is judged in both directions. The judgment found three spec keys the registration did not publish; each was measured through the real SchemaRenderer first and all three are honoured, so they are declared (objectui#11111 decision 3 = B). Descriptions state what the renderer does, including where it departs from the spec row's prose: a `style` inside the `map` block wins over `mapStyle` (reported, not resolved here). Member pins: map / data / staticData / filter / sort / navigation in one new file; dataSource promotes ObjectMap.elementDataSource.test.tsx after reading it end to end. unjudgedBlocks 3 -> 2, owner 7 -> 6, census judged 36 -> 37 / ledgered 12 -> 11. Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT Co-authored-by: Claude <noreply@anthropic.com>
…UIActionSchema.size, the calendar pin text, the timeline navigation wording (objectui#11168 slice 3) - `UIActionSchema.size` takes the `action:button` row's vocabulary by reference (`ActionButtonProps['size']`): `default` and `icon` were TS2322 while the row, the registration and the Button primitive all take them. Measured through the real SchemaRenderer: a group MEMBER draws each of the five sizes exactly as an action:button does. Pinned in ui-action-size-11168.test.ts. - The guard's `object-calendar.calendar` pin text called `allDayField` objectui-local and the spec block four keys; 17.5.0 declares all five. - The timeline `navigation` description (record 5924396134's two nits): `split`'s reason no longer attaches to `page` / `none`, and `openNewTab` outranks every mode EXCEPT `none`, which is read first. The docs table says the same. The map description written in this slice had copied the first nit; corrected with it. Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT Co-authored-by: Claude <noreply@anthropic.com>
…is slice falsifies; type the map pin without any (objectui#11168 slice 3) - `.changeset/8655-object-tree-props-schema-typed.md` ships in the same release and says `navigation` stays undeclared with its cast, and that declaring `tree` on the node would fork. objectui#11168 slice 3 declared both by reference to the 17.5.0 row, so the entry gets a dated, append-only note; its frontmatter is byte-identical. - The new map pin used `any` in three places; they are `unknown` now (lint: 0 warnings on the file). Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT Co-authored-by: Claude <noreply@anthropic.com>
|
changeset-claim-re-read
|
✅ 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
|
…jectui#11168 slice 3, round 2) Picks up the Spec Main Shape Gate repair (PR #11343, 47e3ce0) and objectui#11299 (770cc5b). Merged cleanly: no conflicting hunk, so nothing was resolved by hand. Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT Co-authored-by: Claude <noreply@anthropic.com>
…pec row says — the seat's ruling A (objectui#11168 slice 3, round 2) The installed `@objectstack/spec` 17.5.0 describe for `object-map.mapStyle` reads "Read before `map.style`". `getMapConfig` computed that order but the declared-block path handed `config.style` back first, so a map writing both drew `map.style`. The seat ruled the renderer follows the spec (option A, record 5926309662): the block path now returns the same `mapStyle || map.style` the flat path does. - The registration's `mapStyle` description is the spec's describe, verbatim, and a pin row holds it equal to the installed row's. - The precedence is pinned both ways through the real SchemaRenderer: both written draws mapStyle, either alone draws itself, neither draws the demo tiles (the control). The "measured, not endorsed" row is gone. - README, plugin-map.mdx and the guard's object-map.map pin claim follow; the slice's own pending changeset is corrected and a patch changeset states the behaviour change. No authored producer writes both (census in the changeset): the flatteners write mapStyle on the block-less path only. Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT 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
|
…s; the object-form columns label says what columns does (objectui#11168 slice 3, round 2 addendum) Two text nits carried from PR B's at-tier record: - plugin-view README "### View tabs" (and its docs page) listed `form.layout` as `vertical | horizontal | inline | grid`; it now lists `vertical | horizontal`. The docs-page line is byte-identical to PR B's own edit, so the later merge is clean there. - The designer label for `object-form.columns` read "Columns (grid layout)" / 「列数(网格布局)」, pointing at the retired `grid` layout. Measured through the real SchemaRenderer, `columns` is live under either layout: 1 stacks, 2-4 set the field grid's column count, absent infers from the field count (the spec row: "Number of field columns (multi-column forms), honoured under either `layout`"). The labels now read "Field columns (1–4)" / 「字段列数(1–4)」; the en pin follows. Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT 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
|
…jectui#11168 slice 3, round 3) origin/main db0beb2: PR B (#11351, 17dc167), objectui#11331 (bef9f20) and #11337 (db0beb2). Two conflicts, both resolved by measurement: - apps/console registry-inputs-spec-parity.test.ts: the per-owner total. Census of the merged ledgers: unjudgedBlocks 2 (object-gantt, object-timeline), offSpecInputs 0, unpublishedKeys 3, refusedArms 0 (B emptied the map), memberPins 0, so objectui#11168 owns 5; 8652 and 8649 stay 0. Neither side's 6 or 7. - packages/app-shell i18n.ts (en and zh): B's deletions of option.layout.inline / grid kept, with this branch's reworded object-form.columns row. Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT Co-authored-by: Claude <noreply@anthropic.com>
…jectui#11168 slice 3, round 3, second merge) origin/main advanced to 2124d04 after the first round-3 merge (25901fc), bringing #11329 (fd6f5da) and seven more. No conflict. The guard auto-merged: main's side is MEMBER_PINS prose only (record:quick_actions.requiredPermissions, object-calendar.navigation, object-kanban.navigation) and one comment, kept as main has them. The census of the merged ledgers is unchanged: unjudgedBlocks 2, offSpecInputs 0, unpublishedKeys 3, refusedArms 0, memberPins 0; objectui#11168 owns 5, 8652 and 8649 own 0. Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT Co-authored-by: Claude <noreply@anthropic.com>
…jectui#11168 slice 3, round 3, third merge) origin/main advanced to 5638529 while the round-3 gates ran: #11317 (docs), #11352 (types, fields) and #11350 (plugin-grid, core). No conflict. #11352 also edits packages/types objectql.ts, objectql.zod.ts and zod-mirror-parity.test.ts, which this branch edits too; it auto-merged, and the gates are re-run at this head. The guard is untouched by this merge, so its census still reads as at 2817b5f: objectui#11168 5, unjudgedBlocks 2, refusedArms 0. Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT 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: Read-only, as briefed: card objectui#11168 (body and all 19 comments), PR #11335 (body, 35 files, the diff against its merge-base ① Derived judgments
Wording — RIGHT. The calendar pin text now says five keys, and The guard — RIGHT. Eager imports of The merges — RIGHT. The merge-base of the head with Changesets — RIGHT as bodies, with one false sentence in two of them (below).
② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL |
…jectui#11168 slice 3, round 4) origin/main d0fba91 (#11346, #11359, #11361, #11367). It touches pnpm-lock.yaml, which this branch also edits (the plugin-tree devDependency link); no conflict. Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT Co-authored-by: Claude <noreply@anthropic.com>
…avigation block open the record page through the host's navigator, as measured since objectui#11293 (objectui#11168 slice 3, round 4) The at-tier record on PR #11335 (5928833509) found the `navigation` descriptions this PR publishes on object-tree and object-map, and edits on object-timeline, saying `page` and a block without `mode` open nothing. objectui#11293 (2124d04, in this branch since 2817b5f) hands a `page` click with no `onNavigate` to the record navigator the host publishes on RelatedRecordActionsContext, and the three renderers pass none. Measured first through the real SchemaRenderer, with the host's navigator mounted and without (40 rows, predictions written before the run, all as predicted): under the navigator `page` and `{ size: 'lg' }` call openRecord(objectName, id) once, with no overlay and no tab, on all three blocks; the ABSENT key, `none` and (map, timeline) `split` open nothing and do not navigate; a tree or map that names no objectName opens nothing; with no navigator `page` and the mode-less block open nothing. - The three registration descriptions, both READMEs and the timeline README, plugin-map.mdx, the plugin-timeline.mdx table and callout, and the two MEMBER_PINS rows now say that, in the kanban/calendar 11293 wording. The absent-key clause is kept. - The tree and map pins gain a navigator-mounted row set through the real SchemaRenderer; timelineNavigationMembers-8654 gains one and its no-host rows are renamed to say so. - The prose of 11168-object-tree-inputs, 11168-object-map-inputs and 11168-timeline-navigation-wording follows; frontmatter untouched. - 8253-export-tree-view-config gains a dated, append-only note on "the flat ObjectTreeSchema"; the earlier 4323 bytes are byte-identical (cmp). - action-group-menu-inputs-11168 gains the row the UIActionSchema.size changeset's claim was owed: a group member at each of the five sizes draws the same class list as an action:button at that size, with its control. Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT Co-authored-by: Claude <noreply@anthropic.com>
…ry, whose `page` sentence objectui#11293 and this slice's description overtake (objectui#11168 slice 3, round 4) The pending .changeset/8654-timeline-navigation.md says `page` and a mode-less block open nothing on a standalone timeline and that the input description says so. Both stopped holding in this release. Append-only: the earlier 1484 bytes and the frontmatter are byte-identical to d0fba91 (cmp -n 1484 exit 0). Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT Co-authored-by: Claude <noreply@anthropic.com>
…a node that names no object (objectui#11168 slice 3, round 4)
`{ objectName = OBJECT } = {}` takes its default for an explicit
`objectName: undefined`, so the row meant to mount a tree / map on inline
rows alone still named the object and saw `openRecord` called. The helper
now takes `namesObject`; the probe that measured this case before the pin
omitted the key and read nothing opened.
Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT
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: Delta record on round 4, after ① Derived judgmentsThe merge Round 4's own lines — read in full. Three commits ( The FAIL item of The new wording against the hook at this head. With the key ABSENT a click opens nothing: the hook's no-config branch calls
The pins — RIGHT. The tree and map pins each gain a navigator-mounted describe through the real The appended notes — RIGHT. The three changeset proses. Semver and ② Semver levelCarried from ③ Boundary flags
Implemented-by: VERDICT: FAIL |
…'s rows came from — `data.object` on the object provider, else `objectName` (objectui#11168 slice 3, round 5) The delta record on PR #11335 (5929481787) found the tree's round-4 clause too narrow: ObjectTree hands the hook `resolveRecordSourceObjectName(schema, data) ?? objectName`, which reads `data.object` whenever `data` is the object provider. Measured first through the real SchemaRenderer under the host's record navigator, predictions written before the run (6 rows, all as predicted): `data: { provider: 'object', object: X }` with no objectName opens X's record page on `page` and on a mode-less block; with `objectName: Y` beside it X opens and X is the object queried; a value provider beside `objectName: Y` opens Y; inline rows with no objectName, and a value provider with none, open nothing. - The registration description, the tree README, the 11168-object-tree-inputs prose and the guard's object-tree.navigation MEMBER_PINS row now say that. - The tree pin gains two navigator-mounted rows on the object-provider shape: no objectName, and both written. Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT 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: Delta record on round 5, after ① Derived judgmentsRound 5's shape — RIGHT. One commit, The The new wording against the resolver and the hook — RIGHT. The two pin rows — RIGHT. Both mount through the real Changeset — RIGHT. ② Semver levelCarried from ③ Boundary flags
Implemented-by: VERDICT: PASS |
Takes 20d23be, which carries PR #11335 (objectui#11168 slice 3), so the object-master-detail-form.details parity row can be patched on this branch (seat answer A). The merge touches none of this branch's files. Claude-Session: https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh Co-authored-by: Claude <noreply@anthropic.com>
…al) (objectstack-ai#11361) Fixes objectstack-ai#11356 ## Summary `next` 16.3.3 carries GHSA-vcvr-r3jv-pc5j (critical, CVSS 9.5, "Remote Code Execution in next/og ImageResponse"). The fixed release is 16.3.6. On `main` at `2124d04111`, `apps/site/package.json` pins `"next": "16.3.3"` and `pnpm-lock.yaml` resolves `next@16.3.3`. The surface is reachable: `apps/site/app/og/docs/[...slug]/route.tsx` imports `ImageResponse` from `'next/og'`, and the production build prerenders 184 PNGs through it. This PR moves to the patched release. It supersedes Dependabot's objectstack-ai#11235 (manifest only, so every install step failed with `ERR_PNPM_OUTDATED_LOCKFILE`) and objectstack-ai#11236 (a full re-resolution that gave nanoid and vite each an extra physical copy). It adds no OSV or audit exemption, and the lockfile is not hand-edited. ## Change - `apps/site/package.json`: the `next` line only, `16.3.3` to `16.3.6`, still an exact pin. No other objectui manifest declares `next`: `git grep '"next"'` over every `package.json` at `2124d04111` has exactly one hit, this line. - `pnpm-lock.yaml`: regenerated with `pnpm install --lockfile-only` under pnpm 10.31.0 (the `packageManager` version), starting from `main`'s lockfile unchanged. ## Lockfile 55 lines changed on each side. - `packages:` has 1767 entries before and after. 10 are removed and 10 added: `next`, `@next/env` and the eight `@next/swc-*` binaries, each 16.3.3 to 16.3.6. - `snapshots:` has 1773 entries before and after. 14 are removed and 14 added: the same 10, plus the four snapshot keys that embed the `next` version as a peer. Those are `fumadocs-core`, `fumadocs-mdx`, `fumadocs-ui`, and `better-auth`, whose optional `next` peer resolves to the workspace copy. None of the four changes version. - Two importers change: `apps/site`, and `packages/auth` through the `better-auth` snapshot key only (its specifier and version are unchanged). - The string `16.3.3` no longer appears anywhere in the lockfile. ## Verification Every row below was measured on `76e9ee3`, this branch's head. `main` was still at `2124d04111` when this PR was opened, so there was nothing to merge. The runs used Node 22.23.3. This container ships Node 22.22.0, and the repository's `engine-strict=true` refuses it, because `jsdom@30.0.1` requires `^22.22.2`. CI is unaffected: its `setup-node` asks for `22.x`, which takes the newest 22 release. See Acceptance notes. | Check | Result | |---|---| | `node scripts/check-lockfile-integrity.mjs --base-ref origin/main` (the gate objectstack-ai#11236 failed) | exit 0, `VERDICT clean`: 1773 resolutions on both sides, no backward move, no new copy. `--self-test` exit 0, 29 cases | | `pnpm install --frozen-lockfile` | exit 0, and the installed `apps/site/node_modules/next` reads 16.3.6 | | Control leg: `main`'s lockfile with this branch's manifest, `pnpm install --frozen-lockfile` | exit 1, `ERR_PNPM_OUTDATED_LOCKFILE` (the objectstack-ai#11235 failure). Restored afterwards: `git diff HEAD` empty, blob hash equal to HEAD's | | `node scripts/check-lockfile-dedupe.mjs` (bare, the hard verdict) | exit 0, `VERDICT deduped` | | `pnpm turbo run build --filter='@object-ui/site'` (the `Build Docs` command), under the shared verify lock, `--concurrency=2` | exit 0, 30/30 tasks, `Next.js 16.3.6 (Turbopack)`. The site task was a cache miss, so it really ran: 559 static pages, including 184 `/og/docs/**/image.png` prerendered through `ImageResponse`, each a real PNG | | `pnpm --filter @object-ui/site type-check` (`fumadocs-mdx && next typegen && tsc --noEmit`) | exit 0 | | `pnpm --filter @object-ui/auth type-check`, then `pnpm exec vitest run packages/auth/` (the importer whose snapshot key moved) | exit 0. 28 files, 282 tests passed | | The 10 test files that read `pnpm-lock.yaml` or resolve through `apps/site`: `ci-cd-pipeline-doc`, `docs-build-trigger`, `check-installed-spec-pin-claims`, `check-lockfile-integrity`, `check-lockfile-dedupe`, `check-pre-install-import-graph`, `markdown-test-inputs`, `check-doc-links`, `check-changeset-presence`, `MetadataService.specKeyReference` | 10 files, 479 tests passed | | `node scripts/check-changeset-presence.mjs` | exit 0, "no changeset is owed" | | `check:control-bytes`, `check:new-line-citations`, `check:pre-install-import-graph`, `check:phantom-deps`, `check:unused-deps`, `type-check:coverage` | all exit 0 | | `node scripts/check-governed-queue-guard.mjs --test apps/site/package.json pnpm-lock.yaml` | `NOT GOVERNED` | Left to CI: the repository-wide `pnpm lint` and `pnpm type-check`, the eight test shards, coverage, `Build & E2E`, and `Bundle Analysis`. No package source changes. ## OSV-Scanner, before and after I ran OSV-Scanner v2.3.8 as `scan --offline-vulnerabilities --lockfile=pnpm-lock.yaml`, against its offline npm database. The binary's SHA-256 (`bc98e153…`) matches the release's checksum file. objectui has no OSV workflow, so this is a reading, not a gate. | Tree | Exit | Result | |---|---|---| | `main` at `2124d04111` | 1 | 62 rows, 1 Critical: `GHSA-vcvr-r3jv-pc5j`, `next` 16.3.3 | | this branch at `76e9ee3` | 1 | 61 rows, 0 Critical | The two row sets differ by exactly one line: the `next` advisory is gone, and no row is added. The other 61 rows are already on `main` and are outside this card. ## Changeset None. `apps/site` is `"private": true`, and no published package's source or contract changes. `node scripts/check-changeset-presence.mjs` settles it (exit 0, above). ## Acceptance notes - **Overlap with draft objectstack-ai#11335** (another seat). It also touches `pnpm-lock.yaml`, but only in the `importers:` block of a different package (it adds an `@object-ui/sdui-parser` workspace link). Its hunks are disjoint from this PR's. If git reports a conflict for whichever lands second, regenerate with `pnpm install --lockfile-only`; do not hand-merge. - **Alert count.** The push banner reported 2 critical Dependabot alerts on the default branch. OSV over `pnpm-lock.yaml` reads 1 Critical, the one fixed here. Where the second one comes from was not measured. - **Node floor.** Root `engines.node` declares `>=22.11`, and `CONTRIBUTING.md` repeats it. But `jsdom@30.0.1` declares `^22.22.2 || ^24.15.0 || >=26.0.0`, and `.npmrc` sets `engine-strict=true`. So on Node 22.22.0, which the declared floor admits, `pnpm install --frozen-lockfile` stops with `ERR_PNPM_UNSUPPORTED_ENGINE` naming jsdom. This PR does not change that. It is reported to the dispatching seat for a separate card. Locally I used a checksum-verified Node 22.23.3 from a scratch directory rather than turning `engine-strict` off. Session: `https://claude.ai/code/session_018gA1pE6eJtwHhqx72G8U9X` --- _Generated by [Claude Code](https://claude.ai/code/session_018gA1pE6eJtwHhqx72G8U9X)_ Co-authored-by: Claude <noreply@anthropic.com>
…tail's sort field is derived only (objectui#11070 round 9) (objectstack-ai#11376) Refs objectstack-ai#11070 Clause-②: yes (narrowing). `MasterDetailDetailConfig.sortField` retires, and `GridFieldMetadata` gains `sort_field`. It is priced in the changeset. Round 9 of objectui#11070: the seat's answer B on `sort_field` (`5928627070`), plus `grid.mdx`'s Features prose. Claim `5929173058`; dev session `session_01TdiauJaVCHuj45EzZGUxHh`. DRAFT: no ready, no enqueue. ## What changes - **(i) `sort_field` is declared** (`@object-ui/types`). `GridFieldMetadata.sort_field?: string`. Its JSDoc says what it does (the CHILD field the grid stamps with each row's index on every change, so a drag-reorder persists; not one of the columns) and who produces it (`deriveDetail` in `@object-ui/plugin-form`, through `MasterDetailForm`). `GridField` drops `UndeclaredGridKeys` and reads its config as `Partial` of `GridFieldMetadata` alone, so every key it reads is declared there (`@object-ui/fields`, comments and one type annotation; no runtime change). - **(ii) The authored override retires** (`@object-ui/plugin-form`). `MasterDetailDetailConfig` has no `sortField` member. The derived value travels on the form's internal per-detail record (`DetailEntry.sortField`, beside the existing internal `amountFieldDef`), set only from `derived.sortField`, and the grid config reads `sort_field: entry.sortField`. The `d.sortField ?? derived.sortField` bridge is gone from both derive branches. The amount override (`amountField`) is untouched. - Why `DetailEntry` and not a second config type: the authored object is spread into the resolved config (`{ ...d, … }`), and a fully configured detail keeps it as is. A resolved-config type that carried `sortField` would therefore still carry a stray authored `sortField` at runtime on that path, which is the dual read this round removes. On `DetailEntry` the field has one writer, the derivation. - **(iii) `grid.mdx`.** The typed fence writes `sort_field: 'position'`, and the field-level key list documents it the way round 8 documented `total_field` and `add_label`. The Features list was audited bullet by bullet against `GridField` (below). - **Pins.** The 11144 row "an authored sort field wins over the derived one" retires with the member; its three derived-`position` / amount rows stay green. The 8071 members pin is re-counted and re-pointed: header prose (eleven members, four renamed on the way, `sort_field` derived), row 2 with `sortField` moved to the off-list set (not read), a new row 2c (derived pick vs a written `sortField`, with a lit control), and a compile-time block (an exact `keyof MasterDetailDetailConfig` list, a `@ts-expect-error` refusal of `sortField`, and a lit control). `GridField.fieldKeys-11070` gains a `sort_field` drag-reorder row and its no-key control, and its `declared` fixture writes `sort_field`. - **Changeset.** `.changeset/11070-grid-sort-field-round9.md`: `@object-ui/types` minor (one new optional member, a feature), `@object-ui/plugin-form` minor (a published member retires: breaking, priced minor under the fixed group's policy, stated), `@object-ui/fields` patch (no runtime change). Dated, append-only notes on the two pending changesets this round makes false: `11070-grid-field-keys-round8` ("`sort_field` … stays read and undeclared") and `11144-inline-columns-sort-amount` ("or a detail's own `amountField` / `sortField` still wins"). ## The `sortField` census (measured first) objectui base `1a88ce22` (exact-word `git grep -w`), objectstack `origin/main` `e952cff5` (same query; control `amountField` hits 12 files there). | Site | Role | Before | After | |---|---|---|---| | `deriveDetail` (`plugin-form/src/deriveMasterDetail.ts`) | writer (derived) | the child's first `position` / `sort_order` / `sequence` / `line_no` / `line_number` / `sort` field | unchanged: the one producer | | Authored documents, example apps, catalog fixtures, docs (objectui) | writer | 0 | 0 | | objectstack: spec, examples, docs, `os migrate meta` conversions | writer | 0 (`sortField` and `sort_field` both) | 0 | | `attachInlineSubforms` (app-shell) | writer of `subforms[]` | writes `childObject` / `relationshipField` / `inlineMode` / `title` / `columns` / `amountField`; no `sortField` | unchanged | | Drawer / Modal `subforms` types | declared shape | no `sortField` member | unchanged | | `object-master-detail-form` registration `inputs` (`plugin-form/src/index.tsx`) | declared shape | `details`: array, no members | unchanged | | 11144 pin, row "an authored sort field wins" | test writer | `sortField: 'line_no'` | row retired | | 8071 pin, row 2 | test writer | `sortField: 'position'` as a member | an off-list key, asserted not read | | `MasterDetailDetailConfig.sortField` | declared member | declared | retired (compile error) | | `MasterDetailForm` resolve effect, both derive branches | reader | `d.sortField ?? derived.sortField` | `derived.sortField`, on `DetailEntry` | | `MasterDetailForm` grid config | reader | `sort_field: d.sortField` | `sort_field: entry.sortField` | | `MasterDetailForm`, fully configured detail (no derive) | reader | the authored `sortField` only | none (nothing is derived there, before or after) | | `GridField` `cfg.sort_field` | reader | through `UndeclaredGridKeys` | declared `GridFieldMetadata.sort_field` | objectui#11144's thread names no user of the authored override: the issue body says `attachInlineSubforms` carries only `inlineColumns` and `inlineAmountField`, and the claim's "an authored `amountField` (`inlineAmountField`) or `sortField` still wins" states a symmetry, not a writer. ## `grid.mdx` Features audit | Bullet | Against `GridField` | Disposition | |---|---|---| | Table Display: clean tabular layout | true | kept, with "every row shown" | | Pagination Preview: first 5 rows, "Showing X of Y" | false: every row renders (`rows.map` in both the read-only and editable bodies), and no "Showing" text exists in the widget | removed | | Type-Specific Rendering: each column renders according to its type | true for editable cells; in read-only mode only `date` / `datetime` / `time`, `file` and `lookup` cells are formatted (probe below) | corrected to say exactly that | | Read-Only Mode: full table view without editing | true | kept | | Responsive: scrollable for many columns | true (`overflow-x-auto`, per-type minimum widths) | kept | ## M3 (`scripts/measure-strict-authoring-face.mjs --json`, types dist built) | | base `1a88ce22` | `5376e31a` (this diff) | |---|---|---| | nodes / strict-refused / strict-only / red today | 2229 / 77 / 46 / 31 | 2229 / 77 / 46 / 31 | | documents / strict-refused whole tree / red-today documents | 601 / 72 / 24 | 601 / 72 / 24 | | per-component rows | identical | identical | Only `rendererMentionsOfUndeclaredKeys` (a raw word count over renderer sources: `change` and `key` each one lower, `field` and `schema` each one higher, from this diff's comments) and `unloadableModules` (environmental: app-shell unbuilt at base) differ. H3 holds. ## Ablations (predictions written before each run; every mutation through objectstack `scripts/ablation-replace.mjs`, anchor hit as declared, blob changed, restored to the HEAD blob with `git diff HEAD` empty) - **A1, an undeclared `sort_field` read** (the member deleted from `GridFieldMetadata`, types rebuilt; dist marker 1 to 0). Predicted and observed: fields `tsc --noEmit` TS2339 on `cfg.sort_field`; fields `tsc -p tsconfig.test.json` TS2353 on the `declared` fixture and on `grid({ sort_field })`; `check:doc-snippets` 1 failed block, `grid.mdx` TS2353 `sort_field`. Restore leg: types rebuilt, dist marker back to 1, `git status --porcelain` empty. - **A2, an authored `sortField` type-checking again** (`sortField?: string;` planted back on `MasterDetailDetailConfig`). Predicted and observed: plugin-form `tsc --noEmit` green; `tsc -p tsconfig.test.json` exactly 2 errors in the 8071 file, TS2322 on the member list and TS2578 on the now-unused `@ts-expect-error`. - **A3, the `??` bridge restored** (both `sortField: derived.sortField,` sites to `(d as any).sortField ?? derived.sortField`). Predicted and observed over the three touched test files: 1 failed, 24 passed; the failure is row 2c, `expected 'line_no' to be 'position'`. - **A4, the false Features bullet restored** in `grid.mdx` (the "Showing X of Y" pagination claim). Predicted and observed: `check:doc-snippets`, `check:doc-fences`, `docs:check-links`, `check:doc-types` and `check:doc-example-ids` all green. No gate can see a prose bullet; this round's correction is held by review only. ## Local verification (final head `2c59d3b5`, the merge of `origin/main` `c82ff391` on top of `5376e31a`) - Builds through the verify lock: the `check-doc-snippet-types --build-filter` closure, `turbo --concurrency=2`, 35 of 35 tasks. - `type-check` green for `@object-ui/types`, `@object-ui/fields` and `@object-ui/plugin-form` (each `tsc --noEmit` plus its test project; `--listFiles` shows the touched test files are in those programs). The type-checker set is these three: `MasterDetailDetailConfig` is referenced only inside plugin-form (a comment in an `apps/console` test names it), and `GridFieldMetadata` gains one optional member, which no consumer can break. - vitest (`--maxWorkers=2`, through the lock): `packages/plugin-form/` 159 files, 1836 passed and 1 skipped; `packages/types/` plus every `GridField*` / `LineItems*` test, 325 files and 8103 tests passed. - Green: `check:doc-snippets` (768 of 768 blocks), `check:doc-fences`, `docs:check-links`, `check:control-bytes`, `check:changeset-claims`, `check:pending-changeset-literals`, `check-changeset-presence`, `changeset:check`, `check-changeset-overwrite` (it reports the two dated notes, its case 2), `check:new-line-citations` (0 new), `check:test-path-roots`, `check:doc-types`, `check:doc-example-ids`, `check:vi-mock-specifiers`, `check:readme-exports`, `check-type-check-coverage`. `check-governed-queue-guard --test`: NOT GOVERNED. - The claims gate names two pending changesets on touched files (`6357-basechema-bind-declaration` on `grid.mdx`, `6625-retire-fieldmeta-decimals` on `GridField.tsx`). Both paragraphs were read and still hold. - Narrowed lint (`eslint --no-inline-config --format json`) over the 6 changed ts/tsx files: 0 errors, 68 warnings, the same per-file warning counts as the base blobs. Population: the config's `**/*.{ts,tsx}` blocks (the `.mdx` and `.md` files match none). File count 6, read from the json. Invariance: no `parserOptions.project` or `projectService`, and no rule in `eslint-rules/` reads the filesystem, so this diff cannot move a verdict on an untouched file. ## Hypotheses - H1 holds: 0 authored writers in either repository; the writers are `deriveDetail` and two test fixtures. - H2 holds: a type split was enough, with no renderer change, for every child that has a sort-named field and is derived. A fully configured detail derives nothing, before and after; that is objectui#11144's own carrier note, not a change here. - H3 holds: M3 does not move. ## Stop condition reached: the `object-master-detail-form.details` parity row (excluded file, PR objectstack-ai#11335) `apps/console/src/__tests__/registry-inputs-spec-parity.test.ts` is not touched. Its `MEMBER_PINS['object-master-detail-form.details']` row stays mechanically green (the locator needs only the pin file to exist and to name the block and the key). Its `pins` prose, though, is now false in two places: "five members reach the grid through ONE hand-written object and four are RENAMED on the way (`sortField` to `sort_field`, …)" and "names five of the twelve members the renderer reads". The brief says to stop and report before that row changes, so the row is reported, not edited, and the dev report puts it to the seat with options. ## Acceptance notes (not this round's scope) - Read-only `GridField` formats only `date` / `datetime` / `time`, `file` and `lookup` cells. A scratch probe (uncommitted, removed) rendered one row `{ amount: 1234.5, status: 'paid', qty: 1234.5 }` with `currency`, `select` (option label `Paid`) and `number` columns: read-only cells read `1234.5`, `paid`, `1234.5`; the same row in list mode reads `1,234.5`, `Paid`, `1,234.5`. No reach was measured (the docs page's read-only example writes values that read the same either way), so it is noted here and not filed. - `grid.mdx`'s "Integration with Advanced Grid" list (outside the Features list) offers add/remove rows, drag-and-drop reordering and formula columns as plugin features, which the basic grid also has. Left as is. - `docs/adr/0001-master-detail-subform.md` (governed) names `sort_field` and `row[sortField] = index`. An ADR records the decision taken; left as is. ## Patch (answer A) — added by the seat - Seat answer A (`5929940616` on objectui#11070) to this PR's open question. After PR objectstack-ai#11335 landed, `origin/main` `20d23bef` was merged in as `36313b7f`; the merge touches none of this PR's files. - `09d3ebf0` edits exactly the two clauses of the `object-master-detail-form.details` row's `pins` prose in `apps/console/src/__tests__/registry-inputs-spec-parity.test.ts`: "four members … each RENAMED … beside `columns` and the DERIVED `sort_field`", and "five of the eleven members". - The same commit moves the 8071 pin's fully configured `sortField` check from row 2 into row 2c, so row 2 keeps exactly the two off-list members that the parity row describes. - The follow-up dev report is `5930257276`. The dev's contract forbids a dev PATCH of this body, so the seat wrote this section. --- _Generated by [Claude Code](https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Refs #11168
Clause-②: yes (narrowing). It widens:
object-treepublishesdata,staticData,filterandnavigation;object-mappublishesmapStyle,navigationandenableClustering;ObjectTreeSchema.objectNamegoes optional;UIActionSchema.sizeadmitsdefaultandicon. It narrows: onObjectTreeSchemathe keysdata(nowViewData),tree,navigationandstaticDataused to ride the node's passthrough unjudged, and each is now judged on both faces. The fixed group ships it asminor.objectui#11168 slice 3, part 1 of 2. Decision 3 = B of objectui#11111 applies (record
5902351047): every key is decided by its own measurement through the realSchemaRenderer, and only what the renderer honours is declared. Part 2 is theobject-form.layout/ objectui#7759 Group C narrowing, opened as a separate PR at the PM seat's split boundary.What this PR takes
object-treeobjectNameno longer required; theObjectTreeSchemamirror aligned on both faces (pointer5909523766, and thetreeDECLARE verdict owed in the 8655 header)object-mapUIActionSchema.size(booking5923506054item 1)action:buttonrow's vocabulary by referenceallDayFieldis a spec key since 17.5.0; the text now says five keysnavigationdescription (record5924396134)Handed back as a worklist, at a block boundary, to keep this reviewable:
object-ganttandobject-timeline. They stay booked OWED inUNJUDGED_SPEC_BLOCKS. Theobject-timeline.navigationMEMBER_PINSline waits for the timeline's slice, because the guard refuses a pin for a block it does not judge. The pin filetimelineNavigationMembers-8654.test.tsxis ready.Measurements (before any edit, on the unchanged renderer)
object-tree(pointer5909523766):objectui validate/safeValidateSchemaon astaticData-only tree with noobjectNamegaveinvalid_typeatobjectName. The spec row accepted the same tree. ThroughSchemaRendererit drew all three rows. The page validator raisedmissing-required-prop(error) onobjectNameandunknown-proponstaticData.filternarrowed the inline rows.navigation: { mode: 'drawer' }opened the record overlay. With the key absent, nothing opened.object-map:mapStylereplaces the demo tiles. (Its precedence againstmap.styleis ruled and changed in round 2; see the section below.)enableClustering: trueclusters 2 nearby markers into one cluster. Absent, 2 markers stay 2.falsekeeps 120 markers unclustered, where absent clusters them.navigation:drawer,modalandpopoveropen the record.new_windowandopenNewTabcallwindow.open('/store/record/m1', '_blank').split,noneand an absent key open nothing.pageand a mode-less block open the record page through the record navigator the host publishes (openRecord('store', 'm1')). They open nothing under a host that publishes no navigator, or on a map that names noobjectName(round 4, measured after objectui#11293).noneoutranksopenNewTab.UIActionSchema.size: a group MEMBER and anaction:buttondraw identical classes for each size:default/mdgiveh-10 px-4,smgivesh-9 px-3,lggivesh-11 px-8, andicongivesh-10 w-10. The old type refuseddefaultandicon(TS2322).The
ObjectTreeSchemamirror (rides here, by measurement)The pointer's measurement is a public-door reach. It is in the claimed file surface and the same family, so the mirror moves in this PR:
objectNameis optional, behind the record-source refinement the map and gantt mirrors carry.datais typedViewData.staticDatais declared.treeandnavigationtake the spec'sTreeConfigSchema/NavigationConfigSchemaby reference.dataSourcebinding is not counted as a record source here. The tree's registration is not gate-wrapped, so a binding reaches nothing.requireRecordSourcegained abinding: 'not-read'option for exactly this, and its message then names only the three rungs.ObjectTreenow readsnavigationwithout a cast. The ledgers that re-derive this moved with it:SPEC_DERIVED_PAIRS+1 and the imported-defaultsIMPORTEDlist +1. The 9550 barrel pin row forobjectNameisstring | undefined. The 8655 typed-schema pin's two "stays undeclared" rows flipped to declared, as its own 17.5.0 note predicted.Ledger arithmetic, by census of the edited guard
95bd23c90unjudgedBlockscapobject-gantt,object-timeline)offSpecInputs/unpublishedKeys/refusedArms/memberPinsobjectui#11168MEMBER_PIN_EXEMPTION_CEILINGMember pins (12, registered in
MEMBER_PINS)object-tree:tree,data,staticData,filter,navigation. All are in one new file,packages/plugin-tree/src/__tests__/objectTreeInputs-11168.test.tsx.object-map:map,data,staticData,filter,sort,navigationare in one new file,packages/plugin-map/src/__tests__/objectMapInputs-11168.test.tsx.dataSourcePROMOTESObjectMap.elementDataSource.test.tsx, after reading it end to end (binding object, view filter/sort, AND-ing, the unresolvable view, the unmappedlimit, ascending default).Ablations (predictions written first; every leg restored, blob == HEAD,
git diff HEADempty)navigationinputObjectTreeSchema.objectNamerequired againObjectMaphandsnavigation: undefinedto the hookUIActionSchema.sizeback to'sm' / 'md' / 'lg'getMapConfigblock path back toconfig.style || stylemapStyledraws" redVitest aliases
@object-ui/*to source, so nodistsits between mutation and assertion for A to C. D is a tsc leg over source.Gates (at
17f1664ecunless noted)packages/types: 308 files, 7758 tests passed.plugin-tree+plugin-map+plugin-timeline+ the guard +components/src/renderers/action+ every outside test reader ofobject-tree,object-maporObjectTreeSchema(core, list, view, react, sdui-parser, cli, schema-catalog, scripts): 158 files, 2852 passed, 34 skipped (pre-existingdateOnlyZoneskips).types,plugin-tree,plugin-map,plugin-timeline,componentsandconsoleall exit 0, with the script name echoed. Each ran after building its dependency closure (console:turbo build --filter=@object-ui/console^..., 34 tasks).@object-ui/sdui-parserdevDependency), unused-deps, self-import, esm-specifierscheck-governed-queue-guard --teston the 28 changed paths: NOT GOVERNED. The controlAGENTS.mdanswers exit 3.eslint --format jsonover the 16 touched TS files gives 0 errors.--print-configshows 118 rules in effect, andparserOptions.project/projectServiceare null, so the linting is not type-aware and untouched files cannot move. One added line carries ano-explicit-anywarning:ObjectTreeSchema.staticData?: any[], spelled as all five sibling mirrors spell it, paired withz.array(z.any()).@objectstack/specfrom objectstackmainand injects it into the install. The gate is red repo-wide on objectui#11330 (the PM seat's heads-up), and this diff touches none of its five sites. A source read of objectstackmain(dff98c1f) shows the sameaction:buttonsize vocabulary and the sameTreeConfigSchemathe new type pins rely on.package.jsonfloor orsideEffectsarray moved.Changesets
11168-object-tree-inputs(plugin-treeminor),11168-types-object-tree-mirror(typesminor),11168-object-map-inputs(plugin-mapminor),11168-ui-action-size(typesminor),11168-timeline-navigation-wording(plugin-timelinepatch) and, in round 2,11168-map-style-precedence(plugin-mappatch).8655-object-tree-props-schema-typed.mdships in the same release and saysnavigation/treestay undeclared. This slice falsifies that, so the file gets a dated, append-only note; its frontmatter is byte-identical.object-map.mapStyleprecedence: ruled A, the renderer follows the spec (round 2)The installed 17.5.0 describe reads 「MapLibre style URL or spec, overriding the public demo tiles. Read before
map.style; NOT the base nodestyle, which is an inline CSS record」. The renderer read astyleinside themapblock first (getMapConfigreturnedstyle: config.style || style), so a map writing both drewmap.style. The seat ruled option A (record5926309662, under 协议为基准).getMapConfig's declared-block path now returns the samemapStyle || map.stylethe flat path does.mapStyledescription is the spec's describe verbatim, and a pin row holds it equal to the installed row's.SchemaRenderer: both written ⇒mapStyledraws; onlymap.style⇒ it draws; onlymapStyle⇒ it draws; neither ⇒ the demo tiles (the control).mapStyleis written only by theListView/ObjectViewflatteners, on the path with nomapblock. objectstack carries it only in the spec and its docs. So no authored producer writes both, and no stored document changes what it draws.11168-map-style-precedence(plugin-mappatch) states the behaviour change. README,plugin-map.mdxand the guard'sobject-map.mappin claim now teach the spec's order.Round 2 head
94db25d50, on merge289df609b(origin/main770cc5ba4merged in, clean, picking up the Spec Main Shape Gate repair47e3ce008). Re-run there:types,plugin-mapandconsole(exit 0, afterturbo build --filter=@object-ui/console^...);packages/plugin-map+ the guard: 38 files, 488 passed;The round-1 union figures above are at
17f1664ec.Acceptance notes (noted, not filed)
parentField/labelField/fields/defaultExpandedDepthare still declared onObjectTreeSchemaand still outrank thetreeblock's members, while the 17.5.0 row refuses them flat. That is the host-composed handoff. Retiring them is a separate decision, so the README now teaches thetreeblock. Carrier: 承接者:无.object-kanban/object-calendarnavigationdescriptions (objectui#8652, landed) carry the same "openNewTaboutranks the mode" wording, which is untrue fornone. They are outside this claim. Carrier: 承接者:无.Session:
https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT(dispatched by thedomain:spec @ objectuiseat).Generated by Claude Code