feat(sdui-parser): the manifest marks the html tier's intrinsic tags tier: 'html', ported from objectui's lockstep copy - #20582
Conversation
…ckstep copy objectui's sdui-parser, at the console pin dd3f7e1be356, admits `tier: 'html'` on RegistryConfigLike, declares ManifestComponent.tier, has manifestFromConfigs write exactly `'html'` or omit the key, and has generateBlockList section the html tier under its own count. This copy still declared `'public' | 'internal'` and dropped the key, so the tracked manifest serialised through it carried the html tier's intrinsic tags with no marker. The three hunks are copied byte-for-byte from the pin; the tests are objectui's own cases for the same mechanism. Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude <noreply@anthropic.com>
…rinsic tags carry `tier: 'html'` Regenerated with scripts/gen-sdui-manifest-node.mjs over objectui's built tree at dd3f7e1be356, serialised through the ported manifestFromConfigs. Same 107 components in the same order; the only change is `"tier": "html"` on the 48 entries objectui's HTML_TIER_INTRINSICS roster stamps. The record is re-recorded (sha256, date). The unported serializer over the same tree reproduced the old artefact byte-for-byte (sha256 4073897dcdc1), so the tree is the one the tracked file was cut from. Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude <noreply@anthropic.com>
The port widens RegistryConfigLike.tier and adds ManifestComponent.tier, and the console's shipped manifest gains the key on 48 entries: an additive widening of two published surfaces. Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check4 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 — 2 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 f7dd5fce19fa32c590907fa5ab72671af8f28552 && git checkout f7dd5fce19fa32c590907fa5ab72671af8f28552
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f11b5f20a2ee22698c6647c2fb76aa67e99a7a53 ffbb0c48023cb993634c6b4a0b75cf4c0c4bfdaf && git checkout -B drift-repro f11b5f20a2ee22698c6647c2fb76aa67e99a7a53 && git merge --no-ff ffbb0c48023cb993634c6b4a0b75cf4c0c4bfdaf
node scripts/docs-audit/affected-docs.mjs --json f11b5f20a2ee22698c6647c2fb76aa67e99a7a53 |
Contract reviewServed-tier: Inputs: card #20112 (body, all 7 comments, the ruling ① Derived judgmentsAccept-set and public-surface changes the diff implies, each judged:
Question (1), stated once: the ported lines are byte-faithful to objectui at Check-runs on ② Semver level
③ Boundary flagsOpen question 1 (should the seat flip Out-of-scope findings and deviations, each answered or escalated:
Implemented-by: VERDICT: PASS |
…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>
Fixes #20112
Clause-②: yes (widening)
Executes the one item left on this card after PR #20436: port objectui's
tierplumbing into this repository's copy ofpackages/sdui-parser, regeneratesdui.manifest.jsonso the html tier's intrinsic tags carrytier: 'html', and re-record. This follows ruling A (5852014527) as triage re-derived it at pindd3f7e1be356(5882165416). Baseorigin/mainf11b5f20a2, headffbb0c4802.What changed
packages/sdui-parser/src/{index,types,codegen}.ts: the three hunks of objectuibaac95a261(objectui#10735), copied byte-for-byte from objectui at the pindd3f7e1be356:RegistryConfigLike.tieradmits'html';ManifestComponent.tier?: 'html'is new;manifestFromConfigswrites exactly'html'or omits the key;generateBlockListtitles the curated count and lists the html tier in its own section.sdui.manifest.json+scripts/sdui-manifest.record.json: regenerated withscripts/gen-sdui-manifest-node.mjsover objectui's built tree at the pin."tier": "html"lands on 48 entries and nothing else moves. The record's sha256 and date are re-recorded.packages/sdui-parser/src/__tests__/html-tier-manifest.test.ts: objectui's own ten cases for the mechanism (html-tier-manifest-10735.test.tsat the pin), with this repository's header..changeset/20112-sdui-parser-html-tier-stamp.md:minorfor@objectstack/sdui-parser(a new accepted value and a new type member) and for@objectstack/console(itsdistships the regenerated manifest).Byte-faithfulness, measured. For each of the three files, the
+/-lines of this diff equal the+/-lines of objectuibaac95a261for the same file (index 15 lines, types 17, codegen 39;cmpidentical). Every added line is present verbatim in the file at the pin.generateBlockList,ManifestComponentandmanifestFromConfigsextracted whole arecmp-identical to the pin. The one later objectui commit to these files in the window (fec3b1a8b) rewrites two citation comments unrelated totier.The PM's mechanism hypotheses, measured
tier?: 'public' | 'internal' | 'html'(index.ts:87) andManifestComponent.tier(types.ts:165, member at:180). This copy declared'public' | 'internal'(index.ts:72). The tracked manifest had 107 components and 0 carrying anytierkey."tier": "html", no other tier value appears, and 0 entries change any other field (each entry deep-compared withtierremoved).git diff --stat: 48 insertions, 0 deletions. The 48th tag iscode. objectui#10756 (PR objectui#10776,29b45f6a0) added it toHTML_TIER_INTRINSICSafter the triage count was taken, and the pin contains it.divandkbdstay absent.4073897dcdc1…, unchanged file).d0666ac585db…, 103874 bytes.node scripts/check-sdui-manifest.mjs:✓ … intact (sha256 d0666ac585db…, 107 components) and recorded at the live objectui pin dd3f7e1be356….check:sdui-lockstepis green both before and after the port. With the three source files restored tof11b5f20a2(the drift state: 0 hits for the three-arm union, 0 forManifestComponent.tier), it printsOK — this copy is byte-identical to objectui@dd3f7e1be356 …. It compares the grammar region, the diagnostic-code set and thenot-a-containerpredicate, never a type union. So it cannot see this drift. The gate is not widened (see Acceptance notes).gen:sdui-locksteprewrotepackages/sdui-parser/objectui-lockstep.jsonbyte-identically (0 diff lines), so the lockstep record was already current at the pin. The re-recorded artefact is the manifest's record.compile()over the three shippedkind:'html'pages (capability-map,command-center-jsx,start-here), against the old manifest and against the new one:ok=truewith 0 errors and 0 warnings on both sides;boxdeleted from the manifest turnsstart-heretook=false, 54 errors.@objectstack/lint's production witness reads the regenerated file from disk and passes (wired run over the three shipped html pages is clean).Verification
All runs are at head
ffbb0c4802unless noted.pnpm --filter @objectstack/sdui-parser build, thenexec vitest run --maxWorkers=2, thentypecheck: 12 files, 199 tests passed;tsc --noEmitclean.--listFilesshows the typecheck program includes the new test file.9a77e00415. The three source files were restored tof11b5f20a2, with a restore trap:grep -c(three-arm union 1→0,tier?: 'html'0, html section 0);tsc --noEmitexits 2 with six errors on the test (TS2322 ×4, TS2339, TS2367).git checkout HEAD -- …. Each blobhash-objectequalsHEAD, andgit diff HEADis empty.pnpm --filter '@objectstack/lint^...' build, then@objectstack/lintvitest run --maxWorkers=2(115 files, 5358 passed, 5 skipped) andtypecheck(tsc --noEmitpluscheck:test-typecheckOK).pnpm lint(eslint over the whole repository,--no-inline-config): exit 0 atffbb0c4802. This was a full run, not a narrowed one.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands: 71 derived commands, all run. Reconciled with--ran:71 derived famil(ies) accounted for — 68 run, 3 NOT-MEASURED.dist):check:dual-build-cjs-loads,check:lean-entry-closure,check:type-check-debt. Narrowed stand-in for the first:require()ofdist/index.jsandimport()ofdist/index.mjsboth loadmanifestFromConfigs/compile.check:bash32-floor's--self-testfails 7 of 179 cases on this macOS host (bash 3.2). Control: the same command in a detached worktree atf11b5f20a2fails the same 7 of 179. This diff touches no shell and not that script.check-changeset-fixed,check-published-list-mirrors,check:authz-resolver,check:console-injection,check:error-code-casing,check:filter-alias-parity,check:i18n-stale-fill,check:lockstep-package-count,check:widget-option-census,spec check:react-blocks,check:cli-examples-parity,check:scaffold-emission-policy.check:published-readme-exportsis NOT MEASURED (exit 3).@objectstack/sdui-parserships no README.node scripts/check-issue-citations.mjs --base origin/main: exit 0 (4 cross-repo citations).origin/mainis stillf11b5f20a2, an ancestor of the head, so nothing needed merging.check-changeset-no-major,check-empty-changesetandcheck-adr-0087-registration(all--base origin/main): exit 0.Acceptance notes
check:sdui-lockstepcannot see a type-union drift between the two copies, as H2 measures. This port closes the one such drift the card named. The gate is deliberately not widened.RegistryConfigLikealso carrieslazy?: booleanand a longerisContainerdocblock. objectui hasbody-dialect.tsandprovenance.ts, which this copy lacks.kanban-quick-add.ts,dashboard-widget-options.ts,parse.tsandvalidate.tsdiffer outside the recorded region. None of that is this card's. Carrier: none.scripts/gen-sdui-manifest-node.mjsfails on macOS with the defaultTMPDIR. Its resolve hook comparesparentURLwith a runner URL built fromos.tmpdir()(/var/folders/…), but Node reports the realpath (/private/var/folders/…). So no bare specifier is re-anchored, and the run exits 1 with 16Cannot find package '@object-ui/…'failures. WithTMPDIRset to a realpath directory it succeeds. That is how this PR's regeneration ran. Reported to the seat, not fixed here.check:sdui-lockstep"holds the two copies byte-equal". It holds the grammar region, the code set and the containment predicate only. The header is stale prose. Carrier: none..changeset/console-dd3f7e1be356.mdstates that the published manifest declares the html tier's elements "markedtier: 'html'". For the manifest@objectstack/consoleships, that was untrue until this PR. It is true once this lands in the same release..cache/objectui-dd3f7e1be356, symlinked read-only into this worktree's.cache/and removed afterwards), rather than rebuilding objectui on a shared host. The control leg under H1 proves that tree reproduces the tracked artefact byte-for-byte.Clause-②: nois the claim's declaration, copied verbatim. The changeset grades both packagesminor(a new accepted value and a new type member; a new key on the shipped manifest), so the level axis passes under either reading of the line.Declared narrowing — verification ran UNLOCKED.
scripts/pm/os-verify-lock.shcould not take the shared verify lock on this host: no usable
flock. The sharedverify lock is declared Linux-only (
flockis util-linux, and a stock macOS doesnot 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.
Generated by Claude Code