decision-os-mcp 0.3.2 → 0.4.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.
@@ -1,551 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
- import { existsSync } from "fs";
3
- import { readFile, rm, mkdir } from "fs/promises";
4
- import { join } from "path";
5
- import { tmpdir } from "os";
6
- import { DecisionOSStorage } from "../src/storage.js";
7
-
8
- let storage: DecisionOSStorage;
9
- let testDir: string;
10
-
11
- beforeEach(async () => {
12
- testDir = join(tmpdir(), `decision-os-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
13
- await mkdir(testDir, { recursive: true });
14
- storage = new DecisionOSStorage(testDir);
15
- await storage.initialize();
16
- });
17
-
18
- afterEach(async () => {
19
- await rm(testDir, { recursive: true, force: true });
20
- });
21
-
22
- // ============================================================================
23
- // ACTIVE CASE PERSISTENCE
24
- // ============================================================================
25
-
26
- describe("active case persistence", () => {
27
- it("persists active case to .active-case file", async () => {
28
- const c = await storage.createCase({ title: "test case" });
29
- const activeCasePath = join(testDir, ".active-case");
30
- expect(existsSync(activeCasePath)).toBe(true);
31
-
32
- const persisted = (await readFile(activeCasePath, "utf-8")).trim();
33
- expect(persisted).toBe(c.id);
34
- });
35
-
36
- it("restores active case on re-initialization", async () => {
37
- const c = await storage.createCase({ title: "test case" });
38
-
39
- // Create a new storage instance pointing at same dir
40
- const storage2 = new DecisionOSStorage(testDir);
41
- await storage2.initialize();
42
-
43
- expect(storage2.getActiveCase()).toBe(c.id);
44
- });
45
-
46
- it("clears .active-case file when set to null", async () => {
47
- await storage.createCase({ title: "test case" });
48
- await storage.setActiveCase(null);
49
-
50
- const activeCasePath = join(testDir, ".active-case");
51
- expect(existsSync(activeCasePath)).toBe(false);
52
- });
53
-
54
- it("clears stale .active-case if case directory was deleted", async () => {
55
- const c = await storage.createCase({ title: "test case" });
56
-
57
- // Manually delete the case directory
58
- await rm(join(testDir, "cases", c.id), { recursive: true, force: true });
59
-
60
- // Re-initialize — should clear stale reference
61
- const storage2 = new DecisionOSStorage(testDir);
62
- await storage2.initialize();
63
-
64
- expect(storage2.getActiveCase()).toBeNull();
65
- expect(existsSync(join(testDir, ".active-case"))).toBe(false);
66
- });
67
-
68
- it("setActiveCase updates the persisted file", async () => {
69
- const c1 = await storage.createCase({ title: "first case" });
70
- const c2 = await storage.createCase({ title: "second case" });
71
-
72
- // c2 is now active (createCase sets it)
73
- const activeCasePath = join(testDir, ".active-case");
74
- const persisted = (await readFile(activeCasePath, "utf-8")).trim();
75
- expect(persisted).toBe(c2.id);
76
-
77
- // Switch back to c1
78
- await storage.setActiveCase(c1.id);
79
- const persisted2 = (await readFile(activeCasePath, "utf-8")).trim();
80
- expect(persisted2).toBe(c1.id);
81
- });
82
- });
83
-
84
- // ============================================================================
85
- // CASES & PRESSURE EVENTS (core mechanics)
86
- // ============================================================================
87
-
88
- describe("cases", () => {
89
- it("creates a case with auto-generated id", async () => {
90
- const c = await storage.createCase({ title: "Add tile caching" });
91
- expect(c.id).toMatch(/^\d{4}-add-tile-caching$/);
92
- expect(c.status).toBe("ACTIVE");
93
- expect(c.title).toBe("Add tile caching");
94
- });
95
-
96
- it("sets created case as active", async () => {
97
- const c = await storage.createCase({ title: "test" });
98
- expect(storage.getActiveCase()).toBe(c.id);
99
- });
100
-
101
- it("lists cases sorted by id", async () => {
102
- await storage.createCase({ title: "first" });
103
- await storage.createCase({ title: "second" });
104
- await storage.createCase({ title: "third" });
105
-
106
- const cases = await storage.listCases();
107
- expect(cases).toHaveLength(3);
108
- expect(cases[0].title).toBe("first");
109
- expect(cases[2].title).toBe("third");
110
- });
111
-
112
- it("closes a case with outcome signals", async () => {
113
- const c = await storage.createCase({ title: "test" });
114
- const result = await storage.closeCase(c.id, { regret: 1, notes: "could improve" });
115
-
116
- expect(result.case.status).toBe("COMPLETED");
117
- expect(result.case.signals?.outcome?.regret).toBe("1");
118
- expect(result.case.signals?.outcome?.notes).toBe("could improve");
119
- });
120
- });
121
-
122
- describe("pressure events", () => {
123
- it("logs a pressure event to the active case", async () => {
124
- const c = await storage.createCase({ title: "test" });
125
- const pe = await storage.logPressure({
126
- expected: "API returns 200",
127
- actual: "API returns 403",
128
- adaptation: "Added auth header",
129
- remember: "This API requires auth",
130
- });
131
-
132
- expect(pe.id).toMatch(/^PE-\d{4}$/);
133
- expect(pe.case_id).toBe(c.id);
134
- expect(pe.expected).toBe("API returns 200");
135
- });
136
-
137
- it("throws when logging without active case", async () => {
138
- await expect(
139
- storage.logPressure({
140
- expected: "x",
141
- actual: "y",
142
- adaptation: "z",
143
- remember: "w",
144
- })
145
- ).rejects.toThrow("No active case");
146
- });
147
-
148
- it("updates the case's pressure_events list", async () => {
149
- const c = await storage.createCase({ title: "test" });
150
- const pe = await storage.logPressure({
151
- expected: "x",
152
- actual: "y",
153
- adaptation: "z",
154
- remember: "w",
155
- });
156
-
157
- const updated = await storage.getCase(c.id);
158
- expect(updated?.pressure_events).toContain(pe.id);
159
- });
160
-
161
- it("searches pressure events by query", async () => {
162
- await storage.createCase({ title: "test" });
163
- await storage.logPressure({
164
- expected: "database query fast",
165
- actual: "query took 5 seconds",
166
- adaptation: "added index",
167
- remember: "Always index foreign keys",
168
- context_tags: ["DATABASE"],
169
- });
170
- await storage.logPressure({
171
- expected: "API returns JSON",
172
- actual: "API returns XML",
173
- adaptation: "added parser",
174
- remember: "Check content type",
175
- });
176
-
177
- const results = await storage.searchPressures("database");
178
- expect(results).toHaveLength(1);
179
- expect(results[0].remember).toBe("Always index foreign keys");
180
- });
181
- });
182
-
183
- // ============================================================================
184
- // AUTO-FORGET
185
- // ============================================================================
186
-
187
- describe("auto-forget", () => {
188
- it("forgets case with regret 0 and no PEs", async () => {
189
- const c = await storage.createCase({ title: "clean task" });
190
- const result = await storage.closeCase(c.id, { regret: 0 });
191
-
192
- expect(result.forgotten).toBe(true);
193
- expect(existsSync(join(testDir, "cases", c.id))).toBe(false);
194
- });
195
-
196
- it("forgets case with regret 0 and all PEs promoted", async () => {
197
- const c = await storage.createCase({ title: "learned task" });
198
- const pe = await storage.logPressure({
199
- expected: "x",
200
- actual: "y",
201
- adaptation: "z",
202
- remember: "w",
203
- context_tags: ["TEST"],
204
- });
205
-
206
- // Promote the PE to a foundation
207
- await storage.promoteToFoundation({
208
- title: "Test foundation",
209
- default_behavior: "Do the thing",
210
- context_tags: ["TEST"],
211
- source_pressures: [pe.id],
212
- });
213
-
214
- const result = await storage.closeCase(c.id, { regret: 0 });
215
- expect(result.forgotten).toBe(true);
216
- expect(existsSync(join(testDir, "cases", c.id))).toBe(false);
217
- });
218
-
219
- it("keeps case with regret 0 but unpromoted PEs", async () => {
220
- const c = await storage.createCase({ title: "has lessons" });
221
- await storage.logPressure({
222
- expected: "x",
223
- actual: "y",
224
- adaptation: "z",
225
- remember: "w",
226
- });
227
-
228
- const result = await storage.closeCase(c.id, { regret: 0 });
229
- expect(result.forgotten).toBe(false);
230
- expect(existsSync(join(testDir, "cases", c.id))).toBe(true);
231
- });
232
-
233
- it("keeps case with regret 1+", async () => {
234
- const c = await storage.createCase({ title: "regretful task" });
235
- const result = await storage.closeCase(c.id, { regret: 1 });
236
-
237
- expect(result.forgotten).toBe(false);
238
- expect(existsSync(join(testDir, "cases", c.id))).toBe(true);
239
- });
240
-
241
- it("keeps case with regret 2 even without PEs", async () => {
242
- const c = await storage.createCase({ title: "big regret" });
243
- const result = await storage.closeCase(c.id, { regret: 2 });
244
-
245
- expect(result.forgotten).toBe(false);
246
- expect(result.case.status).toBe("COMPLETED");
247
- });
248
-
249
- it("clears active case after forgetting", async () => {
250
- const c = await storage.createCase({ title: "clean task" });
251
- expect(storage.getActiveCase()).toBe(c.id);
252
-
253
- await storage.closeCase(c.id, { regret: 0 });
254
- expect(storage.getActiveCase()).toBeNull();
255
- });
256
- });
257
-
258
- // ============================================================================
259
- // SUGGEST REVIEW
260
- // ============================================================================
261
-
262
- describe("suggest_review", () => {
263
- it("returns empty review when no cases exist", async () => {
264
- const review = await storage.suggestReview();
265
- expect(review.foundation_candidates).toHaveLength(0);
266
- expect(review.blocking_forgetting).toHaveLength(0);
267
- expect(review.high_regret_no_pe).toHaveLength(0);
268
- expect(review.summary).toContain("Nothing to review");
269
- });
270
-
271
- it("identifies cases blocking forgetting", async () => {
272
- const c = await storage.createCase({ title: "blocking case" });
273
- await storage.logPressure({
274
- expected: "x",
275
- actual: "y",
276
- adaptation: "z",
277
- remember: "w",
278
- });
279
- await storage.closeCase(c.id, { regret: 0 });
280
-
281
- const review = await storage.suggestReview();
282
- expect(review.blocking_forgetting).toHaveLength(1);
283
- expect(review.blocking_forgetting[0].case_id).toBe(c.id);
284
- expect(review.blocking_forgetting[0].unpromoted_pe_count).toBe(1);
285
- });
286
-
287
- it("identifies high-regret cases with no PEs", async () => {
288
- const c = await storage.createCase({ title: "regretful case" });
289
- await storage.closeCase(c.id, { regret: 2 });
290
-
291
- const review = await storage.suggestReview();
292
- expect(review.high_regret_no_pe).toHaveLength(1);
293
- expect(review.high_regret_no_pe[0].case_id).toBe(c.id);
294
- expect(review.high_regret_no_pe[0].regret).toBe("2");
295
- });
296
-
297
- it("finds foundation candidates from clustered PEs", async () => {
298
- // Create two cases with PEs sharing context tags
299
- const c1 = await storage.createCase({ title: "case one" });
300
- await storage.logPressure({
301
- expected: "database fast",
302
- actual: "database slow",
303
- adaptation: "added index",
304
- remember: "Index foreign keys",
305
- context_tags: ["DATABASE", "PERFORMANCE"],
306
- });
307
- await storage.closeCase(c1.id, { regret: 1 });
308
-
309
- const c2 = await storage.createCase({ title: "case two" });
310
- await storage.logPressure({
311
- expected: "query returns quickly",
312
- actual: "query times out",
313
- adaptation: "optimized join",
314
- remember: "Watch N+1 queries",
315
- context_tags: ["DATABASE", "PERFORMANCE"],
316
- });
317
- await storage.closeCase(c2.id, { regret: 1 });
318
-
319
- const review = await storage.suggestReview();
320
- expect(review.foundation_candidates.length).toBeGreaterThanOrEqual(1);
321
-
322
- const dbCandidate = review.foundation_candidates.find(
323
- (fc) => fc.shared_tags.includes("DATABASE")
324
- );
325
- expect(dbCandidate).toBeDefined();
326
- expect(dbCandidate!.pressure_events).toHaveLength(2);
327
- });
328
-
329
- it("does not flag promoted PEs as candidates", async () => {
330
- const c = await storage.createCase({ title: "promoted case" });
331
- const pe = await storage.logPressure({
332
- expected: "x",
333
- actual: "y",
334
- adaptation: "z",
335
- remember: "w",
336
- context_tags: ["TEST"],
337
- });
338
-
339
- await storage.promoteToFoundation({
340
- title: "Test foundation",
341
- default_behavior: "Do the thing",
342
- context_tags: ["TEST"],
343
- source_pressures: [pe.id],
344
- });
345
- await storage.closeCase(c.id, { regret: 0 });
346
- // Case was forgotten (regret 0, all PEs promoted)
347
-
348
- const review = await storage.suggestReview();
349
- expect(review.foundation_candidates).toHaveLength(0);
350
- expect(review.blocking_forgetting).toHaveLength(0);
351
- });
352
- });
353
-
354
- // ============================================================================
355
- // FOUNDATIONS
356
- // ============================================================================
357
-
358
- describe("foundations", () => {
359
- it("promotes pressure events to a foundation", async () => {
360
- const c = await storage.createCase({ title: "test" });
361
- const pe = await storage.logPressure({
362
- expected: "x",
363
- actual: "y",
364
- adaptation: "z",
365
- remember: "w",
366
- context_tags: ["AUTH"],
367
- });
368
-
369
- const foundation = await storage.promoteToFoundation({
370
- title: "Auth requires token",
371
- default_behavior: "Always pass auth token",
372
- context_tags: ["AUTH"],
373
- source_pressures: [pe.id],
374
- });
375
-
376
- expect(foundation.id).toMatch(/^F-\d{4}$/);
377
- expect(foundation.confidence).toBe(1);
378
- expect(foundation.scope).toBe("PROJECT");
379
- });
380
-
381
- it("marks promoted PEs with foundation id", async () => {
382
- const c = await storage.createCase({ title: "test" });
383
- const pe = await storage.logPressure({
384
- expected: "x",
385
- actual: "y",
386
- adaptation: "z",
387
- remember: "w",
388
- });
389
-
390
- const foundation = await storage.promoteToFoundation({
391
- title: "Test",
392
- default_behavior: "Do it",
393
- context_tags: ["TEST"],
394
- source_pressures: [pe.id],
395
- });
396
-
397
- const pressures = await storage.getPressureEvents(c.id);
398
- expect(pressures[0].promoted_to_foundation).toBe(foundation.id);
399
- });
400
-
401
- it("filters foundations by context_tags", async () => {
402
- await storage.createCase({ title: "test" });
403
- const pe1 = await storage.logPressure({
404
- expected: "x", actual: "y", adaptation: "z", remember: "w",
405
- });
406
- const pe2 = await storage.logPressure({
407
- expected: "a", actual: "b", adaptation: "c", remember: "d",
408
- });
409
-
410
- await storage.promoteToFoundation({
411
- title: "Auth thing",
412
- default_behavior: "Do auth",
413
- context_tags: ["AUTH"],
414
- source_pressures: [pe1.id],
415
- });
416
- await storage.promoteToFoundation({
417
- title: "DB thing",
418
- default_behavior: "Do db",
419
- context_tags: ["DATABASE"],
420
- source_pressures: [pe2.id],
421
- });
422
-
423
- const authOnly = await storage.getFoundations({ context_tags: ["AUTH"] });
424
- expect(authOnly).toHaveLength(1);
425
- expect(authOnly[0].title).toBe("Auth thing");
426
- });
427
-
428
- it("filters foundations by min_confidence", async () => {
429
- await storage.createCase({ title: "test" });
430
- const pe = await storage.logPressure({
431
- expected: "x", actual: "y", adaptation: "z", remember: "w",
432
- });
433
-
434
- const f = await storage.promoteToFoundation({
435
- title: "Low confidence",
436
- default_behavior: "Maybe do this",
437
- context_tags: ["TEST"],
438
- source_pressures: [pe.id],
439
- });
440
-
441
- // Foundation starts at confidence 1
442
- const highConfidence = await storage.getFoundations({ min_confidence: 2 });
443
- expect(highConfidence).toHaveLength(0);
444
-
445
- const lowConfidence = await storage.getFoundations({ min_confidence: 1 });
446
- expect(lowConfidence).toHaveLength(1);
447
- });
448
-
449
- it("removes a foundation by id", async () => {
450
- await storage.createCase({ title: "test" });
451
- const pe1 = await storage.logPressure({
452
- expected: "x", actual: "y", adaptation: "z", remember: "w",
453
- });
454
- const pe2 = await storage.logPressure({
455
- expected: "a", actual: "b", adaptation: "c", remember: "d",
456
- });
457
-
458
- const f1 = await storage.promoteToFoundation({
459
- title: "Keep this one",
460
- default_behavior: "Stay",
461
- context_tags: ["KEEP"],
462
- source_pressures: [pe1.id],
463
- });
464
- const f2 = await storage.promoteToFoundation({
465
- title: "Remove this one",
466
- default_behavior: "Go away",
467
- context_tags: ["REMOVE"],
468
- source_pressures: [pe2.id],
469
- });
470
-
471
- const removed = await storage.removeFoundation(f2.id);
472
- expect(removed).toBe(true);
473
-
474
- const remaining = await storage.getFoundations();
475
- expect(remaining).toHaveLength(1);
476
- expect(remaining[0].id).toBe(f1.id);
477
- expect(remaining[0].title).toBe("Keep this one");
478
- });
479
-
480
- it("returns false when removing non-existent foundation", async () => {
481
- const removed = await storage.removeFoundation("F-9999");
482
- expect(removed).toBe(false);
483
- });
484
-
485
- it("returns false when no foundations file exists", async () => {
486
- // Fresh storage with no foundations file at all
487
- const removed = await storage.removeFoundation("F-0001");
488
- expect(removed).toBe(false);
489
- });
490
- });
491
-
492
- // ============================================================================
493
- // QUICK PRESSURE (via logPressure with defaults)
494
- // ============================================================================
495
-
496
- describe("quick pressure (logPressure with defaults)", () => {
497
- it("accepts minimal fields with defaults filled in", async () => {
498
- await storage.createCase({ title: "test" });
499
-
500
- // Simulate what quick_pressure does in index.ts
501
- const remember = "Expected: API fast… but: API slow…";
502
- const adaptation = "(captured for review)";
503
-
504
- const pe = await storage.logPressure({
505
- expected: "API fast",
506
- actual: "API slow",
507
- adaptation,
508
- remember,
509
- });
510
-
511
- expect(pe.adaptation).toBe("(captured for review)");
512
- expect(pe.remember).toContain("API fast");
513
- expect(pe.remember).toContain("API slow");
514
- });
515
- });
516
-
517
- // ============================================================================
518
- // POLICY CHECK
519
- // ============================================================================
520
-
521
- describe("policy check", () => {
522
- it("requires options comparison for high risk", () => {
523
- const result = storage.checkPolicy({ risk_level: "HIGH" });
524
- expect(result.require_options_comparison).toBe(true);
525
- expect(result.validation_level).toBe("STRICT");
526
- });
527
-
528
- it("returns BASIC for low risk", () => {
529
- const result = storage.checkPolicy({ risk_level: "LOW" });
530
- expect(result.require_options_comparison).toBe(false);
531
- expect(result.validation_level).toBe("BASIC");
532
- });
533
-
534
- it("returns STANDARD for medium risk", () => {
535
- const result = storage.checkPolicy({ risk_level: "MEDIUM" });
536
- expect(result.validation_level).toBe("STANDARD");
537
- });
538
-
539
- it("requires comparison for hard reversibility", () => {
540
- const result = storage.checkPolicy({ reversibility: "HARD" });
541
- expect(result.require_options_comparison).toBe(true);
542
- });
543
-
544
- it("requires STRICT for security boundary", () => {
545
- const result = storage.checkPolicy({
546
- affected_surface: ["SECURITY_BOUNDARY"],
547
- });
548
- expect(result.require_options_comparison).toBe(true);
549
- expect(result.validation_level).toBe("STRICT");
550
- });
551
- });
package/tsconfig.json DELETED
@@ -1,19 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "NodeNext",
5
- "moduleResolution": "NodeNext",
6
- "outDir": "./dist",
7
- "rootDir": "./src",
8
- "strict": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "forceConsistentCasingInFileNames": true,
12
- "declaration": true,
13
- "declarationMap": true,
14
- "sourceMap": true,
15
- "resolveJsonModule": true
16
- },
17
- "include": ["src/**/*"],
18
- "exclude": ["node_modules", "dist", "test"]
19
- }