diff --git a/src/resources/extensions/sf/self-feedback.js b/src/resources/extensions/sf/self-feedback.js index 310c1e2f6..0d6507929 100644 --- a/src/resources/extensions/sf/self-feedback.js +++ b/src/resources/extensions/sf/self-feedback.js @@ -45,6 +45,7 @@ import { listSelfFeedbackEntries, resolveSelfFeedbackEntry, } from "./sf-db.js"; +import { createMemory } from "./memory-store.js"; const SELF_FEEDBACK_HEADER = "# SF Self-Feedback\n\n" + @@ -274,6 +275,57 @@ function importLegacyJsonlToDb(basePath) { } } +/** + * Mirror a self-feedback resolution into the memory subsystem + * (sf-mp4rp6y2-31jfau). Closures carry knowledge — what kind of issue, + * what the operator/agent decided, citations to commits or requirements. + * Writing them as memory entries lets detectors call + * getRelevantMemoriesRanked(entry.kind) before filing duplicates, and + * lets reflection passes consult prior closure rationale across cycles. + * + * Best-effort: any failure is silently swallowed. The resolution itself + * already landed; memory write is observability + future detector + * consumption, not a correctness requirement. + * + * Called from markResolved success branch. + */ +function writeResolutionToMemory(entry, resolution) { + try { + if (!entry || typeof entry !== "object") return; + const evidenceKind = resolution?.evidence?.kind ?? "unknown"; + const reason = + typeof resolution?.reason === "string" && resolution.reason.trim() + ? resolution.reason.trim() + : "(no reason recorded)"; + const summary = + typeof entry.summary === "string" && entry.summary.trim() + ? entry.summary.trim().slice(0, 400) + : "(no summary)"; + const content = `[${entry.kind}] ${summary}\n→ ${evidenceKind}: ${reason.slice(0, 600)}`; + const tagSet = new Set(); + if (typeof entry.kind === "string") tagSet.add(entry.kind); + tagSet.add(`evidence:${evidenceKind}`); + const commitSha = resolution?.evidence?.commitSha; + if (typeof commitSha === "string" && commitSha) { + tagSet.add(`commit:${commitSha.slice(0, 12)}`); + } + const requirementId = resolution?.evidence?.requirementId; + if (typeof requirementId === "string" && requirementId) { + tagSet.add(`requirement:${requirementId}`); + } + createMemory({ + category: "self-feedback-resolution", + content, + confidence: 0.9, + source_unit_type: "self-feedback", + source_unit_id: entry.id, + tags: Array.from(tagSet), + }); + } catch { + // Best-effort — never fail the resolution path. + } +} + function appendResolutionToJsonl(basePath, entryId, resolution, resolvedAt) { const path = projectJsonlPath(basePath); const record = { @@ -549,6 +601,15 @@ export function markResolved(entryId, resolution, basePath = process.cwd()) { // how to replay these events into existing DB rows. appendResolutionToJsonl(basePath, entryId, resolution, resolvedAt); regenerateSelfFeedbackMarkdown(basePath); + // Mirror the closure into memory-store (sf-mp4rp6y2-31jfau) so + // detectors and reflection passes can consult prior closure + // rationale via getRelevantMemoriesRanked. Best-effort. + try { + const entry = listSelfFeedbackEntries().find((e) => e.id === entryId); + if (entry) writeResolutionToMemory(entry, resolution); + } catch { + // memory mirror is observability; never fail the resolution + } } return mutated; } catch { diff --git a/src/resources/extensions/sf/tests/self-feedback-db.test.mjs b/src/resources/extensions/sf/tests/self-feedback-db.test.mjs index 06cdc1f61..f304f41aa 100644 --- a/src/resources/extensions/sf/tests/self-feedback-db.test.mjs +++ b/src/resources/extensions/sf/tests/self-feedback-db.test.mjs @@ -295,6 +295,80 @@ test("markResolved_accepts_agent_fix_with_no_commit_sha_or_ungrokable_path", () assert.equal(ok2, true); }); +test("markResolved_mirrors_resolution_into_memory_store_with_tags", async () => { + const project = makeForgeProject(); + const filed = recordSelfFeedback( + { + kind: "gap:visible-in-memory", + severity: "medium", + summary: "test entry whose closure should land in memory", + }, + project, + ); + + const ok = markResolved( + filed.entry.id, + { + reason: "fixed by deadbeef in src/foo.js", + evidence: { kind: "agent-fix", commitSha: "deadbeefcafe" }, + }, + project, + ); + assert.equal(ok, true); + + // Verify the memory entry landed. getActiveMemories returns camelCase + // fields (sourceUnitId, not source_unit_id). + const { getActiveMemories } = await import("../sf-db.js"); + const memories = getActiveMemories(); + const closureMem = memories.find((m) => m.sourceUnitId === filed.entry.id); + assert.ok(closureMem, "expected a memory entry sourced to the resolved entry"); + assert.equal(closureMem.category, "self-feedback-resolution"); + assert.match(closureMem.content, /\[gap:visible-in-memory\]/); + assert.match(closureMem.content, /agent-fix: fixed by deadbeef/); + const tags = Array.isArray(closureMem.tags) ? closureMem.tags : []; + assert.ok(tags.includes("gap:visible-in-memory"), "kind tag missing"); + assert.ok(tags.includes("evidence:agent-fix"), "evidence-kind tag missing"); + assert.ok( + tags.some((t) => t.startsWith("commit:deadbeef")), + "commit tag missing", + ); +}); + +test("markResolved_memory_mirror_handles_human_clear_without_commit_tags", async () => { + const project = makeForgeProject(); + const filed = recordSelfFeedback( + { + kind: "upstream-rollup:noise-cluster", + severity: "medium", + summary: "out-of-scope close — no commit/requirement to tag", + }, + project, + ); + + const ok = markResolved( + filed.entry.id, + { + reason: "out of scope for inline-fix worker", + evidence: { kind: "human-clear" }, + }, + project, + ); + assert.equal(ok, true); + + const { getActiveMemories } = await import("../sf-db.js"); + const closureMem = getActiveMemories().find( + (m) => m.sourceUnitId === filed.entry.id, + ); + assert.ok(closureMem, "human-clear closures should also land in memory"); + const tags = Array.isArray(closureMem.tags) ? closureMem.tags : []; + assert.ok(tags.includes("upstream-rollup:noise-cluster")); + assert.ok(tags.includes("evidence:human-clear")); + assert.ok( + !tags.some((t) => t.startsWith("commit:")), + "no commit tag when no commitSha supplied", + ); +}); + test("markResolved_appends_resolution_event_to_jsonl_audit_log", () => { const project = makeForgeProject(); const result = recordSelfFeedback(