Skip to content

feat(sdui-parser): the manifest marks the html tier's intrinsic tags tier: 'html', ported from objectui's lockstep copy - #20582

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-20112-sdui-parser-tier-port
Sep 29, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-20112-sdui-parser-tier-port

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #20112
Clause-②: yes (widening)

Executes the one item left on this card after PR #20436: port objectui's tier plumbing into this repository's copy of packages/sdui-parser, regenerate sdui.manifest.json so the html tier's intrinsic tags carry tier: 'html', and re-record. This follows ruling A (5852014527) as triage re-derived it at pin dd3f7e1be356 (5882165416). Base origin/main f11b5f20a2, head ffbb0c4802.

What changed

  • packages/sdui-parser/src/{index,types,codegen}.ts: the three hunks of objectui baac95a261 (objectui#10735), copied byte-for-byte from objectui at the pin dd3f7e1be356:
    • RegistryConfigLike.tier admits 'html';
    • ManifestComponent.tier?: 'html' is new;
    • manifestFromConfigs writes exactly 'html' or omits the key;
    • generateBlockList titles the curated count and lists the html tier in its own section.
  • sdui.manifest.json + scripts/sdui-manifest.record.json: regenerated with scripts/gen-sdui-manifest-node.mjs over 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.ts at the pin), with this repository's header.
  • .changeset/20112-sdui-parser-html-tier-stamp.md: minor for @objectstack/sdui-parser (a new accepted value and a new type member) and for @objectstack/console (its dist ships the regenerated manifest).

Byte-faithfulness, measured. For each of the three files, the +/- lines of this diff equal the +/- lines of objectui baac95a261 for the same file (index 15 lines, types 17, codegen 39; cmp identical). Every added line is present verbatim in the file at the pin. generateBlockList, ManifestComponent and manifestFromConfigs extracted whole are cmp-identical to the pin. The one later objectui commit to these files in the window (fec3b1a8b) rewrites two citation comments unrelated to tier.

The PM's mechanism hypotheses, measured

  • H0: holds. At the pin, objectui declares tier?: 'public' | 'internal' | 'html' (index.ts:87) and ManifestComponent.tier (types.ts:165, member at :180). This copy declared 'public' | 'internal' (index.ts:72). The tracked manifest had 107 components and 0 carrying any tier key.
  • H1: holds, with a different count: 48, not 47. After the port and regeneration, the manifest keeps the same 107 keys in the same order. 48 entries gain "tier": "html", no other tier value appears, and 0 entries change any other field (each entry deep-compared with tier removed). git diff --stat: 48 insertions, 0 deletions. The 48th tag is code. objectui#10756 (PR objectui#10776, 29b45f6a0) added it to HTML_TIER_INTRINSICS after the triage count was taken, and the pin contains it. div and kbd stay absent.
    • Control leg for the build tree: the UNPORTED serializer over the same built tree reproduced the tracked artefact byte-for-byte (sha256 4073897dcdc1…, unchanged file).
    • After the port: sha256 d0666ac585db…, 103874 bytes. node scripts/check-sdui-manifest.mjs: ✓ … intact (sha256 d0666ac585db…, 107 components) and recorded at the live objectui pin dd3f7e1be356….
  • H2: holds. check:sdui-lockstep is green both before and after the port. With the three source files restored to f11b5f20a2 (the drift state: 0 hits for the three-arm union, 0 for ManifestComponent.tier), it prints OK — this copy is byte-identical to objectui@dd3f7e1be356 …. It compares the grammar region, the diagnostic-code set and the not-a-container predicate, never a type union. So it cannot see this drift. The gate is not widened (see Acceptance notes).
    • Re-record: gen:sdui-lockstep rewrote packages/sdui-parser/objectui-lockstep.json byte-identically (0 diff lines), so the lockstep record was already current at the pin. The re-recorded artefact is the manifest's record.
  • H3: holds. compile() over the three shipped kind:'html' pages (capability-map, command-center-jsx, start-here), against the old manifest and against the new one:
    • all three are ok=true with 0 errors and 0 warnings on both sides;
    • the full result objects, tree included, are deep-equal (0 of 3 moved).
    • Lit control: the same harness with box deleted from the manifest turns start-here to ok=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 ffbb0c4802 unless noted.

  • pnpm --filter @objectstack/sdui-parser build, then exec vitest run --maxWorkers=2, then typecheck: 12 files, 199 tests passed; tsc --noEmit clean. --listFiles shows the typecheck program includes the new test file.
  • Ablation, from the committed fix at 9a77e00415. The three source files were restored to f11b5f20a2, with a restore trap:
    • landing proven by grep -c (three-arm union 1→0, tier?: 'html' 0, html section 0);
    • the new test file goes 2 failed / 8 passed: the stamp case and the block-list section case. The other eight pin the readers the stamp must not change, and stay green by design;
    • tsc --noEmit exits 2 with six errors on the test (TS2322 ×4, TS2339, TS2367).
    • Restored with git checkout HEAD -- …. Each blob hash-object equals HEAD, and git diff HEAD is empty.
  • Consumer, the only importer: pnpm --filter '@objectstack/lint^...' build, then @objectstack/lint vitest run --maxWorkers=2 (115 files, 5358 passed, 5 skipped) and typecheck (tsc --noEmit plus check:test-typecheck OK).
  • pnpm lint (eslint over the whole repository, --no-inline-config): exit 0 at ffbb0c4802. 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.
    • NOT MEASURED (exit 3, PREREQUISITE NOT MET, each needs every package's dist): check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt. Narrowed stand-in for the first: require() of dist/index.js and import() of dist/index.mjs both load manifestFromConfigs / compile.
    • One red, host-bound: check:bash32-floor's --self-test fails 7 of 179 cases on this macOS host (bash 3.2). Control: the same command in a detached worktree at f11b5f20a2 fails the same 7 of 179. This diff touches no shell and not that script.
  • Artifact-roster rows that could apply, all exit 0: 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-exports is NOT MEASURED (exit 3). @objectstack/sdui-parser ships no README.
  • node scripts/check-issue-citations.mjs --base origin/main: exit 0 (4 cross-repo citations). origin/main is still f11b5f20a2, an ancestor of the head, so nothing needed merging.
  • check-changeset-no-major, check-empty-changeset and check-adr-0087-registration (all --base origin/main): exit 0.

Acceptance notes

  • check:sdui-lockstep cannot 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.
  • The two copies still differ outside the lockstep's three comparisons. objectui's RegistryConfigLike also carries lazy?: boolean and a longer isContainer docblock. objectui has body-dialect.ts and provenance.ts, which this copy lacks. kanban-quick-add.ts, dashboard-widget-options.ts, parse.ts and validate.ts differ outside the recorded region. None of that is this card's. Carrier: none.
  • scripts/gen-sdui-manifest-node.mjs fails on macOS with the default TMPDIR. Its resolve hook compares parentURL with a runner URL built from os.tmpdir() (/var/folders/…), but Node reports the realpath (/private/var/folders/…). So no bare specifier is re-anchored, and the run exits 1 with 16 Cannot find package '@object-ui/…' failures. With TMPDIR set to a realpath directory it succeeds. That is how this PR's regeneration ran. Reported to the seat, not fixed here.
  • The generator's header says 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.
  • The pending .changeset/console-dd3f7e1be356.md states that the published manifest declares the html tier's elements "marked tier: 'html'". For the manifest @objectstack/console ships, that was untrue until this PR. It is true once this lands in the same release.
  • The regeneration read an existing built objectui tree at the pin (another worktree's .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-②: no is the claim's declaration, copied verbatim. The changeset grades both packages minor (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.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.

pnpm --filter @objectstack/sdui-parser build
pnpm --filter @objectstack/sdui-parser build && pnpm --filter @objectstack/sdui-parser exec vitest run --maxWorkers=2 && pnpm --filter @objectstack/sdui-parser typecheck
pnpm lint
pnpm --workspace-concurrency=2 --filter '@objectstack/lint^...' build
pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 --reporter=verbose src/validate-jsx-pages.production-witness.test.ts src/validate-jsx-pages.test.ts
pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 && pnpm --filter @objectstack/lint typecheck

Generated by Claude Code

hotlong and others added 3 commits September 29, 2026 12:46
…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>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

Which tree this was computed on

This run read content/docs from f7dd5fce19fa32c590907fa5ab72671af8f28552 — the merge of head ffbb0c48023cb993634c6b4a0b75cf4c0c4bfdaf into base f11b5f20a2ee22698c6647c2fb76aa67e99a7a53, which is what actions/checkout gives a pull_request run. Not the PR head.

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

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ffbb0c48023cb993634c6b4a0b75cf4c0c4bfdaf
Local-runs: none

Inputs: card #20112 (body, all 7 comments, the ruling 5852014527 and triage's re-derivation 5882165416 included), PR #20582 (body, 7-file list, net diff against main at f11b5f20a2), the head's check-runs (two REST reads, the later at 05:21 UTC), and objectui at the pin dd3f7e1be356 read with git show from the local clone (no checkout). The port is judged against ruling A as triage re-derived it: item 1 (the lockstep tier port) was the one item left; items 2 and 3 landed in PR #20436.

① Derived judgments

Accept-set and public-surface changes the diff implies, each judged:

  1. RegistryConfigLike.tier admits 'html' (packages/sdui-parser/src/index.ts) — an accept-set widening on the serializer's input, a type exported from the package's one entry. Right. The 15 +/- lines of this file equal the 15 +/- lines objectui baac95a261 made to the same file, and every added line is verbatim in the file at the pin. Not judged wrong: the interface as a whole still differs from the pin outside this hunk (the isContainer and binding docblocks, objectui's lazy?: boolean) — pre-existing divergence, disclosed in the PR's Acceptance notes, not this card's.
  2. ManifestComponent.tier?: 'html' is new (types.ts) — a new optional member on a published type. Right. 17 + lines, identical to objectui's hunk; the interface extracted whole is byte-identical to the pin.
  3. manifestFromConfigs writes tier: c.tier === 'html' ? 'html' : undefined — the manifest gains exactly one value on exactly the stamped entries, and JSON.stringify drops the undefined, so every curated entry serialises as before. Right. The function extracted whole is byte-identical to the pin; the manifest diff (item 5) is the proof that nothing else moved.
  4. generateBlockList counts curated blocks in its title and sections the html tier (codegen.ts, 39 +/- lines identical to objectui's hunk; the function extracted whole is byte-identical to the pin). Right, and it owes no regeneration: no file outside packages/sdui-parser/src calls generateBlockList or carries a # SDUI public blocks artefact, so no tracked block list drifts. generateDts is untouched and has no caller outside the package either.
  5. sdui.manifest.json: 48 hunks, every one a single added line "tier": "html", and 0 deleted lines; 107 components before and after, same key order, top-level shape unchanged. The 48 keys are exactly objectui's HTML_TIER_INTRINSICS at the pin (packages/core/src/registry/html-tier-intrinsics.ts: 38 flow/inline tags, span, table, label, and the 7 sectioning tags). div and kbd stay absent. Right. Triage's 47 was counted before objectui#10756 (29b45f6a0) added code to the roster; the pin contains it, so 48 is the correct count and triage's figure is superseded, not contradicted. So (2) of the seat's questions: the regeneration changes exactly the tier key on the html-tier entries and nothing else.
  6. scripts/sdui-manifest.record.json: sha256 and generatedAt moved, nothing else. The file at the head hashes to d0666ac585db8fa974471d9ebf33b906be776d77a170e7e5ecac774095d7b26a (103874 bytes), equal to the record; components: 107 equal; objectuiSha unchanged and equal to .objectui-sha at the base. Right — check-sdui-manifest.mjs's TAMPER and STALENESS legs both hold on the bytes.
  7. compile() verdicts on the three shipped html pages: unchanged — question (3). Structural, not run: at the base, no reader in parse.ts or validate.ts reads a manifest entry's tier (the only tier read in the package is manifestFromConfigs's publicOnly filter on the registry-config side); the manifest's key set, inputs and isContainer are byte-identical before and after. A whitelist reader cannot move on a key it never reads. The wired witness (@objectstack/lint's validate-jsx-pages.production-witness.test.ts, which reads the regenerated file from disk) runs in Test Core — see the check-run line below; not inferred here.
  8. The lockstep record packages/sdui-parser/objectui-lockstep.json is unchanged, and rightly so. It holds objectui's side only, recorded at dd3f7e1be356: the grammar region of parse.ts, the 26 diagnostic codes and the containment predicate of validate.ts. None of the three is touched by this diff, so a re-record is byte-identical. Triage's "re-record the lockstep" is discharged by the manifest record (item 6); the lockstep record was already current. Right. The gate's blindness to a type-union drift (the dev's H2) is real, was named by triage first, and is deliberately not widened here — carried to ③.
  9. Test file html-tier-manifest.test.ts: objectui's ten cases from html-tier-manifest-10735.test.ts at the pin, verbatim in every it body; the header docblock is this repository's and the three describe titles drop the (objectui#10735) suffix. Disclosed as a deviation; no case is weakened. Right.
  10. Public exports: no export name is added or removed from @objectstack/sdui-parser; @objectstack/console publishes no new entry. The only public-surface growth is the two type widenings (1, 2) and the new key on the shipped manifest (5). No consumer in this repository switches on ManifestComponent.tier or RegistryConfigLike.tier; packages/lint imports only parseJsx, compile and Manifest.

Question (1), stated once: the ported lines are byte-faithful to objectui at dd3f7e1be356. For each of the three source files the PR's +/- line set equals baac95a261's for the same file (index 15, types 17, codegen 39), every added line is present verbatim in the pin file, and the three ported units extracted whole (generateBlockList, ManifestComponent, manifestFromConfigs) are byte-identical to the pin. The one later objectui commit to these files in the window (fec3b1a8b) is citation prose and does not reach the ported lines.

Check-runs on ffbb0c4802 at the later read (05:21Z), newest run per name, 32 names: 22 success, 5 skipped (Auto Label, Build Docs, Check PR Size, Console Pin Gate, Packed-tarball smoke), 5 in_progress — Lint & Repo Gates (the job that runs check:sdui-lockstep and check-sdui-manifest.mjs), Test Core (1/6), (5/6), (6/6), Type Check · workspace. Completed and green among the ones this diff bears on: Build Core, Check Changeset (two runs, the newer started 05:13:31Z after the body edit at 05:13:26Z, so the level axis judged the flipped line), Type Check · source gates, Type Check · consumer gates, Type Check · debt ledger, Test Core (2/6)(3/6)(4/6), Dogfood Regression Gate and its three shards, Dogfood Verify CLI, Temporal Conformance, the four PM guards. No check has failed. The five in progress are recorded as in progress, not inferred.

② Semver level

  • @objectstack/sdui-parser: minor — right. Two purely additive widenings of a published type surface (a new accepted union member on RegistryConfigLike.tier, a new optional member on ManifestComponent). The 2026-09-04 ruling (batch [WIP] Add query enhancements and advanced validation features #35) puts an additive widening of a published package's public surface at minor or above; nothing is removed or renamed, so no (narrowing) arm, no migration line and no ADR-0087 marker is owed. The feat(sdui-parser) title agrees with the level.
  • @objectstack/console: minor — right. scripts/build-console.sh copies the tracked sdui.manifest.json into the console's dist, so the published tarball's bytes gain a key on 48 entries: additive. The pending .changeset/console-dd3f7e1be356.md already grades the console minor for this release, so this entry cannot lower anything and states the manifest change under its own name. A patch would also have been defensible for shipped bytes; minor is not wrong.
  • One changeset file naming two packages is the right shape: both publish, and the claim's "one .changeset/20112-*.md if a published package changes" is met by one file.
  • Clause-②: yes (widening) — the seat's flip is right, question (4). The level axis defines the declaration as "a new key on a published payload"; this PR puts tier on 48 entries of the manifest the console ships, and adds an accepted value and a type member to @objectstack/sdui-parser's exported types. objectui declared Clause-②: yes and graded minor for the identical act (ruling A, item 1: minor, Clause-②: yes), and this port is that act's other half. The claim's original no would have been a false declaration under the directional rule; amending it before landing was the correct repair. One refinement for the record: under the contract-review reference's own rule, a file reachable only outside a package's exports map is shipped bytes, not a published accept set — the console's exports maps only ./package.json, so the manifest leg alone would not have triggered yes. The yes stands on the sdui-parser leg, whose two types are reachable from the package entry. The arm (widening) is legal beside yes and reads as not breaking. The level axis is satisfied: @objectstack/sdui-parser, whose src/** the diff moves, is graded minor, and Check Changeset concluded success on the head after the body edit.

③ Boundary flags

Open question 1 (should the seat flip Clause-②: no to yes?): answered — B, and already done. The PR body line 2 reads Clause-②: yes (widening) and the claim comment was amended in place to Clause-②: yes; judged right in ②.

Out-of-scope findings and deviations, each answered or escalated:

  • scripts/gen-sdui-manifest-node.mjs exits 1 on macOS with the default TMPDIR (the resolve hook compares parentURL against a runner URL built from os.tmpdir() while Node reports the realpath). Escalated to the seat: file it. No issue exists (REST search on the dedupe words returns only feat(sdui-parser): the manifest marks the html tier's intrinsic tags tier: 'html', ported from objectui's lockstep copy #20582 and [Decision] may a kind:'html' page author intrinsic HTML tags (div, a)? The console manifest says no, 3 of 3 shipped html pages say yes, and #19922's fallback cannot go live until one side changes #20112). It is the sole producer of a tracked artefact, it fails on the maintainer's own host, and the fix shape is one realpathSync on the mkdtemp dir. Outside this PR's file surface; not a reason to hold this PR, because the control leg (the unported serializer over the same tree reproduced the tracked artefact's sha256 4073897dcdc1… byte-for-byte) shows the workaround changed nothing in the output.
  • check:bash32-floor self-test fails 7 of 179 on this macOS host — the same 7 at the base f11b5f20a2, so host-bound; the diff touches no shell; CI runs Linux bash 5. Answered: not this diff.
  • The two parser copies still differ outside the lockstep's three comparisons, and the gate is blind to a type-union drift (H2: green on both the drift leg and the head). Answered for this card (the ruling's item was the port, and the dev rightly did not widen the gate inside it) and escalated as a follow-up card: a lockstep record that cannot see RegistryConfigLike.tier diverging is the gap triage itself named in 5882165416. Carrier: none today; the seat decides whether to file or fold it into the next lockstep card.
  • The generator's header claims check:sdui-lockstep "holds the two copies byte-equal" — stale prose; the gate holds three regions. Answered: doc nit, belongs with the previous item.
  • The pending .changeset/console-dd3f7e1be356.md says the published manifest marks the html tier tier: 'html' — untrue for the console's shipped manifest until this PR, true once both land in one release. Escalated as a landing-order flag: PR chore: version packages #17076 (chore: version packages) is open; if it cuts a release before this PR merges, that changeset ships a false sentence in the console CHANGELOG. This PR should land first.
  • The regeneration read another worktree's built objectui tree at the pin, symlinked read-only rather than rebuilding on the shared host. Answered: the record's modulesRoot is unchanged and the control leg proves the tree reproduces the tracked artefact; the provenance is sound.
  • gen:sdui-lockstep refused with the clone at main (head off pin) and was re-run against the pin tree, writing byte-identically. Answered: that refusal is the gate working as designed, and item 8 above explains why the record could not move.
  • Test header and describe titles differ from objectui's. Answered: disclosed; the cases are verbatim.
  • Verification ran without the shared verify lock (no flock on macOS) — a declared narrowing. Answered: the check-runs on the head are the gate verdicts this record reads; local results are the dev's evidence, not the gate.
  • On merge, Fixes #20112 auto-closes the card. Seat follow-through, not a diff matter: the auto-close leaves pm:dispatched, pm:blocking and domain:cli on a closed card, and [finding] the CLI's @objectstack/console manifest fallback can never resolve: resolveSduiManifest() asks for @objectstack/console/dist/sdui.manifest.json, but the console's exports map publishes only ./package.json #19922's Blocked-by: #20112 index resolves at the same moment; the landing sweep clears the labels.

Implemented-by: claude/issue-20112-sdui-parser-tier-port
Reviewed-by: local_1d2a197c-c20e-4e90-9be8-413d4d432289

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 29, 2026 05:39
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 29, 2026
Merged via the queue into main with commit a093ce3 Sep 29, 2026
43 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20112-sdui-parser-tier-port branch September 29, 2026 06:48
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

1 participant