Skip to content

test(metadata-admin): pin BLOCK_CONFIG field names to the schemas their blocks are judged by - #8282

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-8216-block-config-schema-parity
Sep 7, 2026
Merged

test(metadata-admin): pin BLOCK_CONFIG field names to the schemas their blocks are judged by#8282
baozhoutao merged 1 commit into
mainfrom
claude/issue-8216-block-config-schema-parity

Conversation

@claude

@claude claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Part of objectui#8216 — the census and the gate land here; three ledgered names and the exemption bucket keep their own cards, so this does not settle that card on its own.

What this is

A vitest gate in the package (previews/__tests__/block-config-schema-parity-8216.test.ts), not a root check:* script — the apps/console/src/__tests__/registry-inputs-spec-parity.test.ts precedent, and the root package.json scripts block is held by another in-flight PR.

block-config.ts's own header states the rule this mechanises — "keep each field name aligned with the property name the corresponding renderer reads" — and nothing checked it. check-designer-field-key-parity.mjs judges the field / object / permission payloads and never reads this table. What covered BLOCK_CONFIG was a per-block prose pin in block-config.test.ts, each written AFTER a defect was found by hand: objectui#3829, objectui#5212, objectstack#6888, objectui#7772.

The census — measurement first, per the card

22 blocks. Both oracle tables are read whole and DERIVED; neither is hand-listed, so a block that gains or loses a schema changes the verdict with no edit here.

bucket count blocks
spec props (ComponentPropsMap[type], STRICT — refuses by name) 18 every page:* / record:* / element:text / element:image / element:number / element:button, plus object-grid / object-form / object-metric / object-kanban
node arm (@object-ui/types/zod, BaseSchema.passthrough() — declares, never refuses) 4 object-grid, object-form, object-kanban, grid
both 3 object-grid, object-form, object-kanban
neither — explicit exemption 3 element:definition-list, element:repeater, ai:input

Judged population: 19 blocks, 68 top-level controls plus the item controls of 4 array fields.

The node table is derived by reading the type literal off every exported Zod object (104 arms resolved); the spec table has 42 rows. PageComponentType has 34 members and ComponentPropsMap carries 8 more that are not in it, which is why the two tables are read separately rather than joined.

MISSING direction — 3 names, all ledgered

name face verdict card that owns the resolution
object-kanban.limit spec unrecognized_keys naming limit objectui#8172 (already open, exact match)
page:tabs.items[].key spec unrecognized_keys naming key at items.0 objectui#8278 (filed from this census)
object-form.formType node not a declared key of the zod mirror objectui#6152 (already carries this exact row)

Each takes a DIFFERENT correct resolution — one is upstream in @objectstack/spec, one is a control rename plus two locale tables plus a nested read-door decision, one is a packages/types mirror edit — so none is a rider on a tooling PR. That is the position check-designer-field-key-parity.mjs's own header takes for the same reason. Two of the three were already owned by open cards before this run; the dedup that established that is recorded below.

RETIRED direction — 0 names

No control writes a tombstoned key. Both tombstone mechanisms are exercised as instruments rather than assumed: PageHeaderProps.icon (ADR-0087 D2, a listed key replaced by z.never()) and ObjectKanbanSchema.groupField (retirementTombstone()), each paired with a live neighbour that must read back as NOT tombstoned.

REQUIRED direction — measured, reported, NOT gated

The card asks for it and asks that it not be gated (objectui#7772's triage records "an inspector need not expose every declared key" as a product decision). Measured across all 25 (block, face) rows:

  • top level: 0 blocks have a required schema key with no control;
  • nested: 2children is required on page:tabs.items[] and on page:accordion.items[] and has no item control in either. That key is the canvas's nesting slot, written by dragging a block into the tab or panel, not by the inspector. It is the one row a future REQUIRED gate would have to exempt.

Non-vacuity for that measurement: 12 of the 25 rows DO carry required keys (object-form@node: objectName/mode, record:related_list@spec: objectName/relationshipField, and ten more), so the zero is a reading rather than a broken probe.

Ablation — the acceptance criterion

Run on the COMMITTED tree at 27e838489, mutating block-config.ts, one leg at a time, each restored with git checkout HEAD -- ABSOLUTE_PATH under a trap ... EXIT INT TERM. No build step is involved: the gate imports ../block-config relatively and the two oracle packages through the root vitest alias table, so nothing resolves through a dist. Exit codes captured by redirect, never through a pipe.

leg mutation on-disk proof gate exit the gate's own naming
0 — positive control none tree byte-identical to HEAD 0 13 passed
A — MISSING, spec face page:card.bordered renamed to borderedTypo removed-anchor 0, injected-anchor 1 1 page:card::borderedTypo@spec [MISSING] not a declared key (schema declares 7 keys)
B — RETIRED, spec face page:header regains the tombstoned icon removed-anchor 1, injected-anchor 1 1 page:header::icon@spec [RETIRED] declared but tombstoned — the parser refuses every value by name
C — RETIRED, node face object-kanban regains groupField removed-anchor 1, injected-anchor 1 1 object-kanban::groupField@node [RETIRED] … plus object-kanban::groupField@spec [MISSING] …

Leg B is the one that proves the retired channel is not subsumed by the missing one: icon is a LISTED key of PageHeaderProps, so the missing-direction check stays green on it and only the tombstone judge can catch it.

Restore verified per leg by BLOB HASH, not by an exit code: git hash-object on the file equals HEAD:…block-config.ts = 25163b0022b2f647cef99141d00f771b65babcc3 after every leg, and git diff HEAD is empty. An empty hash reads as failure, not as "nothing to compare".

Gates, with exit codes

gate exit
the new test alone 0 (13 passed)
pnpm exec vitest run packages/app-shell/src/views/metadata-admin/previews/ 0 (48 files, 557 tests)
pnpm --filter '@object-ui/app-shell^...' build (dependency closure first) 0
pnpm --filter @object-ui/app-shell type-check 0
pnpm --filter @object-ui/app-shell lint 0 (0 errors, 2920 pre-existing warnings package-wide; 6 of them no-explicit-any in the new file, the same pattern the neighbouring block-config.test.ts carries)
node scripts/check-changeset-presence.mjs 0
node scripts/check-changeset-no-major.mjs 0
node scripts/check-governed-queue-guard.mjs --test both changed paths 0 — NOT GOVERNED
pnpm check:control-bytes 0 (6580 tracked files)
pnpm check:unreferenced-sources 0

type-check really does read the new file: packages/app-shell/tsconfig.test.json includes src/**/*.test.ts. pnpm lint:root deliberately ignores packages/*/**, so the package's own lint target is the one that covers this file and it is the one that was run.

packages/app-shell is @object-ui/console plus two examples' dependency, so turbo ls --affected names four packages; nothing imports a test file, so the edge is the dependency graph rather than this change.

Note on the toolchain: pnpm --filter X build --concurrency=2 forwards the flag to the package script and tsup dies on it. The working spelling is pnpm --workspace-concurrency=2 --filter X build, before the filter.

Findings filed from the census (unassigned, finding only)

Deduped against all 452 open issues listed over REST plus a local grep, with BLOCK_CONFIG as the control term that had to hit and did.

  • objectui#8278 — page:tabs items write key; PageTabsProps declares value and the renderer reads only value, falling back to an index-derived id. The author's stable tab identifier lands under a key nothing reads, which is the objectui#2257 addressability problem returning through the designer.
  • objectui#8279 — element:definition-list items write label / value; DefinitionListRenderer reads term / description. Every definition list built in the designer renders an empty term and a literal em-dash for every row, with items.length non-zero so the renderer's own empty state never appears. Found by reading, precisely because this block is one of the three the gate cannot see.
  • objectui#8280 — ai:input has a curated panel and four translation entries while being absent from the palette, from PageComponentType, from ComponentPropsMap and from every real renderer.
  • objectui#8281 — the structural card behind all three exemptions: three BLOCK_CONFIG blocks resolve to no runtime-judgeable schema on either face, with the four routes laid out. Every exemption row in this PR cites it.

Two candidate findings were NOT filed because the dedup found them already owned: object-kanban.limit is objectui#8172 verbatim, and object-form.formType is a named row in objectui#6152's UnmirroredDeclared table (objectql.zod.ts#ObjectFormSchema).

Deviations from the dispatch, stated

  1. block-config.ts is NOT edited; all three names are ledgered instead. The dispatch allowed closing a small gap (three is small). None of the three is closable inside the declared file surface: limit needs @objectstack/spec, formType needs packages/types, and page:tabs.items[].key needs metadata-admin/i18n.ts (the field name is the last segment of its translation key, so a rename moves an entry in both locale tables) plus a decision about documents already carrying the old spelling nested inside items[], which stripRetiredBlockProps does not reach. Ledgering with a card each is this repo's own precedent for exactly that situation.
  2. A changeset was added, outside the declared surface, because check-changeset-presence.mjs demanded one for a src/ file of a released package. It has EMPTY frontmatter — the explicit no-release declaration AGENTS.md calls a first-class pass. No skip-changeset label was applied; in this repo that label is inert and the empty changeset is the mechanism.
  3. Both faces are gated, with distinct violation ids. The card names "the node schemas"; the spec props shapes are the only face that can REFUSE, and the node arms are the only face carrying retirementTombstone(). Dropping either would make one of the two directions unmeasurable, so both are read and each violation says which face reported it.

What this PR does not claim

PageComponent.properties is z.record(z.string(), z.unknown()), so a page document carrying a refused block prop still PARSES — the card measured that and it is still true. ComponentPropsMap[type] is reached by dispatching on the sibling type, which is what the platform's component-props lint does and no schema can express. So a spec-face violation here means "the platform's component-props lint reports this key", not "the save returns 422".

Base f20e57d62; head 27e838489. Session session_01FhBNJcLRZLe8M87VcUgpKr.


Generated by Claude Code

…ir blocks are judged by

`previews/block-config.ts`'s header states the rule — "keep each field `name`
aligned with the property name the corresponding renderer reads" — and nothing
mechanical checked it. `check-designer-field-key-parity.mjs` judges the field /
object / permission payloads and never reads this table; what covered
BLOCK_CONFIG was a per-block prose pin in `block-config.test.ts`, each written
after a defect was found by hand (objectui#3829, objectui#5212,
objectstack#6888, objectui#7772).

Two derived oracles, because the table is keyed by designer block type and that
is not one vocabulary: `ComponentPropsMap[type]` from `@objectstack/spec/ui`
(strict — the face that refuses by name) and the `@object-ui/types/zod`
component arms indexed by their `type` literal (passthrough — the face carrying
`retirementTombstone()`). Neither table is hand-listed. Both directions are
judged: a name the oracle does not declare, and a name it still lists but
tombstones. Nested array item controls are judged against the array element
shape, and an unreadable element shape is reported rather than skipped.

Census over 22 blocks: 18 resolve to a spec props shape, 4 to a node arm, 3 to
neither and carry a self-deleting exemption (objectui#8281). Three live
violations are ledgered with the card that owns each resolution —
`object-kanban.limit` (objectui#8172), `page:tabs.items[].key` (objectui#8278),
`object-form.formType` (objectui#6152) — and the ledger ratchets both ways.
Zero blocks have a required schema key with no control; that direction is
reported on the card, not gated (objectui#7772 records it as a product
decision).

Part of objectui#8216

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

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3472.1 KB 3512.7 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index--2K1PeVY.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) 15.67KB 5.75KB
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) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
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.41KB 1.23KB
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.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
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) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
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) 497.99KB 113.90KB
core (index.js) 7.20KB 2.88KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 189.15KB 52.56KB
fields (index.js) 243.14KB 61.38KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
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) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
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) 5.12KB 1.74KB
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) 15.16KB 3.68KB
plugin-calendar (index.js) 48.03KB 13.29KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 193.54KB 46.04KB
plugin-dashboard (index.js) 131.43KB 34.43KB
plugin-designer (index.js) 213.21KB 43.63KB
plugin-detail (index.js) 248.56KB 63.89KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.58KB 56.63KB
plugin-kanban (index.js) 52.83KB 14.63KB
plugin-list (index.js) 113.38KB 27.73KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.46KB 20.80KB
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) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
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.74KB 1.41KB
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) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
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 (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

@claude

claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT (#8216, measurement + gate) — reviewed on the tree at head 27e838489 against origin/main fc32921aa, read 2026-09-07T08:17Z; domain:devx @ objectui seat, PM session session_01FhBNJcLRZLe8M87VcUgpKr. Merges clean.

Verified from the diff, not the report:

Routing decisions (the dev's two open questions): the card CLOSES when this lands — it is a tooling card, the tool exists and is ablation-proven, every ledgered name has its owning card and the ledger cannot lose one silently. The REQUIRED-direction gate is filed as its own finding at landing (it needs #7772's product decision restated before a gate can hold).

Next: CI convergence → flip ready → post-flip guard → auto-merge SQUASH → queue confirmation → landing probe (the test file present on re-fetched origin/main with its LEDGER and EXEMPT tables; control: block-config.test.ts still present) → LANDED here and on #8216, then close the card.


Generated by Claude Code

@claude

claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Armed: PR #8282 flipped ready at 2026-09-07T08:29:40Z on head 27e838489 after CI converged all green (31 checks, read 08:28:55Z); post-flip Governed Surface Queue Guard completed/success at 08:29:56Z; auto-merge SQUASH enabled; queued (timestamp in the REST timeline). Landing stroke follows the merge: probe on re-fetched origin/main (the gate test present with its LEDGER and EXEMPT tables; control: block-config.test.ts still present), LANDED here and on #8216, then the card closes and the REQUIRED-direction gate is filed as its own finding. — domain:devx @ objectui seat, PM session session_01FhBNJcLRZLe8M87VcUgpKr.


Generated by Claude Code

Merged via the queue into main with commit 72b84e6 Sep 7, 2026
34 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-8216-block-config-schema-parity branch September 7, 2026 09:38
@claude

claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

LANDED: merged at 2026-09-07T09:38:06Z as 72b84e62c (squash — one parent, via the merge queue, after its group was rebuilt behind #8276's 09:36Z merge). Content probe on re-fetched origin/main at 998d846aa (read 09:38Z): previews/__tests__/block-config-schema-parity-8216.test.ts present with its EXEMPT table and the six card-bearing rows (3 exemptions + 3 ledger rows); control block-config.test.ts present. Merge commit is an ancestor of origin/main. Card #8216 closes on this landing (routing decision in the ACCEPT); the REQUIRED-direction gate is filed as its own finding. — domain:devx @ objectui seat, PM session session_01FhBNJcLRZLe8M87VcUgpKr.


Generated by Claude Code

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.

2 participants