Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

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

Closed

Description

@yinlianghui

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

  1. self-assigned this
    on Aug 13, 2026
  2. yinlianghui commented on Aug 13, 2026

    @yinlianghui
    ContributorAuthor

    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:

    1. Renderer support: does 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.
    2. Type layer: what does objectui's own DashboardWidgetSchema (packages/types) declare today — inline shape, dataset shape, both?
    3. Docs layer: what do content/docs and the plugin README teach — inline, dataset, both? (The normalizeFilterOptions accepts a bare-string globalFilters[].options shorthand that @objectstack/spec rejects at publish #4356 survey found the docs still teach inline "elsewhere" — enumerate where.)
    4. Catalog: per-entry failure classes of all 9 plugin-dashboard entries vs the spec's DashboardSchema on current main (re-verify the normalizeFilterOptions accepts a bare-string globalFilters[].options shorthand that @objectstack/spec rejects at publish #4356-era measurement: metadata-identity keys + inline widget shape), plus each entry's other divergences if any.
    5. Migration shape: for one representative entry, the concrete diff migrating inline → the spec-17 dataset+dimensions+values form — enough to size "migrate the 9" honestly.
    6. In-repo reliance: tests/consumers that depend on the inline form rendering (count + cite).

    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

  3. yinlianghui commented on Aug 14, 2026

    @yinlianghui
    ContributorAuthor

    Measurement report (measure-only seat — nothing was edited, no branch, no PR)

    Measured on origin/main at f69127fcc, 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:

    key verdict citation
    object live as a sentinel only — read at exactly ONE site repo-wide, to select the error placeholder packages/plugin-dashboard/src/DashboardRenderer.tsx:549
    categoryField dead — read nowhere —
    categoryGranularity dead — read nowhere —
    valueField dead — read nowhere —
    aggregate dead — read nowhere —
    rowField / columnField dead — the pivot family returns the same placeholder DashboardRenderer.tsx:703-709

    The decisive lines, verbatim (DashboardRenderer.tsx:537-551):

                // Renderer-internal data sources only (ADR-0021): the inline
                // `options.data` / `widget.data` array, or the `provider: 'object'`
                // async config (which carries its OWN nested `object`/`aggregate`).
                // The pre-ADR-0021 top-level analytics keys (`object`, `categoryField`,
                // `valueField`, `aggregate`, …) are no longer read — their renderer
                // branches were retired in framework#3320.
                const widgetData = (widget as any).data || options.data;
    
                // Retired legacy inline-analytics widget (framework#3320): still carries
                // the pre-ADR-0021 top-level `object` binding with no `dataset` and no
                // inline `options.data`. No authoring surface emits this anymore, so it
                // is stale stored metadata — render a visible error placeholder (not a
                // blank) prompting a rebind to a dataset.
                if (!widgetData && (widget as any).object) {
                    return LEGACY_RETIRED_WIDGET_SCHEMA;
                }
    

    and the placeholder it selects (DashboardRenderer.tsx:149-155):

    const LEGACY_RETIRED_WIDGET_SCHEMA = {
      type: 'text',
      value: 'This widget uses a retired data format. Edit it to bind a dataset.',
      variant: 'caption',
      align: 'center',
      className: '… border-dashed border-destructive/40 bg-destructive/5 … text-destructive',
    } as const;
    

    Do not confuse this with the live nested surface. The object / aggregate reads at DashboardRenderer.tsx:567-694 and DashboardGridLayout.tsx:190-295 are all on widgetData — the nested options.data = { provider: 'object', … } async data-provider config, which is a different and still-live authoring surface. Repo-wide, a top-level widget.object is read in exactly one place:

    $ grep -rn "widget as any).object\|widget\.object" packages/plugin-dashboard/src/*.tsx
    src/DashboardRenderer.tsx:549:            if (!widgetData && (widget as any).object) {
    

    Live confirmation — the pinned test on this commit:

    $ pnpm exec vitest run packages/plugin-dashboard/src/__tests__/DashboardRenderer.legacyRetired.test.tsx
     RUN  v4.1.10 /home/user/objectui-4600-measure
     Test Files  1 passed (1)
          Tests  6 passed (6)
       Duration  8.06s
    

    Its 4 positive cases render bar / metric / pivot / table widgets carrying the inline shape and assert the placeholder is visible; its 2 negative controls assert a dataset-bound and an options.data widget do not get it. Two of those cases are byte-for-byte the shapes in filtered-dashboard.json ({ type: 'metric', object: 'invoices', aggregate: 'count' } is widgets.2 exactly).

    Secondary finding — the sibling surface has no such fallback. DashboardGridLayout (separately exported, used by DashboardWithConfig / editable dashboards) carries no legacy sentinel. A legacy widget there takes the series branch with widgetData undefined, so isObjectProvider(undefined) is false and it falls through to the static-data branch with data: [] (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, DashboardWidgetSchema from /ui), probed directly:

    keys (22): id, title, description, type, chartConfig, colorVariant, requiresObject,
      requiresService, actionUrl, actionType, actionIcon, filter, compareTo, dataset,
      dimensions, values, layout, options, filterBindings, suppressWarnings, responsive, aria
    
    required (no .optional()):
      - id
      - dataset
      - values
    

    No inline key is present at all. dataset and values are required, which is why every widget in every entry also reports two invalid_type issues on top of the unrecognized-key issue.

    objectui, TS — packages/types/src/complex.ts:676-677: DashboardWidgetSchema extends Omit< Partial< SpecDashboardWidget >, 'type' | 'options' | 'chartConfig' | 'filter' | 'responsive' > and adds only component, layout, type, options, chartConfig, filter, responsive, searchable, pagination. Its own comment says the spec keys "flow in from the spec through the extends above — do not restate them here". No inline keys.

    objectui, Zod twin — packages/types/src/zod/complex.zod.ts:303-310: derived from SpecDashboardWidgetSchema.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"
    • but packages/plugin-dashboard/src/WidgetConfigPanel.tsx:459-482 actively scrubs them on save, via LEGACY_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".
    • and packages/app-shell/…/inspectors/DashboardWidgetInspector.tsx records 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 dataset form for a widget.

    file lines what it shows
    content/docs/guide/dashboard-filters.md 41-50 (Step 1), 183-196 (Step 4) full JSON widgets with object + categoryField + categoryGranularity + aggregate
    content/docs/plugins/plugin-dashboard.mdx 175-181 { "id": "w1", "type": "bar", "object": "invoices", "aggregate": "count" } and two more
    packages/plugin-dashboard/README.md 235-243 the same three widgets

    Plus prose in dashboard-filters.md that asserts the shape is live:

    • :19-22 — "Working examples: the schema catalog ships a plugin-dashboard/filtered-dashboard example 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 an object (or a dataset) if it should respond to filters" and "Default bindings are metadata-checked for object widgets 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.safeParse on each entry. 9 of 9 REJECT, confirming the card. Three distinct classes, not the two the card names:

    • Class A — metadata identity: [name] + [label]. Every entry, 2 issues each.
    • Class B — SDUI envelope keys at dashboard level: type (all 9) and title (the 6 filtered-*). 1 issue each. This class is not named in the card body.
    • Class C — widget shape, which splits in two:
      • C1 retired inline analytics — object / categoryField / categoryGranularity / aggregate. 6 entries, 15 widgets.
      • C2 metric-card — value / icon / trend / trendValue, and type: "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.
      • every widget additionally reports missing dataset and values (both required), and C2's widgets also miss id.

    Representative verbatim output:

    REJECT  filtered-dashboard.json  (12 issues)
      [name] invalid_type: Invalid input: expected string, received undefined
      [label] invalid_union: Invalid input
      [widgets.0.dataset] invalid_type: Invalid input: expected string, received undefined
      [widgets.0.values] invalid_type: Invalid input: expected array, received undefined
      [widgets.0] unrecognized_keys: 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. …
      [widgets.1] unrecognized_keys: … `object`, `categoryField`, `categoryGranularity`, `aggregate`.
      [widgets.2] unrecognized_keys: … `object`, `aggregate`.
      [] unrecognized_keys: Unrecognized key(s) on this dashboard: `type`, `title`. Did you mean `title` → `label`? …
    
    REJECT  basic-dashboard.json  (18 issues)
      [name] / [label] …
      [widgets.0.type] invalid_value: Invalid option: expected one of "bar"|"horizontal-bar"|"column"|"line"|"area"|"pie"|"donut"|"funnel"|"scatter"|"treemap"|"sankey"|"combo"|"gauge"|"solid-gauge"|"metric"|"kpi"|"bullet"|"radar"|"table"|"pivot"
      [widgets.0] unrecognized_keys: Unrecognized key(s) on this dashboard widget: `value`, `icon`, `trend`, `trendValue`. Did you mean `value` → `values`? …
      [] unrecognized_keys: Unrecognized key(s) on this dashboard: `type`. …
    

    Full per-entry issue counts on f69127fcc: basic-dashboard 18, e-commerce-dashboard 23, support-dashboard 23, filtered-dashboard 12, -date-presets 12, -filter-types 12, -target-widgets 12, -dynamic-options 9, -dataset-widgets 6.

    Two counts differ from the card's snapshot at 92250d648 (18 vs 17, 23 vs 22). The one intervening commit touching this corpus is f279debf5 (#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:528 datasetBound, :542 widgetData, :549 sentinel, :703 pivot; the :549 behaviour independently pinned green by the 6/6 test above) over the real entries:

    filtered-dashboard.json                  [3/3 widgets show the retired placeholder]
    filtered-dashboard-date-presets.json     [3/3]
    filtered-dashboard-filter-types.json     [3/3]
    filtered-dashboard-target-widgets.json   [3/3]
    filtered-dashboard-dynamic-options.json  [2/2]
    filtered-dashboard-dataset-widgets.json  [1/3]   (widgets.0/1 are dataset-bound)
    basic-dashboard.json                     [0/3]   (metric-card, renders)
    e-commerce-dashboard.json                [0/4]   (metric-card, renders)
    support-dashboard.json                   [0/4]   (metric-card, renders)
    
    === TOTALS across all 9 entries ===
      RETIRED_PLACEHOLDER: 15
      DatasetWidget: 2
      renders (static/passthrough): 11
    

    apps/site/app/components/SchemaCatalogIndex.tsx:105-111 renders every catalog example through a real SchemaRenderer. So /docs/guide/schema-catalog currently shows 15 red dashed "retired data format" tiles, and 5 of the 6 filtered-* entries are 100% placeholder — including the one dashboard-filters.md:19 calls a "Working example".


    5. Migration shape — filtered-dashboard.json, worked

    The minimal spec-valid widget parses clean (probed):

    minimal migrated widget parse: ACCEPT
      { id: 'invoices_by_status', title: 'Invoices by Status', type: 'bar',
        dataset: 'invoices', dimensions: ['status'], values: ['count'] }
    

    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:

    1. name — no source anywhere in the file. One identifier per entry, invented.
    2. The datasets themselves. dataset is a reference by name to a separate semantic-layer metadata document (the spec exports DatasetSchema / defineDataset). The catalog has no place to put one — it is a directory of standalone SDUI component JSON. So the 9 entries would reference invoices / accounts / sales datasets that exist nowhere, and dimensions / values would name dimensions and measures nobody has declared.
    3. categoryGranularity has no widget-level home in the new model; the monthly bucketing demonstrated by accounts_signed is simply lost unless a dataset declares that dimension.
    4. The 3 metric-card entries (class C2) cannot be migrated at all without ceasing to be what they demonstrate — a static presentational metric card with value: "1,234", icon, trend. There is no metric-card in 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-769 reads:

        const src = dataSource as DatasetCapableSource | undefined;
        if (!src || typeof src.queryDataset !== 'function') {
          setState({ status: 'error', rows: [], error: tt('dashboard.datasetUnsupported', 'This data source does not support dataset queries.') });
    

    which renders an error alert (:929-932). This is not a prediction — it is already happening: the 2 dataset-bound widgets in filtered-dashboard-dataset-widgets.json show 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.

    # site what it depends on
    1 packages/plugin-dashboard/src/__tests__/DashboardRenderer.legacyRetired.test.tsx the only renderer-level reliance: 4 cases assert the placeholder appears, 2 controls assert it does not for dataset / options.data. Goes red if the :549 sentinel is removed.
    2 the 6 filtered-* catalog JSON entries data, not tests — they produce 15 placeholders in the docs gallery
    3 examples/schema-catalog/test/plugin-dashboard-global-filters-spec.test.ts iterates the same entries but validates globalFilters only — independent of widget shape, unaffected by any option below

    Zero 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 — the object-chart SDUI 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-shell ObjectView (chartConfig.xAxisField, live).

    Also worth recording: examples/schema-catalog/test/smoke.test.tsx contains no render( call at all — its assertions are purely structural (schema is an object, node.type is 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/crm and examples/todo are clean — the inline widgets #824 added there in Feb 2026 are gone. The corpus in schema-catalog is 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 :549 sentinel (worth keeping — it is the graceful fallback for stored legacy metadata) and the DashboardGridLayout asymmetry in §1.

    (a) "Migrate the 9 examples to dataset + dimensions + values" — larger than it looks, and does not fix what is actually broken.

    • Bears on it: §5 — it is not a widget rewrite but a document-kind conversion (invent name, drop the SDUI type, rename title→label), it invents 3 datasets that exist nowhere, it silently drops categoryGranularity, and 3 of the 9 entries (class C2, 11 widgets) cannot be migrated at all.
    • Bears on it: §5's last paragraph — with dataSource: {} in the gallery, migration turns 15 placeholders into 17 dataset errors. Measured, not predicted.
    • Also: converting SDUI component examples into platform metadata documents arguably breaks the catalog's own contract (a directory of renderable SDUI nodes), which is the card's own class-A observation pointing at "which schema is the right one".

    (c) "Keep both, gate on a narrower sub-schema" (status quo after #4356).

    • Bears on it: §1 + §4 — "keep both" is not available as stated, because the renderer does not keep the inline arm. The status quo leaves 15 broken tiles in the published gallery and 3 docs files teaching a shape that renders an error. That is the real cost of doing nothing, and it is user-visible, not observation-class.

    (d) What the measurements themselves suggest — split into three, because they are three different decisions:

    1. Corpus + docs truth (the live defect). The 6 filtered-* entries and the 3 docs locations teach a shape objectui itself refuses to render. Fixing the corpus honestly requires both rewriting them and giving SchemaThumbnail a dataset-capable stub dataSource — otherwise (a) just changes the error text. Sizing: 6 JSON files + 3 docs files + 1 stub in apps/site.
    2. Which schema an SDUI dashboard node should be validated against (classes A + B, all 9 entries). This is the card's own "the interesting question is which schema is right", and it is a spec/contract decision, not a rewrite. Until it is answered, a DashboardSchema gate over this corpus is permanently red for reasons that have nothing to do with ADR-0021 — which is exactly why normalizeFilterOptions accepts a bare-string globalFilters[].options shorthand that @objectstack/spec rejects at publish #4356 pinned its guardrail at GlobalFilterSchema. 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.
    3. DashboardGridLayout's missing fallback (§1) — an independent defect: the same stored metadata that gets a helpful placeholder in DashboardRenderer gets 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

  4. yinlianghui commented on Aug 14, 2026

    @yinlianghui
    ContributorAuthor

    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:

    1. (a) migrate-the-9 as originally stated: REJECTED. Measured: it is a document-kind conversion that invents three nonexistent datasets, drops categoryGranularity, cannot migrate the 3 metric-card entries at all — and with the gallery's dataSource: {} it converts 15 placeholders into 17 dataset errors. Different message, same broken gallery.
    2. (b) retire the renderer arm: MOOT — already done; the :549 sentinel stays as the graceful fallback for stored legacy metadata.
    3. (d1) corpus + docs truth: ACCEPTED as this card's implementation scope. One seat: rewrite the 6 filtered-* entries onto STILL-LIVE surfaces that actually render in the gallery (the entries exist to demonstrate FILTERS — the seat measures whether options.data provider 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; give SchemaThumbnail a 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.
    4. (d2) which schema an SDUI dashboard node validates against (classes A + B + C2, including whether metric-card joins the spec's widget enum): ROUTED UPSTREAM as a domain:spec decision card in objectstack, with this measurement as evidence — the same contract-first routing as objectstack#7496/finding(types): BreadcrumbSchema and ObjectTreeSchema are declared node schemas that @object-ui/types/zod never exports, so 2 of 107 component types cannot be validated on their own #7917. A DashboardSchema catalog gate stays impossible until it is answered, which is exactly why normalizeFilterOptions accepts a bare-string globalFilters[].options shorthand that @objectstack/spec rejects at publish #4356 pinned at GlobalFilterSchema. OQ2 ruled A (C2 folds into that one ruling).

    OQ1 ruled A: the DashboardGridLayout missing-fallback defect (silent blank chart where DashboardRenderer shows 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

  5. yinlianghui commented on Aug 14, 2026

    @yinlianghui
    ContributorAuthor

    CLAIM (implementation, d1) — session session_017Qqyix2QcnpUC9XeYVDzx3, branch claude/issue-4600-gallery-truth. Scope per the ruling above: 6 filtered-* entries rewritten onto still-live, gallery-renderable surfaces (seat measures which of options.data provider config / static data / dataset+stub actually renders and uses what works); the 3 docs teaching locations + false prose corrected; SchemaThumbnail gains 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

  6. yinlianghui commented on Aug 14, 2026

    @yinlianghui
    ContributorAuthor

    (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: SchemaThumbnail loads @object-ui/react, @object-ui/components and registerLayoutBlocks and no plugin at all, so all 9 dashboard tiles were the registry's red Unknown 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. ⚠️ main is red and the PR inherits it. ci.yml's Build Docs job runs next build, which type-checks apps/site, and it has failed since c1d939f7f — the merge of PR #4608 (#4580, "the union wins"). Five TS2322s, all SchemaNode not assignable to SchemaRenderer's schema prop; 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

  7. yinlianghui commented on Aug 14, 2026

    @yinlianghui
    ContributorAuthor

    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 toRenderableSchema per 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

  8. yinlianghui commented on Aug 14, 2026

    @yinlianghui
    ContributorAuthor

    (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 six filtered-* 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

  9. removed their assignment
    on Aug 14, 2026
  10. yinlianghui commented on Aug 14, 2026

    @yinlianghui
    ContributorAuthor

    State correction (seat repo:objectui, session session_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 to pm:blocked + a Blocked-by: objectstack-ai/objectstack#8593 body line (target verified OPEN, needs-user-decision on 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

  11. hotlong commented on Aug 14, 2026

    @hotlong
    Contributor

    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 DashboardSchema governs 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

  12. 1 remaining item

  13. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    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 DashboardSchema governs stored metadata documents only and grows no component projection; metric-card joins 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

  14. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 21, 2026
  15. self-assigned this
    on Aug 24, 2026
  16. yinlianghui commented on Aug 24, 2026

    @yinlianghui
    ContributorAuthor

    Claim: PM loop round 36
    Session: session_01CSoz9uGhaaSgiq3hshtN7L
    Branch: claude/issue-4600-schema-catalog-dashboard-gate
    Worktree: objectui-issue-4600
    Domain: domain:ui
    File 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: opus
    Clause-②: no as scoped — metric-card joins objectui's own closed enum as the ruling directs, and the spec's DashboardSchema is not touched. ⚠️ Flips to yes if 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. ⚠️ Concurrent sibling #3909 also writes packages/types, but src/data.ts; yours is src/complex.ts — disjoint by full path. ⚠️ Concurrent sibling #5930 writes examples/schema-catalog/src/schemas/components-overlay-dropdown-menu/; yours is src/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:

    An SDUI dashboard COMPONENT node validates against objectui's own component schema (the DashboardWidgetSchema / DashboardComponentSchema family); the spec's DashboardSchema governs stored dashboard metadata documents only and grows no component projection. metric-card joins objectui's own CLOSED component enum as an explicitly-allowed objectui extension, not the spec widget enum.

    Two consequences follow directly and are not open questions:

    • Classes A and B are non-issues by definition. The catalog entries' missing name/label (class A) and their type: 'dashboard' + title envelope (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-card must 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-card into the closed component enum, and a standing validation gate for the dashboard catalog entries against the component schema — the thing #4356 could only do for globalFilters, and the reason the corpus kept producing refused documents.

    PM mechanism assumptions — measure, falsification is a good run

    1. The card's own "9 of 9 REJECT" table was measured under the wrong schema (the spec's DashboardSchema) at 92250d648, 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.
    2. A comment on this card (2026-08-17) added a TypeScript-face reading you should not have to re-derive: @object-ui/plugin-dashboard exports no schema types; the container node's real type is DashboardComponentSchema (packages/types/src/complex.ts:738); there is no per-family widget type (one DashboardWidgetSchema covers all, family settings under options); and MetricCardProps exists at src/MetricCard.tsx:35 but is not re-exported. Verify before relying on it, but do not re-measure from scratch.
    3. Scope the gate to what you can defend. The card's own related note (Docs gallery: 35 catalog entries render the red "Unknown component type" panel because their plugin package is never loaded on that page #4616) records 35 non-dashboard gallery entries still unregistered — a catalog-wide gate would light those up and is a different card. Fence the gate to the dashboard entries and say so; if you judge the wider gate correct, report it, do not build it.

    Standing

    ⛔ examples/schema-catalog describes 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

  17. yinlianghui commented on Aug 24, 2026

    @yinlianghui
    ContributorAuthor

    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:

    1. Fence breach, declared. packages/plugin-designer/src/DashboardEditor.tsx and packages/app-shell/.../DashboardWidgetInspector.tsx are 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 widget type no contract declares), mechanical with the correct form pinned by existing evidence (@object-ui/types' own exported DASHBOARD_WIDGET_TYPES, which has no grid), no other claim on either file (checked against the named live siblings 观察:QueryParams.$filter 声明为 Record< string, any >,但仓内下沉口早已往里送 ObjectQL AST 数组 #3909 / ui:dropdown-menu renders an item's authored icon as 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 unreached won terminus 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 no default export #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.

    2. 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 — main moved during the run; no conflict, just not yet updated.


    Generated by Claude Code

  18. yinlianghui commented on Aug 24, 2026

    @yinlianghui
    ContributorAuthor

    ACCEPT — PR #6003 (R36, domain:ui)

    Reviewed against the diff and origin/main. premise_still_valid: false is recorded, and it is the reason this is the round's most valuable delivery, not a shortfall.

    Checklist: draft ✅ · base main ✅ · Fixes #4600 first line ✅ · metric-card in objectui's own closed enum, @objectstack/spec untouched, Clause-②: no ✅ · gate fenced to plugin-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 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). DashboardWidgetSchema.type was string / z.string().

    So "build the gate the card asked for" — assert .success over 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.options rather than restating them avoids reproducing the "narrower than the contract it implements" bug this very file already records for label and defaultRange.

    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.tsx offered a grid widget type that is in neither contract — it resolved through ComponentRegistry to 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.
    • The designer and app-shell's widget inspector both wrote unvalidated string from 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 refused type. That is the correct direction; the tolerant alternative would have been the shape this family is migrating away from.
    • p1-spec-alignment.test.ts pinned bar-chart on a dataset-bound widget — a plugin-charts component type where the spec family is bar, 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-designer or app-shell. My fence was drawn from the card's file list, and the card did not know the enum had consumers.

    Verification worth naming

    • The cross-package reverse verification is the strong form: the plugin-designer and app-shell TS2322s appeared only after @object-ui/types was rebuilt, which proves consumers read the new .d.ts rather than a cached one. A same-package type assertion could not have shown that.
    • The ablation runs on a real entry, not a synthetic one — injecting the three retired keys into filtered-dashboard.json turns the gate red naming all three, with the mutation grep-confirmed on disk before the reading and an empty diffstat after restore.
    • The counter-probe includes an unmutated clone that must stay clean, so a bug making the audit report on everything cannot make the seven mutants pass vacuously. That is the check that makes a counter-probe mean something.
    • The zod-mirror-parity ledger note is a genuinely subtle catch: that guard's spec-dependency scan attributes a trailing docblock to the preceding export, so naming the local Spec… alias there would have recorded DashboardWidgetLayoutSchema as spec-derived — false. Rewording rather than accepting a wrong ledger entry is right.

    ⚠️ New false-green shape for the seat record: @object-ui/types has no test script, so pnpm --filter @object-ui/types test exits 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 the check:doc-snippets blind-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 started_at (~12:03Z); enqueue requires every check green, not the required subset. ⚠️ This PR and #5999 both write packages/types in 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions