@@ -122,7 +122,7 @@ import { validateSeedReplaySafety } from './validate-seed-replay-safety.js';
122122import { validateSeedStateMachine } from './validate-seed-state-machine.js' ;
123123import { validateVisibilityPredicates } from './validate-visibility-predicates.js' ;
124124import { validatePredicatePathRefs } from './validate-predicate-path-refs.js' ;
125- import { validateSecurityPosture } from './validate-security-posture.js' ;
125+ import { validateSecurityPosture , validateSecurityRoleWord } from './validate-security-posture.js' ;
126126import { validateOrgAxisRedLines } from './validate-org-axis-red-lines.js' ;
127127import { validateSharingRuleEnforceability } from './validate-sharing-rule-enforceability.js' ;
128128import { validateRlsPredicateEnforceability } from './validate-rls-predicate-enforceability.js' ;
@@ -1126,75 +1126,110 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [
11261126 // sees one refusal, never two. The stated cost of moving was imaginary; the
11271127 // reason it has not moved is the measured one below.
11281128 //
1129- // Why the move is not taken HERE, measured rather than assumed (#7576 stage 1) :
1129+ // The move IS taken now — the #7891 programme's three slices, in order :
11301130 //
1131- // - The four shipped stacks (showcase, CRM, todo, the `blank` template — 30
1132- // objects, 10 permission sets, 1 book, 12 positions, 3 apps, 24 seeds) are
1133- // CLEAN of `error` findings at both surfaces. No shipped app trips.
1134- // - The PLATFORM's own runtime write path does. Declaring `object` here makes
1135- // `security-owd-unset` refuse any object published without an OWD, and that
1136- // is the shape the runtime create door actually emits: it turns 26 writes
1137- // into 422s across 8 files of `@objectstack/metadata-protocol`'s own suite,
1138- // and `METADATA_CREATE_SEEDS.object` — the authoritative minimal create body
1139- // — carries no `sharingModel` either. That is a strictness rollout
1140- // (#4001 pattern), not a registry-honesty fix, and its repair sites are in
1141- // packages this card may not edit.
1142- // - `permission` and `book` failed for a different, structural reason —
1143- // REPAIRED under #8309. The gate used to carry `objects` as resolution
1144- // context and nothing else (`RuntimeStackContext`), so the three
1145- // cross-collection rules judged a snapshot missing the collection they
1146- // compare against. Measured: one simulated runtime write per shipped
1147- // permission set produced 38 `security-master-detail-ungranted` warnings
1148- // where the same rule over the whole stack produces 4 — with one set in
1149- // the snapshot, every detail object the tenant's OTHER sets grant read as
1150- // ungranted. `security-private-no-readscope` and
1151- // `security-book-audience-unknown-set` failed identically. The snapshot
1152- // now carries `permissions` and `books` in both differential passes and
1153- // `TYPE_TO_STACK_KEY` maps both types, so the per-write verdict agrees
1154- // with the whole-stack one (pinned in
1155- // `validate-security-posture.runtime-surface.test.ts`); what remains for
1156- // these two types is ONLY the `runtimeTypes` declaration itself — #8310.
1131+ // - #8307: the ADR-0091 seed pair crossed (`runtimeTypes: ['seed']`), with
1132+ // the isolation proof that the differential cancels every finding this
1133+ // function derives from the sibling collections.
1134+ // - #8309: the snapshot repair. The gate used to carry `objects` and
1135+ // nothing else, so the three cross-collection rules judged a universe
1136+ // missing the collection they compare against (measured: 38 phantom
1137+ // `security-master-detail-ungranted` per-write vs 4 whole-stack,
1138+ // PR #7886). `RuntimeStackContext` now carries `permissions`/`books` in
1139+ // BOTH differential passes and `TYPE_TO_STACK_KEY` maps both types.
1140+ // - #8310 (this state): `runtimeTypes` gains `permission` + `book`.
1141+ // `object` measured DIRTY and stays behind — see below.
11571142 //
1158- // The residue that WAS ready, and now crosses (#8307): the two ADR-0091 seed
1159- // rules (`security-grant-expired-at-authoring`, `security-delegation-missing-
1160- // reason`) read only `stack.data[]` and cross the wall together as a whole
1161- // sub-family, with zero measured trips (re-run for this card — see
1162- // `validate-security-posture.runtime-surface.test.ts`). `security-role-word`
1163- // is deliberately NOT in this slice: it judges six collections, and wiring the
1164- // two that need no snapshot would split ONE rule id across the wall — a door
1165- // where a position named `sales_role` is refused and an object named
1166- // `sales_role` is not, which is the #7220 failure this table already refuses
1167- // to build.
1143+ // Why `object` is still not declared, re-measured on the #8308-repaired
1144+ // tree rather than inherited (#4001: zero breakage is demonstrated, never
1145+ // assumed). The OLD blocker is genuinely gone: with `object` declared, the
1146+ // full `@objectstack/metadata-protocol` suite passes (the 26-refusal
1147+ // measurement predates #8308's `METADATA_CREATE_SEEDS.object` repair) and a
1148+ // replay of every shipped-corpus object through the real gate refuses
1149+ // nothing. But one package over the same declaration still breaks the
1150+ // platform's own write paths — measured on this exact tree:
11681151 //
1169- // `object` / `permission` / `book` remain UNDECLARED here (that is #8310):
1170- // (a) declaring `object` makes `security-owd-unset` refuse every OWD-less
1171- // runtime object publish — a strictness rollout (#4001), not a wiring fix;
1172- // (b) `permission` / `book` needed a second collection the per-write
1173- // snapshot did not carry, and were measured inventing findings without it
1174- // (38 vs 4 over the shipped corpus). #8309 closed (b) — the snapshot now
1175- // carries `permissions`/`books` and both types map to stack keys — so
1176- // declaring them here is #8310's remaining one-line flip. This
1177- // entry is the WHOLE `validateSecurityPosture` function (all 13 rule ids), not
1178- // a per-rule split: `runtimeTypes: ['seed']` is safe to declare on the whole
1179- // entry ONLY because of `runtime-gate.ts`'s baseline/candidate differential —
1180- // a `seed` write's candidate stack carries `objects` IDENTICAL to the
1181- // baseline (only `data` differs), so every finding this function derives from
1182- // `stack.objects` / `stack.permissions` / `stack.positions` / `stack.apps` /
1183- // `stack.books` is produced, byte-identical, in BOTH passes and cancels in the
1184- // diff — only the ADR-0091 pair's `stack.data[]` reads can differ. Splitting
1185- // this entry into a seed-only registration is not needed for correctness; it
1186- // would only be needed if a future rule in this function read `stack.data[]`
1187- // in a way that also depended on `stack.objects` context (none does today).
1152+ // - `@objectstack/objectql`: 83 tests across 13 files fail, every one
1153+ // `security-owd-unset` (85 refusals) — the suites publish objects with
1154+ // no authored `sharingModel` through `saveMetaItem`.
1155+ // - `@objectstack/rest`: 12 tests across 3 files — the same owd-unset
1156+ // class, PLUS two genuine CONTRACT collisions no fixture edit can
1157+ // honestly settle: `meta-object-owd-gate.test.ts` pins #7674's ADR-0094
1158+ // 403 `owd_external_wider` door, which this 422 gate now PREEMPTS for
1159+ // the same defect (`saveMetaItem` runs this table first), and it pins
1160+ // that a write with NO OWD keys at all SAVES (ADR-0094 reads absence as
1161+ // the D1 `private` default) — which `security-owd-unset` exists to
1162+ // refuse (absence must be an authored decision).
1163+ //
1164+ // So declaring `object` is not a wiring fix and not even only fixture
1165+ // repair in two packages outside this card's surface: it is a decision
1166+ // about which door answers for OWD defects (403 ADR-0094 vocabulary vs
1167+ // 422 lint vocabulary) and whether an unauthored OWD refuses at runtime.
1168+ // That decision is escalated on #8310; until it is ruled, `object` stays
1169+ // undeclared and the pins in
1170+ // `validate-security-posture.runtime-surface.test.ts` record both what
1171+ // WOULD happen (via the gate's own snapshot builder) and that it does not.
1172+ //
1173+ // `security-role-word` is NOT in this entry any more — that is what the
1174+ // `validateSecurityRoleWord` entry below records. It judges six collections
1175+ // (objects, fields, actions, permission sets, positions, apps — plus books),
1176+ // and `positions`/`apps` are neither carried by the per-write snapshot nor
1177+ // mapped in `TYPE_TO_STACK_KEY`, so declaring `permission`/`book` on a
1178+ // function that still contained it would have enforced ONE rule id for a
1179+ // strict subset of its collections: a door where a permission set named
1180+ // `role_manager` is refused and a position named `sales_role` walks through
1181+ // — the #7220 failure this table refuses to build, in either direction. The
1182+ // rule therefore stays behind WHOLE (#8310's explicit call), as its own
1183+ // entry.
1184+ //
1185+ // This entry remains the rest of the D7 block (12 rule ids) as ONE
1186+ // registration, not a per-rule split: the baseline/candidate differential is
1187+ // what keeps a write of one declared type from leaking the other rules'
1188+ // whole-stack findings — every finding derived from a sibling collection is
1189+ // produced byte-identically in both passes and cancels in the diff. Only
1190+ // findings the written item itself adds are attributed to the write.
11881191 {
11891192 name : 'validateSecurityPosture' ,
11901193 tier : 'gating' ,
11911194 input : 'parsed' ,
11921195 commands : ALL ,
11931196 source : 'packages/lint/src/validate-security-posture.ts' ,
11941197 surfaces : CLI_AND_RUNTIME ,
1195- runtimeTypes : [ 'seed' ] ,
1198+ runtimeTypes : [ 'seed' , 'permission' , 'book' ] ,
11961199 run : ( stack ) => validateSecurityPosture ( stack ) ,
11971200 } ,
1201+ // [ADR-0090 D3 / #8310] The vocabulary freeze, split out of
1202+ // `validateSecurityPosture` the day the rest of that block crossed the
1203+ // runtime wall — so that it could stay behind WHOLE rather than cross for
1204+ // three of the six collections it judges (#7220: one rule id must sit on ONE
1205+ // side of the wall). The split is a surface boundary, not taste: the rule's
1206+ // verdict and findings are byte-identical to before on every CLI command
1207+ // (both entries run on all three), and the runtime door does not run it for
1208+ // ANY type.
1209+ //
1210+ // The road to crossing is concrete and short, recorded here so the next
1211+ // seat prices it correctly: carry `positions`/`apps` in
1212+ // `RuntimeStackContext` + `CONTEXT_STACK_KEYS`, map both types in
1213+ // `TYPE_TO_STACK_KEY` (both are `allowRuntimeCreate: true`, so the writes
1214+ // are real), then declare `runtimeTypes: ['object', 'permission', 'book',
1215+ // 'position', 'app']` on THIS entry — all six collections in one edit, the
1216+ // #7220 discipline satisfied.
1217+ {
1218+ name : 'validateSecurityRoleWord' ,
1219+ tier : 'gating' ,
1220+ input : 'parsed' ,
1221+ commands : ALL ,
1222+ source : 'packages/lint/src/validate-security-posture.ts' ,
1223+ surfaces : CLI_ONLY ,
1224+ surfaceReason :
1225+ 'P2 (#4463)/#8310: judges six collections (objects, fields, actions, permission sets, ' +
1226+ 'positions, apps — plus books), and the per-write snapshot neither carries nor maps ' +
1227+ 'positions/apps. Wiring it for the mapped types alone would enforce one rule id for three of ' +
1228+ 'its six collections — the #7220 split (an object named sales_role refused while a position ' +
1229+ 'named sales_role walks through). It crosses whole — positions/apps carried, mapped and ' +
1230+ 'declared — or stays behind; it stays behind until that wiring exists.' ,
1231+ run : ( stack ) => validateSecurityRoleWord ( stack ) ,
1232+ } ,
11981233 // ADR-0105 D6 — the org tree is a REPORTING dimension. An RLS policy or
11991234 // sharing rule that walks it builds a second permission hierarchy (the
12001235 // dual-hierarchy mistake ADR-0057 D5 retired) and cannot widen Layer 0 anyway,
0 commit comments