Skip to content

Commit ac720a9

Browse files
os-billclaude
andauthored
fix(spec): re-read four sharing proof-registry reasons now that sharing_rule is governed (#18797)
Fixes #18589 Clause-②: no ## What was wrong `packages/spec/scripts/liveness/proof-registry.mts` recorded four `blockedReason` entries whose stated reason rested on one premise: sharing rules are authored at STACK level and `sharing_rule` is not a governed metadata type, so there is no ledger entry to ratchet. PR #18587 (landed as `e0d05538c0`) seeded `packages/spec/liveness/sharing_rule.json` and put `sharing_rule` in the gate's `GOVERNED` array — which makes that premise false. A recorded WHY that has silently stopped being true is one level up from the keys this ledger governs. A **fifth carrier the card did not enumerate** is fixed in the same pass, because the DARK control is a file-level reading: the `rls-check-post-image` entry's code comment carried the same sentence ("stack-level sharing rules are not a governed metadata type, so only `check` binds"). ## What changed — per entry, read against what each proof ACTUALLY exercises No `bound` flag and no `ledgerBindings` entry changes. This PR changes recorded reasons only. | entry | verdict | evidence | |---|---|---| | `bu-hierarchy-sharing` | stays unbound — new reason | the proof calls `stack.kernel.getService('sharingRules').defineRule({… criteria, recipientType, recipientId …}, SYS)`: the RUNTIME column shape. `SharingRuleSchema` and `bootstrapDeclaredSharingRules` are not on its path, so no authorable `sharing_rule.*` key is written. Binding `sharedWith.type` here would be the owner-anchor/allowTransfer mistake. | | `sharing-rule-criteria-required` | stays unbound — and must NOT bind `condition` | it POSTs a runtime body to `/api/v1/sharing/rules`. Its own header states the mechanism: "The endpoint plucks its body field-by-field into `SharingRuleService.defineRule`; `SharingRuleSchema` is never on that path." The ledger coordinate now exists; this proof is still not evidence for it. | | `declarative-rbac-seeding` | stays unbound — recorded as a REAL binding candidate | the showcase authors the rules through `defineSharingRule` (`examples/app-showcase/src/security/sharing-rules.ts`: `condition`, `sharedWith: { type, value }`, `object`, `name`), `bootstrapDeclaredSharingRules` seeds them, and the proof asserts the landed row (`object_name`, `recipient_type`, `recipient_id`, and the CEL to `criteria_json` translation). Adoption is a separate ADR-0054 §3 act — see "Why the binding is not in this PR". | | `sharing-rule-org-scoped-listing` | stays unbound — new reason | fixtures are created over `POST /sharing/rules` (the criteria-required shape), and what the file pins is a READ-SCOPE filter inside `SharingRuleService`, not the behaviour of any authored key. | | `rls-check-post-image` (comment) | unchanged binding | `sharing_rule.condition` IS a governed entry since #18587, so that half is no longer un-bindable for want of a coordinate; only `check` binds here because adopting it is its own ADR-0054 §3 act with its own candidate question. | ## Why the binding is not in this PR `declarative-rbac-seeding` is a real candidate, and adopting it is a **ledger act**, not a registry act: `BOUND_PROOF_PATHS` makes `check-liveness.mts` require the matching `proof` on every cited `sharing_rule.json` row (`report.proofMissing`), and `proof-registry.test.ts`'s wiring suite asserts the same from the other side (it also needs a `sharing_rule` row in its `ledgerFor` map). `packages/spec/liveness/sharing_rule.json` deliberately claims `proof` on no row — its own `_note`: "No `proof` is claimed on any row here: binding a high-risk class is a separate ADR-0054 §3 act, one class at a time, and it is filed rather than slipped in." That file is read-only under this card's declared file surface, and WHICH of the five exercised props the class owns is a decision of its own (`condition` is also exercised by `showcase-d3-d4-capabilities`). Reported for filing instead. ## The ledger reading the seat could not verify The card's "17 classified (16 live, 1 planned)" is the GATE's count, and it is correct as such — but it is **not** the number of authored rows. Read first-hand from `packages/spec/liveness/sharing_rule.json` and from `check-liveness.mts --dump sharing_rule`: - **10 classified rows are authored in the ledger file**: 9 `live` (`name`, `label`, `description`, `object`, `active`, `accessLevel`, `sharedWith.type`, `sharedWith.value`, `condition`) + **1 `planned`** (`type`, the one-member `SharingRuleType` discriminator). - The walk adds **7 framework envelope fields** that carry no ledger row and are auto-classified `live` by `FRAMEWORK_FIELDS` (`_lock`, `_lockReason`, `_lockSource`, `_lockDocsUrl`, `_provenance`, `_packageId`, `_packageVersion`). - 10 + 7 = **17 classified, live 16, planned 1** — exactly what `pnpm --filter @objectstack/spec check:liveness` prints. ## Acceptance controls **LIT — the premise really is false** (symbol/array membership, not a substring grep): parsing the `GOVERNED` symbol out of `check-liveness.mts` reads length **39**, `includes('sharing_rule')` **true** at index **36**; negative controls `sharing_rules` / `sharing` / `not_a_metadata_type` all read **false**. The gate's own runtime leg agrees: it prints `sharing_rule` in "governed types:" and emits the per-type row `sharing_rule 17 classified (live 16, planned 1)`, which only exists because the loop iterates `GOVERNED`. **DARK — the assertion now reads 0, with a non-zero control.** The predicate folds the TypeScript string-concatenation seams (`' + '`) before matching, because the reasons are split across source literals mid-phrase; a line-oriented predicate reads a false zero there. It deliberately uses no POSIX ERE bracket spelling, which is the other false-zero trap. | predicate | BASE `6de7a2d6e6` (control) | this branch | |---|---|---| | `not a governed metadata type` | 3 | 0 | | `not as a property of a governed metadata type` | 1 | 0 | | `not on a per-type authorable property` | 1 | 0 | | `no ledger entry to ratchet`, scoped to the four sharing entries | 1 | 0 | | **total** | **6** | **0** | The predicate carries its own self-test (a synthetic split-literal sample carrying all three spellings must read 3; it does, on both runs), so the zero is a measurement and not a broken regex. ## Changeset: `skip-changeset`, measured not inferred `npm pack --dry-run --json` in `packages/spec`: 275 published entries, **0** under `scripts/`, and `proof-registry.mts` is not among them. Positive control on the same reading: 41 `liveness/*.json` ledger files ARE published, so the measurement can see a spec-owned data file when one ships. This diff therefore moves zero published bytes. ## Verification Run on `968d6e0a55`, in a dedicated worktree: - `pnpm --filter @objectstack/spec test` — 486 files, **14015 passed**, 1 skipped. - `pnpm --filter @objectstack/spec exec vitest run scripts/liveness/proof-registry.test.ts` — 39 passed (the registry-invariant and wiring suite). - `pnpm --filter @objectstack/spec typecheck` — exit 0 (`tsc --noEmit` + `check:scripts-typecheck` + `check:test-typecheck`). - `pnpm --filter @objectstack/spec check:liveness` — exit 0; counts unchanged. - `pnpm lint` (repo-wide `eslint . --no-inline-config`) — exit 0. Full population, no narrowing to declare. - The gate families derived by `node scripts/pm/dispatch-gates.mjs --commands` for this diff: **46 of 50 green**, including `check:nul-bytes`, `check:published-files`, `check:cross-package-test-inputs`, `check:test-source-alias`, `check:pm-governed-merges`, `check:adr-0087-registration`. - **NOT MEASURED (4)**: `check:dts-closure`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:sourcemap-no-sources-content` — each exits **3, PREREQUISITE NOT MET** ("nothing was swept … NOT a pass and NOT a finding") because a fresh worktree has no `dist/` for any of the 81 packages. They read built output repo-wide; this diff changes a liveness script that is in no package's build inputs and in no `files[]`. Declared to CI, where the closure is built. ## Acceptance notes (noted, not filed) - `bootstrapDeclaredSharingRules` threads `label: r.label ?? r.name`, so an unauthored `label` stores the rule NAME rather than staying empty. That is the ledger's recorded behaviour for the `label` row, not a defect — noted only because it is the sort of thing a future binding decision touches. Successor: none — no PR or person is heading into that file for this reason. --- _Generated by [Claude Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 182bbde commit ac720a9

1 file changed

Lines changed: 44 additions & 15 deletions

File tree

‎packages/spec/scripts/liveness/proof-registry.mts‎

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -262,8 +262,12 @@ export const HIGH_RISK_CLASSES: HighRiskClass[] = [
262262
proofRef: 'packages/qa/dogfood/test/showcase-d3-d4-capabilities.dogfood.test.ts#showcase-d3-d4-capabilities',
263263
bound: true,
264264
// The same file also pins the ADR-0058 D3 compound sharing `condition`
265-
// (`&&`), which silently skipped the AND before #1887 — but stack-level
266-
// sharing rules are not a governed metadata type, so only `check` binds.
265+
// (`&&`), which silently skipped the AND before #1887. That half is no
266+
// longer un-bindable for want of a coordinate: `sharing_rule` was seeded
267+
// into the ledger by #18587, so `sharing_rule.condition` is a governed
268+
// entry. Only `check` binds HERE because adopting that one is a separate
269+
// ADR-0054 §3 act with its own candidate question — `declarative-rbac-seeding`
270+
// authors and asserts the same key end to end (#18589).
267271
ledgerBindings: [{ type: 'permission', path: 'rowLevelSecurity.check' }],
268272
},
269273
{
@@ -468,9 +472,15 @@ export const HIGH_RISK_CLASSES: HighRiskClass[] = [
468472
bound: false,
469473
ledgerBindings: [],
470474
blockedReason:
471-
'sharing rules are authored at STACK level (`sharingRules`), which is not a governed metadata '
472-
+ 'type — the ledger governs per-type property surfaces, and there is no `permission.*` entry '
473-
+ 'for the rule\'s recipient kind.',
475+
'this proof never AUTHORS the spec shape. It calls `SharingRuleService.defineRule` on the booted '
476+
+ 'kernel with the RUNTIME column shape — `criteria` as an already-compiled FilterCondition, '
477+
+ '`recipientType`/`recipientId` — so `SharingRuleSchema` and `bootstrapDeclaredSharingRules` are '
478+
+ 'not on its path and no authorable `sharing_rule.*` key is exercised; what it pins is the '
479+
+ 'BU-subtree expansion inside the service. Binding `sharedWith.type` to it would be the '
480+
+ 'owner-anchor/allowTransfer mistake: a proof cited for a property it does not author. '
481+
+ 'Premise corrected 2026-09-17 (#18589): the old reason rested on this type having no ledger '
482+
+ 'coordinate at all, which #18587 supplied by seeding packages/spec/liveness/sharing_rule.json. '
483+
+ 'The blocker is the proof, not the ledger.',
474484
},
475485
{
476486
id: 'sharing-rule-criteria-required',
@@ -488,11 +498,15 @@ export const HIGH_RISK_CLASSES: HighRiskClass[] = [
488498
bound: false,
489499
ledgerBindings: [],
490500
blockedReason:
491-
'same shape as `showcase-bu-hierarchy-sharing`: the criteria is authored at STACK level '
492-
+ '(`sharingRules[].condition`), not as a property of a governed metadata type, so there is no '
493-
+ 'ledger entry to ratchet. Registered so the tag is not an orphan; it runs unconditionally in '
494-
+ 'the dogfood suite. The invariant itself is recorded in the empty-state registry '
495-
+ '(sharing `condition` → `closed`), which is the surface that CAN carry it.',
501+
'the ledger coordinate now EXISTS (#18587 seeded `sharing_rule`, and `condition` is a `live` row '
502+
+ 'on it) — and this proof still must NOT bind it. It POSTs a RUNTIME body to '
503+
+ '`/api/v1/sharing/rules`, a route that reaches `SharingRuleService.defineRule` with '
504+
+ '`SharingRuleSchema` never on the path (the proof\'s own header records exactly that), so the '
505+
+ 'authorable key is never written and a binding here would fake the kind of evidence this table '
506+
+ 'exists to refuse. Registered so the tag is not an orphan; it runs unconditionally in the '
507+
+ 'dogfood suite. The invariant itself is recorded in the empty-state registry '
508+
+ '(sharing `condition` → `closed`), which is the surface that CAN carry it. '
509+
+ 'Premise corrected 2026-09-17 (#18589).',
496510
},
497511
{
498512
id: 'declarative-rbac-seeding',
@@ -506,8 +520,18 @@ export const HIGH_RISK_CLASSES: HighRiskClass[] = [
506520
bound: false,
507521
ledgerBindings: [],
508522
blockedReason:
509-
'seeding acts on STACK-level `roles`/`sharingRules` collections, not on a per-type authorable '
510-
+ 'property — same shape as bu-hierarchy-sharing.',
523+
'NOT the bu-hierarchy shape, and no longer blocked on a missing coordinate: this proof DOES '
524+
+ 'author the spec shape. The showcase declares its rules through `defineSharingRule` '
525+
+ '(examples/app-showcase/src/security/sharing-rules.ts), `bootstrapDeclaredSharingRules` seeds '
526+
+ 'them, and the proof asserts the landed row — `object_name`, `recipient_type`, `recipient_id` '
527+
+ 'and the CEL→`criteria_json` translation — i.e. `name`/`object`/`sharedWith.type`/'
528+
+ '`sharedWith.value`/`condition` end to end, every one of them a `live` row on the '
529+
+ '`sharing_rule` ledger #18587 seeded. It is a REAL binding candidate, held back only because '
530+
+ 'ADR-0054 §3 adopts one class at a time and adoption is a ledger act: each cited row must carry '
531+
+ 'the matching `proof`, which that ledger deliberately claims on no row yet, and WHICH of the '
532+
+ 'five props this class owns is a decision of its own (`condition` is also exercised by '
533+
+ '`showcase-d3-d4-capabilities`). Left to that act rather than slipped in here; #18589 reports it '
534+
+ 'for filing.',
511535
},
512536
{
513537
id: 'permission-model-zoo',
@@ -719,9 +743,14 @@ export const HIGH_RISK_CLASSES: HighRiskClass[] = [
719743
bound: false,
720744
ledgerBindings: [],
721745
blockedReason:
722-
'same shape as `showcase-bu-hierarchy-sharing` and `sharing-rule-criteria-required`: the rules '
723-
+ 'are authored at STACK level (`sharingRules`), which is not a governed metadata type, and what '
724-
+ 'this file pins is a read-scope filter inside SharingRuleService. No ledger entry to ratchet.',
746+
'the fixtures are created over the REST admin route (`POST /sharing/rules`), which reaches '
747+
+ '`SharingRuleService.defineRule` without `SharingRuleSchema` — the `sharing-rule-criteria-required` '
748+
+ 'shape, not the `declarative-rbac-seeding` one — so no authorable key is written; and what this '
749+
+ 'file pins is a READ-SCOPE filter inside SharingRuleService (which org-less rows an org-bound '
750+
+ 'admin may list), which is not the behaviour of any `sharing_rule.*` property. '
751+
+ 'Premise corrected 2026-09-17 (#18589): the old reason rested on the absence of a ledger '
752+
+ 'coordinate, which #18587 supplied. The coordinate exists — this proof is simply not evidence '
753+
+ 'for it.',
725754
},
726755
{
727756
id: 'sharing-rule-org-less-caller',

0 commit comments

Comments
 (0)