diff --git a/.changeset/17385-chartconfig-liveness-drill.md b/.changeset/17385-chartconfig-liveness-drill.md new file mode 100644 index 0000000000..a3c8b9e838 --- /dev/null +++ b/.changeset/17385-chartconfig-liveness-drill.md @@ -0,0 +1,16 @@ +--- +"@objectstack/spec": patch +--- + +**Clause-②: no** — no schema key moves, no accept set widens or narrows, no export changes. This is the liveness ledger stating what the renderer actually does with an authored `chartConfig`, at one verdict per key instead of one blanket verdict for fourteen. + +`packages/spec/liveness/dashboard.json`'s `widgets.chartConfig` row is **drilled**: it now carries `children`, one status + evidence per `ChartConfigSchema` key, re-measured against this checkout's own `.objectui-sha` pin `53ded82bf7a4`. The row ships — `packages/spec` publishes `liveness/` whole — so this changeset is a measurement, not a convention: `npm pack --dry-run` puts `liveness/dashboard.json`, `liveness/README.md` and `liveness/state-counts.md` in the tarball (38 files under `liveness/`), and the fourth changed path, the undrilled-containers baseline under `scripts/`, is not in it (0 files under `scripts/`). + +Per-key verdicts, all pinned in the renderer repo: + +- **12 live.** Nine chrome keys are lowered onto the chart schema by `chartConfigPresentation`, one guard each — `title`, `subtitle`, `description`, `colors` (split two ways into the positional palette and the per-category map), `height`, `showLegend`, `showDataLabels`, `annotations`, `interaction`. `xAxis`, `yAxis` and `series` join them by a different route: `mergeAuthoredPresentation` merges their **presentation** onto the bindings the dataset selection derived, dropping exactly the two binding keys `ChartAxis.field` and `ChartSeries.name` so that series membership and the plotted column stay with the dataset. +- **2 dead.** `chartConfig.type` parses and does nothing on a dashboard widget — the widget's own `type` picks the chart family — and `chartConfig.aria` has no reader on either face: the chart implementation declares no `aria` prop and the ARIA injection reads the flat `ariaLabel` / `ariaDescribedBy` / `role`. Both are pinned as **negatives** by name in the renderer's own tests, which is what makes them re-askable rather than merely asserted. + +Neither `dead` verdict is acted on here. Recording a verdict is what feeds the ADR-0049 enforce-or-remove worklist; executing one moves a published accept set and is a separate, ruled piece of work. + +The drill also makes six containers one level further down visible for the first time (`xAxis`, `yAxis`, `series`, `annotations`, `interaction`, `aria` — 39 child keys). They are **recorded** in the shrink-only undrilled-containers baseline rather than drilled: fanning this row's verdicts down over them would manufacture verdicts with no evidence behind them, which is the one thing the drill rule forbids by name. diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index f2f7d536e0..32cf9a4958 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -901,7 +901,7 @@ marker where the Notes cell goes, never a guess at what belongs there. | page | fully live + one planned | | view | list/form drilled via `children` (#2998 Track B); list.{responsive,performance} + form.{defaultSort,aria} REMOVED 2026-07-30 (#3896 close-out sweep — list aria/data stay live); **form.data was that sweep's one CORRECTION** — the removal attempt broke the build (`defineForm` writes `data.provider='schema'` onto every metadata form, `metadata-protocol` serves it), so it stands `live` with re-verified evidence; form.{buttons,defaults} live (framework#1894 / #2998); audit-era DEAD lines superseded by re-verification. **The dead set is six, not the four removals above**: #4534 (the last #4001 batch, batch 6e) declared three CONTAINER-level keys this row had never classified — `name` and `label`, both `dead`, and `object`, `live`. All three are properties of the `views: [...]` *container*, not of a view: `name` is dead as a BODY key because the live one is the `sys_metadata` row column the door supplies, and `label` is container display metadata with no reader. Neither is `authorWarn`'d and both are deliberately KEPT — the platform's own writers send `name` (artifact-shipped containers, the metadata-validation sweep), so tombstoning it would reject shapes we write ourselves. `object` is the container's object binding, and it was *stripped on every parse* until #4534 declared it. Separately, the level-2 dead residue (userActions.buttons, addRecord.mode/formView, tabs[].order) is noted on parents and is **not** in the counts — one drill level only **#9340**: `list.map` declared — the eighth visualization block (`ListMapConfigSchema`), keys mirroring objectui plugin-map's documented read set. FLIPPED `planned` → `live` 2026-08-24 (#11442): objectui#5908 landed `resolveListMapConfig`, which merges the view-level `map` block over the legacy `options.map` bag before `ListView.tsx`'s `case 'map'` forwards it into `ObjectMap`, with the same merged config also feeding the visualization-switcher's capability gate so a view binding coordinates only in the spec block is no longer filtered out of `allowedVisualizations` either (objectui#5042) | | report | dataset-bound (ADR-0021); the aria/performance LEDGER entries were stale — the keys left the schema in the report-liveness close-out; deleted 2026-07-30 as hygiene. Audit-era `chart` DEAD superseded (framework#1890 / #3441) | -| dashboard | ADR-0021 dataset widgets (#3251; DashboardWidgetSchema `.strict()`); `aria`/`performance` (and widget `performance` + PerformanceConfigSchema) REMOVED 2026-07-30 (#3896 close-out sweep — no renderer applied any of them); audit-era `globalFilters`/`dateRange` DEAD superseded (framework#2501) **#4956**: `widgets` DRILLED — the row jumps 20 → 41 classified because all 22 widget-level keys enter the count at once. They had never been classified at all: the entry carried one blanket `live` plus a `note` asserting they were classified "in the DashboardWidgetSchema subtree", and no such subtree existed in any of the 28 ledger files. That gap, not any evidence, is what carried `widgets[].responsive` through the #3896 sweep that removed both its sibling `widgets[].performance` and its literal namesake `view.responsive` — `view` is drilled, so `list.responsive` got asked and went out. New dead 6 = `responsive` (retired #4876/#4995, tombstone keeps the row) + `colorVariant` + `actionUrl`/`actionType`/`actionIcon` + `aria`. The action trio is the sharpest: no renderer draws a per-widget action button at all (every `actionUrl` read in DashboardRenderer is scoped to `header.actions[]`), yet `validate-dashboard-action-refs.ts` enforces reference integrity on it and its docblock calls it "the per-widget button" — a lint guarding an affordance that does not exist. `requiresService` is the counter-example worth remembering: dead by every objectui measurement, and LIVE server-side (`filterDashboardForUser`, ADR-0057 D10) — judging a widget key from the renderer repo alone would have retired an enforced gate. `compareTo` is `live` on ONE path only (inline object-provider charts); on the ADR-0021 dataset path the string arms are dropped and `{ offset }` throws in the executor. **#6774** moves the row 33/8 → 34/7: `colorVariant` CORRECTED dead → live 2026-08-09, the enforce leg of #5010 ruling B landing from the renderer side (objectui#3359 / PR objectui#3799, absorbed by pin `09987b68`). Worth reading beside `requiresService` above, because it is the same lesson from the other end — that row warns against judging a widget key from the renderer repo alone, and this one is a `dead` verdict that was correct in this repo AND correct in the renderer repo on the day it was measured, and stopped being either when a cross-repo decision was implemented. A ledger row is a claim with a timestamp; `verifiedAt` is what makes the claim re-askable. It also empties the dashboard warn set, so the author-side lint now says nothing about any widget key — `dashboard` stays in the lint's TYPE_COLLECTIONS all the same (the `webhook`/`email_template` resolved state) | +| dashboard | ADR-0021 dataset widgets (#3251; DashboardWidgetSchema `.strict()`); `aria`/`performance` (and widget `performance` + PerformanceConfigSchema) REMOVED 2026-07-30 (#3896 close-out sweep — no renderer applied any of them); audit-era `globalFilters`/`dateRange` DEAD superseded (framework#2501) **#4956**: `widgets` DRILLED — the row jumps 20 → 41 classified because all 22 widget-level keys enter the count at once. They had never been classified at all: the entry carried one blanket `live` plus a `note` asserting they were classified "in the DashboardWidgetSchema subtree", and no such subtree existed in any of the 28 ledger files. That gap, not any evidence, is what carried `widgets[].responsive` through the #3896 sweep that removed both its sibling `widgets[].performance` and its literal namesake `view.responsive` — `view` is drilled, so `list.responsive` got asked and went out. New dead 6 = `responsive` (retired #4876/#4995, tombstone keeps the row) + `colorVariant` + `actionUrl`/`actionType`/`actionIcon` + `aria`. The action trio is the sharpest: no renderer draws a per-widget action button at all (every `actionUrl` read in DashboardRenderer is scoped to `header.actions[]`), yet `validate-dashboard-action-refs.ts` enforces reference integrity on it and its docblock calls it "the per-widget button" — a lint guarding an affordance that does not exist. `requiresService` is the counter-example worth remembering: dead by every objectui measurement, and LIVE server-side (`filterDashboardForUser`, ADR-0057 D10) — judging a widget key from the renderer repo alone would have retired an enforced gate. `compareTo` is `live` on ONE path only (inline object-provider charts); on the ADR-0021 dataset path the string arms are dropped and `{ offset }` throws in the executor. **#6774** moves the row 33/8 → 34/7: `colorVariant` CORRECTED dead → live 2026-08-09, the enforce leg of #5010 ruling B landing from the renderer side (objectui#3359 / PR objectui#3799, absorbed by pin `09987b68`). Worth reading beside `requiresService` above, because it is the same lesson from the other end — that row warns against judging a widget key from the renderer repo alone, and this one is a `dead` verdict that was correct in this repo AND correct in the renderer repo on the day it was measured, and stopped being either when a cross-repo decision was implemented. A ledger row is a claim with a timestamp; `verifiedAt` is what makes the claim re-askable. It also empties the dashboard warn set, so the author-side lint now says nothing about any widget key — `dashboard` stays in the lint's TYPE_COLLECTIONS all the same (the `webhook`/`email_template` resolved state). **#17385** DRILLS `widgets.chartConfig` — 14 per-key verdicts where the row had carried one blanket `live`, re-measured against `.objectui-sha` pin `53ded82bf7a4`: 12 live (the nine chrome keys `chartConfigPresentation` lowers, plus `xAxis`/`yAxis`/`series`, whose PRESENTATION merges onto the derived bindings while `ChartAxis.field` and `ChartSeries.name` are dropped so membership stays with the dataset) and dead 2 — `type`, which parses and does nothing because the widget's own `type` owns the chart family, and `aria`, which has no reader on either face. Both are pinned as NEGATIVES in objectui, which is what makes them re-askable rather than merely asserted. ⚠️ The drill made SIX containers one level further down visible for the first time (`xAxis`/`yAxis`/`series`/`annotations`/`interaction`/`aria`, 39 child keys); they are RECORDED, not drilled — fanning this row's verdicts down over them would manufacture verdicts, and the evidence work is a separate measurement. Note the cell's previous last stated position (`34/7`) had already drifted one `dead` behind the generated artifact before this change; the counts columns are generated and are the authority | | query | **not a metadata type** — the REQUEST surface (`QuerySchema`: client SDK QueryBuilder output; the `POST /data/:object/query` body), governed via `SPEC_ONLY_SCHEMAS` (#4286). The gate resolves 1 experimental at the depth this ledger drills; the 7 marker-experimental search affordances sit one level deeper, below what this ledger declares (the walk recurses since #17424, but only where a `children` map is written, and none is written here) — resolved from `[EXPERIMENTAL — not enforced]` describe markers, not ledger entries (search `fuzzy`/`operator`/`boost`/`minScore`/`language`/`highlight` + `aggregations[].filter` — declared engine affordances no executor receives). The #4286 sweep closed out same-release: `having` ENFORCED 2026-07-31 (engine-side post-aggregation filter, both paths; was finding 1); dead 4 = the tombstoned removals `joins`/`windowFunctions`/`cursor`/`distinct` — REMOVED 2026-07-31 (retiredKey keeps each in the walked shape so the rows stay; protocol-17 semantic migrations; the JoinNode + WindowFunctionNode clusters and the `QueryBuilder.cursor()`/`.distinct()` producers deleted with their keys; `distinct`'s mis-wired REST count suppression deleted too — finding 2). **#6815** adds the 5th dead: `aggregations[].distinct` REMOVED 2026-08-09 (live → dead, `-1` live). It is the one member of this ledger the #4286 sweep could not have caught with the question it asked — that sweep looked for keys NO executor reads, and this one had a reader: the objectql in-memory fallback deduplicated before applying the function while all five other faces (driver-sql, driver-turso, driver-mongodb, driver-memory, service-analytics' `AGGREGATE_SQL`) ignored it, so one query answered two plausible NUMBERS depending on which backend served it. The lesson for the next audit is the question, not the key: a per-key `live` verdict is only as good as the count of faces it was measured across, and this row's 2026-07-31 evidence (`in-memory-aggregation.ts:167,204-206`) was TRUE and still the wrong verdict. `count_distinct` is the surviving spelling (enforce leg, #6409) | | datasource | seeded 2026-08-01 (#4487) — the **highest dead ratio of any governed type** (20 of 43), and it was ungoverned until now, which is not a coincidence: #4410/#4465/#4481 found six inert keys here by hand, two security-shaped (`schemaMode` left an external DB constructible as `managed` with DDL ungated; `ssl` configured nothing while looking configured). Dead set = `capabilities.*` (all 11 — the engine gates pushdown on the runtime driver's `supports.*` object, a non-overlapping vocabulary), `healthCheck.*` (3 — nothing schedules a datasource probe; the 20 `healthCheck` hits in the repo all belong to the PLUGIN health monitor and other surfaces), `retryPolicy.*` (4 — `retryPolicy` IS enforced on `hook` and `job`, which is what makes this one read alive; the shapes differ), `external.label`, `external.requirePermission`. **`capabilities.readOnly` is the one to know**: it reads as a safety switch, gates nothing, and two shipped prescriptions pointed authors at it until #4487 — `external.allowWrites: false` is the enforced write gate. `config` is a `z.record`, so its per-driver keys sit outside the walk (recorded in the entry's note, not silently skipped) **批 A CLOSED 2026-08-02 (#4583)**: the `capabilities` block — 11 flags, every one dead and authorWarn'd — was REMOVED rather than bridged; pushdown comes from the runtime driver's own `supports.*`, so there was nothing to connect it to. Its rows are deleted (strict-removal route), which is why dead falls 20 → 9. `readOnly` was the reason the audit was worth doing: it read as a safety switch, gated nothing, and had already been MOVED twice toward somewhere it might be enforced (#4410, #4465) — the shipped CRM example called a datasource a read replica on the strength of it while the datasource took writes. Removing it does NOT hand the author a working alternative: `external.allowWrites` only gates FEDERATED datasources, so a managed one has no read-only gate at all (#4584). Remaining 9 = healthCheck ×3 + retryPolicy ×4 + external ×2, batches B/C/D of #4583 **BATCHES B/C/D CLOSED 2026-08-02 — datasource now has ZERO dead properties**, down from the 20 it was seeded with (the highest dead ratio of any governed type). `retryPolicy` ×4 and `healthCheck` ×3 went as whole blocks, `external.label` / `external.requirePermission` as keys. None was bridgeable: each already had a different LIVE mechanism doing the job — the boot policy, the driver handle's on-demand `ping()`/`checkHealth()`, the top-level `label`, and ordinary permission sets + RLS. The `retryPolicy` rejection deliberately refuses to offer a rename: `hook`/`job` retryPolicy ARE enforced but spell the delay `backoffMs`, and that inconsistency is itself the evidence nothing read the datasource one (#4488's sharpest trap) | | webhook | **not a registered metadata type** — governed via the gate's spec-only schema override (`SPEC_ONLY_SCHEMAS`), not `getMetadataTypeSchema`; folding it onto the registry is the #3490 reassessment. This row once read 0/1/16 ("the ENTIRE authoring surface is dead", #3461) and both halves of that were CLOSED same-quarter: #3489 built the materializer bridge (authored `webhooks:` entries now land as `sys_webhook` dispatcher rows) and #3494 pruned the aspirational props outright — so the surviving surface is fully live. Kept in the table as the worked example that a dead verdict is a worklist entry, not a tombstone: enforce-or-remove resolved this one by ENFORCING | diff --git a/packages/spec/liveness/dashboard.json b/packages/spec/liveness/dashboard.json index 93dabafa87..a5fdd41fff 100644 --- a/packages/spec/liveness/dashboard.json +++ b/packages/spec/liveness/dashboard.json @@ -47,10 +47,108 @@ }, "chartConfig": { "status": "live", - "verifiedAt": "2026-09-10", + "verifiedAt": "2026-09-12", "evidenceScope": "cross-repo", "evidence": "objectui @53ded82bf: packages/core/src/utils/chart-presentation.ts#chartConfigPresentation lowers the NINE chrome keys onto the chart schema, one guard per key — showLegend, showDataLabels, title, subtitle, description, height, annotations, interaction, and colors split two ways into the positional palette and categoryColors; objectui @53ded82bf: packages/core/src/utils/chart-presentation.ts#mergeAuthoredPresentation merges the authored xAxis / yAxis / series PRESENTATION onto the bindings the dataset selection derived, through #mergeAuthoredSeries and #axisPresentation, dropping only the two binding keys ChartSeries.name and ChartAxis.field so that membership stays with the dataset; objectui @53ded82bf: packages/plugin-dashboard/src/DatasetWidget.tsx:1415 calls mergeAuthoredPresentation, :1483 calls chartConfigPresentation and :1529 spreads both results onto the chart schema handed to the renderer; objectui @53ded82bf: packages/plugin-dashboard/src/__tests__/DatasetWidget.chartConfig.dom.test.tsx pins the DOM effect of each forwarded chrome key; objectui @53ded82bf: packages/plugin-dashboard/src/__tests__/DatasetWidget.comboPresentation.test.tsx pins the series and axis merge in the DOM", - "note": "chart presentation config on the dataset-bound chart path — read PER KEY, not forwarded as a bag. RE-MEASURED 2026-09-10 against this checkout's own `.objectui-sha` pin 53ded82bf7a4, and the previous reading is superseded in two independent ways. FIRST, THE CITATION MOVED REPOS-INTERNALLY: `chartConfigPresentation` was lifted out of `plugin-dashboard` into `@object-ui/core`'s `chart-presentation` module by objectui#4877, so the old pointer `objectui @230ffd875: packages/plugin-dashboard/src/DatasetWidget.tsx:380-429` — still byte-exact at the commit it names, every one of its ten sub-line citations included — lands on the RE-EXPORT block at that same range in the pinned tree, and the nine `if`s it describes are in another package. Note what could not have caught this: a foreign path is counted and never resolved by this gate, on purpose, so an objectui citation cannot rot the way a repo-local one does — only a hand re-measurement finds it, and this row is the second in this file to be re-anchored that way. SECOND, THE COUNT CHANGED: 12 of the 14 keys `ChartConfigSchema` declares now reach the renderer on this face, not 9. `xAxis` / `yAxis` / `series` were recorded here as unforwarded on the stated grounds that they are derived from the dataset selection and an authored value would shadow the derived binding. objectui#4229 ruled that belief half right and shipped the split it implies: the dataset owns series MEMBERSHIP and the column each binding reads — concretely `ChartSeries.name` and `ChartAxis.field`, which are dropped on the way through — while every other key on those same objects is the author's and merges onto the derived binding by name/key match with the explicit binding winning. So those three keys are LIVE on this face today. The two that still do not reach it are `type` and `aria`. `type` is deliberate: the widget's own `type` picks the chart family through CHART_TYPE_MAP and objectui pins that nothing outranks it, so a dashboard author's `chartConfig.type` parses and does nothing while the same key is live on the react tier, published as a flat `dataProps` entry by `ui/react-blocks.ts`. `aria` has no reader on either face: the chart implementation declares no `aria` prop, the renderer's ARIA injection reads the flat `ariaLabel` / `ariaDescribedBy` / `role`, and the react block's `dataProps` omits it. ⛔ Do not read this row as a per-key classification: `chartConfig` still carries no `children`, and the walk drills exactly ONE level, so a `children` map written at this coordinate would sit at depth two where the gate never looks — the drill question, the `type` / `aria` dispositions and the authored-versus-derived precedence the protocol still does not state are all open on #17385. HISTORY, kept because it is why this row is read carefully: the entry once read 'chart-config bag forwarded to the chart renderer', which was never measured — at that time exactly one key, `showLegend` via #3135, was lowered, and the overstatement mis-steered the #5022 measurement for half its length. #5175 measured the real 1-of-14 state and #7016 forwarded the keys the chart block measurably delivers." + "note": "chart presentation config on the dataset-bound chart path — read PER KEY, not forwarded as a bag. RE-MEASURED 2026-09-10 against this checkout's own `.objectui-sha` pin 53ded82bf7a4, and the previous reading is superseded in two independent ways. FIRST, THE CITATION MOVED REPOS-INTERNALLY: `chartConfigPresentation` was lifted out of `plugin-dashboard` into `@object-ui/core`'s `chart-presentation` module by objectui#4877, so the old pointer `objectui @230ffd875: packages/plugin-dashboard/src/DatasetWidget.tsx:380-429` — still byte-exact at the commit it names, every one of its ten sub-line citations included — lands on the RE-EXPORT block at that same range in the pinned tree, and the nine `if`s it describes are in another package. Note what could not have caught this: a foreign path is counted and never resolved by this gate, on purpose, so an objectui citation cannot rot the way a repo-local one does — only a hand re-measurement finds it, and this row is the second in this file to be re-anchored that way. SECOND, THE COUNT CHANGED: 12 of the 14 keys `ChartConfigSchema` declares now reach the renderer on this face, not 9. `xAxis` / `yAxis` / `series` were recorded here as unforwarded on the stated grounds that they are derived from the dataset selection and an authored value would shadow the derived binding. objectui#4229 ruled that belief half right and shipped the split it implies: the dataset owns series MEMBERSHIP and the column each binding reads — concretely `ChartSeries.name` and `ChartAxis.field`, which are dropped on the way through — while every other key on those same objects is the author's and merges onto the derived binding by name/key match with the explicit binding winning. So those three keys are LIVE on this face today. The two that still do not reach it are `type` and `aria`. `type` is deliberate: the widget's own `type` picks the chart family through CHART_TYPE_MAP and objectui pins that nothing outranks it, so a dashboard author's `chartConfig.type` parses and does nothing while the same key is live on the react tier, published as a flat `dataProps` entry by `ui/react-blocks.ts`. `aria` has no reader on either face: the chart implementation declares no `aria` prop, the renderer's ARIA injection reads the flat `ariaLabel` / `ariaDescribedBy` / `role`, and the react block's `dataProps` omits it. DRILLED 2026-09-12: this row now carries `children`, one verdict per `ChartConfigSchema` key, and THAT is the per-key classification — read the children, not this blanket `live`, which says only that the bag as a whole reaches a consumer. The drill was not expressible until today: the walk used to stop after one level, in silence, so a `children` map written at this coordinate sat at depth two where the gate never looked (#17424 taught it to recurse, and made its boundary announce itself). The sentence this replaces said exactly that, and it stopped being true the morning #17424 landed — which is the same rot this row has now been re-anchored for twice, one layer up from a citation. Drilling the row surfaced six containers one level further down (`xAxis`, `yAxis`, `series`, `annotations`, `interaction`, `aria`, 39 child keys between them); they are RECORDED in scripts/liveness/undrilled-containers.baseline.json rather than drilled, because the evidence work has not been done and fanning this row's verdicts down over them would manufacture verdicts — the one thing the drill rule forbids by name. ⚠️ `xAxis` / `yAxis` / `series` are the sharpest of those six: each is `live` as a whole while one of its OWN sub-keys is inert by design (`ChartAxis.field`, `ChartSeries.name` — dropped so membership stays with the dataset), so a blanket reading of those three children is wrong in a direction this ledger exists to catch. What is still open on #17385 is the PROTOCOL half only: the 2026-09-12 ruling on decision batch #121 (C+D) has the protocol state the ownership split — the dataset decides which series exist and which column each reads, `chartConfig` carries appearance — and REFUSE `xAxis` / `yAxis` / `series` and `type` by name on a dataset-bound widget, with a major changeset and an ADR-0087 semantic migration. That moves a published accept set and is the spec seat's to land; `aria`'s ADR-0049 disposition is its own card (#17751). HISTORY, kept because it is why this row is read carefully: the entry once read 'chart-config bag forwarded to the chart renderer', which was never measured — at that time exactly one key, `showLegend` via #3135, was lowered, and the overstatement mis-steered the #5022 measurement for half its length. #5175 measured the real 1-of-14 state and #7016 forwarded the keys the chart block measurably delivers.", + "children": { + "type": { + "status": "dead", + "verifiedAt": "2026-09-12", + "evidenceScope": "cross-repo", + "note": "RE-MEASURED 2026-09-12 at this checkout's own `.objectui-sha` pin 53ded82bf7a4, the whole ChartConfigSchema key set at once. No consumer on this face: `chartConfigPresentation` has no `type` branch at all — the lowering is a whitelist, and its own docblock states the exclusion criterion (\"It does not fight the dataset derivation. `type` stays out: the calling surface's own type already picks the chart family\"). `DatasetWidget` resolves the family from the WIDGET's own `type` through `CHART_TYPE_MAP` and writes that as `chartType` on the chart schema; the `...chartPresentation` spread that follows it never carries a `type` or `chartType` key, so an authored `chartConfig.type` cannot outrank or even reach it. Pinned as a NEGATIVE by name: \"ignores chartConfig.type — the widget type owns the chart family\" (DatasetWidget.chartConfig.test.tsx). ⚠️ Read the coordinate, not the key name: the SAME `ChartConfigSchema.type` is live one metadata root over, on the react tier, where `packages/spec/src/ui/react-blocks.ts` publishes it in ``'s `dataProps` and its own comment explains why it cannot ride the props bag (it is the SDUI envelope's component discriminator). This row is about `dashboard.widgets[].chartConfig.type` and that is where it is dead — no per-face verdict vocabulary is invented for it. ⚠️ `type` is also the one REQUIRED key of `ChartConfigSchema`, so every dashboard author who writes a `chartConfig` at all is made to write a key this face ignores. DISPOSITION IS ALREADY RULED AND IS NOT THIS ROW'S TO EXECUTE: the 2026-09-12 ruling on decision batch #121 refuses `type` on a dataset-bound widget by name, with a major changeset and an ADR-0087 semantic migration — that is the protocol half of #17385 and it belongs to the spec seat, not to this drill." + }, + "title": { + "status": "live", + "verifiedAt": "2026-09-12", + "evidenceScope": "cross-repo", + "evidence": "objectui @53ded82bf: packages/core/src/utils/chart-presentation.ts#chartConfigPresentation; objectui @53ded82bf: packages/plugin-dashboard/src/DatasetWidget.tsx#DatasetWidget (calls mergeAuthoredPresentation, then chartConfigPresentation, and spreads both results onto the chart schema handed to SchemaRenderer); objectui @53ded82bf: packages/plugin-dashboard/src/__tests__/DatasetWidget.chartConfig.dom.test.tsx; objectui @53ded82bf: packages/plugin-dashboard/src/__tests__/DatasetWidget.chartConfig.test.tsx", + "note": "Lowered onto the chart schema when it is a non-empty string, and drawn by `AdvancedChartImpl` in its ChartFrame. Pinned in the DOM: \"draws chartConfig.title / .subtitle above the plot\", with its own negative \"adds no title chrome when chartConfig declares none\"." + }, + "subtitle": { + "status": "live", + "verifiedAt": "2026-09-12", + "evidenceScope": "cross-repo", + "evidence": "objectui @53ded82bf: packages/core/src/utils/chart-presentation.ts#chartConfigPresentation; objectui @53ded82bf: packages/plugin-dashboard/src/DatasetWidget.tsx#DatasetWidget (calls mergeAuthoredPresentation, then chartConfigPresentation, and spreads both results onto the chart schema handed to SchemaRenderer); objectui @53ded82bf: packages/plugin-dashboard/src/__tests__/DatasetWidget.chartConfig.dom.test.tsx; objectui @53ded82bf: packages/plugin-dashboard/src/__tests__/DatasetWidget.chartConfig.test.tsx", + "note": "Same lowering and the same DOM pin as `title` — a non-empty string reaches the ChartFrame." + }, + "description": { + "status": "live", + "verifiedAt": "2026-09-12", + "evidenceScope": "cross-repo", + "evidence": "objectui @53ded82bf: packages/core/src/utils/chart-presentation.ts#chartConfigPresentation; objectui @53ded82bf: packages/plugin-dashboard/src/DatasetWidget.tsx#DatasetWidget (calls mergeAuthoredPresentation, then chartConfigPresentation, and spreads both results onto the chart schema handed to SchemaRenderer); objectui @53ded82bf: packages/plugin-dashboard/src/__tests__/DatasetWidget.chartConfig.dom.test.tsx; objectui @53ded82bf: packages/plugin-dashboard/src/__tests__/DatasetWidget.chartConfig.test.tsx", + "note": "Lowered as the chart container's accessible name — `role=\"img\"` plus `aria-label`. Pinned in the DOM by name: \"announces chartConfig.description as the chart graphic accessible name\", against the negative \"leaves the graphic unlabelled when no description is declared\". This is the accessibility channel that actually works on this face, and it is why `aria` beside it is left unforwarded rather than flattened into the same slot." + }, + "xAxis": { + "status": "live", + "verifiedAt": "2026-09-12", + "evidenceScope": "cross-repo", + "evidence": "objectui @53ded82bf: packages/core/src/utils/chart-presentation.ts#mergeAuthoredPresentation; objectui @53ded82bf: packages/core/src/utils/chart-presentation.ts#axisPresentation; objectui @53ded82bf: packages/plugin-dashboard/src/DatasetWidget.tsx#DatasetWidget (calls mergeAuthoredPresentation, then chartConfigPresentation, and spreads both results onto the chart schema handed to SchemaRenderer); objectui @53ded82bf: packages/plugin-dashboard/src/__tests__/DatasetWidget.comboPresentation.test.tsx", + "note": "PRESENTATION forwards, BINDING does not, and the split is structural rather than a guard. `axisPresentation` copies `title` / `format` / `min` / `max` / `stepSize` / `showGridLines` / `position` / `logarithmic` off the authored axis and drops `field`, because `normalizeChartSchema` synthesises series out of `yAxis[].field` when a chart declares none — so a forwarded `field` would be a live membership channel. The plotted x column stays `buildChartSeries`' `xAxisKey`, i.e. the widget's `dimensions[0]`. Pinned by name: \"does not let an authored axis `field` reach the renderer\". ⚠️ The key is `live` and one of its OWN sub-keys is inert; that asymmetry is why the sub-keys are not classified here — see the container disposition for this coordinate." + }, + "yAxis": { + "status": "live", + "verifiedAt": "2026-09-12", + "evidenceScope": "cross-repo", + "evidence": "objectui @53ded82bf: packages/core/src/utils/chart-presentation.ts#mergeAuthoredPresentation; objectui @53ded82bf: packages/core/src/utils/chart-presentation.ts#axisPresentation; objectui @53ded82bf: packages/plugin-dashboard/src/DatasetWidget.tsx#DatasetWidget (calls mergeAuthoredPresentation, then chartConfigPresentation, and spreads both results onto the chart schema handed to SchemaRenderer); objectui @53ded82bf: packages/plugin-dashboard/src/__tests__/DatasetWidget.comboPresentation.test.tsx", + "note": "The same `axisPresentation` per array entry, plus one thing the x-axis has no analogue for: the COUNT of entries is itself presentation — it is what turns on a secondary axis — so every declared entry keeps its slot even when it carried nothing but the dropped `field`. Pinned by name: \"declares two y-axes and binds each series to the authored one\" and \"keeps an axis slot that declared only its `field`\"." + }, + "series": { + "status": "live", + "verifiedAt": "2026-09-12", + "evidenceScope": "cross-repo", + "evidence": "objectui @53ded82bf: packages/core/src/utils/chart-presentation.ts#mergeAuthoredPresentation; objectui @53ded82bf: packages/core/src/utils/chart-presentation.ts#mergeAuthoredSeries; objectui @53ded82bf: packages/core/src/utils/chart-presentation.ts#seriesPresentation; objectui @53ded82bf: packages/plugin-dashboard/src/DatasetWidget.tsx#DatasetWidget (calls mergeAuthoredPresentation, then chartConfigPresentation, and spreads both results onto the chart schema handed to SchemaRenderer); objectui @53ded82bf: packages/plugin-dashboard/src/__tests__/DatasetWidget.comboPresentation.test.tsx", + "note": "Merged onto the bindings the dataset selection derived, matched by `name` against the derived `dataKey`, first entry wins. `seriesPresentation` carries `type` (narrowed to bar/line/area) / `yAxis` / `label` / `color` / `stack` / `variant` / `dashArray` / `opacity` and drops `name`, so an author can restyle a derived series but cannot add, remove or re-point one: \"ignores an authored series naming a measure outside the selection\", \"leaves a derived series with no authored entry untouched\", \"lets a bar widget declare one line series (a derived combo)\"." + }, + "colors": { + "status": "live", + "verifiedAt": "2026-09-12", + "evidenceScope": "cross-repo", + "evidence": "objectui @53ded82bf: packages/core/src/utils/chart-presentation.ts#chartConfigPresentation; objectui @53ded82bf: packages/plugin-dashboard/src/DatasetWidget.tsx#DatasetWidget (calls mergeAuthoredPresentation, then chartConfigPresentation, and spreads both results onto the chart schema handed to SchemaRenderer); objectui @53ded82bf: packages/plugin-dashboard/src/__tests__/DatasetWidget.chartConfig.test.tsx", + "note": "Both arms of the overloaded union reach the renderer, through two DIFFERENT props, and the split happens in the lowering: a `string[]` becomes the positional palette `colors`, a `{ value: color }` record becomes `categoryColors` merged OVER the category dimension field's own select/lookup option colours — the precedence the spec field comment states. Pinned in both directions: \"lowers an array `colors` as the positional palette\" / \"lowers a record `colors` as the per-category map, not as the palette\"." + }, + "height": { + "status": "live", + "verifiedAt": "2026-09-12", + "evidenceScope": "cross-repo", + "evidence": "objectui @53ded82bf: packages/core/src/utils/chart-presentation.ts#chartConfigPresentation; objectui @53ded82bf: packages/plugin-dashboard/src/DatasetWidget.tsx#DatasetWidget (calls mergeAuthoredPresentation, then chartConfigPresentation, and spreads both results onto the chart schema handed to SchemaRenderer); objectui @53ded82bf: packages/plugin-dashboard/src/__tests__/DatasetWidget.chartConfig.dom.test.tsx; objectui @53ded82bf: packages/plugin-dashboard/src/__tests__/DatasetWidget.chartConfig.test.tsx", + "note": "Applied as the chart container's inline height when it is a finite number greater than zero; a non-positive value is dropped in favour of the container default rather than collapsing the plot. Pinned in the DOM (\"applies chartConfig.height over the container default\") and at the unit boundary (\"drops a non-positive or non-numeric height\")." + }, + "showLegend": { + "status": "live", + "verifiedAt": "2026-09-12", + "evidenceScope": "cross-repo", + "evidence": "objectui @53ded82bf: packages/core/src/utils/chart-presentation.ts#chartConfigPresentation; objectui @53ded82bf: packages/plugin-dashboard/src/DatasetWidget.tsx#DatasetWidget (calls mergeAuthoredPresentation, then chartConfigPresentation, and spreads both results onto the chart schema handed to SchemaRenderer); objectui @53ded82bf: packages/plugin-dashboard/src/__tests__/DatasetWidget.showLegend.test.tsx; objectui @53ded82bf: packages/plugin-dashboard/src/__tests__/DatasetWidget.chartConfig.test.tsx", + "note": "The FIRST key ever lowered on this face (#3135) and for a long time the only one — the whole reason the row above it once read \"chart-config bag forwarded to the chart renderer\" and was false. Forwarded in both directions, pinned in both directions, with the third pin asserting the key is absent when no `chartConfig` is declared so the renderer default survives." + }, + "showDataLabels": { + "status": "live", + "verifiedAt": "2026-09-12", + "evidenceScope": "cross-repo", + "evidence": "objectui @53ded82bf: packages/core/src/utils/chart-presentation.ts#chartConfigPresentation; objectui @53ded82bf: packages/plugin-dashboard/src/DatasetWidget.tsx#DatasetWidget (calls mergeAuthoredPresentation, then chartConfigPresentation, and spreads both results onto the chart schema handed to SchemaRenderer); objectui @53ded82bf: packages/plugin-dashboard/src/__tests__/DatasetWidget.chartConfig.test.tsx", + "note": "Forwarded as a boolean in both directions and printed by the chart block as a Recharts `LabelList`. Pinned: \"forwards showDataLabels in both directions\"." + }, + "annotations": { + "status": "live", + "verifiedAt": "2026-09-12", + "evidenceScope": "cross-repo", + "evidence": "objectui @53ded82bf: packages/core/src/utils/chart-presentation.ts#chartConfigPresentation; objectui @53ded82bf: packages/plugin-dashboard/src/DatasetWidget.tsx#DatasetWidget (calls mergeAuthoredPresentation, then chartConfigPresentation, and spreads both results onto the chart schema handed to SchemaRenderer); objectui @53ded82bf: packages/plugin-dashboard/src/__tests__/DatasetWidget.chartConfig.test.tsx", + "note": "Forwarded when the array is non-empty, and drawn as ReferenceLine / ReferenceArea over the plot. The empty array is deliberately dropped instead of emitted — pinned as \"drops an empty annotations array instead of emitting a dead key\"." + }, + "interaction": { + "status": "live", + "verifiedAt": "2026-09-12", + "evidenceScope": "cross-repo", + "evidence": "objectui @53ded82bf: packages/core/src/utils/chart-presentation.ts#chartConfigPresentation; objectui @53ded82bf: packages/plugin-dashboard/src/DatasetWidget.tsx#DatasetWidget (calls mergeAuthoredPresentation, then chartConfigPresentation, and spreads both results onto the chart schema handed to SchemaRenderer); objectui @53ded82bf: packages/plugin-dashboard/src/__tests__/DatasetWidget.chartConfig.test.tsx", + "note": "Forwarded when it is a non-array object, and honoured by the chart block as the tooltip toggle plus `Brush`. Pinned with `annotations` in \"forwards annotations and the interaction toggles\"." + }, + "aria": { + "status": "dead", + "verifiedAt": "2026-09-12", + "evidenceScope": "cross-repo", + "note": "RE-MEASURED 2026-09-12 at pin 53ded82bf7a4: no reader on EITHER face, which is what separates this row from `type` beside it. `chartConfigPresentation`'s docblock names it as \"the one declared key with no reader at all\": `AdvancedChartImpl` declares no `aria` prop, and `SchemaRenderer`'s ARIA injection reads the FLAT `ariaLabel` / `ariaDescribedBy` / `role`, never a nested `aria` object. It is left unforwarded on purpose rather than papered over with a caller-side flattening, which would also collide with the accessible name `description` already sets on this very chart. On the react tier it is not published either — `packages/spec/src/ui/react-blocks.ts` lists thirteen `dataProps` for `` and `aria` is the one `ChartConfigSchema` key missing from that list. Pinned as a negative from both spellings: \"ignores chartConfig.aria — no accessible name appears anywhere\" and \"ignores aria — nested and flattened\". Same false-compliance shape as the widget-level `aria` two rows up and as the dashboard-level `aria` the #3896 sweep removed — an accessibility guarantee an author can declare and nothing honours. ⛔ NOT retired here: `aria`'s ADR-0049 enforce-or-remove disposition is its own card (#17751, named by the 2026-09-12 ruling on decision batch #121 as the sibling spec card), and recording the verdict is what feeds that worklist — executing it is not this drill's." + } + } }, "colorVariant": { "status": "live", diff --git a/packages/spec/liveness/state-counts.md b/packages/spec/liveness/state-counts.md index 06956d2da1..4fcea1ad80 100644 --- a/packages/spec/liveness/state-counts.md +++ b/packages/spec/liveness/state-counts.md @@ -41,7 +41,7 @@ for both corollaries. | `page` | 23 | 0 | 0 | 0 | 1 | 24 | | `view` | 77 | 0 | 0 | 11 | 0 | 88 | | `report` | 21 | 0 | 0 | 0 | 0 | 21 | -| `dashboard` | 34 | 0 | 0 | 8 | 0 | 42 | +| `dashboard` | 45 | 0 | 0 | 10 | 0 | 55 | | `webhook` | 19 | 0 | 0 | 0 | 0 | 19 | | `query` | 16 | 0 | 0 | 5 | 0 | 21 | | `datasource` | 30 | 0 | 0 | 0 | 0 | 30 | @@ -63,4 +63,4 @@ for both corollaries. | `batch_endpoints` | 5 | 0 | 0 | 2 | 0 | 7 | | `route_generation` | 0 | 0 | 0 | 4 | 0 | 4 | | `realtime_subscription` | 0 | 0 | 0 | 6 | 0 | 6 | -| **total** | **850** | **5** | **1** | **93** | **10** | **959** | +| **total** | **861** | **5** | **1** | **95** | **10** | **972** | diff --git a/packages/spec/scripts/liveness/check-liveness.test.ts b/packages/spec/scripts/liveness/check-liveness.test.ts index a101341c2c..e4a52508db 100644 --- a/packages/spec/scripts/liveness/check-liveness.test.ts +++ b/packages/spec/scripts/liveness/check-liveness.test.ts @@ -981,12 +981,20 @@ describe('check:liveness — the drill recurses past one level (#17424)', () => // depth-two container is now VISIBLE. Before the walk recursed, a container // sitting under a drilled child was neither classified, nor deferred, nor // recorded — it was not in any population at all, which is why nothing could - // ever have gone red about it. `dashboard/widgets.chartConfig` is the #17385 - // coordinate that could not be drilled until this landed. - it('SEES a container that sits under a drilled child — the coordinate #17385 is blocked on', () => { + // ever have gone red about it. `dashboard/widgets.chartConfig` was the #17385 + // coordinate that could not be drilled until this landed — and it has since + // BEEN drilled, on that card, which is why this pin no longer names it. Two + // coordinates stand in its place and the pair is deliberate: `widgets.compareTo` + // is its exact structural replacement (a container that is a drilled child, the + // shape that was invisible before the recursion), and `widgets.chartConfig.xAxis` + // is one level deeper again — a container under TWO drilled levels, which exists + // only because the #17385 drill landed. A pin naming a coordinate that a card is + // about goes stale the moment that card lands; naming the SHAPE does not. + it('SEES a container that sits under a drilled child, at either depth', () => { const r = report(); const seen = [...r.undrilled.map((u: any) => u.key), ...r.deferredContainers.map((d: string) => d.split(' → ')[0])]; - expect(seen).toContain('dashboard/widgets.chartConfig'); + expect(seen).toContain('dashboard/widgets.compareTo'); + expect(seen).toContain('dashboard/widgets.chartConfig.xAxis'); }); it('is green against a verbatim copy of the shipped ledgers', () => { @@ -1011,15 +1019,20 @@ describe('check:liveness — the drill recurses past one level (#17424)', () => it('classifies the depth-2 keys, moving the verdict counts a blanket entry could not move', () => { const control = report(freshRoot('depth2-counts-control')).types.dashboard; const root = freshRoot('depth2-counts'); - nestChildren(root, 'dashboard', 'widgets', 'chartConfig', { - title: { status: 'experimental', evidence: 'packages/spec/liveness/README.md:1', verifiedAt: '2026-09-12' }, + nestChildren(root, 'dashboard', 'widgets', 'options', { + dateGranularity: { status: 'experimental', evidence: 'packages/spec/liveness/README.md:1', verifiedAt: '2026-09-12' }, }); - setChildField(root, 'dashboard', 'widgets', 'chartConfig', 'childrenDefault', 'live'); + setChildField(root, 'dashboard', 'widgets', 'options', 'childrenDefault', 'live'); const after = report(root).types.dashboard; - // One coordinate in, fourteen out: the blanket verdict on `chartConfig` is - // replaced by a verdict per key, and one of them is a status the container - // never carried. That difference is the whole point of drilling. - expect(after.classified).toBe(control.classified + 13); + // One coordinate in, five out: the blanket verdict on `options` is replaced + // by a verdict per key, and one of them is a status the container never + // carried. That difference is the whole point of drilling. (This read + // `chartConfig` until #17385 drilled it in the shipped ledger — the control + // tree then already carried the fourteen verdicts the mutation was supposed + // to introduce, so the delta collapsed to zero and the arithmetic measured + // nothing. The subject has to be a container the shipped ledger has NOT + // drilled, or the test grades the fixture instead of the walk.) + expect(after.classified).toBe(control.classified + 4); expect(after.byStatus.experimental ?? 0).toBe((control.byStatus.experimental ?? 0) + 1); }); diff --git a/packages/spec/scripts/liveness/undrilled-containers.baseline.json b/packages/spec/scripts/liveness/undrilled-containers.baseline.json index d18b2c8cad..566fd49e5a 100644 --- a/packages/spec/scripts/liveness/undrilled-containers.baseline.json +++ b/packages/spec/scripts/liveness/undrilled-containers.baseline.json @@ -73,7 +73,12 @@ "dashboard/dateRange", "dashboard/globalFilters", "dashboard/header", - "dashboard/widgets.chartConfig", + "dashboard/widgets.chartConfig.annotations", + "dashboard/widgets.chartConfig.aria", + "dashboard/widgets.chartConfig.interaction", + "dashboard/widgets.chartConfig.series", + "dashboard/widgets.chartConfig.xAxis", + "dashboard/widgets.chartConfig.yAxis", "dashboard/widgets.compareTo", "dashboard/widgets.layout", "dashboard/widgets.options",