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,223 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
- import { existsSync } from "fs";
3
- import { rm, mkdir, writeFile } from "fs/promises";
4
- import { join } from "path";
5
- import { tmpdir } from "os";
6
- import YAML from "yaml";
7
- import { HierarchicalDecisionOSStorage } from "../src/hierarchical-storage.js";
8
-
9
- let projectDir: string;
10
- let globalDir: string;
11
- let baseDir: string;
12
-
13
- /**
14
- * Helper to set up a project + global .decision-os structure in a temp dir.
15
- * We can't rely on ~ for tests, so we structure it manually.
16
- */
17
- async function setupLayers(): Promise<{
18
- storage: HierarchicalDecisionOSStorage;
19
- projectDosPath: string;
20
- globalDosPath: string;
21
- }> {
22
- baseDir = join(tmpdir(), `dos-hier-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
23
- projectDir = join(baseDir, "myproject");
24
- const projectDosPath = join(projectDir, ".decision-os");
25
- globalDir = join(baseDir, "global");
26
- const globalDosPath = join(globalDir, ".decision-os");
27
-
28
- // Create project .decision-os
29
- await mkdir(join(projectDosPath, "cases"), { recursive: true });
30
- await mkdir(join(projectDosPath, "defaults"), { recursive: true });
31
- await writeFile(
32
- join(projectDosPath, "config.yaml"),
33
- YAML.stringify({ project: "test-project", version: 1, scope: "PROJECT" })
34
- );
35
- await writeFile(
36
- join(projectDosPath, "defaults", "foundations.yaml"),
37
- YAML.stringify({ foundations: [] })
38
- );
39
-
40
- // Create global .decision-os (in a parent dir, not ~)
41
- await mkdir(join(globalDosPath, "cases"), { recursive: true });
42
- await mkdir(join(globalDosPath, "defaults"), { recursive: true });
43
- await writeFile(
44
- join(globalDosPath, "config.yaml"),
45
- YAML.stringify({ project: "_global", version: 1, scope: "GLOBAL" })
46
- );
47
- await writeFile(
48
- join(globalDosPath, "defaults", "foundations.yaml"),
49
- YAML.stringify({ foundations: [] })
50
- );
51
-
52
- // HierarchicalDecisionOSStorage discovers layers by walking up.
53
- // We test with just the project layer since global discovery uses homedir().
54
- const storage = new HierarchicalDecisionOSStorage(projectDir);
55
- await storage.initialize();
56
-
57
- return { storage, projectDosPath, globalDosPath };
58
- }
59
-
60
- afterEach(async () => {
61
- if (baseDir && existsSync(baseDir)) {
62
- await rm(baseDir, { recursive: true, force: true });
63
- }
64
- });
65
-
66
- // ============================================================================
67
- // LAYER DISCOVERY
68
- // ============================================================================
69
-
70
- describe("layer discovery", () => {
71
- it("discovers project .decision-os from workspace path", async () => {
72
- const { storage, projectDosPath } = await setupLayers();
73
- expect(storage.getProjectPath()).toBe(projectDosPath);
74
- });
75
-
76
- it("throws when no .decision-os found", async () => {
77
- const emptyDir = join(tmpdir(), `dos-empty-${Date.now()}`);
78
- await mkdir(emptyDir, { recursive: true });
79
-
80
- // This should throw since there's no .decision-os anywhere
81
- // (unless user has ~/.decision-os which we can't control in tests)
82
- try {
83
- const s = new HierarchicalDecisionOSStorage(emptyDir);
84
- // If it didn't throw, it found ~/.decision-os — that's fine
85
- await s.initialize();
86
- } catch (e) {
87
- expect((e as Error).message).toContain("No .decision-os found");
88
- }
89
-
90
- await rm(emptyDir, { recursive: true, force: true });
91
- });
92
- });
93
-
94
- // ============================================================================
95
- // FOUNDATION RELEVANCE RANKING
96
- // ============================================================================
97
-
98
- describe("foundation relevance ranking", () => {
99
- it("ranks foundations matching active case tags first", async () => {
100
- const { storage } = await setupLayers();
101
-
102
- // Create a case with specific affected_surface
103
- const c = await storage.createCase({
104
- title: "Database migration",
105
- signals: {
106
- context: {
107
- affected_surface: ["DATA_MODEL", "BACKEND"],
108
- },
109
- },
110
- });
111
-
112
- // Create two foundations — one relevant, one general
113
- const pe1 = await storage.logPressure({
114
- expected: "x", actual: "y", adaptation: "z", remember: "w",
115
- });
116
- const pe2 = await storage.logPressure({
117
- expected: "a", actual: "b", adaptation: "c", remember: "d",
118
- });
119
-
120
- await storage.promoteToFoundation({
121
- title: "UI pattern",
122
- default_behavior: "Use this UI pattern",
123
- context_tags: ["UI_UX", "FRONTEND"],
124
- source_pressures: [pe1.id],
125
- });
126
- await storage.promoteToFoundation({
127
- title: "Data modeling pattern",
128
- default_behavior: "Model data this way",
129
- context_tags: ["DATA_MODEL", "BACKEND"],
130
- source_pressures: [pe2.id],
131
- });
132
-
133
- const context = await storage.getContext();
134
-
135
- // Data model foundation should come first (matches case tags)
136
- expect(context.relevant_foundations[0].title).toBe("Data modeling pattern");
137
- expect((context.relevant_foundations[0] as any)._relevance).toBe("directly_relevant");
138
- expect((context.relevant_foundations[1] as any)._relevance).toBe("general");
139
- });
140
-
141
- it("returns all foundations when no active case", async () => {
142
- const { storage } = await setupLayers();
143
-
144
- const c = await storage.createCase({ title: "temp" });
145
- const pe = await storage.logPressure({
146
- expected: "x", actual: "y", adaptation: "z", remember: "w",
147
- });
148
- await storage.promoteToFoundation({
149
- title: "Some foundation",
150
- default_behavior: "Do it",
151
- context_tags: ["TEST"],
152
- source_pressures: [pe.id],
153
- });
154
- await storage.closeCase(c.id, { regret: 1 });
155
-
156
- // No active case now
157
- const context = await storage.getContext();
158
- expect(context.active_case).toBeNull();
159
- expect(context.relevant_foundations.length).toBeGreaterThanOrEqual(1);
160
- });
161
- });
162
-
163
- // ============================================================================
164
- // CLOSE CASE WITH FORGETTING (through hierarchical layer)
165
- // ============================================================================
166
-
167
- describe("close case with forgetting", () => {
168
- it("returns forgotten flag through hierarchical storage", async () => {
169
- const { storage } = await setupLayers();
170
-
171
- const c = await storage.createCase({ title: "clean task" });
172
- const result = await storage.closeCase(c.id, { regret: 0 });
173
-
174
- expect(result.forgotten).toBe(true);
175
- });
176
-
177
- it("preserves case with unpromoted PEs", async () => {
178
- const { storage } = await setupLayers();
179
-
180
- const c = await storage.createCase({ title: "has lessons" });
181
- await storage.logPressure({
182
- expected: "x", actual: "y", adaptation: "z", remember: "w",
183
- });
184
- const result = await storage.closeCase(c.id, { regret: 0 });
185
-
186
- expect(result.forgotten).toBe(false);
187
- });
188
- });
189
-
190
- // ============================================================================
191
- // SUGGEST REVIEW (through hierarchical layer)
192
- // ============================================================================
193
-
194
- describe("suggest review", () => {
195
- it("delegates suggest_review to project layer", async () => {
196
- const { storage } = await setupLayers();
197
-
198
- const c = await storage.createCase({ title: "test" });
199
- await storage.logPressure({
200
- expected: "x", actual: "y", adaptation: "z", remember: "w",
201
- });
202
- await storage.closeCase(c.id, { regret: 0 });
203
-
204
- const review = await storage.suggestReview();
205
- expect(review.blocking_forgetting).toHaveLength(1);
206
- });
207
- });
208
-
209
- // ============================================================================
210
- // ACTIVE CASE PERSISTENCE (through hierarchical layer)
211
- // ============================================================================
212
-
213
- describe("active case persistence via hierarchical storage", () => {
214
- it("persists active case through setActiveCase", async () => {
215
- const { storage } = await setupLayers();
216
-
217
- const c = await storage.createCase({ title: "test" });
218
- expect(storage.getActiveCase()).toBe(c.id);
219
-
220
- await storage.setActiveCase(null);
221
- expect(storage.getActiveCase()).toBeNull();
222
- });
223
- });