Skip to content

Commit ffba02e

Browse files
docs(spec): record:related_list's parentDataSource is described as rendered (#24578)
Fixes #23585 Clause-②: no `record:related_list`'s `parentDataSource` is rendered by the console at its pin, and `@objectstack/spec` now says so. Before this change, the key's describe opened with `[PLANNED — not yet rendered]`, its TSDoc said no renderer reads it, and the liveness `_note` recorded it as planned. All three told an author, an AI author first, not to use a binding the console renders. The change is describe, TSDoc and ledger text only: `RecordRelatedListProps` accepts exactly what it accepted before, no key is added, and every refusal keeps its issue `code` and `path`. ## The new describe, verbatim > The PARENT record this related list hangs off, bound by a query of its own: an object binding `{ object, view?, filter?, sort?, limit? }`, the shape `dataSource` takes. Read by the single-record rule: the FIRST row of the bound set by its `sort` is the parent (declare `limit: 1` to say one row is meant), and when no row answers, the block renders a loud absence, never an empty list, which would read as "this record has no related records". `relationshipValueField` is read on that row. When this key is absent, the record page's record context remains the parent. The node-level `dataSource` on this block keeps its published meaning, the CHILD rows the list draws (it fills `objectName`, `columns`, `filter`, `sort` and `limit`), and is never read as the parent. Only the marker left. Every sentence after it holds at the pin (P1 below), so no sentence was added or removed. ## P1 — the pin carries the renderer, and the renderer does what each clause says - `.objectui-sha` on `origin/main` is `4016370af3c9c9dafec2bc5299afd8e8079701b8`. In `../objectui`, `git merge-base --is-ancestor d81e460 4016370` exits 0, and `git merge-base --is-ancestor d81e460 fe11eb1` (objectui `origin/main`) exits 0. An exit 0 proves itself, so no control leg is owed. `git diff --stat 4016370 fe11eb1` over the two renderer files prints nothing: objectui `main` reads the key the same way. - `scripts/sdui-manifest.record.json` records `objectuiSha` `4016370af3…`, and `sdui.manifest.json` lists `parentDataSource` as an input of `record:related_list`. The console this repository builds is built from that pin. - Every read below is `git show 4016370:PATH` in `../objectui`. R is `packages/plugin-detail/src/renderers/`. - **The key is read.** `R record-related-list.tsx:810` `const parentBinding = schema?.parentDataSource;`, handed to `ParentRecordBinding` at `:814`–`:824`. - **The FIRST row by its `sort` is the parent.** `R recordBinding.tsx:429` `const params: QueryParams = { $top: 1 };` and `:431` `if (sortKey) params.$orderby = JSON.parse(sortKey);`. The row is read through `useResolvedDataSource` (`:384`), the page's own adapter, so under the caller's identity (module doc `:45`–`:46`). - **No row is a loud absence, never an empty list.** `R recordBinding.tsx:475`: when `absent` is given and the settled read has no row, `BoundRecordProvider` returns `absent` in place of its children. The related list passes `absent={NoParentRecord …}` at `R record-related-list.tsx:821`, and `NoParentRecord` (`:772`–`:786`) draws a `role="status"` notice. The absence replaces the children, so no child query is sent. A read the server refuses as forbidden reads the same as no row (`R recordBinding.tsx:449`). - **`relationshipValueField` is read on that row.** `R record-related-list.tsx:259`–`:263` reads it on the record context (`ctx?.recordId`, or `ctx?.data[relationshipValueField]`). `BoundRecordProvider` provides the bound row as that context. - **Absent, the record page's record is the parent.** `R recordBinding.tsx:274`: when `isElementDataSourceConfig(binding)` is false (no binding), `ParentRecordBinding` returns its children as they are, so the record page's record context still reaches the list. - **The node-level `dataSource` is still the CHILD rows.** `R record-related-list.tsx:724`–`:729` maps `columns`, `filter`, `sort` and `limit` onto the list. The parent gate is given the parent binding alone (`R recordBinding.tsx`, `ParentRecordBinding` docblock). - objectui's own pin, `R __tests__/record-related-list.parentDataSource-12294.test.tsx`, carries a case for each clause: first row by `sort`, `relationshipValueField` on the bound row, no row is the absence with no child query, 403 is the same absence, no `parentDataSource` keeps the page record, and the registration publishes the input. No describe clause is untrue at the pin, so the prohibition does not apply. ## P2 — the marker and the note were present on `origin/main` `5b14b7f3ad` `component.zod.ts:1743` (docblock) and `:1747` (describe) carried `[PLANNED — not yet rendered]`. `liveness/page.json:3`'s `_note` carried the `PLANNED RECORD (#23134 …)` sentence. The card's criterion, on this head: `git grep -n "PLANNED — not yet rendered" -- packages/spec/src/ui/component.zod.ts` names `:1170` and `:1175` only, both on the `emptyState` description (out of scope, G4). `git grep -c "PLANNED RECORD (#23134" -- packages/spec/liveness/page.json` answers nothing (exit 1). ## P3 — every text here that called `parentDataSource` planned, unread or not rendered Measured with `git grep -n parentDataSource` and `git grep -n "not yet rendered"` over the whole tree. Rewritten here: 1. `packages/spec/src/ui/component.zod.ts`, the describe: the marker is gone. 2. The same file, the key's TSDoc: the "PLANNED, not yet read" paragraph is now "RENDERED by the console", naming the read points with the `objectui:` realm marker and the pin. The same docblock said the `api` arm of `dataSource` "is still unrendered". That is false since objectui `c4dd7078ed` (recorded in this ledger's `_note`), so the clause now says why the key takes the object binding only: it is the shape the sibling blocks read, and the shape objectui's parent read takes (`isElementDataSourceConfig` asks the binding for an `object`). 3. `packages/spec/liveness/page.json`, `_note`: the `PLANNED RECORD (#23134 …)` sentence is a `LIVE RECORD`, in the form the `ElementApiDataSource` record took when its renderer shipped. It cites the pin and each objectui path with the `objectui:` realm marker. 4. The same file, the `regions` row's component-member read-point list, entry [10]: `record:related_list.parentDataSource (PLANNED)` pointed at the old measurement (objectui `@f0532d8`, "the record context read"). It now lists the read points at `@4016370` in the list's own `objectui @sha FILE#SYMBOL` spelling. The ledger keeps no `parentDataSource` row; this entry is its one piece of `parentDataSource` evidence, so I read it as inside the claim's "the `parentDataSource` row's status, evidence" item. Named here so a reviewer can disagree. 5. `content/docs/references/ui/component.mdx:1971`, regenerated by `gen:docs`. 6. `packages/spec/src/ui/component-record-related-list-parent-23134.test.ts`, pin E: it asserted the describe opens with the marker. Re-trued, not deleted: it now asserts that `check-liveness`'s planned matcher (`MARKER_RE.planned`, copied with its source named) reads nothing in the describe, with a DARK case showing the same matcher catches the replaced marker. Named, not edited: - `.changeset/pre/23134-related-list-parent-data-source.md:13` and `packages/spec/CHANGELOG.md:2200` ("Planned, not yet rendered"). Both are the consumed changeset and the released entry for `4cc3ff9`. They were true when that release was cut, so they are not factual errors, and both are release-owned. - `content/docs/**` (hand-written) and `skills/**`: no hit for `parentDataSource`. ## G1–G4 - **G1.** The ledger carries no `parentDataSource` row, so there is no row status to move and no author warning. The liveness record is the `_note` (item 3) and the evidence entry (item 4). `pnpm --filter @objectstack/spec run check:liveness` exits 0 (in the gate run below). - **G2.** `pnpm --filter @objectstack/spec build`, then `check:generated` named one stale artifact, `content/docs/references/**`. `gen:docs` rewrote `component.mdx:1971` (1 line). A second `check:generated`: "All 14 generated artifacts are up to date". `authorable-surface.base.json` did not move. - **G3.** `.changeset/23585-related-list-parent-data-source-rendered.md`, `patch` on `@objectstack/spec`, `Clause-②: no`. - **G4.** At the pin and at objectui `main`, `git grep -n emptyState -- packages/plugin-detail/src` outside tests answers nothing. The renderer for the `:1170`/`:1175` `emptyState` markers (objectui#12932) has not shipped, so those markers are still true. ## Tests - `pnpm --filter @objectstack/spec test` on `131a5615e9`: 701 files passed, 21312 tests passed, 1 todo. - `pnpm --filter @objectstack/spec typecheck` on `131a5615e9`: exit 0; `check:test-typecheck` OK. - The pin file alone: 18 passed. - **Reverse verification, one time.** With the change committed, `scripts/ablation-replace.mjs` put the marker back into the describe (anchor 1 → 0, blob `2d039146d858` → `d8a66adf1db4`). The pin file then failed in the expected direction: 1 failed, 17 passed, the failure being E's "no planned marker" case. The tool restored the file: blob equal to HEAD, `git diff HEAD` empty. - The merge of `origin/main` (`04669a467e`, `packages/objectql` and `packages/services/service-automation` only) touches no package this diff touches, so the spec suite was not re-run on the merge commit. ## Gates Run on the final head, `git rev-parse --short HEAD` = `53624a10cf`. `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths; change set from the merge base `04669a467`) derives 111 commands from the 5 changed paths. Each ran with its exit code captured before any pipe: 111 of 111 exit 0. `--ran` on that record: "111 derived, 111 run, 0 NOT-MEASURED, 0 UNRUN". - A first run on `131a5615e9` recorded six PREREQUISITE NOT MET (exit 3): the two `@objectstack/lint` doc gates, `check:skill-examples`, `check:docs-transcript-drift`, `check:dual-build-cjs-loads` and `check:lean-entry-closure`. After the `@objectstack/lint` and `@objectstack/objectql` closures were built, all six measured on `53624a10cf` and exit 0. That earlier run is not the reading; the one above is. - Verdict lines on `53624a10cf`: `check:generated` "All 14 generated artifacts are up to date"; `check:docs` "227 generated files in sync with packages/spec"; `check:liveness` exit 0 ("1038 live · 1 experimental · 1 live-elsewhere · 112 dead · 23 planned = 1175 classified"); `check:nul-bytes` OK; `check:authorable-surface` exit 0. - Local-only, outside the derivation: `pnpm check:dev-prereqs` exit 0. - Every family CI runs is read from this PR's check-runs, not re-run here. ## Acceptance notes - The two `emptyState` markers in `component.zod.ts` (`:1170`, `:1175`) stay. Their renderer has not shipped (G4). - The released `4cc3ff9` text that calls the key planned stays as released (P3, "Named, not edited"). --- _Generated by [Claude Code](https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2667e24 commit ffba02e

5 files changed

Lines changed: 49 additions & 17 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
`record:related_list`'s `parentDataSource` is described as rendered
6+
7+
Clause-②: no (describe and TSDoc text only: `RecordRelatedListProps` accepts exactly what it accepted before, and every refusal keeps its issue `code` and `path`)
8+
9+
- The `parentDataSource` describe no longer opens with `[PLANNED — not yet rendered]`. The console renders the key: a related list reads its parent record from the binding, the first row by its `sort`, and draws a notice in its place when no row answers, never an empty list. Without the key, the record page's record is the parent, as before. The rest of the describe is unchanged.
10+
- The key's TSDoc names the console's read points in place of the planned note, and no longer calls the `api` data source unrendered.

‎content/docs/references/ui/component.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1968,7 +1968,7 @@ Sort field and direction pair
19681968
| **objectName** | `string` | ✅ | Related object name (e.g., "task", "opportunity") |
19691969
| **relationshipField** | `string` | ✅ | Field on related object that points to this record (e.g., "account_id") |
19701970
| **relationshipValueField** | `string` | optional (default: `"id"`) | Parent-record field whose value relationshipField stores (default 'id'; e.g. 'name' for name-keyed junctions). |
1971-
| **parentDataSource** | `{ object: string; view?: string; filter?: object[]; sort?: object[]; … }` | optional | [PLANNED — not yet rendered] The PARENT record this related list hangs off, bound by a query of its own: an object binding `{ object, view?, filter?, sort?, limit? }`, the shape `dataSource` takes. Read by the single-record rule: the FIRST row of the bound set by its `sort` is the parent (declare `limit: 1` to say one row is meant), and when no row answers, the block renders a loud absence, never an empty list, which would read as "this record has no related records". `relationshipValueField` is read on that row. When this key is absent, the record page's record context remains the parent. The node-level `dataSource` on this block keeps its published meaning, the CHILD rows the list draws (it fills `objectName`, `columns`, `filter`, `sort` and `limit`), and is never read as the parent. |
1971+
| **parentDataSource** | `{ object: string; view?: string; filter?: object[]; sort?: object[]; … }` | optional | The PARENT record this related list hangs off, bound by a query of its own: an object binding `{ object, view?, filter?, sort?, limit? }`, the shape `dataSource` takes. Read by the single-record rule: the FIRST row of the bound set by its `sort` is the parent (declare `limit: 1` to say one row is meant), and when no row answers, the block renders a loud absence, never an empty list, which would read as "this record has no related records". `relationshipValueField` is read on that row. When this key is absent, the record page's record context remains the parent. The node-level `dataSource` on this block keeps its published meaning, the CHILD rows the list draws (it fills `objectName`, `columns`, `filter`, `sort` and `limit`), and is never read as the parent. |
19721972
| **columns** | `string[] \| { field: string; label?: string \| Record<string, string>; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | optional | Fields to display in the related list — either plain field-name strings, or the same per-column entries a saved list view declares (`ListColumn`: `field`, plus `label`, `width`, `align`, `hidden`, `sortable`, `summary`, …). A view-supplied list may arrive in the `ListColumn` spelling: objectui composes a saved view's `columns` onto this block verbatim, and this key declares the SAME union as `listViews[].columns`. One spelling per list — the two arms are exclusive, so an array mixing strings and column objects is refused. Optional: when omitted, columns derive from the related object's highlightFields / default list columns (a related list is just another surface that lists that object). Override chain: child highlightFields → field-level relatedListColumns (field-name strings only) → this inline list. |
19731973
| **sort** | `string \| { field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Sort order for related records |
19741974
| **limit** | `integer` | optional (default: `5`) | Number of records to display initially |

‎packages/spec/liveness/page.json‎

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

‎packages/spec/src/ui/component-record-related-list-parent-23134.test.ts‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
// D — CONTROL: the node-level `dataSource` on this block is untouched. It is
1919
// still the child-row binding (#6953) and parses exactly as it did,
2020
// with or without a parent binding beside it.
21-
// E — the describe opens with the `[PLANNED — not yet rendered]` marker,
21+
// E — the describe carries no planned marker: the console renders the key
22+
// (objectui `d81e46043`, at the `.objectui-sha` pin), and a marker is
2223
// the one part of the prose a consumer parses (`check-liveness.mts`
2324
// `MARKER_RE.planned` reads it as the key's liveness status).
2425

@@ -202,10 +203,21 @@ describe('D — CONTROL: the node-level `dataSource` on this block is the child-
202203
});
203204
});
204205

205-
describe('E — the describe carries the liveness marker the ledger reads', () => {
206-
it('opens with the planned marker, which `check-liveness` reads as the key status', () => {
206+
/**
207+
* `MARKER_RE.planned` in `scripts/liveness/check-liveness.mts`, copied: the one
208+
* reading of the prose a consumer parses (as the key's liveness status).
209+
*/
210+
const PLANNED_MARKER = /\[planned|not yet implemented|coming soon/i;
211+
212+
describe('E — the describe carries no planned marker: the console renders the key', () => {
213+
it('no planned marker, so `check-liveness` reads no planned status off the describe', () => {
207214
const key = (RecordRelatedListProps as unknown as { shape: Record<string, { description?: string }> })
208215
.shape.parentDataSource;
209-
expect(key.description?.startsWith('[PLANNED — not yet rendered]')).toBe(true);
216+
expect(typeof key.description).toBe('string');
217+
expect(PLANNED_MARKER.test(key.description!)).toBe(false);
218+
});
219+
220+
it('DARK: the marker this describe opened with is caught by the same matcher', () => {
221+
expect(PLANNED_MARKER.test('[PLANNED — not yet rendered] The PARENT record this related list hangs off')).toBe(true);
210222
});
211223
});

‎packages/spec/src/ui/component.zod.ts‎

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1707,7 +1707,8 @@ export const RecordRelatedListProps = strictObject({
17071707
* as "this record has no related records", a wrong fact in the silent
17081708
* direction. The API arm of `dataSource` is not admitted: the card fixes
17091709
* the shape as the object binding's (object, view, filter, sort, limit),
1710-
* the one the sibling blocks read, and that arm is still unrendered.
1710+
* the one the sibling blocks read and the one objectui's parent read
1711+
* takes (`isElementDataSourceConfig` asks the binding for an `object`).
17111712
*
17121713
* Absent, the parent is the record page's record context, as it always was
17131714
* (objectui `RecordRelatedListBody` reads `useRecordContext()`'s `recordId`
@@ -1734,17 +1735,26 @@ export const RecordRelatedListProps = strictObject({
17341735
* ⛔ No `os validate` consistency finding against a sibling `record:details`
17351736
* binding: the ruling's own exclusion (a new gate defaults to no).
17361737
*
1737-
* PLANNED, not yet read: no renderer reads this key (measured on objectui;
1738-
* recorded in the `liveness/page.json` `_note`).
1739-
* Its reader is the related-list leg of objectui#12294, which lands once a
1740-
* spec release carrying the key is installable. The liveness record is the
1741-
* `liveness/page.json` `_note` (a component-level key carries no ledger row:
1742-
* the walk stops at the undrilled `page/regions` container) and the
1743-
* `[PLANNED — not yet rendered]` marker the describe opens with; when that
1744-
* leg lands, the marker leaves and the `_note` sentence is rewritten.
1738+
* RENDERED by the console. The block hands this key to its parent read
1739+
* (objectui: packages/plugin-detail/src/renderers/record-related-list.tsx,
1740+
* whose `RecordRelatedListRenderer` passes `schema.parentDataSource` to
1741+
* `ParentRecordBinding`; the read itself is
1742+
* objectui: packages/plugin-detail/src/renderers/recordBinding.tsx). Its
1743+
* `BoundRecordProvider` asks the page's own adapter, so under the caller's
1744+
* identity, for one row (`$top: 1`) by the binding's `sort`, and provides
1745+
* that row as the record context the list body reads. No row, or a read the
1746+
* server refuses as forbidden, draws the block's loud absence in its place
1747+
* (`NoParentRecord`), and no child rows are queried. Unbound, the list is
1748+
* left as it is, so the record page's record stays the parent. Measured at
1749+
* the console pin (`.objectui-sha` `4016370af3`), which carries the reader
1750+
* (objectui `d81e46043`, objectui#12407, the related-list leg of
1751+
* objectui#12294). The liveness record is the `liveness/page.json` `_note`
1752+
* (a component-level key carries no ledger row: the walk stops at the
1753+
* undrilled `page/regions` container); the describe carries no liveness
1754+
* marker.
17451755
*/
17461756
parentDataSource: ElementDataSourceSchema.optional().describe(
1747-
'[PLANNED — not yet rendered] The PARENT record this related list hangs off, bound by a query of its own: an object binding `{ object, view?, filter?, sort?, limit? }`, the shape `dataSource` takes. Read by the single-record rule: the FIRST row of the bound set by its `sort` is the parent (declare `limit: 1` to say one row is meant), and when no row answers, the block renders a loud absence, never an empty list, which would read as "this record has no related records". `relationshipValueField` is read on that row. When this key is absent, the record page\'s record context remains the parent. The node-level `dataSource` on this block keeps its published meaning, the CHILD rows the list draws (it fills `objectName`, `columns`, `filter`, `sort` and `limit`), and is never read as the parent.',
1757+
'The PARENT record this related list hangs off, bound by a query of its own: an object binding `{ object, view?, filter?, sort?, limit? }`, the shape `dataSource` takes. Read by the single-record rule: the FIRST row of the bound set by its `sort` is the parent (declare `limit: 1` to say one row is meant), and when no row answers, the block renders a loud absence, never an empty list, which would read as "this record has no related records". `relationshipValueField` is read on that row. When this key is absent, the record page\'s record context remains the parent. The node-level `dataSource` on this block keeps its published meaning, the CHILD rows the list draws (it fills `objectName`, `columns`, `filter`, `sort` and `limit`), and is never read as the parent.',
17481758
),
17491759
/**
17501760
* [#18639] The SAME union `listViews[].columns` declares (`view.zod.ts`) —

0 commit comments

Comments
 (0)