Skip to content

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

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

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

Conversation

@os-bill

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

Copy link
Copy Markdown
Collaborator

Part of #18582

Clause-②: no

Seeds packages/spec/liveness/sharing_rule.json and moves sharing_rule out of PENDING_GOVERNANCE into GOVERNED. This is one of the three debts on that card; connector and analytics_cube stay on it, which is why the first line is Part of and not a closing keyword.

What the coverage line says, before and after

PR #18581 made this line print on every run precisely so a change like this is visible. Measured on this branch:

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

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

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

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

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

The classification, and the one row that is not live

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

Every row carries a producer, and that is the substance of the work. The authoring shape is not the enforced shape: ADR-0057 D6 makes the sys_sharing_rule row canonical and bootstrapDeclaredSharingRules translates each authored key into it at boot — nothing re-parses SharingRuleSchema at enforcement time. So a consumer pointer alone would prove only that a column is read, never that the authored value reaches it. That is the seed.env shape (#4837) applied to a whole type rather than to one key.

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

The negative rests on a census with a lit control, not a bare grep: the population that reads a declared rule item is the seeder, four packages/lint validators and objectui's create-door client validation; searching that population for r.type / rule.type returns the one defensive comparison and nothing else, while the same search for r.condition / rule.condition returns hits in three of them.

Preview read points enumerated, per the #7131 mechanical rule — registerBuiltinPreviews() (objectui @dda8f381) registers twenty types and sharing_rule is not one of them. Recorded in the ledger rather than skipped, because "the type has no registered preview" is the sentence a later sweep needs. What objectui does consume is the whole shape, on the CREATE door only (AUTHOR_SHAPE_ONLY_TYPES).

One decoy named so the next census does not trip on it: objectui's own SharingRuleConfig (packages/types/src/permissions.ts) is a different shape entirely, re-exported twice and read by nothing. It matches this type by name only.

File surface — three deviations, all mechanically forced, none discretionary

The dispatch scoped this to the ledger, the two check-liveness.mts rows and a changeset, with liveness/README.md read-only. Three files outside that surface had to move, because the gates read them off GOVERNED and off the ledger set:

  1. packages/spec/liveness/README.md — reconcileReadmeTable fails on a GOVERNED type with no row in the "Current state" table, and the heading's N governed types is checked three ways against the rows and against GOVERNED.length. A check:liveness that exits 0 and a read-only README are not both reachable. Row written by measurement; heading 36 → 37; the tail paragraph that described the map as holding three debts now says which one is paid.
  2. packages/spec/liveness/state-counts.md — generated, merge=os-regen, proved fresh by the same gate. Regenerated with pnpm --filter @objectstack/spec gen:liveness-counts, never hand-edited.
  3. docs/qa/platform-checklist/coverage.json — this ratchet derives its universe from packages/spec/liveness/*.json, so seeding a ledger is exactly what makes the kind UNCLASSIFIED there (check:platform-checklist exit 1, reproduced before and after). Mapped to the three items that already exercise the surface — access-security.sharing-rules-widen, access-security.sharing-rule-authoring-ui, access-security.record-share-grant-revoke — rather than waived. No checklist item is authored here, and a waiver would have been false.

A changeset is owed and is not skip-changeset: packages/spec's files[] includes liveness, so the new ledger ships inside the tarball. patch.

Gates

Derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, run with each exit code landed on disk before being read, then reconciled with --ran:

dispatch-gates --ran: 65 derived famil(ies) accounted for — 63 run, 2 NOT-MEASURED
  (2 DERIVED from a recorded exit 3), 0 UNRUN.
  • check:liveness exits 0, with the coverage line above.
  • check:generated — exit 0 on a built tree (15 of 15 artifacts current). It exits 1 on an unbuilt one and says so itself; packages/spec was built under the shared verify lock before the verdict was read.
  • check:platform-checklist — exit 1 before the coverage entry, exit 0 after: 36 kinds mapped, 1 waived.
  • check:pm-dispatch-gates exit 0 (1746 self-test cases; the battery takes ~520s on this box).
  • packages/spec — typecheck exit 0, test exit 0 (483 files / 13780 tests). The liveness script suites the edited gate owns, plus every test whose text names check-liveness, run as their own slice: 13 files / 378 tests, exit 0.
  • NOT MEASURED (2) — check:dual-build-cjs-loads and check:lean-entry-closure, both exit 3 · PREREQUISITE NOT MET: they read a whole-repo dist/ this container has not built. Neither pass nor finding. CI builds the closure.
  • Known and already filed, not a finding here — check:cross-package-test-inputs exits 1 once packages/spec has been built and 0 when it has not ([finding] check:cross-package-test-inputs passes in CI and fails on a built tree — its verdict is a function of gitignored build state #18353 / [finding] check:cross-package-test-inputs answers 1 or 0 depending on whether packages/spec has been BUILT — the author who follows AGENTS.md is the only one who sees the red, and CI never does #18440). Recorded in both states; its message names packages/spec/dist/ and packages/cli/test/init-created-files-summary.e2e.test.ts, neither of which this diff touches.
  • pnpm lint is CI's whole-repo run. The narrowing here is a measurement, not a skip: eslint's own files declaration is **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}, so five of the six changed files (.md, .json) are outside its universe by that declaration; the sixth, packages/spec/scripts/liveness/check-liveness.mts, linted clean — --format json reports 1 file, 0 errors, 0 warnings. eslint.config.mjs states in its own prose that this repo "never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file", so nothing in this diff can move the verdict on a file it does not touch. Gate numbers are quoted from runs at 2f932b09.

Acceptance notes

Noted, not filed, and one to file:

  • To file — the four sharing-related entries in packages/spec/scripts/liveness/proof-registry.mts record a blockedReason whose premise this PR falsifies: bu-hierarchy-sharing, sharing-rule-criteria-required, declarative-rbac-seeding and sharing-rule-org-scoped-listing each say sharing rules are "authored at STACK level, which is not a governed metadata type … so there is no ledger entry to ratchet". There is one now. showcase-declarative-rbac-seeding in particular authors sharingRules[] on the showcase stack and asserts the seeded row's object_name, recipient_type, recipient_id and translated criteria_json — i.e. it exercises five of these keys end to end and is a genuine ADR-0054 binding candidate. ⛔ Deliberately not done here: binding a high-risk class is a separate ADR-0054 §3 act, one class at a time, and sharing-rule-criteria-required is the counter-example that makes it a judgement rather than a sweep — it POSTs the runtime body to /sharing/rules and never authors the spec key, so it must not bind condition.
  • Noted, not filed — packages/spec/liveness/README.md's "Adding a type" recipe lists four steps and none of them is the README row, the heading count, the generated state-counts.md or the platform-checklist coverage entry, all four of which the gates now require. The recipe predates those three checks. Whoever seeds connector or analytics_cube next will walk into the same three red gates this PR did; that seat is the one that will carry it.

Generated by Claude Code

…e first of three PENDING_GOVERNANCE debts

`sharing_rule` moves out of `PENDING_GOVERNANCE` and into `GOVERNED` with a
seeded `packages/spec/liveness/sharing_rule.json`: every authorable key of
`SharingRuleSchema` classified against a reading of what actually consumes it.

The shape fact that decides every row: the authoring shape is NOT the enforced
shape. ADR-0057 D6 makes the `sys_sharing_rule` row canonical and
`bootstrapDeclaredSharingRules` translates each authored key into it at boot, so
every consumer reads a COLUMN a producer had to populate. Each row therefore
carries a `producer` (#4837) naming the threading site rather than a consumer
pointer alone.

9 live + 1 planned over 10 authored keys (17 classified with the ADR-0010
envelope). `type` is the one non-live row: the `SharingRuleType` discriminator
has exactly one member and its only reader is a defensive `=== 'owner'`
comparison unreachable for every value the schema admits — `planned` on the
`action.operation` precedent, and deliberately not an enforce-or-remove
candidate.

`sharedWith` is drilled, so the change adds zero rows to the undrilled-container
baseline. The README state table, its heading count and the generated
`state-counts.md` move with `GOVERNED` because `check:liveness` reconciles all
three; the `#18133` note over the map records the paid debt instead of leaving a
stale "three" standing over a map of two.

Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
Co-authored-by: Claude <noreply@anthropic.com>
… this type forces

Two files the ledger's own gates demand, neither of them optional:

- `.changeset/18582-sharing-rule-liveness-ledger.md` — `packages/spec`'s `files[]`
  includes `liveness`, so a new ledger IS published; this is a `patch`, not a
  `skip-changeset` diff.
- `docs/qa/platform-checklist/coverage.json` — the capability ratchet DERIVES its
  universe from `packages/spec/liveness/*.json`, so seeding a ledger is what makes
  the kind UNCLASSIFIED there. Mapped to the three items that already exercise the
  surface (`sharing-rules-widen`, `sharing-rule-authoring-ui`,
  `record-share-grant-revoke`) rather than waived — no item is authored here, and a
  waiver would have been false.

Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling labels Sep 17, 2026
@os-bill
os-bill marked this pull request as ready for review September 17, 2026 06:10
@os-bill
os-bill added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit e0d0553 Sep 17, 2026
37 checks passed
@os-bill
os-bill deleted the claude/issue-18582-sharing-rule-liveness-ledger branch September 17, 2026 06:32
os-bill pushed a commit that referenced this pull request Sep 17, 2026
…falsified

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

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

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

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

Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
Co-authored-by: Claude <noreply@anthropic.com>
os-bill pushed a commit that referenced this pull request Sep 17, 2026
…falsified

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

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

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

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

Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
Co-authored-by: Claude <noreply@anthropic.com>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants