fix(FN-2545): persist memory audit extraction state and polish MemoryView

- Persist extract-to-audit continuity state in memory insights so extraction progress survives follow-up runs
- Add core and dashboard route tests that cover audit extraction flow and prevent regressions
- Restore lint/typecheck baseline by removing obsolete planning subtask route wiring from dashboard routes
- Apply MemoryView UX refinements with updated component logic and dedicated styling adjustments
This commit is contained in:
Fusion
2026-04-26 02:15:54 -07:00
committed by gsxdsm
parent 785b557aaf
commit 7527a7ddaf
7 changed files with 335 additions and 152 deletions

View File

@@ -1100,6 +1100,31 @@ describe("memory-insights audit generation", () => {
expect(report.extraction.duplicateCount).toBe(2);
});
it("reuses persisted extraction state when explicit metadata is omitted", async () => {
const runAt = new Date().toISOString();
writeFileSync(
join(tempDir, MEMORY_WORKING_PATH),
"## Architecture\n\nDurable notes\n\n## Conventions\n\nConventions",
);
await processAndAuditInsightExtraction(tempDir, {
rawResponse: JSON.stringify({
summary: "Persisted extraction summary",
insights: [{ category: "pattern", content: "Persisted insight" }],
}),
stepSuccess: true,
runAt,
});
const report = await generateMemoryAudit(tempDir);
expect(report.extraction.runAt).toBe(runAt);
expect(report.extraction.success).toBe(true);
expect(report.extraction.summary).toBe("Persisted extraction summary");
expect(report.checks.find((c) => c.id === "recent-extraction")?.passed).toBe(true);
});
it("should include pruning outcome in report", async () => {
writeFileSync(join(tempDir, MEMORY_WORKING_PATH), "## Architecture\n\nNotes");

View File

@@ -81,6 +81,9 @@ export const MEMORY_INSIGHTS_PATH = ".fusion/memory-insights.md";
/** Path to memory audit report relative to project root. */
export const MEMORY_AUDIT_PATH = ".fusion/memory-audit.md";
/** Path to persisted memory audit state (latest extraction/pruning metadata). */
export const MEMORY_AUDIT_STATE_PATH = ".fusion/memory-audit-state.json";
/** Default cron schedule for insight extraction: daily at 2 AM. */
export const DEFAULT_INSIGHT_SCHEDULE = "0 2 * * *";
@@ -167,6 +170,17 @@ export interface MemoryAuditCheck {
details: string;
}
/** Persisted extraction metadata used by audits/routes. */
export interface MemoryExtractionMetadata {
runAt: string;
success: boolean;
insightCount: number;
duplicateCount: number;
skippedCount: number;
summary: string;
error?: string;
}
/** Result of a memory audit run. */
export interface MemoryAuditReport {
/** ISO-8601 timestamp of the audit. */
@@ -187,15 +201,7 @@ export interface MemoryAuditReport {
lastUpdated?: string;
};
/** Extraction metadata. */
extraction: {
runAt: string;
success: boolean;
insightCount: number;
duplicateCount: number;
skippedCount: number;
summary: string;
error?: string;
};
extraction: MemoryExtractionMetadata;
/** Pruning operation outcome. */
pruning: {
applied: boolean;
@@ -210,6 +216,13 @@ export interface MemoryAuditReport {
health: "healthy" | "warning" | "issues";
}
/** Persisted state for audit continuity across requests. */
interface MemoryAuditState {
extraction?: MemoryExtractionMetadata;
pruning?: PruneOutcome;
updatedAt: string;
}
/** Input for processing an insight extraction run. */
export interface ProcessRunInput {
/** Raw AI response text from the insight extraction step. */
@@ -326,6 +339,81 @@ export async function writeMemoryAudit(rootDir: string, content: string): Promis
await writeFile(filePath, content, "utf-8");
}
/**
* Read persisted memory audit state (`memory-audit-state.json`).
*
* Returns `null` when no prior state exists.
*/
async function readMemoryAuditState(rootDir: string): Promise<MemoryAuditState | null> {
const filePath = join(rootDir, MEMORY_AUDIT_STATE_PATH);
if (!existsSync(filePath)) {
return null;
}
try {
const raw = await readFile(filePath, "utf-8");
const parsed = JSON.parse(raw) as Partial<MemoryAuditState>;
const extraction = isValidExtractionMetadata(parsed.extraction) ? parsed.extraction : undefined;
const pruning = isValidPruneOutcome(parsed.pruning) ? parsed.pruning : undefined;
return {
extraction,
pruning,
updatedAt: typeof parsed.updatedAt === "string" && parsed.updatedAt.trim()
? parsed.updatedAt
: new Date().toISOString(),
};
} catch {
return null;
}
}
/**
* Persist memory audit state (`memory-audit-state.json`).
*/
async function writeMemoryAuditState(rootDir: string, state: MemoryAuditState): Promise<void> {
const filePath = join(rootDir, MEMORY_AUDIT_STATE_PATH);
const dir = join(rootDir, ".fusion");
if (!existsSync(dir)) {
await mkdir(dir, { recursive: true });
}
await writeFile(filePath, JSON.stringify(state, null, 2), "utf-8");
}
function isValidExtractionMetadata(value: unknown): value is MemoryExtractionMetadata {
if (!value || typeof value !== "object") {
return false;
}
const candidate = value as Partial<MemoryExtractionMetadata>;
return (
typeof candidate.runAt === "string" &&
typeof candidate.success === "boolean" &&
typeof candidate.insightCount === "number" &&
typeof candidate.duplicateCount === "number" &&
typeof candidate.skippedCount === "number" &&
typeof candidate.summary === "string" &&
(candidate.error === undefined || typeof candidate.error === "string")
);
}
function isValidPruneOutcome(value: unknown): value is PruneOutcome {
if (!value || typeof value !== "object") {
return false;
}
const candidate = value as Partial<PruneOutcome>;
return (
typeof candidate.applied === "boolean" &&
typeof candidate.reason === "string" &&
typeof candidate.sizeDelta === "number" &&
typeof candidate.originalSize === "number" &&
typeof candidate.newSize === "number"
);
}
// ── AI Prompt Construction ───────────────────────────────────────────
/**
@@ -1103,20 +1191,19 @@ function countInsightsInMarkdown(markdown: string): number {
*/
export async function generateMemoryAudit(
rootDir: string,
lastExtraction?: {
runAt: string;
success: boolean;
insightCount: number;
duplicateCount: number;
skippedCount: number;
summary: string;
error?: string;
},
lastExtraction?: MemoryExtractionMetadata,
pruningOutcome?: PruneOutcome,
): Promise<MemoryAuditReport> {
const checks: MemoryAuditCheck[] = [];
const now = new Date().toISOString();
const persistedState =
lastExtraction === undefined || pruningOutcome === undefined
? await readMemoryAuditState(rootDir)
: null;
const effectiveExtraction = lastExtraction ?? persistedState?.extraction;
const effectivePruning = pruningOutcome ?? persistedState?.pruning;
// ── Check 1: Working memory file presence ──────────────────────────
const workingMemoryPath = join(rootDir, MEMORY_WORKING_PATH);
const workingMemoryExists = existsSync(workingMemoryPath);
@@ -1261,26 +1348,26 @@ export async function generateMemoryAudit(
}
// ── Check 6: Recent extraction activity ───────────────────────────
if (lastExtraction) {
const extractionAge = Date.now() - new Date(lastExtraction.runAt).getTime();
if (effectiveExtraction) {
const extractionAge = Date.now() - new Date(effectiveExtraction.runAt).getTime();
const oneWeekMs = 7 * 24 * 60 * 60 * 1000;
checks.push({
id: "recent-extraction",
name: "Recent extraction activity",
passed: lastExtraction.success && extractionAge < oneWeekMs,
details: lastExtraction.success
? `Last successful extraction ${formatTimeAgo(lastExtraction.runAt)} (${lastExtraction.insightCount} insights, ${lastExtraction.duplicateCount} duplicates skipped)`
: `Last extraction failed: ${lastExtraction.error || "Unknown error"}`,
passed: effectiveExtraction.success && extractionAge < oneWeekMs,
details: effectiveExtraction.success
? `Last successful extraction ${formatTimeAgo(effectiveExtraction.runAt)} (${effectiveExtraction.insightCount} insights, ${effectiveExtraction.duplicateCount} duplicates skipped)`
: `Last extraction failed: ${effectiveExtraction.error || "Unknown error"}`,
});
// Check 7: Extraction summary quality
checks.push({
id: "extraction-summary",
name: "Extraction produces meaningful summaries",
passed: lastExtraction.success && lastExtraction.summary.length > 10,
details: lastExtraction.success
? `Summary: "${lastExtraction.summary.slice(0, 100)}${lastExtraction.summary.length > 100 ? "..." : ""}"`
passed: effectiveExtraction.success && effectiveExtraction.summary.length > 10,
details: effectiveExtraction.success
? `Summary: "${effectiveExtraction.summary.slice(0, 100)}${effectiveExtraction.summary.length > 100 ? "..." : ""}"`
: "No meaningful summary available",
});
} else {
@@ -1293,14 +1380,14 @@ export async function generateMemoryAudit(
}
// ── Check 8: Pruning outcome ──────────────────────────────────────
if (pruningOutcome) {
if (effectivePruning) {
checks.push({
id: "pruning-applied",
name: "Memory pruning outcome",
passed: pruningOutcome.applied,
details: pruningOutcome.applied
? `Pruning applied: ${pruningOutcome.originalSize}${pruningOutcome.newSize} chars (${pruningOutcome.sizeDelta >= 0 ? "+" : ""}${pruningOutcome.sizeDelta} chars)`
: `Pruning skipped: ${pruningOutcome.reason}`,
passed: effectivePruning.applied,
details: effectivePruning.applied
? `Pruning applied: ${effectivePruning.originalSize}${effectivePruning.newSize} chars (${effectivePruning.sizeDelta >= 0 ? "+" : ""}${effectivePruning.sizeDelta} chars)`
: `Pruning skipped: ${effectivePruning.reason}`,
});
}
@@ -1332,15 +1419,15 @@ export async function generateMemoryAudit(
categories: categoryCounts,
lastUpdated,
},
extraction: lastExtraction
extraction: effectiveExtraction
? {
runAt: lastExtraction.runAt,
success: lastExtraction.success,
insightCount: lastExtraction.insightCount,
duplicateCount: lastExtraction.duplicateCount,
skippedCount: lastExtraction.skippedCount,
summary: lastExtraction.summary,
error: lastExtraction.error,
runAt: effectiveExtraction.runAt,
success: effectiveExtraction.success,
insightCount: effectiveExtraction.insightCount,
duplicateCount: effectiveExtraction.duplicateCount,
skippedCount: effectiveExtraction.skippedCount,
summary: effectiveExtraction.summary,
error: effectiveExtraction.error,
}
: {
runAt: "",
@@ -1350,7 +1437,7 @@ export async function generateMemoryAudit(
skippedCount: 0,
summary: "No extraction runs recorded",
},
pruning: pruningOutcome ?? {
pruning: effectivePruning ?? {
applied: false,
reason: "No pruning run recorded",
sizeDelta: 0,
@@ -1536,15 +1623,7 @@ export async function processAndAuditInsightExtraction(
input: ProcessRunInput,
): Promise<MemoryAuditReport> {
// Track extraction info for the audit
let extractionInfo: {
runAt: string;
success: boolean;
insightCount: number;
duplicateCount: number;
skippedCount: number;
summary: string;
error?: string;
};
let extractionInfo: MemoryExtractionMetadata;
let pruneOutcome: PruneOutcome | undefined;
try {
@@ -1585,6 +1664,19 @@ export async function processAndAuditInsightExtraction(
};
}
// Persist latest extraction/pruning state for future audit reads (best-effort)
try {
await writeMemoryAuditState(rootDir, {
extraction: extractionInfo,
pruning: pruneOutcome,
updatedAt: new Date().toISOString(),
});
} catch (err) {
console.error(
`[memory-audit] Failed to persist audit state: ${err instanceof Error ? err.message : String(err)}`,
);
}
// Generate the audit report with pruning info
const auditReport = await generateMemoryAudit(rootDir, extractionInfo, pruneOutcome);