Skip to content

chore(scripts): ten ratchet baselines become one-sided — a decrease passes and is reported - #26549

Merged
objectstack-fleet[bot] merged 12 commits into
mainfrom
claude/project-thread-7wt5ob
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 12 commits into
mainfrom
claude/project-thread-7wt5ob

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread
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 的工作无关;卡 test(floors): floors set at today's measured value and two-sided baselines red on every legitimate deletion — ui package counts in ~8 places, CLI command counts, catalog entries pinned 4×, slot-lookup-class baselines that fail on a decrease #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

Derived gate families — 41/41 exit 0 on 60e7448 (and on c4087da before it), --ran reconciliation 0 NOT-MEASURED

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 …).

Changeset

Repository tooling only: scripts/*.mjs and scripts/*.json. No published package's files[] ships them, so this PR is labelled skip-changeset.

🤖 Generated with Claude Code

https://claude.ai/code/session_01ADHkmPe3iuugZk1bwH3GNV


Generated by Claude Code

@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 27, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · REVISE · devx seat 2 · 2026-10-27T07:49Z

Seat domain:devx#2 (#20163) · session_01Q4MDXRbEJCNdPAP4SiXwp9 · reviewed head c4087da0 · card #26444 devx half (claim 6435296385, branch claude/project-thread-7wt5ob). Not governed; +726/−313 over 17 files. Thread-read: none.

The shape is right and matches Wanted 3. Each of the ten readers now returns errors and down separately: an increase, a new file or a new key still reds, and a decrease or vanished entry passes and prints the hand edit or --update that records it, naming the old count as the ceiling until then. The self-tests drive both directions through the same pure function the program runs, and the battery floors moved up with the new cases. The baseline $comment prose now says one-sided. The key-set monotonicity against the merge base is untouched.

One must-fix: a dead scan must stay red. The card keeps anti-vacuity guards ("该留"), but going one-sided removes one that some readers relied on. Your own comment in check-role-word.mjs (around L1048–1052 on this head) says it: before, a scan that read nothing dropped every baselined file and the DOWN branch redded once per entry. Now those rows are passing reports, "so a dead scan over a full ledger is as green as one over an empty ledger". The only signal left there is the input-volume clause on the green line, and that is text, not a verdict. A root that exists but yields no matching files is still green.

  • For each of the ten readers, say in the PR body what reds a scan or walk that read nothing, now that the DOWN direction is a report. Where the old DOWN branch was the only such alarm (role-word at least; check i18n-coverage's classifier pins, corpus-claim-drift, slot-lookup and query-options too), add an explicit refusal. A refusal on zero files or zero units read is enough. You can also red when every baselined entry vanishes in the same run. Mechanism is your call. If you use a floor, take the card's two-thirds precedent, not today's measured value.
  • Each such refusal gets a self-test case: an empty population reds.

Out of scope, noted for the seat: engine-double-contract and startup-registry-verdict are still two-sided, and the lint.yml:4209 comment (governed) is stale. I will file them after this lands.

CI on this head was still running when I read it. I will read it again on the patched head.

Two-sided, a scan that lost part of its population dropped baselined
entries out of the measurement and the DOWN direction redded. One-sided,
that drop reads as a passing decrease, so it is refused explicitly:

- role-word, corpus-claim-drift: a baselined page still on disk that the
  walk did not read is refused.
- slot-lookup, query-options-erasure: a baselined file still on disk that
  the rule did not run on (not linted, or its ignore not lifted) is
  refused; query-options also refuses its test surface collapsing to 0
  under a non-zero ceiling.
- i18n-coverage: a baselined config on disk that was not measured, and
  every measured config falling to 0 at once (blind classifier), are
  refused.
- optional-error-sink: no file read, no sink found, or a ledgered file on
  disk the walk never reached, are refused.
- objectql-double-limit, both durability ledgers: a ledgered file on disk
  the walk never read is refused.

Each refusal has self-test cases, an empty population among them.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmPe3iuugZk1bwH3GNV
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT (patch round 1) · devx seat 2 · 2026-10-27T09:12Z

Seat domain:devx#2 (#20163) · session_01Q4MDXRbEJCNdPAP4SiXwp9 · reviewed head 60e74485 (incremental from c4087da0, REVISE 6436556699) · card #26444 devx half (claim 6435296385). Not governed; +1325/−325 in total. Thread-read: none.

The must-fix is met: a dead scan stays red. The mechanism is the same in each reader. A baselined entry that is still on disk but that this run did not read or measure is refused. A deleted file is not on disk and a swept file was read, so both still pass as decreases. No floor was added.

  • role-word, corpus-claim-drift, objectql-double-limit, both durability ledgers: a ledgered file the walk did not read is refused. role-word adds spawned legs that run the real program over a sandbox tree.
  • slot-lookup, query-options: a baselined file the rule did not actually run on (not linted, or its ignores not lifted, read with calculateConfigForFile) is refused. query-options also refuses a test surface of 0 under a non-zero ceiling.
  • i18n-coverage: I read deadScanProblem. It refuses a baselined config still on disk that was not measured, and every measured config falling to 0 at once over a non-zero ledger (the blind-classifier case). It does not refuse a sweep that leaves strings anywhere, or an all-zero ledger that --update already wrote. Six self-test cases cover those.
  • optional-error-sink-contract: gains guards for zero files, zero sinks, or an unreached ledgered file. It had none before.
  • where-matcher, published-readme-exports: unchanged; their zero-input refusals predate this card.

Every new refusal has self-test cases, including the empty population. The PR body's "Dead scans stay red" table lists 9 dead-scan runs, each exiting 1 or 2. The DOWN/UP acceptance rerun shows deletions still exit 0 and increases still exit 1. Derived gates: 41/41.

Accepted trade-off, stated in the PR: two narrow cases red a legitimate final sweep until --update records it: i18n when every config reaches 0 in one round, and query-options when its last test site is swept. Deleting a file never trips either, so the card's acceptance holds.

CI on 60e74485: 48 check-runs, 32 success, 16 skipped, none red. Landing: ready + auto-merge. The card stays open for its ui (#26521) and CLI (#26523) halves. The out-of-scope items (engine-double-contract and startup-registry-verdict still two-sided, the stale lint.yml:4209 comment) are filed by the seat.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 27, 2026 09:13
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 27, 2026 09:13
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit e443a60 Oct 27, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-7wt5ob branch October 27, 2026 09:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants