From 29502770a23a128fa4a0c3cb04ddac9d58230b52 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 12:09:08 +0000 Subject: [PATCH 1/2] =?UTF-8?q?docs(adr-0079):=20addendum=20=E2=80=94=20a?= =?UTF-8?q?=20formula=20title=20reads=20one=20hop=20through=20a=20referenc?= =?UTF-8?q?e,=20and=20is=20not=20sortable=20or=20searchable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records ruling 6245544505 (letter A on objectstack-ai/cloud#2925) as it shipped in #24274 and #24307: the read path's one-hop preload under the reader's permissions, the lint's two refusals, the hook-body title accessor's channel, and the declared-unsortable answer with its remedy. One appended section and one header pointer line; nothing above edited. Claude-Session: https://claude.ai/code/session_01RVjnmZ6Jv6NcLDwow6RpNi Co-authored-by: Claude --- docs/adr/0079-record-display-name.md | 77 ++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/docs/adr/0079-record-display-name.md b/docs/adr/0079-record-display-name.md index aeb222f08b5..e7f8e170070 100644 --- a/docs/adr/0079-record-display-name.md +++ b/docs/adr/0079-record-display-name.md @@ -7,6 +7,7 @@ - **Builds on**: [ADR-0078](./0078-no-silently-inert-metadata.md) (author-time completeness must not be cloud-only), [ADR-0061](./0061-record-search-architecture.md) (`$search` field resolution reads the display field) - **Precedent for**: [ADR-0085](./0085-object-semantic-roles-over-surface-hint-blocks.md) (`compactLayout` → `highlightFields` follows this ADR's alias mechanics "exactly"), [ADR-0098](./0098-pinyin-search-companion-column.md) (the search companion column is fed by this ADR's resolved display field) - **Consumers**: `@objectstack/spec` (`data/display-name.ts`, `data/object.zod.ts`, `data/search-fields.ts`, `ai/solution-blueprint.zod.ts`), `@objectstack/objectql` (registry materialization seam, `$search` expansion, search companion), `@objectstack/metadata-protocol` (REST `$searchFields` ingress gate), `@objectstack/lint` (`validate-record-title`, `data-model-rules` R9), `@objectstack/platform-objects` and the first-party plugins/services (41 explicit `nameField:` designations), the `objectstack-data` / `objectstack-formula` skills +- **Addenda**: [2026-10-18 (#24197) — a formula title reads one hop through a reference, and is not sortable or searchable](#addendum-2026-10-18-24197--a-formula-title-reads-one-hop-through-a-reference-and-is-not-sortable-or-searchable) --- @@ -335,3 +336,79 @@ the 2026-06-28 discussion. gate and a query engine disagreeing about what is searchable. - **Accept `primaryField` as an additional tolerated alias.** Rejected explicitly by the maintainer in #6326 (D9). + +--- + +## Addendum (2026-10-18, #24197) — a formula title reads one hop through a reference, and is not sortable or searchable + +Records ruling 6245544505 (letter A, direction (a), on `objectstack-ai/cloud#2925`), +shipped by #24274 (`d537a318b`) and #24307 (`1bf5f6e17c`). It states what those +two changes put on `main` and decides nothing more; no section above is edited. +The ruling, verbatim: + +> The formula read path preloads one hop of the referenced fields a formula +> names (page-batched, as validation rules do); lint admits one hop and refuses +> two; a title formula over lookups sorts and searches by its stored value or is +> declared unsortable — written as an ADR-0079 note. + +**The read path** (`find`, `findOne`, and the insert and update responses; +`packages/objectql/src/engine.ts`): + +1. A formula field reads ONE hop through a reference field (`lookup`, + `master_detail`, `user`, `tree`), as `record.FIELD.RELATED`. The read path + preloads the related fields the formula names once per page, with one batched + read per reference field and never one per row, under the reader's own + permissions. +2. A related record that is missing, or that the reader may not read, leaves the + formula `null` with a reported reason, never a computed fallback; the reason + does not say which of the two held. An empty reference is not a missing + record: the formula is evaluated over the empty value. +3. A second hop, and a reference read both through and as a plain value, are + refused by the lint (`traversal-multi-hop`, `traversal-bare-and-traversed`) + and read `null` with the same reason at run time. A related object's own + formula that reads a hop of its own reads `null` when it is itself read as a + hop. +4. Field-level security on the carrying object's own reference column does not + stop the hop: formulas are computed before the field mask is applied to the + result, as for any own column a formula reads. The control for a related name + a reader may not see is field-level security on the formula field itself. + +**Hook and action bodies** (`packages/runtime/src/sandbox/body-runner.ts`): + +5. `ctx.title()` evaluates the object's title formula over the in-flight record + state the hook fires on, and serves its one hop the way a read does: one + batched read per reference field, on the body's own `ctx.api` channel. That + is the channel's identity (SYSTEM under `runAs: 'system'`) and its open + transaction's connection. So a title over lookups names the related records + the in-flight foreign keys point at, and a related record that channel may + not read, a missing one, or a second hop leaves the title `null`, as the REST + read of the same record shows no title. +6. A channel that cannot read (a `runAs: 'user'` hook whose trigger resolved no + user) serves no hop: a title over the record's own columns resolves, and a + title over a reference answers `null`. +7. Field-level security on the title field itself is not applied to + `ctx.title()`. +8. `ctx.title('FIELD')` answers the related record's title exactly as its + `findOne` served it, that record's own one-hop title included. + +**Sort and search — the ruling's "declared unsortable" arm:** + +9. A formula value, a composite title over lookups included, is stored nowhere; + it is computed on each read. A sort by it is refused (`400 INVALID_SORT`). A + search cannot reach it: a request's `searchFields` naming it is refused + (`400 INVALID_FIELD`), a `searchableFields` entry naming it is dropped, and + the auto-default search set never includes it. An object that needs to sort + or search by its composite title must store the value in a field of its own, + written when its sources change. + +**What this qualifies above, without editing it:** + +- The first bullet of [Alternatives considered](#alternatives-considered) says a + text formula `nameField` is something the server *can* return and query. For + a title formula over lookups, the server returns it (items 1 and 5) but cannot + sort or search by it (item 9). +- D1 lists a text `formula` among the "real STORED" title fields. It is a + declared field of the object, but no column holds its value (item 9). +- The surfaces this addendum describes are the engine read path and the + hook-body title accessor. It makes no statement about any other consumer of + a record's title. From 6d6ecfe5a50dc6a3e79ab990b0e37e07d078307f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 12:58:34 +0000 Subject: [PATCH 2/2] docs(adr-0079): the addendum's ctx.title() group names hook bodies only ctx.title() is installed on the hook face alone (body-runner.ts buildTitleSurface; the action-face context carries no title), so the group heading no longer says "and action bodies". The provenance sentence now says what is on main after the two changes, since the sort and search refusals predate both. Claude-Session: https://claude.ai/code/session_01RVjnmZ6Jv6NcLDwow6RpNi Co-authored-by: Claude --- docs/adr/0079-record-display-name.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/adr/0079-record-display-name.md b/docs/adr/0079-record-display-name.md index e7f8e170070..08f161de65a 100644 --- a/docs/adr/0079-record-display-name.md +++ b/docs/adr/0079-record-display-name.md @@ -342,8 +342,8 @@ the 2026-06-28 discussion. ## Addendum (2026-10-18, #24197) — a formula title reads one hop through a reference, and is not sortable or searchable Records ruling 6245544505 (letter A, direction (a), on `objectstack-ai/cloud#2925`), -shipped by #24274 (`d537a318b`) and #24307 (`1bf5f6e17c`). It states what those -two changes put on `main` and decides nothing more; no section above is edited. +shipped by #24274 (`d537a318b`) and #24307 (`1bf5f6e17c`). It states what is on +`main` after those two changes and decides nothing more; no section above is edited. The ruling, verbatim: > The formula read path preloads one hop of the referenced fields a formula @@ -373,7 +373,7 @@ The ruling, verbatim: result, as for any own column a formula reads. The control for a related name a reader may not see is field-level security on the formula field itself. -**Hook and action bodies** (`packages/runtime/src/sandbox/body-runner.ts`): +**Hook bodies** (`packages/runtime/src/sandbox/body-runner.ts`, the hook face): 5. `ctx.title()` evaluates the object's title formula over the in-flight record state the hook fires on, and serves its one hop the way a read does: one