# Rule Graduation Ledger

CLAUDE.md prose rules are graduated into machine checks (lint rules, ratchets,
boundary tests, fitness tests) so the doc converges back to architecture
description. Once a rule graduates, it is **deleted from CLAUDE.md** — the
check is the new source of truth, and this ledger preserves the mapping so the
history is not lost.

Graduation criteria (per #1528): a prose rule graduates only when a machine
check makes the mistake impossible-or-loud. Rules that encode judgment
("don't destroy old state before new state is confirmed") stay as prose.

| Rule # | Original rule text | Check location | Graduated in |
|--------|--------------------|----------------|--------------|
| 31 | **Core package files must never have host-specific prefixes** — generic modules in `@remnic/core` must use generic names (e.g. `recall-audit.ts`, not `openclaw-recall-audit.ts`); host adapters wrap core, not the other way around. | `scripts/check-review-patterns.sh` check #15 — `find packages/remnic-core/src -maxdepth 1 -type f \( -name "openclaw-*" -o -name "hermes-*" -o -name "codex-*" \)` is **BLOCKING** (`fail`, exits 1). Scoped to top-level generic modules; legitimate host adapters in `memory-extension/`, `connectors/`, `adapters/` are excluded. | #1638 |
| 26 | **Import via package name, not relative cross-package paths** — `import { X } from "@remnic/core"` not `import { X } from "../../../remnic-core/src/foo.js"`. Directory renames silently break relative imports with no package-dependency signal. | `scripts/check-review-patterns.sh` check #36 — `grep -rnE 'from .*\.\..*remnic-(core|cli)/src'` is **BLOCKING** (`fail`, exits 1). Zero current violations. Scoped to `.ts` files in `packages/`, excluding tests and node_modules. | #TBD |
| 27 | **Guard `slice(-n)` against `n === 0`** — `entries.slice(-0)` equals `slice(0)` and returns ALL entries. Always check `if (n <= 0)` before negating for slice. The `-0 === 0` footgun is a JavaScript-specific trap. | `scripts/check-review-patterns.sh` check #35 — `grep -rnE '\.slice\(-[a-zA-Z_]'` is **WARN-level** (19 existing call sites use computed offsets, many guarded by `Math.max(1,...)` above). New code should add a guard or justify safety. Scoped to `.ts` files in `packages/remnic-core/src/` and `packages/remnic-cli/src/`, excluding tests and comments. | #TBD |
| 36 | **String `"false"` is truthy in JavaScript** — `--config installExtension=false` produces `"false"` (string), which `!== false` evaluates as `true`. Coerce boolean-like strings (`"false"`, `"0"`, `"no"`, `"off"`) at config-read boundaries using a shared helper. | `scripts/check-review-patterns.sh` check #19 — `grep -rn '!== false\|!= false'` on config/option/flag values is **WARN-level** (147 existing sites; most are typed booleans already coerced by the config layer via `coerceBool()`/`coerceBooleanLike()`). New code should use the shared coercion helpers. Scoped to `.ts` files in `packages/`, excluding tests and comments. | #TBD |
| 46 | **Escape literal template parts before building regex** — when constructing regex from user-provided templates, always `escapeRegex()` on the prefix/suffix. Empty prefix+suffix produces a match-everything regex. Special `$` in replacement strings corrupts output — use a replacement function or escape `$` → `$$`. | `scripts/check-review-patterns.sh` check #26 — `grep -rn 'new RegExp(' \| grep -v 'escapeRegex\|escape(' \| grep -i 'template\|prefix\|suffix\|pattern\|format\|config'` is **WARN-level** (13 existing sites flagged). New code should escape user-derived parts with `escapeRegex()`. Scoped to `.ts` files in `packages/ src/`, excluding tests. | #TBD |
| 53 | **Status filters must enumerate ALL non-active states** — filtering only `superseded` and `archived` but not `quarantined`, `rejected`, or `pending_review` causes stale data in user-facing outputs. Define an explicit `ACTIVE_STATUSES` set rather than an ad-hoc exclusion list. When adding a new status, grep ALL filters. | `scripts/check-review-patterns.sh` check #37 — `grep -rnE '(status\|frontmatter\.status)\s*(===\|!==)\s*"(superseded\|archived\|...)"' \| awk '$1 >= 2'` (files with 2+ such comparisons) is **WARN-level**. The shared `ACTIVE_STATUSES` set is exported from `contradiction-scan.ts`. New code should use the set instead of ad-hoc lists. Scoped to `.ts` files in `packages/remnic-core/src/` and `packages/remnic-cli/src/`, excluding tests. | #TBD |
| 39 | **Feature gates must be identical across all code paths** — if `temporalSupersessionEnabled` gates the QMD path but not the recent-scan fallback path, behavioral divergence depends on which recall path is exercised. Enumerate every path when adding a feature gate. | `packages/remnic-core/src/capabilities.test.ts` — characterization tests (#1523) verify every `CapabilitySet` field projects from its `<field>Enabled` config flag (true/false variants). Prove-fail: hardcoding a capability to `true` instead of reading from config causes the false-variant test to fail. | #TBD |
| 42 | **Read and write paths must resolve through the same namespace layer** — if search uses namespace-aware resolution, get/delete must too. Un-namespaced search in multi-principal deployments exposes cross-tenant data. Constrain search scope via session-derived namespace resolution. | `packages/remnic-core/src/scopes/scope-plan.test.ts` + `access-service-observe-scope.test.ts` + `access-service-observe-lcm-parity.test.ts` — ScopePlan resolver parity suite (#1521) snapshots namespace sets for read/write/observe paths. Prove-fail: breaking `resolveScopePlan` to return `["default"]` causes 9/10 tests to fail. | #TBD |
| 47 | **Shared mutable objects must not leak across connections/sessions** — a single mutable `clientInfo` shared across MCP connections lets one session adapter metadata bleed into another. Use per-connection instances or deep-copy. | `scripts/check-review-patterns.sh` check #27 — `grep -rn 'this.(clientInfo|sessionInfo|connectionInfo|adapterInfo)s*='` is **WARN-level**. Zero current violations. Scoped to `.ts` files in `packages/` and `src/`, excluding tests. | #TBD |
| 48 | **Enum defaults must be least-privileged** — defaulting to `"approved"` or `"enabled"` when a decision/status enum is missing is a security vulnerability. Always default to `"rejected"`, `"pending"`, `"disabled"`, or `"none"`. | `scripts/check-review-patterns.sh` check #28 — `grep -rn '|| "approved"|?? "approved"||| "enabled"|?? "enabled"||| "active"|?? "active"'` is **WARN-level** (35 existing sites). New code should default to least-privileged values. | #TBD |
| 54 | **Never delete before write in file replace operations** — `rmSync(target)` then `renameSync(tmp, target)` loses data permanently if rename fails. Write to temp first, then rename atomically. | `scripts/check-review-patterns.sh` check #33 — greps for `rmSync|unlinkSync|removeSync` followed by `renameSync` within 5 lines is **WARN-level** (5 current sites flagged). New code should write-to-temp-then-rename atomically. | #TBD |
| 55 | **Documented behavior must have a corresponding implementation and test** — if docs say "timeout is applied to all daemon calls", the provider must forward the timeout parameter AND a test must verify it. CI publish workflows must validate `github.ref == 'refs/heads/main'` on the job. | `scripts/check-docs-parity.mjs` (#1527) + `scripts/check-review-patterns.sh` check #34. Docs-parity verifies every `remnic <cmd>` in fenced doc blocks resolves to a registered CLI command; check #34 verifies CI publish workflows have `github.ref` branch protection. Prove-fail: adding a nonexistent command in a fenced block causes "drift detected". | #TBD |
| 17 | **Expand `~` in all user-facing path inputs** — Node.js `fs` does NOT expand `~`. Use `expandTilde()` consistently, never ad-hoc regex. Applies to `memoryDir`, `--config`, env var paths, and `--memory-dir`. | `scripts/check-review-patterns.sh` check #7 — `grep -rn '\\.replace(/^~/'` outside `expandTilde` is **WARN-level**. Prove-fail: a seeded `p.replace(/^~/, ...)` is flagged. | #TBD |
| 18 | **Validate JSON parse result type** — `JSON.parse('null')` succeeds but `null` is not a valid config. Always check `typeof result === 'object' && result !== null` after parsing before property access. | `scripts/check-review-patterns.sh` check #9 — `JSON.parse(` in config/settings/install/doctor paths lacking a `typeof`/`null`/`isPlainObject`/`isValid` check within 5 lines is **WARN-level** (2 real sites). Proven: fires on a config-path parse without a type guard. | #TBD |
| 32 | **Line parsers must track position during iteration, not use indexOf** — `content.indexOf(line)` returns the first occurrence, not the current parsing position. Maintain a running offset variable. | `scripts/check-review-patterns.sh` check #16 — `.indexOf(` in parser/position/offset/line/parse/block code in `packages/remnic-core/src/` is **WARN-level** (33 sites). | #TBD |
| 35 | **Time-range filters must use exclusive upper bounds** — `ts <= toMs` causes double-counting at midnight boundaries. Use `ts < toMs` for half-open `[start, end)` intervals. | `scripts/check-review-patterns.sh` check #18 — `<= …Time/ts/ms/date/now/stamp` in filter/window/range/briefing/recall code is **WARN-level** (12 sites). | #TBD |
| 38 | **Sort object keys before hashing/serializing** — `Object.entries({city, country})` vs `Object.entries({country, city})` produce different strings, breaking deduplication. Sort keys before serializing for any hash/content-dedup operation. | `scripts/check-review-patterns.sh` check #20 — `Object.entries(` in hash/dedup/serial/finger code is **WARN-level** (2 sites). | #TBD |
| 41 | **Match loop iterator method to the data you need** — `for (const v of map.values())` when you also need the key references an undefined/outer-scope variable. Use `.entries()` to destructure both key and value. | `scripts/check-review-patterns.sh` check #23 — `for … .values()` loops are flagged when a key/id is referenced in the body; **WARN-level** (1 site). | #TBD |
| 45 | **Config schema minimums must honor documented disable values** — if docs say "set to 0 to disable", both the JSON schema `minimum` AND the code path must accept 0. `Math.max(1, value)` with `minimum: 1` silently overrides the documented disable intent. PR #399. | `scripts/check-review-patterns.sh` check #25 — `"minimum": 1` in plugin JSON schema is **WARN-level** (3 sites). | #TBD |
| 50 | **CI must never silence test/type failures** — `|| true` on `pytest`, `mypy`, `tsc`, or equivalent in CI makes broken code pass. Each quality gate must be a separate CI step that fails the build. PR #349. | `scripts/check-review-patterns.sh` check #29 — `.github/workflows/*` steps with `|| true` or `continue-on-error: true` on test/lint/type/verify steps are **WARN-level**. Prove-fail: a seeded `run: echo "test" || true` is flagged. | #TBD |
| 51 | **Reject invalid user input instead of silently defaulting** — invalid `--format`, `--since`, `--focus`, MCP parameters, or briefing window tokens must throw errors listing valid options. Applies to ALL input surfaces: CLI, MCP tools, API endpoints, config parsing. PR #396. | **Enforcing mechanism:** `#1525` validation-boundary operation registry (all MCP/HTTP/CLI ops validate input and throw) **+** `scripts/check-review-patterns.sh` check #30 — `|| config.*`/`?? .default` silent-default patterns on format/since/focus/window/briefing values are **WARN-level** (1 site). | #TBD |
| 52 | **Validation allow-lists must exactly match handled values** — if `BRIEFING_FORMAT_ALLOWED` includes `"text"` but downstream only handles `"markdown"`/`"json"`, the validator accepts what code can't process. Dead switch cases after name normalization must be removed. PR #396. | `scripts/check-review-patterns.sh` check #31 — `_ALLOWED`/`VALID_FORMATS`/`VALID_` arrays are compared against the count of `case`/`if`/`else` branch handlers in the file; **WARN-level**. Prove-fail: a seeded `BRIEFING_FORMAT_ALLOWED = ["text","markdown","json"]` with a single `case` is flagged. | #TBD |
| 10 | **Never interpolate unsanitized values into shell scripts** — pass host/port/config values via environment variables, never via string interpolation. | `scripts/check-review-patterns.sh` check #2 — `\${` interpolation in exec/spawn/shell/command/script code in connectors + CLI is **WARN-level**. Scoped to `.ts`/`.js` in `packages/remnic-core/src/connectors/` and `packages/remnic-cli/src/`. | #TBD |
| 15 | **Sync lock files after dependency changes** — changing `workspace:*` specifiers or adding/removing packages requires `pnpm install` to update `pnpm-lock.yaml`. | `scripts/check-review-patterns.sh` check #5 — rejects `package-lock.json` in the pnpm workspace and runs `pnpm install --frozen-lockfile` consistency check. **BLOCKING** on lockfile drift. | #TBD |
| 16 | **Clean up ALL test globals in teardown** — include unkeyed globals like `__openclawEngramOrchestrator` in `resetGlobals()` helpers. | `scripts/check-review-patterns.sh` check #6 — test files referencing `Orchestrator` without a `resetGlobals`/`afterEach` cleanup are **WARN-level** (2 sites). | #TBD |
| 19 | **Sort comparators must return 0 for equal items** — a comparator returning `1` for both directions violates the contract and produces non-deterministic ordering. | `scripts/check-review-patterns.sh` check #8 — `return 1` near `sort` in `.ts` files is **WARN-level**. Scoped to non-test code. | #TBD |
| 22 | **Config resolution must be deduplicated** — the slot → PLUGIN_ID → LEGACY_PLUGIN_ID resolution must use the shared utility, not be reimplemented. | `scripts/check-review-patterns.sh` check #10 — `slots.memory`/`LEGACY_PLUGIN_ID`/`resolveRemnicPluginEntry` usage outside import/export lines across >3 files is **WARN-level**. | #TBD |
| 28 | **Coerce CLI values to expected types at input boundaries** — `--config port=5555` produces `"5555"`; `typeof saved === "number"` rejects it on reinstall. | `scripts/check-review-patterns.sh` check #13 — `typeof ... === "number"` on port/config/prev/saved/stored/options values in `packages/` is **WARN-level**. | #TBD |
| 29 | **Force-flush must bypass dedupe** — explicit flush surfaces must pass `skipDedupeCheck: true`. | `scripts/check-review-patterns.sh` check #14 — `flushSession`/`forceFlush`/`queueBufferedExtraction` calls are **WARN-level**. | #TBD |
| 30 | **New filters/transforms must have configuration gates** — every new recall filter, config transformation, or behavioral override needs an `enabled` check. | `packages/remnic-core/src/capabilities.test.ts` (#1523) — characterization tests verify every `CapabilitySet` field projects from its `<field>Enabled` config flag. Prove-fail: hardcoding a capability to `true` causes the false-variant test to fail. | #TBD |
| 33 | **Test mock function signatures must match production interfaces** — mismatched mocks make tests pass vacuously. | `scripts/check-review-patterns.sh` check #17 — `fn(() => {` zero-argument mocks in `.test.ts` files are **WARN-level**. | #TBD |
| 37 | **Cache invalidation must clear ALL cache layers** — if `invalidateAllMemoriesCache()` only clears the hot cache, stale data persists. | `scripts/check-review-patterns.sh` check #21 (fixed batch 5: BRE `\|` → ERE `|`, `grep -c || echo` double-output bug fixed) — files with `invalidateAll`/`clearAll`/`resetAll`/`flushAll` but >5 cache refs and <2 invalidate calls are **WARN-level**. Prove-fail: seeded file with 7 cache refs and 1 invalidateAll fires. | #TBD |
| 40 | **Serialized promise chains must recover from rejection** — `writeChain = writeChain.then(fn)` without `.catch()` permanently poisons the chain. | `scripts/check-review-patterns.sh` check #22 (fixed batch 5: narrow `chain\s*=` pattern → broad `\`b\`w+\`s*=\`s*\`w+\.then(` catching actual self-referential promise chains; `serialize-mutations` excluded as the sanctioned chokepoint) — **WARN-level** (24 real sites flagged). | #TBD |
| 44 | **Don't index content that failed to persist** — phantom `factHashIndex` entries cause future extractions to be silently dedup-suppressed against non-existent stored facts. PR #399. | `scripts/check-review-patterns.sh` check #24 (fixed batch 5: stale `contentHashIndex\` → current `factHashIndex`, BRE → ERE) — `factHashIndex.add()`/`addByHash()` calls in `packages/remnic-core/src/` are **WARN-level** (6 real sites flagged). | #TBD |
| 56 | **Never merge before AI reviewers post** — `cursor[bot]` and `chatgpt-codex-connector[bot]` take 2-5 min; merging immediately races past them. | `scripts/pre-merge-check.sh <PR#>` — **BLOCKING**: verifies (1) both AI reviewers have posted, (2) zero unresolved threads remain. Enforced as a mandatory pre-merge gate. | #TBD |
| 9 | **Legacy env var fallback chains** — always try `REMNIC_*` first, then fall back to `ENGRAM_*`. Applies to config parsing, hook scripts, and daemon label lookups. | `scripts/check-review-patterns.sh` check #40 (NEW) — flags `process.env.ENGRAM_*` reads lacking a `REMNIC_*` partner within 2 lines (WARN; 0 current violations). Prove-fail: a seeded standalone `process.env.ENGRAM_PROBE` is flagged. | #TBD |
| 14 | **Validate CLI flag arguments exist** — `--format`, `--focus`, `--since` without a following value must throw an error, not silently default. | **#1525 validation-boundary operation registry** — all MCP/HTTP/CLI ops validate input and throw — **+** `check-review-patterns.sh` check #30 (silent-default WARN). Same mechanism rule 51 graduated on. | #TBD |
| 24 | **Reject file paths used as directory arguments** — `existsSync` returns true for files; use `statSync().isDirectory()` when a directory is expected. | `scripts/check-review-patterns.sh` check #38 (NEW) — flags `existsSync()` on `*Dir`/`*Folder`/`memoryDir` variables without an `isDirectory()` guard (WARN; 37 existing sites). | #TBD |
| 57 | **À-la-carte packages must stay optional at every install layer** — `@remnic/bench`/`@remnic/export-weclone`/`@remnic/import-weclone` and ALL other optional peers must load via computed-specifier dynamic imports and never appear in a base package's runtime `dependencies` or tsup `noExternal`. | `scripts/check-review-patterns.sh` check #39 (NEW) — **BLOCKING, peerDependenciesMeta-driven**: every package a base declares optional in `peerDependenciesMeta` must NOT appear in that base's `dependencies` or tsup `noExternal` (covers bench + all `@remnic/import-*` + connectors + coding-graph + plugins automatically; the `external` array is correct and exempt). Prove-fail: seeding `@remnic/import-chatgpt` into `remnic-cli` dependencies fails the build. | #TBD |
