Skip to content

Commit ffc93fd

Browse files
committed
feat(memory): close verified learning lifecycle for #681
Add conflict detection, analyze/redteam learning influence, CLI review ops, and UAT-LEARN-1..5 coverage so durable memory only lands after explicit approval.
1 parent cab0437 commit ffc93fd

18 files changed

Lines changed: 1105 additions & 18 deletions

File tree

docs/memory.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,15 +234,60 @@ Retention and redaction defaults:
234234
severity, add a clearer `description`, or remove the `productPaths` until the
235235
check is stable.
236236

237+
## Verified Learning Lifecycle
238+
239+
Flat memory sections are useful, but durable engineering knowledge should come
240+
from verified outcomes: confirmed regressions, repairs, refuted hypotheses,
241+
accepted risks, incidents, ADRs, conventions, ownership changes, and proof
242+
recipes.
243+
244+
Use versioned `learningEvents` in `.codedecay/memory.json`:
245+
246+
```bash
247+
# Preview a proposal (does not mutate memory)
248+
npx codedecay memory learning --action propose --input learning-event.json
249+
250+
# Persist the proposal
251+
npx codedecay memory learning --action propose --input learning-event.json --apply
252+
253+
# Explicit human review
254+
npx codedecay memory learning --action approve --event-id <id> --actor kunal --reason "Verified against payout retry CI" --apply
255+
npx codedecay memory learning --action reject --event-id <id> --apply
256+
npx codedecay memory learning --action supersede --event-id <id> --apply
257+
npx codedecay memory learning --action expire --event-id <id> --apply
258+
npx codedecay memory learning --action revoke --event-id <id> --apply
259+
```
260+
261+
Rules:
262+
263+
- Agent output, PR text, comments, and external memory stay `proposed` until an
264+
explicit approve/reject/supersede/expire/revoke operation.
265+
- Trusted runtime/tool evidence can raise proposal confidence, but never silently
266+
writes durable approved memory.
267+
- Every event keeps source evidence IDs, scope (repo/revision/files/symbols),
268+
trust class, creator, timestamps, review status, and an audit trail.
269+
- Retrieval only surfaces approved, in-scope, non-expired events and explains
270+
inclusion and suppression.
271+
- Refuted hypotheses affect ranking only inside a narrowly matched scope; they
272+
cannot globally disable a rule.
273+
- Redteam/analyze reports show when a prior approved learning influenced
274+
investigation or proof planning (`memory-learning-influenced`).
275+
276+
Conflict detection flags duplicates, contradictions (for example confirmed
277+
regression vs refuted hypothesis), and ownership/architecture overlaps that
278+
should supersede stale routing.
279+
237280
## Report Behavior
238281

239282
When memory matches a PR, CodeDecay may add:
240283

241284
- findings for impacted invariants
242285
- findings for past regression areas
243286
- findings for matching architecture notes
287+
- findings for approved learning events that match the change
244288
- recommended checks for flows
245289
- recommended commands from the memory file
290+
- recommended proof recipes from approved learnings
246291

247292
CodeDecay does not run memory commands automatically. They are reported as
248293
project-specific checks for the user or future execution adapters.

packages/cli/src/commands/memory.ts

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,29 @@
11
import { readFileSync } from "node:fs";
22
import { dirname, extname, resolve } from "node:path";
33
import {
4+
appendLearningEventProposal,
5+
applyLearningEventOperation,
6+
detectLearningConflicts,
47
importCodeDecayMemory,
58
learnCodeDecayMemory,
69
loadCodeDecayMemory,
7-
writeCodeDecayMemory
10+
writeCodeDecayMemory,
11+
type MemoryLearningEventInput,
12+
type MemoryLearningConflict
813
} from "@submuxhq/codedecay-memory";
914
import { write } from "../io";
1015
import {
1116
parseMemoryArgs,
1217
parseMemoryImportArgs,
1318
parseMemoryLearnArgs,
19+
parseMemoryLearningArgs,
1420
parseMemorySetupArgs
1521
} from "../parsers/args";
1622
import {
1723
renderMemory,
1824
renderMemoryImportResult,
19-
renderMemoryLearnResult
25+
renderMemoryLearnResult,
26+
renderMemoryLearningResult
2027
} from "../renderers/memory";
2128
import {
2229
createMemorySetupResult,
@@ -37,6 +44,14 @@ export function runMemoryCommand(context: CliCommandContext, dependencies: Memor
3744
return;
3845
}
3946

47+
if (context.args[0] === "learning") {
48+
runMemoryLearningCommand({
49+
...context,
50+
args: context.args.slice(1)
51+
}, dependencies);
52+
return;
53+
}
54+
4055
const options = parseMemoryArgs(context.args);
4156
const cwd = resolve(context.runtimeCwd, options.cwd ?? ".");
4257
const rootDir = dependencies.resolveRepoRoot(cwd, { format: "markdown" });
@@ -94,6 +109,54 @@ export function runMemoryLearnCommand(context: CliCommandContext, dependencies:
94109
);
95110
}
96111

112+
export function runMemoryLearningCommand(context: CliCommandContext, dependencies: MemoryCommandDependencies): void {
113+
const options = parseMemoryLearningArgs(context.args);
114+
const cwd = resolve(context.runtimeCwd, options.cwd ?? ".");
115+
const rootDir = dependencies.resolveRepoRoot(cwd, { format: "markdown" });
116+
const loadedMemory = loadCodeDecayMemory(rootDir);
117+
const timestamp = new Date().toISOString();
118+
let memory = loadedMemory.memory;
119+
let eventId = options.eventId;
120+
let conflicts: MemoryLearningConflict[] = detectLearningConflicts(memory);
121+
122+
if (options.action === "propose") {
123+
const inputPath = resolve(context.runtimeCwd, options.input!);
124+
const proposal = JSON.parse(readFileSync(inputPath, "utf8")) as MemoryLearningEventInput;
125+
// Proposals always land as reviewStatus=proposed; approve/reject/etc. are explicit ops.
126+
const appended = appendLearningEventProposal(memory, {
127+
...proposal,
128+
timestamp: proposal.timestamp ?? timestamp,
129+
creator: proposal.creator ?? options.actor
130+
});
131+
memory = appended.memory;
132+
eventId = appended.event.id;
133+
conflicts = appended.conflicts;
134+
} else {
135+
memory = applyLearningEventOperation(memory, {
136+
eventId: options.eventId!,
137+
action: options.action,
138+
actor: options.actor,
139+
timestamp,
140+
reason: options.reason,
141+
evidenceIds: options.evidenceIds
142+
});
143+
conflicts = detectLearningConflicts(memory);
144+
}
145+
146+
const writtenPath = options.apply ? writeCodeDecayMemory(rootDir, memory) : undefined;
147+
write(
148+
context.runtime.stdout,
149+
renderMemoryLearningResult({
150+
format: options.format,
151+
action: options.action,
152+
eventId: eventId!,
153+
writtenPath,
154+
conflicts,
155+
applied: options.apply
156+
})
157+
);
158+
}
159+
97160
function parseMemoryLearningInput(inputPath: string): unknown {
98161
const raw = readFileSync(inputPath, "utf8");
99162
if (isMarkdownPath(inputPath)) {

packages/cli/src/docs/command-docs/state.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,40 @@ export const STATE_COMMAND_DOCS: Record<string, CommandDoc> = {
1717
memory: {
1818
name: "memory",
1919
summary: "Show local repo memory.",
20-
usage: ["codedecay memory [options]", "codedecay memory setup [options]"],
20+
usage: [
21+
"codedecay memory [options]",
22+
"codedecay memory setup [options]",
23+
"codedecay memory learning --action <action> [options]"
24+
],
2125
description: [
2226
"Load `.codedecay/memory.json` and render the normalized memory sections used by redteam and agent workflows.",
23-
"`codedecay memory setup` prints safe setup guidance for local, Mem0, and Supermemory providers without installing packages or touching tracked config."
27+
"`codedecay memory setup` prints safe setup guidance for local, Mem0, and Supermemory providers without installing packages or touching tracked config.",
28+
"`codedecay memory learning` proposes or reviews versioned learning events (approve/reject/supersede/expire/revoke) without auto-approving untrusted sources."
2429
],
2530
options: [
2631
{ flag: "--cwd <path>", description: "Repository working directory (default: current directory)" },
2732
{ flag: "--format <format>", description: "json or markdown (default: json for memory, markdown for setup)" },
2833
{ flag: "setup --provider <provider>", description: "local, mem0, supermemory, or all (default: all)" },
29-
{ flag: "setup --apply", description: "Write .codedecay/local/memory-providers.yml review snippet" }
34+
{ flag: "setup --apply", description: "Write .codedecay/local/memory-providers.yml review snippet" },
35+
{ flag: "learning --action <action>", description: "propose|approve|reject|supersede|expire|revoke" },
36+
{ flag: "learning --event-id <id>", description: "Existing learning event id (required except propose)" },
37+
{ flag: "learning --input <path>", description: "JSON learning event proposal (required for propose)" },
38+
{ flag: "learning --actor <name>", description: "Reviewer/proposer identity (default: maintainer)" },
39+
{ flag: "learning --reason <text>", description: "Audit reason for the operation" },
40+
{ flag: "learning --evidence-id <id>", description: "Optional evidence id (repeatable)" },
41+
{ flag: "learning --apply", description: "Write `.codedecay/memory.json` instead of preview only" }
3042
],
3143
examples: [
3244
"codedecay memory --format markdown",
3345
"codedecay memory --cwd ../my-repo --format json",
3446
"codedecay memory setup --provider all",
35-
"codedecay memory setup --provider supermemory --apply"
47+
"codedecay memory setup --provider supermemory --apply",
48+
"codedecay memory learning --action propose --input learning.json",
49+
"codedecay memory learning --action approve --event-id learn_abc --apply"
3650
],
3751
notes: [
38-
"Memory setup is preview-only by default. It does not install packages, call providers, or edit `.codedecay/config.yml`."
52+
"Memory setup is preview-only by default. It does not install packages, call providers, or edit `.codedecay/config.yml`.",
53+
"Learning events stay proposed until an explicit approve/reject/supersede/expire/revoke operation."
3954
]
4055
},
4156
"memory-import": {

packages/cli/src/parsers/args.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export { parseExecuteArgs } from "./execute";
1111
export { parseLlmReviewArgs } from "./llm-review";
1212
export { parseLoopArgs } from "./loop";
1313
export { parseMcpArgs } from "./mcp";
14-
export { parseMemoryArgs, parseMemoryImportArgs, parseMemoryLearnArgs, parseMemorySetupArgs } from "./memory";
14+
export { parseMemoryArgs, parseMemoryImportArgs, parseMemoryLearnArgs, parseMemoryLearningArgs, parseMemorySetupArgs } from "./memory";
1515
export { parseMigrationArgs } from "./migration";
1616
export { parseRevalidateArgs } from "./revalidate";
1717
export { parseRuntimeArgs } from "./runtime";

packages/cli/src/parsers/memory.ts

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import type { MemoryImportOptions, MemoryLearnOptions, MemoryOptions, MemorySetupOptions, MemorySetupProvider } from "../types";
1+
import type {
2+
MemoryImportOptions,
3+
MemoryLearnOptions,
4+
MemoryLearningOptions,
5+
MemoryOptions,
6+
MemorySetupOptions,
7+
MemorySetupProvider
8+
} from "../types";
29
import { parseConfigFormat, requireValue } from "./primitives";
310
import { HelpRequested, throwUnknownOption } from "./shared";
411

@@ -247,3 +254,135 @@ export function parseMemoryLearnArgs(args: string[]): MemoryLearnOptions {
247254

248255
return options;
249256
}
257+
258+
export function parseMemoryLearningArgs(args: string[]): MemoryLearningOptions {
259+
const options: MemoryLearningOptions = {
260+
format: "json",
261+
apply: false,
262+
action: "approve",
263+
actor: "maintainer",
264+
reason: "Explicit human review of learning event."
265+
};
266+
267+
for (let index = 0; index < args.length; index += 1) {
268+
const arg = args[index];
269+
if (!arg) {
270+
continue;
271+
}
272+
273+
if (arg === "--help" || arg === "-h") {
274+
throw new HelpRequested();
275+
}
276+
277+
if (arg.startsWith("--cwd=")) {
278+
options.cwd = arg.slice("--cwd=".length);
279+
continue;
280+
}
281+
if (arg === "--cwd") {
282+
options.cwd = requireValue(args, index, arg);
283+
index += 1;
284+
continue;
285+
}
286+
287+
if (arg.startsWith("--format=")) {
288+
options.format = parseConfigFormat(arg.slice("--format=".length));
289+
continue;
290+
}
291+
if (arg === "--format") {
292+
options.format = parseConfigFormat(requireValue(args, index, arg));
293+
index += 1;
294+
continue;
295+
}
296+
297+
if (arg === "--apply") {
298+
options.apply = true;
299+
continue;
300+
}
301+
302+
if (arg.startsWith("--action=")) {
303+
options.action = parseLearningAction(arg.slice("--action=".length));
304+
continue;
305+
}
306+
if (arg === "--action") {
307+
options.action = parseLearningAction(requireValue(args, index, arg));
308+
index += 1;
309+
continue;
310+
}
311+
312+
if (arg.startsWith("--event-id=")) {
313+
options.eventId = arg.slice("--event-id=".length);
314+
continue;
315+
}
316+
if (arg === "--event-id") {
317+
options.eventId = requireValue(args, index, arg);
318+
index += 1;
319+
continue;
320+
}
321+
322+
if (arg.startsWith("--actor=")) {
323+
options.actor = arg.slice("--actor=".length);
324+
continue;
325+
}
326+
if (arg === "--actor") {
327+
options.actor = requireValue(args, index, arg);
328+
index += 1;
329+
continue;
330+
}
331+
332+
if (arg.startsWith("--reason=")) {
333+
options.reason = arg.slice("--reason=".length);
334+
continue;
335+
}
336+
if (arg === "--reason") {
337+
options.reason = requireValue(args, index, arg);
338+
index += 1;
339+
continue;
340+
}
341+
342+
if (arg.startsWith("--input=")) {
343+
options.input = arg.slice("--input=".length);
344+
continue;
345+
}
346+
if (arg === "--input") {
347+
options.input = requireValue(args, index, arg);
348+
index += 1;
349+
continue;
350+
}
351+
352+
if (arg.startsWith("--evidence-id=")) {
353+
options.evidenceIds = [...(options.evidenceIds ?? []), arg.slice("--evidence-id=".length)];
354+
continue;
355+
}
356+
if (arg === "--evidence-id") {
357+
options.evidenceIds = [...(options.evidenceIds ?? []), requireValue(args, index, arg)];
358+
index += 1;
359+
continue;
360+
}
361+
362+
throwUnknownOption(arg, "memory learning");
363+
}
364+
365+
if (options.action === "propose" && !options.input) {
366+
throw new Error('Missing value for --input. Propose requires a JSON learning event file.');
367+
}
368+
369+
if (options.action !== "propose" && !options.eventId) {
370+
throw new Error('Missing value for --event-id. Use "codedecay memory learning --help" for usage.');
371+
}
372+
373+
return options;
374+
}
375+
376+
function parseLearningAction(value: string): MemoryLearningOptions["action"] {
377+
if (
378+
value === "approve" ||
379+
value === "reject" ||
380+
value === "supersede" ||
381+
value === "expire" ||
382+
value === "revoke" ||
383+
value === "propose"
384+
) {
385+
return value;
386+
}
387+
throw new Error(`Invalid --action ${value}. Expected approve|reject|supersede|expire|revoke|propose.`);
388+
}

0 commit comments

Comments
 (0)