coding-friend-cli 1.16.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +12 -0
  2. package/dist/{chunk-D4EWPGBL.js → chunk-C5LYVVEI.js} +1 -1
  3. package/dist/{chunk-X5WEODUD.js → chunk-CYQU33FY.js} +1 -0
  4. package/dist/{chunk-QNLL3ZDF.js → chunk-G6CEEMAR.js} +3 -3
  5. package/dist/{chunk-4DB4XTSL.js → chunk-KTX4MGMR.js} +15 -1
  6. package/dist/{chunk-KJUGTLPQ.js → chunk-YO6JKGR3.js} +38 -2
  7. package/dist/{config-AIZJJ5D2.js → config-LZFXXOI4.js} +276 -14
  8. package/dist/{dev-WJ5QQ35B.js → dev-R3IYWZ3M.js} +2 -2
  9. package/dist/{disable-JDVOQNZG.js → disable-R6K5YJN4.js} +2 -2
  10. package/dist/{enable-JBJ4Q2S7.js → enable-HF4PYVJN.js} +2 -2
  11. package/dist/{host-NA7LZ4HX.js → host-SYZH3FVC.js} +4 -4
  12. package/dist/index.js +78 -18
  13. package/dist/{init-FZ3GG53E.js → init-YK6YRTOT.js} +102 -6
  14. package/dist/{install-I3GOS56Q.js → install-Q4PWEU43.js} +4 -4
  15. package/dist/{mcp-DLS3J6QJ.js → mcp-TBEDYELW.js} +4 -4
  16. package/dist/memory-7RM67ZLS.js +668 -0
  17. package/dist/postinstall.js +1 -1
  18. package/dist/{session-E3CZJJZQ.js → session-H4XW2WXH.js} +1 -1
  19. package/dist/{statusline-6HQCDWBD.js → statusline-6Y2EBAFQ.js} +1 -1
  20. package/dist/{uninstall-JN5YIKKM.js → uninstall-3PSUDGI4.js} +3 -3
  21. package/dist/{update-OWS4IJTG.js → update-WL6SFGGO.js} +4 -4
  22. package/lib/cf-memory/CHANGELOG.md +15 -0
  23. package/lib/cf-memory/README.md +284 -0
  24. package/lib/cf-memory/package-lock.json +2790 -0
  25. package/lib/cf-memory/package.json +31 -0
  26. package/lib/cf-memory/scripts/migrate-frontmatter.ts +134 -0
  27. package/lib/cf-memory/src/__tests__/daemon-e2e.test.ts +223 -0
  28. package/lib/cf-memory/src/__tests__/daemon.test.ts +407 -0
  29. package/lib/cf-memory/src/__tests__/dedup.test.ts +103 -0
  30. package/lib/cf-memory/src/__tests__/embeddings.test.ts +292 -0
  31. package/lib/cf-memory/src/__tests__/lazy-install.test.ts +210 -0
  32. package/lib/cf-memory/src/__tests__/markdown-backend.test.ts +410 -0
  33. package/lib/cf-memory/src/__tests__/migration.test.ts +255 -0
  34. package/lib/cf-memory/src/__tests__/migrations.test.ts +288 -0
  35. package/lib/cf-memory/src/__tests__/minisearch-backend.test.ts +262 -0
  36. package/lib/cf-memory/src/__tests__/ollama.test.ts +48 -0
  37. package/lib/cf-memory/src/__tests__/schema.test.ts +128 -0
  38. package/lib/cf-memory/src/__tests__/search.test.ts +115 -0
  39. package/lib/cf-memory/src/__tests__/temporal-decay.test.ts +54 -0
  40. package/lib/cf-memory/src/__tests__/tier.test.ts +293 -0
  41. package/lib/cf-memory/src/__tests__/tools.test.ts +83 -0
  42. package/lib/cf-memory/src/backends/markdown.ts +318 -0
  43. package/lib/cf-memory/src/backends/minisearch.ts +203 -0
  44. package/lib/cf-memory/src/backends/sqlite/embeddings.ts +286 -0
  45. package/lib/cf-memory/src/backends/sqlite/index.ts +549 -0
  46. package/lib/cf-memory/src/backends/sqlite/migrations.ts +188 -0
  47. package/lib/cf-memory/src/backends/sqlite/schema.ts +120 -0
  48. package/lib/cf-memory/src/backends/sqlite/search.ts +296 -0
  49. package/lib/cf-memory/src/bin/cf-memory.ts +2 -0
  50. package/lib/cf-memory/src/daemon/entry.ts +99 -0
  51. package/lib/cf-memory/src/daemon/process.ts +220 -0
  52. package/lib/cf-memory/src/daemon/server.ts +166 -0
  53. package/lib/cf-memory/src/daemon/watcher.ts +90 -0
  54. package/lib/cf-memory/src/index.ts +45 -0
  55. package/lib/cf-memory/src/lib/backend.ts +23 -0
  56. package/lib/cf-memory/src/lib/daemon-client.ts +163 -0
  57. package/lib/cf-memory/src/lib/dedup.ts +80 -0
  58. package/lib/cf-memory/src/lib/lazy-install.ts +274 -0
  59. package/lib/cf-memory/src/lib/ollama.ts +76 -0
  60. package/lib/cf-memory/src/lib/temporal-decay.ts +19 -0
  61. package/lib/cf-memory/src/lib/tier.ts +107 -0
  62. package/lib/cf-memory/src/lib/types.ts +109 -0
  63. package/lib/cf-memory/src/resources/index.ts +62 -0
  64. package/lib/cf-memory/src/server.ts +20 -0
  65. package/lib/cf-memory/src/tools/delete.ts +38 -0
  66. package/lib/cf-memory/src/tools/list.ts +38 -0
  67. package/lib/cf-memory/src/tools/retrieve.ts +52 -0
  68. package/lib/cf-memory/src/tools/search.ts +47 -0
  69. package/lib/cf-memory/src/tools/store.ts +70 -0
  70. package/lib/cf-memory/src/tools/update.ts +62 -0
  71. package/lib/cf-memory/tsconfig.json +15 -0
  72. package/lib/cf-memory/vitest.config.ts +7 -0
  73. package/lib/learn-host/CHANGELOG.md +4 -0
  74. package/lib/learn-host/package.json +1 -1
  75. package/package.json +1 -1
@@ -0,0 +1,410 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdirSync, rmSync, existsSync, readFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { tmpdir } from "os";
5
+ import matter from "gray-matter";
6
+ import { MarkdownBackend } from "../backends/markdown.js";
7
+ import type { StoreInput } from "../lib/types.js";
8
+
9
+ let testDir: string;
10
+ let backend: MarkdownBackend;
11
+ let counter = 0;
12
+
13
+ beforeEach(() => {
14
+ testDir = join(tmpdir(), `cf-memory-test-${Date.now()}-${++counter}`);
15
+ mkdirSync(testDir, { recursive: true });
16
+ backend = new MarkdownBackend(testDir);
17
+ });
18
+
19
+ afterEach(() => {
20
+ rmSync(testDir, { recursive: true, force: true });
21
+ });
22
+
23
+ const sampleInput: StoreInput = {
24
+ title: "API Authentication Pattern",
25
+ description: "Auth module uses JWT tokens stored in httpOnly cookies",
26
+ type: "fact",
27
+ tags: ["auth", "jwt", "api"],
28
+ content: "# API Auth\n\nThe project uses JWT tokens with refresh rotation.",
29
+ };
30
+
31
+ describe("MarkdownBackend", () => {
32
+ describe("store()", () => {
33
+ it("creates a markdown file with correct frontmatter", async () => {
34
+ const memory = await backend.store(sampleInput);
35
+
36
+ expect(memory.id).toBe("features/api-authentication-pattern");
37
+ expect(memory.slug).toBe("api-authentication-pattern");
38
+ expect(memory.category).toBe("features");
39
+ expect(memory.frontmatter.title).toBe("API Authentication Pattern");
40
+ expect(memory.frontmatter.type).toBe("fact");
41
+ expect(memory.frontmatter.tags).toEqual(["auth", "jwt", "api"]);
42
+ expect(memory.frontmatter.importance).toBe(3);
43
+ expect(memory.frontmatter.source).toBe("conversation");
44
+
45
+ // Verify file on disk
46
+ const filePath = join(
47
+ testDir,
48
+ "features",
49
+ "api-authentication-pattern.md",
50
+ );
51
+ expect(existsSync(filePath)).toBe(true);
52
+
53
+ const raw = matter(readFileSync(filePath, "utf-8"));
54
+ expect(raw.data.title).toBe("API Authentication Pattern");
55
+ expect(raw.data.type).toBe("fact");
56
+ expect(raw.content.trim()).toContain("# API Auth");
57
+ });
58
+
59
+ it("maps type to correct category folder", async () => {
60
+ const types = [
61
+ { type: "fact", category: "features" },
62
+ { type: "preference", category: "conventions" },
63
+ { type: "context", category: "decisions" },
64
+ { type: "episode", category: "bugs" },
65
+ { type: "procedure", category: "infrastructure" },
66
+ ] as const;
67
+
68
+ for (const { type, category } of types) {
69
+ const memory = await backend.store({
70
+ ...sampleInput,
71
+ title: `Test ${type}`,
72
+ type,
73
+ });
74
+ expect(memory.category).toBe(category);
75
+ expect(existsSync(join(testDir, category))).toBe(true);
76
+ }
77
+ });
78
+
79
+ it("handles duplicate slugs by appending timestamp", async () => {
80
+ const m1 = await backend.store(sampleInput);
81
+ const m2 = await backend.store(sampleInput);
82
+
83
+ expect(m1.slug).toBe("api-authentication-pattern");
84
+ expect(m2.slug).not.toBe(m1.slug);
85
+ expect(m2.slug).toContain("api-authentication-pattern-");
86
+ });
87
+
88
+ it("respects custom importance and source", async () => {
89
+ const memory = await backend.store({
90
+ ...sampleInput,
91
+ importance: 5,
92
+ source: "auto-capture",
93
+ });
94
+
95
+ expect(memory.frontmatter.importance).toBe(5);
96
+ expect(memory.frontmatter.source).toBe("auto-capture");
97
+ });
98
+ });
99
+
100
+ describe("retrieve()", () => {
101
+ it("retrieves a stored memory by ID", async () => {
102
+ await backend.store(sampleInput);
103
+ const memory = await backend.retrieve(
104
+ "features/api-authentication-pattern",
105
+ );
106
+
107
+ expect(memory).not.toBeNull();
108
+ expect(memory!.frontmatter.title).toBe("API Authentication Pattern");
109
+ expect(memory!.content).toContain("JWT tokens");
110
+ });
111
+
112
+ it("returns null for non-existent ID", async () => {
113
+ const memory = await backend.retrieve("features/nonexistent");
114
+ expect(memory).toBeNull();
115
+ });
116
+
117
+ it("returns null for invalid ID format", async () => {
118
+ expect(await backend.retrieve("invalid")).toBeNull();
119
+ expect(await backend.retrieve("")).toBeNull();
120
+ });
121
+ });
122
+
123
+ describe("search()", () => {
124
+ beforeEach(async () => {
125
+ await backend.store(sampleInput);
126
+ await backend.store({
127
+ title: "Database Migration Guide",
128
+ description: "How to run database migrations with Prisma",
129
+ type: "procedure",
130
+ tags: ["database", "prisma", "migration"],
131
+ content: "# Migrations\n\nRun npx prisma migrate dev.",
132
+ });
133
+ await backend.store({
134
+ title: "CORS Bug Fix",
135
+ description: "Fixed CORS issue on /api/upload endpoint",
136
+ type: "episode",
137
+ tags: ["cors", "api", "bug"],
138
+ content: "# CORS Fix\n\nAdded missing Origin header to allowlist.",
139
+ });
140
+ });
141
+
142
+ it("finds by title match", async () => {
143
+ const results = await backend.search({ query: "authentication" });
144
+ expect(results.length).toBeGreaterThan(0);
145
+ expect(results[0].memory.frontmatter.title).toContain("Authentication");
146
+ expect(results[0].matchedOn).toContain("title");
147
+ });
148
+
149
+ it("finds by description match", async () => {
150
+ const results = await backend.search({ query: "JWT" });
151
+ expect(results.length).toBeGreaterThan(0);
152
+ expect(results[0].matchedOn).toContain("description");
153
+ });
154
+
155
+ it("finds by tag match", async () => {
156
+ const results = await backend.search({ query: "prisma" });
157
+ expect(results.length).toBeGreaterThan(0);
158
+ expect(results[0].matchedOn).toContain("tags");
159
+ });
160
+
161
+ it("finds by content match", async () => {
162
+ const results = await backend.search({ query: "allowlist" });
163
+ expect(results.length).toBeGreaterThan(0);
164
+ expect(results[0].matchedOn).toContain("content");
165
+ });
166
+
167
+ it("filters by type", async () => {
168
+ const results = await backend.search({
169
+ query: "api",
170
+ type: "episode",
171
+ });
172
+ expect(results.length).toBe(1);
173
+ expect(results[0].memory.frontmatter.type).toBe("episode");
174
+ });
175
+
176
+ it("filters by tags", async () => {
177
+ const results = await backend.search({
178
+ query: "api",
179
+ tags: ["cors"],
180
+ });
181
+ expect(results.length).toBe(1);
182
+ expect(results[0].memory.frontmatter.title).toContain("CORS");
183
+ });
184
+
185
+ it("respects limit", async () => {
186
+ const results = await backend.search({ query: "api", limit: 1 });
187
+ expect(results.length).toBe(1);
188
+ });
189
+
190
+ it("ranks title matches higher than content matches", async () => {
191
+ const results = await backend.search({ query: "api" });
192
+ expect(results.length).toBeGreaterThan(1);
193
+ // Title/description matches should score higher
194
+ expect(results[0].score).toBeGreaterThanOrEqual(results[1].score);
195
+ });
196
+
197
+ it("returns empty array for no matches", async () => {
198
+ const results = await backend.search({ query: "zzz_nonexistent_zzz" });
199
+ expect(results).toEqual([]);
200
+ });
201
+ });
202
+
203
+ describe("list()", () => {
204
+ beforeEach(async () => {
205
+ await backend.store(sampleInput);
206
+ await backend.store({
207
+ title: "Code Style",
208
+ description: "Always use async/await",
209
+ type: "preference",
210
+ tags: ["style"],
211
+ content: "Use async/await.",
212
+ });
213
+ });
214
+
215
+ it("lists all memories", async () => {
216
+ const metas = await backend.list({});
217
+ expect(metas.length).toBe(2);
218
+ });
219
+
220
+ it("filters by type", async () => {
221
+ const metas = await backend.list({ type: "fact" });
222
+ expect(metas.length).toBe(1);
223
+ expect(metas[0].frontmatter.type).toBe("fact");
224
+ });
225
+
226
+ it("filters by category", async () => {
227
+ const metas = await backend.list({ category: "conventions" });
228
+ expect(metas.length).toBe(1);
229
+ expect(metas[0].category).toBe("conventions");
230
+ });
231
+
232
+ it("respects limit", async () => {
233
+ const metas = await backend.list({ limit: 1 });
234
+ expect(metas.length).toBe(1);
235
+ });
236
+
237
+ it("sorts by updated date descending", async () => {
238
+ const metas = await backend.list({});
239
+ const dates = metas.map((m) => m.frontmatter.updated);
240
+ expect(dates[0]).toBe(dates[1]); // same day in tests
241
+ });
242
+
243
+ it("returns empty for non-existent docs dir", async () => {
244
+ const emptyBackend = new MarkdownBackend("/tmp/nonexistent-cf-test");
245
+ const metas = await emptyBackend.list({});
246
+ expect(metas).toEqual([]);
247
+ });
248
+ });
249
+
250
+ describe("update()", () => {
251
+ it("updates title and tags", async () => {
252
+ await backend.store(sampleInput);
253
+ const updated = await backend.update({
254
+ id: "features/api-authentication-pattern",
255
+ title: "Updated Auth Pattern",
256
+ tags: ["security", "oauth"],
257
+ });
258
+
259
+ expect(updated).not.toBeNull();
260
+ expect(updated!.frontmatter.title).toBe("Updated Auth Pattern");
261
+ // Tags are merged
262
+ expect(updated!.frontmatter.tags).toContain("auth");
263
+ expect(updated!.frontmatter.tags).toContain("security");
264
+ expect(updated!.frontmatter.tags).toContain("oauth");
265
+ });
266
+
267
+ it("appends content", async () => {
268
+ await backend.store(sampleInput);
269
+ const updated = await backend.update({
270
+ id: "features/api-authentication-pattern",
271
+ content: "## New Section\n\nAdditional info.",
272
+ });
273
+
274
+ expect(updated).not.toBeNull();
275
+ expect(updated!.content).toContain("JWT tokens");
276
+ expect(updated!.content).toContain("New Section");
277
+ });
278
+
279
+ it("returns null for non-existent memory", async () => {
280
+ const result = await backend.update({
281
+ id: "features/nonexistent",
282
+ title: "Test",
283
+ });
284
+ expect(result).toBeNull();
285
+ });
286
+ });
287
+
288
+ describe("delete()", () => {
289
+ it("deletes a stored memory", async () => {
290
+ await backend.store(sampleInput);
291
+ const deleted = await backend.delete(
292
+ "features/api-authentication-pattern",
293
+ );
294
+ expect(deleted).toBe(true);
295
+
296
+ const retrieved = await backend.retrieve(
297
+ "features/api-authentication-pattern",
298
+ );
299
+ expect(retrieved).toBeNull();
300
+ });
301
+
302
+ it("returns false for non-existent memory", async () => {
303
+ const deleted = await backend.delete("features/nonexistent");
304
+ expect(deleted).toBe(false);
305
+ });
306
+ });
307
+
308
+ describe("stats()", () => {
309
+ it("returns correct counts", async () => {
310
+ await backend.store(sampleInput);
311
+ await backend.store({
312
+ title: "Another Fact",
313
+ description: "Test",
314
+ type: "fact",
315
+ tags: [],
316
+ content: "Content",
317
+ });
318
+ await backend.store({
319
+ title: "A Bug",
320
+ description: "Test bug",
321
+ type: "episode",
322
+ tags: [],
323
+ content: "Bug content",
324
+ });
325
+
326
+ const stats = await backend.stats();
327
+ expect(stats.total).toBe(3);
328
+ expect(stats.byCategory.features).toBe(2);
329
+ expect(stats.byCategory.bugs).toBe(1);
330
+ expect(stats.byType.fact).toBe(2);
331
+ expect(stats.byType.episode).toBe(1);
332
+ });
333
+
334
+ it("returns zeros for empty dir", async () => {
335
+ const stats = await backend.stats();
336
+ expect(stats.total).toBe(0);
337
+ });
338
+ });
339
+
340
+ describe("CRUD cycle", () => {
341
+ it("store → retrieve → update → delete", async () => {
342
+ // Store with unique title to avoid any collision
343
+ const input = {
344
+ ...sampleInput,
345
+ title: "CRUD Test Memory " + Date.now(),
346
+ };
347
+ const stored = await backend.store(input);
348
+ expect(stored.id).toBeTruthy();
349
+
350
+ // Retrieve
351
+ const retrieved = await backend.retrieve(stored.id);
352
+ expect(retrieved).not.toBeNull();
353
+ expect(retrieved!.frontmatter.title).toBe(input.title);
354
+
355
+ // Update
356
+ const updated = await backend.update({
357
+ id: stored.id,
358
+ title: "Updated Title",
359
+ content: "Additional content.",
360
+ });
361
+ expect(updated!.frontmatter.title).toBe("Updated Title");
362
+
363
+ // Verify update persisted
364
+ const reread = await backend.retrieve(stored.id);
365
+ expect(reread!.frontmatter.title).toBe("Updated Title");
366
+ expect(reread!.content).toContain("Additional content");
367
+
368
+ // Delete
369
+ const deleted = await backend.delete(stored.id);
370
+ expect(deleted).toBe(true);
371
+
372
+ // Verify deleted
373
+ const gone = await backend.retrieve(stored.id);
374
+ expect(gone).toBeNull();
375
+ });
376
+ });
377
+
378
+ describe("edge cases", () => {
379
+ it("handles special characters in title", async () => {
380
+ const memory = await backend.store({
381
+ ...sampleInput,
382
+ title: "What's the API/Auth flow? (v2.0)",
383
+ });
384
+ expect(memory.slug).toBe("what-s-the-api-auth-flow-v2-0");
385
+ });
386
+
387
+ it("handles empty docs directory", async () => {
388
+ const metas = await backend.list({});
389
+ expect(metas).toEqual([]);
390
+
391
+ const results = await backend.search({ query: "anything" });
392
+ expect(results).toEqual([]);
393
+ });
394
+
395
+ it("close() is a no-op", async () => {
396
+ await expect(backend.close()).resolves.toBeUndefined();
397
+ });
398
+
399
+ it("rejects path traversal in retrieve/update/delete", async () => {
400
+ await backend.store(sampleInput);
401
+
402
+ expect(await backend.retrieve("../../etc/passwd")).toBeNull();
403
+ expect(await backend.retrieve("features/../../../etc/passwd")).toBeNull();
404
+ expect(
405
+ await backend.update({ id: "../../etc/passwd", title: "hack" }),
406
+ ).toBeNull();
407
+ expect(await backend.delete("../../etc/passwd")).toBe(false);
408
+ });
409
+ });
410
+ });
@@ -0,0 +1,255 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdirSync, rmSync, writeFileSync, readFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { tmpdir } from "os";
5
+ import { execFileSync } from "child_process";
6
+
7
+ let testDir: string;
8
+ let counter = 0;
9
+
10
+ const scriptPath = join(
11
+ import.meta.dirname,
12
+ "../../scripts/migrate-frontmatter.ts",
13
+ );
14
+
15
+ beforeEach(() => {
16
+ testDir = join(tmpdir(), `cf-memory-migration-${Date.now()}-${++counter}`);
17
+ mkdirSync(join(testDir, "features"), { recursive: true });
18
+ mkdirSync(join(testDir, "conventions"), { recursive: true });
19
+ mkdirSync(join(testDir, "decisions"), { recursive: true });
20
+ mkdirSync(join(testDir, "bugs"), { recursive: true });
21
+ });
22
+
23
+ afterEach(() => {
24
+ rmSync(testDir, { recursive: true, force: true });
25
+ });
26
+
27
+ function runMigration(): string {
28
+ return execFileSync("npx", ["tsx", scriptPath, testDir], {
29
+ encoding: "utf-8",
30
+ timeout: 15000,
31
+ });
32
+ }
33
+
34
+ describe("migrate-frontmatter script", () => {
35
+ it("adds type, importance, source fields to a features file", () => {
36
+ writeFileSync(
37
+ join(testDir, "features", "test.md"),
38
+ `---
39
+ title: "Test Feature"
40
+ description: "A test feature"
41
+ tags: [test, feature]
42
+ created: 2026-01-01
43
+ updated: 2026-03-05
44
+ ---
45
+
46
+ # Test Feature
47
+ Content here.
48
+ `,
49
+ );
50
+
51
+ const output = runMigration();
52
+ expect(output).toContain("Updated: features/test.md");
53
+
54
+ const content = readFileSync(join(testDir, "features", "test.md"), "utf-8");
55
+ expect(content).toContain("type: fact");
56
+ expect(content).toContain("importance: 3");
57
+ expect(content).toContain("source: conversation");
58
+ });
59
+
60
+ it("maps conventions to preference type", () => {
61
+ writeFileSync(
62
+ join(testDir, "conventions", "naming.md"),
63
+ `---
64
+ title: "Naming Convention"
65
+ description: "How we name things"
66
+ tags: [naming]
67
+ created: 2026-01-01
68
+ updated: 2026-01-01
69
+ ---
70
+
71
+ # Naming
72
+ `,
73
+ );
74
+
75
+ runMigration();
76
+
77
+ const content = readFileSync(
78
+ join(testDir, "conventions", "naming.md"),
79
+ "utf-8",
80
+ );
81
+ expect(content).toContain("type: preference");
82
+ });
83
+
84
+ it("maps decisions to context type", () => {
85
+ writeFileSync(
86
+ join(testDir, "decisions", "arch.md"),
87
+ `---
88
+ title: "Architecture Decision"
89
+ description: "Why we chose X"
90
+ tags: [architecture]
91
+ created: 2026-01-01
92
+ updated: 2026-01-01
93
+ ---
94
+
95
+ # Decision
96
+ `,
97
+ );
98
+
99
+ runMigration();
100
+
101
+ const content = readFileSync(
102
+ join(testDir, "decisions", "arch.md"),
103
+ "utf-8",
104
+ );
105
+ expect(content).toContain("type: context");
106
+ });
107
+
108
+ it("maps bugs to episode type", () => {
109
+ writeFileSync(
110
+ join(testDir, "bugs", "crash.md"),
111
+ `---
112
+ title: "App Crash"
113
+ description: "Fix for the crash bug"
114
+ tags: [bug]
115
+ created: 2026-01-01
116
+ updated: 2026-01-01
117
+ ---
118
+
119
+ # Bug
120
+ `,
121
+ );
122
+
123
+ runMigration();
124
+
125
+ const content = readFileSync(join(testDir, "bugs", "crash.md"), "utf-8");
126
+ expect(content).toContain("type: episode");
127
+ });
128
+
129
+ it("preserves existing content unchanged", () => {
130
+ const originalContent = "# Test\n\nSome **bold** content with `code`.";
131
+ writeFileSync(
132
+ join(testDir, "features", "preserve.md"),
133
+ `---
134
+ title: "Preserve Test"
135
+ description: "Should preserve content"
136
+ tags: [test]
137
+ created: 2026-01-01
138
+ updated: 2026-01-01
139
+ ---
140
+
141
+ ${originalContent}
142
+ `,
143
+ );
144
+
145
+ runMigration();
146
+
147
+ const content = readFileSync(
148
+ join(testDir, "features", "preserve.md"),
149
+ "utf-8",
150
+ );
151
+ expect(content).toContain(originalContent);
152
+ });
153
+
154
+ it("is idempotent — running twice produces same result", () => {
155
+ writeFileSync(
156
+ join(testDir, "features", "idempotent.md"),
157
+ `---
158
+ title: "Idempotent Test"
159
+ description: "Should be idempotent"
160
+ tags: [test]
161
+ created: 2026-01-01
162
+ updated: 2026-01-01
163
+ ---
164
+
165
+ # Test
166
+ `,
167
+ );
168
+
169
+ runMigration();
170
+ const afterFirst = readFileSync(
171
+ join(testDir, "features", "idempotent.md"),
172
+ "utf-8",
173
+ );
174
+
175
+ const output2 = runMigration();
176
+ const afterSecond = readFileSync(
177
+ join(testDir, "features", "idempotent.md"),
178
+ "utf-8",
179
+ );
180
+
181
+ expect(afterSecond).toBe(afterFirst);
182
+ expect(output2).toContain("Skipped: features/idempotent.md");
183
+ });
184
+
185
+ it("preserves date format (no ISO conversion)", () => {
186
+ writeFileSync(
187
+ join(testDir, "features", "dates.md"),
188
+ `---
189
+ title: "Date Test"
190
+ description: "Should preserve dates"
191
+ tags: [test]
192
+ created: 2026-01-01
193
+ updated: 2026-03-05
194
+ ---
195
+
196
+ # Test
197
+ `,
198
+ );
199
+
200
+ runMigration();
201
+
202
+ const content = readFileSync(
203
+ join(testDir, "features", "dates.md"),
204
+ "utf-8",
205
+ );
206
+ expect(content).toContain("created: 2026-01-01");
207
+ expect(content).toContain("updated: 2026-03-05");
208
+ expect(content).not.toContain("T00:00:00");
209
+ });
210
+
211
+ it("skips files that already have all fields", () => {
212
+ writeFileSync(
213
+ join(testDir, "features", "complete.md"),
214
+ `---
215
+ title: "Complete"
216
+ description: "Already complete"
217
+ type: fact
218
+ tags: [test]
219
+ importance: 4
220
+ created: 2026-01-01
221
+ updated: 2026-01-01
222
+ source: manual
223
+ ---
224
+
225
+ # Complete
226
+ `,
227
+ );
228
+
229
+ const output = runMigration();
230
+ expect(output).toContain("Skipped: features/complete.md");
231
+ });
232
+
233
+ it("handles root-level markdown files", () => {
234
+ writeFileSync(
235
+ join(testDir, "root-doc.md"),
236
+ `---
237
+ title: "Root Doc"
238
+ description: "A root level doc"
239
+ tags: [root]
240
+ created: 2026-01-01
241
+ updated: 2026-01-01
242
+ ---
243
+
244
+ # Root
245
+ `,
246
+ );
247
+
248
+ runMigration();
249
+
250
+ const content = readFileSync(join(testDir, "root-doc.md"), "utf-8");
251
+ expect(content).toContain("type: fact");
252
+ expect(content).toContain("importance: 3");
253
+ expect(content).toContain("source: conversation");
254
+ });
255
+ });