Repository navigation
feat(spec)!: ai:chat_window is retired — refused by name at the schema door, the floating chat overlay is the AI chat entry point (#21504) - #21531
Conversation
…shape Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
…tire-ai-chat-window Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 138 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 7846296e0a847b299ea993b44b7adb5ad58369dc && git checkout 7846296e0a847b299ea993b44b7adb5ad58369dc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ad7c3518983a1bb63fd4601954ac92d055124e42 3f17d5229e35c592af07f771f7d1118f26e2678e && git checkout -B drift-repro ad7c3518983a1bb63fd4601954ac92d055124e42 && git merge --no-ff 3f17d5229e35c592af07f771f7d1118f26e2678e
node scripts/docs-audit/affected-docs.mjs --json ad7c3518983a1bb63fd4601954ac92d055124e42
|
Contract reviewServed-tier: Read-only review of PR #21531 (card #21504) against the net diff on Check-runs on the head, read at 2026-10-03T03:42Z: 32 runs — 18 ① Derived judgmentsEvery accept-set and public-surface change the diff implies, named right or wrong against the ruling and the precedent's actual shape.
② Semver levelChangeset ③ Boundary flagsDev flags (eight deviations, two out-of-scope findings, no open questions), each answered:
Implemented-by: VERDICT: PASS Generated by Claude Code |
ACCEPT — PR #21531 @
|
…parse on react, full and slotted pages (objectstack-ai#21459) (objectstack-ai#21547) Fixes objectstack-ai#21459 Clause-②: yes (narrowing) ## Summary This PR executes ruling A (record `5964312254`). `PageSchema` now accepts `requires` only when `kind` is `html` or `jsx`, the two kinds the metadata save door compiles. On a `react`, `full` or `slotted` page it is refused at parse, and so is a page that omits `kind`, which defaults to `full`. The refusal names the key, the page's kind and the compiled kinds. - **The refusal: `checkPageRequiresKind`** (`packages/spec/src/ui/page.zod.ts`). It is an exported object-level check chained onto `PageSchema` with `.superRefine(...)`, right after `checkPageSourceCompleteness`. That is the mechanism this file already uses for kind-conditional rules, so no second one is added. Its vocabulary is the new export `COMPILED_PAGE_KINDS = ['html', 'jsx']`. The issue is `code: 'custom'` at `path: ['requires']`. The message opens with "`requires` is refused on a `kind: 'react'` page" (or the page's kind), names `html` and its deprecated alias `jsx`, says "Delete the key.", and ends with the house `os migrate meta --from 17` sentence. It carries no tracker number. - **The describe** now says the key exists only on html / jsx pages and is refused at parse on the other kinds. Its opening sentence, "derived from the source at save — omit it", is unchanged byte for byte, because the reconciliation ledger row quotes it. - **The kit (ADR-0087):** - D2 conversion `page-requires-non-compiled-kind-removed` (step 18, `order: 58`, `retiredFromLoadPath: true`, `retiredAfter: '17.6.0'`). It strips the key from `react`, `full`, `slotted` and kind-less pages. - D3 semantic entry `page-requires-non-compiled-kind-refused`, which carries `conversionIds: [page-requires-non-compiled-kind-removed]`. - A hand-written `STEP18_RATIONALE` fragment (`order: 66`, placed after `ui-ai-chat-window-retired`, which landed first at 65). The registry regions were regenerated with `gen:migration-registry`. - No tombstone and no `RETIRED_KEYS_BY_MAJOR` row: the key stays live on html pages. - **Ledgers:** - `liveness/page.json`: the `requires` row stays `live`. Its evidence gains the PARSE reader, its note's per-kind clause shrinks to the compiled kinds, and `verifiedAt` is now 2026-10-03. The state counts do not move. - The reconciliation row in `metadata-form-zod-reconciliation.test.ts`: its per-kind clause now reads "on every other kind the parse refuses it". - **Generated:** `api-surface/ui.json` and `export-origins/ui.json` gain the two exports, and `content/docs/references/ui/page.mdx` re-renders the describe. All three were produced by `check:generated --fix`, never edited by hand. - **Changeset:** `@objectstack/spec` `minor` with the BREAKING banner, `Clause-②: yes (narrowing)`, a FROM → TO table, and the ADR-0087 marker `registered page-requires-non-compiled-kind-removed, page-requires-non-compiled-kind-refused`. No runtime code changes. The save door, the load report and objectui are untouched. ## Premise checks (at base `c98a72d69e`, re-read on the merged tree) - **A1 census: zero producers.** Every `requires:` hit in `examples/**`, `packages/apps/**` (none there), `content/docs/**` and `skills/**` is the stack-level capability list. In examples that is `app-crm`, `app-showcase` and `app-todo`'s `objectstack.config.ts`, plus prose comments naming capability tokens. The only page bodies that carry `requires` are the 11 `htmlPage(...)` sites in `packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts`, all `kind: 'html'`, so they are still accepted. The proximity scan (each `requires:` hit within 15 lines of a page marker) found them, which is its positive control. No test pinned acceptance on a non-compiled kind. objectui at the `.objectui-sha` pin `89cad75d55` shows page `requires` only on stamped html fixtures, plus one compile-only type fixture (`twins-spec-by-reference-9736.test.ts:158`). The ruling's cloud and hotcrm census stands. - **A2, the default kind.** An omitted `kind` parses as `full`, and the refinement runs on the parsed value. So `{ requires: [...] }` with no `kind` is refused as a `full` page, and the message adds "(`full` is also the kind of a page that omits `kind`)". A `.shape` mirror without the default reaches the check with `kind` absent and gets the same issue, which the exports-parity fixture pins. - **A2, the empty array.** On a non-compiled kind the save door never looks at `[]`: `compileHtmlPage` returns `undefined`, so `findHtmlPageSourceGaps` answers `null` and `stampHtmlPageRequires` returns the body as written. The load report answers `[]` for it, which reports nothing. So `[]` had no effect anywhere. The ruling's words refuse the key ("accepts `requires` only when `kind` is `html` or `jsx`"), not its contents, so `[]` is refused too, and the conversion strips it too. Both are pinned. ## A4: the stored-row disposition is a mechanical drop, so it has a D2 conversion The drop is lossless. On those kinds nothing derived the list, no renderer read it, and the Studio page editor already drops it on every save. Its one reader was the load report's warning. With the conversion: - a stored row at rest replays it at every rehydration seam, loading with one conversion notice instead of a `metadata_spec_invalid` warning and a `_diagnostics` badge at every boot; - an artifact built by 17.6.0 or earlier replays it too; - an authored source is still refused at parse, because the conversion is retired from the load path, and `os migrate meta --from 17` lists the edit. This is pinned at the real seam: `loadMetaFromDb` over a seeded stored react page carrying `requires` gives `loaded: 1, errors: 0, invalid: 0`, one notice naming the conversion, no `[page_requires_plugin_absent]` line (the manifest lacks the plugin, so an unconverted list would have been reported) and no `[metadata_spec_invalid]` line. ## A6: reader branches the parse boundary now makes unreachable for non-compiled kinds (none changed here) - `packages/metadata-protocol/src/runtime-authoring-gate.ts:616-627`, `findPageRequiresAbsentFromManifest`, which is "Kind-agnostic on purpose" (TSDoc at `:610`). A non-compiled page can no longer reach it: the save door refuses the key, and at load the stored-row conversion strips it before `reportPageRequiresAbsentAtLoad` (`protocol.ts:24418`, called on the converted body) reads it. The kind-agnostic reach now serves only html / jsx rows, and its TSDoc sentence is a follow-up candidate. - `packages/metadata-protocol/src/protocol.ts:24526`, the `reportPageRequiresAbsentAtLoad` TSDoc "How a stored page gets here". It is still true, but now only of html rows. - `runtime-authoring-gate.ts:679` and `:728` were already gated to html / jsx by `compileHtmlPage` (`:634`). No change. - objectui `builtinComponents.tsx` `pageSaveBody` (at the pin) deletes `requires` on every kind. On non-compiled kinds that is now redundant, and harmless. No test pinned the old acceptance, so no existing test changed meaning. ## A9: merge state PR objectstack-ai#21531 landed as `48eb9c193f`. This branch merged `origin/main` twice through `scripts/pm/os-regen-merge.sh`: `dafb0f6d1e` after objectstack-ai#21531, and `e38149267c` at `49161683fb`. - `ui/page.zod.ts` and `migrations/registry.ts` merged textually clean. Both rationale fragments are kept (`ui-ai-chat-window-retired` 65, this one 66), and the regenerated regions match `check:migration-registry`. - The three `os-regen` artifacts both sides touched were regenerated on the merged tree, and the delta against main is exactly the two exports and the describe row. - Sibling entries survive, with equal counts on main and head for `'ui-ai-chat-window-retired'` (3), `'ui/AIChatWindowProps'` (2) and `'ai:chat_window'` (17). - None of the 10 open PRs touches `ui/page.zod.ts`, `migrations/registry.ts`, `conversions/registry.ts`, `liveness/page.json` or the reconciliation test. - Since `49161683fb`, main gained `1ac7308d7a` and `41b13331cd`. Neither touches a file in this diff. ## Tests (all at head `e38149267c` unless noted) - `@objectstack/spec` test (`vitest run --project local`): 604 files, 17888 passed, 1 todo. Run at `d7cd797549`; the later merge moved nothing under `packages/spec`. - `@objectstack/spec` typecheck: exit 0. That covers `tsc --noEmit`, `check:scripts-typecheck` and `check:test-typecheck` (52 files / 246 errors held, unchanged). - `@objectstack/metadata-protocol` test: 205 files passed, 3 skipped (3157 tests passed). Typecheck exit 0, and `--listFiles` includes `protocol.runtime-authoring-gate.test.ts`. - New and changed pins: - `packages/spec/src/ui/page-requires-compiled-kinds.test.ts`: the refusal on `react`, `full` and `slotted`, checked for code, path and named subjects; the omitted-kind default; the empty array; html / jsx controls; no `requires` on every kind; the stack door envelope (`STACK_SCHEMA_INVALID`, 422, at `pages.1.requires`); and the conversion. The conversion pins cover the stored-row strip with a notice, html / jsx kept, strip-iff-refused over the whole kind vocabulary, unknown kind left alone, artifact replay, idempotence and retired-from-load-path. The file also pins the ledger wiring and that there is no tombstone, with `ui/Page:assignedProfiles` as the control. - `object-refinement-check-exports.test.ts`: the new export is catalogued with 8 fixtures, and the parity, bijection, attachment-by-identifier and barrel-identity legs hold. - `protocol.runtime-authoring-gate.test.ts`: the save door refuses `requires` on a `react`, `full`, `slotted` or kind-less page with `{ code: 'INVALID_METADATA', status: 422 }`, one `custom` issue at `requires`, no `jsx-*` compile finding, and nothing persisted. The html control still saves and stamps. The load pin is described above. - **Ablation** through `scripts/ablation-replace.mjs`, run under the verify lock. The mutation replaces the kind condition `if ((COMPILED_PAGE_KINDS as readonly string[]).includes(kind)) return;` with a bare `return;`, so the key is accepted on every kind again. The source-resolved spec suites need no rebuild. - Mutation landed: anchor 1 → 0, blob `6b13a7df8e44` → `1fd2575e28e3`, marker on disk 1. - Red leg: 13 failed, 174 passed. The three non-compiled-kind refusal pins are red, along with the omitted-kind, empty-array, stack-door and strip-iff-refused pins and the exports parity and bijection legs. The html / jsx controls stayed green. - Restore: blob equals HEAD (`6b13a7df8e44`) and `git diff HEAD` is empty. Green leg: 187 passed. ## Gates `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 116 commands at `e38149267c`. All 116 were run and their exit codes recorded. `--ran` reports: "116 derived famil(ies) accounted for — 115 run, 1 NOT-MEASURED". The NOT-MEASURED one is `pnpm check:dual-build-cjs-loads` (exit 3, PREREQUISITE NOT MET: it needs every package's `dist/`, a repo-wide build), and it is declared to CI. `check:skill-examples` and `check:lean-entry-closure` first exited 3 for missing `client-react` / `objectql` builds. After building those closures they re-ran to exit 0. Highlights, each exit 0: - `check:generated` reports "all up to date" for 15 artifacts; - `check:liveness`, `check:migration-registry`, `check:spec-changes`, `check:upgrade-guide`, `check:api-surface`, `check:export-origins`, `check:docs`; - `check:adr-0087-registration`, `check-changeset-no-major` (also run with this body as the `--event` payload), `check-empty-changeset`; - `check:doc-authoring`, `check:nul-bytes`, `check:cross-package-test-inputs`, `check:engine-double-contract`, `check:type-check-debt`. A narrowed eslint run (`--no-inline-config --format json`) covered the 8 changed `.ts` files and found 0 errors and 0 warnings. This repo's eslint config enables no type-aware linting, so the diff cannot move a verdict on an untouched file. ## Acceptance notes - **objectui census tripwire.** objectui's `spec-object-refinements-7715.test.ts` census (at the pin) counts `PageSchema`'s object-level checks and lists `attached: ['checkPageSourceCompleteness']` for `PageNodeSchema`. It is built to go red when the spec adds a check. At objectui's next `@objectstack/spec` bump, that row will ask for `checkPageRequiresKind` to be attached, or declared not attachable. Runtime behaviour needs no objectui change, as the ruling says, but that census will need one row. The Console Pin Gate only builds objectui, and nothing objectui compiles against changed type, so it is unaffected. - `examples/app-showcase` and every other example author no page-level `requires`, so no example changed. --- _Generated by [Claude Code](https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…ect-calendar take the shape each block reads; object-grid columns held (objectstack-ai#21464, stage 2) (objectstack-ai#21559) Part of objectstack-ai#21464 Clause-②: yes (narrowing) ## Fix round (contract review `5966757164`, item 1) The review found that the grid reads `options` off an AUTHORED `object-grid` column. The group-header formatter (`plugin-grid/src/ObjectGrid.tsx:2997-3001` at the pin) takes the column whose `field` is the grouping field, reads `colOverride?.options || objectDefField?.options` with the column winning, and draws the group-header labels from it. objectui pins that as behaviour in `gridGroupingMembers-8071.test.tsx:260-301`. `ListColumn` declares no `options`, so the by-reference `columns` narrowing refused a value the grid draws, a working writer by the seat's test (`5966636964`). This round: - returns `object-grid` `columns` to `z.unknown()` and re-adds it to the enumeration pin's ledger as `held-for-decision`, with the reader `ObjectGrid.tsx:2158` and `:2997-3001` and the carrier objectstack-ai/objectui#11544, in the shape the pin uses for kanban `conditionalFormatting` (objectstack-ai/objectui#11522); - removes its cases from the companion pin (§1 three, §2 five, §3 one); - corrects the four texts that said the grid never reads `options`: the member docblock, the changeset's FROM → TO table, the changeset's "Who is affected" paragraph, and this body's A3 list. `editable` stays described as unread, which the review confirmed; - narrows the D3 entry, its generated registry region and the rationale fragment to the eight members, and regenerates the reference page. The other eight members stand as reviewed. ## What this does Stage 2 (S-list) of the `ComponentPropsMap` `z.unknown()` close-out, per triage `5961300594`, the seat answer `5963787404` (staging A) and the claim `5965611825`. Eight members the list blocks read with a fixed shape were `z.unknown()` (an array of it for the lists). Any value passed the component-props gate, and the renderer dropped or substituted an off-shape one with no report. Each member now takes the shape its block reads, measured at the `.objectui-sha` pin `89cad75d55`. The family's ninth, `object-grid` `columns`, is held (above). | row · member | was | now | read point at the pin | |:--|:--|:--|:--| | `object-grid` · `columns` | `z.array(z.unknown())` | **held**, unchanged: `z.array(z.unknown())` | `plugin-grid/src/ObjectGrid.tsx:2158` `normalizeColumns`; the group-header formatter reads an authored column's `options` (`:2997-3001`), which `ListColumn` does not declare (objectstack-ai/objectui#11544) | | `object-grid` · `fields` | `z.array(z.unknown())` | `z.array(z.string())`, the measured shape (no list-view counterpart) | `plugin-grid/src/ObjectGrid.tsx:1946`; the draw path looks each entry up as `objectSchema.fields[fieldName]` (`:3969`, `:4012`) | | `object-grid` · `selection` | `z.unknown()` | `ListViewSchema.shape.selection` (`SelectionConfigSchema`), by reference | `.type`, `:4799-4812` | | `object-grid` · `selectable` | `z.unknown()` | `boolean`, `'single'` or `'multiple'`, the measured shape | `:4813-4815`, handed to the table at `:5333`; `components/src/renderers/complex/data-table.tsx:644` `resolveSelectionMode` | | `object-grid` · `rowActions` | `z.array(z.unknown())` | `ListViewSchema.shape.rowActions`, by reference | `:1834-1835`, `string[]` | | `object-grid` · `bulkActions` | `z.array(z.unknown())` | `ListViewSchema.shape.bulkActions`, by reference | `:4763` `batchActions ?? bulkActions`, then `resolveBulkActions` by name | | `object-grid` · `batchActions` | `z.array(z.unknown())` | the same def as `bulkActions` | the same read, which takes `batchActions` first | | `object-kanban` · `columns` | `z.array(z.unknown())` | all bare value strings, or all lanes `{ id, title, cards?, limit?, className?, collapsed? }` (module-private `ObjectKanbanLaneSchema`) | `plugin-kanban/src/ObjectKanban.tsx:1177-1188` dispatches on the first entry; `index.tsx:129-158` buckets by `id` and keeps static `cards`; `KanbanImpl.tsx:540`, `:568`, `:742` read `limit`, `className`, `collapsed` | | `object-calendar` · `calendar` | `z.unknown()` | `ListViewSchema.shape.calendar` (`CalendarConfigSchema`), by reference | `plugin-calendar/src/ObjectCalendar.tsx:294-297` returns the block as the config; `:857`, `:1056`, `:1141` read its five bindings | A static kanban card is a record row. Its `id` and `title` are typed, and the rest of the card is the row's own values. That one new `z.unknown()` member, `object-kanban columns[].cards[].*`, carries a `records` line in the enumeration pin. **`bulkActions` / `batchActions` (A4).** The grid reads `schema.batchActions ?? schema.bulkActions` (`ObjectGrid.tsx:4763`), so `batchActions` is the second spelling of one capability, read first. objectui's own type calls it the legacy alias. `ListViewSchema` declares only `bulkActions`. Both members now hold `bulkActions`'s def, and neither is retired here. **`selection` default.** `SelectionConfigSchema` defaults `type` to `none`. The grid reads an object with no `type` as ON (objectui#9837, ruling A-prime: presence enables). That default reaches only a parsed document, never the bag the grid reads. It is the list view's declaration either way, and objectui#9837 holds the question. The member's docblock records it. ## The census (A1), whole A writer is a page-component node: an object literal naming the type, a literal annotated with the block's type, a `schema={{…}}` on the block's React component, a call into a local helper that builds the node, or a direct parse through the row. Each member's value is resolved through same-file constants. The control is `objectName` on the same nodes. The instrument is a TypeScript-AST walk over every `.ts`, `.tsx`, `.js`, `.json`, `.md`, `.mdx` and `.yaml` file, with fenced code in the documents parsed too. | corpus | `object-grid` nodes | `object-kanban` nodes | `object-calendar` nodes | control `objectName` | |:--|--:|--:|--:|:--| | objectstack `49161683fb` (`examples/`, `packages/` incl. `packages/apps/`, `content/`, `skills/`, `apps/`) | 57 | 30 | 5 | 47 / 27 / 4 | | objectui `89cad75d55` (whole tree) | 689 | 240 | 160 | 293 / 108 / 98 | | row · member | objectstack values (parse) | objectui static values (distinct) · parse · refused · not static | |:--|:--|:--| | grid `columns` (held, not narrowed) | 5 | 310 (143) · not parsed by this PR · 20 not static | | grid `fields` | 0 | 16 (10) · 13 · 3 · 2 | | grid `selection` | 0 | 17 (4) · 17 · 0 · 1 | | grid `selectable` | 0 | 2 (1) · 2 · 0 · 1 | | grid `rowActions` | 0 | 10 (5) · 10 · 0 · 1 | | grid `bulkActions` | 0 | 23 (9) · 21 · 2 · 1 | | grid `batchActions` | 0 | 5 (3) · 5 · 0 · 0 | | kanban `columns` | 1 (1) | 108 (39) · 107 · 1 · 12 | | calendar `calendar` | 0 | 60 (26) · 58 · 2 · 6 | The objectui values were parsed through the built rows on this branch. Across the eight typed members, objectstack holds one value (the protocol docs' lane example, which parses), and objectui holds 241 static values: 233 parse and 8 are refused. The grid `columns` row is listed for completeness. Its 5 objectstack values (the showcase's two grids among them) and 310 objectui values meet no new shape here. ## Writer parse results (A3) No refused value is one the renderer draws. Each of the 8 refused objectui values is a test fixture whose value the renderer drops, skips or refuses: - **2 `{ name }` entries in `bulkActions`** (`bulkActionMembers-8071`, `bulkActionDefsUnusableMember-8730`). The fold skips them, and both tests assert the skip. - **3 object entries in `fields`** (`serverGroupedSelectIdentity-11105`). They copy the node the list view hands the grid at run time, as that test's own header says, so they are not an authored page. - **A lane `color`** (`objectKanbanColumnMembers-8071`). The console retired it, and the test marks it an undeclared member. The lane now refuses it with a prescription naming `className`. - **The calendar's retired `dateField` / `endField` aliases** (`calendar-date-alias-refusal-8355`). The test asserts their refusal. The 24 values that are not static are helper parameters, `.map` results and the run-time hand-offs (`plugin-view/src/ObjectView.tsx:2462`, `plugin-designer`). None is an authored page. **The held member.** Under the first head, `object-grid` `columns` was narrowed by reference, and this list carried 40 refused grid-column values. Among them, "16 column keys the grid never reads" counted 14 `editable` and 2 `options`, and said no authored-column read names either key. That was right for `editable` and wrong for `options`: the 2 `options` values (`gridGroupingMembers-8071`) are drawn in the group headers. So `columns` is held, and its 40 values are no longer refused by this PR. Under the triage caveat ("a narrowing that would refuse a measured writer is reported, not shipped silently"), this list is the report. A3 is applied with the seat's test (`5966636964`): a writer is a value the renderer draws. A refused fixture that probes the renderer's drop is not one. On that test the eight members stand, and `columns` is held. ## A2, per member Typed by reference (4): grid `selection`, `rowActions`, `bulkActions`; calendar `calendar`. Typed to the same def (1): grid `batchActions`. Typed to the renderer's read (3): grid `fields`, `selectable`; kanban `columns`. Held for a ruling (1): grid `columns` (objectstack-ai/objectui#11544). None is runner-forwarded. ## The pin (A5) - Eight `staged` lines leave the enumeration pin's ledger, and its `list-family` stage goes with them. `object-grid` `columns[]` stays as `held-for-decision`. One `records` line is added for `object-kanban columns[].cards[].*`. - **`no-reader` is removed.** It had no user since PR objectstack-ai#21531, the pin's own checks never require a kind to be in use, and no other file pins its vocabulary (`git grep no-reader` finds only an unrelated prose use in `scripts/pm/check-half-states.mjs`). - The typed members are pinned in their own file, `component-list-family-typed-members.pin.test.ts`, as objectstack-ai#21445's were. §1 checks that each declared shape parses, byte-identical, or to what the list view's schema answers where a default materializes. §2 checks each refusal by code and path, and for the kanban's two-array union by the arm's own issue. §3 checks identity with the list view's defs and the measured vocabularies. §4 checks the D3 registration. - **Ablation, fix round**, at `8b276755c2`: `object-grid` `rowActions` was reverted to `z.array(z.unknown()).optional()`, with no ledger line. It landed: anchor x1 to x0, replacement x0 to x1, blob `285edfb205` to `2b6b2f8643`. Red: **Tests 4 failed | 95 passed (99)**. The enumeration pin's §1 received exactly `[ 'object-grid rowActions[]' ]`, its census-equals-ledger control failed, and the companion pin failed its two `rowActions` cases. The restore was proven: blob after restore `285edfb205` == blob at HEAD, and `git diff HEAD` was empty. Green rerun: **Tests 99 passed (99)**. - **Ablation, first head**, at `870e7327ec`, via `node scripts/ablation-replace.mjs` in wrap mode. The mutation reverted `object-grid` `selection` to `z.unknown().optional()`, with no ledger line. It landed: anchor x1 to x0, replacement x0 to x1, blob `e60c46e776` to `2111ab1538`. Both pins went red: **Tests 7 failed | 101 passed (108)**. The enumeration pin's §1 received exactly `[ 'object-grid selection' ]`, and its census-equals-ledger control failed. The typed-member pin failed its five `selection` cases. The restore was proven: blob after restore `e60c46e776` == blob at HEAD, and `git diff HEAD` was empty. The green rerun showed **Tests 108 passed (108)**. The pins import `./component.zod` from source, so no build sits between mutation and run. ## The rest of the kit (A6, A7) - `component-type-vocabulary.ts`: the `KNOWN_COMPONENT_TYPES` docblock now lists `ai:chat_window` among the kept retired rows (contract review `5965171663` item 10). Comment only. - ADR-0087 D3 entry `ui-object-grid-kanban-calendar-list-members-typed`, and its step-18 rationale fragment at order 66. objectstack-ai#21459's PR objectstack-ai#21547 landed (`72af58c621`) while this was in review, with its own fragment `page-requires-non-compiled-kind-refused` also at 66, in a different gap. The second merge kept both fragments and both D3 entries (each id found twice in `registry.ts`), and `check:migration-registry` reads the generated regions current with no regeneration owed. The registry header allows equal orders, which render in `id` order. - Regenerated by `check:generated --fix` (only what it proved stale): `content/docs/references/ui/component.mdx`, and the strictness-ledger counts for `ui/`. The `ui/` count moves from 189 to 191 sites: +1 strict (the lane) and +1 passthrough (the card). In the fix round only the reference page was stale, and its `columns` row is back to `any[]`. - Changeset: `@objectstack/spec` `minor`, a **BREAKING** banner, `Clause-②: yes (narrowing)`, a FROM → TO table, the measured census, and the ADR-0087 marker `registered`. - The `ObjectGridPropsParsed` docblock had said the parsed state differs "on exactly one key — `data`". That was already untrue after objectstack-ai#21445, and this change adds the `selection.type` default. It now names the defaults. ## Gates, at `8b276755c2` (after merging `origin/main` `ce532184d1` through `os-regen-merge.sh`) - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 114 commands (9 paths, 688 changed lines, merge base `ce532184d1`). Every exit code was written to disk before any pipe. `--ran` reconciled: **114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN**. - On this head, after a full `turbo run build` (72 tasks), all 114 ran and every one exited 0. On the first head, six gates answered `PREREQUISITE NOT MET` (exit 3) before the build; those lines were kept apart, not counted. - `pnpm --filter @objectstack/spec build`: exit 0. `check:generated`: exit 0, "All 15 generated artifacts are up to date". `check:liveness`, `check:migration-registry`, `check:strictness-ledger`, `check:authorable-surface` and `check:api-surface`: exit 0. - `pnpm --filter @objectstack/spec test`: **Test Files 606 passed (606), Tests 17952 passed, 1 todo**. `typecheck`: exit 0, `check:test-typecheck: OK`. - `pnpm --filter @objectstack/lint test` (the one import side of `ComponentPropsMap`): **119 files, 5620 tests passed**. - `pnpm check:doc-authoring` and `pnpm check:nul-bytes`: exit 0. - `check-widening-tells`: `--declaration no` gives exit 4 with 9 T1 tells, all at the new lane schema's keys inside the former `z.unknown()` bag. That is the shape the gate's own text rules a true refusal, so declare `yes`. `--declaration yes` gives exit 0. - The changeset gates were run with this body as the `--event` payload; their verdict lines are in the dev report. - NOT MEASURED: the Console Pin Gate, Dogfood and the full `pnpm lint`. Reason: they are CI-owned. objectui's source indexes `SpecObjectCalendarProps['data']`, `SpecObjectFormProps['layout']` and `SpecObjectKanbanProps['swimlaneField']`, none of them a narrowed member. ## Acceptance notes - The held `columns`: every `ListColumnSchema` member is read by the grid at the pin (the draw path, `useColumnSummary` at `:3170-3177` for `summary`), and the grid reads one key `ListColumn` does not declare, `options`, in the group headers. Typing `columns` waits on objectstack-ai/objectui#11544. - objectui's own tests that probe the dropped shapes (above) will see the spec refuse those values when objectui bumps `@objectstack/spec`. The objectui block mirror takes the grid row by reference (`ObjectGridBlockSchema.properties`). No objectui edit was made here. objectstack-ai#21464 remains open for S-form, S-metric and S-objectui-held. object-kanban `conditionalFormatting` stays held on objectstack-ai/objectui#11522, and object-grid `columns` on objectstack-ai/objectui#11544. --- _Generated by [Claude Code](https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #21504
Clause-②: yes (narrowing)
Retires the
ai:chat_windowpage component type under ADR-0049 enforce-or-remove, refused by name throughRETIRED_PAGE_COMPONENT_TYPES, per triage ruling5963897014and theuser:profileprecedent (#14159, landed in PR #15112). No renderer for it ever shipped: objectui leaves it unregistered on purpose, because the floating chat overlay is the supported AI chat entry point. So an authored node validated clean and then drew "Unknown component type".ai:suggestionis not touched; triage ruled it a different class.What changed
ui/page.zod.ts'ai:chat_window'leavesPageComponentTypeand joinsRETIRED_PAGE_COMPONENT_TYPES. Its message names the floating chat overlay as the supported entry point and carries no tracker number.PageComponentSchema.typealready consults the map, so the open string arm cannot re-admit the name. The type's.describe()and the enum docblock name the new member.ui/component.zod.tsAIChatWindowProps(mode,agentId,context,aria) is deleted. TheComponentPropsMap['ai:chat_window']row stays asretiredComponentProps('ai:chat_window'): az.neverthat refuses every bag,{}included, with the same prescription.ui-ai-chat-window-retired, oneSTEP18_RATIONALEfragment, and theRETIRED_DEFS_BY_MAJOR[18]entryui/AIChatWindowProps, generated bygen:migration-registry. No D2 conversion, see below.component.test.tsgets a new describe withcode,path,paramsand the first sentence at each door: the map, the row (empty and populated bags),PageComponentSchema,PageSchemaat the element path, and the enum error map.ai:suggestionis the control and parses at every door, and the open arm stays open.component-type-vocabulary.test.ts: the type stays known, leaves the typo candidates, andai:stays reserved. The oldshould parse ai:chat_window with defaultaccept pin is flipped into the new describe.component-props-unknown-members.pin.test.ts(PR #21502) loses itsai:chat_windowcontext{}"no-reader" line, because the member left with the def.api-surface/,export-origins/,declaration-map/,authorable-surface/(4 lines, proof: "def no longer emitted by this build"),authorable-defaults/,json-schema.manifest/(1 key),docs-import-surface.baseline.json, the strictness-ledger counts, and the reference docs. Each was regenerated by its generator. The manifest and authorable-surface lines were deleted as the build gate prescribes, and its proof check accepted them.authorable-surface.base.jsonis untouched.content/docs/ui/pages.mdxcomponent list says the truth.validate-page-field-bindings.tsusedAIChatWindowProps.agentIdas its example of a non-field string prop. The example now names a live one,ElementImagePropsSchema.alt.@objectstack/specminor, BREAKING banner,Clause-②: yes (narrowing), FROM → TO table, ADR-0087 markerregistered ui-ai-chat-window-retired.How this follows the precedent, and the one place the claim's wording differs
The PR #15112 shape is matched element by element: the enum value removed, a map entry, the enum error map and the node-level check sharing one string, the row kept as
retiredComponentProps, pins at the three doors with a control, a vocabulary pin, regenerated artifacts, the docs list and aminorchangeset. The claim and the dispatch said that "theComponentPropsMaprow ... go[es]". The precedent does not delete the row. It replaces the row's props schema with a whole-bag refusal, and its docblock says why: deleting the row would turn a loud retirement into a silent skip on every reader that dispatches on it. Two tests on this tree depend on that.component-type-vocabulary.test.tsrequires every member ofRETIRED_PAGE_COMPONENT_TYPESto stayisKnownComponentType. objectui'sregistry-inputs-spec-parity.test.tsat the pin classifies a retired type through its kept row. So the row stays andAIChatWindowPropsgoes. That is how "the type leaves ... itsComponentPropsMaprow" lands here.Two kit items go beyond PR #15112, because the ruling asked for them. #15112 had no D3 entry: its row had zero keys, and its disposition was
not-required (no-migration-prescription). This one has a D3 entry because triage named one. It is in the element-node shape ofelement-filter-and-form-node-refused. It also has aRETIRED_DEFS_BY_MAJORentry, because a published def leaves the build. No D2 conversion is registered. The precedent registered none, and the only edit is deleting an authored page node, which is a layout decision that conversions must not make.Census (A1), at
529d9711fbai:chat_window:examples/**0,packages/apps/**0,apps/**0,skills/**0.content/docs/**had 3 hits: 2 in the auto-generated references and 1 in thepages.mdxlist. The otherpackages/**hits were the spec's own list and row, its tests, and a frozen formatter fixture inscripts/format-type.test.ts, which is left as is, as feat(spec): user:profile is explicitly not author-placeable — refused by name at the schema door (#14159) #15112 leftuser:profilein it. Control in the same query shape:element:dividerappears in 3 example files andrecord:detailsin 12.AIChatWindowProps: the def, the generated artifacts, and one illustrative comment inpackages/lint. That comment is updated, and it is not a consumer.89cad75d55: there is no renderer, and the palette excludes the type. No non-test source importsAIChatWindowPropsor indexes the row. The'ai:chat_window'key sits only inRecordobjects keyed by string, and in comments and tests. So the Console Pin Gate's build does not reference the removed names.skills/**names nothing, so it is untouched (no Tier H).Ablation (both legs committed first, mutated through
scripts/ablation-replace.mjswith an anchor that must hit, restored by blob)The subject resolves by relative import (
./page.zod,./component.zod) tosrc, so nodistrebuild was involved.ai:chat_windowentry fromRETIRED_PAGE_COMPONENT_TYPES(anchor 1 → 0, blob7d2faf0b1f79→501c4251ee90). The observed direction is louder than the pin.component.test.tsfails to load, with 1 file failed and no tests run. The error reads: retiredComponentProps:ai:chat_windowhas no RETIRED_PAGE_COMPONENT_TYPES entry (page.zod.ts). The kept row's factory refuses a row that has lost its prescription.emptyProps('ai:chat_window'). Result: 5 failed and 360 passed. The failures are the map pin, the row pin,PageComponentSchemaby name,PageSchemaat the element path, and the enum error map.HEAD(7d2faf0b1f79,891d947a3232),git diff HEADis empty, andgit statusis clean. Re-run after the restore:component.test.tshas 365 of 365 passing.Gates, at
3f17d5229e(mergedorigin/main88fb5e85a0, which touched no spec, lint or docs file this PR edits)@objectstack/specbuildexit 0.check:generated:✓ All 15 generated artifacts are up to date.@objectstack/spectest(--project local): 603 files, 17845 passed and 1 todo.typecheckexit 0, withcheck:test-typecheck: OK.@objectstack/lintbuild closure, thentest: 119 files, 5615 passed and 5 skipped.flags ai:chat_windowruns off the map.typecheckexit 0.check:livenessexit 0,check:migration-registry✓ ... is current (354 semantic, 246 retired-key, 218 retired-def),check:doc-authoringexit 0,check:nul-bytesOK (... no raw ASCII control bytes).--eventpayload:check-adr-0087-registration✓ 1 declared-breaking changeset(s) ... registered ui-ai-chat-window-retired (new here: ui-ai-chat-window-retired).check-changeset-no-major✓ LEVEL AXIS: this PR declares clause-② yes (narrowing).check-empty-changesetexit 0.dispatch-gates --ran:✓ 114 derived famil(ies) accounted for — 112 run, 2 NOT-MEASURED.check:dual-build-cjs-loads, reason: it needs a whole-repopnpm build, and it exited 3 with PREREQUISITE NOT MET.check:type-check-debt, reason: repo-wide tsc, killed by a 420s per-gate timeout on the shared box. The two packages this PR touches typecheck green.@objectstack/spectest:repo(--project repo), reason: one idle worker under 2% CPU for 10 minutes with no output, stopped. It is CI's.Acceptance notes
@objectstack/spec. It is the same step objectui#7122 took foruser:profileandelement:form. At the pin,block-config.test.ts("every exclusion names a real spec type") and theUNJUDGED_SPEC_BLOCKSentry inregistry-inputs-spec-parity.test.ts("NOT REGISTERED, DELIBERATELY", whose class is exclusive with "RETIRED UPSTREAM") both still treatai:chat_windowas a live enum member. They are objectui's tests against its installed spec, so they do not affect this repo's CI. Who picks it up: the next objectui spec-bump PR.no-readerreason kind no longer has a user. It is left as ledger vocabulary.defaultAgentfor whatagentIdwas used for. objectui at the pin forwardsapp.defaultAgent, limited toask/build, to the chat dock's one agent resolver (app-shell/src/layout/ChatDock.tsx:242,hooks/surfaceAgent.ts:127-128).Generated by Claude Code