Skip to content

Commit 9059082

Browse files
fix(spec): reportForm offers a joined block's dataset as the ref:dataset picker, marked required (#21714) (#21819)
Fixes #21714 Clause-②: no Claim `5987856715` (the card's Change, unblocked by the director seat `5987403710`). In `reportForm`, the "Joined blocks" repeater's `dataset` row now declares `widget: 'ref:dataset'` and `required: true`. Studio's report inspector now draws a joined report's block `dataset` as the dataset picker, with the required marker, instead of a free-text cell with no marker. Since #21702 (PR #21712), the joined arm of `ReportSchema` refuses a block that binds no `dataset`, at `blocks.N.dataset`. The old cell therefore let an author add a block that was invalid by construction. ## What changes - **The row** (`packages/spec/src/ui/report.form.ts`, the "Joined blocks" repeater): `{ field: 'dataset', label: 'Dataset' }` becomes `{ field: 'dataset', label: 'Dataset', widget: 'ref:dataset', required: true }`, with a comment saying why. No other row, section or form changes. - **One pin** (`packages/spec/src/ui/report-joined-block-dataset.test.ts`, beside the tests for the refusal itself): the row declares `widget: 'ref:dataset'` and `required: true`. The pin reads the exported `reportForm`, which is the output of `defineForm`'s parse, so a form schema that stripped either key would also turn it red. The nested row is the same strict, recursive `FormFieldSchema` that already carries `widget` and `required` on top-level fields. - **Changeset:** `@objectstack/spec` `patch` (measurement below). - ⛔ No schema, accept-set, type or export change. `JoinedReportBlockSchema.dataset` stays optional on the block shape, and the joined arm's refusal is unchanged. `defineForm` returns `FormViewParsed`, so `reportForm`'s exported type does not move. No objectui file. ## The renderer side, measured at the pin `.objectui-sha` on `main` is `9dfaca654311`. Every reading below comes from `git show 9dfaca654311:PATH` in an objectui clone, read-only. `git merge-base --is-ancestor b508ac50d9 9dfaca654311` exits 0, so objectui PR 11611's merge is in the pin. That reading is an exit 0 and therefore holds in a shallow clone too. | claim | where, at objectui `9dfaca654311` | |:--|:--| | `ref:dataset` is registered in `WIDGETS` | `packages/app-shell/src/views/metadata-admin/widgets.tsx:3035` (`'ref:dataset': RefDatasetWidget`). The widget is at `:683`, and it is declared `'control'` at `:3144`. | | `RepeaterField` resolves a row field's widget from the row spec | `packages/app-shell/src/views/metadata-admin/SchemaForm.tsx:2809`: the grid cell calls `resolveFieldWidget({ name: s.field, schema: sub, fieldSpec: s, widgetContext })`. The card layout passes `fieldSpec={s}` to `FieldRow` (`:2884`), which resolves through the same function (`:1838`). `inferWidget` returns an explicit `fieldSpec.widget` first (`:447`), and a spec that pins a widget skips every name detector (`:1031`). | | it passes the row field's `required` | `SchemaForm.tsx:2799` (grid cell) and `:2882` (card row) both pass `required={Boolean(s.required)}`. The grid header draws the marker at `:2772`. For a non-boolean field, `FieldRow` draws the star (`:1878`, `:1917`) and passes `aria-required` through `FieldControl` (`:1930`). | The joined `blocks` repeater declares no `widget`, so `useGrid` is false (`SchemaForm.tsx:2643`) and its rows render in the card layout, as `FieldRow`s. Three things connect the inspector to this row: - `ReportDefaultInspector.tsx:374`–`:381` feeds the dataset catalog as `widgetContext.datasets`, and passes it to the spec form at `:652`. - `report-schema.ts:80`–`:126` (`getReportForm`) prunes only top-level section fields, so the `blocks` row specs reach `SchemaForm` unchanged. - The inspector reads `reportForm` from `@objectstack/spec/ui` (`report-schema.ts:25`). objectstack's console build injects this tree's spec into that import (`scripts/build-console.sh`, `OBJECTSTACK_SPEC_DIST`). **Browser check: not done.** It is optional on the card. It would need objectui's console built at the pin with this tree's spec injected, plus a backend and a logged-in Studio session, which is heavy on a shared container. The static chain above is the evidence. The browser reading of the card's second pin is still open. ## Reverse verification The fix was committed first (`02307d05b6`). Then `node scripts/ablation-replace.mjs` replaced the anchor `widget: 'ref:dataset', required: true }` with `widget: 'ref:dataset' }` in `report.form.ts`: anchor count 1 to 0, blob `8bc1038e3612` to `b0da741da01a`. The test imports `./report.form` from source, so no `dist/` is in the resolution path. - **Predicted:** the new pin turns red at `expect(row?.required).toBe(true)` and the other 16 stay green. - **Observed:** `Tests 1 failed | 16 passed (17)`, `AssertionError: expected undefined to be true` at `report-joined-block-dataset.test.ts:261`. The direction is the usual one, red. - **Restore** (the tool's own leg, plus a shell trap): the blob equals HEAD (`8bc1038e3612`) and `git diff HEAD` is empty. ## Regenerated artifacts None needed. `pnpm --filter @objectstack/spec build` followed by `check:generated` reports all 15 generated artifacts up to date. The only generated files built from form specs are the nine packages' i18n bundles, which carry `label` and `helpText`. `pnpm check:i18n` reports `OK (9 package(s) — all bundles in sync, no undeclared authoring keys)`. ## Changeset measurement `@objectstack/spec`'s `files[]` ships `dist` and `src/**/*.zod.ts`. `report.form.ts` itself is not packed (`npm pack --dry-run --json`: 2068 entries, no `report.form.ts`), but its bundled bytes are. After the build, `widget: "ref:dataset", required: true` occurs in 6 packed files: `dist/ui/index.{js,mjs}`, `dist/system/index.{js,mjs}` and the two `dist/browser/system` twins. Positive control: an unchanged string of the same form, `Additional dataset-bound blocks stacked into a single report`, occurs in the same 6. The diff therefore publishes from `@objectstack/spec`, so it carries a `patch` changeset and no `skip-changeset`. ## Tests and gates, at `d7c811ff38` - `pnpm --filter @objectstack/spec test`: `Test Files 615 passed (615)`, `Tests 18361 passed | 1 todo`. - `pnpm --filter @objectstack/spec typecheck` exits 0: `tsc --noEmit`, `check:scripts-typecheck` and `check:test-typecheck` (`52 file(s) / 246 error(s) / 135 pinned signature(s) held`). Per `tsc --listFilesOnly`, the edited test file is in `tsconfig.test.json`'s program (1 hit) and not in `tsconfig.json`'s. - The three `platform-objects` tests that walk `reportForm` (`repeater-row-properties`, `report-form-echo-decisions`, `dataset-panel-echo-decisions`): `3 passed`, `62 passed`. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 86 commands. - Every exit code was recorded before any pipe. - `--ran` reconciliation: `86 derived, 86 run, 0 NOT-MEASURED, 0 UNRUN`. - Five gates first exited 3 (`PREREQUISITE NOT MET`) and `check:type-check-debt` ran past my per-command timeout. All six were re-run after a full workspace build (`turbo run build --concurrency=2`, 72 of 72 tasks) and exit 0. - `check-changeset-no-major --base origin/main` exits 0. Its clause-② level axis is PR-scoped and not applicable on a local run; CI reads this body's `Clause-②` line. ## Acceptance notes - `reportForm` reaches Studio from the `@objectstack/spec` that the console bundles. Through objectstack's console build that is this tree, because of the `OBJECTSTACK_SPEC_DIST` injection. A standalone objectui build uses its own lockfile's `@objectstack/spec` and gets the row once a spec release carrying it lands there. - The top-level `dataset` hint (`report.form.ts:39`) still never renders in the inspector, which prunes that field and draws its own curated picker (`FORM_FIELDS_OWNED_ELSEWHERE`, `report-schema.ts:33`). That was already known on the card, and nothing here changes it. --- _Generated by [Claude Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent a3ffc45 commit 9059082

3 files changed

Lines changed: 33 additions & 1 deletion

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
`reportForm`: the "Joined blocks" repeater's `dataset` column now declares `widget: 'ref:dataset'` and `required: true`. Studio's report inspector draws a joined report's block `dataset` as the dataset picker, with the required marker, instead of a free-text cell with no marker.
6+
7+
Clause-②: no
8+
9+
- **Why.** A `joined` report refuses a block that binds no `dataset`, at `blocks.N.dataset`. The form offered that column as plain text and did not mark it, so a newly added block failed on save.
10+
- **Which renderer honours it.** The pinned console registers `ref:dataset` in its widget registry. Its repeater takes each column's widget and `required` from the row spec, in both the grid and the card layout.
11+
- ⛔ Only this one form row changes. No schema, parse, export or accept-set change: `JoinedReportBlockSchema.dataset` stays optional on the block shape, and the joined arm's refusal is unchanged.

‎packages/spec/src/ui/report-joined-block-dataset.test.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { describe, expect, it } from 'vitest';
2727
import { getMetadataTypeSchema } from '../kernel/metadata-type-schemas';
2828
import { MIGRATIONS_BY_MAJOR, RETIRED_KEYS_BY_MAJOR } from '../migrations/registry';
2929
import { ObjectStackDefinitionSchema, defineStack } from '../stack.zod';
30+
import { reportForm } from './report.form';
3031
import { JoinedReportBlockSchema, ReportSchema, defineReport } from './report.zod';
3132

3233
const ENTRY_ID = 'ui-report-joined-block-dataset-required';
@@ -245,6 +246,22 @@ describe('the joined reports measured at the census parse unchanged', () => {
245246
});
246247
});
247248

249+
describe('the report form offers a block\'s `dataset` as the dataset picker, marked required (#21714)', () => {
250+
it('the "Joined blocks" repeater\'s `dataset` row declares `widget: \'ref:dataset\'` and `required: true`', () => {
251+
// Studio's report inspector renders this row spec: the widget hint picks
252+
// the cell's control, and `required` draws the column's marker and the
253+
// cell's `aria-required`. Without both, a new block is a free-text cell the
254+
// refusal above rejects on save.
255+
const blocks = (reportForm.sections as any[])
256+
.flatMap((s) => s.fields ?? [])
257+
.find((f: any) => f?.field === 'blocks');
258+
expect(blocks?.type).toBe('repeater');
259+
const row = (blocks.fields as any[]).find((f) => f?.field === 'dataset');
260+
expect(row?.widget).toBe('ref:dataset');
261+
expect(row?.required).toBe(true);
262+
});
263+
});
264+
248265
describe('the ADR-0087 ledger', () => {
249266
it('registers one D3 entry at protocol 18, with no D2 conversion, prescribing the binding', () => {
250267
const entries = MIGRATIONS_BY_MAJOR[18]!.semantic.filter((e) => e.id === ENTRY_ID);

‎packages/spec/src/ui/report.form.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,11 @@ export const reportForm = defineForm({
8282
// A `chart` input sat here until #20161: the key was removed from
8383
// `JoinedReportBlockSchema` because no renderer ever drew a block
8484
// chart, so this control offered a setting that plotted nothing.
85-
{ field: 'dataset', label: 'Dataset' },
85+
// #21714: a joined report refuses a block that binds no `dataset`
86+
// (#21702), so this column declares the `ref:dataset` picker the
87+
// top-level binding declares, marked required: a block is bound
88+
// in the form before save.
89+
{ field: 'dataset', label: 'Dataset', widget: 'ref:dataset', required: true },
8690
{ field: 'rows', label: 'Rows' },
8791
{ field: 'columns', label: 'Columns' },
8892
{ field: 'values', label: 'Values' },

0 commit comments

Comments
 (0)