The engine can now RETIRE a fact via an operator-gated, explicitly-signalled
temporal-update supersede — completing the consolidation motor's one S2 TODO
("no supersede in S2 — that's S3"). A `supersedes` signal on a candidate retires
the stale fact (re-minted to an archival id, status: superseded, REPLACED IN
PLACE, retained as audit) and installs the new winner under the canonical key-id,
so mintEntityId(key) always points at the live fact.
- consolidate.ts: Candidate.supersedes? + ProfileDiff.supersedes (SupersedeOp
carries the full winner fact, so applyDiff stays a pure projector). proposeDiff
value-guarded routing fork (routes only when the target holds a DIFFERENT value —
else a re-sent signal would self-supersede every run) + intra-batch
first-supersede-wins guard. applyDiff replace-in-place + value-matched state-check
(oldId present, active, value===oldValue) → idempotent re-apply + stale-diff safe;
superseded facts never bumped/promoted (supersede wins). Decay excludes superseded.
archivalId seeded with the pre-archival id (collision-free).
- cli.ts: renderDiffMd `## Supersessions (old → new)` (rendered last, only when
present → zero-supersession diffs stay byte-identical); validateCandidates optional
single-line `supersedes`; `--gather` profileFacts filtered to active (superseded
archival facts never re-presented as live context).
- Tests: +12 brain (consolidate 10 + consolidate-cli 2). TDD: RED (6 fail) → GREEN
(94/94). BRAIN_TESTS_FLOOR 82->94; ASSERT_BASELINE_FLOOR unchanged at 80 (no new
test-runner.sh section). Gate 95/0/0.
- Docs: consolidation-loop.md rule table + honest-limit reconciled (the operator
gate is the only classification net); engine docstring updated.
All 13 success criteria deterministically tested (unlike S3a, no behavioural-only
SC). READ-only gate unchanged — brain consolidate --apply --confirm stays the sole
profile.md writer. Scope held: scripts/brain/ only; temporal-update only
(condition-dependent/distractor deferred).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
129 lines
7.3 KiB
TypeScript
129 lines
7.3 KiB
TypeScript
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { execFileSync } from "node:child_process";
|
|
import { mkdtempSync, rmSync, existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
import { join, dirname } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { tmpdir } from "node:os";
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
const CLI = join(HERE, "..", "src", "cli.ts");
|
|
|
|
function runCli(root: string, args: string[]): { stdout: string; code: number } {
|
|
try {
|
|
const stdout = execFileSync("node", ["--import", "tsx", CLI, ...args], {
|
|
env: { ...process.env, LINKEDIN_STUDIO_DATA: root },
|
|
encoding: "utf8",
|
|
});
|
|
return { stdout, code: 0 };
|
|
} catch (e: any) {
|
|
return { stdout: (e.stdout ?? "") + (e.stderr ?? ""), code: e.status ?? 1 };
|
|
}
|
|
}
|
|
|
|
const candidatesFile = (root: string, items: unknown): string => {
|
|
const p = join(root, "candidates.json");
|
|
writeFileSync(p, JSON.stringify(items), "utf8");
|
|
return p;
|
|
};
|
|
const profilePath = (root: string) => join(root, "brain", "profile.md");
|
|
const pendingJson = (root: string) => join(root, "brain", "pending-diff.json");
|
|
const pendingMd = (root: string) => join(root, "brain", "pending-diff.md");
|
|
const validCand = { key: "topic", value: "AI safety", provenance: "published", source: "manual", observed_date: "2026-06-23" };
|
|
|
|
describe("brain consolidate CLI (SC5)", () => {
|
|
let root: string;
|
|
beforeEach(() => {
|
|
root = mkdtempSync(join(tmpdir(), "brain-consol-"));
|
|
runCli(root, ["init"]); // seed brain/profile.md
|
|
});
|
|
afterEach(() => rmSync(root, { recursive: true, force: true }));
|
|
|
|
test("--gather reads published bodies (filtered by last_run) and writes nothing", () => {
|
|
runCli(root, ["ingest", "--file", (() => { const f = join(root, "p.md"); writeFileSync(f, "A post about AI safety governance.", "utf8"); return f; })()]);
|
|
const before = readFileSync(profilePath(root), "utf8");
|
|
const { stdout, code } = runCli(root, ["consolidate", "--gather"]);
|
|
assert.equal(code, 0);
|
|
assert.match(stdout, /AI safety governance/, "gather surfaces the published body");
|
|
assert.equal(readFileSync(profilePath(root), "utf8"), before, "gather leaves profile.md unchanged");
|
|
});
|
|
|
|
test("--propose writes pending-diff.{md,json}, leaves profile.md unchanged", () => {
|
|
const before = readFileSync(profilePath(root), "utf8");
|
|
const { code } = runCli(root, ["consolidate", "--propose", "--candidates", candidatesFile(root, [validCand])]);
|
|
assert.equal(code, 0);
|
|
assert.ok(existsSync(pendingJson(root)), "pending-diff.json written");
|
|
assert.ok(existsSync(pendingMd(root)), "pending-diff.md written");
|
|
assert.equal(readFileSync(profilePath(root), "utf8"), before, "propose leaves profile.md unchanged");
|
|
const diff = JSON.parse(readFileSync(pendingJson(root), "utf8"));
|
|
assert.equal(diff.additions.length, 1);
|
|
});
|
|
|
|
test("--propose REJECTS a malformed candidate (missing field), no write", () => {
|
|
const { code } = runCli(root, ["consolidate", "--propose", "--candidates", candidatesFile(root, [{ key: "x", value: "y" }])]);
|
|
assert.notEqual(code, 0, "non-zero exit on malformed candidate");
|
|
assert.ok(!existsSync(pendingJson(root)), "no diff written");
|
|
});
|
|
|
|
test("--propose REJECTS a candidate whose value contains a newline, no write", () => {
|
|
const bad = { ...validCand, value: "line one\nline two" };
|
|
const { code } = runCli(root, ["consolidate", "--propose", "--candidates", candidatesFile(root, [bad])]);
|
|
assert.notEqual(code, 0, "non-zero exit on multiline value");
|
|
assert.ok(!existsSync(pendingJson(root)), "no diff written");
|
|
});
|
|
|
|
test("--apply --confirm writes profile.md + consolidation-state.json", () => {
|
|
runCli(root, ["consolidate", "--propose", "--candidates", candidatesFile(root, [validCand])]);
|
|
const { code } = runCli(root, ["consolidate", "--apply", "--diff", pendingJson(root), "--confirm"]);
|
|
assert.equal(code, 0);
|
|
assert.match(readFileSync(profilePath(root), "utf8"), /AI safety/, "fact applied to profile.md");
|
|
assert.ok(existsSync(join(root, "brain", "consolidation-state.json")), "last-run sidecar written");
|
|
});
|
|
|
|
test("--apply WITHOUT --confirm refuses and writes nothing", () => {
|
|
runCli(root, ["consolidate", "--propose", "--candidates", candidatesFile(root, [validCand])]);
|
|
const before = readFileSync(profilePath(root), "utf8");
|
|
const { code } = runCli(root, ["consolidate", "--apply", "--diff", pendingJson(root)]);
|
|
assert.notEqual(code, 0, "refuses without --confirm");
|
|
assert.equal(readFileSync(profilePath(root), "utf8"), before, "profile.md untouched");
|
|
});
|
|
|
|
test("init/ingest/published still work (regression)", () => {
|
|
assert.equal(runCli(root, ["init"]).code, 0);
|
|
const f = join(root, "q.md"); writeFileSync(f, "another post", "utf8");
|
|
assert.equal(runCli(root, ["ingest", "--file", f]).code, 0);
|
|
assert.equal(runCli(root, ["published", "list"]).code, 0);
|
|
});
|
|
|
|
const supCand = { key: "topic", value: "AI governance", provenance: "published", source: "manual", observed_date: "2026-06-24", supersedes: "topic" };
|
|
|
|
test("S3b-SC6 — Supersessions section renders one line per entry; a plain diff renders none; gated apply writes superseded", () => {
|
|
// plain propose → NO Supersessions section (byte-identity decoy)
|
|
runCli(root, ["consolidate", "--propose", "--candidates", candidatesFile(root, [validCand])]);
|
|
assert.ok(!readFileSync(pendingMd(root), "utf8").includes("## Supersessions"), "no section when 0 supersessions");
|
|
// apply to seed the active fact, then propose a supersede of it
|
|
runCli(root, ["consolidate", "--apply", "--diff", pendingJson(root), "--confirm"]);
|
|
runCli(root, ["consolidate", "--propose", "--candidates", candidatesFile(root, [supCand])]);
|
|
const md = readFileSync(pendingMd(root), "utf8");
|
|
assert.match(md, /## Supersessions/, "section rendered when a supersession exists");
|
|
assert.match(md, /AI safety.*→.*AI governance/, "old → new line rendered");
|
|
const diff = JSON.parse(readFileSync(pendingJson(root), "utf8"));
|
|
assert.equal(diff.supersedes.length, 1, "one supersede op in the JSON");
|
|
// the gated apply writes the superseded status to profile.md
|
|
runCli(root, ["consolidate", "--apply", "--diff", pendingJson(root), "--confirm"]);
|
|
assert.match(readFileSync(profilePath(root), "utf8"), /superseded/, "apply writes the superseded status");
|
|
});
|
|
|
|
test("S3b-SC10 — --gather excludes superseded facts from profileFacts", () => {
|
|
runCli(root, ["consolidate", "--propose", "--candidates", candidatesFile(root, [validCand])]);
|
|
runCli(root, ["consolidate", "--apply", "--diff", pendingJson(root), "--confirm"]);
|
|
runCli(root, ["consolidate", "--propose", "--candidates", candidatesFile(root, [supCand])]);
|
|
runCli(root, ["consolidate", "--apply", "--diff", pendingJson(root), "--confirm"]);
|
|
// profile now holds an archival "AI safety" (superseded) + winner "AI governance" (active)
|
|
const { stdout } = runCli(root, ["consolidate", "--gather", "--json"]);
|
|
const out = JSON.parse(stdout);
|
|
const values = out.profileFacts.map((f: any) => f.value);
|
|
assert.ok(values.includes("AI governance"), "active winner present in gather");
|
|
assert.ok(!out.profileFacts.some((f: any) => f.status === "superseded"), "no superseded fact leaks into gather");
|
|
});
|
|
});
|