@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.
- 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 +8 -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/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/filling/filling.ts +88 -6
- 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 +2 -0
- 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/runtime/security.ts +155 -0
- package/src/runtime/server.ts +320 -258
- 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
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic Slots - 의미론적 슬롯 검증 시스템
|
|
3
|
+
*
|
|
4
|
+
* 슬롯에 목적과 제약을 명시하여 AI가 그 범위 내에서만 구현하도록 유도
|
|
5
|
+
*
|
|
6
|
+
* @module guard/semantic-slots
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { validateSlotConstraints, type SlotConstraints } from "@mandujs/core/guard";
|
|
11
|
+
*
|
|
12
|
+
* const constraints: SlotConstraints = {
|
|
13
|
+
* maxLines: 50,
|
|
14
|
+
* maxCyclomaticComplexity: 10,
|
|
15
|
+
* requiredPatterns: ["input-validation", "error-handling"],
|
|
16
|
+
* forbiddenPatterns: ["direct-db-write"],
|
|
17
|
+
* allowedImports: ["server/domain/*", "shared/utils/*"],
|
|
18
|
+
* };
|
|
19
|
+
*
|
|
20
|
+
* const result = await validateSlotConstraints(filePath, constraints);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { readFile } from "fs/promises";
|
|
25
|
+
import { join, relative, normalize } from "path";
|
|
26
|
+
|
|
27
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
28
|
+
// Types
|
|
29
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 슬롯 제약 조건
|
|
33
|
+
*/
|
|
34
|
+
export interface SlotConstraints {
|
|
35
|
+
/** 최대 코드 라인 수 */
|
|
36
|
+
maxLines?: number;
|
|
37
|
+
|
|
38
|
+
/** 최대 순환 복잡도 (Cyclomatic Complexity) */
|
|
39
|
+
maxCyclomaticComplexity?: number;
|
|
40
|
+
|
|
41
|
+
/** 필수 패턴 (구현에 포함되어야 함) */
|
|
42
|
+
requiredPatterns?: SlotPattern[];
|
|
43
|
+
|
|
44
|
+
/** 금지 패턴 (구현에 포함되면 안 됨) */
|
|
45
|
+
forbiddenPatterns?: SlotPattern[];
|
|
46
|
+
|
|
47
|
+
/** 허용된 import 경로 (glob 패턴) */
|
|
48
|
+
allowedImports?: string[];
|
|
49
|
+
|
|
50
|
+
/** 금지된 import 경로 (glob 패턴) */
|
|
51
|
+
forbiddenImports?: string[];
|
|
52
|
+
|
|
53
|
+
/** 허용된 함수/메서드 호출 */
|
|
54
|
+
allowedCalls?: string[];
|
|
55
|
+
|
|
56
|
+
/** 금지된 함수/메서드 호출 */
|
|
57
|
+
forbiddenCalls?: string[];
|
|
58
|
+
|
|
59
|
+
/** 커스텀 검증 규칙 */
|
|
60
|
+
customRules?: CustomRule[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 슬롯 패턴 (문자열 또는 정규식)
|
|
65
|
+
*/
|
|
66
|
+
export type SlotPattern =
|
|
67
|
+
| "input-validation"
|
|
68
|
+
| "error-handling"
|
|
69
|
+
| "pagination"
|
|
70
|
+
| "authentication"
|
|
71
|
+
| "authorization"
|
|
72
|
+
| "logging"
|
|
73
|
+
| "caching"
|
|
74
|
+
| "direct-db-write"
|
|
75
|
+
| "external-api-call"
|
|
76
|
+
| "sensitive-data-log"
|
|
77
|
+
| "hardcoded-secret"
|
|
78
|
+
| string;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 커스텀 검증 규칙
|
|
82
|
+
*/
|
|
83
|
+
export interface CustomRule {
|
|
84
|
+
/** 규칙 이름 */
|
|
85
|
+
name: string;
|
|
86
|
+
/** 검증 정규식 */
|
|
87
|
+
pattern: string;
|
|
88
|
+
/** 이 패턴이 있어야 하는지(required) 없어야 하는지(forbidden) */
|
|
89
|
+
type: "required" | "forbidden";
|
|
90
|
+
/** 위반 시 메시지 */
|
|
91
|
+
message: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 슬롯 메타데이터 (Filling API에서 선언)
|
|
96
|
+
*/
|
|
97
|
+
export interface SlotMetadata {
|
|
98
|
+
/** 슬롯 목적 설명 */
|
|
99
|
+
purpose?: string;
|
|
100
|
+
|
|
101
|
+
/** 상세 설명 */
|
|
102
|
+
description?: string;
|
|
103
|
+
|
|
104
|
+
/** 제약 조건 */
|
|
105
|
+
constraints?: SlotConstraints;
|
|
106
|
+
|
|
107
|
+
/** 소유자/담당자 */
|
|
108
|
+
owner?: string;
|
|
109
|
+
|
|
110
|
+
/** 태그 */
|
|
111
|
+
tags?: string[];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 제약 조건 위반
|
|
116
|
+
*/
|
|
117
|
+
export interface ConstraintViolation {
|
|
118
|
+
/** 위반 유형 */
|
|
119
|
+
type:
|
|
120
|
+
| "max-lines-exceeded"
|
|
121
|
+
| "max-complexity-exceeded"
|
|
122
|
+
| "missing-required-pattern"
|
|
123
|
+
| "forbidden-pattern-found"
|
|
124
|
+
| "forbidden-import"
|
|
125
|
+
| "forbidden-call"
|
|
126
|
+
| "custom-rule-violation";
|
|
127
|
+
|
|
128
|
+
/** 위반 메시지 */
|
|
129
|
+
message: string;
|
|
130
|
+
|
|
131
|
+
/** 심각도 */
|
|
132
|
+
severity: "error" | "warn";
|
|
133
|
+
|
|
134
|
+
/** 위반 위치 (라인 번호) */
|
|
135
|
+
line?: number;
|
|
136
|
+
|
|
137
|
+
/** 관련 코드 조각 */
|
|
138
|
+
code?: string;
|
|
139
|
+
|
|
140
|
+
/** 수정 제안 */
|
|
141
|
+
suggestion?: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 슬롯 검증 결과
|
|
146
|
+
*/
|
|
147
|
+
export interface SlotValidationResult {
|
|
148
|
+
/** 유효 여부 */
|
|
149
|
+
valid: boolean;
|
|
150
|
+
|
|
151
|
+
/** 파일 경로 */
|
|
152
|
+
filePath: string;
|
|
153
|
+
|
|
154
|
+
/** 슬롯 메타데이터 */
|
|
155
|
+
metadata?: SlotMetadata;
|
|
156
|
+
|
|
157
|
+
/** 위반 목록 */
|
|
158
|
+
violations: ConstraintViolation[];
|
|
159
|
+
|
|
160
|
+
/** 분석 통계 */
|
|
161
|
+
stats: {
|
|
162
|
+
lines: number;
|
|
163
|
+
cyclomaticComplexity: number;
|
|
164
|
+
importCount: number;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/** 제안 사항 */
|
|
168
|
+
suggestions: string[];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
172
|
+
// Pattern Definitions
|
|
173
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 기본 패턴 정의
|
|
177
|
+
*/
|
|
178
|
+
const PATTERN_DEFINITIONS: Record<string, { regex: RegExp; description: string }> = {
|
|
179
|
+
// 필수 패턴들
|
|
180
|
+
"input-validation": {
|
|
181
|
+
regex: /\.(parse|safeParse|validate|check)\s*\(|z\.(object|string|number|array)\(|yup\.|joi\./,
|
|
182
|
+
description: "Input validation using Zod, Yup, Joi, or similar",
|
|
183
|
+
},
|
|
184
|
+
"error-handling": {
|
|
185
|
+
regex: /try\s*\{[\s\S]*?catch|\.catch\s*\(|throw\s+new\s+\w*Error/,
|
|
186
|
+
description: "Error handling with try-catch or .catch()",
|
|
187
|
+
},
|
|
188
|
+
"pagination": {
|
|
189
|
+
regex: /page|limit|offset|cursor|skip|take/i,
|
|
190
|
+
description: "Pagination parameters handling",
|
|
191
|
+
},
|
|
192
|
+
"authentication": {
|
|
193
|
+
regex: /auth|token|session|jwt|bearer/i,
|
|
194
|
+
description: "Authentication check",
|
|
195
|
+
},
|
|
196
|
+
"authorization": {
|
|
197
|
+
regex: /role|permission|access|can|allow|deny/i,
|
|
198
|
+
description: "Authorization/permission check",
|
|
199
|
+
},
|
|
200
|
+
"logging": {
|
|
201
|
+
regex: /console\.(log|info|warn|error)|logger\.|log\(/,
|
|
202
|
+
description: "Logging statements",
|
|
203
|
+
},
|
|
204
|
+
"caching": {
|
|
205
|
+
regex: /cache|redis|memcached|ttl/i,
|
|
206
|
+
description: "Caching logic",
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
// 금지 패턴들
|
|
210
|
+
"direct-db-write": {
|
|
211
|
+
regex: /\.(insert|update|delete|remove|save)\s*\(|INSERT\s+INTO|UPDATE\s+.*SET|DELETE\s+FROM/i,
|
|
212
|
+
description: "Direct database write operation",
|
|
213
|
+
},
|
|
214
|
+
"external-api-call": {
|
|
215
|
+
regex: /fetch\s*\(|axios\.|http\.(get|post|put|delete)|\.request\s*\(/,
|
|
216
|
+
description: "External API call",
|
|
217
|
+
},
|
|
218
|
+
"sensitive-data-log": {
|
|
219
|
+
regex: /console\.(log|info)\s*\([^)]*(?:password|secret|token|key|credential)/i,
|
|
220
|
+
description: "Logging sensitive data",
|
|
221
|
+
},
|
|
222
|
+
"hardcoded-secret": {
|
|
223
|
+
regex: /(?:password|secret|api_?key|token)\s*[:=]\s*['"][^'"]{8,}['"]/i,
|
|
224
|
+
description: "Hardcoded secret or credential",
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
229
|
+
// Analysis Functions
|
|
230
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 코드 라인 수 계산 (빈 줄, 주석 제외)
|
|
234
|
+
*/
|
|
235
|
+
export function countCodeLines(content: string): number {
|
|
236
|
+
const lines = content.split("\n");
|
|
237
|
+
let count = 0;
|
|
238
|
+
let inBlockComment = false;
|
|
239
|
+
|
|
240
|
+
for (const line of lines) {
|
|
241
|
+
const trimmed = line.trim();
|
|
242
|
+
|
|
243
|
+
// 블록 주석 시작
|
|
244
|
+
if (trimmed.includes("/*") && !trimmed.includes("*/")) {
|
|
245
|
+
inBlockComment = true;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 블록 주석 끝
|
|
250
|
+
if (inBlockComment) {
|
|
251
|
+
if (trimmed.includes("*/")) {
|
|
252
|
+
inBlockComment = false;
|
|
253
|
+
}
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 빈 줄 또는 한 줄 주석
|
|
258
|
+
if (trimmed === "" || trimmed.startsWith("//") || trimmed.startsWith("*")) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
count++;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return count;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* 순환 복잡도 계산 (간단한 근사치)
|
|
270
|
+
* 분기문 개수 + 1
|
|
271
|
+
*/
|
|
272
|
+
export function calculateCyclomaticComplexity(content: string): number {
|
|
273
|
+
// 분기문 패턴들
|
|
274
|
+
const branchPatterns = [
|
|
275
|
+
/\bif\s*\(/g,
|
|
276
|
+
/\belse\s+if\s*\(/g,
|
|
277
|
+
/\bwhile\s*\(/g,
|
|
278
|
+
/\bfor\s*\(/g,
|
|
279
|
+
/\bcase\s+[^:]+:/g,
|
|
280
|
+
/\bcatch\s*\(/g,
|
|
281
|
+
/\?\s*[^:]+\s*:/g, // 삼항 연산자
|
|
282
|
+
/&&/g,
|
|
283
|
+
/\|\|/g,
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
let complexity = 1; // 기본 경로
|
|
287
|
+
|
|
288
|
+
for (const pattern of branchPatterns) {
|
|
289
|
+
const matches = content.match(pattern);
|
|
290
|
+
if (matches) {
|
|
291
|
+
complexity += matches.length;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return complexity;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* import 문 추출
|
|
300
|
+
*/
|
|
301
|
+
export function extractImports(content: string): string[] {
|
|
302
|
+
const imports: string[] = [];
|
|
303
|
+
|
|
304
|
+
// ES6 import
|
|
305
|
+
const esImports = content.matchAll(/import\s+(?:.*?\s+from\s+)?['"]([^'"]+)['"]/g);
|
|
306
|
+
for (const match of esImports) {
|
|
307
|
+
imports.push(match[1]);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// CommonJS require
|
|
311
|
+
const cjsImports = content.matchAll(/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g);
|
|
312
|
+
for (const match of cjsImports) {
|
|
313
|
+
imports.push(match[1]);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Dynamic import
|
|
317
|
+
const dynamicImports = content.matchAll(/import\s*\(\s*['"]([^'"]+)['"]\s*\)/g);
|
|
318
|
+
for (const match of dynamicImports) {
|
|
319
|
+
imports.push(match[1]);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return imports;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* 함수 호출 추출
|
|
327
|
+
*/
|
|
328
|
+
export function extractFunctionCalls(content: string): string[] {
|
|
329
|
+
const calls: string[] = [];
|
|
330
|
+
|
|
331
|
+
// 메서드 호출: obj.method() 또는 method()
|
|
332
|
+
const callMatches = content.matchAll(/(?:(\w+)\.)?(\w+)\s*\(/g);
|
|
333
|
+
for (const match of callMatches) {
|
|
334
|
+
if (match[1]) {
|
|
335
|
+
calls.push(`${match[1]}.${match[2]}`);
|
|
336
|
+
} else {
|
|
337
|
+
calls.push(match[2]);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return [...new Set(calls)]; // 중복 제거
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* 패턴 존재 여부 확인
|
|
346
|
+
*/
|
|
347
|
+
export function checkPattern(content: string, pattern: SlotPattern): boolean {
|
|
348
|
+
const definition = PATTERN_DEFINITIONS[pattern];
|
|
349
|
+
if (definition) {
|
|
350
|
+
return definition.regex.test(content);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// 문자열 또는 정규식으로 처리
|
|
354
|
+
if (pattern.startsWith("/") && pattern.endsWith("/")) {
|
|
355
|
+
const regexPattern = pattern.slice(1, -1);
|
|
356
|
+
const result = safeRegexTest(regexPattern, content);
|
|
357
|
+
// 에러 발생 시 문자열 검색으로 폴백
|
|
358
|
+
if (!result.success) {
|
|
359
|
+
return content.includes(pattern);
|
|
360
|
+
}
|
|
361
|
+
return result.matched;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return content.includes(pattern);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* glob 패턴 매칭 (간단한 구현)
|
|
369
|
+
*/
|
|
370
|
+
function matchGlob(path: string, pattern: string): boolean {
|
|
371
|
+
// * → [^/]*, ** → .*
|
|
372
|
+
const regexPattern = pattern
|
|
373
|
+
.replace(/\*\*/g, "<<<DOUBLE_STAR>>>")
|
|
374
|
+
.replace(/\*/g, "[^/]*")
|
|
375
|
+
.replace(/<<<DOUBLE_STAR>>>/g, ".*");
|
|
376
|
+
|
|
377
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
378
|
+
return regex.test(path);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
382
|
+
// ReDoS Protection
|
|
383
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
384
|
+
|
|
385
|
+
/** ReDoS 공격 방지를 위한 최대 패턴 길이 */
|
|
386
|
+
const MAX_PATTERN_LENGTH = 200;
|
|
387
|
+
|
|
388
|
+
/** ReDoS 공격 방지를 위한 최대 콘텐츠 길이 (커스텀 규칙용) */
|
|
389
|
+
const MAX_CONTENT_LENGTH_FOR_CUSTOM_REGEX = 100_000;
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* 위험한 정규식 패턴 감지
|
|
393
|
+
* 중첩된 quantifier, 과도한 그룹 등 ReDoS 취약점 유발 패턴 탐지
|
|
394
|
+
*/
|
|
395
|
+
function isUnsafeRegexPattern(pattern: string): boolean {
|
|
396
|
+
// 패턴 길이 제한
|
|
397
|
+
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// 위험한 패턴들 (중첩 quantifier, 백트래킹 유발)
|
|
402
|
+
const dangerousPatterns = [
|
|
403
|
+
/\([^)]*[+*][^)]*\)[+*]/, // (a+)+ 또는 (a*)*
|
|
404
|
+
/\([^)]*\|[^)]*\)[+*]/, // (a|b)+ with alternatives
|
|
405
|
+
/\.[+*]\.[+*]/, // .+.+ 또는 .*.*
|
|
406
|
+
/\(\?\:[^)]+\)[+*]{2,}/, // (?:...){n}+ 과도한 반복
|
|
407
|
+
];
|
|
408
|
+
|
|
409
|
+
for (const dangerous of dangerousPatterns) {
|
|
410
|
+
if (dangerous.test(pattern)) {
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* 안전한 정규식 테스트 실행
|
|
420
|
+
* @returns 매칭 결과 또는 에러 시 false
|
|
421
|
+
*/
|
|
422
|
+
function safeRegexTest(
|
|
423
|
+
pattern: string,
|
|
424
|
+
content: string,
|
|
425
|
+
maxContentLength = MAX_CONTENT_LENGTH_FOR_CUSTOM_REGEX
|
|
426
|
+
): { success: boolean; matched: boolean; error?: string } {
|
|
427
|
+
// 패턴 안전성 검사
|
|
428
|
+
if (isUnsafeRegexPattern(pattern)) {
|
|
429
|
+
return {
|
|
430
|
+
success: false,
|
|
431
|
+
matched: false,
|
|
432
|
+
error: `Pattern may cause ReDoS: ${pattern.substring(0, 50)}...`,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// 콘텐츠 길이 제한 (ReDoS 방지)
|
|
437
|
+
const safeContent =
|
|
438
|
+
content.length > maxContentLength ? content.substring(0, maxContentLength) : content;
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
const regex = new RegExp(pattern);
|
|
442
|
+
const matched = regex.test(safeContent);
|
|
443
|
+
return { success: true, matched };
|
|
444
|
+
} catch {
|
|
445
|
+
return {
|
|
446
|
+
success: false,
|
|
447
|
+
matched: false,
|
|
448
|
+
error: `Invalid regex pattern: ${pattern.substring(0, 50)}`,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
454
|
+
// Validation Functions
|
|
455
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* 슬롯 제약 조건 검증
|
|
459
|
+
*/
|
|
460
|
+
export async function validateSlotConstraints(
|
|
461
|
+
filePath: string,
|
|
462
|
+
constraints: SlotConstraints,
|
|
463
|
+
rootDir?: string
|
|
464
|
+
): Promise<SlotValidationResult> {
|
|
465
|
+
const violations: ConstraintViolation[] = [];
|
|
466
|
+
const suggestions: string[] = [];
|
|
467
|
+
|
|
468
|
+
// 파일 읽기
|
|
469
|
+
let content: string;
|
|
470
|
+
try {
|
|
471
|
+
content = await readFile(filePath, "utf-8");
|
|
472
|
+
} catch {
|
|
473
|
+
return {
|
|
474
|
+
valid: false,
|
|
475
|
+
filePath,
|
|
476
|
+
violations: [
|
|
477
|
+
{
|
|
478
|
+
type: "custom-rule-violation",
|
|
479
|
+
message: `Cannot read file: ${filePath}`,
|
|
480
|
+
severity: "error",
|
|
481
|
+
},
|
|
482
|
+
],
|
|
483
|
+
stats: { lines: 0, cyclomaticComplexity: 0, importCount: 0 },
|
|
484
|
+
suggestions: [],
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// 통계 계산
|
|
489
|
+
const lines = countCodeLines(content);
|
|
490
|
+
const cyclomaticComplexity = calculateCyclomaticComplexity(content);
|
|
491
|
+
const imports = extractImports(content);
|
|
492
|
+
const calls = extractFunctionCalls(content);
|
|
493
|
+
|
|
494
|
+
// 1. 최대 라인 수 검증
|
|
495
|
+
if (constraints.maxLines && lines > constraints.maxLines) {
|
|
496
|
+
violations.push({
|
|
497
|
+
type: "max-lines-exceeded",
|
|
498
|
+
message: `Code has ${lines} lines, exceeds limit of ${constraints.maxLines}`,
|
|
499
|
+
severity: "warn",
|
|
500
|
+
suggestion: "Consider splitting into smaller functions or extracting logic to separate modules",
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// 2. 최대 복잡도 검증
|
|
505
|
+
if (constraints.maxCyclomaticComplexity && cyclomaticComplexity > constraints.maxCyclomaticComplexity) {
|
|
506
|
+
violations.push({
|
|
507
|
+
type: "max-complexity-exceeded",
|
|
508
|
+
message: `Cyclomatic complexity is ${cyclomaticComplexity}, exceeds limit of ${constraints.maxCyclomaticComplexity}`,
|
|
509
|
+
severity: "warn",
|
|
510
|
+
suggestion: "Reduce branching logic, extract helper functions, or use early returns",
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// 3. 필수 패턴 검증
|
|
515
|
+
if (constraints.requiredPatterns) {
|
|
516
|
+
for (const pattern of constraints.requiredPatterns) {
|
|
517
|
+
if (!checkPattern(content, pattern)) {
|
|
518
|
+
const def = PATTERN_DEFINITIONS[pattern];
|
|
519
|
+
violations.push({
|
|
520
|
+
type: "missing-required-pattern",
|
|
521
|
+
message: `Missing required pattern: ${pattern}${def ? ` (${def.description})` : ""}`,
|
|
522
|
+
severity: "error",
|
|
523
|
+
suggestion: `Add ${pattern} to the implementation`,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// 4. 금지 패턴 검증
|
|
530
|
+
if (constraints.forbiddenPatterns) {
|
|
531
|
+
for (const pattern of constraints.forbiddenPatterns) {
|
|
532
|
+
if (checkPattern(content, pattern)) {
|
|
533
|
+
const def = PATTERN_DEFINITIONS[pattern];
|
|
534
|
+
violations.push({
|
|
535
|
+
type: "forbidden-pattern-found",
|
|
536
|
+
message: `Forbidden pattern found: ${pattern}${def ? ` (${def.description})` : ""}`,
|
|
537
|
+
severity: "error",
|
|
538
|
+
suggestion: `Remove ${pattern} from the implementation or move to appropriate layer`,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// 5. import 검증
|
|
545
|
+
if (constraints.allowedImports || constraints.forbiddenImports) {
|
|
546
|
+
for (const importPath of imports) {
|
|
547
|
+
// 금지된 import 확인
|
|
548
|
+
if (constraints.forbiddenImports) {
|
|
549
|
+
for (const forbidden of constraints.forbiddenImports) {
|
|
550
|
+
if (matchGlob(importPath, forbidden)) {
|
|
551
|
+
violations.push({
|
|
552
|
+
type: "forbidden-import",
|
|
553
|
+
message: `Forbidden import: ${importPath} (matches ${forbidden})`,
|
|
554
|
+
severity: "error",
|
|
555
|
+
suggestion: `Remove this import or use an allowed alternative`,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// 허용된 import 확인 (allowedImports가 있으면 그 외는 모두 금지)
|
|
562
|
+
if (constraints.allowedImports && constraints.allowedImports.length > 0) {
|
|
563
|
+
// 외부 패키지 (node_modules)는 기본 허용
|
|
564
|
+
const isExternalPackage = !importPath.startsWith(".") && !importPath.startsWith("@/") && !importPath.startsWith("~/");
|
|
565
|
+
if (!isExternalPackage) {
|
|
566
|
+
const isAllowed = constraints.allowedImports.some((allowed) => matchGlob(importPath, allowed));
|
|
567
|
+
if (!isAllowed) {
|
|
568
|
+
violations.push({
|
|
569
|
+
type: "forbidden-import",
|
|
570
|
+
message: `Import not in allowed list: ${importPath}`,
|
|
571
|
+
severity: "warn",
|
|
572
|
+
suggestion: `Only these imports are allowed: ${constraints.allowedImports.join(", ")}`,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// 6. 함수 호출 검증
|
|
581
|
+
if (constraints.forbiddenCalls) {
|
|
582
|
+
for (const call of calls) {
|
|
583
|
+
for (const forbidden of constraints.forbiddenCalls) {
|
|
584
|
+
if (call === forbidden || call.endsWith(`.${forbidden}`)) {
|
|
585
|
+
violations.push({
|
|
586
|
+
type: "forbidden-call",
|
|
587
|
+
message: `Forbidden function call: ${call}`,
|
|
588
|
+
severity: "error",
|
|
589
|
+
suggestion: `Remove or replace this function call`,
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// 7. 커스텀 규칙 검증 (ReDoS 방어 적용)
|
|
597
|
+
if (constraints.customRules) {
|
|
598
|
+
for (const rule of constraints.customRules) {
|
|
599
|
+
const result = safeRegexTest(rule.pattern, content);
|
|
600
|
+
|
|
601
|
+
// 정규식 에러 시 경고만 추가하고 건너뜀
|
|
602
|
+
if (!result.success) {
|
|
603
|
+
violations.push({
|
|
604
|
+
type: "custom-rule-violation",
|
|
605
|
+
message: `Unable to apply rule: ${result.error}`,
|
|
606
|
+
severity: "warn",
|
|
607
|
+
});
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (rule.type === "required" && !result.matched) {
|
|
612
|
+
violations.push({
|
|
613
|
+
type: "custom-rule-violation",
|
|
614
|
+
message: rule.message,
|
|
615
|
+
severity: "error",
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (rule.type === "forbidden" && result.matched) {
|
|
620
|
+
violations.push({
|
|
621
|
+
type: "custom-rule-violation",
|
|
622
|
+
message: rule.message,
|
|
623
|
+
severity: "error",
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// 제안 생성
|
|
630
|
+
if (violations.length === 0) {
|
|
631
|
+
suggestions.push("✅ All constraints satisfied");
|
|
632
|
+
} else {
|
|
633
|
+
const errorCount = violations.filter((v) => v.severity === "error").length;
|
|
634
|
+
const warnCount = violations.filter((v) => v.severity === "warn").length;
|
|
635
|
+
|
|
636
|
+
if (errorCount > 0) {
|
|
637
|
+
suggestions.push(`Fix ${errorCount} error(s) before proceeding`);
|
|
638
|
+
}
|
|
639
|
+
if (warnCount > 0) {
|
|
640
|
+
suggestions.push(`Consider addressing ${warnCount} warning(s) for better code quality`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return {
|
|
645
|
+
valid: violations.filter((v) => v.severity === "error").length === 0,
|
|
646
|
+
filePath,
|
|
647
|
+
violations,
|
|
648
|
+
stats: {
|
|
649
|
+
lines,
|
|
650
|
+
cyclomaticComplexity,
|
|
651
|
+
importCount: imports.length,
|
|
652
|
+
},
|
|
653
|
+
suggestions,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* 슬롯 메타데이터에서 제약 조건 추출 (파일 파싱)
|
|
659
|
+
*/
|
|
660
|
+
export async function extractSlotMetadata(filePath: string): Promise<SlotMetadata | null> {
|
|
661
|
+
try {
|
|
662
|
+
const content = await readFile(filePath, "utf-8");
|
|
663
|
+
|
|
664
|
+
// .purpose() 호출 찾기
|
|
665
|
+
const purposeMatch = content.match(/\.purpose\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/);
|
|
666
|
+
const purpose = purposeMatch?.[1];
|
|
667
|
+
|
|
668
|
+
// .description() 호출 찾기
|
|
669
|
+
const descMatch = content.match(/\.description\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/);
|
|
670
|
+
const description = descMatch?.[1];
|
|
671
|
+
|
|
672
|
+
// .constraints() 호출 찾기
|
|
673
|
+
// Note: 이 파서는 간단한 객체만 지원합니다.
|
|
674
|
+
// 복잡한 constraints는 런타임에 getSemanticMetadata()로 조회하세요.
|
|
675
|
+
const constraintsMatch = content.match(/\.constraints\s*\(\s*(\{[\s\S]*?\})\s*\)/);
|
|
676
|
+
let constraints: SlotConstraints | undefined;
|
|
677
|
+
if (constraintsMatch) {
|
|
678
|
+
try {
|
|
679
|
+
// 간단한 객체 리터럴 파싱 - 키-값 쌍을 개별 추출
|
|
680
|
+
const objStr = constraintsMatch[1];
|
|
681
|
+
const result: Record<string, unknown> = {};
|
|
682
|
+
|
|
683
|
+
// 숫자 값 추출 (maxLines: 50 등)
|
|
684
|
+
const numberMatches = objStr.matchAll(/(\w+)\s*:\s*(\d+)/g);
|
|
685
|
+
for (const match of numberMatches) {
|
|
686
|
+
result[match[1]] = parseInt(match[2], 10);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// 문자열 배열 추출 (requiredPatterns: ["a", "b"] 등)
|
|
690
|
+
const arrayMatches = objStr.matchAll(/(\w+)\s*:\s*\[([\s\S]*?)\]/g);
|
|
691
|
+
for (const match of arrayMatches) {
|
|
692
|
+
const items = match[2].match(/['"`]([^'"`]+)['"`]/g);
|
|
693
|
+
if (items) {
|
|
694
|
+
result[match[1]] = items.map((s) => s.slice(1, -1));
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (Object.keys(result).length > 0) {
|
|
699
|
+
constraints = result as SlotConstraints;
|
|
700
|
+
}
|
|
701
|
+
} catch {
|
|
702
|
+
// 파싱 실패 시 무시 - 런타임에 getSemanticMetadata() 사용 권장
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (!purpose && !description && !constraints) {
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return {
|
|
711
|
+
purpose,
|
|
712
|
+
description,
|
|
713
|
+
constraints,
|
|
714
|
+
};
|
|
715
|
+
} catch {
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* 슬롯 목록 검증
|
|
722
|
+
*/
|
|
723
|
+
export async function validateSlots(
|
|
724
|
+
slotFiles: string[],
|
|
725
|
+
defaultConstraints?: SlotConstraints
|
|
726
|
+
): Promise<{
|
|
727
|
+
totalSlots: number;
|
|
728
|
+
validSlots: number;
|
|
729
|
+
invalidSlots: number;
|
|
730
|
+
results: SlotValidationResult[];
|
|
731
|
+
}> {
|
|
732
|
+
const results: SlotValidationResult[] = [];
|
|
733
|
+
|
|
734
|
+
for (const filePath of slotFiles) {
|
|
735
|
+
// 파일에서 메타데이터 추출 시도
|
|
736
|
+
const metadata = await extractSlotMetadata(filePath);
|
|
737
|
+
const constraints = metadata?.constraints || defaultConstraints;
|
|
738
|
+
|
|
739
|
+
if (constraints) {
|
|
740
|
+
const result = await validateSlotConstraints(filePath, constraints);
|
|
741
|
+
result.metadata = metadata || undefined;
|
|
742
|
+
results.push(result);
|
|
743
|
+
} else {
|
|
744
|
+
// 제약 조건 없으면 기본 검증만
|
|
745
|
+
results.push({
|
|
746
|
+
valid: true,
|
|
747
|
+
filePath,
|
|
748
|
+
violations: [],
|
|
749
|
+
stats: { lines: 0, cyclomaticComplexity: 0, importCount: 0 },
|
|
750
|
+
suggestions: ["No constraints defined for this slot"],
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const validSlots = results.filter((r) => r.valid).length;
|
|
756
|
+
|
|
757
|
+
return {
|
|
758
|
+
totalSlots: slotFiles.length,
|
|
759
|
+
validSlots,
|
|
760
|
+
invalidSlots: slotFiles.length - validSlots,
|
|
761
|
+
results,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
766
|
+
// Default Constraints
|
|
767
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* 기본 슬롯 제약 조건
|
|
771
|
+
*/
|
|
772
|
+
export const DEFAULT_SLOT_CONSTRAINTS: SlotConstraints = {
|
|
773
|
+
maxLines: 100,
|
|
774
|
+
maxCyclomaticComplexity: 15,
|
|
775
|
+
forbiddenPatterns: ["hardcoded-secret", "sensitive-data-log"],
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* API 슬롯용 권장 제약 조건
|
|
780
|
+
*/
|
|
781
|
+
export const API_SLOT_CONSTRAINTS: SlotConstraints = {
|
|
782
|
+
maxLines: 80,
|
|
783
|
+
maxCyclomaticComplexity: 12,
|
|
784
|
+
requiredPatterns: ["input-validation", "error-handling"],
|
|
785
|
+
forbiddenPatterns: ["hardcoded-secret", "sensitive-data-log"],
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* 읽기 전용 API 슬롯용 제약 조건
|
|
790
|
+
*/
|
|
791
|
+
export const READONLY_SLOT_CONSTRAINTS: SlotConstraints = {
|
|
792
|
+
maxLines: 50,
|
|
793
|
+
maxCyclomaticComplexity: 10,
|
|
794
|
+
requiredPatterns: ["error-handling"],
|
|
795
|
+
forbiddenPatterns: ["direct-db-write", "hardcoded-secret"],
|
|
796
|
+
};
|