Skip to content

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 into
mainfrom
claude/issue-21714-joined-block-dataset-widget
Oct 5, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21714-joined-block-dataset-widget

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 FieldRows. 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

claude added 2 commits October 5, 2026 04:08
…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>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation protocol:ui tests tooling labels Oct 5, 2026
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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): node scripts/docs-audit/affected-docs.mjs --json 18c7dfd2e68cd2630420080b49a5f6a62fe60a6a → packageMentionDocs.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 5, 2026 05:25
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 5, 2026 05:25
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 5, 2026
Merged via the queue into main with commit 9059082 Oct 5, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21714-joined-block-dataset-widget branch October 5, 2026 06:05
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/s tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec(ui): reportForm's "Joined blocks" row offers a block's dataset as free text with no required, so Studio saves the block #21702 now refuses

2 participants