Skip to content

spec(ui): widen record:related_list.columns to the saved-view ListColumn union - #19235

Merged
os-bill merged 5 commits into
mainfrom
claude/issue-18639-related-list-columns-union
Sep 20, 2026
Merged

os-bill merged 5 commits into
mainfrom
claude/issue-18639-related-list-columns-union

Conversation

@os-bill

@os-bill os-bill commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator

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 columnListColumnSchema'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 acceptedz.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 (4d341f733c82) 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 test500 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 finding(tooling): six packages/spec gates answer an unmet prerequisite with exit 1 — the code a real finding uses — while this repo declares EXIT_PREREQUISITE_NOT_MET=3 and argues why; successor to the #13983/#14008 migration #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-declarationsdeclaration 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-bytesOK (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

…stColumn` union

`RecordRelatedListProps.columns` declared `z.array(z.string())` while the
saved-view key the same spec declares — `listViews[].columns` — declared
`z.union([z.array(z.string()), z.array(ListColumnSchema)])`, and objectui
composes a saved view's columns onto this block verbatim
(`dataSource.view` → `composeElementDataSource` → `savedViewColumns`). Two
published declarations disagreed about one key.

The block key now takes the SAME union by reference — `ListColumnSchema` is
imported from the view face rather than re-spelled — so a decorated saved view
arrives here in the spelling it was authored in, and the two declarations
cannot drift apart. The `describe()` names the override chain and the
`ListColumn` spelling, and says the arms are exclusive because the schema
enforces that.

Scope fences from the ruling are unchanged and pinned: `field.relatedListColumns`
stays child field-name strings only, and the `field-column-lists-canonicalized`
conversion still folds its object entries to the identity string.

Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
Co-authored-by: Claude <noreply@anthropic.com>
…d-list columns

`check:generated` proved exactly two artifacts stale and `--fix` regenerated
those two, never the whole set: `api-surface-declarations/ui.txt` (the
declaration text of `RecordRelatedListProps` and `ComponentPropsMap`) and
`content/docs/references/ui/component.mdx`, which now renders the nested
`RecordRelatedListProps.columns[number]` shape the union admits.

Changeset: `@objectstack/spec` minor, `Clause-②: yes (widening)`.

Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:ui tests tooling labels Sep 20, 2026
@github-actions

github-actions Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 2 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/api-surface-declarations/ui.txt), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-driven.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/data-modeling/external-datasources.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/data-modeling/field-types.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/data-modeling/validation-rules.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/deployment/troubleshooting.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/deployment/validating-metadata.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/getting-started/quick-reference.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/kernel/contracts/data-engine.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/protocol/backward-compatibility.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/protocol/objectql/types.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/protocol/objectui/concept.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/protocol/objectui/layout-dsl.mdx (via RecordRelatedListProps (symbol, a top-level const object))
  • content/docs/ui/forms.mdx (via FieldSchema (symbol, a top-level const))

4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v12.mdx (via RecordRelatedListProps (symbol, a top-level const object))
  • content/docs/releases/v14.mdx (via RecordRelatedListProps (symbol, a top-level const object))
  • content/docs/releases/v17/17-0.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-1.mdx (via FieldSchema (symbol, a top-level const))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/api-surface-declarations/ui.txt) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 136 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 1b82c519df9db93891514b814b4d8e16e42d5d5cpackageMentionDocs.

Which tree this was computed on

This run read content/docs from f75e45b239fac4f6a05fa0fb578866409d6f9aaa — the merge of head 91b9137c498b0202023dbbc58f86122750e2067a into base 1b82c519df9db93891514b814b4d8e16e42d5d5c, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f75e45b239fac4f6a05fa0fb578866409d6f9aaa && git checkout f75e45b239fac4f6a05fa0fb578866409d6f9aaa
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1b82c519df9db93891514b814b4d8e16e42d5d5c 91b9137c498b0202023dbbc58f86122750e2067a && git checkout -B drift-repro 1b82c519df9db93891514b814b4d8e16e42d5d5c && git merge --no-ff 91b9137c498b0202023dbbc58f86122750e2067a

node scripts/docs-audit/affected-docs.mjs --json 1b82c519df9db93891514b814b4d8e16e42d5d5c

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 1b82c519df9db93891514b814b4d8e16e42d5d5c → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…in both comments

Widening `record:related_list.columns` to the saved-view `ListColumn` union
falsified one clause in two in-repo comments, both of which claimed the
page-block sibling was "the same strings-only shape" as
`field.relatedListColumns`.

Both now state what is true after the change AND that the divergence is
deliberate: the block key takes the saved-view union because objectui composes
a saved view's columns onto it verbatim, while `field.relatedListColumns` stays
child field-name strings only by ruling and its
`field-column-lists-canonicalized` conversion still folds object entries to the
identity string. Comment text only — no schema, no refusal behaviour, no
conversion and no assertion is touched, and the diff carries no non-comment
line.

Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
Co-authored-by: Claude <noreply@anthropic.com>

os-bill commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 7469c1dbe6f0b52986e415f9591bd5f82c7ca61d

① Derived judgments

Trees. Every reading names its tree: HEAD-WT = a fresh worktree at 7469c1dbe6 with pnpm install --frozen-lockfile --offline (exit 0) and pnpm --filter @objectstack/spec build (exit 0), so tests, generators and gates could actually run there; MAIN-WT = a fresh worktree at origin/main 9059a9472a (deps installed) from which the PM instruments were run — the four instruments used (check-widening-tells.mjs, check-changeset-no-major.mjs, check-adr-0087-registration.mjs, check-clause2-carriers.mjs) are blob-identical at the head and at origin/main, so the version question does not arise. Base is the merge-base e233db9dbb. Both worktrees were removed afterwards.

  1. Same union, not a lookalike — by identity, at the member level. Probe run as a scratch vitest file INSIDE packages/spec/src/ui/ (HEAD-WT), importing ./component.zod, ./view.zod and ../data/field.zod — the same specifier shapes the source uses; removed afterwards, porcelain empty. Readings: the object arm's array element of RecordRelatedListProps.shape.columns (unwrapped from optional) === ListColumnSchematrue; the same for ListViewSchema.shape.columnstrue; block element === view element → true. What is NOT shared, stated so nobody over-reads "same union": the z.union wrapper (false), the z.array(z.string()) string arm (false; both are a plain string with 0 checks) and the z.array(ListColumnSchema) wrapper (false) are constructed separately at each site — exactly the composition shape page.zod.ts:574 already uses, because view.zod.ts exports no named columns union. Control: the same probe importing ListColumnSchema by ABSOLUTE path also read true here (vitest resolved both specifiers to one module instance), so the implementer's probe artefact did not reproduce in this environment; harmless either way, since the committed pin uses source-shaped specifiers. Ablation leg B (item 6) is the operational proof that a structurally matching lookalike is discriminated.

  2. Scope fences, measured. packages/spec/src/conversions/registry.ts blob 3c7081708a at base == at head (byte-unchanged; field-column-lists-canonicalized at :7269, still toMajor: 18, retiredFromLoadPath: true). field.relatedListColumns at head :1471 is still z.array(z.string({...}).min(1)).optional(); my own FieldSchema.safeParse of a lookup field with relatedListColumns: [{ field: 'amount', label: 'Amount', width: 120 }]success: false, code invalid_type, message beginning Related-list columns are child FIELD-NAME strings; [{ field: 'amount' }] also refused; ['status', 'amount'] accepted.

  3. Exclusivity is enforced, not merely described. ['name', { field: 'amount' }] → refused, invalid_union; the reverse order [{ field: 'amount' }, 'name'] → refused, invalid_union; all-strings accepted, all-objects accepted with label / link / width / align / hidden / sortable / summary still present in the parsed value; [] accepted; ListViewSchema gives the same answers on the mixed and all-object fixtures. The describe's "the two arms are exclusive, so an array mixing strings and column objects is refused" is true at this head.

  4. The describe. Read at component.zod.ts:1179 (HEAD-WT). It names the chain (child highlightFields → field-level relatedListColumns (field-name strings only) → this inline list), says a view-supplied list may arrive in the ListColumn spelling and why, names listViews[].columns as the SAME union, lists the member keys, and states the exclusivity item 3 measured. An author reading only that sentence writes either ['a', 'b'] or [{ field, ... }], never a mix, and knows an omitted key derives. One thing it does not say — see ③ follow-ups.

  5. Comment-only round, re-proved. git diff e233db9dbb 7469c1dbe6 -- packages/spec/src/data/ has 24 added/removed lines and every one begins with * after whitespace (the implementer's filter reproduced: zero non-comment lines); no /** or */ boundary moved; the hunks touch only the relatedListColumns TSDoc and the test-file header. The new text checked clause by clause at head: the block key declares the ListColumnSchema union (true, item 1); objectui composes a saved view's columns onto that block verbatim (true at the pin, item 10); this key stays strings-only by ruling (true — ruling 5713646766 fences it by name; measured in item 2); the conversion "still folds an object entry here down to its identity string" (true of the conversion's apply and of its fixture, which is what the pin reads — with a precision note in ③: the conversion is retiredFromLoadPath, so on the load path an object entry is refused, which the same TSDoc's previous sentence already says). Nothing was repaired into a second false statement.

  6. Ablation, re-run by me at this head (HEAD-WT). Baseline src/ui/component.test.ts: 351 passed. Anchor z.array(ListColumnSchema), // the saved view's own per-column decoration hits exactly once. Leg A (arm → z.array(z.string()); blob 4d341f733c8893e25858): 4 failed / 347 passed — identity pin expected ZodString ... to be [Function], parity pin block and saved view disagree about [{"field":"amount","label":"Amount","width":120}]: expected false to be true, strictness pin no longer sees unrecognized_keys, decoration pin refused with invalid_union. Leg B (arm → lookalike z.array(z.object({ field, label?, width? })); blob → 12fe7f18f4): 4 failed / 347 passed — identity pin expected ZodObject ... to be [Function], parity pin disagree about [{"field":"amount","bogus":1}]: expected true to be false, strictness pin expected REJECTION of [{"field":"amount","bogus":1}], decoration pin expected [ …(3) ] to match object (the lookalike silently strips link / align / hidden / sortable / summary). The failure text discriminates the two legs. Restore, both times: git hash-object == git rev-parse HEAD:path (4d341f733c), porcelain empty.

  7. The ② declaration against the gates — see ②.

  8. os-regen routing. git check-attr merge -- PATH for each of the 7 changed paths, run in HEAD-WT (and again in the main checkout; .gitattributes blob 56d9f9bcbb is identical at base, head and origin/main): os-regen on 2content/docs/references/ui/component.mdx, packages/spec/api-surface-declarations/ui.txt; unspecified on the other 5 — .changeset/18639-related-list-columns-listcolumn-union.md, packages/spec/src/data/field.zod.ts, packages/spec/src/data/inline-related-columns.test.ts, packages/spec/src/ui/component.test.ts, packages/spec/src/ui/component.zod.ts.

  9. Generated artefacts, with a firing control (HEAD-WT, after a real build). The build rewrote no tracked file (porcelain empty after it). check:generated on the clean tree: 16 of 16 up to date, check:api-surface-declarations and check:docs included. Control: deleted the hidden line from the RecordRelatedListProps block in ui.txt and the hidden row from the columns[number] table in component.mdxcheck:generated exit 1, exactly 2 of 16 stale (those two); --fix regenerated exactly gen:api-surface-declarations and gen:docs; both blobs back to HEAD (2b350eb0b8, f9270fb19f), porcelain empty. The committed artefacts are what the tooling produces on this tree, and the checker demonstrably fires.

  10. The consumer, read at the PIN. .objectui-sha at head, base and origin/main = 53ded82bf7 (untouched by this PR). At that sha: packages/core/src/data-scope/element-data-source.ts:211-214 savedViewColumns returns view.columns unchanged; :236-237 composed.columns = columns; packages/react/src/element-data-source/ElementDataSourceGate.tsx:216-217 writes next.columns = composed.columns when the block's own columns is unauthored or []; the block maps columns: true (record-related-list.tsx:295-296) and reads the key at :183 through any. So what the consumer sends is a saved view's columns verbatim — listViews[].columns, which is the union this key now declares: the widened declaration matches what arrives. The implementer read dda8f3815d (objectui HEAD at the time, an ancestor of objectui origin/main e86445f574), NOT the pin; the file is a different blob at the pin (0d312484), at dda8f381 (df19d5f2) and at objectui origin/main (8104e38b), but the load-bearing lines are identical at all three, so the premise holds on the pin as well.

Also run at head (HEAD-WT): pnpm check:adr-anchors exit 0 (the diff adds no ADR id); pnpm check:nul-bytes exit 0; pnpm --filter @objectstack/spec typecheck exit 0 (test-typecheck ledger unchanged: 54 / 259 / 144); src/data/inline-related-columns.test.ts, component-record-block-field-security.test.ts, page.test.ts, view.test.ts: 570 passed. CI at this head: 35 check runs, none failed.

② Semver level

@objectstack/spec minor, Clause-②: yes (widening) — correct, and measured rather than taken on the PR's word (all four from MAIN-WT against the base..head diff):

  • check-widening-tells.mjs --declaration no --diff pr.diffexit 4, 2 tells (T1 component.zod.ts:1176 columns: z.union([; T2 component.zod.ts:19, the ListColumnSchema import); --declaration yes → exit 0. The instrument sees a widening in this diff, so yes (widening) is the direction the diff actually has.
  • check-changeset-no-major.mjs --base e233db9dbb --head 7469c1dbe6 --event event.json (payload built from the live PR object): exit 0 — no major; LEVEL AXIS reads yes (widening), carrier needs:contract-review IS on the PR, no moved package graded patch.
  • check-adr-0087-registration.mjs --base --head: exit 0 — 1 non-breaking changeset, no disposition owed.
  • check-clause2-carriers.mjs --pair 19235: exit 0 (claim Clause-②: yes, PR body yes (widening)).
  • AGENTS.md rule: yes takes at least minor; nothing previously admitted is refused, nothing renamed or retired, no producer is required to write the new arm. Type-level note: the inferred type of the key widens to string[] | ListColumn[], so a downstream TypeScript consumer that assigned it to string[] would need a narrowing; the in-repo consumers (grep at head) do not, and the pinned objectui reads it through any. Under the launch-window convention that still grades minor.

③ Boundary flags

Answered against the two report comments on #18639 (5747170075, 5747387415) and the seat augmentation (5747181231):

  • NOT MEASURED × 2 — check:dual-build-cjs-loads, check:type-check-debt. Not measured by me either (each needs every workspace package built). Escalated to CI, which is what the implementer proposed and which has answered at this exact head: check:dual-build-cjs-loads runs in ci.yml job Build Core (job 106010778078, success); check:type-check-debt runs in lint.yml job Type Check · debt ledger (job 106010721454, success). Closed by CI, not by a local reading.
  • Declared record substitution (5 families re-run after building their closures). Legitimate: the first-sweep codes were prerequisite refusals (exit 3, and the exit-1 conflation carded as finding(tooling): six packages/spec gates answer an unmet prerequisite with exit 1 — the code a real finding uses — while this repo declares EXIT_PREREQUISITE_NOT_MET=3 and argues why; successor to the #13983/#14008 migration #19227), which are "could not answer", not readings; the re-run codes are the readings and both were declared. I did not re-run the five; Lint & Repo Gates (job 106010721449) is green at this head.
  • Lock queue-timeout. Process deviation only; no gate was narrowed. Accepted.
  • C1 carrier split (exit 4 at round 1). Resolved: the PR now carries needs:contract-review and --pair 19235 reads exit 0 (my run above).
  • Scope fence / augmentation. The seat augmented the claim's file surface for two comment-only edits (5747181231); item 5 proves the diff under packages/spec/src/data/ is comment-only and true. view.zod.ts blob 358aed5fc7 is identical at base and head — read, not edited, as claimed.
  • Probe artefact. Did not reproduce here (item 1); no action.
  • No ablation in round 2. Closed by this review: both legs re-run at 7469c1dbe6 (item 6).
  • Stale card line numbers. Re-derived at head: component.zod.ts:1176-1179 (the key), view.zod.ts:2228-2231 (the saved-view union), view.zod.ts:821 (ListColumnSchema).
  • Coordinator-message corrections. The augmentation id is 5747181231 (5747062086 does not exist — I read the card thread directly, 4 comments); the PR carries protocol:data as reported.

Follow-ups, none blocking:

  • Union shared by member, not by binding. Only ListColumnSchema is one reference; the z.union / z.array wrappers and the string arm are re-spelled per site (as at page.zod.ts:574). The residual drift vector is the string arm — a .min(1) added on one side, say, is caught by the parity pin only if a fixture exercises it ([] is accepted on both today). When view.zod.ts is free (HELD by feat(spec): declare the author-settable row ceiling for the page-shaped view configs #19226 now), lift the union into one named export there and take it by reference at both page.zod.ts and component.zod.ts; the identity pin then tightens from element to binding.
  • Describe precedence clause. At the pin the view's columns FILL this key only when no inline list is authored (ElementDataSourceGate.tsx:216-217); the describe says "composes … onto this block verbatim" but not where the view sits relative to an authored inline list. One clause closes it; objectui#9593 is where that behaviour gets pinned, so it can ride with the pin bump.
  • TSDoc precision in field.zod.ts. "the conversion still folds" is true of the ADR-0087 upgrade step; adding "(retired from the load path)" keeps a reader from inferring runtime folding. The preceding sentence already states the parse-time refusal.
  • Instrument observation, not this PR's: check-widening-tells raised a T2 tell on an added IMPORT line (component.zod.ts:19), reading an import list as a closed set. Harmless under yes; a false-positive shape for a future no diff.
  • Outside this PR: the objectui pin 53ded82bf7 is not an ancestor of objectui origin/main e86445f574 (it is reachable from several objectui feature branches). Not caused or touched here; noted because pin and HEAD were asked to be distinguished.

Implemented-by: claude/issue-18639-related-list-columns-union
Reviewed-by: session_01JbZnqu8bt6YqfJsr9vaFb3

VERDICT: PASS


Generated by Claude Code

`main` moved under this branch: #19226 landed the author-settable row ceiling
and touched the same two generated artifacts this branch owns. Both are routed
to the `os-regen` merge driver, which merges them with exit 0 while silently
keeping one side, so `scripts/pm/os-regen-merge.sh` was run: it merged
`origin/main`, took main's side of the two artifacts in the worktree, and
committed the merge first. This is its step 4 — regenerate on the committed
merge, never a text merge and never a hand edit.

Regenerated with the repo's own tooling on a REAL build (no `OS_SKIP_DTS`):
`build` -> `gen:schema` -> `gen:api-surface-declarations` -> `gen:docs`.
`check:api-surface-declarations` reads "declaration text unchanged (17 entry
points, 5364 declarations)" and `check:generated` is green on all 16 artefacts.

Both sides asserted present afterwards, against the STAGED index blobs as well
as the worktree, with a dark control reading 0: #19226's
`ui/GalleryConfig:limit` / `ui/KanbanConfig:limit` / `ui/TimelineConfig:limit`,
`DEFAULT_VIEW_ROW_LIMIT` and `KanbanConfigParsed`; and this branch's own
`RecordRelatedListProps.columns` union with its nested `columns[number]` docs
table.

Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
Co-authored-by: Claude <noreply@anthropic.com>

os-bill commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 91b9137c498b0202023dbbc58f86122750e2067a

① Derived judgments

Second at-tier record on a NEW head. The first, 5747522705 (head 7469c1dbe6), PASSED; this one answers the live question — did the sync (PR #19226 landing on main and drifting this PR's two generated artefacts) change anything a contract review must judge, and did it drop or smuggle anything — and still renders the full record on this head. Trees, each reading names its own: HEAD-WT = a fresh worktree at 91b9137c49, pnpm install --frozen-lockfile --offline exit 0, a REAL pnpm --filter @objectstack/spec build exit 0 (no OS_SKIP_DTS), porcelain empty after it — tests, generators and gates could run there; MAIN-WT = a fresh worktree at origin/main 1b82c519df (deps installed) from which the four instruments ran (check-widening-tells.mjs blob 032bd9db74 and check-clause2-carriers.mjs blob a76e4ce0c1 are identical at head and main; check-changeset-no-major.mjs and check-adr-0087-registration.mjs live under scripts/, not scripts/pm/); REPRO-WT = a scratch worktree at the previous head 7469c1dbe6 in which I ran the driver merge myself. All three removed afterwards. The base is now the merge-base 1b82c519df (= origin/main, an ancestor of the head: git merge-base --is-ancestor YES); the first record's base was e233db9dbb.

  1. Implementation bytes did not move. git rev-parse 7469c1dbe6:PATH 91b9137c49:PATH, per file: component.zod.ts 4d341f733c82 = 4d341f733c82; component.test.ts 13cc425e654d = same; field.zod.ts 8bf5f106193e = same; inline-related-columns.test.ts b437ca5d890d = same; the changeset da5b62ce4d88 = same. All five SAME. The only source file that differs between the two heads is view.zod.ts (358aed5fc71d to 65853f52f3b7), and it moved by MAIN's hand (feat(spec): declare the author-settable row ceiling for the page-shaped view configs #19226), not this PR's: its head blob equals origin/main's, and git diff e233db9dbb 1b82c519df -- view.zod.ts touches neither ListColumnSchema nor the saved-view columns union (the only changed line naming columns is a TSDoc sentence about KanbanConfigSchema). So the first record's judgments on the hand-written surface transfer by identity; the executable ones are re-measured anyway (items 6 and 7).

  2. The driver's drop — reproduced at the index-blob grain — and both sides at the head, by PATH. REPRO-WT: git merge --no-edit --no-ff 1b82c519df at 7469c1dbe6 with this clone's registered merge.os-regen.driver: exit 0, zero conflict markers in either routed path, auto-committed. Index blobs (git show :PATH): ui.txt = 2b350eb0b8 (OURS, the old head's blob), component.mdx = f9270fb19f (OURS); worktree the same; feat(spec): declare the author-settable row ceiling for the page-shaped view configs #19226's tokens (DEFAULT_VIEW_ROW_LIMIT, KanbanConfigParsed) in the merged index blob of ui.txt: 0; os-regen-pending in that worktree's GIT_DIR names exactly the two paths; firing control: the unrouted view.zod.ts index blob = main's (an ordinary path merged normally). The tree my merge produced is byte-identical to the PR's own merge commit 888bfffc (git diff --stat between them: empty). So 888bfffc IS the driver's silent drop of main's side, exactly as the charter (scripts/pm/os-regen-merge.sh, blob c291f1d784, identical at head and main) says it will be, and the regen commit 91b9137c (+35/−2 over 888bfffc, those two files only) is the repair. At the HEAD 91b9137c49, both sides, by path and line:

    • feat(spec): declare the author-settable row ceiling for the page-shaped view configs #19226's side. ui/GalleryConfig:limit at packages/spec/authorable-surface/ui.json:489 and packages/spec/authorable-defaults/ui.json:51; ui/KanbanConfig:limit at :599 / :60; ui/TimelineConfig:limit at :1173 / :109 — these three live in NO declaration file, so a grep scoped to api-surface-declarations/ is a range error, not a loss. DEFAULT_VIEW_ROW_LIMIT at api-surface-declarations/ui.txt:5490-5491 (also api-surface/ui.json, export-origins/ui.json); KanbanConfigParsed at ui.txt:9762-9763; the ten limit: z.ZodDefault rows at ui.txt:3175 through :10335 (main carries the same ten at :3108 through :10268); feat(spec): declare the author-settable row ceiling for the page-shaped view configs #19226's one added component.mdx row (the timeline limit) at component.mdx:831. Dark controls: the two ui.txt tokens read 0 at 7469c1dbe6 and 0 at 888bfffc; ui/GalleryConfig:zzz_nonexistent reads 0 at the head.
    • This PR's side. The widened columns declaration at ui.txt:3100 (ComponentPropsMap) and :19306 (RecordRelatedListProps) — the two lines this PR adds; the hits at :9493 and :14071 are the pre-existing saved-view declarations, present at both bases. The ### Nested Shape: RecordRelatedListProps.columns[number] table at component.mdx:1381.
    • Everything else feat(spec): declare the author-settable row ceiling for the page-shaped view configs #19226 touched — 15 files: protocol.mdx, object.mdx, view.mdx, api.txt, data.txt, root.txt, system.txt, api-surface/ui.json, authorable-defaults/ui.json, authorable-surface/ui.json, export-origins/ui.json, view.zod.ts, view.test.ts, type-alias-convention.pin.test.ts, .changeset/17393-view-row-ceiling.md — is blob-identical between the head and origin/main.
    • The lines main..head REMOVES from the two shared artefacts are exactly three: the old columns string[] mdx row and the two old columns: z.ZodOptional(z.ZodArray(z.ZodString)) declaration lines. Nothing of feat(spec): declare the author-settable row ceiling for the page-shaped view configs #19226's is removed.
  3. Nothing smuggled. Three-dot contribution at e233db9dbb..7469c1dbe6 and at 1b82c519df..91b9137c49: the same seven paths (A the changeset; M component.mdx, ui.txt, field.zod.ts, inline-related-columns.test.ts, component.test.ts, component.zod.ts) — no path appeared or disappeared. At the CONTENT grain: the sorted added/removed line sets of the PR's contribution to ui.txt (138 lines) and component.mdx (21 lines) are IDENTICAL between the two ranges, and the five hand-written files' added/removed lines hash identically (md5 50071892e0ef…) in both ranges. GitHub's three-dot totals at the head: 7 files, +384/−10.

  4. Generated artefacts re-derived, with a firing control (HEAD-WT, after the real build). check:generated on the clean tree: 16 of 16 up to date (reading of record — see the honesty note). Firing control from that baseline: deleted the hidden line from the RecordRelatedListProps block in ui.txt and the hidden row from the columns[number] table in component.mdx (blobs 2cfe91bb48e1 to 5893a3634265, d40ed33acb29 to 4fdbe3f7f873); check:generated exit 1, exactly 2 of 16 stalecheck:api-surface-declarations ("RecordRelatedListProps (const) declaration text changed … 1 reshaped") and check:docs; --fix regenerated exactly gen:api-surface-declarations and gen:docs; both blobs back to HEAD (2cfe91bb48e1, d40ed33acb29); porcelain empty. Standalone check:api-surface-declarations: "declaration text unchanged ✓ (17 entry points, 5364 declarations)" — 5362 in the first record; the two more are feat(spec): declare the author-settable row ceiling for the page-shaped view configs #19226's DEFAULT_VIEW_ROW_LIMIT and KanbanConfigParsed. Honesty note: my FIRST post-build check:generated read check:docs alone stale. That tree had had a check:generated of mine running CONCURRENTLY with the build (its check:authorable-surface step rewrites the gitignored json-schema/ input the docs renderer reads); the subsequent --fix wrote back the HEAD blobs with an EMPTY porcelain, and a second clean run read 16/16. It was my tree's transient, not the PR's, and the firing control above was re-run from the 16/16 baseline. Stated so that "1 stale" in my logs is not read as a finding.

  5. os-regen routing. git check-attr merge -- PATH for every changed path (7), in HEAD-WT; .gitattributes blob 56d9f9bcbb identical at base, head and main: os-regen on 2content/docs/references/ui/component.mdx, packages/spec/api-surface-declarations/ui.txt; unspecified on 5.changeset/18639-related-list-columns-listcolumn-union.md, packages/spec/src/data/field.zod.ts, packages/spec/src/data/inline-related-columns.test.ts, packages/spec/src/ui/component.test.ts, packages/spec/src/ui/component.zod.ts. Same partition as the first record.

  6. The first record's ① items, re-measured on this head (HEAD-WT). A scratch vitest file INSIDE packages/spec/src/ui/, importing ./component.zod, ./view.zod, ../data/field.zod and ../conversions/registry — the source's own specifier shapes — removed afterwards, porcelain empty:

    • Same union by identity. Block object-arm element === ListColumnSchematrue; view element === ListColumnSchematrue; block element === view element → true. Not shared, as before: the z.union wrapper, the string arm and the z.array(ListColumnSchema) wrapper (false ×3) — member-level sharing, exactly the first record's reading.
    • Exclusivity enforced. ['name', { field: 'amount' }] → refused invalid_union; [{ field: 'amount' }, 'name'] → refused invalid_union; all-strings accepted; all-objects accepted with label / link / width / align / summary / hidden / sortable present in the PARSED value; [] accepted; 'name', [{}] and [{ field: 'amount', bogus: 1 }] refused; ListViewSchema gives the same verdict on every fixture.
    • Fence 1. FieldSchema.safeParse of a lookup with relatedListColumns: [{ field: 'amount', label: 'Amount', width: 120 }] → refused, invalid_type, message beginning "Related-list columns are child FIELD-NAME strings"; [{ field: 'amount' }] refused; ['status', 'amount'] accepted. field.zod.ts itself is byte-identical to the passed head (item 1).
    • Fence 2. packages/spec/src/conversions/registry.ts blob 3c7081708a at e233db9dbb = at 1b82c519df = at 91b9137c49 — byte-unchanged across both bases and the head; field-column-lists-canonicalized present, surface field.inlineColumns[].field / field.relatedListColumns[] object entries, toMajor: 18, retiredFromLoadPath: true, fixture folds to ['status', 'amount', 'issued_on'].
    • The describe at component.zod.ts:1179 reads unchanged: override chain, ListColumn spelling, listViews[].columns, "verbatim", "the two arms are exclusive".
  7. Ablation re-run at this head through scripts/ablation-replace.mjs (WRAP mode, HEAD-WT). Baseline src/ui/component.test.ts: 351 passed. Anchor z.array(ListColumnSchema), // the saved view's own per-column decoration hits exactly once. Leg A (arm to z.array(z.string()); blob 4d341f733c82 to 0904406b8290): 4 failed / 347 passed — decoration pin refused with invalid_union, identity pin "expected ZodString … to be [Function]", parity pin "disagree about [{field:amount,label:Amount,width:120}]: expected false to be true", strictness pin no longer sees unrecognized_keys. Leg B (arm to a lookalike z.object({ field, label?, width? }); blob to 9d5d72ad0c70): 4 failed / 347 passed — decoration pin "expected [ …(3) ] to match object" (the lookalike silently strips), identity pin "expected ZodObject … to be [Function]", parity pin "disagree about [{field:amount,bogus:1}]: expected true to be false", strictness pin "expected REJECTION". Restore, both legs: blob == HEAD (4d341f733c82), git diff HEAD empty, porcelain empty. The failure text still discriminates the two legs, so the pins are not vacuous on this head either.

  8. The consumer at the pin. .objectui-sha blob a9b031ceea identical at e233db9dbb, 1b82c519df and 91b9137c49; content 53ded82bf7 — untouched by the sync. Spot-read at that commit in the objectui clone: element-data-source.ts:211-213 savedViewColumns returns view.columns; :236-237 composed.columns = columns; ElementDataSourceGate.tsx:217 if (!authored && composed.columns !== undefined) next.columns = composed.columns. The same lines the first record read; the premise holds on the pin. (The pin is still not an ancestor of objectui origin/main e86445f574 — outside this PR, as noted before.)

  9. The card thread now has NINE comments. The ninth, 5747741805 (04:57Z, after the first record), is the implementer's round-3 report of this exact sync. Every reading in it that I re-took agrees: merge auto-committed as 888bfffc; the pre-commit refusal at step 3 is the marker doing its job; the staged repair +35/−2 on two files; the both-sides paths (including that the three :limit rows live only in authorable-surface / authorable-defaults); 5364 declarations; 16/16. It also refutes its own earlier attribution of the round-1 probe artefact, which is consistent with item 6 here: the pin with source-shaped specifiers reads true.

Also run at head (HEAD-WT): committed component.test.ts 351 passed; the five pin-bearing files (component, inline-related-columns, view, page, component-record-block-field-security) 927 passed; pnpm --filter @objectstack/spec typecheck exit 0 (test-typecheck ledger unchanged: 54 / 259 / 144); pnpm check:adr-anchors exit 0 ("OK (53 anchored file(s) … 36692 citation(s) across 4726 file(s) resolve"); the PR's diff writes no ADR id — the single ADR string inside pr.diff sits on an UNCHANGED context line of component.mdx (the redactFields row); pnpm check:nul-bytes exit 0 (9015 files); no control bytes in the five hand-written files. CI at this head: 39 check runs — 34 success, 5 skipped, 0 failed; Build Core (job 106021416361), Lint & Repo Gates (106021382373) and Type Check · debt ledger (106021382219) all success.

② Semver level

@objectstack/spec minor, Clause-②: yes (widening) — still correct, re-adjudicated on this head against the repo's own gates (MAIN-WT, diff 1b82c519df..91b9137c49):

  • check-widening-tells.mjs --declaration no --diff pr.diffexit 4, 2 tells (T1 component.zod.ts:1176 columns: z.union([; T2 component.zod.ts:19, the ListColumnSchema import) — the same two as the first record, the import-line one still the false-positive shape noted there; --declaration yes → exit 0. The instrument sees a widening; yes (widening) is the direction the diff has.
  • check-changeset-no-major.mjs --base 1b82c519df --head 91b9137c49 --event event.json (payload built from the live PR object over REST) → exit 0: no major; LEVEL AXIS reads yes (widening), carrier needs:contract-review IS on the PR, direction arm widening, no moved package graded patch.
  • check-adr-0087-registration.mjs --base --head → exit 0 — 1 non-breaking changeset, no disposition owed.
  • check-clause2-carriers.mjs --pair 19235exit 0; it reads the thread complete (9 rows, one page), selects 5747539585 as the ONE governing claim (pool of exactly one, after Release: 5747537574), and finds the label in the same state on card and PR.
  • Changeset blob da5b62ce4d88 unchanged; the AGENTS.md rule unchanged — yes takes at least minor; nothing previously admitted is refused, nothing renamed or retired, no producer is required to write the new arm. The first record's type-level note (string[] | ListColumn[] inference; in-repo consumers unaffected; the pinned objectui reads through any) still applies.

③ Boundary flags

The live question. Did the sync change anything a contract review must judge — no: implementation bytes identical (①1), contribution identical at path AND line grain (①3), the union / fences / exclusivity re-measured unchanged (①6, ①7). Did it drop anything — no: both sides present at the head by path (①2), and the head's artefacts are what the generators produce on a real build (①4). Did it smuggle anything — no (①3). The drop the driver makes is REAL — reproduced by me at the index-blob grain — and is confined to the intermediate commit 888bfffc, which the head repairs.

Is this head WORSE than the head that passed, in any respect? One, and it is a property of the relay rather than of the change: the branch now carries an intermediate commit (888bfffc) whose tree is red under check:generated (main's rows absent from the two routed artefacts). This repo squash-merges (every first-parent commit on origin/main between e233db9dbb and 1b82c519df has one parent), so that commit never reaches main; it is bisect-noise on the branch only. Not blocking, not a contract matter. In every other respect the head is equal or better: base moved to 1b82c519df with origin/main an ancestor of the head, mergeable_state clean, CI 34/5/0.

What still stands from 5747522705. Its ① items 1–10, re-measured here where executable and transferred by identity where the bytes are unchanged; its ② verdict, re-run; its ③ readings: the two NOT MEASURED families (check:dual-build-cjs-loads, check:type-check-debt) — still not measured by me, still closed by CI at THIS head (job ids above); the scope-fence / augmentation reading — the comment-only diff under packages/spec/src/data/ is byte-identical, view.zod.ts read and not edited (its head blob is main's); the probe-artefact note — now also withdrawn by the implementer itself (①9). Its follow-ups carry forward unchanged, none blocking: union shared by member not by binding — view.zod.ts is now FREE (#19226 landed), so lifting the union into one named export there is actionable whenever the seat wants it; the describe precedence clause (the view fills this key only when no inline list is authored, ElementDataSourceGate.tsx:217), one clause that can ride with objectui#9593; the field.zod.ts TSDoc "(retired from the load path)" precision; the widening-tells import-line tell (instrument, not this PR).

Claim thread, read in full (9 comments). Live claim 5747539585; file-surface augmentation 5747181231 authorises the two comment-only edits; 5747533468 is a prose retraction that is INVALID under the current rule (a retraction is the Release: line, one channel — the instrument prints that rule and applies it) and stands only as a record; Release: 5747537574 plus the one fresh Claim: is the repair the rule prescribes, and the instrument now reads a pool of exactly one. The thread is in the shape the protocol accepts.

Not verified by me, stated: the two NOT MEASURED families (CI's, above); the implementer's full-package run (14642 tests) — I ran the five pin-bearing files (927) and the first record plus CI cover the rest; the "MM" staging sequence in the implementer's own worktree (that worktree no longer exists on disk; I reproduced the merge and the drop in a worktree of my own instead, which is the stronger reading).

Implemented-by: claude/issue-18639-related-list-columns-union
Reviewed-by: session_01JbZnqu8bt6YqfJsr9vaFb3

VERDICT: PASS


Generated by Claude Code

os-bill commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

Provenance —— 同步后的达档复核已采纳,三条落地前检通过

domain:spec seat 2 执行席(session_01JbZnqu8bt6YqfJsr9vaFb3),2026-09-20T05:20Z,与剥标、ready、arm 同一动作。

Record of record: 本 PR 评论 5747832209,Head-sha: 91b9137c498b0202023dbbc58f86122750e2067a,VERDICT: PASS。⛔ 逐字采纳。前一份记录 5747522705(head 7469c1dbe6,亦 PASS)因 head 后移已不指向要落地的东西,按 「head 后移或无结论才重挂」 重挂重审。

档位证据(读自子代理自己的转录,常量现场 import):transcript lines=233 / assistant 107 / 107 条全部 claude-fable-5-1 ✅ AT TIER。⛔ get_session 未采信。

授权链(⚠️ 本席先前两次引错了增补评论的 id,施工席两次纠正,此处按实际):本卡活认领 5747539585 · 文件面增补 5747181231 · Release: 5747537574 · 无效的散文撤回 5747533468(留作记录,⛔ 不删)。

三条落地前检:

# 读数
达档条款②复核 PASS 在案 ✅ 5747832209,形状七项全真,零尖括号
双载体已清,机读 ✅ PR 与卡 #18639 同笔剥标;--pair 19235(仪器取自 origin/main)→ exit 0,governing claim = 5747539585,池中恰好一条
全绿,⛔ 非必查子集 ✅ 39 个 check:34 绿、5 skip、0 红、0 未完;mergeable_state: clean

⭐ 本轮第一次有人复现了 os-regen 的静默丢弃,而不是从暂存区推断

复核自己在旧 head 7469c1dbe6 上带着已注册的驱动跑了一遍 git merge 1b82c519df:

888bfffc 就是那次丢弃;重生成提交 91b9137c(+35/−2)是修复。 这不再是推断。

两侧在 head 上的存活,按路径点名:#19226ui/{Gallery,Kanban,Timeline}Config:limitauthorable-surface/ui.json(:489/:599/:1173)与 authorable-defaults/ui.json(:51/:60/:109)——在任何 declaration 文件里都没有;DEFAULT_VIEW_ROW_LIMITui.txt:5490-5491;本 PR 自己的 union 在 ui.txt:3100/:19306。其余 15 个 #19226 文件与 main 逐字节相同。暗对照全 0。

入队前的 os-regen 漂移,在 arm 这一刻重测

merge-base = 1b82c519df = origin/main      ⇒ 漂移 0(主干在该区间动了 0 个文件)
git merge-tree --write-tree origin/main <head>  →  纯树 sha,零冲突路径
os-regen 路由:7 条路径中 2 条(component.mdx、ui.txt)

串行接力:本席此刻队列里没有其它 PR;#19223 复核三轮全 PASS 但被同一个 ui.txt 挡着,本席刻意让它排第二(见 5747598025),⇒ 放行本张即满足「一次只放行一个」。

③ 里唯一一条「比通过的那个更差」

中间提交 888bfffccheck:generated 下是红的。⇒ 本仓 squash 合并(两个基之间所有首父提交均单父),它永远到不了 main。判为接力属性而非契约属性。

⚠️ 复核自陈的一条诚实注记:它第一次跑 check:generated 读到 check:docs 单独陈旧,查明是它自己树上的并发瞬态(另一个已启动的 check:generatedcheck:authorable-surface 步重写了被 gitignore 的 json-schema/ 输入);--fix 写回 HEAD blob、porcelain 空,第二次干净运行 16/16,亮对照从那个基线重做。⛔ 记下来而不是抹掉。

落地路径:ready → auto-merge → 合并队列。⛔ 队列外不合并。


Generated by Claude Code

@os-bill
os-bill marked this pull request as ready for review September 20, 2026 05:20
@os-bill
os-bill added this pull request to the merge queue Sep 20, 2026
Merged via the queue into main with commit 7056ca5 Sep 20, 2026
44 checks passed
@os-bill
os-bill deleted the claude/issue-18639-related-list-columns-union branch September 20, 2026 05:41
os-bill pushed a commit that referenced this pull request Sep 20, 2026
…ged tree

Baseline drift, not a code change — the fourth sync lap on this branch. Main's
#19226 and #19235 moved `packages/spec/api-surface-declarations/{data,root,
system,ui}.txt`, and that directory is a `merge=os-regen` path, so the merge
produced four files current for neither side.

Regenerated from a real build of the merged tree (34/34 declaration files
emitted; ⛔ no `OS_SKIP_DTS`), via `scripts/pm/os-regen-merge.sh`, with
`MERGE_HEAD` confirmed absent first — the build opens with `gen:schema`, and
running that in MERGE state is the anchor-rollback trap.

⚠️ The `MM` grade was live here and was read on purpose. After regenerating,
the index held main's side (803/535) while the worktree held the regeneration
(323/4); a bare `git commit` would have landed the index. `git add -A` first,
then `git diff --cached` re-read as the 323/4 it should be, and every one of the
four index blobs hash-matches its worktree file.

BOTH SIDES asserted by quoted-exact name over the WHOLE TREE with paths printed,
then again against the index blobs, with a dark control reading 0 files:
this branch's facade signature and prescription constant; #19226's
`DEFAULT_VIEW_ROW_LIMIT`, `KanbanConfigParsed` and the three
`ui/{Gallery,Kanban,Timeline}Config:limit` keys; #19235's
`RecordRelatedListProps.columns[number]` and `z.array(ListColumnSchema)`; and
#19219's `ObjectTimelinePropsSchema` carried forward. Note the three `limit`
keys live ONLY in `authorable-surface/ui.json` and `authorable-defaults/ui.json`
and the related-list row ONLY in `content/docs/references/ui/component.mdx` —
a grep scoped to the declaration files reads 0 for them out of range, not loss.

Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec(ui): widen RecordRelatedListProps.columns to the saved-view ListColumn union — the upstream half of objectui#9593 (batch #144 item 3, letter A)

2 participants