Skip to content

feat(plugin-dashboard): a widget's component envelope draws a metric-card and nothing else (objectui#12254) - #12265

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-12254-envelope-draw-retire
Oct 12, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-12254-envelope-draw-retire

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #12254
Clause-②: no (narrowing)

Both dashboard surfaces now draw a widget's legacy component envelope by the rule the authoring faces have stated since objectui#12163: a metric-card node draws as before, and any other node draws the existing retired-format placeholder and sends no query. This executes the follow-on that ruling C on objectui#12163 named (6121298298, taken with letter A, 6125043394), as triage 6125692023 directs: a read-only census first, then, on a count of 0, the retirement.

Census of stored use (read-only)

The census reads metadata as stored, and source. Each store is read structurally: a TypeScript-AST and JSON/YAML walker visits every widgets array element that carries component, every component: { type … } object literal anywhere, and every type: 'custom'. A control count of widgets arrays reached is given for each store, so a 0 is a reading and not a walk that found nothing.

Store What was read Non-metric-card envelope nodes outside tests and docs
objectui: examples/, apps/, templates, fixtures origin/main 0cd322d7, 7655 json/yaml/js/ts files (the 87 unparsed are JSONC tsconfig files) 0. Control: 212 widgets arrays in 125 files reached, the 9 examples/schema-catalog dashboards among them. Every envelope hit is a test in packages/types or packages/plugin-dashboard.
objectstack: examples/, templates, seeds origin/main e49aaec7, 9494 files 0. Control: 177 widgets arrays in 76 files, among them the app-crm, app-showcase (3) and app-todo dashboards and platform-objects' system_overview.
objectstack-ai/hotcrm dashboards depth-1 read-only clone at 0f84c9c7, 570 files, nothing executed 0. Control: 5 dashboards, 62 widget entries; the shared widget factories return funnel and metric widgets with no component.
cloud: seeded and marketplace packages UNREAD. git clone --depth 1 answered "could not read Username for 'https://github.com': terminal prompts disabled"; the REST read answered 403 "GitHub access to this repository is not enabled for this session". UNREAD, not 0.
The dashboard designers writer code at 0cd322d7: plugin-designer's DashboardEditor; Studio's DashboardDefaultInspector, DashboardWidgetInspector and DashboardPreview 0. No writer emits component, and no type picker offers custom. They write id, title, type, layout, colorVariant, dataset, dimensions, values and filterBindings; an update spreads { ...w, ...updates }, which carries a stored key through and never authors one.
Studio's raw JSON source view the save door, objectstack origin/main e49aaec7 Cannot store a component key at all: saveMetaItem validates against @objectstack/spec's DashboardSchema, whose widget is a strictObject that refuses component by name (QUARANTINED_WIDGET_KEYS, pinned by the spec's dashboard.test.ts "quarantine branch").
Stored databases on this container find for *.db / *.sqlite* outside node_modules none present

An inference, not a read: a cloud package loaded through defineStack or saved through saveMetaItem is parsed by the same strict spec widget, which refuses a component key whatever node it holds. That bounds the unread store; it does not count it.

What changes

  • isRetiredEnvelopeNode (legacyRetiredWidget.ts), the one predicate both surfaces read, now answers true for every envelope node but a metric-card: a node whose type is not a member of DASHBOARD_COMPONENT_WIDGET_TYPES (the closed list the zod faces route on), a node with no type, a string or a number. It used to name object-metric alone (objectui#11466), which is now one case of the rule. An empty envelope still holds no node and draws by its family.
  • DashboardRenderer and DashboardGridLayout: the envelope branch hands the renderer toDashboardNodeType(node), only ever a metric-card now. The toRenderableSchema bridge for a primitive is gone from both, since a primitive in the envelope now draws the placeholder.
  • Made false by this change, so repaired in it: DashboardRenderer drew a custom widget with no component as a prompt to "set component to a UIComponent schema". A UIComponent other than a metric-card now draws the retired placeholder, so the prompt sent authors to a dead end. That widget now draws unsupportedWidgetSchema('custom'), which DashboardGridLayout already drew for it. No placeholder, prop, export, spec key or i18n key is added; one bespoke placeholder is removed.
  • The filter broadcast's doc comment drops the envelope's object-chart and object-data-table, which no longer draw there. The custom dispatch family's doc says what it draws now.
  • Docs: content/docs/plugins/plugin-dashboard.mdx and the package README state the draw rule where they stated that the surfaces still drew any stored node.
  • Changeset: @object-ui/plugin-dashboard minor, BREAKING stated in prose with FROM and TO examples.

@object-ui/types and the zod faces are not touched.

Pins

New: packages/plugin-dashboard/src/__tests__/envelopeDrawRetired-12254.test.tsx, run on both surfaces:

  • The retired draw. Nine envelope inputs: a custom widget's text, object-chart (with the filter bar set), object-data-table, card, a node with no type, the namespaced plugin-dashboard:metric-card node key, a string, a number and true. Each draws LEGACY_RETIRED_WIDGET_SCHEMA by identity, puts nothing of the node on screen and sends no find, aggregate or queryDataset. Each is also refused at component by the zod face, so the draw and authoring agree.
  • A typeless envelope (holding an object-chart, a number, or true) keeps its card heading around the placeholder.
  • A custom widget with no component draws unsupportedWidgetSchema('custom').
  • CONTROL: a metric-card envelope (typeless, and in a custom widget) draws its figure under the namespaced node key, exactly { ...card, type: 'plugin-dashboard:metric-card' }, and the zod face accepts it.
  • Unchanged: the object-metric envelope pin objectui#11466 wrote (inlineObjectMetricRetired-11525.test.tsx), and both surfaces' legacyRetired suites.

Fixture triage, by what each fixture pinned:

  • inlineObjectMetricRetired-11525.test.tsx: its last block pinned that the filter bar still scoped an envelope's object-chart and object-data-table, which is the branch this change removes. The block is replaced by the new file's cases, and a dated note explains the change.
  • dashboard-producer-node-types-11514.test.tsx: section 3 pinned the toRenderableSchema bridge for a primitive in the envelope, which this change removes. It is replaced the same way.
  • nodeSlotDeclaration-11170.test.tsx: it used a text node only as a probe of the widgets[].component node slot. It is re-spelled to a metric-card carrying the mark. The slot declaration stays true, because the renderer still hands the slot's node to SchemaRenderer.

Reverse verification

Both legs ran through ablation-replace.mjs (WRAP mode) from committed dd076421. The expected direction was stated before each run, in the new file's header.

  • Leg 1, the old predicate planted back (object-metric only): Tests 24 failed | 38 passed (62). Every retired-draw case and every heading case is red on both surfaces. The four CONTROLs, both custom-no-component cases and the whole 11525 file are green. The anchor went from 1 hit to 0, and the blob from fd792f2d to e3670325. On restore, the blob is again fd792f2d, equal to HEAD, and git diff HEAD is empty.
  • Leg 2, the old custom prompt planted back: Tests 1 failed | 29 passed (30). The red one is the renderer's custom-no-component case; the grid never had that branch. The blob went from 3a7628c5 to 74db5575, and on restore it equals HEAD with git diff HEAD empty.

The tests import the subject by relative source path, so no dist/ is involved.

Verification at dd076421

  • pnpm exec vitest run --maxWorkers=2 packages/plugin-dashboard/: Test Files 175 passed (175), Tests 1749 passed | 6 skipped (1755).
  • pnpm --filter @object-ui/plugin-dashboard type-check (tsc --noEmit && tsc -p tsconfig.test.json): exit 0. The test program lists all four touched test files.
  • pnpm --filter @object-ui/plugin-dashboard lint (eslint .): exit 0, 0 errors.
  • Exit 0, run after the scoped doc-gate build the gates name: check:doc-snippets (788 of 788 blocks judged, 0 failed), check:doc-examples, check:readme-exports, check:doc-fences, check:doc-json, docs:check-links, check:doc-types, check:doc-example-ids and check:doc-example-readers.
  • Exit 0: check:control-bytes, check:new-line-citations (0 new), check:vi-mock-inherit, check:vi-mock-specifiers, check:vi-mock-override-shape, check:test-path-roots, check:changeset-claims, check:pending-changeset-literals, check:unreferenced-sources, check:phantom-deps, check:unused-deps, check:component-surface-parity, check:handler-key-reads, check-changeset-no-major.mjs and check-changeset-presence.mjs.
  • check-governed-queue-guard.mjs --test over the diff: NOT GOVERNED.
  • NOT MEASURED: check:eager-closure. Reason: it reads apps/console/dist/eager-closure.json, a console build this run did not make. The diff adds no import that is not already in the package's closure.

A contract-tier review is owed before the queue: this narrows what the published renderer draws, as the claim records.

Acceptance notes

  • custom stays in objectui's widget vocabulary (DASHBOARD_WIDGET_TYPE_EXTENSIONS). After this change a custom widget draws a metric-card envelope or a placeholder, nothing else. Whether the custom type itself retires is the D question's other half, and it was not measured here. Carrier: none.
  • unsupportedWidgetSchema's wording, "chart type is not supported yet", now also covers custom, which is not a chart family. That is existing wording; this change does not touch it. Carrier: none.

Generated by Claude Code

…ic-card` and nothing else (objectui#12254)

Both dashboard surfaces now draw the legacy `component` envelope by the
rule the authoring faces have stated since objectui#12163: a `metric-card`
node draws as before, and any other node (another type, no type, a
primitive) draws the existing retired-format placeholder and sends no
query. One predicate decides it for both, `isRetiredEnvelopeNode`, read
from `DASHBOARD_COMPONENT_WIDGET_TYPES`; the `object-metric` case
objectui#11466 retired is one case of it.

A read-only census of stored use found no producer of a non-`metric-card`
envelope node outside tests and docs.

A `custom` widget with no `component` on `DashboardRenderer` drew a prompt
to set `component` to a UIComponent schema, which this made false; it now
draws the labelled placeholder `DashboardGridLayout` already drew.

Claude-Session: https://claude.ai/code/session_01G5TSaEwbEQsKMjhzTB4rEd
Co-authored-by: Claude <noreply@anthropic.com>
…the placeholder being a text node too (objectui#12254)

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 71 chunks) 3129.3 KB 3204.6 KB
Main entry chunk (gzip) 73.1 KB 350 KB
Entry file index-C5lP0oDp.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) 20.69KB 7.62KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
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.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
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) 41.19KB 11.12KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
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.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 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) 588.51KB 141.71KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 244.49KB 68.27KB
fields (index.js) 280.00KB 71.39KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
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.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.26KB 11.71KB
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) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
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.04KB 3.92KB
plugin-calendar (index.js) 54.31KB 15.86KB
plugin-charts (index.js) 84.72KB 23.27KB
plugin-chatbot (index.js) 201.72KB 48.07KB
plugin-dashboard (index.js) 143.78KB 38.84KB
plugin-designer (index.js) 233.77KB 50.02KB
plugin-detail (index.js) 251.35KB 66.36KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 178.36KB 46.26KB
plugin-gantt (index.js) 178.63KB 45.19KB
plugin-grid (index.js) 255.09KB 70.91KB
plugin-kanban (index.js) 53.23KB 16.71KB
plugin-list (index.js) 123.35KB 31.54KB
plugin-map (index.js) 27.24KB 9.03KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 45.65KB 12.81KB
plugin-timeline (index.js) 39.21KB 11.85KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 92.11KB 23.27KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
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) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.81KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
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) 1.13KB 0.65KB
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) 5.78KB 2.70KB
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 (node-slots.js) 7.18KB 2.34KB
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) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
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: dd07642179c23c1196e776492c7d15a889728672
Local-runs: none

PR #12265 (card #12254), head dd07642179c23c1196e776492c7d15a889728672, base main, merge-base 0cd322d7, net diff 11 files (+345 / −144). Read: the card body and all four comments (triage 6125692023, claim 6126370732, dev report 6127106358, seat answer 6127141666), ruling C 6121298298 and the letter-A record 6125043394 on objectui#12163, the PR body, file list, its one comment (the CI budget report; no review comments), the net diff against main, and the 33 check-runs on the head. The spec claim was read in the objectstack checkout (packages/spec/src/ui/dashboard.zod.ts). Nothing was built, run or re-run.

Check-runs on the head: 30 completed success, 3 skipped by design on a PR (Test (coverage), Test (coverage shard), dependabot), 0 failures, none in progress. Among them Repo Gates, Lint, Type Check, Test plus shards 1–12 and Test (dist pins), Build & E2E, Build Docs, Bundle Analysis, the four Changeset * gates, Governed Surface Queue Guard, Line Citation Gate, and the advisory Spec Main Shape Gate (completed, success). The one PR comment is CI's Console Performance Budget: eager closure 3129.3 KB of a 3204.6 KB budget, PASS — which answers the dev's one NOT MEASURED local gate (check:eager-closure).

① Derived judgments

The published surface of @object-ui/plugin-dashboard is its exports map (. only) plus src/index.tsx; neither changes, and index.tsx exports none of isRetiredEnvelopeNode, LEGACY_RETIRED_WIDGET_SCHEMA, entryComponent, toDashboardNodeType or unsupportedWidgetSchema. @object-ui/types and @object-ui/react are untouched (diff stat 0). So every contract change here is a change in what the two published renderers DRAW for a stored input. Each one, judged:

  1. The predicate, isRetiredEnvelopeNode (legacyRetiredWidget.ts) — right. FROM a two-member retired set (object-metric, plugin-dashboard:object-metric) TO: a truthy envelope node is retired unless its type is a member of DASHBOARD_COMPONENT_WIDGET_TYPES, which is ['metric-card'] on this head (packages/types/src/complex.ts:2182). That is the authoring accept set by construction: the zod slot arm is z.enum(DASHBOARD_COMPONENT_WIDGET_TYPES) with every other node sent to WidgetComponentSlotRefusal (zod/complex.zod.ts:1342, :1412), and the TypeScript twin is component?: DashboardWidgetSlotComponentSchema (complex.ts:2310). One constant, read by both faces and the draw, so they cannot drift. A falsy envelope still answers false, as before (a falsy component was never an envelope on either surface).
  2. DashboardRenderer envelope draw — right, a narrowing. A non-metric-card node now returns the EXISTING LEGACY_RETIRED_WIDGET_SCHEMA object (content "This widget uses a retired data format. Edit it to bind a dataset.", which the changeset quotes verbatim) and sends no query. This is the retirement the card owes, to the existing placeholder. ⛔ No new placeholder: holds.
  3. DashboardGridLayout envelope draw — right, the same narrowing, through the same imported predicate.
  4. The toRenderableSchema bridge removed from both surfaces — right. It bridged the removed SchemaNode arm of the slot type (gone since PR objectui#12229): a string, number or true in the envelope used to draw as text; now each is a truthy non-object, type undefined, retired, so it draws the placeholder under the card heading. Both faces refuse a primitive at component; pinned for all three primitives on both surfaces. After the if (authoredComponent) narrowing the node is a DashboardWidgetSlotComponentSchema, which SchemaRenderer takes with no bridge (Type Check success). toRenderableSchema stays exported from @object-ui/react with many other callers; nothing is orphaned.
  5. The namespaced node key plugin-dashboard:metric-card stored in the envelope — right, and the one edge worth naming. It drew before (the registry has that key) and is retired now, because the slot's accept set is the widget-vocabulary spelling metric-card that both faces take; the namespaced key is the renderer's internal registration that toDashboardNodeType produces, not an authorable spelling. Pinned, and the pin also asserts the zod face refuses it. The draw equals authoring here too.
  6. A bare metric node in the envelope — right, covered by the rule, not pinned by name. toDashboardNodeType used to move it onto plugin-dashboard:metric and draw the inline metric; metric is not a member of the one-element list, so it now draws the placeholder, which is what the zod enum refuses it as. Consistent; the changeset's "any node other than a metric-card" covers it.
  7. The filter-bar broadcast no longer reaches an envelope object-chart or object-data-table — right, a consequence of 2. Neither node reaches isFilterableComponentSchema any more. Declared in the changeset; the 11525 block that pinned the broadcast on the envelope is replaced with a dated note, and the new file pins the retirement WITH the filter bar set so the broadcast's absence is read, not assumed.
  8. DashboardRenderer's bespoke custom-with-no-component prompt removed — right; a drawn-output change outside the entryComponent branch, judged in ③ as the dev's own flag. The prompt told an author to "set component to a UIComponent schema", which after items 1–5 is exactly what authoring refuses and the draw retires, so the prompt became false in this change. It now falls through to unsupportedWidgetSchema('custom') (content "「custom」chart type is not supported yet", quoted verbatim in the changeset), which DashboardGridLayout already drew for that input on main (the grid has no custom arm; it ends in unsupportedWidgetSchema(widgetType)). One placeholder removed, none added; the two surfaces now agree. Ruling C's "D not taken" is respected: custom stays in WidgetDispatchFamily and DASHBOARD_WIDGET_TYPE_EXTENSIONS, and a custom widget holding a metric-card still draws the card (CONTROL pinned on both surfaces).
  9. Empty or falsy envelope — unchanged, right. The widget draws by its family, as before.
  10. A metric-card lacking value in the envelope — unchanged, out of this card. The predicate judges type only, so such a node draws as it did before while the zod face refuses it (objectui#11483). This PR neither widens nor narrows that; it is the pre-existing type-level accept set the card asked the slot to draw.
  11. Docs (content/docs/plugins/plugin-dashboard.mdx, packages/plugin-dashboard/README.md) — right. The paragraph that said the surfaces "still draw a stored envelope as they always did" now states the rule the code implements; "the labelled placeholder described next" resolves to the following paragraph's "「type」chart type is not supported yet". WidgetDispatchFamily's custom docblock is comment-only.
  12. Pins — right. envelopeDrawRetired-12254.test.tsx: 30 cases on two surfaces (9 retired inputs, 3 heading cases, the custom-no-component case, 2 CONTROLs, each ×2). Each retired case asserts, in one test, that the surface hands SchemaRenderer the placeholder BY IDENTITY, that nothing of the node reaches screen or renderer, that find/aggregate/queryDataset are not called, AND that the zod face refuses the same widget at component — so the draw's accept set and authoring's are pinned equal, not merely described equal. Fixture triage: 11514 §3 (primitive bridge) and the 11525 broadcast block pinned the removed branch and are retired with dated notes; 11170 is re-spelled to a metric-card and the slot declaration stays true. Reverse verification (two WRAP legs, direction stated in the file header before the run, restore equals HEAD) is read from the PR body; it is the dev's own run, not re-run here.
  13. The ruling's conditions in the diff — hold. ⛔ No new placeholder (one removed). The census is read-only and names each unread store: cloud is a row of its own, "UNREAD, not 0", with both refusals quoted (the clone's "terminal prompts disabled" and the REST 403), and every read store carries a control count of widgets arrays reached, so a 0 is a reading. A count above 0 would have shipped no code: every readable store read 0 outside tests and docs. The spec claim the PR makes is true on the objectstack checkout: QUARANTINED_WIDGET_KEYS is ['component', 'data'] in WIDGET_GUIDANCE_SETS (dashboard.zod.ts:188), fed as guidanceSets to the strictObject that is DashboardWidgetSchema (:1081), so a spec-valid stored widget cannot carry component at all.

No derived judgment is wrong. Nothing widens.

② Semver level

  • Changeset: .changeset/12254-envelope-draw-retire.md declares '@object-ui/plugin-dashboard': minor — the one released package whose source the diff touches (packages/plugin-dashboard/src/**; content/docs is not a package). The body states BREAKING in prose with a FROM example (an envelope object-chart drawing, filter-scoped) and a TO example (the placeholder, plus the two spec-form rewrites), and lists the two further draw changes (the custom prompt, the broadcast). This is the shape objectui's policy requires (AGENTS.md 版本号策略: ⛔ no major in a changeset; objectui's own breaking changes ship as minor with the breaking semantics in the body). Changeset Bump Policy, Changeset Declaration, Changeset Claim Re-read and Changeset Fixed Group Check are all success on the head. Level: right.
  • The declaration line: the PR body's second line reads Clause-②: no (narrowing) at line start, the value no first after the colon, the arm (narrowing) — one of the four legal combinations the fleet's one reader (scripts/pm/clause2-line.mjs) accepts, and the one that means "not a widening, but breaking". It matches the diff: no accept set or public export widens, and what the two published renderers draw narrows to the authoring accept set. The claim 6126370732 declares the same value and arm, and its condition ("holds only if the census reads 0 and the draw retires") is the case that shipped. Right.

③ Boundary flags

  • open_questions[0] — the dispatch order's clause 2 vs clause 3 on the UNREAD cloud store; seat took A (6127141666). Answered; A is right, and this record does not inherit it, it re-derives it. The governing text is the ruling, not the dispatch order: triage 6125692023 both lists cloud as a store to read and prescribes the fallback in the same breath — "A store it cannot read is named as unread" — and then "a count of 0 retires the draw". Clause 3 of the seat's own order ("an unread store: write no code") is the seat's over-reach, as the seat itself concedes; the dev named the conflict instead of choosing silently, which is the correct move, and held the PR at draft. What remains is the residual an UNREAD row leaves: a cloud-stored dashboard written around spec validation. Bounded, not counted: (a) spec's DashboardWidget is a strictObject refusing component by name (verified, item 13), so any package that passed defineStack or saveMetaItem cannot carry the envelope; (b) objectui's own faces have refused the node since PR objectui#12229 landed as 6fb281d6; (c) a surviving stored envelope degrades to the existing placeholder and sends no query — visible, not a crash and not silent data. The seat recorded the spec reading as an inference, not a read, which is the honest spelling. Not escalated. A seat with cloud read access may append its count to the card; nothing in this record waits on it.
  • Dev deviation: one edit outside the entryComponent branch, inside a claimed file. Judged in ① item 8: a defect this change created, repaired in it, ⛔ no new placeholder, declared in the changeset. Right.
  • Dev deviation: ablation leg 1 re-planted the old predicate only, not the old toRenderableSchema bridge. The primitive cases are red either way, as the dev states (the placeholder is absent under the old predicate regardless). A reverse-verification detail, not a contract matter. Read.
  • Dev deviation: not rebased onto the 3 commits after 0cd322d7. mergeable_state is clean; the dev names the three and none touches a file in this diff; the queue rebuilds on main. Read.
  • Dev deviation: model-free commit trailers; os-dev-report as marker line plus raw JSON. Both follow objectui AGENTS.md; neither is a contract matter. Read.
  • Dev NOT MEASURED: check:eager-closure. Answered by the head's Bundle Analysis check-run (success) and the CI budget comment on the PR (eager closure PASS, 3129.3 KB of 3204.6 KB). Resolved.
  • out_of_scope_findings, carrier none for both. (i) The custom type stays in the vocabulary: that is ruling C's own "D not taken", and this card's scope was the draw, so leaving it is right; it is the D question's other half for a later card if ever measured. (ii) unsupportedWidgetSchema's wording "chart type is not supported yet" now also covers custom: existing wording, polish only. Neither is a defect of this change; neither needs a card to land it.
  • No reviewer flag of my own rises to a FAIL. The one edge I would have asked about, the namespaced plugin-dashboard:metric-card key (① item 5), is pinned on both surfaces and agrees with authoring.

Implemented-by: claude/issue-12254-envelope-draw-retire
Reviewed-by: session_01G5TSaEwbEQsKMjhzTB4rEd

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 12, 2026 15:41
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 12, 2026 15:42
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 12, 2026
Merged via the queue into main with commit e8fc452 Oct 12, 2026
34 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-12254-envelope-draw-retire branch October 12, 2026 15:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation plugin tests

Projects

None yet

2 participants