Repository navigation
schema-catalog: all 9 plugin-dashboard examples are refused by the spec's DashboardSchema — the AI few-shot corpus teaches the pre-ADR-0021 widget shape #4600
Description
Found while implementing #4356 (Phases 0+1), measuring whether the guardrail that card asked for could be built as suggested. Not fixed there — that card's scope is the globalFilters[].options shorthand, and this is strictly larger.
Blocked-by: objectstack-ai/objectstack#8593
What was measured
Parsing every examples/schema-catalog/src/schemas/plugin-dashboard/*.json entry with the real @objectstack/spec DashboardSchema, on origin/main at 92250d648:
REJECT basic-dashboard.json (17 issues)
REJECT e-commerce-dashboard.json (22 issues)
REJECT filtered-dashboard.json
REJECT filtered-dashboard-dataset-widgets.json
REJECT filtered-dashboard-date-presets.json
REJECT filtered-dashboard-dynamic-options.json
REJECT filtered-dashboard-filter-types.json
REJECT filtered-dashboard-target-widgets.json
REJECT support-dashboard.json
9 of 9. Fixing #4356's bare-string options shorthand does not change this — those three entries stay refused for the reasons below, which is why #4356 pinned its guardrail at GlobalFilterSchema (the sub-schema that owns the property that regressed) rather than at DashboardSchema as the objectstack#7917 survey had suggested. That narrowing is deliberate and recorded in that PR; this issue is the remainder.
Two distinct causes, and they are not the same kind of problem:
1. Metadata identity keys — arguably by design. Every entry reports [name] Invalid input: expected string, received undefined and [label] Invalid input. These are objectui SDUI component schemas ({ "type": "dashboard", "title": …, "columns": … }), not stored platform metadata documents, so they legitimately carry no name / label identity. If that is the intended reading, then DashboardSchema is simply the wrong schema for an SDUI node and the interesting question is which schema is right.
2. The widget shape — this is the real finding. Most entries' widgets use the pre-ADR-0021 inline analytics form:
[widgets.0] Unrecognized key(s) on this dashboard widget: `object`, `categoryField`, `aggregate`.
• The pre-ADR-0021 inline analytics shape (`object` + `categoryField` + `valueField` + `aggregate`,
pivot `rowField`/`columnField`) was removed — bind a `dataset` and select `dimensions` + `values`
by name. Renderer-only settings belong under `options`. Undeclared top-level keys were dropped
silently before strict validation, shipping inert metadata; a stale or mis-layered key is now a
loud parse error.
@objectstack/spec 17 removed that shape. And objectui's own DashboardWidgetSchema (packages/types/src/complex.ts) does not declare object / categoryField / aggregate either — it takes its keys from the spec through extends and adds only component, layout, type, options, chartConfig, filter, responsive. So these examples author keys that neither contract declares. Nothing catches it because the entries are imported as JSON and the catalog's only assertions are structural (smoke.test.tsx) and render-without-throw.
Why it matters
This is the same defect class as #4356, in the same corpus, with the same blast radius — and #4356's was judged worth a card. examples/schema-catalog/package.json describes itself as the "single source of truth for example schemas consumed by the docs site, smoke tests, and AI few-shot retrieval", and test/fields-form-hosted.test.tsx records the #3910 lesson verbatim: "these examples are the docs site's field demos and a few-shot retrieval source for AI authors, so what they show is what gets copied."
So an AI author retrieving filtered-dashboard.json today is taught a widget shape the platform refuses at publish, exactly as it was being taught the bare-string options shorthand until #4356. The objectstack#7917 survey's core point applies unchanged: the stored population of these documents is still growing, because the corpus is still producing them.
Uncertainty, stated plainly
Not measured here: whether objectui's renderer still supports the inline analytics form at runtime. It evidently renders (the catalog's render-without-throw tests pass), so this may be a live compat arm rather than dead metadata — in which case the finding is a documented-tolerance divergence of the #4356 shape (renderer accepts, spec refuses) and wants the same enforce-or-retire treatment, not a silent rewrite of the examples. Whoever picks this up should establish that before choosing between "migrate the 9 examples to dataset + dimensions + values" and "retire the renderer arm too".
Related, both closed and from an earlier era: #824 (examples' dashboard/widget metadata vs spec) and #3965 (48 catalog examples using the deprecated div component type) — same corpus, same failure mode, which suggests the catalog needs a standing spec-validation gate rather than another one-off sweep. #4356 added one for globalFilters only.
Generated by Claude Code
State note (seat, 2026-08-14): Blocked-by: objectstack-ai/objectstack#8593 added above — the remainder of this card (which schema an SDUI dashboard COMPONENT node validates against; classes A/B + metric-card) gates on that spec-seat decision. The C1 half (retired inline-analytics shape in the example entries) was already fixed by PR #4615. See the state-correction comment of the same date.
Activity
yinlianghui commentedon Aug 13, 2026
CLAIM (measure-only) — session
session_017Qqyix2QcnpUC9XeYVDzx3. This seat EDITS NOTHING: its deliverable is the measurement this card's own body says its ruling needs ("whether the renderer still supports the inline analytics form was NOT measured, and that determines whether the fix is 'migrate the 9 examples' or 'retire the renderer arm too'"), reported here for PM ruling. No branch, no PR, no changeset.Measurement scope:
plugin-dashboard's widget path still READ the pre-ADR-0021 inline analytics keys (object+categoryField+aggregate)? Trace the code (file:line), and answer working-render vs dead-code vs partial with a scratch render probe if needed.DashboardWidgetSchema(packages/types) declare today — inline shape, dataset shape, both?content/docsand the plugin README teach — inline, dataset, both? (ThenormalizeFilterOptionsaccepts a bare-stringglobalFilters[].optionsshorthand that@objectstack/specrejects at publish #4356 survey found the docs still teach inline "elsewhere" — enumerate where.)plugin-dashboardentries vs the spec'sDashboardSchemaon current main (re-verify thenormalizeFilterOptionsaccepts a bare-stringglobalFilters[].optionsshorthand that@objectstack/specrejects at publish #4356-era measurement: metadata-identity keys + inline widget shape), plus each entry's other divergences if any.dataset+dimensions+valuesform — enough to size "migrate the 9" honestly.Output: a findings comment on this issue with the six answers (verbatim probe outputs, file:line citations) and the option space with measured costs — no recommendation required beyond stating which facts bear on which option. Read-only everywhere; scratch probes in the session scratchpad only. ⛔ Zero repo file edits; ⛔ no interference with the in-flight #4580-round2 seat's packages (reads are fine).
Generated by Claude Code
Generated by Claude Code
yinlianghui commentedon Aug 14, 2026
Measurement report (measure-only seat — nothing was edited, no branch, no PR)
Measured on
origin/mainatf69127fcc, against installed@objectstack/spec@17.0.0-rc.6. The card's own stated uncertainty — "Not measured here: whether objectui's renderer still supports the inline analytics form at runtime" — is the first thing below, and it resolves the other way from the card's guess, which collapses most of the option space.Headline: the renderer arm is already retired (framework#3320). The inline shape does not render a chart — it renders a red dashed error placeholder reading "This widget uses a retired data format. Edit it to bind a dataset." So these 9 entries are not merely "refused at publish while rendering fine". 15 of their 28 widgets are visibly broken in objectui's own docs-site gallery today, and 3 docs files still teach the shape that produces them.
1. Renderer support for the inline analytics shape
Verdict: retired, with a diagnostic — not a live compat arm, and not silently dead.
Method: static trace of every read, plus running the pinned test on this commit (real output below).
Per-key verdicts, at widget top level:
objectpackages/plugin-dashboard/src/DashboardRenderer.tsx:549categoryFieldcategoryGranularityvalueFieldaggregaterowField/columnFieldpivotfamily returns the same placeholderDashboardRenderer.tsx:703-709The decisive lines, verbatim (
DashboardRenderer.tsx:537-551):and the placeholder it selects (
DashboardRenderer.tsx:149-155):Do not confuse this with the live nested surface. The
object/aggregatereads atDashboardRenderer.tsx:567-694andDashboardGridLayout.tsx:190-295are all onwidgetData— the nestedoptions.data = { provider: 'object', … }async data-provider config, which is a different and still-live authoring surface. Repo-wide, a top-levelwidget.objectis read in exactly one place:Live confirmation — the pinned test on this commit:
Its 4 positive cases render
bar/metric/pivot/tablewidgets carrying the inline shape and assert the placeholder is visible; its 2 negative controls assert adataset-bound and anoptions.datawidget do not get it. Two of those cases are byte-for-byte the shapes infiltered-dashboard.json({ type: 'metric', object: 'invoices', aggregate: 'count' }iswidgets.2exactly).Secondary finding — the sibling surface has no such fallback.
DashboardGridLayout(separately exported, used byDashboardWithConfig/ editable dashboards) carries no legacy sentinel. A legacy widget there takes the series branch withwidgetDataundefined, soisObjectProvider(undefined)is false and it falls through to the static-data branch withdata: [](DashboardGridLayout.tsx:215-225) — i.e. a blank chart with no diagnostic, precisely the outcome the retirement's own test header says must not happen ("must render a VISIBLE error placeholder prompting a rebind, not a blank widget"). The graceful fallback was applied to one of the two surfaces. Flagging, not fixing — see the option space.2. Type layer
Spec (
@objectstack/spec@17.0.0-rc.6,DashboardWidgetSchemafrom/ui), probed directly:No inline key is present at all.
datasetandvaluesare required, which is why every widget in every entry also reports twoinvalid_typeissues on top of the unrecognized-key issue.objectui, TS —
packages/types/src/complex.ts:676-677:DashboardWidgetSchemaextendsOmit< Partial< SpecDashboardWidget >, 'type' | 'options' | 'chartConfig' | 'filter' | 'responsive' >and adds onlycomponent,layout,type,options,chartConfig,filter,responsive,searchable,pagination. Its own comment says the spec keys "flow in from the spec through theextendsabove — do not restate them here". No inline keys.objectui, Zod twin —
packages/types/src/zod/complex.zod.ts:303-310: derived fromSpecDashboardWidgetSchema.shape. No inline keys. TS and Zod agree.The one surface that still declares them is the config panel pair, and it is not an authoring path to the dead shape:
packages/types/src/designer.ts:538-552—interface DashboardWidgetConfig(object?,categoryField?, …)packages/types/src/zod/complex.zod.ts:452-459—DashboardWidgetConfigSchema, commented "for DashboardConfigPanel"packages/plugin-dashboard/src/WidgetConfigPanel.tsx:459-482actively scrubs them on save, viaLEGACY_ANALYTICS_KEYS = ['object', 'categoryField', 'categoryGranularity', 'valueField', 'aggregate', 'aggregation', 'rowField', 'columnField', …], so that "switching type (or saving a legacy widget) never re-emits the dead shape".packages/app-shell/…/inspectors/DashboardWidgetInspector.tsxrecords that the inline object query "was removed (framework#3251), so no Studio surface can author the dead shape".So: no authoring surface emits this shape. The catalog corpus is the last producer in the repo.
3. Docs layer
Three files teach the retired inline form for a dashboard widget; zero files teach the
datasetform for a widget.content/docs/guide/dashboard-filters.mdobject+categoryField+categoryGranularity+aggregatecontent/docs/plugins/plugin-dashboard.mdx{ "id": "w1", "type": "bar", "object": "invoices", "aggregate": "count" }and two morepackages/plugin-dashboard/README.mdPlus prose in
dashboard-filters.mdthat asserts the shape is live::19-22— "Working examples: the schema catalog ships aplugin-dashboard/filtered-dashboardexample plus variants…" (all 3 of that entry's widgets render the retired placeholder):266-269— "Inline (object-based) and dataset-bound widgets can mix freely on one filtered dashboard.":285-291— "Bind the widget to anobject(or adataset) if it should respond to filters" and "Default bindings are metadata-checked forobjectwidgets only"Deliberately excluded from this census (checked, different live surfaces — do not batch-rewrite them):
content/docs/plugins/plugin-report.mdx:250—report.columns[].aggregate, the spec-report surface, unrelated and live.packages/plugin-dashboard/README.md:230—optionsFrom.valueField, the global-filter dynamic-options surface, live.4. Catalog per-entry failure classes (all 9, verbatim)
DashboardSchema.safeParseon each entry. 9 of 9 REJECT, confirming the card. Three distinct classes, not the two the card names:[name]+[label]. Every entry, 2 issues each.type(all 9) andtitle(the 6filtered-*). 1 issue each. This class is not named in the card body.object/categoryField/categoryGranularity/aggregate. 6 entries, 15 widgets.metric-card—value/icon/trend/trendValue, andtype: "metric-card"is not in the spec's 20-value widget enum at all. 3 entries, 11 widgets. This is not an ADR-0021 problem; it is a widget family objectui has and the spec does not model.datasetandvalues(both required), and C2's widgets also missid.Representative verbatim output:
Full per-entry issue counts on
f69127fcc:basic-dashboard18,e-commerce-dashboard23,support-dashboard23,filtered-dashboard12,-date-presets12,-filter-types12,-target-widgets12,-dynamic-options9,-dataset-widgets6.Two counts differ from the card's snapshot at
92250d648(18 vs 17, 23 vs 22). The one intervening commit touching this corpus isf279debf5(#4356 / PR #4601); I did not chase the attribution, and the class breakdown is unchanged — the delta does not bear on any option below.What these entries render today (the part the card assumed was fine)
Replaying the renderer's own fork (
DashboardRenderer.tsx:528datasetBound,:542widgetData,:549sentinel,:703pivot; the:549behaviour independently pinned green by the 6/6 test above) over the real entries:apps/site/app/components/SchemaCatalogIndex.tsx:105-111renders every catalog example through a realSchemaRenderer. So/docs/guide/schema-catalogcurrently shows 15 red dashed "retired data format" tiles, and 5 of the 6filtered-*entries are 100% placeholder — including the onedashboard-filters.md:19calls a "Working example".5. Migration shape —
filtered-dashboard.json, workedThe minimal spec-valid widget parses clean (probed):
Migrating the whole entry means changing the document's kind, not just its widgets — classes A and B are envelope changes:
{ // Class B: the SDUI envelope keys must GO. `type: "dashboard"` is rejected, // `title` must become `label`. "name": "sales_overview", // Class A: invented — no source in the file "label": "Sales Overview", // from the old `title` "columns": 2, "gap": 4, "dateRange": { … }, // unchanged, accepted "globalFilters": [ … ], // unchanged, accepted (already fixed by #4356) "widgets": [ { "id": "invoices_by_status", "title": "Invoices by Status", "type": "bar", "dataset": "invoices", // invented — no such dataset metadata exists "dimensions": ["status"], // from `categoryField` "values": ["count"] }, // from `aggregate: "count"` { "id": "accounts_signed", "title": "Accounts Signed", "type": "line", "dataset": "accounts", // invented "dimensions": ["signed_at"], // `categoryGranularity: "month"` has NO // top-level equivalent — a granularity // belongs to the dataset's dimension // definition, not the widget "values": ["count"], "filterBindings": { "dateRange": "signed_at", "region": "sales_region" } }, { "id": "total_invoices", "title": "Total Invoices (all regions)", "type": "metric", "dataset": "invoices", // invented "values": ["count"], "filterBindings": { "region": false } } ] }Information that is missing and must be invented — this is the honest sizing input:
name— no source anywhere in the file. One identifier per entry, invented.datasetis a reference by name to a separate semantic-layer metadata document (the spec exportsDatasetSchema/defineDataset). The catalog has no place to put one — it is a directory of standalone SDUI component JSON. So the 9 entries would referenceinvoices/accounts/salesdatasets that exist nowhere, anddimensions/valueswould name dimensions and measures nobody has declared.categoryGranularityhas no widget-level home in the new model; the monthly bucketing demonstrated byaccounts_signedis simply lost unless a dataset declares that dimension.metric-cardentries (class C2) cannot be migrated at all without ceasing to be what they demonstrate — a static presentational metric card withvalue: "1,234",icon,trend. There is nometric-cardin the spec's widget enum and no dataset behind a hard-coded string.And the measured sting — migration does not fix the rendering. The docs gallery passes
dataSource: {}(apps/site/app/components/SchemaThumbnail.tsx:25).DatasetWidget.tsx:767-769reads:which renders an error alert (
:929-932). This is not a prediction — it is already happening: the 2 dataset-bound widgets infiltered-dashboard-dataset-widgets.jsonshow that error in the gallery today. So migrating the 15 inline widgets converts 15 "retired data format" placeholders into 17 "does not support dataset queries" errors. Different message, same broken gallery.6. In-repo reliance on the inline form rendering
Measured by grepping every renderer read plus every test that constructs such a widget. The reliance is on the placeholder, not on any chart.
packages/plugin-dashboard/src/__tests__/DashboardRenderer.legacyRetired.test.tsxdataset/options.data. Goes red if the:549sentinel is removed.filtered-*catalog JSON entriesexamples/schema-catalog/test/plugin-dashboard-global-filters-spec.test.tsglobalFiltersonly — independent of widget shape, unaffected by any option belowZero tests or consumers depend on the inline form producing a chart, a metric, a table or a pivot. Checked and excluded as different surfaces:
packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx:277-282(objectName+categoryField— theobject-chartSDUI block's props, live);packages/types/src/__tests__/dashboard-config.test.ts(the config-panel type, not the renderer);DashboardWidgetInspector.test.tsx(asserts the Studio surface is gone);plugin-view/plugin-list/app-shellObjectView(chartConfig.xAxisField, live).Also worth recording:
examples/schema-catalog/test/smoke.test.tsxcontains norender(call at all — its assertions are purely structural (schemais an object,node.typeis a non-empty string). The catalog has no render-without-throw test for this category, so nothing in CI would have noticed the 15 placeholders.Note for the record:
examples/crmandexamples/todoare clean — the inline widgets #824 added there in Feb 2026 are gone. The corpus inschema-catalogis the only survivor.Option space, with measured costs
(b) "Retire the renderer arm too" — already done; there is nothing to buy. framework#3320 retired it before this card was filed. The card's premise that this might be "a live compat arm" is disproven. Residue: the
:549sentinel (worth keeping — it is the graceful fallback for stored legacy metadata) and theDashboardGridLayoutasymmetry in §1.(a) "Migrate the 9 examples to
dataset+dimensions+values" — larger than it looks, and does not fix what is actually broken.name, drop the SDUItype, renametitle→label), it invents 3 datasets that exist nowhere, it silently dropscategoryGranularity, and 3 of the 9 entries (class C2, 11 widgets) cannot be migrated at all.dataSource: {}in the gallery, migration turns 15 placeholders into 17 dataset errors. Measured, not predicted.(c) "Keep both, gate on a narrower sub-schema" (status quo after #4356).
(d) What the measurements themselves suggest — split into three, because they are three different decisions:
filtered-*entries and the 3 docs locations teach a shape objectui itself refuses to render. Fixing the corpus honestly requires both rewriting them and givingSchemaThumbnaila dataset-capable stubdataSource— otherwise (a) just changes the error text. Sizing: 6 JSON files + 3 docs files + 1 stub inapps/site.DashboardSchemagate over this corpus is permanently red for reasons that have nothing to do with ADR-0021 — which is exactly whynormalizeFilterOptionsaccepts a bare-stringglobalFilters[].optionsshorthand that@objectstack/specrejects at publish #4356 pinned its guardrail atGlobalFilterSchema. A standing catalog gate (the card's closing suggestion) needs this answered first, or it needs to be a widget-level gate rather than a document-level one.DashboardGridLayout's missing fallback (§1) — an independent defect: the same stored metadata that gets a helpful placeholder inDashboardRenderergets a silent blank chart in the editable surface. Small, self-contained, and unrelated to the corpus.Recommendation (offered, not required). Do (d1) and (d3); do not do (a) as stated, and do not open (d2) as part of this card. The business pull is real but narrow and it is entirely on (d1): the corpus is a few-shot retrieval source and the gallery is published, so today it both teaches and demonstrates a dead shape — that is a live authoring-quality defect, and it is the half that is cheap and unambiguous. (d2) changes a contract and should be a maintainer ruling with its own card; guessing it wrong would be far more expensive than leaving classes A and B red for now, since nothing enforces them today anyway. The AI-authoring axis also argues for (d1) over (c): what makes this shape hard to get wrong is not another consumer-side tolerance but removing the last producer of it, which the config panel already did in code (
LEGACY_ANALYTICS_KEYS) and the corpus has not yet done in data.Generated by Claude Code
Generated by Claude Code
yinlianghui commentedon Aug 14, 2026
PM ruling on the measurement above (delegated, veto window open). The measurement inverts this card's premise — the renderer's inline arm is ALREADY retired (framework#3320) and renders a diagnostic placeholder, so the real, user-visible defect is that the published docs gallery shows 15 "retired data format" tiles today, including an entry the docs call a "Working example", and nothing in CI can see it (the smoke test contains no render call). Disposition in four parts:
categoryGranularity, cannot migrate the 3 metric-card entries at all — and with the gallery'sdataSource: {}it converts 15 placeholders into 17 dataset errors. Different message, same broken gallery.:549sentinel stays as the graceful fallback for stored legacy metadata.filtered-*entries onto STILL-LIVE surfaces that actually render in the gallery (the entries exist to demonstrate FILTERS — the seat measures whetheroptions.dataprovider config, static data, or dataset-bound-plus-stub yields working charts and uses what works); fix the 3 docs teaching locations plus the false "Working examples" / "mix freely" prose; giveSchemaThumbnaila dataset-capable stub dataSource so the 2 dataset-bound widgets render; and add the missing render-based gallery pin for this category (no retired-placeholder tile, no error tile) so this cannot silently regress again. The 3 metric-card entries render fine and stay as-is — their spec-validity is (d2)'s question, not a rendering defect.metric-cardjoins the spec's widget enum): ROUTED UPSTREAM as adomain:specdecision card in objectstack, with this measurement as evidence — the same contract-first routing as objectstack#7496/finding(types):BreadcrumbSchemaandObjectTreeSchemaare declared node schemas that@object-ui/types/zodnever exports, so 2 of 107 component types cannot be validated on their own #7917. ADashboardSchemacatalog gate stays impossible until it is answered, which is exactly whynormalizeFilterOptionsaccepts a bare-stringglobalFilters[].optionsshorthand that@objectstack/specrejects at publish #4356 pinned atGlobalFilterSchema. OQ2 ruled A (C2 folds into that one ruling).OQ1 ruled A: the
DashboardGridLayoutmissing-fallback defect (silent blank chart whereDashboardRenderershows the rebind prompt) is filed standalone (searched: no open issue mentions it) and dispatched as its own small seat — a renderer fix does not belong inside a docs/examples card.Generated by Claude Code
Generated by Claude Code
yinlianghui commentedon Aug 14, 2026
CLAIM (implementation, d1) — session
session_017Qqyix2QcnpUC9XeYVDzx3, branchclaude/issue-4600-gallery-truth. Scope per the ruling above: 6filtered-*entries rewritten onto still-live, gallery-renderable surfaces (seat measures which ofoptions.dataprovider config / static data / dataset+stub actually renders and uses what works); the 3 docs teaching locations + false prose corrected;SchemaThumbnailgains a dataset-capable stub dataSource; a render-based gallery pin (no retired-placeholder tile, no error tile) closes the CI blind spot. ⛔ NOT here: metric-card entries (render fine; spec-validity is objectstack#8593's ruling),packages/plugin-dashboard/src/**(#4612's seat), re-teaching the bare-string filter shorthand (the #4356 guardrail is live). Phase (d2) waits upstream on objectstack#8593.Generated by Claude Code
Generated by Claude Code
yinlianghui commentedon Aug 14, 2026
(d1) implemented — PR #4615 (draft). Two notes the PM needs before 复核:
1. The gallery was broken one layer deeper than the measurement could see. The measurement seat replayed
DashboardRenderer's fork over the entries, which is accurate for what those entries produce once the plugin is loaded. Rendering them through the gallery's actual registration set says something else:SchemaThumbnailloads@object-ui/react,@object-ui/componentsandregisterLayoutBlocksand no plugin at all, so all 9 dashboard tiles were the registry's redUnknown component type: dashboard (OBJUI-001)panel — the 15 placeholders and 2 dataset errors were never reachable on that page. Rewriting the entries alone would therefore have changed nothing visible. The PR adds the registration (gallery-only,apps/site) alongside the stub and the rewrite; a census of the rest of that gap (≈32 more entries across calendar / chatbot / editor / gantt / map / markdown / timeline / kanban) is filed as #4616.2.⚠️
mainis red and the PR inherits it.ci.yml'sBuild Docsjob runsnext build, which type-checksapps/site, and it has failed sincec1d939f7f— the merge of PR #4608 (#4580, "the union wins"). FiveTS2322s, allSchemaNodenot assignable toSchemaRenderer'sschemaprop; four of the five are in files PR #4615 does not touch (InteractiveDemo×2,LiveSplitDemo,playground/page.tsx). Evidence and the two contract options are on #4617. It is deliberately not fixed in PR #4615: the honest fix is a published prop-type decision in@object-ui/react, and casting at the call sites is the consumer-side tolerance contract-first forbids. Every other gate on the PR is green.Generated by Claude Code
Generated by Claude Code
fix(site): SchemaThumbnail names the layout registrar directly again (#…
yinlianghui commentedon Aug 14, 2026
PM step-7 复核 of the (d1) delivery: ACCEPTED — flip deferred. PR #4615's work is verified (premise correction honest and load-bearing: the gallery page never loaded the dashboard plugin, so the seat's added registration is what makes every other fix visible; entries rewritten on probe-measured surfaces with the filter pedagogy byte-identical; render pin red 15→green with a teeth-proof; #4356 guardrail green both sides; metric-cards untouched per objectstack#8593; no changeset owed, measured). It stays DRAFT deliberately: main's Build Docs is red (#4617, inherited from PR #4608's landing), so no branch can pass that gate until the fix lands. Sequence: #4617's urgent seat is dispatched (bridge the five apps/site sites via
toRenderableSchemaper the #4548-Q2 ruling + close the turbo task-name gap); when it lands and main greens, #4615 flips and arms. #4616 (35 unresolved gallery entries — loading-strategy decision) and #4618 (table-widget update-depth crash) enter PM triage.Generated by Claude Code
Generated by Claude Code
yinlianghui commentedon Aug 14, 2026
(d1) LANDED — PR #4615 squash-merged as
e028dfcd8(after a semantic conflict resolution with #4621's bridge call, both changes standing). The published gallery now renders working charts for the sixfiltered-*entries, the docs teach live shapes, and the render pin closes the CI blind spot. This card's remaining scope is (d2) only — parked upstream-waiting on objectstack#8593 (which schema an SDUI dashboard node validates against, incl. metric-card's enum question). Restart condition: #8593 ruled. Related follow-ups tracked separately: #4616 (35 non-dashboard gallery entries still unregistered), #4618 (table-widget update-depth crash).Generated by Claude Code
Generated by Claude Code
yinlianghui commentedon Aug 14, 2026
State correction (seat
repo:objectui, sessionsession_01RnQd8iMMUwXQEV1crFmQiQ, round 1): this card was label-bare with a stale assignee from the signed-off predecessor session (session_017Qqyix2QcnpUC9XeYVDzx3; its handoff record #4635 declares zero in flight). Corrected topm:blocked+ aBlocked-by: objectstack-ai/objectstack#8593body line (target verified OPEN,needs-user-decisionon the spec seat, this hour), assignee cleared. The card returns to the dispatchable pool when #8593 is ruled; its restart re-check is the body's measurement rerun against the then-current spec.Generated by Claude Code
hotlong commentedon Aug 14, 2026
Unblock notice: the schema ruling this card's catalog-gate work was soft-blocked on has landed (objectstack-ai/objectstack#8593, maintainer 2026-08-14): option 1 — SDUI component nodes validate against objectui's own component schema; spec's
DashboardSchemagoverns stored metadata documents only; metric-card joins objectui's own closed component enum as an explicitly-allowed extension. The gate can now be built against a schema that actually exists.Generated by Claude Code
fix(examples,docs): the dashboard gallery renders working charts agai…
fix(site): the catalog gallery registers every plugin its entries need (
1 remaining item
os-support-ai commentedon Aug 18, 2026
Unlock scan (objectstack-wide triage seat, fire ~19:47Z 2026-08-18): the blocking spec decision objectstack#8593 was ruled by the maintainer 2026-08-14 (recorded verbatim on that card): option 1 — an SDUI dashboard COMPONENT node validates against objectui's own component schema; the spec's
DashboardSchemagoverns stored metadata documents only and grows no component projection;metric-cardjoins objectui's closed component enum as an explicitly-allowed extension (not the spec widget enum).pm:blocked→pm:queue. The remainder of this card (classes A/B + metric-card; the catalog gate against the component schema) is fully specified by that ruling — quote it in the dispatch. C1 was already fixed by PR #4615.Generated by Claude Code
docs(plugin-dashboard): README 的 onSchemaChange 持久化示例与手动注册段按真实契约重写 (o…
yinlianghui commentedon Aug 24, 2026
Claim: PM loop round 36⚠️ Flips to ⚠️ Concurrent sibling #3909 also writes ⚠️ Concurrent sibling #5930 writes
Session:
session_01CSoz9uGhaaSgiq3hshtN7LBranch:
claude/issue-4600-schema-catalog-dashboard-gateWorktree:
objectui-issue-4600Domain:
domain:uiFile surface:
examples/schema-catalog/src/schemas/plugin-dashboard/**,packages/types/src/complex.ts(the closed component enum), the catalog gate you add, + tests + a changeset (stop on breach; explain in the report)Container & model:
L,mode:subagent,model: opusClause-②: no as scoped —
metric-cardjoins objectui's own closed enum as the ruling directs, and the spec'sDashboardSchemais not touched.yesif the honest fix turns out to need a change in@objectstack/spec— that is another repo and another lane. Stop and report.Serial constraints cleared:
packages/types/**released when PR #5981 (#3917) merged 10:37:11Z.packages/types, butsrc/data.ts; yours issrc/complex.ts— disjoint by full path.examples/schema-catalog/src/schemas/components-overlay-dropdown-menu/; yours issrc/schemas/plugin-dashboard/. ⛔ Do not add a catalog-WIDE gate — scope it to the dashboard entries (see below).⛔ RULED 2026-08-14 — not re-litigable
Maintainer, verbatim: 「同意你的建议」, approving option 1:
Two consequences follow directly and are not open questions:
name/label(class A) and theirtype: 'dashboard'+titleenvelope (class B) were only "rejections" because the wrong schema was being applied. Do not add identity keys to the entries and do not strip the envelope.metric-cardmust land in a CLOSED enum, not an open "extension allowed" escape hatch. The triage block that fed this ruling names that explicitly. An open hatch would re-create the hole the gate exists to close.What remains (scope is (d2) only)
C1 is already fixed — PR #4615 migrated the retired inline-analytics shape in the entries, so the six
filtered-*entries render live charts. ⛔ Do not redo it. Re-verify rather than assume; the card is 11 days old.The deliverable is the ruling's implementation:
metric-cardinto the closed component enum, and a standing validation gate for the dashboard catalog entries against the component schema — the thing #4356 could only do forglobalFilters, and the reason the corpus kept producing refused documents.PM mechanism assumptions — measure, falsification is a good run
DashboardSchema) at92250d648, 11 days and one C1 fix ago. Re-measure against the component schema the ruling names, on the current tree. The interesting number is how many entries the correct schema refuses — that is the gate's real starting population, and it may well be zero, which would be a legitimate and valuable delivery.@object-ui/plugin-dashboardexports no schema types; the container node's real type isDashboardComponentSchema(packages/types/src/complex.ts:738); there is no per-family widget type (oneDashboardWidgetSchemacovers all, family settings underoptions); andMetricCardPropsexists atsrc/MetricCard.tsx:35but is not re-exported. Verify before relying on it, but do not re-measure from scratch.Standing
⛔
examples/schema-catalogdescribes itself as the single source of truth for the docs site, smoke tests and AI few-shot retrieval — what these entries show is what gets copied. A gate that passes because it validates nothing is worse than no gate: include a counter-probe proving the gate actually rejects a deliberately-malformed entry. Draft PR,Fixes #4600. Push the branch early. Post the terminal report here (plain-text first-line marker) and return it.Generated by Claude Code
feat(types): close the dashboard widget type vocabulary; admit metric…
test(types): register the new vocabulary in the zod mirror ledger
yinlianghui commentedon Aug 24, 2026
os-dev-report
{ "issue": 4600, "status": "done", "branch": "claude/issue-4600-schema-catalog-dashboard-gate", "pr": "https://github.com/objectstack-ai/objectui/pull/6003", "premise_still_valid": false, "summary": "PM mechanism assumption 1 is FALSIFIED, in the direction the dispatch anticipated. Re-measured against the schema the 2026-08-14 ruling names (objectui's own DashboardComponentSchema), 9 of 9 plugin-dashboard entries are ACCEPTED, not rejected — the card's '9 of 9 REJECT' table was the spec's DashboardSchema answering a question that was not asked. Classes A/B needed nothing, and C1 (PR #4615) re-verified as already landed. But acceptance was near-vacuous: measured before any change, the same safeParse also accepted `type: 'zzz-not-a-widget-type'`, a widget with no `type` at all, and a widget carrying the retired `object`/`categoryField`/`aggregate` keys (silently stripped) — so the gate the card asked for would have passed by validating almost nothing. Delivered therefore: (1) the ruling's closed vocabulary — `DashboardWidgetSchema.type` goes from `string`/`z.string()` to a CLOSED enum built from the spec's 20 families BY REFERENCE plus two named objectui sets, `DASHBOARD_WIDGET_TYPE_EXTENSIONS` (list, custom) and `DASHBOARD_COMPONENT_WIDGET_TYPES` (metric-card — objectui's own component enum, NOT the spec widget enum; @objectstack/spec untouched, Clause-② stays `no`); (2) the standing gate, fenced to plugin-dashboard per assumption 3, asserting parse + closed-vocabulary membership + no-silent-key-drop, routing each widget to the schema that owns it (metric-card is a component node -> passthrough BaseSchema, which is why its registry-input props survive); (3) a 7-case counter-probe plus an unmutated control. The closure surfaced three real drifts, all fixed under the bounded in-place exemption and named in the PR body: the dashboard designer offered a `grid` widget type neither contract declares (refused at publish — designer-saves-what-server-rejects), the designer and app-shell's widget inspector both wrote unvalidated `string` from select boxes, and a types fixture pinned `bar-chart` (a plugin-charts component type) on a dataset-bound widget.", "tests": "All at HEAD a54b8bca4 (union re-run AFTER the final commit). TYPECHECK, 7 packages (types, plugin-dashboard, plugin-designer, app-shell, sdui-parser, core, example-schema-catalog): `TYPECHECK_EXIT=0`, `grep -cE 'error TS' = 0`. Exit code captured before any pipe. TESTS: `pnpm exec vitest run packages/types/src/__tests__ examples/schema-catalog/test` -> exit 0, vitest's own verdict lines `Test Files 65 passed (65)` / `Tests 2240 passed (2240)`; plugin-dashboard+plugin-designer `Test Files 88 passed (88)` / `Tests 802 passed (802)`; app-shell metadata-admin `Test Files 195 passed (195)` / `Tests 1982 passed | 1 skipped (1983)`. New gate alone: `Tests 21 passed (21)`. LINT: per-package on all four touched packages, `LINT_EXIT=0`, eslint's own lines `0 errors` (251/67/2646/1 warnings, all pre-existing); a full-repo `eslint . --no-inline-config` (3623 files, from eslint's --format json count) also completed in-cap — the one finding in a file this PR touches is at index.ts:1190, ~900 lines from any hunk, an artefact of --no-inline-config suppressing that file's own eslint-disable pair. TRAP: `@object-ui/types` has NO `test` script — `pnpm --filter @object-ui/types test` exited 0 having run nothing (zero-match). Caught and rerun via root vitest; the 585-test green is the real reading. REVERSE VERIFICATION (TS, predicted direction red, observed red): restoring `type: 'bar-chart'` -> `error TS2322: Type '\"bar-chart\"' is not assignable to type 'DashboardWidgetTypeName | undefined'`; mutation confirmed on disk before the run by anchored counts (injected \"bar-chart\" = 1, removed \"type: 'bar'\" = 0), restore confirmed by count 0 and an EMPTY diffstat, restored leg exit 0. The cross-package half is separate and stronger: the plugin-designer and app-shell TS2322s appeared only AFTER `@object-ui/types` was rebuilt, so consumers demonstrably read the new .d.ts, not a cached one. ABLATION (gate teeth, on a REAL entry): injected `object`+`categoryField`+`aggregate` into filtered-dashboard.json; mutation proven on disk by anchored greps (categoryField 1, aggregate 1) plus a non-empty diffstat BEFORE reading any result; gate went RED (exit 1) naming exactly those three keys on the real entry; restore leg proven by grep count 0 and an EMPTY diffstat, rerun green 21/21. REBUILD NOTE, stated rather than assumed: this ablation's mutated artifact is a catalog JSON entry that the test reaches through the relative `../src/index.js` import, NOT through any package's dist — so no rebuild sits in the mutation's resolution path, and the fact that the mutation changed the verdict at all is itself the proof no stale artifact was read. The schema half of the test DOES come from `@object-ui/types` dist, which was rebuilt (exit 0) before every measurement reported here. Trap `trap '<restore>' EXIT INT TERM` was armed in both the ablation and the reverse-verification scripts.", "open_questions": [ { "question": "Should `DashboardWidgetSchema` keep SILENTLY DROPPING keys that no contract declares? Filed as #6002 rather than decided here — it is a published-contract change whose blast radius (stored dashboards, the designer emit path, component nodes in the widget slot) is unmeasured, and it is what still lets the pre-ADR-0021 inline-analytics keys validate clean.", "options": [ "A .strict() — refuse undeclared keys, matching the ADR-0049 treatment the retired keys already get (refused by name with a tombstone, not stripped). Needs a stored/designer population measurement first.", "B .passthrough() — keep undeclared keys instead of deleting them; consistent with BaseSchema, strictly better than silent loss, but still cannot REPORT a stale key.", "C leave as-is and document objectui widget validation as lossy for undeclared keys, with the new catalog gate as the only place the loss is detected." ], "recommendation": "A on the declared=enforced and hard-to-get-wrong axes, with B as a cheap interim strictly better than today — but gated on measuring the stored/designer population first. Not this card's to spend; #6002 carries the measurement." }, { "question": "Should the gate go catalog-WIDE? Per assumption 3 I fenced it to plugin-dashboard and am reporting, not building.", "options": [ "A keep it fenced to plugin-dashboard (what shipped)", "B widen to the whole catalog — this lights up the 35 unregistered non-dashboard gallery entries #4616 already records" ], "recommendation": "A. B is #4616's decision, not a side effect of this card; the gate is written per-category and widening it later is a small change once #4616 is ruled." } ], "out_of_scope_findings": [ "filed as #6002: `DashboardWidgetSchema` silently DROPS every undeclared widget key (z.object strip semantics) while spec-RETIRED keys are refused by name with a tombstone — the asymmetry that lets the pre-ADR-0021 shape validate clean; needs a .strict()/.passthrough() contract decision", "not filed, fixed in-place under the bounded exemption and named in the PR body: plugin-designer's DashboardEditor offered a `grid` widget type that is in neither contract and is refused at publish; the designer and app-shell's widget inspector wrote unvalidated `string` from their select boxes; packages/types' p1-spec-alignment fixture pinned `bar-chart` (a plugin-charts component type) on a dataset-bound widget", "noted, not filed (already tracked): #4616's 35 unregistered non-dashboard gallery entries — the reason the gate is fenced to plugin-dashboard" ] }Two deviations worth PM's eye, neither hidden:
Fence breach, declared.
packages/plugin-designer/src/DashboardEditor.tsxandpackages/app-shell/.../DashboardWidgetInspector.tsxare outside the dispatch fence. They are compile breaks caused by the in-fence contract tightening, taken under the bounded in-place exemption: same defect class (a widgettypeno contract declares), mechanical with the correct form pinned by existing evidence (@object-ui/types' own exportedDASHBOARD_WIDGET_TYPES, which has nogrid), no other claim on either file (checked against the named live siblings 观察:QueryParams.$filter声明为Record< string, any >,但仓内下沉口早已往里送 ObjectQL AST 数组 #3909 /ui:dropdown-menurenders an item's authorediconas raw TEXT — the catalog fixture named with-icons.json draws the words "edit", "copy", "trash" beside its labels #5930 / record:path container labels are hardcoded English, and one of them names a role-less div (so it names nothing) #5956 / record:path marks an unreachedwonterminus with colour alone — after #5916 it still announces as a plain upcoming stage #5957 /createLazyPlugin's own JSDoc teaches a call that does not compile —import('@object-ui/plugin-grid')has nodefaultexport #5949 — all disjoint), and no new gate family. Both are named with evidence in the PR body. The alternative was leaving TS open while Zod closed, which is the declared-vs-enforced split this repo's own parity suite records as the bug.Commit trailers. Both commits carry
Part of #4600. The standing clause says squashed branches should declare the card relation once in the PR body only. They are consistent with each other and non-closing, so a squash produces no contradiction, and correcting it would need a force-push — which is forbidden. Flagging rather than rewriting.CI was not waited on, per the standing clause: the branch is pushed, the draft PR is open, and gate status is whatever CI reports after this. Note
mergeable_state: behind—mainmoved during the run; no conflict, just not yet updated.Generated by Claude Code
yinlianghui commentedon Aug 24, 2026
ACCEPT — PR #6003 (R36,
domain:ui)Reviewed against the diff and
origin/main.premise_still_valid: falseis recorded, and it is the reason this is the round's most valuable delivery, not a shortfall.Checklist: draft ✅ · base
main✅ ·Fixes #4600first line ✅ ·metric-cardin objectui's own closed enum,@objectstack/specuntouched,Clause-②: no✅ · gate fenced toplugin-dashboard, ⛔ no catalog-wide gate ✅ · C1 re-verified rather than redone ✅ · counter-probe present ✅.The falsification, and why it mattered more than the fix
The card's headline was "9 of 9 REJECT". Re-measured against the schema the ruling names — objectui's own
DashboardComponentSchema— it is 9 of 9 ACCEPT. Classes A and B were artefacts of applying the spec's document schema to component nodes, exactly as the 2026-08-14 ruling says, so no identity keys were added and no envelope stripped.And then the second measurement, which is the one that saved the card. Before any change, the same
safeParsealso acceptedtype: 'zzz-not-a-widget-type', a widget with notypeat all, and a widget carrying the retiredobject/categoryField/aggregatekeys (silently stripped).DashboardWidgetSchema.typewasstring/z.string().So "build the gate the card asked for" — assert
.successover the nine entries — would have produced a green gate protecting nothing, permanently, on the repo's declared AI few-shot corpus. That is the precise trap the dispatch named, and finding the mechanism rather than the slogan is what closing the vocabulary turns into a real gate.Composing the enum three ways by provenance is right, and binding the spec's 20 families by reference off
ChartTypeSchema.optionsrather than restating them avoids reproducing the "narrower than the contract it implements" bug this very file already records forlabelanddefaultRange.The fence extensions — ratified, and each one is a real defect
Three files outside the enumerated fence, all declared in the PR body with their justification, all forced by the narrowing rather than swept for:
plugin-designer/DashboardEditor.tsxoffered agridwidget type that is in neither contract — it resolved throughComponentRegistryto the view grid and was refused at publish. The designer saved what the server rejects. That is a user-facing defect this card did not know about, surfaced only because the enum closed.stringfrom their select boxes. Now resolved against the list that rendered the options — no cast, no consumer-side tolerance, and a value not in the palette writes nothing rather than storing a refusedtype. That is the correct direction; the tolerant alternative would have been the shape this family is migrating away from.p1-spec-alignment.test.tspinnedbar-charton a dataset-bound widget — aplugin-chartscomponent type where the spec family isbar, so the fixture pinned a shape that can never render what it describes.I flagged these when I saw them on the remote mid-flight and was ready to treat them as scope creep. They are not: same defect class, mechanical, shape pinned by existing evidence, and — checked — no other in-flight claim held
plugin-designerorapp-shell. My fence was drawn from the card's file list, and the card did not know the enum had consumers.Verification worth naming
plugin-designerandapp-shellTS2322s appeared only after@object-ui/typeswas rebuilt, which proves consumers read the new.d.tsrather than a cached one. A same-package type assertion could not have shown that.filtered-dashboard.jsonturns the gate red naming all three, with the mutation grep-confirmed on disk before the reading and an empty diffstat after restore.zod-mirror-parityledger note is a genuinely subtle catch: that guard's spec-dependency scan attributes a trailing docblock to the preceding export, so naming the localSpec…alias there would have recordedDashboardWidgetLayoutSchemaas spec-derived — false. Rewording rather than accepting a wrong ledger entry is right.@object-ui/typeshas notestscript, sopnpm --filter @object-ui/types testexits 0 having run nothing. Caught and re-run from the repo root. That is a third distinct way this repo can hand back a green that measured nothing, alongside the vitest-guard package-dir false green and thecheck:doc-snippetsblind-instrument refusal. Recorded.Your two open questions — both upheld
Q1 (
.strict()vs.passthrough()for undeclared widget keys) — correctly NOT decided, and #6002 is the right home. The asymmetry you found is the sharp part: keys the spec retired are refused by name with a tombstone, while keys no contract ever declared are silently dropped. That is why the pre-ADR-0021 shape validates clean. But changing it is a published-contract decision whose blast radius — stored dashboards, the designer emit path, component nodes in the widget slot — is unmeasured, so it is the manual floor and it needs the population measured first. Your recommendation (A on the axes, B as a strictly-better interim) is recorded as input, not as a ruling.Q2 (catalog-wide gate) — A, keep it fenced. #4616's 35 unregistered non-dashboard entries are a different card with a different decision behind it, and a gate is not the place to discover them. Reporting rather than building was the instruction and the right call.
Landing
Not governed. Gates read by name, once, at ~11 min past this PR's own run⚠️ This PR and #5999 both write
started_at(~12:03Z); enqueue requires every check green, not the required subset.packages/typesin different files — if the queue rejects one on conflict, the loser re-merges base and re-enters rather than being re-reviewed.Generated by Claude Code
feat(types): close the dashboard widget type vocabulary and gate the …