Skip to content

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

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-21504-retire-ai-chat-window
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-21504-retire-ai-chat-window

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21504
Clause-②: yes (narrowing)

Retires the ai:chat_window page component type under ADR-0049 enforce-or-remove, refused by name through RETIRED_PAGE_COMPONENT_TYPES, per triage ruling 5963897014 and the user:profile precedent (#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:suggestion is not touched; triage ruled it a different class.

What changed

surface change
ui/page.zod.ts 'ai:chat_window' leaves PageComponentType and joins RETIRED_PAGE_COMPONENT_TYPES. Its message names the floating chat overlay as the supported entry point and carries no tracker number. PageComponentSchema.type already 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.ts AIChatWindowProps (mode, agentId, context, aria) is deleted. The ComponentPropsMap['ai:chat_window'] row stays as retiredComponentProps('ai:chat_window'): a z.never that refuses every bag, {} included, with the same prescription.
ADR-0087 D3 semantic entry ui-ai-chat-window-retired, one STEP18_RATIONALE fragment, and the RETIRED_DEFS_BY_MAJOR[18] entry ui/AIChatWindowProps, generated by gen:migration-registry. No D2 conversion, see below.
pins component.test.ts gets a new describe with code, path, params and the first sentence at each door: the map, the row (empty and populated bags), PageComponentSchema, PageSchema at the element path, and the enum error map. ai:suggestion is 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, and ai: stays reserved. The old should parse ai:chat_window with default accept pin is flipped into the new describe.
enumeration pin component-props-unknown-members.pin.test.ts (PR #21502) loses its ai:chat_window context{} "no-reader" line, because the member left with the def.
generated 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.json is untouched.
docs content/docs/ui/pages.mdx component list says the truth.
lint (comment only) validate-page-field-bindings.ts used AIChatWindowProps.agentId as its example of a non-field string prop. The example now names a live one, ElementImagePropsSchema.alt.
changeset @objectstack/spec minor, BREAKING banner, Clause-②: yes (narrowing), FROM → TO table, ADR-0087 marker registered 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 a minor changeset. The claim and the dispatch said that "the ComponentPropsMap row ... 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.ts requires every member of RETIRED_PAGE_COMPONENT_TYPES to stay isKnownComponentType. objectui's registry-inputs-spec-parity.test.ts at the pin classifies a retired type through its kept row. So the row stays and AIChatWindowProps goes. That is how "the type leaves ... its ComponentPropsMap row" 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 of element-filter-and-form-node-refused. It also has a RETIRED_DEFS_BY_MAJOR entry, 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 529d9711fb

  • ai: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 the pages.mdx list. The other packages/** hits were the spec's own list and row, its tests, and a frozen formatter fixture in scripts/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 left user:profile in it. Control in the same query shape: element:divider appears in 3 example files and record:details in 12.
  • AIChatWindowProps: the def, the generated artifacts, and one illustrative comment in packages/lint. That comment is updated, and it is not a consumer.
  • objectui at the pin 89cad75d55: there is no renderer, and the palette excludes the type. No non-test source imports AIChatWindowProps or indexes the row. The 'ai:chat_window' key sits only in Record objects 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.mjs with an anchor that must hit, restored by blob)

The subject resolves by relative import (./page.zod, ./component.zod) to src, so no dist rebuild was involved.

  • Leg A, the dispatch's ablation: delete the ai:chat_window entry from RETIRED_PAGE_COMPONENT_TYPES (anchor 1 → 0, blob 7d2faf0b1f79 → 501c4251ee90). The observed direction is louder than the pin. component.test.ts fails to load, with 1 file failed and no tests run. The error reads: retiredComponentProps: ai:chat_window has no RETIRED_PAGE_COMPONENT_TYPES entry (page.zod.ts). The kept row's factory refuses a row that has lost its prescription.
  • Leg B, so the by-name pins themselves judge: the same deletion, plus the row turned into a non-throwing emptyProps('ai:chat_window'). Result: 5 failed and 360 passed. The failures are the map pin, the row pin, PageComponentSchema by name, PageSchema at the element path, and the enum error map.
  • Restore: both files' blobs equal HEAD (7d2faf0b1f79, 891d947a3232), git diff HEAD is empty, and git status is clean. Re-run after the restore: component.test.ts has 365 of 365 passing.

Gates, at 3f17d5229e (merged origin/main 88fb5e85a0, which touched no spec, lint or docs file this PR edits)

  • @objectstack/spec build exit 0. check:generated: ✓ All 15 generated artifacts are up to date.
  • @objectstack/spec test (--project local): 603 files, 17845 passed and 1 todo. typecheck exit 0, with check:test-typecheck: OK.
  • @objectstack/lint build closure, then test: 119 files, 5615 passed and 5 skipped. flags ai:chat_window runs off the map. typecheck exit 0.
  • check:liveness exit 0, check:migration-registry ✓ ... is current (354 semantic, 246 retired-key, 218 retired-def), check:doc-authoring exit 0, check:nul-bytes OK (... no raw ASCII control bytes).
  • Changeset gates, with this body as the --event payload: 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-changeset exit 0.
  • dispatch-gates --ran: ✓ 114 derived famil(ies) accounted for — 112 run, 2 NOT-MEASURED.
    • NOT MEASURED: check:dual-build-cjs-loads, reason: it needs a whole-repo pnpm build, and it exited 3 with PREREQUISITE NOT MET.
    • NOT MEASURED: 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.
    • Both are declared to CI.
  • NOT MEASURED: @objectstack/spec test:repo (--project repo), reason: one idle worker under 2% CPU for 10 minutes with no output, stopped. It is CI's.

Acceptance notes

  • objectui will need a reconciliation when it next bumps @objectstack/spec. It is the same step objectui#7122 took for user:profile and element:form. At the pin, block-config.test.ts ("every exclusion names a real spec type") and the UNJUDGED_SPEC_BLOCKS entry in registry-inputs-spec-parity.test.ts ("NOT REGISTERED, DELIBERATELY", whose class is exclusive with "RETIRED UPSTREAM") both still treat ai:chat_window as 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.
  • The enumeration pin's no-reader reason kind no longer has a user. It is left as ledger vocabulary.
  • The prescription names defaultAgent for what agentId was used for. objectui at the pin forwards app.defaultAgent, limited to ask / 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

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:ui tests tooling labels Oct 3, 2026
@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec, touching 10 documentable anchor(s). ⚠️ 9 changed file(s) yielded no anchor (packages/lint/src/validate-page-field-bindings.ts, packages/spec/api-surface/ui.json, packages/spec/authorable-defaults/ui.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/cluster.mdx (via RETIRED_DEFS_BY_MAJOR (symbol, a top-level const object))
  • content/docs/protocol/objectui/layout-dsl.mdx (via ComponentPropsMap (symbol, a top-level const object), PageComponentSchema (symbol, a top-level const))
  • content/docs/ui/pages.mdx (via PageComponentType (symbol, a top-level const object))

⛔ 6 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via PageComponentSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-1.mdx (via ComponentPropsMap (symbol, a top-level const object))
  • content/docs/releases/v17/17-3.mdx (via ComponentPropsMap (symbol, a top-level const object))
  • content/docs/releases/v17/17-4.mdx (via ComponentPropsMap (symbol, a top-level const object))
  • content/docs/releases/v17/17-5.mdx (via ComponentPropsMap (symbol, a top-level const object))
  • content/docs/releases/v17/17-6.mdx (via RETIRED_DEFS_BY_MAJOR (symbol, a top-level const object))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 9 changed file(s) yielded no anchor (packages/lint/src/validate-page-field-bindings.ts, packages/spec/api-surface/ui.json, packages/spec/authorable-defaults/ui.json, …) — pages documenting those are invisible to this run
  • 8 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 138 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json ad7c3518983a1bb63fd4601954ac92d055124e42 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7846296e0a847b299ea993b44b7adb5ad58369dc — the merge of head 3f17d5229e35c592af07f771f7d1118f26e2678e into base ad7c3518983a1bb63fd4601954ac92d055124e42, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs ad7c3518983a1bb63fd4601954ac92d055124e42 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 3f17d5229e35c592af07f771f7d1118f26e2678e
Local-runs: none

Read-only review of PR #21531 (card #21504) against the net diff on main at the head above, the card's body and every comment (ruling 5963897014, claim 5964418092, dev report 5965081737), the head's check-runs, and the precedent the ruling names: #14159 and its landing PR #15112 (97a22639b4), read at refs. Nothing was built, run or re-run.

Check-runs on the head, read at 2026-10-03T03:42Z: 32 runs — 18 success, 2 skipped, 12 in_progress, 0 failed. Success includes Check Changeset, Spec property liveness, Governed Surface Queue Guard, Build Core, Build Docs, Type Check · source gates, Type Check · debt ledger, Dogfood Verify CLI, Dogfood Regression Gate (3/3), the card/branch/single-writer claim checks and the docs checks. Skipped: Console Pin Gate (the filter job's console output did not select it — no console build input moved) and the opt-in packed-tarball smoke. Still in progress: Test Core 1–6/6, Lint & Repo Gates, Type Check · workspace, Type Check · consumer gates, Dogfood Regression Gate 1/3 and 2/3, Temporal Conformance. An in-progress gate is a reading, not a pass: those families own their own verdicts, and the landing waits on them as it did for #15112. Nothing judged below is contradicted by any completed run.

① Derived judgments

Every accept-set and public-surface change the diff implies, named right or wrong against the ruling and the precedent's actual shape.

  1. PageComponentType loses 'ai:chat_window' (ui/page.zod.ts) — an accept-set narrowing on the enum parsed alone; the enum's own error map, keyed on issue.input, returns the prescription only for the ex-legal value (pinned, with ai:chat as the stranger control). Right — the precedent's element 1, the same shape.
  2. PageComponentSchema.type refuses the name at the node — the existing superRefine consults RETIRED_PAGE_COMPONENT_TYPES, so the new map entry alone closes the open string arm: code: 'custom', params.retiredComponentType, the node's own path, bare and populated, and at the element path of an authored page. Right — this is the door the ruling's "⛔ de-advertising alone is not enough" demands, and the precedent's element 2; no new mechanism was needed because feat(spec): user:profile is explicitly not author-placeable — refused by name at the schema door (#14159) #15112 built it.
  3. ComponentPropsMap['ai:chat_window'] becomes retiredComponentProps('ai:chat_window') — z.never, refusing {} and populated bags with invalid_type and the same string. Right, and the one place the claim's wording and the landing differ. The claim and the ruling's bullet say the row "goes"; the ruling's governing sentence is "follows the user:profile precedent", and PR feat(spec): user:profile is explicitly not author-placeable — refused by name at the schema door (#14159) #15112 kept the row (its component.zod.ts adds retiredComponentProps, with a docblock saying why deleting the row would demote a loud retirement to a silent skip on every reader that dispatches on it). At this head component-type-vocabulary.ts derives KNOWN from the row keys and the vocabulary test requires every map member to stay known, so the row-less reading would have gone red. The type leaves the row as a live props schema; the row stays as the refusal. Judged as the precedent's shape, not a deviation from it.
  4. AIChatWindowProps leaves @objectstack/spec/ui, and ui/AIChatWindowProps leaves the published JSON Schema set — a public-surface removal the precedent never had (its row carried no def). Reflected in api-surface/, export-origins/, declaration-map/, the manifest key, four authorable-surface/ key lines, the authorable-defaults/ mode = "float" line and the docs-import baseline; registered as RETIRED_DEFS_BY_MAJOR[18] 'ui/AIChatWindowProps', beside the step-18 siblings ui/FormFieldPublicPicker and ui/Theme, which is what the manifest deletion gate prescribes. The def had zero readers (census: the only non-spec mention was an illustrative comment in packages/lint, now re-pointed; objectui at the pin imports nothing by that name). authorable-surface.base.json is correctly untouched: it is the pinned anchor for the deletion gate and still carries the earlier retired defs' lines. Right, and declared in the changeset under "What is removed from the exports".
  5. Vocabulary consequences — KNOWN_COMPONENT_TYPE_CANDIDATES loses the name (derived from the map, so the suggester cannot rename an author into it), isKnownComponentType('ai:chat_window') stays true, and RESERVED_COMPONENT_TYPE_NAMESPACES is unchanged because ai:suggestion still populates ai:. Pinned with ai:suggestion as the lit control. Right — the one divergence from feat(spec): user:profile is explicitly not author-placeable — refused by name at the schema door (#14159) #15112 (where user: left the reserved set) is the derivation doing its job.
  6. The kit: D3 entry ui-ai-chat-window-retired under step 18, its STEP18_RATIONALE fragment, the retired-def entry, and no D2 conversion. The ruling asked for a D3 entry; feat(spec): user:profile is explicitly not author-placeable — refused by name at the schema door (#14159) #15112 had none (disposition not-required), so this goes beyond the precedent on the ruling's own instruction. No conversion matches the precedent's reasoning: the only edit is deleting an authored node, which the conversion layer does not do. Step 18 is where this head's post-cut retirements already sit (translation-widget-sub-caption-retired, ui-form-layout-inline-grid-retired). The entry's replacement and the door's prescription carry one instruction, and its acceptanceCriteria names the params.retiredComponentType the door actually emits. Right. spec-changes.json and docs/protocol-upgrade-guide.md are untouched, and correctly so: at this head neither projects any step-18 entry yet (the landed siblings are absent from both), so the changeset's line saying they "follow the schema" is prose ahead of the projection, not a missing regeneration.
  7. The prescription's substance — names the floating chat overlay as the entry point, gives the imperative fix, cites ADR-0049, carries no tracker number (pinned), and points agentId's intent at the app's defaultAgent. Verified against ui/app.zod.ts at the head: defaultAgent is the ADR-0063 surface-binding knob whose resolvable values are the two platform agents, ask by default and build for authoring surfaces, resolved by the assistant chat endpoint. Right.
  8. ai:suggestion untouched — pinned as the control at every door. Right, per the ruling's "Not here".
  9. Everything else is comment, docs or generated: the enumeration pin loses its ai:chat_window no-reader line because the row has no members; the packages/lint docblock example is re-pointed at a live prop; content/docs/ui/pages.mdx says the truth; the references and the strictness counts are regenerated. scripts/format-type.test.ts's frozen fixture still lists the name exactly as feat(spec): user:profile is explicitly not author-placeable — refused by name at the schema door (#14159) #15112 left user:profile in it, and the dated 2026-06 liveness audit still lists it as aspirational as it does element:filter / element:form — records, not drift. Right.
  10. One residual the review found (not a dev flag): the KNOWN_COMPONENT_TYPES docblock in component-type-vocabulary.ts enumerates the kept retired rows as user:profile plus element:filter / element:form; ai:chat_window now belongs to that list, so the sentence is one member short. Comment-only, no publish change — the same class feat(spec): user:profile is explicitly not author-placeable — refused by name at the schema door (#14159) #15112's review carried as a report line (its deviation 2). Carried as a note for the next PR that edits that file; not a fail item.

② Semver level

Changeset .changeset/ai-chat-window-retired.md: @objectstack/spec minor, the BREAKING banner, Clause-②: yes (narrowing), a FROM → TO table, and the adr-0087 marker comment reading registered ui-ai-chat-window-retired. What the diff publishes: an accept-set narrowing at three doors (①1–3) and a public export removal (①4). Under strict semver both grade major; under the launch-window convention check-changeset-no-major.mjs enforces until GA, a breaking change ships as minor with the banner as the carrier — the level #15112 shipped at, and the level the ui/Theme and ui/FormFieldPublicPicker def retirements shipped at. The Check Changeset run on this head is success (the no-major guard, and the level axis: clause ② declared, @objectstack/spec graded minor). Level: right. The Clause-②: line reads yes (narrowing) in the PR body, the changeset and the claim, and the content is a narrowing (an authored type refused where it parsed clean before). Right. The ADR-0087 disposition is registered, not the precedent's not-required, because a D3 entry exists here. Right.

③ Boundary flags

Dev flags (eight deviations, two out-of-scope findings, no open questions), each answered:

Implemented-by: claude/issue-21504-retire-ai-chat-window
Reviewed-by: session_01YDt3PzwfrkuFzUBF89WPmM

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

ACCEPT — PR #21531 @ 3f17d522 (#21504)

domain:spec seat 2 (session_01YDt3PzwfrkuFzUBF89WPmM), holder of claim 5964418092 · 2026-10-03T03:55Z

  • Shape (read on GitHub): a draft against main. The first line is Fixes #21504, and Clause-②: yes (narrowing) sits at a line start. 22 files, +393 / −74, most of them generated.
  • Review: the contract review at CONTRACT_REVIEW_TIER, 5965171663, reads PASS on this head.
    • Retired at three doors: 'ai:chat_window' leaves PageComponentType, and the page node refuses it by name through RETIRED_PAGE_COMPONENT_TYPES. The refusal message names the floating chat overlay, points agentId's intent at the app's defaultAgent, and carries no tracker number.
    • The props bag is refused too: the ComponentPropsMap row becomes retiredComponentProps, which refuses every bag.
    • AIChatWindowProps leaves the public surface. It is registered in RETIRED_DEFS_BY_MAJOR[18], and the D3 entry ui-ai-chat-window-retired sits under step 18. There is no D2 conversion, as in the precedent.
    • ai:suggestion is untouched and is the pinned control.
    • Release: @objectstack/spec minor with the BREAKING banner and a FROM → TO table. The ADR-0087 marker reads registered.
  • The row is kept, and this seat accepts it. The claim said the ComponentPropsMap row "goes". The ruling's governing sentence is "follows the user:profile precedent", and PR feat(spec): user:profile is explicitly not author-placeable — refused by name at the schema door (#14159) #15112 kept the row as the refusal. The vocabulary test at this base also requires every retired type to stay known. The claim's wording was this seat's paraphrase, and the precedent's shape governs. The review reads the row the same way (①3).
  • CI on 3f17d522: 33 success and 2 skipped by design (Console Pin Gate, packed-tarball smoke).
  • Governed surface: check-governed-merges --pr 21531 reads NOT governed: 0 of 22 paths, 467 changed lines.
  • Serial: at this read, no other open PR touches ui/page.zod.ts, ui/component.zod.ts, the enumeration pin or migrations/registry.ts. [Decision] page requires on the page kinds whose source is never compiled at save (react, full, slotted): refuse it at parse, or keep admitting a key nothing derives? #21459 (ruled A, PageSchema.requires) also writes ui/page.zod.ts and the step-18 rationale. Its claim names this PR, and its dev merges main after this lands.

Carried, decided here:

Next: ready, auto-merge, the queue.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 3, 2026 03:57
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 3, 2026 03:57
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 3, 2026
Merged via the queue into main with commit 48eb9c1 Oct 3, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21504-retire-ai-chat-window branch October 3, 2026 04:28
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/m tests tooling

Projects

None yet

2 participants