Skip to content

fix(spec): the protocol 17 → 18 upgrade rationale states each cited decision in words instead of a tracker number (stage 9) - #21698

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20749-registry-step18
Oct 4, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20749-registry-step18

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #20749
Clause-②: no

Stage 9 of the domain:spec lane's share of the runtime-string burn-down (ruling 5902360492, form D): the author-visible tracker ids in step 18's rationale in packages/spec/src/migrations/registry.ts, per the pointer 5968465093 and stage 8's landing 5976757373. Every cited decision is now stated in words, or the citation is dropped where its sentence already said what was decided. Text only. The test strings (class (e)) are a later stage, so this PR closes no card.

What changed

  • STEP18_RATIONALE: 49 of its 79 fragments carried 88 tracker ids. 72 are objectstack issue numbers, 12 point into objectui (nine issues spelled objectui#N, one issue spelled ui#N twice, one PR), and 4 are director decision-batch numbers that the gate's id pattern also counts. Exactly those 49 fragments changed, and each now reads id-free. Fragment ids, orders and the other 30 fragments are byte-identical.
  • Two two-digit decision-batch numbers (decision batch #91, #77) sat inside two of the same parentheticals. The gate's pattern (three to five digits) does not count them. They went with the sentence they belonged to, and the ruling they named is stated instead.
  • No reader pinned the rewritten words. Every string and regex literal in the repository's code was matched against each fragment's old and new text. No assertion that matched the old text fails on the new one. See "Readers" below.
  • Nothing to regenerate. No generated artefact prints step 18 yet: docs/protocol-upgrade-guide.md prints majors up to PROTOCOL_MAJOR (17), and spec-changes.json carries no step rationale. check:generated reads all 15 artefacts up to date at this head. os migrate meta prints the text at runtime today, because its chain runs to the highest registered major.
  • One @objectstack/spec patch changeset, Clause-②: no. The rationale ships in dist/migrations.
  • No step-17 text, no code comment, no test title, and no schema, order, id, conversion or behaviour change.

Census at the base (A1)

Base 7d0781482d, the worktree before any edit, which is the claim's stamp. I reused stage 8's instruments byte-identically (census.cjs, md5 6e42a45a926d375013c32d62f16a296e; skeleton.cjs, md5 a43a72c63f55c883da7a54b6f600ca08) and held the census to the same reference: check-doc-authoring.mjs --census, run from a scratch copy whose PACKAGES_PROSE_ROOT / PACKAGES_PROSE_EXCLUDED point at packages/spec/src and at nothing. The copy is byte-identical to the one stages 7 and 8 used (md5 8edc9a4064f55d2766c1eb0d3e8d889f; the live gate differs from it only on those two lines). Result: 88 = 88 ids, 0 differences per (line, id).

region of migrations/registry.ts messages ids gate literals
step 17 (const step17) 0 0 0
step 18 (STEP18_RATIONALE, lines 5025–6408) 49 88 81
step 18 entries (surface / replacement / reason / acceptanceCriteria, generated regions) 0 0 0
rest of the file 0 0 0

After

At 9b8c7f38d7: step 18 reads 0 / 0, step 17 still reads 0 / 0, and packages/spec/src outside test strings reads 0 messages / 0 ids. The gate reference agrees (0 sites). Test strings are unchanged at 1803 / 1919.

Text-only proof

Stage 8's skeleton.cjs, base copy against the head file: SAME — 22335 skeleton tokens, 3508 string groups, 49 changed (each id-bearing before, id-free after), diagnostics 0/0. A + chain's string run is one group, so a fragment's whole text is one group; the 49 changed groups are the 49 fragments, and every other string in the file (fragment ids, entry ids, surfaces, conversions) is byte-identical. Controls, each mutation counted on disk on a scratch copy of the head file (anchor hit 1, mutation landed):

control expected got
rename the step18 binding DIFF, exit 1 exit 1
a fragment's order 43 → 44 DIFF, exit 1 exit 1
a fragment's id string renamed VIOLATION, exit 3 exit 3
a step-18 entry's surface edited VIOLATION, exit 3 exit 3
a literal re-split inside a + chain SAME, exit 0 exit 0, 49 changed
an id-free fragment's text edited VIOLATION, exit 3 exit 3
one fragment reverted to its base text SAME, 48 changed exit 0, 48 changed
an id planted in a rewritten fragment VIOLATION, exit 3 exit 3

Lines grew where words replaced a number: the longest new line is 111 characters, inside the region's existing maximum of 117. There is no max-len rule; ESLint reads 0 / 0 on the file.

Per-record table

Every cited record was read through REST with all its comments (59 objectstack issues and PRs, 11 objectui issues and PRs). The four three-digit decision-batch numbers are not cards of this repository: objectstack issues 118, 121, 132 and 210 are unrelated broken-link reports and a docs PR from 2026-01, which is the control that they are batch numbers. Each batch's decision was read from where it is recorded (a ruling comment, or the spec CHANGELOG). Numbers below are written bare.

fragment (order) record decision as read (where) in the text now
metadata-plugin-additional-types-retired (2) 8586 remove additionalTypes (ruling 5293094846) dropped
field-scale-precision-integer-refused (3) 8321 refuse malformed scale / precision at authoring (body, ACCEPT 5296942541) dropped
same 7501 enforce scale by rejecting, never by rounding (ruling 5250623270) "the write-time scale check, which refuses an over-scale value rather than rounding it"
admin-export-wildcard-removed (4) 8681 remove allowExport from the '*' entry of both admin sets (ruling 5299825940) dropped
same 5491 remove member_default's '*' object grant on create / read / edit (ruling 5219845380) "the earlier removal of member_default's CRUD wildcard"
record-chatter-position-vocabulary-converged (5) 8762 the renderer's vocabulary, a conversion, the defaults dropped (ruling 5299771841) dropped; the ruling's date stays
element-input-target-variable-retired (6) 9198 retire the hint, the retirement route (ACCEPT 5311252358) dropped
element-filter-retired (7) 9220 retire element:filter at element grain (verdict 5312176877) dropped
same 9198 its sweep recorded the element-grain lead and left it "the wider finding that the targetVariable retirement recorded"
element-form-retired (8) 9249 retire element:form at element grain (cloud reading 5350190376) dropped
same 9220 the element:filter retirement's own verdict sweep found it "the element:filter shape … recorded by that retirement's own verdict sweep"
same 7751 direction A: the object-* blocks' props enter ComponentPropsMap, so the component-props gate judges them (ruling 5261743573) "its props declared for the component-props gate"
field-inline-and-related-list-columns-closed (9) 9227 a strict element schema for inlineColumns, relatedListColumns in the same pass (ruling 5315735776) dropped
same objectui 3951 the published spelling name wins, the reader is fixed, no tolerant dual read (ruling 5236150020) "objectui aligned the widget to name and retired the field spelling with no tolerant alias"
cube-metric-filters-retired (10) 10414 the remove leg (triage 5363699539) dropped
same 10298 the dataset path answered unfiltered aggregates under the measure's name (body) "the same defect the dataset path had"
same 10411 (PR) the strategy compiles a measure's field and filter on both doors (ACCEPT 5360094389 on 10298) "repaired … when the analytics strategy began compiling each dataset measure's filter"
record-highlights-field-icon-retired (12) 10054 option A, retire (ruling 5364978909) dropped; the ruling's date stays
same 8691 the rail's row declares what the renderer reads; icon is refused, since no render path reads it (ACCEPT 5296436860) "the shape that got the reference-rail icon refused"
same 5176 A: declare readonly, because the chip's gate reads it (ruling 5196402876) "declared because the chip's read-only gate reads it"
page-component-responsive-retired (15) 11027 B: retire, and repair the four redirect texts (ruling 5380752244) dropped
same 4876 A: retire widgets[].responsive (ruling 5169512655) dropped; "tombstone" already says it
object-grid-default-sort-retired (17) 11805 retire, the changeset not major (ruling 5404972152) dropped
same objectui 5861, objectui 4869 2026-08-22 「接受所有」: lower the legacy pair through the shared sort sink now, and retire table.defaultSort on its own card (ruling 5377367456 on 4869) "the producer half of objectui's table.defaultSort retirement, which the maintainer's 2026-08-22 「接受所有」 ruling on objectui's sort sink ordered"
permission-restore-purge-bits-retired (18) 12497 the implementing card of the ruling below (body) dropped
same 1883 B: retire the two bits; the card stays open as the M2 anchor, and the keys return with the feature (ruling 5421209848); gating operations that do not exist is false compliance (ruling 5163046733) "which chose retiring the two bits over gating operations that do not exist"; "the M2 lifecycle initiative …, which stays open as their anchor"
same 8106 (PR) pins the engine's seven-verb dispatch vocabulary (body) "which a test pins"
same 3004 owner_id is server-managed for non-privileged writers (body); allowTransfer enforced through that write guard (1883 ruling 5163046733) "the server guards who may rewrite a record's owner"
field-reference-to-spelling-retired (21) 13700, objectui 6837 C: the server normalizes the protocol and the renderer only executes it; half 1 on the server, half 2 deletes objectui's arms (ruling 5475017957; PASS 5480047935) "the server half of the maintainer's 2026-08-31 ruling …"; "which the ruling's objectui half deletes"
same 4923 equal values: the shadowed alias is deleted with a notice; different values: both kept, the strict door names both (ruling 5173149122) "the house precedence for a shadowed alias"
compliance-deadline-keys-retired (23) 14477 A: retire per family; e-signature too unless roadmapped (ruling 5518646938) dropped; "the deadline-key ruling"
same 15513 A: the three families retire whole, not roadmapped (ruling 5548577921) dropped
duration-keys-unit-in-key (24) 14478 B: a gate plus a conversion of every offender, no baseline (ruling 5518649320) dropped; the ruling's date stays
same 14519 folded into that ruling (5546967881) dropped
metadata-manager-config-inert-cache-keys-retired (25) 14478 the rename gave ttl its ttlSeconds spelling "the duration rename's respelling of ttl"
same 15624 the outer three keys are read by nothing (triage 5548953183) dropped
cron-positions-deleted (26) 16320, 15954 retire the seven cron positions family by family (option A), not mark them experimental (ruling 5559778263) "the 2026-09-06 ruling retired each family rather than marking it experimental"
same 3786 derive a list from its single source, never a hand copy (sweep 5131678201) dropped; the sentence says what the lint does
list-view-page-mount-retired (27) 17063 「撤」, 2026-09-09 (body) dropped
object-kanban-quick-add-retired (28) 17260, objectui 8285, batch 91 B: the board grows no inline record-creation path, and the key retires (ruling 5583979207) "the director-seat ruling of 2026-09-08 that the board grows no inline record-creation path and retires the key"
list-view-sort-string-clause-retired (29) 17053, objectui 8221, batch 77 B: the legacy string clause is retired, one spelling, the array (ruling 5567944420) "ruled 2026-09-07: the legacy string clause is retired, one spelling, the array"
same objectui 8758 (PR) convertSortToQueryParams refuses a runtime string; merged 2026-09-09, before the spec half "whose consumer half shipped in objectui first"
chart-config-aria-retired (31) batch 118 item 2 recommendation C: remove rather than enforce, the protocol judged wrong for this one key (spec CHANGELOG entry 2bf6ef1) "maintainer decision of 2026-09-12 — judge the protocol wrong for this one key"
dashboard-widget-chart-config-structure-refused (32) batch 121 item 1 C+D: the dataset owns structure, chartConfig appearance, and the structural keys are refused by name (ruling 5644017639 on card 17385) dropped; the ruling's date stays, and the sentence states the split
translation-per-app-settings-platform-only (33) 15178, batch 132 item 2 ②: the bundle type splits, and the per-app bundle refuses settings (ruling 5653315643) "maintainer ruling 2026-09-13: settings copy belongs to the platform"
same 19620, batch 210 item 2 B: settings leaves the item door too, one app metadata type, one shape (ruling 5770445203) "maintainer ruling 2026-09-22: one app metadata type, two authoring doors, one accepted shape"
object-tenancy-organization-field-retired (34) 19054 off the authorable surface, kept as a platform fact (body) dropped
page-component-filter-record-to-rule-array (36) objectui 6206 B: one filter spelling platform-wide, the rule array (ruling 5406409590) "ruled 2026-08-25: one filter spelling platform-wide, the rule array"
same 17321 B: a partial conversion of the lossless subset; combinators stay as stored (ruling 5644018752) "ruled 2026-09-12"; the sentence states the rest
view-item-owner-hidden-retired (37) 20085 retire both keys (triage 5826969296) dropped
ui-report-joined-chart-retired (38) 20161 retire (triage 5852548444) dropped
view-overlay-owner-hidden-retired (39) 20230, 20085 follow the view item's disposition for the same pair (triage 5856621469) "the view item's disposition for the same key pair, followed here as triage directed"
ui-form-layout-inline-grid-retired (40) 20221 retire inline / grid (triage 5855767378) dropped
same 18900 A′: a capability mainstream platforms have gets its consumer once; one they lack retires (ruling 5727134555); triage applied it: multi-column already exists, as columns "the maintainer's family criterion (a capability mainstream platforms have is served once, here by columns)"
currency-config-precision-retired (41) 19992 retire (triage 5817146460) dropped
permission-rls-tags-retired (42) 20321 RETIRE by the criterion (triage 5860425529) dropped; the sentence states the criterion
flow-decision-edge-branching-first-match (45) 15429 「跟主流对齐」: first match, inclusive only when declared (ruling 5793803317, 2026-09-23) dropped; the ruling's date added
ui-object-master-detail-form-details-closed (56) 20928 a strict detail entry (ACCEPT 5936904192) dropped
ui-record-line-items-props-closed (57) 21142 the row and the producer fix (ACCEPT 5941249538) dropped
ui-object-grid-export-options-closed (59) 21229 the object form only, refused, not lifted (triage 5939380297) dropped
translation-widget-sub-caption-retired (60) 21257, objectui 11389 C: retire both ends (ruling 5942430353, 2026-10-01) "maintainer ruling 2026-10-01"
same 5428 item 4 the sub-caption got its own key beside widget.description (ruling 5200254075, 2026-08-06), which C reverses "which reverses the 2026-08-06 ruling that gave it a translation key of its own"
object-grid-resizable-columns-retired (62) 21445 retire the alias (triage 5958164933) dropped
same objectui 6152 resizable is canonical, and the alias retires with no window (seat answer 5957903936) "objectui's ruling that resizable is canonical"
ui-object-grid-row-members-typed (63) 21445 type the members (triage 5958164933) dropped
ui-object-map-gantt-tree-navigation-typed (64), list members (66), form members (67), metric aggregate / trend (69), compare-to (71), drill-down (72), grid columns (73) 21464 the staged close-out (seat answer 5963787404) dropped ×7; each sentence names its stage
ui-ai-chat-window-retired (65) 21504 retire (triage 5963897014) dropped
element-text-variant-heading-subheading-retired (68) 21015 release 2 of objectui's ruled two-release split (body) dropped; the sentence names the split
object-master-detail-form-detail-sort-field-retired (70) 21589 retire (triage 5969870827) dropped
same objectui 11070 (round 9) the authored override retires; the sort field is derived from the child object (seat answer 5928627070, landed 5930904717) "the spec half of objectui's own retirement of the override"

Readers

  • Searched by substring, not by guess. A TypeScript-AST pass over all 9878 tracked code and text files collected every string literal (8+ characters) and regex literal, and tested each against each fragment's old and new text, and against the joined rationale. A reader is owed a move only if it matched the old text and not the new. In the files that read the registry or the rationale, the only literals that lose a match are generic words and patterns that match some fragment by coincidence ('decision', 'objectui', /100/), plus the tracker-id detectors, which are absence checks that now pass. No pin needed moving.
  • The step-18 readers that exist, and why they hold:
    • ui/component.test.ts pins the phrase "retires object-kanban's quickAdd" and pins that the rationale never says kanban-ui. That second pin shaped the rewrite: objectui's ruling kept the control on kanban-ui, a node type objectui has since retired, so the new text states the ruling without naming it.
    • system/compliance-families-retirement.test.ts pins /retires those three compliance-shaped families WHOLE/, kept verbatim.
    • data/cube-metric-expression-types-retirement.test.ts reads a fragment this stage does not touch.
    • scripts/step18-rationale-merge.test.ts pins the fragments' sort and that the rationale equals their joined text. Neither moves.
  • No reverse verification was owed: no pin moved, so there is no moved assertion to turn red. The text-only proof's controls and the census's lit control stand in its place.
  • Parallel copies, not readers: the spec CHANGELOG, the 17.1 release notes and three liveness-ledger notes carry their own copies of some old phrases. They are release-owned or ledger notes, and nothing compares them with the rationale.

Tests and gates (at 9b8c7f38d7)

All builds and tests ran through scripts/pm/os-verify-lock.sh, with each exit code written to a file before reading.

  • pnpm --filter @objectstack/spec build: exit 0, 38/38 dts.
  • pnpm --filter @objectstack/spec check:generated: exit 0, all 15 artefacts up to date.
  • The step-18 readers, local project: migrations.test.ts, component.test.ts and the cube-metric test, 561 / 561. Repo project: step18-rationale-merge.test.ts and the compliance test, 19 / 19. The other 19 repo-project tests that import the registry: 314 / 314. conversions-major18-merge.test.ts: 12 / 12.
  • Full spec suite, local project: 610 files, 18113 passed, 1 todo.
  • pnpm --filter @objectstack/spec run typecheck: exit 0.
  • A turbo build of 71 / 71 packages, then the derived union: 84 / 84 exit 0, reconciled by dispatch-gates --ran (84 run, 0 NOT-MEASURED, 0 UNRUN). Three gates first answered PREREQUISITE NOT MET (exit 3, nothing measured) before the 71-package build, and passed after it.
  • ESLint on registry.ts, a proven narrowing: 1 file, 0 errors, 0 warnings. The population comes from ESLint's own config (calculateConfigForFile returns a config, isPathIgnored is false). Invariance: parserOptions.project and projectService are null, so no type-aware rule exists and an untouched file's verdict cannot move.
  • dist: the new phrases are in dist/migrations/index.js and index.mjs; the old runtime phrases probed are in no dist/migrations file. The ids still present in dist are code comments, which this stage does not touch.
  • NOT MEASURED locally: the spec repo project as a whole (stopped at the foreground limit with no output; its registry readers ran above), and scripts/build-schemas-check-mode.test.ts, which reads semantic surface strings and never the rationale. Both are CI's, as is the repo-wide pnpm lint.

Overlap

#21654's PR #21687 (draft, this seat) adds one STEP18_RATIONALE fragment (order 74, no tracker id in its text) and one D3 entry. A local git merge-tree of this head with its head a9d2d5d453 is clean, and registry.ts has no merge driver, so that result is the plain text merge GitHub runs. Whichever lands later merges origin/main through scripts/pm/os-regen-merge.sh. origin/main was re-fetched just before opening this PR (38bef8cf95): the four commits since the base touch neither this file, the conversions registry nor the guide, and a local git merge-tree with it is clean.

Acceptance notes

  • Out of this stage's reach, noted: registry.ts's code comments still carry many ids (the step-18 docblock and the generated entries' comments), and so does dist, which keeps comments. They are the comment lane's share.
  • Kept on purpose: decision-inbox batch 4 and batch 5 name a batch without a # number and are not ids. Present-tense clauses whose follow-through has since landed (for example "held up today only by objectui's fallback arms") were left as written: form D asks for the decision, and the decision is stated.
  • Ledger notes: packages/spec/liveness/dashboard.json, page.json and view.json cite some of the same decision batches in their notes. They are not runtime strings and sit outside this census; nothing here touched them.

Clause-② and changeset

Clause-②: no. Nothing authorable, exported or typed is added, removed or renamed; only the value of MIGRATIONS_BY_MAJOR[18].rationale changes. It ships in dist and os migrate meta prints it, so one @objectstack/spec patch changeset is owed, and skip-changeset does not apply.


Generated by Claude Code

claude added 2 commits October 4, 2026 05:25
…n words, not tracker numbers

The 49 STEP18_RATIONALE fragments that carried a tracker id (88 ids: 72
objectstack, 12 objectui, 4 decision-batch numbers) now say what the cited
record decided, or drop a citation the sentence already explained. Text
only: fragment ids, orders, entries, conversions and schemas are unchanged.

Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tooling labels Oct 4, 2026
@github-actions

github-actions Bot commented Oct 4, 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 38bef8cf95f1d0e3d2dc268eeef5e6b30718eed1 → packageMentionDocs.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 4, 2026 07:22
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 4, 2026 07:22
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 4, 2026
Merged via the queue into main with commit f623e2f Oct 4, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20749-registry-step18 branch October 4, 2026 08:00
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…t blocks[i].dataset, naming the block (objectstack-ai#21702) (objectstack-ai#21712)

Fixes objectstack-ai#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 objectstack-ai#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. objectstack-ai#21699 (74 to
76) and objectstack-ai#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: objectstack-ai#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>
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 size/m tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants