Skip to content

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

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

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

Conversation

@os-bill

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

Copy link
Copy Markdown
Collaborator

Fixes #18589

Clause-②: no

What was wrong

packages/spec/scripts/liveness/proof-registry.mts recorded four blockedReason entries whose stated reason rested on one premise: sharing rules are authored at STACK level and sharing_rule is not a governed metadata type, so there is no ledger entry to ratchet. PR #18587 (landed as e0d05538c0) seeded packages/spec/liveness/sharing_rule.json and put sharing_rule in the gate's GOVERNED array — which makes that premise false. A recorded WHY that has silently stopped being true is one level up from the keys this ledger governs.

A fifth carrier the card did not enumerate is fixed in the same pass, because the DARK control is a file-level reading: the rls-check-post-image entry's code comment carried the same sentence ("stack-level sharing rules are not a governed metadata type, so only check binds").

What changed — per entry, read against what each proof ACTUALLY exercises

No bound flag and no ledgerBindings entry changes. This PR changes recorded reasons only.

entry verdict evidence
bu-hierarchy-sharing stays unbound — new reason the proof calls stack.kernel.getService('sharingRules').defineRule({… criteria, recipientType, recipientId …}, SYS): the RUNTIME column shape. SharingRuleSchema and bootstrapDeclaredSharingRules are not on its path, so no authorable sharing_rule.* key is written. Binding sharedWith.type here would be the owner-anchor/allowTransfer mistake.
sharing-rule-criteria-required stays unbound — and must NOT bind condition it POSTs a runtime body to /api/v1/sharing/rules. Its own header states the mechanism: "The endpoint plucks its body field-by-field into SharingRuleService.defineRule; SharingRuleSchema is never on that path." The ledger coordinate now exists; this proof is still not evidence for it.
declarative-rbac-seeding stays unbound — recorded as a REAL binding candidate the showcase authors the rules through defineSharingRule (examples/app-showcase/src/security/sharing-rules.ts: condition, sharedWith: { type, value }, object, name), bootstrapDeclaredSharingRules seeds them, and the proof asserts the landed row (object_name, recipient_type, recipient_id, and the CEL to criteria_json translation). Adoption is a separate ADR-0054 §3 act — see "Why the binding is not in this PR".
sharing-rule-org-scoped-listing stays unbound — new reason fixtures are created over POST /sharing/rules (the criteria-required shape), and what the file pins is a READ-SCOPE filter inside SharingRuleService, not the behaviour of any authored key.
rls-check-post-image (comment) unchanged binding sharing_rule.condition IS a governed entry since #18587, so that half is no longer un-bindable for want of a coordinate; only check binds here because adopting it is its own ADR-0054 §3 act with its own candidate question.

Why the binding is not in this PR

declarative-rbac-seeding is a real candidate, and adopting it is a ledger act, not a registry act: BOUND_PROOF_PATHS makes check-liveness.mts require the matching proof on every cited sharing_rule.json row (report.proofMissing), and proof-registry.test.ts's wiring suite asserts the same from the other side (it also needs a sharing_rule row in its ledgerFor map). packages/spec/liveness/sharing_rule.json deliberately claims proof on no row — its own _note: "No proof is claimed on any row here: binding a high-risk class is a separate ADR-0054 §3 act, one class at a time, and it is filed rather than slipped in." That file is read-only under this card's declared file surface, and WHICH of the five exercised props the class owns is a decision of its own (condition is also exercised by showcase-d3-d4-capabilities). Reported for filing instead.

The ledger reading the seat could not verify

The card's "17 classified (16 live, 1 planned)" is the GATE's count, and it is correct as such — but it is not the number of authored rows. Read first-hand from packages/spec/liveness/sharing_rule.json and from check-liveness.mts --dump sharing_rule:

  • 10 classified rows are authored in the ledger file: 9 live (name, label, description, object, active, accessLevel, sharedWith.type, sharedWith.value, condition) + 1 planned (type, the one-member SharingRuleType discriminator).
  • The walk adds 7 framework envelope fields that carry no ledger row and are auto-classified live by FRAMEWORK_FIELDS (_lock, _lockReason, _lockSource, _lockDocsUrl, _provenance, _packageId, _packageVersion).
  • 10 + 7 = 17 classified, live 16, planned 1 — exactly what pnpm --filter @objectstack/spec check:liveness prints.

Acceptance controls

LIT — the premise really is false (symbol/array membership, not a substring grep): parsing the GOVERNED symbol out of check-liveness.mts reads length 39, includes('sharing_rule') true at index 36; negative controls sharing_rules / sharing / not_a_metadata_type all read false. The gate's own runtime leg agrees: it prints sharing_rule in "governed types:" and emits the per-type row sharing_rule 17 classified (live 16, planned 1), which only exists because the loop iterates GOVERNED.

DARK — the assertion now reads 0, with a non-zero control. The predicate folds the TypeScript string-concatenation seams (' + ') before matching, because the reasons are split across source literals mid-phrase; a line-oriented predicate reads a false zero there. It deliberately uses no POSIX ERE bracket spelling, which is the other false-zero trap.

predicate BASE 6de7a2d6e6 (control) this branch
not a governed metadata type 3 0
not as a property of a governed metadata type 1 0
not on a per-type authorable property 1 0
no ledger entry to ratchet, scoped to the four sharing entries 1 0
total 6 0

The predicate carries its own self-test (a synthetic split-literal sample carrying all three spellings must read 3; it does, on both runs), so the zero is a measurement and not a broken regex.

Changeset: skip-changeset, measured not inferred

npm pack --dry-run --json in packages/spec: 275 published entries, 0 under scripts/, and proof-registry.mts is not among them. Positive control on the same reading: 41 liveness/*.json ledger files ARE published, so the measurement can see a spec-owned data file when one ships. This diff therefore moves zero published bytes.

Verification

Run on 968d6e0a55, in a dedicated worktree:

  • pnpm --filter @objectstack/spec test — 486 files, 14015 passed, 1 skipped.
  • pnpm --filter @objectstack/spec exec vitest run scripts/liveness/proof-registry.test.ts — 39 passed (the registry-invariant and wiring suite).
  • pnpm --filter @objectstack/spec typecheck — exit 0 (tsc --noEmit + check:scripts-typecheck + check:test-typecheck).
  • pnpm --filter @objectstack/spec check:liveness — exit 0; counts unchanged.
  • pnpm lint (repo-wide eslint . --no-inline-config) — exit 0. Full population, no narrowing to declare.
  • The gate families derived by node scripts/pm/dispatch-gates.mjs --commands for this diff: 46 of 50 green, including check:nul-bytes, check:published-files, check:cross-package-test-inputs, check:test-source-alias, check:pm-governed-merges, check:adr-0087-registration.
  • NOT MEASURED (4): check:dts-closure, check:dual-build-cjs-loads, check:lean-entry-closure, check:sourcemap-no-sources-content — each exits 3, PREREQUISITE NOT MET ("nothing was swept … NOT a pass and NOT a finding") because a fresh worktree has no dist/ for any of the 81 packages. They read built output repo-wide; this diff changes a liveness script that is in no package's build inputs and in no files[]. Declared to CI, where the closure is built.

Acceptance notes (noted, not filed)

  • bootstrapDeclaredSharingRules threads label: r.label ?? r.name, so an unauthored label stores the rule NAME rather than staying empty. That is the ledger's recorded behaviour for the label row, not a defect — noted only because it is the sort of thing a future binding decision touches. Successor: none — no PR or person is heading into that file for this reason.

Generated by Claude Code

…falsified

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

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

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

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

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

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

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

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

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

Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…ey it requires (objectstack-ai#18816)

Fixes objectstack-ai#18747

Clause-②: no

## Where the two defects actually are on `main`

The card's line numbers were taken on a branch head. Re-anchored by
symbol against
`origin/main` at the base of this branch (`034f5a3afd`), in
`packages/spec/scripts/lib/dropped-refinements.ts`:

| the card said | on `main` it is at | symbol |
|---|---|---|
| `:453` | **`:496`** | the first `throw new Error` in
`readDroppedRefinementsBaseline` |
| `:456-458` | **`:499-501`** | the `entry.sites` check and the second
`throw new Error` |
| module docblock `:68` | **`:66-70`** | the "Every entry carries a
`reason`" sentence |
| (not named) | **`:134-150`** | the `DroppedRefinementsEntry` docblock
that contradicts it |

## Which side is true — measured from the consumers, not chosen

The card's open question was whether the refusal should name `sites` or
the shape
should really carry `count`/`reason`, and which docblock governs. Every
consumer that
reads this ledger answers `sites`, and none of them reads `count` or
`reason` at all:

| consumer | reading |
|---|---|
| `DroppedRefinementsEntry` (the shipped interface) | one member:
`readonly sites: readonly string[]` |
| `packages/spec/dropped-refinements.baseline.json` | 243 entries;
**243** carry `sites`, **0** carry `count`, **0** carry `reason` |
| `checkDroppedRefinements` | reads `entry.sites` only — set difference
plus a length compare |
| the `unreasoned` refusal | fires on `entry.sites.length === 0`, and
the gate prints "carry an empty `sites` list" |
| `build-schemas.ts` remedy text (both the undeclared and the miscounted
arms) | prints the corrected entry as `"sites": [ ... ]` |
| the ledger's own `description` field | documents `sites` and nothing
else |
| `dropped-refinements.test.ts` "the committed ledger" | asserts
`entry.sites.length` per entry and that
`measured.droppedRefinementSites` equals their sum |

So `sites` is the contract and the `DroppedRefinementsEntry` docblock
governs. The two
wrong texts are both residue of the module this one was copied from: in
`packages/spec/scripts/lib/unemitted-schemas.ts` the entries really are
`{ cause, reason }`, its refusal really does say so, and its gate really
does require a
non-empty `reason` (`entry.reason.trim() === ''`). Copying the file
carried the
vocabulary across without the shape.

## LIT — the red leg, both messages verbatim

The trap is a sequence, so the probe walks it: take a structurally
broken ledger, read
what the reader says the shape is, write that shape, and hand it back to
the same
function.

**BEFORE** (the module restored to `origin/main`, the rest of the tree
unchanged):

```text
[step 1] input: {"entries": []}
  REFUSED   dropped-refinements.baseline.json: "entries" must be an object of key -> { count, reason }

[step 2] the ledger an author writes by FOLLOWING step 1
         input: {"entries":{"system/TraceSamplingConfig":{"count":1,"reason":"zod projects no custom check"}}}
  REFUSED   dropped-refinements.baseline.json: entry "system/TraceSamplingConfig" needs a `sites` array of path strings
```

The repair written from the message is refused by the **same function**,
four lines
below the message that prescribed it.

**AFTER**:

```text
[step 1] input: {"entries": []}
  REFUSED   dropped-refinements.baseline.json: "entries" must be an object of key -> { sites: string[] }

[step 3] the ledger an author writes by FOLLOWING step 1
         input: {"entries":{"system/TraceSamplingConfig":{"sites":["properties.rate"]}}}
  ACCEPTED  (1 entry/entries)
```

The mutation leg and the restore leg were each proved on disk (the
deleted text present
and the injected text absent, then the reverse), and the restore was
verified
byte-identical to `HEAD` by `git hash-object` (`9615f4e0c0…` both sides)
with an empty
`git diff HEAD` and an empty `git status --porcelain`. The probe itself
lives outside
the repository and nothing of it is committed.

## DARK — a legitimate ledger passes on both legs, and nothing else
moved

| reading | BEFORE | AFTER |
|---|---|---|
| the real committed ledger through `readDroppedRefinementsBaseline` |
ACCEPTED — 243 entries, 737 sites | ACCEPTED — 243 entries, 737 sites |
| `pnpm --filter @objectstack/spec test` | 487 files / **14051** passed,
0 failed | 487 files / **14055** passed, 0 failed |
| `dropped-refinements.test.ts` | 23 tests | 27 tests |

The `+4` is exactly the four tests this PR adds. The BEFORE row is a
real run, not
arithmetic: both files were checked out at the merge base, the suite was
run, and both
were restored and re-verified byte-identical to `HEAD`.

Nothing else in the repository pins either message —
`git grep "must be an object of key"` returns exactly two hits, this one
and
`unemitted-schemas.ts`'s own (which is correct for its own shape, and is
the live
control on that grep).

## The pin is a closed loop, not a wording match

Error prose is not pinned here on its spelling; what is pinned is the
**named subject**
and the property that makes this class of defect a trap: whatever the
refusal names has
to be what the reader then accepts. Four cases in
`packages/spec/scripts/dropped-refinements.test.ts`:

1. the shape diagnostic names `sites`;
2. a ledger written to that shape is then ACCEPTED — the loop closes;
3. **LIT CONTROL** — the shape the old diagnostic named is refused, and
that refusal
still says `sites` (without this leg the first two pass on a reader that
accepts
   anything);
4. the shape diagnostic names no key the entry shape does not have.

The docblock half has no pin, deliberately: no consumer parses a
docblock, and a
source-text assertion over prose is a gate that fails on rewording
rather than on
regression.

## Also in this diff, declared

- The module docblock's **shrink-only bullet** said the ratchet
re-checks "a recorded
`count`" — the same contradiction as the `reason` sentence the card
names, in the
paragraph above it, against the same `DroppedRefinementsEntry` docblock
("The unit is
the SITE and not a count, deliberately"). Repaired in place under the
bounded
exemption: same defect class as this card, same file, mechanical, the
corrected form
already fixed by the entry docblock, no new verification surface, and no
other claim
holds any `dropped-refinements*` path (measured across all 26 open
`claude/issue-*`
PRs, with `proof-registry.mts` reading out for objectstack-ai#18797 as the live
control).
- `packages/spec/scripts/dropped-refinements.test.ts` was listed
read-only on the claim.
  It is written here, and only to carry this card's own regression pin.

## The card's second open question — is there a third site?

`unemitted-schemas.ts`, the sibling the docblock calls itself "Identical
to", was read:
it has the same defect **nowhere**. Its docblock claim, its refusal
text, its interface
and its ledger all agree on `{ cause, reason }`. It is the correct
template, not a
second instance.

## Changeset — measured, not inferred from the path

`npm pack` on `packages/spec` after a full build, then grep over the
packed bytes
(142,490,031 of them):

| reading | hits |
|---|---|
| tarball entries under `package/scripts/` | **0** |
| `DROPPED_REFINEMENTS_BASELINE_FILE` in the packed bytes | **0** |
| `readDroppedRefinementsBaseline` | **0** |
| `must be an object of key -> { sites: string[] }` | **0** |
| positive control — entries under `package/src/` | 203 |
| positive control — entries under `package/json-schema/` | 1530 |
| positive control — `x-dropped-refinements` in the packed bytes | 486 |
| positive control — `ObjectSchema` in the packed bytes | 965 |

`packages/spec/scripts/**` is absent from the package's `files[]`, and
the build after
this change leaves `git status` clean, so no `dist/` byte moves either.
Nothing this
diff changes publishes from any released package, so it carries
`skip-changeset` rather
than a changeset.

## Verification

- `pnpm --filter @objectstack/spec build` — exit 0 (34/34 declaration
files present).
- `pnpm --filter @objectstack/spec test` — exit 0, 487 files / 14055
tests.
- `pnpm --filter @objectstack/spec typecheck` — exit 0, including
`tsconfig.scripts.json` (which is what compiles the edited file) and the
test layer.
- `node scripts/pm/dispatch-gates.mjs --commands` derived **57**
families from the
change set; all 57 were run and reconciled with `--ran`: **54 exit 0**,
**3 exit 3 =
NOT MEASURED** (`check:dual-build-cjs-loads`,
`check:lean-entry-closure`,
`check:type-check-debt` — each refuses its own prerequisite because only
`packages/spec` is built in this worktree; all three read built output,
which this
  diff cannot move, and CI builds the full closure).
- `pnpm check:nul-bytes` exit 0, plus a direct control-character scan of
both edited
files — no match, with a live non-zero control on a file that carries
one.
- `origin/main` was merged in before this PR was opened (clean, no
`os-regen` deferral).

---
_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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants