From ed082e38383eeaf48b2a55807325d789d06780c8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 21:29:10 +0000 Subject: [PATCH 1/5] spec(data): name the served name-only reference value and the display name it carries The referenceNameVisible describe now names the ADR-0079 display name the engine resolves (nameField, else displayNameField, else the derived title field) and every case objectql serves the Record # floor in. The served { id, name, readRefused: true } gets a closed ReferenceNameOnlyValueSchema, joined into the expanded lookup value union, and the engine's ReferenceNameOnlyValue type now derives from it. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01JQwxbahxR9HAJnTs7UKhmn --- .../src/engine-reference-name-visible.ts | 16 ++---- .../spec/scripts/strictness-ledger.test.ts | 5 +- packages/spec/src/data/field-value.test.ts | 22 ++++++++ packages/spec/src/data/field-value.zod.ts | 50 ++++++++++++++++++- packages/spec/src/data/field.zod.ts | 39 ++++++++++----- .../src/type-alias-convention.pin.test.ts | 14 ++++-- 6 files changed, 115 insertions(+), 31 deletions(-) diff --git a/packages/objectql/src/engine-reference-name-visible.ts b/packages/objectql/src/engine-reference-name-visible.ts index 0e830d73ba4..7a28ebe2444 100644 --- a/packages/objectql/src/engine-reference-name-visible.ts +++ b/packages/objectql/src/engine-reference-name-visible.ts @@ -65,23 +65,15 @@ */ import { isTitleEligible, resolveDisplayField, resolveRecordDisplayName } from '@objectstack/spec/data'; +// The value a `referenceNameVisible` reference is served as when the reader +// was refused its target. Derived from the spec's `ReferenceNameOnlyValueSchema`, +// which owns the shape; the engine never declares its own copy. +import type { ReferenceNameOnlyValue } from '@objectstack/spec/data'; import type { ExecutionContext } from '@objectstack/spec/kernel'; import { withoutOperationPrivateKeys } from '@objectstack/core'; import type { ObjectQL } from './engine.js'; import { isReadRefusal } from './engine-file-reference-hydration.js'; -/** - * The value a `referenceNameVisible` reference is served as when the reader - * was refused its target: the id the referencing row holds, the target's - * resolved display name, and the refusal said in-band. Read-only by - * construction: the stored form of a reference is the bare id alone. - */ -export interface ReferenceNameOnlyValue { - id: string; - name: string; - readRefused: true; -} - /** * The `ObjectQL` members {@link serveReferenceNames} reads, every one of them * and no other, each typed AS the class member. See the module header of diff --git a/packages/spec/scripts/strictness-ledger.test.ts b/packages/spec/scripts/strictness-ledger.test.ts index bb407778595..b4e2c0b8a6e 100644 --- a/packages/spec/scripts/strictness-ledger.test.ts +++ b/packages/spec/scripts/strictness-ledger.test.ts @@ -130,9 +130,10 @@ describe('site counting reads the AST, not the source text', () => { // from `field.zod.ts`. As with the chart count above, the number is // incidental — the assertion that carries the meaning is the idiom read. // 3 → 4 at #22593, which ADDED `FileRefusedValueSchema` (a strictObject - // site) — the count is still incidental. + // site) — the count is still incidental. 4 → 5 at #29579, which ADDED + // `ReferenceNameOnlyValueSchema` (a strictObject site). const fv = analyzeSites(at('data/field-value.zod.ts')); - expect(fv).toHaveLength(4); + expect(fv).toHaveLength(5); expect(fv.find((s) => s.name === 'FileValueSchema')?.idiom).toBe('z.looseObject'); }); }); diff --git a/packages/spec/src/data/field-value.test.ts b/packages/spec/src/data/field-value.test.ts index fa73df1dadb..db3126475fc 100644 --- a/packages/spec/src/data/field-value.test.ts +++ b/packages/spec/src/data/field-value.test.ts @@ -32,6 +32,7 @@ import { referenceCarrierOf, LocationValueSchema, FileRefusedValueSchema, + ReferenceNameOnlyValueSchema, } from './field-value.zod'; import { findClosestMatches } from '../shared/suggestions.zod'; @@ -313,6 +314,27 @@ describe('valueSchemaFor — stored form (field-zoo reality)', () => { ok({ type: 'lookup' }, 'acc_1', 'expanded'); // unresolvable ids stay ids }); + it('a reader refused the target of a referenceNameVisible field reads the NAMED name-only value', () => { + // The served `{ id, name, readRefused: true }`, per element on a multi-value field. + for (const type of ['lookup', 'user']) { + ok({ type }, { id: 'acc_1', name: 'Acme', readRefused: true }, 'expanded'); + } + ok({ type: 'lookup', multiple: true }, [{ id: 'acc_1', name: 'Record #acc_1', readRefused: true }, 'acc_2'], 'expanded'); + // Closed on its own: the marker is a literal `true`, the id is a record id, + // the name is present, and nothing else of the target rides on it. + const parse = (v: unknown) => ReferenceNameOnlyValueSchema.safeParse(v); + expect(parse({ id: 'acc_1', name: 'Acme', readRefused: true }).success).toBe(true); + expect(parse({ id: 'acc_1', name: 'Acme', readRefused: false }).success).toBe(false); + expect(parse({ id: 'acc_1', readRefused: true }).success).toBe(false); + expect(parse({ id: 'acc_1', name: '', readRefused: true }).success).toBe(false); + expect(parse({ id: '{"id":"acc_1"}', name: 'Acme', readRefused: true }).success).toBe(false); + const widened = parse({ id: 'acc_1', name: 'Acme', readRefused: true, owner: 'usr_1' }); + expect(widened.success).toBe(false); + expect(widened.error?.issues.map((i) => i.code)).toContain('unrecognized_keys'); + // Read-only: the STORED form stays the bare id alone. + bad({ type: 'lookup' }, { id: 'acc_1', name: 'Acme', readRefused: true }); + }); + it('a SERIALIZED embedded record is not an id, in either form', () => { // The shape the ADR-0104 D1 scan's own header names — "a `lookup` holding // an expanded record object" — as it actually reaches a SQL deployment: as diff --git a/packages/spec/src/data/field-value.zod.ts b/packages/spec/src/data/field-value.zod.ts index b3535d1a426..e91212ba468 100644 --- a/packages/spec/src/data/field-value.zod.ts +++ b/packages/spec/src/data/field-value.zod.ts @@ -685,6 +685,51 @@ export const ReferenceIdValueSchema = lazySchema(() => ); export type ReferenceIdValue = z.input; +/** + * Reference value served to a reader REFUSED the record it points at, on a + * `lookup` or `user` field that declares `referenceNameVisible` — an + * `expanded` read form, beside the still-bare id and the expanded record. + * + * `$expand` on such a field answers a target the reader may not read (the + * object's read gate or its row scope refuses it) with the id the referencing + * record holds, the target's ADR-0079 display name, and `readRefused: true`, + * per element on a `multiple: true` field. `name` is what + * `resolveRecordDisplayName` answers for the target (its `nameField`, else its + * `displayNameField`, else the derived title field), or the `Record #` floor + * with the id where a field-level rule on that field holds — the field's own + * `referenceNameVisible` description lists those cases. The marker is the + * refusal said in-band, the `metadataRefused` precedent of + * {@link FileRefusedValueSchema}: a renderer tells this value from a target + * the reader expanded with a narrow projection, shows the name as text and + * links nothing. + * + * Read-only by construction: the STORED form is {@link ReferenceIdValueSchema} + * alone, so this object is never a value a record write accepts. Closed, so + * nothing else of the target can ride on it. The expanded union keeps its open + * record member, so naming this shape accepts nothing that was refused before. + */ +export const ReferenceNameOnlyValueSchema = lazySchema(() => strictObject( + { + surface: 'this name-only reference value', + // Customer-facing text: no issue ids (the anchors are in the JSDoc above). + history: 'A name-only reference value is served by the platform and never written: it carries the ' + + 'id the record holds, the referenced record\'s display name and the refusal marker, nothing else.', + }, + { + id: ReferenceIdValueSchema.describe('The id of the referenced record, as the referencing record holds it'), + name: z.string().min(1).describe( + 'The referenced record\'s display name (its object\'s `nameField`, else `displayNameField`, else the ' + + 'derived title field), or the `Record #` floor with the id where a field-level rule on that field holds', + ), + readRefused: z.literal(true).describe( + 'The reader was refused the referenced record (the object\'s read gate or its row scope): only its ' + + 'id and display name are served, and the name links nothing. Distinct from an expanded record and ' + + 'from a bare id.', + ), + }, +)); +export type ReferenceNameOnlyValue = z.input; + function optionCodes(def: ValueShapeFieldDef): string[] { if (!Array.isArray(def.options)) return []; return def.options.map((o) => (typeof o === 'object' && o !== null ? String(o.value) : String(o))); @@ -722,8 +767,11 @@ export function valueSchemaFor(def: ValueShapeFieldDef, form: ValueForm = 'store // Expanded form: `$expand` replaces the id in place with the related // record object (objectql engine `expandRelatedRecords`). The record's // own shape is that object's contract, not this field's — hence open. + // A reader refused the target on a `referenceNameVisible` field is + // served the named name-only value instead; the open member already + // accepted it, so naming it changes no verdict. return form === 'expanded' - ? z.union([ReferenceIdValueSchema, z.record(z.string(), z.unknown())]) + ? z.union([ReferenceIdValueSchema, ReferenceNameOnlyValueSchema, z.record(z.string(), z.unknown())]) : ReferenceIdValueSchema; } if (FILE_REFERENCE_TYPES.has(t)) { diff --git a/packages/spec/src/data/field.zod.ts b/packages/spec/src/data/field.zod.ts index ccb34347a02..291b9f5ec09 100644 --- a/packages/spec/src/data/field.zod.ts +++ b/packages/spec/src/data/field.zod.ts @@ -1733,19 +1733,28 @@ export const FieldSchema = lazySchema(() => { * Declared per referencing field by the application author, never on the * target object: the application decides whose names its own pages reveal. * `true` declares that a principal who can read the record carrying this - * field MAY see, for the record this field points at, its display name (the - * target object's `nameField` value, ADR-0079) and its id, even when that - * principal cannot read the target record. Nothing else of the target: + * field MAY see, for the record this field points at, its display name and + * its id, even when that principal cannot read the target record. The name + * is the ADR-0079 D4 display name `resolveRecordDisplayName` answers: the + * target object's `nameField`, else its `displayNameField`, else the derived + * title field. The served value is `ReferenceNameOnlyValueSchema` + * (`field-value.zod.ts`). Nothing else of the target: * `$expand` on this field MUST return the same name and id only, the field's * own `displayField` MUST NOT widen what is shown, and the renderer shows the * name as text with no link to the target record. A principal who can read * the target is served exactly what it is served without the key. * * The key lifts the RECORD-level refusal only. A field-level rule on the - * name field itself still holds: when the reader's field-level security - * keeps that field from it, the field declares a `maskingRule`, or it is a - * formula reading through another reference, the name served is the - * ADR-0079 floor (`Record #` and the id). + * name field itself still holds, and the engine reads that field only where + * it computes nothing the security layer owns. The name served is the + * ADR-0079 floor (`Record #` and the id) in five cases (objectql's + * `servableNameField`): the reader's field-level security keeps + * that field from it; the field declares a `maskingRule`; it is a formula + * reading through another reference; an explicit `nameField` (or + * `displayNameField`) points at a column that is neither title-typed nor + * `autonumber`; or no field-level answer can be had for the reader. A + * target with no resolvable or no non-empty name reads as the same floor, + * as it does for every reader. * * Absent or `false` keeps the restricted-reference posture: a lookup whose * target the reader cannot read shows no name. Valid on `lookup` and on @@ -1755,14 +1764,18 @@ export const FieldSchema = lazySchema(() => { referenceNameVisible: z.boolean().optional().describe( 'Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can ' + 'read the record carrying this field may see the referenced record\'s display name (the target ' - + 'object\'s `nameField` value) and id even when that principal cannot read the referenced record. ' - + 'Served on `$expand`: a target the reader is refused (the object\'s read gate or its row scope) ' - + 'answers `{ id, name, readRefused: true }`, per element on a multi-value field; an unexpanded read ' + + 'object\'s `nameField`, else its `displayNameField`, else the derived title field) and id even when ' + + 'that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused ' + + '(the object\'s read gate or its row scope) answers `{ id, name, readRefused: true }` ' + + '(`ReferenceNameOnlyValueSchema`), per element on a multi-value field; an unexpanded read ' + 'answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field ' + 'returns the same name and id only, the field\'s `displayField` does not widen it, and the name ' - + 'renders as text with no link to the target. A field-level rule on the name field still holds: ' - + 'when the reader may not read that field, it is masked, or it is a formula reading through another ' - + 'reference, the name is the `Record #` floor with the id. A principal who can read the target is ' + + 'renders as text with no link to the target. A field-level rule on the name field still holds, and ' + + 'the name field is read only where nothing the security layer owns must be computed: the name is the ' + + '`Record #` floor with the id when the reader may not read that field, it declares a `maskingRule`, ' + + 'it is a formula reading through another reference, an explicit `nameField` points at a column that ' + + 'is neither title-typed nor autonumber, or no field-level answer can be had for the reader. A target ' + + 'with no resolvable or no non-empty name reads as the same floor. A principal who can read the target is ' + 'served exactly what it is served without the key. Absent or false keeps the restricted-reference ' + 'posture for a target the reader cannot read. Declared on the referencing field only; there is no ' + 'target-object switch. Refused on any other type.' diff --git a/packages/spec/src/type-alias-convention.pin.test.ts b/packages/spec/src/type-alias-convention.pin.test.ts index d23cfeab05b..44a19e3a384 100644 --- a/packages/spec/src/type-alias-convention.pin.test.ts +++ b/packages/spec/src/type-alias-convention.pin.test.ts @@ -289,7 +289,7 @@ import type * as M197 from './api/push-delivery.zod.js'; import type * as M198 from './system/push-device.zod.js'; // --------------------------------------------------------------------------- -// 818 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared. +// 819 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared. // // That number is machine-checked, not hand-kept. The runtime companion at the // bottom of this file recomputes the pin count from the source and asserts that @@ -885,6 +885,7 @@ export type Iso_data_fieldValue__FileValueSchema = Assert, z.infer< typeof M178.InstantValueSchema > >>; export type Iso_data_fieldValue__LocationValueSchema = Assert, z.infer< typeof M178.LocationValueSchema > >>; export type Iso_data_fieldValue__ReferenceIdValueSchema = Assert, z.infer< typeof M178.ReferenceIdValueSchema > >>; +export type Iso_data_fieldValue__ReferenceNameOnlyValueSchema = Assert, z.infer< typeof M178.ReferenceNameOnlyValueSchema > >>; // data/field.zod.ts // #8993 partial masking (`FieldMaskingKeepSchema`): keepHead/keepTail are plain @@ -1762,7 +1763,7 @@ describe('ADR-0122 type-alias convention', () => { // this title and the section header above the pin list — are now asserted // against the recomputed count below, so neither can go stale without a red // test naming it. - it('still declares all 818 isomorphic pins', () => { + it('still declares all 819 isomorphic pins', () => { // The truth of each pin is proved by tsc, not here — an `Assert>` // that stops holding is a compile error with the alias named. What tsc // cannot notice is a pin that was DELETED: removing the assertion removes @@ -2752,7 +2753,14 @@ describe('ADR-0122 type-alias convention', () => { // required and optional strings, enums, a boolean and ISO instants, no // default and no transform (Iso_system_pushDevice__UserDeviceSchema). // +1 added. - expect(pins).toHaveLength(818); + // 818 -> 819 is #29579: the new exported `ReferenceNameOnlyValueSchema` + // (the expanded read form a reader refused the target of a + // `referenceNameVisible` field is served) is a closed + // `{ id, name, readRefused: true }` with no default and no transform in + // its tree, so it is pinned here + // (Iso_data_fieldValue__ReferenceNameOnlyValueSchema) rather than given a + // `ReferenceNameOnlyValueParsed` synonym. +1 added. + expect(pins).toHaveLength(819); // The count is stated in PROSE twice as well — this case's title and the // section header above the pin list — and until commit c6b05c76a nothing read either From 0d181d4145a22f1170df2328feb87b77922f8d23 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 21:36:27 +0000 Subject: [PATCH 2/5] spec(data): regenerate references and ledgers for ReferenceNameOnlyValueSchema; changeset Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01JQwxbahxR9HAJnTs7UKhmn --- .changeset/29579-reference-name-only-value.md | 17 +++++++++++++++++ content/docs/references/data/field-value.mdx | 19 ++++++++++++++++--- content/docs/references/data/field.mdx | 2 +- content/docs/references/data/object.mdx | 4 ++-- content/docs/references/index.mdx | 10 +++++----- content/docs/references/system/migration.mdx | 4 ++-- .../data.md | 10 +++++----- packages/spec/api-surface/data.json | 2 ++ packages/spec/authorable-surface/data.json | 3 +++ packages/spec/declaration-map/data.json | 2 ++ .../spec/dropped-refinements.baseline.json | 5 +++++ packages/spec/export-origins/data.json | 2 ++ packages/spec/json-schema.manifest/data.json | 1 + 13 files changed, 63 insertions(+), 18 deletions(-) create mode 100644 .changeset/29579-reference-name-only-value.md diff --git a/.changeset/29579-reference-name-only-value.md b/.changeset/29579-reference-name-only-value.md new file mode 100644 index 00000000000..9baa611b6b0 --- /dev/null +++ b/.changeset/29579-reference-name-only-value.md @@ -0,0 +1,17 @@ +--- +'@objectstack/spec': minor +--- + +spec(data): the name-only reference value a refused reader is served has a named schema, and `referenceNameVisible` says which name it carries + +Clause-②: no +Metadata-protocol: added + +A `lookup` or `user` field that declares `referenceNameVisible: true` answers `$expand` with `{ id, name, readRefused: true }` for a target the reader may not read. Until now that value had no name in the spec, and the field's description named the wrong source for `name` and only some of the cases where the name is withheld. + +- **New export.** `ReferenceNameOnlyValueSchema` / `ReferenceNameOnlyValue` from `@objectstack/spec/data`: a closed object of the record id the referencing row holds, a non-empty `name`, and `readRefused: true`. It is a read form only. The stored form of a reference stays the bare id, so a record write never accepts it. +- **Expanded lookup value.** `valueSchemaFor(def, 'expanded')` for the reference types lists the new schema beside the bare id and the open record object. The open record member already accepted this value, so no value parses differently than before. +- **Which name.** The `referenceNameVisible` description now says the name is the referenced object's display name as `resolveRecordDisplayName` resolves it: its `nameField`, else its `displayNameField`, else the derived title field. Before, it named `nameField` alone. +- **When the name is withheld.** The description now lists every case where the `Record #` floor with the id is served instead of the name: the reader's field-level security keeps the name field from it; the field declares a `maskingRule`; it is a formula reading through another reference; an explicit `nameField` points at a column that is neither title-typed nor autonumber; or no field-level answer can be had for the reader. A target with no resolvable or no non-empty name reads as the same floor. + +**For consumers.** Code that types a lookup's expanded value can import `ReferenceNameOnlyValue` and check `readRefused === true` to tell a refused target from an expanded record. Show the name as text and link nothing. diff --git a/content/docs/references/data/field-value.mdx b/content/docs/references/data/field-value.mdx index cec50d67f33..176be86b7a5 100644 --- a/content/docs/references/data/field-value.mdx +++ b/content/docs/references/data/field-value.mdx @@ -1,7 +1,7 @@ --- title: Field Value schema — Data Protocol reference navTitle: Field Value -description: "Field runtime VALUE-shape contract. Reference for Address, AddressValue, CalendarDateValue, ClockTimeValue and 7 more: every property with its type and default." +description: "Field runtime VALUE-shape contract. Reference for Address, AddressValue, CalendarDateValue, ClockTimeValue and 8 more: every property with its type and default." --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -41,8 +41,8 @@ this contract has (ADR-0104 performance budget). ## TypeScript Usage ```typescript -import { AddressSchema, AddressValueSchema, CalendarDateValueSchema, ClockTimeValueSchema, FileLikeValueSchema, FileReferenceIdValueSchema, FileRefusedValueSchema, FileValueSchema, InstantValueSchema, LocationValueSchema, ReferenceIdValueSchema } from '@objectstack/spec/data'; -import type { Address, AddressValue, CalendarDateValue, ClockTimeValue, FileLikeValue, FileReferenceIdValue, FileRefusedValue, FileValue, InstantValue, LocationValue, ReferenceIdValue } from '@objectstack/spec/data'; +import { AddressSchema, AddressValueSchema, CalendarDateValueSchema, ClockTimeValueSchema, FileLikeValueSchema, FileReferenceIdValueSchema, FileRefusedValueSchema, FileValueSchema, InstantValueSchema, LocationValueSchema, ReferenceIdValueSchema, ReferenceNameOnlyValueSchema } from '@objectstack/spec/data'; +import type { Address, AddressValue, CalendarDateValue, ClockTimeValue, FileLikeValue, FileReferenceIdValue, FileRefusedValue, FileValue, InstantValue, LocationValue, ReferenceIdValue, ReferenceNameOnlyValue } from '@objectstack/spec/data'; // Validate data const result = AddressSchema.parse(data); @@ -193,3 +193,16 @@ Type: `string` --- +## ReferenceNameOnlyValue + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | The id of the referenced record, as the referencing record holds it | +| **name** | `string` | ✅ | The referenced record's display name (its object's `nameField`, else `displayNameField`, else the derived title field), or the `Record #` floor with the id where a field-level rule on that field holds | +| **readRefused** | `true` | ✅ | The reader was refused the referenced record (the object's read gate or its row scope): only its id and display name are served, and the name links nothing. Distinct from an expanded record and from a bare id. | + + +--- + diff --git a/content/docs/references/data/field.mdx b/content/docs/references/data/field.mdx index 5662c123c50..82ebce01b7c 100644 --- a/content/docs/references/data/field.mdx +++ b/content/docs/references/data/field.mdx @@ -78,7 +78,7 @@ const result = CurrencyConfigSchema.parse(data); | **reference** | `string` | optional | Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects. Never a stored-metadata table (`sys_metadata`, `sys_metadata_history`): their rows are served only through the metadata API, so the engine never hydrates a reference into one, and `os validate`, `os build` and the object save door refuse it. On a `tree` field it is optional and, if given, must be the declaring object's own name — the object schema refuses any other target, and writes the own name when it is left out. | | **referenceVia** | `string` | optional | Declares this text field as the id half of a polymorphic pointer pair (ADR-0052 §5 ActivityPointer): the value is a record id of the object named by the SIBLING FIELD this key names — e.g. `record_id` with `referenceVia: 'object_name'`. The sibling must be a declared field on the same object holding an object machine name. Text fields only; mutually exclusive with `reference` (a static and a per-record target contradict). Enforced today at seed load: the value resolves as a natural key against the object the sibling column names, and an unresolvable pointer is refused loudly instead of stored verbatim. Adds no referential integrity or $expand behavior. | | **referenceDeletedWhen** | `{ field: string; value: string }` | optional | Marks the record a `referenceVia` pointer names as PLATFORM-DELETED on rows where a sibling `select` field holds one declared option — e.g. `record_id` with `{ field: 'cancel_reason', value: 'record_deleted' }`. A renderer then draws a tombstone from that option's own label, reads nothing and links nothing. Valid only beside `referenceVia`; `field` must be a single-value `select` on the same object declaring inline `options`, and `value` one of its option values — any other shape is refused at parse. | -| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField` value) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }`, per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds: when the reader may not read that field, it is masked, or it is a formula reading through another reference, the name is the `Record #` floor with the id. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | +| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField`, else its `displayNameField`, else the derived title field) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }` (`ReferenceNameOnlyValueSchema`), per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds, and the name field is read only where nothing the security layer owns must be computed: the name is the `Record #` floor with the id when the reader may not read that field, it declares a `maskingRule`, it is a formula reading through another reference, an explicit `nameField` points at a column that is neither title-typed nor autonumber, or no field-level answer can be had for the reader. A target with no resolvable or no non-empty name reads as the same floor. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | | **mailAssociation** | `boolean` | optional | Inbound mail association (`email` fields only): TRUE marks this field as identifying a correspondent. When a mailbox service ingests a message, it associates the message with every record of this object whose value in this field equals one of the message's participant addresses (From, Sender, Reply-To, To and Cc, plus those of a forwarded message it carries; ASCII case-insensitive; the receiving mailbox's own addresses excluded; only within the mailbox's organization). A message no record matches waits in the mailbox's unassociated pool. Refused on any other type. Takes effect only where a mailbox service is composed. | | **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted | | **inlineEdit** | `boolean \| Enum<'grid' \| 'form'>` | optional | Edit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form. | diff --git a/content/docs/references/data/object.mdx b/content/docs/references/data/object.mdx index ba9b37ba982..815f698f04b 100644 --- a/content/docs/references/data/object.mdx +++ b/content/docs/references/data/object.mdx @@ -247,7 +247,7 @@ const result = ApiMethod.parse(data); | **reference** | `string` | optional | Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects. Never a stored-metadata table (`sys_metadata`, `sys_metadata_history`): their rows are served only through the metadata API, so the engine never hydrates a reference into one, and `os validate`, `os build` and the object save door refuse it. On a `tree` field it is optional and, if given, must be the declaring object's own name — the object schema refuses any other target, and writes the own name when it is left out. | | **referenceVia** | `string` | optional | Declares this text field as the id half of a polymorphic pointer pair (ADR-0052 §5 ActivityPointer): the value is a record id of the object named by the SIBLING FIELD this key names — e.g. `record_id` with `referenceVia: 'object_name'`. The sibling must be a declared field on the same object holding an object machine name. Text fields only; mutually exclusive with `reference` (a static and a per-record target contradict). Enforced today at seed load: the value resolves as a natural key against the object the sibling column names, and an unresolvable pointer is refused loudly instead of stored verbatim. Adds no referential integrity or $expand behavior. | | **referenceDeletedWhen** | `{ field: string; value: string }` | optional | Marks the record a `referenceVia` pointer names as PLATFORM-DELETED on rows where a sibling `select` field holds one declared option — e.g. `record_id` with `{ field: 'cancel_reason', value: 'record_deleted' }`. A renderer then draws a tombstone from that option's own label, reads nothing and links nothing. Valid only beside `referenceVia`; `field` must be a single-value `select` on the same object declaring inline `options`, and `value` one of its option values — any other shape is refused at parse. | -| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField` value) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }`, per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds: when the reader may not read that field, it is masked, or it is a formula reading through another reference, the name is the `Record #` floor with the id. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | +| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField`, else its `displayNameField`, else the derived title field) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }` (`ReferenceNameOnlyValueSchema`), per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds, and the name field is read only where nothing the security layer owns must be computed: the name is the `Record #` floor with the id when the reader may not read that field, it declares a `maskingRule`, it is a formula reading through another reference, an explicit `nameField` points at a column that is neither title-typed nor autonumber, or no field-level answer can be had for the reader. A target with no resolvable or no non-empty name reads as the same floor. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | | **mailAssociation** | `boolean` | optional | Inbound mail association (`email` fields only): TRUE marks this field as identifying a correspondent. When a mailbox service ingests a message, it associates the message with every record of this object whose value in this field equals one of the message's participant addresses (From, Sender, Reply-To, To and Cc, plus those of a forwarded message it carries; ASCII case-insensitive; the receiving mailbox's own addresses excluded; only within the mailbox's organization). A message no record matches waits in the mailbox's unassociated pool. Refused on any other type. Takes effect only where a mailbox service is composed. | | **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted | | **inlineEdit** | `boolean \| Enum<'grid' \| 'form'>` | optional | Edit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form. | @@ -618,7 +618,7 @@ const result = ApiMethod.parse(data); | **reference** | `string` | optional | Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects. Never a stored-metadata table (`sys_metadata`, `sys_metadata_history`): their rows are served only through the metadata API, so the engine never hydrates a reference into one, and `os validate`, `os build` and the object save door refuse it. On a `tree` field it is optional and, if given, must be the declaring object's own name — the object schema refuses any other target, and writes the own name when it is left out. | | **referenceVia** | `string` | optional | Declares this text field as the id half of a polymorphic pointer pair (ADR-0052 §5 ActivityPointer): the value is a record id of the object named by the SIBLING FIELD this key names — e.g. `record_id` with `referenceVia: 'object_name'`. The sibling must be a declared field on the same object holding an object machine name. Text fields only; mutually exclusive with `reference` (a static and a per-record target contradict). Enforced today at seed load: the value resolves as a natural key against the object the sibling column names, and an unresolvable pointer is refused loudly instead of stored verbatim. Adds no referential integrity or $expand behavior. | | **referenceDeletedWhen** | `{ field: string; value: string }` | optional | Marks the record a `referenceVia` pointer names as PLATFORM-DELETED on rows where a sibling `select` field holds one declared option — e.g. `record_id` with `{ field: 'cancel_reason', value: 'record_deleted' }`. A renderer then draws a tombstone from that option's own label, reads nothing and links nothing. Valid only beside `referenceVia`; `field` must be a single-value `select` on the same object declaring inline `options`, and `value` one of its option values — any other shape is refused at parse. | -| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField` value) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }`, per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds: when the reader may not read that field, it is masked, or it is a formula reading through another reference, the name is the `Record #` floor with the id. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | +| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField`, else its `displayNameField`, else the derived title field) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }` (`ReferenceNameOnlyValueSchema`), per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds, and the name field is read only where nothing the security layer owns must be computed: the name is the `Record #` floor with the id when the reader may not read that field, it declares a `maskingRule`, it is a formula reading through another reference, an explicit `nameField` points at a column that is neither title-typed nor autonumber, or no field-level answer can be had for the reader. A target with no resolvable or no non-empty name reads as the same floor. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | | **mailAssociation** | `boolean` | optional | Inbound mail association (`email` fields only): TRUE marks this field as identifying a correspondent. When a mailbox service ingests a message, it associates the message with every record of this object whose value in this field equals one of the message's participant addresses (From, Sender, Reply-To, To and Cc, plus those of a forwarded message it carries; ASCII case-insensitive; the receiving mailbox's own addresses excluded; only within the mailbox's organization). A message no record matches waits in the mailbox's unassociated pool. Refused on any other type. Takes effect only where a mailbox service is composed. | | **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted | | **inlineEdit** | `boolean \| Enum<'grid' \| 'form'>` | optional | Edit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form. | diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index f2196d8f9e6..22671ce949d 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,7 +1,7 @@ --- title: Protocol reference — every schema by module navTitle: Protocol Reference -description: Every schema published by @objectstack/spec — 1604 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1605 schemas across 14 protocol modules --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -23,7 +23,7 @@ counts are sums of the rows they head. Regenerate with | [AI Protocol](/docs/references/ai) | 12 | 68 | Agents, tools, skills, RAG and knowledge sources, model registry, conversations. | | [API Protocol](/docs/references/api) | 35 | 474 | REST contracts, endpoints, routing, realtime, batch, discovery. | | [Automation Protocol](/docs/references/automation) | 15 | 78 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. | -| [Data Protocol](/docs/references/data) | 31 | 174 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. | +| [Data Protocol](/docs/references/data) | 31 | 175 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. | | [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. | | [Integration Protocol](/docs/references/integration) | 1 | 23 | The single connector protocol (ADR-0097) — catalog descriptors and provider-bound instances. | | [Kernel Protocol](/docs/references/kernel) | 30 | 152 | Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. | @@ -34,7 +34,7 @@ counts are sums of the rows they head. Regenerate with | [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. | | [System Protocol](/docs/references/system) | 39 | 292 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | | [UI Protocol](/docs/references/ui) | 16 | 175 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **208** | **1604** | 14 protocol modules | +| **Total** | **208** | **1605** | 14 protocol modules | --- @@ -135,7 +135,7 @@ Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execu ## Data Protocol -**Source:** `packages/spec/src/data/` · **Import:** `@objectstack/spec/data` · **31 pages, 174 schemas** +**Source:** `packages/spec/src/data/` · **Import:** `@objectstack/spec/data` · **31 pages, 175 schemas** Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. @@ -160,7 +160,7 @@ Objects, fields, queries, filters, datasources and drivers — the ObjectQL laye | [`external-catalog.zod.ts`](/docs/references/data/external-catalog) | `ExternalCatalog`, `ExternalColumn`, `ExternalTable` | | [`feed.zod.ts`](/docs/references/data/feed) | `FeedFilterMode`, `FeedItemType` | | [`field.zod.ts`](/docs/references/data/field) | `CurrencyConfig`, `Field`, `FieldMaskingKeep`, `FieldMaskingRule`, `FieldType`, `InlineGridColumn`, `LocationCoordinates`, `SelectOption`, `UniqueScope` | -| [`field-value.zod.ts`](/docs/references/data/field-value) | `Address`, `AddressValue`, `CalendarDateValue`, `ClockTimeValue`, `FileLikeValue`, `FileReferenceIdValue`, `FileRefusedValue`, `FileValue`, `InstantValue`, `LocationValue`, `ReferenceIdValue` | +| [`field-value.zod.ts`](/docs/references/data/field-value) | `Address`, `AddressValue`, `CalendarDateValue`, `ClockTimeValue`, `FileLikeValue`, `FileReferenceIdValue`, `FileRefusedValue`, `FileValue`, `InstantValue`, `LocationValue`, `ReferenceIdValue`, `ReferenceNameOnlyValue` | | [`filter.zod.ts`](/docs/references/data/filter) | `ComparisonOperator`, `EqualityOperator`, `FieldOperators`, `FieldReference`, `FilterArray`, `FilterCondition`, `NormalizedFilter`, `NowAnchor`, `QueryFilter`, `RangeOperator`, `SetOperator`, `SpecialOperator`, `StringOperator` | | [`hook.zod.ts`](/docs/references/data/hook) | `Hook`, `HookContext`, `HookEvent`, `HookRefusal` | | [`hook-body.zod.ts`](/docs/references/data/hook-body) | `ExpressionBody`, `HookBody`, `HookBodyCapability`, `ScriptBody` | diff --git a/content/docs/references/system/migration.mdx b/content/docs/references/system/migration.mdx index a31b01998f1..a48663c7374 100644 --- a/content/docs/references/system/migration.mdx +++ b/content/docs/references/system/migration.mdx @@ -81,7 +81,7 @@ Add a new field to an existing object | **reference** | `string` | optional | Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects. Never a stored-metadata table (`sys_metadata`, `sys_metadata_history`): their rows are served only through the metadata API, so the engine never hydrates a reference into one, and `os validate`, `os build` and the object save door refuse it. On a `tree` field it is optional and, if given, must be the declaring object's own name — the object schema refuses any other target, and writes the own name when it is left out. | | **referenceVia** | `string` | optional | Declares this text field as the id half of a polymorphic pointer pair (ADR-0052 §5 ActivityPointer): the value is a record id of the object named by the SIBLING FIELD this key names — e.g. `record_id` with `referenceVia: 'object_name'`. The sibling must be a declared field on the same object holding an object machine name. Text fields only; mutually exclusive with `reference` (a static and a per-record target contradict). Enforced today at seed load: the value resolves as a natural key against the object the sibling column names, and an unresolvable pointer is refused loudly instead of stored verbatim. Adds no referential integrity or $expand behavior. | | **referenceDeletedWhen** | `{ field: string; value: string }` | optional | Marks the record a `referenceVia` pointer names as PLATFORM-DELETED on rows where a sibling `select` field holds one declared option — e.g. `record_id` with `{ field: 'cancel_reason', value: 'record_deleted' }`. A renderer then draws a tombstone from that option's own label, reads nothing and links nothing. Valid only beside `referenceVia`; `field` must be a single-value `select` on the same object declaring inline `options`, and `value` one of its option values — any other shape is refused at parse. | -| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField` value) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }`, per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds: when the reader may not read that field, it is masked, or it is a formula reading through another reference, the name is the `Record #` floor with the id. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | +| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField`, else its `displayNameField`, else the derived title field) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }` (`ReferenceNameOnlyValueSchema`), per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds, and the name field is read only where nothing the security layer owns must be computed: the name is the `Record #` floor with the id when the reader may not read that field, it declares a `maskingRule`, it is a formula reading through another reference, an explicit `nameField` points at a column that is neither title-typed nor autonumber, or no field-level answer can be had for the reader. A target with no resolvable or no non-empty name reads as the same floor. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | | **mailAssociation** | `boolean` | optional | Inbound mail association (`email` fields only): TRUE marks this field as identifying a correspondent. When a mailbox service ingests a message, it associates the message with every record of this object whose value in this field equals one of the message's participant addresses (From, Sender, Reply-To, To and Cc, plus those of a forwarded message it carries; ASCII case-insensitive; the receiving mailbox's own addresses excluded; only within the mailbox's organization). A message no record matches waits in the mailbox's unassociated pool. Refused on any other type. Takes effect only where a mailbox service is composed. | | **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted | | **inlineEdit** | `boolean \| Enum<'grid' \| 'form'>` | optional | Edit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form. | @@ -515,7 +515,7 @@ Add a new field to an existing object | **reference** | `string` | optional | Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects. Never a stored-metadata table (`sys_metadata`, `sys_metadata_history`): their rows are served only through the metadata API, so the engine never hydrates a reference into one, and `os validate`, `os build` and the object save door refuse it. On a `tree` field it is optional and, if given, must be the declaring object's own name — the object schema refuses any other target, and writes the own name when it is left out. | | **referenceVia** | `string` | optional | Declares this text field as the id half of a polymorphic pointer pair (ADR-0052 §5 ActivityPointer): the value is a record id of the object named by the SIBLING FIELD this key names — e.g. `record_id` with `referenceVia: 'object_name'`. The sibling must be a declared field on the same object holding an object machine name. Text fields only; mutually exclusive with `reference` (a static and a per-record target contradict). Enforced today at seed load: the value resolves as a natural key against the object the sibling column names, and an unresolvable pointer is refused loudly instead of stored verbatim. Adds no referential integrity or $expand behavior. | | **referenceDeletedWhen** | `{ field: string; value: string }` | optional | Marks the record a `referenceVia` pointer names as PLATFORM-DELETED on rows where a sibling `select` field holds one declared option — e.g. `record_id` with `{ field: 'cancel_reason', value: 'record_deleted' }`. A renderer then draws a tombstone from that option's own label, reads nothing and links nothing. Valid only beside `referenceVia`; `field` must be a single-value `select` on the same object declaring inline `options`, and `value` one of its option values — any other shape is refused at parse. | -| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField` value) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }`, per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds: when the reader may not read that field, it is masked, or it is a formula reading through another reference, the name is the `Record #` floor with the id. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | +| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField`, else its `displayNameField`, else the derived title field) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }` (`ReferenceNameOnlyValueSchema`), per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds, and the name field is read only where nothing the security layer owns must be computed: the name is the `Record #` floor with the id when the reader may not read that field, it declares a `maskingRule`, it is a formula reading through another reference, an explicit `nameField` points at a column that is neither title-typed nor autonumber, or no field-level answer can be had for the reader. A target with no resolvable or no non-empty name reads as the same floor. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | | **mailAssociation** | `boolean` | optional | Inbound mail association (`email` fields only): TRUE marks this field as identifying a correspondent. When a mailbox service ingests a message, it associates the message with every record of this object whose value in this field equals one of the message's participant addresses (From, Sender, Reply-To, To and Cc, plus those of a forwarded message it carries; ASCII case-insensitive; the receiving mailbox's own addresses excluded; only within the mailbox's organization). A message no record matches waits in the mailbox's unassociated pool. Refused on any other type. Takes effect only where a mailbox service is composed. | | **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted | | **inlineEdit** | `boolean \| Enum<'grid' \| 'form'>` | optional | Edit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form. | diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts/data.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts/data.md index a60c482ffbc..a6b3d4c1fa5 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts/data.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts/data.md @@ -21,7 +21,7 @@ The `strict` column is the one the campaign schedules against; it counts both th | Dir | Sites | strict | passthrough | catchall | strip | |---|---|---|---|---|---| -| `data/` | 158 | 93 | 2 | 0 | 63 | +| `data/` | 159 | 94 | 2 | 0 | 63 | ## `data/` — sites @@ -44,7 +44,7 @@ classify and is not listed (it becomes reportable the day it grows its first sit | `driver/sqlite.zod.ts` | 2 | | `driver/turso.zod.ts` | 2 | | `external-catalog.zod.ts` | 4 | -| `field-value.zod.ts` | 4 | +| `field-value.zod.ts` | 5 | | `field.zod.ts` | 15 | | `filter.zod.ts` | 13 | | `hook-body.zod.ts` | 2 | @@ -57,7 +57,7 @@ classify and is not listed (it becomes reportable the day it grows its first sit | `seed-loader.zod.ts` | 12 | | `seed.zod.ts` | 2 | | `validation.zod.ts` | 6 | -| **total** | **158** | +| **total** | **159** | ## `data/` — open @@ -65,7 +65,7 @@ Per file, how many of its sites still silently discard unknown keys. The `Class` column that decides the bucket split is hand-written in the ledger; the arithmetic over it is here. -**63 strip of 158**, in 10 file(s). +**63 strip of 159**, in 10 file(s). | File | Strip | Sites | |---|---|---| @@ -79,7 +79,7 @@ over it is here. | `hook.zod.ts` | 5 | 8 | | `query.zod.ts` | 3 | 5 | | `seed-loader.zod.ts` | 12 | 12 | -| **total** | **63** | **158** | +| **total** | **63** | **159** | | Bucket | Sites | |---|---| diff --git a/packages/spec/api-surface/data.json b/packages/spec/api-surface/data.json index d07f7566771..592bc739b9c 100644 --- a/packages/spec/api-surface/data.json +++ b/packages/spec/api-surface/data.json @@ -648,6 +648,8 @@ "RedactedDatasourceConfig (interface)", "ReferenceIdValue (type)", "ReferenceIdValueSchema (const)", + "ReferenceNameOnlyValue (type)", + "ReferenceNameOnlyValueSchema (const)", "ReferenceResolution (type)", "ReferenceResolutionError (type)", "ReferenceResolutionErrorSchema (const)", diff --git a/packages/spec/authorable-surface/data.json b/packages/spec/authorable-surface/data.json index ca8d8efe0f4..569ad6c4ac8 100644 --- a/packages/spec/authorable-surface/data.json +++ b/packages/spec/authorable-surface/data.json @@ -815,6 +815,9 @@ "data/QueryWithTransport:where", "data/QueryWithTransport:windowFunctions [RETIRED]", "data/RangeOperator:$between", + "data/ReferenceNameOnlyValue:id", + "data/ReferenceNameOnlyValue:name", + "data/ReferenceNameOnlyValue:readRefused", "data/ReferenceResolution:field", "data/ReferenceResolution:fieldType", "data/ReferenceResolution:multiple", diff --git a/packages/spec/declaration-map/data.json b/packages/spec/declaration-map/data.json index 42d67397f28..a7dd6829cdd 100644 --- a/packages/spec/declaration-map/data.json +++ b/packages/spec/declaration-map/data.json @@ -253,6 +253,8 @@ "RangeOperatorSchema": "data/RangeOperator", "ReferenceIdValue": "data/ReferenceIdValue", "ReferenceIdValueSchema": "data/ReferenceIdValue", + "ReferenceNameOnlyValue": "data/ReferenceNameOnlyValue", + "ReferenceNameOnlyValueSchema": "data/ReferenceNameOnlyValue", "ReferenceResolution": "data/ReferenceResolution", "ReferenceResolutionError": "data/ReferenceResolutionError", "ReferenceResolutionErrorSchema": "data/ReferenceResolutionError", diff --git a/packages/spec/dropped-refinements.baseline.json b/packages/spec/dropped-refinements.baseline.json index 28c44baf555..16409c709b2 100644 --- a/packages/spec/dropped-refinements.baseline.json +++ b/packages/spec/dropped-refinements.baseline.json @@ -1083,6 +1083,11 @@ "" ] }, + "data/ReferenceNameOnlyValue": { + "sites": [ + "id" + ] + }, "data/SQLDriverConfig": { "sites": [ "" diff --git a/packages/spec/export-origins/data.json b/packages/spec/export-origins/data.json index 421dda8f040..e785991047e 100644 --- a/packages/spec/export-origins/data.json +++ b/packages/spec/export-origins/data.json @@ -636,6 +636,8 @@ "RedactedDatasourceConfig": "src/data/datasource-credential-redaction.ts#RedactedDatasourceConfig (interface)", "ReferenceIdValue": "src/data/field-value.zod.ts#ReferenceIdValue (type)", "ReferenceIdValueSchema": "src/data/field-value.zod.ts#ReferenceIdValueSchema (const)", + "ReferenceNameOnlyValue": "src/data/field-value.zod.ts#ReferenceNameOnlyValue (type)", + "ReferenceNameOnlyValueSchema": "src/data/field-value.zod.ts#ReferenceNameOnlyValueSchema (const)", "ReferenceResolution": "src/data/seed-loader.zod.ts#ReferenceResolution (type)", "ReferenceResolutionError": "src/data/seed-loader.zod.ts#ReferenceResolutionError (type)", "ReferenceResolutionErrorSchema": "src/data/seed-loader.zod.ts#ReferenceResolutionErrorSchema (const)", diff --git a/packages/spec/json-schema.manifest/data.json b/packages/spec/json-schema.manifest/data.json index f0da49cd8df..5ed61954d1c 100644 --- a/packages/spec/json-schema.manifest/data.json +++ b/packages/spec/json-schema.manifest/data.json @@ -136,6 +136,7 @@ "data/QueryWithTransport", "data/RangeOperator", "data/ReferenceIdValue", + "data/ReferenceNameOnlyValue", "data/ReferenceResolution", "data/ReferenceResolutionError", "data/ReplicationConfig", From aa107802f5ee54839685191e5f47980f7699c97b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 22:38:46 +0000 Subject: [PATCH 3/5] spec(data): declare the new export as a widening, pin the display-name describe, name the displayNameField alias Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01JQwxbahxR9HAJnTs7UKhmn --- .changeset/29579-reference-name-only-value.md | 6 ++--- content/docs/references/data/field.mdx | 2 +- content/docs/references/data/object.mdx | 4 ++-- content/docs/references/system/migration.mdx | 4 ++-- .../data/field-reference-name-visible.test.ts | 24 +++++++++++++++++-- packages/spec/src/data/field.zod.ts | 5 ++-- 6 files changed, 33 insertions(+), 12 deletions(-) diff --git a/.changeset/29579-reference-name-only-value.md b/.changeset/29579-reference-name-only-value.md index 9baa611b6b0..e8f9c9a016d 100644 --- a/.changeset/29579-reference-name-only-value.md +++ b/.changeset/29579-reference-name-only-value.md @@ -4,14 +4,14 @@ spec(data): the name-only reference value a refused reader is served has a named schema, and `referenceNameVisible` says which name it carries -Clause-②: no +Clause-②: yes (widening) Metadata-protocol: added A `lookup` or `user` field that declares `referenceNameVisible: true` answers `$expand` with `{ id, name, readRefused: true }` for a target the reader may not read. Until now that value had no name in the spec, and the field's description named the wrong source for `name` and only some of the cases where the name is withheld. - **New export.** `ReferenceNameOnlyValueSchema` / `ReferenceNameOnlyValue` from `@objectstack/spec/data`: a closed object of the record id the referencing row holds, a non-empty `name`, and `readRefused: true`. It is a read form only. The stored form of a reference stays the bare id, so a record write never accepts it. -- **Expanded lookup value.** `valueSchemaFor(def, 'expanded')` for the reference types lists the new schema beside the bare id and the open record object. The open record member already accepted this value, so no value parses differently than before. +- **Expanded lookup value.** `valueSchemaFor(def, 'expanded')` for the reference types lists the new schema beside the bare id and the open record object. The open record member already accepted this value, so no value parses differently than before; the new export and its authorable-surface rows are what the declaration records. - **Which name.** The `referenceNameVisible` description now says the name is the referenced object's display name as `resolveRecordDisplayName` resolves it: its `nameField`, else its `displayNameField`, else the derived title field. Before, it named `nameField` alone. -- **When the name is withheld.** The description now lists every case where the `Record #` floor with the id is served instead of the name: the reader's field-level security keeps the name field from it; the field declares a `maskingRule`; it is a formula reading through another reference; an explicit `nameField` points at a column that is neither title-typed nor autonumber; or no field-level answer can be had for the reader. A target with no resolvable or no non-empty name reads as the same floor. +- **When the name is withheld.** The description now lists every case where the `Record #` floor with the id is served instead of the name: the reader's field-level security keeps the name field from it; the field declares a `maskingRule`; it is a formula reading through another reference; an explicit `nameField` (or its `displayNameField` alias) points at a column that is neither title-typed nor autonumber; or no field-level answer can be had for the reader. A target with no resolvable or no non-empty name reads as the same floor. **For consumers.** Code that types a lookup's expanded value can import `ReferenceNameOnlyValue` and check `readRefused === true` to tell a refused target from an expanded record. Show the name as text and link nothing. diff --git a/content/docs/references/data/field.mdx b/content/docs/references/data/field.mdx index 82ebce01b7c..d79d5707e73 100644 --- a/content/docs/references/data/field.mdx +++ b/content/docs/references/data/field.mdx @@ -78,7 +78,7 @@ const result = CurrencyConfigSchema.parse(data); | **reference** | `string` | optional | Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects. Never a stored-metadata table (`sys_metadata`, `sys_metadata_history`): their rows are served only through the metadata API, so the engine never hydrates a reference into one, and `os validate`, `os build` and the object save door refuse it. On a `tree` field it is optional and, if given, must be the declaring object's own name — the object schema refuses any other target, and writes the own name when it is left out. | | **referenceVia** | `string` | optional | Declares this text field as the id half of a polymorphic pointer pair (ADR-0052 §5 ActivityPointer): the value is a record id of the object named by the SIBLING FIELD this key names — e.g. `record_id` with `referenceVia: 'object_name'`. The sibling must be a declared field on the same object holding an object machine name. Text fields only; mutually exclusive with `reference` (a static and a per-record target contradict). Enforced today at seed load: the value resolves as a natural key against the object the sibling column names, and an unresolvable pointer is refused loudly instead of stored verbatim. Adds no referential integrity or $expand behavior. | | **referenceDeletedWhen** | `{ field: string; value: string }` | optional | Marks the record a `referenceVia` pointer names as PLATFORM-DELETED on rows where a sibling `select` field holds one declared option — e.g. `record_id` with `{ field: 'cancel_reason', value: 'record_deleted' }`. A renderer then draws a tombstone from that option's own label, reads nothing and links nothing. Valid only beside `referenceVia`; `field` must be a single-value `select` on the same object declaring inline `options`, and `value` one of its option values — any other shape is refused at parse. | -| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField`, else its `displayNameField`, else the derived title field) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }` (`ReferenceNameOnlyValueSchema`), per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds, and the name field is read only where nothing the security layer owns must be computed: the name is the `Record #` floor with the id when the reader may not read that field, it declares a `maskingRule`, it is a formula reading through another reference, an explicit `nameField` points at a column that is neither title-typed nor autonumber, or no field-level answer can be had for the reader. A target with no resolvable or no non-empty name reads as the same floor. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | +| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField`, else its `displayNameField`, else the derived title field) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }` (`ReferenceNameOnlyValueSchema`), per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds, and the name field is read only where nothing the security layer owns must be computed: the name is the `Record #` floor with the id when the reader may not read that field, it declares a `maskingRule`, it is a formula reading through another reference, an explicit `nameField` (or its `displayNameField` alias) points at a column that is neither title-typed nor autonumber, or no field-level answer can be had for the reader. A target with no resolvable or no non-empty name reads as the same floor. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | | **mailAssociation** | `boolean` | optional | Inbound mail association (`email` fields only): TRUE marks this field as identifying a correspondent. When a mailbox service ingests a message, it associates the message with every record of this object whose value in this field equals one of the message's participant addresses (From, Sender, Reply-To, To and Cc, plus those of a forwarded message it carries; ASCII case-insensitive; the receiving mailbox's own addresses excluded; only within the mailbox's organization). A message no record matches waits in the mailbox's unassociated pool. Refused on any other type. Takes effect only where a mailbox service is composed. | | **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted | | **inlineEdit** | `boolean \| Enum<'grid' \| 'form'>` | optional | Edit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form. | diff --git a/content/docs/references/data/object.mdx b/content/docs/references/data/object.mdx index 815f698f04b..fcc3b76a837 100644 --- a/content/docs/references/data/object.mdx +++ b/content/docs/references/data/object.mdx @@ -247,7 +247,7 @@ const result = ApiMethod.parse(data); | **reference** | `string` | optional | Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects. Never a stored-metadata table (`sys_metadata`, `sys_metadata_history`): their rows are served only through the metadata API, so the engine never hydrates a reference into one, and `os validate`, `os build` and the object save door refuse it. On a `tree` field it is optional and, if given, must be the declaring object's own name — the object schema refuses any other target, and writes the own name when it is left out. | | **referenceVia** | `string` | optional | Declares this text field as the id half of a polymorphic pointer pair (ADR-0052 §5 ActivityPointer): the value is a record id of the object named by the SIBLING FIELD this key names — e.g. `record_id` with `referenceVia: 'object_name'`. The sibling must be a declared field on the same object holding an object machine name. Text fields only; mutually exclusive with `reference` (a static and a per-record target contradict). Enforced today at seed load: the value resolves as a natural key against the object the sibling column names, and an unresolvable pointer is refused loudly instead of stored verbatim. Adds no referential integrity or $expand behavior. | | **referenceDeletedWhen** | `{ field: string; value: string }` | optional | Marks the record a `referenceVia` pointer names as PLATFORM-DELETED on rows where a sibling `select` field holds one declared option — e.g. `record_id` with `{ field: 'cancel_reason', value: 'record_deleted' }`. A renderer then draws a tombstone from that option's own label, reads nothing and links nothing. Valid only beside `referenceVia`; `field` must be a single-value `select` on the same object declaring inline `options`, and `value` one of its option values — any other shape is refused at parse. | -| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField`, else its `displayNameField`, else the derived title field) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }` (`ReferenceNameOnlyValueSchema`), per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds, and the name field is read only where nothing the security layer owns must be computed: the name is the `Record #` floor with the id when the reader may not read that field, it declares a `maskingRule`, it is a formula reading through another reference, an explicit `nameField` points at a column that is neither title-typed nor autonumber, or no field-level answer can be had for the reader. A target with no resolvable or no non-empty name reads as the same floor. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | +| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField`, else its `displayNameField`, else the derived title field) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }` (`ReferenceNameOnlyValueSchema`), per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds, and the name field is read only where nothing the security layer owns must be computed: the name is the `Record #` floor with the id when the reader may not read that field, it declares a `maskingRule`, it is a formula reading through another reference, an explicit `nameField` (or its `displayNameField` alias) points at a column that is neither title-typed nor autonumber, or no field-level answer can be had for the reader. A target with no resolvable or no non-empty name reads as the same floor. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | | **mailAssociation** | `boolean` | optional | Inbound mail association (`email` fields only): TRUE marks this field as identifying a correspondent. When a mailbox service ingests a message, it associates the message with every record of this object whose value in this field equals one of the message's participant addresses (From, Sender, Reply-To, To and Cc, plus those of a forwarded message it carries; ASCII case-insensitive; the receiving mailbox's own addresses excluded; only within the mailbox's organization). A message no record matches waits in the mailbox's unassociated pool. Refused on any other type. Takes effect only where a mailbox service is composed. | | **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted | | **inlineEdit** | `boolean \| Enum<'grid' \| 'form'>` | optional | Edit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form. | @@ -618,7 +618,7 @@ const result = ApiMethod.parse(data); | **reference** | `string` | optional | Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects. Never a stored-metadata table (`sys_metadata`, `sys_metadata_history`): their rows are served only through the metadata API, so the engine never hydrates a reference into one, and `os validate`, `os build` and the object save door refuse it. On a `tree` field it is optional and, if given, must be the declaring object's own name — the object schema refuses any other target, and writes the own name when it is left out. | | **referenceVia** | `string` | optional | Declares this text field as the id half of a polymorphic pointer pair (ADR-0052 §5 ActivityPointer): the value is a record id of the object named by the SIBLING FIELD this key names — e.g. `record_id` with `referenceVia: 'object_name'`. The sibling must be a declared field on the same object holding an object machine name. Text fields only; mutually exclusive with `reference` (a static and a per-record target contradict). Enforced today at seed load: the value resolves as a natural key against the object the sibling column names, and an unresolvable pointer is refused loudly instead of stored verbatim. Adds no referential integrity or $expand behavior. | | **referenceDeletedWhen** | `{ field: string; value: string }` | optional | Marks the record a `referenceVia` pointer names as PLATFORM-DELETED on rows where a sibling `select` field holds one declared option — e.g. `record_id` with `{ field: 'cancel_reason', value: 'record_deleted' }`. A renderer then draws a tombstone from that option's own label, reads nothing and links nothing. Valid only beside `referenceVia`; `field` must be a single-value `select` on the same object declaring inline `options`, and `value` one of its option values — any other shape is refused at parse. | -| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField`, else its `displayNameField`, else the derived title field) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }` (`ReferenceNameOnlyValueSchema`), per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds, and the name field is read only where nothing the security layer owns must be computed: the name is the `Record #` floor with the id when the reader may not read that field, it declares a `maskingRule`, it is a formula reading through another reference, an explicit `nameField` points at a column that is neither title-typed nor autonumber, or no field-level answer can be had for the reader. A target with no resolvable or no non-empty name reads as the same floor. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | +| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField`, else its `displayNameField`, else the derived title field) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }` (`ReferenceNameOnlyValueSchema`), per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds, and the name field is read only where nothing the security layer owns must be computed: the name is the `Record #` floor with the id when the reader may not read that field, it declares a `maskingRule`, it is a formula reading through another reference, an explicit `nameField` (or its `displayNameField` alias) points at a column that is neither title-typed nor autonumber, or no field-level answer can be had for the reader. A target with no resolvable or no non-empty name reads as the same floor. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | | **mailAssociation** | `boolean` | optional | Inbound mail association (`email` fields only): TRUE marks this field as identifying a correspondent. When a mailbox service ingests a message, it associates the message with every record of this object whose value in this field equals one of the message's participant addresses (From, Sender, Reply-To, To and Cc, plus those of a forwarded message it carries; ASCII case-insensitive; the receiving mailbox's own addresses excluded; only within the mailbox's organization). A message no record matches waits in the mailbox's unassociated pool. Refused on any other type. Takes effect only where a mailbox service is composed. | | **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted | | **inlineEdit** | `boolean \| Enum<'grid' \| 'form'>` | optional | Edit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form. | diff --git a/content/docs/references/system/migration.mdx b/content/docs/references/system/migration.mdx index a48663c7374..80ff16df199 100644 --- a/content/docs/references/system/migration.mdx +++ b/content/docs/references/system/migration.mdx @@ -81,7 +81,7 @@ Add a new field to an existing object | **reference** | `string` | optional | Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects. Never a stored-metadata table (`sys_metadata`, `sys_metadata_history`): their rows are served only through the metadata API, so the engine never hydrates a reference into one, and `os validate`, `os build` and the object save door refuse it. On a `tree` field it is optional and, if given, must be the declaring object's own name — the object schema refuses any other target, and writes the own name when it is left out. | | **referenceVia** | `string` | optional | Declares this text field as the id half of a polymorphic pointer pair (ADR-0052 §5 ActivityPointer): the value is a record id of the object named by the SIBLING FIELD this key names — e.g. `record_id` with `referenceVia: 'object_name'`. The sibling must be a declared field on the same object holding an object machine name. Text fields only; mutually exclusive with `reference` (a static and a per-record target contradict). Enforced today at seed load: the value resolves as a natural key against the object the sibling column names, and an unresolvable pointer is refused loudly instead of stored verbatim. Adds no referential integrity or $expand behavior. | | **referenceDeletedWhen** | `{ field: string; value: string }` | optional | Marks the record a `referenceVia` pointer names as PLATFORM-DELETED on rows where a sibling `select` field holds one declared option — e.g. `record_id` with `{ field: 'cancel_reason', value: 'record_deleted' }`. A renderer then draws a tombstone from that option's own label, reads nothing and links nothing. Valid only beside `referenceVia`; `field` must be a single-value `select` on the same object declaring inline `options`, and `value` one of its option values — any other shape is refused at parse. | -| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField`, else its `displayNameField`, else the derived title field) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }` (`ReferenceNameOnlyValueSchema`), per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds, and the name field is read only where nothing the security layer owns must be computed: the name is the `Record #` floor with the id when the reader may not read that field, it declares a `maskingRule`, it is a formula reading through another reference, an explicit `nameField` points at a column that is neither title-typed nor autonumber, or no field-level answer can be had for the reader. A target with no resolvable or no non-empty name reads as the same floor. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | +| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField`, else its `displayNameField`, else the derived title field) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }` (`ReferenceNameOnlyValueSchema`), per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds, and the name field is read only where nothing the security layer owns must be computed: the name is the `Record #` floor with the id when the reader may not read that field, it declares a `maskingRule`, it is a formula reading through another reference, an explicit `nameField` (or its `displayNameField` alias) points at a column that is neither title-typed nor autonumber, or no field-level answer can be had for the reader. A target with no resolvable or no non-empty name reads as the same floor. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | | **mailAssociation** | `boolean` | optional | Inbound mail association (`email` fields only): TRUE marks this field as identifying a correspondent. When a mailbox service ingests a message, it associates the message with every record of this object whose value in this field equals one of the message's participant addresses (From, Sender, Reply-To, To and Cc, plus those of a forwarded message it carries; ASCII case-insensitive; the receiving mailbox's own addresses excluded; only within the mailbox's organization). A message no record matches waits in the mailbox's unassociated pool. Refused on any other type. Takes effect only where a mailbox service is composed. | | **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted | | **inlineEdit** | `boolean \| Enum<'grid' \| 'form'>` | optional | Edit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form. | @@ -515,7 +515,7 @@ Add a new field to an existing object | **reference** | `string` | optional | Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects. Never a stored-metadata table (`sys_metadata`, `sys_metadata_history`): their rows are served only through the metadata API, so the engine never hydrates a reference into one, and `os validate`, `os build` and the object save door refuse it. On a `tree` field it is optional and, if given, must be the declaring object's own name — the object schema refuses any other target, and writes the own name when it is left out. | | **referenceVia** | `string` | optional | Declares this text field as the id half of a polymorphic pointer pair (ADR-0052 §5 ActivityPointer): the value is a record id of the object named by the SIBLING FIELD this key names — e.g. `record_id` with `referenceVia: 'object_name'`. The sibling must be a declared field on the same object holding an object machine name. Text fields only; mutually exclusive with `reference` (a static and a per-record target contradict). Enforced today at seed load: the value resolves as a natural key against the object the sibling column names, and an unresolvable pointer is refused loudly instead of stored verbatim. Adds no referential integrity or $expand behavior. | | **referenceDeletedWhen** | `{ field: string; value: string }` | optional | Marks the record a `referenceVia` pointer names as PLATFORM-DELETED on rows where a sibling `select` field holds one declared option — e.g. `record_id` with `{ field: 'cancel_reason', value: 'record_deleted' }`. A renderer then draws a tombstone from that option's own label, reads nothing and links nothing. Valid only beside `referenceVia`; `field` must be a single-value `select` on the same object declaring inline `options`, and `value` one of its option values — any other shape is refused at parse. | -| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField`, else its `displayNameField`, else the derived title field) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }` (`ReferenceNameOnlyValueSchema`), per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds, and the name field is read only where nothing the security layer owns must be computed: the name is the `Record #` floor with the id when the reader may not read that field, it declares a `maskingRule`, it is a formula reading through another reference, an explicit `nameField` points at a column that is neither title-typed nor autonumber, or no field-level answer can be had for the reader. A target with no resolvable or no non-empty name reads as the same floor. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | +| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField`, else its `displayNameField`, else the derived title field) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }` (`ReferenceNameOnlyValueSchema`), per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds, and the name field is read only where nothing the security layer owns must be computed: the name is the `Record #` floor with the id when the reader may not read that field, it declares a `maskingRule`, it is a formula reading through another reference, an explicit `nameField` (or its `displayNameField` alias) points at a column that is neither title-typed nor autonumber, or no field-level answer can be had for the reader. A target with no resolvable or no non-empty name reads as the same floor. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | | **mailAssociation** | `boolean` | optional | Inbound mail association (`email` fields only): TRUE marks this field as identifying a correspondent. When a mailbox service ingests a message, it associates the message with every record of this object whose value in this field equals one of the message's participant addresses (From, Sender, Reply-To, To and Cc, plus those of a forwarded message it carries; ASCII case-insensitive; the receiving mailbox's own addresses excluded; only within the mailbox's organization). A message no record matches waits in the mailbox's unassociated pool. Refused on any other type. Takes effect only where a mailbox service is composed. | | **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted | | **inlineEdit** | `boolean \| Enum<'grid' \| 'form'>` | optional | Edit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form. | diff --git a/packages/spec/src/data/field-reference-name-visible.test.ts b/packages/spec/src/data/field-reference-name-visible.test.ts index 8fb19873be6..42e52909ec3 100644 --- a/packages/spec/src/data/field-reference-name-visible.test.ts +++ b/packages/spec/src/data/field-reference-name-visible.test.ts @@ -5,8 +5,8 @@ * referencing field (director ruling 6574398310, letter B). * * TRUE declares that a principal who can read the record carrying the field - * may see the referenced record's display name (the target's `nameField`) and - * id even when it cannot read the target; nothing else of the target. Absent + * may see the referenced record's display name (the target's `nameField`, + * else `displayNameField`, else the derived title field) and id even when it cannot read the target; nothing else of the target. Absent * or false keeps the restricted-reference posture. The key lives on the * referencing field only: the ruling refuses a target-object switch. * @@ -187,4 +187,24 @@ describe('the JSON-schema projection of FieldSchema carries referenceNameVisible expect(d).toContain('no link to the target'); expect(d).toContain('no target-object switch'); }); + + it('names the display name the engine resolves and the schema of the served value', () => { + const d = prop!.description!; + // The ADR-0079 D4 order `resolveRecordDisplayName` answers, not `nameField` alone. + expect(d).toContain('`nameField`, else its `displayNameField`, else the derived title field'); + expect(d).toContain('`ReferenceNameOnlyValueSchema`'); + }); + + it('lists every case the `Record #` floor is served in place of the name', () => { + const d = prop!.description!; + expect(d).toContain('`Record #` floor with the id'); + // One marker per floor case objectql's `servableNameField` serves. + expect(d).toContain('the reader may not read that field'); + expect(d).toContain('it declares a `maskingRule`'); + expect(d).toContain('a formula reading through another reference'); + expect(d).toContain('an explicit `nameField` (or its `displayNameField` alias) points at a column that is neither title-typed nor autonumber'); + expect(d).toContain('no field-level answer can be had for the reader'); + // And the floor every reader gets, with or without the key. + expect(d).toContain('no resolvable or no non-empty name reads as the same floor'); + }); }); diff --git a/packages/spec/src/data/field.zod.ts b/packages/spec/src/data/field.zod.ts index 291b9f5ec09..f84a7961a63 100644 --- a/packages/spec/src/data/field.zod.ts +++ b/packages/spec/src/data/field.zod.ts @@ -1773,8 +1773,9 @@ export const FieldSchema = lazySchema(() => { + 'renders as text with no link to the target. A field-level rule on the name field still holds, and ' + 'the name field is read only where nothing the security layer owns must be computed: the name is the ' + '`Record #` floor with the id when the reader may not read that field, it declares a `maskingRule`, ' - + 'it is a formula reading through another reference, an explicit `nameField` points at a column that ' - + 'is neither title-typed nor autonumber, or no field-level answer can be had for the reader. A target ' + + 'it is a formula reading through another reference, an explicit `nameField` (or its `displayNameField` ' + + 'alias) points at a column that is neither title-typed nor autonumber, or no field-level answer can be ' + + 'had for the reader. A target ' + 'with no resolvable or no non-empty name reads as the same floor. A principal who can read the target is ' + 'served exactly what it is served without the key. Absent or false keeps the restricted-reference ' + 'posture for a target the reader cannot read. Declared on the referencing field only; there is no ' From 43a7db69a4e2755b9bb333d69165511e964b178f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 23:02:13 +0000 Subject: [PATCH 4/5] spec(data): count the new ledger row in the dropped-refinements header totals Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01JQwxbahxR9HAJnTs7UKhmn --- packages/spec/dropped-refinements.baseline.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/spec/dropped-refinements.baseline.json b/packages/spec/dropped-refinements.baseline.json index 16409c709b2..3d7cad37de8 100644 --- a/packages/spec/dropped-refinements.baseline.json +++ b/packages/spec/dropped-refinements.baseline.json @@ -2,8 +2,8 @@ "description": "Shrink-only ledger of every PUBLISHED JSON Schema that is STILL WIDER than the Zod type it was generated from, because a rule written as `.refine()` reaches the runtime and not the file (#18670). `z.toJSONSchema()` has no arm for a `custom` check: a plain record, the same record with a `.refine()`, and the same record with an ABORTING `.refine()` all project byte-identically (measured on zod 4.4.3, the version packages/spec resolves). So a document one of these files ACCEPTS can still be refused at parse time, and an author -- or an AI -- validating against packages/spec/json-schema/** finds out a release later. Each `sites` path is a position under that schema at which a refinement is dropped; the same paths are written onto the artifact itself as `x-dropped-refinements`. Item 2 closed the first patterns: a refinement DECLARED through the closed list in src/shared/refinement-projection.ts is emitted into the published file, reads `projected` rather than `dropped`, and its row LEAVES this ledger in the same PR -- which is why the ledger shrinks and never grows on a repair. Every refinement outside that closed list stays here, and adding an arm to the list is a public-contract decision, not a refactor. Hand-edited on purpose and with no `gen:` script: a generator would let a new gap be admitted by running a command instead of by a decision, which is the silence this ledger exists to end. Adding, removing or moving a site fails packages/spec/scripts/build-schemas.ts until the line moves with it, and the failure prints the corrected entry in full. ⛔ Do not delete or weaken a refinement to shorten this file -- the runtime rule is correct; it is the projection that is silent, and the remedy is to teach the closed list a NAMED pattern, never to drop the rule.", "measured": { "zod": "4.4.3", - "publishedSchemasWithDroppedRefinements": 248, - "droppedRefinementSites": 998, + "publishedSchemasWithDroppedRefinements": 249, + "droppedRefinementSites": 999, "refinementSitesThatDidProject": 369, "refinementSitesWithNoJsonFormToCompare": 0 }, From 505d409df054a7e2e5dd2d4c71296ff29edb9e00 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 01:16:50 +0000 Subject: [PATCH 5/5] fix(fields): the name-only reference value's type comes from the spec export Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01JQwxbahxR9HAJnTs7UKhmn --- ui/packages/fields/src/index.tsx | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/ui/packages/fields/src/index.tsx b/ui/packages/fields/src/index.tsx index 999bae7633f..1a9178e5875 100644 --- a/ui/packages/fields/src/index.tsx +++ b/ui/packages/fields/src/index.tsx @@ -15,6 +15,7 @@ import { ComponentRegistry, percentDisplayValue, type PercentScale, getRecordDis // `resolveFieldScale` is the same kind of ask: the width an ABSENT `scale` // means is the protocol's answer, not this package's (objectui#9843). import { valueSchemaFor, resolveFieldScale } from '@objectstack/spec/data'; +import type { ReferenceNameOnlyValue } from '@objectstack/spec/data'; import { useLocalization, useDisplayLocale, formatDisplayNumber } from '@object-ui/i18n'; import { Badge, Avatar, AvatarImage, AvatarFallback, Button, Checkbox, EmptyValue, cn, setFieldReadFaceResolver } from '@object-ui/components'; import { Check, Copy, Phone as PhoneIcon, MapPin, CircleQuestionMark, Lock, Star } from 'lucide-react'; @@ -3240,23 +3241,17 @@ function RestrictedLookupReference({ label }: { label: string }): React.ReactEle } /** - * The value a `lookup` / `user` field declaring `referenceNameVisible: true` - * is served on `$expand` when the reader may read the record carrying it but - * is refused the record it points at (director ruling 6574398310, letter B): - * the id the row holds, the target's display name, and the refusal said in - * the value. The server's own type is `ReferenceNameOnlyValue` in - * `@objectstack/objectql` (`engine-reference-name-visible.ts`); it is served - * once per element on a `multiple: true` field. `name` is the target's - * ADR-0079 display name, or the floor `Record #` when a field-level rule - * on the name column holds it back, and is drawn exactly as served. - */ -interface ReferenceNameOnlyValue { - id: string; - name: string; - readRefused: true; -} - -/** + * `ReferenceNameOnlyValue` (imported from `@objectstack/spec/data`, where + * `ReferenceNameOnlyValueSchema` owns the shape and the engine imports the same + * type) is the value a `lookup` / `user` field declaring + * `referenceNameVisible: true` is served on `$expand` when the reader may read + * the record carrying it but is refused the record it points at (director + * ruling 6574398310, letter B): the id the row holds, the target's display + * name, and the refusal said in the value, once per element on a + * `multiple: true` field. `name` is the target's ADR-0079 display name, or the + * floor `Record #` when a field-level rule on the name column holds it + * back, and is drawn exactly as served. + * * Is this one reference value the served name-only form? Judged on the * in-band `readRefused: true` the server writes, never on the shape of the * rest: an expanded record the reader may read can also carry just an id and