Skip to content

fix(spec): re-read four sharing proof-registry reasons now that sharing_rule is governed - #18797

Merged
os-bill merged 1 commit into
mainfrom
claude/issue-18589-proof-registry-stale-blocked-reason
Sep 17, 2026
Merged

os-bill merged 1 commit into
mainfrom
claude/issue-18589-proof-registry-stale-blocked-reason

Conversation

@os-bill

@os-bill os-bill commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

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

…falsified

`sharing_rule` became a governed metadata type when #18587 seeded
packages/spec/liveness/sharing_rule.json, so the four sharing-related
`blockedReason` entries in proof-registry.mts — plus one comment on
`rls-check-post-image` carrying the same sentence — were recording a reason that
had stopped being true.

Each entry is re-read against what its proof ACTUALLY exercises, not swept:

- bu-hierarchy-sharing, sharing-rule-org-scoped-listing and
  sharing-rule-criteria-required never author the spec shape (they call
  SharingRuleService.defineRule on the booted kernel, or POST a runtime body to
  /api/v1/sharing/rules), so they stay unbound — for a reason that is true.
- declarative-rbac-seeding DOES author it (showcase defineSharingRule →
  bootstrapDeclaredSharingRules → the asserted sys_sharing_rule row), so it is
  recorded as a real ADR-0054 §3 binding candidate and deferred to that separate
  act: adoption is a ledger act, since every cited row must carry `proof`.

No `bound` flag and no `ledgerBindings` change; no published bytes move.

Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
Co-authored-by: Claude <noreply@anthropic.com>
@os-bill os-bill added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 17, 2026 — with Claude
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 9846f2763c2bb17cbd115643ba4f2086bf832538packageMentionDocs.

@os-bill
os-bill marked this pull request as ready for review September 17, 2026 21:59
@os-bill
os-bill added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit ac720a9 Sep 17, 2026
41 checks passed
@os-bill
os-bill deleted the claude/issue-18589-proof-registry-stale-blocked-reason branch September 17, 2026 22:19
os-bill pushed a commit that referenced this pull request Sep 18, 2026
…e sharing_rule ledger note

The `_note` of `packages/spec/liveness/sharing_rule.json` quoted the
`declarative-rbac-seeding` entry's `blockedReason` VERBATIM. PR #18797
(`ac720a9865`) rewrote that reason, so the quoted string stopped existing
in the very file the note sends a reader to.

The substance was never wrong — the seeding does falsify the entry's
original premise — so this replaces the quotation rather than the
judgement: cite the registry and the stable `declarative-rbac-seeding`
id, state the substance in the note's own words, and quote nothing.

Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate tooling

Projects

None yet

2 participants