Repository navigation
Commit e443a60
chore(scripts): ten ratchet baselines become one-sided — a decrease passes and is reported (#26549)
<!-- claude-projects-attribution: {"github_login":"os-steve"} -->
_Requested by **Steve** · [project
thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9MprVKsmUWQJJ3yeQLxiEMG)_
Part of #26444
Part of #26431
Clause-②: no
Metadata-protocol: none
The devx half of #26444 only. The ui floors are #26521 and the CLI
floors are #26523, so this PR does not close the card.
## 维护者速读
- 改了什么:10
个棘轮读者(slot-lookup、query-options-erasure、role-word、i18n-coverage、corpus-claim-drift、objectql-double-limit、where-matcher、optional-error-sink、durability
两本账、published-readme-exports)从「两侧」改成「单侧」:基线里的数减少或文件被删 →
通过,并在输出里报告;增加或新文件 → 照旧变红。空扫描仍然变红:基线里列着、文件还在磁盘上、这次却没读到,就拒绝(第一轮席位复核加的)。
- 为什么改:删一个文件或修好一处,就要跑 `--update` 才能过 CI,跟这个 PR 的工作无关;卡 #26444 的 Wanted
3。
- 风险与回滚:代价是「余量」。减少后没跑 `--update`,旧数仍是上限,可以悄悄补回同样多的违例;每条报告都写明了余量。回滚就是
revert 本 PR,没有数据或接线变化。
-
先看:`scripts/check-query-options-erasure-ratchet.mjs`(唯一带聚合上限的一本)、`scripts/check-i18n-coverage.mjs`(比较逻辑抽成纯函数)、`scripts/check-durability-degradation-log-level.mjs`(两本账共用的
reconcileLedger)。
- 两分钟验证:`rm content/docs/api/error-catalog.mdx && node
scripts/check-role-word.mjs; echo $?; git checkout HEAD --
content/docs/api/error-catalog.mdx`,应看到 `1 baselined file(s) improved
(passing, reported)`,退出码 0。
- 需要你做的:无,席位落地。
## What changes
Each reader keeps its error path for the increase direction and gains a
pure, separately-named decrease report:
| Reader | Baseline | Decrease (now passes, printed on stdout) |
Self-test |
|:---|:---|:---|:---|
| `check-slot-lookup-ratchet.mjs` | `slot-lookup-baseline.json` |
`ratchetDownReport()` | battery 3 floor 6 → 11 |
| `check-query-options-erasure-ratchet.mjs` |
`query-options-erasure-baseline.json` (per-file + `testSurface`
aggregate) | `ratchetDownReport()` (exported beside `diffRatchet`) |
battery 2 floor 8 → 15 |
| `check-role-word.mjs` | `role-word-baseline.json` |
`ratchetVerdict().down` | new battery (7), roster floor 14 → 15 |
| `check-i18n-coverage.mjs` | `i18n-coverage-baseline.json` |
`ratchetVerdict().down` (comparison lifted out of the top level) | new
battery (6), roster floor 6 → 7 |
| `check-corpus-claim-drift.mjs` | `corpus-claim-drift-baseline.json` |
`ratchetVerdict().down` | new battery (7), roster floor 17 → 18 |
| `check-objectql-double-limit.mjs` |
`objectql-double-limit.baseline.json` | `reconcileDown()` | battery
floor 47 → 52 |
| `check-where-matcher-conformance.mjs` |
`where-matcher-conformance.baseline.json` | `reconcileDown()` | new
battery (5), roster floor 3 → 4 |
| `check-optional-error-sink-contract.mjs` |
`optional-error-sink-contract.baseline.json` | `reconcileLedger().stale`
| floor 66 → 70 |
| `check-durability-degradation-log-level.mjs` |
`durability-degradation.baseline.json`,
`durability-read-invention.baseline.json` | `reconcileLedger().stale`
(shared by both rules) | one row per partition; floors 68/57/125 →
69/58/127 |
| `check-published-readme-exports.mjs` |
`published-readme-exports.baseline.json` | stale entries printed on
stdout; exit 0 | new end-to-end battery (3) on the scratch workspace,
roster floor 27 → 28 |
Every decrease line names its headroom: the old number stays the
ceiling, or the stale key keeps licensing that site, until `--update`
(or the hand edit, for the two gates with no `--update`) records it.
`--update` is unchanged and remains the way to write the report down.
Kept as errors on purpose:
- A NEW file or key, and a grown count (the defect each gate exists
for).
- Every key-set monotonicity check ("ADDED to the baseline").
- corpus-claim-drift: a ledger row naming a rule `VOCABULARY` no longer
holds. That is an unfinished retirement, not a decrease.
The `$comment` headers of the seven baselines that described two-sided
reconciliation now describe the one-sided one. Only `$comment` text
changed; no counts or keys moved.
## Premise, measured on `origin/main` b7f2b0c before the change
All ten readers were two-sided, and none was already one-sided. A real
deletion of a listed file, run on the base tree, went red:
| Gate | Deletion on base | Exit |
|:---|:---|:---|
| role-word | `content/docs/api/error-catalog.mdx` | 1 (`baselined file
is clean/gone … ratchet DOWN`) |
| corpus-claim-drift | `content/docs/protocol/kernel/http-protocol.mdx`
| 1 (`improved 1 → 0 … ratchet DOWN`) |
| objectql-double-limit |
`packages/metadata-protocol/src/protocol-publish-drafts-advisories.test.ts`
| 1 (`clean or gone -- ratchet DOWN`) |
| optional-error-sink |
`packages/services/service-settings/src/settings-service.types.ts` | 1
(`stale entry … shrink-only`) |
| durability (both ledgers) | stale entry injected (simulates a gone
file) | 1 each |
| where-matcher | stale entry injected (the ledger is empty) | 1 (`clean
or gone` + `ADDED`) |
| slot-lookup, query-options | base self-test cases `a fall must be
ratcheted down` / `a cleaned file must be dropped` assert 1 error | read
off the pure `diffRatchet` |
| i18n-coverage, published-readme-exports | code read: decrease / stale
branch pushes an error and exits 1 | not run on base (build needed) |
## Acceptance: deleting a listed file reds no ratchet; a new violation
still reds
Run on head `60e74485`, after the dead-scan round. The first run, on
`c4087da0`, gave the same exits. Each mutation is applied, the gate
runs, then the tree is restored with `git checkout HEAD --` plus removal
of new files, and the restore is checked with `git status` (clean every
time).
| Gate | DOWN: deleted a listed file → exit | UP: added a violation →
exit |
|:---|:---|:---|
| role-word | `error-catalog.mdx` removed → **0**, reported | one more
"role" in the same page → **1** |
| corpus-claim-drift | `http-protocol.mdx` removed → **0**, reported |
duplicated the claim line → **1** |
| objectql-double-limit | `protocol-publish-drafts-advisories.test.ts`
removed → **0**, reported | copied it to a new path → **1** (NEW) |
| where-matcher | ledger is empty, so no real deletion is possible;
self-test only | new test file (copied from a fixture) with an
early-return matcher → **1** (NEW) |
| optional-error-sink | `settings-service.types.ts` removed → **0**,
stale reported | new `interface { error?: … }` → **1** |
| durability (log-level) | stale entry injected → **0**, reported | new
catch around `syncSchema` logging `warn` → **1** |
| durability (read invention) | stale entry injected → **0**, reported |
(the UP above runs both rules in one process) |
| slot-lookup | `packages/core/examples/kernel-features-example.ts`
removed → **0**, reported | one more `getService<any>` in that file →
**1** |
| query-options-erasure |
`packages/core/src/security/resolve-authz-context.ts` removed → **0**,
reported | one more `query as any` in that file → **1** |
| i18n-coverage | `examples/app-todo/objectstack.config.ts` removed →
**0**, reported | one `zh-CN` label line deleted → **1** |
| published-readme-exports | ledger is empty, so a stale entry was
injected → **0**, reported | an invented import in
`packages/spec/README.md` → **1** |
## Dead scans stay red (seat review, round 1)
When the ratchets were two-sided, a scan that lost part of its
population dropped those baselined entries out of the measurement, and
the DOWN direction went red once per entry. Now that the ratchets are
one-sided, that same drop would read as a passing decrease. So the gate
now asks the scan itself: if a baselined entry is still **on disk** but
the run did not read it, the gate refuses. A deleted file is not on
disk, and a swept file was read, so both still pass. The table lists,
for each reader, what reds a scan or walk that read nothing. "New" marks
a refusal added in this round. Each new refusal has self-test cases, and
one of them is an empty population.
| Reader | What reds a scan that read nothing | New self-test |
|:---|:---|:---|
| role-word | missing ROOT (existing, #9932); **new:** a baselined page
on disk that the walk did not read | battery (7), including two spawned
legs: a ledgered page parked in a skipped subtree exits 1, and the same
entry with the page deleted exits 0 |
| corpus-claim-drift | missing ROOT (existing); a scan that read nothing
also leaves every VOCABULARY row unreachable (existing); **new:** a
baselined page on disk that the walk did not read | battery (5) |
| slot-lookup | ESLint throws on a target that matches no file; the
rule-gone and rescoped refusals (existing); **new:** a baselined file on
disk that the rule did not run on (not linted, or its `ignores` not
lifted, read from `calculateConfigForFile`) | battery 6 (7), including
two live legs: with the ignore left in place the resolved config has no
rule, and with it lifted the resolved config has the rule |
| query-options-erasure | the same ESLint and rule-gone guards
(existing); **new:** a baselined non-test file the rule did not run on;
**new:** the test surface measuring 0 against a non-zero ceiling |
battery 6 (8) |
| i18n-coverage | zero configs refused, exit 3 (existing, #10907);
**new:** a baselined config on disk that the round did not measure;
**new:** every measured config reading 0 while the ledger still holds
strings (`os lint` renamed its `i18n/` rules, or the classifier went
blind) | battery (6) |
| objectql-double-limit | DISCOVERED: zero graded doubles (existing);
**new:** a baselined file on disk that the corpus walk never read | 4
cases |
| where-matcher-conformance | DISCOVERED: zero matchers found
(existing). The ledger is empty, so there is no row a partial scan could
turn into a decrease. | none (unchanged) |
| optional-error-sink | **new:** no source file read, no sink declaring
`error` found at all, or a ledgered file on disk the walk never reached.
Before this round nothing caught it: a dead scan reported all five rows
as stale and passed. | 5 cases |
| durability (both ledgers) | a scan that read nothing leaves
FAILURE_PROPAGATION_SITES / READ_FAILURE_DISCRIMINATORS stale, which
still reds (existing, left two-sided); **new:** a ledgered file on disk
the walk never read | one row per partition |
| published-readme-exports | DOCUMENTS = 0 and the other population
refusals (existing, self-tested) | none (unchanged) |
No floor was introduced, so none rots on the next `--update`. Each
refusal checks an identity (on disk and not read) or a zero.
There are two places where a legitimate final sweep is refused until
`--update` records it. The message names that remedy, and the zero
ceiling that `--update` writes disarms the refusal:
- i18n-coverage, when the last untranslated strings in every measured
config are translated in one round.
- query-options, when the last test-code erasure is swept.
Deleting a file never trips either refusal.
Dead-scan demos on `60e74485`. In each one, the gate's own walk or lift
was broken for a single run, the gate was run, and the file was restored
with `git checkout HEAD --` and a clean `git status`:
| Gate | Mutation (script only) | Exit |
|:---|:---|:---|
| role-word | `content/docs/api` added to SKIP_SUBTREES | **1**, names
the 2 ledgered pages it did not read |
| corpus-claim-drift | `content/docs/protocol/kernel` added to
SKIP_SUBTREES | **1**, names `http-protocol.mdx` |
| slot-lookup | the measuring config lifts no `ignores` | **2**, 25
baselined files the rule did not run on |
| query-options | the same, for its non-test run | **2**, 15 files |
| query-options | the test globs not lifted | **2**, test surface 0
against a ceiling of 236 |
| i18n-coverage | the classifier matches `i18nx/` instead of `i18n/` |
**1**, every config at 0 against 527 |
| optional-error-sink | SCAN_ROOTS pointed at a missing directory |
**1**, no source file read |
| objectql-double-limit | the walk skips `metadata-protocol` | **1**,
UNREAD: 49 baselined files |
| durability (read invention) | `packages/objectql/src` dropped from the
read-seam roots | **1**, names `engine.ts` |
## Tests
- `--self-test` of all ten readers: exit 0, each carrying its new
decrease-passes and increase-reds cases (table above).
- Production run of all ten readers on head: exit 0.
- No package test exercises these scripts. The 15 `*.test.ts` files that
name one of them only mention it in comments; those were read and none
asserts behaviour this PR changes.
## Acceptance notes
- **Headroom is the stated cost.** A one-sided ratchet left above
reality licenses that many new sites until someone runs `--update`. The
decrease report names the number on every run. The query-options
test-surface line spells out `licenses N new test-code erasure(s)`.
- **The detector-weakening signal moved to the self-tests.** A rule
narrowed so far that live counts fall used to red the production run of
slot-lookup, query-options and i18n-coverage. It now prints decrease
reports. The self-tests, which drive the real rule over synthetic
sources in both directions, are now the only thing that catches it. The
slot-lookup header and the i18n classifier comment say this.
- **eslint-ignore couplings (slot-lookup, query-options), re-read as the
card asked.** The baseline keys are still the `ignores` of their
`eslint.config.mjs` blocks, and both ratchets still measure listed files
with the ignore lifted, so growth in a listed file stays red. A key
whose file is gone stays an `ignores` glob until `--update` drops it,
which does no harm. If a file is later recreated at that same path, it
rides the stale count. The report says to drop the entry.
- **Keyed ledgers** (optional-error-sink, both durability ledgers,
readme-exports): a stale row re-licenses that exact key until it is
deleted. The report says so.
- Not touched: `.github/workflows/lint.yml` (no step's invocation
changed). Its comment at line 4209 still calls the query-options test
count "decrease-only". It is a governed file and only a comment.
- Left two-sided, outside this card's list:
`engine-double-contract.baseline.json` and
`startup-registry-verdict.baseline.json` (both still fail on a stale
entry), and the durability vocabulary staleness checks
(`FAILURE_PROPAGATION_SITES`, `READ_FAILURE_DISCRIMINATORS`), which are
declarations rather than baselines.
-
`packages/metadata-protocol/src/protocol.metadata-store-outage.test.ts:636-639`
has a comment saying the durability ledger and code are "pinned to each
other in both directions". Only the restored-limb direction still reds.
This is a comment outside the file surface, so it is left alone.
## Verification
<details><summary>Derived gate families — 41/41 exit 0 on 60e7448 (and
on c4087da before it), --ran reconciliation 0 NOT-MEASURED</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 41 families, and each was run with
its exit captured before any pipe. `pnpm check:entry-guard` first went
red, because `reconcileLedger` was exported from a file with an
unguarded top level. The function was made module-private (commit
c4087da), and the gate then exited 0. `pnpm check:i18n-coverage` and
`pnpm check:published-readme-exports` ran after a full `pnpm build`
under `os-verify-lock`.
After the dead-scan round, the same 41 families were derived again (the
set was unchanged) and run again on `60e74485`, and all 41 exited 0. On
that head, `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --ran FILE` printed: `41 derived famil(ies)
accounted for — 41 run, 0 NOT-MEASURED (a DERIVED zero …)`.
</details>
<details><summary>Changeset</summary>
Repository tooling only: `scripts/*.mjs` and `scripts/*.json`. No
published package's `files[]` ships them, so this PR is labelled
`skip-changeset`.
</details>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01ADHkmPe3iuugZk1bwH3GNV
---
_Generated by [Claude
Code](https://claude.ai/code/session_01ADHkmPe3iuugZk1bwH3GNV)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 9f181af commit e443a60
17 files changed
Lines changed: 1325 additions & 325 deletions
File tree
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
536 | 536 | | |
537 | 537 | | |
538 | 538 | | |
539 | | - | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
540 | 544 | | |
541 | 545 | | |
542 | 546 | | |
543 | | - | |
544 | | - | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
545 | 611 | | |
546 | 612 | | |
547 | 613 | | |
| |||
657 | 723 | | |
658 | 724 | | |
659 | 725 | | |
| 726 | + | |
| 727 | + | |
660 | 728 | | |
661 | 729 | | |
662 | 730 | | |
663 | 731 | | |
664 | | - | |
| 732 | + | |
665 | 733 | | |
666 | 734 | | |
667 | 735 | | |
| |||
1232 | 1300 | | |
1233 | 1301 | | |
1234 | 1302 | | |
| 1303 | + | |
| 1304 | + | |
| 1305 | + | |
| 1306 | + | |
| 1307 | + | |
| 1308 | + | |
| 1309 | + | |
| 1310 | + | |
| 1311 | + | |
| 1312 | + | |
| 1313 | + | |
| 1314 | + | |
| 1315 | + | |
| 1316 | + | |
| 1317 | + | |
| 1318 | + | |
| 1319 | + | |
| 1320 | + | |
| 1321 | + | |
| 1322 | + | |
| 1323 | + | |
| 1324 | + | |
| 1325 | + | |
| 1326 | + | |
| 1327 | + | |
| 1328 | + | |
| 1329 | + | |
| 1330 | + | |
| 1331 | + | |
| 1332 | + | |
| 1333 | + | |
| 1334 | + | |
| 1335 | + | |
| 1336 | + | |
| 1337 | + | |
| 1338 | + | |
| 1339 | + | |
| 1340 | + | |
| 1341 | + | |
| 1342 | + | |
| 1343 | + | |
| 1344 | + | |
| 1345 | + | |
1235 | 1346 | | |
1236 | 1347 | | |
1237 | 1348 | | |
| |||
1545 | 1656 | | |
1546 | 1657 | | |
1547 | 1658 | | |
1548 | | - | |
1549 | | - | |
1550 | | - | |
1551 | | - | |
1552 | | - | |
1553 | | - | |
1554 | | - | |
1555 | | - | |
| 1659 | + | |
| 1660 | + | |
| 1661 | + | |
| 1662 | + | |
1556 | 1663 | | |
1557 | | - | |
1558 | | - | |
1559 | | - | |
1560 | | - | |
1561 | | - | |
1562 | | - | |
1563 | | - | |
1564 | | - | |
1565 | | - | |
1566 | | - | |
1567 | | - | |
1568 | | - | |
1569 | | - | |
1570 | | - | |
1571 | | - | |
| 1664 | + | |
| 1665 | + | |
| 1666 | + | |
| 1667 | + | |
| 1668 | + | |
| 1669 | + | |
1572 | 1670 | | |
1573 | 1671 | | |
1574 | 1672 | | |
| |||
0 commit comments