Skip to content

Commit 05077d4

Browse files
fix(spec): shard the generated liveness and strictness counts so PRs moving different units stop conflicting on a committed total (#20532)
Fixes #20361 Clause-②: no ## What changed - **Liveness counts are sharded.** `gen:liveness-counts` writes `packages/spec/liveness/state-counts/TYPE.md`, one shard per governed type (40). Each shard carries only its own row. `packages/spec/liveness/state-counts.md` is deleted. No total is committed anywhere. `check:liveness` sums the shards at read time, prints the sum on its success line, and carries it in `--json` as `countsTotal`. The generator prints the sum too. - **Strictness-ledger counts are sharded the same way**, because a measurement showed the same shared-total shape (see below). `gen:strictness-ledger` writes `docs/audits/2026-07-unknown-key-strictness-ledger.counts/DIR.md`, one shard per `packages/spec/src` directory with object sites (5 triaged, 9 untriaged). The global section, the posture total row and the global bucket split are summed at read time by `check:strictness-ledger` and `gen:strictness-ledger`, and committed nowhere. The single `….counts.md` is deleted. - **Both gates reconcile shard by shard.** They name a MISSING directory or shard, a STALE shard (with the shard's path and its first differing line), a STRAY file that no unit renders, and the RETIRED single file if a merge brings it back. Both generators rewrite only the shards whose bytes moved, prune strays, and delete the retired file. These are new legs inside the two existing gates; ⛔ no new gate or `check:` script was added. The text-shard read/write/reconcile helpers live in `packages/spec/scripts/lib/sharded-artifacts.ts` and serve both artifacts. - **Routing.** `.gitattributes`, `scripts/regen-artifacts.mjs` and `packages/spec/scripts/check-generated.ts` route the two directories (`…/**` with `merge=os-regen`), so the local driver still owns a same-unit collision. - **Readers of the retired names.** Updated: the liveness README (the two paragraphs that linked the file, and the `connector` Notes cell that named it), the `_note` of `liveness/book.json` and of `liveness/translation.json`, and seven links in the strictness ledger. Those links now point at the shards; the per-directory anchors (`#ui--open` and the rest) keep working inside each shard. ## Measured before the change Dispatch base `2b24b8b823`. Probe: a bare `--shared` clone with no merge driver registered (`git config --get merge.os-regen.driver` exits 1). Each side was regenerated with the real generator and committed, then `git merge-tree --write-tree --name-only` was run on the pair. | pair | result | |---|---| | liveness: `field.useGrouping` planned→dead vs `sharing_rule.type` planned→live | exit 1, CONFLICT (content) in `state-counts.md`. The two rows are 36 lines apart; the only overlap is the total row. | | liveness: `field.useGrouping` planned→dead vs `sharing_rule.type` planned→dead (equal delta) | exit 0 and WRONG. The merged total reads `dead 149 / planned 8`; the two moves make 150 / 7. | | strictness: 1 strict site added in `ui/` vs 2 added in `data/` | exit 1, CONFLICT (content) in `….counts.md`. Both `.zod.ts` sources merged clean. | ## Measured after the change The same probe on branch head `1be2134dc1`, with the real generators. | pair | result | |---|---| | liveness: planned→dead vs planned→live (the conflicting pair above) | exit 0. Each side touched only its own shard. | | liveness: equal delta (the clean-but-wrong pair above) | exit 0. The merged tree was materialised: `check:liveness` exits 0 on it, and its read-time total is `150 dead · 7 planned`, which is correct. | | liveness: two moves of `field` | exit 1, conflict on `state-counts/field.md` only. This residue is expected and the local driver owns it. | | strictness: `ui/` vs `data/` | exit 0 | | strictness: two moves in `ui/` | exit 1 on `….counts/ui.md` (plus the shared source file) | **The pin** is `packages/spec/scripts/count-shards-merge.test.ts`. It builds a throwaway repository with the real attribute line and no driver, commits each real renderer's output, and asks `git merge-tree`: - pairs that move different units (different deltas, equal deltas, adjacent liveness rows, adjacent untriaged directories) merge clean, and the merged tree equals the regeneration of both moves; - a same-unit pair conflicts on that unit's shard and nowhere else. This is the lit control. ## Parity - **Liveness.** The sum of the 40 shards equals the total the single file committed: 940/5/1/148/9/1103 at the dispatch base. This branch then merged main twice, taking in main's `qa` move and its `rest_api` move. At main `fb386074f5` both sides read 949/5/1/139/9/1103, and every per-type row is byte-equal to main's last single-file table. - **Strictness.** All 96 per-directory rows are byte-equal to main's `….counts.md`. The only rows dropped are the cross-directory ones: - the posture total 461/331/4/1/125; - the global buckets 1/0/120/3/0/1; - the global measures: 5 dirs, 461 sites, 125 strip, 22 files. The gate's read-time sums print exactly those numbers. - **Pinned.** `check-liveness.test.ts` asserts that the printed total equals the sum of the shards on disk, read back row by row. `strictness-ledger-doc.test.ts` asserts that the read-time totals equal the sums of the shard rows on disk. ## Verification **The full union ran at `ac64401fec`.** That commit is the head after the first main merge that met the retired file, plus its regeneration. - **Gates.** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 93 families, and I ran each one with its exit code captured before any pipe. `--ran` reconciliation: 93 derived, 91 run, 2 NOT-MEASURED, 0 UNRUN. - **90 exit 0.** This includes `check:liveness`, `check:strictness-ledger`, `check:generated` (15/15 up to date), `check:merge-driver`, `check:cross-package-test-inputs`, `check:nul-bytes`, `check:adr-0087-registration --base origin/main`, `check:api-surface` and `check:pm-dispatch-gates` (1976/1976). - **1 red, already red on main:** `check:platform-checklist`. It reports one ✗, the anchor `packages/plugins/plugin-auth/src/auth-plugin.ts#twoFactor` in `docs/qa/platform-checklist/areas/identity-auth.json` (ABSENT SYMBOL). The same single ✗ appears on origin/main `fb194c70e5` and `4a1df19656`, and on the dispatch base `2b24b8b823`. This diff does not touch it. - **NOT MEASURED:** `check:dual-build-cjs-loads` and `check:type-check-debt`. Both exit 3 (PREREQUISITE NOT MET) because they need a whole-workspace build. They are declared to CI. - **Spec tests** (`vitest run --project local --maxWorkers=2`, then `--project repo`): local 573 files, 16820 passed and 1 todo; repo 40 files, 709 passed. - **Spec typecheck** (`pnpm --filter @objectstack/spec typecheck`: tsc, the scripts program and the test program): exit 0. `tsc -p tsconfig.scripts.json --listFiles` includes all 10 changed script and test files. - **Narrowed lint.** `eslint --no-inline-config --format json` over the 13 changed `.ts`/`.mts`/`.mjs` files: 13 files linted, 0 errors, 0 warnings. `--print-config` resolves a config for each of the 13 files. `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`, no typed rules), so this diff cannot move the verdict on any untouched file. The repo-wide `pnpm lint` is CI's. - **One-shot reverse verification of the strictness gate's new legs.** I put the retired file back, dropped in a stray shard, and skewed one shard's posture row through `scripts/ablation-replace.mjs`. Each run exited 1 with exactly one drift, named RETIRED, STRAY and STALE respectively. Restoration was proven by blob equals HEAD, `git diff HEAD` empty and `git status --porcelain` empty. The liveness legs are pinned permanently in `check-liveness.test.ts` (missing, skewed, retired, row-set, hand-count, and the verbatim-copy control). **Re-run at the final head `0190e0e55f`**, after the second main merge (main's `rest_api` move) and its regeneration: - the ratchet family, all exit 0: `check:liveness`, `check:strictness-ledger`, `check:generated` (after a fresh spec build, 15/15), `check:merge-driver`, `check:nul-bytes`, `check:cross-package-test-inputs`; - the liveness and strictness tests: local 11 files, 271 passed; repo 4 files, 80 passed. A driver-free `merge-tree` of `0190e0e55f` against origin/main `0368a336db` exits 0. ## Acceptance notes - **The one-time transition for other in-flight PRs.** A PR that edits `state-counts.md` or `….counts.md` meets a modify/delete on its next base merge. The settlement is to keep the deletion (`git rm`) and run the generator. Both gates refuse the retired file while it is present, and both generators delete it. This branch met exactly that twice, with main's `qa` and `rest_api` moves, and settled it that way. - **An adjacent-line README overlap with PR 20458.** The `connector` Notes cell named the retired file, so this branch updates it. That cell sits directly above the `analytics_cube` row, which PR 20458 edits. A driver-free `merge-tree` of this head against that PR's head `f639af5f3d` conflicts on `packages/spec/liveness/README.md` only. Whichever of the two lands second keeps both lines. The file is hand-written and not driver-managed. - **Historical prose left alone** (carrier: none). The `scripts/pm/dispatch-gates.mjs` docblock names `liveness/state-counts.md` inside a measurement pinned at `ad54eb342`. `packages/spec/CHANGELOG.md` and earlier `.changeset` entries are release-owned. The strictness ledger's merge-queue narrative (it says `counts.md` was recorded as pending) is past tense. - **AGENTS.md is still accurate.** Its sentence that the three hottest artifacts are sharded stays true: five directories are sharded now. It is governed and is not edited here. - **Changeset.** `@objectstack/spec` gets a patch changeset. `npm pack --dry-run` measured 40 `liveness/state-counts/*` files shipped, `liveness/state-counts.md` absent (control: `liveness/README.md` and `liveness/book.json` present), and nothing under `docs/audits` shipped. - **A second hot spot, reported to the seat and not changed here:** the hand-written `step18.rationale` tail in `packages/spec/src/migrations/registry.ts`. Two synthetic appends at the dispatch base still conflict driver-free. 16 of 101 registry-touching first-parent commits in the 14 days to `2b24b8b823` rewrote that closing line. --- _Generated by [Claude Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 03b19d9 commit 05077d4

76 files changed

Lines changed: 2419 additions & 703 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
`liveness/state-counts.md` is replaced by `liveness/state-counts/<type>.md`: the generated liveness counts are one shard per governed type, and no total is committed anywhere.
6+
7+
Clause-②: no — no schema key moves, no accept set widens or narrows, no export changes. What changes is the layout of a generated table that ships in the tarball, and no count in it moves.
8+
9+
The ledgers ship inside this package (`files[]` includes `liveness`), so the changed tarball bytes are: `liveness/state-counts.md` removed, forty `liveness/state-counts/<type>.md` shards added (each carries exactly the row that file published for its type), and the prose in `liveness/README.md`, `liveness/book.json` and `liveness/translation.json` that named the removed file.
10+
11+
- **Where a count now lives.** A type's row is `liveness/state-counts/<type>.md`, byte-for-byte the row the single file carried. The table's total is not in any file: `pnpm --filter @objectstack/spec check:liveness` sums the shards when it reads them, prints the sum on its success line, and carries it in `--json` as `countsTotal`. At this release the sum is the total the removed file published: 940 live · 5 experimental · 1 live-elsewhere · 148 dead · 9 planned = 1103 classified.
12+
- **Why.** Every change that moved a liveness verdict rewrote the single file's total row, and GitHub's server-side merge runs no custom merge driver, so any two such changes in flight conflicted on that one line. With one file per type, changes that move different types touch different files.
13+
- **Anything that read `liveness/state-counts.md` from the published package** reads the shard for the type it wants, or sums the shards for the total. `gen:liveness-counts` rewrites only the shards whose counts moved and deletes the removed file if a merge brings it back; `check:liveness` fails while it is present.

‎.gitattributes‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,25 @@
6767
# cleanly because they do not overlap, and the subtotal merges clean and WRONG
6868
# (seven cases in one day). Note it is the counts file, not the ledger — the
6969
# ledger's prose is hand-written and must never be resolved by regenerating.
70+
# #20361 sharded it into `….counts/`, one file per source directory, and stopped
71+
# committing its cross-directory totals — the same measured conflict as the
72+
# liveness counts below, in the same driver-less server-side merge.
7073
#
7174
# The liveness state table's counts joined at #7377 for the same reason, one file
7275
# over — 9 of its 30 rows had drifted from the gate before anyone re-ran the
73-
# documented snippet. Same split and the same caveat: `liveness/state-counts.md`
74-
# is the numbers and is driver-managed; `liveness/README.md` is the Notes prose —
76+
# documented snippet. Same split and the same caveat: `liveness/state-counts/` is
77+
# the numbers and is driver-managed; `liveness/README.md` is the Notes prose —
7578
# hand-written measurement of how each type got where it is — and is NOT.
7679
# Regenerating a Note would fabricate a verdict, which that README calls worse
7780
# than a missing row.
7881
#
82+
# #20361 then SHARDED those counts, one file per governed type, and stopped
83+
# committing a total: the single file's shared total row was rewritten by every
84+
# liveness PR, so in the driver-less server-side merge any two of them conflicted
85+
# and each landing left every other one `dirty`, with no CI run until a
86+
# merge-and-regenerate round. The cure is the one the header above records for
87+
# the three hottest artifacts; each gate sums its shards when it reads them.
88+
#
7989
# The elevation census page joined at #13646 — a generated `file:line` anchor
8090
# table whose correct merged values are on NEITHER side of a conflict (measured on
8191
# #13625: five conflicted anchors resolved to 4408/5771/6019/6382/6575 against
@@ -138,7 +148,7 @@
138148
# neither side.
139149

140150
packages/spec/spec-changes.json merge=os-regen
141-
packages/spec/liveness/state-counts.md merge=os-regen
151+
packages/spec/liveness/state-counts/** merge=os-regen
142152
packages/spec/authorable-surface/** merge=os-regen
143153
packages/spec/authorable-surface.base.json merge=os-regen
144154
packages/spec/authorable-defaults/** merge=os-regen
@@ -149,7 +159,7 @@ packages/spec/export-origins/** merge=os-regen
149159
packages/spec/declaration-map/** merge=os-regen
150160
packages/spec/api-surface-signatures.json merge=os-regen
151161
docs/protocol-upgrade-guide.md merge=os-regen
152-
docs/audits/2026-07-unknown-key-strictness-ledger.counts.md merge=os-regen
162+
docs/audits/2026-07-unknown-key-strictness-ledger.counts/** merge=os-regen
153163
content/docs/references/** merge=os-regen
154164
content/docs/permissions/system-context.mdx merge=os-regen
155165
skills/*/references/_index.md merge=os-regen

‎docs/audits/2026-07-unknown-key-strictness-ledger.counts.md‎

Lines changed: 0 additions & 267 deletions
This file was deleted.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<!-- GENERATED — DO NOT EDIT BY HAND. -->
2+
<!-- Regenerate: pnpm --filter @objectstack/spec gen:strictness-ledger -->
3+
4+
# `ai/` — unknown-key strictness counts (generated)
5+
6+
The object-site total of `packages/spec/src/ai/`, computed from the AST
7+
(`packages/spec/scripts/lib/strictness-ledger.ts`). The directory is untriaged:
8+
the ledger classifies it coarsely, per directory rather than per file, so this
9+
total is the one number measured here.
10+
11+
The verdicts, the evidence and the exemption rationales live in
12+
[the ledger itself](../2026-07-unknown-key-strictness-ledger.md) and are
13+
hand-written; **this file has no prose to preserve** and is regenerated whole.
14+
One file per directory, and no total across directories is committed anywhere:
15+
`check:strictness-ledger` sums the shards when it reads them. **Never
16+
hand-patch a number here** — fix the code or the verdict and regenerate.
17+
18+
## Site total (untriaged)
19+
20+
| Dir | Sites |
21+
|---|---|
22+
| `ai/` | 78 |
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<!-- GENERATED — DO NOT EDIT BY HAND. -->
2+
<!-- Regenerate: pnpm --filter @objectstack/spec gen:strictness-ledger -->
3+
4+
# `api/` — unknown-key strictness counts (generated)
5+
6+
The object-site total of `packages/spec/src/api/`, computed from the AST
7+
(`packages/spec/scripts/lib/strictness-ledger.ts`). The directory is untriaged:
8+
the ledger classifies it coarsely, per directory rather than per file, so this
9+
total is the one number measured here.
10+
11+
The verdicts, the evidence and the exemption rationales live in
12+
[the ledger itself](../2026-07-unknown-key-strictness-ledger.md) and are
13+
hand-written; **this file has no prose to preserve** and is regenerated whole.
14+
One file per directory, and no total across directories is committed anywhere:
15+
`check:strictness-ledger` sums the shards when it reads them. **Never
16+
hand-patch a number here** — fix the code or the verdict and regenerate.
17+
18+
## Site total (untriaged)
19+
20+
| Dir | Sites |
21+
|---|---|
22+
| `api/` | 431 |

0 commit comments

Comments
 (0)