Skip to content

fix(sdui-parser): an html page literal of the wrong type is a compile error, not a warning - #21678

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21671-literal-type-mismatch-error
Oct 4, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21671-literal-type-mismatch-error

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #21671
Clause-②: no (narrowing)

What changed

The html-tier compiler (@objectstack/sdui-parser compile) now grades every type-mismatch as error, not only the ones whose input declares an enum arm. aggregate="count" on an object-metric (repo-root sdui.manifest.json declares aggregate as type: "object") now fails the compile, so os build fails on it. Before, it compiled ok with one warning, the build stayed green, and the tile drew no number. The diagnostic code and message are unchanged. No new code and no new gate.

The mechanism, measured

The brief assumed that literals and expressions both reach checkType, and that a signal would have to be passed in to tell them apart. Measured on 93a54b87e7, that is not how it works:

  • validateTree (packages/sdui-parser/src/validate.ts) sends a value that isExpr matches (the parser's deferred $expr marker) to inert-expression, which is a warning. It calls checkType only in the else branch. So every value that reaches checkType is already a literal: a quoted attribute (a string), a bare attribute (true), or a braced value that interpretBrace materialized in full.
  • interpretBrace is all-or-nothing. Probed: ["a", foo], {"a": foo} and {a: 1, b: x.y} each become ONE $expr marker; [1,2] and {function: "count"} materialize in full. So a container never reaches the type check with an expression inside it.

So the expression case is already separate before checkType runs, and no new signal is needed. The change is the severity, plus a restated header that names the second certain fact (a literal's coarse type) next to the enum's closed list. A braced expression still gets the inert-expression warning, and a test pins that.

checkMemberTypes follows the same rule (the brief left this to measurement). Its members come from a container that was materialized in full, so each member is a literal too, and a member that no declared arm accepts is just as certain a mismatch. Its header already said "Severity mirrors checkType's rule", and that stays true. member-type-mismatch is now error.

Unchanged on purpose: the single-arm invalid-enum diagnostic is byte-identical, severity included (the existing pin in union-arm-type-mismatch.test.ts passes untouched).

Census (first step): every stored html page source, compiled against the committed sdui.manifest.json

It was run with compile from @objectstack/sdui-parser (source), not with grep. The page modules were imported and each kind: 'html' export's source was compiled (capability-map.page.ts interpolates, so a regex would have read a different string). Every fenced block in skills/**/*.md and content/docs/ui/*.mdx that has a lowercase tag was compiled too. A positive control (aggregate="count") was compiled in the same run.

source literal handed to a non-string input verdict
examples/app-showcase/src/ui/pages/command-center-jsx.page.ts (CommandCenterJsxPage) none (0 diagnostics) clean, nothing to fix
examples/app-showcase/src/ui/pages/capability-map.page.ts (CapabilityMapPage) none (0 diagnostics) clean, nothing to fix
examples/app-showcase/src/ui/pages/start-here.page.ts (StartHerePage) none (0 diagnostics) clean, nothing to fix
skills/objectstack-ui/rules/pages.md:154 block (its line 160 is the object-metric example) none: line 160 reads aggregate={{"function":"count"}}, so PR #21667's fix is confirmed on origin/main clean, nothing to fix
hotcrm no kind: 'html' page in this repo (both packages/metadata/src/__fixtures__/hotcrm-*.artifact.json have 0) not applicable
other fenced blocks in skills/** and content/docs/ui/** React-tier or non-page code (each fails at no-root or forbidden-tag, which shows they are not html-tier sources) not applicable
control: aggregate="count" before: ok=true, [warning] type-mismatch; after: ok=false, [error] type-mismatch the census can see the case

There are zero writers to fix, so no example or skill file changes in this PR.

Pins (packages/sdui-parser/src/__tests__/literal-type-mismatch-error.test.ts)

The inputs are copied verbatim from the tracked sdui.manifest.json and written inline, so the test reads nothing outside its package.

  • aggregate="count": exactly one { severity: 'error', code: 'type-mismatch', message: 'object-metric prop "aggregate" expected an object' } (the real message has the tag in angle brackets), and ok === false.
  • aggregate={{"function":"count"}}: zero diagnostics, ok === true.
  • A string literal on a number (object-kanban limit), a boolean (invert) and an array (filter) input: each gives one error type-mismatch, and ok === false.
  • An expression handed to an object input (aggregate={count}), and a container that holds an expression: each gives exactly one warning inert-expression, no type-mismatch, and ok === true.

Three existing pins described the old rule, and they were updated: the non-enum union case and the single string-arm case in union-arm-type-mismatch.test.ts, and the member severity in member-type-mismatch.test.ts. Their codes and messages are unchanged. Only severity, ok, and the wording that called these "byte-identical" were edited.

os build probe (the html-tier path through packages/cli/src/utils/sdui-manifest.ts)

A scratch project with one kind: 'html' page, the committed sdui.manifest.json copied beside its config, and the CLI run from source (bin/run-dev.js build):

page source before (severity reverted, rebuilt) after (this PR)
aggregate="count" exit 0, a warning that the aggregate prop expected an object, Build complete exit 1, Author-time rules failed (1 issue), a failure that the aggregate prop expected an object
aggregate={{"function":"count"}} not run exit 0, Build complete

The before leg is a one-off ablation run from the committed fix. It used scripts/ablation-replace.mjs (anchor hit, 1 marker on disk), then pnpm --filter @objectstack/sdui-parser build, then ablation-dist-preflight.mjs (marker present in dist/, exit 0). After the probe it was restored with git checkout HEAD --: git diff HEAD was empty and the blob matched HEAD (86cc5784). The package was rebuilt, the --absent preflight passed for both readings, and the probe was re-run with exit 1. No permanent test file was left behind.

Tests run (at 79df1db84f)

  • pnpm --filter @objectstack/sdui-parser test: 14 files, 225 tests passed. typecheck: exit 0.
  • Downstream consumers, after rebuilding sdui-parser (dist checked: 0 copies of the old ternary left). pnpm --filter @objectstack/lint exec vitest run: 119 files, 5627 tests passed. pnpm --filter @objectstack/metadata-protocol exec vitest run: 209 files passed and 3 skipped, 3463 tests passed. CLI unit tier, limited to the 4 files that compile html pages (src/utils/sdui-manifest.test.ts, test/validate-build-gate-parity.test.ts, test/platform-page-i18n-parity.test.ts, test/i18n-section-coverage.test.ts): 129 tests passed. The rest of the CLI unit tier and its integration tier are left to CI.
  • node scripts/pm/dispatch-gates.mjs --commands gave 63 derived commands. 61 exited 0, including check:sdui-lockstep, check:nul-bytes, check:cross-package-test-inputs, check-adr-0087-registration and check-changeset-no-major. NOT MEASURED: check:dual-build-cjs-loads: PREREQUISITE NOT MET, because embedder-openai and service-cluster-redis have no dist/ (packages this diff does not touch). NOT MEASURED: check:type-check-debt: it is a whole-tree tsc ratchet and hit the 240s local timeout. CI runs both.
  • eslint was run on only the 4 changed .ts files, with --no-inline-config --format json: 4 files, 0 errors, 0 warnings. The repo config has no parserOptions.project (type-aware linting is off), so this diff cannot change the lint result of any file it does not touch. CI runs the full pnpm lint.

Changeset

.changeset/21671-html-literal-type-mismatch-error.md: @objectstack/sdui-parser minor. Clause-② conflict for the seat to resolve: the claim states Clause-②: no, and this body carries that line verbatim. The changeset declares Clause-②: no (narrowing), because a page that used to compile (and save, where the host has a component manifest) is now refused. Earlier PRs treated a new refusal at an authoring door as an accept-set narrowing (21459, 20827). The changeset therefore carries the **BREAKING** header, a minor bump under the launch-window convention, and an ADR-0087 not-required (no-migration-prescription) disposition: no key, declaration or stored shape moves. check-adr-0087-registration and check-changeset-no-major both pass, whether the body line has the arm or not (both were simulated locally).

objectui lockstep (declared, not acted on)

objectui has its own copy of this validator, packages/sdui-parser/src/validate.ts, at the .objectui-sha pin ab187972. It still has the old ternary in both checkMemberTypes (:418) and checkType (:465). After this PR the two copies agree on codes, messages and the accepted grammar, and differ only in severity. check:sdui-lockstep compares grammar, codes and the containment predicate, not severity, so it passes. This repo's copy is the stricter one (save gate and os build). The dangerous direction, a page that saves clean and then renders inert, cannot come from this difference. The lockstep header in validate.ts now records this lead. The port belongs to objectui's lane, and this PR does not write to objectui.

Acceptance notes

  • The new severity covers every literal mismatch, including a number literal handed to a string input (label={42}). That was the narrower reading in the triage title ("a string literal against a declared non-string input"). The claim and the brief specify the general rule ("a literal whose coarse type no declared arm accepts"), and the measurement shows that every value at this point is a literal, so the general rule is the one implemented. The census found no writers of either form in the repo.

Generated by Claude Code

claude added 2 commits October 4, 2026 04:02
…h error

Every value reaching checkType is a literal (a braced value the parser cannot
materialize is diverted to inert-expression first), so its coarse type is
final at compile time, as certain as an enum's closed list. aggregate="count"
on object-metric now fails the compile instead of passing os build with a
warning. checkMemberTypes follows the same rule: members of a materialized
literal are literals too.

Claude-Session: https://claude.ai/code/session_01VDtqoecgES7ScQYGbFVDRv
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/m label Oct 4, 2026
@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

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

Coarse fallback — 3 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 eed2dee481126b6655cfb4a809099aa885c8e52b → packageMentionDocs.

Which tree this was computed on

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

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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 79df1db84fb82c96586f224a63c40d7c065c71eb
Local-runs: none

① Changeset .changeset/21671-html-literal-type-mismatch-error.md

  • Package @objectstack/sdui-parser — TRUE. The only package the diff touches (packages/sdui-parser/src/validate.ts + its tests); published (17.6.0 per the claim; no private: true).
  • Bump minor with the **BREAKING** banner, not patch — TRUE. A page source that compiled ok (so saved at the runtime gate and passed os build) is now refused: an accept-set narrowing at an authoring door. AGENTS.md step 3: (narrowing) is BREAKING; ADR-0087 "launch-window exemption" (amended [Decision] 两条裁决援引同一个 launch-window convention,却给出相反的 changeset 等级(minor vs major)—— 退役一个可写键到底发哪一级? #18003) and check-changeset-no-major.mjs: pre-GA a breaking change ships minor with the banner and the disposition as the carriers, major is refused. Precedent on origin/main with the identical shape: .changeset/21620-container-sibling-expansion-name.md (PR fix(metadata-protocol): the save door refuses a view container saved under a name another stored container of the same object expands to (#21620) #21637) and .changeset/21639-view-container-name-collision.md (PR fix(metadata-protocol): one collision predicate at the save door — a stored view container never takes a name already served from elsewhere (#21639, #21638) #21648), both minor + **BREAKING** + Clause-②: no (narrowing) + not-required (no-migration-prescription) for a save door refusing what it accepted. A patch would carry none of the breaking signals the gate reads and would misgrade the change.
  • Clause-② arm no (narrowing) — TRUE. no: the card widens no accept set and no public surface (no export, type, code or message changes; one severity constant per function). (narrowing): the accepted set of html page sources shrinks. breakingDeclaration() reads signals (2) banner and (4) narrowing arm from the changeset body, so the disposition is required and present.
  • ADR-0087 disposition not-required (no-migration-prescription) — TRUE. No authorable key, declaration, export or stored shape is removed, renamed or re-shaped; the refused values live inside a page source string that no ledger entry reads; the other categories are closed in the comment on facts (published, no registry id, not runtime-interface-only / type-surface-only). The body carries no X → Y rewrite (no arrow between code operands, REWRITE_RE), so the category is not self-contradicted; "write the value in the type the input declares" is authoring guidance for a value no conversion can supply, not a FROM → TO prescription. Same category as the two precedents above.
  • Wording for authors — TRUE. Summary line, "What changed", "Why" and "What an author now sees" match the code: validateTree sends $expr markers to inert-expression (warning) and only literals reach checkType; a bare attribute is true (parse.ts:151); os build refusal was probed by the dev (exit 1); os validate / os lint resolve the same manifest gate (packages/cli/src/commands/validate.ts, lint.ts via resolveJsxGateManifest); the save door (runtime-authoring-gate.ts:620–631) maps error severity and returns findings when !result.ok. Example inputs (aggregate, limit, invert, filter, fields) are real sdui.manifest.json inputs of the stated types. One minor nit, not blocking: "instead of compiling with a warning" describes the newly refused class; the enum-arm case already errored, which the body makes clear.
  • PR body Clause-②: no (bare) vs changeset no (narrowing) — the mismatch is NOT acceptable as a final state; the body must carry the arm. AGENTS.md step 3 defines ONE declaration line that the changeset body "also carries", and check-changeset-no-major.mjs reads the PR body as its ONE carrier: with bare no its level axis returns not-declared (stands down), so the PR as declared tells the level gate nothing about its breaking-ness, while the changeset tells the ADR-0087 gate the opposite. Both precedents (fix(metadata-protocol): the save door refuses a view container saved under a name another stored container of the same object expands to (#21620) #21637, fix(metadata-protocol): one collision predicate at the save door — a stored view container never takes a name already served from elsewhere (#21639, #21638) #21648) carried Clause-②: no (narrowing) in body and changeset alike. The dev copied the claim's line verbatim as .claude/agents/os-dev.md requires, so the discrepancy originates in the PM claim (5976312374), not in the diff. Required before enqueue (body-only edit by the seat; the head is unchanged, so this record stands): set the PR body line to Clause-②: no (narrowing). Dev open question 1: option A.

② Code vs. claims

  • validate.ts header matches the implemented behaviour — TRUE. checkType (head :491–:513) and checkMemberTypes (:435–:453) now return severity: 'error' unconditionally; the restated headers say exactly that and give the reason (two certain facts: enum closed list, literal coarse type).
  • Only literals reach checkType / checkMemberTypes — TRUE, confirmed in validateTree (head :233–:290): if (isExpr(value)) pushes the inert-expression warning; checkType runs only in the else branch, and checkMemberTypes only when checkType returned null (container kind accepted). A braced container holding an expression is one $expr marker (pinned: filter={["status", status]} → one inert-expression, ok === true). Expressions therefore never see type-mismatch; the triage's "expressions stay warning" holds by construction, and a test pins it.
  • invalid-enum byte-identical — TRUE. The single-enum-arm block (:496–:503) is outside the diff: same severity: 'error', same code, same message template; the existing pin (union-arm-type-mismatch.test.ts:138–148) is untouched. The header's wording change ("byte-identical" now scoped to invalid-enum) is accurate since the single-string-arm type-mismatch did change severity.
  • LOCKSTEP header note about objectui — TRUE. .objectui-sha is ab187972… at origin/main, at the claim ref and at head (unchanged). objectui packages/sdui-parser/src/validate.ts at that commit still has severity: arms.includes('enum') ? 'error' : 'warning' at :418 (member-type-mismatch) and :465 (type-mismatch), exactly as the report states; codes agree. scripts/check-sdui-lockstep.mjs does not compare severity (the word appears only in its self-test fixtures). No objectui path in the diff; the lead is declared in the header and PR body only, as the claim required.
  • Test edits are severity-only — TRUE. The three existing pins changed warning→error, ok true→false, and the prose that called the string-arm case "byte-identical"; codes and messages unchanged.

③ Scope vs. the triage ruling

  • Within the ruling's certainty argument — TRUE. The triage's operative sentence is "a literal's coarse type … is final at compile time, so it is certain in the same way the enum arm is. It becomes error." That argument does not distinguish a string literal on an object input from a number literal on a string input (label={42}): both are literals whose coarse type no declared arm accepts, and measurement shows every value at checkType is a literal. The title's "string literal against a non-string input" is the motivating instance, not a stated limit.
  • Within the claim's file surface — TRUE. The PM claim (5976312374) states the rule as "A literal whose coarse type no declared arm accepts becomes error" (general form), forbids a new code or gate (honoured: code type-mismatch, message unchanged), and explicitly delegates checkMemberTypes to "the dev's measured call, reported either way" — reported with the measurement (interpretBrace all-or-nothing, so every member is a literal). No triage call is needed; the generalisation relative to the triage title is recorded here as the reviewer's answer to dev open question 2: option A (keep the general rule).
  • Pins as the triage listed — TRUE, in literal-type-mismatch-error.test.ts: aggregate="count" → exactly one error type-mismatch with the stated message and ok === false; aggregate={{"function":"count"}} → zero diagnostics, ok === true; an expression on an object input → one warning inert-expression, no type-mismatch, ok === true. Extra pins: string literal on number / boolean / array inputs, container-with-expression. Inputs copied from sdui.manifest.json (verified: object-metric aggregate: object, invert: boolean, filter: array; object-kanban limit: number). "os build fails on the first pin" was a probe (exit 1, with an ablation before-leg), which is what the claim asked for ("checks"), not a committed test.
  • Census-first — TRUE. Seven rows in the PR body, compiled (not grepped) against the committed manifest, including the interpolating capability-map.page.ts, hotcrm (no html page), skills/docs fenced blocks, and a positive control; zero writers, so no example or skill edits, consistent with PR docs(skills): the html-tier page example spells object-metric's aggregate in the object form #21667 having landed first as the triage required.

Implemented-by: claude/issue-21671-literal-type-mismatch-error
Reviewed-by: session_01VDtqoecgES7ScQYGbFVDRv

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 4, 2026 04:46
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 4, 2026 04:46
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 4, 2026
Merged via the queue into main with commit a4fd82a Oct 4, 2026
43 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21671-literal-type-mismatch-error branch October 4, 2026 05:33
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ui#11466, objectstack-ai#11574, objectstack-ai#11578, objectstack-ai#11581 and objectstack-ai#11583) (objectstack-ai#21710)

Fixes objectstack-ai#21696
Clause-②: no

This moves the bundled Console's objectui pin from `ab1879721595`
(objectstack-ai#21625) to objectui `main` as of the write time,
`2e818d0b51ecdf8fdd9fcbf4b916bcd7fe9a9cf6`, which is past `973fc20f3`.
The Console now carries objectui#11574 (the phantom "(empty)" groups
fix), objectui#11578, objectui#11581, objectui#11576, objectui#11583 and
objectui#11466 (V1 declared node slot). It also carries objectui's own
17.7.0 package release.

⛔ This is not a release act. Version PR objectstack-ai#21352 is untouched, and no tag,
publish or Release was made. Whether 17.7.0 ships with this pin is the
maintainer's call.

## Range

`ab1879721595..2e818d0b51ec` has 22 non-merge commits. `git merge-base
--is-ancestor` exits 0 against objectui `origin/main` for `973fc20f3`,
and `2e818d0b51ec` was objectui `main` when it was read (`git ls-remote`
just before this PR).

From the changesets objectui declared over the range:
- 17 releasing changesets, 1 release-nothing, and 6 commits that carry
no changeset.
- 12 of the 18 changesets were already consumed by objectui's release
`b493919c7`. Each was read from the commit that added it.
- 2 breaking entries, both annotated by the author and both from one
commit.

The range was listed by measurement, not copied from the card. Exactly
one commit subject in the range carries `!`:

`git log --format='%h %s' ab187972..2e818d0b5 | grep -E '^[0-9a-f]+
[a-z]+(\([^)]*\))?!:'` gives `83e3f8377
feat(types,react,core,plugin-dashboard)!: … (objectui#11466)`.

| objectui landing | commit | note |
|---|---|---|
| objectui#11466: node slot and `SchemaRenderer`'s `schema` take
`DeclaredNode`; the dashboard producers name declared nodes; an
envelope's object-metric retires | `83e3f8377` | **declared breaking**,
both breaking entries |
| objectui#11574: a grouping-field switch paints only the server's
groups | `2f54dca53` | smoke below |
| objectui#11581 / objectstack-ai#11578 / objectstack-ai#11576: Create View doors share one builder;
a refused save is said; chart views bind an ADR-0021 dataset |
`b65aa5e65`, `f1c937966`, `d0097af2e` | |
| objectui#11583: a refused metadata write on the view, report and draft
surfaces is said | `e8c0b9614` | past the card's `973fc20f3` |
| objectui#11598: dashboard slot-entry and chart-producer reads compile
without `BaseSchema`'s index signature | `2e818d0b5` (the pin itself) |
|
| objectui#11572 / objectstack-ai#10380: interface and object page relay stored view
config | `09036173c`, `068564691` | |
| objectui#11588: grid summary footer reads currency/precision from the
object field only | `d768c3178` | |
| objectui#11216: `object-kanban` declares `grouping` as the spec's
`GroupingConfig` | `a7557a7d4` | |
| objectui#11573, objectstack-ai#11564: `AnyComponentSchema` / flex child typing |
`bdc9049ed`, `b10c68e58` | |
| objectui release 17.7.0 | `b493919c7` | removes 2726 consumed
`.changeset/*.md` files |

## ADR-0087 disposition of the declared-breaking entries

Both entries are `not-required (no-migration-prescription)`. The
disposition is in `.changeset/console-2e818d0b51ec.md`, which replaces
the script's `adr-0087: TODO` placeholder.

| objectui PR | commit | entry | disposition |
|---|---|---|---|
| objectui#11466 (PR objectui#11512) | `83e3f8377` | A node slot and
`SchemaRenderer`'s `schema` prop take `DeclaredNode` | not-required
(no-migration-prescription). This is objectui's own TypeScript face
(`@object-ui/types` / `@object-ui/react`). No ObjectStack schema
declares or references it. |
| objectui#11466 (PR objectui#11512) | `83e3f8377` | An `object-metric`
node in a dashboard widget's legacy `component` envelope draws the
retired-format prompt | not-required (no-migration-prescription). An
ObjectStack author cannot write that envelope: `ui/dashboard.zod.ts`
refuses a widget's `component` key by name as objectui-internal, and no
example authors one. The showcase's `object-metric` page tiles still
draw their numbers (smoke below). |

`check-adr-0087-registration --base origin/main` gives "1
declared-breaking changeset(s), each carrying an ADR-0087 disposition".
`check-changeset-no-major --base origin/main` gives "This diff
introduces no `major` bump".

## What changed here

- **`.objectui-sha` and `.changeset/console-2e818d0b51ec.md`** were
written by `scripts/bump-objectui.sh
2e818d0b51ecdf8fdd9fcbf4b916bcd7fe9a9cf6 --no-commit` with
`OBJECTUI_ROOT` set to a read-only clone of objectui. The level was
auto-set to `minor`. No `../objectui` sibling exists in this container.
- **`scripts/sdui-manifest.record.json`** was re-recorded by `node
scripts/gen-sdui-manifest-node.mjs` over the tree that `pnpm
objectui:build` built at the pin. **`sdui.manifest.json` is
byte-identical**: 107 components, sha256 `0ead67c1111d…` at both pins,
so no component input moved. The record moves its pin, its `modulesRoot`
and objectui's workspace version (17.6.0 to 17.7.0).
- **`packages/sdui-parser/objectui-lockstep.json`** was re-recorded with
`pnpm gen:sdui-lockstep` against
`OBJECTUI_ROOT=.cache/objectui-2e818d0b51ec`. It records 214 grammar
lines (blob `0131f27cf86d`), 25 codes and containment predicate
`76c18fb95d1f`. All are unchanged, so no port is owed. objectui's
`packages/sdui-parser/src` is byte-identical across the range; only its
CHANGELOG and package.json moved.
- **The 54 asserting pin citations in `packages/spec/src`** were
re-measured at the new pin, not restamped:
- Every anchor in each asserting record was resolved against objectui's
tree at the old pin and mapped through `git diff -U0 ab187972
2e818d0b5`.
  - Four cited files changed:
- `plugin-dashboard/src/index.tsx` (+13/-1, objectui#11466). Lines were
added above the `object-metric` registration, so its start moves `244`
to `256` and the icon input moves `269` to `281`. Both lines are
byte-identical, still `{ name: 'icon', type: 'string' }`.
- `packages/types/src/objectql.ts`. objectui#11216's `grouping` member
landed below the cited `limit` member, so `:4720` did not move.
- `content/docs/plugins/plugin-kanban.mdx`. A `grouping` row landed
below the `limit` row, and the page still teaches `limit: 250`.
- `packages/react/src/SchemaRenderer.tsx` (+21/-20, objectui#11466).
Only the type import, the `SchemaRendererProps` docblock and the
`schema` type changed. The `properties.*` hoist, its strip list and the
`createElement` spread did not, so `object-timeline`'s prop channel
still carries `data`.
  - Every other cited file is byte-identical across the hop.
- Each record gains a dated 2026-10-04 hop sentence and keeps its
`ab1879721` history.
- **The six migration entries' corpus counts** were re-taken with `git
grep -o -F`. That method first reproduced every `ab1879721` number:
10267 files, `objectstack` 16377, `@objectstack/spec` 6665, `timeout`
1348, `useState` 2476, `TTL` 180, `tenant` 1238, `Span` 491.
- The new numbers are 7579 files, 17227, 7134, 1351, 2477, 182, 1317 and
505. The file count drops because objectui's 17.7.0 release removed 2726
consumed changesets.
- Every zero is still zero: 98 tokens were checked, the export lists of
the three cited spec files plus every named key. The only non-zero
tokens are the expected `Span` / `SpanSchema`. The 14 new `Span` hits
are `Spanish` prose.
- `packages/spec/src/migrations/registry.ts` was regenerated with
`gen:migration-registry`.
- **`.changeset/objectui-pin-citations-2e818d0b51ec.md`** is a
`@objectstack/spec` patch, because the `FormField.span` describe and six
migration descriptions name the pin.
`content/docs/references/ui/view.mdx` was regenerated by
`check:generated --fix`.
- **`main` was merged** (`7e0066af7a`) with
`scripts/pm/os-regen-merge.sh` in merge commit `a0a35cb30f`, with no
conflict hunk. Afterwards:
  - `check:generated` reports all 15 artifacts current.
- main's three new semantic entries
(`ui-object-form-fields-names-typed`, `ui-object-gantt-markers-typed`,
`ui-object-timeline-mapping-typed`) are present in the regenerated
registry.
- The pin-citation gate still counts 54 asserting citations. main added
historical ones only (108 to 118).

No example, test or gate needed adapting, and no code changed outside
generated records, citations and changesets.

## Browser smoke: `examples/app-showcase` with the Console built at
`2e818d0b51ec`

The Console was built by `pnpm objectui:build`. The build log reports:
"Console dist matches the objectui pin (objectui@2e818d0b51ec)",
"Single-zod canary: exactly one zod version literal
{major:4,minor:6,patch:5}" and "Console bundle carries THIS tree's
@objectstack/spec, and only it".

The server ran `pnpm dev -- --fresh --ui --no-watch -p 41853` (with
`OS_PORT=41853`) on its own ephemeral DB with the seeded admin. Headless
Chromium (`/opt/pw-browsers/chromium`) drove it, with console messages,
page errors and every non-2xx response captured. Only the PIDs this run
started were stopped.

**Dashboards.** objectui#11466 touches the dashboard producers. There
are no page errors and no "retired" prompt on any of the four
dashboards.

| dashboard | metric tiles (value read in the DOM) | charts drawn |
console errors |
|---|---|---|---|
| Delivery Operations `showcase_ops_dashboard` | Active Projects 2,
At-Risk (Red) 1, Awaiting Review 2, Total Budget 1,090,000 | 4 | 404
`/api/v1/usage/storage`, 404 `/favicon.ico` |
| Revenue Pulse `showcase_revenue_pulse` | Invoices 7, Invoiced Subtotal
1,920, Accounts Signed 3, Accounts (all time) 14, Paid Rate 42.9% | 4 |
404 `/api/v1/usage/storage` |
| Chart Gallery `showcase_chart_gallery` | Total Tasks 10, Avg Progress
47.0, Total Spent 616,000 | 13 | 404 `/api/v1/usage/storage` |
| System Overview `system_overview` (setup) | Total Users 3, Active
Sessions 3, Login Events 3, Config Changes 0 | 2 | 404
`/api/v1/usage/storage` |

The Delivery Operations tiles match the REST data on the same server:
- `showcase_project?status=active` returns 2 rows, `?health=red` returns
1, and the budget sum is 1,090,000.
- `showcase_task?status=in_review` returns 2 rows.
- `showcase_task` has 10 rows, matching the Chart Gallery's Total Tasks.
- `showcase_account` has 14 rows, matching Revenue Pulse's all-time
count.

The `object-metric` page tiles that this repo authors also draw:
- My Work shows Open Tasks 8, In Review 2 and At-Risk Projects 1.
- Command Center (大屏) shows 2 / 8 / 2 / 1 / 14 / 1.1M.

Neither page has a page error.

**Grouping-field switch (objectui#11574).** The drive was Tasks list,
then Group, then Add group field (it selects Title). The select then
switched to Priority, Status and Priority. The painted groups are
counted from the DOM, and the server answers are each `POST
/api/v1/data/showcase_task/query` with `groupBy`, all 200:

| step | painted groups | server rows | "(empty)" groups | stuck
"Loading grid…" |
|---|---|---|---|---|
| Title | 10 (one per task) | 10 | 0 | 0 |
| Priority | 4: High 3, Low 1, Medium 4, Urgent 2 | 4 | 0 | 0 |
| Status | 5: Backlog 2, Done 2, In Progress 2, In Review 2, To Do 2 | 5
| 0 | 0 |
| Priority | 4: High 3, Low 1, Medium 4, Urgent 2 | 4 | 0 | 0 |

The same sequence painted 13, 8 and 8 groups (9, 3 and 4 phantom) at
`ab1879721595`, as objectstack-ai#21625's acceptance notes recorded. **No phantom
"(empty)" groups appear at this pin.**

**Console errors across the run.** The only ones are the browser's
"Failed to load resource: 404" lines for `/api/v1/usage/storage` and
`/favicon.ico`, and the run has 0 page errors. The `usage/storage` 404
was already recorded in objectstack-ai#21625's smoke.

## objectstack-ai#21671 lockstep reading

- objectui's `packages/sdui-parser/src/validate.ts` at `2e818d0b5` is
byte-identical to `ab1879721595`.
- `checkType` (`:450-466`) still grades `type-mismatch` with `severity:
arms.includes('enum') ? 'error' : 'warning'`. `checkMemberTypes`
(`:407-419`) grades `member-type-mismatch` the same way.
- This repo grades both `error` since PR objectstack-ai#21678
(`packages/sdui-parser/src/validate.ts:446`, `:506`). **The gap still
stands at this pin.**
- This repo's "Known lead" note
(`packages/sdui-parser/src/validate.ts:336-340`) names objectui's copy
"at the `.objectui-sha` pin" without a sha, so it is still true and
needs no update. The bump route regenerates nothing there, and
`validate.ts` is not edited.
- `check:sdui-lockstep` does not compare severity, so this is a reading,
not a gate result.

## Gates and tests (head `a0a35cb30f`)

- `node scripts/pm/dispatch-gates.mjs --commands` derived 127 commands
from the 21-path diff. That is the same set before and after the merge.
- At `378d424915` (pre-merge) and again at `a0a35cb30f`, all 127 exited
0. `--ran` reports "127 derived, 127 run, 0 NOT-MEASURED, 0 UNRUN", with
every exit code recorded.
- Also exit 0:
- `check:objectui-pin-citations --verify-anchors` with objectui at the
pin: 54 asserting citations match `2e818d0b5`, and 7 content assertions
are verified.
- `check:objectui-bump` (20 assertions across 5 cases),
`check:sdui-lockstep`, `check-sdui-manifest`, `check:console-sha`,
`check:console-injection`, `check:migration-registry`.
- `@objectstack/spec check:generated`: all 15 artifacts current after
the merge.
- `pnpm --filter @objectstack/spec test`: 612 files, 18185 passed, 1
todo.
- `pnpm --filter @objectstack/spec typecheck`: exit 0.
- `pnpm --filter @objectstack/sdui-parser test`: 225 passed.
- `eslint --no-inline-config` on the 15 changed TS files: 0 errors and 0
warnings. The lint population is `**/*.{ts,…}`
(`eslint.config.mjs:971`). The config enables no type-aware linting
(`:328`), so this diff cannot move a judgment on an untouched file.
Repo-wide `pnpm lint` is left to CI.

## Acceptance notes

- `ui/component.zod.ts`'s `object-grid` `columns` record (the ⚠️
paragraph on `plugin-grid/src/useColumnSummary.ts:558-568`) says, about
`ab1879721595`, that the footer summary reads `currency` /
`defaultCurrency` / `precision` / `scale` off an authored column.
objectui#11588 (`d768c3178`, inside this pin) retires that read. The
record is in the historical spelling and stays true of the pin it names,
so it is left as is.
- Two anchors in records that cite no sha are outside the pin-citation
gate's population and were not re-measured:
- `ObjectView.tsx:882`, in `ui/view.zod.ts`'s [objectstack-ai#5074] block and two view
tests.
- `plugin-dashboard/src/index.tsx:204`, in the `ObjectMetricPropsSchema
icon liveness` test.
- Both already read an unrelated line at `ab1879721595`, and they shift
to `:936` and `:214` here.
- Writes: no PR assignee or label was written. This dispatch's write
budget names neither.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

1 participant