Skip to content

feat(spec,lint): one derived list of page-component slot positions, read by all three page walks (#20940) - #20961

Merged
os-justin merged 5 commits into
mainfrom
claude/issue-20940-page-slot-positions
Oct 1, 2026
Merged

os-justin merged 5 commits into
mainfrom
claude/issue-20940-page-slot-positions

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Closes #20940

Clause-②: yes (widening)

The declaration above is the seat's: ruling 5921583377 took the dev's recommendation, and the claim line was corrected in place to match (scripts/pm/clause2-line.mjs:90: two new exports expand the public surface). The reasoning is in "Clause ② re-read" below.

What this does

Three page walks descend into a component's properties bag, and each kept its own list of positions:

  • the ADR-0087 conversion walker: a module-private COMPONENT_CHILD_KEYS plus items[].children;
  • @objectstack/lint's walkPageComponents: items[].children, children, body, footer;
  • the exported walkAddressedPageComponents: children, items[].children.

So a node in a page:card footer was judged by os lint and skipped by every consumer of the exported walk: translatePage, the CLI extractor and objectui's validator.

Now packages/spec owns one list, derived from the component rows. There is no fourth list.

  • The rows declare the fact. componentSlot() / retiredComponentSlot() are module-private markers in ui/component.zod.ts. Each registers the exact schema instance a row's shape holds, and returns it unchanged, so the parse and the JSON Schema are the unmarked schema's. Five keys are marked componentSlot: PageContainerProps.children, PageCardProps.children, PageCardProps.footer, and children on the page:tabs and page:accordion panel items. PageCardProps.body, a retiredKey tombstone, is marked retiredComponentSlot.
  • pageComponentSlotPositions() derives the list from ComponentPropsMap. It is exported with its type PageComponentSlotPosition from @objectstack/spec/ui. It returns children, body (retired), footer, and the panel position items[].children. Order is the visit order: direct slots in row order, then panels, so children is still walked before items[].children, the order the exported walk's nested-id arbitration has used since i18n: two surfaces a translation bundle cannot address at all — walkAddressedPageComponents misses a slotted page's slots and a tabs node's items[].children, and dashboards.NAME has no globalFilters group #16772. The list is derived on first call and memoized, never at import. The first call reads every row's shape, which constructs the lazySchema rows: measured 17.6 ms, once per process. Two rows that contradict each other on one position throw.
  • All three walks read it. The conversion walker reads every entry. walkAddressedPageComponents and lint's walkPageComponents read the authorable entries, so page:card.footer is now descended by all three.
  • body is handled per the standing ruling. SDUI props 声明与 renderer 不一致:6 处「renderer 兑现但 ComponentPropsMap 未声明」+ 2 处「声明了没人读」(#5068 error 升级的 spec 侧前置) #5775 (maintainer ruling 2026-08-06, direction A, recorded in docs/protocol-upgrade-guide.md and in the PageCardProps.body tombstone and the PageContainerProps guidance) made children the one composition key. It kept the renderers' body read as a back-compat fallback for stored documents, not as an authorable spelling. So:
    • The exported walk still does not descend body.
    • Lint's descent of body was re-read against that ruling, as the claim asked, and removed. An author who writes body is refused by the tombstone's own rename prescription. Judging the sub-tree under a refused key as if it were authored is what the ruling rules out. After the rename the sub-tree is judged under children.
    • The conversion walker keeps descending body. Stored documents still carry it, and a conversion ordered before page-card-body-to-children (for example page-header-subtitle-alias) meets the sub-tree there. page-component-walk.test.ts pins that reach, and removing it would be a reach regression the claim did not order.
  • Recursive typing of children is declined. The component rows are not reached by any load-path parse: PageComponentSchema.properties is an open bag. So a recursive children type would take effect only in lint's props gate, which already judges every nested node through the walk. It would also narrow a published accept set (children legally holds bare id strings and null). And it would not replace any consumer's walk, as the card itself notes.

Clause ② re-read — why the declaration is yes (widening)

  • The claim's original no covered the walk: the exported walk descends a slot the rows already declared. But the design the claim orders ("packages/spec owns one list … lint's walkPageComponents reads it") needs @objectstack/lint, a separate package, to import the list. So @objectstack/spec grows two exports, and api-surface/ui.json gains two rows in this diff.
  • scripts/pm/clause2-line.mjs states the question the declaration answers: 「本卡放宽接受集或扩大公开面吗」. The public surface grows, so the answer is yes.
  • The level rule the gate mechanizes (pr-automation.yml, "WHICH LEVEL", ruling 2026-09-04) says: "a new exported symbol on an index" is a widening that takes at least minor.
  • scripts/pm/check-widening-tells.mjs names "T3: a new row in a published entry point's export listing (packages/spec/api-surface/*.json)" as a widening tell. Under a no it refuses the enqueue.
  • The changeset grades @objectstack/spec minor, so check-changeset-no-major's level axis passes under either value. The seat took this in ruling 5921583377.

Changeset levels

Verification — at d9d0d38cfc (this PR's head when opened)

  • The cut round at 5dc435cb07 re-ran the spec and lint suites and typechecks, check:generated, and the derived gates (report 5922116038 on spec(ui): walkAddressedPageComponents skips page:card's properties.footer as a back-compat spelling, but PageCardProps.footer is a declared, rendered slot, so nodes there go unjudged; the three platform page walks disagree on positions #20940): all green, 91 of 91 derived gates exit 0.

  • pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: 584 files, 17196 passed, 1 todo, exit 0.

  • pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2: 117 files, 5440 passed, exit 0.

  • pnpm --filter @objectstack/spec typecheck && pnpm --filter @objectstack/lint typecheck: exit 0. check:test-typecheck held for both, with the debt ledgers unchanged.

  • pnpm --filter @objectstack/spec check:generated: exit 1 at first, on api-surface/ and export-origins/ (the two new exports). --fix regenerated exactly those two, adding +2 rows each (commit d9d0d38cfc). Then "All 15 generated artifacts are up to date", exit 0. check:authorable-surface and check:docs were green throughout, which is the marker's no-schema-change claim, measured.

  • These gates exited 0: check-adr-0087-registration --base origin/main, check-changeset-no-major --base origin/main, check-empty-changeset --base origin/main, check-closing-keyword-parity, check-issue-citations, check-spec-docblock-symbol-anchors, check:nul-bytes, check:doc-authoring, check:cross-package-test-inputs, check:test-source-alias, check-undeclared-dep-imports, check:issue-citations. They ran at 3dee2204c9; the commit since only adds four generated JSON lines.

  • The changed-file suites were run at each commit: component.test.ts, i18n-resolver.test.ts, page-component-walk.test.ts (671 passed), and lint page-walk.test.ts plus page-walk-conversion-parity.test.ts (21 passed).

Ablations — each run from the committed head, restored and re-greened

All three used scripts/ablation-replace.mjs, whose anchor hit 1 → 0 with the blob changed. Each was restored with git checkout HEAD, and the blob equals HEAD with git diff HEAD empty.

  1. The exported walk stops descending footer, by filtering footer out of its positions.
    • The spec source suite went red, 3 of 300: the footer translate case, the walk trace, and "hands a malformed node in a page:card footer to the visitor".
    • Spec was rebuilt, and ablation-dist-preflight found the marker in 4 built files.
    • Lint's three-walk parity case, which reads spec's dist/, went red, 1 of 5.
    • Restore leg: spec rebuilt, the preflight --absent was clean with a clean tree, and both suites were green again (300/300, 5/5).
  2. Lint's walk takes every list entry, the retired body included, which is its pre-change reach. Lint went red, 4 of 21: the walk case, both conversions:page-component 改写只走 regions[].components[] —— slots.* 与容器嵌套(lint 的 walkPageComponents 会下钻)全部漏改,page-header-subtitle-alias 因此覆盖不到 spec-valid 的 header 节点 #6775 parity cases, and the three-walk parity case. Restored: 21/21.
  3. The row stops marking footer: componentSlot( was removed from PageCardProps.footer. Spec went red, 6 of 671: the derived-list pin, the three exported-walk footer cases, the conversion walker's footer case, and its list-completeness case. This shows the list is derived from the row marker, not written down anywhere else. Restored: 671/671.

Not measured when this PR was opened

Acceptance notes

  • Consumers. objectui needs no change here. Its pin (e) goes red at the spec bump, and objectui deletes that row, as the card and triage say.
  • Visit order. Within one component, lint and the conversion walker now visit direct slots before panels. Previously they visited items[].children first. This is observable only as finding or notice order, for a component carrying both a direct slot and panels.
  • New import in conversions/walk.ts. It now imports ui/component.zod.ts. component.zod.ts's import closure (49 files) was measured and reaches no conversions/ module, so there is no cycle. The derived list is read only inside functions.
  • Prose this change made stale is corrected in this PR (5dc435cb07; the review adoption 5921754490 named this PR the carrier): packages/spec/src/automation/region-slots.ts:35-37, packages/lint/src/page-envelope-audit.ts:59, and packages/lint/src/validate-visibility-predicates.ts:1236 with its test comment at :497.
  • packages/cli/src/utils/i18n-extract.ts comments. Only comments changed there, where they stated the old positions (body/footer undescended; lint's walk "wider"). The claim scopes that file to "where the widened walk moves an assertion". These are prose assertions the widening made false.

Generated by Claude Code

…ead by all three page walks (#20940)

The component rows mark their composition slots (componentSlot /
retiredComponentSlot); pageComponentSlotPositions() derives the one list
from ComponentPropsMap. The conversion walker reads every entry, the
exported walkAddressedPageComponents and lint's walkPageComponents read
the authorable entries: page:card footer is now descended by all three,
and the retired page:card body only by the conversion walker.

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
…arity (#20940)

- component.test.ts: the derived list, its derivation from the rows, the
  marker's transparency, memoization.
- i18n-resolver.test.ts: footer descended and translated; a malformed node
  in a card footer reaches a judging visitor; body still unvisited.
- page-component-walk.test.ts: the conversion walker reaches every listed
  position, retired included.
- lint page-walk tests: footer walked, retired body not; the three walks
  reach the same probes, the retired spelling aside.
- cli parity test: card_footer_child offered and applied on both sides.

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/cli, @objectstack/lint, @objectstack/spec, touching 24 documentable anchor(s). ⚠️ 4 changed file(s) yielded no anchor (packages/lint/src/page-envelope-audit.ts, packages/spec/api-surface/ui.json, packages/spec/export-origins/ui.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/releases/v15.mdx (via PageTabsProps (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
  • 4 changed file(s) yielded no anchor (packages/lint/src/page-envelope-audit.ts, packages/spec/api-surface/ui.json, packages/spec/export-origins/ui.json, …) — pages documenting those are invisible to this run
  • 5 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 — 143 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 5f6b63a6fd71abe96638cec5307d0b1ca38298ba → packageMentionDocs.

Which tree this was computed on

This run read content/docs from d36ccefcbf433f98a1756726bd031d5a6513778f — the merge of head 5dc435cb07ead219e9ff00468d5a4486c16acb56 into base 5f6b63a6fd71abe96638cec5307d0b1ca38298ba, 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 d36ccefcbf433f98a1756726bd031d5a6513778f && git checkout d36ccefcbf433f98a1756726bd031d5a6513778f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5f6b63a6fd71abe96638cec5307d0b1ca38298ba 5dc435cb07ead219e9ff00468d5a4486c16acb56 && git checkout -B drift-repro 5f6b63a6fd71abe96638cec5307d0b1ca38298ba && git merge --no-ff 5dc435cb07ead219e9ff00468d5a4486c16acb56

node scripts/docs-audit/affected-docs.mjs --json 5f6b63a6fd71abe96638cec5307d0b1ca38298ba

⚠️ 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 5f6b63a6fd71abe96638cec5307d0b1ca38298ba → 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: d9d0d38cfc558fddc6b7e426e828bdf84ab51af9
Local-runs: none

Inputs read: card #20940 (body; comments 5920446688, 5920568114, 5921521364, 5921583377), #5775 (body and the 2026-08-06 ruling comment 5202137085), #20969, PR #20961 (body, 14-file list, git diff d7b98176...d9d0d38c, merge-base 75519e1c0a), and the check-runs on the head. Nothing built, run or re-run; the tree was read through git show / git grep only.

① Derived judgments

Accept set: unchanged — right. componentSlot() / retiredComponentSlot() put the instance they are handed into a module-private WeakMap and return that same instance; strictObject hands shape to z.object and closedObject re-instantiates from the same _zod.def, so PageCardProps.shape.footer IS the marked instance and parses as before, and the body tombstone still fires through retiredComponentSlot(retiredKey(...)) (component.test.ts pins the JSON Schema equality, the parse and the tombstone). The only projections that moved are api-surface/ui.json and export-origins/ui.json, +2 rows each; api-surface-signatures.json hashes define* factories only, so no row is owed there. Exactly five componentSlot( calls (component.zod.ts:496 PageContainerProps.children, :872 page:tabs items[].children, :922 PageCardProps.children, :944 PageCardProps.footer, :2092 page:accordion items[].children) and one retiredComponentSlot( (:932 PageCardProps.body) — the PR body's count is exact.

Public surface: widened by exactly two exports on @objectstack/spec/ui — right. pageComponentSlotPositions (function) and PageComponentSlotPosition (interface). Nothing else is added, removed or re-typed; walkAddressedPageComponents' parameter and return shape are as #16772 left them.

One list, truly derived — right. The list is Object.entries(ComponentPropsMap) times each row's .shape (read through the lazySchema proxy, which forwards .shape) times the WeakMap; COMPONENT_CHILD_KEYS (conversions/walk.ts) and lint's hand list (page-walk.ts) are deleted. A grep of spec, lint and cli sources at the head for the 'body', 'footer' and items[].children spellings finds only prose (registry.ts:5318 and :5877 comments, page-envelope-audit.ts:59, validate-visibility-predicates.ts:1236) — descriptive text, no fourth walk. Derived order pinned by test: children, body (retired), footer, items[].children.

The three walks visit the positions they should — right.

  • Exported walkAddressedPageComponents: the authorable entries (children, footer, items[].children); body skipped. The re-attach after the visitor is now generic ({ ...next.properties, ...rebuilt }), so a footer is rebuilt exactly as children is. Footer descent is the card's own direction.
  • Lint walkPageComponents: the authorable entries; it stops descending body. Right against the triage ("not walked as authorable") and the standing ruling as the tree records it (protocol-upgrade-guide.md:124; the PageCardProps.body tombstone; PageContainerProps' body guidance at component.zod.ts:484-492). One consequence is stated too narrowly (see Text, item 2): matching is by SHAPE on every type, so a body array on ANY component — custom types included, which nothing refuses — is now undescended by lint and by everything built on it (validate-component-props, validate-visibility-predicates, page-envelope-audit door 2, and the CLI's walkObjectSections record:details pass, which imports lint's walk). No authored body: [ child list exists in examples/ or package sources at the head, so no in-repo corpus loses a finding.
  • Conversion walker: every entry; reach unchanged (children, body, footer, items[].children). Keeping body is right on the chain order: page-header-subtitle-alias (registry.ts:4936) runs before page-card-body-to-children (registry.ts:5408), so a stored card's sub-tree is still under body when the earlier conversion meets it. Visit order within one component is now direct slots before panels in both lint and the conversion walker — observable as finding / notice order only, and the PR body says so.
  • The triage pin "the three walks visit the same positions on one fixture page" is met on the authorable set, with the retired body as the one pinned difference (page-walk-conversion-parity.test.ts asserts lint equals exported, and conversion equals lint plus card body). The dev's reading is reasoned and the seat accepted it; I concur.

Consumers of the widened walk change only by the footer reach — right, and intended. translatePage: a footer component's copy is now translated through pages.name.components.id (i18n-resolver.test.ts flips the footer case to the translated string); no other route moves. CLI os i18n extract (emitPageComponentCopy): footer keys offered (platform-page-i18n-parity.test.ts adds card_footer_child on both sides); the coverage gate os i18n check consumes the same expected entries. i18n-extract.ts's diff is comments only. objectui's validator judges footer nodes; its pin (e) goes red at the spec bump, as the card states.

Lazy memoized derivation: no import-cycle or ordering hazard — right. The relative-import closure of ui/component.zod.ts at the head (55 files by my resolver; the dev counted 49) contains no conversions/ module and not system/i18n-resolver.ts (only system/constants/system-names.ts), so conversions/walk.ts importing ui/component.zod.ts, and system/i18n-resolver.ts importing both, closes no cycle. The derivation runs on first call inside a walk, never at import; the memo lives on the hoisted function object; the WeakMap const (component.zod.ts:438) is declared above the first marker call (:496), and lazySchema factories run later still (same order under OS_EAGER_SCHEMAS=1). A contradiction between two rows throws on the first walk rather than at build — stated in the docblock; acceptable.

Text someone acts on, tested sentence by sentence. The ones that fail:

  1. FALSE at every moment — the changeset (two sentences) and the PR body name os i18n coverage. No such command exists on main or after this PR: the CLI discovers commands by path (commands/i18n/check.js is os i18n check), packages/cli/src/commands/i18n/ holds check.ts and extract.ts only, I18nCheck declares no aliases, the oclif config maps none, and content/docs spell it os i18n check (eleven hits; "i18n coverage" in the docs is the noun phrase). The changeset is the text that ships as CHANGELOG to an upgrader, who would run a command that is refused. The same misnomer pre-exists in four shipped CHANGELOG rows and the i18n-resolver.ts docblock; that does not make new shipped text true. Fix: os i18n check.
  2. UNDER-BROAD — changeset, lint paragraph: "the rules built on this walk no longer report findings about nodes under a card's body". True of a card and silent about every other type's body array, which the shape match drops too. page-walk.ts's header carries the same framing ("An author who writes it is told by the tombstone itself" — only on page:card and the thin containers). Not false; incomplete.
  3. FALSE since the seat's edit at 23:32:18Z — the PR body's blockquote under the declaration ("The declaration above is the claim's line, copied verbatim as dispatched. The dev disagrees with it and recommends that it read yes."). The line above it now reads yes (widening), which is not the dispatched line, so the paragraph contradicts the line it annotates. PR-body edit; no head move.
  4. Made false by the diff without being touched (the dev's out-of-scope finding, each verified at the head): region-slots.ts:35-36 says spec/conversions/walk.ts "is a pure shape walker that takes no schema dependency" — walk.ts now imports ui/component.zod.ts (region-slots' own import-free constraint still holds and now matters more); page-envelope-audit.ts:59 says door 2 reaches page:card body/footer — body is no longer reached; validate-visibility-predicates.ts:1236 and its test comment at :497 name body as a walked position (the fixture itself uses page:tabs, so the test still passes). All three sit outside the claim's file surface; none has a carrier.

Sentences checked and found true: the five marked keys; #16772's changeset was BREAKING for the return shape only (spec CHANGELOG.md:9604 block), so no banner is owed here; "the commit since 3dee220 only adds four generated JSON lines" (two files, +4); "#5775, maintainer ruling 2026-08-06, direction A" — the ruling comment is headed 方向 A and carries the body item, and protocol-upgrade-guide.md:124 records it in those words; PageComponentSchema.properties is an open bag (z.record(z.unknown())); no load-path parse reaches ComponentPropsMap (no reader outside lint and component.zod.ts itself), which is the declined recursive typing's premise; the ~18 ms / 17.6 ms figure is dev-measured and the docblock says so.

② Semver level

  • @objectstack/spec: minor — right. Two new exported symbols on the ui index; WHICH LEVEL (pr-automation.yml:753-758) says at least minor.
  • @objectstack/lint: patch — right. No export added; walkPageComponents' signature unchanged; the dropped body reach is a diagnostic-reach change under a refused key, not a published accept set.
  • @objectstack/cli: patch — right. Comments only; the behaviour change arrives through the dependency, and the changeset is the upgrader's notice (harmless in the fixed group).
  • Clause-②: yes (widening) — right. The public surface grows by two exports; under no check-widening-tells refuses on two true T3 tells (api-surface/ui.json:320, :513). No (narrowing) arm is owed (no accept set narrows), no BREAKING banner and no ADR-0087 marker (nothing authorable moves, no stored shape or conversion id touched). Check Changeset concluded success at 23:32:25Z, after the PR-body edit, so the level axis was read under yes.
  • The claim comment 5920568114 still reads Clause-②: no (...) at both my reads (updated_at 22:07:38Z, unchanged); the ruling's in-place correction has not landed. The seat completes it.

③ Boundary flags

  • open_questions[0] (the declaration): answered — A, yes (widening). Landed on the PR line; the claim line is pending (② above); the PR blockquote now contradicts the line (①.3).
  • out_of_scope_findings[0] (false T1 in check-widening-tells): answered — filed as finding(pm): check-widening-tells reports a T1 tell on an existing key re-spelled inside a wrapper call (- body: retiredKey( → + body: retiredComponentSlot(retiredKey(); the #16943 net-delta replacement rule should pay it #20969, open, unlabelled, bare for triage. Moves no verdict on this PR.
  • out_of_scope_findings[1] (doc drift, three sites): verified real (①.4); escalated. "Recorded in the acceptance notes" is not a carrier. The seat either files a docs-only follow-up or, since the head moves anyway, admits the three comment lines into this claim's file surface for the rework (they are outside it today, so the dev was right to stop).
  • out_of_scope_findings[2] (objectui pin (e)): as the card states; objectui deletes the row at the spec bump. No action here.
  • Recursive typing of children declined: each reason verified (no load-path parse reaches the rows; it would narrow bare-id and null entries the slot legally holds; it replaces no consumer walk). Accepted.
  • No deviations key in the report: the os-dev template carries none; it is the field for refused commands, and none occurred (three relay writes, each read back). Not a gap.
  • Ablations A1-A3 and the 91-command gate run: as reported; not re-run.

Check-runs on the head (read last; 42 runs, 35 names after dedupe by newest started_at): 30 success, 5 skipped (Auto Label, Build Docs, Check PR Size, Console Pin Gate, Packed-tarball smoke (opt-in)); none failed, none still running. Every gate family answered green; the FAIL below is on shipped text, not on a gate.

What a PASS on the next head needs: (1) changeset: os i18n coverage becomes os i18n check in both sentences, and the lint paragraph names the shape-matched reach ("a body array on any component", not only "a card's"); (2) PR body: the same command fix, and the blockquote under the declaration deleted; (3) seat: the claim line corrected in place, and a carrier named for the three stale comments.

Implemented-by: claude/issue-20940-page-slot-positions
Reviewed-by: session_01Sfe5YjBLwB9J3y8fvm2xq1

VERDICT: FAIL

Adopted and posted by domain:spec seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1) · 2026-09-30T23:45Z · rendered by the seat's at-tier review subagent on this head. The seat read its served tier family from the subagent transcript before posting. FAIL accepted. The seat checked packages/cli/src/commands/i18n/ on main: it holds only check.ts and extract.ts, so os i18n coverage does not exist.

  • Cut round (the dev, on this branch):
    • The changeset's two os i18n coverage becomes os i18n check.
    • The changeset's lint paragraph is widened to what the shape match actually drops: any component's body array, not only a card's.
    • The three comments this diff makes false (region-slots.ts:35-36, page-envelope-audit.ts:59, validate-visibility-predicates.ts:1236 and its test :497) are corrected here. The carrier is this PR, because the diff makes them false. They are cuts, not new prose.
  • The PR body (the seat's, at the next head): the os i18n coverage sentence, and the blockquote under the declaration that now contradicts yes (widening).
  • The claim line: it has read yes (widening) since the seat's in-place edit at 2026-09-30T23:40Z. The review read the claim before that edit.
  • Next: a delta re-review at the new head. The pre-existing os i18n coverage rows in shipped CHANGELOGs and a docblock are not this PR's and stay as they are.

Generated by Claude Code

… note to any `body` array, cut three comments this diff made false (#20940)

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5dc435cb07ead219e9ff00468d5a4486c16acb56
Local-runs: none

Delta re-review of the prior FAIL record 5921754490 (at d9d0d38cfc). Inputs read: card #20940 (body; comments 5920446688, 5920568114 as corrected at 23:40:48Z, 5921521364, 5921583377, 5922116038), #5775 (body and the 2026-08-06 ruling comment 5202137085), #20969, PR #20961 (body as patched at 00:21:44Z, re-read after the seat's second patch at 00:46:42Z — see "Body re-read" below; the 18-file list, git diff d9d0d38cfc 5dc435cb07 for the delta and git diff 75519e1c0a 5dc435cb07 for the net diff against the merge-base), and the check-runs on the head. Nothing built, run or re-run; the tree was read through git show / git grep / git diff only.

The delta is exactly the ordered cut. One append-only commit, 5 files, +9/-10 (the cut-round report says +8/-9; git says +9/-10 — a miscount in the report's narrative, not in anything that ships): .changeset/page-component-slot-positions.md (three hunks), packages/spec/src/automation/region-slots.ts (docblock), packages/lint/src/page-envelope-audit.ts (docblock table), packages/lint/src/validate-visibility-predicates.ts (inline comment) and .test.ts (inline comment). No code line, no test assertion, no generated artifact moves, so the accept set and the public surface are exactly what the prior record judged at d9d0d38cfc; api-surface/ui.json and export-origins/ui.json are unchanged by the delta and owe no regeneration. The commit's trailer pair is model-free. No merge, no rebase: the net diff against 75519e1c0a is the prior 14 files plus the four comment-cut files, 18 files, +716/-209.

① Derived judgments

Accept set: unchanged — right (re-read, delta cannot touch it). Still exactly five componentSlot( calls at component.zod.ts :496, :872, :922, :944, :2092 and one retiredComponentSlot( at :932; the markers return the instance they are handed; check:authorable-surface / check:docs green on the head.

Public surface: widened by exactly two exports on @objectstack/spec/ui — right (re-read). pageComponentSlotPositions, PageComponentSlotPosition; nothing else added, removed or re-typed.

One derived list; the three walks; consumers; lazy derivation — right (re-read). The delta touches none of it. page-walk.ts at the head still reads pageComponentSlotPositions().filter(!retired) and matches each position by shape (Array.isArray(props[key])) on every component type; the exported walk does the same at i18n-resolver.ts:1971; the conversion walker reads every entry.

Text someone acts on — each edited sentence tested against the tree at the head.

Changeset (ships as CHANGELOG):

  1. "os i18n extract offers those keys, and os i18n check counts them" — TRUE on main when this PR lands. packages/cli/src/commands/i18n/ holds check.ts and extract.ts only; check.ts is headed "os i18n check — the coverage gate" and reports translated/expected, so "counts" is the right verb.
  2. "os i18n extract and os i18n check pick up the footer component keys through the shared walk" — TRUE (same tree facts; platform-page-i18n-parity.test.ts pins card_footer_child on both sides, unchanged by the delta).
  3. "it stops walking the retired body spelling. The walk matches by shape, so this drops a body array on any component, not only on page:card" — TRUE: the loop at page-walk.ts takes props[key] whenever it is an array, with no type dispatch; retiredComponentSlot is on PageCardProps.body alone, so "the retired spelling" is correctly singular and the reach statement is correctly general. This is the widening the prior record's item 2 asked for.
  4. "On page:card the tombstone's rename prescription still refuses body, and so does the thin containers' guidance" — TRUE and now correctly scoped: the tombstone at component.zod.ts:932 carries the rename prescription; PageContainerProps (page:section / page:footer / page:sidebar, named "thin containers" at :456) is a strictObject whose guidance.body entry (:490) is surfaced on unrecognized_keys (shared/strict-object.ts:458), i.e. a refusal. No other component refuses body, and the sentence no longer says any does.
  5. "the rules built on this walk no longer report findings about nodes under any component's body array" — TRUE, follows from 3.
  6. Every unedited changeset sentence stands as the prior record judged it. The one residual misnomer, os i18n coverage at i18n-resolver.ts:1867, is an unchanged context line that pre-exists on main (merge-base :1862); no added or removed line in the net diff carries the phrase. Not this PR's, as the adoption ruled.

Code comments cut (the prior record's item 4, each verified at the head):
7. region-slots.ts:35-37 "Deliberately import-free: this module has to be usable from spec/conversions/walk.ts" — TRUE: the module has zero import lines, and walk.ts:16 imports FLOW_REGION_SLOTS_BY_TYPE from it. The false clause ("walk.ts is a pure shape walker that takes no schema dependency" — walk.ts:18 now imports ui/component.zod.js) is gone; the re-wrap changes no other word.
8. page-envelope-audit.ts:59 door 2 "reaches ... page:card → footer" — TRUE: door 2 is walkPageComponents (:102, :323), which no longer descends body. The cell was an example list before (it never named children) and still is; a cut, not new prose.
9. validate-visibility-predicates.ts:1236 "page:card footer" — TRUE for the same reason (the rule iterates walkPageComponents, :1250); same pre-existing incompleteness (children unnamed), unchanged.
10. validate-visibility-predicates.test.ts:497 — TRUE: the fixture at :500-517 is a page:tabs with items[].children; the dropped page:card at properties.body clause described a position this walk no longer visits.

PR body (the seat's, patched at this head):
11. Changeset-levels bullet "os i18n extract / os i18n check now offer footer component keys" — TRUE in effect (the command exists; its expected-key denominator and missing-key listing now include footer keys). "Offer" is the changeset's verb for extract; for check the changeset's own "counts" is the exact one. Loose, not false.
12. The blockquote under the declaration: "ruling 5921583377 took the dev's recommendation, and the claim line was corrected in place to match (scripts/pm/clause2-line.mjs:90 ...)" — TRUE: comment 5920568114 reads Clause-②: yes (widening) with updated_at 2026-09-30T23:40Z; clause2-line.mjs:90-92 is the table row "yes / yes (widening) — a widening ... both take at least minor". The prior record's item 3 is closed.
13. (History — found FALSE at the 00:21:44Z body, closed by the 00:46:42Z body edit; see "Body re-read".) FALSE at the head — Acceptance notes, the paragraph "Prose outside the claim's file surface that this change makes stale. It is left for a carrier, and there is none named", with its three bullets. Every sentence in it was made false by the delta it is supposed to describe: the three sites are corrected IN THIS PR at 5dc435cb07 (the adoption named this PR the carrier); region-slots.ts no longer contains "walk.ts is a pure shape walker that takes no schema dependency"; page-envelope-audit.ts:59 names page:card → footer, not body/footer; validate-visibility-predicates.ts:1236 and the test comment at :497 no longer name body. A reader of the body at this head is told that three stale comments remain with no carrier, and the tree says the opposite. This is the one cut that made another sentence false. True at no moment from 5dc435cb07 on. Fix: a PR-body edit, no head move — e.g. "Prose this change made stale, corrected in the cut round (5dc435cb07): region-slots.ts:35-37, page-envelope-audit.ts:59, validate-visibility-predicates.ts:1236 and its test comment at :497."
14. (History — both retensed by the 00:46:42Z body edit; see "Body re-read".) Stale in tense, not false, non-blocking: (a) the "Clause ② re-read — the dev disagrees with no" section still says "The claim's reason covers the walk" and ends on "Recommendation: the seat rewrites the declaration to yes" — the claim line now carries yes (widening) and no reason, so the referent is historical; the blockquote above it frames the section as the reasoning the ruling adopted, which keeps it readable as a record. (b) "They ran at 3dee2204c9; the commit since only adds four generated JSON lines" is scoped by its heading "Verification — at d9d0d38cfc" and true inside that scope; at the head two commits follow 3dee2204c9. The cut round's readings live in report 5922116038 (91/91 gate commands exit 0, spec and lint suites green at 5dc435cb07), not in the body; the seat may add one line.
15. "Verification — at d9d0d38cfc (this PR's head when opened)" — TRUE: the PR was created 2026-09-30T23:00Z; d9d0d38cfc was committed 22:43:40Z and 5dc435cb07 at 23:47:28Z. The body's "It derives 89 commands" (Not-measured section) carries no tree stamp while both dev reports read 91 at d9d0d38cfc and at 5dc435cb07; the delta cannot touch it, it is not re-derived here, and the reports' readings are the ones of record.

Every other PR-body sentence was judged at d9d0d38cfc and the delta does not reach it; re-read, each stands.

② Semver level

  • @objectstack/spec: minor — right (two new exported symbols on the ui index; unchanged by the delta).
  • @objectstack/lint: patch — right (no export added; the changeset now states the dropped reach in its true breadth: a body array on any component, refused on page:card and the thin containers, silently undescended elsewhere — a diagnostic-reach change under the standing ruling, not a published accept set).
  • @objectstack/cli: patch — right (comments only; the changeset is the upgrader's notice and now names a command that exists).
  • Clause-②: yes (widening) — right, on the PR line and, since 23:40:48Z, on the claim line too; the two match. No (narrowing) arm, no BREAKING banner, no ADR-0087 marker owed (nothing authorable moves; the delta changes no schema). Check Changeset concluded success at 00:22:44Z, after the last body edit, so the level axis was read under the current body.

③ Boundary flags

Cut-round report 5922116038:

  • open_questions: empty. Nothing to answer.
  • out_of_scope_findings[0] (the three stale comments, "now corrected here"): verified at the head (①.7-10). Answered. Its consequence for the PR body (①.13) is the seat's, not the dev's.
  • out_of_scope_findings[1] (finding(pm): check-widening-tells reports a T1 tell on an existing key re-spelled inside a wrapper call (- body: retiredKey( → + body: retiredComponentSlot(retiredKey(); the #16943 net-delta replacement rule should pay it #20969): filed, open, since triaged (bug · priority:p3 · domain:spec · pm:queue). Moves no verdict here.
  • out_of_scope_findings[2] (objectui pin (e)): as the card states; objectui deletes the row at the spec bump. No action here.
  • The dev's explicit stop ("Not edited, because it is outside the three ordered edits: page-walk.ts's header sentence 'An author who writes it is told by the tombstone itself' ... It stays for the seat to judge"): answered — no edit owed. At the head the header names both refusers explicitly (the page:card tombstone and the container rows' guidance for page:section / page:footer / page:sidebar), and the in-function comment states the shape match ("Every authorable slot position, matched by SHAPE on any component type"), so the file as a whole says what the changeset now says. Under-broad in one sentence, not false; the dev was right to stop at the ordered edits.
  • "packages/cli was not touched this round ... its suites were not re-run" and "Ablations A1-A3 were not re-run: this round edits no code and no test assertion": both premises verified — the delta is prose only. The round-1 readings and ablations stand, and the head's check-runs re-ran every gate family.
  • The report's "+8/-9" is a miscount (git: +9/-10); nothing shipped depends on it.
  • No deviations key in either report; the template carries none, and no refused command occurred.

Prior record's "what a PASS needs", item by item: (1) changeset command fix, both sentences — done; lint paragraph widened — done; (2) PR body command fix — done; blockquote — rewritten to a true statement rather than deleted, which closes it; (3) claim line — corrected in place; carrier for the three comments — this PR, and the cuts landed. What the prior list could not foresee is ①.13: the acceptance-notes paragraph that described those comments as uncorrected and carrier-less is now contradicted by the head.

Check-runs on the head (read last; first read 42 runs, 35 names after dedupe by newest started_at): 30 success, 5 skipped (Auto Label, Build Docs, Check PR Size, Console Pin Gate, Packed-tarball smoke (opt-in)); none failed, none still running at that read. All seven required contexts concluded success: Lint & Repo Gates (00:03:50Z), TypeScript Type Check (00:03:32Z), Test Core (00:06:51Z), Dogfood Regression Gate (23:57:16Z), Build Core (23:53:10Z), Temporal Conformance (live PG + MySQL) (23:54:28Z), Governed Surface Queue Guard (23:48:32Z). The six runs started at 00:21:49-52Z were re-triggered by the body edit at 00:21:44Z and all concluded success or skipped. Every gate family answered green; at the 00:21:44Z body the one fail item was PR-body text alone (①.13), which the second body patch closed — see "Body re-read" for the re-read of the check-runs after it.

Body re-read — 2026-10-01T00:47Z, head unchanged at 5dc435cb07ead219e9ff00468d5a4486c16acb56

The seat patched the PR body a second time at 00:46:42Z (updated_at); the head did not move. The body diff against the 00:21:44Z read is exactly the three changes the coordinator named, and nothing else: four lines in "Clause ② re-read", one new line under "Verification", and the acceptance-notes paragraph collapsed to one bullet. Each changed sentence, tested against the tree at the head and the comments it cites:

  • Heading "Clause ② re-read — why the declaration is yes (widening)" — TRUE: the declaration line two sections above reads yes (widening), and the section's bullets are its reasons.
  • "The claim's original no covered the walk: the exported walk descends a slot the rows already declared." — TRUE as the record allows: the claim as dispatched read no (the round-1 report 5921521364 says so; the prior record 5921754490 read no (...) before the in-place correction), and ruling 5921583377 characterises that no the same way ("answered only the payload-key definition"). The original reason text is no longer readable anywhere (edited in place), so the sentence rests on the dev's own paraphrase, corroborated by the ruling. Not false, not unsourced.
  • "The seat took this in ruling 5921583377." — TRUE: that ruling reads "Q1 (the Clause-② line): A, yes (widening)" and orders the PR-body line and the claim line changed to it.
  • Verification: "The cut round at 5dc435cb07 re-ran the spec and lint suites and typechecks, check:generated, and the derived gates (report 5922116038 on spec(ui): walkAddressedPageComponents skips page:card's properties.footer as a back-compat spelling, but PageCardProps.footer is a declared, rendered slot, so nodes there go unjudged; the three platform page walks disagree on positions #20940): all green, 91 of 91 derived gates exit 0." — TRUE against the cited report, item by item: spec typecheck exit 0 (1), lint typecheck exit 0 (2), lint suite 117 files / 5440 passed (3), spec suite 584 files / 17196 passed + 1 todo (4), check:generated "All 15 generated artifacts are up to date" exit 0 (5), dispatch-gates --commands 91 derived / 91 run / all exit 0 and --ran 0 NOT-MEASURED 0 UNRUN (6), every reading stamped at 5dc435cb07. The line names its own head, so the section heading's d9d0d38cfc scope no longer leaves the cut round unrecorded (①.14b closed).
  • Acceptance notes: "Prose this change made stale is corrected in this PR (5dc435cb07; the review adoption 5921754490 named this PR the carrier): region-slots.ts:35-37, page-envelope-audit.ts:59, and validate-visibility-predicates.ts:1236 with its test comment at :497." — TRUE: the adoption under 5921754490 says "The carrier is this PR, because the diff makes them false"; at the head region-slots.ts lines 35-37 are exactly the re-wrapped "Deliberately import-free ... usable from spec/conversions/walk.ts" sentence, page-envelope-audit.ts:59 is the door-2 row naming page:card → footer, validate-visibility-predicates.ts:1236 is the comment line naming page:card footer, and validate-visibility-predicates.test.ts:497 is the comment line "properties.items[].children. A". Every path and line number resolves to the corrected text. ①.13 is closed; the false paragraph and its three quoted stale strings are gone from the body.

No unchanged body sentence is touched by this edit, so every judgment above stands. With this patch every PR-body sentence is true at the head, the shipped text (changeset, code comments) was already true at 5dc435cb07, and the accept set and public surface are as judged. The pre-existing os i18n coverage context line at i18n-resolver.ts:1867 remains main's, outside this PR's diff, as the adoption ruled.

Check-runs re-read after the body edit (00:47:34Z; 49 runs, 35 names after dedupe by newest started_at): the edit re-triggered seven runs at 00:46:47-49Z — Auto Label and Check PR Size skipped; "No other open PR may claim the same issue", "No other open PR may claim the same single-writer path", "Part-of PR must not also close its card" and "The card this PR closes must claim this branch" success; Check Changeset was still in progress at that read (started 00:46:48Z) and concluded success at 00:47:48Z on a re-read at 00:48:26Z — its third success on this head (23:48:36Z, 00:22:44Z, 00:47:48Z), reading the current body. After it: 30 success, 5 skipped, none failed, none running. All seven required contexts stand at success on the head.

Implemented-by: claude/issue-20940-page-slot-positions
Reviewed-by: session_01Sfe5YjBLwB9J3y8fvm2xq1

VERDICT: PASS

Adopted and posted by domain:spec seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1) · 2026-10-01T00:49Z · rendered by the seat's at-tier review subagent on this head. The seat read its served tier family from the subagent transcript before posting. This record has two parts. The delta FAIL at 5dc435cb07 was on the seat's own PR-body paragraph (item 13). The seat then patched the body (the head did not move), and the same reviewer re-read it and changed the verdict to PASS at the same head.


Generated by Claude Code

akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…transparent wrapper (objectstack-ai#21016)

Closes objectstack-ai#20969
Clause-②: no (a PM enqueue gate's verdict changes; no published
package's accept set or public surface moves, per the question in
`scripts/pm/clause2-line.mjs`)

## What

`scripts/pm/check-widening-tells.mjs` raised a false **T1** on PR
objectstack-ai#20961's diff: the change block removes `body: retiredKey(` and adds
`body: retiredComponentSlot(retiredKey(`. That is the same tombstone
with a marker call around it, and the marker hands back the instance it
is given. This PR teaches the objectstack-ai#16943 net-delta block to read that pair
as a replacement. The pair's two true **T3** rows still fire.

On PR objectstack-ai#20961's diff (`git diff 75519e1...d9d0d38`, `--declaration
no`):

| | exit | rows |
|---|---|---|
| before (`dfe5a0863f`) | 4 | T3 `ui.json:320`, T3 `ui.json:513`, **T1
`component.zod.ts:932`** |
| after (`0fd8e1969f`) | 4 | T3 `ui.json:320`, T3 `ui.json:513` |
| after, `--declaration yes` | 0 | n/a |

## The mechanism, re-derived (this corrects the filing)

The filing guessed that the wrapper hides the line's shape from the
budget. It does not: the same hunk re-declares two live keys through the
sibling marker `componentSlot(` (`children`, `footer`), and the budget
already pays for both. Two existing readings each declined correctly on
their own evidence, and that left nothing to pay with:

1. **Removed side.** objectstack-ai#17955 deliberately lets a removed tombstone buy no
T1 unit, so un-retiring a key (tombstone removed, live schema re-added)
still fires.
2. **Added side.** objectstack-ai#17955's tombstone decline reads the value's first
call, finds `retiredComponentSlot(`, and correctly refuses to call the
line a tombstone on that evidence alone. objectstack-ai#18702's resolver classifies
the factory as `writable` because it returns its own argument, and a
wrapper that returns its argument can still put a live arm on it.

## The repair

The repair follows triage's direction: the objectstack-ai#16943 block reads a removed
key and a re-added key **of the same name** as a replacement. It keeps
that credit in a **second currency** and never converts it into a T1
unit:

- A removed tombstone records its **key** in a per-block `retired` map.
- An added line spends one credit only if `wrappedTombstoneKey` reads it
as a tombstone of that **same key**, re-declared through **one** wrapper
that `transparentFactory` proves is transparent. Transparent means the
definition has exactly one `return`, and that return is the bare first
parameter with nothing chained onto it. The proof is read from the same
head blob, in the same pass, by objectstack-ai#18702's resolver.
- The credit is checked **before** the budget, for objectstack-ai#17955's reason: a
tombstone must never fire, and must never spend a unit owed to a live
rename beside it.

"Whatever wraps the value" holds for every wrapper the diff **proves**
transparent, and for no other. `return z.string().or(schema)`, `return
schema.or(z.string())` and a braced branch returning a live schema all
hand a tombstone back with a live arm on it. Paying for those would let
objectstack-ai#17955's un-retiring leg go quiet behind a prefix. A wrapper this reader
cannot certify keeps the tell firing.

## Pins: self-test battery `objectstack-ai#20969 …`, 28 cases, each ablated

Each ablation is one anchor through `scripts/ablation-replace.mjs` in
WRAP mode, which arms the restore on exit/INT/TERM. All five were run at
`0fd8e1969f`. After each one, the restore was proven: blob back to
`29c508a2522f` (the HEAD blob) and `git diff HEAD` empty.

| pin / control | ablation | result |
|---|---|---|
| **the PR objectstack-ai#20961 pair is paid** (its hunk verbatim, fixture line 932) |
spend disabled (the credit check short-circuited to `false`) | 8 of 553
red, including THE FINDING and CLEAN; the CLI on the PR diff shows `T1
component.zod.ts:932` again (3 rows, exit 4) |
| **a renamed key still tells** (`body` tombstone removed, `panel`
re-declared) | same-key match dropped (credit summed across keys) | 1 of
553 red: the renamed-key PIN |
| **a genuinely new key inside a wrapper call still tells**, as
un-retiring through the wrapper (`body:
retiredComponentSlot(z.array(…))`) | value inside the wrapper no longer
has to be a tombstone (any call accepted) | 2 of 553 red: the un-retire
PIN and the reader's null case |
| **a genuinely new key inside a wrapper call still tells**, as a new
key beside the pair, at its own line in either order | the credit paid
as a plain T1 unit (the naive repair) | 10 of 553 red: the new-key,
renamed and un-retire PINs, every widening-wrapper control, and objectstack-ai#17955's
own un-retiring CONTROL |
| widening wrappers (`lenientSlot`, `chainedSlot`, `branchySlot`) still
tell | transparency dropped (every writable factory treated as
transparent) | 3 of 553 red: all three |

Other controls in the battery: a dark control (the same added line with
nothing removed still fires), live arms chained onto the wrapper or onto
the tombstone, one-for-one, block-scoped, never-spends in both orders,
and the imported-wrapper boundary (objectstack-ai#18702's stated, reported silence,
unchanged).

## Measured price

The corpus is the history present in this shallow tree. One graft
boundary, `2e8bd8322b`, was excluded. Every commit went through
`wideningTells` with `headBlobSource` live, and a known blob was probed
first. Up to `a5bce40888`: 1,979 non-merge commits touch
`packages/spec/src`, 1,933 of them carry a non-test `.ts` diff, giving
5,865 file diffs. Running the merge-base version against this version:
**4,915 rows against 4,914, one differing commit, one row moved.** That
commit is `315888d660`, PR objectstack-ai#20961 as it landed, and the moved row is the
false T1 on `PageCardProps.body`. No other row moves and none starts
firing.

## Gates (all at `0fd8e1969f`, the final head)

- `node scripts/pm/check-widening-tells.mjs --self-test`: exit 0, 553
cases pass (525 before this PR, plus 28).
- The `dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`
union: 30 derived, 29 run, all exit 0. `--ran` reconciliation exit 0:
`30 derived famil(ies) accounted for — 29 run, 1 NOT-MEASURED`.
- **NOT MEASURED: `pnpm check:pm-dispatch-gates`**, because its
self-test passes the container's foreground cap (`timeout 560`, exit
124). Declared to CI.
- `npx eslint --no-inline-config --format json
scripts/pm/check-widening-tells.mjs`: 1 file, 0 errors, 0 warnings. The
config for this file (`--print-config`) has no `parserOptions.project`,
so linting is not type-aware and this diff cannot change any untouched
file's verdict. The repo-wide `pnpm lint` is left to CI.

## Acceptance notes

- **Quiet direction, stated:** a wrapper body that *mutates* the
instance it hands back still reads as transparent, and a text reader
cannot see that. At `a5bce40888`, 8 of 6,388 top-of-line definitions on
`packages/spec/src/**` read as transparent, and none of them mutates
what it returns. objectstack-ai#17955's own residual (a live arm chained on the
closing line of a multi-line tombstone) also reaches inside a wrapper
unchanged. Its population is 0, and its overturn condition is unchanged.
- **Loud residuals, kept on purpose:** these still fire: a new tombstone
re-declared through a transparent wrapper with no same-key removal, two
wrapper levels, and a wrapper whose own tail carries arguments. The
header records the overturn condition.
- `origin/main` was merged into the branch at `a5bce40888`, which brings
in PR objectstack-ai#20961 itself. No conflicts, and main had not touched this script.
- No changeset: `scripts/pm/**` ships in no package, so `skip-changeset`
applies.
- `definitionSites` / `localFormPattern` interpolate a factory name into
a RegExp without escaping `$`, so a file-local factory named with `$` is
reported unresolved (IMPORTED reason) instead of resolved. Population: 0
`$`-named definitions under `packages/spec/src` at `0fd8e1969f`. A
stated silence, not a quiet one; carrier: none (dev report
`5923104992`).

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ui#11163 and objectui#11199) (objectstack-ai#20990)

Fixes objectstack-ai#20949

Clause-②: no (narrowing)

Bumps the console pin: `.objectui-sha` `db11afd4967c` -> `e420df310f5b`,
objectui `origin/main` HEAD at bump time (2026-09-30T22:03Z). The pin's
`apps/console/package.json` is `@object-ui/console` 17.6.0, the same
version the old pin carried (`npm view @object-ui/console dist-tags`
answers `latest: 17.6.0`).

## Containment (objectui `merge-base --is-ancestor C e420df310f5b`,
private full clone, `rev-parse --is-shallow-repository` = false)

| commit | subject | result |
|---|---|---|
| `f4ed2387e9` | record:related_list reads its declared `actions`
(objectui#11163, PR objectui#11263) | exit 0, contained |
| `f8334f8777` | the permission editor's RLS policy list draws each
policy's label and description (objectui#11199, PR objectui#11215) |
exit 0, contained |
| control `db11afd4967c` (old pin) | | exit 0, contained (90 commits
behind the new pin) |
| negative control: `e420df310f5b` as ancestor of `db11afd4967c` | |
exit 1 |

On `main`: `git branch -r --contains e420df310f5b` lists `origin/main`,
and `merge-base --is-ancestor e420df310f5b origin/main` exits 0.

Riders: objectui#11270 and objectui#11253 are not in the range (0 hits
in the `db11afd4967c..e420df310f5b` log). So the three rider texts on
objectstack-ai#20949 (comment `5920572123`) are not due at this pin.

## What changed

- `.objectui-sha` and `.changeset/console-e420df310f5b.md`, written by
`scripts/bump-objectui.sh` (`@objectstack/console` minor). The range has
87 releasing changesets. 5 of them are declared breaking, and all 5 are
on objectui's own packages (see "Resolved since the first report").
- `sdui.manifest.json` and `scripts/sdui-manifest.record.json`, from
`node scripts/gen-sdui-manifest-node.mjs` (no arguments): 107
components, `@object-ui/core` 17.6.0.
- `packages/sdui-parser/objectui-lockstep.json`, from `pnpm
gen:sdui-lockstep` against the pin's own checkout: 214 grammar-region
lines, 25 diagnostic codes on objectui's side.
- **The `@objectstack/sdui-parser` port the lockstep demands.** This is
the seat's ruling A on objectstack-ai#20949 (`5923109668`), which widened the claim's
surface. It mechanically mirrors objectui `6f864cf62` (objectui#8285).
  - `packages/sdui-parser/src/kanban-quick-add.ts` is deleted.
- Its wiring in `validate.ts` and its four barrel exports in `index.ts`
(`checkKanbanQuickAdd`, `INERT_QUICK_ADD`, `QUICK_ADD_HOST_TYPES`,
`QUICK_ADD_KEY`) are removed.
- `__tests__/inert-quick-add.test.ts` is rewritten for the new state, as
objectui rewrote its own pin: an authored `quickAdd` on `object-kanban`
draws the prop walk's own `unknown-prop` warning, and the barrel no
longer publishes the four names.
  - There is no new diagnostic.
- No other workspace package imports the four names (`git grep`, 0 hits
outside `packages/sdui-parser`). `@objectstack/lint` and
`@objectstack/metadata-protocol` import only `compile`, `parseJsx` and
types.
- `.changeset/sdui-parser-retire-inert-quick-add.md` declares the export
removal: `minor`, `**BREAKING**` under the launch-window convention, a
`!` summary and `Clause-②: no (narrowing)`.
- `pnpm check:sdui-lockstep` now exits 0: the copy agrees with objectui
at `e420df310f5b` on all 25 codes.
- The 49 asserting pin citations in `packages/spec/src`, re-measured at
the new pin (next section). Also the regenerated
`packages/spec/src/migrations/registry.ts` and
`content/docs/references/ui/view.mdx`, and
`.changeset/objectui-pin-citations-e420df310f5b.md` (`@objectstack/spec`
patch). The `FormField.span` describe and six migration-entry
descriptions ship the pin sha, so the text change publishes.
- `pnpm objectui:build` at the new pin built objectui against this
tree's client and spec. It found no API break against framework code: 34
of 34 objectui dependency builds succeeded, with 0 TypeScript errors.
- `origin/main` was merged three times through
`scripts/pm/os-regen-merge.sh`, at `e32638aeac`, `b1da822c0f` and
`62ec639b04`.
- The first two overlapped only on the generated `registry.ts`, and
`gen:migration-registry` reproduced the merged file byte for byte.
- The third overlapped on `component.zod.ts` and `component.test.ts`
(objectstack-ai#20961's slot positions). It text-merged cleanly, and
`check:objectui-pin-citations` still reads 49 asserting citations
matching.
- After each merge `check:generated` read every artifact current, so no
regeneration commit was owed.

## Pin-citation re-measure

Each record was handled in the same way. Its anchors were mapped through
the `db11afd4967c..e420df310f5b` diff of the file each one cites, and
each anchor was re-read at the new pin. Line hashes and corpus counts
were taken again with the method the record names: `git hash-object` of
the cited block, and `git grep -o -F` over the tracked tree. Only after
that were the pin and the moved numbers updated. Each record keeps its
earlier hops as history. `check:objectui-pin-citations` now reports 49
asserting citations matching `e420df310` and 7 anchor content assertions
verified against objectui at the pin.

Records whose CLAIMS moved, not only their numbers. These want a human
read:

- **`action:*` rows** (`component.zod.ts`): objectui#11168 slice 1
rewrote all four registrations' `inputs`.
- `action:button` now publishes 27 of its 29 keys, all but `endpoint`
and `undoable`. It used to publish 7.
  - `action:menu` now publishes `size` and `visible`.
- `action:group` no longer publishes its group-level `name`, and
publishes the primitive's four sizes.
- The three publication sentences are corrected. `action:icon`'s `label`
is now read five times, where the record said four: objectui#11212 added
a fault label.
- **New, recorded and not declared:** objectui#11182 has `action:group`
and `action:menu` consume the host's evaluated `disabled` verdict. So a
`properties.disabled` on either block now greys it out at render, while
their strict rows refuse the key at save. The docblocks say so.
Declaring the key is a contract decision, not a pin re-measure.
- **`object-kanban` `limit`:** objectui#9853 renamed the default
`DEFAULT_KANBAN_LIMIT` to `DEFAULT_KANBAN_FETCH_BATCH_SIZE`. It is still
100. The query line changed on the anchor itself.
- **`object-kanban` `quickAdd` records:** objectui#8285 and
objectui#11234 landed objectui's half of the retirement. The board now
renders an internal `KanbanBoardCore`, which reads neither key. So
`ObjectKanban.tsx` names `quickAdd` and `onQuickAdd` 2 times each, all
four in the comments that record the cut, where the records said 0. The
records also now say that the schema-only `kanban-ui` block they point
to was retired in objectui (objectui#8257), long before this pin.
- **`object-tree`:** objectui#8348 moved the tree's rung-1 call from the
`undeclared` arm to `view-data`. It also dropped the `?? schema.data`
fallback from the host-data read. So the renderer and the row now agree
on a bare array. The "WIDER than this row" sentences are dated to the
pin where they held.
- **`object-map`:** one sentence said an authored `data` array still
reaches the renderer through the React props channel. That has been
false since objectui#9571, which predates `db11afd4967c`. objectui's own
docblock was only corrected on this hop, so the record is corrected now.
The `sort` record said the registration declares no `sort` input.
objectui#8220 now declares one, in the same array form, and also on
`object-gantt` and `object-timeline`.

Files: `component.zod.ts`, `component.test.ts`, `view.zod.ts`,
`dataset.zod.ts`, `functional-completeness.ts`,
`api-methods-batch-conformance.test.ts`, the six
`migrations/entries/semantic/18.*` entries, and the regenerated
`registry.ts` and `view.mdx`. No key, default, enum member or export
moves.

## Boot at the new pin

- **Build:** `scripts/build-console.sh` ran at `e420df310f5b` with this
tree's client and spec injected, and its VERDICT was command-exit 0.
  - The canary `import/jobs` is present.
- `assert-console-spec-injection` passes: 22 of 22 injected-only
descriptions are present, and all 6 published-only ones are absent.
  - `chunk-membership.json` reads `client` in `vendor-objectstack`.
- The tracked manifest it shipped is recorded at this pin. The dist is
62660 KB.
- **Server:** `examples/app-showcase` booted with `objectstack dev --ui
--fresh --no-seed-admin --no-watch -p 41949` on the whole-workspace
build of the merged tree. `bootstrap-status` answered `hasOwner: false`.
- **Registration:** headless Chromium (`/opt/pw-browsers/chromium`)
opened `/_console/setup` and submitted the owner form.
  - `bootstrap-status` flipped to `hasOwner: true`.
- In-page `get-session` returned the new owner with positions
`platform_admin`, `org_owner` and `everyone`, and `isPlatformAdmin:
true`.
- The wizard exited to `/_console/home`, which lists Showcase and Setup.
- **Second sign-in:** a fresh context signed in through
`/_console/login`.
- Showcase opened
`/_console/apps/com.example.showcase/page/showcase_capability_map`.
- Setup opened
`/_console/apps/com.objectstack.setup/dashboard/system_overview`.
  - Neither page showed a compile or render error.
- **Errors:** 0 page errors, 0 `TypeError` and 0 5xx across both passes.
- **Non-2xx:**
  - `401 GET /api/v1/auth/get-session` before each sign-in.
- `404 GET /api/v1/usage/storage`, which objectstack-ai#20638's boot recorded at the
old pin too.
- The browser's implicit `404 /favicon.ico`. The console's `index.html`
declares an empty-href icon link, and that file is byte-identical at
both pins.
- **Note:** a first drive from `127.0.0.1` was refused at sign-up with
403 (Better Auth: `Invalid origin`). The server publishes `localhost`,
and the drive from `localhost` is the record above. That attempt wrote
nothing; `hasOwner` stayed false.
- **Teardown:** only the four recorded PIDs were stopped. The port is
free, and the `--fresh` tempdir went with the process.
- **Not repeated after the sdui-parser port:** the port touches nothing
the console loads. `build-console.sh` injects only this tree's
`@objectstack/client` and `@objectstack/spec` into the bundle.
`@objectstack/sdui-parser` runs server-side, in the JSX-page save gate
(`@objectstack/lint`, `@objectstack/metadata-protocol`). The change
there is one diagnostic code for an authored `quickAdd` on
`object-kanban` (`inert-quick-add` becomes `unknown-prop`, still a
warning), which the registration and sign-in path never reaches.

## Spec compatibility

objectui at `e420df310f5b` resolves every `@objectstack/*` package to
17.5.0 in its lockfile, the published `latest`. Its declared ranges top
out at `^17.5.0`. Its own CI on that commit concluded success for both
`Type Check` and `Spec Main Shape Gate`. The first compiles against the
installed 17.5.0 faces, and the second against objectstack `main`.

Here, objectui's dependency build ran against
`node_modules/@objectstack/spec` 17.5.0 with 0 TypeScript errors. No
objectui change in the range needs an unreleased spec.

## Governed surface

None of the 25 changed paths is a row of `GOVERNED_SURFACES` in
`scripts/pm/check-governed-merges.mjs` (`governedPathsIn` returns none).
No release act was performed. The diff is 1950 changed lines, under the
5000-line human-merge threshold.

## Resolved since the first report

Both answers are on objectstack-ai#20949 in `5923109668`.

1. **ADR-0087 disposition, the maintainer's ruling** 「not-required
(推荐)」: `.changeset/console-e420df310f5b.md` now carries `adr-0087:
not-required (no-migration-prescription)` in place of the bump script's
placeholder. It is worded after objectstack-ai#20638's and fitted to this diff:
- It names the paths this diff moves, and states that the diff adds,
removes or renames no ObjectStack-authorable key.
- It names the five declared-breaking entries as objectui's own
surfaces: `52aad5cef`, `615346d61`, `6f864cf62`, `3c13675e5` and
`846cec0ef`.
- It notes that `object-kanban.quickAdd` is already registered on this
side as `page.component.object-kanban.quickAdd` (objectstack-ai#17260).
- The sdui-parser changeset carries the same disposition, worded as the
ObjectStack mirror of `6f864cf62`.
- `check-adr-0087-registration --base origin/main` exits 0: 2
declared-breaking changesets, each with a disposition.
2. **`check:sdui-lockstep` code-drift `inert-quick-add`, the seat's
ruling A:** the port is above, and the gate exits 0.

Gates at head `62ec639b04`: 125 derived, 125 run, 0 NOT-MEASURED, 0
unrun, and all 125 exit 0. The 11 artifact-roster gates whose roster
sits in a touched directory also all exit 0.
- `@objectstack/spec`: tests 17211 passed and 1 todo, across 584 files;
typecheck exit 0.
- `@objectstack/sdui-parser`: 217 tests passed across 13 files;
typecheck exit 0.
- `@objectstack/lint` (its three JSX-page and lazy-deps suites): 20
tests passed.
- `@objectstack/metadata-protocol`: 2896 tests passed and 19 skipped.
- Whole-workspace build: 72 of 72 tasks.
- The rewritten `inert-quick-add` pin was reverse-checked once. With the
three pre-port sources restored from `b1da822c0f`, 4 of its 6 rows go
red. The `false` and braced-marker rows stay green, because the interim
never fired for them. Restored, `git diff HEAD` is empty and both edited
files' blobs equal `HEAD`'s.

---
_Generated by [Claude
Code](https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6)_

---------

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

Projects

None yet

2 participants