# @geonosis/ratchet

## 3.7.0

### Minor Changes

- cd20fc3: `catalogueArguments` counts the messages of a language that name other ICU template arguments than the source language's message under the same key (#188). Each message is read by `@formatjs/icu-messageformat-parser`, inside `plural` and `select` options and rich-text tags. A tag counts as an argument under its name, `<b>`, because intl-messageformat 12.1.3 needs a value for it and throws `MissingValueError` without one, so a language renaming `<b>` to `<link>` is counted. `{locale}` is one path segment: `messages/archive/en-old.json` is no language under `messages/{locale}.json`. `exceptions` declares a message allowed to differ, with its reason, and an exception that stops differing is counted, so no reason outlives its case. Params: `source`, `catalogues` (`messages/{locale}.json`), `exceptions`.
- e0f0882: New `@geonosis/gate`: the heavy slots, the machine-wide heavy lock and the `geonosis-gate` daemon, moved out of `@geonosis/ratchet` with their behaviour and tests unchanged (#215, plan 049 T5). It joins the fixed group; `geonosis-gate serve`, `status` and `stop` are its bin now, and a repo that starts the daemon adds `@geonosis/gate` by name.
  
  `@geonosis/ratchet` no longer ships the `geonosis-gate` bin and re-exports none of what moved. Import these from `@geonosis/gate` instead: `@geonosis/ratchet: acquireExclusive`, `@geonosis/ratchet: availableMemoryMb`, `@geonosis/ratchet: buildLockPath`, `@geonosis/ratchet: DEFAULT_MEMORY_MB_PER_SLOT`, `@geonosis/ratchet: describeBudget`, `@geonosis/ratchet: describeHolders`, `@geonosis/ratchet: GATE_PARENT`, `@geonosis/ratchet: GATE_RUN`, `@geonosis/ratchet: GateAccepted`, `@geonosis/ratchet: GateAnswer`, `@geonosis/ratchet: gateCacheDir`, `@geonosis/ratchet: gateDaemonPid`, `@geonosis/ratchet: gateKey`, `@geonosis/ratchet: gateLastLine`, `@geonosis/ratchet: gateLogPath`, `@geonosis/ratchet: GateLost`, `@geonosis/ratchet: GateRefused`, `@geonosis/ratchet: GateRun`, `@geonosis/ratchet: gateRunsDir`, `@geonosis/ratchet: gateSocketPath`, `@geonosis/ratchet: gateStatus`, `@geonosis/ratchet: HEAVY_SLOT_HELD`, `@geonosis/ratchet: HeavyBudget`, `@geonosis/ratchet: heavyBudget`, `@geonosis/ratchet: heavyLockPath`, `@geonosis/ratchet: heavySlotDir`, `@geonosis/ratchet: HeavySlotHolder`, `@geonosis/ratchet: heavySlots`, `@geonosis/ratchet: HeavySlotTimeout`, `@geonosis/ratchet: Holder`, `@geonosis/ratchet: readCacheEntry`, `@geonosis/ratchet: runViaGate`, `@geonosis/ratchet: serveGate`, `@geonosis/ratchet: slotHolders`, `@geonosis/ratchet: slotInherited`, `@geonosis/ratchet: stopGate`, `@geonosis/ratchet: submitToGate`, `@geonosis/ratchet: takeSlot`, `@geonosis/ratchet: waitForBuild`, `@geonosis/ratchet: watchGateParent`.
  
  `@geonosis/verify`, `@geonosis/evals`, `@geonosis/rails` and `@geonosis/release` import them from `@geonosis/gate` and depend on it. The doctor's fixed group and the metapackage's by-name list name `@geonosis/gate`.
- 1dabee8: JSONC is read by Microsoft's `jsonc-parser` in place of the comment-and-comma stripper `@geonosis/base` carried, and a wrangler config of any extension is read by wrangler's own config reader (`experimental_readRawConfig`), as a `wrangler.toml` already was:
  
  - **Refused now, as oxlint and wrangler refuse it:** a comment between two tokens, such as `1/* why */0`, which the stripper joined into `10`. The doctor, the ratchet's counters, lint-parity, the MCP preamble, `geonosis explain`, `create-geonosis --plan`, the evals scorer and `deriveLocalOnlyConfig` name the line of what they could not read.
  - **Read now:** a config whose lines end in a carriage return alone, which wrangler and TypeScript read and the stripper refused; the lint plugin reads such a `tsconfig.json`'s `paths`.
  - **`geonosis explain`** refuses, naming the file, when a file it reads does not parse, where it answered with defaults.
  - **New:** `@geonosis/ratchet` and `@geonosis/cli` export `parseJsonc`, which throws a `SyntaxError` naming the error and its line.
  - **Removed:** `@geonosis/base: parseJsonc` (`@geonosis/base` takes no dependency) and `@geonosis/release: parseJsonc`. Import `parseJsonc` from `@geonosis/ratchet`, or depend on `jsonc-parser`.
- 5f03fe8: `geonosis-ratchet --prove` plants a finding of an `oxlintRule` entry's own `rule` (#158). It planted a `no-explicit-any` for every entry, so a census of `react/jsx-key` was PROVEN by a rule it never counts. The plant is the first that exists:
  
  - the ten census rules ship a source file each (`RULE_PLANTS`), every one checked against oxlint 1.88.0 to report exactly one finding under that rule: `no-explicit-any`, `iframe-missing-sandbox`, `jsx-key`, `jsx-no-duplicate-props`, `jsx-no-script-url`, `jsx-no-undef`, `no-children-prop`, `no-danger-with-children`, `refs` and `set-state-in-effect`;
  - a kit plugin rule is planted from the reach corpus of the plugin installed in the repo: a corpus file that fires the rule, one that fires only that rule first, linted alone under the rule's level and options in the config the counter reads, or the whole corpus where no single file fires it;
  - an entry may declare `plant: { path, source }`, planted beside a copy of the config the counter reads;
  - any other rule is UNPROVEN, naming the rule and the `plant` to declare.
  
  The plant is read under the entry's own `rule` spelling and through its `config` where it names one, so `--prove` measures what the run measures. `oxlintRule` reads `rule: "biological-architecture/no-literal-copy"`, the config's spelling, as oxlint prints it, `biological-architecture(no-literal-copy)`: it read 0 where `no-literal-copy` read 197, and `--prove` said PROVEN. A `plugin/rule` oxlint accepts but prints under another plugin name (`@typescript-eslint/no-explicit-any`, printed `typescript(no-explicit-any)`) is refused, naming the spelling oxlint prints; `ruledSeparately` reads `plugin/rule` the same way.
  
  A counter may declare `plantFor(params, cwd)`: a probe for that entry, or the reason none can be planted.
- 957ee47: The ratchet keys each finding of `oxlintErrors`, `oxlintWarnings` and `oxlintRule`, as Betterer does: by file, rule and a hash of the whole flagged line (whitespace collapsed), never by line number, kept in `gate-baseline.findings.json` beside the baseline. One fix plus one new finding at an unchanged total now fails the run, naming the new finding's file, line, column and rule. Before, it passed. A fixed finding is banked with the number. A counter with a number and no findings yet has them written without a judgement, and the run passes. A findings file that does not parse refuses those counters. `--raise` records a finding that moved (a renamed file, an edited line) at the same number. `--prove` plants a new finding beside a removed one at an unchanged total and requires the refusal. An entry whose command prints paths from a subdirectory names it as `pathsFrom`. Counters that read no findings are unchanged (#215, plan 049 T6). The plugin's write guard refuses an edit of `gate-baseline.findings.json` as it refuses the baseline's, and the question a person is asked before `--raise` at the baseline says the findings read now replace those held.
- 281381c: The walk gains an `axe` probe through `@axe-core/playwright`, an optional peer: `critical` and `serious` violations are findings of class `accessibility-error` (blocking, major), `moderate` and `minor` of class `accessibility-warning`, and a missing peer exits 2 naming `npm i -D @axe-core/playwright` (#215, plan 049 T3). The report counts both classes whenever `axe` ran, and the summary names both beside it. The `walkFindings` counter takes `classes: ["accessibility-error"]` and `["accessibility-warning"]`, so a gate can count the errors without the warnings.

### Patch Changes

- 1830b16: `@geonosis/base/node` exports `treeHashOf`, `treeFilesOf`, `filesThatMoved`, `isolatedGit`, `withoutGitRedirects`, `GIT_IDENTITY`, `answerInformational` and `informationalAsk`, moved from the ratchet's source so a package below the ratchet can use them (#215, plan 049 T5). `@geonosis/ratchet` still exports every one of them, unchanged.
- 415b7f5: `doctorWarnings` reads the doctor's JSON report from the command's stdout alone. It used to read stdout and stderr joined, so a line on stderr after the report hid it: in a pnpm repo with only `geonosis` installed and no bun on the PATH, the door's `geonosis: bun is not on PATH, so this runs on node.` line made the counter `CANNOT MEASURE`. `CommandResult` carries `stdout` beside the joined `output`, and a recorded run without `stdout` is refused, naming the command.
- 6552c12: A counter command that starts with a kit tool, `geonosis-<tool>` (the `doctorWarnings` default `geonosis-doctor --json` among them), runs `node_modules/.bin/geonosis <tool>` when the repo installs only the `geonosis` door and not the tool's own bin, as a pnpm repo with only `geonosis` installed does; it used to fail with exit 127. The recorded identity reads `geonosis-<tool>` and `geonosis <tool>` as the same instrument, so a fall measured through either spelling under a baseline recorded with the other is banked rather than refused as unproven. The default command and the counter's fingerprint are unchanged, so an existing baseline keeps banking falls.
- 91e682b: A counter or verify step naming `geonosis-<tool>` is run as `geonosis <tool>` only for a tool the `geonosis` door runs, and only those spellings share a measurement identity. `geonosis-rails`, `geonosis-gate`, `geonosis-themekit`, `geonosis-evals`, `geonosis-mcp` and `geonosis-release-cloudflare` used to be rewritten to a `geonosis <name>` the door refuses.
- fcaa6ab: The `fastTierMs` counter's refusals, no gate report and a report of another tier, name the tier to run as `geonosis verify <tier>`, the door, instead of `geonosis-verify <tier>`, which under pnpm is a command only where `@geonosis/verify` is a direct dependency (#68).
- 5ecf548: An oxlint finding is keyed by its file, its rule and a hash of its whole line, whitespace collapsed. It was keyed by the text from its column to the end of the line, so fixing `id: any` while adding `customer: any` in the same file, or `one: any = 1` while adding `two: any = 1`, passed at the same total: both keyed `any` or `any = 1`. Now each new one fails the run, named. A moved or re-indented line keeps its key; identical lines in one file share a key, counted. An edited flagged line reads as a new finding, which `geonosis ratchet --raise` at the baseline number records. `gate-baseline.findings.json` moves to scheme 2: findings kept under scheme 1 are read as none and written again, not judged, on the next run.
- c19e51a: A message that tells a person what to run names it through the `geonosis` door: the baseline write refusal and the ratchet's FAIL name `geonosis ratchet --raise …`, an unproven fall `geonosis ratchet --bank …`, a missing or stale recording `geonosis ratchet --record …`, the session doctor `geonosis doctor`, the Stop hook, `geonosis verify` and the doctor's `runner` check `geonosis verify prove`, the ledger's handoff check `geonosis ledger handoff`, its push guard `geonosis ledger land --attest-only`, a plan envelope's next step `geonosis ledger plan check`, the release's smoke and migrations messages `geonosis release …`, each tool's `--help` and `--print-config-shape` pointers `geonosis <tool> …`, the write guard on `.geonosis/` `geonosis verify fast`, and init and post-update's census line name `geonosis ratchet` and `geonosis verify` for the gates init writes. Init no longer asks the root to declare `@geonosis/ratchet` and `@geonosis/verify` for their bins: its gates call the door. Under pnpm a tool's own bin is a command only where its package is a direct dependency (#68).
- 1974fe6: `geonosis-ratchet --prove` measures an `oxlintRule` plant under the config the counter names (its `config`, or `.oxlintrc.json`), with that rule's level and options as the config sets them. Before, a kit rule was planted under its corpus options and a census rule under a one-rule config, so a counter whose config scoped `biological-architecture/no-literal-copy` to `paths: ["^nowhere/"]` read 0 in every run and `--prove` said PROVEN, reading 7. Such a counter is now UNPROVEN, naming the level its config sets and the corpus files the rule no longer reaches there. A `plant` the counter declares is used when no corpus file is reached, and is planted under the same config, read with `jsonc-parser` and with its `extends` resolved. A kit rule is now planted from one corpus file that fires it, as described; the lookup missed every file and always planted the whole corpus.
- 8eda0d0: A refusal that names a command spells it the way it was reached. Run through the `geonosis` door, `geonosis walk` and `geonosis review` refuse an unknown flag with `run geonosis walk --help` and `run geonosis review --help`, and `geonosis verify-arch --check` names `geonosis verify-arch --format … --output …` as the command that rewrites a stale drawing; run by their own bins they name those bins as before. The `walkFindings` counter with no walk report says to run `geonosis walk`. Each named the tool's own bin, which under pnpm is a command only where its package is a direct dependency (#68).
- b505542: A tool run through the door prints its usage the way it was run: `geonosis ratchet --help` says `geonosis ratchet [--cwd <dir>] …`, not `geonosis-ratchet …`, and the same for every tool the door fronts. The door tells the tool in `GEONOSIS_DOOR`, naming the bin it started, so a tool that tool starts in turn keeps its own name. `@geonosis/base/node` exports `doorSpelled`, `doorEnvFor` and `DOOR_ENV`. A tool run by its own bin prints its own name, as before (#68).
- cc48e0e: The `walkFindings` counter takes `classes: ["content-integrity"]`, the class of the walk's four newest probes (`thin-page`, `empty-section`, `money-format`, `orphan-url`). Before, the counter's own list of classes lacked it, so those findings could be counted only in the total. A test in the kit's repository holds the counter's list to the walk's own list of classes, the one its exported `DefectClass` type is drawn from. The walk's package description counts fourteen probes, thirteen computed-DOM readings and axe's accessibility rules; it counted eight.
- 7492312: `geonosis-ratchet --prove` reads a `walkFindings` entry's `classes` as the run does. It planted one `fabricated-claim` finding and measured it with no `classes`, so an entry naming a misspelled class (`accessibility-errors`) was PROVEN while every run refused it. Now `--prove` plants one finding of each class the entry names and measures them under those `classes`: a misspelled class is CANNOT MEASURE, naming it, and a real one reads its plant.
- Updated dependencies [1830b16]
- Updated dependencies [e0f0882]
- Updated dependencies [1dabee8]
- Updated dependencies [74147f7]
- Updated dependencies [b505542]
  - @geonosis/base@3.7.0
  - @geonosis/gate@3.7.0

## 3.6.1

### Patch Changes

- 71be23b: The `geonosis-gate` daemon keeps only green reports. A red can come of the machine's load, an ignored file or the environment, so once the run that made it is over, the next verify runs the tier again. Before, the red was answered from the cache until `--fresh`. A cached green is answered with when and at what load it was made: `answered from the green kept at … (3 min ago, load 4.1)`. The tree a run is keyed by now includes the install record npm, pnpm or yarn writes under `node_modules`. A repo names the environment variables a tier's verdict depends on under `keyEnv` in `geonosis.json`, and a change in their values runs the tier again, through the daemon and through verify's own green record (#209).
- Updated dependencies [ac7f890]
  - @geonosis/base@3.6.1

## 3.6.0

### Patch Changes

- e65a3b0: Every temp directory a kit package makes is held by the process that made it (`<dir>.holder`, the process id), through `heldTempDir` from `@geonosis/base/node`. The holder goes when that process exits. Before, a sweep of unheld `geonosis-*` directories, run by any test run on the machine, could delete a live one. An evals session waiting on its model holds no file open, and lost its workdir that way 11 minutes into a scored run.
- adb7c4d: - `cloneCount`'s probe passes `formats: []` as well as `ignore: []`. GNU `cat` refused the trailing `-f`, so `geonosis-ratchet --prove` was red on every Linux CI (#161).
  - A repo whose command names a jscpd config (`-c`/`--config`), or that has a `.jscpd.json`, keeps its own ignore and format lists. The counter appends `-i` or `-f` only for a list the repo named in `geonosis.ratchet.json`. Before, the defaults replaced the config's lists (#162).
  - A counter that grew under a measurement that changed, or that nothing vouches for, is reported instrument first: fix the instrument if it moved wrongly, raise only for a true number.
- Updated dependencies [630d44c]
- Updated dependencies [e65a3b0]
- Updated dependencies [f275d25]
  - @geonosis/base@3.6.0

## 3.5.0

### Minor Changes

- 2cc5f78: When a run changes the tree it ran on, verify names the step and the files: `green not recorded: fast/10 changed scripts/gate-baseline.identity.json while it ran` (#130). Before, it said only "a step changed the tree". Verify watches the tree after each group of steps, and `@geonosis/ratchet` exports `treeFilesOf` and `filesThatMoved` beside `treeHashOf` to name the files.

### Patch Changes

- 2cc5f78: `oxc-parser` is accepted at `^0.154.0` beside `^0.152.0 || ^0.153.0`.
- 2cc5f78: The oxlint counters `geonosis init` writes, and the ratchet's default oxlint command, pass `--report-unused-disable-directives-severity=error`. A disable directive that disables nothing is then an error the ratchet counts (#139).
- 2cc5f78: The oxlint unix-format reader counts a finding that names no rule, as oxlint prints an unused disable directive (`… Unused oxlint-disable directive … [Error]`). Before, the summary counted it and no line did, so a counter refused the run under `--report-unused-disable-directives-severity=error`.
- Updated dependencies [2cc5f78]
- Updated dependencies [b01150f]
  - @geonosis/base@3.5.0

## 3.4.1

### Patch Changes

- @geonosis/base@3.4.1

## 3.4.0

### Minor Changes

- 053eb94: New counter, `mirroredRowTypes` (#73). It builds the D1 schema the way wrangler does: every sqlite
  `release.migrations` directory, applied in wrangler's order to an in-memory SQLite. It then counts
  the TypeScript types kept by hand beside a table, and cites each one with the `$inferSelect` type
  that replaces it. The `geonosis-backend` skill now names Drizzle's D1 driver as the default data
  layer and says what `backend-d1` judges on Drizzle and on the raw binding. It also carries the
  probed recipe for generated migrations, including the baseline for an applied hand-written history.
- f328b39: `geonosis-ratchet --raise <key>=<n> --because "<why>"` raises one baseline. It measures that counter with its tier and params and writes `<n>` and the counter's identity only when the counter reads exactly `<n>` and `<n>` is above the baseline. It refuses with exit 2 any other reading, a fall, a key the config or the baseline does not hold, a blank reason, and a counter that cannot measure, and it prints the sentence for the commit message. The ratchet's FAIL for a grown counter, `--prove` (a raise above the reading must be refused), the plugin's baseline refusal and the `gates` rule file name it. In a session under the plugin, a command that runs `geonosis-ratchet` with `--raise` asks the person first, as does a command too long to read that names both.
- ab2f233: `HEAVY_SLOT_HELD` (`@geonosis/ratchet`) and `BASE_ENV` and `SLOTS_ENV` (`@geonosis/verify`) name the
  variables a verify tier hands its steps (`GEONOSIS_HEAVY_SLOT_HELD`, `GEONOSIS_BASE`,
  `GEONOSIS_SLOTS`), so a repo whose test run must not inherit them can list them from one place.

### Patch Changes

- 22981ae: A kit bin starts on bun when bun is on PATH and falls back to Node when it is not (#52). An npm
  repo with no bun used to get `env: bun: No such file or directory` and exit 127 from a bin that
  needs only the Node its `engines` declares. The shim is now a POSIX shell line ahead of the same
  JavaScript: it `exec`s bun when it finds one, otherwise it says once on stderr that it runs on Node
  and how to get bun, and `exec`s Node. Under `npx`, `npm exec`, `pnpm exec`, `node_modules/.bin` and a
  direct path it adds about 3 ms to a start on bun. Windows stays a non-goal. The doctor's `tool runtime`
  warning no longer says a machine without bun cannot start the bins.
- 77f9a0a: Every reader of a config oxlint or wrangler reads as JSONC now reads it as `@geonosis/base` does (#90).
  `probelessRules` and `unreachableTierRules` no longer refuse a `.oxlintrc.json` with comments or
  trailing commas, and the MCP preamble names its rules. `deriveLocalOnlyConfig` reads a wrangler
  config with block comments and comments after a value, and keeps a `,}` inside a string. The lint
  plugin's own reader no longer strips the comma from a string holding `,}`. `testbed` and `evals`
  declared `@geonosis/base` as a devDependency while importing it; it is now a dependency.
- 4086919: `geonosis update` now catches a bump that splits one of the repo's packages into a second snapshot
  (#93). This happens when the kit brings a second version of an optional peer. In a Medusa tree, a
  second `yaml` forked vite, then `@medusajs/framework`, then `core-flows`, so every workflow
  registered twice and the backend refused to boot. The update had said `SKIPPED build — the lockfile
  moved only what this bump repinned`. It now compares pnpm's snapshot keys as well as versions. A
  package whose peers resolve differently counts as moved, the build runs, and the report names it:
  
      @medusajs/core-flows@2.19.0: 1 → 2 snapshots (new peer yaml@2.9.1)
  
  `bump-report.json` carries the splits in `lockSplits`. The kit's own `yaml` range now starts at
  `^2.0.0`, the oldest version its tests pass against, so pnpm reuses the `yaml` a repo already has
  instead of adding a second one.
- Updated dependencies [ebb216e]
- Updated dependencies [7492bfa]
- Updated dependencies [13e1386]
  - @geonosis/base@3.4.0

## 3.3.0

### Minor Changes

- fb08b28: `geonosis-ratchet --bank <key,…>` and `--bank-unproven` lock in falls someone has confirmed (#58): `--prove` runs for exactly those counters first and any that does not prove is refused, then their numbers and their identities are written in the same run, so no hand edit and no second full run. A key that grew or could not be measured is refused and nothing is written. An `UNPROVEN` fall is reported as one line per counter and one shared paragraph, which names the command.
- fb08b28: `geonosis-ratchet --jobs <n>`, or `"jobs": <n> | "heavy"` in `geonosis.ratchet.json`, measures independent counters at the same time, never more than the machine's heavy budget (#55). Entries naming one `command` or one `"lock"` run together, one after the other; an entry with `"exclusive": true` runs alone, last, and so does an `oxlintRule` entry with a `config`, which writes a strict copy into the tree while it measures. Each group is measured by a child process of the bin, so `CounterContext.run` stays synchronous; the report keeps the order of the config. The default is 1, as before. Six independent tools of this repo took 87 s and 43 s one at a time and 27 s and 32 s at `--jobs 4` on a machine at load 50 to 70. `lock` and `exclusive` are scheduling only and are not part of a counter's identity.

### Patch Changes

- fb08b28: `geonosis-ratchet --prove` proves every configured counter and every half of each, instead of returning at the first one that is not proven (#34): the counters after it were never attempted and the output did not say so, which read as `sumOfCounts` counters being skipped. Each counter prints its PROVEN / UNPROVEN / CANNOT FAIL line, the lock is proved whatever failed before it, and a failure ends with `prove FAIL — N of M counters unproven: a, b` and exit 2. `ProveResult` gains `counters: { key, proven }[]`. No counter's fingerprint changes.
- fb08b28: The published manifests no longer declare `engines.bun`, which yarn 1 warned about once per installed package (`The engine "bun" appears to be invalid`). A package that starts its bins on bun now names it in `devEngines.runtime`, the same list the kit's root declares; measured on yarn 1.22.22, npm 11.13.0, pnpm 11.8.0 and 12.10.1 and bun 1.4.2, none of them warns on that field in a dependency. A test over every published manifest holds that `engines` has only keys yarn 1 knows and that the list equals the root's, and a yarn 1 install of those manifests prints no engine line (#46).
- fb08b28: `oxlintRule` entries with the same `config` and the same `command` are measured by one oxlint run over one strict config raising all of their rules, and each key is counted from the lines of its own rule (#56). 17 entries over this repo's 1,600 files read the same 17 counts as 17 separate runs and took 4 to 7 s instead of 132 s. An entry whose `command` differs keeps a run of its own. `oxlintRule` changes fingerprint: its identity reads as changed once, so a fall in the first run after the upgrade is `UNPROVEN` and a rise or an equal count is judged as before; every other counter's fingerprint is unchanged.
- fb08b28: `geonosis-ratchet --record <name>` records every entry of the counter called `<name>` when no key bears that name, so a tier with one `testFailures` entry per workspace needs one step, `--record testFailures`, not one key per suite.
- fb08b28: A counter can read a run a tier step already made when the run is a vitest JSON report (#51): `geonosis-ratchet --record <key>` stores the report with the run, so an entry naming `output` counts and names the failing tests from the recording and runs nothing. Before, a `report: "vitest-json"` entry with `output` could not be read back: the recorded command carried a temp path no later run could match. `--record` takes several keys (`--record typecheckErrors,testFailures:apps/web`) and exits with the worst of them, and it records nothing when one of the keys is unknown. A scaffolded repo's `full` tier runs `tsc` four times and `vitest` six; with its fast-tier steps replaced by `geonosis-ratchet --record <key>` and `output` on the entries, three and three, and the ratchet step falls from 13.4 s, 11.0 s and 6.2 s to 2.7 s, 3.4 s and 3.5 s on a loaded machine.
- fb08b28: A counter whose command names `{report}` must declare the `report` it stands for; one that does not is refused at load, before a shell writes a file literally called `{report}`.
- fb08b28: A counter command that starts with `pnpm exec <bin>`, `npx <bin>` or `bunx <bin>` runs `node_modules/.bin/<bin>` when it exists, and every counter command runs with `pnpm_config_verify_deps_before_run=error` unless the environment or `pnpm-workspace.yaml` already sets it (#57). `pnpm exec` costs 3 s of start-up per command under load and installs when the manifests and `node_modules` disagree, which replaced the kit under a running gate. `binCommandOf`, `localBin`, `resolvedCommand` and `gateEnv` are exported for the tools that print or run a command.
- fb08b28: `geonosis-ratchet --record` in a tree git cannot describe yet — a fresh scaffold before its first commit — runs the tool, prints what it said and exits as it did, writing nothing and saying so; a counter with an `output` file there runs its own command instead of reading a recording it cannot tie to a tree. It refused before, so a freshly scaffolded repo's first `verify` failed at its first step.
- fb08b28: `testFailures` in `report: "vitest-json"` mode names the failing tests when the count rises, and keeps the report (#48). The evidence is `FAIL <file> > <full name> — <first line of the message>` for each failed assertion in the report, at most eight, then how many more there were and `report kept at <path>`; the report is written to `.geonosis/reports/<key>.vitest.json` (or the entry's `reportPath`) and is no longer deleted after the run. A report left by an earlier run is removed before the command runs. Only `testFailures` changes fingerprint: its identity reads as changed once, so a fall in that counter in the first run after the upgrade is `UNPROVEN` (`geonosis-ratchet --bank testFailures`), and a rise or an equal count is judged as before. No other counter's fingerprint moves.
- fb08b28: `geonosis update` under a release-age cooldown (pnpm `minimumReleaseAge`, bun `minimumReleaseAge`) checks the third-party packages the kit's packages declare, once the kit's own packages are exempt or the manager's override is in `--install-args`. It stops with exit 2, before writing, and lists each package whose newest release satisfying the declared range is younger than the age: under an exemption that means no old enough release satisfies the range, under an override it means the newest one is young, because the override lifts the age for every package. The README's "Under a release-age cooldown" holds the exemption each manager has, measured: pnpm `minimumReleaseAgeExclude` takes `'geonosis'` and `'@geonosis/*'`, bun `minimumReleaseAgeExcludes` takes exact names and ignores a glob, npm has no per-package exemption. The kit's cut holds what it publishes to the same age: `release:check` refuses a range in a publishable manifest (`dependencies`, `optionalDependencies`, required `peerDependencies`) that no release at least `release.thirdPartyAge` days old (default 7) satisfies, and names the range that would. The published floors of `oxc-parser`, `pixelmatch`, `@modelcontextprotocol/sdk` and six `@radix-ui` packages are lowered to a release that old, with the lockfile still resolving the latest. The doctor's version reader now reads a partial version (`>=19`, `^1`) and an `||` list of such ranges (#67).

## 3.2.0

### Minor Changes

- deef511: A floor's surface grows only after a deletion (D-076, plan 029 W119). A `floorSurface` entry may carry `earnedBy: { consumer, envelope? }`: when its baseline was raised in the working tree against the committed one, `geonosis-ratchet` reads the declared consumer's added and deleted lines from `deletions` in `.geonosis/envelopes/release-adoption.json` and judges the raise against the committed number unless the figure is negative, quoting the figure, or saying that none was read. `geonosis-release adoption` writes that figure: a consumer named in `portfolio.consumers` gets a `DELETION` line, read from the `bump-report.json` its artifact holds, and `deletions` in the envelope. `geonosis-release consumers` prints `release.consumers` (a list of `owner/repo`) as JSON, which the kit's `rc` workflow now reads instead of a second list in the CI settings; `templates/kit-rc.yml` is the file a consumer adds to answer a kit pull request with a `kit-rc/<repo>` commit status and an artifact in the layout `geonosis portfolio` reads (W117).
- dd5f57a: REFUSAL-IN-MINOR: `geonosis-verify` refuses a step, a `proves` or a parallel group in `geonosis.json` that carries a key nothing reads, naming the key and the keys a step takes. It dropped the key and loaded the step without it, so a step declared `provse` ran with no proof and said nothing. Rename or delete the key. `unknownKeyOf(block, known)` is exported from `@geonosis/ratchet`, the one test of "a key this reader does not know" that `geonosis-release` now uses too (#728).
- 58eaf3a: The drift check (root package scripts only: a script run with `--filter`, `-C` or `-r` is not followed) warns when a counter that times a tier's own gate report (`fastTierMs`) is run by a ratchet step of that same tier: it reads the previous completed run and cannot measure a fresh checkout, so the ceiling belongs in `geonosis.json` `budgets.<tier>` (D-042), or the counter in a tier that tier does not run. A counter can now say which tier it times (`timesTier`). The smoke refuses to compare a baseline recorded under another command (`compareToBaseline`), and `release:check` records it again.
- a4a7bba: `optionRefusalIn(said)` is the one reader of a rule's "requires option" refusal: the `option` and `rule` fields of an error or a report, then the `data` of a report, then the sentence, from an error, a report or a printed finding. The doctor's missing-option check and the parity tool's refusal check both read through it, so a plugin that words its refusal differently and hands the pair over as fields is a refusal to both, not reach to one and configured to the other.
- deef511: A counter can read the run a step already made.
  `geonosis-ratchet --record <key>` runs the counter's own tool once, prints what it said, writes the run to the file the entry's `output` names (the command, its exit code and its output, tied to the tree) and exits as the tool did. An entry that names `output` reads that file and does not run its command: a recording of another tree is a refusal naming both tree hashes, a recording of another command is a refusal naming both, a missing recording is a refusal naming the command that writes one, and a tree that changed while the tool ran is not recorded at all. Entries naming one `output` and one command are served by one recording. `recordCounter` and `recordedRun` are the two halves.
- deef511: `geonosis-ledger shape lock --from <manifest>` validates a brief's manifest, resolves its red commit to a full hash and writes the runner-owned `.geonosis/brief.json`, a path the plugin refuses an agent's write to, so the engineer cannot loosen the lock it works under. `geonosis-ledger shape check [--brief <manifest>]` judges an engineer's diff against the RED commit a brief was committed as (D-072). The manifest names `ticket`, `red`, `files`, `locked` and optional `exports`; every path that differs from the red commit, committed, staged, unstaged or untracked, is judged. A locked test that was changed, deleted or moved, a file the manifest does not list, an exported name the red commit held and the file no longer exports, and an exported name neither the red commit nor `exports` names are each refused by path (exit 1); a body-only diff passes; a missing or malformed manifest, or a red commit the tree cannot diff against, is exit 2. New exports of `@geonosis/ledger`: `shapeCheck`, `lockManifest`, `readManifest`, `formatShape`, `MANIFEST_PATH`, `BriefManifest`, `ShapeOutcome`, `ShapeRefusal`. `@geonosis/ratchet` gains `exportedNamesOf(path, source)`, the sorted names a TypeScript or JavaScript source exports (types, destructured bindings, re-exports and `export *` included), read with the parser the counters use.
  The plugin's Bash guard runs `shape check` on `git commit` while `.geonosis/brief.json` is present and refuses the commit with the lock's own lines; a lock that could not run lets the commit through and says so. The engineer definition says to start on the RED commit and that the shape lock is the gate, and the `geonosis-plan` skill says how to commit the applied brief and lock the manifest.
- deef511: `geonosis-gate serve | status | stop` (`@geonosis/ratchet`): one daemon per machine that runs a tier once for every `geonosis-verify` over the same tree and flags, answers the report it kept to a verify over a tree it already read, and queues the heavy slots first-come within the budget. The budget reads `GEONOSIS_HEAVY_SLOTS`, then the new `geonosis.json → caps.heavy`, then the machine. `geonosis-verify --fresh` runs again whatever the daemon holds; `--exclusive` never asks it. `takeSlot` leases its slot through the daemon and uses the slot files when none runs, and the Stop hook reads a kept green for the tree and hands `fast` to the daemon. REFUSAL-IN-MINOR: `geonosis-verify --prove` now also plants a failure inside a parallel group and fails a runner that loses it or runs the steps after it, and `loadCaps` refuses a `caps.heavy` that is not a whole number of at least 1.
- dd5f57a: A tier rule now declares the tier folder it reaches (`rule.tier`), and `unreachableTierRules` counts the tier rules enabled over a workspace that ships tier folders but none of that rule's tier: they have no possible finding and print what a clean tree prints. A workspace with no tier folder is not judged; a plugin that declares no tier is a refusal.
  
  `geonosis-ratchet --prove` now plants the tool failing as well as a finding. A counter must answer a failing tool with a `CounterError`, and a counter that ships no `refuses` probe reads UNPROVEN. The report names a counter's halves (`PROVEN archViolations (command)`, `UNPROVEN archViolations (match)`) and the summary counts halves. A counter you wrote needs `refuses: { input?, command?, params? }` beside its `probe`.
  
  A named `roots` entry of `commentLines`, `suppressionCount` and `orphanTodos` that is not a directory is now a refusal rather than zero findings, and a `pnpm-workspace.yaml` that does not parse refuses the counters that read it.
  
  A win one counter measured is now written to the baseline even when another counter could not be measured (a floor whose `dist/` is not built no longer holds every other floor's win back); the run still fails at the refusal and says which number was lowered.
  
  `waitForBuild(root)` and `buildLockPath(root)` are exported for a tier that must not run beside a build.
  
  `typecheckErrors` takes a `report` in place of a `command`: it reads the `{ errors, tree }` file a typecheck step wrote and refuses one whose tree is not the working tree in front of it, naming both digests, so `verify:release` no longer runs the same tsc program twice. `treeHashOf(cwd)` is exported for the step that writes it.
- 67aefe3: A verify step may declare `"history": true`: its `prove` copy is made a repository whose one commit is the copy, with `refs/geonosis/verified` at it, and runs with the host's `GIT_` redirects and git config out of the way, so a step that compares against the mark can be watched failing. `@geonosis/ratchet` exports the isolated git runner (`isolatedGit`, `withoutGitRedirects`, `GIT_IDENTITY`) that `@geonosis/release`'s smoke already used and now imports from there.

### Patch Changes

- f5632b7: New `@geonosis/base`: the zero-dependency bottom of the method, holding what packages used to copy. `@geonosis/base` is `isRecord`, `isStringList`, `stringsOf`, the error text helpers and the markdown readers (`headingsOf`, `sectionUnder`, `fencedLines`, `titleOf`), safe in a browser or a Worker; `@geonosis/base/node` is `packageRootFrom`, `versionOf` and `isDirectory`; `@geonosis/base/bin-harness` is `binRunner(runtime)` for running a package's bin in a test, with `GIT_ISOLATED` for a bin that shells to git.
  `cli`, `create-geonosis`, `ledger`, `mcp`, `observability`, `rails`, `release`, `review`, `themekit`, `verify`, `verify-arch` and `walk` import the guards from it instead of carrying their own; their behaviour is unchanged and each now installs `@geonosis/base`, which a consumer never declares.
  The ratchet keeps its own copies: the build runs on its source before any package exists, so nothing it imports may be a package the build produces.
- a4a7bba: Migration note for consumers upgrading from before 2.11.0: a counter that reads a kit bin's piped output, where that output could exceed 64 KiB, has a baseline recorded before the fix that is a floor. The bins used to call `process.exit()` after an asynchronous write, and through a pipe on macOS everything past 64 KiB was dropped with a clean exit code, so a count over the output read as a stable, smaller number. Re-measure the counter after the upgrade and adopt the true value, with the reason in the commit. The README section "A counter that reads a kit bin through a pipe" says how.
- 5404b2a: Dependencies move to their newest release the install policy accepts: `@modelcontextprotocol/sdk` 1.32.1 (`@geonosis/mcp`), `oxc-parser` 0.153.0 (`@geonosis/ratchet`, `@geonosis/verify`), and the `squawk-cli` 2.68.0 that `@geonosis/release` and `geonosis` ask for as an exact optional peer, so the install line a refusal prints is `squawk-cli@2.68.0`. A scaffold written by `create-geonosis` pins `oxlint` ^1.87.0, `oxfmt` ^0.72.0, `vitest` ^5.0.3 and `@types/node` ^26.6.4.
- 58eaf3a: `rulesNamedBy` and the corpus reach scope read a rule set to `"allow"`, `"0"` or `false` as off, as oxlint does — they counted only `"off"` and `0`, so such a rule was judged for reach it was never configured to have. One `ruleIsOff`, exported from `@geonosis/ratchet`, is what the doctor, the ratchet and lint-parity all read.
- 58eaf3a: `geonosis-ratchet`, `geonosis-verify` and `geonosis-lint-parity` end with `process.exitCode` instead of `process.exit()`, so a report longer than a pipe holds (a `--json` run over a few thousand findings or counters) reaches the reader whole. `process.exit()` after a write past 64 KiB cut the report at the pipe's size, which for `--json` is a document that no longer parses.
- 58eaf3a: An empty law file counts as no lines in `lawLineCount` and in the doctor's ceiling check; it counted as one. Both now read lines the way the seam counter does, from one `lineCount`.
- 58eaf3a: A baseline entry that is not a number (`"5"`, `null`, a list) is now refused with its key named, where it was adopted over and the debt it held silently reset to the new reading. A baseline or `geonosis.ratchet.json` that does not parse, or is not an object, names the file in its refusal; a `null` counter entry gets the config's own sentence rather than a `TypeError`.
- 58eaf3a: A run waiting for a heavy slot reads the machine's memory once per pass over the slots instead of once per slot: on macOS each read runs `vm_stat`, so a full table cost one process per slot on every poll.
- 58eaf3a: `oxlintRule` with a `config` now forces a rule to `error` however it is parked (`0`, `false`, `"allow"`, `"0"` as well as `"warn"` and `"off"`), and no longer rewrites a different rule whose name merely ends the same way.
- 58eaf3a: `sumOfCounts` refuses a `match` that reads lines of the output but captures no number (no capture group, or a non-numeric capture) instead of summing it as zero and banking a false win.
- 58eaf3a: An `adoption.seams` glob is read by one matcher in `@geonosis/ratchet`: `*` and `**` are wildcards and every other character, `?` and braces included, is itself. The `adoptionSeamLines` counter, the doctor's seams check and `smoke snapshot` agreed on none of it before — `src/a?.ts` made the `a` optional, and the doctor accepted brace globs the counter counted as nothing. `smoke snapshot` now refuses a consumer `geonosis.json` that does not parse instead of recording a tree with no seams.
- dd5f57a: A counter's measurement identity records a fingerprint of the counter's own code and compares that instead of the package version, so a release that left a counter alone no longer makes its next fall UNPROVEN or its next rise say the measurement changed. A record from before fingerprints is still compared by version, once. A rise now names the identity file a hand-raised number leaves stale.
  
  Migration note: a floor you correct upward because a kit bug produced it must land in the same commit as the bump that fixes the instrument, never ahead of it. A shrink read by the still-buggy build is refused when the recorded measurement differs, and the number it replaced is kept in `gate-baseline.identity.json`, but only the same-commit order keeps the correction from ever being overwritten.
- 58eaf3a: A counter's report, or an `.oxlintrc.json`, whose top level is not a JSON object is refused by name instead of failing with a TypeError, and an unreadable counter report now reads "does not parse". No other output changed: the rest of this release's diff removes duplicated internals.

## 3.1.0

### Patch Changes

- bd82a2e: `fastTierMs`, `bumpDurationMs` and `timeToGreenDoctorMs` are budgets: they fail over their ceiling and tolerance, and a faster run is held, never banked and never UNPROVEN (#26).

## 3.0.0

### Major Changes

- 559c117: The heavy budget reads the memory the kernel can hand out — `vm_stat`'s free, inactive, speculative and purgeable pages on macOS, `MemAvailable` under the cgroup's limit on Linux — on any runtime. Under bun, `process.availableMemory()` is the free pages alone (1,849 MB against Node's 5,637 MB on one 16 GB Mac), which put every gate on fewer heavy slots than the machine had. `HEAVY_SLOTS`, a number read once at import that nothing used, is gone; `heavySlots()` reads it fresh.
- 670f7c1: Every bin runs on bun: its shebang is `#!/usr/bin/env bun`, so a repo that runs the tools needs bun on `PATH`. On pnpm 12, `devEngines.runtime` names it and the install puts it in `node_modules/.bin`:
  
  ```json
  "devEngines": { "runtime": { "name": "bun", "version": "^1.4.2", "onFail": "download" } }
  ```
  
  The runtime floors (foundations, domains, kits) are unchanged: they declare no runtime of their own and run on Node, Workers or a browser bundle.

### Minor Changes

- 9b91c6a: `unreadableSince` and `SinceKind` are exported from `@geonosis/ratchet`: the one reader of a `--since` date, plan number or git ref, which `geonosis-ledger`, `geonosis-release` and `geonosis-verify` each carried a copy of.

### Patch Changes

- 0f272e2: A tree says which bun the kit's bins start on. Every package with a bin declares `engines.bun`, at the range the kit itself runs. `create-geonosis` writes a pnpm repo's `devEngines.runtime` and `packageManager` as the kit's own manifest has them, so pnpm 12 installs that bun. `geonosis-doctor`'s drift check WARNs, on the `tool runtime` line, where an installed kit package asks for bun and the root neither runs on it nor declares it, and prints the block to add.
- 1e38e4e: `geonosis-themekit`, `geonosis-rails` and `geonosis-evals` read their arguments with `node:util`'s `parseArgs`. A flag one does not know is refused by name, exit 2, where the hand-written readers ignored it and ran without it; a flag given twice keeps its last value in all three (evals kept the first). `geonosis-evals compare --tolerance x=0.1 a.json b.json` reads `a.json` and `b.json` as the scores: the old reader took the tolerance's value for the first score file. `geonosis-ratchet` and `geonosis-verify`, which already refused what they did not know, read their flags the same way; verify still refuses a flag that belongs to another of its commands. `geonosis-doctor` reads its flags with `parseArgs` too, its optional `--baseline-against` ref through the parser's tokens. `@geonosis/ledger`'s exported `parseArgs(argv, valued, switches)` is `node:util`'s underneath and takes the switches it accepts, refusing an undeclared one and a valued flag followed by another flag (`--note --plan 3` used to store `--plan` as the note); `rejectUnknownSwitches` is gone. `geonosis-walk`, `geonosis-review` and `geonosis-lint-parity` parse the same way; parity keeps its own words for a bare `--corpus` or `--expect-changed`. `geonosis-observability`, `geonosis-verify-arch`, `geonosis-testbed` and `geonosis-visual-diff` follow; the last two used to accept a flag they did not know (testbed ignored it, visual-diff read it as a switch) and now refuse it. `@geonosis/release`'s exported `parseArgs` keeps its `Parsed` shape over `node:util`'s. `@geonosis/cli`'s `parseArgv` and `create-geonosis`'s `parseArgs` are built on it too, keeping their own refusals; `parseArgv` now refuses a valued flag followed by another flag instead of taking that flag as its value.
- 5560282: A counter's command runs with the repo's `node_modules/.bin` first on `PATH`, as every package manager's `run` does, so the defaults name the tool directly: `oxlint …`, `tsc --noEmit`, `vitest run`. `npx` refused to run at all in a repo whose `devEngines.runtime` names bun.
- feb9983: Every dependency at its latest minor, oxlint 1.85 included. The storefront kit's remaining written-in copy is now props with English defaults (`closeLabel`, `currencySymbol`, `perServingLabel`, `containsLabel`, `editLabel`, `requiredMarker`), and `RecipeCard` renders `labels.toolsHeading`, which it used to ignore for a hard-coded "Tools".
- 7d381dd: Two counters that run the same command in one ratchet run share one run of it: `oxlintErrors` and `oxlintWarnings` read one whole-repo lint instead of two.
- a6006d7: One workspace reader. `workspaceDirs` reads `pnpm-workspace.yaml` with a YAML parser and expands its patterns with `node:fs` `globSync`, so a `!` exclusion is honoured and a `**` pattern has no depth limit. `@geonosis/verify` and `@geonosis/ledger` read the workspace through it instead of their own parsers.
- 53c995b: `geonosis-ledger proof capture` writes the repo's own path as `.` and the home directory as `~`, so a proof records the command, not the machine it ran on; vitest's `RUN` header printed both. `knipIssues` also hashes a `knip.ts` or `knip.js` config, which knip loads as readily as `knip.config.ts`.

## 2.13.0

### Patch Changes

- f422aea: One envelope writer. `@geonosis/doctor`, `evals`, `ledger`, `lint-parity`, `rails` and `release` each carried a copy of the ratchet's; they now depend on `@geonosis/ratchet` and re-export its writer, so the envelope law has one source.
- f422aea: One source per question. `@geonosis/release` takes its heavy slot from `@geonosis/ratchet`; `@geonosis/cli` reads the registry and writes candidate overrides through `@geonosis/release`; the doctor's `deployed` line is `release`'s comparison, and now reads a `wrangler.toml` too. `@geonosis/doctor` no longer exports `NOT_WRITTEN`; `@geonosis/release`'s `NOTHING_SAYS` is the sentence.
- f422aea: A counter whose command writes its report to a fresh temporary path records the command as configured, so the path no longer marks every run a changed instrument and a genuine fall is banked.
- ae995df: A `testFailures` regression names the failing tests the runner printed, vitest's `FAIL` lines and bun's `(fail)` lines, instead of the tail of the run.

## 2.12.0

### Minor Changes

- 7c3879a: `oxlintErrors` and `oxlintWarnings` take `ruledSeparately`: rules ratcheted under their own `oxlintRule` key, which the aggregate leaves out (#7). Turning on a stricter rule over pre-existing debt adds one `oxlintRule` entry, adopted once at its honest number and shrink-only from then on, while the aggregate holds. The ratchet refuses a rule named there that no `oxlintRule` entry counts, so leaving a rule out cannot switch it off.

## 2.11.1

### Patch Changes

- e67fc1b: The heavy budget reads memory from Node's `process.availableMemory()`, which counts the reclaimable cache and a container's limit. On macOS `os.freemem()` counts only untouched pages. It read 497 MB on a 16 GB machine with 4.7 GB to spend, which bound every heavy gate to 1 slot where the cores allowed 7.

## 2.11.0

### Minor Changes

- 0f99e26: A fall taken with an instrument that moved is no longer banked.
  
  The ratchet only moves down, so the two directions are not symmetrical. A rise is recoverable: it
  fails, someone looks, and the baseline's own header sanctions raising a number with the reason in
  the commit. A fall is not. Once banked, the lower number is the ceiling from then on, and if the
  fall came from the instrument seeing less rather than from the debt going away, nobody ever finds
  out. One consumer's `migrationWarnings` sat at 51 for eleven days while the truth was 58 — our own
  output truncation, stable and invisible, banked as a win.
  
  `gate-baseline.identity.json` already recorded what measured each number, and already noticed when
  the command or the build behind a baseline had changed. It now changes exactly one thing. A number
  that FELL while its recorded identity CHANGED is `UNPROVEN`: the baseline is left exactly as it was,
  nothing is written to the identity file either, and the run exits 1. A rise under a changed identity
  still fails, as it always did, and nothing else moves — a fall under an identity that MATCHES is
  still banked and still passes, and a fall whose identity is merely UNKNOWN (every baseline written
  before identities were kept) is still banked, because absence is not a change.
  
  `UNPROVEN` is a sibling of `ADOPTED`, not of a regression: the counter measured fine, and what the
  run cannot do is vouch for the comparison. So the message says how to accept the win deliberately —
  confirm the fall is real, then lower that key in `gate-baseline.json` by hand, with the reason in
  the commit. The tool will not write a floor it cannot stand behind; a human still can.
  
  A rewrite another counter earned is held back while a fall is unproven, the same way it is held
  back beside a counter that grew: a baseline holding one number that was earned and one that was not,
  with nothing in the file saying which, is the state this package exists to prevent.
- c84e4a0: `cloneCount` reads the JS and TS family by default (a new `formats` param, passed as jscpd's `-f`), so duplicated prose and captured tool output no longer count as clones; `"formats": []` reads every format. Expect the baseline to fall once, refused as UNPROVEN until lowered by hand.
- 7db8a78: `commentLines` reads comments from `oxc-parser` (a new dependency), so JSX text no longer bends the count, and a file the parser refuses (or a `.vue`, `.svelte` or `.astro` file) stops the count and is named. The code-file walk reads every depth and skips `dist`, `coverage`, `node_modules` and `__fixtures__` by whole name, so `distance.ts` is read. Expect `commentLines`, `orphanTodos` and `suppressionCount` baselines to move once.
- 0f99e26: **The comments law is a number.** "Default: none — a diff adding any other comment is returned" was
  prose in `plugin/agents/geonosis-engineer.md` and no gate held it. `commentLines` counts the source
  lines carrying a comment, so comment volume may only shrink.
  
  The reading is lexical, not line-shaped: `^\s*(//|\*|/\*)` counts a `*` inside a template literal
  and misses a `// why` after code, and both errors move the number without a comment being written or
  removed. It is not a parse either — a comment is trivia rather than a node, and a parse would refuse
  the `.vue`, `.svelte` and `.astro` files the scanner reads. Strings, template literals and regex
  literals are tracked, so `/\/\//g` is a pattern and not the start of a comment; a line is counted
  once however many comments it carries.
  
  `roots` says where to look and defaults to the whole tree, minus the dependencies, build output and
  fixtures every file-scanning counter already skips. Nothing in it is specific to one repo.
- 0f99e26: Three defects a consumer measured against 2.10.0 (#17, #18, #19) and one this tree found.
  
  `geonosis-ratchet`: a counter's command that crashed is never a reading (#18). Every counter that
  runs a tool declares the exit codes it reads — `[0, 1]` for a linter, `[0, 1, 2]` for tsc, `[0]`
  where nothing is declared — and an entry may override them with `okExits`. Any other exit refuses
  the counter, and so does a non-zero exit whose output is a Node stack trace, whatever the set says.
  Before, a counter script that threw had its trace's line numbers summed as findings: reported as a
  regression when they summed high, and BANKED as a shrink when they summed low — the baseline
  rewritten to a number no measurement produced. Expect `CANNOT MEASURE` where a command exits with
  a code its tool never uses for findings; declare `okExits` on that entry if the tool is right.
  
  `geonosis-doctor`, `geonosis`, `geonosis-release`, `geonosis-observability`: the bins set
  `process.exitCode` and let the runtime drain instead of calling `process.exit()` after writing the
  report (#17). Through a pipe on macOS, anything past 64 KiB was dropped with a clean exit code —
  the doctor delivered 65,536 of 323,665 bytes, `release migrations` 50 locations for 51 warnings.
  
  The Claude plugin's Stop hook (#19): a cached fast-tier report is fresh only while the tree it was
  judged over is still here, gitignored artifacts included — deleting a `playwright-report/` the
  lint step had read now relaunches the tier instead of replaying the stale FAIL for ever. And a
  replayed report says how many of its steps never ran, red or green.
- 0f99e26: Four rules see what they were blind to, and eleven readings refuse instead of banking a zero.
  
  `tissue-no-hooks` and `compound-must-be-stateless` report a hook called as a member —
  `React.useState(0)`, `hooks.useCart()` — where they read only a bare name. `time-through-the-door`
  sees the library through every import door (`export * from`, `import()`, `require()`), and
  `subpath-over-barrel` reads `export { X } from '<root>'` as the import it is. Expect new findings on
  a tree that spelled a hook through its default React import.
  
  `geonosis-ratchet`: a shell killed by a signal, a formatter that exited non-zero naming no file, a
  `turbo boundaries` that printed no summary, a `geonosis.json` that will not parse, a stale report at
  a named `reportPath`, and an oxlint run that answered in a shape `expectFormat` did not ask for are
  each a `CounterError` now. Every one of them read as a number and lowered the baseline to it.
  
  `geonosis-doctor`: `deployed`, `seams`, `rails` and `drift` say FAIL over a `geonosis.json` that is
  there and will not parse, where each had read it as a repo that declared nothing.
- 0f99e26: `commentLines` takes an `ignore`, so a generated tree stops moving the number.
  
  A directory a build writes and git never tracks holds no comment anybody wrote, and counting it
  made the reading depend on whether the build had run: on this repo 259 of 25,978 comment lines sit
  in 280 generated files, so the tree read 25,719 before `pnpm build` and 25,978 after. A clone that
  ran the gate before building would have banked the lower figure as a win, and every built run after
  it would have failed on a rise nobody caused.
  
  The default is `[]`, so a repo that names nothing reads what it always read. A path is relative to
  the repo root and takes its whole subtree with it. Git is deliberately not consulted: `git
  check-ignore` honours `.git/info/exclude` and a developer's global excludes, which would make a
  gate's number depend on one machine's untracked settings.
- 0f99e26: `cloneCount` takes an `ignore`, and leaves manifests and generated changelogs out by default.
  
  npm fixes a `package.json`'s shape and a release tool writes a `CHANGELOG.md`, so a clone between
  two of them is debt nobody can pay down: on this repo they are 203 of 1,677 clones, 36 across 40
  manifests and 167 across 38 changelogs. The default is `["**/package.json", "**/CHANGELOG.md"]`; a
  repo with another layout names its own list, and `"ignore": []` scans everything as before.
  
  MEASURED on jscpd 5.3.0: the list is passed as `-i`, which REPLACES the `ignore` of a `-c` config
  rather than adding to it. A repo that keeps exclusions in `.jscpd.json` moves them into this list in
  the same commit as the upgrade, or its number rises by whatever those exclusions were holding back.
- 0f99e26: A counter's params are part of its recorded identity.
  
  `gate-baseline.identity.json` recorded the command and the build behind each number, and a fall
  taken under either of those having changed is refused as `UNPROVEN` rather than banked. That guard
  only ever protected counters that shell out. A counter configured by params alone — one whose
  `command` is `null` because it reads files rather than running a tool — is reconfigured by editing
  its options, and the command string it does not have cannot say so. Both halves of it happened on
  one day: a duplication counter gained an `ignore` option, its command string grew the flag, and its
  203-finding fall was correctly refused; a comment counter gained the same kind of `ignore` option,
  ran no subprocess, compared as the same measurement, and banked a 259-line fall automatically at
  `ratchet PASS`. A params-configured counter could be quietly narrowed and the lower ceiling locked
  in forever.
  
  The identity now records `params` beside `command` and `version`: everything the config entry said
  except its `key`, which names the record, its `tiers`, which say when the counter runs rather than
  how it reads, and its `command`, which is already recorded as the command that actually ran. They
  are recorded verbatim, because this file is committed and read by humans, and a hash would say that
  something changed without saying what.
  
  Params are compared as configuration, not as text. Keys are ordered before both writing and
  comparing, at every depth, so the same options written in another order are the same measurement
  and no refusal is manufactured over a reserialisation; array order is kept, because that is data.
  
  An identity recorded before this carries no params at all, and reads `UNKNOWN` — you genuinely
  cannot tell. UNKNOWN banks a fall exactly as it did before, so no existing baseline is refused over
  a record that predates the field. Nothing softens either: a command or a build that moved still
  reads `CHANGED`, on a record with params and on one without.
- ad81ece: A fall is banked only when the recorded measurement vouches for it: an UNKNOWN identity (nothing recorded, an unreadable file, a hand-edited number) is refused as UNPROVEN like a changed one, and a run that meets a hand-edited number exactly records the measurement behind it. The identity now holds a digest of each settings file the tool reads — the counters' own `reads` globs, or an entry's `reads` — so a rule switched off in `.oxlintrc.json` no longer banks the fall it causes.
- 0f99e26: Two defects the gates found (rows 576, 577) and five frictions one consumer measured (#1, #2, #5,
  #9, #10), plus the stale-build guard every adopter-from-a-checkout hits.
  
  `geonosis-ratchet`: a red test suite is a reading again (row 576). The #18 crash detector ran
  before a counter's `okExits` and refused every red suite — an `AssertionError` over frames, and
  Node's trailer whenever a failing assertion quoted the child it ran — so `testFailures` refused
  at the one moment it had something to say. A crash is now a STREAM whose last lines are a trace
  (the runtime's trailer, or an error line over at-frames with nothing after), judged at any exit
  code; a runner that quoted a trace and printed its summary after it is read. `node crash.mjs`
  still refuses, and so does the same crash hidden behind a pipe's exit 0. And a file-set counter
  names its offenders (#2): `unformattedFiles` cites the files it named, most recently modified
  first, under a line that says so. The `evidence` hook receives `cwd`, and the report keeps a
  counter's FIRST ten citations rather than the tail.
  
  `geonosis-doctor`: an `exercised` line per config lists the rules the plugin ships that the
  config never mentions — absent, told apart from off (#5) — so a release's new rules are a list,
  not an audit. Expect one new WARN on any config that leaves shipped rules unnamed. A `drift`
  line reports a `pnpm` block in `package.json` that pnpm 11 does not read (#1): FAIL under a pin
  of 11 or later, with the keys, the new home and the install repair. The observability age and
  probe questions SKIP for a sink that reaches nothing (#10) instead of measuring how recently a
  developer ran the app.
  
  `geonosis`: the update's restart line and the MCP skew refusal say the bins in
  `node_modules/.bin` are already the new release (#9).
  
  Every kit bin's stale-build guard compares CONTENT, not only mtime: a checkout rewrites `src`
  with the same content and the next `pnpm build` is a cache hit that leaves the current `dist`
  untouched, so every bin refused with "run pnpm build" — the action that had just changed
  nothing. Each shim keeps a digest of `src` from the last run that found `dist` current; a real
  change still refuses, and the refusal names `pnpm build --force --filter=<package>`.
- 0f99e26: A baseline number now records WHAT measured it, and a number that moves says whether the
  instrument moved too.
  
  A bare number cannot answer the only question that matters when it changes: was today's reading
  taken the way the baseline's was? One consumer's `migrationWarnings` read 51 for eleven days over
  an unchanged tree while the truth was 58 — our own output truncation had been cutting the report.
  Green, stable, and wrong, and nothing in the file could have said so.
  
  `geonosis-ratchet` now writes a sibling file beside the baseline — `gate-baseline.json` becomes
  `gate-baseline.identity.json` — holding, per key, the command the reading was actually taken with,
  the build that took it, the number it was recorded for, and the number it replaced. It is written
  exactly where a number is written: on adoption and on a shrink, never over a refusal. The baseline
  itself is untouched, so a hand-editing human sees the same one-key-one-number file they saw before.
  
  When a counter GROWS and its recorded identity differs, the failure now says that the measurement
  itself changed since the baseline was recorded — so the rise may be the instrument arriving rather
  than the code regressing — and names what differs. When a counter SHRINKS and its identity differs,
  the pass says the same thing in the other direction, and says where the number it overwrote is
  kept. That direction is the dangerous one: a rise fails, so a human argues with it; a fall passes
  silently and destroys the record. A consumer who corrects a truncated 51 up to the true 58, with a
  reason in the commit, used to meet a still-buggy build that read 51 again, called it `improved -7`
  and wrote the correction away with nothing kept.
  
  No verdict changed. A rise still fails, a fall is still banked, adoption still refuses to pass, and
  an identity that matches says NOTHING — a regression under an unchanged instrument is a regression,
  and an improvement under one is an improvement. A missing identity reads as UNKNOWN, in those
  words, never as unchanged: every baseline written before this release has none, and a reader that
  cannot tell "nothing there" from "I could not see" is the defect this exists to stop repeating.

### Patch Changes

- 0f99e26: Every bin's stale-build guard now sees the workspace dependencies its build read. A `dist`
  current against its own `src` could still have been built against an older dependency's `dist` —
  literally, where tsup inlined a workspace devDependency into it — and a dependency whose own `src`
  had moved past its `dist` was read stale by every bin above it; the guard answered "current" for
  both. Now each shim holds every `workspace:` dependency, transitively, to the same reading as its
  own package (never built, stale against its src, current), and refuses when this `dist` is older
  than a dependency's — by content, so a dependency rebuilt to the same bytes with
  `--force --filter` is the same build and does not poison the tree. Unknown is still stale: the
  first run over a tree where a dependency was rebuilt alone, before any record, refuses once and
  names `--force --filter=<this package>`. Only a checkout with `src/` beside `dist/` sees any of
  this; the published package ships no `src/` and the guard is skipped. Measured: within noise for a
  bin with one dependency, +8 ms for `geonosis`, which walks fourteen.
- 0f99e26: Every bin's stale-build guard decides, and never throws. The digest guard's refusal read
  `package.json` to name `pnpm build --force --filter=<name>`; over a checkout with no manifest
  beside the bin it ended with exit 1 and a `node:fs` frame instead of exit 2 and a sentence. The
  name is now a courtesy the refusal does without. The contract, stated: a source newer by mtime whose
  content the bin saw beside a current dist is not stale; one whose content differs is, and the
  refusal names the fix; one the bin has no record for is stale — unknown is stale; a missing dist is
  "never built". Each is exit 2 with a sentence. Only a checkout with `src/` beside `dist/` sees any
  of this; the published package ships no `src/` and the guard is skipped.
- 0f99e26: The kit names no consumer. Docs, doc comments and messages describe the repos a pattern was measured in by their stack (a Medusa storefront, a Workers + D1 app, a Mercur v1 marketplace). Example configs are named the same way: `examples/medusa-storefront.*.json`, `examples/workers-app.*.json`. No behaviour changes.
- 0f99e26: A counter that reads a number that is not a count — `NaN`, infinite, negative — is refused as unmeasured. `NaN` compares false both ways, so it was judged `held` and the gate passed a reading it never took.
- 0f99e26: The README's counter table lists every counter the ratchet ships — `suppressionCount`,
  `adoptionSeamLines`, `floorSurface`, `doctorWarnings`, `bumpDurationMs`, `timeToGreenDoctorMs` and
  `openBacklogRows` had no row — and names all four counters that accept a `tolerance`, where it said
  only two did. A test holds both to the registry.

## 2.10.0

## 2.9.0

## 2.8.0

## 2.7.0

## 2.6.1

### Patch Changes

- e1db50f: `geonosis-ratchet` reads `geonosis.ratchet.json` BEFORE it queues for a heavy slot. The slot
  directory is machine-wide — it bounds heavy work on the box, not per repo — so a second repo's
  `geonosis-verify` holds it legitimately, and a run with nothing to count was spending the resource
  that bounds heavy work in order to discover it had no heavy work, waiting `GEONOSIS_HEAVY_WAIT_MS`
  (20 minutes by default) to then exit 2. Measured 2026-09-07: with during.day's verify holding the
  only slot on a memory-clamped one-slot box, this kit's own
  `registry.test.ts > passes the child bin exit code through verbatim` — which spawns a ratchet into a
  config-less directory expecting an instant refusal — sat for 979,800 ms against a 60,000 ms timeout,
  three runs running. It now refuses in about a second with every slot held, and a bin case holds it
  there: a config-less run must name `geonosis.ratchet.json` and must never say it is waiting.
- db89b0b: Two Stop-hook defects, both caught by making the instrument go red once. A heavy slot is inherited down a process tree: the holder marks its environment with the slot's path, and a descendant whose mark names a living ancestor's slot takes none of its own — a ratchet inside a verify waited on the slot its parent held, for ever, on a one-slot runner (CI's), and now waits on nobody; a tool that takes twice in one process still gets two. The Stop hook judges a run over a dirty tree: a tree's identity is HEAD plus every change on it, the hook's own files aside; a run launched over that identity is read at the next turn end and not re-run while the tree stands, where before an uncommitted edit meant every turn end launched the tier again and none was judged. The kit's own fast tier now builds first, so the hook never judges bins whose `dist` is older than the `src` under test.

## 2.6.0

### Minor Changes

- 58565d7: Plan 029, the factory floor: the gate learns to run things at once, and the heavy budget stops
  being one laptop's number.
  
  `@geonosis/verify` — `TierStep` carries `group`, the number that says which steps run at once, so a
  tier can declare `{ "parallel": [ … ] }` groups and `{ "command": "… {workspace} …", "per":
  "workspace" }` steps. New: `affected.ts` (`affected`, `affectedOf`, `workspacesIn`, `changedSince`,
  `mergeBase`, `ROOT_SHAPED`, types `Affected` and `Workspace`), `PerWorkspace`, `ParallelStep`,
  `CommandStep` on `config`, and `planSteps`, `runGroups`, `shellRunAsync`, `PlannedStep`,
  `AsyncStepRun` on `run`. `GateReport` gains an optional `affected` block. `runSteps`, `shellRun` and
  `stepIds` are unchanged. The bin gains `--affected`, `--since <ref>` and `--jobs <n>`.
  
  `@geonosis/ratchet` and `@geonosis/release` — `heavySlots()` and `DEFAULT_HEAVY_SLOTS` beside
  `HEAVY_SLOTS`, which is no longer the constant 2: the budget is `GEONOSIS_HEAVY_SLOTS` or
  `availableParallelism() - 1`, read on every claim rather than at module load (D-073).
  
  `@geonosis/ledger` — `shape.ts`: `shapeCheck`, `readManifest`, `exportsOf`, `formatShape` and the
  types `BriefManifest`, `ShapeRefusal`, `ShapeOutcome` — the lock that refuses an engineer diff
  touching a locked test, renaming an export or adding a file the brief did not name (D-072). Not yet
  on the package's public entry: the CLI door lands with W115's dispatch unit.
  
  `geonosis` — the plugin's post-edit hook takes its formatter from `geonosis.json → stack.format`
  (D-070) and the stop hook reads the machine's heavy budget.
- 58565d7: The heavy budget is memory-aware (D-081, amending D-073). `heavyBudget()` — on `@geonosis/ratchet`'s door, copied into `@geonosis/release` and the plugin's Stop hook and held to one answer by a test — is the smaller of one less than the cores and the memory the machine can spend divided by what one run holds: `geonosis.json → stack.memoryMbPerSlot`, 512 MB when nothing is declared. `GEONOSIS_HEAVY_SLOTS` still wins outright; `GEONOSIS_HEAVY_MEMORY_MB` is the memory to spend instead of the machine's. `heavySlots()` is the budget's number read fresh, and `describeBudget()` names the number and both inputs. `geonosis-verify --jobs` defaults to it, its report carries it as `heavy`, and the run prints it; the kit's own vitest workers read it too. Measured on a 4-core, 3.4 GB box: three slots over 1.4 GB swapped; two do not.

### Patch Changes

- 58565d7: The oxlint counters read the graphical format's Unicode markers — `×` for an error, `⚠` for a warning — beside the ASCII `x` and `!` a pipe gets. CI's terminal printed the Unicode pair, `oxlintRule` saw two findings it could attribute to no rule, and refused as its contract says; a laptop's vitest child never printed them.

## 2.5.3

## 2.5.2

## 2.5.1

### Patch Changes

- 7f24da6: A machine-wide heavy budget, two slots, held by every tool that spends one (#385): measured load 48
  with during.day's Stop-hook tier, this kit's Stop-hook tier, its ratchet and an engineer's suite all
  running at once — nothing held the two-slot budget across the two repos, and three kit tests read
  red under it that were green alone. `@geonosis/ratchet` ships `takeSlot`/`heavySlotDir` (a lock
  directory under `os.tmpdir()/geonosis-heavy`, two slot files each holding `{ pid, command, cwd,
  startedAt }`, a dead holder reaped on the next take, a taker that finds both held waiting once
  before refusing naming them); `geonosis-verify` (each tier run) and `geonosis-ratchet` (the run, not
  `--prove`'s inner unit) take a slot for the run's duration. `@geonosis/release`'s `smoke
  baseline`/`smoke run` take one too, from their own copy (this layer imports nothing, same posture as
  `envelope.ts`). The plugin's Stop hook reads a fresh green `gate-report.<tier>.json` — its own
  recorded commit equal to the current `HEAD` with a clean tree — and reports PASS without re-running;
  otherwise it peeks the same two slots (never claims one itself, or the verify child it is about to
  spawn would be racing against its own reservation) and blocks naming both holders rather than
  running a third tier.

## 2.5.0

## 2.4.3

## 2.4.2

## 2.4.1

## 2.4.0

### Minor Changes

- 3b7b2ae: A `doctorWarnings` counter: the warnings `geonosis-doctor` reports about the repo it runs in, as a number that may only fall. The kit lints itself with its own preset and ratchets itself with its own counters, and pointed the doctor at consumers only — so its findings reached a session-start hook, where nothing fails a gate, and an unfalsifiable FAIL sat unread for as long as the marker had existed.
  
  Its unit is the RULE, not the finding. The doctor reports a rule that cannot be exercised once per config that enables it, which is right for a reader — each config is its own edit — and wrong for a ratchet: a repo loading the plugin in twelve configs would read thirteen rules as one hundred and fifty-six, and adding a legitimate twelfth config would fail the gate for a good change. That is the shape that retired `openBacklogRows`, where a shrink-only number over a quantity that rises honestly teaches people to stop being honest. Measured here, it is the difference between 30 and 17. The check stays part of a warning's identity, so two checks with something to say about one file remain two pieces of debt.
  
  The FAIL count is deliberately not this counter's business: the doctor's own exit code is, and a repo runs the doctor as its own gate step so the reader gets its tail rather than a counter's summary of it.
- 56e0f33: A counter the baseline has never held is ADOPTED rather than refused. It used to throw `has no number for "<key>" — add it before enabling the counter`, and the D-007 write guard refuses the agent exactly that edit, because everything that scores the agent is written by the runner — so two of the kit's own rails together made a counter impossible to enable without breaking one of them. The runner measures it, writes it, and reports `ADOPTED` on its own line.
  
  What it does NOT do is call that a pass. The run exits 1 and says the number was measured for the first time and not judged: a first reading can be anything (a mis-escaped `match` reading 0 was banked as a floor once), and a floor nobody has looked at is not a floor anybody agreed to. The second run is the one that judges, against a number a reader has now seen. Nothing is adopted over a refusal, because a run that could not measure one counter has not earned the right to write any number.
  
  This loosens a refusal rather than tightening one, so no configuration that worked before stops working.

### Patch Changes

- bef73fb: Every shim tells a tree that was never built from a build that fell behind. `newest(dist)` answers 0 for a directory that is not there and for one that is merely old, so both conditions were one comparison and one sentence — and the staleness sentence was printed over trees that had never been built at all. It blocked a session's turn in a fresh git worktree, which has no `dist` because `dist` is not committed, with a message about a build going stale. Only one of the two is evidence of a mistake, and the two send a reader to different places.
  
  The test that holds it plants BOTH cases and requires different sentences, which the previous one never did: it planted only a stale `dist`, so the check had never been shown a case whose answer should differ. Named for what it is, since the same gap produced two wrong published numbers the same day — a probe never shown a differing case has not been shown to discriminate, which is this repo's own "a gate never seen red has not been shown to measure", one layer down, applied to the thing doing the observing.
  
  (This changeset names four packages of the fixed group deliberately: the group versions as one number and a changeset's description lands only in the packages it names, so naming several puts it in more than one place a reader might look. See #356.)

## 2.3.1

## 2.3.0

### Minor Changes

- 3dac9a7: **`archViolations` declares `match` as a reading param (#30).** Its shipped probe plants a scanner
  printing `✗` lines, so it proves the command and never a `match` a repo configured for its own
  scanner's shape — the one counter with a reading param that escaped #140's mechanism. A configured
  `match` now reads UNPROVEN until the entry declares `probe: { sample, expect }` beside it, and the
  README says what a probe does and does not prove.
- 3dac9a7: **A `dx` counter family, fed from the kit's own reports (#93).** `bumpDurationMs` and
  `timeToGreenDoctorMs` read `.geonosis/bump-report.json` — which now records how long the bump took
  and how long each step that RAN took; `openBacklogRows` counts the rows of a register nobody has
  closed, `path` required and `done` the repo's own word. A bump whose doctor was red has no
  time-to-green, and all three refuse rather than reading a zero.
- 3dac9a7: **One `--since`, and a report a machine can read (#97).** `--since` names a plan number, a day and
  a git ref in three commands, and all three took any string: `plan check --since garbage` exempted
  nothing and printed the verdict it would have printed with no flag, `stats --since notadate` put
  every recorded day outside the window and printed `0 turns` as a pass rate. One reader now declares
  which kind each means and refuses an unreadable value by name. `geonosis-ratchet`,
  `geonosis-verify-arch` and `geonosis-lint-parity` take `--json`: the same report, on stdout,
  whatever the verdict — `--json` with an explicit `--format` is refused rather than resolved.

### Patch Changes

- 3dac9a7: **Portability measured rather than assumed (#100).** The machine-wide heavy lock honours
  `XDG_CACHE_HOME` instead of hardcoding `~/.cache`, ignoring a relative value as the spec requires.
  `onPath` replaces the containment lookup that joined its argument into a `shell: true` command
  line: a `sandbox-exec`/`bwrap` under a directory with a space in its name read as absent and failed
  the run open.
- 3dac9a7: **One documented spelling (#90).** Every README of a package the metapackage fronts now names
  `geonosis <tool>` — the door that pins the whole toolchain at one version — beside the bare bin, and
  a `tooling/` test holds it. `#230`'s stamp test lands with it: every flag `docs/orchestration.md`'s
  adoption loop names has to carry the version it first ships in, because the loop once told a
  consumer to run a flag the published version did not have.

## 2.2.0

### Minor Changes

- d15a654: **A floor adoption is measured (#280).** The user's rule — a floor is adopted the day it deletes
  more than it adds — had no instrument: during.day's four db floors came out +222/−225 and its
  workflows floor +1807/−1493, and both numbers were read by nobody until they were asked for. A
  consumer declares `adoption.seams: [globs]` in `geonosis.json` (default none, and a repo that
  declares none gets no figure rather than a figure about everything). `geonosis update` then reports
  lines deleted against lines added over those globs, `git diff --numstat` between the commit the bump
  started from and the working tree, in the verdict and in the bump report. `geonosis-ratchet` gains
  `adoptionSeamLines`, the lines those globs still hold — a number the ratchet already refuses to let
  grow. A snapshot records the same figure for its consumer and `geonosis-release adoption` carries it
  per consumer (MEASURED: a consumer's `.geonosis/` is an excluded directory, so their own bump report
  never reaches a recording — the figure is taken where the tree is read).
- d15a654: **`floorSurface`: a floor's public surface is a ratchet (#294).** `createConnections` has five doors
  where a consumer needs two, and #248 was fixed by making one of them open its own frame rather than
  by removing a door — every added door is a line to learn, a hole to exam and a promise the next
  major has to keep, and nothing made the growth visible while it happened. The counter reads the
  exported names out of the built declaration file each door in `exports` promises — declared in
  place, re-exported from a chunk, or listed in a brace group, since a bundler exposes a name all
  three ways — and refuses a package whose promised `.d.ts` is not built rather than reading an
  unbuilt surface as empty.

### Patch Changes

- d15a654: **An envelope writer refuses a root it has been told is not its (#241).** A suite ran one over the
  SHARED repo root mid-run: parallel suites made the comparison racy, and the file it left behind is
  one the next run reads as real. Every one of the seven writer copies now refuses to write when
  `GEONOSIS_ENVELOPES_FORBIDDEN_ROOT` names the root it was pointed at, saying so and pointing at a
  scratch root instead. The kit's own vitest config sets it to the repo root, so a test that writes an
  envelope there names itself — which is how the offender that filed this row was found again.
- d15a654: **`knipIssues` cites the issues, not just the count (#269).** The same tree read 5 on CI and 6 on a
  laptop and the envelope carried the number alone, so nobody could see which line had moved. The
  counter now cites every issue knip printed under the heading it printed it under — quoted, never
  re-parsed — and the `Unresolved imports` class names what did the resolving: the command and the
  runtime that ran it, because an unresolved import is a fact about a resolver and not about a tree.

## 2.1.0

## 2.0.0

### Minor Changes

- 97bc8e9: **A debt marker names the plan that owns it.** New rule `todo-names-its-plan` (preset `tooling`): a marker must cite a plan — `TODO(025):` — or it is work nobody scheduled. The marker words are an option and the plan-id shape is an option, both empty-by-default, and enabled with no markers the rule refuses the run. Comments come from the PARSER, so the word in a string literal or after the `//` of a URL is not a finding. New counter `orphanTodos` counts what a repo already has — markers naming no plan, and markers naming a plan that is not on disk — so the law can be adopted as a ratchet rather than as a sweep. The kit enables the rule on itself; every example in the rule's own prose cites a real plan, because exempting a rule's source from the rule is the exemption that never comes back.

### Patch Changes

- 74ae3ec: **`knipIssues`'s refusal names the third way out (#206).** A configured `headings` list that drops a heading knip printed is refused — right, #178 — but the text offered only "add it" or "excuse it", and a repo that never meant to narrow anything read it as an instruction to enumerate every heading it has. The refusal now also says: remove `"headings"` altogether and count every heading knip prints.

## 1.4.0

### Minor Changes

- ce2167f: **`suppressionCount` — lawLineCount's sibling (#134).** Occurrences of eslint/oxlint/biome-disable, @ts-expect-error and @ts-ignore across source files (fixtures, deps and prose excluded; `roots` scopes it), shrink-only like every number. A repo arriving with 261 suppressions cannot adopt law 3 as a wall, but it can adopt it as a ratchet.
- 0d99139: **`--prove` exercises the reading the config asks for (#140), and a match that reads nothing refuses (#141).** during.day's live incident: a mis-escaped `match` read 0 from the real command, prove said PROVEN off a plant the configured match never saw, and the ratchet lowered the baseline — ten pieces of debt erased with a congratulatory message. Now: a counter declares its `readingParams`; a config customizing one must declare `probe: { sample, expect }` beside it (the sample its reading must count) or the prove line says UNPROVEN and the run does not pass; and `sumOfCounts` throws, naming the match and the first line, when output is present and the match reads none of it — nothing-found and could-not-read are different answers.
- 5df33fd: **The report envelope, balanced at write time.** `writeEnvelope` writes `{ tool, version, considered, read, refused[], excused[], findings[], durationMs }` to `.geonosis/envelopes/<tool>.json` and refuses — throwing, writing nothing — unless `considered === read + refused.length + excused.length`. Four denominator bugs landed in one day, all of them green, all of them green because the tool published the numerator and nobody published the denominator. A run that has lost count of its own inputs now renders no verdict at all rather than a wrong one. `versionOf(import.meta.url)` answers the same in `src/` and in the bundled `dist/`, so an envelope names the build that wrote it.
- a430c81: **A counter that blew up no longer hides the ones behind it, and the run writes its envelope.** `runRatchet` collects every counter it could not measure into `refusals` instead of throwing on the first, so one broken tool no longer leaves the rest of the gate unrun and unreported — the fix `migrations` already made for an unreadable file. The run still fails, at exit 2, and each refusal now names its cause AND the command that reproduces it, read back off the run rather than guessed from the config. The baseline rewrite is suppressed whenever anything refused: a win locked in over a census that only half happened buys a lower floor with a number nobody took. `geonosis-ratchet` writes `.geonosis/envelopes/ratchet.json` — every configured counter leaves by exactly one door, measured, left out by `--tier`, or refused — and refuses to render a verdict at all if those do not add up.
- 3de7f28: **One answer to "where does the baseline live" (#120).** `resolveBaseline(cwd)` is exported from `@geonosis/ratchet` — the ratchet config's `baseline`, else the default, never a throw. The rails deny rail now denies the baseline where `geonosis.ratchet.json` says it LIVES (the default alone let a tampering write land, measured on sandbox-exec, on a repo whose baseline is `scripts/gate-baseline.json`); the mcp preamble reads counters from the declared path instead of saying "no counter is ratcheted" over twenty. The layer walls forbid rails and mcp importing ratchet, so each carries a ten-line reader pinned behaviour-equal to `resolveBaseline` by a conformance test — which also pins the class: the literal filename is greppable only in the resolver, the pinned readers, the writers of the default pair, and documented prose.

### Patch Changes

- 8f5dc0c: **`cloneCount` says when its tool printed nothing at all.** The refusal used to read `no "Found N clones" line:` followed by an empty tail, which tells a reader neither what happened nor what to run — a tool that printed nothing and a tool that printed a paragraph this cannot parse are two different problems. Found by the new refusal-law contract test, which drives each refusal the envelope law introduced and requires the sentence to carry a cause worth acting on and the command that reproduces it.
- d23aef3: **Each tier records its own gate report (#121).** `geonosis-verify <tier>` writes `.geonosis/gate-report.<tier>.json` beside the shared latest-run report (which stays, for every reader that wants "what ran last"). `fastTierMs` reads its own tier's file first — `fast` straight after `full` refused deterministically while every step printed ok (dielime, reproduced on purpose). The shared file remains a fallback for trees written by an older verify, but a wrong-tier fallback is a refusal naming the file the right tier writes.
- 565db59: **Regression evidence cites the lines the counter counted (#122).** A counter may now declare `evidence(output, params)`, and the report prints THOSE lines under `<-- REGRESSED` instead of the tail of the run. `oxlintRule` cites findings attributed to its own rule, `oxlintErrors` cites errors and `oxlintWarnings` warnings — dielime read a +2 on one rule with two unrelated WARNING findings underneath it, because those were the last lines oxlint printed while the two real errors sat higher up. The count was right the whole time; the lines sent the reader to the wrong files. A counter that names nothing, or recognises nothing in a run, still falls back to the tail.
- ef29fc0: READMEs: a tool catalog publishes value vocabularies, not just field names — a zero from a wrong filter reads as good news (#135); and the pairing rule — a scoped fast tier is safe exactly when the full tier is total, every hard cap needs a full-tier counter (#136). Both measured in dielime.

## 1.3.0

### Minor Changes

- 2d1d841: **`geonosis-ratchet --help` ran the full ratchet (#66).** There was no usage, no `--help` branch and
  no unknown-flag rejection: everything unrecognised fell through to a complete measurement — a run
  that can rewrite `gate-baseline.json`, started by a typo. Measured at 141 s on the kit's own tree.
  It now prints its usage at exit 0 for `--help`/`-h`, and refuses any flag outside the known set by
  name at exit 2, before the lock is taken.
  
  **It also answers `--print-config-shape` (#75)**: the shape of `geonosis.ratchet.json` and `gate-baseline.json`, including why two counters may not write one key.
  
  **The README names the NODE_OPTIONS/nested-pnpm trap (#63)**, with the `env -u NODE_OPTIONS` shape and the doctor line that asks for it.

### Patch Changes

- db1cebd: **The "a scan that could not run is not a clean scan" answer is pinned, permanently (#80).**
  during.day removed `geonosis.verify-arch.json` deliberately to see what the gate would say, and
  `archViolations` refused rather than reading zero findings. That answer may never regress: the
  failure it prevents is silent and self-reinforcing — a zero the ratchet BANKS, rewriting the
  baseline down off a scan that never happened, green for ever after. The fixture runs the real
  scanner in a directory with no config and the real counter over exactly what it printed; a captured
  sample would prove the regex and not the pair, and the pair is what broke. Shown to fail against a
  planted regression before it was kept.
- f37950f: Counter output capture no longer drops the tail of large outputs: `spawnSync` replaces the merged subshell, both streams arrive whole (stdout then stderr — cross-stream interleave was never promised on the error path and is no longer promised on success), and a 200k-line output keeps its summary line (dielime Block D, #111).

## 1.2.0

## 1.1.0

## 1.0.1

### Patch Changes

- 1c2ee0b: **Every package ships its declarations and says where they are.** `@geonosis/visual-diff` and
  `@geonosis/verify` emitted no `.d.ts` at all (TS7016 in a consumer: the comparator every visual test
  calls became `any` at the one seam that decides whether a screenshot regressed), and no package
  declared `types` — the nineteen that worked did so through the sibling-file fallback, which ends the
  day `exports` gains a condition. Every export now carries a `types` condition beside `default`, every
  manifest a `types` field, and `tooling/types.test.ts` holds all twenty-one to it after the build.
  
  `geonosis-verify-arch` prints a check once when its label is its id ("direction, orphan", not
  "direction direction").

## 1.0.0

### Major Changes

- e6a54d5: **`archViolations` stops reading "could not measure" as zero.** A scanner that found something exits
  non-zero. So does a scanner that could not run at all, and the counter told them apart by neither —
  it counted marker lines and returned the number, so a run that printed nothing because it never
  happened came back 0.
  
  That is the silent zero at its most expensive, because the ratchet BANKS it: a baseline of 4 gets
  rewritten down to 0 and the commit that "earned" it is a config with a typo in it.
  `geonosis-verify-arch` exits **2** for exactly that — no config, an unknown pack, an unknown format
  — and under `--format ratchet` prints nothing while doing so. Its exit 2 is now visible.
  
  A non-zero exit with zero matching lines is a `CounterError` naming the command, the status and the
  tail of what it printed. A non-zero exit WITH findings still counts them, which is the normal
  reading and the half that must not become collateral: a scanner exiting 1 because it found four
  things is the tool working.
  
  The probe changed with it. It used to `cat` a captured sample, which proved the regex and nothing
  else — a real scanner printing another shape still read 0 and the probe still said PROVEN. It now
  plants a scanner and runs it: findings on stdout, a non-zero exit because it found them, and the
  counter has to come back with the number.
- 9858ee5: `error-page-status`, a ninth probe from a production incident: a page whose body is an error surface
  and whose status is healthy. Every curl-style health check, uptime monitor and load balancer reads
  such a page as fine while nobody can use it. The status comes from the page's own response, so
  `ProbeContext` gains `status` — a second request would ask a different question of a server that may
  not answer twice the same way. `patterns` is required (D-031); a page that answered 400 or worse is
  a server telling the truth, and the walk refuses that URL on its own. `walkFindings` counts the new
  class.
- c575f44: Five counters, `tolerance`, and a baseline rewrite that only ever goes down.
  
  - `disabledCiJobs` — lines of `if: false` across the workflow files (`dir`). A job switched off to
    get a release through is debt exactly like a lint error: invisible, because the pipeline goes
    green. A condition that merely mentions `false` is not one, and a repo with no workflows
    directory reads 0 rather than refusing; a workflow that is there and cannot be read refuses.
  - `bundleBytes` — one integer out of whatever the repo's sizing command printed (`command`,
    `match`), separators and all. With `match` it takes the group that pattern names and refuses when
    it matches nothing, rather than falling back to any integer and reporting the wrong number as
    confidently as the right one.
  - `fastTierMs` — `finishedAt − startedAt` from the gate report `geonosis-verify` wrote (`report`,
    `tier`). It refuses a report of another tier: the full tier takes minutes, and timing it against
    the fast tier's baseline would report a regression of the wrong thing.
  - `testsWithoutRunner` — workspaces holding tests with no `test` script. Unmeasured and passing are
    indistinguishable from outside, so the count of unmeasured things has to be a number of its own.
  - `packagesWithoutTypecheck` — workspaces with no `typecheck` script (`script`).
  
  `tolerance` is new on the verdict: the fraction of the baseline a number may drift UP before the
  ratchet calls it growth. Bytes and milliseconds move on a dependency patch nobody chose, and a gate
  that fails on +40 bytes is a gate that gets switched off. It forgives noise upward only — a shrink
  of any size still lowers the baseline — and a `tolerance` that is not a non-negative number stops
  the run naming the entry, because one that silently became `NaN` reads exactly like a counter that
  can never grow.
  
  **Fix:** a baseline rewrite now lowers only the numbers that SHRANK. Writing every measured key back
  was a harmless no-op while `held` implied `now === baseline`. Tolerance breaks that implication, and
  the blanket rewrite would have laundered growth-within-tolerance into the new floor on the first
  unrelated win — a baseline going up on a pass.
  
  `examples/dielime.ratchet.json` is dielime's Phase 3 config: 36 keys, one `testFailures` entry per
  workspace. It keeps `--format=unix` and `expectFormat` on the two oxlint counters, which dielime's
  own config drops — the summary cross-check can tell a misparse from a clean run only when the run
  printed a summary, and a warnings-only run in an unreadable shape prints one that says nothing
  about errors. Pinning the format is what closes that window.
- 90d38ec: **`geonosis-backend` — the landmine list as the mechanical layer for backend work.** Forty-six
  failures that read as success, each as symptom → cause → detection → fix, grouped by the engine it
  is TRUE of: nineteen `medusa`, two `sagaflow-cf`, twenty-five `any`. You arrive with a symptom and
  leave with a fix; that is the whole design, and it is why the entries are a table and not prose.
  
  Every entry points at the kit rule or counter that catches it — `emit-declares-attempts`,
  `plugin-route-namespaced`, `named-when-block`, `mutating-step-declares-retry`,
  `store-route-scopes-tenant-data`, `no-d1-transaction`, `step-opens-its-own-cell`,
  `queries-require-org-scope`, `no-unregistered-plugin-dir`, `no-mock-db-in-integration`,
  `layer-walls`, `ssot-no-process-env`, `disabledCiJobs`, `testsWithoutRunner`, `knipIssues` — or says
  **`no gate yet`**, and those rows are the backlog's next rules said out loud rather than omitted
  into what would read as full coverage.
  
  Every entry cites a document in this repo: Medusa's seventeen (`docs/analysis-medusa-2026-08.md`
  §4), Midday's eighteen (`docs/analysis-midday-2026-08.md` §4), nine consumer landmines from
  `docs/rules-backlog.md`, and two engine facts `RULES.md` states as the reason two presets exist. A
  landmine that could not be cited was dropped.
  
  Pinned by a test that PARSES it rather than reading it: the count, all four fields per entry, a
  source path that exists on disk, and a gate name that the plugin or the ratchet actually exports —
  so a rule renamed in the plugin fails the suite instead of leaving the skill pointing at a gate
  nobody has. The two analyses' own numbering is read from the analyses, so an eighteenth Medusa
  landmine fails the build until the skill carries it.
  
  Version-only entry for `@geonosis/ratchet`: the plugin's manifest takes its version from that
  package, and the release group moves together.
- c418f91: The `geonosis` Claude Code plugin — the method as enforcement rather than advice.
  
  `plugin/` ships five hooks, Node ≥ 22, zero dependencies, every tool resolved from the CONSUMER's
  `node_modules/.bin` so a repo that has not adopted a tool is never blocked on a tool it lacks:
  
  - **SessionStart** runs `geonosis-doctor` if it is installed and prints a one-line digest of
    `.oxlintrc.json` — which plugins are loaded, how many architecture rules are at `error`. It never
    fails a session.
  - **PreToolUse / Bash** refuses `prettier` (law 4) and `--no-verify` (law 7), and hands a
    `git commit` to `geonosis-ledger` — the message its `-m` flags compose, and whether the staged
    diff delivers anything but comments. The detectors are the ledger's, never reimplemented here; a
    repo without it gets a note and no opinion.
  - **PreToolUse / Edit|Write|MultiEdit** refuses a write that ADDS `eslint-disable`,
    `oxlint-disable`, `@ts-ignore` or `@ts-expect-error` (law 3), any write to `gate-baseline.json` or
    under `.geonosis/` (D-007 — the scored agent never writes the scoreboard), and a lint config that
    sets a rule to `"warn"`, `"off"`, `0` or `1` (law 2). It reads what the call would ADD, never what
    it removes, so the edit that DELETES a suppression is not refused by the guard against them.
  - **PostToolUse / Edit|Write** runs oxfmt on the file just written. It never blocks.
  - **Stop** runs `geonosis-verify fast` and will not let the turn end on a red one, capped at five
    blocks per session — an agent retrying the same broken thing five times has a problem it will not
    solve on the sixth, so the sixth prints the report and lets go.
  
  `.claude-plugin/marketplace.json` offers it from `./plugin`. `pnpm generate:skill` now writes the
  plugin's copy of the mechanical skill, the engineer agent with the machine-specific path taken out,
  and `plugin.json` carrying the kit's own version; staleness tests fail when any of the four drift.
- 5f09ddc: Every hook is driven end to end, as Claude Code drives it.
  
  Each script is SPAWNED with real stdin JSON over a throwaway repo carrying a `geonosis.json`, a
  `gate-baseline.json`, an `.oxlintrc.json` and a `node_modules/.bin` linked to the BUILT
  `geonosis-verify` and `geonosis-ratchet`. A hook tested by calling its policy function proves the
  policy; only a spawn proves the exit code, the JSON on stdout and the tool resolution — which is
  the whole of what Claude Code actually reads.
  
  The Stop hook is exercised against a real fast tier that really goes red: a law file one line over
  its baseline, so the ratchet grows and the hook blocks with exit 2, the failing step and its tail on
  stderr. Then the sixth block prints the report and exits 0; a green run clears the session's count;
  `stop_hook_active` short-circuits; and a repo with a BACKLOG that did not regress is not blocked at
  all — which is the whole adoption argument, tested rather than asserted.
  
  The suite was falsified before it was trusted: making Stop exit 0 instead of 2, dropping the
  delivery check, and raising the cap to 50 each turned exactly the tests that should have gone red,
  red.
- 60f4d8f: **The law ceiling in both consumers, and an instrument for finding out whether cutting the law
  helped.** `lawLineCount` is now in `examples/dielime.ratchet.json` and
  `examples/during-day.ratchet.json`, in the **fast** tier — it reads one file, and a law that grew is
  exactly the regression a turn should not be allowed to end on. Measured read-only 2026-08-30: those
  two laws are **382** and **1,104** lines against guidance of 200, and those are the baselines each
  repo starts from.
  
  `geonosis-verify stats [--since <YYYY-MM-DD>] [--json]` reports the **first-try pass rate**: the
  share of turn-ends whose fast tier was green on the first run. `--since` prints that window AND
  everything before it, so a before/after comes from one command and one instrument.
  
  The Stop hook now records what it gated. It had to: `stop-blocks.json` is the block cap's working
  memory and a **green turn deletes its own row from it**, so a session that never blocked and one
  that blocked and recovered are both absent — there was no history to read. The new
  `.geonosis/stop-turns.json` is two integers a day, and the hook writes it itself because a hook is
  fetched as a git tree with no install step and may import nothing; a test reads the hook's own output
  back through `readStats` so the two spellings of the format cannot drift apart.
  
  Three refusals, all the same refusal — **an empty record is not a perfect score.** No record says so
  and gives no rate rather than 100 %; a record that will not parse is an error rather than zero turns;
  and a turn the hook never gated (`stop_hook_active`, where the gate does not run) is not counted,
  because there was no first try to have.
  
  `docs/law-diet.md` is the M6 procedure end to end: arm the ceiling, record a before, cut in one
  commit on a day you write down, read the after against the same instrument — and write down what
  happened even when nothing moved, because a cut that changed nothing says the law was not the
  binding constraint.
- 89c6769: The ratchet's own layers are walled, and the walls have a door that is proved open.
  
  The three ratchet layers named FILE paths only, so `packages/ratchet/src/index.ts` — the published
  entry — sat in no layer and could import anything, the CLI included, with lint green. It is
  `ratchet-api` now, and the formatter it shares with the CLI has moved into a `ratchet-report` layer
  of its own rather than being counted as part of the CLI it is not.
  
  The dogfood suite gains a second kind of plant. A wall that refused everything would be as useless
  as one that refused nothing, so each layer's permitted import — `verify` reaching the lock through
  `@geonosis/ratchet` — is planted and asserted to stay CLEAN. Without that, the next engineer widens
  `mayImport` to get their work through and the wall goes with it.
- b2d915f: **Fix:** a counter's command no longer loses what it wrote to stderr.
  
  `<command> 2>&1` binds the redirect to the LAST command of the string. `echo boom >&2; echo fine`
  therefore merged nothing, and on a ZERO exit `execSync` hands back stdout alone — so a tool that
  prints its findings to stderr and exits 0 read as a tool that found nothing. That is the exact shape
  of a gate that has quietly stopped gating, and it is the one direction in which this kind of bug is
  dangerous: the number comes back lower than the truth and the baseline banks it.
  
  On a NON-ZERO exit the line survived, because the failure path concatenates both captured buffers —
  but concatenation put every stderr line after every stdout line, moving each diagnostic away from
  the line it was about, which is what the evidence tail on a regression is for.
  
  The command now runs in a subshell, so the shell itself merges the streams in the order they were
  written. Measured before and after: `echo boom >&2` on a zero exit read `""` and now reads `boom`;
  `echo one; echo two >&2; echo three; exit 4` read `one three two` and now reads `one two three`.
- 7762a01: **`testFailures` in summary mode reads the runner's summary LINE, not the word "fail" anywhere in
  the output.** The regex was unanchored, so anything a command printed on its way to the runner was a
  candidate count. Reproduced: a command that printed `naming 4 failing test(s)` and then a green
  `Tests  3 passed (3)` was read as **four failures** — a green run regressing a baseline. That is the
  false RED mirroring the false green `report: "vitest-json"` exists for; both come from reading
  something that is not the runner's own last word.
  
  Two dialects now, and both were captured from the binary rather than guessed at. vitest 3.2.7's
  `      Tests  1 failed | 1 passed (2)` — its own label, in its own column, closed by the total in
  parentheses — and bun 1.4.0's ` 1 fail` on a line of its own, which it prints on a green run too,
  saying 0. Nothing else counts: `Test Files  1 failed (1)` counts FILES, `(fail) one [11.71ms]` is
  bun naming one, and a wrapper's prose is a wrapper's prose. All three used to be read as failures.
  
  Three consequences beyond the fix. A suite vitest skipped whole (`Tests  2 skipped (2)`) reads 0
  instead of refusing — it is a run that happened and found nothing red. A suite that threw before
  collecting (`Tests  no tests`, beside `Test Files  1 failed (1)`) is now **refused** instead of
  being read as one failing test: nothing there is a count of failing tests, and a gate that cannot
  measure has not passed. And summaries **add up**, so a command that invokes the runner twice no
  longer banks the second suite's reds as a win.
  
  The refusal names both dialects, so a repo whose runner speaks a third one gets a message instead of
  a wrong number. `--prove` grew a probe: `summary (vitest)`, `summary (bun)` and `vitest-json` are
  each shown reading their own planted finding, because a dialect nobody proved is a dialect nobody
  has been shown to read.
- 48bce0c: **Fix:** only a counter that measures a quantity accepts a `tolerance`.
  
  Any counter took one. `{"counter":"lawLineCount","tolerance":0.5}` over a two-line law with a
  baseline of 2 printed `ratchet PASS` at three lines — a 50 % growth, waved through. The same edit is
  `"tolerance": 100` on `oxlintErrors`: the gate switched off from the config file, with the run still
  reporting green. That is law 2 — never downgrade a rule — wearing a friendlier name, and it was the
  easiest edit in the repo to get past a reviewer.
  
  `Counter` gains `tolerates?: true`, declared only by `bundleBytes` and `fastTierMs`. Whether a
  number is a measured quantity or a count of findings is the counter's to know, not the config's.
  Anything else now stops the run, naming the entry:
  
  ```
  geonosis-ratchet: "oxlintErrors" does not accept a tolerance — only a counter that measures a
  quantity declares one
  ```
  
  The plugin's write guard gains its first **ask**. A write to `geonosis.ratchet.json` that introduces
  or raises a `"tolerance"` is neither refused nor allowed: it goes in front of the human as
  `permissionDecision: "ask"`. Widening a band is legitimate and is not the agent's call — a band
  somebody chose and a band somebody's agent chose are not the same thing. A tolerance carried through
  an edit unchanged is not a change and says nothing.
- 461233c: Docs for the two new things: `docs/plugin.md` and `packages/verify/README.md`.
  
  `docs/plugin.md` covers installing the plugin from a local path and from the remote once it is
  pushed, what each of the five hooks does and the reason it does it that way, the five-block cap,
  which files the runner owns and why the agent may not write them, what to put in `geonosis.json`,
  and — the section every README skips — what the plugin CANNOT do. A `fast` tier of `["true"]` passes
  for ever; only `geonosis-ratchet --prove` shows a counter can go red.
  
  `packages/verify/README.md` is the runner's own: `--prove` first, the config with no default tier,
  the three exit codes and why a report that could not be written is exit 2, the report shape with the
  `<tier>/<n>` id rule, and `--exclusive`.
  
  `docs/README.md` indexes both. The ratchet README gains rows for the five new counters, a section on
  `tolerance` and the shrank-only rewrite it forces, and the landmine that cost an afternoon: a
  loader or shim named RELATIVELY in a counter's command resolves to nothing under `--prove`, whose
  probes run in a scratch directory. Write it `$PWD/…` and it works in both.
- e48501b: `@geonosis/verify` — one gate runner, with a report a hook can read.
  
  `geonosis-verify <tier>` runs the shell steps a repo declared under `verify` in `geonosis.json`,
  serially, stopping at the first red, and writes `.geonosis/gate-report.json`:
  `{ tier, startedAt, finishedAt, ok, steps: [{ id, command, ok, exitCode, ms, tail, skipped? }] }`.
  A step whose text is another tier's name is that tier, inlined in place; the inlined steps keep the
  tier that declared them, so `fast/2` is the same command whether the operator ran `fast` or ran the
  `full` tier that inlines it.
  
  There is no default tier and there never will be one: no config, no `verify` object, or an unknown
  tier is refused by name with exit 2, and so is a green run whose report could not be written — a
  gate that cannot record has not passed. A failed step is exit 1.
  
  `--exclusive` takes the ratchet's machine-wide lock through `@geonosis/ratchet`'s published entry
  (`--exclusive-timeout <seconds>` bounds the wait), so a verify and a ratchet on one laptop serialise
  against each other rather than each against itself. `--prove` plants a tier with one passing step
  and one that exits 3, runs the real binary over it in a temp directory, and checks the failure
  reached the report — a runner that reports green is worth nothing until it has been watched going
  red on purpose.
  
  The kit walls the new package with its own `layer-walls` rule: `verify` may import `ratchet-api`
  and nothing behind it, planted against in the dogfood suite like the other walls.
- 7e8d2c1: `walkFindings`: the defects `geonosis-walk` reported, as debt that may only shrink. Reads the walk's
  JSON report (`report`), counts findings across every page, and scopes to named classes with
  `classes` — refusing a class the walk does not have rather than reporting a confident zero for a
  typo. A missing or unparsable report is a refusal, never a zero (D-030): the walk writes no report
  when it could not run, and answering zero there would make its loudest failure the best number on
  the board.

## 0.6.0

### Minor Changes

- **No counter changed.** The version moves because the packages release as one fixed group.

- **Two things this package cannot see about itself now have somewhere to be seen from.** A baseline
  is lowered in place when a number shrinks, which is also what lets a branch raise one back up and
  stay green on every gate it runs; and a workspace whose suite no `testFailures` entry covers is
  unmeasured, which reads exactly like green. `geonosis-doctor --baseline-against <ref>` asks the
  first from outside the checkout, and its `runner` check names the workspaces the second applies
  to. Measured on the two consumers the day it shipped: 28 such workspaces in one, 8 in the other.
  See [`@geonosis/doctor`](https://www.npmjs.com/package/@geonosis/doctor).

## 0.5.0

### Minor Changes

- **New rule `cell-no-cross-feature-organelles` (preset `frontend`).** `organelle-dependency` is scoped
  to `organelles/` and `no-cross-feature-stores` only judges stores, so a CELL importing another
  feature's ORGANELLE passed both while both stayed green. Option `sharedFeatures`, default
  `['shared']` — the same option and the same default as `organelle-dependency`'s, because the two
  rules draw one boundary from two sides. Measured on a consumer: three findings in two files.
  
  **The corpus carries both spellings of every direction rule.** A direction rule judges where an
  import lands, so `@/features/billing/x` and `../../billing/x` are one breach; the corpus previously
  carried whichever spelling each rule's author happened to write. Fifteen fixtures added. No rule
  changed.
  
  **`geonosis-lint-parity --corpus --expect-changed <rule,…>`.** A changelog line that names a rule is
  a claim; this makes it data the corpus judges, and exits 1 on a claim the corpus is silent about or a
  change nobody claimed. Mandatory for any release whose notes name a rule.
  
  **`testFailures` reads the runner's JSON report** with `report: "vitest-json"`, refusing when the
  report is absent, unparsable, missing `numFailedTests`, or says `success: false` while naming none.
  `Counter.probe` takes a list, so `--prove` proves each reading mode by name.
  
  **`geonosis-ratchet --prove` proves the lock**, by running two children of the CLI over a slow
  `--hold` and requiring the second to start after the first finished.
  
  `@geonosis/ratchet`'s source is layered — `core/`, `counters/`, `report.ts`, `cli.ts` — with no
  change to what `@geonosis/ratchet` exports or to a single line the CLI prints.

## 0.4.0

### Patch Changes

- `--exclusive` did not serialise, and the test that should have said so was blamed for it.
  
  `openSync(path, 'wx')` is atomic about the NAME only: the file it creates is empty until the holder
  lands on the next syscall, and a run polling in that window reads a lock nobody can parse, calls it
  stale — `taking over a stale heavy lock (pid unreadable is gone)` — removes it and takes it. Two runs,
  one lock, both certain they held it. It reproduced three times in five on a loaded laptop, which is
  the only kind of machine the lock is for.
  
  The holder is now written under a private name and hard-linked into place: `link` fails `EEXIST`
  exactly as `'wx'` did, and when it succeeds there was never an instant at which the lock existed
  without its holder inside it. The staging name is dropped either way, so a contended run leaves
  nothing behind in `~/.cache/geonosis/`.
  
  The test's ordering is causal now rather than temporal: the holding run leaves its critical section
  only when the test opens a barrier, and the test opens it when the OTHER run says it is waiting.

## 0.3.0

## 0.2.1

### Patch Changes

- Gates prove they can fail (plan 019, K2.1–K2.3).
  
  - **`geonosis-ratchet --prove`** — every counter ships a falsification probe: a known-bad input it
    must be able to read. `--prove` runs each configured counter's probe in a temp directory, never in
    the repo, and exits 2 on the first counter that reads 0, misreads, throws, or ships no probe.
    A counter that has never been seen read a planted finding has not been shown to measure (D-029).
  - **`--exclusive`** — a machine-wide lock at `~/.cache/geonosis/heavy.lock` before anything heavy,
    with a bounded printed wait (`--exclusive-timeout`, default 1800s), stale-holder takeover, and
    release on SIGINT/SIGTERM.
  - **`typecheckErrors` refuses on an unbuilt workspace** — TS2305/TS2307 naming a package of this
    workspace is a missing build, not debt, and a number there would be wrong (D-030).
  
  The plugin and `@geonosis/lint-parity` carry no change in this release; they move because the three
  packages are a fixed version group.

## 0.2.0

### Minor Changes

- Loose coupling, first slice — and a posture change that will break a config on purpose.
  
  **`@geonosis/lint-parity` is a new package.** Findings parity, rule reach, the corpus-manifest
  contract and the `geonosis-lint-parity` bin now live on their own. The tool knows no rule name and
  no plugin: two oxlint configs and a tree are the whole input, and a corpus arrives as a directory
  whose own `manifest.json` says which rules it speaks for. A parity tool welded to one rule plugin
  can only ever check that plugin.
  
  **BREAKING (plugin): the `geonosis-lint-parity` bin and the `./parity` export are gone.** Install
  `@geonosis/lint-parity` and pass `--corpus node_modules/@geonosis/oxlint-plugin-biological-architecture/corpus`
  — the plugin still ships the corpus, which is the part that is genuinely its own. `--corpus` now
  requires its directory.
  
  **BREAKING (plugin): a rule enabled without the options it needs refuses the run.** `no-brand-names`,
  `no-raw-html-atoms`, `documents-share-one-table`, `document-sagas-are-generic`,
  `step-opens-its-own-cell` and `layer-walls` throw from `create()` naming the missing option, instead
  of returning an empty visitor. A consumer that enabled one bare will fail lint until it configures
  the rule or turns it off — which is the point: one consumer ran `no-raw-html-atoms` at "error" for
  months with no element→atom map, over 136 raw elements, and the gate was green. An enabled rule that
  cannot fire is indistinguishable from compliance. A rule nobody enabled still does nothing.
  
  **New rule `layer-walls`, new preset `tooling`.** A repo declares its own layers as data —
  `{ name, paths, mayImport }` — and the rule enforces the direction between them: relative sources
  resolved against the importing file first, `import()`, `require()` and both re-export forms visited,
  externals not walls unless asked. The kit enables it on itself.
  
  The plugin also ships type declarations now, for all three entries.

## 0.1.2

### Patch Changes

- Four findings from the 0.1.1 code review, plus a version that disagreed with itself.
  
  **`--corpus` scopes its verdict to the rules its corpus can speak for.** It built the list of
  configured rules from every rule id in both configs, but the shipped corpus only exercises this
  plugin's. Any real consumer config also enables `eqeqeq`, `no-throw-literal` and
  `typescript/no-explicit-any`, so `--corpus` reported `FAIL — fired under neither` on every
  adoption. A corpus now declares its own scope in a `manifest.json` generated from the plugin's
  rules export; rules it does not name are listed under "outside this corpus" with no verdict, and
  neither the exit code nor `neither` considers them. `--corpus <dir>` points the run at another
  plugin's corpus. Matching is on full `plugin/rule` ids on both sides, so
  `typescript(no-unused-vars)` can never stand as evidence for somebody else's `no-unused-vars`.
  
  **Parity identifies a finding by its place and its rule, not its wording.** `path:line:col` plus
  `plugin(rule)`; message and severity are excluded. Diffing whole lines meant a rule whose message
  was copy-edited read as one finding lost and one gained — exit 1, a stopped adoption over a
  sentence. Those pair up in a new `messageChanged` list, reported under its own heading and written
  to `changed.txt`, and never reach the exit code. Multiset semantics are unchanged.
  
  **`oxlintRule` gets the cross-check the error and warning counters already had.** It counted
  `(rule)` anywhere in the output, so a format that printed rule ids differently read as 0 and the
  ratchet banked the "win" by rewriting the baseline down — and a quoted source line containing
  `(a-rule-name)` counted as a finding. The count now comes off recognised finding lines only, and a
  run with findings where none attributes itself readably is refused. The strict-config copy also
  upgrades the `["off", { … }]` array form, which is how every rule with options is parked
  mid-migration.
  
  **The warnings-only window is documented and closable.** oxlint exits 0 when a run found only
  warnings, so the counters' non-zero-exit backstop cannot catch an unrecognised format on such a
  run. The README states this plainly and says why `--deny-warnings` is not the fix. New optional
  counter param `expectFormat: "unix" | "agent" | "default"` refuses output in no known shape even on
  a clean exit, once npm's own chatter is stripped. Unset is the default and changes nothing.
  
  **The plugin reported `meta.version` 0.1.0 while shipping 0.1.1.** It is read from `package.json`
  now, and a test asserts the two agree.

## 0.1.1

### Patch Changes

- The defects the first real adoption exposed.
  
  - **The oxlint counters had never measured anything.** `oxlintErrors` and `oxlintWarnings` matched
    `/: error /`, which is the shape of oxlint's compact `agent` format alone, while every config asks
    for `--format=unix`. They now read all three shapes oxlint prints and cross-check the count
    against the tool's own summary, raising `CounterError` rather than returning a number they cannot
    corroborate — a blind counter reports 0 and lets the ratchet rewrite the baseline DOWN as a win.
  - **`cell-must-not-compose-cell` fired on the barrel `cells-folder-index-is-barrel` demands.**
    dielime's adoption reported 32 errors, every one a `cells/index.ts` re-exporting siblings, which
    is the shape the other rule requires at the same severity. The two rules now read one shared list
    of barrel filenames. A real cell importing or re-exporting another cell still fires.
  - **`geonosis-lint-parity`.** The adoption proof — same tree, two configs, a findings diff — as a
    bin instead of forty tool calls. Exits 1 when a finding the old config caught is missing under the
    new one. `--corpus` answers the prior question first: does each configured rule fire anywhere at
    all? The package now ships the reach corpus that makes that answerable, with a file for every one
    of its 54 rules.
  - **The ratchet shows what grew.** A regressed counter prints the last ten lines of its own command
    output under the `<-- REGRESSED` line, so the report answers the question it used to raise.
