Skip to content

feat(spec): ToolExecutionContext.surfaceContext — what the user is currently discussing (cloud#1610) - #11417

Merged
os-zhuang merged 1 commit into
mainfrom
feat/surface-context-contract-1610
Aug 23, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
feat/surface-context-contract-1610

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Contract half of cloud#1610 (the maintainer's 「AI 该知道我们在讨论什么」 design, 契约先行 per the epic's agreed order). Optional, advisory surfaceContext on ToolExecutionContext: Studio pillar, the selected artifact with its metadata-type discriminator (an interfaces nav leaf may be a page / object view / dashboard / report — the type is what makes 「改这个」 unambiguous), an optional finer selection, and the canvas mode. Strictly additive beside currentObjectName/currentViewName; never an access input.

Cloud's parse+inject half and the console's derive+send half follow in their own repos. spec suite: 415 files / 11122 tests green.

🤖 Generated with Claude Code

…rrently discussing (cloud#1610)

The chat context contract carried only currentObjectName/currentViewName,
so the agents never knew WHICH pillar, flow, dashboard, page or report a
question was about — measured live: with an object OPEN, the ask agent
answered 'no such object' (cloud#1610). This adds the optional, advisory
surfaceContext: pillar, artifact WITH its metadata-type discriminator
(an interfaces nav leaf may be a page/object/dashboard/report — the type
is what disambiguates '改这个'), an optional finer selection, and the
canvas mode. Strictly additive; consumers treat every field as optional
and never use it for access decisions. Cloud parses+injects it and the
console derives it from route+selection in their own halves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 3 documentable anchor(s).

8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/contracts/metadata-service.mdx (via sales_overview (literal))
  • content/docs/protocol/kernel/i18n-standard.mdx (via sales_overview (literal))
  • content/docs/protocol/knowledge.mdx (via ToolExecutionContext (symbol))
  • content/docs/protocol/objectui/actions.mdx (via sales_overview (literal))
  • content/docs/protocol/objectui/concept.mdx (via sales_overview (literal))
  • content/docs/protocol/objectui/index.mdx (via sales_overview (literal))
  • content/docs/ui/apps.mdx (via sales_overview (literal))
  • content/docs/ui/dashboards.mdx (via sales_overview (literal))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via ToolExecutionContext (symbol))
  • content/docs/releases/v9.mdx (via ToolExecutionContext (symbol))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 45 of 222 client-bound route-ledger rows — the other 177 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 126 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json bb006446adb558e5a86c009df9c0b08e98833011 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1c53206282d00935c7c18343f29e663cd3fd597c — the merge of head 1dcb7342478b62aeb7432b574fc90c51f83f2dd7 into base bb006446adb558e5a86c009df9c0b08e98833011, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 1c53206282d00935c7c18343f29e663cd3fd597c && git checkout 1c53206282d00935c7c18343f29e663cd3fd597c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin bb006446adb558e5a86c009df9c0b08e98833011 1dcb7342478b62aeb7432b574fc90c51f83f2dd7 && git checkout -B drift-repro bb006446adb558e5a86c009df9c0b08e98833011 && git merge --no-ff 1dcb7342478b62aeb7432b574fc90c51f83f2dd7

node scripts/docs-audit/affected-docs.mjs --json bb006446adb558e5a86c009df9c0b08e98833011

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs bb006446adb558e5a86c009df9c0b08e98833011 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling labels Aug 23, 2026
@os-zhuang
os-zhuang enabled auto-merge August 23, 2026 16:41
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 23, 2026
Merged via the queue into main with commit 122ef38 Aug 23, 2026
33 checks passed
@os-zhuang
os-zhuang deleted the feat/surface-context-contract-1610 branch August 23, 2026 17:07
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ne measure with a dimension too — refuse two or more at values, and rename the check to checkDashboardWidgetChartMeasureArity (objectstack-ai#21293) (objectstack-ai#21425)

Fixes objectstack-ai#21293
Clause-②: yes (narrowing)

Extends objectstack-ai#20958's per-type measure-arity rule to the dimensioned arm, as
triage ruled on objectstack-ai/objectui#11417 (comment `5943580519`,
carried onto this card as `5944795015`): for `pie`, `donut`, `funnel`,
`treemap` and `sankey`, two or more measures are refused **whatever the
dimension**. It uses objectstack-ai#20958's mechanism and message shape: the same
check, one more arm, and one ADR-0087 semantic entry in objectstack-ai#20958's shape.
No second rule is added. `scatter` and `radar` with a dimension are
outside the ruling and parse as before.

## What changed

- **`packages/spec/src/ui/dashboard.zod.ts`**
- New module-private `SINGLE_SERIES_CHART_TYPES` (`:784`): `pie` /
`donut` / `funnel` / `treemap` / `sankey`, typed `as const satisfies
readonly ChartType[]`. It is private, like the metric family's list. The
check, its refusal text and the `values` doc string read it.
- The check gains a second arm, the **single-series arm**. When
`dimensions` is declared and non-empty, `values` has two or more
members, and `type` is one of the five, it emits ONE `custom` issue at
`values`. The issue names the widget `id`, the count and the type, and
says the type "draws ONE series whatever its `dimensions`". It steers to
`type: 'table'` (a column per measure), a bar-family type (one bar per
measure in each category), or one widget per measure.
- The dimensionless arm is unchanged, byte for byte, in both verdict and
message. The arms are exclusive on `dimensions`, so a dimensionless
`pie` still gets the dimensionless message, and only one issue.
- The metric family still returns early (its sibling check owns it), and
so do types outside `ChartTypeSchema`.
- **Renamed export:** `checkDashboardWidgetDimensionlessMeasureArity` is
now `checkDashboardWidgetChartMeasureArity` (`:878`, chained at
`:1461`). The reason is in "The rename" below.
- The `values` doc string now states the single-series rule. It reads
the private constant.
- **ADR-0087 entry**
`packages/spec/src/migrations/entries/semantic/18.dashboard-widget-single-series-multi-measure-refused.ts`,
plus the `gen:migration-registry` lap. Nothing between the markers was
hand-edited. Its acceptance criteria name the doors as measured (below),
state that the TODO cannot name per-document measures, and list what is
and is not refused. It also records the export rename (FROM → TO).
- **The objectstack-ai#20958 entry, amended in two sentences**
(`18.dashboard-widget-dimensionless-multi-measure-refused.ts`). It named
the old export, and it said "the same seven types WITH a dimension keep
accepting several measures exactly as before". This change makes both
false, and `os migrate meta` prints both entries in the same hop. They
now name the new export (noting the old name) and point the five at this
entry. This file is outside the claim's listed file surface; see
Deviations.
- **Tests**
- `dashboard.test.ts`: a new `[objectstack-ai#21293]` block, plus the fixture triage
below.
- `object-refinement-check-exports.test.ts`: the parity catalogue row is
renamed, and its fixtures are triaged.
- **Changeset**
`.changeset/21293-single-series-multi-measure-refused.md`: `minor`, a
**BREAKING** banner, the `(narrowing)` arm, and exactly one ADR-0087
marker (`registered
dashboard-widget-single-series-multi-measure-refused`). It carries a
FROM → TO table that includes the export rename.
- **Regenerated:**
- `api-surface/ui.json` and `export-origins/ui.json`: one row renamed in
each, `-1 / +1`.
  - The `values` row of `content/docs/references/ui/dashboard.mdx`.
  - `src/migrations/registry.ts`.

## The rename, and what objectui's mirror chains today

The check now judges widgets WITH a dimension, so
`...DimensionlessMeasureArity` names a boundary that no longer exists. A
mirror deciding which export to chain reasons from that name first. This
is the cheapest moment there will ever be to change it, because nothing
chains it yet:

- At the `.objectui-sha` pin `89cad75d55702cc4f267bead5bf267de575d5842`,
`git grep "DimensionlessMeasureArity\|MULTI_MEASURE_TYPES"` returns **0
hits**. The control, `checkDashboardWidgetMetricMeasureArity` in
`packages/types/src/zod/complex.zod.ts`, returns 2. objectui's
`DashboardWidgetSchema` mirror chains `checkDashboardWidgetStageOrder`
and `checkDashboardWidgetMetricMeasureArity` only
(`complex.zod.ts:1304`, and the `attached` row in
`spec-object-refinements-7715.test.ts:135`).
- On objectui `origin/main` `5988b6b53` the readings are the same: 0
hits, control 2.
- In this repository the old name had no consumer outside
`packages/spec`'s own tests (`git grep`).
- The spec object's check COUNT is unchanged at three, so objectui's
census test sees no count change at the bump.

A rename is a removal plus an addition, so the changeset carries the
import FROM → TO, and `Clause-②` is `yes`.

## Measured before building (the dispatch's four hypotheses)

The branch base is `4b20c84748`. It is newer than the dispatch's
`1d0600bf66`, and `dashboard.zod.ts` did not change between the two.

- **H1, holds.** The check returned early on any non-empty `dimensions`
(old `:834`), and `DASHBOARD_WIDGET_MULTI_MEASURE_TYPES` listed the
eight multi-measure types. At the base I built `dist` and ran
`DashboardWidgetSchema.safeParse` on `{ id, type, dataset, dimensions:
['stage'], values: ['revenue', 'cost'] }`. It returned **ACCEPT** for
`pie`, `donut`, `funnel`, `treemap`, `sankey`, `scatter`, `radar`, `bar`
and `table`. One measure returned ACCEPT on all of them. The lit
control, the same body plus `bogusProp`, returned REFUSE
`unrecognized_keys` on every type.
- **H2, holds.** objectui's renderer at the pin
(`packages/plugin-charts/src/AdvancedChartImpl.tsx`) binds the first
series on exactly these arms:
- `pie` / `donut` at `:2095` (`const pieDataKey = series[0]?.dataKey ||
'value'`);
- `funnel` at `:2161`, `treemap` at `:2243` and `sankey` at `:2278`
(`const dataKey = series[0]?.dataKey || 'value'`).
  - `radar` maps every series (`:2406` onward, `series.map(...)`).
- objectui's `DatasetWidget.tsx` at the pin already names the same five
as `SINGLE_SERIES_CHART_FAMILIES` (`:387`–`:413`), which is the objectstack-ai#11417
diagnostic. It says `scatter` "refuses a second series out loud".
- **H3, partly falsified.** There is no declared "order":
`build-migration-registry.ts` derives the order by sorting entry ids
("Order is DERIVED, never declared"). The serial constraint did hold. PR
objectstack-ai#21399 (`analytics-query-window-non-negative-integer`) landed on `main`
first, so this branch merged `origin/main` `db3fee3dc9` through
`scripts/pm/os-regen-merge.sh` (merge commit `b3b3c516f9`).
  - The registry text-merged clean.
- `gen:migration-registry` on the merged tree rewrote it byte-identical:
346 semantic entries, `git status` empty.
- All three sibling entries are present at HEAD. Quoted-exact-name `git
grep`, HEAD vs `origin/main`:
`analytics-query-window-non-negative-integer` 1/1,
`dashboard-widget-dimensionless-multi-measure-refused` 1/1, and
`dashboard-widget-single-series-multi-measure-refused` 1 at HEAD, 0 on
main.
- **H4, census: no authored hit.** I scanned brace-local literals
carrying `values: [...]` over every tracked
`.ts/.tsx/.js/.mjs/.cjs/.json/.md/.mdx/.yml` file.
- objectstack at `4b20c84748`: 496 literals, 33 on the seven types, 1
dimensioned multi-measure on the five. That one is
`packages/spec/src/ui/object-refinement-check-exports.test.ts:365`, the
fixture that pinned the old acceptance, triaged below.
- Lit control: 9 dimensioned multi-measure literals on other types
(`table` / `combo` widgets in `examples/app-showcase`, and others).
- objectui at the pin: 480 literals, 13 on the seven, and 1 hit on the
five. That hit is the prose example in
`.changeset/11417-dashboard-single-series-dropped-measure.md:10`, not
metadata.
- `examples/**`, the showcase and CRM apps, and
`packages/platform-objects` dashboards carry zero instances.
- Dynamically built fixtures do not show up in a static scan. I
inspected each downstream test that loops over chart types or builds
widgets (`lint` `validate-widget-bindings`, `runtime-gate`,
`validate-dashboard-widget-options`, and `metadata-protocol`'s two donut
fixtures). All carry one measure.

## Doors after the change (built `dist`, one-shot probe, deleted
afterwards, `git status` clean)

| body (`dimensions: ['stage']`) | `DashboardSchema` |
`getMetadataTypeSchema('dashboard')` | `ObjectStackDefinitionSchema` |
`defineStack` |
|---|---|---|---|---|
| `pie`, 2 measures | REFUSE `widgets.0.values:custom` | REFUSE
`widgets.0.values:custom` | REFUSE
`dashboards.0.widgets.0.values:custom` | throws `defineStack validation
failed (1 issue)`, naming the widget |
| `sankey`, 2 measures | REFUSE | REFUSE | REFUSE | throws |
| `pie`, 1 measure (control) | ACCEPT | ACCEPT | ACCEPT | returns |
| `radar`, 2 measures (control) | ACCEPT | ACCEPT | ACCEPT | returns |
| `bar`, 2 measures (control) | ACCEPT | ACCEPT | ACCEPT | returns |

I did not re-measure `os validate` or the metadata save path. The entry
does not claim them. They parse through the same schema at the same
attachment point as objectstack-ai#20958, which measured both.

## Fixture triage (pins whose premise this change makes false)

- **`object-refinement-check-exports.test.ts`.** The row "two measures
on a `pie` WITH a dimension — out of this rule" (`refusesAt: []`) is
this arm's subject now. It becomes five refusing rows, one per
single-series type. Accepting rows are added for `scatter` / `radar`
with a dimension and for a one-measure dimensioned `pie`.
- **`dashboard.test.ts`, the objectstack-ai#20958 block.** "accepts `%s` with ONE
dimension and two measures" iterated all seven types. It keeps `scatter`
and `radar`, and the five move to the new block as refusals.
- **`dashboard.test.ts`, the objectstack-ai#17779 block.** "leaves `%s` — every
NON-metric type — accepting three measures" ran on `WIDGET_BASE`, which
carries a dimension. Its own claim is that the METRIC check leaves
non-metric types alone, so that claim is now read off the metric export
directly for all 15 types. The parse leg keeps the 10 types no check
refuses.
- **objectstack-ai#20958's dimensionless pins are unchanged.** That covers the
refusals, the explicit `dimensions: []` refusals, the message pin and
the metric-family one-issue pin.

## New pins (`[objectstack-ai#21293]` block in `dashboard.test.ts`)

- Each of the five, with a dimension and two measures: REFUSE, `custom`
at `values`, message naming the type.
- The control: one measure on each of the five with a dimension: ACCEPT.
- Each of the five with TWO dimensions: REFUSE.
- `scatter` / `radar` with a dimension and two measures: ACCEPT.
- Every multi-measure type with a dimension and three measures: ACCEPT.
- **Taxonomy read off the door:** with a dimension, the set of
`ChartTypeSchema` types refused at two measures is exactly the metric
family plus the five.
- A dimensionless widget of each of the five keeps the dimensionless
message, word for word and as ONE issue. The parse leg equals the direct
call.
- The metric family with a dimension keeps its own refusal, and the
chart export stays silent on it.
- Direct-call legs: a dimensioned `pie` is refused; a wider-enum type
(`custom`) and a dimensioned `scatter` are not.
- `DashboardSchema.widgets[]` travel, and the `values` doc string.
- The old export name is absent from `@objectstack/spec/ui`. The new
export is the lit control.

## Verification

Every reading below is at HEAD `b3b3c516f9` unless it names another
commit.

**Reverse verification**, at committed `221df98d72`, through
`scripts/ablation-replace.mjs`:

- The mutation turned the single-series arm's type guard into an
unconditional `return`. The subject is imported from `src` by relative
path, so no `dist` is involved.
- The prediction was "turns red", and that is the direction observed.

```
anchor x1 -> x0 · marker x0 -> x1 · blob 9a8c228 -> cfdc26ae3cbf (mutation landed)
RED   vitest exit=1   19 failed | 342 passed (361)   (dashboard.test.ts + object-refinement-check-exports.test.ts)
restored blob 9a8c228 == HEAD blob, git diff HEAD empty; marker count after restore 0
```

The 19 red tests:

- 14 in the new block: 5 refusals, 5 two-dimension refusals, the
taxonomy pin, the message pin, the direct-call pin and the travel pin;
- 5 parity rows in the exports catalogue, one per dimensioned
single-series fixture.

**Tests.** Each is the package's own vitest run. Downstream packages
were chosen by direction: the dependents of `@objectstack/spec` that
import or parse the dashboard schemas (`git grep`). This is not the full
`...@objectstack/spec` sweep.

| package | result |
|---|---|
| `@objectstack/spec` (`--project local`) | 600 files, 17666 passed, 1
todo, exit 0 |
| `@objectstack/spec` typecheck (tsc + scripts + test layer) | exit 0
(and at `221df98d72` before the merge). The test-layer ledger held at 52
files / 246 errors / 135 signatures; `tsc -p tsconfig.test.json
--listFiles` lists both touched test files and the new entry. |
| `@objectstack/lint` | 119 files, 5575 tests, exit 0 |
| `@objectstack/sdui-parser` | 13 files, 218 tests, exit 0 |
| `@objectstack/platform-objects` | 59 files, 949 tests, exit 0 |
| `@objectstack/metadata-protocol` | 201 passed, 3 skipped (204 files);
2983 passed, 19 skipped; exit 0 |
| `@objectstack/metadata` | 56 files, 836 tests, exit 0 |
| `@objectstack/service-analytics` | 168 files; 3794 passed, 89 skipped;
exit 0 |
| `@objectstack/cli` (`--project unit`) | 247 files, 3538 tests, exit 0.
The integration tier is declared to CI: no spawn entry or integration
file is touched. |
| `@objectstack/objectql` (`--project local`) | 364 files, 7360 tests,
exit 0 |

**Generated artefacts.** `pnpm --filter @objectstack/spec
check:generated` reports "All 15 generated artifacts are up to date" on
the merged tree, after a rebuild. `check:migration-registry` reports
`registry.ts is current (346 semantic, 243 retired-key, 212
retired-def)`.

**Gates.**

- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` at `b3b3c516f9` derives 115 commands. The 10 changed paths
are measured against merge base `db3fee3dc`; 671 changed lines.
- All 115 ran. `--ran` reconciliation: "115 derived, 115 run, 0
NOT-MEASURED, 0 UNRUN (a DERIVED zero)".
- Six first answered `PREREQUISITE NOT MET` (exit 3) and were re-run to
a real exit 0 after a workspace build:
- `check:doc-formula-expressions` and `check:doc-security-posture`
(`lint` / `formula` not built);
  - `check:skill-examples` (`client-react` not built);
  - `check:docs-transcript-drift` (`lint` not built);
  - `check:dual-build-cjs-loads` (86 packages had no `dist`);
  - `check:lean-entry-closure` (`objectql` not built).
- Compared with the seat's dispatch-time list (109), the re-derivation
adds six: the changeset-driven `check-empty-changeset` pair,
`release-rehearsal-clone --self-test`, `release-pending-publish
--self-test`, `check:objectui-changeset` and
`check:pm-changeset-deadline-census`. All six ran.
- Verdict lines:
- `check-adr-0087-registration --base origin/main`:
`[BREAKING+bang+clause-②-narrowing] registered
dashboard-widget-single-series-multi-measure-refused (new here:
dashboard-widget-single-series-multi-measure-refused)`.
- `check:api-surface`: "public API surface + factory signatures
unchanged ✓", against the regenerated snapshot.
- `check:doc-authoring`: no internal issue-id in the 17250
customer-facing strings. The refusal text carries none.
  - `check:nul-bytes`: OK.

**`Clause-②`, measured.** `node scripts/pm/check-widening-tells.mjs
--declaration no --diff` on `git diff origin/main...HEAD` exits 4:

- It reports five T2 tells, `dashboard.zod.ts:785`–`:789`. These are the
members of the module-private `SINGLE_SERIES_CHART_TYPES` `as const`
array. The array narrows the accept set, so these tells are false as
widenings.
- T3 does not fire, because the `api-surface/ui.json` hunk removes the
old row beside the new one.
- The export listing does gain a row:
`checkDashboardWidgetChartMeasureArity (function)`, the renamed export.
- `--declaration yes` exits 0.

So the measured arm is `Clause-②: yes (narrowing)`. The `yes` holds
because a renamed check function is exported for objectui's `.shape`
mirror to chain, which is the dispatch's own example. The `(narrowing)`
holds because the accept set shrinks.

**Lint**, a measured narrowing rather than the repo-wide run (that run
belongs to CI):

- I ran `eslint --no-inline-config --format json` over the 6 touched
`.ts` files. The JSON reports 6 files, 0 errors and 0 warnings.
- Population: `eslint --print-config` returns a rule set for each file,
so none is ignored.
- Invariance: `parserOptions.project` and `projectService` are null for
every file, and `eslint.config.mjs:327` states that it never enables
type-aware linting. This diff therefore cannot move a verdict on any
untouched file.

## Deviations

- **One file outside the claim's listed surface:** the objectstack-ai#20958 entry. Two
of its sentences, one naming the export and one saying the dimensioned
arm keeps accepting, became false in this change. `os migrate meta`
prints both entries in one hop, so leaving them would print a
contradiction. The standing dev definition requires fixing released text
that a change makes false. The claim's "stop on breach" reads the other
way, so the conflict is named here rather than settled silently. The
edit is two sentences, and the registry region is regenerated, not
hand-edited.

## Acceptance notes

None of these is filed. None is a reproducible defect, a contract
violation or an authoring trap.

- **`check-widening-tells` T2 fires on a module-private `as const` set
whose members narrow.** The declaration is `yes` for the export row
anyway, so the tells decide nothing here. The matcher was not touched.
The precedent (`SINGLE_MEASURE_WIDGET_TYPES`) predates the gate.
Carrier: none.
- **The single-series set is not exported.** objectui's dataset widget
keeps its own renderer-derived copy (`SINGLE_SERIES_CHART_FAMILIES`,
"Kept here, once, because the renderer has no declaration of it to
read"). No import of a spec list is measured anywhere. If objectui's
mirror or diagnostic later wants it, exporting it is a free widening.
Carrier: none.
- **No `STEP18_RATIONALE` fragment**, following objectstack-ai#20958's precedent.

## Downstream: objectui's mirror (not in this PR;
objectstack-ai/objectui#11417 carries it)

`@object-ui/types` builds `DashboardWidgetSchema` from a `.shape`
spread, so it runs only the checks it chains, and today it chains
neither arm. At the pin bump that carries this change:

- Import `checkDashboardWidgetChartMeasureArity` from
`@objectstack/spec/ui`.
- Chain it in `packages/types/src/zod/complex.zod.ts` after
`.superRefine(checkDashboardWidgetMetricMeasureArity)`.
- Add it to the `attached` list of the `DashboardWidgetSchema
(complex.zod.ts)` row in `spec-object-refinements-7715.test.ts`.
- Refusal shape: ONE `custom` issue at `path: ['values']`. With a
dimension, the message starts "Widget `ID` declares N measures on `type:
'T'`, a single-series type (...) that draws ONE series whatever its
`dimensions`". Without one, it keeps objectstack-ai#20958's "declares N measures with
no `dimensions`" text.

objectstack-ai/objectui#11417 remains open: this PR carries only the
spec half.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant