Repository navigation
Commit 7056ca5
Fixes #18639
Clause-②: yes (widening)
Ruling A on objectstack-ai/objectui#9593 — decision batch #144 item 3,
maintainer verbatim 「9593 A,其他同意」 (comment 5713646766). This is the
upstream spec half. The objectui half is objectstack-ai/objectui#9593;
it unblocks on the released package and the pin bump, not on this merge,
and nothing here touches that repository.
## The disagreement this closes
`RecordRelatedListProps.columns` declared `z.array(z.string())`. The
saved-view key the **same spec** declares — `listViews[].columns` —
declared `z.union([z.array(z.string()), z.array(ListColumnSchema)])`.
And objectui copies a saved view's `columns` onto the block
**verbatim**: `composeElementDataSource` does `const columns =
savedViewColumns(view); if (columns !== undefined) composed.columns =
columns;` with no normalisation in between
(`packages/core/src/data-scope/element-data-source.ts`, read at objectui
`dda8f3815d`). So a view whose columns carried `label` / `width` /
`hidden` / `summary` arrived at a block that declared it could not carry
them. Two published declarations, one key.
## What changed
`packages/spec/src/ui/component.zod.ts` — one key:
- `columns` becomes the same union, with `ListColumnSchema` **imported
from the view face** rather than re-spelled. `component.zod.ts` already
takes `NavigationConfigSchema` and `TimelineConfigSchema` from
`./view.zod` for exactly this reason, and `InterfacePageConfigSchema` in
`page.zod.ts` already composes this very union the same way.
`packages/spec/src/ui/view.zod.ts` is **read only** — it is not edited
by this PR.
- the `describe()` names the override chain (child `highlightFields` →
field-level `relatedListColumns` → this inline list), says a
view-supplied list may arrive in the `ListColumn` spelling and why, and
says the two arms are exclusive.
`.changeset/18639-related-list-columns-listcolumn-union.md` —
`@objectstack/spec` **minor**, carrying the `Clause-②: yes (widening)`
line.
Generated artefacts: `check:generated` proved exactly **2 of 16** stale
and `--fix` regenerated those two and no others —
`packages/spec/api-surface-declarations/ui.txt` and
`content/docs/references/ui/component.mdx`, which now renders the nested
`RecordRelatedListProps.columns[number]` table. Nothing was hand-edited
or anchor-edited.
## The union is the SAME union, and the pins say so three ways
1. **Reference identity.** The object arm's array element is asserted
`toBe(ListColumnSchema)` — for this key *and* for
`ListViewSchema.shape.columns`, then the two are asserted equal to each
other. One def, two carriers.
2. **Verdict parity.** Ten fixtures (string arrays, decorated arrays, an
unknown member, a mixed array, a bad nested `prefix.type`, an empty
object, two non-arrays) are asserted to get the **same** accept/refuse
answer from this block key and from the saved-view key.
3. **The refusal text.** An unknown member is refused with
`unrecognized_keys` whose message names *this list column* —
`ListColumnSchema`'s own surface word. A re-spelled lookalike would also
refuse, and would not say that.
Acceptance is pinned on the parsed **value**, not on `success`: the
decoration keys the ruling wants on the screen are asserted still
present after the parse. A schema that accepted and stripped would be
the same defect one layer up.
## Prose and schema agree, and the agreement is pinned
The `describe()` claims exclusivity, so exclusivity is measured:
`['name', { field: 'amount' }]` matches neither arm and is refused
(`invalid_union`). A separate pin reads the description back and asserts
it still carries the override chain, the `ListColumn` spelling, the
`listViews[].columns` reference and the exclusivity clause — so the two
cannot drift apart silently.
## Ablation — two legs, each proven on disk
Both legs go through `scripts/ablation-replace.mjs`, which refuses an
anchor that does not hit exactly once and verifies the write against the
**disk** (anchor count down, replacement count up, `git hash-object`
blob changed) rather than against an exit code, then restores in WRAP
mode and proves the restore against `HEAD`. No rebuild is needed between
legs: `component.test.ts` imports `./component.zod` as **source** inside
the same package, so nothing resolves through `dist/` here.
**Leg A — remove the widened arm.** `z.array(ListColumnSchema)` becomes
`z.array(z.string())`. On disk: anchor `x1 -> x0`, blob `4d341f733c82`
-> `7cadd11f55f7`. Reading: **4 failed | 347 passed (351)** — the
acceptance pin, the identity pin, the verdict-parity pin and the
strictness pin all go red together. The mixed-array, non-array and
`describe()` pins stay green, and that is the point: with the arm gone
the description would still have promised the `ListColumn` spelling
while the schema refused it, which is the prose-without-schema shape
this PR is careful about.
**Leg B — a LOOKALIKE object arm instead of the saved-view def.**
`z.array(z.object({ field, label?, width? }))`. On disk: anchor `x1 ->
x0`, blob `4d341f733c82` -> `1a95d6eb5996`. Reading: **4 failed | 347
passed (351)**, and the failures say exactly why a lookalike is not good
enough:
- the decoration-survival pin prints `link`, `align`, `summary`,
`hidden` and `sortable` **silently stripped** from the parsed value —
the lookalike accepts the input and throws away the keys the ruling
wants rendered;
- the strictness pin shows `{ field: 'amount', bogus: 1 }` now
**accepted** — `z.object` strips unknown keys where `ListColumnSchema`
names them;
- the identity and parity pins go red on their own terms.
So the "SAME union, not a lookalike" requirement is not decoration on
the commit message; it is the thing three of these pins measure.
**Restore — proven, both legs.** `blob after restore == blob at HEAD
(4d341f7)` and `git diff HEAD` empty, asserted by the tool on the
absolute path (an empty hash would be read as failure, not as a match).
Final green leg on the restored tree: **351 passed (351)**, and `git
status --porcelain` is empty at `007310f74d`.
## Scope fences from the ruling — held by measurement, not by intent
- `field.relatedListColumns` (`data/field.zod.ts`) is **unchanged** and
still strings-only: the pin drives a real `FieldSchema` parse and
asserts a `ListColumn`-shaped entry is refused with the derivation
prescription (`FIELD-NAME strings`).
- the `field-column-lists-canonicalized` conversion
(`conversions/registry.ts`) is **unchanged**: the pin asserts its
declared surface string and that its fixture still folds `{ field:
'status', label: 'Status' }` down to `'status'` — the decoration is
still dropped on that key, which is exactly what widening the block
sibling does not do.
## Verification
- `pnpm --filter @objectstack/spec test` — **500 test files / 14636
tests passed**; `src/ui/component.test.ts` alone is 351.
- `pnpm --filter @objectstack/spec typecheck` — green, all three legs
(`tsc --noEmit`, `check:scripts-typecheck`, `check:test-typecheck`: 54
files / 259 errors / 144 pinned signatures, ledger unchanged).
- `pnpm check:adr-anchors` — exit **0**: `check-adr-anchors: OK (53
anchored file(s), every governing ADR still referenced; 133 decision
number(s) ...; 36678 citation(s) across 4723 file(s) resolve ...)`. This
PR writes **no** ADR id anywhere — source, test, changeset or this body.
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived **107** families at `007310f74d`;
all 107 were run, each exit code captured before any pipe, and
reconciled with `--ran` in the tool’s `COMMAND :: exit CODE` form: **107
accounted, 105 run, 2 NOT MEASURED, 0 unrun.** Seven were non-zero on
the first sweep and every one of them refused its own prerequisite
rather than reporting a finding — six with exit 3, and
`check:skill-examples` with exit **1**, which is the conflation carded
as #19227. Five went green once their closure was built (`pnpm --filter
'@objectstack/lint...' --filter '@objectstack/client-react...' --filter
'@objectstack/objectql...' build`): `check:doc-formula-expressions`,
`check:doc-security-posture`, `check:docs-transcript-drift`,
`check:lean-entry-closure`, `check:skill-examples`. The two that stay
**NOT MEASURED** are `check:dual-build-cjs-loads` and
`check:type-check-debt`; both need every workspace package built, both
say so in their own output, and both are CI's to answer.
- `pnpm --filter @objectstack/spec check:generated` — green after the
regeneration (16 artefacts).
- `pnpm --filter @objectstack/spec check:api-surface-declarations` —
`declaration text unchanged ✓ (17 entry points, 5362 declarations)`.
- `MANIFEST=... check:react-declaration-parity --strict` — exit 0, `no
new DECLARATION divergence vs accepted baseline`. The registry still
declares `columns` as `{ type: 'array', of: 'string' }`; this gate
compares prop NAMES, not types, so the widening does not move it. The
type half is the objectui card's.
- `pnpm check:nul-bytes` — `OK (scanned 9012 text file(s) ... no raw
ASCII control bytes)`, plus a direct control-byte scan over the three
hand-written files.
## Two line numbers on the card are stale
The card says to anchor on the symbol, and the tree disagrees with its
line numbers. Re-derived on `origin/main` `e233db9dbb`:
`RecordRelatedListProps` is at `component.zod.ts:1136` (card: near
:1092) and the saved-view union is at `view.zod.ts:2230` with
`ListColumnSchema` at `:821` (card: :2021). The card's substantive
reading holds.
## Acceptance notes
Noted, not filed — a stale cross-reference this PR creates, in two
**in-repo source comments**, neither of which reaches published docs:
- `packages/spec/src/data/field.zod.ts` — the TSDoc above
`relatedListColumns` says "the page-block sibling
`record:related_list.columns` is the same strings-only shape". After
this PR it is not. Only the `.describe()` below it renders into
`content/docs/references/data/field.mdx`, and that sentence stays true,
so nothing published is wrong.
- `packages/spec/src/data/inline-related-columns.test.ts` — its file
header repeats the same clause in item 4.
Not corrected here: the ruling fences `field.zod.ts` by name, and the
dispatch repeats that fence, so the correction is left for the seat
rather than crossed quietly. Both files were measured **free** across
all 20 open PRs (340 changed paths, read 2026-09-20), so it is a
one-edit follow-up whenever the seat wants it. The new TSDoc on
`component.zod.ts` states the current truth from this side, so a reader
arriving from the block never sees the stale claim.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1b82c51 commit 7056ca5
7 files changed
Lines changed: 384 additions & 10 deletions
File tree
- .changeset
- content/docs/references/ui
- packages/spec
- api-surface-declarations
- src
- data
- ui
Lines changed: 15 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1366 | 1366 | | |
1367 | 1367 | | |
1368 | 1368 | | |
1369 | | - | |
| 1369 | + | |
1370 | 1370 | | |
1371 | 1371 | | |
1372 | 1372 | | |
| |||
1378 | 1378 | | |
1379 | 1379 | | |
1380 | 1380 | | |
| 1381 | + | |
| 1382 | + | |
| 1383 | + | |
| 1384 | + | |
| 1385 | + | |
| 1386 | + | |
| 1387 | + | |
| 1388 | + | |
| 1389 | + | |
| 1390 | + | |
| 1391 | + | |
| 1392 | + | |
| 1393 | + | |
| 1394 | + | |
| 1395 | + | |
| 1396 | + | |
| 1397 | + | |
| 1398 | + | |
| 1399 | + | |
1381 | 1400 | | |
1382 | 1401 | | |
1383 | 1402 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3097 | 3097 | | |
3098 | 3098 | | |
3099 | 3099 | | |
3100 | | - | |
| 3100 | + | |
| 3101 | + | |
| 3102 | + | |
| 3103 | + | |
| 3104 | + | |
| 3105 | + | |
| 3106 | + | |
| 3107 | + | |
| 3108 | + | |
| 3109 | + | |
| 3110 | + | |
| 3111 | + | |
| 3112 | + | |
| 3113 | + | |
| 3114 | + | |
| 3115 | + | |
| 3116 | + | |
| 3117 | + | |
| 3118 | + | |
| 3119 | + | |
| 3120 | + | |
| 3121 | + | |
| 3122 | + | |
| 3123 | + | |
| 3124 | + | |
| 3125 | + | |
| 3126 | + | |
| 3127 | + | |
| 3128 | + | |
| 3129 | + | |
| 3130 | + | |
| 3131 | + | |
| 3132 | + | |
| 3133 | + | |
| 3134 | + | |
| 3135 | + | |
| 3136 | + | |
| 3137 | + | |
| 3138 | + | |
| 3139 | + | |
| 3140 | + | |
| 3141 | + | |
| 3142 | + | |
| 3143 | + | |
| 3144 | + | |
| 3145 | + | |
| 3146 | + | |
| 3147 | + | |
| 3148 | + | |
| 3149 | + | |
| 3150 | + | |
| 3151 | + | |
| 3152 | + | |
| 3153 | + | |
| 3154 | + | |
| 3155 | + | |
| 3156 | + | |
| 3157 | + | |
| 3158 | + | |
| 3159 | + | |
| 3160 | + | |
| 3161 | + | |
| 3162 | + | |
| 3163 | + | |
| 3164 | + | |
| 3165 | + | |
| 3166 | + | |
| 3167 | + | |
3101 | 3168 | | |
3102 | 3169 | | |
3103 | 3170 | | |
| |||
19236 | 19303 | | |
19237 | 19304 | | |
19238 | 19305 | | |
19239 | | - | |
| 19306 | + | |
| 19307 | + | |
| 19308 | + | |
| 19309 | + | |
| 19310 | + | |
| 19311 | + | |
| 19312 | + | |
| 19313 | + | |
| 19314 | + | |
| 19315 | + | |
| 19316 | + | |
| 19317 | + | |
| 19318 | + | |
| 19319 | + | |
| 19320 | + | |
| 19321 | + | |
| 19322 | + | |
| 19323 | + | |
| 19324 | + | |
| 19325 | + | |
| 19326 | + | |
| 19327 | + | |
| 19328 | + | |
| 19329 | + | |
| 19330 | + | |
| 19331 | + | |
| 19332 | + | |
| 19333 | + | |
| 19334 | + | |
| 19335 | + | |
| 19336 | + | |
| 19337 | + | |
| 19338 | + | |
| 19339 | + | |
| 19340 | + | |
| 19341 | + | |
| 19342 | + | |
| 19343 | + | |
| 19344 | + | |
| 19345 | + | |
| 19346 | + | |
| 19347 | + | |
| 19348 | + | |
| 19349 | + | |
| 19350 | + | |
| 19351 | + | |
| 19352 | + | |
| 19353 | + | |
| 19354 | + | |
| 19355 | + | |
| 19356 | + | |
| 19357 | + | |
| 19358 | + | |
| 19359 | + | |
| 19360 | + | |
| 19361 | + | |
| 19362 | + | |
| 19363 | + | |
| 19364 | + | |
| 19365 | + | |
| 19366 | + | |
| 19367 | + | |
| 19368 | + | |
| 19369 | + | |
| 19370 | + | |
| 19371 | + | |
| 19372 | + | |
| 19373 | + | |
19240 | 19374 | | |
19241 | 19375 | | |
19242 | 19376 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1454 | 1454 | | |
1455 | 1455 | | |
1456 | 1456 | | |
1457 | | - | |
1458 | | - | |
| 1457 | + | |
| 1458 | + | |
| 1459 | + | |
| 1460 | + | |
| 1461 | + | |
| 1462 | + | |
| 1463 | + | |
| 1464 | + | |
| 1465 | + | |
| 1466 | + | |
| 1467 | + | |
| 1468 | + | |
| 1469 | + | |
1459 | 1470 | | |
1460 | 1471 | | |
1461 | 1472 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
28 | | - | |
29 | | - | |
30 | | - | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
31 | 36 | | |
32 | 37 | | |
33 | 38 | | |
| |||
0 commit comments