Repository navigation
feat(spec,lint): one derived list of page-component slot positions, read by all three page walks (#20940) - #20961
Conversation
…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>
…ngeset (#20940) Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
…w ui exports (#20940) Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): ⛔ 1 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 143 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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
|
Contract reviewServed-tier: 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, ① Derived judgmentsAccept set: unchanged — right. Public surface: widened by exactly two exports on One list, truly derived — right. The list is The three walks visit the positions they should — right.
Consumers of the widened walk change only by the footer reach — right, and intended. Lazy memoized derivation: no import-cycle or ordering hazard — right. The relative-import closure of Text someone acts on, tested sentence by sentence. The ones that fail:
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 ② Semver level
③ Boundary flags
Check-runs on the head (read last; 42 runs, 35 names after dedupe by newest What a PASS on the next head needs: (1) changeset: Implemented-by: VERDICT: FAIL Adopted and posted by
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>
Contract reviewServed-tier: Delta re-review of the prior FAIL record 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): ① Derived judgmentsAccept set: unchanged — right (re-read, delta cannot touch it). Still exactly five Public surface: widened by exactly two exports on One derived list; the three walks; consumers; lazy derivation — right (re-read). The delta touches none of it. Text someone acts on — each edited sentence tested against the tree at the head. Changeset (ships as CHANGELOG):
Code comments cut (the prior record's item 4, each verified at the head): PR body (the seat's, patched at this head): Every other PR-body sentence was judged at ② Semver level
③ Boundary flagsCut-round report 5922116038:
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 Body re-read — 2026-10-01T00:47Z, head unchanged at
|
…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>
…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>
Closes #20940
Clause-②: yes (widening)
What this does
Three page walks descend into a component's
propertiesbag, and each kept its own list of positions:COMPONENT_CHILD_KEYSplusitems[].children;@objectstack/lint'swalkPageComponents:items[].children,children,body,footer;walkAddressedPageComponents:children,items[].children.So a node in a
page:cardfooter was judged byos lintand skipped by every consumer of the exported walk:translatePage, the CLI extractor and objectui's validator.Now
packages/specowns one list, derived from the component rows. There is no fourth list.componentSlot()/retiredComponentSlot()are module-private markers inui/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 markedcomponentSlot:PageContainerProps.children,PageCardProps.children,PageCardProps.footer, andchildrenon thepage:tabsandpage:accordionpanel items.PageCardProps.body, aretiredKeytombstone, is markedretiredComponentSlot.pageComponentSlotPositions()derives the list fromComponentPropsMap. It is exported with its typePageComponentSlotPositionfrom@objectstack/spec/ui. It returnschildren,body(retired),footer, and the panel positionitems[].children. Order is the visit order: direct slots in row order, then panels, sochildrenis still walked beforeitems[].children, the order the exported walk's nested-id arbitration has used since i18n: two surfaces a translation bundle cannot address at all —walkAddressedPageComponentsmisses a slotted page'sslotsand a tabs node'sitems[].children, anddashboards.NAMEhas noglobalFiltersgroup #16772. The list is derived on first call and memoized, never at import. The first call reads every row's shape, which constructs thelazySchemarows: measured 17.6 ms, once per process. Two rows that contradict each other on one position throw.walkAddressedPageComponentsand lint'swalkPageComponentsread the authorable entries, sopage:card.footeris now descended by all three.bodyis handled per the standing ruling. SDUI props 声明与 renderer 不一致:6 处「renderer 兑现但 ComponentPropsMap 未声明」+ 2 处「声明了没人读」(#5068 error 升级的 spec 侧前置) #5775 (maintainer ruling 2026-08-06, direction A, recorded indocs/protocol-upgrade-guide.mdand in thePageCardProps.bodytombstone and thePageContainerPropsguidance) madechildrenthe one composition key. It kept the renderers'bodyread as a back-compat fallback for stored documents, not as an authorable spelling. So:body.bodywas re-read against that ruling, as the claim asked, and removed. An author who writesbodyis 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 underchildren.body. Stored documents still carry it, and a conversion ordered beforepage-card-body-to-children(for examplepage-header-subtitle-alias) meets the sub-tree there.page-component-walk.test.tspins that reach, and removing it would be a reach regression the claim did not order.childrenis declined. The component rows are not reached by any load-path parse:PageComponentSchema.propertiesis an open bag. So a recursivechildrentype 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 (childrenlegally holds bare id strings andnull). And it would not replace any consumer's walk, as the card itself notes.Clause ② re-read — why the declaration is
yes (widening)nocovered the walk: the exported walk descends a slot the rows already declared. But the design the claim orders ("packages/specowns one list … lint'swalkPageComponentsreads it") needs@objectstack/lint, a separate package, to import the list. So@objectstack/specgrows two exports, andapi-surface/ui.jsongains two rows in this diff.scripts/pm/clause2-line.mjsstates the question the declaration answers: 「本卡放宽接受集或扩大公开面吗」. The public surface grows, so the answer is yes.pr-automation.yml, "WHICH LEVEL", ruling 2026-09-04) says: "a new exported symbol on anindex" is a widening that takes at leastminor.scripts/pm/check-widening-tells.mjsnames "T3: a new row in a published entry point's export listing (packages/spec/api-surface/*.json)" as a widening tell. Under anoit refuses the enqueue.@objectstack/specminor, socheck-changeset-no-major's level axis passes under either value. The seat took this in ruling5921583377.Changeset levels
@objectstack/spec:minor. It adds two exports:pageComponentSlotPositions,PageComponentSlotPosition. The exported walk's signature and return shape are unchanged. The one earlier widening of this walk (i18n: two surfaces a translation bundle cannot address at all —walkAddressedPageComponentsmisses a slotted page'sslotsand a tabs node'sitems[].children, anddashboards.NAMEhas noglobalFiltersgroup #16772) was banner-BREAKING only for its return-shape change, so none is declared here.@objectstack/lint:patch. No export is added, andwalkPageComponents's signature is unchanged. Its reach drops the retiredbodyper the ruling, and the tombstone still refuses the key itself.@objectstack/cli:patch. There is no code change, only corrected comments. The changeset tells an upgrader thatos i18n extract/os i18n checknow offer footer component keys.check-adr-0087-registrationreads the changeset as non-breaking (exit 0).Verification — at
d9d0d38cfc(this PR's head when opened)The cut round at
5dc435cb07re-ran the spec and lint suites and typechecks,check:generated, and the derived gates (report5922116038on spec(ui):walkAddressedPageComponentsskipspage:card'sproperties.footeras a back-compat spelling, butPageCardProps.footeris 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-typecheckheld for both, with the debt ledgers unchanged.pnpm --filter @objectstack/spec check:generated: exit 1 at first, onapi-surface/andexport-origins/(the two new exports).--fixregenerated exactly those two, adding +2 rows each (commitd9d0d38cfc). Then "All 15 generated artifacts are up to date", exit 0.check:authorable-surfaceandcheck:docswere 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 at3dee2204c9; 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 lintpage-walk.test.tspluspage-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 withgit checkout HEAD, and the blob equals HEAD withgit diff HEADempty.footer, by filteringfooterout of its positions.page:cardfooter to the visitor".ablation-dist-preflightfound the marker in 4 built files.dist/, went red, 1 of 5.--absentwas clean with a clean tree, and both suites were green again (300/300, 5/5).bodyincluded, 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.footer:componentSlot(was removed fromPageCardProps.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
packages/cliunit tier,test/platform-page-i18n-parity.test.ts. It imports built@objectstack/cloud-connection/mcp/platform-objects, whose dependency closure this fresh worktree had not built (a first attempt died with the container). The expectation is edited:card_footer_childis offered and applied on both sides. The run and its reading go in the os-dev-report on spec(ui):walkAddressedPageComponentsskipspage:card'sproperties.footeras a back-compat spelling, butPageCardProps.footeris a declared, rendered slot, so nodes there go unjudged; the three platform page walks disagree on positions #20940.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon this diff. It derives 89 commands: 12 were run directly (listed above), and 5 more are covered insidecheck:generated(check:api-surface,check:authorable-surface,check:docs,check:liveness,check:strictness-ledger). The rest go in the os-dev-report; CI runs the full farm.Acceptance notes
items[].childrenfirst. This is observable only as finding or notice order, for a component carrying both a direct slot and panels.conversions/walk.ts. It now importsui/component.zod.ts.component.zod.ts's import closure (49 files) was measured and reaches noconversions/module, so there is no cycle. The derived list is read only inside functions.5dc435cb07; the review adoption5921754490named this PR the carrier):packages/spec/src/automation/region-slots.ts:35-37,packages/lint/src/page-envelope-audit.ts:59, andpackages/lint/src/validate-visibility-predicates.ts:1236with its test comment at:497.packages/cli/src/utils/i18n-extract.tscomments. Only comments changed there, where they stated the old positions (body/footerundescended; 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