From fa101162097f86fb7d5f74a0ed28b3aaaef9912d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 08:08:40 +0000 Subject: [PATCH] fix(spec): the two-master controlled_by_parent refusal names its release Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz --- .changeset/23387-two-master-names-release.md | 5 +++++ packages/spec/src/data/controlled-by-parent-master.ts | 4 ++-- packages/spec/src/data/object.test.ts | 6 +++++- 3 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 .changeset/23387-two-master-names-release.md diff --git a/.changeset/23387-two-master-names-release.md b/.changeset/23387-two-master-names-release.md new file mode 100644 index 00000000000..0a7f466746b --- /dev/null +++ b/.changeset/23387-two-master-names-release.md @@ -0,0 +1,5 @@ +--- +"@objectstack/spec": patch +--- + +fix(spec): the two-required-masters `controlled_by_parent` refusal names its release ("since @objectstack/spec 18"), as the no-master refusal does. Message text only: the refused shapes and the prescribed fix are unchanged. diff --git a/packages/spec/src/data/controlled-by-parent-master.ts b/packages/spec/src/data/controlled-by-parent-master.ts index b3e9109ee63..68f4ec7cd31 100644 --- a/packages/spec/src/data/controlled-by-parent-master.ts +++ b/packages/spec/src/data/controlled-by-parent-master.ts @@ -94,8 +94,8 @@ export function controlledByParentMasterRefusal( requiredLookups, message: `Object '${name}' declares sharingModel 'controlled_by_parent' with ${masters.length} required ` - + `\`master_detail\` fields (${masters.map((m) => `\`${m}\``).join(', ')}). ADR-0055 §1: a ` - + 'controlled-by-parent object derives its master from exactly one required `master_detail` field, so ' + + `\`master_detail\` fields (${masters.map((m) => `\`${m}\``).join(', ')}). ADR-0055 §1: since ` + + '@objectstack/spec 18, a controlled-by-parent object derives its master from exactly one required `master_detail` field, so ' + 'with two the declaration does not say which master governs its rows, and the platform derives none ' + '(every read is denied, every write refused). Keep `master_detail` on the one master, and declare ' + 'every other parent as a `lookup` field.', diff --git a/packages/spec/src/data/object.test.ts b/packages/spec/src/data/object.test.ts index f7e0753bb15..e8e8d90f267 100644 --- a/packages/spec/src/data/object.test.ts +++ b/packages/spec/src/data/object.test.ts @@ -1632,13 +1632,17 @@ describe('ObjectSchema refuses a controlled_by_parent object without exactly one }); it('REFUSES no relation at all, and two required master_details, at the two lint-shared paths', () => { - expect(issuesOf(cbp({ title: { type: 'text' } })).map((i) => i.path)).toEqual(['sharingModel']); + const none = issuesOf(cbp({ title: { type: 'text' } })); + expect(none.map((i) => i.path)).toEqual(['sharingModel']); const two = issuesOf(cbp({ a: { type: 'master_detail', reference: 'master_a', required: true }, b: { type: 'master_detail', reference: 'master_b', required: true }, })); expect(two.map((i) => i.path)).toEqual(['fields']); expect(two[0].message).toMatch(/^Object 'cbp_line' declares sharingModel 'controlled_by_parent' with 2 required `master_detail` fields \(`a`, `b`\)\. ADR-0055 §1:/); + // Both refusals date themselves: 17.7.0 accepts either shape, so each names the release that refuses it. + expect(two[0].message).toContain('since @objectstack/spec 18, a controlled-by-parent object derives'); + expect(none[0].message).toContain('since @objectstack/spec 18, a controlled-by-parent object derives'); }); it('judges controlled_by_parent only — every other sharing model keeps its relations as authored', () => {