Repository navigation
Commit 4cc3ff9
Fixes #23134
Clause-②: yes (widening)
`record:related_list` declares its PARENT record with a binding key of
its own, `parentDataSource`, beside the child-row `dataSource`. This is
the spec half of ruling B on objectstack-ai/objectui#12296 (ruling
comment 6137226306). The objectui reader is the related-list leg of
objectstack-ai/objectui#12294 and is not in this PR.
## What changed
- `RecordRelatedListProps` (the
`ComponentPropsMap['record:related_list']` row,
`packages/spec/src/ui/component.zod.ts`) gains `parentDataSource:
ElementDataSourceSchema.optional()`. That is the element data-source
object binding `{ object, view?, filter?, sort?, limit? }`, referenced
from `page.zod.ts` and not copied.
- The describe states the three facts the card names. First, this key is
the parent record, read by the single-record rule: the first row by
`sort`; when no row answers, a loud absence, never an empty list.
Second, when the key is absent, the record page's record context remains
the parent. Third, `dataSource` on this block keeps its published
child-row meaning and is never read as the parent. The describe opens
with `[PLANNED — not yet rendered]`.
- `PageComponentSchema` and `ElementDataSourceSchema` are untouched.
`page.zod.ts` is not in the diff, and the node-level `dataSource` is
byte-for-byte unchanged.
- Generated: `authorable-surface/ui.json` gains one row
(`ui/RecordRelatedListProps:parentDataSource`).
`content/docs/references/ui/component.mdx` gains the key's row and its
nested shape. `authorable-surface.base.json` is not touched.
- Liveness: one PLANNED RECORD sentence is appended to the
`liveness/page.json` `_note`, with no row (see measurement 5).
- A `minor` changeset for `@objectstack/spec`.
- No `os validate` finding is added, as the ruling directs.
## Measurements (dispatch zone 2)
All objectui reads below were taken at objectui `main` `f0532d8a`,
fetched 2026-10-13. Spec reads were taken at this repo's base
`e540e7ffb` and re-checked after merging `origin/main` `d4aaeaf0e`.
1. **Where the key lives: the props door, as the PM assumed. The card's
`page.zod.ts` path does not hold.** The node-level `dataSource` is
`PageComponentSchema.dataSource` (`page.zod.ts:696`), one shape for
every component type, so a node key there could be authored on all of
them. The per-type door is `ComponentPropsMap`, whose
`record:related_list` row is `RecordRelatedListProps` in
`component.zod.ts` (`:10602` at base). objectui's `SchemaRenderer`
hoists `properties` onto the node, so a props key reaches the renderer
on `schema`. The key therefore lives on `RecordRelatedListProps`.
objectui reaches the child `dataSource` through
`RecordRelatedListRenderer`, which is `elementDataSourceBlock(...)`
wrapping `ElementDataSourceGate` with the
`RECORD_RELATED_LIST_DATA_SOURCE` mapping
(`plugin-detail/src/renderers/record-related-list.tsx:689-705`). The
gate maps the binding's `object` onto `objectName`.
2. **The name: `parentDataSource`.** Spellings read:
- the three sibling record blocks read their one binding as node-level
`dataSource`: `recordBindingBlock` reads
`isElementDataSourceConfig(schema.dataSource)`
(`plugin-detail/src/renderers/recordBinding.tsx:124`, `:147`), and wraps
`record-details.tsx:1016`, `record-highlights.tsx:199` and
`record-quick-actions.tsx:460`;
- objectui's strict face declares that binding as `dataSource:
RECORD_BLOCK_DATA_SOURCE` on those three arms, where
`RECORD_BLOCK_DATA_SOURCE` is
`stripImportedDefaults(SpecElementDataSourceSchema)`
(`types/src/zod/public-blocks.zod.ts:663`, `:688`, `:711`, `:963`);
- the related list's own parent today comes from `useRecordContext()`
(`record-related-list.tsx:196`, `:257-261`);
- spec: `PageComponentSchema.dataSource` is the only `dataSource` key.
The one other block with a parent half, `record:line_items`, spells that
half `parentObject` / `parentId` (`component.zod.ts:2615-2619` at base).
The proposed name takes the `dataSource` stem from the binding family
and the `parent` prefix from `record:line_items`.
Rejected alternatives:
- `parentRecord`: reads as a record value (a row or an id), not a
binding, and leaves the `dataSource` family.
- `recordDataSource`: both bindings on this block resolve to records, so
`record` does not say which one.
- `parentSource`: `source` already means other things in the spec, such
as the page variable writer `source`, `optionsSource`, and the `source`
alias of `object` on the data-source shape itself.
- `parentBinding`: names the concept, not the family's spelling. No spec
key ends in `Binding`.
- `parent`: too terse, and it collides in reading with the tree and
gantt `parentField`.
- `parentObject` + `parentId` (the `record:line_items` shape): a
different shape (a name plus an id in metadata), not the `dataSource`
shape the ruling fixes.
The PM and the contract reviewer judge the name.
3. **The shape: reused by reference, with no new field.** The key's
inner def is `ElementDataSourceSchema`'s own def. The new test pins
`_zod.def` identity against `ElementDataSourceSchema` and against the
object arm of `PageComponentSchema.dataSource`. The single-record rule
is stated on that shape without a new field: the first row by `sort`,
with `limit: 1` as the author's way to say one row is meant. The `api`
arm of `dataSource` is not admitted, because the card fixes the shape as
the object binding's (object, view, filter, sort, limit) and the
siblings' strict face takes that arm alone. No fork from the shape side.
4. **The fork reading: no renderer reads `dataSource` on this block as
the parent.** I searched every objectui source file that names
`record:related_list` at `f0532d8a`:
- the renderer: `dataSource` is the child binding through the gate, and
the parent is `useRecordContext()`;
- the tab-count probe in
`components/src/renderers/layout/containers.tsx:614-626`: it reads the
parent from `ctx.data.id`;
- the CLI's `record-block-binding-hint.ts`: it excludes
`record:related_list` from the one-record set and gives the reason "its
binding is the binding of its CHILD rows";
- hotcrm at `0f84c9c`: authors related lists on record pages with no
`dataSource`.
One objectstack reader needs naming, so it is stated here rather than
left silent. `os validate`'s `page-field-unknown` rule checks the
related list's parent-side `relationshipValueField` against the
component's walked object
(`packages/lint/src/validate-page-field-bindings.ts:617`). For a node
with a `dataSource`, that object is `dataSource.object`
(`packages/lint/src/page-walk.ts:136-137`). The rule's own comment says
the parent is the page object, so this is a defect against its stated
reading, not a parent meaning given to `dataSource`. Repro (scratch
probe against the lint source, page object `account`, child `contact`):
- child via `properties.objectName`: one finding, `columns[0]` checked
against `contact`, and nothing on `relationshipValueField` (correct);
- child via `dataSource: { object: 'contact' }` (the published child
shape): no `columns` finding, and a false `relationshipValueField` "not
a field on object contact".
I judged this not to be the ruling's fork and continued. It is reported
as a finding for the seat to file (Acceptance notes below), and the PM
is asked to confirm that judgement.
5. **Liveness: no ledger row. A `_note` record and the describe marker
carry it.** The ledger walk never reaches a component-level key:
`page/regions` is a recorded undrilled container, as the existing
`page.json` `_note` already says for the `ElementApiDataSourceSchema`
PLANNED record. So the key is recorded the way that precedent is: the
`[PLANNED — not yet rendered]` describe marker (which
`check-liveness.mts` `MARKER_RE.planned` reads), a PLANNED RECORD
sentence in the `liveness/page.json` `_note` naming the carrier, and the
authorable-surface row. `authorHint` has no row to attach to.
## Pins
New file:
`packages/spec/src/ui/component-record-related-list-parent-23134.test.ts`,
17 cases.
- **A:** a full binding parses and keeps what the shape keeps. A bare `{
object }` parses. There is no default. The key's def is the data-source
def, identical to the object arm of the node's `dataSource`.
- **B:** what the shape refuses is refused at `parentDataSource`, judged
by codes and paths: no `object`, `objectName` (unrecognized), a
record-form `filter`, the `api` arm, and a non-object value.
- **C:** no other `ComponentPropsMap` row declares the key. Every other
row refuses it by name (`unrecognized_keys`), except the two retired
types, which refuse every bag (derived, not listed). The related list
itself does not name it unknown.
- **D (CONTROL, the child binding):** the node-level child `dataSource`
parses unchanged with and without a parent binding beside it, and the
parent binding survives in `properties`.
- **E:** the describe opens with the planned marker.
## Verification
- Ablation (one-time, through `scripts/ablation-replace.mjs`, which
restores itself, plus an outer EXIT/INT/TERM trap). The key was renamed
`parentDataSource` to `zzAblatedParent` on disk: anchor x1 to x0, blob
`5fc72026` to `c9701e67`. The new file went red: **12 failed / 5 passed
of 17**. The 5 that stayed green are the controls that should: the base
document, the no-default case, the population check, the other rows
refusing the key, and the child binding parsing. The file was restored
with blob equal to HEAD `5fc72026` and `git diff HEAD` empty. No build
or dist leg was needed: the test imports `./component.zod` from `src`.
- Type-level reverse check against the REBUILT `dist/ui/index.d.ts` (a
scratch tsc project outside the repo). The green leg exits 0: the key is
accepted, and both `@ts-expect-error` lines are consumed, one for
`object: 1` and one for the `api` arm. The mutated leg exits 2 with two
errors: `parentDataSourceX` gets TS2561, and the `api` arm without its
expect-error gets TS2353 on `provider`.
- `os validate` door, probed against the lint source with the rebuilt
spec:
- `parentDataSource` on `record:details` is reported as
`component-props-unknown-key` (warning);
- a valid binding on `record:related_list` yields no finding;
- a malformed one yields `component-props-invalid` /
`component-props-unknown-key` at its own path.
- Final head `d4b3e7264` (after merging `origin/main` `d4aaeaf0e`
through `scripts/pm/os-regen-merge.sh`, then rebuilding spec), all
through the shared verify lock: `pnpm --filter @objectstack/spec
typecheck` exit 0 (`check:test-typecheck: OK`, no new debt), and `pnpm
--filter @objectstack/spec test` **662 files passed, 20125 tests passed,
1 todo**. `pnpm --filter @objectstack/spec test:repo` passed at the
pre-merge head `0e5ae9203` (57 files, 997 tests). It was not re-run
after the merge: the merge brought spec changes disjoint from this diff,
and CI runs it (declared narrowing). `check:generated` reports all 14
artifacts up to date on the merged tree.
- ESLint, narrowed and proven, at `d4b3e7264`. The changed files in
eslint's `**/*.{ts,…}` globs are the two `.ts` files; the other four
(`.md`, `.mdx`, `.json`) match no lint glob. `--format json` reports 2
files, 0 errors, 0 warnings. Invariance: `eslint.config.mjs` enables no
type-aware linting (no `parserOptions.project`), so this diff cannot
move a verdict on an untouched file.
- Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` on `d4b3e7264` derives 110 commands. All 110
were run with exit codes captured before any pipe. **109 exit 0.** Six
first exited 3 (PREREQUISITE NOT MET); after a targeted `turbo run
build` of lint, formula, client-react and objectql (35/35 cached) they
re-ran at 0. **`pnpm check:platform-checklist` exits 1, and it is not
this diff:** its 8 problems are absent symbols in
`packages/metadata-protocol/src/protocol.ts` and
`packages/services/service-storage/src/attachment-access-hooks.ts`, and
the gate's inputs are byte-identical to `origin/main` `d4aaeaf0e` (`git
diff origin/main HEAD -- docs/qa packages/metadata-protocol
packages/services scripts` is empty; the symbol
`anonymousFormIntakeOrgScopeRefusal` has zero hits on `origin/main`).
That is a main-red. `--ran` reconcile: 110 derived, 110 run, 0
NOT-MEASURED, 0 UNRUN, all 110 coded.
## Acceptance notes
- **Finding, class (a), for the seat to file:** the `page-field-unknown`
rule resolves the related list's parent-side `relationshipValueField`
against `dataSource.object` when the child is bound through
`dataSource`. It also skips the child-side `columns` / `sort` / `filter`
/ `relationshipField`, because it reads only `properties.objectName`.
Reach: `os validate` gives a false warning on the published
child-binding shape, and a missed check. Seam:
`spec:PageComponentSchema.dataSource (record:related_list child binding)
→ runtime:packages/lint/src/validate-page-field-bindings.ts:617
(checkRefs(split.parent, objectName)) via
packages/lint/src/page-walk.ts:136-137`. When that is fixed, the
parent-side refs have a declared object to resolve against:
`parentDataSource.object` when present, otherwise the page object. That
is a resolution fix to an existing rule, not a new finding. Dedupe
words: `page-field-unknown related_list dataSource
relationshipValueField`, `relatedListFieldRefs parent object`,
`walkPageComponents dataSource object related list`.
- For the objectui leg (carrier: objectstack-ai/objectui#12294, the
related-list leg): the `page:tabs` count probe reads the parent from
`ctx.data.id` (`containers.tsx:614-626`). A related list bound by
`parentDataSource` on an `app` page gets no auto-derived tab count
unless that leg feeds it. Noted, not filed.
- `RecordRelatedListProps` still requires `objectName`, even when the
child `dataSource` supplies it (the objectui renderer type says so in
its own docblock). That predates this card and is unchanged here.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01BRQ9Mp1yH7R1az6MaUM18H)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 920bc12 commit 4cc3ff9
6 files changed
Lines changed: 316 additions & 2 deletions
File tree
- .changeset
- content/docs/references/ui
- packages/spec
- authorable-surface
- liveness
- src/ui
| 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 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1938 | 1938 | | |
1939 | 1939 | | |
1940 | 1940 | | |
| 1941 | + | |
1941 | 1942 | | |
1942 | 1943 | | |
1943 | 1944 | | |
| |||
1951 | 1952 | | |
1952 | 1953 | | |
1953 | 1954 | | |
| 1955 | + | |
| 1956 | + | |
| 1957 | + | |
| 1958 | + | |
| 1959 | + | |
| 1960 | + | |
| 1961 | + | |
| 1962 | + | |
| 1963 | + | |
| 1964 | + | |
1954 | 1965 | | |
1955 | 1966 | | |
1956 | 1967 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1231 | 1231 | | |
1232 | 1232 | | |
1233 | 1233 | | |
| 1234 | + | |
1234 | 1235 | | |
1235 | 1236 | | |
1236 | 1237 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| |||
0 commit comments