feat(spec): register the ADR-0087 semantic entry ui-html-page-div-refused (#20592) - #20610
Conversation
…used The protocol-18 step records the html-tier div refusal a manifest-less project meets once the CLI's JSX page gate reaches the manifest @objectstack/console ships: box for a plain wrapper, and how to prove the rewrite done. registry.ts regenerated by gen:migration-registry; spec-changes.json and the upgrade guide regenerate byte-identical, because step 18 is above the current protocol major. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
Its default range runs to the chain terminus, protocol 18, so the entry reaches authors through migrate meta now; only the upgrade guide and spec-changes.json wait for the protocol major. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 137 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 3c7b0ca10dc39d0f0f519577457b286bb3e255df && git checkout 3c7b0ca10dc39d0f0f519577457b286bb3e255df
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0f6dcac5e99d0c6211f0d8a0e150a112d78a776f 95ae699a5bb78a0cf6e11f687b08d63c459d484b && git checkout -B drift-repro 0f6dcac5e99d0c6211f0d8a0e150a112d78a776f && git merge --no-ff 95ae699a5bb78a0cf6e11f687b08d63c459d484b
node scripts/docs-audit/affected-docs.mjs --json 0f6dcac5e99d0c6211f0d8a0e150a112d78a776f |
Contract reviewServed-tier: ① Derived judgmentsDiff read as Accept-set changes implied: none — right. No Zod schema, tombstone, guidance map or D2 conversion moves. Public-surface changes implied: one, additive data — right. Moment key: [now] = true on surface — right.
replacement — right, sourced from the pin.
reason — right in substance; three sentences are true only after #20589, one is over-broad by a condition.
acceptanceCriteria — right.
Form D: the four author-shown fields carry no tracker number; the tracker ids sit in the file's header comment (and its generated copy) only. Changeset text ( The landing window, named: the card's Order lets the entry land first, and the dev measured PR #20589's marker flipping to ② Semver level
③ Boundary flagsDev report (
Out-of-scope findings, both verified true here and escalated to the #19922
Reviewer's escalations:
Check-runs on Implemented-by: VERDICT: PASS Adopted and posted by Generated by Claude Code |
…data/analytics.zod.ts to the commits that decided them (stage 7) (objectstack-ai#20616) Part of objectstack-ai#20234 Clause-②: no Stage 7 of the staged sweep: `packages/spec/src/stack.zod.ts` and `packages/spec/src/data/analytics.zod.ts`, both freed by landings (PR objectstack-ai#20579 and PR objectstack-ai#20458). Its claim is `5885635758`. Every comment or docblock line in those two files that cited a tracker number answering 404 now cites the commit on `main` that decided its rule, in ruling C+D's form C, and says in its own words what was decided. Comments only: 12 lines out, 12 in, across 2 files. No code token, string literal or `describe()` text moves. No dead site stays: none of the 12 is read by literal. The census is the gate's own `node scripts/check-issue-citations.mjs --census --json`, filtered to the two paths. Before: base `0f6dcac5e9`, board enumerated (185 pages, frontier objectstack-ai#20611). After: head `cc0580d404`, board enumerated (185 pages, frontier objectstack-ai#20615). ## Measurement | file (under `packages/spec/src/`) | dead before | after | numbers, then anchor | |---|---:|---:|---| | `stack.zod.ts` | 9 | 0 | objectstack-ai#10485 ×2 (`:415`, `:1023`) to `35ad101bc`; objectstack-ai#6238 (`:633`) to `c8d6f6e08`; objectstack-ai#14192 (`:1233`) to `4d0d9445a`; objectstack-ai#14686 ×2 (`:3037`, `:3194`) to `279431e7a`; objectstack-ai#14662 ×3 (`:4510`, `:5070`, `:5293`) to `35dffeace` | | `data/analytics.zod.ts` | 3 | 0 | objectstack-ai#10194 ×3 (`:404`, `:407`, `:485`) to `2306a765c` | | **2 files** | **12** | **0** | 6 numbers removed, 6 distinct shas | Per-file counts at base equal the claim's (9 and 3, from stage 6's census). A second instrument agrees site for site: every `#N` in the two files, classified by the TypeScript parser, and each of the 84 distinct numbers of 100 or more probed by REST `issues/N` without following redirects (the other 3 are the ordinals `Prime Directive objectstack-ai#12`, `batch objectstack-ai#23`, `batch objectstack-ai#57`). - Base: 244 sites, all in comments (0 strings, 0 code). 78 numbers answer 200 and 6 answer 404: the same 6 numbers and the same 12 sites as the gate. - Its string-class positive control found 11 string sites in `kernel/manifest-unknown-keys.test.ts` and `packages/cli/src/utils/lower-callables.test.ts`. - Head: 232 sites, 78 numbers, all 78 answer 200 (the same 78), none answers 404. - Lit controls objectstack-ai#16862, objectstack-ai#16847 and objectstack-ai#17698 answered 200 at every checkpoint (3 at base, 3 at head); dead controls objectstack-ai#16714, objectstack-ai#16715 and objectstack-ai#16697 answered 404 at every checkpoint. ## Why each anchor decides its line Each sha resolves uniquely, is an ancestor of `origin/main` (and of the base), and has one parent. No file under `docs/adr/**`, `docs/NORTH-STAR.md` or `scripts/adr-anchors/` names any of the six numbers or records these rules, so each takes the commit rung, as stages 1–6 did. - **objectstack-ai#10485 to `35ad101bc`** (`:415`, `:1023`): retires the `themes` carrier key and `ThemeSchema` under ADR-0049. Its message records the ruling, "Ruled B (退役授权面, 2026-08-21)", and its own `stack.zod.ts` diff wrote both lines. `:415` keeps ADR-0049 and the ruling in its words; the D3 entry `stack-themes-carrier-retired` it names on `:423` is unchanged. This is the anchor stages 1, 5 and 6 used for the same retirement. - **objectstack-ai#6238 to `c8d6f6e08`** (`:633`): widens the array member of `functions` so its `handler` also takes the lowered string ref, which is the fix for `objectstack build` refusing its own array output. Its message names objectstack-ai#6238, and its own diff wrote the line. objectstack-ai#4343 and objectstack-ai#4976 on the same line stay (both 200). - **objectstack-ai#14192 to `4d0d9445a`** (`:1233`): turns `ManifestSchema` and its nested blocks into `strictObject` and flips the assembled-body strip pin to a refusal pin; each of its sub-commits names objectstack-ai#14192. The line itself was written later by `c78c9180de`, whose own message says "objectstack-ai#14192 closed ManifestSchema with strictObject", so the commit that closed it is the anchor. - **objectstack-ai#14686 to `279431e7a`** (`:3037`, `:3194`): "defineStack refuses two actions that resolve to one scope-qualified runtime key". Its subject names objectstack-ai#14686, and its diff adds `collectDuplicateActionKeyErrors` and the changeset for that refusal. Both lines were written later by `773a99960a` (PR objectstack-ai#15022), whose message describes the same "same-key rule, which runs before the merge". - **objectstack-ai#14662 to `35dffeace`** (`:4510`, `:5070`, `:5293`): "composeStacks refuses two stacks whose actions resolve to one scope-qualified runtime key". It checks the composed set with the rule `defineStack` applies within one stack, with no `actionConflict` option (maintainer ruling 2026-09-03). Its message does not name objectstack-ai#14662; its own `stack.zod.ts` diff wrote all three `(objectstack-ai#14662)` lines. - **objectstack-ai#10194 to `2306a765c`** (`analytics.zod.ts:404`, `:407`, `:485`): binds `analytics_cube` (and `theme`) in `UNREGISTERED_KIND_SCHEMAS`, so `PUT /meta/analytics_cube/:name` parses through `CubeSchema`, and gives `CubeSchema` the `...MetadataProtectionFields` spread. Its message names objectstack-ai#10194, and its own diff wrote all three lines. The `[objectstack-ai#10194]` markers become `[commit 2306a76]`, the spelling stages 1 and 5 already use in `kernel/metadata-type-schemas.ts`. ## Mechanical proof - **Token guard** (my `tokcmp.mjs`: TypeScript 6.0.3 leaf tokens, JSDoc kinds excluded, controls mutate the head text in memory only). Base `0f6dcac5e9` against the head, 2 files, 17,249 base tokens: - Real run: 0 files with a token change (exit 0). - Comment-insertion control (`data/analytics.zod.ts`): 0 (exit 0). - Code-insertion positive control (`stack.zod.ts`, a declaration appended): DIFFER at token 15388 (exit 1). - String positive control (the first `StringLiteral` the parser locates in each file): DIFFER at token 5 (exit 1), once per file. - `describe()` positive control (the first `.describe()` string argument the parser locates: `stack.zod.ts:133`, `analytics.zod.ts:244`): DIFFER at tokens 507 and 442 (exit 1). - **Line balance**: `stack.zod.ts` +9/−9, `data/analytics.zod.ts` +3/−3; line counts equal at base and head (5344 and 853). - **Tracker numbers**: added-not-removed is empty in both files, and no `PR #N` is on an added line. Net-removed: 12 sites, 6 numbers. The only numbers on added lines are objectstack-ai#4343 and objectstack-ai#4976, which stay on `:633`. - **Shas**: 6 distinct on added lines, 0 on removed lines. - `rev-parse --disambiguate` answers 1 object for each. - `merge-base --is-ancestor` exits 0 for each, against `origin/main` `7510663c87` and against the base; each is single-parent; the repository is not shallow. - **Literal readers**: all 26 string, template and regex literals in the repository that carry one of the six numbers (42 code files) were matched against the two files' base text: 0 occur there. Each removed line was also cut into 4-word windows (96) and searched across the tree: the 9 hits inside string literals are other files' own test titles sharing a phrase ("the ADR-0010 protection envelope", "an assembled body is"), and none reads either file. The source-text readers of the two files read code, not these comments: `compose-stacks-refusal-envelopes.test.ts` counts `throw new Error(`, and `check-stack-collection-maps.mjs` and `check-skill-top-level-keys.mjs` read the declared collections and keys. ## Tests and gates (at head `cc0580d404`) - `pnpm exec turbo run build --concurrency=2 --filter=./packages/* --filter=./packages/*/*` under `os-verify-lock`: Tasks 71 successful, 71 total, VERDICT command-exit 0. - `pnpm --filter @objectstack/spec check:generated` under the lock: all 15 generated artifacts up to date, `check:docs` over `content/docs/references/**` included; VERDICT command-exit 0. No reference page projects any of the 12 lines, so none is regenerated. - `vitest run --maxWorkers=2` under the lock over the two files' own suites (`src/stack*`, `src/compose-stacks*`, `src/define-stack*`, `src/assembled-package-body`, `src/data/analytics*`, `src/data/cube*`): Test Files 35 passed (35), Tests 976 passed (976). - The 37 spec suites that read source text across `src/`, or carry one of these numbers, under the lock: Test Files 37 passed (37), Tests 759 passed (759). - `scripts/{category-title,dist-freshness,dist-freshness-adoption,file-description,strictness-ledger,strictness-ledger-doc,root-index,skill-map-guards,export-origins,split-entries,root-entry-type-nameability.pin}`, `scripts/liveness/{evidence,tombstoned-row-status}`; - `src/type-alias-convention.pin`, `src/eager-entry-import`, `src/api/{api-entry-graph.pin,auth,export-job-family-retirement}`, `src/ai/tool-confirmation-prescription-tense.pin`, `src/data/{currency-mode-family-closure.pin,external-lookup-retirement}`, `src/identity/position-delegatable-enforcer.pin`, `src/integration/{connector-connection-timeout-retirement,connector-resilience-keys-retirement}`, `src/security/rls-tags-retirement`, `src/shared/{alias-integrity,retired-key-migrate-sentence}`, `src/system/{compliance-families-retirement,constants/platform-object-names,email-template-floor-locale-parity.pin,message-queue-retirement}`, `src/ui/{action-requires-confirmation-docblock.pin,i18n,interaction-config-retirement,strictness-batch14}`, `src/kernel/{manifest-unknown-keys,metadata-type-schemas}`. - Left to CI: `scripts/{build-schemas-check-mode,def-key-collisions,openapi-self-consistency}` (each rebuilds artifacts in a temp tree) and `scripts/{check-generated-ledger,check-generated-fix-rebuild.pin}` (read the ledger and `dist`). None reads comment text. - `pnpm --filter @objectstack/spec typecheck` under the lock: exit 0; `check:test-typecheck` OK (53 files / 251 errors / 138 pinned signatures held). - Lint, a proven narrowing: `eslint --no-inline-config --format json` over the 2 files gives 2 files, 0 errors, 0 warnings. - `isPathIgnored` is false for both, read through eslint's API. - `eslint.config.mjs:327-328` says type-aware linting is never enabled, so a comment edit cannot move an untouched file's verdict. - The repo-wide `pnpm lint` is CI's. - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`: 79 families derived and run, every one exit 0. `--ran` reads "79 derived, 79 run, 0 NOT-MEASURED, 0 UNRUN". Among them: - `pnpm check:issue-citations` (self-test, 114 cases in 8 batteries) and the live diff-scoped `node scripts/check-issue-citations.mjs`: it judged the 2 citations on added lines, objectstack-ai#4343 and objectstack-ai#4976, and both are live issues. - `pnpm check:doc-authoring`: 16,804 customer-facing strings across 1,179 spec sources clean; the sibling baseline holds. - `pnpm check:stack-collection-maps`: 8 enumerations reconciled against 31 declared collections. - Changeset: `patch` for `@objectstack/spec`. Both files are `src/**/*.zod.ts`, which `files[]` ships verbatim, and the rewritten docblocks reach `dist`: "posture: commit 4d0d944 closed" and "[commit 2306a76] This docblock used to say" are each in 2 `.d.ts`, their old spellings in 0. Positive control: the unchanged neighbouring sentence "BY INHERITANCE — an undeclared key on one is REFUSED" is in the same 2 `.d.ts`. - Merge probe: a no-driver `merge-tree` of the head onto `origin/main` `7510663c87`, from a bare shared clone, exits 0. The 3 commits `main` gained since the base touch neither file nor the citation or derivation scripts, and a re-derivation prints the same 79 commands. No merge was made. - No ablation or reverse verification: the change is comment-only, so there is no behaviour to invert. ## Hypotheses (measured first) 1. **Holds.** 12 dead sites at the tip, 9 in `stack.zod.ts` and 3 in `data/analytics.zod.ts`, equal per file to stage 6's census. 2. **Holds.** Read at 2026-09-29T07:36Z and again at 08:16Z, after the last push and before this PR was opened: all open PRs' full file lists (9 PRs, 166 files at the second read) and the newest `Claim:` on all 11 `pm:dispatched` cards. None names either file, except this card's own claim. 3. **Holds, with nothing to keep.** All 12 sites are comments. No test string, exported string or `describe()` text carries one, and no test or script reads any of them by literal. 4. **Holds.** No generated reference page projects these lines; `check:docs` is green with no regeneration. ## Deviations - None to the file surface: the 12 claimed lines and one changeset, no generated page needed. - Commit trailers follow AGENTS.md's model-free pair (`Claude-Session` plus `Co-authored-by: Claude`); the pre-push trailer check passed on every push. ## Acceptance notes **What stays for later stages.** The gate's census at this PR's head (base `0f6dcac5e9` plus this PR) reads **248** dead sites (29 numbers) in `packages/spec/src`. The only `packages/spec/src` change `main` has made since the base (objectstack-ai#20610's migrations entry and registry) adds four live numbers and removes none, so 248 also stands at the tip `7510663c87` plus this PR: - `migrations/` **233**: objectstack-ai#20233 edits the same entry files (PR objectstack-ai#20607 holds `migrations/registry.ts`). - `conversions/registry.ts` **12**: PRs objectstack-ai#20570 and objectstack-ai#20587 hold it. - `integration/connector.zod.ts` **1**: PR objectstack-ai#20587 (objectstack-ai#20287). - `data/api-derivation.ts:163` (objectstack-ai#6259) and `identity/identity.zod.ts:230` (objectstack-ai#8715), **1** each: kept because tests read them by literal, so removing them is form D. **Outside the gate's census: test files.** The gate defers `*.test.ts`. The same six dead numbers still stand at 15 comment sites and 10 test-title strings in `packages/spec/src` test files: - `data/analytics-strictness-batchd.test.ts:96` (comment, objectstack-ai#10194) and its title `:93`. This file is in the `analytics*` set stages 3 and 4 excluded while PR objectstack-ai#20458 held it; `analytics-date-range-two-bound-window.test.ts` and `cube-member-inner-name-retirement.test.ts` were in that set too and are not re-measured here. - The package root: `compose-stacks-action-echo.test.ts:20`, `:34`, `:200` (objectstack-ai#14686) and titles `:176`, `:224`; `compose-stacks-action-key-collision.test.ts:3` (objectstack-ai#14662); `stack-top-level-strict.test.ts:103` (objectstack-ai#10485) and title `:128`; `type-alias-convention.pin.test.ts:257`, `:1572`, `:1937` (objectstack-ai#10485). - `shared/`: `metadata-collection.test.ts:250`, `metadata-url-spelling.test.ts:51`, `:72`, `:168` (objectstack-ai#10485), `:257` (objectstack-ai#10194), title `:254`. `automation/sync-retirement.test.ts:207` (objectstack-ai#10485). - `kernel/`: `manifest-unknown-keys.test.ts`, four titles (objectstack-ai#14192); `metadata-type-schemas.test.ts:422`, a title (objectstack-ai#10194). - Stage 6 took the package root, `shared/` and `automation/` through the gate's census, which never lists a test file, so test-file comment lines there may carry other dead numbers as well. That wider population is not measured here. **Outside `packages/spec/src`.** The same six numbers stand at 44 more sites (`packages/{metadata-protocol,objectql,rest,runtime,cli,core,metadata,qa}`, `examples/`, `scripts/`, `packages/spec/scripts/`), and at 19 sites in `migrations/` (the objectstack-ai#20233 area). **Rung.** The objectstack-ai#10485 retirement also has the ADR-0087 D3 entry `stack-themes-carrier-retired`, which `:423` already names. This PR takes the commit rung, as stages 1–6 did. **Wording, each true of its commit.** `:3037` and `:3194` now read "commit 279431e's same-key refusal": the refusal that commit added, in lines `773a99960a` wrote. `:1233` reads "commit 4d0d944 closed `ManifestSchema`", in a line `c78c9180de` wrote. --- _Generated by [Claude Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…ough @objectstack/console/package.json, so a project without its own manifest gets full component checking (objectstack-ai#20589) Fixes objectstack-ai#19922 Clause-②: no ## What this changes `resolveSduiManifest()` (`packages/cli/src/utils/sdui-manifest.ts`) is the one resolver `os validate`, `os compile` / `os build` and `os lint` use to arm the JSX page gate. `os dev` and `os start` run `compile` before they boot when `dist/objectstack.json` is missing or `--compile` is passed, and `dev`'s default watch mode reruns it when a watched file changes. The resolver's second place to look, the copy `@objectstack/console` ships as `dist/sdui.manifest.json`, asked Node for that file by its own subpath. The console's `exports` map publishes `./package.json` alone, so the resolve threw `ERR_PACKAGE_PATH_NOT_EXPORTED`, a `catch` swallowed it, and every project with no `sdui.manifest.json` of its own had its `kind: 'html'` pages checked at parse level only. The fallback now resolves `@objectstack/console/package.json` from the CLI's own location (`import.meta.url`, the CLI's declared dependency in the same fixed release group) and joins `dist/sdui.manifest.json` to its directory, through a new `consoleSduiManifestPath(origin)`. The console's `exports` stays closed. `resolveSduiManifest(cwd, consoleOrigin)` gains an optional origin, used only by the pins. The old module header handed one decision to whoever made this leg reachable: what a broken shipped copy should do. It now gets the project leg's rule. A shipped copy that is present but cannot be read or parsed is `unusable` (new `source: 'console'`), and the command is refused with exit 1, naming the file, with the remedy "reinstall @objectstack/console". It is never read as "not found". With no page to check it is read by nothing and not refused, the same as the project leg. ## This round (the seat's unlock record `5890591366` on objectstack-ai#19922) The ledger entry `ui-html-page-div-refused` landed on `main` (objectstack-ai#20592, PR objectstack-ai#20610), and Version Packages objectstack-ai#17076 consumed `.changeset/sdui-manifest-one-producer.md`. This round: 1. **Merged `origin/main` at `f1e921ab8e`** (a merge, not a rebase; merge commit `f9cb969f44`). One conflict: `.changeset/sdui-manifest-one-producer.md`, modify/delete, resolved in favour of `main`'s deletion. `build-json-failure-conversions.e2e.test.ts` and `validate-json-failure-conversions.e2e.test.ts` auto-merged: `main` changed other regions of both, and the `box` fixture line and its docblock sentence survived. The branch's delta against `main` is exactly the 7 intended files. `main` has since gained one commit (`cd901d7a5f`), which touches none of them. 2. **The correction moved into this PR's changeset.** The released note is `@objectstack/console` 17.5.0, patch entry `28ce612`. A new paragraph goes through its closing paragraph one sentence at a time: - two sentences stop being true with this release: the file is no longer "only present in the tarball", and the CLI fallback no longer "keeps parse-level validation"; - two still hold: `exports` is unchanged. No `CHANGELOG.md`, no `content/docs/releases/` and nothing under `packages/spec/` is edited. 3. **The ADR-0087 marker** now reads `not-required (already-registered ui-html-page-div-refused)` with its reason. The gate's verdict: "check-adr-0087-registration: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … not-required (already-registered)", exit 0. 4. **Two sentences re-measured and corrected:** - **"the html-tier renderer still renders `div`" was false.** At the pinned objectui `dd3f7e1be3`, read with `git show` from the sibling checkout (nothing checked out, nothing edited): - `packages/components/src/renderers/layout/page.tsx:487-488` builds the html compile's whitelist from `getKnownTypes()` minus `deprecationFor(t, 'html')`; - `packages/components/src/renderers/basic/div.tsx` registers `div` with `deprecated.surfaces: ['json', 'html']`; - `nameHtmlTierReplacement` turns the resulting `forbidden-tag` into a refusal naming the replacement. That pin shipped in `@objectstack/console` 17.5.0: its CHANGELOG entry `3cf6449` says a `kind:'html'` page that authors a `div` "is refused at compile time, and the error names `box`". The changeset now says the console has refused `div` since 17.5.0, and that what is new is every other tag the manifest does not declare. The console's html compile accepts every non-deprecated registered component, while the manifest declares the public contract plus the html intrinsics. Measured below with `avatar`. - **"`objectstack compile` (which `dev` and `start` run first)" was inexact.** It now says exactly when they run it: `dev.ts:319` compiles on `flags.compile` or a missing artifact, and `dev.ts:383` re-runs it in watch mode; `start.ts:228-232` has the same condition. ## Premise and hypotheses, measured Round-1 readings (on `f11b5f20a2`) are kept where they still hold. Round-2 readings are on `77338a7186`: Node v26.7.0, macOS. - **H0 (premise holds).** On unmodified `f11b5f20a2`, a real `os init` project with a `kind: 'html'` page rooted in `div` passes `os validate`, `os compile` and `os lint` at exit 0. Each prints only the parse-level notice, and it does so even with a `cmp`-identical copy of the tracked manifest at `packages/console/dist/sdui.manifest.json`. From `packages/cli/dist`, the old subpath throws `ERR_PACKAGE_PATH_NOT_EXPORTED`. - **H1 (the route finds the file in both layouts).** - Workspace: `consoleSduiManifestPath()` answers `packages/console/dist/sdui.manifest.json`. - Installed package: `npm pack` of `packages/console` with a stand-in dist lists `dist/sdui.manifest.json`. Extracted under a scratch `node_modules`, the old subpath throws `ERR_PACKAGE_PATH_NOT_EXPORTED` from a sibling CLI origin, while `resolveSduiManifest` answers `resolved`. - **H2 (Clause-② arm: narrowing).** Round 2, merged tree, with the console copy present (`cmp`-identical stand-in): - a `div` page gives exit 1 (`jsx-forbidden-tag`, `jsx-unknown-component`); - a `box` page gives exit 0, with no findings; - an `avatar` page gives exit 1 (`jsx-forbidden-tag`, `jsx-unknown-component`). With no copy, all three exit 0 with the notice only. `avatar` is registered at the pin (`renderers/data-display/avatar.tsx:17`) and not deprecated, so the console's html compile renders it and nothing refused it before this change. That is the narrowing the `(narrowing)` arm and BREAKING rest on. -⚠️ **Round 1 misread this half.** It took "the renderer still renders `div`" from ruling A's reading, which predates objectui#10757, instead of reading the pin. The pin had landed on `main` (objectstack-ai#20436) before round 1 ran. For `div`, this change moves a refusal the 17.5.0 console already gives at render time to author time. The arm still holds because of the undeclared tags. - **CLI fixtures the live fallback newly refuses** (round 1, with the console copy present): 23 tests went red across `build-json-failure-conversions.e2e` (5), `validate-json-failure-conversions.e2e` (4) and `jsx-gate-manifest-notice.e2e` (14). `lint-conversion-notices.e2e` stayed green, but its page is refused too. The three conversion fixtures moved from `div` to `box`. The notice file's 14 manifest-less cases are skipped by name where the CLI's own console copy exists. They run in the CI job, which builds no console, and their rules are pinned hermetically in `src/utils/sdui-manifest.test.ts`. - **Examples:** only `examples/app-showcase` carries html pages (three). - **H3 (shipped pages stay clean)**, round 2, merged tree. `main` brought a regenerated `sdui.manifest.json` carrying `tier: 'html'` marks (objectstack-ai#20582). - `validate-jsx-pages.production-witness.test.ts`: 5/5 pass. - `examples/app-showcase` with the console copy present: exit 0 on `os validate` / `os compile` / `os lint`, with zero `jsx-*` / `sdui/*` findings. - **H4 (ablation, round 1)**, through `node scripts/ablation-replace.mjs` in WRAP mode: - the anchor `resolve(CONSOLE_PACKAGE_JSON)` went 1 → 0, and `resolve(CONSOLE_SDUI_MANIFEST)` (the old subpath) 0 → 1; - 4 console-leg pins went red ("expected undefined to be defined"); - restore: the blob is back at the HEAD blob `be1f8d4ad33e`, and `git diff HEAD` is empty. The pins import the subject from `src/`, so no `dist/` sits on that path. This round changed no source or test file. ## Tests, at `77338a7186` - The whole CLI `unit` project (`--project unit --maxWorkers=2`) with a real-path `TMPDIR`: 234/234 files, 3347/3347 tests. With the default macOS `TMPDIR`: 232/234. The 2 files are `published-subpath-console.pin` and `published-subpath-hook-body.pin`, 5 cases comparing `/var` against `/private/var`. They are host-only and untouched here. - `pnpm --filter @objectstack/cli typecheck` (`tsc --noEmit` plus `check:test-typecheck`): exit 0. - The four touched nightly `*.e2e` files (`OS_TEST_TIERS=nightly --project integration`): - with the console copy present: 53 passed, 14 skipped; - without it (the CI state): 67/67 passed. The rest of the integration layer is declared to CI. ## Gates, at `77338a7186` - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`: 63 derived, the same 63 as round 1. All 63 exit 0, and `--ran` reconciled "63 run, 0 NOT-MEASURED (a DERIVED zero — all 63 recorded an exit code and none of them is 3)". `check:dual-build-cjs-loads` first answered PREREQUISITE NOT MET, then exit 0 after building its eight missing packages. - `check-adr-0087-registration --base origin/main`, `check-empty-changeset --base origin/main` (it now reads "No changeset from the merge base modified or deleted by this diff") and `check-changeset-no-major --base origin/main`: all exit 0. - Roster rows that could apply, all exit 0: `check-changeset-fixed`, `check-sdui-manifest` (plus `--self-test`), `check:authz-resolver`, `check:error-code-casing`, `check:filter-alias-parity`, `check:cli-examples-parity`, `check:published-readme-exports`, `check:scaffold-emission-policy`, `check:console-injection`. - `pnpm lint` (full repo, not narrowed): exit 0, no output. - `node scripts/check-issue-citations.mjs --base origin/main`: exit 0. - `check:nul-bytes`: exit 0, plus a control-byte scan of the 7 changed files: 0. **Declared narrowing — verification ran UNLOCKED.** `scripts/pm/os-verify-lock.sh` could not take the shared verify lock on this host: no usable `flock`. The shared verify lock is declared Linux-only (`flock` is util-linux, and a stock macOS does not ship it), so the command below was run directly, without the lock — a declared narrowing, not a silent one. No serialization guarantee held for this run, nor for any sibling agent in this container while it ran. every build, test, typecheck, ablation and `pnpm lint` command named above ## Acceptance notes - **Where the `div` → `box` prescription reaches an upgrader.** The CLI's refusal text does not carry it: the gate answers "is not an allowed component" / "is not a known component", from `@objectstack/sdui-parser` (`parse.ts`). The upgrade guide does not carry it either: `packages/spec/scripts/build-upgrade-guide.ts:78` loops majors up to `PROTOCOL_MAJOR`, `PROTOCOL_VERSION` is `17.0.0`, and `docs/protocol-upgrade-guide.md` does not name `ui-html-page-div-refused`. What does carry it: - this changeset's FROM → TO table; - the console's own render-time refusal, which names `box`; - `objectstack migrate meta --from 17`. Measured on a stack with a `div` page, it lists the entry as one of 242 "manual change(s) require your judgment", headed "⚠ [protocol 18] kind:'html' page source …", with `box` as the replacement, and exits 0. - The ledger entry's own `why` text (`packages/spec/src/migrations/entries/semantic/18.ui-html-page-div-refused.ts`, the spec seat's file) still says "`objectstack compile` (which `dev` and `start` run first)", the phrasing corrected here. Noted, not edited. - Release order, flagged by the seat in `5890591366`: Version Packages PR objectstack-ai#20639 carries the ledger entry's changeset. If it merges before this PR, the ledger row ships one release ahead of the CLI refusal it describes. - `packages/cli/src/utils/scaffold-validate.ts` (the note at :128-:133) was re-read. It is true now, so it is not edited. A pre-existing imprecision stays as it was: `os init` reads the invoker's directory, which may carry its own `sdui.manifest.json` (this repository's root does). - Comment drift outside this claim, noted only: - `.github/workflows/lint.yml` (:899) and `scripts/check-sdui-manifest.mjs` (:28-30, :240) still say `resolveSduiManifest()` degrades to parse-only silently; - the header of `packages/lint/src/validate-jsx-pages.ts` still calls manifest validation "not wired"; - `docs/qa/platform-checklist/areas/studio-authoring.json` describes the showcase tree as flex/div/a. - The five macOS-only `published-subpath-*` failures come from a `tmpdir()` path compared with the real path that module resolution returns. They are host-specific. - Measurement scaffolding was all in scratch, or in this worktree's gitignored `packages/console/dist/`, with each stand-in trap-removed. `git status --porcelain` printed 0 lines after every run. --- _Generated by [Claude Code](https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289)_ --------- Co-authored-by: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
Closes #20592
Registers
ui-html-page-div-refused, the protocol-18 ADR-0087 semantic entry for the html-tierdivrefusal that PR #20589 brings to projects with nosdui.manifest.jsonof their own (div→box).Clause-②: no
Generated by Claude Code