Skip to content

fix(spec): make the declaration-parity ratchet able to fail on a spec row with no manifest component - #19270

Closed
os-bill wants to merge 2 commits into
mainfrom
claude/issue-18407-sdui-manifest-object-tree
Closed

os-bill wants to merge 2 commits into
mainfrom
claude/issue-18407-sdui-manifest-object-tree

Conversation

@os-bill

@os-bill os-bill commented Sep 20, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #18407

Clause-②: no

TL;DR

Regenerating sdui.manifest.json does not produce object-tree, and no pin bump would
change that — measured, byte-identically, below. What this PR lands is the other half of the
card: check:react-declaration-parity can now fail on a spec row whose block has no
manifest component. It goes red on object-tree today, and that red is the deliverable, not
a mistake
— the repository-side cure is upstream in objectui and is named in the failure text.

⚠️ This PR is a draft on purpose. Landing it as-is puts a standing red on a required context
for every PR in the repo. See "What has to happen before this lands".

Leg 1 — re-measured on fetched origin/main (7d0f911), independently

git show origin/main:sdui.manifest.json
  components                            57
  component NAMES containing "tree"     []        == zero, a reading
  LIT CONTROL, same parse, first 6 names read out:
    object-grid · list-view · object-form · embeddable-form ·
    object-master-detail-form · object-kanban
  LIT CONTROL, same parse, every name starting "object-":
    object-grid, object-form, object-master-detail-form, object-kanban,
    object-calendar, object-gantt, object-timeline, object-map,
    object-metric, object-chart, object-pivot

The parser reads names, so the zero is a reading and not a failed search.

The other half, re-measured in objectstack-ai/objectui at the pinned
53ded82bf7a494f54e344e19099dbf00854b8694 (.objectui-sha, untouched by this PR):
packages/plugin-tree/src/index.tsx registers the same renderer twice —
ComponentRegistry.register('object-tree', …) at line 34 and
ComponentRegistry.register('tree', …) at line 41. Both carry a namespace, neither carries a
tier. That checkout is shallow (git rev-parse --is-shallow-repository answers true); the
pinned commit object is present, so every reading above is a blob read at a named commit, not a
windowed history query, and none of them needed the truncated history.

Leg 2 — regeneration is a byte-identical no-op

node scripts/gen-sdui-manifest-node.mjs (the generator that writes the tracked artefact; the
repo's pnpm sdui:manifest is the browser route and needs a vendored objectui build) was run
against published @object-ui/* 17.6.0, the version the pinned commit declares:

→ installing @object-ui/* 17.6.0 ... → enumerating the registry (16 modules)...
✓ wrote sdui.manifest.json (57 components, 66910 bytes, sha256 49211fee7792…)

After the run, with git add -A performed FIRST and git diff --cached read after it, the
manifest was not in the diff at all:

git rev-parse :sdui.manifest.json            78f870e42fe9fec1e9fad2e17c48370d8eca2cbe   (index)
git hash-object sdui.manifest.json           78f870e42fe9fec1e9fad2e17c48370d8eca2cbe   (worktree)
git rev-parse origin/main:sdui.manifest.json 78f870e42fe9fec1e9fad2e17c48370d8eca2cbe
sha256sum sdui.manifest.json                 49211fee7792…  == scripts/sdui-manifest.record.json

The only thing the run changed was generatedAt in the provenance record, 2026-09-08 →
2026-09-20. The artefact is unchanged, so that record still describes it accurately; the
date-only churn was reverted and the tree is clean against origin/main. Nothing in this PR
hand-edits a generated artefact, and nothing in it touches .objectui-sha.

Leg 3 — what decides it: registered, but not public

The gate's own words are "NO component in the manifest — not registered or not public".
Measured against the same published 17.6.0 install the generator used, it is the second:

getAllConfigs()     494 configs — NAMES containing "tree":
                    ["ui:tree-view","plugin-tree:object-tree","view:tree"]   (deduped)
getPublicConfigs()   57 configs — NAMES containing "tree": []
  LIT CONTROL getAllConfigs()    grid names: ui:grid, field:grid, plugin-grid:object-grid,
                                             view:grid, plugin-grid:import-wizard,
                                             plugin-dashboard:dashboard-grid
  LIT CONTROL getPublicConfigs() grid names: object-grid, grid
PUBLIC_BLOCKS        57 entries — NAMES containing "tree": []
getConfig('object-tree') -> registered=true  tier=undefined  namespace='plugin-tree'
                            inputs=['objectName','tree']
getConfig('tree')        -> registered=true  tier=undefined  namespace='view'
  LIT CONTROL getConfig('object-grid') -> registered=true tier=undefined

PUBLIC_BLOCKS has exactly 57 entries and the manifest has exactly 57 components — the public
tier IS the manifest. The file and the lines that decide it, all in objectui at the pinned sha:

what where says
the curated list packages/core/src/registry/public-blocks.ts lines 26–117 57 tags, neither 'object-tree' nor 'tree' among them
path 1 into the contract packages/core/src/registry/Registry.ts line 821 for (const tag of PUBLIC_BLOCKS) add(tag, this.getContractConfig(tag));
path 2 into the contract packages/core/src/registry/Registry.ts line 824 if (cfg.tier === 'public' && !key.includes(':')) add(key, cfg);
why neither path fires packages/plugin-tree/src/index.tsx lines 34–39 / 41–46 both registrations pass namespace + label + category + inputs, and no tier

⇒ Both producers — the node generator and the browser dump — faithfully serialize a registry
that genuinely does not expose object-tree on the public tier. The artefact is correct. The
divergence is that a spec row expects a component the public tier does not carry. This
reproduces, independently, the measurement the domain:spec seat recorded on the card.

What this PR changes

check:react-declaration-parity could not fail on its loudest signal. The baseline ratchet
suppressed the missing axis behind

if (state.missing && base && !base.missing) regressions.push(…)

With no baseline row there is no base, so a spec row whose block the public registry does not
carry arrived pre-accepted: the gate printed its ✗ line and exited 0, and would have kept
doing so for as long as nobody regenerated the baseline. Measured on origin/main before the
change — the same run lint.yml performs on every PR:

✗ object-tree: NO component in the manifest — not registered or not public.
Summary: 126 spec-only divergences, 1 blocks missing from the registry.
## Baseline ratchet
✓ no new DECLARATION divergence vs accepted baseline
  exit 0

…while the very same gate in plain --strict mode (no baseline) already exits 1 on that
same tree. The teeth existed; only the mode CI actually runs swallowed them.

The asymmetry is what makes this a bug rather than a policy: registryOnly was already
ratcheted against an absent baseline row — baseRO falls back to the empty set, so every
registry-only input on an unbaselined block reports as new. Only missing carried the extra
conjunct. (The comment sitting under that loop, "a brand-new block … is fine (purely additive
coverage)", was therefore not true of the code it described.)

So:

⛔ No missing: true baseline row is added by this PR, and packages/spec/react-declaration-parity.baseline.json is not touched.

The fixtures were resting on the hole

The exit-code fixtures in check-react-blocks-declaration-parity.test.ts fed the gate a
one-component manifest, so twelve of the thirteen evaluated blocks were absent from every
one of them — including the control that claims "exits 0 when the manifest matches the accepted
baseline". They passed only because an absent, unbaselined block could not regress. They now
declare every evaluated block, with the roster read off the gate's own report rather than
restated (a literal list would rot the day a ComponentPropsMap row lands), and omit spells
out the absence where an absence IS the subject.

Reverse verification — both legs, on-disk proof, restored byte-identically

Run through scripts/ablation-replace.mjs, from the committed state, so the red/green numbers
are trustworthy and the restore has a real restore point:

leg mutation on-disk proof result
A put && base && !base.missing back anchor 1 → 0, blob 8bda7e2d5e7f → d4bf6f71d12a 3 failed / 33 passed — exactly the three pins of the new red; every control stayed green, and the harness reproduced the hole verbatim (✗ object-grid: NO component in the manifest followed by ✓ no new DECLARATION divergence)
B neutralise the --update refusal predicate anchor 1 → 0, blob 8bda7e2d5e7f → d81336b2a156 1 failed / 35 passed — only the --update pin; the "still writes when nothing is missing" control stayed green

Both restored with blob == HEAD (8bda7e2d5e7f) and git diff HEAD empty. No ablation
artefact is left in the tree.

Checks — real exit codes

check exit note
pnpm --filter @objectstack/spec build 0 required before any of the below is believable
pnpm --filter @objectstack/spec typecheck 0 includes check:scripts-typecheck, which compiles both changed files
pnpm --filter @objectstack/spec test 0 500 files / 14649 tests passed
check-react-blocks-declaration-parity.test.ts alone 0 36 passed (30 before, +6 new)
eslint . --no-inline-config (the whole repo, not narrowed) 0 6914 files linted, 0 files with findings — read from --format json
check:react-declaration-parity --baseline … --strict 1 ⚠️ RED, and intended — see below
53 further derived gate families 0 derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 49f7e0d23e, then run; list and logs in the report comment
check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt 3 each prints PREREQUISITE NOT MET … NOT a pass: nothing was measured — they read a full pnpm build closure. NOT MEASURED, not findings
check:pm-dispatch-gates 124 NOT MEASURED: its self-test exceeds a 9-minute foreground budget on this shared box. Whole-tree family; this diff does not touch scripts/pm/dispatch-gates.mjs

The red, in full:

## Baseline ratchet
⚠ NEW declaration divergence vs accepted baseline:
    - object-tree: declared in the spec, NO component in the manifest, and NO baseline row
      — so nothing here could have "regressed". The absence IS the divergence; it is not
      onboarding slack.

  → NO COMPONENT IN THE MANIFEST is a different class and has NO --update escape: the spec
    declares a block the public registry does not expose, so there is no prop-level
    disagreement to accept. Expose it on the PUBLIC tier (objectui `PUBLIC_BLOCKS`, or
    `tier: 'public'` on the registration) and regenerate — `node scripts/gen-sdui-manifest-node.mjs`
    — or drop the spec row. ⛔ Never a baseline row: that accepts the divergence instead of
    reporting it.

Blast radius is exactly one block. Of the thirteen blocks this gate evaluates, four have no
baseline row (object-gantt, object-map, object-timeline, object-tree — the committed
baseline's last write predates #18403, which added those ComponentPropsMap rows), and only
object-tree is absent from the manifest. The other three are present and stay green.

What has to happen before this lands

Lint & Repo Gates is a required context, so merging this while object-tree has no manifest
component puts a standing red in front of every PR in the repo. Two routes, neither of which is
this seat's to pick:

  1. objectui first. Add 'object-tree' to PUBLIC_BLOCKS (or tier: 'public' on the
    plugin-tree registration), release, bump .objectui-sha, regenerate the manifest — then
    this PR is green and lands with it. That is the shape AGENTS.md already prescribes for a
    change that breaks against the pinned sibling: it ships together with the sibling fix and the
    pin bump, or it does not ship. ⛔ .objectui-sha is held by open PR sdui-parser: port inert-quick-add and member-type-mismatch from objectui, re-record the lockstep and bump the pin #18723, so this seat did
    not touch it; and note a pin bump alone would not help — the pinned SOURCE lacks the entry
    too, measured above.
  2. spec side. Drop the object-tree row from ComponentPropsMap, so the spec stops
    declaring a block the public tier does not carry. That reverses one arm of the decision landed in
    feat(spec): object-map, object-gantt and object-tree get ComponentPropsMap rows, derived from the renderers' read points (objectui#8348 Q1-C) #18403, and is not a seat's call.

⛔ Deliberately not offered as a third route: writing the missing: true baseline row. The
#18305 round declined to write one for a stated reason, and this PR makes that refusal
mechanical rather than a matter of remembering.

Acceptance notes

  • #17735 is a different cell on the same file (the tracked manifest vs the browser dump
    disagreeing on the fields the parity gate consults) and is out of scope here — it is not
    addressed by this PR and remains open. The relevant half was measured only incidentally: both
    producers agree about object-tree, because both read getPublicConfigs().
  • scripts/sdui-manifest.record.json carries a generatedAt that is now older than the last
    run which reproduced the artefact byte-for-byte. Noted, not filed: nothing reads that field,
    and re-recording a date under an unchanged sha256 is churn on a generated provenance record.
  • No changeset. Measured rather than assumed: @objectstack/spec's files[] is
    dist, json-schema, liveness, prompts, llms.txt, README.md, src/**/*.zod.ts, CHANGELOG.md, api-surface, api-surface-declarations, spec-changes.json; packages/spec/scripts/** is in
    none of them. Grepping every one of those paths after a build for missingRegression,
    MISSING_IS_NEVER_RATCHETED_AWAY, fullManifestFor, evaluatedBlockTypes and stillMissing
    returns 0 files each, with positive controls ComponentPropsMap → 38 files and
    PageComponentSchema → 50 files on the same corpus. This diff publishes nothing, so it takes
    skip-changeset.

Generated by Claude Code

… pre-accepted

`check:react-declaration-parity` could not fail on its loudest signal. The
baseline ratchet suppressed `missing` behind `state.missing && base &&
!base.missing`, so a block with NO baseline row could not regress: the gate
printed the "NO component in the manifest" line and exited 0. The asymmetry was
unintentional — the `registryOnly` axis is already ratcheted against an absent
baseline row (an empty accepted set), only `missing` carried the extra conjunct.

`missing` is now judged every run, baseline or not, and `--update` refuses to
write a baseline while any block is missing — so the reflex that clears a fresh
red cannot mint the `missing: true` acceptance either. The failure text names the
upstream cure (expose the block on the registry public tier and regenerate, or
drop the spec row) and states that a baseline row is never it.

Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
Co-authored-by: Claude <noreply@anthropic.com>
…on the hole

The exit-code fixtures fed the gate a ONE-COMPONENT manifest, so twelve of the
thirteen evaluated blocks were absent from every one of them. They passed only
because an absent block with no baseline row could not regress — the fixtures
were resting on the very hole this branch closes. They now declare every
evaluated block, with the roster read off the gate's own report rather than
restated, and `omit` spells out the absence when an absence IS the subject.

New pins: an absent block with no baseline row reds and names itself; a
`missing: true` baseline row does not silence it; `--update` refuses to write
one; and two controls — the same run with the block present exits 0, and
`--update` still writes when nothing is missing.

Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
Co-authored-by: Claude <noreply@anthropic.com>
@os-bill os-bill added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 20, 2026 — with Claude
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see

Coarse fallback — 0 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 1739f71879f27d10c07fbde77fcf0728ed9abb81 → packageMentionDocs.

os-bill commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: 1/1 CONTRACT_REVIEW_TIER
Reviewed-by: session_01JbZnqu8bt6YqfJsr9vaFb3 (isolated reviewer subagent; no direction taken from the dispatching seat)
Reviewed at 2026-09-20T09:02Z · PR #19270 (draft) for card #18407 · base origin/main 1739f71879; control tree 7d0f911da9 = the merge-base, and the two commits between them touch only field.zod.ts, a repeater test, a changeset and a docs page — none of the gate, the manifest, the baseline or packages/spec/package.json.

49f7e0d23ea931a925a5ec0f672bf205ac639190

Evidence: the card body and its triage ruling, the PR (diff, body, check-runs, one job log), origin/main, objectui at the pinned sha and at its newest main, and the published @object-ui/* 17.6.0 tarballs. Seat comments were read; nothing was adopted from them — every number below is re-taken.

① Serves the card, and only that — the half that can be delivered here.

  • The diff is exactly two files, packages/spec/scripts/check-react-blocks-declaration-parity.ts and its .test.ts; git diff --name-only 7d0f911da9..49f7e0d23e lists nothing else — no manifest, no provenance record, no .objectui-sha, no baseline, no changeset.
  • The card's zero, re-taken on origin/main: sdui.manifest.json has 57 components; names containing "tree" → []; LIT CONTROL on the same parse, names starting object- → object-grid, object-form, object-master-detail-form, object-kanban, object-calendar, object-gantt, object-timeline, object-map, object-metric, object-chart, object-pivot (11). The gate on origin/main under CI's exact invocation (--baseline react-declaration-parity.baseline.json --strict) prints ✗ object-tree: NO component in the manifest and then ✓ no new DECLARATION divergence → exit 0; plain --strict on the same tree → exit 1. The card's sharp half ("cannot fail") reproduces.
  • Regeneration re-measured rather than taken: built packages/sdui-parser/dist, installed the 17 published @object-ui/* 17.6.0 packages from npm (the version packages/core/package.json declares at objectui 53ded82b, read through OBJECTUI_ROOT), ran node scripts/gen-sdui-manifest-node.mjs --modules-root … on the control tree: ✓ wrote sdui.manifest.json (57 components, 66910 bytes, sha256 49211fee7792…); git hash-object before = after = origin/main = 78f870e42fe9; git status shows only scripts/sdui-manifest.record.json (its date-only field). In the regenerated file, names containing "tree" → [], LIT CONTROL 11 object- names. Byte-identical no-op, confirmed.
  • Why it is absent, re-measured with my own probe on that same install: getAllConfigs() 494 configs — tree names ui:tree-view, plugin-tree:object-tree, view:tree (LIT CONTROL, grid names: 6); getPublicConfigs() 57 — tree names [] (LIT CONTROL: object-grid, grid); exported PUBLIC_BLOCKS 57 — tree [] (LIT CONTROL: object-grid, grid); getConfig('object-tree') → registered, tier=undefined, namespace=plugin-tree, inputs objectName, tree. At objectui source 53ded82b (shallow clone, commit present): packages/plugin-tree/src/index.tsx registers 'object-tree' at line 35 (namespace plugin-tree) and 'tree' at line 42 (namespace view), neither with a tier; packages/core/src/registry/public-blocks.ts has 57 entries, a tree grep exits 1, LIT CONTROL 'object-grid' line 28 and 'grid' line 100; Registry.ts public-tier paths at lines 821 (PUBLIC_BLOCKS), 824 and 828 (tier === 'public' on the config and on the entry meta) — none can fire. Also checked what the PR did not: objectui's newest main (078f2e4f) still has no tree entry in PUBLIC_BLOCKS, still registers without a tier, and @object-ui/core is still 17.6.0 — a pin bump to today's head would not produce the entry either.
  • Scope verdict: the triage ruling names two deliverables — an object-tree entry in the manifest, and "spec row with no manifest row" going red on its own. The first is not producible from this repository: both producers faithfully serialise a public tier that does not carry the block, so the cure is objectui's curation or the spec row, a decision. The diff delivers the second, touches nothing else, and is declared Part of, not a close. Not a substitution.

② Clause-②: no is a true reading of this diff.

  • @objectstack/spec files[] at the head (byte-identical to main): dist, json-schema, liveness, prompts, llms.txt, README.md, src/**/*.zod.ts, CHANGELOG.md, api-surface, api-surface-declarations, spec-changes.json — scripts/ in none. Positive control on the same manifest: npm pack --dry-run at the head packs 293 entries, 0 under scripts/ and 0 matching declaration-parity, while src/ui/component.zod.ts, src/ui/page.zod.ts and README.md are packed.
  • exports (19 keys) names no scripts/ path, and nothing under packages/spec/src imports either changed file (the only src → scripts imports are three test files pulling scripts/lib/export-origins-testkit, untouched). No published payload gains a key, no closed set moves, no export or registry entry moves. The one accept-set that narrows is the CI gate's own --update, which is tooling, not a published contract. skip-changeset is the label AGENTS.md assigns to exactly this.

③ Sound, and nothing is left vacuous — with one body error and two costs to name.

  • Real exit codes, this act:
run head 49f7e0d2 control 7d0f911d
gate --baseline react-declaration-parity.baseline.json --strict (CI's line) 1 — object-tree: … NO baseline row 0 — same ✗ line, then ✓ no new
gate plain --strict 1 1
the gate's own suite (vitest run --project local, that file) 0 — 36 passed 30 tests in the file
pnpm --filter @objectstack/spec typecheck (incl. check:scripts-typecheck, check:test-typecheck) 0 —
  • (a) Fixtures. Verified by swapping one file at a time between the two trees in their own worktrees (blobs checked: gate 6f6ccc1a ↔ 8bda7e2d, test f53bfe53 ↔ 20bbd1ae). Main's old test file against the head gate: 6 failed / 24 passed — exits 0 when the manifest matches the accepted baseline, carries the caveat on a clean baseline ratchet too, a block not yet in the baseline is additive, accepts a node-level key … (dataSource), accepts the other node-level key … (className), applies to the react-block half too — each a one-component-manifest fixture expecting a clean ratchet; they passed on main only because twelve absent, unbaselined blocks could not regress. Head's test file against the main gate: 4 failed / 32 passed — exactly the four #18407 pins (absent block with no baseline row is a regression; the failure text sends the reader upstream; a missing: true row does not buy silence; --update refuses), with both CONTROL cases green. The repaired fixtures discriminate in both directions. evaluatedBlockTypes() carries its own lit control (it throws unless object-form, list-view and object-grid are read off the report); the head run evaluates 13 blocks, the baseline has 9 rows, the four without a row are object-gantt, object-map, object-timeline and object-tree, and only object-tree is absent from the manifest — blast radius one block, as the PR says.
  • (b) Landing. ⚠️ The PR body says the red lands in Lint & Repo Gates. It does not. In lint.yml at origin/main the parity step (line 6344) is inside job typecheck-consumers (Type Check · consumer gates), a lane of the aggregate typecheck (TypeScript Type Check, needs: all four lanes); Lint & Repo Gates (job lint) carries check-sdui-manifest.mjs, not this gate. The PR's own CI on this head agrees: Type Check · consumer gates → failure, its log ending in the object-tree … NO baseline row block and Exit status 1; TypeScript Type Check → failure. The ruleset on main (REST, HTTP 200) lists seven required contexts, and TypeScript Type Check and Lint & Repo Gates are both among them, so the consequence the body draws holds — merged as-is, a required context is red on every later PR and on main's own runs, and the queue lands nothing until objectui exposes the block or the spec row is dropped — but the context it names is wrong. ⛔ Not patched by this review; correct it on the next body edit the seat must make anyway (readying the draft). The hold is the draft flag alone; that decision is outside this review and is not the verdict.
  • (c) Circumvention, probed on the real manifest at the head: a hand-written baseline row object-tree: {missing: false} → exit 1 (block vanished from the manifest); a hand-written {missing: true} row → exit 1 (does NOT accept this); the reflex, --update against a copy of the committed baseline → exit 1, refusing to write a baseline while 1 block(s) have NO component … - object-tree, and the copy's sha256 is unchanged before and after. Control, main's gate, same command → exit 0, wrote declaration-parity baseline … (13 blocks), and the written file carries object-tree: {registryOnly: [], missing: true} — the acceptance the head refuses to mint. What remains is outside the gate: editing the gate or the workflow step, hand-editing sdui.manifest.json (caught by check-sdui-manifest.mjs sha256-vs-record in the lint job) or removing the ComponentPropsMap row — each visible in a diff, none a baseline-side silence.
  • Two costs, named, not blocking: while any block is missing, --update refuses to write anything, so a legitimate new registry-only accept on another block needs a hand edit of the baseline until object-tree is resolved (the failure text says so); and the committed baseline's _comment still describes the old semantics ("flags only NEW registry-only inputs or newly-missing blocks") and cannot be refreshed through --update for the same reason.

Measured first-hand in this act: the card's zero and its lit controls on origin/main; the manifest regeneration on the control tree from a fresh npm install of the published 17.6.0 set and the hash comparison; the registry probe on that install (all / public / PUBLIC_BLOCKS / getConfig) with grid controls; the objectui readings at the pinned sha and at newest main; files[], exports and npm pack --dry-run; both gate modes on both trees; the head suite and typecheck; both one-file-swap ablations; the three baseline and --update probes with a main control; the lint.yml job graph, the ruleset's required contexts, the PR's check-run conclusions and one job log.
Taken from someone else's reading and NOT verified: the PR's two surgical ablations through scripts/ablation-replace.mjs (whole-file swaps were run instead); its full pnpm --filter @objectstack/spec test (14649 tests), whole-repo eslint, pnpm build and the "53 derived gate families" — NOT MEASURED here; the browser route (pnpm sdui:manifest, Playwright plus a vendored objectui build) — NOT MEASURED, so #17735's cell is untouched by this review; the Registry.ts public-tier paths were read as source and exercised only through the published dist.

PASS


Generated by Claude Code

os-bill commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

Provenance correction to contract-review record 5748831865 (same reviewer, 2026-09-20T09:05Z) — the control TREE label, not any reading.

The pinned control worktree was re-pinned under this act — its reflog reads checkout: moving from 7d0f911da9 to origin/main at 2026-09-20T08:54:20Z, landing on fb33767cf1 — and the control batch started at 2026-09-20T08:54:31Z. So the two control gate runs (--baseline … --strict → exit 0, plain --strict → exit 1), the --update control (exit 0, writes object-tree: {registryOnly: [], missing: true}) and the manifest regeneration were taken on fb33767cf1, not on 7d0f911da9 as the record's ③ table column and preamble say. The two one-file-swap ablations were pinned with git restore --source=7d0f911da9 and are labelled correctly.

Why the readings stand for both trees: every input of the gate is byte-identical at 7d0f911da9 and fb33767cf1 — check-react-blocks-declaration-parity.ts 6f6ccc1adf72, its .test.ts f53bfe5340e3, react-declaration-parity.baseline.json 0c4691269c67, sdui.manifest.json 78f870e42fe9, packages/spec/package.json a8ddb4ec2226, lint.yml 1818c40c42f7; git diff 7d0f911da9..fb33767cf1 over the manifest record, the pin, manifest-prescription.ts, component.zod.ts, react-blocks.ts, page.zod.ts and both manifest scripts is empty (what did move: field.zod.ts and four test files). Re-run explicitly at fb33767cf1 after the move was noticed: baseline mode exit 0 (✗ object-tree … NO component, Summary: 126 spec-only divergences, 1 blocks missing, ✓ no new DECLARATION divergence), plain --strict exit 1, --update control exit 0 with the missing: true row — identical to the record. Verdict unchanged: PASS.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Closed unmerged — the maintainer re-affirmed ruling 丙: the declaration-parity ratchet retires

domain:spec execution seat 1 (session_013RDBh5DqXd2xnLwvHLgLFr), 2026-09-23T08:51Z. On the maintainer's word, verbatim 「丙」, in this seat's session, answering 「丙(关掉 #19270,门禁退役)还是 A(保留,等指针升级后合入)」. Full record with provenance: #17735 (the ruling's home).

This PR taught check:react-declaration-parity to fail on a spec row with no manifest component. Its measurements were correct, and its one live case, object-tree, was fixed upstream (objectui#10064 via objectui PR #10192). But ruling 丙 retires the gate itself (the script, its test, its baseline, the lint.yml step and the cut-rc step) under #17735. Landing a fix to a gate slated for deletion buys nothing. #18407 keeps its own remainder: the manifest regenerated at a pin past the objectui fix, which the in-flight pin bump #19503 carries.

⛔ Closed by the seat on the maintainer's instruction, ⛔ not merged, ⛔ nothing reverted. The branch stays for its record.


Generated by Claude Code

akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…he declaration-parity ratchet and the browser dump (objectstack-ai#19921)

Fixes objectstack-ai#17735
Clause-②: no

Executes maintainer ruling **丙** on objectstack-ai#17735 (ruling record `5724940904`,
batch objectstack-ai#151 item 2; verification record `5717306177`; maintainer
re-affirmation `5791879338` over ruling objectstack-ai#197 A's PR objectstack-ai#19270 path). The
fence notes `5756239563` and `5786373289` are cleared: PR objectstack-ai#19270 was
closed unmerged by the re-affirmation, and the census in claim
`5800589054` found every path clear. PR objectstack-ai#18608 (squash `681ebe4246`)
already discharged the earlier ruling's item 4 (the `check-generated.ts`
`why` string). This PR does steps 1–3 of 丙 and leaves step 4's list in
place. The witness ledger is the one exception, shrunk by three rows
(see Deviations).

⚠️ **Governed surfaces:** `AGENTS.md` and `docs/adr/**` (Tier H), plus
`.claude/hooks/**` (Tier S, comments only). This PR stays **draft** and
lands by the maintainer's hand.

## Step 3 first: the byte comparison decided the shape of this PR

Both producers were run once over one built tree,
`.cache/objectui-62597c588072` (objectui at pin
`62597c588072636e9c30ea35b3d89b1e46fd765d`, built by `pnpm
objectui:build`, `VERDICT command-exit 0`):

```text
$ node scripts/gen-sdui-manifest-node.mjs            # the new built-tree producer (this PR)
✓ wrote sdui.manifest.json (59 components, 85726 bytes, sha256 22c83c9ec701…)
$ PLAYWRIGHT_BROWSERS_PATH=SCRATCH/pw bash scripts/gen-sdui-manifest.sh   # the browser dump, as on main
✓ wrote 59 public blocks → packages/console/dist/sdui.manifest.json
$ cmp packages/console/dist/sdui.manifest.json sdui.manifest.json; echo CMP_EXIT=$?
CMP_EXIT=0
$ sha256sum packages/console/dist/sdui.manifest.json sdui.manifest.json
22c83c9ec70163559c2bd8f7b1613174d8660ad9949d15ed483a4189b7857c9a  packages/console/dist/sdui.manifest.json
22c83c9ec70163559c2bd8f7b1613174d8660ad9949d15ed483a4189b7857c9a  sdui.manifest.json
```

The outputs are **identical**, so the ruling's copy branch applies.
`build-console.sh` now copies the tracked `sdui.manifest.json` into
`packages/console/dist/`. `scripts/gen-sdui-manifest.sh`, `pnpm
sdui:manifest` and cut-rc's Playwright install step are retired. The
dump ran against the container's chromium 1194, exposed under the
revision name that playwright 1.62.1 asks for through a scratch
`PLAYWRIGHT_BROWSERS_PATH`. Nothing was installed. The dump script's
trailing ratchet did not run: it is guarded on the baseline file, which
this branch had already deleted, and that guard sits after the dump
wrote its file.

## Step 1: M1 is the standing producer

- **`scripts/gen-sdui-manifest-node.mjs`** takes no arguments.
- It derives the modules root `.cache/objectui-SHA12/apps/console` from
`.objectui-sha`.
- It refuses to run, with `pnpm objectui:build` as the remedy, when the
tree is missing, when the tree's HEAD is not the pin, or when the tree
is not built (`apps/console/node_modules/@object-ui/core/dist/index.js`
absent). It also refuses any argument, so the retired
`--objectui-version` and `--modules-root` fail loudly instead of being
ignored.
  - The temp-npm-install default is deleted.
- The runner file is written to its own `mkdtemp` dir and removed
afterwards. A resolve hook re-anchors bare specifiers to the modules
root, so module resolution is unchanged and nothing lands in the build
tree. Measured: 0 runner files in `apps/console`, 0 leftover temp dirs.
  - Output is deterministic: two runs, `cmp` identical.
- **`scripts/sdui-manifest.record.json`** now records `source:
built-tree`, `modulesRoot` and `objectuiWorkspaceVersion` (read from the
built `@object-ui/core/package.json`), and its `//` block is rewritten.
- **`scripts/check-sdui-manifest.mjs`** is re-keyed.
- The presence check now requires `source` (it must equal `built-tree`),
`modulesRoot` and `objectuiWorkspaceVersion`.
- Check 4 compares `objectuiWorkspaceVersion`, and the header prose and
every remedy line name `pnpm objectui:build && node
scripts/gen-sdui-manifest-node.mjs`.
- The self-test seed writes the new keys. One new row, `a record from
the retired npm-install route is RED`, raises the floor to 12.
- **npm-route prose rewritten** in: the generator header (the
dead-citation fact of objectstack-ai#18627 is restated without a number), the record's
`//` block, `build-console.sh`'s trailing reminder, the `AGENTS.md`
pin-move paragraph (located by content), `docs/releases-maintenance.md`
(the "After the pin moves" section, the pin-policy step 4 and the rc-cut
step 2), `scripts/bump-objectui.sh` (header, NEXT STEP text and the
not-on-main warning) and the `lint.yml` comment on the
`check-sdui-manifest` step.
- **Artefact regenerated at the pin:** 57 → **59** components (`box` and
`object-tree` added), 66,910 → **85,726** bytes, sha256 `49211fee7792…`
→ `22c83c9ec701…`.
- 19 components differ in their input-name set: 38 names appear only in
the new artefact and 3 only in the old.
  - `dataSource` is now on 15 components (it was on 0).
  - `actionType` replaces `type` on `action:button` and `action:icon`.
  - `flex.isContainer` is now `true`.
  - `dashboard.refreshInterval` became `refreshIntervalSeconds`.
- The rest are `object-kanban`/`object-calendar` inputs, plus
`text.align` and `text.variant`.

## Step 2: the parity ratchet retires

Removed:
`packages/spec/scripts/check-react-blocks-declaration-parity.ts`, its
test, `packages/spec/react-declaration-parity.baseline.json`, the gate's
only helper `packages/spec/scripts/manifest-prescription.ts` (the two
deleted files were its only importers), the
`check:react-declaration-parity` script, the `lint.yml` step, and the
cut-rc steps `Install a Playwright browser for the manifest dump` and
`Declaration-parity ratchet at the committed pin (ADR-0082 D4)`.

ADR-0082 decision 4 gains exactly one status line: 「retired 2026-09-18
by maintainer ruling on objectstack-ai#17735; the artefact stays for the CLI witness
and the freshness gate」. That is an amendment, not a rewrite.

Knock-on edits:

- `check:generated`'s `EXTERNAL_INPUT_REQUIRED` bucket is now empty. The
bucket and its reconciliation are kept, and the ledger test asserts `0
needing an external input`.
- `published-projection-choke-point.test.ts` loses the deleted file's
allowance row.
- `gen-sdui-manifest.sh` goes with its three tests
(`gen-sdui-manifest-cleanup`, `-collision` and `-write-target`), which
are also removed from `vitest.repo-tests.json`.
- `pnpm check:cross-package-test-inputs` reported two dispositions after
the deletion, and both are applied. It reported
`scripts/gen-sdui-manifest.sh` as an unheld glob for
`create-objectstack`, so it comes out of the declaration and
`turbo.json`. It reported that `check-generated-ledger.test.ts` no
longer reads outside its package, so it comes out of the spec repo-tests
list.

## Folded cards (ruling step 5)

objectstack-ai#18627 (dead citation in the generator header), objectstack-ai#18633
(`releases-maintenance.md` clause and the instruction that caused the
version skew) and objectstack-ai#18632 (the parity header's 「dumped from」 sentence)
are folded here. All three are already closed as not planned, each with
a pointer to this card, so there is nothing further to close. objectstack-ai#18407
stays open, and it is not addressed here. **objectstack-ai#14490 re-measure reading:**
the regenerated artefact has `actionType` on `action:button` and
`action:icon`, and neither block has an input named `type`. The artefact
also contains `object-tree` now.

## Changesets

- `skip-changeset` is the ruling's route for the scripts and the ADR
line.
- Because step 3's copy landed, this PR adds
`.changeset/sdui-manifest-one-producer.md` (`@objectstack/console:
patch`).
- Which labels to apply is the seat's call. I wrote none.

⚠️ **Premise correction for the changeset.** Ruling step 6 describes the
dist as gaining 「a file it already shipped from the browser dump」.
Measured, that is false. The published `@objectstack/console` 17.0.0,
17.3.0 and 17.4.0 tarballs contain **0** `sdui.manifest.json` (control:
`package/dist/index.html` 1 each). `pnpm run release` re-runs
`build-console.sh`, which recreates `dist/` from scratch, so the RC
cut's copy never reached a tarball. The changeset states that. It also
states that the console's `exports` map (`./package.json` only) keeps
the file unreachable through `exports` for now.

## Deviations, and why

1. **`packages/lint/src/sdui-jsx-baseline.json` is shrunk by 3 rows**,
although the dispatch listed it as untouched. With the regenerated
manifest, `flex` declares `isContainer`, so the 32 `jsx-not-a-container`
warnings no longer fire. The witness test (unedited) fails with 「STALE
ledger rows … delete these rows … in this same PR」. The rows were
deleted, none was added, and no count was raised, which is the ledger's
own shrink-only rule. The ruling's step 1 regeneration forces this.
2. **Comment-only edits outside the claimed file surface**, made because
the dispatch asked for no dangling reference:
- `.claude/hooks/guard-process-kill.sh` and its selftest said `pgrep -s`
teardown is "live in two tracked scripts", one of which this PR deletes.
- `scripts/pm/os-verify-lock.sh`, `scripts/publish-smoke.sh`,
`scripts/check-sdui-lockstep.mjs`,
`scripts/check-pnpm-filter-targets.mjs` and
`scripts/pnpm-filter-targets.mjs` (the `FOREIGN_SCOPES` reason string).
- `packages/spec/src/ui/react-blocks.ts` (a line comment naming the
deleted baseline).
- `docs/audits/2026-06-react-blocks-conformance.md` (a retirement note
above its run instructions).
3. `scripts/objectui-changeset-digest.mjs`'s self-test asserted that the
bump prints `pnpm sdui:manifest`. It now asserts the bump prints `node
scripts/gen-sdui-manifest-node.mjs`, `pnpm objectui:build` and `NEXT
STEP`.

## References kept on purpose (history, not pointers)

- `packages/spec/CHANGELOG.md` (release-owned).
- ADR-0082's body and addenda (amendment only).
- `docs/protocol-upgrade-guide.md`, and the `packages/spec/src`
narratives in `component.zod.ts` (×3), `conversions/registry.ts`,
`migrations/registry.ts` and
`retired-keys/17.ui__RecordDetailsProps__layout.ts`. These explain why a
past gate stayed green.
- `docs/audits/...` (dated record).
- Past-tense comments in `publish-smoke.sh`,
`publish-smoke-port-collision.test.ts`,
`scaffold-e2e-boot-probe.test.ts` and `lib/docs-import-surface.ts`.
- Fixture strings in `scripts/check-agent-test-spelling.mjs` and
`scripts/pm/ci-failure.mjs`.

## Acceptance notes

- `component.zod.ts` still has one forward-looking sentence: the
`check:react-declaration-parity` gate 「carries the spec↔objectui parity
burden going forward」. It is TSDoc on published spec source, so it was
left for the spec seat.
- The witness test's title still says 「57-component public tier」. The
test is unedited per the dispatch, and its assertion reads `length > 0`.

## Local verification

All results below were taken on head `b7d0b3693a`, the final commit,
after `git rev-parse --short HEAD`.

- **Gate union:**
- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` derived 155 families.
- All 155 were run, each with its exit code recorded before any pipe,
and **all 155 exited 0**. That includes `check:pm-dispatch-gates` (1905
cases, run detached because it exceeds the foreground cap), plus
`check:dual-build-cjs-loads`, `check:lean-entry-closure` and
`check:type-check-debt` after a full `turbo run build
--filter='./packages/*' --filter='./packages/*/*'` (72/72 tasks).
- Reconciliation with `--ran` exited 0: 「155 derived, 155 run, 0
NOT-MEASURED, 0 UNRUN」.
- NOT MEASURED, as the derivation prints them: the 10 workflow-valued
families, the six CI jobs scheduled by these paths (Test Core, Temporal
Conformance, Dogfood Regression Gate, Dogfood Verify CLI, Build Core,
Console Pin Gate), and the workspace, examples and downstream type-check
programs. These belong to CI.
- **Lint:** eslint narrowed to the 12 changed lintable files,
`--no-inline-config --format json`.
  - The JSON reports 12 files, 0 errors and 0 warnings.
- This narrowing is safe because `eslint.config.mjs` enables no
type-aware linting (no `parserOptions.project`), and nothing imports a
deleted file. So no untouched file's verdict can move. The repo-wide
`pnpm lint` belongs to CI.
- Tests:
  - `@objectstack/lint` full `vitest run`: 108 files, 4121 tests passed.
- spec targeted run (`check-generated-ledger`,
`published-projection-choke-point`, `publish-smoke-port-collision`,
`publish-smoke-boot-failure`): 4 files, 38 tests passed.
- `@objectstack/vitest-filter-preflight` `config-wiring-sweep.test.ts`:
65 passed.
  - `@objectstack/spec typecheck` (src, scripts and test layer): exit 0.
- Self-tests: `bump-objectui.selftest.sh` 20/20,
`guard-process-kill.selftest.sh` 69/69, `os-verify-lock.sh --self-test`
pass, `check-sdui-manifest.mjs --self-test` 12 cases.
- Gate at the pin, with the objectui oracle: `node
scripts/check-sdui-manifest.mjs --require-objectui` exit 0, reporting
「@object-ui 17.6.0 is the version objectui 62597c588072… declares」.
- Ablation (one-shot, restored byte-exact by
`scripts/ablation-replace.mjs`): with the re-key reverted to
`['objectuiSha', 'sha256', 'components']`, the self-test reds on exactly
the new row (「a record from the retired npm-install route is RED —
expected RED, got GREEN」, exit 1). The restore leaves the blob equal to
HEAD `b6484613ce99` and `git diff HEAD` empty.
- Generator refusals, probed on a scratch copy (each exit 1, nothing
written): an argument, no tree, a tree at the wrong HEAD, and a tree at
the pin but not built.
- `build-console.sh`'s new block was exercised on fixtures: fresh
(copied, ✓ line), stale record pin (copied, ⚠ with the regeneration
step) and missing artefact (exit 1). A full `pnpm objectui:build` on
this branch's first commit also passed; that run's ending came before
the copy block existed.

## 维护者速读(草稿)

**改了什么** —— `sdui.manifest.json` 只剩一个生产者:`gen-sdui-manifest-node.mjs`
直接读 pin 上已构建好的 objectui 树,不再从 npm 装旧包。声明对齐门禁(ADR-0082 D4)和浏览器 dump
一起退役。console 构建把这份受跟踪的 manifest 原样拷进 dist。

**为什么改** —— 裁决 丙。实测两个生产者在同一棵已构建树上逐字节相同(`cmp` 退出码
0),所以浏览器那一路没有存在的必要。manifest 重新生成后,缺的 `dataSource`、`actionType` 等 38
个输入全部回来了。

**风险与代价(含回滚)** —— 以后升 pin 要先 `pnpm objectui:build`(约 10 分钟),再跑生成器。每个 PR
的门禁照旧离线,不变。console 包会第一次真的带上这个文件:此前发布的 17.0.0、17.3.0、17.4.0 都没有。但包的
`exports` 只导出 `./package.json`,CLI 现在还读不到它,所以对用户没有行为变化。回滚就是 revert 这个
PR。

**席位意见** ——

**你要做的** —— 审阅后亲手合并(治理面:AGENTS.md、ADR)。

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

---------

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

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants