@mandujs/core 0.9.45 → 0.10.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 (41) hide show
  1. package/package.json +1 -1
  2. package/src/brain/doctor/config-analyzer.ts +498 -0
  3. package/src/brain/doctor/index.ts +10 -0
  4. package/src/change/snapshot.ts +46 -1
  5. package/src/change/types.ts +13 -0
  6. package/src/config/index.ts +8 -2
  7. package/src/config/mcp-ref.ts +348 -0
  8. package/src/config/mcp-status.ts +348 -0
  9. package/src/config/metadata.test.ts +308 -0
  10. package/src/config/metadata.ts +293 -0
  11. package/src/config/symbols.ts +144 -0
  12. package/src/contract/index.ts +26 -25
  13. package/src/contract/protection.ts +364 -0
  14. package/src/error/domains.ts +265 -0
  15. package/src/error/index.ts +25 -13
  16. package/src/filling/filling.ts +88 -6
  17. package/src/guard/analyzer.ts +7 -2
  18. package/src/guard/config-guard.ts +281 -0
  19. package/src/guard/decision-memory.test.ts +293 -0
  20. package/src/guard/decision-memory.ts +532 -0
  21. package/src/guard/healing.test.ts +259 -0
  22. package/src/guard/healing.ts +874 -0
  23. package/src/guard/index.ts +119 -0
  24. package/src/guard/negotiation.test.ts +282 -0
  25. package/src/guard/negotiation.ts +975 -0
  26. package/src/guard/semantic-slots.test.ts +379 -0
  27. package/src/guard/semantic-slots.ts +796 -0
  28. package/src/index.ts +2 -0
  29. package/src/lockfile/generate.ts +259 -0
  30. package/src/lockfile/index.ts +186 -0
  31. package/src/lockfile/lockfile.test.ts +410 -0
  32. package/src/lockfile/types.ts +184 -0
  33. package/src/lockfile/validate.ts +308 -0
  34. package/src/runtime/security.ts +155 -0
  35. package/src/runtime/server.ts +320 -258
  36. package/src/utils/differ.test.ts +342 -0
  37. package/src/utils/differ.ts +482 -0
  38. package/src/utils/hasher.test.ts +326 -0
  39. package/src/utils/hasher.ts +319 -0
  40. package/src/utils/index.ts +29 -0
  41. package/src/utils/safe-io.ts +188 -0
@@ -0,0 +1,379 @@
1
+ /**
2
+ * Semantic Slots Tests
3
+ */
4
+
5
+ import { describe, it, expect, beforeAll, afterAll } from "bun:test";
6
+ import { mkdir, rm, mkdtemp } from "fs/promises";
7
+ import { join } from "path";
8
+ import { tmpdir } from "os";
9
+ import {
10
+ validateSlotConstraints,
11
+ validateSlots,
12
+ extractSlotMetadata,
13
+ countCodeLines,
14
+ calculateCyclomaticComplexity,
15
+ extractImports,
16
+ extractFunctionCalls,
17
+ checkPattern,
18
+ DEFAULT_SLOT_CONSTRAINTS,
19
+ API_SLOT_CONSTRAINTS,
20
+ type SlotConstraints,
21
+ } from "./semantic-slots";
22
+
23
+ // ═══════════════════════════════════════════════════════════════════════════
24
+ // Test Setup
25
+ // ═══════════════════════════════════════════════════════════════════════════
26
+
27
+ let TEST_DIR: string;
28
+
29
+ const VALID_SLOT_CODE = `
30
+ import { z } from "zod";
31
+ import { userService } from "server/domain/user";
32
+
33
+ const schema = z.object({
34
+ page: z.number().min(1),
35
+ limit: z.number().max(100),
36
+ });
37
+
38
+ export default Mandu.filling()
39
+ .purpose("사용자 목록 조회 API")
40
+ .constraints({ maxLines: 50 })
41
+ .get(async (ctx) => {
42
+ try {
43
+ const input = schema.parse(ctx.query);
44
+ const users = await userService.list(input);
45
+ return ctx.json({ users });
46
+ } catch (error) {
47
+ return ctx.json({ error: "Failed to fetch users" }, 500);
48
+ }
49
+ });
50
+ `;
51
+
52
+ const INVALID_SLOT_CODE = `
53
+ import { db } from "database/client";
54
+
55
+ // This slot has issues:
56
+ // 1. Too many lines
57
+ // 2. High complexity
58
+ // 3. Missing validation
59
+ // 4. Direct DB write
60
+ // 5. Hardcoded secret
61
+
62
+ const API_KEY = "sk-12345678901234567890";
63
+
64
+ export default Mandu.filling()
65
+ .get(async (ctx) => {
66
+ if (ctx.query.a) {
67
+ if (ctx.query.b) {
68
+ if (ctx.query.c) {
69
+ if (ctx.query.d) {
70
+ if (ctx.query.e) {
71
+ console.log("password:", ctx.body.password);
72
+ await db.insert({ a: 1 });
73
+ }
74
+ }
75
+ }
76
+ }
77
+ }
78
+ return ctx.json({ ok: true });
79
+ });
80
+ `;
81
+
82
+ const COMPLEX_CODE = `
83
+ function process(x) {
84
+ if (x > 0) {
85
+ if (x < 10) {
86
+ return "small";
87
+ } else if (x < 100) {
88
+ return "medium";
89
+ }
90
+ }
91
+
92
+ for (let i = 0; i < x; i++) {
93
+ if (i % 2 === 0) {
94
+ console.log(i);
95
+ }
96
+ }
97
+
98
+ while (x > 0) {
99
+ x--;
100
+ }
101
+
102
+ switch (x) {
103
+ case 1: return "one";
104
+ case 2: return "two";
105
+ default: return "other";
106
+ }
107
+ }
108
+ `;
109
+
110
+ beforeAll(async () => {
111
+ TEST_DIR = await mkdtemp(join(tmpdir(), "test-semantic-slots-"));
112
+ await mkdir(join(TEST_DIR, "slots"), { recursive: true });
113
+
114
+ // 유효한 슬롯 파일
115
+ await Bun.write(join(TEST_DIR, "slots", "valid.slot.ts"), VALID_SLOT_CODE);
116
+
117
+ // 문제 있는 슬롯 파일
118
+ await Bun.write(join(TEST_DIR, "slots", "invalid.slot.ts"), INVALID_SLOT_CODE);
119
+
120
+ // 복잡한 코드 파일
121
+ await Bun.write(join(TEST_DIR, "slots", "complex.ts"), COMPLEX_CODE);
122
+ });
123
+
124
+ afterAll(async () => {
125
+ await rm(TEST_DIR, { recursive: true, force: true });
126
+ });
127
+
128
+ // ═══════════════════════════════════════════════════════════════════════════
129
+ // Unit Tests - Analysis Functions
130
+ // ═══════════════════════════════════════════════════════════════════════════
131
+
132
+ describe("Semantic Slots - Analysis", () => {
133
+ describe("countCodeLines", () => {
134
+ it("should count only code lines, excluding single-line comments and blanks", () => {
135
+ const code = `
136
+ // Comment
137
+ const x = 1;
138
+
139
+ const y = 2;
140
+
141
+ // Another comment
142
+ const z = 3;
143
+ `.trim();
144
+
145
+ const count = countCodeLines(code);
146
+ // const x, const y, const z = 3 lines
147
+ expect(count).toBe(3);
148
+ });
149
+
150
+ it("should exclude multi-line block comments", () => {
151
+ const code = `
152
+ const a = 1;
153
+ /*
154
+ * Block comment
155
+ * spanning multiple lines
156
+ */
157
+ const b = 2;
158
+ `.trim();
159
+
160
+ const count = countCodeLines(code);
161
+ // const a, const b = 2 lines
162
+ expect(count).toBe(2);
163
+ });
164
+
165
+ it("should count lines in real code", () => {
166
+ const code = `const x = 1;\nconst y = 2;`;
167
+ const count = countCodeLines(code);
168
+ expect(count).toBe(2);
169
+ });
170
+ });
171
+
172
+ describe("calculateCyclomaticComplexity", () => {
173
+ it("should calculate complexity for complex code", () => {
174
+ const complexity = calculateCyclomaticComplexity(COMPLEX_CODE);
175
+
176
+ // 1 (base) + if + if + else if + for + if + while + 2 cases = ~10
177
+ expect(complexity).toBeGreaterThan(5);
178
+ });
179
+
180
+ it("should return 1 for simple code", () => {
181
+ const simple = "const x = 1;";
182
+ expect(calculateCyclomaticComplexity(simple)).toBe(1);
183
+ });
184
+ });
185
+
186
+ describe("extractImports", () => {
187
+ it("should extract ES6 imports", () => {
188
+ const code = `
189
+ import { foo } from "module-a";
190
+ import bar from "module-b";
191
+ import * as baz from "module-c";
192
+ `;
193
+ const imports = extractImports(code);
194
+
195
+ expect(imports).toContain("module-a");
196
+ expect(imports).toContain("module-b");
197
+ expect(imports).toContain("module-c");
198
+ });
199
+
200
+ it("should extract require statements", () => {
201
+ const code = `
202
+ const x = require("module-a");
203
+ const { y } = require("module-b");
204
+ `;
205
+ const imports = extractImports(code);
206
+
207
+ expect(imports).toContain("module-a");
208
+ expect(imports).toContain("module-b");
209
+ });
210
+ });
211
+
212
+ describe("extractFunctionCalls", () => {
213
+ it("should extract function and method calls", () => {
214
+ const code = `
215
+ foo();
216
+ obj.bar();
217
+ nested.deep.method();
218
+ `;
219
+ const calls = extractFunctionCalls(code);
220
+
221
+ expect(calls).toContain("foo");
222
+ expect(calls).toContain("obj.bar");
223
+ expect(calls).toContain("deep.method");
224
+ });
225
+ });
226
+
227
+ describe("checkPattern", () => {
228
+ it("should detect input-validation pattern", () => {
229
+ const code = 'const result = schema.parse(input);';
230
+ expect(checkPattern(code, "input-validation")).toBe(true);
231
+ });
232
+
233
+ it("should detect error-handling pattern", () => {
234
+ const code = "try { foo(); } catch (e) { console.error(e); }";
235
+ expect(checkPattern(code, "error-handling")).toBe(true);
236
+ });
237
+
238
+ it("should detect direct-db-write pattern", () => {
239
+ const code = "await db.insert({ name: 'test' });";
240
+ expect(checkPattern(code, "direct-db-write")).toBe(true);
241
+ });
242
+
243
+ it("should detect hardcoded-secret pattern", () => {
244
+ const code = 'const apiKey = "sk-12345678901234567890";';
245
+ expect(checkPattern(code, "hardcoded-secret")).toBe(true);
246
+ });
247
+ });
248
+ });
249
+
250
+ // ═══════════════════════════════════════════════════════════════════════════
251
+ // Unit Tests - Validation
252
+ // ═══════════════════════════════════════════════════════════════════════════
253
+
254
+ describe("Semantic Slots - Validation", () => {
255
+ describe("validateSlotConstraints", () => {
256
+ it("should pass validation for valid slot", async () => {
257
+ const constraints: SlotConstraints = {
258
+ maxLines: 50,
259
+ requiredPatterns: ["error-handling"],
260
+ forbiddenPatterns: ["hardcoded-secret"],
261
+ };
262
+
263
+ const result = await validateSlotConstraints(
264
+ join(TEST_DIR, "slots", "valid.slot.ts"),
265
+ constraints
266
+ );
267
+
268
+ expect(result.valid).toBe(true);
269
+ expect(result.violations.length).toBe(0);
270
+ });
271
+
272
+ it("should detect max lines violation", async () => {
273
+ const constraints: SlotConstraints = {
274
+ maxLines: 5, // 매우 낮은 제한
275
+ };
276
+
277
+ const result = await validateSlotConstraints(
278
+ join(TEST_DIR, "slots", "valid.slot.ts"),
279
+ constraints
280
+ );
281
+
282
+ expect(result.violations.some((v) => v.type === "max-lines-exceeded")).toBe(true);
283
+ });
284
+
285
+ it("should detect forbidden patterns", async () => {
286
+ const constraints: SlotConstraints = {
287
+ forbiddenPatterns: ["hardcoded-secret", "direct-db-write"],
288
+ };
289
+
290
+ const result = await validateSlotConstraints(
291
+ join(TEST_DIR, "slots", "invalid.slot.ts"),
292
+ constraints
293
+ );
294
+
295
+ expect(result.violations.some((v) => v.type === "forbidden-pattern-found")).toBe(true);
296
+ });
297
+
298
+ it("should detect missing required patterns", async () => {
299
+ const constraints: SlotConstraints = {
300
+ requiredPatterns: ["input-validation", "authentication"],
301
+ };
302
+
303
+ const result = await validateSlotConstraints(
304
+ join(TEST_DIR, "slots", "invalid.slot.ts"),
305
+ constraints
306
+ );
307
+
308
+ expect(result.violations.some((v) => v.type === "missing-required-pattern")).toBe(true);
309
+ });
310
+
311
+ it("should validate with default constraints", async () => {
312
+ const result = await validateSlotConstraints(
313
+ join(TEST_DIR, "slots", "invalid.slot.ts"),
314
+ DEFAULT_SLOT_CONSTRAINTS
315
+ );
316
+
317
+ // DEFAULT_SLOT_CONSTRAINTS에 hardcoded-secret 금지 포함
318
+ expect(result.violations.length).toBeGreaterThan(0);
319
+ });
320
+ });
321
+
322
+ describe("extractSlotMetadata", () => {
323
+ it("should extract purpose from slot file", async () => {
324
+ const metadata = await extractSlotMetadata(join(TEST_DIR, "slots", "valid.slot.ts"));
325
+
326
+ expect(metadata).not.toBeNull();
327
+ expect(metadata?.purpose).toBe("사용자 목록 조회 API");
328
+ });
329
+
330
+ it("should return null for files without metadata", async () => {
331
+ const metadata = await extractSlotMetadata(join(TEST_DIR, "slots", "complex.ts"));
332
+
333
+ expect(metadata).toBeNull();
334
+ });
335
+ });
336
+
337
+ describe("validateSlots", () => {
338
+ it("should validate multiple slots", async () => {
339
+ const slotFiles = [
340
+ join(TEST_DIR, "slots", "valid.slot.ts"),
341
+ join(TEST_DIR, "slots", "invalid.slot.ts"),
342
+ ];
343
+
344
+ const result = await validateSlots(slotFiles, API_SLOT_CONSTRAINTS);
345
+
346
+ expect(result.totalSlots).toBe(2);
347
+ expect(result.validSlots).toBeGreaterThanOrEqual(0);
348
+ expect(result.results.length).toBe(2);
349
+ });
350
+ });
351
+ });
352
+
353
+ // ═══════════════════════════════════════════════════════════════════════════
354
+ // Integration with Filling API
355
+ // ═══════════════════════════════════════════════════════════════════════════
356
+
357
+ describe("Semantic Slots - Filling API Integration", () => {
358
+ it("should be importable in filling context", async () => {
359
+ // 단순히 import가 가능한지 확인
360
+ const { ManduFilling } = await import("../filling/filling");
361
+ const filling = new ManduFilling();
362
+
363
+ // 메서드 체이닝 테스트
364
+ const result = filling
365
+ .purpose("Test API")
366
+ .description("This is a test")
367
+ .constraints({ maxLines: 50 })
368
+ .tags("test", "example")
369
+ .owner("test-team");
370
+
371
+ const metadata = result.getSemanticMetadata();
372
+
373
+ expect(metadata.purpose).toBe("Test API");
374
+ expect(metadata.description).toBe("This is a test");
375
+ expect(metadata.constraints?.maxLines).toBe(50);
376
+ expect(metadata.tags).toContain("test");
377
+ expect(metadata.owner).toBe("test-team");
378
+ });
379
+ });