Uh oh!
There was an error while loading. Please reload this page.
docs(plugin-security): freeze note on assertControlledByParentWrite (#9137) - #9199
Conversation
…9137) Direction 4 of the #8772 ruling ("immediately"): a load-bearing comment stating that this guard's absent-master-FK branch is the sole enforcement point for three authorable controlled_by_parent master-reference shapes (master_detail with no required; required+readonly; required+system) until both ramp legs land — the builder-force slice (#9138) and the lint-at-v18 slice (#9139). No behaviour change. Also corrects an adjacent [#8959] paragraph in the same branch that had gone stale: it still described #8772 as "open and unruled", which stopped being true on 2026-08-16. Left alone, it would have directly contradicted the new freeze note one paragraph below it. Co-Authored-By: Claude Code <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 12 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also reference the affected code. These are read-only:
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#9137
What
Adds the load-bearing freeze note the #8772 ruling's Direction 4 calls for
("immediately", ahead of or alongside the first ramp slice) at
assertControlledByParentWrite's absent-master-FK branch inpackages/plugins/plugin-security/src/security-plugin.ts. No behaviourchange — comment-only.
The note states, in place:
if(and theomissionRefusedByValidationpredicate it reads) is thesole enforcement point for three authorable
controlled_by_parentmaster-reference shapes:
master_detailwith norequired;required: true+readonly;required: true+system— the last three rows ofA
controlled_by_parentobject may declare its master reference withoutrequired, so the master-access guard is the only thing preventing an unreachable orphan detail row #8772's five-shape measurement table, and exactly the three shapesrecord-validator.tsskips before its required check ever runs(
if (def.system || def.readonly) continue;, insert-mode:993/update-mode
:1003).shapes — concretely, do not widen
omissionRefusedByValidationto admitthem — until both ramp legs land: (a) the authoring builder forces
required: trueundercontrolled_by_parent(spec builder: forcerequired: trueon amaster_detailreference undercontrolled_by_parent(ruled Direction 2 of #8772) #9138), and (b) lint refusesthe shape at the v18 boundary (lint: promote
relationship/master-detail-requiredfrom warning to error, scoped tocontrolled_by_parent— ruled for the v18 boundary (Direction 1 of #8772) #9139, held for the v18 window).detail row whose master FK is null, which the
controlled_by_parentreadfilter (
fk IN (readable masters)) can never match — unreadable by anyone,and
422 MISSING_REQUIRED_FIELDon every later by-id write, permanently(no payload the caller could send would restore a field the request never
carried).
omissionRefusedByValidationfield declaration itself, since that predicate is the actual knob a future
change would widen.
Also corrects an adjacent, now-stale
[#8959]paragraph in the samebranch. It still read "#8772proposes a lint … it is open and unruled" —
true when #8959 landed it, false today: #8772 was ruled and closed on
2026-08-16 (comment 5306089973).
Left uncorrected, it would have sat one paragraph above this PR's own freeze
note and contradicted it. The corrected paragraph keeps #8959's actual
finding intact and current: no publish-time gate refuses these three shapes
yet — that remains true until #9138and#9139 both land — but it no
longer misdescribes #8772 itself as unruled.
Premise re-check (the card asked for this before writing)
assertControlledByParentWriteis confirmed live atsecurity-plugin.ts:5121onorigin/main(git grep).2026-08-15, closing A missing required master-detail parent still answers 422 MISSING_REQUIRED_FIELD with no
fields[]and a[Security]message — while the same field, present-but-unresolvable, answers 400 VALIDATION_FAILED withfields[](#7474 residual, 17.0.0 GA) #8688) already landed a conditional stand-down keyedon
CbpRelation.omissionRefusedByValidation. For the three shapes thisnote is about, that predicate is always
falseby construction(
def?.type === 'master_detail' && !!def?.required && !def?.readonly && !def?.system), so the guard remains the sole, unconditional enforcementpoint for exactly those three — which is what the note now says, rather
than the flat "still unconditional" framing the card anticipated as the
default outcome of the re-check.
fields[]and a[Security]message — while the same field, present-but-unresolvable, answers 400 VALIDATION_FAILED withfields[](#7474 residual, 17.0.0 GA) #8688 is not "still open and unruled." It was ruled and merged (PRfix(plugin-security): a required master-detail parent omitted on insert answers 400 VALIDATION_FAILED with fields[] (#8688) #8879, 2026-08-15). Its proposed route — stand the guard down for an
absent master FK on insert — already landed, correctly narrowed to the one
shape
validateRecordactually covers. The freeze note is writtenaccordingly: it protects against widening an already-landed, conditional
stand-down, not against a still-hypothetical change. (Reported per the
card's own instruction to adjust the framing rather than write a stale
warning.)
record-validator.ts'sif (def.system || def.readonly) continue;(
:993insert-mode,:1003update-mode) plusvalidateOne'sdef.required && isMissing(value)check: exactly 3, matching the card andA
controlled_by_parentobject may declare its master reference withoutrequired, so the master-access guard is the only thing preventing an unreachable orphan detail row #8772's five-row measurement table.all three shapes already exists —
controlled-by-parent-sharing.test.ts'sit.eachblock ('[#8688] 422 MISSING_REQUIRED_FIELD survives on insert: %s'), added by PR fix(plugin-security): a required master-detail parent omitted on insert answers 400 VALIDATION_FAILED with fields[] (#8688) #8879. Perthe card's "check first" instruction, no duplicate test was added.
Changeset
Comment-only, no behaviour change ⇒ no changeset.
skip-changesetlabelapplied.
Tests
All commands below were re-run at this PR's final commit,
1a67152fc(git rev-parse --short HEAD).pnpm --filter '@objectstack/plugin-security^...' build— dependencyclosure, green.
pnpm --filter @objectstack/plugin-security typecheck— green (tsc --noEmit, exit 0).pnpm --filter @objectstack/plugin-security test— 66 test files, 1279tests, all passing (includes the already-pinned three-shape
it.eachabove).
node scripts/check-cross-package-test-inputs.mjs,node scripts/check-test-source-alias.mjs,node scripts/check-type-source-resolution.mjs— the three path-derivedgates from
node scripts/pm/dispatch-gates.mjs— all green.node scripts/check-adr-anchors.mjs— green (this file is ADR-0055content-anchored; the anchor is citation-based, not line-based, so the
comment edit does not disturb it).
node scripts/check-nul-bytes.mjs— clean.node scripts/check-i18n-bundles.mjs(afterpnpm exec turbo run build --filter=@objectstack/cli) — "9 package(s) — all bundles in sync, noundeclared authoring keys" (comment-only edit produces no drift).
Generated by Claude Code