Commit ac720a9
fix(spec): re-read four sharing proof-registry reasons now that
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>sharing_rule is governed (#18797)1 parent 182bbde commit ac720a9
1 file changed
Lines changed: 44 additions & 15 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
262 | 262 | | |
263 | 263 | | |
264 | 264 | | |
265 | | - | |
266 | | - | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
267 | 271 | | |
268 | 272 | | |
269 | 273 | | |
| |||
468 | 472 | | |
469 | 473 | | |
470 | 474 | | |
471 | | - | |
472 | | - | |
473 | | - | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
474 | 484 | | |
475 | 485 | | |
476 | 486 | | |
| |||
488 | 498 | | |
489 | 499 | | |
490 | 500 | | |
491 | | - | |
492 | | - | |
493 | | - | |
494 | | - | |
495 | | - | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
496 | 510 | | |
497 | 511 | | |
498 | 512 | | |
| |||
506 | 520 | | |
507 | 521 | | |
508 | 522 | | |
509 | | - | |
510 | | - | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
511 | 535 | | |
512 | 536 | | |
513 | 537 | | |
| |||
719 | 743 | | |
720 | 744 | | |
721 | 745 | | |
722 | | - | |
723 | | - | |
724 | | - | |
| 746 | + | |
| 747 | + | |
| 748 | + | |
| 749 | + | |
| 750 | + | |
| 751 | + | |
| 752 | + | |
| 753 | + | |
725 | 754 | | |
726 | 755 | | |
727 | 756 | | |
| |||
0 commit comments