@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
package/src/index.ts CHANGED
@@ -14,6 +14,8 @@ export * from "./brain";
14
14
  export * from "./watcher";
15
15
  export * from "./router";
16
16
  export * from "./config";
17
+ export * from "./lockfile";
18
+ export * from "./utils";
17
19
  export * from "./seo";
18
20
  export * from "./island";
19
21
  export * from "./intent";
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Mandu Lockfile 생성 🔐
3
+ *
4
+ * 설정 파일에서 lockfile 생성
5
+ */
6
+
7
+ import { computeConfigHash, normalizeForHash } from "../utils/hasher.js";
8
+ import {
9
+ type ManduLockfile,
10
+ type LockfileGenerateOptions,
11
+ LOCKFILE_SCHEMA_VERSION,
12
+ } from "./types.js";
13
+
14
+ // ============================================
15
+ // Lockfile 생성
16
+ // ============================================
17
+
18
+ /**
19
+ * 설정에서 Lockfile 생성
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * const config = await loadConfig();
24
+ * const lockfile = generateLockfile(config, {
25
+ * manduVersion: "0.9.46",
26
+ * includeSnapshot: true,
27
+ * });
28
+ * ```
29
+ */
30
+ export function generateLockfile(
31
+ config: Record<string, unknown>,
32
+ options: LockfileGenerateOptions = {},
33
+ mcpConfig?: Record<string, unknown> | null
34
+ ): ManduLockfile {
35
+ const {
36
+ manduVersion = getManduVersion(),
37
+ environment = detectEnvironment(),
38
+ includeSnapshot = false,
39
+ includeMcpServerHashes = true,
40
+ } = options;
41
+
42
+ // 설정 해시 계산
43
+ const configHash = computeConfigHash(config);
44
+
45
+ // MCP 설정 해시 (있는 경우)
46
+ const { mcpHashSource, mcpServers } = resolveMcpSources(config, mcpConfig);
47
+ let mcpConfigHash: string | undefined;
48
+ let mcpServerHashes: ManduLockfile["mcpServers"];
49
+
50
+ if (mcpHashSource && Object.keys(mcpHashSource).length > 0) {
51
+ mcpConfigHash = computeConfigHash(mcpHashSource);
52
+
53
+ if (includeMcpServerHashes && mcpServers) {
54
+ mcpServerHashes = {};
55
+ for (const [name, serverConfig] of Object.entries(mcpServers)) {
56
+ mcpServerHashes[name] = {
57
+ hash: computeConfigHash(serverConfig),
58
+ version: extractServerVersion(serverConfig),
59
+ };
60
+ }
61
+ }
62
+ }
63
+
64
+ // Lockfile 생성
65
+ const lockfile: ManduLockfile = {
66
+ schemaVersion: LOCKFILE_SCHEMA_VERSION,
67
+ manduVersion,
68
+ configHash,
69
+ generatedAt: new Date().toISOString(),
70
+ environment,
71
+ };
72
+
73
+ // 선택적 필드 추가
74
+ if (mcpConfigHash) {
75
+ lockfile.mcpConfigHash = mcpConfigHash;
76
+ }
77
+
78
+ if (mcpServerHashes) {
79
+ lockfile.mcpServers = mcpServerHashes;
80
+ }
81
+
82
+ if (includeSnapshot) {
83
+ const normalized = normalizeForHash(config);
84
+ const snapshotConfig =
85
+ normalized && typeof normalized === "object"
86
+ ? (normalized as Record<string, unknown>)
87
+ : {};
88
+
89
+ if (mcpServers) {
90
+ const normalizedMcp = normalizeForHash(mcpServers);
91
+ if (normalizedMcp !== undefined) {
92
+ snapshotConfig.mcpServers = normalizedMcp;
93
+ }
94
+ }
95
+
96
+ lockfile.snapshot = {
97
+ config: snapshotConfig,
98
+ environment: environment ?? "development",
99
+ };
100
+ }
101
+
102
+ return lockfile;
103
+ }
104
+
105
+ /**
106
+ * MCP 설정에서 별도 Lockfile 데이터 생성
107
+ */
108
+ export function generateMcpLockData(
109
+ mcpConfig: Record<string, unknown>
110
+ ): { hash: string; servers: ManduLockfile["mcpServers"] } {
111
+ const { mcpHashSource, mcpServers } = resolveMcpSources({}, mcpConfig);
112
+ const hash = mcpHashSource ? computeConfigHash(mcpHashSource) : computeConfigHash(mcpConfig);
113
+ const servers: ManduLockfile["mcpServers"] = {};
114
+
115
+ if (mcpServers) {
116
+ for (const [name, serverConfig] of Object.entries(mcpServers)) {
117
+ servers[name] = {
118
+ hash: computeConfigHash(serverConfig),
119
+ version: extractServerVersion(serverConfig),
120
+ };
121
+ }
122
+ }
123
+
124
+ return { hash, servers };
125
+ }
126
+
127
+ // ============================================
128
+ // 유틸리티
129
+ // ============================================
130
+
131
+ /**
132
+ * mandu 버전 가져오기
133
+ */
134
+ function getManduVersion(): string {
135
+ // 실제 구현에서는 package.json에서 읽거나 빌드 시 주입
136
+ try {
137
+ // @ts-ignore - 빌드 시 주입되는 값
138
+ if (typeof __MANDU_VERSION__ !== "undefined") {
139
+ // @ts-ignore
140
+ return __MANDU_VERSION__;
141
+ }
142
+ } catch {
143
+ // ignore
144
+ }
145
+
146
+ // 기본값
147
+ return "0.0.0";
148
+ }
149
+
150
+ /**
151
+ * 현재 환경 감지
152
+ */
153
+ function detectEnvironment(): "development" | "production" | "ci" {
154
+ // CI 환경 감지
155
+ if (
156
+ process.env.CI === "true" ||
157
+ process.env.GITHUB_ACTIONS === "true" ||
158
+ process.env.GITLAB_CI === "true" ||
159
+ process.env.JENKINS_URL
160
+ ) {
161
+ return "ci";
162
+ }
163
+
164
+ // NODE_ENV 기반
165
+ if (process.env.NODE_ENV === "production") {
166
+ return "production";
167
+ }
168
+
169
+ return "development";
170
+ }
171
+
172
+ /**
173
+ * 서버 설정에서 버전 추출
174
+ */
175
+ function extractServerVersion(
176
+ serverConfig: unknown
177
+ ): string | undefined {
178
+ if (typeof serverConfig !== "object" || serverConfig === null) {
179
+ return undefined;
180
+ }
181
+
182
+ const config = serverConfig as Record<string, unknown>;
183
+
184
+ // version 필드 직접 확인
185
+ if (typeof config.version === "string") {
186
+ return config.version;
187
+ }
188
+
189
+ // args에서 버전 패턴 추출 시도 (예: @package/name@1.2.3)
190
+ if (Array.isArray(config.args)) {
191
+ for (const arg of config.args) {
192
+ if (typeof arg === "string") {
193
+ const match = arg.match(/@[\w-]+\/[\w-]+@([\d.]+)/);
194
+ if (match) {
195
+ return match[1];
196
+ }
197
+ }
198
+ }
199
+ }
200
+
201
+ return undefined;
202
+ }
203
+
204
+ // ============================================
205
+ // 해시 재계산
206
+ // ============================================
207
+
208
+ /**
209
+ * 현재 설정의 해시만 빠르게 계산
210
+ */
211
+ export function computeCurrentHashes(
212
+ config: Record<string, unknown>,
213
+ mcpConfig?: Record<string, unknown> | null
214
+ ): { configHash: string; mcpConfigHash?: string } {
215
+ const configHash = computeConfigHash(config);
216
+
217
+ const { mcpHashSource } = resolveMcpSources(config, mcpConfig);
218
+ const mcpConfigHash = mcpHashSource && Object.keys(mcpHashSource).length > 0
219
+ ? computeConfigHash(mcpHashSource)
220
+ : undefined;
221
+
222
+ return { configHash, mcpConfigHash };
223
+ }
224
+
225
+ // ============================================
226
+ // MCP 설정 해석
227
+ // ============================================
228
+
229
+ export function resolveMcpSources(
230
+ config: Record<string, unknown>,
231
+ mcpConfig?: Record<string, unknown> | null
232
+ ): {
233
+ mcpHashSource?: Record<string, unknown>;
234
+ mcpServers?: Record<string, unknown>;
235
+ } {
236
+ if (mcpConfig && typeof mcpConfig === "object") {
237
+ const mcpServers = (mcpConfig as Record<string, unknown>).mcpServers;
238
+ if (mcpServers && typeof mcpServers === "object" && !Array.isArray(mcpServers)) {
239
+ return {
240
+ mcpHashSource: mcpConfig,
241
+ mcpServers: mcpServers as Record<string, unknown>,
242
+ };
243
+ }
244
+ return {
245
+ mcpHashSource: mcpConfig,
246
+ mcpServers: mcpConfig,
247
+ };
248
+ }
249
+
250
+ const configServers = config.mcpServers as Record<string, unknown> | undefined;
251
+ if (configServers && typeof configServers === "object") {
252
+ return {
253
+ mcpHashSource: configServers,
254
+ mcpServers: configServers,
255
+ };
256
+ }
257
+
258
+ return {};
259
+ }
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Mandu Lockfile I/O 📁
3
+ *
4
+ * Lockfile 읽기/쓰기 및 공개 API
5
+ */
6
+
7
+ import { mkdir } from "node:fs/promises";
8
+ import path from "node:path";
9
+ import {
10
+ type ManduLockfile,
11
+ type LockfileError,
12
+ LOCKFILE_PATH,
13
+ LOCKFILE_DIR,
14
+ LOCKFILE_SCHEMA_VERSION,
15
+ } from "./types.js";
16
+
17
+ // ============================================
18
+ // 읽기
19
+ // ============================================
20
+
21
+ /**
22
+ * Lockfile 읽기
23
+ *
24
+ * @param projectRoot 프로젝트 루트 디렉토리
25
+ * @returns Lockfile 또는 null (없는 경우)
26
+ * @throws 파싱 오류 시
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * const lockfile = await readLockfile(process.cwd());
31
+ * if (lockfile) {
32
+ * console.log(`Config hash: ${lockfile.configHash}`);
33
+ * }
34
+ * ```
35
+ */
36
+ export async function readLockfile(
37
+ projectRoot: string
38
+ ): Promise<ManduLockfile | null> {
39
+ const lockfilePath = path.join(projectRoot, LOCKFILE_PATH);
40
+
41
+ try {
42
+ const file = Bun.file(lockfilePath);
43
+ const exists = await file.exists();
44
+
45
+ if (!exists) {
46
+ return null;
47
+ }
48
+
49
+ const content = await file.text();
50
+ const data = JSON.parse(content) as ManduLockfile;
51
+
52
+ // 스키마 버전 체크
53
+ if (data.schemaVersion !== LOCKFILE_SCHEMA_VERSION) {
54
+ console.warn(
55
+ `[Mandu] Lockfile schema version mismatch: expected ${LOCKFILE_SCHEMA_VERSION}, got ${data.schemaVersion}`
56
+ );
57
+ }
58
+
59
+ return data;
60
+ } catch (error) {
61
+ if (error instanceof SyntaxError) {
62
+ throw new Error(
63
+ `Failed to parse lockfile at ${lockfilePath}: ${error.message}`
64
+ );
65
+ }
66
+ throw error;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * MCP 설정 읽기 (.mcp.json)
72
+ */
73
+ export async function readMcpConfig(
74
+ projectRoot: string
75
+ ): Promise<Record<string, unknown> | null> {
76
+ const mcpPath = path.join(projectRoot, ".mcp.json");
77
+
78
+ try {
79
+ const file = Bun.file(mcpPath);
80
+ const exists = await file.exists();
81
+ if (!exists) return null;
82
+
83
+ const content = await file.text();
84
+ const data = JSON.parse(content) as Record<string, unknown>;
85
+ return data ?? null;
86
+ } catch (error) {
87
+ if (error instanceof SyntaxError) {
88
+ throw new Error(`Failed to parse .mcp.json at ${mcpPath}: ${error.message}`);
89
+ }
90
+ throw error;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Lockfile 존재 여부 확인
96
+ */
97
+ export async function lockfileExists(projectRoot: string): Promise<boolean> {
98
+ const lockfilePath = path.join(projectRoot, LOCKFILE_PATH);
99
+ const file = Bun.file(lockfilePath);
100
+ return file.exists();
101
+ }
102
+
103
+ // ============================================
104
+ // 쓰기
105
+ // ============================================
106
+
107
+ /**
108
+ * Lockfile 쓰기
109
+ *
110
+ * @param projectRoot 프로젝트 루트 디렉토리
111
+ * @param lockfile Lockfile 데이터
112
+ *
113
+ * @example
114
+ * ```typescript
115
+ * const lockfile = generateLockfile(config);
116
+ * await writeLockfile(process.cwd(), lockfile);
117
+ * ```
118
+ */
119
+ export async function writeLockfile(
120
+ projectRoot: string,
121
+ lockfile: ManduLockfile
122
+ ): Promise<void> {
123
+ const lockfileDir = path.join(projectRoot, LOCKFILE_DIR);
124
+ const lockfilePath = path.join(projectRoot, LOCKFILE_PATH);
125
+
126
+ // 디렉토리 생성
127
+ await mkdir(lockfileDir, { recursive: true });
128
+
129
+ // JSON 포맷팅 (가독성)
130
+ const content = JSON.stringify(lockfile, null, 2);
131
+
132
+ // 쓰기
133
+ await Bun.write(lockfilePath, content);
134
+ }
135
+
136
+ /**
137
+ * Lockfile 삭제
138
+ */
139
+ export async function deleteLockfile(projectRoot: string): Promise<boolean> {
140
+ const lockfilePath = path.join(projectRoot, LOCKFILE_PATH);
141
+
142
+ try {
143
+ const file = Bun.file(lockfilePath);
144
+ const exists = await file.exists();
145
+
146
+ if (!exists) {
147
+ return false;
148
+ }
149
+
150
+ const { unlink } = await import("node:fs/promises");
151
+ await unlink(lockfilePath);
152
+ return true;
153
+ } catch {
154
+ return false;
155
+ }
156
+ }
157
+
158
+ // ============================================
159
+ // 유틸리티
160
+ // ============================================
161
+
162
+ /**
163
+ * Lockfile 경로 가져오기
164
+ */
165
+ export function getLockfilePath(projectRoot: string): string {
166
+ return path.join(projectRoot, LOCKFILE_PATH);
167
+ }
168
+
169
+ /**
170
+ * Lockfile 오류 생성 헬퍼
171
+ */
172
+ export function createLockfileError(
173
+ code: LockfileError["code"],
174
+ message: string,
175
+ details?: Record<string, unknown>
176
+ ): LockfileError {
177
+ return { code, message, details };
178
+ }
179
+
180
+ // ============================================
181
+ // Re-exports
182
+ // ============================================
183
+
184
+ export * from "./types.js";
185
+ export * from "./generate.js";
186
+ export * from "./validate.js";