feat(spec): govern sharing_rule — seed its liveness ledger and pay the first of #18582's three debts - #18587
Merged
Conversation
…e first of three PENDING_GOVERNANCE debts `sharing_rule` moves out of `PENDING_GOVERNANCE` and into `GOVERNED` with a seeded `packages/spec/liveness/sharing_rule.json`: every authorable key of `SharingRuleSchema` classified against a reading of what actually consumes it. The shape fact that decides every row: the authoring shape is NOT the enforced shape. ADR-0057 D6 makes the `sys_sharing_rule` row canonical and `bootstrapDeclaredSharingRules` translates each authored key into it at boot, so every consumer reads a COLUMN a producer had to populate. Each row therefore carries a `producer` (#4837) naming the threading site rather than a consumer pointer alone. 9 live + 1 planned over 10 authored keys (17 classified with the ADR-0010 envelope). `type` is the one non-live row: the `SharingRuleType` discriminator has exactly one member and its only reader is a defensive `=== 'owner'` comparison unreachable for every value the schema admits — `planned` on the `action.operation` precedent, and deliberately not an enforce-or-remove candidate. `sharedWith` is drilled, so the change adds zero rows to the undrilled-container baseline. The README state table, its heading count and the generated `state-counts.md` move with `GOVERNED` because `check:liveness` reconciles all three; the `#18133` note over the map records the paid debt instead of leaving a stale "three" standing over a map of two. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
… this type forces Two files the ledger's own gates demand, neither of them optional: - `.changeset/18582-sharing-rule-liveness-ledger.md` — `packages/spec`'s `files[]` includes `liveness`, so a new ledger IS published; this is a `patch`, not a `skip-changeset` diff. - `docs/qa/platform-checklist/coverage.json` — the capability ratchet DERIVES its universe from `packages/spec/liveness/*.json`, so seeding a ledger is what makes the kind UNCLASSIFIED there. Mapped to the three items that already exercise the surface (`sharing-rules-widen`, `sharing-rule-authoring-ui`, `record-share-grant-revoke`) rather than waived — no item is authored here, and a waiver would have been false. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
Contributor
📓 Docs Drift Check
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
This was referenced Sep 17, 2026
os-bill
marked this pull request as ready for review
September 17, 2026 06:10
os-bill
pushed a commit
that referenced
this pull request
Sep 17, 2026
…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
pushed a commit
that referenced
this pull request
Sep 17, 2026
…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>
This was referenced Sep 17, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #18582
Clause-②: no
Seeds
packages/spec/liveness/sharing_rule.jsonand movessharing_ruleout ofPENDING_GOVERNANCEintoGOVERNED. This is one of the three debts on that card;connectorandanalytics_cubestay on it, which is why the first line isPart ofand not a closing keyword.What the coverage line says, before and after
PR #18581 made this line print on every run precisely so a change like this is visible. Measured on this branch:
The other counters move the way a seeded type should move them, and one of them deliberately does not:
sharing_rulerow)sharing_rule 17 classified (live 16, planned 1)path#symbolanchors resolvedThe last row is the point of drilling
sharedWith: the new type adds zero rows toundrilled-containers.baseline.json.The classification, and the one row that is not
liveTen authored keys (plus the seven ADR-0010 envelope fields the gate auto-classifies). Nine are
live;typeisplanned.Every row carries a
producer, and that is the substance of the work. The authoring shape is not the enforced shape: ADR-0057 D6 makes thesys_sharing_rulerow canonical andbootstrapDeclaredSharingRulestranslates each authored key into it at boot — nothing re-parsesSharingRuleSchemaat enforcement time. So a consumer pointer alone would prove only that a column is read, never that the authored value reaches it. That is theseed.envshape (#4837) applied to a whole type rather than to one key.typeisplanned, deliberately neitherlivenordead.SharingRuleTypehas exactly one member andCriteriaSharingRuleSchemapins the key asz.literal('criteria'), so every value an author can write is the same value. Its only reader in this repo is a defensiveif (r.type === 'owner')in the seeder, unreachable for anything the schema admits (ownerrules left the authoring surface with ADR-0078).live— nothing dispatches on it; a comparison against a value the schema rejects is Prime Directive chore: version packages #10'scaselabel with no reachable call site;dead— the key is required, so it is not a silent no-op an author can get wrong, and the schema header records the intent (kept as the discriminant so a future enforced rule type re-joins as a union member). Marking itdeadwould put a required literal on the ADR-0049 enforce-or-remove worklist, where removing it breaks every authored rule to delete nothing.action.operation, the same shape — a one-member discriminator heldplanneduntil a runtime half dispatched on it, then flipped tolive(spec liveness: flipActionSchema.operation/patchfromplannedtoliveonce the runtime executor lands, evidence anchored on the runtime reads (follow-up of #14092) #15080).The negative rests on a census with a lit control, not a bare grep: the population that reads a declared rule item is the seeder, four
packages/lintvalidators and objectui's create-door client validation; searching that population forr.type/rule.typereturns the one defensive comparison and nothing else, while the same search forr.condition/rule.conditionreturns hits in three of them.Preview read points enumerated, per the #7131 mechanical rule —
registerBuiltinPreviews()(objectui @dda8f381) registers twenty types andsharing_ruleis not one of them. Recorded in the ledger rather than skipped, because "the type has no registered preview" is the sentence a later sweep needs. What objectui does consume is the whole shape, on the CREATE door only (AUTHOR_SHAPE_ONLY_TYPES).One decoy named so the next census does not trip on it: objectui's own
SharingRuleConfig(packages/types/src/permissions.ts) is a different shape entirely, re-exported twice and read by nothing. It matches this type by name only.File surface — three deviations, all mechanically forced, none discretionary
The dispatch scoped this to the ledger, the two
check-liveness.mtsrows and a changeset, withliveness/README.mdread-only. Three files outside that surface had to move, because the gates read them offGOVERNEDand off the ledger set:packages/spec/liveness/README.md—reconcileReadmeTablefails on aGOVERNEDtype with no row in the "Current state" table, and the heading'sN governed typesis checked three ways against the rows and againstGOVERNED.length. Acheck:livenessthat exits 0 and a read-only README are not both reachable. Row written by measurement; heading 36 → 37; the tail paragraph that described the map as holding three debts now says which one is paid.packages/spec/liveness/state-counts.md— generated,merge=os-regen, proved fresh by the same gate. Regenerated withpnpm --filter @objectstack/spec gen:liveness-counts, never hand-edited.docs/qa/platform-checklist/coverage.json— this ratchet derives its universe frompackages/spec/liveness/*.json, so seeding a ledger is exactly what makes the kindUNCLASSIFIEDthere (check:platform-checklistexit 1, reproduced before and after). Mapped to the three items that already exercise the surface —access-security.sharing-rules-widen,access-security.sharing-rule-authoring-ui,access-security.record-share-grant-revoke— rather than waived. No checklist item is authored here, and a waiver would have been false.A changeset is owed and is not
skip-changeset:packages/spec'sfiles[]includesliveness, so the new ledger ships inside the tarball.patch.Gates
Derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, run with each exit code landed on disk before being read, then reconciled with--ran:check:livenessexits 0, with the coverage line above.check:generated— exit 0 on a built tree (15 of 15 artifacts current). It exits 1 on an unbuilt one and says so itself;packages/specwas built under the shared verify lock before the verdict was read.check:platform-checklist— exit 1 before the coverage entry, exit 0 after:36 kinds mapped, 1 waived.check:pm-dispatch-gatesexit 0 (1746 self-test cases; the battery takes ~520s on this box).packages/spec—typecheckexit 0,testexit 0 (483 files / 13780 tests). The liveness script suites the edited gate owns, plus every test whose text namescheck-liveness, run as their own slice: 13 files / 378 tests, exit 0.check:dual-build-cjs-loadsandcheck:lean-entry-closure, bothexit 3 · PREREQUISITE NOT MET: they read a whole-repodist/this container has not built. Neither pass nor finding. CI builds the closure.check:cross-package-test-inputsexits 1 oncepackages/spechas been built and 0 when it has not ([finding] check:cross-package-test-inputs passes in CI and fails on a built tree — its verdict is a function of gitignored build state #18353 / [finding]check:cross-package-test-inputsanswers 1 or 0 depending on whetherpackages/spechas been BUILT — the author who follows AGENTS.md is the only one who sees the red, and CI never does #18440). Recorded in both states; its message namespackages/spec/dist/andpackages/cli/test/init-created-files-summary.e2e.test.ts, neither of which this diff touches.pnpm lintis CI's whole-repo run. The narrowing here is a measurement, not a skip: eslint's ownfilesdeclaration is**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}, so five of the six changed files (.md,.json) are outside its universe by that declaration; the sixth,packages/spec/scripts/liveness/check-liveness.mts, linted clean —--format jsonreports 1 file, 0 errors, 0 warnings.eslint.config.mjsstates in its own prose that this repo "never enables type-aware linting (noparserOptions.project, no typed@typescript-eslintrules) for ANY file", so nothing in this diff can move the verdict on a file it does not touch. Gate numbers are quoted from runs at2f932b09.Acceptance notes
Noted, not filed, and one to file:
packages/spec/scripts/liveness/proof-registry.mtsrecord ablockedReasonwhose premise this PR falsifies:bu-hierarchy-sharing,sharing-rule-criteria-required,declarative-rbac-seedingandsharing-rule-org-scoped-listingeach say sharing rules are "authored at STACK level, which is not a governed metadata type … so there is no ledger entry to ratchet". There is one now.showcase-declarative-rbac-seedingin particular authorssharingRules[]on the showcase stack and asserts the seeded row'sobject_name,recipient_type,recipient_idand translatedcriteria_json— i.e. it exercises five of these keys end to end and is a genuine ADR-0054 binding candidate. ⛔ Deliberately not done here: binding a high-risk class is a separate ADR-0054 §3 act, one class at a time, andsharing-rule-criteria-requiredis the counter-example that makes it a judgement rather than a sweep — it POSTs the runtime body to/sharing/rulesand never authors the spec key, so it must not bindcondition.packages/spec/liveness/README.md's "Adding a type" recipe lists four steps and none of them is the README row, the heading count, the generatedstate-counts.mdor the platform-checklist coverage entry, all four of which the gates now require. The recipe predates those three checks. Whoever seedsconnectororanalytics_cubenext will walk into the same three red gates this PR did; that seat is the one that will carry it.Generated by Claude Code