Repository navigation
Commit ed15448
Fixes #21702
Clause-②: yes (narrowing)
Triage direction `5977861131` (enforce), claim `5977924610`. The joined
arm of `ReportSchema`'s refinement now refuses each block of a `joined`
report that binds no `dataset`, at `blocks[i].dataset`, naming the
block, with the prescription to bind the block to a dataset. The
refinement comment "each block dataset-bound" and the reports guide's
type-table cell "(each block dataset-bound)" were declarations; they are
now enforced.
## What changes
- **The refusal** (`packages/spec/src/ui/report.zod.ts`). A per-block
arm inside the joined branch of `ReportSchema`'s `superRefine`, right
after the existing "needs `blocks`" check. For each block whose
`dataset` is undefined it adds one `custom` issue at `['blocks', i,
'dataset']`. The message comes from a module-private builder,
`joinedBlockDatasetRequired`, worded like the neighbouring joined
refusals:
```text
a `joined` report draws each block from that block's own `dataset`, and
block `NAME` binds none, so nothing queries it and it draws no rows.
Bind the block to a dataset: set its `dataset` to the dataset whose
measures (`values`) and dimensions (`rows`) it shows.
```
`NAME` is the block's `name`. A block whose `name` is empty (its own
`too_small` issue, which does not stop the refinement) is named by
position instead, as `blocks[1]`.
- **What stays accepted.** `JoinedReportBlockSchema.dataset` stays
`.optional()`: `blocks` is read on a `joined` report only, so the
requirement lives on the joined arm, and a block on a non-joined report
is not judged (pinned). No type, export or JSON Schema key changes. The
block's `dataset` `.describe()` now reads "Dataset name to bind
(ADR-0021); a joined report refuses a block without one", and
`content/docs/references/ui/report.mdx` is regenerated with `gen:docs`
(two table rows).
- **The ADR-0087 kit**, in #21687's shape:
- the D3 semantic entry
`entries/semantic/18.ui-report-joined-block-dataset-required.ts`;
- its step-18 rationale fragment in `STEP18_RATIONALE` at **order 77**,
inserted where its id sorts. 77 is the next free order: the highest on
`main` is 76, re-read at `7e0066af7a` just before this PR. #21699 (74 to
76) and #21698 landed before this branch's base `16d241a6af`, so no
merge was needed;
- `registry.ts` regenerated with `gen:migration-registry`;
- one BREAKING `@objectstack/spec` `minor` changeset,
`.changeset/21702-joined-report-block-dataset-required.md`, carrying the
`Clause-②` line, the `adr-0087: registered
ui-report-joined-block-dataset-required` disposition marker and a FROM →
TO table.
The fragment and entry text state each decision in words and carry no
tracker number. No tombstone (no key is removed) and no D2 conversion
(only the author knows which dataset a block was meant to show).
- **`content/docs/ui/reports.mdx`: no edit.** No sentence there became
false; the type-table cell became true. See Acceptance notes.
## Census, at base `16d241a6af`, before any edit
`git grep` for a `type: 'joined'` report across `examples/**`,
`packages/**`, `skills/**`, `content/docs/**`, `docs/**`, `scripts/**`
and `apps/**` gave 33 lines in 15 files: CHANGELOG quotations, the
spec's own comments, and these reports:
| where | joined reports | unbound blocks |
|:--|:--|:--|
| `examples/app-showcase` `TaskOverviewReport` | 1 | 0 |
| `content/docs/ui/reports.mdx` example | 1 | 0 |
| `packages/lint` `validate-chart-bindings.test.ts` (raw stacks, never
parsed) | 4 | 0 |
| `packages/platform-objects` `report-form-echo-decisions.test.ts` | 2 |
0 |
| `packages/spec` tests (`report.test.ts`,
`filter-save-door-face-parity.test.ts`,
`joined-report-block-type.test.ts`) and the
`report-joined-chart-removed` conversion fixture | 10 | 0 |
| `packages/metadata-protocol`
`protocol.invalid-metadata-422-face-inventory.test.ts` | 1 | **1**,
unbound on purpose (below) |
| `skills/**` | 0 | 0 |
- Triage measured hotcrm `4054ec26` (1 joined report, 0 unbound blocks)
and cloud `2205b530` (none). I took those readings as given. I read
hotcrm's `src/sales/reports/churn.report.ts` once, to shape the
preservation fixture. Every block binds a dataset: three bind
`account_metrics` and the fourth, `recently_closed_lost`, binds
`opportunity_metrics`. So triage's line "every block binds
`account_metrics`" is slightly off; its conclusion, zero unbound blocks,
stands.
- **objectui (read only, at `2e818d0b51` and at this repo's pin
`ab18797215`).** Studio's joined-report authoring **can** save a block
with no `dataset`. That producer is this narrowing's reach:
- `ReportDefaultInspector.tsx` renders the blocks through `SchemaForm`
with the spec `reportForm` "Joined blocks" repeater;
- `RepeaterField`'s `add()` seeds a blank row with every column
`undefined`;
- the row's `dataset` column is free text and not required (the block's
derived JSON Schema `required` is `["name"]`, measured);
- the bundled `ReportSchema` (`clientValidation.ts`) and the save door
both accepted the result.
Filed as objectstack-ai/objectui#11601: category ①, no labels, dedupe
query and hit count in its body. After the spec bump, Studio's live
validation and the save door both refuse such a block at
`blocks.N.dataset`.
- The renderers, at the pin: `DatasetReportRenderer`'s joined branch
passes `String(block.dataset ?? '')` to each block's table, and that
table's query hook goes idle on an empty name (`:443`). A report whose
blocks all lack one fails `isDatasetReport` and falls through to the
presentation bridge. `DrillDownDrawer`'s `isDatasetBoundReport` lists
the records instead of drawing it.
## Fixture triage: one test-only file outside the claim's file surface
`protocol.invalid-metadata-422-face-inventory.test.ts` section 5 (the
joined-report `chart` door pins) left its block **unbound on purpose**:
the door's author-time gate refuses an unresolvable dataset with
`chart-dataset-unknown`, and the stub engine had no dataset universe.
The new refusal turned 2 of its 23 tests red (`Tests 2 failed | 21
passed`): the container case got a second issue at `blocks.0.dataset`,
and the CONTROL was refused at `blocks.0.dataset`. That second red is
the metadata save door (`422 INVALID_METADATA`, `writeFace:
'meta-envelope'`) refusing the probe shape.
- **Disposition: add the declaration.** The block binds `task_metrics`,
and the harness gains an optional `makeProtocol({ datasets })`, which
registers that dataset through `registry.listItems('dataset')`. Every
other caller passes nothing, so the registry lists nothing, as before.
Result: `Tests 23 passed (23)`.
- **The registration is load-bearing.** I mutated the CONTROL to call
`makeProtocol()` without datasets (`scripts/ablation-replace.mjs`,
anchor 1 → 0, blob `f718099c2d` → `9dc51bd7b8`). Predicted 1 red / 22
green; observed `Tests 1 failed | 22 passed`. The failure was
`chart-dataset-unknown` at `reports[0].blocks[0].dataset`, "Declared
datasets: (none)". Restore: blob back to `f718099c2d`, which equals
HEAD, and `git diff HEAD` is empty.
- This file is outside the claim's declared file surface. It is reported
in the dev report as a deviation.
- Queue check: #21706, queued at this writing, appends a section to the
same file at line 570 and later. A local `git merge-tree` of this head
with that queue head exits 0. The file is not `merge=os-regen` routed,
so that local answer is also GitHub's.
## Doors, tested and probed
- **Pins** (`packages/spec/src/ui/report-joined-block-dataset.test.ts`,
16 tests):
- the triage probe shape is refused once per block at `blocks.0.dataset`
and `blocks.1.dataset`, each naming its block; a bound block beside an
unbound one draws one issue, at the unbound index; an empty name is
named by position;
- the new issue joins the other joined-arm refusals rather than
replacing them;
- taking the advice parses;
- CONTROLS: a block on a non-joined report, and
`JoinedReportBlockSchema` parsed on its own, both still parse;
- doors: `defineReport` throws; the registered `report` type schema
refuses (and accepts the bound report); `ObjectStackDefinitionSchema`,
the stack parse `objectstack validate` runs, refuses at
`reports.1.blocks.{0,1}.dataset`; `defineStack` answers
`STACK_SCHEMA_INVALID` / 422;
- preservation: the showcase `TaskOverviewReport` (mirrored byte for
byte) parses with output equal to its input, and a fixture shaped like
hotcrm's `customer_churn_signals` parses, gaining only the `drilldown`
default;
- ledger: one D3 entry at protocol 18 with no conversion; the step-18
rationale names it; no `JoinedReportBlock:dataset` tombstone, with a
control on a known tombstone.
- **`objectstack validate`, the real CLI** (`packages/cli/bin/run.js`,
built here). Fixtures were temporary, in the session scratchpad, outside
the repo; `@objectstack/spec` resolved to this worktree's build.
- The triage probe stack (two blocks, each only `name` + `label`):
**exit 1**, `"code": "STACK_SCHEMA_INVALID"`, `defineStack validation
failed (2 issues)` at `reports.0.blocks.0.dataset` and
`reports.0.blocks.1.dataset`, each with the message above.
- CONTROL, the same report with both blocks bound to a declared dataset:
**exit 0**, `"valid": true`.
- The showcase app (`examples/app-showcase/objectstack.config.ts`, four
reports including `TaskOverviewReport`): **exit 0**, `"valid": true`.
- **The CLI door's verdict is this arm's, proven through `dist`.** The
CLI reads `@objectstack/spec` through `dist`, so the mutation was
rebuilt:
- mutated leg: the `ctx.addIssue` of the new arm became a `globalThis`
marker assignment (anchor 1 → 0, blob `fadd536165` → `2cbfdb9feb`);
`@objectstack/spec` rebuilt; `ablation-dist-preflight.mjs` found the
marker in `dist` (exit 0); the probe then gave **exit 0**, `"valid":
true`. That reproduces the card's reading at `main`;
- restore leg: blob back to `fadd536165`, which equals HEAD, and `git
diff HEAD` is empty; rebuilt; `--absent` found the marker in none of 228
built files (exit 0); the probe gave **exit 1** `STACK_SCHEMA_INVALID`
again.
## Ablation of the refusal, at the pins (predicted first)
`scripts/ablation-replace.mjs` replaced the arm's `ctx.addIssue(...)`
with a no-op (anchor 1 → 0, blob `fadd536165` → `45f34370c5`). The pins
import `./report.zod` relatively, which resolves to `src`, so no build
was needed. Predicted: 8 red, the 4 refusal pins and the 4 door pins; 8
green, the advice, the 2 controls, the 2 preservation pins and the 3
ledger pins; `report.test.ts` untouched. Observed: `Tests 8 failed | 80
passed (88)` over the pin file and `report.test.ts`. The 8 were exactly
the predicted ones. Restore: blob equals HEAD (`fadd536165`), and `git
diff HEAD` is empty.
## Verification (all at head `c4e3ab9631` unless noted)
- `@objectstack/spec` full suite (`vitest run --project local
--maxWorkers=2`): `Test Files 613 passed (613)`, `Tests 18201 passed | 1
todo`.
- `pnpm --filter @objectstack/spec typecheck` (tsc, scripts, test layer:
`check:test-typecheck: OK`) and `pnpm --filter
@objectstack/metadata-protocol typecheck`: exit 0. `--listFiles` shows
both edited test files in their programs.
- Consumers that parse a joined report: `platform-objects`
`report-form-echo-decisions.test.ts` 35 passed; `lint`
`validate-chart-bindings.test.ts` 47 passed;
`@objectstack/example-showcase` whole suite `32 files, 399 passed`;
`metadata-protocol` face-inventory 23 passed.
- `pnpm --filter @objectstack/spec check:generated`: one artifact stale
before regeneration (`content/docs/references/**`), regenerated with
`gen:docs`; then `check:generated` exit 0.
- Derived gates: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 115 commands. Every one was run,
with its exit code recorded before any pipe. `--ran` reconciled **115
derived, 115 run, 0 NOT-MEASURED, 0 UNRUN**.
- Two first exited 3 (prerequisite not met, packages with no `dist`):
`check:skill-examples` and `check:dual-build-cjs-loads`. Both were
re-run green after building those packages. All 115 exited 0.
- Among them: `check:adr-0087-registration`, `check:changeset-no-major`,
`check:migration-registry`, `check:spec-changes`, `check:upgrade-guide`,
`check:authorable-surface`, `check:api-surface`, `check:docs`,
`check:doc-authoring`, `check:issue-citations`, `check:nul-bytes`,
`check:cross-package-test-inputs`.
- eslint, narrowed and proven:
1. population: `eslint.config.mjs`'s `**/*.{ts,…}` block covers all five
changed `.ts` files;
2. `--format json`: 5 files, 0 errors, 0 warnings;
3. invariance: the config enables no type-aware linting (no
`parserOptions.project`, as its own comment states), so this diff cannot
move a verdict on an untouched file.
The repo-wide `pnpm lint` is CI's.
- Declared to CI: the path-scheduled jobs (Test Core shards, Dogfood,
Temporal Conformance, Build Core), the whole-workspace type-check lanes
and every downstream consumer suite of `@objectstack/spec` beyond the
four above.
## Acceptance notes (observations, not filed)
- `reports.mdx` says a `joined` report "must declare at least one
block". That is still true, but it no longer names every requirement:
each block must now also bind a `dataset`. Not edited, under the claim's
rule that only a sentence this change makes false is edited. Carrier:
none.
- The block row in the spec's own `reportForm`
(`packages/spec/src/ui/report.form.ts`, "Joined blocks" repeater) offers
`dataset` with no `ref:dataset` widget and no `required`.
objectstack-ai/objectui#11601 names this as a possible spec-side fix for
the Studio producer, to be measured first. Carrier:
objectstack-ai/objectui#11601's acting seat.
- The requirement is a refinement, so the published JSON Schema does not
carry it. `ui/Report` already sits in
`dropped-refinements.baseline.json`, and the new arm adds no site there.
The `.describe()` text is now the JSON-Schema-visible hint.
- The joined arm, this check included, does not run while a block
carries an unknown key or a wrong-typed member: that issue aborts the
refinement. The dataset refusal then arrives after that first issue is
fixed. This is existing behaviour shared by every arm of the refinement.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent ced3e1a commit ed15448
7 files changed
Lines changed: 492 additions & 11 deletions
File tree
- .changeset
- content/docs/references/ui
- packages
- metadata-protocol/src
- spec/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 | |
|---|---|---|---|
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
35 | | - | |
| 35 | + | |
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
| |||
111 | 111 | | |
112 | 112 | | |
113 | 113 | | |
114 | | - | |
| 114 | + | |
115 | 115 | | |
116 | 116 | | |
117 | 117 | | |
| |||
Lines changed: 25 additions & 8 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
97 | 97 | | |
98 | 98 | | |
99 | 99 | | |
100 | | - | |
| 100 | + | |
101 | 101 | | |
102 | 102 | | |
103 | 103 | | |
| |||
131 | 131 | | |
132 | 132 | | |
133 | 133 | | |
134 | | - | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
135 | 144 | | |
136 | 145 | | |
137 | 146 | | |
| |||
396 | 405 | | |
397 | 406 | | |
398 | 407 | | |
399 | | - | |
400 | | - | |
401 | | - | |
402 | | - | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
403 | 420 | | |
404 | 421 | | |
405 | 422 | | |
| |||
408 | 425 | | |
409 | 426 | | |
410 | 427 | | |
411 | | - | |
| 428 | + | |
412 | 429 | | |
413 | 430 | | |
414 | 431 | | |
| |||
421 | 438 | | |
422 | 439 | | |
423 | 440 | | |
424 | | - | |
| 441 | + | |
425 | 442 | | |
426 | 443 | | |
427 | 444 | | |
| |||
Lines changed: 56 additions & 0 deletions
| 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 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
0 commit comments