Skip to content

sdui-parser: port inert-quick-add and member-type-mismatch from objectui, re-record the lockstep and bump the pin - #18723

Closed
os-try-charles wants to merge 4 commits into
mainfrom
claude/issue-17645-sdui-lockstep-port
Closed

os-try-charles wants to merge 4 commits into
mainfrom
claude/issue-17645-sdui-lockstep-port

Conversation

@os-try-charles

Copy link
Copy Markdown
Collaborator

Fixes #17645

Clause-②: yes

Executes the director-seat ruling in card comment 5714974800 (decision batch #146 item 3), maintainer's verbatim 「146 同意」 — letter A: port inert-quick-add and member-type-mismatch into this repo's packages/sdui-parser, re-record the lockstep and bump the pin in the same PR.

Ported from

repo objectstack-ai/objectui (READ-ONLY clone in the dispatch container)
revision cfcc17d9dd04ebb80c092cacb4f4034425f51b29, 2026-09-14T11:40:39Z
path packages/sdui-parser/src/{kanban-quick-add.ts,validate.ts,types.ts,index.ts,codegen.ts}

⚠️ That clone is 3 days older than the card's own reading (the card measured objectui on 2026-09-16). What that means for the lockstep record is stated under "What the record cannot see" below — it is a real bound, not a formality.

What each code actually guards

A reviewer should be able to tell a real port from a string copied across an enum. Neither code is a new red gate: both are warnings, so compile().ok — the save gate's pass/fail — is unchanged and a page that saves today still saves.

inert-quick-add (objectui#8285)

quickAdd on the object-kanban tag reaches no control. The Quick Add button is gated on both halves of a pair — quickAdd and an onQuickAdd handler — and onQuickAdd takes a FUNCTION, which no page on this tier can write (this tier parses, it never executes; a handler attribute draws forbidden-attr) and which the board substitutes none of its own for.

It replaces the diagnostic the prop walk used to emit for that key rather than joining it. The old reading was unknown-prop, "has no prop quickAdd" — false against the published contract, because ComponentPropsMap for that tag does publish quickAdd. An author who checked the spec found the warning contradicted, concluded the manifest was behind the spec, and kept a key that will never do anything; it also could not be told apart from a typo. Two diagnostics for one mistake is the shape checkMemberTypes already refuses, so this one continues the walk.

It is asked ahead of the declaration lookup on purpose: the claim is about the render path, which a declaration cannot change, so declaring quickAdd must not silently disarm it. Three things it deliberately does NOT fire on — a falsy value (the renderer's own gate is quickAdd && onQuickAdd, so the author got what they wrote), a braced value this tier never evaluates (opaque here), and any tag outside the one surviving host registration. Each has a control row in the tests.

member-type-mismatch (objectui#8067)

The coarse type check one level down, over the member kind an input declares. type: 'array' said a value was a list and stopped there, so a member that drifted from the contract was invisible to every layer that reads a declaration.

This is the half that makes it a port and not a string: the code needs a key that did not exist in this copy at all. The change brings ManifestInput.of and all three of its readers:

  • the validator — any declared arm accepting a member clears it; a member no arm accepts is reported, as ONE diagnostic naming every offending position, at error severity when an enum arm is present and warning otherwise;
  • the serializer — manifestFromConfigs canonicalizes of exactly as it canonicalizes type, and publishes NO of for an input that declares none (routing undefined through canonicalizeInputType, whose no-arms fallback is 'string', would make every array in every manifest claim string members it was never told it had);
  • the codegen — a declared member kind narrows the emitted element type, so the .d.ts an author type-checks against and the manifest gate agree.

The member walk runs only once the CONTAINER kind was accepted, so a wrong container draws one diagnostic and not two.

Counts, each with a firing control

Base for every diff reading: 51297e9ee17db23f50d38c1431f5a4dcc47632ff.

Diagnostic codes — 26 = 26, sets identical element-for-element. Measured with the gate's own AST extractor (fingerprintOf), both trees, before and after:

before after
this repo 24 26
objectui @ cfcc17d 26 26

The delta was exactly inert-quick-add and member-type-mismatch — matching the card's 26/24 reading, so acceptance item 4 is met and nothing was absorbed.

The grammar region did not move across the pin hop. This repo's parse.ts region and objectui's at cfcc17d are both 214 lines, blob 0131f27cf86dbabec09b4ae55ae131403667e7e9 — byte-identical, and identical to what the old record held at 53ded82bf. So the pin bump ports no grammar change.

of declarations in the tracked manifest: 0 of 339 inputs. FIRING CONTROL on the same file: the term "inputs" occurs 57 times in sdui.manifest.json, and git diff BASE HEAD -- sdui.manifest.json | grep -c '"inputs"' reads 0 — the whole-file diff is 0 bytes. So the zero is a reading about the tree, not an artifact of this diff. The discriminating contrast: the same grep for of?: over git diff BASE HEAD -- packages/sdui-parser/src reads 2, so the term is one this diff really moves where it moved it.

⚠️ Read that 0-of-339 honestly: member-type-mismatch has no firing input in today's shipped manifest, because that manifest is generated from the PUBLISHED @object-ui/* 17.6.0 packages, which predate the of declarations. It is NOT inert — objectui's SOURCE at cfcc17d declares of in 38 places (e.g. the tabs and panels items inputs in containers.tsx), so those flow in at the next publish+pin, and the reader is pinned here through the real serializer rather than a hand-built fixture. Without this port, this repo's save gate would silently accept member drift that objectui's renderer reports.

objectui was never written to. git status --porcelain in that clone: 0 files. HEAD still cfcc17d9dd04ebb80c092cacb4f4034425f51b29.

Ablation ledger

Three ablations, each through scripts/ablation-replace.mjs — anchor declared and required to HIT, before/after anchor counts, git hash-object blob ids either side, restore proven by blob == HEAD AND an empty git diff HEAD, with an EXIT/INT/TERM trap. The tests resolve the subject through src (a relative intra-package import, no dist hop), which is why these run without a rebuild — measured: the suite passed before dist existed in this worktree at all.

# mutation anchor blob result
1 disarm checkKanbanQuickAdd's key guard 1 → 0 0d5131868ace → 99d89132858e 4 failed / 8 passed
2 make the validator's member check read no declared arms 1 → 0 156a264073f7 → 5cb7912d7f54 7 failed / 14 passed
3 make the codegen narrow no member type 1 → 0 76a7d654f1fd → ba55afbc8c19 3 failed / 18 passed

In each case the failures were exactly the SUBJECT rows and every control row stayed green — which is the split the control rows exist to produce. A first attempt at ablation 1 was REFUSED by the tool because the replacement still contained the anchor (count 1 → 1, "not the declared drop"); it is recorded here because the refusal is the tool working, and the run that produced the reading above is the corrected one.

The pin bump, and its second and third halves

.objectui-sha: 53ded82bf7a494f54e344e19099dbf00854b8694 → cfcc17d9dd04ebb80c092cacb4f4034425f51b29.

Second half — the manifest — is done and is a no-op in content. node scripts/gen-sdui-manifest-node.mjs --objectui-version 17.6.0 regenerates sdui.manifest.json byte-identical: sha256 49211fee7792cf51 before and after. Both pins ship @object-ui/* 17.6.0, so the published registry inputs did not move; only the record's objectuiSha and generatedAt follow the pin. check-sdui-manifest is green.

Third half — check:objectui-pin-citations — is RED and is NOT closed by this PR. See "Acceptance notes".

What the record cannot see

check-sdui-lockstep compares this tree against a VENDORED record of objectui's side. It can see this copy drifting from the recorded objectui state; it cannot see objectui moving AFTER the record was taken. This record was taken at cfcc17d (2026-09-14) — and the card's own reading of objectui is from 2026-09-16. Anything that landed in objectui's packages/sdui-parser in those two days is outside this record and outside this PR's evidence. The gate says so itself on every green run, and only a later pin bump brings the question back. The container's objectui clone is shallow with exactly one commit, so this cannot be narrowed here.

Gates

scripts/pm/dispatch-gates.mjs derived 67 families for this diff; re-derived after git fetch and the list was IDENTICAL, so the tool's STALE TREE warning does not move the answer. Reconciled with --ran recording every exit code: 67 derived, 64 run, 3 NOT-MEASURED, 0 UNRUN (exit 0).

  • check:sdui-lockstep — green, both the gate and --self-test: byte-identical over 214 grammar lines and agreeing on all 26 codes. ⛔ The invariant was never relaxed to buy this: --update re-reads OBJECTUI's side only and never this tree's parser, so it cannot record "whatever we have now"; the green is the port being faithful.
  • check:sdui-manifest — green (present, intact, fresh at the new pin).
  • pnpm --filter @objectstack/sdui-parser test && … typecheck — green, 176 tests / 10 files.
  • check:nul-bytes — green; plus a direct control-byte sweep over the changed files.
  • check:dispatcher-error-vocabulary — was RED on this port and is now green; see "The one deliberate divergence".
  • 3 families are NOT MEASURED, not passes: check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt each exited 3 = PREREQUISITE NOT MET (they need a full pnpm build), which is neither a pass nor a failure.
  • check:query-options-erasure — green at 192s; an earlier 124 was this runner's own 150s timeout, not a finding.
  • check:objectui-pin-citations — RED, deliberately left open. See below.

The one deliberate divergence from objectui

objectui stamps code: INERT_QUICK_ADD. This copy stamps the inline literal code: 'inert-quick-add' and keeps the exported constant. This is not drift and not a shortcut — it is the same documented divergence unconsumed-widget-option already carries next door: check:dispatcher-error-vocabulary's objlitconst shape must reduce a constant at a code: position to a literal, and its literal grammar cannot admit a kebab-case value, so the constant form is an unresolvable-constant finding that cannot be declared away. The gate caught this port doing it, which is the gate working.

⛔ It does not weaken the lockstep: that gate resolves an identifier at a code position through the package's own module-level constants, so both spellings reduce to the same code and the two copies still agree on all 26. A test row pins the literal equal to the constant so the two cannot drift apart.

Changeset

OWED, and measured rather than assumed — .changeset/17645-sdui-parser-lockstep-port.md, minor. The criterion is whether anything PUBLISHED moves: @objectstack/sdui-parser ships files: ["dist", …], and after a build both codes are present in the shipped bytes, against a negative control that reads zero everywhere:

symbol index.js index.mjs index.d.ts
inert-quick-add 3 3 2
member-type-mismatch 1 1 0
checkKanbanQuickAdd 4 3 3
unconsumed-widget-option (positive control) 4 4 4
NEVER_SHIPPED_CONTROL_STRING (negative control) 0 0 0

That reading is also the answer to the Clause ② question the card asked to have re-judged if it measured otherwise: these codes DO reach a published surface, so the needs:contract-review declaration stands. The package's exported API grows by four symbols and one optional type key, which is why this is minor and not patch.

Acceptance notes

check:objectui-pin-citations is RED and this PR does not close it — this needs a decision.

The pin bump invalidates 26 packages/spec/** sources that cite .objectui-sha as "the pin this repo builds against" alongside hand-derived objectui file:line anchors. The gate refuses the cheap repair in terms: ⛔ "Do NOT fix this by replacing the sha… The sha is not the record — the objectui file:line ANCHORS beside it are, and they are only true of the tree they were counted in." Rewriting the sha alone would turn a loud, accurate "unverifiable" into a silent, confident lie.

Two reasons it is not done here, both measured:

  1. It is outside this card's binding file surface. The claim comment declares packages/sdui-parser/, scripts/. Re-measuring 26 spec sources of hand-written cross-repo provenance prose is not a bounded in-place fix.
  2. The re-measure is not performable in this container. Each record carries a "which anchors moved across the hop" narrative, which needs BOTH pin trees. The only objectui checkout here is shallow with exactly one commit: git cat-file -t cfcc17d9dd04… answers commit, while the old pin 53ded82bf7a4… answers "could not get object info" — a firing control on a present object, so the absence is real. Deepening it would mean git WRITES inside a READ-ONLY checkout belonging to someone else, which is exactly what check-sdui-lockstep's own header declines to do for the same reason.

So this is a genuine third half of "bump the pin" that letter A did not price. Landing it needs either a follow-up card scoped to packages/spec/** with a non-shallow objectui checkout, or a maintainer ruling. ⛔ Not absorbed silently, and #17645 is the only card this PR closes.

Other observations, noted and not filed — neither is a defect, and no PR or person is queued on either file, so there is no taker to hand them to:

  • packages/sdui-parser/src/provenance.ts exists in objectui and not here. It is deliberately out of scope: it stamps zero diagnostic codes, so the lockstep's code-set comparison is unaffected (26 = 26 with it absent), and the gate does not compare file lists.
  • objectui's index.ts also exports assertFullyLoaded and its codegen emits an eslint-disable banner; neither is part of either ruled code and neither is carried here.

Generated by Claude Code

… objectui

Both diagnostics existed only in objectui's copy of the parser, so the save
gate and the renderer disagreed about what a page means -- the dialect split
the lockstep invariant forbids.

- inert-quick-add: `quickAdd` on <object-kanban> reaches no control, because
  the Quick Add button is gated on a runtime slot no parsed page can write.
  Replaces the false `unknown-prop` ("has no prop quickAdd") the prop walk
  emitted, which contradicts the contract spec still publishes.
- member-type-mismatch: the coarse type check one level down, over the member
  kind `ManifestInput.of` declares. Brings the `of` key and its three readers
  (validator, serializer canonicalization, codegen element type) with it.

Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
Co-authored-by: Claude <noreply@anthropic.com>
… the new pin

Bumps .objectui-sha to the objectui revision the port was taken from, re-records
objectui's side of the lockstep there, and follows the bump's second half by
regenerating the tracked SDUI manifest.

The manifest regenerates BYTE-IDENTICAL (sha256 49211fee7792cf51 before and
after): both pins ship @object-ui 17.6.0, so the published registry inputs did
not move. Only the record's objectuiSha stamp follows the pin.

Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
Co-authored-by: Claude <noreply@anthropic.com>
…ngeset

check:dispatcher-error-vocabulary cannot reduce a kebab-case SCREAMING_SNAKE
constant at a `code:` position, so objectui's `code: INERT_QUICK_ADD` is an
unresolvable-constant finding here. This is the same documented divergence
`unconsumed-widget-option` already carries: stamp the inline literal, keep the
exported constant, and pin the two equal next door. The lockstep resolves the
identifier through the constant, so both copies still agree on all 26 codes.

The changeset is OWED and measured: both codes reach the published dist (the
package ships `files: [dist]`), against a negative control that reads zero.

Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 17, 2026
@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

15 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/sdui-parser/objectui-lockstep.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/sdui-parser/objectui-lockstep.json) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 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; 100 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 c5d3d1d8b69e84497373c5232a654f0bbffe2b76 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from e12592ef8a530f503ed35a13e8a385db345936f6 — the merge of head 8a778067eb58d9b79241f6546016ea411c1269c8 into base c5d3d1d8b69e84497373c5232a654f0bbffe2b76, 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 e12592ef8a530f503ed35a13e8a385db345936f6 && git checkout e12592ef8a530f503ed35a13e8a385db345936f6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c5d3d1d8b69e84497373c5232a654f0bbffe2b76 8a778067eb58d9b79241f6546016ea411c1269c8 && git checkout -B drift-repro c5d3d1d8b69e84497373c5232a654f0bbffe2b76 && git merge --no-ff 8a778067eb58d9b79241f6546016ea411c1269c8

node scripts/docs-audit/affected-docs.mjs --json c5d3d1d8b69e84497373c5232a654f0bbffe2b76

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

Copy link
Copy Markdown
Collaborator

Director seat, summon #25 (session_012GcsUbuqFGBibkEDMRC1eE), 2026-09-20T14:59Z — to the domain:devx seat that owns this PR.

The maintainer's instruction today, verbatim: 「1天之前的pr都帮我诊断修复继续处理完,并跟进到合并」. This PR is 2.9 days old and red on TypeScript Type Check (Verify every type-check lane succeeded — Type Check · source gates failed). Two more readings: the PR moves .objectui-sha to cfcc17d9dd04, but main already pins 53ded82bf7a4, so the port is behind the pin it was meant to bump and the lockstep record has to be re-taken at main's pin (or a newer one) before this can merge; and the card's reading (2026-09-16) was already newer than the ported revision. ⇒ A rework round is owed now: re-port at the current pin, re-record objectui-lockstep.json, fix the type errors, merge origin/main, gates, push; then the at-tier contract review (needs:contract-review is on the PR) and ready + auto-merge.

⛔ This seat does not touch the branch. If this PR shows no new output within three hours of this comment, the director seat recovers the claim on #17645 under the maintainer's takeover instruction and dispatches the rework itself.


Generated by Claude Code

…lands via #19398 first

The Single-Claim Path Guard on the pin-bump PR fails with "At most one open PR
may modify a declared single-writer path": .objectui-sha is a declared
single-writer path and this draft still rewrites it (53ded82bf7a4 to
cfcc17d9dd04), so it counts as an open claimant. Per ruling batch #200 item 4
the pin bump lands on its own PR first.

So this branch releases the claim: both pin-carried files are restored to
main's blobs byte for byte (.objectui-sha and scripts/sdui-manifest.record.json,
the latter because check-sdui-manifest requires record pin == .objectui-sha).
Nothing else is touched — the parser port, its tests and the changeset are
unchanged.

check:sdui-lockstep is RED in the interim, by design: [pin-moved], the lockstep
record was taken at cfcc17d9dd04 while .objectui-sha now reads 53ded82bf7a4.
This PR stays draft and re-records the lockstep at the new pin in a merge lap
after the pin bump lands.

Claude-Session: https://claude.ai/code/session_012GcsUbuqFGBibkEDMRC1eE
Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator

Closed as superseded by the director seat (session_012GcsUbuqFGBibkEDMRC1eE), 2026-09-20T22:57Z, under ruling batch #201 item 1 — maintainer 「201 A」, record 5753263180 on #17645.

This PR's two-code port (inert-quick-add, member-type-mismatch, kanban-quick-add.ts, both test files with their ablation legs, and .changeset/17645-sdui-parser-lockstep-port.md) lands inside PR #19398 (claude/issue-17429-objectui-pin-bump), which also carries the .objectui-sha bump to 87af769e9a3e with its artefacts and the 27 re-measured packages/spec/src/** citations — the one landing in which check:sdui-lockstep can be green (the devx seat's reading at 5718211831 and the #17429 dev's at #17429 stand: bump without port is [code-drift], port without bump is [pin-moved]). #19398 closes both #17429 and #17645. This branch (claude/issue-17645-sdui-lockstep-port, head 8a778067eb) is left as is for the cherry-pick's provenance; ⛔ nothing on it is force-pushed or deleted by this act. Closing now also clears the 「No other open PR may claim the same issue」 gate on #19398, which fired the moment #19398's body gained Fixes #17645.


Generated by Claude Code

akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…from the PIN, not the record (objectstack-ai#18875)

Fixes objectstack-ai#18819

Clause-②: no

## The defect, re-measured on this branch's base

`origin/main` `36583e989b`. The generator read its two provenance fields
at two different
moments:

```
:129   const version = arg('--objectui-version') ?? record.objectuiPackagesVersion;
:199   objectuiSha: pin,
:200   objectuiPackagesVersion: version,
```

`objectuiSha` is re-read from the LIVE `.objectui-sha`;
`objectuiPackagesVersion` fell back to
the value already sitting in `scripts/sdui-manifest.record.json`. A
regeneration after a pin bump
therefore wrote the NEW pin under the OLD version string — a record
whose two objectui fields
describe two different commits, which is exactly the record
`scripts/check-sdui-manifest.mjs`
check 4 rejects. The person that reds is the one who followed the
documented bump procedure, so
the producer is where it gets fixed.

## Step one, before writing any fix: is that path reachable today?

The counterfactual on `:129` is that the defect is unreachable if
`arg('--objectui-version')` is
never empty. It is reachable. **Paths, not counts.**

**No automated caller exists at all.** No `package.json` script, no
workflow step and no shell
line executes this generator. `scripts/bump-objectui.sh` and
`scripts/build-console.sh` only
`echo` the command. Every invocation is therefore a person or an agent
typing one of the
spellings the tree documents.

Of those spellings, six omit `--objectui-version`:

| path | spelling |
|:---|:---|
| `scripts/check-sdui-manifest.mjs:228` | `Regenerate: node
scripts/gen-sdui-manifest-node.mjs` — printed when the artefact is
missing |
| `scripts/check-sdui-manifest.mjs:275` | `Regenerate: node
scripts/gen-sdui-manifest-node.mjs` — printed on a sha256 mismatch |
| `scripts/sdui-manifest.record.json:10` | `Regenerate + re-record: node
scripts/gen-sdui-manifest-node.mjs` |
| `packages/spec/scripts/manifest-prescription.ts:81` | ``regenerate it
and its record with `node scripts/gen-sdui-manifest-node.mjs` `` |
| `scripts/gen-sdui-manifest-node.mjs:8` | the bare usage line |
| `scripts/gen-sdui-manifest-node.mjs:9` | the `--modules-root DIR`
offline usage line |

Two of those six are printed **by the gate itself**. The remaining
spellings do carry the flag:
`AGENTS.md:156`, `docs/releases-maintenance.md:116` and `:279`,
`scripts/bump-objectui.sh:11` and
`:739`, `scripts/build-console.sh:297`,
`scripts/check-sdui-manifest.mjs:301` `:303` `:320`.

The other paths that name the generator carry no command at all and are
listed here for
completeness: `.github/workflows/lint.yml:799` (a comment),
`packages/spec/scripts/check-generated.ts:391`,
`packages/spec/scripts/check-react-blocks-declaration-parity.ts:85`, and

`packages/spec/scripts/check-react-blocks-declaration-parity.test.ts:565`
`:569` (a pin test on
the prescription text).

## A or B — decided by that reading, not by preference

Zero automated call sites means option B's stated cost ("every call site
has to change") is
nearly zero *in code*. But B is not free: it would turn all six
documented no-flag spellings into
refusals, including the `--modules-root` offline mode, and it would
refuse even when the answer is
sitting in the objectui checkout the bump already required.

So: **A for the default, B for the residue.**

- `--objectui-version` still wins when passed.
- Otherwise the version is what `packages/core/package.json` DECLARES at
the pinned commit, read
through `check-sdui-manifest.mjs`'s exported `readPinnedDeclaredVersion`
— the same function
check 4 judges the written record with, imported rather than re-typed.
Producer and gate now
read one oracle; two readers of two oracles is where this drift came
from.
- When that oracle cannot answer (no objectui checkout, or one that does
not carry the pin) the
generator REFUSES and names both remedies. An unread version is never
converted into a version
string — Route and surface ownership §3, prefer failing to falling back,
and Prime Directive
  objectstack-ai#12's ban on a lenient `??` fallback in place of fixing the producer.

Importing the gate's exported oracle changes no gate:
`scripts/check-sdui-manifest.mjs` is
untouched in this diff, and its CLI body is behind
`isEntrypoint(import.meta.url)`, so importing
it runs nothing.

## Verification

Every leg below ran against a fixture tree in a scratchpad, or against
the real repo with a trap
and a proved restore. `.objectui-sha` and
`scripts/sdui-manifest.record.json` are read-only this
round; neither is in this diff.

### LIT — pin bumped, `--objectui-version` not passed. Both legs read.

A synthetic objectui checkout with two commits (`a183cfbf…` declaring
`17.6.0`, `51ecffcc…`
declaring `18.0.0-fixture`), a stub `@object-ui` install behind
`--modules-root`, and a fixture
record naming the OLD sha at `17.6.0`. The fixture pin is the NEW sha.

| leg | generator | record written | `check-sdui-manifest.mjs` |
|:---|:---|:---|:---|
| before | `36583e989b` | `51ecffccfb32 / 17.6.0` — new pin, **old
version** | **exit 1**, check 4 red |
| after | this branch | `51ecffccfb32 / 18.0.0-fixture` | **exit 0**,
green |

The before leg's gate output is check 4 naming the defect verbatim:

```
the record's two objectui fields describe different commits: objectuiPackagesVersion is "17.6.0",
  but objectui 51ecffccfb32… — the pin the record itself names — declares "18.0.0-fixture" in packages/core/package.json
```

### DARK — pin not bumped. Reads zero, on the REAL artefact.

Not a fixture: the real `scripts/gen-sdui-manifest-node.mjs`, the real
pin
`53ded82bf7a494f54e344e19099dbf00854b8694`, and the real published
`@object-ui/*` 17.6.0 set
installed once and reused by both legs so the install cannot be the
variable. The base generator
was swapped in on disk, its arrival proved by anchor counts before the
leg ran
(`?? record.objectuiPackagesVersion` = 1, `readPinnedDeclaredVersion` =
0, and a `git hash-object`
differing from the HEAD blob), then restored.

```
leg after  (fixed) : 57 components, 66910 bytes, sha256 49211fee7792cf51174930dc2c1be2169d5175f77a83491f68f90b3f0c19e69d
leg before (base)  : 57 components, 66910 bytes, sha256 49211fee7792cf51174930dc2c1be2169d5175f77a83491f68f90b3f0c19e69d
cmp: BYTE-IDENTICAL
```

That sha256 is also the one the tracked record already carries, and the
tracked
`sdui.manifest.json` HEAD blob is still `78f870e42fe9` — unmoved.
Restore proved by state, not by
an exit code: `git diff HEAD` empty and `git status --porcelain` empty
afterwards.

### Two further legs

- **Refusal residue.** Pin bumped, no flag, oracle unreachable
(`OBJECTUI_ROOT` at a directory
with no `.git`): base generator exits 0 and silently writes the drifted
record; this branch
  exits 1, writes no manifest, and leaves the record untouched.
- **Explicit precedence.** `--objectui-version 99.1.2-explicit` is
honoured unchanged on both
  legs; the success line now also names where the version came from.

### Gates

- `node scripts/check-sdui-manifest.mjs` — **exit 0** on the clean tree,
and its check 4 leg
resolved for real: "@object-ui 17.6.0 is the version objectui
53ded82bf7a4… declares in
  packages/core/package.json (read from /home/user/objectui)".
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 26
commands; all 26 run, **all exit 0**, and `--ran` with exit codes
recorded reconciles:
"26 derived famil(ies) accounted for — 26 run, 0 NOT-MEASURED (a DERIVED
zero)".
- The seven roster families the tool flags as "roster under scripts,
which one of your paths is
in — silence is not evidence in EITHER direction" were run too. Six exit
0.
`pnpm check:published-readme-exports` exits **3 — PREREQUISITE NOT MET**
(46 packages unbuilt);
that is NOT MEASURED, not a finding, and this diff moves no package
source and no README.
- `pnpm lint` (`eslint . --no-inline-config`), the whole repo, **exit
0** at `1c3a721ac1`.
- `pnpm check:nul-bytes` exit 0; an independent control-byte scan of the
changed file is clean.

No changeset: nothing published moves. `scripts/**` appears in no
package's `files[]` and the
root package is `private`, so no released package's contents change.

## Acceptance notes

Noted, not filed — none is a reproducible defect, a declared-contract
violation, or a
metadata-authoring trap, so none earns a card under Prime Directive objectstack-ai#10:

- `scripts/check-sdui-manifest.mjs` check 4's header paragraph, and its
check-4 failure text at
`:318-319`, both explain the producer default this PR removes ("The
generator REUSES the version
already recorded when `--objectui-version` is not passed"). After this
lands those sentences
describe history rather than current behaviour. The gate is fenced
read-only for this card, so
they are left exactly as they are. The check itself stays load-bearing —
it still catches a
hand-edited record, an explicitly-wrong `--objectui-version`, and any
other producer.
- `packages/spec/scripts/check-react-blocks-declaration-parity.ts:88-89`
says the node producer
"enumerates the PUBLISHED `@object-ui/*` packages at the version named
in
`scripts/sdui-manifest.record.json`". That sentence is wrong-by-default
after this change. Not
fixed in place: editing it pulls `packages/spec` into the diff and with
it a whole gate family
this card does not otherwise owe, against the bounded-in-place-fix
condition of adding no new
  verification surface.
- The residual hole this PR deliberately does not close: an operator who
passes
`--objectui-version` **explicitly and wrongly** still writes a drifted
record. The generator
could warn when an explicit value disagrees with the resolvable pin
oracle. Check 4 already reds
the resulting record, so this is a loudness improvement, not a
correctness gap.
- objectstack-ai#17735 is not addressed here — whether the published tarballs at a
version were built from the
pinned commit is that card's producer question, and it carries
`needs-user-decision`. This
change moves the version's SOURCE and nothing about that relation.
objectstack-ai#18814's check 4 is not
  modified here either.

Fenced files honoured: `scripts/sdui-manifest.record.json` and
`.objectui-sha` are held by open PR
objectstack-ai#18723 and are read-only in this round. The real DARK leg did rewrite
the record as a side effect
of running the generator, exactly as warned; both were restored from
HEAD and the restore is
proved by an empty `git diff HEAD`, never by an exit code.

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

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…ult, not the record (objectstack-ai#18956)

Fixes objectstack-ai#18879

Clause-②: no

PR objectstack-ai#18875 retired the generator's record-derived version default.
`scripts/gen-sdui-manifest-node.mjs`
now resolves the version in one order — `--objectui-version` when
passed, otherwise what the PINNED
objectui commit declares in `packages/core/package.json` (through
`check-sdui-manifest.mjs`'s own
exported `readPinnedDeclaredVersion`) — and REFUSES when neither
answers. Three passages still
described the retired default as current. This repairs those three, and
leaves four others alone
because they are conditional, not broken.

## The card's six sites are not one class, and there is a seventh

I enumerated every site on this tree and classified each. The census
phrase the card used
(`gen-sdui-manifest-node`, 30 hits tree-wide) plus two wording sweeps
(`REUSE`, `already in the
record` / `already recorded` / `already sitting`) is what the
enumeration is taken from.

Class (a) — literally false today, repaired here:

1. `scripts/check-sdui-manifest.mjs` check 4's problem text (was
:318-319)
2. `packages/spec/scripts/check-react-blocks-declaration-parity.ts` (was
:87-88)
3. `scripts/check-sdui-manifest.mjs` header, check 4's own rationale
(was :43-47) — NOT on the
   card's six; see "The seventh site" below

Class (b) — conditional, not broken, deliberately untouched:

4. `scripts/check-sdui-manifest.mjs:228` (artefact missing) — flagless
`Regenerate:` prescription
5. `scripts/check-sdui-manifest.mjs:275` (sha256 mismatch) — flagless
`Regenerate:` prescription
6. `scripts/sdui-manifest.record.json:10` — flagless prescription; also
held by open PR objectstack-ai#18723
7. `packages/spec/scripts/manifest-prescription.ts:81` — flagless
prescription, and its own text
   already points the reader at where the exact call is printed

## (a) verbatim, before and after

### 1. check 4's problem text

Before (the false sentence is the REASON CLAUSE for the prescription on
the line below it):

```
  describes a registry this pin does not name. The generator REUSES the version already recorded when
  --objectui-version is not passed, which is how a regeneration re-records a new pin under an old version.
  Regenerate against the pin: node scripts/gen-sdui-manifest-node.mjs --objectui-version 9.9.9-what-the-pin-declares
```

After:

```
  describes a registry this pin does not name. The generator reads this version from the PIN, never
  from this record, so a disagreement came from outside that default: a version named explicitly on
  some other run, a hand edit, or a record written before the default was fixed to read the pin.
  Regenerate against the pin: node scripts/gen-sdui-manifest-node.mjs --objectui-version 9.9.9-what-the-pin-declares
```

Both blocks are real output, captured by driving `checkTree` against the
fixture shape the script's
own self-test uses (a green tree plus a throwaway objectui whose
declared version disagrees with the
record). The prescription line is unchanged.

Why the new sentence is true of today's generator:
`gen-sdui-manifest-node.mjs:149-168` takes
`--objectui-version` when passed, otherwise calls
`readPinnedDeclaredVersion(pin)` and `fail`s on any
status but `resolved`; it never reads `record.objectuiPackagesVersion`.
Check 4 only runs when
`pin === record.objectuiSha` (check 3 returns early otherwise), so at
this point the record names the
LIVE pin and its version field disagrees with what that pin declares.
The three routes the new
sentence lists are the ones that survive: an explicit
`--objectui-version` naming another version, a
hand edit, and a record written before that default was fixed.

### 2. the node producer's description in the parity gate header

Before:

```
//   node     `scripts/gen-sdui-manifest-node.mjs` writes the TRACKED repo-root
//            artefact under plain Node — no browser, no objectui build. It
//            enumerates the PUBLISHED `@object-ui/*` packages at the version named
//            in `scripts/sdui-manifest.record.json`.
```

After:

```
//   node     `scripts/gen-sdui-manifest-node.mjs` writes the TRACKED repo-root
//            artefact under plain Node — no browser, no objectui build. It
//            enumerates the PUBLISHED `@object-ui/*` packages at the version
//            `--objectui-version` names, else the one the PINNED commit's
//            `packages/core/package.json` declares — and REFUSES when neither
//            answers. `scripts/sdui-manifest.record.json` is what that run
//            WRITES, never where it reads the version from.
```

Why the new sentence is true of today's generator: the record is the
run's OUTPUT
(`gen-sdui-manifest-node.mjs:229-240` writes `nextRecord` with
`objectuiPackagesVersion: version`),
never its input. The paragraph forty lines below this one in the same
header already says the version
is "read off the pinned commit's `packages/core/package.json`", so the
file now agrees with itself.

### 3. the seventh site — the same defect, one grep-phrase away

The card's census missed it because the wording differs: the problem
text says "the version already
recorded", the header says "the version already in the record", so the
card's own firing control
never sounded on it.

Before:

```
 *      with nothing asking whether V is C's version at all (objectstack-ai#18611). The live
 *      way to write that record is the generator's own default —
 *      `--objectui-version` unset makes it REUSE the version already in the
 *      record, so a regeneration after a pin bump keeps the OLD version string
 *      while re-recording the NEW pin, and checks 1-3 stay green throughout.
```

After:

```
 *      with nothing asking whether V is C's version at all (objectstack-ai#18611). The
 *      generator's own default does not write such a record — `--objectui-version`
 *      unset reads the version off the PIN and REFUSES when the pin cannot be
 *      read — but an explicit wrong version, a hand edit, or a record written
 *      before that default was fixed still produce one, and checks 1-3 stay
 *      green throughout.
```

Repaired in place rather than filed: same file, same defect class, same
gate family, mechanical, and
the correct shape was already pinned by the sibling passage above. A
full scan of all 25 open
non-release PRs (file lists read one by one) found no other claim on
this file.

## Firing control

`git grep -n 'version already recorded' -- '*.mjs' '*.ts' '*.md'`

- before: 1 hit (`scripts/check-sdui-manifest.mjs:318`)
- after: 0 hits, exit 1

The instrument is not dead. Two positive controls of the same command
shape, run on the same tree
after the edit: `'version already sitting in the record'` answers 1 hit
in
`scripts/gen-sdui-manifest-node.mjs`, and `'Regenerate against the pin'`
answers 1 hit in
`scripts/check-sdui-manifest.mjs` — the very file the zero is claimed
about, so the grep demonstrably
still reaches it.

## (b) decided, with live readings, and the decision differs from triage

Triage framed `:228` / `:275` as "a gate handing you a command it will
itself refuse", and half of its
`p2` audience argument rests on that reading. I agree with the PM's
narrowing, and I did not take the
reading off the source: I ran the generator both ways in a scratch root,
so nothing in this tree was
written.

The scratch root was made discriminating on purpose — its record was
seeded with
`objectuiPackagesVersion: "0.0.0-BOGUS-SCRATCH"`, a version that exists
on no registry. Had the
retired default still been live, the run would have tried to install it.

Leg 1 — no resolvable pin (`OBJECTUI_ROOT` pointed at a path with no
checkout): exit 1, nothing
written, and the refusal carries its own two remedies:

```
✗ gen-sdui-manifest-node: no --objectui-version, and the pinned commit's declared version could not be read: no objectui checkout at /nonexistent-objectui (no .git there). Set OBJECTUI_ROOT to one that carries the pin.
  ⛔ This script does NOT fall back to the version in scripts/sdui-manifest.record.json: after a pin
  bump that re-records the NEW pin under the OLD version string, and check-sdui-manifest.mjs check 4
  reds exactly that record. Either:
    - point OBJECTUI_ROOT at an objectui checkout carrying 53ded82bf7a4…, or
    - pass --objectui-version {the @object-ui version that pin ships} explicitly.
```

Leg 2 — resolvable pin, full run including the network install: exit 0.

```
→ installing @object-ui/* 17.6.0 into /tmp/sdui-manifest-gen-OcPzor (npm, network)...
→ enumerating the registry (16 modules)...
✓ wrote sdui.manifest.json (57 components, 66910 bytes, sha256 49211fee7792…)
✓ re-recorded scripts/sdui-manifest.record.json at pin 53ded82bf7a4 / @object-ui 17.6.0
  (version from packages/core/package.json at objectui 53ded82bf7a4… (read from /home/user/objectui))
```

It resolved `17.6.0` — the version objectui declares at the pin — over
the bogus `0.0.0-BOGUS-SCRATCH`
sitting in the record, and overwrote the record with it. The manifest it
produced carries
sha256 `49211fee7792…`, byte-matching the artefact tracked here.

So `:228` and `:275` are not broken. The flagless call they print writes
the correct artefact whenever
the pin's declared version is readable, and when it is not, it refuses
loudly with the remedy in its
own failure text rather than writing a drifted record. That is the
behaviour objectstack-ai#18875 was for. They are
conditional, and a conditional prescription that fails loudly and names
its own fix is not the shape
this repo files cards about. Sites 6 and 7 in the list above are the
same conditional shape; site 7
(`manifest-prescription.ts:81`) even printed itself during this round's
gate sweep, intact and
actionable, when `check:react-declaration-parity` ran without `MANIFEST`
set.

One consequence worth writing down rather than acting on: because the
flagless default now reads the
pin, the flagless call would ALSO repair the check-4 record. The flagged
prescription is kept
unchanged anyway — it names the value in the command, which is the
self-evidencing form, and this
round is prose only.

## DARK — the gate's verdicts do not move

`scripts/check-sdui-manifest.mjs` run before and after the edit, three
arms, both streams captured to
files and compared with `cmp`:

| arm | exit before | exit after | stdout | stderr |
|---|---|---|---|---|
| plain (resolves the `../objectui` sibling) | 0 | 0 | identical, 290
bytes | identical, 0 bytes |
| `OBJECTUI_ROOT` unresolvable | 0 | 0 | identical, 496 bytes |
identical, 0 bytes |
| `OBJECTUI_ROOT` unresolvable + `--require-objectui` | 1 | 1 |
identical, 0 bytes | identical, 295 bytes |

`--self-test` is green and its output is byte-identical before and
after: 11 cases, battery floor 11.
The strings this PR edits live on failure paths, so a green tree never
prints them — which is exactly
why the verdict bytes do not move.

## Gates

Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, run
one by one with each exit code landed to disk before anything read it,
then reconciled with `--ran`
carrying those codes.

```
✓ dispatch-gates --ran: 72 derived famil(ies) accounted for — 70 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3).
```

The two NOT MEASURED are `check:dual-build-cjs-loads` and
`check:lean-entry-closure`. Both exit 3 —
their own `PREREQUISITE NOT MET` code — because they load built output
and this worktree has no full
`pnpm build`. Both self-tests passed (93 and 22 cases). Neither can be
moved by a comment-only diff;
they are declared to CI.

One gate exits 1 under the derived spelling and 0 under CI's:
`check:react-declaration-parity` refuses
when `MANIFEST` is unset, which is a prerequisite, not a red. Run as
`lint.yml` runs it —
`MANIFEST="$PWD/sdui.manifest.json" ... --baseline
react-declaration-parity.baseline.json --strict` —
it exits 0 with "no new DECLARATION divergence vs accepted baseline".

Derivation stability: the 72-family list came out byte-identical at all
three tree states this branch
passed through (`625db0e853`, `0ea26d37e8`, `92eee4dcc5`), so the tool's
STALE TREE notice — raised
because a sibling agent's fetch keeps advancing the shared `origin/main`
ref — changed nothing about
what is owed here.

## Tests and lint

- `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2
scripts/check-react-blocks-declaration-parity.test.ts
scripts/build-schemas-check-mode.test.ts` — 2 files, 115 tests, all
passed. These are the suites that name the two edited scripts.
`scripts/check-sdui-manifest.mjs` has no sibling test file in
`scripts/`; its `--self-test` is its suite, and it is above.
- `pnpm --filter @objectstack/spec typecheck` — exit 0.
- `pnpm --filter @objectstack/spec check:scripts-typecheck` — exit 0;
this is the program that compiles the edited `packages/spec/scripts/**`
file.
- `pnpm --filter @objectstack/spec build && pnpm --filter
@objectstack/spec check:generated` after merging `main` — all 15
generated artifacts up to date.
- Dependency-closure build `pnpm --filter '@objectstack/spec^...' build`
is an empty run here: "No projects matched the filters" —
`packages/spec` has no workspace dependencies.
- Lint, the whole union, as CI runs it, at `92eee4dcc5`: `eslint .
--no-inline-config --format json` over a population of **6855 files**
read from eslint's own JSON output — 0 errors, 0 warnings. Narrowed to
the two edited files: 2 files, 0 errors, 0 warnings.
`eslint.config.mjs:326-328` records that this repo "never enables
type-aware linting (no `parserOptions.project`, no typed
`@typescript-eslint` rules) for ANY file", so nothing in this diff can
move an untouched file's verdict either way.

## Changeset

`skip-changeset`, measured rather than assumed:

- No package manifest in the repo declares a `files[]` entry reaching
`scripts/` — 76 manifests scanned, 0 hits.
- `packages/spec`'s `files[]` is `dist`, `json-schema`, `liveness`,
`prompts`, `llms.txt`, `README.md`, `src/**/*.zod.ts`, `CHANGELOG.md`,
`api-surface`, `spec-changes.json`. `scripts/` is not among them.
- Grepping every one of those published paths (plus all 202
`src/**/*.zod.ts`) for two distinctive strings from this diff returns
zero hits, exit 1. Positive control, same command shape:
`PageComponentSchema` answers from `dist/identity/index.js` and three
siblings.
- `scripts/check-sdui-manifest.mjs` is a repo-root script belonging to
no package.

Nothing published moves, so the tarballs do not move.

## Read-only fences, re-taken at start of work

- `scripts/sdui-manifest.record.json` is held by open PR objectstack-ai#18723 (state
open, draft, 11 files) — confirmed by reading that PR's file list
directly, not inherited. Untouched here. Its own stale-ish prescription
is class (b) anyway, so the fence costs this round nothing.
- The same scan confirmed objectstack-ai#18723 does NOT hold
`check-sdui-manifest.mjs`, `check-react-blocks-declaration-parity.ts` or
`gen-sdui-manifest-node.mjs`, and no other open PR holds any of them.
- `scripts/gen-sdui-manifest-node.mjs` is this round's source of truth
and is untouched.
- No self-test making every printed prescription runnable was added.
That is a new gate and it is not on this card.
- No product code under `packages/spec/src/`.

## Acceptance notes

Observed, deliberately not filed:

- The card counts "six" sites; a full sweep finds a seventh, in the same
file, repaired here under the bounded in-place rule. Noted so the census
phrase is on record: a wording sweep keyed on one spelling ("already
recorded") will not find its sibling spelling ("already in the record").
Who would hit it: the next reader of check 4's rationale — the same
audience as the problem text, which is why it is repaired rather than
noted.
- `docs/releases-maintenance.md:116` / `:279`,
`scripts/bump-objectui.sh:11` / `:705` / `:739` and
`scripts/build-console.sh:282` / `:297` all print the FLAGGED form and
are correct today; `scripts/sdui-manifest.record.json:5` and
`check-sdui-manifest.mjs:19-21` describe the version as "the version
`.objectui-sha` ships", which is more true after objectstack-ai#18875 than before.
None of them is a site. Carrier: none — nothing here needs changing.

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

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…ED, not a census takedown (objectstack-ai#19290)

Fixes objectstack-ai#19077

Clause-②: no

Item **2** of objectstack-ai#19077 — the census-side application. Item 1
(`parseDerivedText`, the
returnable door) landed as PR objectstack-ai#19112, squash `956e0107a`; nothing here
re-does it.

Base: `origin/main` = `215840f4353`, fetched 2026-09-20T09:07Z. Every
reading below was
taken on that base in a dedicated worktree, never on the shared
checkout.

## The defect, and what it did

`scripts/tenant-audit-census.mjs` stores a declared type's text
whitespace-collapsed and
re-parses it as a synthetic alias (`type CensusReceiver = ...;`) to read
the engine-door
rule off it. A type literal may separate its members by a **newline
alone** — legal
TypeScript — and the collapse turns that separator into nothing, so the
synthesis does not
parse. Through `parseSourceFile` that did not fail the SITE: it ended
the process, and
every other site in the corpus lost its verdict with it, under a refusal
naming
`census-receiver-type.ts`, a file that does not exist in the tree.

## The repair — the four points of the minimal hunk, and where each
landed

| point | landed |
|---|---|
| ① the door rule takes the origin `sf` the type text came from |
`readTypeTextDoor(typeText, origin)`; `resolveReceiver` hands its own
`sf` at all four `inlineEngineDoorOrOther` call sites |
| ② the synthetic alias goes through `parseDerivedText` |
`readTypeTextDoor`, one call site, the only synthesis in the file |
| ③ on failure the report is printed against the SITE and the door reads
`false` | the located verdict rides on the site as `derivedFailure`;
`main()` prints it under a per-site `::error::` line |
| ④ a **declared** `NON_ENGINE_REASONS` arm, placed in
`UNDEFENDED_REASONS` | `type-text-not-round-trippable`, in both
artefacts under ENFORCEMENT |

## ⛔ Point ④ and the floor — why this PR also edits the gate

The non-negotiable is that no importer ends up exiting 0 where it exits
non-zero today.
Measured, rather than assumed: `lint.yml:2007-2008` invokes
`scripts/check-tenant-audit-census.mjs` and **never the generator**, and
that gate's own
docblock says why (`censusRefusals` exists because `runCensus`'s
findings were read by
nothing on the way to a CI verdict). So on a corpus holding such a
receiver:

- **today** — `runCensus` calls `parseSourceFile`, which exits 3; the
gate dies with it, CI red.
- **with the repair and no gate arm** — the site would be declared in
the artefacts, the
author would regenerate, and the gate would go **green**. That is the
loud process exit
traded for a quiet subtraction: the same floor drop in a different
costume.
- **as landed** — the site is classified, declared in both artefacts,
printed against its
own file and line with the parse verdict under it, **and refused** by
`censusRefusals`
through `notRoundTrippableSites`, the one spelling the generator and the
gate share.
  `main()` counts it in its exit code too, for the local run.

⇒ the exchange is a process-wide takedown for a located, per-site
refusal. Nothing that
exits non-zero today exits 0 after this, and no site leaves the
population in silence.

## Acceptance — triage's two inputs, as a pair

Both run in `tenant-audit-census.mjs`'s own self-test, which CI drives
through
`node scripts/check-tenant-audit-census.mjs --self-test`
(`lint.yml:2007`). Ten new cases;
the file's self-test goes 49 to 59, the gate's 24 to 27, and the gate's
`census refusals`
battery floor is raised 5 to 7 so the new cases cannot stop running
unnoticed.

1. **A newline-separated inline type literal is CLASSIFIED.** The case
runs the REAL round
trip — a source is parsed, `declaredTypesIn` collapses the declared type
exactly as the
census does, and the resolver reads the door off the stored text — and
reads
`other/type-text-not-round-trippable`. **Lit control:** the semicolon
spelling of the
SAME literal is still PLACED as `engine/inline type literal stating an
engine door`, so
the first case is not a door that rejects everything. Two more: the arm
is in
`UNDEFENDED_REASONS` (control: a defensible arm is not), and the failure
is carried as
   located data naming the SOURCE it was derived from.
2. **A genuinely unparseable input is still REFUSED.** A garbage type
text lands on the
same declared arm and no door is read off it, so it is never placed. The
corpus door is
untouched: sources are still read through `parseSourceFile`, whose
process-exit refusal
is pinned by `ts-parse.mjs --self-test` (landed with item 1) and
enforced for every
   `scripts/**` caller by `check:parse-guard` (exit 0 here).
**Boundary, pinned:** the verb gate runs first, so a newline-separated
literal naming no
write verb is never synthesised and cannot reach the new arm — the
repair's reach is
   exactly the defect's reach.

## The census over the live corpus, before and after

`node scripts/tenant-audit-census.mjs`, base `215840f4353`:

- **before** (2026-09-20T09:08Z): `exit 0`, **576 sources**, 227 write
call sites.
- **after** (2026-09-20T09:20Z): `exit 0`, **576 sources** — output
**byte-identical**
(`diff` exits 0), one undefended subtraction, reason
`type-not-in-corpus`, door-shaped 0.

⚠️ The previous round measured 573 sources; `main` moved between the
rounds. The escalation
trigger did **not** fire: exit 0 means no receiver of this shape exists
in the tree, so the
card stays latent and the grading stays triage's.

## Reverse verification — three ablations, each restored byte-identical

Driven through `scripts/ablation-replace.mjs`, which proves the mutation
landed on disk
(anchor count fell, replacement count rose, blob hash changed) and
proves each restore
(`blob == HEAD blob`, `git diff HEAD` empty).

1. **The door reverts to `parseSourceFile`** — the self-test does not
fail, it **dies**:
`exit 3`, printing the card's own refusal for `census-receiver-type.ts`
with
`1:78 ';' expected`. That is the defect, reproduced from the acceptance
case.
2. **The arm is removed from `UNDEFENDED_REASONS`** (point ④'s own
ablation) — RED, the
   declaration pin fails, 1 of 59.
3. **The gate's refusal loop is emptied** — RED, 2 of 27 gate cases
fail, which is the
   "quiet subtraction" costume failing to pass.

⚠️ Reported rather than quietly retried: ablation 2's first attempt was
**refused** by the
helper because the replacement text was a substring of the anchor, so
the on-disk count
could not rise. It restored and never ran the command; the rerun used a
dropping anchor.

## Gates

`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, derived
from this tree at `ce1b5304101` (4 paths vs merge base `215840f43`,
three-dot): **64
families, all exit 0**, each captured BEFORE any pipe, reconciled with
`--ran`:
`64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN` — a DERIVED zero, every
family carrying a code.

Five of the 64 first answered `PREREQUISITE NOT MET` (exit 3 / exit 1:
not findings,
nothing measured) and were re-run after building what they read —
`pnpm --filter '@objectstack/lint...' build`, then
`pnpm --filter '@objectstack/client-react...' --filter
'@objectstack/client...' build`,
both through `scripts/pm/os-verify-lock.sh` (`VERDICT command-exit 0`).
All five then exit 0.

Outside that total by the tool's own accounting: 52 artifact-roster
families, 11 declared
wide-population families, 14 pending-changeset families, 2
path-scheduled CI jobs.

`pnpm lint` repo-wide is CI's run. The narrowing here is DECLARED and
carries all three
readings: (1) the reachable population comes from eslint's own config
text —
`eslint.config.mjs:328` records that this repo runs one config that
never enables
type-aware linting (no `parserOptions.project`, no typed rules) for ANY
file, measured
there with a positive control; (2) the targeted count is read from
`--format json`: 4
files, 0 errors, exit 0 — the two markdown artefacts report "File
ignored because no
matching configuration was supplied", i.e. eslint does not lint them at
all; (3)
invariance — with no type-aware linting anywhere, a 4-file diff cannot
move the verdict on
a file it does not contain. Taken at `ce1b5304101`.

## Changeset

`skip-changeset`, on three readings: (1) the root package is `private:
true`; (2) all **70**
non-private workspace packages declare a `files[]` (lit control) and
**0** of them have an
entry that climbs out of its own package directory; (3) none of the four
changed paths is
under any package directory. ⇒ root `scripts/**`, `content/docs/**` and
`docs/audits/**`
cannot be in any published payload. `Clause-②: no` follows: this moves a
build-time gate's
failure convention, not `packages/spec`'s accept set or any public
surface.

## ⚠️ Declared deviation — three files outside the dispatched file
surface

The dispatch declared `scripts/tenant-audit-census.mjs` and new tests
under `scripts/`.
This PR also touches:

- `scripts/check-tenant-audit-census.mjs` — the gate-side half of point
④, argued above.
Without it the repair lowers the floor, which the dispatch forbids
outright.
- `content/docs/permissions/tenant-audit-census.mdx` and
`docs/audits/2026-08-tenant-audit-write-call-sites.counts.md` —
GENERATED regions,
rewritten by `node scripts/tenant-audit-census.mjs --write`, which is
the only legal way
to edit them and is what `check-tenant-audit-census.mjs` demands. Point
④ puts the arm in
the enforced undefended table, so the table's prose had to name the
third cause or the
  artefact would explain a row it does not cover.

Measured before touching them, 2026-09-20T09:1xZ: **22 open PRs, 623
file rows** (PR
objectstack-ai#17076 paged past the 100-row cap so the negative is not a truncation
artefact) — **no open
PR holds any of the four paths**, nor `scripts/ts-parse.mjs`. Firing
control: the map does
carry `scripts/` rows (objectstack-ai#19024, objectstack-ai#18414, objectstack-ai#19153, objectstack-ai#18723). Dark control:
`scripts/zzz-no-such-file.mjs` has no row.

⚠️ Also in the regenerated artefacts and NOT caused by this change: the
unenforced
corpus-scale block re-stamps its date and sha and moves 573 to 576
tracked sources, because
`main` moved since it was last written.

## Acceptance notes — noted, not filed

- Triage's **route ①** (stop collapsing the declared type text at
storage time) stays
available and would let such a receiver be PLACED rather than
declared-undefended. It is
a much wider change — the collapsed text is also the display text, the
index-lookup input
and the ledger key — and it is not what the sequenced hunk asked for.
Successor: any
later card that revisits the census's stored type text. Not a defect in
the tree.
- The same defect class at the transpile door (`transpileChecked` on a
lifted snippet) was
already filed as its own card by the `domain:spec#3` seat, with the
boundary carried
  over. ⛔ No second card from here.
- No path literal was added to `scripts/tenant-audit-census.mjs` or
`ts-parse.mjs`;
  `check:watch-hint-literal` exits 0. Successor: none needed.

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

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…ured citations, and fold in the two-code sdui-parser port (objectstack-ai#17645) (objectstack-ai#19398)

Fixes objectstack-ai#17429
Part of objectstack-ai#17645 — that card's deliverable (the two-code port) lands here;
the director seat ends card 17645 with the landing record after the
merge (a seat-written record, not a closing reference). The card's
take-over from the offline `domain:devx` seat is the maintainer's
instruction (「18723 os-try-charles 已下班,你也接手」), recorded on the card; the
carrier reader's cross-author row (C9) cannot be cleared without the
offline holder's own `Release:`, so the card is carried as `Part of`,
not as a closing reference.

Clause-②: yes

Corrected from `no` under ruling batch objectstack-ai#201 item 1 (maintainer 「201 A」,
record 5753263180 on objectstack-ai#17645): this PR now also carries objectstack-ai#17645's two-code
sdui-parser port, so the save gate's diagnostic set grows
(`inert-quick-add`, `member-type-mismatch`); the pin bump and the 27
re-measured citations alone moved no accept set. Both landed on this
branch: the fold-in commit `6ef8344645` and the citation-spelling fix
`fc25a5925d` (the head); the lockstep record needed no re-recording (see
the fold-in section).

(Pin-bump half, as originally declared:) Declared from the diff, not
from the card: nothing in the pin bump adds, removes or renames an
authorable key. The two `packages/spec` edits with a published face are
`.describe()` SENTENCES (`Dataset` measure `format`, `FormField.span`);
`check:authorable-surface` and `check:api-surface` both report their
artefacts current with no regeneration, and `pnpm --filter
@objectstack/spec check:generated` reports all 15 artefacts up to date
after `gen:docs` and `gen:migration-registry` ran. The same authored
metadata is accepted and refused as before, so neither arm of the closed
pair applies.

## What moved

`.objectui-sha`: `53ded82bf7a494f54e344e19099dbf00854b8694` →
`87af769e9a3ee28ace099fdd653d3ebd79fe82e2` (objectui `origin/main` tip
read 2026-09-20; `cfcc17d9dd04` — the revision PR objectstack-ai#18723 ported from —
is an ancestor of it, `git merge-base --is-ancestor` exit 0).

Produced by `scripts/bump-objectui.sh --no-commit 87af769e9a3e`, never
by hand. Everything the pin travels with:

| Artefact | How |
|---|---|
| `.changeset/console-87af769e9a3e.md` | the bump script's own digest —
584 releasing changesets of 891 added across 1156 non-merge commits,
`@objectstack/console: minor`, 98 declared-breaking entries listed |
| `sdui.manifest.json` + `scripts/sdui-manifest.record.json` | `node
scripts/gen-sdui-manifest-node.mjs`. The manifest is BYTE-IDENTICAL
(sha256 `49211fee7792…`, 57 components): the pinned commit declares the
same published `@object-ui` `17.6.0` the old pin did, so only the
provenance record moves. `check:sdui-manifest` exit 0 |
| `packages/sdui-parser/objectui-lockstep.json` | `pnpm
gen:sdui-lockstep` against a worktree parked at the new pin (`--update`
refuses off the pin). See the RED below |
| 27 `.objectui-sha` citations under `packages/spec/src/**` | each
re-MEASURED against the new pin's tree — see the table |
| `content/docs/references/ui/{dataset,view}.mdx` | `gen:docs`, from the
two corrected describes |
| `.changeset/17429-pin-bump-describe-corrections.md` |
`@objectstack/spec: patch` for those two sentences |

Lockfile / override state: `pnpm install` in this worktree produced no
`pnpm-lock.yaml` change, and the bump script writes none — the console
SPA is vendored as a built `dist/`, not as a dependency, so this bump
moves no dependency edge in this repo.

## Fold-in: the two-code sdui-parser port (ruling batch objectstack-ai#201 item 1,
maintainer 「201 A」)

Commit `6ef8344645` brings PR objectstack-ai#18723's port onto this branch
**verbatim**. Eight files, each byte-identical to the blob on
`claude/issue-17645-sdui-lockstep-port` (`git hash-object` here vs `git
rev-parse claude/issue-17645-sdui-lockstep-port:PATH` there — all eight
SAME, no hunk rewritten):

| path | blob |
|---|---|
| `packages/sdui-parser/src/kanban-quick-add.ts` | `20f9f74b9f05` |
| `packages/sdui-parser/src/validate.ts` | `156a264073f7` |
| `packages/sdui-parser/src/types.ts` | `567bc1259bea` |
| `packages/sdui-parser/src/index.ts` | `2ea35795b9cf` |
| `packages/sdui-parser/src/codegen.ts` | `76a7d654f1fd` |
| `packages/sdui-parser/src/__tests__/inert-quick-add.test.ts` |
`723cad21333a` |
| `packages/sdui-parser/src/__tests__/member-type-mismatch.test.ts` |
`4b87cadaa067` |
| `.changeset/17645-sdui-parser-lockstep-port.md` | `7c171fddfc43` |

Deliberately NOT brought from that branch: `.objectui-sha`,
`scripts/sdui-manifest.record.json` (both already this branch's, at
`87af769e9a3e`) and `packages/sdui-parser/objectui-lockstep.json`.

### The lockstep record was neither text-merged nor re-recorded — and
needed neither

This branch already carries the record **taken at the live pin** in the
pin-bump round: `objectui.rev` = `recordedAgainstPin` =
`87af769e9a3ee28ace099fdd653d3ebd79fe82e2`, 26 diagnostic codes
including `inert-quick-add` and `member-type-mismatch`. `--update`
re-reads objectui's side only, so a re-record here would have rewritten
the same bytes. Attempted anyway, and it REFUSED, which is the generator
working: `pnpm gen:sdui-lockstep` exit 1, `[head-off-pin] the objectui
checkout is at c2f0f48329de, and .objectui-sha names 87af769e9a3e`;
record file byte-unchanged across the attempt (`e0a6988b1c9a` before and
after). The remedy it prints — parking the sibling checkout on the pin —
was not taken: that is a git write inside a read-only checkout this
container shares with other agents, and the record it would produce is
the one already committed.

**Result — `check:sdui-lockstep` exit 0** on the head: `OK — this copy
is byte-identical to objectui@87af769e9a3e over 214 grammar line(s)
[blob 0131f27cf86d] and agrees on all 26 diagnostic code(s), across 8
non-test source(s)`. `--self-test` exit 0 alongside.
`check:sdui-manifest` exit 0 (manifest intact, sha256 `49211fee7792`,
recorded at the live pin).

### The type-check red PR objectstack-ai#18723 carried is closed by the pin, not by a
code change

The red was `check:objectui-pin-citations` inside the `Type Check ·
source gates` job. It reds exactly on the two heads of objectstack-ai#18723 that
claimed `.objectui-sha` = `cfcc17d9dd04`; on this branch, with the live
pin, the gate is exit 0 (27 asserting citations match, 48 historical
recorded and not checked, 7 anchor content assertions verified).
Reverse-verified here through `scripts/ablation-replace.mjs`: rewriting
`.objectui-sha` to `cfcc17d9dd04` turns the gate RED with five
anchor-content mismatches; restored and proven (blob back to
`035b6937d641`, `git diff HEAD` empty). So the fix is the fold direction
itself: the port lands on the branch whose citations are already
re-measured at the pin. No edit was made inside
`packages/sdui-parser/**` for it, and no `@ts-ignore` or `any` was added
anywhere; `tsc --listFiles` confirms both new test files are inside the
package program.

### Ported from `cfcc17d9`, re-verified against the LIVE pin

objectui DID move `packages/sdui-parser` between `cfcc17d9dd04` and
`87af769e9a3e`: one unrelated feature, objectui#6771's retired `body`
child-list dialect (`body-dialect.ts` +99, `index.ts` +1, `validate.ts`
+31/−7, its own test). It touches neither ported code and stamps only
`not-a-container` and `unknown-prop`, both already in the 26-code set —
which is why `26 = 26` holds at the live pin with it unported. Against
objectui at `87af769e9a3e` the whole non-comment divergence of the five
ported sources is four items, three pre-existing (`ValidationResult` vs
`ManifestValidationResult`; the `code: 'inert-quick-add'` inline literal
forced by `check:dispatcher-error-vocabulary`, pinned equal to the
constant by a test row; `provenance.ts` and `assertFullyLoaded` /
`RegistryConfigLike.lazy` absent here) and one new since `cfcc17d9`
(`body-dialect.ts` absent here — see Acceptance notes). The port
strictly REDUCES divergence in every file it touches: `validate.ts` 146
→ 73, `index.ts` 114 → 92, `codegen.ts` 30 → 3, `types.ts` 96 → 85 diff
lines vs objectui at the live pin.

### Ablations — re-run on this tree

Three, each through `scripts/ablation-replace.mjs` (anchor ×1, mutation
proven on disk by anchor count and blob change, restore proven by `blob
== HEAD` AND an empty `git diff HEAD`): (1) `checkKanbanQuickAdd` stamps
nothing — **5 failed / 8 passed** of 13; (2) the validator's member
check reads no declared arms — **7 failed / 14 passed** of 21; (3) the
codegen narrows no member type — **3 failed / 18 passed** of 21.
Baselines 13 and 21 passed; in each leg the failures are the SUBJECT
rows and every control row stays green. Both suites import
`../index.js`, a relative intra-package specifier, so no rebuild is in
the resolution path.

## Citation spelling fix (`fc25a5925d`)

CI's `Lint & Repo Gates` went red on `check-issue-citations` at
`6ef8344645`: two issue-number citations on docblock lines this branch
rewrote. `packages/spec/src/ui/view.zod.ts` — `objectstack#17328`
respelled bare `objectstack-ai#17328` (the issue exists, closed completed by merged PR
objectstack-ai#17671; the gate probes only unqualified citations, so the
this-repo-qualified spelling was judged against a board never asked for
it). `packages/spec/src/ui/component.zod.ts` — `objectstack-ai#10274` measured
**deleted** by `--probe-cause` (gone from the board and 404 on the web
endpoint, not transferred); no replacement guessed — the sentence now
states in prose that the number no longer resolves and names the live
record (that block plus `check:objectui-pin-citations`). `node
scripts/check-issue-citations.mjs` exit 0 at the head (31 judged, 0
findings); `check:generated` unchanged (TSDoc, not `.describe()`).

## Post-landing smoke evidence (triage's condition, issue comment
5690507317)

```
bash scripts/pm/os-verify-lock.sh -c 'bash scripts/build-console.sh'
  os-verify-lock: VERDICT command-exit 0 · held the lock 653s (10m53s) · waited 0s
  ✓ 8786 modules transformed.
  ✓ Bundle canary 'import/jobs' present — framework client is in the bundle.
  ✓ Console bundle carries THIS tree's @objectstack/spec, and only it.
  ✓ @objectstack/console dist ready (60228 KB) from objectui@87af769e9a3e
```

| Reading | Value |
|---|---|
| exit code | 0 |
| `packages/console/dist` size | 60228 KB — 51,702,779 bytes, 3,285
files |
| content hash | `sha256
259d2dfecaa12507fd3166a3f91c66c52581990e5ccac3899757027fa6ac108d` over
`find … -type f \| sort \| xargs sha256sum \| sha256sum` |
| provenance stamp | `packages/console/dist/.objectui-sha` =
`87af769e9a3ee28ace099fdd653d3ebd79fe82e2` |
| `pnpm check:console-sha` | exit 0 — "Console dist matches the objectui
pin" |

The console build is GREEN at the new pin. 584 commits of objectui
change came with it and none of them broke it.

## The citations — re-MEASURED, never re-pointed

27 asserting citations, all of them stale at the new pin. Each was
re-derived by reading objectui at BOTH shas and comparing content, not
by arithmetic on a line number. `pnpm --filter @objectstack/spec
check:objectui-pin-citations` exit 0; `--verify-anchors` exit 0 with 7
of 7 content assertions verified against objectui at `87af769e9`.

### Corpus-absence records (7 citations + 6 generated copies)

These cite a zero-hit count over the pinned tree, not a file:line.
Re-counted with `git grep -F -o` at both shas; the METHOD was calibrated
first by reproducing the old pin's own numbers exactly (6409 tracked
files, `useState` 2304).

| Record | Cited | Reads at `87af769e9` | Verdict |
|---|---|---|---|
| `18.kernel-health-check-and-hot-reload-durations-unit-in-key.ts:48` |
13 exports of `plugin-lifecycle-advanced.zod.ts` + `debounceDelay` = 0 /
6409 files; controls `objectstack` 10171, `@objectstack/spec` 3479 | all
still 0 / **8228** files; controls **12966**, **4997** | unchanged
verdict, counts re-measured |
| `18.kernel-runtime-config-timeout-unit-in-key.ts:45` |
`resourceLimits.timeout` 0; controls `timeout` 832, `RuntimeConfig` 236,
`resourceLimits` 2 | 0; **1075**, **240**, 2 — and both `resourceLimits`
hits are still prose in `packages/app-shell` | unchanged |
| `18.logging-durations-unit-in-key.ts:48` | 4 names 0; controls
`useState` 2304, `timeout` **702** | 0; **2383**, **1075** | unchanged —
⚠️ but `timeout` 702 was WRONG AT THE PIN IT NAMED: that corpus reads
832. An error since written, found only by re-measuring, corrected here
|
| `18.system-metrics-jsdoc-durations-unit-in-key.ts:70` | 6 def names +
2 keys 0; controls `window` 2710, `timeout` 832, `period` 160,
`interval` 156, `metrics` 301 | all 0; **3464**, **1075**, **170**,
**170**, **324** | unchanged |
| `18.system-tracing-otel-exporter-durations-unit-in-key.ts:66` | 37
exports of `tracing.zod.ts` + 4 key names 0; `Span` 404 / `SpanSchema`
40 unrelated; controls 10171, 3479 | all 0; **486** / **53**; **12966**,
**4997** | unchanged |
| `18.tenant-schema-cache-ttl-unit-in-key.ts:31` | `schemaCacheTTL` 0;
controls `TTL` 112, `tenant` 819 | 0; **156**, **976** | unchanged |
| `migrations/registry.ts` ×6 | the generated concatenation of the six
above | regenerated with `pnpm --filter @objectstack/spec
gen:migration-registry` | follows its entries |

### Read-point records (20 citations + 7 content assertions)

| Record | Anchor, as cited | At `87af769e9` | Verdict |
|---|---|---|---|
| `data/api-methods-batch-conformance.test.ts:64` |
`ObjectGrid.tsx:3538-3553` | `:3940-3955` | MOVED, span byte-identical
(`git hash-object` `6133933199…` both sides) |
| | `hooks/useBulkExecutor.ts:284-289` | `:298-303` | MOVED, span
byte-identical (`0108334833…`); ⚠️ the FILE is no longer byte-identical
(+36/-22), so the record's old identity argument is gone and the span
was re-read |
| `ui/component.zod.ts` tab-item `icon` + `ui/component.test.ts:376` |
`containers.tsx:730-736` | `:853-859` | MOVED, 7 lines byte-identical |
| | `containers.tsx:789` (registration `items` input) | `:912` | MOVED
**and the LINE CHANGED** — it lost `label: 'Tabs'`, gained `of:
'object'` and a longer description; the member list the record cites is
unchanged |
| `ui/component.zod.ts` accordion-item `icon` + `component.test.ts:286`
| `containers.tsx:919-925` | `:1069-1075` | MOVED, byte-identical |
| | `containers.tsx:966` | `:1116` | MOVED **and the LINE CHANGED**,
same way |
| `ui/component.zod.ts` button `icon` + `component.test.ts:3489` |
`form/button.tsx:43`, `:72`, `:74` | unchanged | UNCHANGED |
| | `form/button.tsx:85-102` (inputs), `:103-107` (defaultProps) |
`:85-97`, `:98-102` | MOVED **and SHRANK** — every input dropped its
`label` and `defaultValue`; the four input names and the ABSENCE of an
`icon` input, which is what the record asserts, both hold |
| | `action/resolve-icon.ts:129-132` (`resolveIcon`) | `:322-328` |
MOVED **and REWRITTEN** — the tail no longer indexes `lucide-react`'s
`icons` record; it asks `recordIconName` and hands the pair to
`lazyIconComponent`. Accept/reject behaviour unchanged |
| | `resolve-icon.ts:117-120`, `:100-105`, `:90-92` | `:302-305`,
`:153-158`, `:143-145` | MOVED, byte-identical |
| | `lib/lazy-icon.tsx:66-92` | `:98-124` | MOVED, byte-identical |
| `ui/component.zod.ts` object-metric `icon` |
`plugin-dashboard/src/index.tsx:204` | `:237` (registration now opens at
`:227`) | MOVED **and the LINE CHANGED** — the input's `label: 'Icon
(Lucide name)'` is GONE; the key is still published, now as a bare `{
name: 'icon', type: 'string' }` |
| | `ObjectMetricWidget.tsx:142` / `:474`; `MetricWidget.tsx:312-321` /
`:373-382`; `lazy-icon.tsx:66-80` | `:174` / `:483`; `:351-360` /
`:412-421`; `:98-112` | MOVED, byte-identical |
| `ui/component.zod.ts` kanban `limit` ×2 + `component.test.ts:3360` |
`ObjectKanban.tsx:264` (`$top: schema.limit ?? DEFAULT_KANBAN_LIMIT`) |
`:676` (`$top: resolveRowLimit(schema.limit, DEFAULT_KANBAN_LIMIT)`) |
MOVED **and REWRITTEN** — objectui#9925 put a refusal in front of it (a
contract-refused cap is dropped and reported at `:553`). The pinned
fact, that `limit` lowers into the query's top-level `$top`, holds |
| | `ObjectKanban.tsx:71`; `plugin-kanban/src/index.tsx:395-398` |
`:84`; `:447-450` | MOVED, byte-identical |
| | `ObjectKanban.tsx:143` + `plugin-kanban/src/types.ts:134`
(`KanbanSchema.limit?: number`) | ⚠️ **VANISHED** | `KanbanSchema` was
RETIRED at this pin (maintainer ruling 2026-09-09) and `types.ts`
declares the member no more. Re-derived rather than re-pointed: the
published twin is `ObjectKanbanSchema`, imported at
`ObjectKanban.tsx:10`, declaring `limit?: number` at
`packages/types/src/objectql.ts:3735` |
| | `ElementDataSourceGate.tsx:236-241` | `:316-331` | MOVED **and
EXTENDED** — the branch gained objectui#9899's
presence-is-not-authorship test and a `describeDisplacedRowLimit` report
|
| `ui/component.zod.ts` kanban `quickAdd` + `component.test.ts:3427` |
`ObjectKanban.tsx:931`; `plugin-kanban/src/index.tsx:196`;
`KanbanImpl.tsx:355` / `:368` | `:1563`; `:313`; `:621` / `:634` |
MOVED, byte-identical. The absence re-counted at this pin: `quickAdd` /
`onQuickAdd` 0 each in `ObjectKanban.tsx`, against 11 (was 6) for the
sibling `onCardClick` |
| `ui/dataset.zod.ts:207` measure `format` |
`dataset-format.ts:185-198`, `date-display.ts:131-164` / `:117`, `:195`
| `:229-264` (routed at `:370`), `:198-233` / `:152`, `:260-262` | ⚠️
**SUBSTANCE REVERSED** — see below |
| `ui/view.zod.ts:2945` `FormField.span` | the widest-tier-only class
claim at `53ded82bf7` | `spanLadderFor`, `form/form.tsx:204-231` | ⚠️
**SUBSTANCE REVERSED** — see below |
| `ui/view.zod.ts` `ListMapConfig`, 7 content assertions |
`ListView.tsx:113` / `:67`; `ObjectView.tsx:1381`; `objectql.zod.ts:562`
/ `:313`; `ObjectMap.tsx:373` / `:378` | `:146` / `:85`; `:1764`;
`:1574` / `:734`; `:385` / `:390` | all 7 MOVED; ⚠️ one LOST ITS SYMBOL
— `FLAT_MAP_CONFIG_KEYS` became the total `FLAT_MAP_CONFIG_SPELLING` map
(objectui#9950), so `style` now DOES reach the flat product, as
`mapStyle`. The quote was re-read, not re-pointed |

### The two records whose SUBSTANCE the range reverses

Both are claims about what the SHIPPED renderer does, so the pin is what
dates them, and both were re-stated from the new tree. Each also carried
a published `.describe()` saying the same now-false thing, which this
bump is what falsifies — so both sentences are corrected here and
`content/docs/references/ui/{dataset,view}.mdx` regenerated from them.
That is the `@objectstack/spec: patch` changeset in this PR.

- **`Dataset` measure `format`** said "a datetime value ignores it".
objectui#8352 is inside the range: `formatMeasureDate`'s datetime arm
now SELECTS a formatter — `relative` to `formatRelativeDate` (`:260`),
`short` to `formatDateTime(v, { locale, style: 'compact' })` (`:261`),
everything else to the bare `formatDateTime(v, { locale })` (`:262`). A
date PATTERN is still ignored on both arms, which is the half that
survives.
- **`FormField.span`** said only the widest container-query tier's class
is emitted, so a `'full'` field was one cell of two at 720px (objectstack-ai#17328).
objectui#9244 / objectui#9253 (`bd09957380`) are inside the range:
`spanLadderFor` emits one clamped col-span class per multi-column tier.
⭐ The previous revision of this block asked in writing to be re-read at
the bump that absorbed it; this is that re-read. The `'auto'` half —
textarea, markdown, html, richtext and repeater — re-measured UNCHANGED.

## Gates

Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (122 commands for this 19-path, 757-line
change set). Run locally, exit codes captured before any pipe:

```
pnpm check:sdui-lockstep                                        EXIT 0   (after the fold-in: 26 = 26 codes, grammar region byte-identical)
pnpm --filter @objectstack/spec check:objectui-pin-citations    EXIT 0   (27 asserting citations match, 7/7 anchors verified)
  … --verify-anchors                                            EXIT 0
node scripts/check-sdui-manifest.mjs                            EXIT 0
pnpm check:objectui-bump                                        EXIT 0
pnpm check:objectui-changeset                                   EXIT 0
pnpm check:console-sha                                          EXIT 0
pnpm --filter @objectstack/spec check:generated                 EXIT 0   (15/15 artefacts current)
MANIFEST=… check:react-declaration-parity                       EXIT 0   (reading unmoved: the manifest is byte-identical)
node scripts/check-adr-0087-registration.mjs --base origin/main EXIT 0
node scripts/check-changeset-no-major.mjs --base origin/main    EXIT 0
node scripts/check-empty-changeset.mjs --base origin/main       EXIT 0
pnpm lint                                                       EXIT 0   (repo-wide, eslint . --no-inline-config)
pnpm --filter @objectstack/spec test                            EXIT 0   (503 files, 14710 tests)
pnpm --filter @objectstack/spec --filter @objectstack/sdui-parser typecheck   EXIT 0
```

plus 27 further derived families, every one exit 0: `check:nul-bytes`,
`check:merge-driver`, `check:watch-hint-literal`,
`check:comment-mask-adoption`, `check:comment-mask-corpus`,
`check:doc-frontmatter`, `check:docs-section-name`,
`check:scripts-symbol-anchors`, `check:spec-docblock-symbol-anchors`,
`check:ci-filter-parity`, `check:keyed-text-bounds`,
`check:undeclared-dep-imports`, `check:section-landing-index`,
`check:liveness`, `check:duration-unit-keys`, `check:llms-txt`,
`check:spec-changes`, `check:upgrade-guide`, `check:strictness-ledger`,
`check:export-origins`, `check:api-surface`, `check:authorable-surface`,
`check:docs`, `check:doc-anchors`, `check:docs-spec-enumerations`,
`check:quick-reference-counts`, `check:corpus-claim-drift`,
`check:changeset-gate-self-tests`, `check:pm-widening-tells`,
`check:published-files`, and both changeset self-tests.

The rest of the 122 are left to CI, which runs the whole farm.
`origin/main` moved 7 commits while this was built and touches NONE of
this diff's 19 paths, so no merge was taken; the queue's rebuilt
generation is what validates the join.

## Acceptance notes

- `check-issue-citations` blind spot (measured on objectstack-ai#17328 at
`6ef8344645`): a citation qualified with this repository's own name
(`objectstack#N` or `owner/repo#N`) is recognised as naming THIS repo
and resolved against the board, but the board is only asked for
unqualified citations and `--probe-cause`'s guard is `!cite.qualifier`
too — a live issue reads `allocated-but-absent` and NOT MEASURED under
`--probe-cause`. Not fixed here (the gate is not this PR's surface);
recorded for the next author of `scripts/check-issue-citations.mjs`.
- objectui's `packages/sdui-parser/body-dialect.ts` (objectui#6771,
landed between `cfcc17d9dd04` and the live pin) has no counterpart in
this copy: for an authored `body` key the two copies stamp different
codes (`not-a-container` under a non-container there, the bare
`unknown-prop` here), invisible to `check:sdui-lockstep`'s code-SET
comparison because both codes are already in the 26-set. Both severities
are `warning`, so `compile().ok` and the save verdict are unchanged. A
third code path the ruling does not name; carrier: the next sdui-parser
port round.
- `provenance.ts` and `assertFullyLoaded` / `RegistryConfigLike.lazy`
remain absent from this copy (zero diagnostic codes; objectstack-ai#18723's acceptance
notes already record `provenance.ts`). `pnpm gen:sdui-lockstep` is
unrunnable in this container because the shared sibling checkout sits
off the pin and the generator's remedy is a git write in someone else's
tree; it cost nothing this round and will bite the next pin bump, which
must park its own checkout.

- `.changeset/18516-span-auto-wide-types.md` (pending, not yet released)
describes the `FormField.span` describe text this PR corrects, including
"At this pin only the widest tier's class is emitted".
`check:empty-changeset` refuses any diff that MODIFIES a changeset
present at the merge base, so it is deliberately left untouched here and
reported instead.
- The `element:button` `icon` `.describe()` still says the renderer
resolves through `lucide-react`'s `icons` map. At the new pin that is
one hop indirect (`recordIconName` + `lazyIconComponent`) but the glyph
set and the accepted spellings are unchanged, so it was left as written
rather than churned.

Original author: the director seat's `os-dev` rounds (pin bump; fold-in
`6ef8344645`; citation fix `fc25a5925d`),
session_012GcsUbuqFGBibkEDMRC1eE; body maintained by the director seat,
`session_012GcsUbuqFGBibkEDMRC1eE`.


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

---------

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

Projects

None yet

3 participants