Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/31470-cbp-omitted-required-raw-parse-door.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@objectstack/spec': patch
---

docs(spec): the semantic migration entry `cbp-optional-master-admitted` and `controlledByParentMasterOf` name how a raw parse reads an omitted `required` on the optional master

Clause-②: no
Metadata-protocol: none

- **Migration entry `cbp-optional-master-admitted`.** Its `replacement` and `acceptanceCriteria` said an omitted `required` on the only `master_detail` of a `controlled_by_parent` object keeps meaning required through `ObjectSchema.create()` and on stored rows, and stopped there. They now also name the raw-parse door, as the maintainer ruled on the optional master: through `ObjectSchema.safeParse`, an object written directly in `defineStack`, or a save door, `required` defaults to `false` before the object is judged, so the omission is admitted as the optional master. `os lint` still reports the omission at `error`. The entry's `id`, `surface` and `reason` are unchanged, and so is its paragraph's meaning in the protocol upgrade guide apart from that added clause.
- **`controlledByParentMasterOf` (JSDoc).** "One whose `required` is not written names no master" holds on a body read as written. On a parsed field map the omission has already defaulted to `false`, so there the field is the optional master.

Text only: no schema, type, export or runtime answer changes. A test now pins the three doors side by side on one omitted `required`: `ObjectSchema.create()` gives the required master, `ObjectSchema.safeParse` and `defineStack` give the optional master.
28 changes: 28 additions & 0 deletions packages/spec/src/data/controlled-by-parent-master.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
type ControlledByParentMasterShape,
} from './index';
import { ObjectSchema } from './object.zod';
import { defineStack } from '../stack.zod';

/**
* [#23272 · ADR-0055 §1] The exported exactly-one-master predicate and
Expand Down Expand Up @@ -136,6 +137,33 @@ describe('controlledByParentMasterRefusal — the export and ObjectSchema agree
expect(controlledByParentMasterOf(built.fields)).toEqual({ field: 'order', optional: false });
});

// [#30905 ruling A, record 6687830530 · #31470] The three doors, side by
// side, on one omitted `required`. The answers differ BY RULING: the builder
// sees the omission and forces the required master; the raw parse and
// `defineStack` over a plain object literal read the parsed `false`, the
// optional master. Refusing the omission at every door was the option the
// ruling did not take; `os lint` reports it at `error` instead.
it('the three doors on an omitted required: create() → required master; safeParse and defineStack → optional master', () => {
const line = { name: 'cbpd3_line', label: 'Line', sharingModel: 'controlled_by_parent' as const, fields: { order: md('cbpd3_header') } };

const built = ObjectSchema.create(line as Parameters<typeof ObjectSchema.create>[0]);
expect(built.fields.order!.required).toBe(true);
expect(controlledByParentMasterOf(built.fields)).toEqual({ field: 'order', optional: false });

const parsed = ObjectSchema.safeParse(line);
expect(parsed.success).toBe(true);
expect(parsed.data!.fields.order!.required).toBe(false);
expect(controlledByParentMasterOf(parsed.data!.fields)).toEqual({ field: 'order', optional: true });

const stack = defineStack({
manifest: { id: 'com.example.cbp-three-doors', namespace: 'cbpd3', version: '0.0.1', type: 'app', name: 'CBP' },
objects: [{ name: 'cbpd3_header', label: 'Header', fields: { name: { type: 'text', label: 'Name' } } }, line],
} as never) as unknown as { objects: Array<{ name: string; fields: Record<string, { required?: boolean }> }> };
const staged = stack.objects.find((o) => o.name === 'cbpd3_line')!;
expect(staged.fields.order!.required).toBe(false);
expect(controlledByParentMasterOf(staged.fields)).toEqual({ field: 'order', optional: true });
});

it('answers undefined for any other sharing model, as the parse accepts it', () => {
for (const sharingModel of ['private', 'public_read', 'public_read_write']) {
const obj = { name: 'pair', label: 'Pair', sharingModel, fields: { a: md('x', { required: true }), b: md('y', { required: true }) } };
Expand Down
8 changes: 7 additions & 1 deletion packages/spec/src/data/controlled-by-parent-master.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,13 @@ export interface ControlledByParentMaster {
* declared `required: false`, that field is the optional master;
* - anything else (two required masters, two `master_detail` fields none of
* them required, one whose `required` is not written, a required `lookup`
* only, nothing) names no master: `undefined`.
* only, nothing) names no master: `undefined`. "Not written" holds only on
* a map read as written (an authored or stored body no parse has touched).
* On a PARSED map the omission has already defaulted to `false`, so through
* a raw parse (`ObjectSchema.safeParse`, an object written directly in
* `defineStack`, a save door) that field IS the optional master, as
* maintainer ruling A on #30905 (record 6687830530) reads it; the builder
* (`ObjectSchema.create()`) forces it to `true` first, the required master.
*
* Judges the master only. Whether the one master is writable (`readonly` /
* `system`) is {@link controlledByParentMasterRefusal}'s question, and the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ import type { SemanticMigration } from '../../types.js';
// no master is read by its owner and by View All on the detail; a row that
// names one is read through its master, as before. An OMITTED `required` keeps
// its meaning where the spec sees the omission (the builder force and the D2
// conversion cbp-master-detail-required-defaulted, both unchanged). No D2
// conversion: nothing is rewritten.
// conversion cbp-master-detail-required-defaulted, both unchanged). A raw
// parse cannot see it: the field default writes `false` first, so an object
// written directly in `defineStack`, or saved through a save door, reads an
// omitted `required` as the optional master. That divergence is ruled, not a
// gap: maintainer ruling A on #30905 (ruling record 6687830530) chose it over
// refusing the omission at every door. No D2 conversion: nothing is rewritten.
//
// The write side followed under ADR-0055 §3 as amended (same ruling A, PR
// #31082), and the `replacement` below records it beside the read side: a
Expand All @@ -37,8 +41,11 @@ export const entry: SemanticMigration = {
+ 'master is unchanged: a row whose stored required master reference is empty is still refused with '
+ '`422 MISSING_REQUIRED_FIELD`. Declare `required: true` when every row must name its master; an '
+ 'omitted `required` keeps meaning '
+ 'required through `ObjectSchema.create()` and on stored rows, and `os lint` reports it at `error` so the '
+ 'author writes one of the two.',
+ 'required through `ObjectSchema.create()` and on stored rows. Through a raw parse (an object written '
+ 'directly in `defineStack`, or saved through a save door) it does not: `required` defaults to `false` '
+ 'before the object is judged, so an omitted `required` there is read as the optional master, as the '
+ 'maintainer ruled rather than refusing the omission at every door. `os lint` reports the omission at '
+ '`error` so the author writes one of the two.',
reason:
'The rule "a rep sees the contacts of the accounts they own; a contact need not have an account" had no '
+ 'declarative form: `controlled_by_parent` refused a `master_detail` that is not required at the builder '
Expand All @@ -60,5 +67,7 @@ export const entry: SemanticMigration = {
+ '"resolves no master relation" line), a member who can read a master reads that master\'s rows, the '
+ 'owner of a row with an empty master reads that row, a holder of View All on the object reads every '
+ 'row with an empty master, and nobody else reads one. An object whose `master_detail` omits `required` '
+ 'is still forced to `required: true` by the builder and converted to it on stored rows.',
+ 'is still forced to `required: true` by the builder and converted to it on stored rows; through a raw '
+ 'parse (`ObjectSchema.safeParse`, an object written directly in `defineStack`, a save door) the same '
+ 'omission is admitted as the optional master, its field parsed as `required: false`.',
};
Loading