@mandujs/core 0.9.46 → 0.11.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.
- package/README.md +79 -10
- package/package.json +1 -1
- package/src/brain/doctor/config-analyzer.ts +498 -0
- package/src/brain/doctor/index.ts +10 -0
- package/src/change/snapshot.ts +46 -1
- package/src/change/types.ts +13 -0
- package/src/config/index.ts +9 -2
- package/src/config/mcp-ref.ts +348 -0
- package/src/config/mcp-status.ts +348 -0
- package/src/config/metadata.test.ts +308 -0
- package/src/config/metadata.ts +293 -0
- package/src/config/symbols.ts +144 -0
- package/src/config/validate.ts +122 -65
- package/src/config/watcher.ts +311 -0
- package/src/contract/index.ts +26 -25
- package/src/contract/protection.ts +364 -0
- package/src/error/domains.ts +265 -0
- package/src/error/index.ts +25 -13
- package/src/errors/extractor.ts +409 -0
- package/src/errors/index.ts +19 -0
- package/src/filling/context.ts +29 -1
- package/src/filling/deps.ts +238 -0
- package/src/filling/filling.ts +94 -8
- package/src/filling/index.ts +18 -0
- package/src/guard/analyzer.ts +7 -2
- package/src/guard/config-guard.ts +281 -0
- package/src/guard/decision-memory.test.ts +293 -0
- package/src/guard/decision-memory.ts +532 -0
- package/src/guard/healing.test.ts +259 -0
- package/src/guard/healing.ts +874 -0
- package/src/guard/index.ts +119 -0
- package/src/guard/negotiation.test.ts +282 -0
- package/src/guard/negotiation.ts +975 -0
- package/src/guard/semantic-slots.test.ts +379 -0
- package/src/guard/semantic-slots.ts +796 -0
- package/src/index.ts +4 -1
- package/src/lockfile/generate.ts +259 -0
- package/src/lockfile/index.ts +186 -0
- package/src/lockfile/lockfile.test.ts +410 -0
- package/src/lockfile/types.ts +184 -0
- package/src/lockfile/validate.ts +308 -0
- package/src/logging/index.ts +22 -0
- package/src/logging/transports.ts +365 -0
- package/src/plugins/index.ts +38 -0
- package/src/plugins/registry.ts +377 -0
- package/src/plugins/types.ts +363 -0
- package/src/runtime/security.ts +155 -0
- package/src/runtime/server.ts +318 -256
- package/src/runtime/session-key.ts +328 -0
- package/src/utils/differ.test.ts +342 -0
- package/src/utils/differ.ts +482 -0
- package/src/utils/hasher.test.ts +326 -0
- package/src/utils/hasher.ts +319 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/safe-io.ts +188 -0
- package/src/utils/string-safe.ts +298 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lockfile 시스템 테스트
|
|
3
|
+
*
|
|
4
|
+
* @see docs/plans/08_ont-run_adoption_plan.md - 섹션 7.2
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, it, beforeEach, afterEach } from "bun:test";
|
|
8
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import {
|
|
11
|
+
generateLockfile,
|
|
12
|
+
computeCurrentHashes,
|
|
13
|
+
} from "./generate.js";
|
|
14
|
+
import {
|
|
15
|
+
readLockfile,
|
|
16
|
+
writeLockfile,
|
|
17
|
+
deleteLockfile,
|
|
18
|
+
lockfileExists,
|
|
19
|
+
getLockfilePath,
|
|
20
|
+
LOCKFILE_PATH,
|
|
21
|
+
} from "./index.js";
|
|
22
|
+
import {
|
|
23
|
+
validateLockfile,
|
|
24
|
+
validateWithPolicy,
|
|
25
|
+
quickValidate,
|
|
26
|
+
isLockfileStale,
|
|
27
|
+
formatValidationResult,
|
|
28
|
+
} from "./validate.js";
|
|
29
|
+
import { LOCKFILE_SCHEMA_VERSION } from "./types.js";
|
|
30
|
+
|
|
31
|
+
// 테스트용 임시 디렉토리
|
|
32
|
+
const TEST_DIR = path.join(process.cwd(), ".test-lockfile-temp");
|
|
33
|
+
|
|
34
|
+
describe("generateLockfile", () => {
|
|
35
|
+
it("should generate lockfile with correct schema version", () => {
|
|
36
|
+
const config = { port: 3000, name: "test" };
|
|
37
|
+
const lockfile = generateLockfile(config);
|
|
38
|
+
|
|
39
|
+
expect(lockfile.schemaVersion).toBe(LOCKFILE_SCHEMA_VERSION);
|
|
40
|
+
expect(lockfile.configHash).toHaveLength(16);
|
|
41
|
+
expect(lockfile.generatedAt).toBeDefined();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should generate deterministic hash for same config", () => {
|
|
45
|
+
const config = { port: 3000, mcpServers: { a: { url: "..." } } };
|
|
46
|
+
|
|
47
|
+
const lockfile1 = generateLockfile(config);
|
|
48
|
+
const lockfile2 = generateLockfile(config);
|
|
49
|
+
|
|
50
|
+
expect(lockfile1.configHash).toBe(lockfile2.configHash);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should include MCP server hashes when enabled", () => {
|
|
54
|
+
const config = {
|
|
55
|
+
mcpServers: {
|
|
56
|
+
sequential: { command: "npx", args: ["-y", "@mcp/seq"] },
|
|
57
|
+
context7: { command: "npx", args: ["-y", "@mcp/c7"] },
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const lockfile = generateLockfile(config, { includeMcpServerHashes: true });
|
|
62
|
+
|
|
63
|
+
expect(lockfile.mcpServers).toBeDefined();
|
|
64
|
+
expect(lockfile.mcpServers?.sequential).toBeDefined();
|
|
65
|
+
expect(lockfile.mcpServers?.context7).toBeDefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should include snapshot when requested", () => {
|
|
69
|
+
const config = { port: 3000 };
|
|
70
|
+
|
|
71
|
+
const lockfile = generateLockfile(config, { includeSnapshot: true });
|
|
72
|
+
|
|
73
|
+
expect(lockfile.snapshot).toBeDefined();
|
|
74
|
+
expect(lockfile.snapshot?.config).toEqual(config);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should set mandu version", () => {
|
|
78
|
+
const config = { port: 3000 };
|
|
79
|
+
|
|
80
|
+
const lockfile = generateLockfile(config, { manduVersion: "1.2.3" });
|
|
81
|
+
|
|
82
|
+
expect(lockfile.manduVersion).toBe("1.2.3");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("computeCurrentHashes", () => {
|
|
87
|
+
it("should compute config hash", () => {
|
|
88
|
+
const config = { port: 3000 };
|
|
89
|
+
const { configHash } = computeCurrentHashes(config);
|
|
90
|
+
|
|
91
|
+
expect(configHash).toHaveLength(16);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should compute MCP config hash when present", () => {
|
|
95
|
+
const config = {
|
|
96
|
+
port: 3000,
|
|
97
|
+
mcpServers: { api: { url: "http://..." } },
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const { configHash, mcpConfigHash } = computeCurrentHashes(config);
|
|
101
|
+
|
|
102
|
+
expect(configHash).toHaveLength(16);
|
|
103
|
+
expect(mcpConfigHash).toHaveLength(16);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should not include MCP hash when no servers", () => {
|
|
107
|
+
const config = { port: 3000 };
|
|
108
|
+
const { mcpConfigHash } = computeCurrentHashes(config);
|
|
109
|
+
|
|
110
|
+
expect(mcpConfigHash).toBeUndefined();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("readLockfile / writeLockfile", () => {
|
|
115
|
+
beforeEach(async () => {
|
|
116
|
+
await mkdir(TEST_DIR, { recursive: true });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
afterEach(async () => {
|
|
120
|
+
await rm(TEST_DIR, { recursive: true, force: true });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should write and read lockfile", async () => {
|
|
124
|
+
const config = { port: 3000 };
|
|
125
|
+
const lockfile = generateLockfile(config);
|
|
126
|
+
|
|
127
|
+
await writeLockfile(TEST_DIR, lockfile);
|
|
128
|
+
const read = await readLockfile(TEST_DIR);
|
|
129
|
+
|
|
130
|
+
expect(read).not.toBeNull();
|
|
131
|
+
expect(read?.configHash).toBe(lockfile.configHash);
|
|
132
|
+
expect(read?.schemaVersion).toBe(LOCKFILE_SCHEMA_VERSION);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should return null when lockfile not found", async () => {
|
|
136
|
+
const read = await readLockfile(TEST_DIR);
|
|
137
|
+
expect(read).toBeNull();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should create .mandu directory if not exists", async () => {
|
|
141
|
+
const config = { port: 3000 };
|
|
142
|
+
const lockfile = generateLockfile(config);
|
|
143
|
+
|
|
144
|
+
await writeLockfile(TEST_DIR, lockfile);
|
|
145
|
+
|
|
146
|
+
const exists = await lockfileExists(TEST_DIR);
|
|
147
|
+
expect(exists).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("deleteLockfile", () => {
|
|
152
|
+
beforeEach(async () => {
|
|
153
|
+
await mkdir(TEST_DIR, { recursive: true });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
afterEach(async () => {
|
|
157
|
+
await rm(TEST_DIR, { recursive: true, force: true });
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("should delete existing lockfile", async () => {
|
|
161
|
+
const lockfile = generateLockfile({ port: 3000 });
|
|
162
|
+
await writeLockfile(TEST_DIR, lockfile);
|
|
163
|
+
|
|
164
|
+
const deleted = await deleteLockfile(TEST_DIR);
|
|
165
|
+
const exists = await lockfileExists(TEST_DIR);
|
|
166
|
+
|
|
167
|
+
expect(deleted).toBe(true);
|
|
168
|
+
expect(exists).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("should return false when lockfile not found", async () => {
|
|
172
|
+
const deleted = await deleteLockfile(TEST_DIR);
|
|
173
|
+
expect(deleted).toBe(false);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("validateLockfile", () => {
|
|
178
|
+
it("should pass validation when config matches", () => {
|
|
179
|
+
const config = { port: 3000, name: "test" };
|
|
180
|
+
const lockfile = generateLockfile(config);
|
|
181
|
+
|
|
182
|
+
const result = validateLockfile(config, lockfile);
|
|
183
|
+
|
|
184
|
+
expect(result.valid).toBe(true);
|
|
185
|
+
expect(result.errors).toHaveLength(0);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("should fail validation when config changed", () => {
|
|
189
|
+
const originalConfig = { port: 3000 };
|
|
190
|
+
const lockfile = generateLockfile(originalConfig);
|
|
191
|
+
|
|
192
|
+
const modifiedConfig = { port: 3001 };
|
|
193
|
+
const result = validateLockfile(modifiedConfig, lockfile);
|
|
194
|
+
|
|
195
|
+
expect(result.valid).toBe(false);
|
|
196
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
197
|
+
expect(result.errors[0].code).toBe("CONFIG_HASH_MISMATCH");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("should detect MCP server additions", () => {
|
|
201
|
+
const originalConfig = {
|
|
202
|
+
mcpServers: { a: { url: "..." } },
|
|
203
|
+
};
|
|
204
|
+
const lockfile = generateLockfile(originalConfig, {
|
|
205
|
+
includeMcpServerHashes: true,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const modifiedConfig = {
|
|
209
|
+
mcpServers: { a: { url: "..." }, b: { url: "..." } },
|
|
210
|
+
};
|
|
211
|
+
const result = validateLockfile(modifiedConfig, lockfile);
|
|
212
|
+
|
|
213
|
+
expect(result.warnings.some((w) => w.code === "MCP_SERVER_ADDED")).toBe(true);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("should detect MCP server removals", () => {
|
|
217
|
+
const originalConfig = {
|
|
218
|
+
mcpServers: { a: { url: "..." }, b: { url: "..." } },
|
|
219
|
+
};
|
|
220
|
+
const lockfile = generateLockfile(originalConfig, {
|
|
221
|
+
includeMcpServerHashes: true,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const modifiedConfig = {
|
|
225
|
+
mcpServers: { a: { url: "..." } },
|
|
226
|
+
};
|
|
227
|
+
const result = validateLockfile(modifiedConfig, lockfile);
|
|
228
|
+
|
|
229
|
+
expect(result.warnings.some((w) => w.code === "MCP_SERVER_REMOVED")).toBe(true);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("should include diff when snapshot available", () => {
|
|
233
|
+
const originalConfig = { port: 3000 };
|
|
234
|
+
const lockfile = generateLockfile(originalConfig, { includeSnapshot: true });
|
|
235
|
+
|
|
236
|
+
const modifiedConfig = { port: 3001 };
|
|
237
|
+
const result = validateLockfile(modifiedConfig, lockfile);
|
|
238
|
+
|
|
239
|
+
expect(result.diff).toBeDefined();
|
|
240
|
+
expect(result.diff?.hasChanges).toBe(true);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe("quickValidate", () => {
|
|
245
|
+
it("should return true for matching config", () => {
|
|
246
|
+
const config = { port: 3000 };
|
|
247
|
+
const lockfile = generateLockfile(config);
|
|
248
|
+
|
|
249
|
+
expect(quickValidate(config, lockfile)).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("should return false for modified config", () => {
|
|
253
|
+
const lockfile = generateLockfile({ port: 3000 });
|
|
254
|
+
|
|
255
|
+
expect(quickValidate({ port: 3001 }, lockfile)).toBe(false);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe("isLockfileStale", () => {
|
|
260
|
+
it("should return false for matching config", () => {
|
|
261
|
+
const config = { port: 3000 };
|
|
262
|
+
const lockfile = generateLockfile(config);
|
|
263
|
+
|
|
264
|
+
expect(isLockfileStale(config, lockfile)).toBe(false);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("should return true for modified config", () => {
|
|
268
|
+
const lockfile = generateLockfile({ port: 3000 });
|
|
269
|
+
|
|
270
|
+
expect(isLockfileStale({ port: 3001 }, lockfile)).toBe(true);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe("validateWithPolicy", () => {
|
|
275
|
+
it("should return pass action when valid", () => {
|
|
276
|
+
const config = { port: 3000 };
|
|
277
|
+
const lockfile = generateLockfile(config);
|
|
278
|
+
|
|
279
|
+
const { action } = validateWithPolicy(config, lockfile, "development");
|
|
280
|
+
|
|
281
|
+
expect(action).toBe("pass");
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("should return warn action in development mode", () => {
|
|
285
|
+
const lockfile = generateLockfile({ port: 3000 });
|
|
286
|
+
|
|
287
|
+
const { action } = validateWithPolicy({ port: 3001 }, lockfile, "development");
|
|
288
|
+
|
|
289
|
+
expect(action).toBe("warn");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("should return error action in build mode", () => {
|
|
293
|
+
const lockfile = generateLockfile({ port: 3000 });
|
|
294
|
+
|
|
295
|
+
const { action } = validateWithPolicy({ port: 3001 }, lockfile, "build");
|
|
296
|
+
|
|
297
|
+
expect(action).toBe("error");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("should return block action in production mode", () => {
|
|
301
|
+
const lockfile = generateLockfile({ port: 3000 });
|
|
302
|
+
|
|
303
|
+
const { action } = validateWithPolicy({ port: 3001 }, lockfile, "production");
|
|
304
|
+
|
|
305
|
+
expect(action).toBe("block");
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("should handle missing lockfile", () => {
|
|
309
|
+
const { result, action } = validateWithPolicy({ port: 3000 }, null, "development");
|
|
310
|
+
|
|
311
|
+
expect(result).toBeNull();
|
|
312
|
+
expect(action).toBe("warn");
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe("formatValidationResult", () => {
|
|
317
|
+
it("should format passing result", () => {
|
|
318
|
+
const config = { port: 3000 };
|
|
319
|
+
const lockfile = generateLockfile(config);
|
|
320
|
+
const result = validateLockfile(config, lockfile);
|
|
321
|
+
|
|
322
|
+
const formatted = formatValidationResult(result);
|
|
323
|
+
|
|
324
|
+
expect(formatted).toContain("✅");
|
|
325
|
+
expect(formatted).toContain("통과");
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("should format failing result", () => {
|
|
329
|
+
const lockfile = generateLockfile({ port: 3000 });
|
|
330
|
+
const result = validateLockfile({ port: 3001 }, lockfile);
|
|
331
|
+
|
|
332
|
+
const formatted = formatValidationResult(result);
|
|
333
|
+
|
|
334
|
+
expect(formatted).toContain("❌");
|
|
335
|
+
expect(formatted).toContain("실패");
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe("getLockfilePath", () => {
|
|
340
|
+
it("should return correct path", () => {
|
|
341
|
+
const projectRoot = "/my/project";
|
|
342
|
+
const lockfilePath = getLockfilePath(projectRoot);
|
|
343
|
+
|
|
344
|
+
expect(lockfilePath).toBe(path.join(projectRoot, LOCKFILE_PATH));
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
describe("real-world scenarios", () => {
|
|
349
|
+
beforeEach(async () => {
|
|
350
|
+
await mkdir(TEST_DIR, { recursive: true });
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
afterEach(async () => {
|
|
354
|
+
await rm(TEST_DIR, { recursive: true, force: true });
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("should handle full lockfile workflow", async () => {
|
|
358
|
+
// 1. 초기 설정으로 lockfile 생성
|
|
359
|
+
const initialConfig = {
|
|
360
|
+
name: "my-project",
|
|
361
|
+
port: 3000,
|
|
362
|
+
mcpServers: {
|
|
363
|
+
sequential: { command: "npx", args: ["-y", "@mcp/seq"] },
|
|
364
|
+
},
|
|
365
|
+
features: { islands: true },
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const lockfile = generateLockfile(initialConfig, {
|
|
369
|
+
manduVersion: "0.9.46",
|
|
370
|
+
includeSnapshot: true,
|
|
371
|
+
includeMcpServerHashes: true,
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
await writeLockfile(TEST_DIR, lockfile);
|
|
375
|
+
|
|
376
|
+
// 2. 동일 설정으로 검증 - 통과해야 함
|
|
377
|
+
const readLock = await readLockfile(TEST_DIR);
|
|
378
|
+
expect(readLock).not.toBeNull();
|
|
379
|
+
|
|
380
|
+
const validResult = validateLockfile(initialConfig, readLock!);
|
|
381
|
+
expect(validResult.valid).toBe(true);
|
|
382
|
+
|
|
383
|
+
// 3. 설정 변경 후 검증 - 실패해야 함
|
|
384
|
+
const modifiedConfig = {
|
|
385
|
+
...initialConfig,
|
|
386
|
+
port: 3001,
|
|
387
|
+
mcpServers: {
|
|
388
|
+
sequential: { command: "npx", args: ["-y", "@mcp/seq"] },
|
|
389
|
+
context7: { command: "npx", args: ["-y", "@mcp/c7"] },
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const invalidResult = validateLockfile(modifiedConfig, readLock!);
|
|
394
|
+
expect(invalidResult.valid).toBe(false);
|
|
395
|
+
expect(invalidResult.diff).toBeDefined();
|
|
396
|
+
|
|
397
|
+
// 4. 새 lockfile 생성으로 갱신
|
|
398
|
+
const newLockfile = generateLockfile(modifiedConfig, {
|
|
399
|
+
manduVersion: "0.9.46",
|
|
400
|
+
includeSnapshot: true,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
await writeLockfile(TEST_DIR, newLockfile);
|
|
404
|
+
|
|
405
|
+
// 5. 갱신 후 검증 - 통과해야 함
|
|
406
|
+
const updatedLock = await readLockfile(TEST_DIR);
|
|
407
|
+
const finalResult = validateLockfile(modifiedConfig, updatedLock!);
|
|
408
|
+
expect(finalResult.valid).toBe(true);
|
|
409
|
+
});
|
|
410
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Lockfile 타입 정의 🔒
|
|
3
|
+
*
|
|
4
|
+
* ont-run의 lockfile 패턴을 참고
|
|
5
|
+
* @see DNA/ont-run/src/lockfile/types.ts
|
|
6
|
+
* @see docs/plans/08_ont-run_adoption_plan.md - 섹션 3.4
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ConfigDiff } from "../utils/differ.js";
|
|
10
|
+
|
|
11
|
+
// ============================================
|
|
12
|
+
// Lockfile 스키마
|
|
13
|
+
// ============================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Mandu Lockfile 구조
|
|
17
|
+
*
|
|
18
|
+
* 위치: .mandu/lockfile.json
|
|
19
|
+
*/
|
|
20
|
+
export interface ManduLockfile {
|
|
21
|
+
/** Lockfile 스키마 버전 (하위 호환성 관리) */
|
|
22
|
+
schemaVersion: 1;
|
|
23
|
+
|
|
24
|
+
/** mandu 버전 */
|
|
25
|
+
manduVersion: string;
|
|
26
|
+
|
|
27
|
+
/** mandu.config 해시 (16자 hex) */
|
|
28
|
+
configHash: string;
|
|
29
|
+
|
|
30
|
+
/** .mcp.json 해시 (선택적) */
|
|
31
|
+
mcpConfigHash?: string;
|
|
32
|
+
|
|
33
|
+
/** 생성 시각 (ISO 8601) */
|
|
34
|
+
generatedAt: string;
|
|
35
|
+
|
|
36
|
+
/** 생성 환경 */
|
|
37
|
+
environment?: "development" | "production" | "ci";
|
|
38
|
+
|
|
39
|
+
/** MCP 서버별 해시 (선택적) */
|
|
40
|
+
mcpServers?: Record<
|
|
41
|
+
string,
|
|
42
|
+
{
|
|
43
|
+
/** 서버 설정 해시 */
|
|
44
|
+
hash: string;
|
|
45
|
+
/** 서버 버전 (있는 경우) */
|
|
46
|
+
version?: string;
|
|
47
|
+
}
|
|
48
|
+
>;
|
|
49
|
+
|
|
50
|
+
/** 설정 스냅샷 (선택적, 디버깅용) */
|
|
51
|
+
snapshot?: {
|
|
52
|
+
/** 정규화된 설정 객체 */
|
|
53
|
+
config: Record<string, unknown>;
|
|
54
|
+
/** 스냅샷 생성 환경 */
|
|
55
|
+
environment: string;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ============================================
|
|
60
|
+
// 검증 결과
|
|
61
|
+
// ============================================
|
|
62
|
+
|
|
63
|
+
export type LockfileErrorCode =
|
|
64
|
+
| "LOCKFILE_NOT_FOUND"
|
|
65
|
+
| "LOCKFILE_PARSE_ERROR"
|
|
66
|
+
| "LOCKFILE_SCHEMA_MISMATCH"
|
|
67
|
+
| "CONFIG_HASH_MISMATCH"
|
|
68
|
+
| "MCP_CONFIG_HASH_MISMATCH"
|
|
69
|
+
| "MANDU_VERSION_MISMATCH";
|
|
70
|
+
|
|
71
|
+
export interface LockfileError {
|
|
72
|
+
code: LockfileErrorCode;
|
|
73
|
+
message: string;
|
|
74
|
+
details?: Record<string, unknown>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type LockfileWarningCode =
|
|
78
|
+
| "LOCKFILE_OUTDATED"
|
|
79
|
+
| "MCP_SERVER_ADDED"
|
|
80
|
+
| "MCP_SERVER_REMOVED"
|
|
81
|
+
| "SNAPSHOT_MISSING";
|
|
82
|
+
|
|
83
|
+
export interface LockfileWarning {
|
|
84
|
+
code: LockfileWarningCode;
|
|
85
|
+
message: string;
|
|
86
|
+
details?: Record<string, unknown>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface LockfileValidationResult {
|
|
90
|
+
/** 검증 통과 여부 */
|
|
91
|
+
valid: boolean;
|
|
92
|
+
/** 심각한 오류 목록 */
|
|
93
|
+
errors: LockfileError[];
|
|
94
|
+
/** 경고 목록 */
|
|
95
|
+
warnings: LockfileWarning[];
|
|
96
|
+
/** 변경사항 (불일치 시) */
|
|
97
|
+
diff?: ConfigDiff;
|
|
98
|
+
/** 현재 설정 해시 */
|
|
99
|
+
currentHash?: string;
|
|
100
|
+
/** lockfile의 해시 */
|
|
101
|
+
lockedHash?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ============================================
|
|
105
|
+
// 환경별 동작 정책
|
|
106
|
+
// ============================================
|
|
107
|
+
|
|
108
|
+
export type LockfileMode = "development" | "build" | "ci" | "production";
|
|
109
|
+
|
|
110
|
+
export interface LockfilePolicyOptions {
|
|
111
|
+
/** 현재 모드 */
|
|
112
|
+
mode: LockfileMode;
|
|
113
|
+
/** lockfile 불일치 시 동작 */
|
|
114
|
+
onMismatch: "warn" | "error" | "block";
|
|
115
|
+
/** lockfile 없을 때 동작 */
|
|
116
|
+
onMissing: "warn" | "error" | "create" | "block";
|
|
117
|
+
/** 우회 허용 여부 */
|
|
118
|
+
allowBypass: boolean;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 환경별 기본 정책
|
|
123
|
+
*
|
|
124
|
+
* - dev: 불일치 시 경고만
|
|
125
|
+
* - build/ci: 불일치 시 실패
|
|
126
|
+
* - prod: 불일치 시 서버 시작 차단
|
|
127
|
+
*/
|
|
128
|
+
export const DEFAULT_POLICIES: Record<LockfileMode, LockfilePolicyOptions> = {
|
|
129
|
+
development: {
|
|
130
|
+
mode: "development",
|
|
131
|
+
onMismatch: "warn",
|
|
132
|
+
onMissing: "warn",
|
|
133
|
+
allowBypass: true,
|
|
134
|
+
},
|
|
135
|
+
build: {
|
|
136
|
+
mode: "build",
|
|
137
|
+
onMismatch: "error",
|
|
138
|
+
onMissing: "error",
|
|
139
|
+
allowBypass: true,
|
|
140
|
+
},
|
|
141
|
+
ci: {
|
|
142
|
+
mode: "ci",
|
|
143
|
+
onMismatch: "error",
|
|
144
|
+
onMissing: "error",
|
|
145
|
+
allowBypass: false,
|
|
146
|
+
},
|
|
147
|
+
production: {
|
|
148
|
+
mode: "production",
|
|
149
|
+
onMismatch: "block",
|
|
150
|
+
onMissing: "block",
|
|
151
|
+
allowBypass: true, // MANDU_LOCK_BYPASS=1로 긴급 우회
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// ============================================
|
|
156
|
+
// 생성 옵션
|
|
157
|
+
// ============================================
|
|
158
|
+
|
|
159
|
+
export interface LockfileGenerateOptions {
|
|
160
|
+
/** mandu 버전 */
|
|
161
|
+
manduVersion?: string;
|
|
162
|
+
/** 환경 */
|
|
163
|
+
environment?: "development" | "production" | "ci";
|
|
164
|
+
/** 스냅샷 포함 여부 */
|
|
165
|
+
includeSnapshot?: boolean;
|
|
166
|
+
/** MCP 서버별 해시 포함 여부 */
|
|
167
|
+
includeMcpServerHashes?: boolean;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ============================================
|
|
171
|
+
// 상수
|
|
172
|
+
// ============================================
|
|
173
|
+
|
|
174
|
+
/** Lockfile 기본 경로 */
|
|
175
|
+
export const LOCKFILE_PATH = ".mandu/lockfile.json";
|
|
176
|
+
|
|
177
|
+
/** Lockfile 디렉토리 */
|
|
178
|
+
export const LOCKFILE_DIR = ".mandu";
|
|
179
|
+
|
|
180
|
+
/** 현재 스키마 버전 */
|
|
181
|
+
export const LOCKFILE_SCHEMA_VERSION = 1;
|
|
182
|
+
|
|
183
|
+
/** 우회 환경변수 이름 */
|
|
184
|
+
export const BYPASS_ENV_VAR = "MANDU_LOCK_BYPASS";
|