Skip to content

feat(spec): govern sharing_rule — seed its liveness ledger and pay the first of #18582's three debts - #18587

Merged
os-bill merged 2 commits into
mainfrom
claude/issue-18582-sharing-rule-liveness-ledger
Sep 17, 2026
Merged

os-bill merged 2 commits into
mainfrom
claude/issue-18582-sharing-rule-liveness-ledger

Conversation

@os-bill

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

Copy link
Copy Markdown
Collaborator

Part of #18582

Clause-②: no

Seeds packages/spec/liveness/sharing_rule.json and moves sharing_rule out of PENDING_GOVERNANCE into GOVERNED. This is one of the three debts on that card; connector and analytics_cube stay on it, which is why the first line is Part of and 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:

before (origin/main @ bb9794a026)
  governance denominator: 30 authorable type(s) — 26 registered kind(s) + 4 unregistered-kind
  stack collection(s) (analytics_cube, connector, sharing_rule, webhook); 27 governed,
  3 awaiting a ledger (analytics_cube, connector, sharing_rule) — a worklist, not a merge gate.
    (+ 9 type(s) governed from OUTSIDE the denominator via SPEC_ONLY_SCHEMAS … 36 governed in total.)

after (this branch)
  governance denominator: 30 authorable type(s) — 26 registered kind(s) + 4 unregistered-kind
  stack collection(s) (analytics_cube, connector, sharing_rule, webhook); 28 governed,
  2 awaiting a ledger (analytics_cube, connector) — a worklist, not a merge gate.
    (+ 9 type(s) governed from OUTSIDE the denominator via SPEC_ONLY_SCHEMAS … 37 governed in total.)

The other counters move the way a seeded type should move them, and one of them deliberately does not:

counter before after
classified properties (no sharing_rule row) sharing_rule 17 classified (live 16, planned 1)
repo-local evidence paths resolved 505 / 505 524 / 524
path#symbol anchors resolved 583 / 583 617 / 617
key-mention pairs anchored 504 of 505 asked, 1 exempt 523 of 524 asked, 1 exempt
live entries citing a PRODUCER 39 48
container coverage (undrilled child keys) 113 containers / 578 keys 113 containers / 578 keys

The last row is the point of drilling sharedWith: the new type adds zero rows to undrilled-containers.baseline.json.

The classification, and the one row that is not live

Ten authored keys (plus the seven ADR-0010 envelope fields the gate auto-classifies). Nine are live; type is planned.

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 the sys_sharing_rule row canonical and bootstrapDeclaredSharingRules translates each authored key into it at boot — nothing re-parses SharingRuleSchema at enforcement time. So a consumer pointer alone would prove only that a column is read, never that the authored value reaches it. That is the seed.env shape (#4837) applied to a whole type rather than to one key.

type is planned, deliberately neither live nor dead. SharingRuleType has exactly one member and CriteriaSharingRuleSchema pins the key as z.literal('criteria'), so every value an author can write is the same value. Its only reader in this repo is a defensive if (r.type === 'owner') in the seeder, unreachable for anything the schema admits (owner rules left the authoring surface with ADR-0078).

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/lint validators and objectui's create-door client validation; searching that population for r.type / rule.type returns the one defensive comparison and nothing else, while the same search for r.condition / rule.condition returns hits in three of them.

Preview read points enumerated, per the #7131 mechanical ruleregisterBuiltinPreviews() (objectui @dda8f381) registers twenty types and sharing_rule is 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.mts rows and a changeset, with liveness/README.md read-only. Three files outside that surface had to move, because the gates read them off GOVERNED and off the ledger set:

  1. packages/spec/liveness/README.mdreconcileReadmeTable fails on a GOVERNED type with no row in the "Current state" table, and the heading's N governed types is checked three ways against the rows and against GOVERNED.length. A check:liveness that 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.
  2. packages/spec/liveness/state-counts.md — generated, merge=os-regen, proved fresh by the same gate. Regenerated with pnpm --filter @objectstack/spec gen:liveness-counts, never hand-edited.
  3. docs/qa/platform-checklist/coverage.json — this ratchet derives its universe from packages/spec/liveness/*.json, so seeding a ledger is exactly what makes the kind UNCLASSIFIED there (check:platform-checklist exit 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's files[] includes liveness, 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:

dispatch-gates --ran: 65 derived famil(ies) accounted for — 63 run, 2 NOT-MEASURED
  (2 DERIVED from a recorded exit 3), 0 UNRUN.
  • check:liveness exits 0, with the coverage line above.
  • check:generatedexit 0 on a built tree (15 of 15 artifacts current). It exits 1 on an unbuilt one and says so itself; packages/spec was 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-gates exit 0 (1746 self-test cases; the battery takes ~520s on this box).
  • packages/spectypecheck exit 0, test exit 0 (483 files / 13780 tests). The liveness script suites the edited gate owns, plus every test whose text names check-liveness, run as their own slice: 13 files / 378 tests, exit 0.
  • NOT MEASURED (2)check:dual-build-cjs-loads and check:lean-entry-closure, both exit 3 · PREREQUISITE NOT MET: they read a whole-repo dist/ this container has not built. Neither pass nor finding. CI builds the closure.
  • Known and already filed, not a finding herecheck:cross-package-test-inputs exits 1 once packages/spec has 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-inputs answers 1 or 0 depending on whether packages/spec has 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 names packages/spec/dist/ and packages/cli/test/init-created-files-summary.e2e.test.ts, neither of which this diff touches.
  • pnpm lint is CI's whole-repo run. The narrowing here is a measurement, not a skip: eslint's own files declaration 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 json reports 1 file, 0 errors, 0 warnings. eslint.config.mjs states in its own prose that this repo "never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) 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 at 2f932b09.

Acceptance notes

Noted, not filed, and one to file:

  • To file — the four sharing-related entries in packages/spec/scripts/liveness/proof-registry.mts record a blockedReason whose premise this PR falsifies: bu-hierarchy-sharing, sharing-rule-criteria-required, declarative-rbac-seeding and sharing-rule-org-scoped-listing each 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-seeding in particular authors sharingRules[] on the showcase stack and asserts the seeded row's object_name, recipient_type, recipient_id and translated criteria_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, and sharing-rule-criteria-required is the counter-example that makes it a judgement rather than a sweep — it POSTs the runtime body to /sharing/rules and never authors the spec key, so it must not bind condition.
  • Noted, not filed — 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 generated state-counts.md or the platform-checklist coverage entry, all four of which the gates now require. The recipe predates those three checks. Whoever seeds connector or analytics_cube next will walk into the same three red gates this PR did; that seat is the one that will carry it.

Generated by Claude Code

…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>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 3 changed file(s) yielded no anchor (packages/spec/liveness/README.md, packages/spec/liveness/sharing_rule.json, packages/spec/liveness/state-counts.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/spec/liveness/README.md, packages/spec/liveness/sharing_rule.json, packages/spec/liveness/state-counts.md) — pages documenting those are invisible to this run
  • 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 — 136 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 e46ac0fb659f2ff35955bec844fd393754f864e2packageMentionDocs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling labels Sep 17, 2026
@os-bill
os-bill marked this pull request as ready for review September 17, 2026 06:10
@os-bill
os-bill added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit e0d0553 Sep 17, 2026
37 checks passed
@os-bill
os-bill deleted the claude/issue-18582-sharing-rule-liveness-ledger branch September 17, 2026 06:32
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants