Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# PCM ledger evidence logs are append-only sets deduped by content hash on read —
# union merge is safe and makes teammate ledgers conflict-free by construction
# (docs/plans/substrate-v2/02-team-memory.md §1).
.forge/ledger/evidence/*.log merge=union
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,22 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Added

- **Proof-Carrying Memory ledger (P1 of the substrate-v2 plan).** `src/ledger.js` — the
pure PCM core (ADR-0006): content-addressed claims over canonical JSON, an oracle
taxonomy in which only independent signals (tests, CI, human accept/revert) may move
confidence, a time-decayed Beta-posterior `val` that decays toward *uncertainty* (never
toward false), the paper's Eq. 3 retrieval score, dependency-free MinHash similarity +
union-find consolidation clustering, and a join-semilattice merge (property-tested:
commutative, associative, idempotent — teammate ledgers converge in any order).
`src/ledger_store.js` — the git-native on-disk ledger (`.forge/ledger/`): one immutable
file per claim sharded by id, append-only hash-deduped evidence logs (union-merge safe,
see `.gitattributes`), tombstones, attic, `LEDGER.md` index, and a CI-friendly
normal-form `verify`. `forge ledger stats|verify|show|import` CLI. The legacy stores
stay the read path in P1: cortex shadow-writes every lesson event (create/confirm/
human-revert contradiction) into the ledger, `forge remember` / `forge recall add`
shadow facts, and `forge ledger import` back-fills history idempotently
(`src/ledger_bridge.js`). Secret-refusal now lives in the ledger core so no claim kind
can store a credential (re-exported from `recall.js` for compatibility).
- **Uniform `--json`.** `doctor`, `route`, `preflight`, `verify`, and `scope` now accept `--json`
(previously only `impact`/`substrate`/`anchor` did) — so CI and scripts can gate on the health
check, the routed tier, the assumption gap, and the verification result.
Expand Down
74 changes: 74 additions & 0 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const COMMANDS = {
cost: "real per-day spend via ccusage + the cost ceiling",
spec: "spec-as-contract — init (OpenSpec) / lock / check drift",
cortex: "self-correcting project memory — status / why <symbol>",
ledger: "proof-carrying memory ledger — stats / verify / show <id> / import",
preflight: "assumption check — what a task names that the repo doesn't define",
route: "recommend the cheapest capable model for a task (+ gateway config)",
impact: "predict blast radius for a symbol or file from the atlas graph",
Expand Down Expand Up @@ -153,6 +154,14 @@ async function run(argv) {
return;
}
const res = r.add(store, name, body);
if (res.ok) {
// Shadow the fact into the PERSONAL ledger beside the global store (repo
// promotion stays an explicit act — docs/plans/substrate-v2/02-team-memory.md §3).
const { join } = await import("node:path");
const { recordFactEvent } = await import("./ledger_bridge.js");
const { epochDay } = await import("./util.js");
recordFactEvent(join(store, "ledger"), name, body, epochDay());
}
console.log(res.ok ? ` saved: ${res.slug}` : ` ${res.reason}`);
if (!res.ok) process.exitCode = 1;
} else if (sub === "consolidate") {
Expand All @@ -164,6 +173,64 @@ async function run(argv) {
}
return;
}
if (cmd === "ledger") {
const ls = await import("./ledger_store.js");
const { epochDay } = await import("./util.js");
const root = process.cwd();
const dir = ls.repoLedger(root);
const sub = argv[1] || "stats";
const json = argv.includes("--json");
const nowDay = epochDay();
if (sub === "stats") {
const s = ls.stats(dir, nowDay);
if (json) return console.log(JSON.stringify(s, null, 2));
console.log(`${BRAND.brand} ledger — proof-carrying memory\n`);
console.log(` claims: ${s.total} (tombstoned ${s.tombstoned})`);
for (const [kind, n] of Object.entries(s.byKind)) console.log(` ${kind}: ${n}`);
console.log(
` val: trusted ${s.val.trusted} · uncertain ${s.val.uncertain} · dormant ${s.val.dormant}`,
);
console.log("\n stored in .forge/ledger/ (git-committable, conflict-free merge)");
return;
}
if (sub === "verify") {
const r = ls.verify(dir);
if (json) return console.log(JSON.stringify(r, null, 2));
console.log(` ${r.ok ? "OK" : "ISSUES"} — ${r.claims} claim(s), ${r.outcomes} outcome(s)`);
for (const i of r.issues) console.log(` - ${i}`);
if (!r.ok) process.exitCode = 1;
return;
}
if (sub === "show") {
const id = argv[2];
const hit = id && ls.loadClaims(dir).find((c) => c.id.startsWith(id));
if (!hit) {
console.error(id ? ` no claim matching ${id}` : "usage: forge ledger show <id-prefix>");
process.exitCode = 1;
return;
}
const { val } = await import("./ledger.js");
return console.log(JSON.stringify({ ...hit, val: val(hit, nowDay) }, null, 2));
}
if (sub === "import") {
const { importLegacy } = await import("./ledger_bridge.js");
const { brainStore } = await import("./brain.js");
const r = importLegacy(root, {
recallStore: brainStore(root),
recallLedger: dir,
nowDay,
});
if (json) return console.log(JSON.stringify(r, null, 2));
console.log(
` imported: ${r.lessons} lesson(s), ${r.facts} fact(s), ${r.outcomes} outcome(s)`,
);
for (const x of r.refused) console.log(` refused: ${x}`);
return;
}
console.error(`ledger: unknown subcommand "${sub}" (stats | verify | show <id> | import)`);
process.exitCode = 1;
return;
}
if (cmd === "atlas") {
const a = await import("./atlas.js");
const sub = argv[1] || "build";
Expand Down Expand Up @@ -260,6 +327,13 @@ async function run(argv) {
return;
}
const res = b.remember(b.brainStore(process.cwd()), name, body);
if (res.ok) {
// Brain is repo-scoped and git-committable → shadow into the REPO ledger.
const { recordFactEvent } = await import("./ledger_bridge.js");
const { repoLedger } = await import("./ledger_store.js");
const { epochDay } = await import("./util.js");
recordFactEvent(repoLedger(process.cwd()), name, body, epochDay());
}
console.log(
res.ok
? ` remembered: ${res.slug} — run \`forge sync\` to inline it into every tool`
Expand Down
20 changes: 20 additions & 0 deletions src/cortex.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// becomes a created/confirmed lesson; an independent human reversal becomes a
// contradiction. Kept fs-thin and deterministic (day + ids passed in) so it's testable
// without any hook wiring.

import { recordLessonEvent } from "./ledger_bridge.js";
import {
confidenceOf,
confirm,
Expand Down Expand Up @@ -83,6 +85,13 @@ export function recordMistake(root, { signals, context, nowDay, episodeId, disti
},
};
if (!save(root, updated).ok) return { action: "refused", p, fires };
// Shadow the confirmation into the PCM ledger (P1 bridge — legacy store stays the
// read path; best-effort by design, never blocks the hook).
recordLessonEvent(root, updated, {
result: "confirm",
ref: `episode:${episodeId}`,
t: nowDay,
});
return {
action: "confirmed",
id: updated.id,
Expand Down Expand Up @@ -115,6 +124,9 @@ export function recordMistake(root, { signals, context, nowDay, episodeId, disti
// Report "created" only when the write actually landed — a refused save (e.g. secret-bearing
// body) must not make the hook believe a lesson exists and try to distill a phantom.
if (!save(root, lesson).ok) return { action: "refused", p, fires };
// Mint-only shadow: a freshly created lesson has zero evidence (creation is not
// confirmation — the ledger's val starts at the 0.5 prior, same as newLesson).
recordLessonEvent(root, lesson, { t: nowDay });
return { action: "created", id: lesson.id, status: lesson.status, p, fires };
}

Expand All @@ -133,6 +145,14 @@ export function recordContradiction(root, { context, nowDay, episodeId }) {
const results = targets.map((l) => {
const updated = contradict(l, nowDay);
const saved = save(root, updated).ok;
// An explicit human reversal is the strongest oracle we have — shadow it.
if (saved)
recordLessonEvent(root, updated, {
result: "contradict",
oracle: "human.revert",
ref: `episode:${episodeId}`,
t: nowDay,
});
return { id: updated.id, status: updated.status, saved };
});
return { action: "contradicted", results };
Expand Down
Loading
Loading