Repository navigation
fix(spec): reportForm offers a joined block's dataset as the ref:dataset picker, marked required (#21714) - #21819
Merged
objectstack-fleet[bot] merged 2 commits intoOct 5, 2026
Conversation
…set picker, marked required The "Joined blocks" repeater's `dataset` row declared only a label, so Studio's report inspector drew a free-text, unmarked cell for a key the joined arm of ReportSchema now refuses when absent (#21702). The row now declares `widget: 'ref:dataset'` (registered in objectui's WIDGETS at the console pin) and `required: true`, and a pin beside the refusal's own tests holds both. Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
Contributor
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 138 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
objectstack-fleet
Bot
deleted the
claude/issue-21714-joined-block-dataset-widget
branch
October 5, 2026 06:05
This was referenced Oct 5, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Oct 7, 2026
… decision in words instead of a tracker number (stage 17) (objectstack-ai#21856) Part of objectstack-ai#20749 Clause-②: no Stage 17 of this card: the next area of class (e), the test strings shipped under `packages/spec/src`, as ruled in `5902360492` on objectstack-ai#20513. This stage takes the third name-ordered file group directly under `packages/spec/src/data/`: the 20 test files from `filter-comparand-type.test.ts` to `filter-view-operator-parity.test.ts`. They carried 95 messages and 99 tracker ids, citing 36 records. Every one of those ids now either states what its record decided, in words (form D), or is dropped where the title already says it. Text only: no assertion, identifier, test count or code comment changes. ## Census at the base (`2799155678`) Instruments: `census10.cjs` (md5 `9d08602ab972b4b8643c90d64d40fa41`), `census.cjs` (md5 `6e42a45a926d375013c32d62f16a296e`), `census-wide.cjs` (md5 `c98410a19529c439adb0afbfb00026a2`) and `dirtable.cjs` (md5 `dda605c54745b4a60cc14c9a686e4eff`). They are byte-identical to the copies stages 10 to 16 used. A literal counts as a test title when its folded message is argument 0 of a `describe` / `it` / `test` call, `.each` / `.skip` / `.only` chains included. Everything else is an "other" string. Both instruments read **1140 messages / 1207 ids in 245 files**, the seat's reading at `2799155678`. That is stage 16's head reading (1137 / 1204) plus the three ids that PRs objectstack-ai#21819 and objectstack-ai#21825 added in new `ui/` test titles, which ride the `ui/` stages. | directory | files | messages / ids | titles | other | |:--|--:|--:|--:|--:| | `ui/` | 84 | 396 / 419 | 378 / 401 | 18 / 18 | | `data/` (this PR: the third 20 files) | 55 | 280 / 299 | 265 / 282 | 15 / 17 | | `api/` | 40 | 189 / 201 | 181 / 193 | 8 / 8 | | `system/` | 34 | 154 / 165 | 128 / 138 | 26 / 27 | | (files directly in `src/`) | 30 | 118 / 120 | 117 / 119 | 1 / 1 | | `ai/` | 1 | 2 / 2 | 0 | 2 / 2 | | `contracts/` | 1 | 1 / 1 | 0 | 1 / 1 | | **total** | **245** | **1140 / 1207** | **1069 / 1133** | **71 / 74** | The group reads **95 messages / 99 ids in 20 files**, the seat's figures, file for file: | file (under `data/`) | messages / ids | titles | other | |:--|--:|--:|--:| | `filter-comparand-type.test.ts` | 9 / 10 | 9 / 10 | 0 | | `filter-cross-field-comparison-class.test.ts` | 4 / 4 | 4 / 4 | 0 | | `filter-dotted-head.test.ts` | 2 / 2 | 2 / 2 | 0 | | `filter-empty-operator.test.ts` | 5 / 5 | 5 / 5 | 0 | | `filter-equality-array-schema-door.test.ts` | 5 / 5 | 5 / 5 | 0 | | `filter-field-reference-lowering.test.ts` | 1 / 1 | 1 / 1 | 0 | | `filter-icontains-parse-door.test.ts` | 3 / 3 | 3 / 3 | 0 | | `filter-like-code-point.test.ts` | 3 / 3 | 3 / 3 | 0 | | `filter-like-nul-pattern.test.ts` | 1 / 1 | 1 / 1 | 0 | | `filter-like-wire-lowering.test.ts` | 3 / 3 | 3 / 3 | 0 | | `filter-lowering.test.ts` | 6 / 6 | 1 / 1 | 5 / 5 | | `filter-ne-array-schema-door.test.ts` | 5 / 6 | 5 / 6 | 0 | | `filter-number-comparand-declared-type.test.ts` | 17 / 17 | 17 / 17 | 0 | | `filter-operator-vocabulary.test.ts` | 2 / 4 | 1 / 1 | 1 / 3 | | `filter-preset-comparand.test.ts` | 2 / 2 | 2 / 2 | 0 | | `filter-save-door-face-parity.test.ts` | 14 / 14 | 7 / 7 | 7 / 7 | | `filter-text-comparand.test.ts` | 3 / 3 | 3 / 3 | 0 | | `filter-text-conformance.test.ts` | 2 / 2 | 1 / 1 | 1 / 1 | | `filter-text-operator-declared-type.test.ts` | 6 / 6 | 6 / 6 | 0 | | `filter-view-operator-parity.test.ts` | 2 / 2 | 2 / 2 | 0 | | **20 files** | **95 / 99** | **81 / 83** | **14 / 16** | Two more test files sit in the same name range and carry no id: `filter-subtree-provenance.test.ts` and `filter-verdict.test.ts`. They are not touched. - **Controls.** Lit, a title: `data/filter.test.ts`, outside the group, reads 18 / 19 at the head as at the base. Lit, "other" strings: the twin expect messages in `system/message-queue-retirement.test.ts` (`:87`, `:127`) still read at the head. Dark: `data/filter-operator-vocabulary.test.ts` reads 0 / 0 at the head while 11 of its comment lines still carry a number. Planted in a scratch copy of the head `data/filter-like-nul-pattern.test.ts`: an id put into a title reads 1 / 1, and an id put into a comment reads 0. - **A wider pattern** (any `#` plus digits) reads the same totals as the gate pattern in all 20 files at the base, and 0 in all 20 at the head. - **At the head:** 1045 messages / 1108 ids in 225 files. The 20 files read 0 / 0, and no other file moved. ## How the area was chosen `data/` has no subdirectory to split by, so its stages take name-ordered file groups near the ~100-id bound. Stage 16's re-cut named this group at 99, and this census reads 99, so the rule needed no re-cut. **Named for the next stages** (re-cut from the head census, 1045 / 1108; `data/` 185 / 200 left, the same cut stages 15 and 16 named): - `data/` in two more stages, name-ordered: 1. `filter.test.ts` to `object.test.ts`: 17 files, 103 messages / 114 ids. `object.test.ts` alone carries 42, so no cut lands nearer the bound; 2. `query-transport.test.ts` to `validation.test.ts` (11 files, 34 / 34) with `data/driver/` (7 files, 48 / 52): 86 ids. - `ui/` 419, about four stages. `api/` 201, two. `system/` 165, two. The files directly in `src/`, 120, one. - The three docblock needles (`ai/build-progress.test.ts:236`, `:237`, `contracts/approval-service.test.ts:274`), one stage with their docblocks. ## What each id became 20 literals (23 ids) now state a decision in words. 3 literals (4 ids) get their subject back in words where the number stood in for it. 72 literals (72 ids) drop a number the title already explains. Every cited record was read with its comments through REST: 35 answer 200. objectstack-ai#14426 answers 404, and its decision was read from what landed: commit `40a44b91b` and its CHANGELOG entry (the `undefined` comparand refusal names the null predicate by its full spellings, so following it never lands in another refusal). The title already says that, so the number is dropped. | record(s) | literal (under `data/`) | now reads | |:--|:--|:--| | objectstack-ai#7872 | `filter-comparand-type.test.ts:41` | "the accepted set — one set, defined at the shared door for every driver". The ruling put the comparand-type set at the shared compile face, the measured superset, instead of per-driver policy. | | objectstack-ai#5869 | `filter-comparand-type.test.ts:255` | "does not judge a non-array list-operator comparand — that SHAPE is refused 400 by the engine’s shape gate". The record's 500 became an enveloped 400 at the engine's list-shape gate. | | objectstack-ai#20347 | `filter-cross-field-comparison-class.test.ts:44` | "the class table — one classification, read by every judge". One comparison classification, lifted from the SQL driver, shared by the lint arms and the driver. | | objectstack-ai#8371 | `filter-dotted-head.test.ts:24` | "classifyDottedFilterHead — the FILTER axis dotted verdict, by the type of the head segment". Option 2: a type-directed verdict on the head segment, with the structured head left unjudged. | | objectstack-ai#19757 | `filter-empty-operator.test.ts:148` | "the empty list stays refused in the equality slot — the ruled refusal of an array there is untouched". Ruling 乙: an array in the equality slot is refused at the shared face, the empty array included. | | objectstack-ai#7597 | `filter-field-reference-lowering.test.ts:56` | "equality triples with a `{ $field }` comparand — lowered to `$eq`, never a bare field spec". | | objectstack-ai#20041 | `filter-like-nul-pattern.test.ts:25` | "hasNulInLikePattern — one predicate, so every door refuses a pattern holding U+0000". One spec predicate beside the dangling-escape one, asked at every door. | | objectstack-ai#7536 | `filter-like-wire-lowering.test.ts:51` | "the wire lowering of `like` / `ilike` — its own operator, never folded onto `$contains`". The caller binds the wildcards. | | objectstack-ai#20116, objectstack-ai#19886 | `filter-ne-array-schema-door.test.ts:243` | "§5 — the stored-filter carrier walk refuses $ne: [...] on save too". The save door refuses what the query faces refuse. objectstack-ai#19886 is dropped: the title already says the slot refuses an array. | | objectstack-ai#20336 (2) | `filter-number-comparand-declared-type.test.ts:59`, `:174` | "the numeric grammar — a JSON number literal naming a finite double" and "numberComparandDoorVerdict — a numeric string narrows to its number, a non-numeric one is refused". | | objectstack-ai#20510 | `filter-number-comparand-declared-type.test.ts:308` | "`aggregated` and `boundByDriver` — the refusal says only what is true at its position". The refusal names an aggregated column as such, and names the PostgreSQL server error only where the driver binds. | | objectstack-ai#5701, objectstack-ai#3948, objectstack-ai#6520 | `filter-operator-vocabulary.test.ts:82`, `:89` | One declared expect message: "(measured when $icontains was staged — a dropped predicate WIDENS, which on an RLS read scope is an unauthorized read)" and "Admitting $icontains with an arm on every JS face, in one PR, is the worked example of the clearing direction." | | objectstack-ai#4706 | `filter-operator-vocabulary.test.ts:121` | "covers the operators retired when `$icontains` replaced `$regex`". Option B: `$regex` retired, `$icontains` added. | | objectstack-ai#8793 | `filter-preset-comparand.test.ts:31` | "FilterConditionSchema — a bare preset name in an ordering comparand is refused at publish". | | objectstack-ai#19889 (2) | `filter-save-door-face-parity.test.ts:308`, `:309` | Two declared `it.each` labels: "an implicit list (an equality-slot refusal, unchanged)" and "a $eq list (an equality-slot refusal, unchanged)". Ruling A: the save door refuses an array in the equality slot. | | objectstack-ai#18113 | `filter-text-comparand.test.ts:51` | "isRefusedTextComparand — the one predicate every face reads, driven through every FILTER_TEXT_CASES case". Ruling D: the predicate lives with the contract's owner. | | objectstack-ai#4706 | `filter-text-conformance.test.ts:162` | A declared expect message: "a case-folding $contains returns both — the answer ruled out when $contains was declared case-sensitive". The record's second question, answered A. | | objectstack-ai#15661 | `filter-text-operator-declared-type.test.ts:184` | "textOperatorDoorVerdict — a text operator over a type that never stores a string is refused". C-deny. | **Subject restored (4 ids):** objectstack-ai#5222 and objectstack-ai#7597 at `filter-comparand-type.test.ts:225` ("leaves a FieldReference alone at every position — cross-field push-down, scalar comparison and the `$eq` lowering own its fate"), objectstack-ai#8296 at `filter-dotted-head.test.ts:40` ("a virtual head is refused: the unmaterializable-field verdict finally reaching the dotted spelling") and objectstack-ai#19514 at `filter-save-door-face-parity.test.ts:743` ("… and the text-comparand arm stays silent"). **Dropped only (72 ids):** objectstack-ai#5146 (2), objectstack-ai#5234, objectstack-ai#5240, objectstack-ai#5298 (3), objectstack-ai#5701, objectstack-ai#5869, objectstack-ai#7536 (2), objectstack-ai#7872 (2), objectstack-ai#8220, objectstack-ai#8793, objectstack-ai#8934, objectstack-ai#14079, objectstack-ai#14426, objectstack-ai#15661 (5), objectstack-ai#18113 (2), objectstack-ai#19514 (3), objectstack-ai#19886 (4), objectstack-ai#19889 (5), objectstack-ai#20116 (7), objectstack-ai#20143 (3), objectstack-ai#20311 (3), objectstack-ai#20336 (5), objectstack-ai#20347 (3), objectstack-ai#20446, objectstack-ai#20502 (8), objectstack-ai#20510, objectstack-ai#21448 (4). - Each of these titles already states the decision it pins: for example "$ne a value: a row with no value satisfies it" for objectstack-ai#5298, "§1 — the enumeration: the save door refuses exactly what the query faces refuse" for objectstack-ai#20116, or "matchesLikePattern — `_` is one code point" for objectstack-ai#20143. - **The `§1` … `§7` markers stay.** They number each file's own sections, which its comments (`// §1 …`) and header name; only the card number in front of them goes. - **Files that carried a record as a file-wide tag** (`filter-cross-field-comparison-class`, `filter-number-comparand-declared-type`, `filter-text-operator-declared-type`, `filter-like-wire-lowering`, `filter-text-comparand`): the verdict's describe now states the decision (above). The others name the contract part they pin (the judged fields, the refusal message, the fixture, the case table), and their child titles state what each part does. The files' header comments still name the records; comments are not touched here. - **Declared labels and messages:** the five `filter-lowering.test.ts` `NULL_ROWS` names feed `it(row.name)` and the seven `filter-save-door-face-parity.test.ts` labels feed a `%s` title; vitest prints both whole, as the head run's names show. ## Readers - **Test-name filters:** none. No tracked script, workflow or config passes `-t` / `--testNamePattern`. - **Snapshots:** none. No `__snapshots__` directory is tracked under `packages/spec`. - **Projects:** none of the 20 files is listed in `packages/spec/vitest.repo-tests.json`; all 20 run in the `local` project. - **By substring:** every old literal, plus a window around each id (260 needles), was searched across the tracked tree outside its own file. No gate, doc, filter, snapshot or `scripts/check-*.mjs` self-test reads one. The 7 needle hits fall on 5 lines: a code comment in `filter-comparand-type.ts:392`, and sibling titles in `objectql` (`engine-number-comparand-declared-type-door.test.ts:447`, `:459`, `:484`) and `rest` (`data-number-comparand-door.test.ts:269`). None reads a spec test title. - **The files by name:** `scripts/check-driver-conformance.mjs` names `filter-text-conformance.ts`, the source module, not the test file's titles. The other references to these file names are code comments, plus one design-doc line (`docs/design/predicate-compilation-convergence.md:377`) that points at `filter-lowering.test.ts`'s `INTRODUCED` constant, not at a title. ## Text-only proof Stage 10's scratch tool (`textonly10.cjs`, md5 `d5e4801dbb4329ab1984da91e92fc47c`) compares base and head file by file on three legs: 1. **Skeleton:** the full AST, with string pieces masked. It must be identical. 2. **Comments:** every comment, byte-equal. 3. **Strings:** each changed string leaf must sit in a test-call title position or on a declared line, must carry a tracker id before, and must carry no `#` plus digits after. The declared lines are `filter-lowering.test.ts:81`, `:83`, `:85`, `:93` and `:95`, `filter-operator-vocabulary.test.ts:82` and `:89`, `filter-save-door-face-parity.test.ts:308`, `:309`, `:311` to `:314` and `:743`, and `filter-text-conformance.test.ts:162`. - **Result:** 20 of 20 files SAME on all three legs, with the per-file counts predicted in writing before the run. - **Totals:** 96 changed string leaves in 95 literals, 81 titles and 15 declared. The diff's `+` and `-` lines are exactly the 96 planned lines, and every file keeps its line count. - **Controls (10 of 10 as predicted, on scratch copies, each anchor hit once):** identifier rename DIFF; numeric literal DIFF; comment edit COMMENT DIFF; a non-title string given an id VIOLATION; a rewritten title given a new id VIOLATION; a title that was id-free at base edited VIOLATION; one title reverted to base SAME; a declared string given a new id VIOLATION; an undeclared `expect` message changed VIOLATION; a title re-split into a `+` chain DIFF. **Test counts:** the 20 files were run at the base, in a separate base worktree, and at the head, with `--project local --project repo`. Both sides read 671 tests, all passed, with the same count and status sequence per file in 20 of 20. 543 full test names change, and each equals the base name with the planned replacements applied (0 mismatches). No full name repeats on either side. ## `main` since the base Re-fetched just before this PR opened, `origin/main` was three commits past the base (`5b2d189e28`). None of them touches `packages/spec` or any of the 20 files, so `main` was not merged. ## Changeset: `skip-changeset` Measured, not assumed: - `npm pack --dry-run` of `@objectstack/spec` lists 2068 files. 0 of the 20 touched files are in it, and no `*.test.ts` at all. The controls `src/data/filter.zod.ts` and `dist/index.mjs` are in it. - In the built `dist/`, a new phrase and an old literal each read in 0 files. The control `Unrecognized key` reads in 42. So this PR publishes nothing, and no changeset is added. ## Verification (at `2ceeffd7e7`) - `pnpm turbo run build` over all packages: 71 / 71. - `@objectstack/spec`: - `vitest run --project local`: 615 files, 18384 passed, 1 todo. - `typecheck` exit 0, including `check:test-typecheck` (52 files / 246 errors / 135 pinned signatures held). Its program holds all 20 touched files, counted with `tsc --listFilesOnly -p tsconfig.test.json`. - `check:generated`: all 15 generated artifacts up to date. - **Gates:** `dispatch-gates --commands` derived 79 families, the same set as stages 13 to 16, and all 79 exit 0. `--ran` reconciles: 79 derived, 79 run, 0 NOT-MEASURED, 0 UNRUN, every family with its exit code recorded. - The five roster families whose rosters sit under a touched directory were also run, and each exits 0: `check:meta-url-spelling`, `check:spec-changes`, `check:authz-resolver`, `check:error-code-casing` and `check:filter-alias-parity`. - **ESLint, a proven narrowing:** `--no-inline-config` over the 20 files reads 0 errors and 0 warnings. The population comes from ESLint's own config: 20 configured, 0 ignored. No file sets `parserOptions.project` or `projectService`, so no untouched file's verdict can move. - `check-governed-merges --test`: NOT governed, 192 changed lines. ## Acceptance notes - **No needle in this group.** Every id was a title or a declared string; no expected value of an assertion over a source docblock was found. The three known needles are untouched. - **Same-id test titles in this card's later stages** go with those stages: 6 lines in `packages/spec/src`. Two are in the next `data/` group (`data/filter.test.ts:576` and `:679`), and four are in `ui/` (`component-object-grid-default-filters.pin.test.ts:56`, `component.test.ts:3518`, `dataset-filter-nested-relation-list.test.ts:220`, `view-filter-rule-value-shape.test.ts:249`). - **Same-id test titles in other packages** are their lanes' test-string shares. A search of `describe` / `it` / `test` lines outside `packages/spec` finds 187 lines in 16 packages and one example app citing ids this PR handled: `driver-sql` 32 (14 files), `service-analytics` 32 (18), `formula` 24 (9), `lint` 21 (8), `objectql` 21 (11), `driver-memory` 13 (10), `driver-turso` 11 (10), `plugin-security` 9 (5), `driver-mongodb` 8 (5), `driver-sqlite-wasm` 6 (5), `rest` 3 (2), `cli` 2, `metadata-protocol` 2, and one each in `core`, `runtime` and `examples/app-todo`. - **Code comments still carry ids** in these files and their sources, for example the `// [objectstack-ai#21448]` line above `filter-save-door-face-parity.test.ts:311` and the header of `filter-operator-vocabulary.test.ts`. Comments are not this card's share, and none is touched here. --- _Generated by [Claude Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_ Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #21714
Clause-②: no
Claim
5987856715(the card's Change, unblocked by the director seat5987403710). InreportForm, the "Joined blocks" repeater'sdatasetrow now declareswidget: 'ref:dataset'andrequired: true. Studio's report inspector now draws a joined report's blockdatasetas the dataset picker, with the required marker, instead of a free-text cell with no marker. Since #21702 (PR #21712), the joined arm ofReportSchemarefuses a block that binds nodataset, atblocks.N.dataset. The old cell therefore let an author add a block that was invalid by construction.What changes
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.packages/spec/src/ui/report-joined-block-dataset.test.ts, beside the tests for the refusal itself): the row declareswidget: 'ref:dataset'andrequired: true. The pin reads the exportedreportForm, which is the output ofdefineForm's parse, so a form schema that stripped either key would also turn it red. The nested row is the same strict, recursiveFormFieldSchemathat already carrieswidgetandrequiredon top-level fields.@objectstack/specpatch(measurement below).JoinedReportBlockSchema.datasetstays optional on the block shape, and the joined arm's refusal is unchanged.defineFormreturnsFormViewParsed, soreportForm's exported type does not move. No objectui file.The renderer side, measured at the pin
.objectui-shaonmainis9dfaca654311. Every reading below comes fromgit show 9dfaca654311:PATHin an objectui clone, read-only.git merge-base --is-ancestor b508ac50d9 9dfaca654311exits 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.9dfaca654311ref:datasetis registered inWIDGETSpackages/app-shell/src/views/metadata-admin/widgets.tsx:3035('ref:dataset': RefDatasetWidget). The widget is at:683, and it is declared'control'at:3144.RepeaterFieldresolves a row field's widget from the row specpackages/app-shell/src/views/metadata-admin/SchemaForm.tsx:2809: the grid cell callsresolveFieldWidget({ name: s.field, schema: sub, fieldSpec: s, widgetContext }). The card layout passesfieldSpec={s}toFieldRow(:2884), which resolves through the same function (:1838).inferWidgetreturns an explicitfieldSpec.widgetfirst (:447), and a spec that pins a widget skips every name detector (:1031).requiredSchemaForm.tsx:2799(grid cell) and:2882(card row) both passrequired={Boolean(s.required)}. The grid header draws the marker at:2772. For a non-boolean field,FieldRowdraws the star (:1878,:1917) and passesaria-requiredthroughFieldControl(:1930).The joined
blocksrepeater declares nowidget, souseGridis false (SchemaForm.tsx:2643) and its rows render in the card layout, asFieldRows. Three things connect the inspector to this row:ReportDefaultInspector.tsx:374–:381feeds the dataset catalog aswidgetContext.datasets, and passes it to the spec form at:652.report-schema.ts:80–:126(getReportForm) prunes only top-level section fields, so theblocksrow specs reachSchemaFormunchanged.reportFormfrom@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). Thennode scripts/ablation-replace.mjsreplaced the anchorwidget: 'ref:dataset', required: true }withwidget: 'ref:dataset' }inreport.form.ts: anchor count 1 to 0, blob8bc1038e3612tob0da741da01a. The test imports./report.formfrom source, so nodist/is in the resolution path.expect(row?.required).toBe(true)and the other 16 stay green.Tests 1 failed | 16 passed (17),AssertionError: expected undefined to be trueatreport-joined-block-dataset.test.ts:261. The direction is the usual one, red.8bc1038e3612) andgit diff HEADis empty.Regenerated artifacts
None needed.
pnpm --filter @objectstack/spec buildfollowed bycheck:generatedreports all 15 generated artifacts up to date. The only generated files built from form specs are the nine packages' i18n bundles, which carrylabelandhelpText.pnpm check:i18nreportsOK (9 package(s) — all bundles in sync, no undeclared authoring keys).Changeset measurement
@objectstack/spec'sfiles[]shipsdistandsrc/**/*.zod.ts.report.form.tsitself is not packed (npm pack --dry-run --json: 2068 entries, noreport.form.ts), but its bundled bytes are. After the build,widget: "ref:dataset", required: trueoccurs in 6 packed files:dist/ui/index.{js,mjs},dist/system/index.{js,mjs}and the twodist/browser/systemtwins. 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 apatchchangeset and noskip-changeset.Tests and gates, at
d7c811ff38pnpm --filter @objectstack/spec test:Test Files 615 passed (615),Tests 18361 passed | 1 todo.pnpm --filter @objectstack/spec typecheckexits 0:tsc --noEmit,check:scripts-typecheckandcheck:test-typecheck(52 file(s) / 246 error(s) / 135 pinned signature(s) held). Pertsc --listFilesOnly, the edited test file is intsconfig.test.json's program (1 hit) and not intsconfig.json's.platform-objectstests that walkreportForm(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/objectstackderived 86 commands.--ranreconciliation:86 derived, 86 run, 0 NOT-MEASURED, 0 UNRUN.PREREQUISITE NOT MET) andcheck:type-check-debtran 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/mainexits 0. Its clause-② level axis is PR-scoped and not applicable on a local run; CI reads this body'sClause-②line.Acceptance notes
reportFormreaches Studio from the@objectstack/specthat the console bundles. Through objectstack's console build that is this tree, because of theOBJECTSTACK_SPEC_DISTinjection. A standalone objectui build uses its own lockfile's@objectstack/specand gets the row once a spec release carrying it lands there.datasethint (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