Repository navigation
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
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…— PENDING_GOVERNANCE reaches empty (objectstack-ai#18609) Fixes objectstack-ai#18582 Clause-②: no `PENDING_GOVERNANCE` reaches **empty**. `connector` and `analytics_cube` — the two debts left on this card after `sharing_rule` was paid by PR objectstack-ai#18587 — move into `GOVERNED` with a ledger each, so every authorable metadata type in the denominator objectstack-ai#18133 widened now has one. ## What was measured **`connector` — 74 properties: 20 `live`, 1 `planned`, 53 `dead`.** - **Which schema the walker really resolves** (the seat's open question 1): `getMetadataTypeSchema('connector')` returns `DeclarativeConnectorEntrySchema`, and that schema is `ConnectorSchema.superRefine(...)`. In Zod 4 a `superRefine` attaches a check to the **same object def** rather than wrapping it, so `shapeOf()` returns `ConnectorSchema`'s shape unchanged: the walked key set is byte-identical to the base's, tombstones included. **The gate cannot tell the two schemas apart.** What the entry schema buys is refusals, which are invisible to the walk and show up only on the three rows where they are the whole verdict. That difference is recorded in the ledger's `_note` and in the README row. - **One schema, two doors** is the shape fact behind the split. The ledger's denominator entry exists for the AUTHORING doors (`defineStack({ connectors })`, `PUT /meta/connector/:name`), while the same `ConnectorSchema` is what `AutomationEngine.registerConnector` parses for a def a plugin or an ADR-0097 provider factory builds in code. So a key can have a real consumer and still do nothing when a metadata author writes it — every row says which door its consumer is fed from, and every `live` row carries a `producer` (objectstack-ai#4837). - The keys an authored entry can reach are exactly the `ConnectorProviderContext` fields plus `name` and `enabled`. `type` and `icon` reach that context and are dropped by **all three** shipped provider factories (`ctx.icon` census: zero reads across `packages/connectors`, with `ctx.label` — four hits — as the lit control). `authentication` is the ledger's one `planned`: refused outright by ADR-0097 §3 (objectstack-ai#7990), never ignored. - The 53 `dead` are four declared subsystems with no engine (`syncConfig` 7, `fieldMappings` 7, `retryConfig` 8, `health` 14), `triggers` (6 — the schema's own docblock already said so, objectstack-ai#3197), the connector's nested `webhooks`, `status`, `metadata`, both timeouts, `actions.description` / `.outputSchema`, and four `retiredKey` tombstones whose rows stay because the key stays in the walked shape (the `rls.priority` precedent). **`analytics_cube` — 29 properties: 17 `live`, 12 `dead`.** - **An honest `dead` was the outcome on 12 rows** (the seat's open question 2), and none was inflated to green the gate. `cube-registry.ts` names three producers into one registry — authored cubes, compiled datasets (ADR-0021) and ad-hoc query inference — and only the first is the door this ledger governs, so a key whose only reader sits on the compiled-dataset path is not live for an authored cube. That is `dimensions.granularities` (read by `dataset-executor#granularityOf`, whose argument is a `CompiledDataset` an authored cube never becomes) and `measures.format`. - **Whether ADR-0049 wants a retirement is answered per row, and mostly the answer is no** — the ledger says so explicitly so the enforce-or-remove channel does not act on the word `dead`. `granularities` and `measures.format` are the dataset compiler's own output channel on a shared shape: deleting them breaks a live internal write. `joins[].sql` is REQUIRED and documented as the ON clause while the strategies synthesise an FK equality and never consult it — a decision, not a sweep. `public` is an access-control flag that gates nothing (three sites write `false`, nothing reads it): a knob that was never wired, not a hole that was opened. `refreshKey.every` / `.sql` are the only rows where retirement is the obvious shape, and even there the showcase example authors them. - **objectstack-ai#10238 is not prejudged.** Whether cube authoring is live end to end remains its own measurement; this ledger answers the per-key question only, and says so in the `_note`. **Two prior in-repo claims were falsified by this measurement and are corrected in the ledgers** (not in their source files — that is out of scope here, and both are filed below): 1. `packages/spec/src/conversions/registry.ts` states `retryConfig` "and the timeouts beside it are untouched — they are live". The word `retryConfig` does not occur anywhere in `packages/` or `examples/` outside `packages/spec`, and every `connectionTimeoutMs` / `requestTimeoutMs` occurrence is a WRITE of the literal 30000 so a def satisfies the post-parse type. 2. `bootstrapDeclaredWebhooks` documents itself as materializing each "stack/connector-authored webhook", while its source is `readDeclared(…, 'webhook')` — metadata items the decomposition registers from the top-level `webhooks:` collection, which a connector's nested array never becomes. ## The gate, red before and green after Both ledgers in place and both types in `GOVERNED`, before the README / counts caught up — `pnpm --filter @objectstack/spec check:liveness`, **exit 1**: ``` ✗ 2 governed type(s) with NO row in the README state table: connector analytics_cube ✗ 1 README state-table heading error(s): heading says 37 governed types, GOVERNED has 39 ✗ the generated count artifact is not current: packages/spec/liveness/state-counts.md is STALE — it does not match what the gate measures right now. first difference at line 67: - | **total** | **878** | **5** | **1** | **96** | **11** | **991** | + | `connector` | 20 | 0 | 0 | 53 | 1 | 74 | ✗ 2 row(s) where README.md and state-counts.md disagree: connector — counted in state-counts.md, no row in the README table analytics_cube — counted in state-counts.md, no row in the README table ✗ 1 UNDECLARED container inheritance — a blanket verdict covers keys nothing classified: connector/webhooks — one verdict covers 21 unclassified child key(s): … ``` That run is also the answer to the seat's warning about `liveness/README.md`: `check-liveness.mts` declares `readmeMissingRows` for exactly this, so the README rows, the heading count and `state-counts.md` are not optional extras — the gate reverse-requires them. After the README rows + heading (37 → 39), `gen:liveness-counts`, and the `connector/webhooks` row in `undrilled-containers.baseline.json` — **exit 0**: ``` governance denominator: 30 authorable type(s) — 26 registered kind(s) + 4 unregistered-kind stack collection(s) (analytics_cube, connector, sharing_rule, webhook); 30 governed, 0 awaiting a ledger. (+ 9 type(s) governed from OUTSIDE the denominator via SPEC_ONLY_SCHEMAS — 39 governed in total.) ✓ every governed-type property, at every depth the ledger drills, is classified, every authorable type — registered kind or unregistered-kind stack collection — is governed or explicitly pending, … and the README state table carries a row for each of the 39 governed type(s) it claims to index. ✓ packages/spec/liveness/state-counts.md is current — the same 39 row(s). ``` `connector/webhooks` is RECORDED in the undrilled baseline rather than deferred or drilled, and the ledger row says why: `WebhookConfigSchema` is `WebhookSchema.extend({ events, signatureAlgorithm })`, so a `deferred` row to the governed `webhook` type would be refused by the gate's key-set EQUALITY check — correctly — and drilling would mean writing 21 child rows of which 8 are the ADR-0010 protection envelope this gate auto-classifies `live` everywhere else. ## Verification | Command | Result | |---|---| | `pnpm --filter @objectstack/spec check:liveness` | exit 0 — `PENDING_GOVERNANCE` empty, 39 governed | | `pnpm --filter @objectstack/spec check:generated` | exit 0 — all 15 generated artifacts up to date | | `pnpm --filter @objectstack/spec check:authorable-surface` | exit 0 — 1536 schemas generated | | `pnpm --filter @objectstack/spec check:api-surface` | exit 0 — public API surface unchanged | | `pnpm --filter @objectstack/spec check:docs` | exit 0 — 223 generated files in sync | | `pnpm --filter @objectstack/spec typecheck` + `check:scripts-typecheck` | exit 0 | | `pnpm --filter @objectstack/spec exec vitest run scripts/liveness/` | 11 files, 315 tests passed | | `pnpm check:platform-checklist` | exit 0 — 38 kinds mapped, 1 waived | | `pnpm check:nul-bytes`, `check:published-files`, `check:merge-driver`, `check:doc-authoring`, the three changeset gates + their self-tests, `check:keyed-text-bounds`, `check:comment-mask-*`, `check:closing-keyword-parity`, `check:pm-*` | exit 0 (22 families) | `packages/spec` has no `lint` script; the repo runs one root `eslint . --no-inline-config`, so the ESLint reading here is a **declared narrowing** with its three pieces of evidence: (1) the universe comes from the config itself — the only config object with a `files` glob for source is `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`, and the seven non-`.mts` paths in this diff are `.json` / `.md`, confirmed by running ESLint on `liveness/connector.json` and getting `File ignored because no matching configuration was supplied`; (2) `--format json` on the one file this diff adds to that universe reports **1 file, 0 errors, 0 warnings**; (3) `eslint.config.mjs`'s own header states 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. The whole-farm run is CI's. Both figures are read at `a442b583fb`. **Widening tells.** `node scripts/pm/check-widening-tells.mjs --declaration no --diff DIFFPATH (this PR's diff)` exits **0 with no tell**, matching PR objectstack-ai#18587 — but its own output is the honest reading and it is not "clean": `8 changed file(s) — 0 judged against a declared surface (no widening tell), 8 NOT MEASURED`, because no declared surface covers ledger JSON, a changeset, a checklist map or a gate script. Reported as NOT MEASURED rather than as a pass. ## File surface | Path | Why | |---|---| | `packages/spec/liveness/connector.json` | new ledger (23 top-level rows, 7 drilled containers) | | `packages/spec/liveness/analytics_cube.json` | new ledger (9 top-level rows, 4 drilled containers) | | `packages/spec/scripts/liveness/check-liveness.mts` | both types into `GOVERNED`; `PENDING_GOVERNANCE` emptied; its `[objectstack-ai#18582]` note rewritten (it said "two left") | | `packages/spec/liveness/README.md` | from the pre-declared OPEN set — two state-table rows and the heading count 37 → 39, both reverse-required by `readmeMissingRows` / `readmeHeadingErrors`; the closing `PENDING_GOVERNANCE` paragraph rewritten | | `packages/spec/liveness/state-counts.md` | OPEN set — regenerated with `gen:liveness-counts`, never hand-edited | | `packages/spec/scripts/liveness/undrilled-containers.baseline.json` | one recorded row, `connector/webhooks`, reverse-required by the container-coverage leg (see above) | | `docs/qa/platform-checklist/coverage.json` | OPEN set — two entries; the map is keyed by ledger name and `check:platform-checklist` reds on an unmapped kind. Existing key order left as it was | | `.changeset/18582-connector-analytics-cube-liveness-ledgers.md` | OPEN set — `patch`, because `liveness` is in this package's published `files[]`, so both ledgers ship in the tarball | Neither `packages/spec/src/ui/view.zod.ts` (PR objectstack-ai#18561) nor `packages/spec/scripts/check-generated.ts` (objectstack-ai#17735) is touched. ## Acceptance notes Seen and deliberately not fixed here — three are findings this seat asks the dispatching seat to file, the rest are noted only: - **to file (contract violation; dedupe words: `retryConfig` live claim, connector timeouts, conversions registry comment):** `packages/spec/src/conversions/registry.ts`'s `connector-rate-limit-config-removed` entry asserts `retryConfig` "and the timeouts beside it are untouched — they are live". Measured false; the comment is what a later reader will trust. - **to file (contract violation; dedupe words: `bootstrapDeclaredWebhooks` docblock, connector-authored webhook, sys_webhook source):** the materializer's docblock claims it materializes each "stack/connector-authored webhook"; its source is `readDeclared(…, 'webhook')`, which a connector's nested array never reaches. - **to file (metadata-authoring trap; dedupe words: `analytics_cube` joins sql ON clause, synthesised FK equality, cube join relationship):** `Cube.joins[].sql` is REQUIRED and documented as the join's ON clause, and both strategies synthesise `ON "parent"."seg" = "alias"."id"` without reading it, so a non-FK join condition returns a 200 carrying different arithmetic than the author declared. `joins[].relationship` is the same shape one key over. - **noted, not filed:** `packages/spec/docs/SYNC_ARCHITECTURE.md` still ticks "✅ Monitoring: Health checks, metrics, logging" and "✅ Conflict Resolution: Multiple strategies" at L3, both unbacked on this surface — the `health` and `syncConfig` subtrees are dead. Carrier: the next PR that acts on the `syncConfig` / `health` ADR-0049 decision; that file is the one an author reads before writing either block. - **noted, not filed:** `analytics_cube.public` is an access-control key that gates nothing. Not filed separately because the ledger row IS the record and the remedy is the ADR-0049 decision the `dead` verdict opens. Carrier: the enforce-or-remove sweep that reads this ledger. - **noted, not filed:** `Metric.name` / `Dimension.name` are required inner fields shadowed by their record key, so a disagreement is silently resolved in the key's favour. Carrier: none — no PR and no person is near these files today; recorded here so a later sweep does not have to re-derive it. --- _Generated by [Claude Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…ing_rule` is governed (objectstack-ai#18797) Fixes objectstack-ai#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 objectstack-ai#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 objectstack-ai#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>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…e sharing_rule ledger note (objectstack-ai#18994) Fixes objectstack-ai#18801 Clause-②: no ## What was wrong The `_note` of `packages/spec/liveness/sharing_rule.json` quoted the `declarative-rbac-seeding` proof-registry entry's `blockedReason` **verbatim**, and named the file to find it in. PR objectstack-ai#18797 (`ac720a9865`) rewrote that reason, so the quoted sentence stopped existing in the very file the note sends a reader to. **The judgement was never wrong.** The seeding does falsify the entry's original premise — the rewritten reason on the entry now records exactly that, as a real ADR-0054 §3 binding candidate held back by the adoption act alone. Only the quotation rotted, which is why this is p3 and why the fix replaces the quote rather than the verdict. ## Shape chosen: A-2 — stop quoting verbatim The card preferred A-2 and left the choice to the dev, because the real question is whether a reader can still **locate** the entry once the quote is gone. Measured, not assumed: | reading | result | |---|---| | `id: 'declarative-rbac-seeding'` declarations in `proof-registry.mts` | **1** | | ...out of all `id:` declarations in that file (firing control for the predicate) | **42** | | `declarative-rbac-seeding` occurrences in that file | **6**, across **5** lines | So the id is a unique key *within the registry* and grepping it lands a reader on the entry. A-1 would have bought a pointer with the same expiry date as the last one: the entry's reason is prose owned by another card's author, and this note has now been broken by a rewrite of it once already. Three things worth stating about the shape: - **The old premise is paraphrased, deliberately not re-quoted.** A paraphrase of a premise that has already been retired cannot rot — the text it describes is frozen in history and nothing will rewrite it again. Re-quoting it would also have re-introduced the exact string this card exists to remove. - **It is the house pattern in the same directory.** `liveness/api.json` and `liveness/qa.json` both cite `proof-registry.mts` by name and claim, and quote none of its prose. This file was the outlier. -⚠️ **Nothing mechanically asserts those ids unique** — there is no uniqueness assertion in `proof-registry.test.ts` or anywhere in `packages/spec/scripts/liveness/`. The id's durability as an anchor is a measured fact about today's tree, not an enforced invariant. See the acceptance note below. ## Acceptance readings All taken at `dc1202c21b` with a **fold-proof** predicate: whitespace folds and TypeScript `' + '` concatenation seams are dissolved before matching, because the registry splits every reason across source literals mid-phrase and a line-oriented grep reads a false zero there. The predicate carries a self-test — three synthetic samples that must each read 1 through a fold or a seam, plus a negative control that must read 0 — and all four behaved as declared on every run, so the zeros below are measurements rather than a broken regex. Needles are written **in full**; corpus is all 8,912 tracked text files via `git ls-files`. **Firing control, same run** — a zero alone is not a reading: | needle (in full) | result | |---|---| | `not on a per-type authorable property` | **0** repo-wide | | ⭐ FIRING CONTROL `declarative-rbac-seeding` | **21** hits in 9 files, same run, same predicate | **Uniqueness, re-taken** — the card's claim was the filing dev's reading and had not been re-run. All three old spellings, before and after: | old spelling (in full) | on `main` | after | |---|---|---| | `not a governed metadata type` | 1 — `packages/spec/src/ai/knowledge-source.zod.ts:106` | 1, unchanged | | `not as a property of a governed metadata type` | 0 | 0 | | `not on a per-type authorable property` | 1 — `packages/spec/liveness/sharing_rule.json:3` | **0** | The claim holds, with the shape made precise: the three spellings do not all hit this one site. Spelling 3 was the only one on the `_note`; spelling 1's single hit is on an unrelated file — `KnowledgeSource` is documented as not being a governed metadata type, nothing to do with sharing rules — and it is deliberately untouched; spelling 2 was already absent. **Substance preserved.** The rewritten `_note` still asserts, in its own words, that the seeding falsifies the entry's original premise, and now says what that premise was and that objectstack-ai#18587 supplied the per-type coordinate it claimed was missing. The sentence was replaced, not deleted. **DARK.** `check:liveness` exits 0 on both legs and its output is **byte-identical** before and after, reporting `sharing_rule 17 classified (live 16, planned 1)` either way. The BEFORE leg is a real measurement, not a no-op: the old quotation was confirmed back on disk (1 occurrence) before that run, and the restore was proven by blob hash matching `HEAD`, an empty `git diff HEAD`, and 0 occurrences afterwards. **Verdicts untouched.** The read-only fence was drawn by kind, not by path: every `status`, `verifiedAt`, `evidence`, `producer` and per-row `note` in the file is byte-identical to `main`. Asserted structurally, not by eyeball — the edit script parses both versions and requires every field except `_note` to compare equal. ## Changeset: a `patch`, measured rather than defaulted `packages/spec`'s `files[]` ships `liveness`, so this file is published content. `npm pack --dry-run --json`, with controls in both directions: - **275** published entries, and `liveness/sharing_rule.json` is among them. - **Positive control**: 39 `liveness/*.json` ledgers ship — the measurement can see a spec-owned data file when one ships. - **Negative control**: **0** entries under `scripts/`, and `scripts/liveness/proof-registry.mts` is not published — which is why objectstack-ai#18797 correctly took `skip-changeset`, and why this card cannot. Published bytes move, and what moves is precisely the pointer a consumer follows, so `skip-changeset` does not apply by its own criterion. A `patch` changeset is written. Precedent for the shape: `.changeset/13272-liveness-cloud-citations-verifiedat-anchors.md`, a `patch` for a liveness-ledger evidence/prose change with no verdict moving. ⛔ No `skip-changeset` label is applied, deliberately — it is an opt-out that would exempt this PR from the very check the changeset satisfies. ## Verification Run in a dedicated worktree at `dc1202c21b`, after merging `origin/main` (which moved `packages/spec`) and rebuilding. - **Gates**: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived **55** commands. All 55 run with exit codes landed to disk first, then reconciled with `--ran`: **54 run green, 1 NOT MEASURED, 0 unrun**. - The one NOT MEASURED is `pnpm check:dual-build-cjs-loads` — recorded exit **3**, `PREREQUISITE NOT MET`, its own words: *"this gate reads built output, and some package has no dist"* across 87 packages. It needs a whole-repo build and is owned by CI's `Build Core`. Exit 3 is neither a pass nor a failure by that gate's design. - `pnpm check:lean-entry-closure` first read the same exit 3; its prerequisite named exactly one package, so `@objectstack/objectql` was built and it was re-run to a real verdict — 2 published conditions measured from a real load, admitted set held exactly. - **Tests**: `pnpm --filter @objectstack/spec test` — **489 files, 14209 passed**. The five liveness-ledger test files were also run on their own: 146 passed. - **Typecheck**: `pnpm --filter @objectstack/spec typecheck` — OK. - **Generated artifacts**: `pnpm --filter @objectstack/spec check:generated` — all 16 up to date after the merge. - **Control characters**: `check:nul-bytes` green, plus a direct scan of the edited file for the wider control-byte class — no hits. - **Lint, narrowed and the narrowing proven** — the three readings, not an assertion: 1. **Population, read from eslint's own config**: every `files:` selector in `eslint.config.mjs` is `{ts,tsx,mts,cts,js,jsx,mjs,cjs}`. Neither `.json` nor `.md` is selected by any of them. 2. **Count, read from `--format json`**: eslint over exactly the 2 changed paths reports on 2 files, 0 errors, each with its own message *"File ignored because no matching configuration was supplied."* 3. **Invariance for untouched files**: type-aware linting is not enabled anywhere — `eslint.config.mjs` states it carries no `parserOptions.project` and no typed rules *for ANY file* — so this diff cannot move the verdict on a file it does not contain. The repo-wide `eslint .` sweep is CI's run and is unaffected by these two paths. ## Acceptance notes Out of scope for this card, filed nowhere and recorded here instead: - `noted, not filed:` the `HIGH_RISK_CLASSES` ids in `packages/spec/scripts/liveness/proof-registry.mts` are not asserted unique anywhere — no check in `proof-registry.test.ts` or its siblings. This is an observation, not a reproducible defect, a contract violation or an authoring trap, so it is not one of the three filing classes. It is worth writing down only because this PR's argument for A-2 rests on the id being a durable anchor, and that rests on a convention rather than on a gate. **Who will meet it:** the next seat to add or rename a `HIGH_RISK_CLASSES` entry — the same file this card was forbidden to edit. Not acted on here. - `noted, not filed:` the same id string is declared a second time in the tree, at `packages/qa/dogfood/test/authz-conformance.matrix.ts:322`. That is deliberate — the conformance matrix names the same proof — and it makes the id a cross-file join key rather than a collision. Recorded so a later reader who greps the id repo-wide and finds two declarations does not read it as drift. **Who will meet it:** anyone following the new `_note` pointer with a repo-wide grep instead of a registry-scoped one. ## Pushback on the brief Reported rather than quietly worked around, per the round convention: 1. **The A-2 wording in the brief mis-attributes the rewrite.** It prescribes saying the entry's reason *"was updated (by objectstack-ai#18587)"*. Measured: objectstack-ai#18587 (`e0d05538c0`) seeded the ledger and put `sharing_rule` in `GOVERNED`, which supplied the coordinate; the `blockedReason` text itself was rewritten by **objectstack-ai#18797** (`ac720a9865`, `Fixes objectstack-ai#18589`). Writing objectstack-ai#18587 as the rewriter would have planted a second wrong pointer in the sentence that exists to stop wrong pointers. The note names objectstack-ai#18797 as the rewriter and objectstack-ai#18587 as what supplied the coordinate. This is a one-token correction inside the shape the brief chose, so it was implemented rather than handed back. 2. **The PM's "5 hits" and this PR's "6" are the same reading.** `declarative-rbac-seeding` occurs 6 times across 5 lines of the registry — line 519 carries it twice. A line count and an occurrence count, not a disagreement. 3. **The base moved twice during the round.** The brief's readings were at `2265bb0a5e`; the worktree was cut at `a484966407`, and `origin/main` reached `d8b12fca97` before the gate list could be derived. Every reading in this PR was re-taken, and `origin/main` was merged in because `dispatch-gates` refused to answer from the stale tree — correctly, since all five of its gate-defining files had moved across that range. 4. **objectstack-ai#18800 was re-taken at the start of work**, as instructed: `state=open`, `assignees []`, labels `pm:queue` / `domain:spec` / `priority:p3`, 0 comments — nobody holds it, so this round does not collide. --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) _Generated by [Claude Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_ --- _Generated by [Claude Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
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