coding-friend-cli 1.36.4 → 1.38.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 (71) hide show
  1. package/README.md +8 -3
  2. package/dist/{chunk-GZT3YPAP.js → chunk-32UAW3NX.js} +1 -1
  3. package/dist/chunk-5QCGEPQO.js +238 -0
  4. package/dist/{chunk-YIXAQ4R2.js → chunk-6NXZKAOV.js} +1 -1
  5. package/dist/{chunk-XHP3EBR3.js → chunk-776ZY62N.js} +112 -4
  6. package/dist/{chunk-N4WZWRCI.js → chunk-BXSCYGWE.js} +85 -37
  7. package/dist/{chunk-AK6RMCAC.js → chunk-CGEMETKQ.js} +44 -30
  8. package/dist/{chunk-XEKQW4RN.js → chunk-ETTZRJ55.js} +1 -1
  9. package/dist/{chunk-IKLMHJH6.js → chunk-GIH6E7GJ.js} +62 -1
  10. package/dist/{chunk-56YJWGAN.js → chunk-ONG4KRYP.js} +60 -47
  11. package/dist/{chunk-X2H7JJMR.js → chunk-PLSJW7QJ.js} +29 -1
  12. package/dist/{chunk-O5ZKLFSL.js → chunk-RCE4OWNX.js} +1 -1
  13. package/dist/{chunk-K4WSC62G.js → chunk-RFNKPAJW.js} +42 -2
  14. package/dist/{clean-Q4VU662V.js → clean-YWEMYDE5.js} +3 -2
  15. package/dist/{config-CTTHWJH5.js → config-NG7SICM4.js} +15 -27
  16. package/dist/{dev-IGHMMEP4.js → dev-PTBNMKZT.js} +3 -3
  17. package/dist/{disable-HL5OEURT.js → disable-MO7AXYYF.js} +20 -3
  18. package/dist/{enable-CZIOCKKJ.js → enable-4GXLQEHD.js} +20 -3
  19. package/dist/{guide-CO2V4ZLZ.js → guide-HCR43LJN.js} +1 -1
  20. package/dist/{host-NCJMUSME.js → host-EV6QIVTI.js} +3 -3
  21. package/dist/index.js +62 -48
  22. package/dist/{init-UABKVDVT.js → init-KYE5CJMW.js} +135 -17
  23. package/dist/{install-R2HDM3N4.js → install-XAFQY5YF.js} +80 -5
  24. package/dist/{learn-K64AUUFB.js → learn-MC4VDHZH.js} +2 -2
  25. package/dist/{mcp-OMQFSJP4.js → mcp-FV6YX3ZT.js} +24 -24
  26. package/dist/{mcp-serve-3FEPFVVQ.js → mcp-serve-PXREYQRZ.js} +10 -1
  27. package/dist/{mcp-serve-learn-OF3ZJUJC.js → mcp-serve-learn-UVXIDOEZ.js} +1 -1
  28. package/dist/{memory-Y6GNALG3.js → memory-DF7OUOTV.js} +7 -7
  29. package/dist/{permission-I3OFJUYA.js → permission-VISPGMUK.js} +38 -4
  30. package/dist/{permissions-J46VP6US.js → permissions-2SBACQHV.js} +2 -2
  31. package/dist/{session-Q2DLQPYP.js → session-QK6OZO3X.js} +3 -2
  32. package/dist/{status-ML4XEDYL.js → status-FVFTJ6GS.js} +18 -14
  33. package/dist/{statusline-KKFJPTI7.js → statusline-AOZHEQSV.js} +2 -2
  34. package/dist/{uninstall-ASLHT3MH.js → uninstall-MF6FRUBO.js} +64 -3
  35. package/dist/{update-DIOH45TE.js → update-QNUCSDSA.js} +7 -4
  36. package/dist/{update-check-25SCLHZO.js → update-check-7LKJLJPM.js} +1 -1
  37. package/lib/cf-memory/src/backends/sqlite/index.ts +142 -85
  38. package/lib/cf-memory/src/lib/claude-md.ts +82 -70
  39. package/lib/cf-memory/src/lib/status-frame.ts +2 -2
  40. package/lib/cf-memory/src/tools/delete.ts +1 -1
  41. package/lib/cf-memory/src/tools/store.ts +6 -6
  42. package/lib/cf-memory/src/tools/update.ts +5 -11
  43. package/package.json +32 -4
  44. package/lib/cf-memory/README.md +0 -317
  45. package/lib/cf-memory/src/__tests__/claude-md.test.ts +0 -343
  46. package/lib/cf-memory/src/__tests__/daemon-client.test.ts +0 -146
  47. package/lib/cf-memory/src/__tests__/daemon-e2e.test.ts +0 -223
  48. package/lib/cf-memory/src/__tests__/daemon.test.ts +0 -407
  49. package/lib/cf-memory/src/__tests__/dedup.test.ts +0 -103
  50. package/lib/cf-memory/src/__tests__/embeddings.test.ts +0 -292
  51. package/lib/cf-memory/src/__tests__/lazy-install.test.ts +0 -210
  52. package/lib/cf-memory/src/__tests__/markdown-backend.test.ts +0 -450
  53. package/lib/cf-memory/src/__tests__/migration.test.ts +0 -255
  54. package/lib/cf-memory/src/__tests__/migrations.test.ts +0 -288
  55. package/lib/cf-memory/src/__tests__/minisearch-backend.test.ts +0 -262
  56. package/lib/cf-memory/src/__tests__/ollama.test.ts +0 -48
  57. package/lib/cf-memory/src/__tests__/project-id.test.ts +0 -43
  58. package/lib/cf-memory/src/__tests__/schema.test.ts +0 -128
  59. package/lib/cf-memory/src/__tests__/search.test.ts +0 -115
  60. package/lib/cf-memory/src/__tests__/status-frame.test.ts +0 -136
  61. package/lib/cf-memory/src/__tests__/temporal-decay.test.ts +0 -54
  62. package/lib/cf-memory/src/__tests__/tier.test.ts +0 -367
  63. package/lib/cf-memory/src/__tests__/tools.test.ts +0 -83
  64. package/lib/cf-memory/vitest.config.ts +0 -7
  65. package/lib/learn-host/.prettierignore +0 -4
  66. package/lib/learn-host/.prettierrc +0 -8
  67. package/lib/learn-host/README.md +0 -113
  68. package/lib/learn-host/eslint.config.mjs +0 -6
  69. package/lib/learn-mcp/README.md +0 -187
  70. package/lib/learn-mcp/src/__tests__/docs.test.ts +0 -117
  71. package/lib/learn-mcp/vitest.config.ts +0 -8
@@ -1,450 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
- import { mkdirSync, rmSync, existsSync, readFileSync, readdirSync } 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("index_only=true returns Memory without writing when file exists", async () => {
89
- // Pre-create the file via normal store
90
- const original = await backend.store(sampleInput);
91
- const filePath = join(
92
- testDir,
93
- "features",
94
- "api-authentication-pattern.md",
95
- );
96
- expect(existsSync(filePath)).toBe(true);
97
-
98
- // Snapshot file content before index_only call
99
- const contentBefore = readFileSync(filePath, "utf-8");
100
-
101
- // Now store with index_only — should NOT create a second file
102
- const indexed = await backend.store({ ...sampleInput, index_only: true });
103
-
104
- // Should return clean slug (no timestamp suffix)
105
- expect(indexed.slug).toBe("api-authentication-pattern");
106
- expect(indexed.id).toBe("features/api-authentication-pattern");
107
- expect(indexed.category).toBe("features");
108
- expect(indexed.frontmatter.title).toBe(sampleInput.title);
109
- expect(indexed.frontmatter.type).toBe(sampleInput.type);
110
- expect(indexed.content).toBe(sampleInput.content);
111
-
112
- // Verify no duplicate file was created (only one .md file in features/)
113
- const files = readdirSync(join(testDir, "features")).filter((f: string) =>
114
- f.endsWith(".md"),
115
- );
116
- expect(files.length).toBe(1);
117
-
118
- // Verify existing file was not modified
119
- expect(readFileSync(filePath, "utf-8")).toBe(contentBefore);
120
- });
121
-
122
- it("index_only=true throws when file does not exist", async () => {
123
- await expect(
124
- backend.store({ ...sampleInput, index_only: true }),
125
- ).rejects.toThrow(/index_only.*file not found/i);
126
- });
127
-
128
- it("respects custom importance and source", async () => {
129
- const memory = await backend.store({
130
- ...sampleInput,
131
- importance: 5,
132
- source: "auto-capture",
133
- });
134
-
135
- expect(memory.frontmatter.importance).toBe(5);
136
- expect(memory.frontmatter.source).toBe("auto-capture");
137
- });
138
- });
139
-
140
- describe("retrieve()", () => {
141
- it("retrieves a stored memory by ID", async () => {
142
- await backend.store(sampleInput);
143
- const memory = await backend.retrieve(
144
- "features/api-authentication-pattern",
145
- );
146
-
147
- expect(memory).not.toBeNull();
148
- expect(memory!.frontmatter.title).toBe("API Authentication Pattern");
149
- expect(memory!.content).toContain("JWT tokens");
150
- });
151
-
152
- it("returns null for non-existent ID", async () => {
153
- const memory = await backend.retrieve("features/nonexistent");
154
- expect(memory).toBeNull();
155
- });
156
-
157
- it("returns null for invalid ID format", async () => {
158
- expect(await backend.retrieve("invalid")).toBeNull();
159
- expect(await backend.retrieve("")).toBeNull();
160
- });
161
- });
162
-
163
- describe("search()", () => {
164
- beforeEach(async () => {
165
- await backend.store(sampleInput);
166
- await backend.store({
167
- title: "Database Migration Guide",
168
- description: "How to run database migrations with Prisma",
169
- type: "procedure",
170
- tags: ["database", "prisma", "migration"],
171
- content: "# Migrations\n\nRun npx prisma migrate dev.",
172
- });
173
- await backend.store({
174
- title: "CORS Bug Fix",
175
- description: "Fixed CORS issue on /api/upload endpoint",
176
- type: "episode",
177
- tags: ["cors", "api", "bug"],
178
- content: "# CORS Fix\n\nAdded missing Origin header to allowlist.",
179
- });
180
- });
181
-
182
- it("finds by title match", async () => {
183
- const results = await backend.search({ query: "authentication" });
184
- expect(results.length).toBeGreaterThan(0);
185
- expect(results[0].memory.frontmatter.title).toContain("Authentication");
186
- expect(results[0].matchedOn).toContain("title");
187
- });
188
-
189
- it("finds by description match", async () => {
190
- const results = await backend.search({ query: "JWT" });
191
- expect(results.length).toBeGreaterThan(0);
192
- expect(results[0].matchedOn).toContain("description");
193
- });
194
-
195
- it("finds by tag match", async () => {
196
- const results = await backend.search({ query: "prisma" });
197
- expect(results.length).toBeGreaterThan(0);
198
- expect(results[0].matchedOn).toContain("tags");
199
- });
200
-
201
- it("finds by content match", async () => {
202
- const results = await backend.search({ query: "allowlist" });
203
- expect(results.length).toBeGreaterThan(0);
204
- expect(results[0].matchedOn).toContain("content");
205
- });
206
-
207
- it("filters by type", async () => {
208
- const results = await backend.search({
209
- query: "api",
210
- type: "episode",
211
- });
212
- expect(results.length).toBe(1);
213
- expect(results[0].memory.frontmatter.type).toBe("episode");
214
- });
215
-
216
- it("filters by tags", async () => {
217
- const results = await backend.search({
218
- query: "api",
219
- tags: ["cors"],
220
- });
221
- expect(results.length).toBe(1);
222
- expect(results[0].memory.frontmatter.title).toContain("CORS");
223
- });
224
-
225
- it("respects limit", async () => {
226
- const results = await backend.search({ query: "api", limit: 1 });
227
- expect(results.length).toBe(1);
228
- });
229
-
230
- it("ranks title matches higher than content matches", async () => {
231
- const results = await backend.search({ query: "api" });
232
- expect(results.length).toBeGreaterThan(1);
233
- // Title/description matches should score higher
234
- expect(results[0].score).toBeGreaterThanOrEqual(results[1].score);
235
- });
236
-
237
- it("returns empty array for no matches", async () => {
238
- const results = await backend.search({ query: "zzz_nonexistent_zzz" });
239
- expect(results).toEqual([]);
240
- });
241
- });
242
-
243
- describe("list()", () => {
244
- beforeEach(async () => {
245
- await backend.store(sampleInput);
246
- await backend.store({
247
- title: "Code Style",
248
- description: "Always use async/await",
249
- type: "preference",
250
- tags: ["style"],
251
- content: "Use async/await.",
252
- });
253
- });
254
-
255
- it("lists all memories", async () => {
256
- const metas = await backend.list({});
257
- expect(metas.length).toBe(2);
258
- });
259
-
260
- it("filters by type", async () => {
261
- const metas = await backend.list({ type: "fact" });
262
- expect(metas.length).toBe(1);
263
- expect(metas[0].frontmatter.type).toBe("fact");
264
- });
265
-
266
- it("filters by category", async () => {
267
- const metas = await backend.list({ category: "conventions" });
268
- expect(metas.length).toBe(1);
269
- expect(metas[0].category).toBe("conventions");
270
- });
271
-
272
- it("respects limit", async () => {
273
- const metas = await backend.list({ limit: 1 });
274
- expect(metas.length).toBe(1);
275
- });
276
-
277
- it("sorts by updated date descending", async () => {
278
- const metas = await backend.list({});
279
- const dates = metas.map((m) => m.frontmatter.updated);
280
- expect(dates[0]).toBe(dates[1]); // same day in tests
281
- });
282
-
283
- it("returns empty for non-existent docs dir", async () => {
284
- const emptyBackend = new MarkdownBackend("/tmp/nonexistent-cf-test");
285
- const metas = await emptyBackend.list({});
286
- expect(metas).toEqual([]);
287
- });
288
- });
289
-
290
- describe("update()", () => {
291
- it("updates title and tags", async () => {
292
- await backend.store(sampleInput);
293
- const updated = await backend.update({
294
- id: "features/api-authentication-pattern",
295
- title: "Updated Auth Pattern",
296
- tags: ["security", "oauth"],
297
- });
298
-
299
- expect(updated).not.toBeNull();
300
- expect(updated!.frontmatter.title).toBe("Updated Auth Pattern");
301
- // Tags are merged
302
- expect(updated!.frontmatter.tags).toContain("auth");
303
- expect(updated!.frontmatter.tags).toContain("security");
304
- expect(updated!.frontmatter.tags).toContain("oauth");
305
- });
306
-
307
- it("appends content", async () => {
308
- await backend.store(sampleInput);
309
- const updated = await backend.update({
310
- id: "features/api-authentication-pattern",
311
- content: "## New Section\n\nAdditional info.",
312
- });
313
-
314
- expect(updated).not.toBeNull();
315
- expect(updated!.content).toContain("JWT tokens");
316
- expect(updated!.content).toContain("New Section");
317
- });
318
-
319
- it("returns null for non-existent memory", async () => {
320
- const result = await backend.update({
321
- id: "features/nonexistent",
322
- title: "Test",
323
- });
324
- expect(result).toBeNull();
325
- });
326
- });
327
-
328
- describe("delete()", () => {
329
- it("deletes a stored memory", async () => {
330
- await backend.store(sampleInput);
331
- const deleted = await backend.delete(
332
- "features/api-authentication-pattern",
333
- );
334
- expect(deleted).toBe(true);
335
-
336
- const retrieved = await backend.retrieve(
337
- "features/api-authentication-pattern",
338
- );
339
- expect(retrieved).toBeNull();
340
- });
341
-
342
- it("returns false for non-existent memory", async () => {
343
- const deleted = await backend.delete("features/nonexistent");
344
- expect(deleted).toBe(false);
345
- });
346
- });
347
-
348
- describe("stats()", () => {
349
- it("returns correct counts", async () => {
350
- await backend.store(sampleInput);
351
- await backend.store({
352
- title: "Another Fact",
353
- description: "Test",
354
- type: "fact",
355
- tags: [],
356
- content: "Content",
357
- });
358
- await backend.store({
359
- title: "A Bug",
360
- description: "Test bug",
361
- type: "episode",
362
- tags: [],
363
- content: "Bug content",
364
- });
365
-
366
- const stats = await backend.stats();
367
- expect(stats.total).toBe(3);
368
- expect(stats.byCategory.features).toBe(2);
369
- expect(stats.byCategory.bugs).toBe(1);
370
- expect(stats.byType.fact).toBe(2);
371
- expect(stats.byType.episode).toBe(1);
372
- });
373
-
374
- it("returns zeros for empty dir", async () => {
375
- const stats = await backend.stats();
376
- expect(stats.total).toBe(0);
377
- });
378
- });
379
-
380
- describe("CRUD cycle", () => {
381
- it("store → retrieve → update → delete", async () => {
382
- // Store with unique title to avoid any collision
383
- const input = {
384
- ...sampleInput,
385
- title: "CRUD Test Memory " + Date.now(),
386
- };
387
- const stored = await backend.store(input);
388
- expect(stored.id).toBeTruthy();
389
-
390
- // Retrieve
391
- const retrieved = await backend.retrieve(stored.id);
392
- expect(retrieved).not.toBeNull();
393
- expect(retrieved!.frontmatter.title).toBe(input.title);
394
-
395
- // Update
396
- const updated = await backend.update({
397
- id: stored.id,
398
- title: "Updated Title",
399
- content: "Additional content.",
400
- });
401
- expect(updated!.frontmatter.title).toBe("Updated Title");
402
-
403
- // Verify update persisted
404
- const reread = await backend.retrieve(stored.id);
405
- expect(reread!.frontmatter.title).toBe("Updated Title");
406
- expect(reread!.content).toContain("Additional content");
407
-
408
- // Delete
409
- const deleted = await backend.delete(stored.id);
410
- expect(deleted).toBe(true);
411
-
412
- // Verify deleted
413
- const gone = await backend.retrieve(stored.id);
414
- expect(gone).toBeNull();
415
- });
416
- });
417
-
418
- describe("edge cases", () => {
419
- it("handles special characters in title", async () => {
420
- const memory = await backend.store({
421
- ...sampleInput,
422
- title: "What's the API/Auth flow? (v2.0)",
423
- });
424
- expect(memory.slug).toBe("what-s-the-api-auth-flow-v2-0");
425
- });
426
-
427
- it("handles empty docs directory", async () => {
428
- const metas = await backend.list({});
429
- expect(metas).toEqual([]);
430
-
431
- const results = await backend.search({ query: "anything" });
432
- expect(results).toEqual([]);
433
- });
434
-
435
- it("close() is a no-op", async () => {
436
- await expect(backend.close()).resolves.toBeUndefined();
437
- });
438
-
439
- it("rejects path traversal in retrieve/update/delete", async () => {
440
- await backend.store(sampleInput);
441
-
442
- expect(await backend.retrieve("../../etc/passwd")).toBeNull();
443
- expect(await backend.retrieve("features/../../../etc/passwd")).toBeNull();
444
- expect(
445
- await backend.update({ id: "../../etc/passwd", title: "hack" }),
446
- ).toBeNull();
447
- expect(await backend.delete("../../etc/passwd")).toBe(false);
448
- });
449
- });
450
- });
@@ -1,255 +0,0 @@
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
- });