Repository navigation
feat(spec): ToolExecutionContext.surfaceContext — what the user is currently discussing (cloud#1610) - #11417
Conversation
…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>
📓 Docs Drift CheckThis PR changes 1 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 126 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
…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>
Contract half of cloud#1610 (the maintainer's 「AI 该知道我们在讨论什么」 design, 契约先行 per the epic's agreed order). Optional, advisory
surfaceContextonToolExecutionContext: Studiopillar, the selectedartifactwith its metadata-type discriminator (an interfaces nav leaf may be a page / object view / dashboard / report — the type is what makes 「改这个」 unambiguous), an optional finerselection, and the canvasmode. Strictly additive besidecurrentObjectName/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