Skip to content

Commit 43f4766

Browse files
os-billclaude
andauthored
fix(spec): stop verbatim-quoting a rotted proof-registry reason in the sharing_rule ledger note (#18994)
Fixes #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 #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 #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 #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 #18587)"*. Measured: #18587 (`e0d05538c0`) seeded the ledger and put `sharing_rule` in `GOVERNED`, which supplied the coordinate; the `blockedReason` text itself was rewritten by **#18797** (`ac720a9865`, `Fixes #18589`). Writing #18587 as the rewriter would have planted a second wrong pointer in the sentence that exists to stop wrong pointers. The note names #18797 as the rewriter and #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. **#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>
1 parent 2767af8 commit 43f4766

2 files changed

Lines changed: 15 additions & 1 deletion

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
`liveness/sharing_rule.json` — the file `_note` stops quoting the `declarative-rbac-seeding` proof-registry entry VERBATIM, so the pointer it hands a reader survives the next rewrite of that entry's prose (#18801).
6+
7+
The ledgers ship inside this package, so this is a pointer a consumer can actually follow. The note said the entry's `blockedReason` "reads" a specific sentence and quoted it. PR #18797 (`ac720a9865`) rewrote that reason — correctly, because #18587 had made its premise false — and the quoted sentence stopped existing in the very file the note sends a reader to. Measured repo-wide with a fold-proof predicate (whitespace folds and TypeScript `' + '` concatenation seams dissolved before matching, because the registry splits every reason across source literals mid-phrase): the quoted string read **0** on `main`, while the entry id `declarative-rbac-seeding` read **18** in the same run.
8+
9+
- **The judgement was never wrong; the quotation was.** The seeding does falsify the entry's original premise, and the rewritten reason on the entry now records exactly that — as a real ADR-0054 §3 binding candidate held back by the adoption act. The note still asserts it, in its own words.
10+
- **What replaces the quote is an id, not a better sentence.** `declarative-rbac-seeding` is the entry's key: exactly **1** of the registry's **42** `id:` declarations spells it, and it reads 6 occurrences across 5 lines of `scripts/liveness/proof-registry.mts` — so a reader who greps it lands on the entry rather than on nothing. Quoting prose that changes is what rotted; an id does not rot on someone else's schedule. ⚠️ Measured, not assumed: nothing *asserts* those ids unique — the one other declaration of this id in the tree is `packages/qa/dogfood/test/authz-conformance.matrix.ts`, which names the same proof on purpose.
11+
- **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.
12+
- **The two sibling ledgers already wrote it this way.** `liveness/api.json` and `liveness/qa.json` cite `proof-registry.mts` by name and claim, and quote none of its prose.
13+
14+
No verdict moved. Every `status`, `verifiedAt`, `evidence`, `producer` and per-row `note` in the file is byte-identical to `main`; the only changed field is `_note`, and `check:liveness` reports `sharing_rule 17 classified (live 16, planned 1)` before and after.

‎packages/spec/liveness/sharing_rule.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"type": "sharing_rule",
3-
"_note": "SharingRuleSchema (packages/spec/src/security/sharing.zod.ts) — `SharingRuleSchema = CriteriaSharingRuleSchema`, the one authorable rule form. Seeded 2026-09-17 (#18582): the SECOND of the three PENDING_GOVERNANCE debts #18133 declared when PR #18581 widened the governance denominator from the registered kinds to `authorableTypes()`; `connector` and `analytics_cube` are still owed on that card. NOT a registered metadata KIND — it is bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reaches this walk through `getMetadataTypeSchema`'s unregistered-kind fallback, so the ledger governs it while `listMetadataTypeSchemaTypes()` still does not enumerate it. THE SHAPE FACT THAT DECIDES EVERY ROW BELOW: the authoring shape is not the enforced shape. ADR-0057 D6 makes the RUNTIME row canonical (`sys_sharing_rule`: `object_name` + `criteria_json` + `recipient_type`/`recipient_id` + `access_level`) and `bootstrapDeclaredSharingRules` TRANSLATES each authored key into it at boot — nothing re-parses `SharingRuleSchema` at enforcement time. So every consumer cited below reads a COLUMN that a producer had to populate, and every row therefore carries a `producer` (#4837) naming the threading site: a consumer citation alone would be the `seed.env` shape, where the mechanism was right and nobody supplied the input. PREVIEW READ POINTS ENUMERATED (the #7131 mechanical rule, objectui @dda8f381): `registerBuiltinPreviews()` in packages/app-shell/src/views/metadata-admin/previews/index.ts registers twenty types and `sharing_rule` is NOT one of them — this type has no registered metadata-admin preview. Recorded 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: `clientValidation.ts`'s `AUTHOR_SHAPE_ONLY_TYPES` gates the metadata-admin CREATE door on `SharingRuleSchema` itself (the EDIT door is deliberately not gated — a served body carries the `_diagnostics` read decoration this `.strict()` schema rejects), so an authored rule that fails this schema is refused before it is written. DECOY, do not cite it as a consumer: objectui's own `SharingRuleConfig` (objectui packages/types/src/permissions.ts) is a DIFFERENT shape — `{ type: 'role'|'user'|'group'|'public', entity, actions, filter }` — re-exported twice and read by nothing. It matches this type by name only. RUNTIME PROOF THAT ALREADY EXISTS, unbound: packages/qa/dogfood/test/showcase-declarative-rbac-seeding.dogfood.test.ts 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 `name`/`object`/`sharedWith.type`/`sharedWith.value`/`condition` end to end. It is registered in ../scripts/liveness/proof-registry.mts as `declarative-rbac-seeding` with `bound: false`, whose `blockedReason` reads \"not on a per-type authorable property\" — a premise this seeding falsifies. ⛔ 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.",
3+
"_note": "SharingRuleSchema (packages/spec/src/security/sharing.zod.ts) — `SharingRuleSchema = CriteriaSharingRuleSchema`, the one authorable rule form. Seeded 2026-09-17 (#18582): the SECOND of the three PENDING_GOVERNANCE debts #18133 declared when PR #18581 widened the governance denominator from the registered kinds to `authorableTypes()`; `connector` and `analytics_cube` are still owed on that card. NOT a registered metadata KIND — it is bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reaches this walk through `getMetadataTypeSchema`'s unregistered-kind fallback, so the ledger governs it while `listMetadataTypeSchemaTypes()` still does not enumerate it. THE SHAPE FACT THAT DECIDES EVERY ROW BELOW: the authoring shape is not the enforced shape. ADR-0057 D6 makes the RUNTIME row canonical (`sys_sharing_rule`: `object_name` + `criteria_json` + `recipient_type`/`recipient_id` + `access_level`) and `bootstrapDeclaredSharingRules` TRANSLATES each authored key into it at boot — nothing re-parses `SharingRuleSchema` at enforcement time. So every consumer cited below reads a COLUMN that a producer had to populate, and every row therefore carries a `producer` (#4837) naming the threading site: a consumer citation alone would be the `seed.env` shape, where the mechanism was right and nobody supplied the input. PREVIEW READ POINTS ENUMERATED (the #7131 mechanical rule, objectui @dda8f381): `registerBuiltinPreviews()` in packages/app-shell/src/views/metadata-admin/previews/index.ts registers twenty types and `sharing_rule` is NOT one of them — this type has no registered metadata-admin preview. Recorded 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: `clientValidation.ts`'s `AUTHOR_SHAPE_ONLY_TYPES` gates the metadata-admin CREATE door on `SharingRuleSchema` itself (the EDIT door is deliberately not gated — a served body carries the `_diagnostics` read decoration this `.strict()` schema rejects), so an authored rule that fails this schema is refused before it is written. DECOY, do not cite it as a consumer: objectui's own `SharingRuleConfig` (objectui packages/types/src/permissions.ts) is a DIFFERENT shape — `{ type: 'role'|'user'|'group'|'public', entity, actions, filter }` — re-exported twice and read by nothing. It matches this type by name only. RUNTIME PROOF THAT ALREADY EXISTS, unbound: packages/qa/dogfood/test/showcase-declarative-rbac-seeding.dogfood.test.ts 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 `name`/`object`/`sharedWith.type`/`sharedWith.value`/`condition` end to end. It is registered in ../scripts/liveness/proof-registry.mts as `declarative-rbac-seeding` with `bound: false`; grep that id there for the entry’s current reason — ⛔ it is deliberately NOT quoted here. This note used to quote that reason VERBATIM, #18797 rewrote it, and the quoted string stopped existing in the very file the note sends you to: the id is the durable anchor, the prose is not. The substance is unchanged and now sits on the entry itself — this seeding falsifies the entry’s original premise, that it acted only on the stack-level collections and reached no authorable key of a governed type (#18587 having supplied the per-type coordinate that premise said was missing) — so the registry records the class as a REAL ADR-0054 §3 binding candidate, held back by the adoption act alone. ⛔ 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.",
44
"props": {
55
"name": {
66
"status": "live",

0 commit comments

Comments
 (0)