Skip to content

Commit 47bc621

Browse files
docs(spec): the optional-master entry and docblock name the raw-parse door's reading of an omitted required (#31523)
<!-- claude-projects-attribution: {"github_login":"marchtian"} --> _Requested by **田騏** · [project thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSiMiP5QfRf2t6EvZJ7zdWomo)_ Fixes #31470 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么:** 只改文字和一个测试,不改任何行为。语义迁移条目 `cbp-optional-master-admitted` 的 `replacement` 和 `acceptanceCriteria`、以及 `controlledByParentMasterOf` 的 docblock,补上「原始解析入口」的读法;另加一个测试把三个入口并排钉住。 - **为什么改:** 子对象唯一的 `master_detail` 不写 `required` 时,`ObjectSchema.create()` 读成必填主字段,而 `ObjectSchema.safeParse` / `defineStack` / 保存入口读成可选主字段。这个分歧是你在 #30905 裁定的(ruling record 6687830530,选 A「放行为可选主字段」),但条目和 docblock 只写了 `create()` 和存储行,读者会以为省略永远等于必填。座位据此对本卡选 B(只改文字),不是 A(A 会推翻现行裁决)。 - **风险与回滚:** 没有运行时风险:无 schema、类型、导出或运行时答案变化。条目文字随 `@objectstack/spec` 的 `./migrations` 发布(`os migrate` 会读),所以带 patch changeset。回滚即还原这三个文件和 changeset。 - **先看:** `packages/spec/src/migrations/entries/semantic/18.cbp-optional-master-admitted.ts` 里 `replacement` 末句和 `acceptanceCriteria` 末句。 - **两分钟验证:** `pnpm --filter @objectstack/spec exec vitest run --project local src/data/controlled-by-parent-master.test.ts`,看新测试「the three doors on an omitted required」:`create()` 给必填主字段,`safeParse` 与 `defineStack` 给可选主字段。 - **需要你做的:** 无,座位做契约复核后落地。 ## What changes Card #31470 asked to pick A (refuse an omitted `required` at the raw parse and in `defineStack`) or B (keep the reading and say so). The seat picked **B**: maintainer ruling A on #30905 (ruling record 6687830530) already decided that at the raw parse doors (an object written directly in `defineStack`, the save doors) an omitted `required` on a `controlled_by_parent` object's only `master_detail` is read as the optional master, and it explicitly did not take "refuse the omission at every door". Option A on this card would reverse that ruling. What was wrong is the prose, which named only the doors that see the omission. - **`18.cbp-optional-master-admitted.ts` (semantic migration entry).** - `replacement`: after "an omitted `required` keeps meaning required through `ObjectSchema.create()` and on stored rows", it now says that through a raw parse (an object written directly in `defineStack`, or saved through a save door) `required` defaults to `false` before the object is judged, so the omission is read as the optional master, as the maintainer ruled rather than refusing it at every door. `os lint` still reports the omission at `error`. - `acceptanceCriteria`: the last sentence gains the same clause: through `ObjectSchema.safeParse`, `defineStack` or a save door, the omission is admitted as the optional master, its field parsed as `required: false`. - The header comment names the ruling record. `id`, `surface` and `reason` are unchanged. - **`controlledByParentMasterOf` (JSDoc).** "One whose `required` is not written names no master" 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 that field is the optional master; the builder forces it to `true` first. "Reads `required` as written" is kept. ## Tests One new `it` in `packages/spec/src/data/controlled-by-parent-master.test.ts`, citing the ruling record, pins the three doors on one omitted `required`: - `ObjectSchema.create()`: `required: true`, `controlledByParentMasterOf` gives `{ field: 'order', optional: false }`; - `ObjectSchema.safeParse`: success, `required: false`, `{ field: 'order', optional: true }`; - `defineStack`: `required: false`, `{ field: 'order', optional: true }`. Measured first: no existing test pinned all three in one place. `controlled-by-parent-master.test.ts` (the omitted-required case) pins `create()` and `safeParse`; `defineStack` is pinned separately in `conversions/cbp-master-detail-required-defaulted.test.ts` and `data/object.test.ts`. ## Acceptance notes - **Changeset, not `skip-changeset`:** the entry ships. `@objectstack/spec` exports `./migrations` from `dist`, and `os migrate` prints the entry's `replacement`. Patch changeset `.changeset/31470-cbp-omitted-required-raw-parse-door.md`. - **The D2 conversion docblock** the ruling called stale under A (`conversions/entries/18/cbp-master-detail-required-defaulted.ts`) was already fixed on `main` by #31055 (`f1206610a9`): it says a raw parse reads the omitted key as the optional master. Nothing owed here. - **ADR-0055** already states the raw-parse reading (its "An omitted `required` keeps meaning required where the omission is visible" bullet); not touched. - **"Save door" wording** is taken from ruling record 6687830530, which names the save doors among the raw-parse doors. This PR did not re-measure the save door's conversion order. ## Verification Before the merge of `origin/main` (head `425cef8315`): - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 85 commands for this 4-file diff; all 85 ran and exit 0. - `NODE_OPTIONS=--max-old-space-size=6144 pnpm --filter @objectstack/spec run typecheck` :: exit 0 (the default heap OOMs in `check:test-typecheck`). - `pnpm --filter @objectstack/spec exec eslint` on the three touched source/test files :: exit 0. - `vitest run --project local` on `controlled-by-parent-master.test.ts`, `cbp-master-detail-required-defaulted.test.ts` and `src/migrations` :: 7 files, 252 tests passed. After `git merge origin/main` (merge commit `5e9d2802a6`, the PR head): - spec `gen:schema && gen:docs` :: exit 0, no tree change. `pnpm --filter @objectstack/spec run check:generated` :: exit 0 (14 artifacts current, after a spec rebuild; the first run read a pre-merge `dist`). - `dispatch-gates --commands` derived the same 85 commands; after a `packages/*` build all 85 ran and exit 0. - `dispatch-gates --ran`: `85 derived famil(ies) accounted for — 85 run, 0 NOT-MEASURED`, exit 0. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017ihL2diS4EEKVj8TfNEQAs <!-- signed: domain:spec#4 · marchtian · session_017ihL2eAG5J8aqAyPZif4BJ --> Co-authored-by: Claude <noreply@anthropic.com>
1 parent a3ac7fb commit 47bc621

4 files changed

Lines changed: 62 additions & 6 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
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
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **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.
11+
- **`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.
12+
13+
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.

‎packages/spec/src/data/controlled-by-parent-master.test.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
type ControlledByParentMasterShape,
1313
} from './index';
1414
import { ObjectSchema } from './object.zod';
15+
import { defineStack } from '../stack.zod';
1516

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

140+
// [#30905 ruling A, record 6687830530 · #31470] The three doors, side by
141+
// side, on one omitted `required`. The answers differ BY RULING: the builder
142+
// sees the omission and forces the required master; the raw parse and
143+
// `defineStack` over a plain object literal read the parsed `false`, the
144+
// optional master. Refusing the omission at every door was the option the
145+
// ruling did not take; `os lint` reports it at `error` instead.
146+
it('the three doors on an omitted required: create() → required master; safeParse and defineStack → optional master', () => {
147+
const line = { name: 'cbpd3_line', label: 'Line', sharingModel: 'controlled_by_parent' as const, fields: { order: md('cbpd3_header') } };
148+
149+
const built = ObjectSchema.create(line as Parameters<typeof ObjectSchema.create>[0]);
150+
expect(built.fields.order!.required).toBe(true);
151+
expect(controlledByParentMasterOf(built.fields)).toEqual({ field: 'order', optional: false });
152+
153+
const parsed = ObjectSchema.safeParse(line);
154+
expect(parsed.success).toBe(true);
155+
expect(parsed.data!.fields.order!.required).toBe(false);
156+
expect(controlledByParentMasterOf(parsed.data!.fields)).toEqual({ field: 'order', optional: true });
157+
158+
const stack = defineStack({
159+
manifest: { id: 'com.example.cbp-three-doors', namespace: 'cbpd3', version: '0.0.1', type: 'app', name: 'CBP' },
160+
objects: [{ name: 'cbpd3_header', label: 'Header', fields: { name: { type: 'text', label: 'Name' } } }, line],
161+
} as never) as unknown as { objects: Array<{ name: string; fields: Record<string, { required?: boolean }> }> };
162+
const staged = stack.objects.find((o) => o.name === 'cbpd3_line')!;
163+
expect(staged.fields.order!.required).toBe(false);
164+
expect(controlledByParentMasterOf(staged.fields)).toEqual({ field: 'order', optional: true });
165+
});
166+
139167
it('answers undefined for any other sharing model, as the parse accepts it', () => {
140168
for (const sharingModel of ['private', 'public_read', 'public_read_write']) {
141169
const obj = { name: 'pair', label: 'Pair', sharingModel, fields: { a: md('x', { required: true }), b: md('y', { required: true }) } };

‎packages/spec/src/data/controlled-by-parent-master.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,13 @@ export interface ControlledByParentMaster {
157157
* declared `required: false`, that field is the optional master;
158158
* - anything else (two required masters, two `master_detail` fields none of
159159
* them required, one whose `required` is not written, a required `lookup`
160-
* only, nothing) names no master: `undefined`.
160+
* only, nothing) names no master: `undefined`. "Not written" holds only on
161+
* a map read as written (an authored or stored body no parse has touched).
162+
* On a PARSED map the omission has already defaulted to `false`, so through
163+
* a raw parse (`ObjectSchema.safeParse`, an object written directly in
164+
* `defineStack`, a save door) that field IS the optional master, as
165+
* maintainer ruling A on #30905 (record 6687830530) reads it; the builder
166+
* (`ObjectSchema.create()`) forces it to `true` first, the required master.
161167
*
162168
* Judges the master only. Whether the one master is writable (`readonly` /
163169
* `system`) is {@link controlledByParentMasterRefusal}'s question, and the

‎packages/spec/src/migrations/entries/semantic/18.cbp-optional-master-admitted.ts‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ import type { SemanticMigration } from '../../types.js';
1111
// no master is read by its owner and by View All on the detail; a row that
1212
// names one is read through its master, as before. An OMITTED `required` keeps
1313
// its meaning where the spec sees the omission (the builder force and the D2
14-
// conversion cbp-master-detail-required-defaulted, both unchanged). No D2
15-
// conversion: nothing is rewritten.
14+
// conversion cbp-master-detail-required-defaulted, both unchanged). A raw
15+
// parse cannot see it: the field default writes `false` first, so an object
16+
// written directly in `defineStack`, or saved through a save door, reads an
17+
// omitted `required` as the optional master. That divergence is ruled, not a
18+
// gap: maintainer ruling A on #30905 (ruling record 6687830530) chose it over
19+
// refusing the omission at every door. No D2 conversion: nothing is rewritten.
1620
//
1721
// The write side followed under ADR-0055 §3 as amended (same ruling A, PR
1822
// #31082), and the `replacement` below records it beside the read side: a
@@ -37,8 +41,11 @@ export const entry: SemanticMigration = {
3741
+ 'master is unchanged: a row whose stored required master reference is empty is still refused with '
3842
+ '`422 MISSING_REQUIRED_FIELD`. Declare `required: true` when every row must name its master; an '
3943
+ 'omitted `required` keeps meaning '
40-
+ 'required through `ObjectSchema.create()` and on stored rows, and `os lint` reports it at `error` so the '
41-
+ 'author writes one of the two.',
44+
+ 'required through `ObjectSchema.create()` and on stored rows. Through a raw parse (an object written '
45+
+ 'directly in `defineStack`, or saved through a save door) it does not: `required` defaults to `false` '
46+
+ 'before the object is judged, so an omitted `required` there is read as the optional master, as the '
47+
+ 'maintainer ruled rather than refusing the omission at every door. `os lint` reports the omission at '
48+
+ '`error` so the author writes one of the two.',
4249
reason:
4350
'The rule "a rep sees the contacts of the accounts they own; a contact need not have an account" had no '
4451
+ 'declarative form: `controlled_by_parent` refused a `master_detail` that is not required at the builder '
@@ -60,5 +67,7 @@ export const entry: SemanticMigration = {
6067
+ '"resolves no master relation" line), a member who can read a master reads that master\'s rows, the '
6168
+ 'owner of a row with an empty master reads that row, a holder of View All on the object reads every '
6269
+ 'row with an empty master, and nobody else reads one. An object whose `master_detail` omits `required` '
63-
+ 'is still forced to `required: true` by the builder and converted to it on stored rows.',
70+
+ 'is still forced to `required: true` by the builder and converted to it on stored rows; through a raw '
71+
+ 'parse (`ObjectSchema.safeParse`, an object written directly in `defineStack`, a save door) the same '
72+
+ 'omission is admitted as the optional master, its field parsed as `required: false`.',
6473
};

0 commit comments

Comments
 (0)