From 523f1019b82fae9be869057dd0e6c5fb9846c545 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 16:11:32 +0000 Subject: [PATCH] =?UTF-8?q?feat(memory):=20make=20the=20ledger=20the=20def?= =?UTF-8?q?ault=20store=20=E2=80=94=20finish=20the=20M2=20migration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flips FORGE_LEDGER_ONLY on by default: the legacy files (.forge/lessons/*.md, recall/brain fact files) are no longer written, and every read materializes from the PCM ledger. The ledger has been the convergent dual-write store since P1, so existing memory is already present; `forge ledger import` back-fills pre-ledger history. FORGE_LEDGER_ONLY=0 is a one-release escape hatch. Closed the ledger-only read/write gaps the flip exposed (the real reason it couldn't ship before): - brain.remember (and the forge_remember MCP tool) now shadow the fact into the repo ledger, so a fact is not lost when no file is written. - cortex_features.featuresForEdit and cortex_hook_main's distill lesson lookup now read the merged ledger view instead of the empty legacy file store. (cortex.js's injection/summary/distillation read paths were already ledger-aware.) Tests: the legacy-store suites are pinned to the FORGE_LEDGER_ONLY=0 escape hatch (the file path they exercise); ledger_only.test.js gains a default-mode end-to-end test of the full create->confirm->promote learning loop plus explicit default-vs-escape-hatch assertions. docs/GUIDE.md updated for the new default. Verified: full suite 1072 pass / 0 fail / 2 skip, biome 0 errors, tsc clean, `docs check` green, and a live `forge remember` writes no legacy file and persists to the ledger. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01C7htTiesXKLvAUtv6ET2jz --- CHANGELOG.md | 16 +++++++++ docs/GUIDE.md | 15 +++++---- src/brain.js | 15 +++++++-- src/cortex_features.js | 6 ++-- src/cortex_hook_main.js | 4 +-- src/util.js | 12 +++---- test/cli_cortex.test.js | 4 +++ test/cortex.test.js | 4 +++ test/cortex_distill.test.js | 4 +++ test/cortex_emit.test.js | 4 +++ test/cortex_hook.test.js | 4 +++ test/cortex_hook_main.test.js | 4 +++ test/cortex_mcp.test.js | 4 +++ test/cortex_preedit.test.js | 4 +++ test/doctor.test.js | 4 +++ test/knowledge_router.test.js | 4 +++ test/ledger_bridge.test.js | 4 +++ test/ledger_only.test.js | 63 +++++++++++++++++++++++++++++++---- test/ledger_read.test.js | 4 +++ test/lessons_store.test.js | 4 +++ test/recall.test.js | 4 +++ 21 files changed, 161 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c374606..df93aeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed + +- **The PCM ledger is now the DEFAULT memory store — the legacy-store migration is + finished (M2).** `FORGE_LEDGER_ONLY` defaults on: `.forge/lessons/*.md` and recall/brain + fact files are no longer written, and every read (cortex injection/summary, routing, + `recall`/`brain`, the pre-edit advisory, doctor) materializes from the ledger. The ledger + has been the convergent dual-write store since P1, so existing memory is already there; + `forge ledger import` back-fills any pre-ledger history. `FORGE_LEDGER_ONLY=0` is a + one-release escape hatch that restores the legacy file store. The legacy-store test + suites are pinned to the escape hatch; a new end-to-end test exercises the full + create→confirm→promote learning loop under the default. + ### Fixed - **`reconcileFacts` no longer risks wiping memory under `FORGE_LEDGER_ONLY` (M2).** The @@ -13,6 +25,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ledger-only there are no fact files, so it would have tombstoned every fact. It is now a guarded no-op when ledger-only is active (the ledger IS the store then — there is nothing to reconcile against). Covered by a new no-data-loss regression test. +- **Closed the ledger-only read/write gaps the default flip exposed (M2).** `brain.remember` + (and the `forge_remember` MCP tool) now shadow the fact into the ledger, so a fact is no + longer lost when no file is written; `cortex_features.featuresForEdit` and the distill + loop's lesson lookup now read the merged ledger view instead of the legacy file store. ## [0.26.1] - 2026-07-20 diff --git a/docs/GUIDE.md b/docs/GUIDE.md index e08021b..29acc25 100644 --- a/docs/GUIDE.md +++ b/docs/GUIDE.md @@ -746,12 +746,13 @@ the recall store shadows into, so recall facts become portable across machines. **Retiring the legacy stores.** Since P1 the ledger has been the convergent WRITE store (every lesson/fact dual-writes into it) and reads are the merged view (legacy ∪ ledger). -Set **`FORGE_LEDGER_ONLY=1`** to finish the job: the legacy files (`.forge/lessons/*.md`, -recall/brain fact files) stop being written and every read — cortex injection, routing, -`recall`/`brain` — comes from the ledger alone. Run `forge ledger import` first (idempotent) -so nothing local is stranded; the ledger is content-addressed and merges conflict-free, so -it is a complete standalone store. Default off keeps the legacy files as the canonical -local copy. +The ledger is now the **default and only store**: the legacy files (`.forge/lessons/*.md`, +recall/brain fact files) are no longer written, and every read — cortex injection, routing, +`recall`/`brain` — comes from the ledger alone (materialized from its content-addressed, +conflict-free claims). Upgrading from an older version? Run `forge ledger import` once +(idempotent) so any pre-ledger local history lands in the ledger. Set **`FORGE_LEDGER_ONLY=0`** +(the one-release escape hatch) to temporarily restore the legacy file store while external +tooling migrates. ### `forge reuse` — proof-carrying code cache @@ -1302,7 +1303,7 @@ code reads but this table misses fails CI on the forge repo): | `FORGE_LLM_HTTP` | `1` forces direct HTTP (Anthropic Messages or OpenAI-compatible, per the resolved provider) instead of the `claude` CLI; automatic when the CLI is absent | | `FORGE_ENFORCE` | `1` turns the substrate advisory into a hard block on the strongest signals | | `FORGE_AUTOSYNC` | `0` disables the Stop-hook AGENTS.md auto-repair | -| `FORGE_LEDGER_ONLY` | `1` retires the legacy stores — stop writing `lessons/*.md` + recall/brain fact files; the ledger is the only store (reads materialize from it) | +| `FORGE_LEDGER_ONLY` | Ledger-only is the DEFAULT (the ledger is the sole store). `0` is the escape hatch — restores the legacy `lessons/*.md` + recall/brain file store while external tooling migrates | | `FORGE_EMBED` / `FORGE_EMBED_MODEL` / `FORGE_EMBED_TIMEOUT_MS` | optional embeddings tier (ADR-0005) | | `FORGE_HOME` | override `~/.forge` (recall store location) | | `FORGE_ROOT` | repo root override for the MCP server | diff --git a/src/brain.js b/src/brain.js index 6887cf6..a487b98 100644 --- a/src/brain.js +++ b/src/brain.js @@ -6,6 +6,7 @@ // silently truncated the way Claude's native 200-line MEMORY.md is (#39811). import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; +import { shadowFact } from "./ledger_bridge.js"; import { ledgerFacts, mergeFactSlugs } from "./ledger_read.js"; import { listStored, add as recallAdd } from "./recall.js"; @@ -15,10 +16,20 @@ export const brainStore = (targetRoot = process.cwd()) => join(targetRoot, ".for // see `forge remember`), so that is where merged teammate facts arrive. const brainLedger = (store) => join(dirname(store), "ledger"); -/** Store one fact (secret-refused by recall) and rebuild the inlined index. */ +/** Store one fact (secret-refused by recall) and rebuild the inlined index. The fact is + * ALSO shadowed into the repo ledger, so it persists as a claim — required under + * FORGE_LEDGER_ONLY (no file is written, so the ledger is the only home) and harmless + * otherwise (shadowFact is idempotent/content-addressed). Best-effort: a bridge failure + * must never break a remember that already stored the fact. Shadow BEFORE buildIndex so + * the index picks the ledger fact up under ledger-only. */ export function remember(store, name, body) { const res = recallAdd(store, name, body); - if (res.ok) buildIndex(store); + if (res.ok) { + try { + shadowFact(brainLedger(store), name, body); + } catch {} + buildIndex(store); + } return res; } diff --git a/src/cortex_features.js b/src/cortex_features.js index 85c0faa..70f7f0b 100644 --- a/src/cortex_features.js +++ b/src/cortex_features.js @@ -4,8 +4,8 @@ // zero-dep grep gives a rough fan-out today; adopting a graph MCP (agent-lsp/Serena) drops // a precise call graph straight in without touching the predictor. import { execFileSync } from "node:child_process"; +import { mergedLessons } from "./ledger_read.js"; import { confidenceOf, matchScore } from "./lessons.js"; -import { load } from "./lessons_store.js"; /** * Pure: edit + gathered signals → the predictor feature vector (all features in [0,1]). @@ -68,7 +68,9 @@ export function grepFanout(root, symbol) { /** Build the feature vector for a real edit from actual repo state (best-effort, degrades). */ export function featuresForEdit(root, edit, { nowDay = 0 } = {}) { - const activeLessons = load(root).filter((l) => l.status === "active"); + // Ledger-aware read: the merged view (legacy ∪ ledger) so this works under + // FORGE_LEDGER_ONLY (no legacy files) and also sees merged teammate lessons. + const activeLessons = mergedLessons(root, nowDay).filter((l) => l.status === "active"); const callerCount = grepFanout(root, edit.symbol); return computeFeatures(edit, { activeLessons, diff --git a/src/cortex_hook_main.js b/src/cortex_hook_main.js index 92fc3f8..3acbb76 100644 --- a/src/cortex_hook_main.js +++ b/src/cortex_hook_main.js @@ -19,7 +19,7 @@ import { processSession, readSession, } from "./cortex_hook.js"; -import { load } from "./lessons_store.js"; +import { mergedLessons } from "./ledger_read.js"; import { enforceDecision, substrateCheck, substrateContext } from "./substrate.js"; import { epochDay } from "./util.js"; @@ -31,7 +31,7 @@ async function enrichCreated(root, results) { if (!created.length) return; const { distill } = await import("./cortex_distill.js"); for (const r of created) { - const lesson = load(root).find((l) => l.id === r.id); + const lesson = mergedLessons(root, epochDay()).find((l) => l.id === r.id); if (!lesson) continue; const better = distill({ context: lesson.trigger, diff --git a/src/util.js b/src/util.js index c89ca5e..504aaf0 100644 --- a/src/util.js +++ b/src/util.js @@ -24,13 +24,13 @@ export const toPosix = (p) => String(p).replaceAll("\\", "/"); export const MS_PER_DAY = 86400000; export const epochDay = () => Math.floor(Date.now() / MS_PER_DAY); -// Legacy-store retirement (ROADMAP): the PCM ledger is already the convergent WRITE -// store (dual-write via ledger_bridge) and serves a merged read (ledger_read). With -// FORGE_LEDGER_ONLY=1 the legacy files (lessons/*.md, recall/brain fact files) stop -// being written and reads come from the ledger alone — the ledger becomes the only -// store. Default off: the legacy files remain the canonical local copy. +// Legacy-store retirement (ROADMAP): the PCM ledger is the convergent WRITE store +// (dual-write via ledger_bridge since P1) and serves the read view (ledger_read). The +// ledger is now the DEFAULT and only store — legacy files (lessons/*.md, recall/brain +// fact files) are no longer written or read. Set FORGE_LEDGER_ONLY=0 (the one-release +// escape hatch) to restore the legacy file store while external tooling migrates. export const ledgerOnly = () => - process.env.FORGE_LEDGER_ONLY === "1" || process.env.FORGE_LEDGER_ONLY === "true"; + process.env.FORGE_LEDGER_ONLY !== "0" && process.env.FORGE_LEDGER_ONLY !== "false"; export function hasBin(bin) { try { diff --git a/test/cli_cortex.test.js b/test/cli_cortex.test.js index f262285..ac99354 100644 --- a/test/cli_cortex.test.js +++ b/test/cli_cortex.test.js @@ -7,6 +7,10 @@ import { test } from "node:test"; import { fileURLToPath } from "node:url"; import { recordMistake } from "../src/cortex.js"; +// Default is now ledger-only; these cases exercise the legacy FILE store (the +// FORGE_LEDGER_ONLY=0 escape hatch). Pin it here so they test that path directly. +process.env.FORGE_LEDGER_ONLY = "0"; + const CLI = fileURLToPath(new URL("../src/cli.js", import.meta.url)); const runCli = (args, cwd) => spawnSync("node", [CLI, ...args], { cwd, encoding: "utf8" }); diff --git a/test/cortex.test.js b/test/cortex.test.js index becc136..ac7ff53 100644 --- a/test/cortex.test.js +++ b/test/cortex.test.js @@ -6,6 +6,10 @@ import { test } from "node:test"; import { lessonsForContext, recordContradiction, recordMistake, summary } from "../src/cortex.js"; import { load } from "../src/lessons_store.js"; +// Default is now ledger-only; these cases exercise the legacy FILE store (the +// FORGE_LEDGER_ONLY=0 escape hatch). Pin it here so they test that path directly. +process.env.FORGE_LEDGER_ONLY = "0"; + const fixture = () => mkdtempSync(join(tmpdir(), "forge-cortex-")); const ctx = { symbols: ["validateToken"], diff --git a/test/cortex_distill.test.js b/test/cortex_distill.test.js index c0c891d..5770e0c 100644 --- a/test/cortex_distill.test.js +++ b/test/cortex_distill.test.js @@ -9,6 +9,10 @@ import { processSession } from "../src/cortex_hook.js"; import { load } from "../src/lessons_store.js"; import { fakeAnthropic } from "./_fixtures.js"; +// Default is now ledger-only; these cases exercise the legacy FILE store (the +// FORGE_LEDGER_ONLY=0 escape hatch). Pin it here so they test that path directly. +process.env.FORGE_LEDGER_ONLY = "0"; + test("buildPrompt names the location and asks for strict JSON", () => { const p = buildPrompt({ context: { symbols: ["computeTax"] }, diff --git a/test/cortex_emit.test.js b/test/cortex_emit.test.js index 230696f..f569c0f 100644 --- a/test/cortex_emit.test.js +++ b/test/cortex_emit.test.js @@ -6,6 +6,10 @@ import { test } from "node:test"; import { processSession } from "../src/cortex_hook.js"; import { canonical } from "../src/sync.js"; +// Default is now ledger-only; these cases exercise the legacy FILE store (the +// FORGE_LEDGER_ONLY=0 escape hatch). Pin it here so they test that path directly. +process.env.FORGE_LEDGER_ONLY = "0"; + const fixture = () => mkdtempSync(join(tmpdir(), "forge-emit-")); const brokenSession = () => [ { type: "bash", command: "npm test", exitCode: 1 }, diff --git a/test/cortex_hook.test.js b/test/cortex_hook.test.js index 7c59064..a67e805 100644 --- a/test/cortex_hook.test.js +++ b/test/cortex_hook.test.js @@ -12,6 +12,10 @@ import { } from "../src/cortex_hook.js"; import { load } from "../src/lessons_store.js"; +// Default is now ledger-only; these cases exercise the legacy FILE store (the +// FORGE_LEDGER_ONLY=0 escape hatch). Pin it here so they test that path directly. +process.env.FORGE_LEDGER_ONLY = "0"; + const fixture = () => mkdtempSync(join(tmpdir(), "forge-hook-")); test("classifyEvent normalizes edits, bash, and prompts", () => { diff --git a/test/cortex_hook_main.test.js b/test/cortex_hook_main.test.js index 1824e32..636050e 100644 --- a/test/cortex_hook_main.test.js +++ b/test/cortex_hook_main.test.js @@ -7,6 +7,10 @@ import { test } from "node:test"; import { fileURLToPath } from "node:url"; import { load } from "../src/lessons_store.js"; +// Default is now ledger-only; these cases exercise the legacy FILE store (the +// FORGE_LEDGER_ONLY=0 escape hatch). Pin it here so they test that path directly. +process.env.FORGE_LEDGER_ONLY = "0"; + const ENTRY = fileURLToPath(new URL("../src/cortex_hook_main.js", import.meta.url)); const feed = (mode, payload) => spawnSync("node", [ENTRY, mode], { diff --git a/test/cortex_mcp.test.js b/test/cortex_mcp.test.js index f68f9cf..bbf5974 100644 --- a/test/cortex_mcp.test.js +++ b/test/cortex_mcp.test.js @@ -8,6 +8,10 @@ import { fileURLToPath } from "node:url"; import { processSession } from "../src/cortex_hook.js"; import { handle } from "../src/cortex_mcp.js"; +// Default is now ledger-only; these cases exercise the legacy FILE store (the +// FORGE_LEDGER_ONLY=0 escape hatch). Pin it here so they test that path directly. +process.env.FORGE_LEDGER_ONLY = "0"; + test("handle: initialize advertises the forge-cortex server", async () => { const r = await handle({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} }); assert.equal(r.result.serverInfo.name, "forge-cortex"); diff --git a/test/cortex_preedit.test.js b/test/cortex_preedit.test.js index 40603d5..e5f85cc 100644 --- a/test/cortex_preedit.test.js +++ b/test/cortex_preedit.test.js @@ -7,6 +7,10 @@ import { test } from "node:test"; import { fileURLToPath } from "node:url"; import { processSession } from "../src/cortex_hook.js"; +// Default is now ledger-only; these cases exercise the legacy FILE store (the +// FORGE_LEDGER_ONLY=0 escape hatch). Pin it here so they test that path directly. +process.env.FORGE_LEDGER_ONLY = "0"; + const ENTRY = fileURLToPath(new URL("../src/cortex_hook_main.js", import.meta.url)); const preEdit = (root, file) => spawnSync("node", [ENTRY, "pre-edit"], { diff --git a/test/doctor.test.js b/test/doctor.test.js index 38bedf1..11a860c 100644 --- a/test/doctor.test.js +++ b/test/doctor.test.js @@ -15,6 +15,10 @@ import { processSession } from "../src/cortex_hook.js"; import { doctor, repairFailure } from "../src/doctor.js"; import { mergeSettings } from "../src/init.js"; +// Default is now ledger-only; these cases exercise the legacy FILE store (the +// FORGE_LEDGER_ONLY=0 escape hatch). Pin it here so they test that path directly. +process.env.FORGE_LEDGER_ONLY = "0"; + const fixture = () => mkdtempSync(join(tmpdir(), "forge-doctor-")); test("doctor reports subsystem health in the standard vocabulary (P1-06)", () => { diff --git a/test/knowledge_router.test.js b/test/knowledge_router.test.js index 162a5ad..8079b75 100644 --- a/test/knowledge_router.test.js +++ b/test/knowledge_router.test.js @@ -15,6 +15,10 @@ import { } from "../src/knowledge_router.js"; import { loadClaims, repoLedger } from "../src/ledger_store.js"; +// Default is now ledger-only; these cases exercise the legacy FILE store (the +// FORGE_LEDGER_ONLY=0 escape hatch). Pin it here so they test that path directly. +process.env.FORGE_LEDGER_ONLY = "0"; + const CLI = fileURLToPath(new URL("../src/cli.js", import.meta.url)); test("routeFact: every home recognized from unseen phrasings (per-family routing)", () => { diff --git a/test/ledger_bridge.test.js b/test/ledger_bridge.test.js index b188ec1..eedf876 100644 --- a/test/ledger_bridge.test.js +++ b/test/ledger_bridge.test.js @@ -18,6 +18,10 @@ import { newLesson } from "../src/lessons.js"; import { save } from "../src/lessons_store.js"; import { readFact, add as recallAdd } from "../src/recall.js"; +// Default is now ledger-only; these cases exercise the legacy FILE store (the +// FORGE_LEDGER_ONLY=0 escape hatch). Pin it here so they test that path directly. +process.env.FORGE_LEDGER_ONLY = "0"; + const tmp = () => mkdtempSync(join(tmpdir(), "forge-bridge-")); const ctx = { symbols: ["computeTax"], files: ["src/tax.ts"], keywords: [] }; const strongSignals = [{ signal: "S1" }, { signal: "S6" }]; diff --git a/test/ledger_only.test.js b/test/ledger_only.test.js index 3f83932..3659d11 100644 --- a/test/ledger_only.test.js +++ b/test/ledger_only.test.js @@ -1,12 +1,14 @@ -// Legacy-store retirement (FORGE_LEDGER_ONLY): with the switch on, the legacy files -// (lessons/*.md, recall/brain fact files) are not written and reads come from the -// ledger alone. Default off keeps the legacy files canonical (covered by every other -// suite). These tests exercise the on path end-to-end at the store layer. +// Legacy-store retirement (FORGE_LEDGER_ONLY): ledger-only is now the DEFAULT — the +// legacy files (lessons/*.md, recall/brain fact files) are not written and reads come +// from the ledger alone. FORGE_LEDGER_ONLY=0 is the escape hatch that restores the file +// store (covered by the legacy-pinned suites). These tests exercise the default path +// end-to-end, from the store layer up through the cortex learning loop. import assert from "node:assert/strict"; import { existsSync, mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { test } from "node:test"; +import { recordMistake, summary } from "../src/cortex.js"; import { reconcileFacts, recordLessonEvent, shadowFact } from "../src/ledger_bridge.js"; import { ledgerFacts, ledgerLessons, mergedLessons } from "../src/ledger_read.js"; import { newLesson } from "../src/lessons.js"; @@ -71,13 +73,27 @@ test("ledger-only: a minted lesson is served from the ledger via ledgerLessons + }); }); -test("default (off): save() DOES write the legacy .md file", () => { - withEnv({ FORGE_LEDGER_ONLY: undefined }, () => { +test("escape hatch (FORGE_LEDGER_ONLY=0): save() DOES write the legacy .md file", () => { + withEnv({ FORGE_LEDGER_ONLY: "0" }, () => { const root = tmpRoot(); const s = save(root, makeLesson("lsn_c")); assert.equal(s.ok, true); assert.equal(s.ledgerOnly, undefined); - assert.equal(existsSync(lessonsDir(root)), true, "legacy lessons dir is written by default"); + assert.equal( + existsSync(lessonsDir(root)), + true, + "legacy lessons dir written with the escape hatch", + ); + }); +}); + +test("default is ledger-only: save() writes no file even with FORGE_LEDGER_ONLY unset", () => { + withEnv({ FORGE_LEDGER_ONLY: undefined }, () => { + const root = tmpRoot(); + const s = save(root, makeLesson("lsn_d")); + assert.equal(s.ok, true); + assert.equal(s.ledgerOnly, true, "ledger-only is the default now"); + assert.equal(existsSync(lessonsDir(root)), false, "no legacy lessons dir by default"); }); }); @@ -112,3 +128,36 @@ test("ledger-only: reconcileFacts is a no-op and never tombstones ledger facts ( assert.equal(ledgerFacts(ledgerDir).length, 2, "both facts survive — no memory wiped"); }); }); + +// End-to-end under the DEFAULT (ledger-only): the cortex learning loop must create, +// promote, and surface a lesson entirely through the ledger — no legacy files. +test("default ledger-only: recordMistake create→confirm promotes via the ledger; summary counts it", () => { + withEnv({ FORGE_LEDGER_ONLY: undefined, FORGE_AUTHOR: "Tester " }, () => { + const root = mkdtempSync(join(tmpdir(), "forge-lonly-e2e-")); + const ctx = { symbols: ["validateToken"], files: ["src/auth/token.ts"], keywords: [] }; + const strong = [{ signal: "S1" }, { signal: "S6" }]; + const first = recordMistake(root, { + signals: strong, + context: ctx, + nowDay: 1, + episodeId: "ep1", + }); + assert.equal(first.action, "created"); + assert.equal(first.status, "candidate"); + const again = recordMistake(root, { + signals: strong, + context: ctx, + nowDay: 2, + episodeId: "ep2", + }); + assert.equal(again.action, "confirmed", "recurrence confirms via the ledger, not a duplicate"); + assert.equal(again.status, "active", "a recurrence promotes the lesson"); + // Read surfaces work off the ledger under the default. + const merged = mergedLessons(root, 2); + assert.equal(merged.length, 1, "exactly one lesson (no fork)"); + assert.equal(merged[0].status, "active"); + const s = summary(root, 2); + assert.equal(s.total, 1); + assert.equal(s.active, 1, "cortex summary counts the ledger lesson"); + }); +}); diff --git a/test/ledger_read.test.js b/test/ledger_read.test.js index 93ce4ca..9660a28 100644 --- a/test/ledger_read.test.js +++ b/test/ledger_read.test.js @@ -19,6 +19,10 @@ import { newLesson } from "../src/lessons.js"; import { load, save } from "../src/lessons_store.js"; import { add as recallAdd, list as recallList, reindex as recallReindex } from "../src/recall.js"; +// Default is now ledger-only; these cases exercise the legacy FILE store (the +// FORGE_LEDGER_ONLY=0 escape hatch). Pin it here so they test that path directly. +process.env.FORGE_LEDGER_ONLY = "0"; + const tmp = () => mkdtempSync(join(tmpdir(), "forge-readflip-")); /** A lesson claim the way ledger_bridge.lessonClaim mints one (body = content only, diff --git a/test/lessons_store.test.js b/test/lessons_store.test.js index 5bb9611..3fad1d3 100644 --- a/test/lessons_store.test.js +++ b/test/lessons_store.test.js @@ -15,6 +15,10 @@ import { } from "../src/lessons_store.js"; import { fakeAnthropic } from "./_fixtures.js"; +// Default is now ledger-only; these cases exercise the legacy FILE store (the +// FORGE_LEDGER_ONLY=0 escape hatch). Pin it here so they test that path directly. +process.env.FORGE_LEDGER_ONLY = "0"; + const fixture = () => mkdtempSync(join(tmpdir(), "forge-lessons-")); const sample = () => { diff --git a/test/recall.test.js b/test/recall.test.js index 059b461..03220fd 100644 --- a/test/recall.test.js +++ b/test/recall.test.js @@ -6,6 +6,10 @@ import { test } from "node:test"; import { add, consolidate, list } from "../src/recall.js"; import { fakeGithubPat } from "./_fixtures.js"; +// Default is now ledger-only; these cases exercise the legacy FILE store (the +// FORGE_LEDGER_ONLY=0 escape hatch). Pin it here so they test that path directly. +process.env.FORGE_LEDGER_ONLY = "0"; + const store = () => mkdtempSync(join(tmpdir(), "forge-recall-")); test("add stores a fact and updates the index", () => {