@mandujs/core 0.9.46 → 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 +318 -256
  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,326 @@
1
+ /**
2
+ * 결정론적 해싱 테스트
3
+ *
4
+ * @see docs/plans/08_ont-run_adoption_plan.md - 섹션 7.1
5
+ */
6
+
7
+ import { describe, expect, it } from "bun:test";
8
+ import {
9
+ computeConfigHash,
10
+ verifyConfigIntegrity,
11
+ compareConfigHashes,
12
+ normalizeForHash,
13
+ isHashable,
14
+ } from "./hasher.js";
15
+
16
+ describe("computeConfigHash", () => {
17
+ it("should produce same hash regardless of key order", () => {
18
+ const config1 = { a: 1, b: 2, c: 3 };
19
+ const config2 = { c: 3, a: 1, b: 2 };
20
+ const config3 = { b: 2, c: 3, a: 1 };
21
+
22
+ const hash1 = computeConfigHash(config1);
23
+ const hash2 = computeConfigHash(config2);
24
+ const hash3 = computeConfigHash(config3);
25
+
26
+ expect(hash1).toBe(hash2);
27
+ expect(hash2).toBe(hash3);
28
+ });
29
+
30
+ it("should produce different hash for different values", () => {
31
+ const config1 = { a: 1 };
32
+ const config2 = { a: 2 };
33
+
34
+ expect(computeConfigHash(config1)).not.toBe(computeConfigHash(config2));
35
+ });
36
+
37
+ it("should handle nested objects with deterministic ordering", () => {
38
+ const config1 = {
39
+ server: { port: 3000, host: "localhost" },
40
+ database: { url: "postgres://...", pool: 10 },
41
+ };
42
+ const config2 = {
43
+ database: { pool: 10, url: "postgres://..." },
44
+ server: { host: "localhost", port: 3000 },
45
+ };
46
+
47
+ expect(computeConfigHash(config1)).toBe(computeConfigHash(config2));
48
+ });
49
+
50
+ it("should handle arrays (order matters)", () => {
51
+ const config1 = { items: [1, 2, 3] };
52
+ const config2 = { items: [1, 2, 3] };
53
+ const config3 = { items: [3, 2, 1] };
54
+
55
+ expect(computeConfigHash(config1)).toBe(computeConfigHash(config2));
56
+ expect(computeConfigHash(config1)).not.toBe(computeConfigHash(config3));
57
+ });
58
+
59
+ it("should exclude specified keys", () => {
60
+ const config1 = { a: 1, secret: "abc123" };
61
+ const config2 = { a: 1, secret: "xyz789" };
62
+
63
+ const hash1 = computeConfigHash(config1, { exclude: ["secret"] });
64
+ const hash2 = computeConfigHash(config2, { exclude: ["secret"] });
65
+
66
+ expect(hash1).toBe(hash2);
67
+ });
68
+
69
+ it("should produce hash of specified length", () => {
70
+ const config = { test: "value" };
71
+
72
+ expect(computeConfigHash(config, { length: 8 })).toHaveLength(8);
73
+ expect(computeConfigHash(config, { length: 16 })).toHaveLength(16);
74
+ expect(computeConfigHash(config, { length: 32 })).toHaveLength(32);
75
+ });
76
+
77
+ it("should handle empty objects", () => {
78
+ expect(() => computeConfigHash({})).not.toThrow();
79
+ expect(computeConfigHash({})).toHaveLength(16);
80
+ });
81
+
82
+ it("should handle null and undefined", () => {
83
+ expect(() => computeConfigHash(null)).not.toThrow();
84
+ expect(() => computeConfigHash(undefined)).not.toThrow();
85
+ });
86
+ });
87
+
88
+ describe("normalizeForHash", () => {
89
+ it("should convert Date to ISO string", () => {
90
+ const date = new Date("2025-01-28T00:00:00.000Z");
91
+ const normalized = normalizeForHash({ date });
92
+
93
+ expect(normalized).toEqual({ date: "2025-01-28T00:00:00.000Z" });
94
+ });
95
+
96
+ it("should convert BigInt to string with n suffix", () => {
97
+ const normalized = normalizeForHash({ big: BigInt(12345) });
98
+
99
+ expect(normalized).toEqual({ big: "12345n" });
100
+ });
101
+
102
+ it("should convert URL to href string", () => {
103
+ const url = new URL("https://example.com/path?query=1");
104
+ const normalized = normalizeForHash({ url });
105
+
106
+ expect(normalized).toEqual({ url: "https://example.com/path?query=1" });
107
+ });
108
+
109
+ it("should convert RegExp to string", () => {
110
+ const normalized = normalizeForHash({ pattern: /test/gi });
111
+
112
+ expect(normalized).toEqual({ pattern: "/test/gi" });
113
+ });
114
+
115
+ it("should remove functions by default", () => {
116
+ const normalized = normalizeForHash({
117
+ a: 1,
118
+ fn: () => console.log("test"),
119
+ });
120
+
121
+ expect(normalized).toEqual({ a: 1 });
122
+ });
123
+
124
+ it("should remove Symbol by default", () => {
125
+ const normalized = normalizeForHash({
126
+ a: 1,
127
+ sym: Symbol("test"),
128
+ });
129
+
130
+ expect(normalized).toEqual({ a: 1 });
131
+ });
132
+
133
+ it("should remove undefined values (like JSON.stringify)", () => {
134
+ const normalized = normalizeForHash({
135
+ a: 1,
136
+ b: undefined,
137
+ c: 3,
138
+ });
139
+
140
+ expect(normalized).toEqual({ a: 1, c: 3 });
141
+ });
142
+
143
+ it("should handle Map as sorted entries", () => {
144
+ const map = new Map([
145
+ ["z", 1],
146
+ ["a", 2],
147
+ ["m", 3],
148
+ ]);
149
+ const normalized = normalizeForHash({ data: map }) as any;
150
+
151
+ expect(normalized.data.__type__).toBe("Map");
152
+ expect(normalized.data.entries[0][0]).toBe("a"); // 정렬됨
153
+ });
154
+
155
+ it("should handle Set as sorted array", () => {
156
+ const set = new Set([3, 1, 2]);
157
+ const normalized = normalizeForHash({ data: set }) as any;
158
+
159
+ expect(normalized.data.__type__).toBe("Set");
160
+ expect(normalized.data.items).toEqual([1, 2, 3]); // 정렬됨
161
+ });
162
+
163
+ it("should detect circular references", () => {
164
+ const obj: any = { a: 1 };
165
+ obj.self = obj;
166
+
167
+ const normalized = normalizeForHash(obj) as any;
168
+ expect(normalized.self).toBe("__circular__");
169
+ });
170
+
171
+ it("should handle NaN, Infinity, -Infinity", () => {
172
+ const normalized = normalizeForHash({
173
+ nan: NaN,
174
+ inf: Infinity,
175
+ negInf: -Infinity,
176
+ });
177
+
178
+ expect(normalized).toEqual({
179
+ nan: "__NaN__",
180
+ inf: "__Infinity__",
181
+ negInf: "__-Infinity__",
182
+ });
183
+ });
184
+
185
+ it("should normalize Error objects", () => {
186
+ const error = new TypeError("test error");
187
+ const normalized = normalizeForHash({ error }) as any;
188
+
189
+ expect(normalized.error.__type__).toBe("Error");
190
+ expect(normalized.error.name).toBe("TypeError");
191
+ expect(normalized.error.message).toBe("test error");
192
+ });
193
+ });
194
+
195
+ describe("verifyConfigIntegrity", () => {
196
+ it("should return true for matching config and hash", () => {
197
+ const config = { server: { port: 3000 }, debug: true };
198
+ const hash = computeConfigHash(config);
199
+
200
+ expect(verifyConfigIntegrity(config, hash)).toBe(true);
201
+ });
202
+
203
+ it("should return false for modified config", () => {
204
+ const config = { server: { port: 3000 }, debug: true };
205
+ const hash = computeConfigHash(config);
206
+
207
+ const modifiedConfig = { server: { port: 3001 }, debug: true };
208
+
209
+ expect(verifyConfigIntegrity(modifiedConfig, hash)).toBe(false);
210
+ });
211
+
212
+ it("should work with exclude option", () => {
213
+ const config = { a: 1, timestamp: Date.now() };
214
+ const hash = computeConfigHash(config, { exclude: ["timestamp"] });
215
+
216
+ // timestamp가 달라도 해시는 같아야 함
217
+ const laterConfig = { a: 1, timestamp: Date.now() + 1000 };
218
+
219
+ expect(verifyConfigIntegrity(laterConfig, hash, { exclude: ["timestamp"] })).toBe(true);
220
+ });
221
+ });
222
+
223
+ describe("compareConfigHashes", () => {
224
+ it("should compare two configs", () => {
225
+ const config1 = { a: 1, b: 2 };
226
+ const config2 = { b: 2, a: 1 };
227
+ const config3 = { a: 1, b: 3 };
228
+
229
+ const result1 = compareConfigHashes(config1, config2);
230
+ const result2 = compareConfigHashes(config1, config3);
231
+
232
+ expect(result1.equal).toBe(true);
233
+ expect(result2.equal).toBe(false);
234
+ });
235
+ });
236
+
237
+ describe("isHashable", () => {
238
+ it("should return true for hashable values", () => {
239
+ expect(isHashable({ a: 1 })).toBe(true);
240
+ expect(isHashable([1, 2, 3])).toBe(true);
241
+ expect(isHashable("string")).toBe(true);
242
+ expect(isHashable(123)).toBe(true);
243
+ expect(isHashable(null)).toBe(true);
244
+ expect(isHashable(new Date())).toBe(true);
245
+ });
246
+
247
+ it("should return false for unhashable values", () => {
248
+ expect(isHashable(undefined)).toBe(false);
249
+ expect(isHashable(() => {})).toBe(false);
250
+ expect(isHashable(Symbol("test"))).toBe(false);
251
+ });
252
+ });
253
+
254
+ describe("real-world scenarios", () => {
255
+ it("should handle mandu config-like structure", () => {
256
+ const manduConfig = {
257
+ name: "my-project",
258
+ port: 3000,
259
+ mcpServers: {
260
+ sequential: {
261
+ command: "npx",
262
+ args: ["-y", "@anthropic/sequential-mcp"],
263
+ },
264
+ context7: {
265
+ command: "npx",
266
+ args: ["-y", "@context7/mcp"],
267
+ },
268
+ },
269
+ features: {
270
+ islands: true,
271
+ ssr: true,
272
+ },
273
+ };
274
+
275
+ const hash = computeConfigHash(manduConfig);
276
+ expect(hash).toHaveLength(16);
277
+
278
+ // 키 순서가 다른 동일한 설정
279
+ const sameConfigDifferentOrder = {
280
+ features: {
281
+ ssr: true,
282
+ islands: true,
283
+ },
284
+ mcpServers: {
285
+ context7: {
286
+ args: ["-y", "@context7/mcp"],
287
+ command: "npx",
288
+ },
289
+ sequential: {
290
+ args: ["-y", "@anthropic/sequential-mcp"],
291
+ command: "npx",
292
+ },
293
+ },
294
+ port: 3000,
295
+ name: "my-project",
296
+ };
297
+
298
+ expect(computeConfigHash(sameConfigDifferentOrder)).toBe(hash);
299
+ });
300
+
301
+ it("should handle MCP config with sensitive data exclusion", () => {
302
+ const mcpConfig1 = {
303
+ servers: {
304
+ api: {
305
+ url: "https://api.example.com",
306
+ token: "secret-token-123",
307
+ },
308
+ },
309
+ };
310
+
311
+ const mcpConfig2 = {
312
+ servers: {
313
+ api: {
314
+ url: "https://api.example.com",
315
+ token: "different-token-456",
316
+ },
317
+ },
318
+ };
319
+
320
+ // token을 제외하면 동일한 해시
321
+ const hash1 = computeConfigHash(mcpConfig1, { exclude: ["token"] });
322
+ const hash2 = computeConfigHash(mcpConfig2, { exclude: ["token"] });
323
+
324
+ expect(hash1).toBe(hash2);
325
+ });
326
+ });
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Mandu 결정론적 해싱 유틸리티 🔐
3
+ *
4
+ * ont-run의 해싱 기법을 참고하여 구현
5
+ * @see DNA/ont-run/src/lockfile/hasher.ts
6
+ *
7
+ * 특징:
8
+ * - 키 순서에 관계없이 동일한 해시 생성 (결정론적)
9
+ * - 비직렬화 요소(함수, Date, BigInt 등) 정규화
10
+ * - 민감 키 제외 옵션
11
+ */
12
+
13
+ import { createHash } from "node:crypto";
14
+
15
+ // ============================================
16
+ // 타입 정의
17
+ // ============================================
18
+
19
+ export interface HashOptions {
20
+ /** 해시 알고리즘 (기본값: sha256) */
21
+ algorithm?: "sha256";
22
+ /** 해시 길이 (기본값: 16) */
23
+ length?: number;
24
+ /** 해시에서 제외할 키 */
25
+ exclude?: string[];
26
+ }
27
+
28
+ export interface NormalizeOptions {
29
+ /** 제외할 키 패턴 */
30
+ exclude?: string[];
31
+ /** Date를 ISO 문자열로 변환 (기본값: true) */
32
+ dateToIso?: boolean;
33
+ /** BigInt를 문자열로 변환 (기본값: true) */
34
+ bigintToString?: boolean;
35
+ /** Map/Set을 배열로 변환 (기본값: true) */
36
+ collectionToArray?: boolean;
37
+ /** 함수 제거 (기본값: true) */
38
+ removeFunction?: boolean;
39
+ /** Symbol 제거 (기본값: true) */
40
+ removeSymbol?: boolean;
41
+ }
42
+
43
+ // ============================================
44
+ // 정규화
45
+ // ============================================
46
+
47
+ /**
48
+ * 객체를 해싱 가능한 형태로 정규화
49
+ *
50
+ * 정규화 규칙:
51
+ * 1. 키를 알파벳 순으로 정렬
52
+ * 2. undefined 키 제거 (JSON.stringify와 동일)
53
+ * 3. 함수, Symbol 제거
54
+ * 4. Date → ISO 문자열
55
+ * 5. BigInt → 문자열 + 'n' 접미사
56
+ * 6. Map → [key, value] 배열
57
+ * 7. Set → 정렬된 배열
58
+ * 8. 순환 참조 감지
59
+ */
60
+ export function normalizeForHash(
61
+ value: unknown,
62
+ options: NormalizeOptions = {},
63
+ seen: WeakSet<object> = new WeakSet()
64
+ ): unknown {
65
+ const {
66
+ exclude = [],
67
+ dateToIso = true,
68
+ bigintToString = true,
69
+ collectionToArray = true,
70
+ removeFunction = true,
71
+ removeSymbol = true,
72
+ } = options;
73
+
74
+ // null
75
+ if (value === null) return null;
76
+
77
+ // undefined → 제거됨 (반환하지 않음)
78
+ if (value === undefined) return undefined;
79
+
80
+ // 원시형
81
+ if (typeof value === "boolean" || typeof value === "number") {
82
+ // NaN, Infinity 처리
83
+ if (Number.isNaN(value)) return "__NaN__";
84
+ if (value === Infinity) return "__Infinity__";
85
+ if (value === -Infinity) return "__-Infinity__";
86
+ return value;
87
+ }
88
+
89
+ if (typeof value === "string") {
90
+ return value;
91
+ }
92
+
93
+ // BigInt
94
+ if (typeof value === "bigint") {
95
+ return bigintToString ? `${value}n` : value;
96
+ }
97
+
98
+ // Symbol
99
+ if (typeof value === "symbol") {
100
+ return removeSymbol ? undefined : `Symbol(${value.description ?? ""})`;
101
+ }
102
+
103
+ // 함수
104
+ if (typeof value === "function") {
105
+ return removeFunction ? undefined : `[Function: ${value.name || "anonymous"}]`;
106
+ }
107
+
108
+ // 객체 타입 처리
109
+ if (typeof value === "object") {
110
+ // 순환 참조 감지
111
+ if (seen.has(value)) {
112
+ return "__circular__";
113
+ }
114
+ seen.add(value);
115
+
116
+ // Date
117
+ if (value instanceof Date) {
118
+ return dateToIso ? value.toISOString() : value;
119
+ }
120
+
121
+ // URL
122
+ if (value instanceof URL) {
123
+ return value.href;
124
+ }
125
+
126
+ // RegExp
127
+ if (value instanceof RegExp) {
128
+ return value.toString();
129
+ }
130
+
131
+ // Error
132
+ if (value instanceof Error) {
133
+ return {
134
+ __type__: "Error",
135
+ name: value.name,
136
+ message: value.message,
137
+ };
138
+ }
139
+
140
+ // Map
141
+ if (value instanceof Map) {
142
+ if (!collectionToArray) return value;
143
+ const entries: [unknown, unknown][] = [];
144
+ for (const [k, v] of value.entries()) {
145
+ entries.push([
146
+ normalizeForHash(k, options, seen),
147
+ normalizeForHash(v, options, seen),
148
+ ]);
149
+ }
150
+ // 키로 정렬 (결정론적)
151
+ entries.sort((a, b) => {
152
+ const aKey = toSortableString(a[0]);
153
+ const bKey = toSortableString(b[0]);
154
+ return aKey.localeCompare(bKey);
155
+ });
156
+ return { __type__: "Map", entries };
157
+ }
158
+
159
+ // Set
160
+ if (value instanceof Set) {
161
+ if (!collectionToArray) return value;
162
+ const items: unknown[] = [];
163
+ for (const item of value) {
164
+ items.push(normalizeForHash(item, options, seen));
165
+ }
166
+ // 정렬 (결정론적)
167
+ items.sort((a, b) => {
168
+ const aStr = toSortableString(a);
169
+ const bStr = toSortableString(b);
170
+ return aStr.localeCompare(bStr);
171
+ });
172
+ return { __type__: "Set", items };
173
+ }
174
+
175
+ // 배열
176
+ if (Array.isArray(value)) {
177
+ return value.map((item) => normalizeForHash(item, options, seen));
178
+ }
179
+
180
+ // 일반 객체 - 키 정렬
181
+ const sortedKeys = Object.keys(value).sort();
182
+ const result: Record<string, unknown> = {};
183
+
184
+ for (const key of sortedKeys) {
185
+ // 제외 키 체크
186
+ if (exclude.includes(key)) continue;
187
+
188
+ const v = (value as Record<string, unknown>)[key];
189
+ const normalized = normalizeForHash(v, options, seen);
190
+
191
+ // undefined는 포함하지 않음 (JSON.stringify와 동일)
192
+ if (normalized !== undefined) {
193
+ result[key] = normalized;
194
+ }
195
+ }
196
+
197
+ return result;
198
+ }
199
+
200
+ return value;
201
+ }
202
+
203
+ // ============================================
204
+ // 해싱
205
+ // ============================================
206
+
207
+ /**
208
+ * 설정 객체의 결정론적 해시 계산
209
+ *
210
+ * @example
211
+ * ```typescript
212
+ * const hash1 = computeConfigHash({ a: 1, b: 2 });
213
+ * const hash2 = computeConfigHash({ b: 2, a: 1 });
214
+ * console.log(hash1 === hash2); // true (키 순서 무관)
215
+ * ```
216
+ */
217
+ export function computeConfigHash(
218
+ config: unknown,
219
+ options: HashOptions = {}
220
+ ): string {
221
+ const { algorithm = "sha256", length = 16, exclude = [] } = options;
222
+
223
+ // 1. 정규화
224
+ const normalized = normalizeForHash(config, { exclude });
225
+
226
+ // 2. JSON 문자열화 (이미 정렬되어 있음)
227
+ // undefined나 함수만 있는 경우 빈 문자열 처리
228
+ const jsonString = normalized === undefined ? "" : JSON.stringify(normalized);
229
+
230
+ // 3. 해싱
231
+ const hash = createHash(algorithm).update(jsonString).digest("hex");
232
+
233
+ // 4. 길이 조절
234
+ return hash.slice(0, length);
235
+ }
236
+
237
+ /**
238
+ * 설정 무결성 검증
239
+ *
240
+ * @example
241
+ * ```typescript
242
+ * const hash = computeConfigHash(config);
243
+ * // ... 나중에 ...
244
+ * const isValid = verifyConfigIntegrity(config, hash);
245
+ * ```
246
+ */
247
+ export function verifyConfigIntegrity(
248
+ config: unknown,
249
+ expectedHash: string,
250
+ options: HashOptions = {}
251
+ ): boolean {
252
+ const actualHash = computeConfigHash(config, options);
253
+ return actualHash === expectedHash;
254
+ }
255
+
256
+ /**
257
+ * 두 설정의 해시 비교
258
+ */
259
+ export function compareConfigHashes(
260
+ config1: unknown,
261
+ config2: unknown,
262
+ options: HashOptions = {}
263
+ ): { equal: boolean; hash1: string; hash2: string } {
264
+ const hash1 = computeConfigHash(config1, options);
265
+ const hash2 = computeConfigHash(config2, options);
266
+
267
+ return {
268
+ equal: hash1 === hash2,
269
+ hash1,
270
+ hash2,
271
+ };
272
+ }
273
+
274
+ // ============================================
275
+ // 유틸리티
276
+ // ============================================
277
+
278
+ /**
279
+ * 해싱 가능 여부 체크
280
+ * (정규화 후에도 의미 있는 데이터가 있는지)
281
+ */
282
+ export function isHashable(value: unknown): boolean {
283
+ const normalized = normalizeForHash(value);
284
+ return normalized !== undefined;
285
+ }
286
+
287
+ /**
288
+ * 해시 충돌 가능성 경고 (개발용)
289
+ * 16자 해시의 충돌 확률은 매우 낮지만, 디버깅용으로 제공
290
+ */
291
+ export function getHashInfo(hash: string): {
292
+ length: number;
293
+ bits: number;
294
+ collisionProbability: string;
295
+ } {
296
+ const bits = hash.length * 4; // hex는 문자당 4비트
297
+ // Birthday paradox 근사: sqrt(2^n) 에서 50% 충돌
298
+ const collisionAt = Math.pow(2, bits / 2);
299
+
300
+ return {
301
+ length: hash.length,
302
+ bits,
303
+ collisionProbability: `~${collisionAt.toExponential(2)} 해시에서 50% 충돌 가능`,
304
+ };
305
+ }
306
+
307
+ // ============================================
308
+ // 내부 유틸
309
+ // ============================================
310
+
311
+ function toSortableString(value: unknown): string {
312
+ try {
313
+ const json = JSON.stringify(value);
314
+ if (typeof json === "string") return json;
315
+ } catch {
316
+ // ignore
317
+ }
318
+ return String(value);
319
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Mandu 유틸리티 모듈
3
+ *
4
+ * ont-run 기법 도입에 따른 핵심 유틸리티
5
+ * @see docs/plans/08_ont-run_adoption_plan.md
6
+ */
7
+
8
+ // 해싱
9
+ export {
10
+ computeConfigHash,
11
+ verifyConfigIntegrity,
12
+ compareConfigHashes,
13
+ normalizeForHash,
14
+ isHashable,
15
+ getHashInfo,
16
+ type HashOptions,
17
+ type NormalizeOptions,
18
+ } from "./hasher.js";
19
+
20
+ // Diff
21
+ export {
22
+ diffConfig,
23
+ formatConfigDiff,
24
+ printConfigDiff,
25
+ summarizeDiff,
26
+ hasConfigChanges,
27
+ type ConfigDiff,
28
+ type DiffFormatOptions,
29
+ } from "./differ.js";