Skip to content

Commit 2ce2e36

Browse files
docs(spec): put the forceCbpMasterDetailRequired "only the security gate" sentence in the past tense (#24310)
Fixes #24291 Clause-②: no ## What changes This PR rewrites one sentence in the `forceCbpMasterDetailRequired` docblock and adds a prose-only `patch` changeset. Nothing else changes. The sentence is in the "Why" paragraph of `packages/spec/src/data/object.zod.ts`, now `:3483`–`:3490`. Before: > Today only the security gate (`assertControlledByParentWrite`) closes that shape, and commit 75b7c24 records that the declaration and the enforcement disagree. This makes the unsafe shape impossible to NEWLY declare: After: > Before commit 75b7c24 only the security gate (`assertControlledByParentWrite`) closed that shape, by refusing the insert at runtime, and that commit records that the declaration and the enforcement disagreed. Since protocol 18 the parse refuses the declaration itself: {@link refuseControlledByParentWithoutOneMaster} refuses a `controlled_by_parent` object none of whose `master_detail` fields declares `required: true`. The builder makes the unsafe shape impossible to NEWLY declare: - "This makes" became "The builder makes". The sentence before it is now about the parse, so "This" would point at the wrong mechanism. - `.changeset/24291-cbp-docblock-tense.md`: `'@objectstack/spec': patch`, prose only, with `Clause-②: no`. No key, schema, export, accepted value or message text changes. All 12 changed lines in `packages/spec` are ` *` comment lines. ## What each claim rests on **The past half: before commit 75b7c24, only the security gate closed that shape, by refusing the insert.** - Commit `75b7c240a3eb` (read through the REST API) adds the builder force and this docblock. Its changeset says: "#8772 measured that only the security gate closed that shape while the declaration surface accepted it." So the old "Today" meant the tree before that commit, and "records" stays true. - I read `packages/plugins/plugin-security/src/security-plugin.ts` at `75b7c240a` through the contents API. That commit does not touch this file, so its parent has the same content. - `resolveCbpRelation` picks a required `master_detail`, then any `master_detail` (`:4949`), then a required `lookup`. - A non-required master resolves with `omissionRefusedByValidation` false (`:4969`). - The insert leg takes the master id from the body (`:5164`). A null id throws `MasterReferenceMissingError` (`:5251`), because the stand-down at `:5250` does not apply. - The record validator never checks a field that is not `required` (PR #24290's contract review, D7). So at runtime, the gate's insert refusal was the only one. **The present half: since protocol 18, the parse refuses the declaration itself.** - `refuseControlledByParentWithoutOneMaster` (`object.zod.ts:2213`) runs inside `ObjectSchemaBase`'s `.superRefine` (`:3143`, called at `:3167`). - It reports the verdict of `controlledByParentMasterRefusal` (`src/data/controlled-by-parent-master.ts`). That function counts a field as a master only when `type === 'master_detail'` and `required === true`, and refuses unless exactly one field counts. - It is a protocol 18 change: the parse refusal is registered by `src/migrations/entries/semantic/18.cbp-master-relation-tiers-retired.ts`. - I ran a one-off probe against the built `dist/data/index.mjs`. No test file was added or kept. Each object below declares `sharingModel: 'controlled_by_parent'`: | `master_detail` fields | `ObjectSchema.safeParse` | `ObjectSchema.create` | |---|---|---| | one, `required` omitted | refused at `["sharingModel"]`, "but no required `master_detail` field" | passes (forced) | | one, `required: false` | refused at `["sharingModel"]` | throws (the force's refusal) | | one, `required: true` | passes | passes | | one required, plus one with `required` omitted | passes | throws at `["fields"]` (two required after the force) | The fourth row is why the new sentence states the exact predicate ("none of whose `master_detail` fields declares `required: true`"). It does not say "a `master_detail` without `required: true`", which would be wrong for that row. **Which door the sentence claims.** It claims only the `ObjectSchema` parse. That means `.parse()`, `.safeParse()` and `create()`, which runs the force (`:3677`) and then `ObjectSchemaBase.parse(withDefaults)` (`:3687`). The sentence says nothing about `defineStack`, `os validate`, the metadata save door or stored rows. The refinement's own docblock says stored rows still register, under a `[metadata_spec_invalid]` diagnostic. I did not read those doors for this PR, so the sentence does not name them. The sentence also drops "only", and it does not say whether the gate still closes the shape. I read what the gate does today: it derives a master from exactly one required `master_detail` (`security-plugin.ts:7923`), and on any write to an object without one it throws `MasterDetailRelationMissingError` (`:8585`). That is not this sentence's subject, so the sentence makes no claim about it. **The symbol reference.** The new text cites the symbol as `{@link refuseControlledByParentWithoutOneMaster}`. That is the form the same docblock already uses one paragraph below the list. It is not a `path#symbol` anchor, so `check-spec-docblock-symbol-anchors` does not judge it. That gate counts `12 symbol (12 declaration, 0 literal)` on this head, the same as on PR #24290. The name has exactly one declaration in the file: `git grep -c "^function refuseControlledByParentWithoutOneMaster("` gives 1. ## Changeset `@objectstack/spec`'s `files` array ships `src/**/*.zod.ts`, so the docblock goes into the tarball as written. It does not reach `dist`. After the build, 0 `dist` files carry the docblock's unchanged phrase "arms the worst measured failure shape", and 0 carry the new "closed that shape, by refusing the". The function's name is in `dist`, which is the positive control. So the published surface is the source file, and the changeset is a `patch` with `Clause-②: no`. This follows PR #24290 and PR #24072. ## Verification The final head is `5d9d774b1`, the merge of `main` at `21070a2f0`. The two commits that merge brought in touch only `packages/lint`, `docs/qa` and one changeset. Build and test ran through `scripts/pm/os-verify-lock.sh`, and every exit code was captured before any pipe. - `pnpm --filter @objectstack/spec build && pnpm --filter @objectstack/spec check:generated`: `VERDICT command-exit 0`, `All 14 generated artifacts are up to date`. Nothing was regenerated. - `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2`: `Test Files 694 passed (694)`, `Tests 21040 passed | 1 todo (21041)`. - `pnpm --filter @objectstack/spec typecheck`: `VERDICT command-exit 0`, ending `check:test-typecheck: OK`. - The build, the tests and the typecheck ran on the pre-merge head `6abb5d815`. The merge brought no `packages/spec` change. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on `5d9d774b1` derived 81 commands from 2 paths against merge base `21070a2f0`. 80 exit 0. The `--ran` reconciliation reads `81 derived, 80 run, 1 NOT-MEASURED, 0 UNRUN`. - `check-spec-docblock-symbol-anchors`: `4972 anchors across 2121 spec sources resolve`. - `check:nul-bytes`: OK. - `check-adr-0087-registration`: `this PR adds no declared-breaking changeset`. - `check-empty-changeset`: green. - `check:doc-formula-expressions` and `check:lean-entry-closure` read built output. Both exit 0, after I built `@objectstack/formula`, `@objectstack/lint` and `@objectstack/objectql`. - **NOT MEASURED: `check:dual-build-cjs-loads`** (exit 3, `PREREQUISITE NOT MET`). It reads every workspace package's `dist`, and 69 package entries had none. I did not run a whole-repo build locally; that is a declared narrowing. CI's `Lint & Repo Gates` runs this gate. - Comment-only check: the `git diff -U0` of `packages/spec` against the base has 12 changed lines, and each one starts with ` *`. Neither changed file contains a control byte. - `git grep "Today only the security gate" -- packages`: 0 hits on this head, 1 on the base `ef6564684`. - No ablation: the diff changes no tested behaviour. - Left to CI: `test:repo` (`Test Core`), `pnpm lint`, the type-check lanes, and the families outside the derived set. ## Acceptance notes - **The same docblock, outside this card's file surface.** The paragraph after the list (`:3498`–`:3507`) says "so a raw parse of a `master_detail` without `required: true` is refused rather than forced". The probe's fourth row reads otherwise: next to a required `master_detail`, a second one without `required` parses green. (`create()` refuses that object, because the force makes both required.) Read as "the object's one `master_detail`", the sentence holds. This is an observation, not a defect. Carrier: none. - **`packages/lint/src/data-model-rules.ts:304`, a code comment.** It says "the security gate's fallbacks stay, so metadata already at rest keeps loading and is still guarded". Since protocol 18, the gate has no fallback: `security-plugin.ts:7923`, and the gate's own docblock says both tiers retired. This is the same family, a comment that names a retired mechanism in the present tense. It is not published: `@objectstack/lint`'s `files` is `dist`, `README.md` and `CHANGELOG.md`. 0 `dist` files carry the phrase, while the neighbouring code string "readable by nobody" is in `dist`. Carrier: none. ## Deviations - `main` was merged once before this PR opened (`5d9d774b1`), as AGENTS.md section 10 asks. - I read commit `75b7c240a` twice through the REST API: the commit itself, and `security-plugin.ts` at that ref. Nothing was fetched into the shared object store. - Labels: none written. --- _Generated by [Claude Code](https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent f5e98a3 commit 2ce2e36

2 files changed

Lines changed: 21 additions & 4 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+
Clause-②: no
6+
7+
The `forceCbpMasterDetailRequired` docblock in `src/data/object.zod.ts` no longer says that today only
8+
the security gate (`assertControlledByParentWrite`) closes the null-master-FK shape of a
9+
`controlled_by_parent` object. That was true before the builder's force landed, and the sentence is now
10+
in the past tense. It also names what has refused the declaration at parse since protocol 18:
11+
`refuseControlledByParentWithoutOneMaster` refuses a `controlled_by_parent` object none of whose
12+
`master_detail` fields declares `required: true`. No key, schema, export, accepted value or message text
13+
changes.

‎packages/spec/src/data/object.zod.ts‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3480,10 +3480,14 @@ function assertReferenceViaSiblingDeclared(objectName: unknown, fields: unknown)
34803480
* master FK, the row lands with a null FK, the derived read filter
34813481
* `masterFK IN (accessible master ids)` can never match null — the row is
34823482
* invisible to everyone — and every later by-id write answers
3483-
* `422 MISSING_REQUIRED_FIELD`. Today only the security gate
3484-
* (`assertControlledByParentWrite`) closes that shape, and commit 75b7c240a records that
3485-
* the declaration and the enforcement disagree. This makes the unsafe shape
3486-
* impossible to NEWLY declare:
3483+
* `422 MISSING_REQUIRED_FIELD`. Before commit 75b7c240a only the security
3484+
* gate (`assertControlledByParentWrite`) closed that shape, by refusing the
3485+
* insert at runtime, and that commit records that the declaration and the
3486+
* enforcement disagreed. Since protocol 18 the parse refuses the declaration
3487+
* itself: {@link refuseControlledByParentWithoutOneMaster} refuses a
3488+
* `controlled_by_parent` object none of whose `master_detail` fields declares
3489+
* `required: true`. The builder makes the unsafe shape impossible to NEWLY
3490+
* declare:
34873491
*
34883492
* - omitted `required` → forced to `true` (the lint rule
34893493
* `relationship/master-detail-required` already computes exactly this fix);

0 commit comments

Comments
 (0)