Skip to content

feat(core,sdui-parser,components): the published manifest declares the html tier's intrinsic elements, marked tier html — div stays out (objectui#10735) - #10753

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-10735-html-tier-manifest
Sep 27, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-10735-html-tier-manifest

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #10735
Clause-②: yes — the published manifest's declared vocabulary grows.

Executes the maintainer's ruling A on objectstack#20112 (batch #225 item 1, 「其他同意」): the published sdui.manifest.json declares the html tier's registered intrinsic set exactly as the registry declares it, with each tag's inputs and child slot; div stays undeclared. Base origin/main 5ad3b88; head 6417e8d (round 2: wording only, one commit on top of 5bd6270).

What changed

  • packages/core/src/registry/html-tier-intrinsics.ts (new): HTML_TIER_INTRINSICS — the 37 tags of html-elements.tsx's TAGS, plus span, table, label and the seven sectioning tags of semantic.tsx (47). Exported from @object-ui/core beside PUBLIC_BLOCKS.
  • packages/core/src/registry/Registry.ts: getPublicConfigs() appends the roster's registered tags LAST, each stamped tier: 'html' in the projection (never written onto the registration); PublicComponentConfig.tier admits 'html'. PUBLIC_BLOCKS is untouched.
  • packages/sdui-parser: ManifestComponent.tier?: 'html'; manifestFromConfigs writes exactly 'html' or omits the key (every curated entry serialises byte-identically); generateBlockList sections the html tier under its own count; RegistryConfigLike.tier admits 'html'. gen-manifest.ts header updated, behaviour unchanged.
  • packages/components/src/renderers/layout/react-page.tsx: the kind:'react' scope builder skips tier === 'html' entries, so no P / A / Img wrapper is injected (file-surface amendment, forced by the card's ⛔ on silent widening — see H2).
  • Tests: new pins in core, sdui-parser, console; react-page-scope.test.tsx and container-declaration-census.test.tsx updated; public-contract.test.ts prose amended (two ledger passages that said curating a sectioning tag would newly widen the manifest).
  • Docs: content/docs/guide/react-pages.md (the html tier is not injected on the react tier), content/docs/guide/plugin-development.md (the component-level tier: 'html' marker).
  • .changeset/10735-html-tier-manifest.md: minor for @object-ui/core, @object-ui/sdui-parser, @object-ui/components.
  • Round 2 (contract review, wording): the react-page skip line is quoted as it now reads — if (!tag || cfg.isContainer || cfg.tier === 'html') continue; — in content/docs/guide/react-pages.md ("skips every container and every html-tier entry"; frontmatter lines 1–4 md5 unchanged, 9c5f94a7…), in both public-contract.test.ts passages, in the container-declaration-census pin, and in layout-containers-declare-containment.test.tsx (comment-only, previously untouched). git grep -F of the old line at head: 2 hits, both inside .changeset/6740-flex-is-container.md — its original paragraph and the dated note that marks it. The 10735 changeset now says "200 ledgered findings" (10 ledger rows whose counts sum to 200).
  • .changeset/6740-flex-is-container.md (pre-existing, @object-ui/components: patch): reconciled in the objectui#10533 dated-note form, scoped "at this change" — ⚠️ **Dated note, 2026-09-27 — the skip line quoted above has since gained a third arm — objectui#10735.** — frontmatter byte-identical (md5 ce888ebd… before and after). check-changeset-overwrite REPORTS the modification (report-only, exit 0) and did not refuse it.
  • ⛔ Not touched: html-elements.tsx, div.tsx, PUBLIC_BLOCKS, the registration-uniqueness test.

H1 — the census (measured from ComponentRegistry.getKnownTypes() after @object-ui/components + apps/console/src/register-plugins.ts load, in the console vitest project)

Instrument for "is an HTML tag": the keys of JSX.IntrinsicElements in @types/react (178 names, HTML and SVG; lit control: div, p, svg in, object-grid out). Registry: 485 known keys, 208 bare. Intersection: 64 bare registrations carry an element name.

  • (a) html-elements.tsx TAGS — 37, all registered: h1 h2 h3 h4 h5 h6 p a blockquote pre strong em b i u small mark sub sup del ins abbr ul ol li dl dt dd figure figcaption img hr br time address cite q.
  • (b) registered elsewhere, declared — 10: span (basic/span.tsx; inputs className, children), table (complex/table.tsx; caption, footer, columns, data, className — no child slot), label (form/label.tsx; text required, className — no child slot), and aside main header nav footer section article (layout/semantic.tsx; className, children; isContainer).
  • (c) excluded — 17:
    • div — deprecated on the JSON surface in favour of box (objectui#3965, PR objectui#6878); kept out by the ruling.
    • code, summary, object — bare namespace fallbacks of field:code / field:summary / field:object (inputs [], no child slot); view — plugin-view's bare key (SVG name coincidence). ⚠️ The ruling's list names code as "registered elsewhere"; measured, the only code registration is the field widget (a code editor / pre block reading value). Declaring it would teach a tag whose children the runtime drops, so it is NOT declared — see Acceptance notes and the report's open question.
    • kbd — a ui component reading keys / label, not named by the ruling; not declared (additive later on a named need; retracting is a narrowing).
    • form input textarea select switch — JSON-surface form components; the tier excludes form controls by design (the exclusion list in html-elements.tsx). progress, dialog — ui components under tag names, not passthroughs, not named.
    • button text image html — already curated in PUBLIC_BLOCKS; they reach the manifest from there, unstamped.

The (c) list is not prose: apps/console/src/__tests__/html-tier-manifest.test.ts holds every HTML-named bare registration to one of PUBLIC_BLOCKS, HTML_TIER_INTRINSICS or its exclusion ledger EXCLUDED_HTML_NAMED (13 entries, each with a reason, each pinned live), so a registration that newly squats on a tag name lands there by absence.

H2 — the fork, the consumer map, and the marker decision

Premise measured true: getPublicConfigs() through manifestFromConfigs is the sole source of the published manifest — objectui's apps/console/dev/manifest-dump.tsx / scripts/dump-public-manifest.mjs, and objectstack's scripts/gen-sdui-manifest-node.mjs (which imports the 16 registry modules from objectui's built tree at .objectui-sha and calls exactly ComponentRegistry.getPublicConfigs() then its lockstep copy's manifestFromConfigs). The manifest is ONE flat components map with no tier marker; compile() derives allowedTags = Object.keys(manifest.components). So listing the html tags in PUBLIC_BLOCKS would have widened the curated JSON-surface vocabulary silently — the fork the card names.

Marker chosen: a per-entry tier: 'html' on ManifestComponent, carried from a projection-only stamp in getPublicConfigs(). Why not a second registry read or a second manifest section: the objectstack generator calls getPublicConfigs() and nothing else, and compile() reads only components — either alternative needs an objectstack-side change before the gate accepts a single html tag, and the second-section shape would leave the gate refusing the tags until its lockstep copy was ported. With the per-entry marker the objectstack gate accepts the tags with NO reader change: it ignores the extra key.

Consumers, and what each does with the marker:

reader reads change
objectstack packages/lint validateJsxPages → compile() keys as whitelist; validateTree reads inputs / namespace none — accepts the tags; ignores tier
objectstack scripts/check-sdui-manifest.mjs components[k].type === k, count, sha none
objectstack scripts/gen-sdui-manifest-node.mjs getPublicConfigs() + lockstep manifestFromConfigs none to run; ⚠️ its lockstep copy of packages/sdui-parser/src/{index,types,codegen}.ts needs this port for the marker to reach the tracked file — until then the tags are declared there WITHOUT tier, which the gate treats identically
objectui react-page.tsx buildComponentScope getPublicConfigs() skips tier === 'html' (else P / A / Img wrappers would be injected)
objectui generateBlockList manifest sections the html tier; curated count unchanged (59)
objectui generateDts manifest emits the tags into JSX.IntrinsicElements (the html-tier authoring surface)
objectui page.tsx html-tier compile getKnownTypes(), not the manifest none (already accepted these tags)
console census tests getPublicConfigs() container-declaration-census reads the curated contract as the unstamped entries; public-contract coverage lists filter by PUBLIC_BLOCKS and are unchanged

Reverse verification that consumers read the REBUILT .d.ts: a scratch console file assigning tier: 'json' to a ManifestComponent and to a PublicComponentConfig — tsc --noEmit exit 2, TS2322: Type '"json"' is not assignable to type '"html"' and … to type '"html" | "public" | "internal" | undefined'; the 'html' assignments compiled. Probe removed; git status clean.

H3 — the regenerated artefacts

Generated with the repo's own generator (buildArtifacts in packages/sdui-parser/scripts/gen-manifest.ts) over the console registration graph with the 16 plugin modules eager-imported in manifest-dump.tsx order, once on the base tree and once on head:

artefact base head
sdui.manifest.json components 59 106 (+47 added, 0 removed, 0 curated entries changed)
div / code / kbd present no no
entries with tier: 'html' / without 0 / 59 47 / 59
sdui-intrinsics.d.ts intrinsic entries 59 106 (+253 lines, −0)
sdui-blocks.md 65 lines, one table 119 lines: # SDUI public blocks (59) unchanged + ## html tier intrinsic elements (47)

Sample head entries: a → inputs className, href, target, rel, title, children (slot); br → className only (void, no slot); label → text (required), className (no slot). The base run's manifest differs from objectstack's tracked sdui.manifest.json only because the pin f8a9d0f predates this base (cmp differs at byte 1183) — the regeneration at the next pin bump is the domain:cli lane's step.

Pins — red on base, green on head

Two mutation legs (implementation files checked out at base, tests at head; trap-guarded restores to HEAD, proven by git diff HEAD empty and blob-hash equality — "RESTORE PROVEN" printed twice):

  • Leg 1 — Registry.ts, sdui-parser index.ts / types.ts / codegen.ts, react-page.tsx at base: Test Files 5 failed (5), Tests 17 failed | 61 passed (78) — every behavioural pin red (the roster-only pins stay green by design).
  • Leg 2 — react-page.tsx alone at base: Tests 1 failed | 8 passed (9) — a PascalCased html-tier tag is an unknown identifier on this tier red (an Img wrapper was injected).
  • Green on head 5bd6270 (round 1) after the restores: Test Files 9 passed (9), Tests 114 passed (114) over the five pin files plus registration-uniqueness.test.tsx (unchanged, green), public-tier.test.ts, tier.test.ts, public-contract.test.ts.

Round 2 (head 6417e8d): the three re-quoted test files plus react-page-scope and html-tier-manifest re-run — public-contract, container-declaration-census, layout-containers-declare-containment, react-page-scope, html-tier-manifest — Test Files 5 passed (5), Tests 89 passed (89).

Broader affected suites at head 5bd6270: console html-tier-manifest, public-contract, public-block-binding-reach, record-block-record-reach; components react-page-scope, container-declaration-census, container-declaration-ratchet, layout-containers-declare-containment, registration-uniqueness; plugin-tree registration.publicTier-10064 — Test Files 10 passed, Tests 137 passed (plus the scratch generator run).

Gates (exit captured before any pipe; verdict lines quoted)

  • Build: turbo run build --filter="@object-ui/console^..." --concurrency=2 under the verify lock — Tasks: 34 successful, 34 total, VERDICT command-exit 0.
  • Type-check (pnpm run type-check, script echoed): @object-ui/core exit 0, @object-ui/sdui-parser exit 0, @object-ui/components exit 0, @object-ui/console exit 0. --listFiles counts the new / edited test files inside their programs (1, 1, 2, 1).
  • Lint, CI-parity: turbo run lint over the four packages — Tasks: 5 successful, 5 total, 0 errors; eslint --format json over the 14 touched files: 0 errors, 22 warnings, all pre-existing no-explicit-any lines not introduced here.
  • check:control-bytes — ✅ check-control-bytes: OK (scanned 8928 tracked text file(s)).
  • check:new-line-citations — VERDICT new-cross-file-line-citations: 0 new citation(s).
  • check-changeset-presence — round 2: ✅ 15 source file(s) of 4 released package(s) changed, and this change declares 1 changeset(s); changeset:check — ✅ No changeset declares a major bump and ✅ All workspace packages are in the changeset fixed group.
  • Round 2 text gates on head 6417e8d: check:control-bytes ✅ OK (scanned 8928 tracked text file(s)); check:new-line-citations VERDICT new-cross-file-line-citations: 0 new citation(s); check-changeset-claims exit 0 (2 bodies judged, no negation; the re-read list is unchanged); check-changeset-overwrite exit 0 report-only — 1 changeset(s) added, 1 modified, 0 deleted, the modified one being .changeset/6740-flex-is-container.md (the dated note above); check-doc-links — Links are valid across 17 scan roots.
  • check:unreferenced-sources OK; check:pending-changeset-literals ✅ No test source names a pending changeset; check:test-path-roots OK; check:doc-types ✅ Every documented component type is registered; check:doc-fences OK; check:registry-bare-names ✓ no new bare-name collision; check:esm-specifiers (specifier leg) OK; check:component-surface-parity report-only exit 0.
  • check:changeset-claims (report-only): 13 pending changesets name a file this change touches; each re-read — the "forwards exactly seven keys per input" claims stay true (this adds a component-level key, no input key), and "none of the 8 is in the curated public contract" stays true (the seven now enter the manifest as html-tier entries, not curated).
  • NOT MEASURED locally, declared to CI: check:readme-exports (exit 1 = 8 self-imports unjudgeable because packages/cli and packages/plugin-ai are outside the built closure; 538 judged, 0 fabricated), check:doc-snippets (exit 2 PRECONDITION NOT MET, same unbuilt packages; the two edited guides gained prose only), check:eager-closure and check:sdui-registration-pins (need a console build), repo-wide pnpm lint (per-package eslint via turbo covered the four touched packages exactly as CI runs them; eslint.config.js declares no type-aware project, so this diff moves no untouched file's verdict).

Serial

Claim read: no open PR touches gen-manifest.ts, public-blocks.ts or html-elements.tsx. Round 1: origin/main fetched into the private ref refs/issue-10735/main (01700dc, four commits past base), git merge-tree --write-tree HEAD refs/issue-10735/main exit 0 (tree 368f01d8). Round 2, before the push of 6417e8d: main re-fetched at 704e05b (nine commits past base: #10741, #10574, #10736, #10745, #10749, #10734, #10733, #10744, #10752), merge-tree --write-tree exit 0 (tree 0d9bcb93); the set of files main moved since base is disjoint from this branch's files (comm -12 empty), so no merge commit was needed.

Acceptance notes

  • div on the html tier is now split across two faces. The published contract refuses it (forbidden-tag), while the renderer's declaration still reads deprecated: { surfaces: ['json'] } (objectui#4000) and the console's html-tier compile (page.tsx, getKnownTypes()) still accepts and renders it. The ruling's item 3 parks the renderer-side retirement in these notes: the reconciliation is a separate card (declare the html surface on div's deprecation with box as the replacement, and decide whether the console compile should read the same roster).
  • "Refused with box named as the replacement" has no mechanism today. The gate's refusal is the parser's generic forbidden-tag text (the tag is not an allowed component); nothing in the manifest or the parser names box for an absent div. Registry.deprecationFor('div', 'json').replacement names it, but the gate never reaches the registry. Not implemented here (a new contract shape); raised in the report's open questions.
  • code: the html-elements.tsx header and ADR-0081 §2 both say code is "registered elsewhere"; the only registration is field:code's bare fallback. On an html page today code resolves to that widget and drops its text — a reproducible runtime defect independent of this card; reported as an out-of-scope finding.
  • label and table are declared as registered, so the HTML-shaped label with text children draws missing-required-prop (text) and not-a-container, and a table authored with element children draws not-a-container; the registered spellings (text="…", columns / data) are clean. Pinned.
  • objectstack side, for the domain:cli lane: regenerate sdui.manifest.json at the pin; port packages/sdui-parser/src/{index,types,codegen}.ts to the lockstep copy so the marker reaches the tracked file; the ledger's 200 a / div rows go stale (the a rows) or stay refused (the div rows, to be rewritten to box as the ruling says).
  • semantic.tsx's comment ("none of these seven tags is in the curated PUBLIC_BLOCKS contract, so there is no injected identifier") remains literally true and its conclusion holds on two grounds now; not edited (no registration change).

Pending changesets re-read (round 2, head 6417e8d)

The changeset-claim-re-read bot lists 12 pending changesets naming files this diff touches (13 in round 1, 6740 now being one this change modifies). Each re-read paragraph by paragraph against head:

  • 5905-componentinput-inputtype-tombstone.md — still true: packages/sdui-parser/src/index.ts still forwards exactly seven keys per input (name, type, of, required, enum, binding, description); this change's tier write sits at component level, beside type / namespace / isContainer, and adds no per-input key.
  • 5905-componentinput-retire-constraint-keys.md — still true, same seven-keys-per-input claim, same reason.
  • 7493-componentinput-retire-label-defaultvalue-advanced.md — still true, same seven-keys-per-input claim; min / max / step / placeholder / label / defaultValue / advanced still reach no manifest.
  • 6067-component-meta-derive-from-canonical.md — still true: it describes the pre-6067 shape of Registry.ts in the past tense; at head ComponentMeta is still CanonicalComponentMeta & RegistryComponentMetaExtras, and RegistryComponentMetaExtras.tier is unchanged ('public' | 'internal'). The 'html' value lives only on the PublicComponentConfig projection.
  • componentinput-reexport-4972.md — still true: Registry.ts still re-exports ComponentInput from @object-ui/types (export type { ComponentInput } from '@object-ui/types').
  • default-children-retired-5051.md — still true: none of the three ComponentMeta declarations offers defaultChildren; this change adds no ComponentMeta key.
  • 6764-container-declaration-census.md — still true as written: react-page.tsx drops containers from the react scope and reads getPublicConfigs(), and none of the 8 is in the CURATED public contract. The seven sectioning tags now ride that read as html-tier entries, stamped tier: 'html' and skipped, which leaves the claim's operative words ("curated public contract") intact; not corrected.
  • 9280-record-highlights-entry-icon-retired.md — still true: gen-manifest.ts still serialises registry inputs into sdui.manifest.json and sdui-intrinsics.d.ts (the header now also names the html tier's stamped entries; behaviour unchanged).
  • components-react-page-published-dts-text-5666.md — still true: react-pages.md's injected-scope note and §Styling still say plain HTML with inline style, not Tailwind; the guide's edited sentence quotes the skip line and adds "and every html-tier entry", nothing about styling.
  • page-source-tailwind-framing-5461.md — still true: react-page.tsx keeps both styling comments (the header note and the buildComponentScope note); this change adds a comment inside the loop and removes none.
  • page-source-tailwind-prose-retraction-5469.md — still true: react-pages.md §Styling wording is unchanged.
  • palette-canonical-discussion-5495.md — still true: public-blocks.ts still records record:chatter as the same block "under a Salesforce-familiar name"; this change adds a header paragraph and touches no roster entry.
  • 6740-flex-is-container.md — CORRECTED (prose-only): its paragraph quotes the react-page skip line as if (!tag || cfg.isContainer) continue;, which this PR changed; a dated note in the objectui#10533 form (⚠️ **Dated note, 2026-09-27 — the skip line quoted above has since gained a third arm — objectui#10735.**) records the current line and keeps the entry as the reading of that change. Frontmatter byte-identical, md5 ce888ebde4729564f67bca69a7928743 before and after. check-changeset-overwrite reports the modification (report-only, exit 0).

Generated by Claude Code

… the manifest, marked tier html

The published sdui.manifest.json is getPublicConfigs() serialised, and that
read was the curated PUBLIC_BLOCKS vocabulary alone, so the objectstack gate
that whitelists a kind:'html' page's tags from it refused every plain HTML tag
the renderer accepts. Add a second roster, HTML_TIER_INTRINSICS (the
html-elements.tsx TAGS, span, table, label and the seven sectioning tags),
which getPublicConfigs() appends stamped tier:'html'; manifestFromConfigs
carries exactly that stamp into a new optional ManifestComponent.tier, so a
whitelist reader takes the tags by key unchanged while a reader that means the
curated vocabulary filters on the stamp: generateBlockList sections the html
tier under its own count and the kind:'react' JSX scope skips stamped entries.

div stays undeclared (deprecated in favour of box); code (the field:code
widget's bare fallback) and kbd are ledgered with reasons. PUBLIC_BLOCKS and
html-elements.tsx are untouched; curated entries serialise byte-identically.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
… LayoutSchema arms

HtmlElementSchema and SemanticElementSchema are deliberately not exported
from the zod barrel, so both pins derive the two enum arms from LayoutSchema
by shape. The label probe asserts what the registration declares: text is
required and there is no child slot, so <label>Name</label> draws
missing-required-prop and not-a-container while <label text="Name" /> is
clean.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 12 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/5905-componentinput-inputtype-tombstone.md

  • names packages/sdui-parser/src/index.ts → packages/sdui-parser/src/index.ts — edited by this change

    The write was measured as a no-op before it was deleted, and re-measured on this branch's base rather than inherited from the card. A structural census over every inputs: array in the repository (211 regions, all tracked TS/TSX/JS sources) scores inputType at exactly ONE authoring site — the plugin-markdown registration — against name 953, type 969, label 966, description 194, enum 119, required 86 and binding 4 in the same pass over the same regions, so the instrument was not blind. The other 192 in-repo inputType hits are a DIFFERENT face: FormField.inputType (zod/form.zod.ts), the text-input renderer's prop, and SchemaBuilder.inputType, none of which sit on a ComponentInput. The publication path is unchanged and was re-confirmed: packages/sdui-parser/src/index.ts forwards exactly seven keys per input — name, type, of, required, enum, binding, description — so an authored inputType could not reach the published sdui.manifest.json even in principle.

.changeset/5905-componentinput-retire-constraint-keys.md

  • names packages/sdui-parser/src/index.ts → packages/sdui-parser/src/index.ts — edited by this change

    All four were declared on ComponentInput and read by nothing, on either path. No consumer reads them off a ComponentInput value, and the manifest serializer (packages/sdui-parser/src/index.ts) forwards exactly seven keys per input — name, type, of, required, enum, binding, description — so a value authored here could not reach the published sdui.manifest.json even in principle. Re-measured on this branch's merge-base rather than inherited from the card: a structural census over every inputs: array in the repository (219 regions, all tracked files) scores min 0, max 0, step 0 and placeholder 0, against name 926, type 926, description 161, enum 114 and required 87 in the same pass over the same regions — the instrument was not blind.

.changeset/6067-component-meta-derive-from-canonical.md

  • names packages/core/src/registry/Registry.ts → packages/core/src/registry/Registry.ts — edited by this change

    packages/core/src/registry/Registry.ts declared its own ComponentMeta: thirteen keys, of which nine were restated from @object-ui/types' base.ts, four were registry-only (tier, namespace, skipFallback, labelling), and tags / description were absent — although both are declared on the canonical type and on the ComponentMetaSchema zod mirror. Two of the three authorities agreed and the registration surface did not, so those two keys were unwritable at exactly the declaration most component registrations import. That is the same two-key delta objectui#5893 had just closed inside @object-ui/types, arriving a third time on a third declaration, and objectui#5671 had already made the identical move for the sibling type ComponentInput in this very file.

.changeset/6764-container-declaration-census.md

  • names renderers/layout/react-page.tsx → packages/components/src/renderers/layout/react-page.tsx — edited by this change

    Scoped by measurement, not by sweep. The census behind this change rendered every registered key through the real SchemaRenderer and put it through validateTree: of 131 bare authoring tags, 58 render schema.children, 5 declared the flag, and 53 did not. These 8 are the subset where the second consumer is provably unaffected — renderers/layout/react-page.tsx drops containers from the kind:'react' JSX scope, but it reads getPublicConfigs() and none of the 8 is in the curated public contract. The remaining 45 are reported on the card rather than swept in, button among them precisely because it IS public.

.changeset/7493-componentinput-retire-label-defaultvalue-advanced.md

  • names packages/sdui-parser/src/index.ts → packages/sdui-parser/src/index.ts — edited by this change

    No manifest ever published them, so no consumer could ever have read them. sdui-parser's serializer (packages/sdui-parser/src/index.ts) forwards exactly seven keys per input — name, type, of, required, enum, binding, description — so a value authored under any of the three never reached sdui.manifest.json, the generated JSX .d.ts, or a diagnostic; its boundary type has no slot for them; the registry's data-source seam reads name only; and neither the designer nor the app-shell inspectors consult registry inputs at all. A structural census over every inputs: array in the repository (re-measured on this change's merge-base, name 951 and type 951 as the controls) counted the writes: label 908, defaultValue 245, advanced 9 — written on nearly every registration, read by nothing.

.changeset/9280-record-highlights-entry-icon-retired.md

  • names gen-manifest.ts → packages/sdui-parser/scripts/gen-manifest.ts — edited by this change

    • packages/types/src/record-components.ts — RecordHighlightsComponentProps.fields[]'s object arm: { name; label?; icon?; type?; readonly? } → { name; label?; type?; readonly? }. The key is removed, not tombstoned: the contract's arm is $strict, so the refusal an author needs already exists upstream and arrives named (invalid_union at the entry). A ?: never tombstone buys nothing here — it is the remedy for a non-strict mirror that would otherwise strip in silence, which is not this arm. - packages/plugin-detail/src/renderers/record-highlights.tsx — the entry normalizer stops copying icon: f?.icon into the normalized entry. That read was unreachable, not merely unused: no author could feed it past the $strict arm, and HeaderHighlight renders no .icon on the far side either, so the copy had no consumer in either direction. - packages/plugin-detail/src/index.tsx — the registry manifest's fields input description sketched the entry as {name,label?,icon?,type?,readonly?} → {name,label?,type?,readonly?}. The inputs ARE the published contract (gen-manifest.ts serializes them into sdui.manifest.json and sdui-intrinsics.d.ts), so leaving the sketch standing would have gone on teaching AI and human authors a key that gets the whole document refused at publish.

.changeset/componentinput-reexport-4972.md

  • names registry/Registry.ts → packages/core/src/registry/Registry.ts — edited by this change

    @object-ui/core's ComponentInput (registry/Registry.ts) and @object-ui/types' plugin-scoped ComponentInput (plugin-scope.ts, published as PluginComponentInput) were structural copies of the interface in @object-ui/types' base.ts. Both are now re-exports of that one declaration, which is the disposition objectui#4580 ruled for the identical shape — a structural copy would reproduce the defect the moment either side moved — and the way core/src/types/index.ts already handles SchemaNode.

.changeset/components-react-page-published-dts-text-5666.md

  • names content/docs/guide/react-pages.md → content/docs/guide/react-pages.md — edited by this change

    • The injected-scope note no longer tells authors that layout is left to "plain HTML + Tailwind". It says plain HTML. - A new paragraph states the styling contract for kind:'react' page source: source is runtime metadata, not build input. Style with inline style objects using hsl(var(--token)) theme colors, and render overlays through ObjectForm with formType "drawer" or "modal" rather than a hand-rolled fixed inset-0 backdrop. Do not author Tailwind utility classes in page source: the console's Tailwind is compiled at build time by scanning the console's own src and there is no safelist, so an authored utility class silently produces no CSS and no error anywhere. os validate reports it as page-source-className-tailwind. (ADR-0065; ADR-0080's 2026-06-30 amendment; see content/docs/guide/react-pages.md.)

.changeset/default-children-retired-5051.md

  • names Registry.ts → packages/core/src/registry/Registry.ts — edited by this change

    If you author plugins against the published register-meta table, drop the key. It is gone from skills/objectui/guides/plugin-development.md, which had been teaching it. A meta that still declares it stays valid: ComponentMetaSchema is a plain z.object, and measured on zod 4.4.3 that STRIPS unknown keys rather than rejecting them — so the key is silently dropped from the parse output instead of failing validation. TypeScript authors get the loud signal instead: all three ComponentMeta declarations (@object-ui/types base.ts and plugin-scope.ts, @object-ui/core Registry.ts) no longer offer it, so re-declaring it is now a compile error.

.changeset/page-source-tailwind-framing-5461.md

  • names content/docs/guide/react-pages.md → content/docs/guide/react-pages.md — edited by this change

    This is a published type surface: the TSDoc ships in @object-ui/types's built .d.ts and is what an author reads on hover over kind. It said a kind:'html' page is "constrained JSX/HTML + Tailwind" — and it links content/docs/guide/react-pages.md, which objectui#5413 has already corrected to say the opposite. Shipped type documentation was contradicting the guide it points readers to.

  • names packages/components/src/renderers/layout/react-page.tsx → packages/components/src/renderers/layout/react-page.tsx — edited by this change

    packages/components/src/renderers/layout/react-page.tsx carries the same correction on its two source comments (the injected-scope note and buildComponentScope), and gains the styling note the file was missing. Those are internal comments — they do not project into any .d.ts and change no export — so they get no entry of their own; there is nothing an @object-ui/components consumer could read in a CHANGELOG and act on.

.changeset/page-source-tailwind-prose-retraction-5469.md

  • names content/docs/guide/react-pages.md → content/docs/guide/react-pages.md — edited by this change

    The tiers themselves are unchanged and every load-bearing claim survives — parse-never-execute, the untrusted-author safety argument for html, and the deprecated 'jsx' alias. Only the styling primitive is corrected, to the wording content/docs/guide/react-pages.md §Styling already uses:

.changeset/palette-canonical-discussion-5495.md

  • names public-blocks.ts → packages/core/src/registry/public-blocks.ts — edited by this change

    The two spellings are one renderer under two names — @object-ui/plugin-detail registers both against RecordChatterRenderer — and the palette deliberately carries one entry per renderer. It carried the wrong one. record:discussion is the canonical spelling: @object-ui/core's public-blocks.ts records record:chatter as that same block "under a Salesforce-familiar name, kept for schemas", app-shell's own page synthesiser (buildDefaultPageSchema) has emitted the canonical name all along, and the console's AI block vocabulary already leaves the alias uncurated for exactly this reason. The palette was the last surface still pointing at the alias, and it did so only because the @objectstack/spec build pinned when the entry was written had no canonical member to offer; the currently pinned build declares both. The author-facing label moves "Chatter feed" → "Discussion". Maintainer ruling 2026-08-22 on objectui#5495.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with 704e05b09 (merge-base with origin/main): 18 file(s) changed outside .changeset/, read against 1573 pending declaration(s) that publish a body (2161 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3065.9 KB 3104.5 KB
Main entry chunk (gzip) 148.3 KB 350 KB
Entry file index-cMpw5cCN.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.57KB 6.15KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.17KB 10.58KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 554.94KB 132.67KB
core (index.js) 9.57KB 3.81KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.91KB 62.28KB
fields (index.js) 259.29KB 65.64KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.65KB 14.69KB
plugin-charts (index.js) 82.37KB 22.57KB
plugin-chatbot (index.js) 198.40KB 47.22KB
plugin-dashboard (index.js) 134.05KB 35.61KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 233.23KB 61.74KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 155.59KB 39.75KB
plugin-gantt (index.js) 169.75KB 41.96KB
plugin-grid (index.js) 218.12KB 59.72KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 114.70KB 28.42KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.91KB 9.05KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 87.83KB 22.01KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 116.21KB 38.14KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 7.50KB 3.05KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.16KB 2.71KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5bd627028968a034355be47487161a66ab00e75d

① Derived judgments

Silent widening of the JSON surface — none, measured. Consumers of getPublicConfigs() / the manifest at head: producers apps/console/dev/manifest-dump.tsx + scripts/dump-public-manifest.mjs, packages/sdui-parser/scripts/gen-manifest.ts (buildArtifacts); readers react-page.tsx buildComponentScope (skips tier === 'html'), compile() (whitelists Object.keys(manifest.components)), generateBlockList / generateDts; page.tsx getJsxManifest reads getKnownTypes(), not the manifest (unchanged); objectui CLI check / validate read the generated KNOWN_SCHEMA_TYPES list and safeValidateSchema (neither reads the manifest); designer palette block-types.ts and the skill/docs generators read neither; 13 test files. Probes at base (merge-base 5ad3b88, impl files) and head, same tests: safeValidateSchema on an h1 / p / div / main node → true at both, code → false at both (the zod HtmlElementSchema arm already admitted them); console html compile (getKnownTypes) → identical verdicts; the injected react-page identifier set, measured through the real page path (typeof X over 297 candidates) → 22 at base, 22 at head, same names, no P / A / Img. Published manifest compile(): h1 refused forbidden-tag at base, clean at head; div refused both; code and kbd refused both. generateBlockList title # SDUI public blocks (59) at both; the 59 curated entries byte-identical and in the same order; sdui-blocks.md first 65 lines identical.

objectstack reader. Lockstep packages/sdui-parser/src/index.ts manifestFromConfigs copies type / namespace / isContainer / inputs only, so tier is never written; compile() whitelists keys; scripts/check-sdui-manifest.mjs checks components[k].type === k, count vs record, sha, pin — no key strictness. Claim "identical until ported" is true. Caveat the body names: check:sdui-lockstep records objectui's sdui-parser/src files (index.ts, types.ts, codegen.ts included), so it goes red at the pin bump until the domain:cli port. Tracked objectstack manifest: 59 at pin 62597c5, no tier key.

The 47, recounted from the registry (console graph, @object-ui/components + register-plugins): 485 known, 208 bare, 178 React intrinsic names, 64 HTML-named = 47 roster + 4 curated (button html image text) + 13 neither (code dialog div form input kbd object progress select summary switch textarea view) — exactly the body's census. Every roster tag is ui-namespaced with its registered inputs; child slot on 42 (34 non-void TAGS, span, 7 sectioning), none on img hr br table label. No HTML-named element passthrough is missing beyond div / code / kbd: the other ten are form controls, field: fallbacks, ui components or plugin-view. span renders its children (probe text rendered).

code / kbd. Bare code is field:code (namespace field, inputs []); rendering {type:'code', children:['hello-probe-text']} yields a Textarea and drops the text; the console html compile accepts code with only not-a-container, the published manifest refuses it. Excluding it is correct and is a knowing narrowing of work item 1's literal list, stated in the body (H1 c, Acceptance notes), the changeset, the roster header and the pinned exclusion ledger. kbd was never on the card. label / table declared as registered: label with text children draws missing-required-prop (error) + not-a-container; the console renders it empty — the refusal is honest.

react-page.tsx. Skip is exact (cfg.tier === 'html'). Leg 2 (skip removed, rebuilt): 62 injected identifiers including A H1 Img Label P Span Table (40 new) and the pin red.

Types. PublicComponentConfig.tier (now Omit OF ComponentMeta, tier plus tier?: ... | 'html'), ManifestComponent.tier?: 'html', RegistryConfigLike.tier — additive published .d.ts widenings in core and sdui-parser; only in-repo type consumer is public-contract.test.ts. minor + Clause-②: yes fits. RegistryComponentMetaExtras.tier unchanged.

Pins. Head: 19 files / 217 tests green (the dev's 14 files + dashboardAuthoredInputs + detailSectionHeaderColorEnum-6955 + my 3 probes). Leg 1 (Registry.ts, parser index/types/codegen, react-page.tsx at base; tests at head): 5 files red, 17 failed / 61 passed of 78 — matches the dev. Leg 2: 1 failed / 8 passed. Leg 3 (stamp dropped, add(tag, { ...cfg })): 4 files red, 10 failed / 71 passed; regen gives 106 unstamped, title (106), 62 injected. Each mutation shown by git diff --stat HEAD and dist witnesses; each restore proven (git diff HEAD empty, 5 blob hashes equal HEAD). registration-uniqueness.test.tsx, html-elements.tsx, div.tsx unchanged; public-blocks.ts header-only.

Regenerated artefacts (repo's buildArtifacts, 16 plugins eager): base 59 / 0 stamped → head 106 / 47, +47 −0, 0 curated changed; div code kbd absent; d.ts 59 → 106 entries, +253 −0; blocks.md gains ## html tier intrinsic elements (47). None of the three is tracked in objectui (git ls-tree: none); the tracked copy is objectstack's, regenerated by the lane at the pin. d.ts + @types/react 19.2.18: 0 conflicting keys at base and head.

② Semver level

minor for @object-ui/core, @object-ui/sdui-parser, @object-ui/components (3 named; all sit in the one fixed group with @object-ui/console, the 4th changed package, tests only). Every changeset sentence measured true, with one loose figure: "200 ledgered rows" is 10 ledger rows whose counts sum to 200 (a 42, div 158) over 3 pages — suggest "200 ledgered findings". The two new guide sentences are true (not injected: measured; the tier: 'html' marker: as implemented). Pending changesets: 12 of 13 stay true; .changeset/6740-flex-is-container.md quotes if (!tag || cfg.isContainer) continue; verbatim, a line this PR changed (claim survives, quote no longer resolves). Title and body: every checked figure reproduces (485/208/178/64, 59→106, +253, 65→119 lines, 17/61/78, 1/8/9, merge-tree 368f01d8).

FAIL item (wording, published text). The PR rewrites the react-page skip line to if (!tag || cfg.isContainer || cfg.tier === 'html') continue; but leaves the OLD line quoted verbatim in three files it edited: content/docs/guide/react-pages.md ("The scope builder skips every container (if (!tag || cfg.isContainer) continue;)" — a published guide), apps/console/src/__tests__/public-contract.test.ts (docblock "builds that scope with …" and the SECTIONING_TAG_UNRULED string the PR edited: "skips EVERY container config (…)"), packages/components/src/renderers/__tests__/container-declaration-census.test.tsx (comment in the it the PR edited), plus untouched layout-containers-declare-containment.test.tsx. Under AGENTS.md rule 11 a cross-file quoted citation must resolve; these no longer do. Replacement text at each site: if (!tag || cfg.isContainer || cfg.tier === 'html') continue; (the guide may add "and every html-tier entry").

③ Boundary flags

  • CI on head: 43 check runs — 39 success, 3 skipped (coverage ×2, dependabot), 1 failure: Inert vi.mock Specifier Check, also failing on main 01700dc (main-red); PR fix(console): mock react-map-gl/maplibre by its bare specifier in the filter-token sweep (objectui#10731) #10749 is open, non-draft, mergeable_state clean, not merged at review time. Test shards 1–8, Test (dist pins), Type Check, Spec Main Shape Gate, Lint, Build & E2E all finished success. PR mergeable_state: unstable (that one check).
  • git merge-tree --write-tree vs main 01700dc: exit 0, tree 368f01d8.
  • Open PRs: 13; none of the other 12 touches any of the 18 files (chore: release packages #5400 release PR checked across 600 files).
  • Draft: true. needs:contract-review: present. Assignee: none.
  • check-governed-merges.mjs --test over the 18 paths, +922/−33: NOT governed, 955 lines under the human-merge threshold, exit 0.
  • Worktree removed; /home/user/objectui checkout untouched (f905090a14, clean).

Implemented-by: claude/issue-10735-html-tier-manifest
Reviewed-by: session_014mXUNuFomfj24w7s1pZzhN

VERDICT: FAIL — one wording defect only: the stale verbatim quote of the react-page skip line in content/docs/guide/react-pages.md and the two edited test files (replacement text above). Behaviour, pins, artefacts, semver and all other published text pass.

The scope builder's skip line gained a third arm (cfg.tier === 'html'),
and four files still quoted the old two-arm line verbatim: the react-pages
guide, two passages in public-contract.test.ts, the container census pin
and layout-containers-declare-containment. Each quote now resolves.
The pending 6740 changeset keeps its own quote as the reading of that change
under a dated note; the 10735 changeset counts 200 ledgered findings, not
rows.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3066.3 KB 3104.5 KB
Main entry chunk (gzip) 148.3 KB 350 KB
Entry file index-DwhZEgfi.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.57KB 6.15KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.17KB 10.58KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 554.94KB 132.67KB
core (index.js) 9.57KB 3.81KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.93KB 62.31KB
fields (index.js) 259.29KB 65.64KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.65KB 14.69KB
plugin-charts (index.js) 82.37KB 22.57KB
plugin-chatbot (index.js) 198.40KB 47.22KB
plugin-dashboard (index.js) 134.11KB 35.62KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 233.23KB 61.74KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 157.08KB 40.13KB
plugin-gantt (index.js) 169.75KB 41.96KB
plugin-grid (index.js) 218.12KB 59.72KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 114.70KB 28.42KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.91KB 9.05KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 87.83KB 22.01KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 116.21KB 38.14KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 7.50KB 3.05KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.16KB 2.71KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 6417e8dec09ad8ab5ccbaaf23a43a1cf13e72312

① Derived judgments

The round-2 delta 5bd62702..6417e8de is one commit (docs(react-pages,tests): quote the react-page skip line as it now reads). Name-status: M ×6 — .changeset/10735-html-tier-manifest.md, .changeset/6740-flex-is-container.md, apps/console/src/__tests__/public-contract.test.ts, content/docs/guide/react-pages.md, packages/components/src/__tests__/layout-containers-declare-containment.test.tsx, packages/components/src/renderers/__tests__/container-declaration-census.test.tsx. Numstat: 1/1, 2/0, 3/3, 2/1, 1/1, 2/2 — 6 files, +11 −8. Content: two docblock/inline comments, one guide sentence, two changeset paragraphs, and ONE test string literal — the SECTIONING_TAG_UNRULED ledger reason (public-contract.test.ts line 567; the line's quoting flipped to double quotes to hold the embedded 'html'). So not strictly comment-only: a ledger string's content moved; its only assertions are toMatch(/#\d+/) and length greater than 40 (lines 664–672), both still met. No executable token moved. Suite choice: NO local re-run (delta is prose, comments and that one ledger string); the run witness is CI on head — Test (shard 1/8) … Test (shard 8/8) and Test (dist pins) all success. The dev's "5 files / 89 tests" stays a claim; nothing contradicts it.

Stale-quote grep at head, whole tree (git grep -n -F "if (!tag || cfg.isContainer) continue;" refs/review/pr-10753): exactly 2 hits, both .changeset/6740-flex-is-container.md (line 27 original paragraph, line 38 dated note). At 5bd62702 the same grep gave 6 hits (the four files plus 6740). The three-arm line is at react-page.tsx:83 and the five re-quoted sites (public-contract.test.ts 536 and 567, react-pages.md 145, layout-containers-declare-containment.test.tsx 131, container-declaration-census.test.tsx 211) plus the 6740 note. Paraphrases stating the container arm only: semantic.tsx:62 ("drops containers from the react scope"), sdui-parser/src/types.ts:160, layout-containers-declare-containment.test.tsx:40 ("JSX scope skips containers"), skills/objectui/guides/plugin-development.md:54,75 ("skipped by the react-page JSX scope"), content/docs/guide/component-registry.md:142 — each true of the container arm and none claims exclusivity; a grep for "every public/curated block is injected" / getPublicConfigs() → PascalCase identifier claims over content/, skills/, docs/, .changeset/, READMEs and the two packages' sources finds nothing. Nothing left to correct.

Frontmatter md5, re-measured: react-pages.md lines 1–4 = 9c5f94a7d13f3333198d913c571b3ecf at base 5ad3b88, at 5bd6270, at head and on main; 6740 frontmatter ('@object-ui/components': patch) = ce888ebde4729564f67bca69a7928743 at all four. Both unchanged, as claimed.

The 6740 dated note sits directly under the paragraph it dates, reads ⚠️ **Dated note, 2026-09-27 — the skip line quoted above has since gained a third arm — objectui#10735.**, scopes "At this change the line read …; it now reads …", and closes "the rest of this entry is kept as the reading of this change" — the same shape as the four notes PR #10533 added on 2026-09-25 (— X has since Y — objectui#N.**, "Later in this same release …"). Precision point only: the form itself originates with objectui#9637 / PR #9734 (2026-09-16); #10533 is the shape being followed, not the origin. Every clause is true at head: base react-page.tsx read the two-arm line, head line 83 reads the three-arm line, getPublicConfigs() appends the roster stamped tier: 'html' (Registry.ts line 1021), the container arm is unchanged.

check-changeset-overwrite source read (scripts/check-changeset-overwrite.mjs): verdict() returns 1 only when OS_CHANGESET_OVERWRITE_ENFORCE === '1' AND findings exist; a modification is reported under case 2 ("CORRECTING a declaration on purpose … Legitimate"). Run read-only against the ref (--base 5ad3b888 --head refs/review/pr-10753): 1 changeset(s) added, 1 modified, 0 deleted, M .changeset/6740-flex-is-container.md, declared at base = declares now @object-ui/components: patch, exit 0. Truly report-only; the body's words match.

The 12 bot-listed dispositions, checked against head (the bot comment was rewritten in place at 05:43:58Z for the round-2 run and lists 12; check-changeset-claims re-run read-only: 18 files outside .changeset/, 12 pending, 2 bodies judged, no negation, exit 0):

  • 5905-inputtype-tombstone, 5905-retire-constraint-keys, 7493-…: sdui-parser/src/index.ts lines 211–224 forward exactly name, type, of, required, enum, binding, description; tier is written at component level (line 209). True.
  • 6067: Registry.ts line 271 ComponentMeta = CanonicalComponentMeta & RegistryComponentMetaExtras; line 177 tier?: 'public' | 'internal'; 'html' only on PublicComponentConfig (line 357). True.
  • componentinput-reexport-4972: line 70 export type { ComponentInput } from '@object-ui/types'. True.
  • default-children-retired-5051: defaultChildren absent from packages/types/src and Registry.ts at head. True.
  • 6764: literal words ("curated public contract") hold and the conclusion holds; the sentence's implicit equation of getPublicConfigs() with the curated contract no longer does. Carried as round 1 judged it (still true); "not corrected" is a defensible reading, not a defect.
  • 9280: gen-manifest.ts diff vs base is its header comment only. True.
  • components-react-page-published-dts-text-5666, page-source-tailwind-prose-retraction-5469: react-pages.md diff vs base is two hunks (lines 93–99 injected-scope addition, 143–145 skip line); §Styling (line 158 on) untouched; "styled inline, not with Tailwind" kept at 147–148. True.
  • page-source-tailwind-framing-5461: react-page.tsx diff vs base = widened cast, third arm, six added comment lines, nothing removed. True.
  • palette-canonical-discussion-5495: public-blocks.ts lines 81–82 still say record:chatter is the same renderer "under a Salesforce-familiar name"; the diff is a header paragraph only. True.
  • 6740: corrected as described (above).

Body vs live, sentence by sentence where changed. Lead: head 6417e8de, "one commit on top of 5bd6270" — true. Round 2 bullet: the four sites, "both public-contract.test.ts passages", layout-containers… "comment-only, previously untouched", 2 grep hits in 6740, md5 9c5f94a7…, "200 ledgered findings" — all reproduce. 6740 bullet: form, scope, md5 ce888ebd…, report-only exit 0 — reproduce. Pins: "Green on head 5bd6270 (round 1)" relabel fine; the round-2 line calls five files "the edited test files" though only three were edited in round 2 (react-page-scope, html-tier-manifest are round-1 files) — loose, minor; suggested "the three re-quoted test files plus react-page-scope and html-tier-manifest". Gates: check-changeset-presence "15 source file(s) of 4 released package(s) … 1 changeset(s)" reproduces exactly (read-only run); changeset:check both lines match CI's Changeset Bump Policy + Changeset Fixed Group Check success; the round-2 text-gate line's counts (8928 files, 0 new citations, 2 bodies, 1/1/0) reproduce or match CI (Control Byte Scan, Line Citation Gate, Internal Docs Link Check success; "17 scan roots" not re-measured). Serial: main 704e05b09d, the nine commits (#10745, #10736, #10574, #10741, #10749, #10734, #10733, #10744, #10752 — "01700dcd7d four commits past base" also correct), tree 0d9bcb93, comm -12 empty — all reproduce. Re-read section: "13 in round 1" matches round 1's record. Open questions (code, div/box, kbd): the body says code "NOT declared", kbd "not declared", div "kept out", box-named refusal "Not implemented here" — none claimed implemented. Aside: no A/A/A ruling comment is visible via REST on the card (3 comments, all fleet-bot) or the PR; nothing in the body depends on one.

② Semver level

Carried from round 1, untouched by the delta: minor for @object-ui/core, @object-ui/sdui-parser, @object-ui/components; Clause-②: yes. The 10735 changeset now reads "200 ledgered findings" (round 1's suggested wording). Round 1's FAIL item is closed: every quote of the skip line outside the 6740 historical note resolves to react-page.tsx:83. Remaining wording notes are minor and non-blocking: (a) "the edited test files" in the round-2 Pins line (replacement above); (b) "the objectui#10533 dated-note form" names the shape's 2026-09-25 precedent, not the form's origin (objectui#9637 / #9734) — acceptable as written.

③ Boundary flags

  • CI on head 6417e8de: 43 check runs, 40 success, 3 skipped (Test (coverage), Test (coverage shard …/4), dependabot), 0 failure — same 43 names as round 1. Inert vi.mock Specifier Check: success (completed 05:43:57Z), green now that fix(console): mock react-map-gl/maplibre by its bare specifier in the filter-token sweep (objectui#10731) #10749 is on main. Test shards 1–8, Test (dist pins), Type Check, Spec Main Shape Gate, Lint, Build & E2E, Changeset Overwrite Report, Changeset Claim Re-read, Governed Surface Queue Guard all success. PR mergeable: true, mergeable_state: behind (base sha 01700dc recorded; origin main is 704e05b).
  • git merge-tree --write-tree refs/review/pr-10753 refs/review/main (main = 704e05b09d, re-fetched at review time): exit 0, tree 0d9bcb933e3b1fba08879f3af83d7846436bd3ab. Merge-base 5ad3b88; main moved 76 files in 9 commits; branch 20 files (+932 −40); intersection empty.
  • Open PRs: 10. None of the nine others touches any of the 20 files except chore: release packages #5400 (changeset-release/main, "chore: release packages", non-draft, mergeable unknown, last updated 2026-09-18), which lists .changeset/6740-flex-is-container.md as removed — release consumption. A modify/delete conflict only if chore: release packages #5400 lands first; if this lands first the release PR regenerates over the note. Ordinary shape; not blocking.
  • Governed predicate: scripts/check-governed-merges.mjs does not exist in the repo; used node scripts/check-governed-queue-guard.mjs --test over the 20 branch paths: ✅ NOT GOVERNED — 20 path(s) checked against 5 governed surface(s); none matched, exit 0.
  • Draft: true. Labels: needs:contract-review present (with package: core, package: components, package: sdui-parser, apps, tests). Assignee: none on the PR (card assignee os-elon-musk; the dev reports the PR assignee write was refused and left to the seat).
  • No worktree created; /home/user/objectui checkout untouched (f905090a14, git status clean); only refs/review/pr-10753 and refs/review/main fetched.

Implemented-by: claude/issue-10735-html-tier-manifest
Reviewed-by: session_014mXUNuFomfj24w7s1pZzhN

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 27, 2026 06:03
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit baac95a Sep 27, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-10735-html-tier-manifest branch September 27, 2026 06:16
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…ding it hides (objectui#10746) (objectstack-ai#10762)

Fixes objectstack-ai#10746
Clause-②: no — a Studio inspector's patch on a type switch; no declared
surface moves

## What changed


`packages/app-shell/src/views/metadata-admin/inspectors/ReportDefaultInspector.tsx`:
the Report type picker's commit is now `commitType`. When the picked
type is `joined`, the patch that commits `type: 'joined'` also carries
`dataset`, `values`, `rows`, `columns`, `chart` and `order` as
`undefined`-valued keys — only those the draft actually holds
(`draft[key] !== undefined`). Any other type commits `{ type }` alone,
as before. The list lives in `JOINED_CONTAINER_CLEARED_KEYS` with the
reasoning beside it; `blocks`, `runtimeFilter` and `drilldown` are never
named.

`order` is a sixth key, beyond the card's five, under the role file's
bounded in-place exemption (same defect class, mechanical, same file,
same gate family, no other claim on the file): the spec's own
`reportForm` hides `order` in the same "Dataset binding" section
(`visibleWhen: "data.type != 'joined'"`), and the INSTALLED spec already
refuses a container `order` on a joined report ("a `joined` report
orders per block — move `order` onto `blocks[]`"), so the same
invisible-key save refusal exists today for `order`. Evidence in H3
below.

New pin file `ReportDefaultInspector.joinedClearsBinding-10746.test.tsx`
(8 tests); changeset `.changeset/10746-joined-report-clears-binding.md`
(`'@object-ui/app-shell': patch`).

## H1 — reproduction on `origin/main` (`1422a920ed`)

The pin file run against the untouched component: `Tests 4 failed | 4
passed (8)`. The defect pin quotes the only `onPatch` argument the type
picker commits today: `AssertionError: expected { type: 'joined' } to
strictly equal { type: 'joined', …(6) }` — the commit is `{ type:
'joined' }` and nothing else. The 4 pins green on base are the
boundary/control pins, which pin what must NOT change.

## H2 — the clear mechanism

`onPatch` is a shallow patch in all three hosts: `ResourceEditPage`
applies it as `handleDraftChange((d) => ({ ...d, ...patch }))`,
`ReportConfigPanel.handlePatch` as `{ ...draftRef.current, ...patch }`,
and `StudioDesignSurface.onPatch` as `setDraft((d) => ({ ...d, ...patch
}))`; each saves through `client.save`, which is `JSON.stringify`. None
offers a delete sentinel, so the one way to drop a key is an
`undefined`-valued key — the spelling this inspector's own `commitChart`
(`chart: next.type ? next : undefined`) and the siblings
(`ActionDefaultInspector`, `ObjectDefaultInspector`,
`DatasetDefaultInspector`'s "Clear all", pinned by its objectui#9372
suite) already use. Measured in the second pin: after the spread the key
is an OWN property holding `undefined` (`Object.hasOwn` true, value
`undefined` — not `null`, not an empty string), and
`JSON.parse(JSON.stringify(committed))` — the shape `client.save` puts
on the wire and the spec parses — has no such property. The spec's
refinement itself skips `undefined` (`if (value === undefined || …)
continue;` in the joined arm), so even the in-memory draft would not be
refused.

- Reach: The clear holds for the patch and for the save that follows it.
The metadata-admin editor rebuilds its draft as the served draft spread
over `layered.effective` (on load, after each save and after publish),
and `effective` is the published layer, so a report whose PUBLISHED
version was bound gets those keys back in the draft after the first
draft save until it is published, and a report already saved `joined`
with stale keys is not repaired on load. Both are the host's
draft-over-baseline merge, objectui#10765.

## H3 — the spec check

Installed `@objectstack/spec` is 17.4.0
(`packages/app-shell/node_modules/@objectstack/spec`). It PREDATES
objectstack-ai/objectstack#20160: `grep 'selects per block'` over its
`dist/` gives 0 hits; the control `grep 'orders per block'` gives 3.
One-off probe with the installed `ReportSchema.safeParse` (not
committed):
- joined + one block: success
- joined + stale `dataset` / `values` / `rows` / `columns` / `chart`:
**success** — 17.4.0 accepts them
- joined + stale `order`: refused, `custom` at `['order']`, "a `joined`
report orders per block — move `order` onto `blocks[]`."
- joined + `blocks: []`: refused at `['blocks']`
- joined + `runtimeFilter` + `drilldown: false`: success
- joined + `dataset: undefined` as an own key: success

So the pins' parse leg measures the `order` half with the installed spec
(before the fix: refused at `['order']`; after: parses), and for the
four selection keys the pins assert ABSENCE and cite the rule read at
objectstack `origin/main` `packages/spec/src/ui/report.zod.ts`:
`JOINED_CONTAINER_SELECTION_KEYS = ['dataset', 'rows', 'columns',
'values']`, one `custom` issue per present key at `path: [key]`, message
"a `joined` report selects per block — move `KEY` onto `blocks[]`, or
delete it; on the container it selects nothing." (`KEY` stands for the
key's own name.) No checkout on this box holds a built spec `dist`
carrying that refusal, and building one was outside this card.

## H4 — edges

- `joined` → non-joined: nothing is restored; the patch is `{ type }`
alone and the author re-binds (pinned).
- non-joined → non-joined: the binding is kept; the patch is `{ type }`
alone (control, pinned).
- `joined` with `blocks[]` present: untouched, same array reference, in
both directions (pinned).
- Undo: there is no undo stack behind the metadata draft.
`ResourceEditPage`'s `Undo2` button is `doDiscardDraft` (ADR-0034:
discard the whole pending draft), not a per-edit undo, and
`handleDraftChange` keeps no history. One undo cannot restore the keys;
switching the type back does not either.
- `runtimeFilter` and `drilldown` survive the switch (pinned): the
joined branch reads both.
- Partially bound report: the patch names only the keys present
(pinned), so `ReportConfigPanel.onFieldChange` sees no phantom clears,
and an unbound report's switch stays `{ type: 'joined' }` (pinned).

## Pins and ablations

Head `3f53c15776`, pin file plus the two existing
`ReportDefaultInspector` suites: `Test Files 3 passed (3) · Tests 37
passed (37)`.

Red on base (`1422a920ed`, component untouched, pin file present): `4
failed | 4 passed` — the defect pin, the own-key/serialised pin, the
parse leg, the partially-bound pin. The other 4 are controls and
boundaries, green on base by construction.

Per-hunk ablations on the committed head, each through objectstack's
`scripts/ablation-replace.mjs` (anchor must hit exactly once; the blob
change is verified on disk; restore proven `blob == HEAD` `a9cb7e9c` and
`git diff HEAD` empty), the prediction written before each run, observed
direction = predicted:
- M1, drop the `joined` guard (`if (nextType === 'joined')` becomes `if
(typeof nextType === 'string')`): predicted CONTROL F red; observed `1
failed | 7 passed` (CONTROL: non-joined → non-joined keeps the binding).
- M2, drop the presence filter (`if (draft[key] !== undefined)
patch[key] = undefined;` becomes unconditional): predicted D and E red;
observed `2 failed | 6 passed` (names only present keys; unbound one-key
patch). The first attempt was a NO-OP the tool refused — the replacement
text was a substring of the anchor, so its on-disk count could not rise
(`1 to 1, a rise of 0`) — and it restored; the leg was re-run with a
distinct replacement.
- M3, add `blocks` to the cleared list: predicted C and G red; observed
`2 failed | 6 passed` (parse leg: the joined report loses its blocks;
CONTROL: blocks untouched).
- M4, drop `order` from the list: predicted A, B and C red; observed `3
failed | 5 passed`.

Assertion spelling: every patch is read with `toStrictEqual`, because
`toHaveBeenCalledWith` / `toEqual` treat an `undefined`-valued key as
absent and would read the defect and the fix alike.

Round 2 (`d4945789f6`, the contract-review wording round: the
changeset's reach sentence and the pin file's three-host comment; no
source line changed — the diff's only non-changeset lines are the
comment lines shown by `git diff -U0`, and `ReportDefaultInspector.tsx`
is untouched): pin file re-run `Tests 8 passed (8)`.

## Gates (local, derived by hand from `package.json` and the workflows —
objectui has no `dispatch-gates.mjs`)

- ① closure build `turbo run build --filter='@object-ui/app-shell^...'
--concurrency=2`: `Tasks: 28 successful, 28 total`, 2m12s, under the
verify lock.
- ② `pnpm --filter @object-ui/app-shell type-check` (`tsc --noEmit &&
tsc -p tsconfig.test.json`): exit 0; `tsc -p tsconfig.test.json
--listFiles` lists the pin file (1 hit among 4673 files).
- ② vitest, the three files above: 37 passed; round 2 pin file: 8
passed.
- `node scripts/check-changeset-presence.mjs`: ✅ (round 2: 2 source
files of 1 released package changed, 1 changeset declared). `node
scripts/check-changeset-no-major.mjs`: ✅. `pnpm check:changeset-claims`:
✅. `pnpm check:pending-changeset-literals`: ✅.
- `pnpm check:control-bytes`: ✅ OK (round 2: 8942 tracked text files).
The role file's control-byte grep over the changed files: no match.
- `pnpm check:new-line-citations`: `0 new citation(s)` (both rounds).
- `pnpm check:test-path-roots`: ✅ OK. `pnpm check:vi-mock-specifiers`: ✅
OK.
- eslint with the package config on the two touched `.tsx` files: exit
0.
- NOT MEASURED locally, CI-owned: `pnpm lint` (repo-wide), the 8 test
shards, `Build & E2E`, `check:i18n-*` (no `t()` key was added or
changed). Round 1 CI on `3f53c157`: 40 success, 3 skipped, 0 failed per
the contract review.

## Serial

Round 2: `origin/main` at `25c7d584e4` (+5 commits over BASE
`1422a920ed`: objectstack-ai#10752, objectstack-ai#10753, objectstack-ai#10708, objectstack-ai#10751, objectstack-ai#10761); none touches the
three files; `git merge-tree --write-tree` exits 0 (clean, tree
`738f2bf534`). No merge commit was needed.

## Acceptance notes

- `chart` is cleared per the triage direction and
objectstack-ai/objectstack#20161 (inert on a joined container); the
installed spec does not refuse it, so its clearing is pinned by absence
only.
- Deviation: the worktree-creating `git fetch origin main` ran against
the shared checkout (the role-file recipe) rather than inside the
worktree as the order asked; `origin/main` did not move (`1422a920ed`
before and after), only the shared checkout's `FETCH_HEAD` was touched.
Every later fetch used a private ref inside the worktree.
- Observation, not filed: `ReportConfigPanel` hosts this inspector too,
merges with the same spread and emits `onFieldChange(key, undefined,
next)` once per cleared key — the documented "field changed" signal,
correct for a live preview.
- The draft-over-baseline merge that re-introduces a published binding
after the first draft save is objectui#10765 (filed by the seat), not
addressed here.

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

---------

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Publish the html tier's registered intrinsic-element vocabulary in sdui.manifest.json (objectstack#20112 ruling A) — div stays deprecated

1 participant