Repository navigation
Commit 20e7d52
feat(spec)!: a
Fixes #22491
Clause-②: yes (narrowing)
Claim `6083229542` (session `session_01KNKBCRDJCu5tGy3TEbvtrF`, branch
`claude/issue-22491-chart-view-needs-dataset`), triage direction
`6082420385`. Every list-view door now refuses a `type: 'chart'` list
view whose **effective** chart binding names no dataset, with the
binding to declare. No default binding is fabricated.
## What changes
- **The rule** (`packages/spec/src/ui/view.zod.ts`):
`checkListViewChartBinding`, exported, attached by identifier at the
same three list-view doors as `checkListViewCalendarVisualization`:
- `ListViewSchema`, which also covers `views[].list` / `listViews`, a
view item `config` and `defineView`;
- `ObjectListViewSchema` (`objects[].listViews`);
- the flattened list overlay member of the view write door (`PUT
/api/v1/meta/view/:name`).
One rule judges every door. There is no door-specific second contract.
- **The effective binding** is the renderer's: the `chart` block, else
the legacy `options.chart` bag, with the block replacing the bag whole.
objectui `packages/plugin-list/src/ListView.tsx`
`resolveListChartBinding`, `schema.chart || schema.options?.chart ||
{}`: `:207` at this repo's `.objectui-sha` pin `f0268ad7`, `:260` at
objectui `2a48bd40`. Read only; nothing in objectui was touched.
- no block and no bag: one `custom` issue at `chart`. Its first sentence
is "This list view is `type: 'chart'` but declares no `chart` block, so
it binds no dataset and there is nothing to plot." The remedy names
`chart.dataset` and `chart.values`.
- no block, and a bag missing `dataset` or `values`: one `custom` issue
per missing key, at `options.chart.dataset` / `options.chart.values`.
Only the overlay carries the bag; the two authoring doors refuse
`options` by name.
- a declared `chart` block: left to its own strict schema, which already
requires both keys at `chart.dataset` / `chart.values`, so nothing is
reported twice.
- The overlay reads `type` on the input side, as
`checkListOverlayTypeNeedsColumns` does. A patch that names no `type`
(the console's toolbar save) is not judged; the view it shadows decides.
- The `chart` slot gains a `.describe()` saying a chart view must bind
one. The reference pages regenerate with it.
- **Ledger:** the D3 semantic entry
`view-chart-binding-dataset-required` (step 18). It has no rationale
fragment, so the hand-written region of `registry.ts` is untouched and
only the generated region moved. There is no D2 conversion, because only
the author knows the dataset. The changeset is `major` on
`@objectstack/spec`, because pre mode `next` takes a breaking change at
`major`. It carries the registered ADR-0087 marker.
## Premise, measured on `origin/main` @ `e148ca98` (before)
| body | overlay member | `ViewMetadataSchema` union | `ListViewSchema`
| `ObjectListViewSchema` |
|---|---|---|---|---|
| `type: 'chart'`, no block | ACCEPT | ACCEPT | ACCEPT | ACCEPT |
| `options.chart: { chartType }` only | ACCEPT | ACCEPT | refused
(`options` unknown key) | refused (same) |
| `chart: { chartType }` | refused at `chart.dataset` / `chart.values` |
refused | refused | refused |
After the change, the first two rows are refused at `chart` and at
`options.chart.dataset` + `options.chart.values`. The container member
(`list`) and the view item member (`config`) refuse them too, at
`list.chart` / `config.chart`.
## Stored rows (Zone 2 item 4), measured
`applyConversionsToStoredItem` is the one rehydration primitive. It
replays only the conversion registry, and no entry touches a list-view
chart block. The read door does not re-validate: it decorates.
`meta-view-chart-binding.test.ts` measures this through the real `PUT` /
`GET /api/v1/meta/view/:name` over SQLite. A row stored with no binding,
planted directly in `sys_metadata`, is served `200` as stored, with
`_diagnostics.valid: false` and the same issue at `chart`. Re-saving the
body that was read answers `422 INVALID_METADATA` at `chart`. So no
stored view becomes unreadable, and each one is refused on its next
save. The ADR-0087 disposition is `registered
view-chart-binding-dataset-required`; the changeset states it, along
with this read-side behaviour.
## Pins
- `packages/spec/src/ui/view-chart-binding.test.ts`, across all three
doors:
- `type: 'chart'` with no block is refused at `chart`. The test checks
the `code`, the path and the message's first sentence, and that the
remedy names both keys.
- Control: a complete block saves and is kept.
- A declared incomplete block gets no second issue.
- Grid views are not judged, including one that lists `chart` in
`allowedVisualizations`.
- Overlay only:
- a bag holding only `chartType` is refused at both keys;
- a bag with `dataset` but no `values` is refused at `values`;
- a complete bag is accepted and kept;
- an incomplete bag under a complete block is accepted, because the
block replaces the bag whole;
- a patch that names no `type` is not judged;
- a drift pin derives `ListChartConfigSchema`'s required keys and asks
the bag for exactly those.
- `packages/spec/src/ui/object-refinement-check-exports.test.ts`
catalogues the export:
- parity and bijection over its fixture matrix;
- three attachments by identifier, chained after the calendar check;
- barrel identity.
- `packages/rest/src/meta-view-chart-binding.test.ts` exercises the real
door (`RestServer` route, then `saveMetaItem`, then ObjectQL with
SQLite):
- the ADR-0112 envelope `422` + `INVALID_METADATA`, the issue path, and
an empty store for both refusals;
- control `200`, with the stored row carrying its binding;
- the stored-row read and re-save described above.
## Fixture triage (consumer radius, not just `packages/spec`)
I grepped `type: 'chart'` across `examples/**`, `packages/**` and
`skills/**`:
- The two chart list views in `examples/app-showcase`
(`project.view.ts`, `task.view.ts`) bind a dataset and measures.
Importing the showcase's `src/ui/views` against the built spec runs
`defineView` at import and parsed 26 list views, 2 of them charts.
- The lint fixtures (`showcase-shape.fixtures.ts`,
`validate-chart-bindings.test.ts`) bind a dataset and a measure.
- The conversion-registry and `additional-types` hits are dashboard
widgets and a metadata type name, not list views.
Two spec pins parsed a block-less chart view as a stand-in for "every
type". Each was re-judged as missing a declaration, and now carries the
binding:
- `view.test.ts`, "keeps every surviving view type accepting exactly as
before";
- `view-form-pagination.test.ts`, "is backed by the door: type 'chart'
parses a pagination block". Studio's view form does have a `chart`
section (`view.form.ts`), so a Studio author can declare the binding.
## Reverse verification
I ran this once in a second worktree at `7836b326`, after committing the
fix. The mutation went through `scripts/ablation-replace.mjs`: the
check's guard line became an unconditional `return`, the anchor count
went 1 to 0, and the blob went `0915637f` to `d75f30ac`. The two spec
pin files then ran **10 failed / 204 passed**:
- every refusal pin went red, at all three doors and in the bag cases;
- the export parity and bijection legs went red;
- every control stayed green.
The direction was the expected one: the pins turned red. The restore was
proven: the blob equals HEAD and `git diff HEAD` is empty. These spec
tests resolve `./view.zod` from source, so no `dist` rebuild was
involved. The REST pin reads the built spec and was not ablated.
## Verification (local; first round at `7836b326`, patch round at
`e295f361`; CI owns the full farm)
Build: `pnpm --filter @objectstack/spec build`, then `check:generated`
found 3 of 15 artifacts stale (`api-surface`, `export-origins`, `docs`).
`--fix` regenerated only those three, and a re-check reported all 15 up
to date. After that I built the dependency closure of `rest`, `lint` and
`metadata*` with turbo: 24/24 tasks.
| run | result |
|---|---|
| `@objectstack/spec` tests, 4 shards (`--project local`) | 158/158
files (5261 tests), 157/158 then re-run, 158/158 files (4012 tests), 156
+ 1 skipped (5032 tests). The one shard-2 red was the pagination pin
re-judged above; it was fixed in `7836b326`, and the four touched spec
test files re-run at `7836b326` gave 4/4 files, 732 tests. |
| `@objectstack/spec` typecheck (`tsc` + scripts + test layer) | exit 0
|
| `@objectstack/lint` tests | 130/130 files, 5943 tests |
| `@objectstack/rest` tests (`--project local`, full) | 269/269 files,
4979 passed / 326 skipped, including `meta-view-chart-binding.test.ts` |
| `@objectstack/rest` typecheck (`tsc` + test layer) | exit 0 |
| `@objectstack/metadata-core` / `metadata-fs` tests | 17/17 files (387
tests), 10/10 files (70 tests) |
| `@objectstack/metadata` / `metadata-protocol` (narrowed, see below) |
4/4 files (51 tests), 31/31 files (1247 tests) |
| `examples/app-showcase` views, imported against the built spec | 26
list views (2 charts) parse |
| eslint on the 8 changed `.ts` files, `--format json` | 8 files, 0
errors, 0 warnings |
| `dispatch-gates --commands` (114 derived) + the 49 artifact-roster
commands | 156 at exit 0. `check:skill-examples` re-ran at 0 after its
`client-react` prerequisite was built. `check:dual-build-cjs-loads` is
NOT MEASURED: it exits 3 with PREREQUISITE NOT MET because it needs
every package built. Three roster guards (`closing-target-claim`,
`partof-closing-keyword`, `single-claim-paths`) exit 2 NOT WIRED without
a PR; `partof-closing-keyword` was re-run on this body and passed (exit
0). `--ran` reports "114 derived famil(ies) accounted for — 113 run, 1
NOT-MEASURED". |
**Declared narrowing (coordinator-approved): `metadata` and
`metadata-protocol` ran their view-door test files only; the full suites
are CI's.**
- Why the narrowing cannot hide a regression: the new check fires only
on `type === 'chart'`. `git grep -nE "['\"]chart['\"]"` over
`packages/metadata`, `packages/metadata-protocol`,
`packages/metadata-core`, `packages/metadata-fs`, `packages/rest` and
`packages/objectql`, leaving out my own new test, finds two hits.
Neither is a list view: one is a report container in
`protocol.invalid-metadata-422-face-inventory.test.ts`, the other a zod
union member in `zod-union-fields.test.ts`.
- A search for iteration of the list-view `type` enum over the same
packages (`shape\.type|overlayTypeValues|'gantt', ?'map'`) finds only
`page` type reads. The control term in the same corpus,
`viewKind|ListViewSchema|ViewMetadataSchema`, hits 4 test files in
`metadata` and 29 in `metadata-protocol`.
- Files run instead: those 33, plus `protocol.stored-conversions`,
`protocol.invalid-metadata-422-face-inventory` and
`protocol.read-decorations` (31 in `metadata-protocol` once
de-duplicated). `metadata`:
`metadata-manager-views-by-object-container`,
`plugin-artifact-view-container-object`, `view-container-name`,
`view-expand`.
- The lint narrowing is a measurement. The population comes from
`eslint.config.mjs`'s own `files: ['**/*.{ts,…}']` objects; the file
count comes from `--format json`. That config never enables type-aware
linting (no `parserOptions.project`; it says so at `:327`), so the diff
cannot move the verdict on any file it does not touch.
**Patch round, at `e295f361`.**
- **Sync.** Merged `origin/main` at `ee8751d4` with
`scripts/pm/os-regen-merge.sh` (no rebase) into merge commit `be6f3e23`.
That brought in protocol 18 from `4e9fe9ff`.
- The script's step-3 hand-off commit `d3900905` regenerated
`content/docs/references/data/object.mdx` from the merged tree, after a
spec build. That build ran after the merge commit, never in the MERGE
state.
- `742960d6` contains only `gen:spec-changes` + `gen:upgrade-guide`
output.
- Every step-18 entry id on `origin/main` is still present after the
merge; the only id added is `view-chart-binding-dataset-required`.
- **Changeset** (`b83e8aaf`): `Clause-②: yes (narrowing)`, and one
sentence naming the new export `checkListViewChartBinding`.
`check-changeset-no-major`, `check-adr-0087-registration` ("registered
view-chart-binding-dataset-required (new here)"),
`check-empty-changeset` and `check:changeset-gate-self-tests` each exit
0.
- **Test Core fix** (`e295f361`): the D3 entry's `reason` named an
objectui tracker id, which the `migrate meta` guidance must not print.
That wording is gone, and the projections were regenerated with
`gen:migration-registry` / `gen:spec-changes` / `gen:upgrade-guide`. No
other `#` plus 4–5 digit id is left in this diff's printed text (refusal
messages, `.describe()`, changeset, entry strings).
- **Re-run, one lock call per suite:**
- the four touched spec test files: 4/4 files, 732 tests (at
`b83e8aaf`);
- spec typecheck: exit 0 (at `b83e8aaf`);
- `rest` `meta-view-chart-binding.test.ts` after a turbo build of
`@objectstack/rest^...` (24/24): 1/1 file, 4 tests (at `b83e8aaf`);
- `packages/cli` `test/migrate-meta-engine-guidance.test.ts` (`--project
integration`), after a turbo build of `@objectstack/cli...` (59/59): 1/1
file, 3 tests (at `e295f361`).
- `e295f361` changes only the entry's prose and the three projections,
so the `b83e8aaf` runs read identical inputs.
- **Gates** at `e295f361`: all 161 union commands (`dispatch-gates
--commands`, 114 derived, plus the 49 artifact-roster commands) were
run, each exit code captured before any pipe.
- 158 exit 0, including `check:spec-changes`, `check:upgrade-guide`,
`check:migration-registry`, `check:generated`,
`check:error-code-provenance`, `check:skill-examples` and
`check:dual-build-cjs-loads`.
- The three PR-context guards (`check-closing-target-claim`,
`check-partof-closing-keyword`, `check-single-claim-paths`) exit 2
without a PR. Re-run against PR #22528, each exits 0.
- `--ran`: "✓ dispatch-gates --ran: 114 derived famil(ies) accounted for
— 114 run, 0 NOT-MEASURED (a DERIVED zero — all 114 recorded an exit
code and none of them is 3)."
- **Declared to CI:** the full `spec`, `lint`, `rest`, `metadata*` and
`cli` suites at the merged head.
## Acceptance notes
- **ADR-0078 §4** says there is "no hard Zod `.refine()` that rejects
existing metadata at registration". This rule sits at the parse doors
(authoring, `defineStack`, the write door). That is the same placement
as the calendar binding check and the joined-report `blocks[i].dataset`
refusal. The runtime registration seam is untouched: views do not
register through it, and stored rows keep loading, with `_diagnostics`.
- **Scope:** only `type: 'chart'` is judged. A grid that only offers a
chart in `appearance.allowedVisualizations` is a degrade, not a dead
screen: objectui's `availableViews` gate asks the same resolver and
never offers an unbound chart. This is recorded and pinned as a
deliberate non-rule.
- `checkViewCompleteness` stays silent on `chart`. The schema doors now
refuse the block-less view, so a completeness finding would
double-report it.
- **Observation, not filed (read-only, unreproduced):** objectui
`packages/app-shell/src/views/ObjectView.tsx` `:3162` reads only
`viewDef.chart` for a `type === 'chart'` view, not `options.chart`. A
view whose only binding is a complete bag therefore renders the unbound
refusal on that route, but plots on `ListView`'s. That makes two
precedences in one renderer repo. It belongs with objectui's
`domain:spec` seat, which files the `ListViewSchema` mirror after this
lands.
- `#22491` names only this card. objectui's mirror is not part of this
PR.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF)_
---------
Co-authored-by: Claude <noreply@anthropic.com>type: 'chart' list view whose effective binding names no dataset is refused at every list-view door (#22528)1 parent 6a3f82e commit 20e7d52
16 files changed
Lines changed: 671 additions & 28 deletions
File tree
- .changeset
- content/docs/references
- api
- data
- ui
- docs
- packages
- rest/src
- spec
- api-surface
- export-origins
- src
- migrations
- entries/semantic
- ui
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1688 | 1688 | | |
1689 | 1689 | | |
1690 | 1690 | | |
1691 | | - | |
| 1691 | + | |
1692 | 1692 | | |
1693 | 1693 | | |
1694 | 1694 | | |
| |||
1773 | 1773 | | |
1774 | 1774 | | |
1775 | 1775 | | |
1776 | | - | |
| 1776 | + | |
1777 | 1777 | | |
1778 | 1778 | | |
1779 | 1779 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
385 | 385 | | |
386 | 386 | | |
387 | 387 | | |
388 | | - | |
| 388 | + | |
389 | 389 | | |
390 | 390 | | |
391 | 391 | | |
| |||
0 commit comments