feat(FN-3199): align memory insight docs with canonical paths

Updates three documentation files to align memory insight and plugin path references with their canonical locations.

Fusion-Task-Id: FN-3199
This commit is contained in:
Fusion
2026-05-04 13:38:03 -07:00
committed by gsxdsm
parent 1f2c9dd043
commit eeab870857
8 changed files with 187 additions and 36 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Store generated memory insight artifacts under `.fusion/memory/` (`memory-insights.md`, `memory-audit.md`, and `memory-audit-state.json`) instead of top-level `.fusion/` files, with compatibility migration for existing legacy files.

View File

@@ -182,8 +182,8 @@ Use project memory for reusable patterns, constraints, and pitfalls that should
Fusion can automatically extract insights from memory and prune transient content. Enable via `insightExtractionEnabled` setting:
- `.fusion/memory/MEMORY.md` — Canonical long-term memory source (inside the layered `.fusion/memory/` workspace) compacted/pruned by extraction jobs
- `.fusion/memory-insights.md` — Distilled insights output
- `.fusion/memory-audit.md` — Audit report after each extraction (includes pruning outcome)
- `.fusion/memory/memory-insights.md` — Distilled insights output
- `.fusion/memory/memory-audit.md` — Audit report after each extraction (includes pruning outcome)
See [Settings Reference](./settings-reference.md#background-memory-summarization--audit) for configuration details.

View File

@@ -40,8 +40,8 @@ Fusion currently has two related but distinct memory systems:
| `MEMORY_DREAMS_FILENAME` | `DREAMS.md` | `memory-backend.ts` |
| `DEFAULT_MEMORY_BACKEND` | `qmd` | `memory-backend.ts` |
| `MEMORY_WORKING_PATH` | `.fusion/memory/MEMORY.md` | `memory-insights.ts` |
| `MEMORY_INSIGHTS_PATH` | `.fusion/memory-insights.md` | `memory-insights.ts` |
| `MEMORY_AUDIT_PATH` | `.fusion/memory-audit.md` | `memory-insights.ts` |
| `MEMORY_INSIGHTS_PATH` | `.fusion/memory/memory-insights.md` | `memory-insights.ts` |
| `MEMORY_AUDIT_PATH` | `.fusion/memory/memory-audit.md` | `memory-insights.ts` |
### 1.3 Exported Surface (Post-Migration)
@@ -264,8 +264,8 @@ Dashboard memory routes must remain rooted in project-scoped memory APIs:
Insight extraction is a separate subsystem that currently uses:
- Working source: `.fusion/memory/MEMORY.md`
- Insight output: `.fusion/memory-insights.md`
- Audit output: `.fusion/memory-audit.md`
- Insight output: `.fusion/memory/memory-insights.md`
- Audit output: `.fusion/memory/memory-audit.md`
It is related to, but not equivalent to, backend selection and prompt instruction logic.

View File

@@ -848,15 +848,15 @@ Fusion can automatically extract insights from project memory and prune transien
1. **Scheduled Extraction**: When `insightExtractionEnabled` is `true`, a background automation runs on the configured `insightExtractionSchedule` (default: daily at 2 AM).
2. **AI-Powered Analysis**: The automation uses an AI agent to read canonical long-term memory (`.fusion/memory/MEMORY.md`) from the layered `.fusion/memory/` workspace plus `.fusion/memory-insights.md`, extract new insights, and produce a pruned working memory candidate.
2. **AI-Powered Analysis**: The automation uses an AI agent to read canonical long-term memory (`.fusion/memory/MEMORY.md`) from the layered `.fusion/memory/` workspace plus `.fusion/memory/memory-insights.md`, extract new insights, and produce a pruned working memory candidate.
3. **Insight Merging**: New insights are automatically merged into `.fusion/memory-insights.md` under the appropriate category (Patterns, Principles, Conventions, Pitfalls, Context). Duplicates are skipped.
3. **Insight Merging**: New insights are automatically merged into `.fusion/memory/memory-insights.md` under the appropriate category (Patterns, Principles, Conventions, Pitfalls, Context). Duplicates are skipped.
4. **Memory Pruning**: The AI agent also produces a pruned version of working memory containing only durable items:
- **Preserved**: Architecture, Conventions, Pitfalls, Context sections with durable content
- **Pruned**: Task-specific notes, one-time observations, outdated entries
5. **Audit Report**: After each extraction run, a `.fusion/memory-audit.md` file is generated with:
5. **Audit Report**: After each extraction run, a `.fusion/memory/memory-audit.md` file is generated with:
- Working memory status (presence, size, sections)
- Insights memory status (insight counts by category)
- Last extraction results (success/failure, insight count, duplicates skipped)
@@ -870,8 +870,8 @@ Fusion can automatically extract insights from project memory and prune transien
|------|-------------|
| `.fusion/memory/MEMORY.md` | Long-term memory (updated when pruning is applied and validated) |
| Legacy top-level memory file | Deprecated migration fallback (compatibility only; not canonical storage) |
| `.fusion/memory-insights.md` | Long-term insights distilled from working memory |
| `.fusion/memory-audit.md` | Human-readable audit report after each extraction |
| `.fusion/memory/memory-insights.md` | Long-term insights distilled from working memory |
| `.fusion/memory/memory-audit.md` | Human-readable audit report after each extraction |
### Settings Interaction

View File

@@ -70,6 +70,18 @@ describe("memory-insights", () => {
const result = await readInsightsMemory(tempDir);
expect(result).toBeNull();
});
it("should migrate legacy top-level insights file on read", async () => {
const legacyPath = join(tempDir, ".fusion", "memory-insights.md");
const content = "# Legacy Insights\n\n- Keep this";
writeFileSync(legacyPath, content);
const result = await readInsightsMemory(tempDir);
expect(result).toBe(content);
expect(existsSync(join(tempDir, MEMORY_INSIGHTS_PATH))).toBe(true);
expect(existsSync(legacyPath)).toBe(false);
});
});
// ── writeInsightsMemory ──────────────────────────────────────────────
@@ -98,6 +110,16 @@ describe("memory-insights", () => {
await writeInsightsMemory(newDir, "test content");
expect(existsSync(join(newDir, MEMORY_INSIGHTS_PATH))).toBe(true);
});
it("should remove legacy top-level insights file after canonical write", async () => {
const legacyPath = join(tempDir, ".fusion", "memory-insights.md");
writeFileSync(legacyPath, "legacy");
await writeInsightsMemory(tempDir, "canonical");
expect(readFileSync(join(tempDir, MEMORY_INSIGHTS_PATH), "utf-8")).toBe("canonical");
expect(existsSync(legacyPath)).toBe(false);
});
});
// ── writeWorkingMemory ──────────────────────────────────────────────
@@ -613,7 +635,7 @@ describe("memory-insights", () => {
describe("constants", () => {
it("should have correct file paths", () => {
expect(MEMORY_WORKING_PATH).toBe(".fusion/memory/MEMORY.md");
expect(MEMORY_INSIGHTS_PATH).toBe(".fusion/memory-insights.md");
expect(MEMORY_INSIGHTS_PATH).toBe(".fusion/memory/memory-insights.md");
});
it("should have sensible defaults", () => {
@@ -657,6 +679,18 @@ describe("memory-insights audit file operations", () => {
const result = await readMemoryAudit(tempDir);
expect(result).toBe(content);
});
it("should migrate legacy top-level audit file on read", async () => {
const legacyPath = join(tempDir, ".fusion", "memory-audit.md");
const content = "# Legacy Audit";
writeFileSync(legacyPath, content);
const result = await readMemoryAudit(tempDir);
expect(result).toBe(content);
expect(existsSync(join(tempDir, MEMORY_AUDIT_PATH))).toBe(true);
expect(existsSync(legacyPath)).toBe(false);
});
});
describe("writeMemoryAudit", () => {
@@ -682,6 +716,16 @@ describe("memory-insights audit file operations", () => {
await writeMemoryAudit(newDir, "test content");
expect(existsSync(join(newDir, MEMORY_AUDIT_PATH))).toBe(true);
});
it("should remove legacy top-level audit file after canonical write", async () => {
const legacyPath = join(tempDir, ".fusion", "memory-audit.md");
writeFileSync(legacyPath, "legacy");
await writeMemoryAudit(tempDir, "canonical");
expect(readFileSync(join(tempDir, MEMORY_AUDIT_PATH), "utf-8")).toBe("canonical");
expect(existsSync(legacyPath)).toBe(false);
});
});
});
@@ -1123,6 +1167,33 @@ describe("memory-insights audit generation", () => {
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);
expect(existsSync(join(tempDir, ".fusion", "memory", "memory-audit-state.json"))).toBe(true);
});
it("migrates legacy top-level audit state to canonical memory directory", async () => {
const runAt = new Date().toISOString();
const legacyStatePath = join(tempDir, ".fusion", "memory-audit-state.json");
writeFileSync(
legacyStatePath,
JSON.stringify({
extraction: {
runAt,
success: true,
insightCount: 2,
duplicateCount: 0,
skippedCount: 0,
summary: "Legacy summary",
},
updatedAt: runAt,
}),
);
const report = await generateMemoryAudit(tempDir);
expect(report.extraction.runAt).toBe(runAt);
expect(report.extraction.summary).toBe("Legacy summary");
expect(existsSync(join(tempDir, ".fusion", "memory", "memory-audit-state.json"))).toBe(true);
expect(existsSync(legacyStatePath)).toBe(false);
});
it("should include pruning outcome in report", async () => {

View File

@@ -55,7 +55,7 @@
* - **Working memory** (`MEMORY.md`): Manual/agent-maintained. No automatic
* pruning — agents are expected to keep it relevant.
*
* - **Insights memory** (`memory-insights.md`): Only grows through
* - **Insights memory** (`.fusion/memory/memory-insights.md`): Only grows through
* extraction. New insights are merged with existing ones. Simple duplicate
* detection prevents re-adding the same insight.
*
@@ -64,7 +64,7 @@
* near-exact content match), it is skipped.
*/
import { readFile, writeFile, mkdir } from "node:fs/promises";
import { readFile, writeFile, mkdir, unlink } from "node:fs/promises";
import { existsSync } from "node:fs";
import { dirname, join } from "node:path";
import type { ProjectSettings } from "./types.js";
@@ -76,13 +76,17 @@ import type { ScheduledTaskCreateInput } from "./automation.js";
export const MEMORY_WORKING_PATH = ".fusion/memory/MEMORY.md";
/** Path to insights memory relative to project root. */
export const MEMORY_INSIGHTS_PATH = ".fusion/memory-insights.md";
export const MEMORY_INSIGHTS_PATH = ".fusion/memory/memory-insights.md";
/** Path to memory audit report relative to project root. */
export const MEMORY_AUDIT_PATH = ".fusion/memory-audit.md";
export const MEMORY_AUDIT_PATH = ".fusion/memory/memory-audit.md";
/** Path to persisted memory audit state (latest extraction/pruning metadata). */
export const MEMORY_AUDIT_STATE_PATH = ".fusion/memory-audit-state.json";
export const MEMORY_AUDIT_STATE_PATH = ".fusion/memory/memory-audit-state.json";
const LEGACY_MEMORY_INSIGHTS_PATH = ".fusion/memory-insights.md";
const LEGACY_MEMORY_AUDIT_PATH = ".fusion/memory-audit.md";
const LEGACY_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 * * *";
@@ -254,17 +258,60 @@ export async function readWorkingMemory(rootDir: string): Promise<string> {
return readFile(filePath, "utf-8");
}
async function migrateLegacyArtifactIfNeeded(
rootDir: string,
canonicalPath: string,
legacyPath: string,
): Promise<void> {
const canonicalFilePath = join(rootDir, canonicalPath);
const legacyFilePath = join(rootDir, legacyPath);
if (existsSync(canonicalFilePath) || !existsSync(legacyFilePath)) {
return;
}
const content = await readFile(legacyFilePath, "utf-8");
const canonicalDir = dirname(canonicalFilePath);
if (!existsSync(canonicalDir)) {
await mkdir(canonicalDir, { recursive: true });
}
await writeFile(canonicalFilePath, content, "utf-8");
try {
await unlink(legacyFilePath);
} catch {
// Best effort cleanup; canonical write already preserves data.
}
}
async function removeLegacyArtifactIfPresent(rootDir: string, legacyPath: string): Promise<void> {
const legacyFilePath = join(rootDir, legacyPath);
if (!existsSync(legacyFilePath)) {
return;
}
try {
await unlink(legacyFilePath);
} catch {
// Best effort cleanup; inability to delete should not block writes.
}
}
/**
* Read the insights memory file (`memory-insights.md`).
* Read the insights memory file (`.fusion/memory/memory-insights.md`).
*
* Returns `null` if the file does not exist, indicating that no insights
* have been extracted yet. The caller should treat this as "no prior
* extraction" and pass `null` to `buildInsightExtractionPrompt()`.
*
* Legacy compatibility: transparently migrates `.fusion/memory-insights.md`
* to the canonical memory workspace path on first read.
*
* @param rootDir - Absolute path to the project root directory.
* @returns The insights memory content, or null if not found.
*/
export async function readInsightsMemory(rootDir: string): Promise<string | null> {
await migrateLegacyArtifactIfNeeded(rootDir, MEMORY_INSIGHTS_PATH, LEGACY_MEMORY_INSIGHTS_PATH);
const filePath = join(rootDir, MEMORY_INSIGHTS_PATH);
if (!existsSync(filePath)) {
return null;
@@ -273,20 +320,21 @@ export async function readInsightsMemory(rootDir: string): Promise<string | null
}
/**
* Write the insights memory file (`memory-insights.md`).
* Write the insights memory file (`.fusion/memory/memory-insights.md`).
*
* Creates the `.fusion` directory if it does not exist.
* Creates the `.fusion/memory/` directory if it does not exist.
*
* @param rootDir - Absolute path to the project root directory.
* @param content - The markdown content to write.
*/
export async function writeInsightsMemory(rootDir: string, content: string): Promise<void> {
const filePath = join(rootDir, MEMORY_INSIGHTS_PATH);
const dir = join(rootDir, ".fusion");
const dir = dirname(filePath);
if (!existsSync(dir)) {
await mkdir(dir, { recursive: true });
}
await writeFile(filePath, content, "utf-8");
await removeLegacyArtifactIfPresent(rootDir, LEGACY_MEMORY_INSIGHTS_PATH);
}
/**
@@ -307,7 +355,7 @@ export async function writeWorkingMemory(rootDir: string, content: string): Prom
}
/**
* Read the memory audit file (`memory-audit.md`).
* Read the memory audit file (`.fusion/memory/memory-audit.md`).
*
* Returns `null` if the file does not exist.
*
@@ -315,6 +363,8 @@ export async function writeWorkingMemory(rootDir: string, content: string): Prom
* @returns The audit file content, or null if not found.
*/
export async function readMemoryAudit(rootDir: string): Promise<string | null> {
await migrateLegacyArtifactIfNeeded(rootDir, MEMORY_AUDIT_PATH, LEGACY_MEMORY_AUDIT_PATH);
const filePath = join(rootDir, MEMORY_AUDIT_PATH);
if (!existsSync(filePath)) {
return null;
@@ -323,28 +373,31 @@ export async function readMemoryAudit(rootDir: string): Promise<string | null> {
}
/**
* Write the memory audit file (`memory-audit.md`).
* Write the memory audit file (`.fusion/memory/memory-audit.md`).
*
* Creates the `.fusion` directory if it does not exist.
* Creates the `.fusion/memory/` directory if it does not exist.
*
* @param rootDir - Absolute path to the project root directory.
* @param content - The markdown content to write.
*/
export async function writeMemoryAudit(rootDir: string, content: string): Promise<void> {
const filePath = join(rootDir, MEMORY_AUDIT_PATH);
const dir = join(rootDir, ".fusion");
const dir = dirname(filePath);
if (!existsSync(dir)) {
await mkdir(dir, { recursive: true });
}
await writeFile(filePath, content, "utf-8");
await removeLegacyArtifactIfPresent(rootDir, LEGACY_MEMORY_AUDIT_PATH);
}
/**
* Read persisted memory audit state (`memory-audit-state.json`).
* Read persisted memory audit state (`.fusion/memory/memory-audit-state.json`).
*
* Returns `null` when no prior state exists.
*/
async function readMemoryAuditState(rootDir: string): Promise<MemoryAuditState | null> {
await migrateLegacyArtifactIfNeeded(rootDir, MEMORY_AUDIT_STATE_PATH, LEGACY_MEMORY_AUDIT_STATE_PATH);
const filePath = join(rootDir, MEMORY_AUDIT_STATE_PATH);
if (!existsSync(filePath)) {
return null;
@@ -370,16 +423,17 @@ async function readMemoryAuditState(rootDir: string): Promise<MemoryAuditState |
}
/**
* Persist memory audit state (`memory-audit-state.json`).
* Persist memory audit state (`.fusion/memory/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");
const dir = dirname(filePath);
if (!existsSync(dir)) {
await mkdir(dir, { recursive: true });
}
await writeFile(filePath, JSON.stringify(state, null, 2), "utf-8");
await removeLegacyArtifactIfPresent(rootDir, LEGACY_MEMORY_AUDIT_STATE_PATH);
}
function isValidExtractionMetadata(value: unknown): value is MemoryExtractionMetadata {
@@ -885,7 +939,7 @@ export function createInsightExtractionAutomation(
## Instructions
1. Read the working memory file at \`.fusion/memory/MEMORY.md\` using your file reading tools
2. Read the existing insights file at \`.fusion/memory-insights.md\` (it may not exist yet)
2. Read the existing insights file at \`.fusion/memory/memory-insights.md\` (it may not exist yet)
3. Analyze the working memory content and identify:
a) **New insights** that should be preserved in long-term memory
b) **Durable content** that should remain in working memory
@@ -1330,7 +1384,7 @@ export async function generateMemoryAudit(
id: "insights-memory-exists",
name: "Insights memory file exists",
passed: false,
details: "File .fusion/memory-insights.md does not exist yet",
details: "File .fusion/memory/memory-insights.md does not exist yet",
});
}

View File

@@ -1957,7 +1957,7 @@ export interface ProjectSettings {
/** When true, enables periodic AI-powered extraction of insights from working memory
* into a distilled long-term memory file. Creates an automation schedule that reads
* `.fusion/memory/MEMORY.md`, identifies patterns/principles/pitfalls, and writes to
* `.fusion/memory-insights.md`. Default: false. */
* `.fusion/memory/memory-insights.md`. Default: false. */
insightExtractionEnabled?: boolean;
/** Cron expression for insight extraction schedule. Only used when
* insightExtractionEnabled is true. Default: "0 2 * * *" (daily at 2 AM). */

View File

@@ -2230,7 +2230,7 @@ describe("GET /api/memory/insights", () => {
beforeEach(() => {
rootDir = mkdtempSync(join(tmpdir(), "fusion-memory-insights-"));
mkdirSync(join(rootDir, ".fusion"), { recursive: true });
mkdirSync(join(rootDir, ".fusion", "memory"), { recursive: true });
store = createMockStore({
getRootDir: vi.fn().mockReturnValue(rootDir),
});
@@ -2248,8 +2248,8 @@ describe("GET /api/memory/insights", () => {
}
it("returns 200 with content and exists:true when insights file exists", async () => {
// Insights file is at .fusion/memory-insights.md
writeFileSync(join(rootDir, ".fusion", "memory-insights.md"), "## Patterns\n- Pattern 1\n- Pattern 2");
// Insights file is at .fusion/memory/memory-insights.md
writeFileSync(join(rootDir, ".fusion", "memory", "memory-insights.md"), "## Patterns\n- Pattern 1\n- Pattern 2");
const res = await GET(buildApp(), "/api/memory/insights");
@@ -2303,8 +2303,8 @@ describe("PUT /api/memory/insights", () => {
expect(res.status).toBe(200);
expect(res.body).toHaveProperty("success", true);
// Verify file was written (insights file is .fusion/memory-insights.md)
const insightsPath = join(rootDir, ".fusion", "memory-insights.md");
// Verify file was written (insights file is .fusion/memory/memory-insights.md)
const insightsPath = join(rootDir, ".fusion", "memory", "memory-insights.md");
expect(existsSync(insightsPath)).toBe(true);
});
@@ -2376,6 +2376,24 @@ describe("POST /api/memory/extract", () => {
mkdirSync(join(rootDir, ".fusion", "memory"), { recursive: true });
writeFileSync(join(rootDir, ".fusion", "memory", "MEMORY.md"), "Working memory content for extraction that is long enough.");
const session = {
state: {
messages: [] as Array<{ role: string; content: string }>,
},
prompt: vi.fn(async function (this: { state: { messages: Array<{ role: string; content: string }> } }) {
const response = JSON.stringify({
summary: "Extracted insights",
insights: [{ category: "pattern", content: "Persist reusable conventions" }],
prunedMemory: "## Architecture\n\nDurable architecture notes.",
});
this.state.messages.push({ role: "assistant", content: response });
return response;
}),
dispose: vi.fn(),
};
vi.mocked(createFnAgent).mockResolvedValue({ session } as never);
const res = await REQUEST(
buildApp(),
"POST",
@@ -2389,6 +2407,9 @@ describe("POST /api/memory/extract", () => {
expect(res.body).toHaveProperty("summary");
expect(res.body).toHaveProperty("insightCount");
expect(res.body).toHaveProperty("pruned");
expect(existsSync(join(rootDir, ".fusion", "memory", "memory-insights.md"))).toBe(true);
expect(existsSync(join(rootDir, ".fusion", "memory", "memory-audit.md"))).toBe(true);
expect(existsSync(join(rootDir, ".fusion", "memory", "memory-audit-state.json"))).toBe(true);
});
});