@yuaone/core 0.1.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 (235) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +15 -0
  3. package/dist/__tests__/context-manager.test.d.ts +6 -0
  4. package/dist/__tests__/context-manager.test.d.ts.map +1 -0
  5. package/dist/__tests__/context-manager.test.js +220 -0
  6. package/dist/__tests__/context-manager.test.js.map +1 -0
  7. package/dist/__tests__/governor.test.d.ts +6 -0
  8. package/dist/__tests__/governor.test.d.ts.map +1 -0
  9. package/dist/__tests__/governor.test.js +210 -0
  10. package/dist/__tests__/governor.test.js.map +1 -0
  11. package/dist/__tests__/model-router.test.d.ts +6 -0
  12. package/dist/__tests__/model-router.test.d.ts.map +1 -0
  13. package/dist/__tests__/model-router.test.js +329 -0
  14. package/dist/__tests__/model-router.test.js.map +1 -0
  15. package/dist/agent-logger.d.ts +384 -0
  16. package/dist/agent-logger.d.ts.map +1 -0
  17. package/dist/agent-logger.js +820 -0
  18. package/dist/agent-logger.js.map +1 -0
  19. package/dist/agent-loop.d.ts +163 -0
  20. package/dist/agent-loop.d.ts.map +1 -0
  21. package/dist/agent-loop.js +609 -0
  22. package/dist/agent-loop.js.map +1 -0
  23. package/dist/agent-modes.d.ts +85 -0
  24. package/dist/agent-modes.d.ts.map +1 -0
  25. package/dist/agent-modes.js +418 -0
  26. package/dist/agent-modes.js.map +1 -0
  27. package/dist/approval.d.ts +137 -0
  28. package/dist/approval.d.ts.map +1 -0
  29. package/dist/approval.js +299 -0
  30. package/dist/approval.js.map +1 -0
  31. package/dist/async-completion-queue.d.ts +56 -0
  32. package/dist/async-completion-queue.d.ts.map +1 -0
  33. package/dist/async-completion-queue.js +77 -0
  34. package/dist/async-completion-queue.js.map +1 -0
  35. package/dist/auto-fix.d.ts +174 -0
  36. package/dist/auto-fix.d.ts.map +1 -0
  37. package/dist/auto-fix.js +319 -0
  38. package/dist/auto-fix.js.map +1 -0
  39. package/dist/codebase-context.d.ts +396 -0
  40. package/dist/codebase-context.d.ts.map +1 -0
  41. package/dist/codebase-context.js +1260 -0
  42. package/dist/codebase-context.js.map +1 -0
  43. package/dist/conflict-resolver.d.ts +191 -0
  44. package/dist/conflict-resolver.d.ts.map +1 -0
  45. package/dist/conflict-resolver.js +524 -0
  46. package/dist/conflict-resolver.js.map +1 -0
  47. package/dist/constants.d.ts +52 -0
  48. package/dist/constants.d.ts.map +1 -0
  49. package/dist/constants.js +141 -0
  50. package/dist/constants.js.map +1 -0
  51. package/dist/context-budget.d.ts +435 -0
  52. package/dist/context-budget.d.ts.map +1 -0
  53. package/dist/context-budget.js +903 -0
  54. package/dist/context-budget.js.map +1 -0
  55. package/dist/context-compressor.d.ts +143 -0
  56. package/dist/context-compressor.d.ts.map +1 -0
  57. package/dist/context-compressor.js +511 -0
  58. package/dist/context-compressor.js.map +1 -0
  59. package/dist/context-manager.d.ts +112 -0
  60. package/dist/context-manager.d.ts.map +1 -0
  61. package/dist/context-manager.js +247 -0
  62. package/dist/context-manager.js.map +1 -0
  63. package/dist/continuous-reflection.d.ts +267 -0
  64. package/dist/continuous-reflection.d.ts.map +1 -0
  65. package/dist/continuous-reflection.js +338 -0
  66. package/dist/continuous-reflection.js.map +1 -0
  67. package/dist/cross-file-refactor.d.ts +352 -0
  68. package/dist/cross-file-refactor.d.ts.map +1 -0
  69. package/dist/cross-file-refactor.js +1544 -0
  70. package/dist/cross-file-refactor.js.map +1 -0
  71. package/dist/dag-orchestrator.d.ts +138 -0
  72. package/dist/dag-orchestrator.d.ts.map +1 -0
  73. package/dist/dag-orchestrator.js +379 -0
  74. package/dist/dag-orchestrator.js.map +1 -0
  75. package/dist/debate-orchestrator.d.ts +301 -0
  76. package/dist/debate-orchestrator.d.ts.map +1 -0
  77. package/dist/debate-orchestrator.js +719 -0
  78. package/dist/debate-orchestrator.js.map +1 -0
  79. package/dist/dependency-analyzer.d.ts +113 -0
  80. package/dist/dependency-analyzer.d.ts.map +1 -0
  81. package/dist/dependency-analyzer.js +444 -0
  82. package/dist/dependency-analyzer.js.map +1 -0
  83. package/dist/design-loop.d.ts +59 -0
  84. package/dist/design-loop.d.ts.map +1 -0
  85. package/dist/design-loop.js +344 -0
  86. package/dist/design-loop.js.map +1 -0
  87. package/dist/doc-intelligence.d.ts +383 -0
  88. package/dist/doc-intelligence.d.ts.map +1 -0
  89. package/dist/doc-intelligence.js +1307 -0
  90. package/dist/doc-intelligence.js.map +1 -0
  91. package/dist/dynamic-role-generator.d.ts +76 -0
  92. package/dist/dynamic-role-generator.d.ts.map +1 -0
  93. package/dist/dynamic-role-generator.js +194 -0
  94. package/dist/dynamic-role-generator.js.map +1 -0
  95. package/dist/errors.d.ts +69 -0
  96. package/dist/errors.d.ts.map +1 -0
  97. package/dist/errors.js +102 -0
  98. package/dist/errors.js.map +1 -0
  99. package/dist/event-bus.d.ts +159 -0
  100. package/dist/event-bus.d.ts.map +1 -0
  101. package/dist/event-bus.js +305 -0
  102. package/dist/event-bus.js.map +1 -0
  103. package/dist/execution-engine.d.ts +425 -0
  104. package/dist/execution-engine.d.ts.map +1 -0
  105. package/dist/execution-engine.js +1555 -0
  106. package/dist/execution-engine.js.map +1 -0
  107. package/dist/git-intelligence.d.ts +306 -0
  108. package/dist/git-intelligence.d.ts.map +1 -0
  109. package/dist/git-intelligence.js +1099 -0
  110. package/dist/git-intelligence.js.map +1 -0
  111. package/dist/governor.d.ts +77 -0
  112. package/dist/governor.d.ts.map +1 -0
  113. package/dist/governor.js +161 -0
  114. package/dist/governor.js.map +1 -0
  115. package/dist/hierarchical-planner.d.ts +313 -0
  116. package/dist/hierarchical-planner.d.ts.map +1 -0
  117. package/dist/hierarchical-planner.js +981 -0
  118. package/dist/hierarchical-planner.js.map +1 -0
  119. package/dist/index.d.ts +121 -0
  120. package/dist/index.d.ts.map +1 -0
  121. package/dist/index.js +123 -0
  122. package/dist/index.js.map +1 -0
  123. package/dist/intent-inference.d.ts +103 -0
  124. package/dist/intent-inference.d.ts.map +1 -0
  125. package/dist/intent-inference.js +605 -0
  126. package/dist/intent-inference.js.map +1 -0
  127. package/dist/interrupt-manager.d.ts +143 -0
  128. package/dist/interrupt-manager.d.ts.map +1 -0
  129. package/dist/interrupt-manager.js +196 -0
  130. package/dist/interrupt-manager.js.map +1 -0
  131. package/dist/kernel.d.ts +564 -0
  132. package/dist/kernel.d.ts.map +1 -0
  133. package/dist/kernel.js +1419 -0
  134. package/dist/kernel.js.map +1 -0
  135. package/dist/language-support.d.ts +232 -0
  136. package/dist/language-support.d.ts.map +1 -0
  137. package/dist/language-support.js +1134 -0
  138. package/dist/language-support.js.map +1 -0
  139. package/dist/llm-client.d.ts +82 -0
  140. package/dist/llm-client.d.ts.map +1 -0
  141. package/dist/llm-client.js +475 -0
  142. package/dist/llm-client.js.map +1 -0
  143. package/dist/mcp-client.d.ts +232 -0
  144. package/dist/mcp-client.d.ts.map +1 -0
  145. package/dist/mcp-client.js +718 -0
  146. package/dist/mcp-client.js.map +1 -0
  147. package/dist/memory-manager.d.ts +200 -0
  148. package/dist/memory-manager.d.ts.map +1 -0
  149. package/dist/memory-manager.js +568 -0
  150. package/dist/memory-manager.js.map +1 -0
  151. package/dist/memory.d.ts +87 -0
  152. package/dist/memory.d.ts.map +1 -0
  153. package/dist/memory.js +341 -0
  154. package/dist/memory.js.map +1 -0
  155. package/dist/model-router.d.ts +245 -0
  156. package/dist/model-router.d.ts.map +1 -0
  157. package/dist/model-router.js +632 -0
  158. package/dist/model-router.js.map +1 -0
  159. package/dist/parallel-executor.d.ts +125 -0
  160. package/dist/parallel-executor.d.ts.map +1 -0
  161. package/dist/parallel-executor.js +201 -0
  162. package/dist/parallel-executor.js.map +1 -0
  163. package/dist/perf-optimizer.d.ts +212 -0
  164. package/dist/perf-optimizer.d.ts.map +1 -0
  165. package/dist/perf-optimizer.js +721 -0
  166. package/dist/perf-optimizer.js.map +1 -0
  167. package/dist/persona.d.ts +305 -0
  168. package/dist/persona.d.ts.map +1 -0
  169. package/dist/persona.js +887 -0
  170. package/dist/persona.js.map +1 -0
  171. package/dist/planner.d.ts +70 -0
  172. package/dist/planner.d.ts.map +1 -0
  173. package/dist/planner.js +264 -0
  174. package/dist/planner.js.map +1 -0
  175. package/dist/qa-pipeline.d.ts +365 -0
  176. package/dist/qa-pipeline.d.ts.map +1 -0
  177. package/dist/qa-pipeline.js +1352 -0
  178. package/dist/qa-pipeline.js.map +1 -0
  179. package/dist/reasoning-adapter.d.ts +116 -0
  180. package/dist/reasoning-adapter.d.ts.map +1 -0
  181. package/dist/reasoning-adapter.js +187 -0
  182. package/dist/reasoning-adapter.js.map +1 -0
  183. package/dist/role-registry.d.ts +55 -0
  184. package/dist/role-registry.d.ts.map +1 -0
  185. package/dist/role-registry.js +192 -0
  186. package/dist/role-registry.js.map +1 -0
  187. package/dist/sandbox-tiers.d.ts +327 -0
  188. package/dist/sandbox-tiers.d.ts.map +1 -0
  189. package/dist/sandbox-tiers.js +928 -0
  190. package/dist/sandbox-tiers.js.map +1 -0
  191. package/dist/security-scanner.d.ts +222 -0
  192. package/dist/security-scanner.d.ts.map +1 -0
  193. package/dist/security-scanner.js +1129 -0
  194. package/dist/security-scanner.js.map +1 -0
  195. package/dist/security.d.ts +93 -0
  196. package/dist/security.d.ts.map +1 -0
  197. package/dist/security.js +393 -0
  198. package/dist/security.js.map +1 -0
  199. package/dist/self-reflection.d.ts +397 -0
  200. package/dist/self-reflection.d.ts.map +1 -0
  201. package/dist/self-reflection.js +908 -0
  202. package/dist/self-reflection.js.map +1 -0
  203. package/dist/session-persistence.d.ts +191 -0
  204. package/dist/session-persistence.d.ts.map +1 -0
  205. package/dist/session-persistence.js +395 -0
  206. package/dist/session-persistence.js.map +1 -0
  207. package/dist/speculative-executor.d.ts +210 -0
  208. package/dist/speculative-executor.d.ts.map +1 -0
  209. package/dist/speculative-executor.js +618 -0
  210. package/dist/speculative-executor.js.map +1 -0
  211. package/dist/state-machine.d.ts +289 -0
  212. package/dist/state-machine.d.ts.map +1 -0
  213. package/dist/state-machine.js +695 -0
  214. package/dist/state-machine.js.map +1 -0
  215. package/dist/sub-agent.d.ts +177 -0
  216. package/dist/sub-agent.d.ts.map +1 -0
  217. package/dist/sub-agent.js +303 -0
  218. package/dist/sub-agent.js.map +1 -0
  219. package/dist/system-prompt.d.ts +26 -0
  220. package/dist/system-prompt.d.ts.map +1 -0
  221. package/dist/system-prompt.js +84 -0
  222. package/dist/system-prompt.js.map +1 -0
  223. package/dist/test-intelligence.d.ts +439 -0
  224. package/dist/test-intelligence.d.ts.map +1 -0
  225. package/dist/test-intelligence.js +1165 -0
  226. package/dist/test-intelligence.js.map +1 -0
  227. package/dist/types.d.ts +632 -0
  228. package/dist/types.d.ts.map +1 -0
  229. package/dist/types.js +6 -0
  230. package/dist/types.js.map +1 -0
  231. package/dist/vector-index.d.ts +314 -0
  232. package/dist/vector-index.d.ts.map +1 -0
  233. package/dist/vector-index.js +618 -0
  234. package/dist/vector-index.js.map +1 -0
  235. package/package.json +41 -0
@@ -0,0 +1,1352 @@
1
+ /**
2
+ * @module qa-pipeline
3
+ * @description 5-Stage QA Agent Pipeline — 코드 변경 후 자동 품질 검증.
4
+ *
5
+ * Stages:
6
+ * 1. Structural — TypeScript 컴파일, ESLint, 순환 import, export 검증
7
+ * 2. Semantic — 유닛/통합 테스트 실행
8
+ * 3. Quality — 복잡도, 함수/파일 길이, TODO, 디버그 문, 보안 스캔
9
+ * 4. Review — LLM 기반 코드 리뷰 (thorough 모드 전용)
10
+ * 5. Decision — 전체 결과 기반 자동 판정 (approve / fix_and_retry / escalate)
11
+ *
12
+ * QALevel 프리셋:
13
+ * - quick: structural only
14
+ * - standard: structural + semantic + quality
15
+ * - thorough: all 5 stages
16
+ */
17
+ import { EventEmitter } from "node:events";
18
+ import { execFile } from "node:child_process";
19
+ import { readFile, readdir, access, constants } from "node:fs/promises";
20
+ import path from "node:path";
21
+ // ══════════════════════════════════════════════════════════════════════
22
+ // Defaults
23
+ // ══════════════════════════════════════════════════════════════════════
24
+ const DEFAULT_QUALITY_GATES = {
25
+ maxCyclomaticComplexity: 15,
26
+ maxFunctionLength: 50,
27
+ maxFileLength: 500,
28
+ minTestCoverage: 0,
29
+ maxTodoCount: -1,
30
+ noNewWarnings: true,
31
+ };
32
+ /** QALevel에 따른 stage toggle 프리셋 */
33
+ function applyLevelDefaults(level) {
34
+ switch (level) {
35
+ case "quick":
36
+ return {
37
+ enableStructural: true,
38
+ enableSemantic: false,
39
+ enableQuality: false,
40
+ enableReview: false,
41
+ enableDecision: true,
42
+ };
43
+ case "standard":
44
+ return {
45
+ enableStructural: true,
46
+ enableSemantic: true,
47
+ enableQuality: true,
48
+ enableReview: false,
49
+ enableDecision: true,
50
+ };
51
+ case "thorough":
52
+ return {
53
+ enableStructural: true,
54
+ enableSemantic: true,
55
+ enableQuality: true,
56
+ enableReview: true,
57
+ enableDecision: true,
58
+ };
59
+ }
60
+ }
61
+ function buildFullConfig(partial) {
62
+ const level = partial.level ?? "standard";
63
+ const levelDefaults = applyLevelDefaults(level);
64
+ return {
65
+ projectPath: partial.projectPath,
66
+ level,
67
+ enableStructural: partial.enableStructural ?? levelDefaults.enableStructural,
68
+ enableSemantic: partial.enableSemantic ?? levelDefaults.enableSemantic,
69
+ enableQuality: partial.enableQuality ?? levelDefaults.enableQuality,
70
+ enableReview: partial.enableReview ?? levelDefaults.enableReview,
71
+ enableDecision: partial.enableDecision ?? levelDefaults.enableDecision,
72
+ autoFix: partial.autoFix ?? true,
73
+ maxFixAttempts: partial.maxFixAttempts ?? 3,
74
+ fixableCategories: partial.fixableCategories ?? ["lint", "format", "imports", "types"],
75
+ qualityGates: { ...DEFAULT_QUALITY_GATES, ...partial.qualityGates },
76
+ buildTimeout: partial.buildTimeout ?? 60_000,
77
+ testTimeout: partial.testTimeout ?? 120_000,
78
+ };
79
+ }
80
+ // Control-flow keywords that increase cyclomatic complexity
81
+ const COMPLEXITY_KEYWORDS = /\b(if|else\s+if|for|while|do|switch|case|catch)\b/g;
82
+ const LOGICAL_OPERATORS = /(\&\&|\|\||\?\?)/g;
83
+ const TERNARY_OPERATOR = /\?[^:?]*:/g;
84
+ // Function detection (named functions, methods, arrow functions assigned to const/let)
85
+ const FUNCTION_PATTERN = /(?:(?:async\s+)?function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\(|(\w+)\s*\([^)]*\)\s*\{|(\w+)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>)/g;
86
+ // ══════════════════════════════════════════════════════════════════════
87
+ // Security Patterns
88
+ // ══════════════════════════════════════════════════════════════════════
89
+ const SECURITY_PATTERNS = [
90
+ { name: "Hardcoded password", pattern: /password\s*=\s*["'][^"']+["']/gi, severity: "critical" },
91
+ { name: "Hardcoded API key", pattern: /(?:api[_-]?key|apikey|secret[_-]?key)\s*[:=]\s*["'][A-Za-z0-9+/=]{16,}["']/gi, severity: "critical" },
92
+ { name: "AWS access key", pattern: /AKIA[0-9A-Z]{16}/g, severity: "critical" },
93
+ { name: "Private key", pattern: /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/g, severity: "critical" },
94
+ { name: "eval() usage", pattern: /\beval\s*\(/g, severity: "high" },
95
+ { name: "new Function() usage", pattern: /new\s+Function\s*\(/g, severity: "high" },
96
+ { name: "SQL injection risk", pattern: /(?:query|execute)\s*\(\s*`[^`]*\$\{/g, severity: "high" },
97
+ { name: "Path traversal", pattern: /\.\.\//g, severity: "medium" },
98
+ { name: "innerHTML assignment", pattern: /\.innerHTML\s*=/g, severity: "medium" },
99
+ ];
100
+ // ══════════════════════════════════════════════════════════════════════
101
+ // QAPipeline
102
+ // ══════════════════════════════════════════════════════════════════════
103
+ /**
104
+ * QAPipeline — 5단계 자동 품질 검증 파이프라인.
105
+ *
106
+ * @example
107
+ * ```typescript
108
+ * const qa = new QAPipeline({ projectPath: "/project", level: "standard" });
109
+ *
110
+ * qa.on("stage:complete", (result) => console.log(result.stage, result.status));
111
+ *
112
+ * const result = await qa.run(["src/foo.ts", "src/bar.ts"]);
113
+ * if (result.decision.action === "approve") {
114
+ * console.log("All checks passed!");
115
+ * }
116
+ * ```
117
+ */
118
+ export class QAPipeline extends EventEmitter {
119
+ config;
120
+ constructor(config) {
121
+ super();
122
+ this.config = buildFullConfig(config);
123
+ }
124
+ // ─── Main ───────────────────────────────────────────────────────────
125
+ /**
126
+ * 전체 파이프라인 실행.
127
+ *
128
+ * @param changedFiles 변경된 파일 경로 목록 (없으면 전체 검사)
129
+ * @param reviewFn LLM 리뷰 함수 (review stage 활성화 시 필요)
130
+ * @returns 파이프라인 전체 결과
131
+ */
132
+ async run(changedFiles, reviewFn) {
133
+ const stages = [];
134
+ const startTime = Date.now();
135
+ const gateResults = [];
136
+ // Stage 1: Structural
137
+ if (this.config.enableStructural) {
138
+ this.emit("stage:start", "structural");
139
+ let result = await this.runStructural();
140
+ stages.push(result);
141
+ this.emit("stage:complete", result);
142
+ // Auto-fix structural issues
143
+ if (result.status === "fail" && this.config.autoFix) {
144
+ for (let i = 0; i < this.config.maxFixAttempts; i++) {
145
+ const fixableChecks = result.checks.filter((c) => c.status === "fail" && c.fixable);
146
+ if (fixableChecks.length === 0)
147
+ break;
148
+ for (const check of fixableChecks) {
149
+ const fix = await this.attemptFix(check, i + 1);
150
+ result.autoFixed.push(fix);
151
+ this.emit("fix:attempt", fix);
152
+ }
153
+ // Re-run structural after fix
154
+ const rerun = await this.runStructural();
155
+ result = { ...rerun, autoFixed: result.autoFixed };
156
+ stages[stages.length - 1] = result;
157
+ if (result.status !== "fail")
158
+ break;
159
+ }
160
+ }
161
+ }
162
+ // Stage 2: Semantic (skip if structural fails critically)
163
+ if (this.config.enableSemantic && !this.hasCriticalFailure(stages)) {
164
+ this.emit("stage:start", "semantic");
165
+ const result = await this.runSemantic(changedFiles);
166
+ stages.push(result);
167
+ this.emit("stage:complete", result);
168
+ }
169
+ // Stage 3: Quality
170
+ if (this.config.enableQuality) {
171
+ this.emit("stage:start", "quality");
172
+ const result = await this.runQuality(changedFiles);
173
+ stages.push(result);
174
+ this.emit("stage:complete", result);
175
+ // Collect gate results from quality checks
176
+ gateResults.push(...this.collectGateResults(result));
177
+ }
178
+ // Stage 4: Review (thorough only, requires reviewFn)
179
+ if (this.config.enableReview && reviewFn && changedFiles?.length) {
180
+ this.emit("stage:start", "review");
181
+ const result = await this.runReview(changedFiles, reviewFn);
182
+ stages.push(result);
183
+ this.emit("stage:complete", result);
184
+ }
185
+ // Stage 5: Decision (always)
186
+ const decision = this.makeDecision(stages);
187
+ // Aggregate counts
188
+ let totalChecks = 0;
189
+ let passed = 0;
190
+ let warnings = 0;
191
+ let failures = 0;
192
+ let autoFixed = 0;
193
+ for (const s of stages) {
194
+ totalChecks += s.checks.length;
195
+ for (const c of s.checks) {
196
+ if (c.status === "pass")
197
+ passed++;
198
+ else if (c.status === "warn")
199
+ warnings++;
200
+ else
201
+ failures++;
202
+ }
203
+ autoFixed += s.autoFixed.filter((f) => f.success).length;
204
+ }
205
+ const overall = this.determineOverall(stages);
206
+ const totalDuration = Date.now() - startTime;
207
+ const pipelineResult = {
208
+ overall,
209
+ stages,
210
+ totalChecks,
211
+ passed,
212
+ warnings,
213
+ failures,
214
+ autoFixed,
215
+ totalDuration,
216
+ gateResults,
217
+ decision,
218
+ };
219
+ this.emit("pipeline:complete", pipelineResult);
220
+ return pipelineResult;
221
+ }
222
+ /**
223
+ * 특정 stage만 단독 실행.
224
+ *
225
+ * @param stage 실행할 stage
226
+ * @param changedFiles 변경된 파일 목록
227
+ * @param reviewFn LLM 리뷰 함수 (review stage 시 필요)
228
+ */
229
+ async runStage(stage, changedFiles, reviewFn) {
230
+ switch (stage) {
231
+ case "structural":
232
+ return this.runStructural();
233
+ case "semantic":
234
+ return this.runSemantic(changedFiles);
235
+ case "quality":
236
+ return this.runQuality(changedFiles);
237
+ case "review":
238
+ if (!reviewFn || !changedFiles?.length) {
239
+ return this.buildStageResult("review", [], [], Date.now(), "skip");
240
+ }
241
+ return this.runReview(changedFiles, reviewFn);
242
+ case "decision":
243
+ return this.buildStageResult("decision", [], [], Date.now(), "skip");
244
+ }
245
+ }
246
+ // ─── Stage 1: Structural Validation ─────────────────────────────────
247
+ /**
248
+ * Stage 1 — 구조 검증: TypeScript 컴파일, ESLint, 순환 import, export 검사.
249
+ */
250
+ async runStructural() {
251
+ const startTime = Date.now();
252
+ const checks = [];
253
+ this.emit("check:run", "TypeScript Compilation");
254
+ const tsCheck = await this.checkTypeScript();
255
+ checks.push(tsCheck);
256
+ this.emit("check:result", tsCheck);
257
+ this.emit("check:run", "ESLint");
258
+ const lintCheck = await this.checkLint();
259
+ checks.push(lintCheck);
260
+ this.emit("check:result", lintCheck);
261
+ this.emit("check:run", "Circular Imports");
262
+ const circularCheck = await this.checkCircularImports();
263
+ checks.push(circularCheck);
264
+ this.emit("check:result", circularCheck);
265
+ this.emit("check:run", "Exports");
266
+ const exportCheck = await this.checkExports();
267
+ checks.push(exportCheck);
268
+ this.emit("check:result", exportCheck);
269
+ return this.buildStageResult("structural", checks, [], startTime);
270
+ }
271
+ /**
272
+ * TypeScript 타입 검사 (tsc --noEmit).
273
+ */
274
+ async checkTypeScript() {
275
+ const hasTsc = await this.fileExists(path.join(this.config.projectPath, "node_modules/.bin/tsc"));
276
+ if (!hasTsc) {
277
+ return {
278
+ name: "TypeScript Compilation",
279
+ status: "pass",
280
+ message: "tsc not found, skipping type check",
281
+ fixable: false,
282
+ severity: "info",
283
+ };
284
+ }
285
+ const result = await this.exec("npx", ["tsc", "--noEmit", "--pretty", "false"], this.config.buildTimeout);
286
+ if (result.exitCode === 0) {
287
+ return {
288
+ name: "TypeScript Compilation",
289
+ status: "pass",
290
+ message: "No type errors",
291
+ fixable: false,
292
+ severity: "info",
293
+ };
294
+ }
295
+ const errorLines = result.stderr
296
+ .split("\n")
297
+ .filter((l) => /error TS\d+/.test(l));
298
+ const errorCount = errorLines.length || 1;
299
+ return {
300
+ name: "TypeScript Compilation",
301
+ status: "fail",
302
+ message: `${errorCount} TypeScript error(s)`,
303
+ details: errorLines.slice(0, 20),
304
+ fixable: this.config.fixableCategories.includes("types"),
305
+ severity: "critical",
306
+ };
307
+ }
308
+ /**
309
+ * ESLint 검사.
310
+ */
311
+ async checkLint() {
312
+ const hasEslint = await this.fileExists(path.join(this.config.projectPath, "node_modules/.bin/eslint"));
313
+ if (!hasEslint) {
314
+ return {
315
+ name: "ESLint",
316
+ status: "pass",
317
+ message: "ESLint not installed, skipping",
318
+ fixable: false,
319
+ severity: "info",
320
+ };
321
+ }
322
+ const result = await this.exec("npx", ["eslint", ".", "--quiet", "--format", "compact"], this.config.buildTimeout);
323
+ if (result.exitCode === 0) {
324
+ return {
325
+ name: "ESLint",
326
+ status: "pass",
327
+ message: "No lint errors",
328
+ fixable: false,
329
+ severity: "info",
330
+ };
331
+ }
332
+ const output = result.stdout + "\n" + result.stderr;
333
+ const errorMatch = output.match(/(\d+) error/);
334
+ const warnMatch = output.match(/(\d+) warning/);
335
+ const errorCount = errorMatch ? parseInt(errorMatch[1], 10) : 1;
336
+ const warnCount = warnMatch ? parseInt(warnMatch[1], 10) : 0;
337
+ const errorLines = output.split("\n").filter((l) => l.includes("Error"));
338
+ if (errorCount === 0 && warnCount > 0) {
339
+ return {
340
+ name: "ESLint",
341
+ status: "warn",
342
+ message: `${warnCount} warning(s)`,
343
+ details: errorLines.slice(0, 10),
344
+ fixable: this.config.fixableCategories.includes("lint"),
345
+ severity: "low",
346
+ };
347
+ }
348
+ return {
349
+ name: "ESLint",
350
+ status: "fail",
351
+ message: `${errorCount} error(s), ${warnCount} warning(s)`,
352
+ details: errorLines.slice(0, 20),
353
+ fixable: this.config.fixableCategories.includes("lint"),
354
+ severity: "high",
355
+ };
356
+ }
357
+ /**
358
+ * 순환 import 검사.
359
+ * 간이 검사: import 그래프를 구축하여 사이클 탐지.
360
+ */
361
+ async checkCircularImports() {
362
+ const files = await this.collectSourceFiles();
363
+ const importGraph = new Map();
364
+ for (const file of files) {
365
+ try {
366
+ const content = await readFile(file, "utf-8");
367
+ const imports = new Set();
368
+ // Match import/export from "..." patterns
369
+ const importRegex = /(?:import|export)\s+.*?from\s+["']([^"']+)["']/g;
370
+ let match;
371
+ while ((match = importRegex.exec(content)) !== null) {
372
+ const importPath = match[1];
373
+ if (importPath.startsWith(".")) {
374
+ const resolved = this.resolveImportPath(file, importPath);
375
+ if (resolved)
376
+ imports.add(resolved);
377
+ }
378
+ }
379
+ importGraph.set(file, imports);
380
+ }
381
+ catch {
382
+ // Skip unreadable files
383
+ }
384
+ }
385
+ // DFS cycle detection
386
+ const cycles = [];
387
+ const visited = new Set();
388
+ const inStack = new Set();
389
+ const dfs = (node, pathStack) => {
390
+ if (inStack.has(node)) {
391
+ const cycleStart = pathStack.indexOf(node);
392
+ if (cycleStart !== -1) {
393
+ cycles.push(pathStack.slice(cycleStart));
394
+ }
395
+ return;
396
+ }
397
+ if (visited.has(node))
398
+ return;
399
+ visited.add(node);
400
+ inStack.add(node);
401
+ pathStack.push(node);
402
+ const deps = importGraph.get(node);
403
+ if (deps) {
404
+ for (const dep of deps) {
405
+ dfs(dep, [...pathStack]);
406
+ }
407
+ }
408
+ inStack.delete(node);
409
+ };
410
+ for (const node of importGraph.keys()) {
411
+ dfs(node, []);
412
+ }
413
+ if (cycles.length === 0) {
414
+ return {
415
+ name: "Circular Imports",
416
+ status: "pass",
417
+ message: "No circular imports detected",
418
+ fixable: false,
419
+ severity: "info",
420
+ };
421
+ }
422
+ const projectDir = this.config.projectPath;
423
+ const cycleDetails = cycles.slice(0, 5).map((cycle) => cycle.map((f) => path.relative(projectDir, f)).join(" → ") + " → (cycle)");
424
+ return {
425
+ name: "Circular Imports",
426
+ status: "warn",
427
+ message: `${cycles.length} circular import(s) detected`,
428
+ details: cycleDetails,
429
+ fixable: false,
430
+ severity: "medium",
431
+ };
432
+ }
433
+ /**
434
+ * Export 검사 — src/ 내 .ts 파일이 아무것도 export하지 않으면 경고.
435
+ */
436
+ async checkExports() {
437
+ const files = await this.collectSourceFiles();
438
+ const noExportFiles = [];
439
+ for (const file of files) {
440
+ // Skip test files, type declaration files, index files
441
+ const basename = path.basename(file);
442
+ if (basename.includes(".test.") ||
443
+ basename.includes(".spec.") ||
444
+ basename.endsWith(".d.ts") ||
445
+ basename === "index.ts") {
446
+ continue;
447
+ }
448
+ try {
449
+ const content = await readFile(file, "utf-8");
450
+ // Check for any export statement
451
+ if (!/\bexport\b/.test(content)) {
452
+ noExportFiles.push(path.relative(this.config.projectPath, file));
453
+ }
454
+ }
455
+ catch {
456
+ // Skip unreadable files
457
+ }
458
+ }
459
+ if (noExportFiles.length === 0) {
460
+ return {
461
+ name: "Exports",
462
+ status: "pass",
463
+ message: "All source files have exports",
464
+ fixable: false,
465
+ severity: "info",
466
+ };
467
+ }
468
+ return {
469
+ name: "Exports",
470
+ status: "warn",
471
+ message: `${noExportFiles.length} file(s) with no exports`,
472
+ details: noExportFiles.slice(0, 10),
473
+ fixable: false,
474
+ severity: "low",
475
+ };
476
+ }
477
+ // ─── Stage 2: Semantic Validation ───────────────────────────────────
478
+ /**
479
+ * Stage 2 — 시맨틱 검증: 테스트 실행.
480
+ *
481
+ * @param changedFiles 변경된 파일 목록 (있으면 관련 테스트만 실행)
482
+ */
483
+ async runSemantic(changedFiles) {
484
+ const startTime = Date.now();
485
+ const checks = [];
486
+ this.emit("check:run", "Tests");
487
+ if (changedFiles && changedFiles.length > 0) {
488
+ const testResult = await this.runAffectedTests(changedFiles);
489
+ checks.push(testResult);
490
+ this.emit("check:result", testResult);
491
+ }
492
+ else {
493
+ const testResult = await this.runAllTests();
494
+ checks.push(testResult);
495
+ this.emit("check:result", testResult);
496
+ }
497
+ return this.buildStageResult("semantic", checks, [], startTime);
498
+ }
499
+ /**
500
+ * 변경된 파일에 대응하는 테스트만 실행.
501
+ */
502
+ async runAffectedTests(changedFiles) {
503
+ // Find test files that match changed source files
504
+ const testPatterns = changedFiles.map((f) => {
505
+ const base = path.basename(f, path.extname(f));
506
+ return base;
507
+ });
508
+ // Try running tests with pattern filter
509
+ const hasTestRunner = await this.detectTestRunner();
510
+ if (!hasTestRunner) {
511
+ return {
512
+ name: "Affected Tests",
513
+ status: "pass",
514
+ message: "No test runner detected, skipping",
515
+ fixable: false,
516
+ severity: "info",
517
+ };
518
+ }
519
+ // Use generic test command
520
+ const result = await this.exec("npx", ["--no", "vitest", "run", "--reporter=verbose", ...testPatterns.map((p) => `--testPathPattern=${p}`)], this.config.testTimeout);
521
+ // Fallback: try node --test
522
+ if (result.exitCode !== 0 && result.stderr.includes("not found")) {
523
+ const nodeResult = await this.exec("node", ["--test", ...changedFiles.filter((f) => f.includes(".test."))], this.config.testTimeout);
524
+ return this.parseTestResult("Affected Tests", nodeResult);
525
+ }
526
+ return this.parseTestResult("Affected Tests", result);
527
+ }
528
+ /**
529
+ * 전체 테스트 스위트 실행.
530
+ */
531
+ async runAllTests() {
532
+ const hasTestRunner = await this.detectTestRunner();
533
+ if (!hasTestRunner) {
534
+ return {
535
+ name: "Full Test Suite",
536
+ status: "pass",
537
+ message: "No test runner detected, skipping",
538
+ fixable: false,
539
+ severity: "info",
540
+ };
541
+ }
542
+ // Try npm test
543
+ const result = await this.exec("npm", ["test", "--if-present"], this.config.testTimeout);
544
+ return this.parseTestResult("Full Test Suite", result);
545
+ }
546
+ /**
547
+ * 테스트 러너 존재 여부 확인.
548
+ */
549
+ async detectTestRunner() {
550
+ try {
551
+ const pkgPath = path.join(this.config.projectPath, "package.json");
552
+ const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
553
+ return !!(pkg.scripts?.test &&
554
+ pkg.scripts.test !== 'echo "Error: no test specified" && exit 1');
555
+ }
556
+ catch {
557
+ return false;
558
+ }
559
+ }
560
+ /**
561
+ * 테스트 실행 결과 파싱.
562
+ */
563
+ parseTestResult(name, result) {
564
+ const output = result.stdout + "\n" + result.stderr;
565
+ if (result.exitCode === 0) {
566
+ const passMatch = output.match(/(\d+)\s*(?:passing|passed|tests?\s*passed)/i);
567
+ const count = passMatch ? passMatch[1] : "all";
568
+ return {
569
+ name,
570
+ status: "pass",
571
+ message: `${count} test(s) passed`,
572
+ fixable: false,
573
+ severity: "info",
574
+ };
575
+ }
576
+ const failMatch = output.match(/(\d+)\s*(?:failing|failed)/i);
577
+ const failCount = failMatch ? failMatch[1] : "some";
578
+ return {
579
+ name,
580
+ status: "fail",
581
+ message: `${failCount} test(s) failed`,
582
+ details: output.split("\n").filter((l) => /fail|error|✗|✘|×/i.test(l)).slice(0, 15),
583
+ fixable: false,
584
+ severity: "high",
585
+ };
586
+ }
587
+ // ─── Stage 3: Quality Gates ─────────────────────────────────────────
588
+ /**
589
+ * Stage 3 — 품질 게이트: 복잡도, 길이, TODO, 디버그 문, 보안 스캔.
590
+ *
591
+ * @param changedFiles 변경된 파일 목록 (없으면 전체 소스)
592
+ */
593
+ async runQuality(changedFiles) {
594
+ const startTime = Date.now();
595
+ const checks = [];
596
+ const files = changedFiles?.length
597
+ ? changedFiles.filter((f) => f.endsWith(".ts") || f.endsWith(".tsx") || f.endsWith(".js") || f.endsWith(".jsx"))
598
+ : await this.collectSourceFiles();
599
+ this.emit("check:run", "Cyclomatic Complexity");
600
+ const complexityCheck = await this.checkComplexity(files);
601
+ checks.push(complexityCheck);
602
+ this.emit("check:result", complexityCheck);
603
+ this.emit("check:run", "Function/File Lengths");
604
+ const lengthCheck = await this.checkLengths(files);
605
+ checks.push(lengthCheck);
606
+ this.emit("check:result", lengthCheck);
607
+ this.emit("check:run", "TODO Count");
608
+ const todoCheck = await this.checkTodos(files);
609
+ checks.push(todoCheck);
610
+ this.emit("check:result", todoCheck);
611
+ this.emit("check:run", "Debug Statements");
612
+ const debugCheck = await this.checkDebugStatements(files);
613
+ checks.push(debugCheck);
614
+ this.emit("check:result", debugCheck);
615
+ this.emit("check:run", "Security Scan");
616
+ const securityCheck = await this.checkSecurity(files);
617
+ checks.push(securityCheck);
618
+ this.emit("check:result", securityCheck);
619
+ return this.buildStageResult("quality", checks, [], startTime);
620
+ }
621
+ /**
622
+ * 순환 복잡도 검사.
623
+ */
624
+ async checkComplexity(files) {
625
+ const violations = [];
626
+ let maxFound = 0;
627
+ for (const file of files) {
628
+ try {
629
+ const content = await readFile(file, "utf-8");
630
+ const metrics = this.analyzeFileMetrics(content);
631
+ for (const fn of metrics.functions) {
632
+ if (fn.complexity > maxFound)
633
+ maxFound = fn.complexity;
634
+ if (fn.complexity > this.config.qualityGates.maxCyclomaticComplexity) {
635
+ violations.push(`${path.relative(this.config.projectPath, file)}: ${fn.name}() complexity=${fn.complexity} (max ${this.config.qualityGates.maxCyclomaticComplexity})`);
636
+ }
637
+ }
638
+ }
639
+ catch {
640
+ // Skip unreadable files
641
+ }
642
+ }
643
+ if (violations.length === 0) {
644
+ return {
645
+ name: "Cyclomatic Complexity",
646
+ status: "pass",
647
+ message: `Max complexity: ${maxFound} (threshold: ${this.config.qualityGates.maxCyclomaticComplexity})`,
648
+ fixable: false,
649
+ severity: "info",
650
+ };
651
+ }
652
+ return {
653
+ name: "Cyclomatic Complexity",
654
+ status: "warn",
655
+ message: `${violations.length} function(s) exceed complexity threshold`,
656
+ details: violations.slice(0, 10),
657
+ fixable: false,
658
+ severity: "medium",
659
+ };
660
+ }
661
+ /**
662
+ * 함수/파일 길이 검사.
663
+ */
664
+ async checkLengths(files) {
665
+ const violations = [];
666
+ for (const file of files) {
667
+ try {
668
+ const content = await readFile(file, "utf-8");
669
+ const lines = content.split("\n");
670
+ const relPath = path.relative(this.config.projectPath, file);
671
+ // File length check
672
+ if (lines.length > this.config.qualityGates.maxFileLength) {
673
+ violations.push(`${relPath}: ${lines.length} lines (max ${this.config.qualityGates.maxFileLength})`);
674
+ }
675
+ // Function length check
676
+ const metrics = this.analyzeFileMetrics(content);
677
+ for (const fn of metrics.functions) {
678
+ if (fn.length > this.config.qualityGates.maxFunctionLength) {
679
+ violations.push(`${relPath}: ${fn.name}() is ${fn.length} lines (max ${this.config.qualityGates.maxFunctionLength})`);
680
+ }
681
+ }
682
+ }
683
+ catch {
684
+ // Skip unreadable files
685
+ }
686
+ }
687
+ if (violations.length === 0) {
688
+ return {
689
+ name: "Function/File Lengths",
690
+ status: "pass",
691
+ message: "All functions and files within length limits",
692
+ fixable: false,
693
+ severity: "info",
694
+ };
695
+ }
696
+ return {
697
+ name: "Function/File Lengths",
698
+ status: "warn",
699
+ message: `${violations.length} length violation(s)`,
700
+ details: violations.slice(0, 10),
701
+ fixable: false,
702
+ severity: "low",
703
+ };
704
+ }
705
+ /**
706
+ * TODO/FIXME/HACK 검사.
707
+ */
708
+ async checkTodos(files) {
709
+ if (this.config.qualityGates.maxTodoCount < 0) {
710
+ return {
711
+ name: "TODO Count",
712
+ status: "pass",
713
+ message: "TODO check disabled",
714
+ fixable: false,
715
+ severity: "info",
716
+ };
717
+ }
718
+ let totalTodos = 0;
719
+ const todoLocations = [];
720
+ for (const file of files) {
721
+ try {
722
+ const content = await readFile(file, "utf-8");
723
+ const lines = content.split("\n");
724
+ for (let i = 0; i < lines.length; i++) {
725
+ if (/\b(TODO|FIXME|HACK|XXX)\b/i.test(lines[i])) {
726
+ totalTodos++;
727
+ if (todoLocations.length < 10) {
728
+ todoLocations.push(`${path.relative(this.config.projectPath, file)}:${i + 1}: ${lines[i].trim().substring(0, 80)}`);
729
+ }
730
+ }
731
+ }
732
+ }
733
+ catch {
734
+ // Skip
735
+ }
736
+ }
737
+ if (totalTodos <= this.config.qualityGates.maxTodoCount) {
738
+ return {
739
+ name: "TODO Count",
740
+ status: "pass",
741
+ message: `${totalTodos} TODO(s) found (max ${this.config.qualityGates.maxTodoCount})`,
742
+ fixable: false,
743
+ severity: "info",
744
+ };
745
+ }
746
+ return {
747
+ name: "TODO Count",
748
+ status: "warn",
749
+ message: `${totalTodos} TODO(s) found (max ${this.config.qualityGates.maxTodoCount})`,
750
+ details: todoLocations,
751
+ fixable: false,
752
+ severity: "low",
753
+ };
754
+ }
755
+ /**
756
+ * 디버그 문 검사 (console.log 등, 테스트 파일 제외).
757
+ */
758
+ async checkDebugStatements(files) {
759
+ const violations = [];
760
+ for (const file of files) {
761
+ // Skip test files
762
+ if (file.includes(".test.") || file.includes(".spec.") || file.includes("__tests__")) {
763
+ continue;
764
+ }
765
+ try {
766
+ const content = await readFile(file, "utf-8");
767
+ const lines = content.split("\n");
768
+ for (let i = 0; i < lines.length; i++) {
769
+ const line = lines[i];
770
+ // Match console.log/warn/error/debug/info but not commented out
771
+ if (/^\s*console\.(log|debug|info)\s*\(/.test(line) && !line.trimStart().startsWith("//")) {
772
+ violations.push(`${path.relative(this.config.projectPath, file)}:${i + 1}: ${line.trim().substring(0, 80)}`);
773
+ }
774
+ }
775
+ }
776
+ catch {
777
+ // Skip
778
+ }
779
+ }
780
+ if (violations.length === 0) {
781
+ return {
782
+ name: "Debug Statements",
783
+ status: "pass",
784
+ message: "No debug statements found",
785
+ fixable: true,
786
+ severity: "info",
787
+ };
788
+ }
789
+ return {
790
+ name: "Debug Statements",
791
+ status: "warn",
792
+ message: `${violations.length} debug statement(s) found`,
793
+ details: violations.slice(0, 10),
794
+ fixable: true,
795
+ severity: "low",
796
+ };
797
+ }
798
+ /**
799
+ * 보안 퀵 스캔 — 하드코딩된 시크릿, eval, SQL 인젝션 등.
800
+ */
801
+ async checkSecurity(files) {
802
+ const findings = [];
803
+ for (const file of files) {
804
+ // Skip test files and node_modules
805
+ if (file.includes("node_modules") || file.includes(".test.") || file.includes(".spec.")) {
806
+ continue;
807
+ }
808
+ try {
809
+ const content = await readFile(file, "utf-8");
810
+ const lines = content.split("\n");
811
+ for (const { name, pattern, severity } of SECURITY_PATTERNS) {
812
+ // Reset lastIndex for global patterns
813
+ pattern.lastIndex = 0;
814
+ for (let i = 0; i < lines.length; i++) {
815
+ pattern.lastIndex = 0;
816
+ if (pattern.test(lines[i]) && !lines[i].trimStart().startsWith("//")) {
817
+ findings.push({
818
+ pattern: name,
819
+ file: path.relative(this.config.projectPath, file),
820
+ line: i + 1,
821
+ severity,
822
+ });
823
+ }
824
+ }
825
+ }
826
+ }
827
+ catch {
828
+ // Skip
829
+ }
830
+ }
831
+ if (findings.length === 0) {
832
+ return {
833
+ name: "Security Scan",
834
+ status: "pass",
835
+ message: "No security issues found",
836
+ fixable: false,
837
+ severity: "info",
838
+ };
839
+ }
840
+ const hasCritical = findings.some((f) => f.severity === "critical");
841
+ const details = findings
842
+ .slice(0, 15)
843
+ .map((f) => `[${f.severity.toUpperCase()}] ${f.file}:${f.line} — ${f.pattern}`);
844
+ return {
845
+ name: "Security Scan",
846
+ status: hasCritical ? "fail" : "warn",
847
+ message: `${findings.length} security finding(s)`,
848
+ details,
849
+ fixable: false,
850
+ severity: hasCritical ? "critical" : "medium",
851
+ };
852
+ }
853
+ // ─── Stage 4: Review Agent ──────────────────────────────────────────
854
+ /**
855
+ * Stage 4 — LLM 기반 코드 리뷰 (thorough 모드 전용).
856
+ *
857
+ * @param changedFiles 변경된 파일 목록
858
+ * @param reviewFn 리뷰를 수행할 LLM 함수
859
+ */
860
+ async runReview(changedFiles, reviewFn) {
861
+ const startTime = Date.now();
862
+ const checks = [];
863
+ try {
864
+ const fileContents = await this.readFiles(changedFiles);
865
+ if (fileContents.size === 0) {
866
+ return this.buildStageResult("review", [], [], startTime, "skip");
867
+ }
868
+ const prompt = this.buildReviewPrompt(fileContents);
869
+ this.emit("check:run", "LLM Code Review");
870
+ const response = await reviewFn(prompt);
871
+ const reviewChecks = this.parseReviewResponse(response);
872
+ checks.push(...reviewChecks);
873
+ for (const check of reviewChecks) {
874
+ this.emit("check:result", check);
875
+ }
876
+ }
877
+ catch (err) {
878
+ checks.push({
879
+ name: "LLM Code Review",
880
+ status: "warn",
881
+ message: `Review failed: ${err instanceof Error ? err.message : String(err)}`,
882
+ fixable: false,
883
+ severity: "low",
884
+ });
885
+ }
886
+ return this.buildStageResult("review", checks, [], startTime);
887
+ }
888
+ /**
889
+ * 리뷰 프롬프트 생성.
890
+ */
891
+ buildReviewPrompt(files) {
892
+ let prompt = `You are a code reviewer. Review the following changed files and provide findings.\n\n`;
893
+ prompt += `For each issue found, respond with a line in this exact format:\n`;
894
+ prompt += `[SEVERITY:STATUS] file:line message\n\n`;
895
+ prompt += `Where:\n`;
896
+ prompt += `- SEVERITY is one of: critical, high, medium, low, info\n`;
897
+ prompt += `- STATUS is one of: fail, warn, pass\n`;
898
+ prompt += `- file is the relative file path\n`;
899
+ prompt += `- line is the line number (0 if not applicable)\n\n`;
900
+ prompt += `Focus on:\n`;
901
+ prompt += `- Logic errors and edge cases\n`;
902
+ prompt += `- Security vulnerabilities\n`;
903
+ prompt += `- Performance issues\n`;
904
+ prompt += `- API contract violations\n`;
905
+ prompt += `- Missing error handling\n\n`;
906
+ prompt += `If the code looks good, respond with:\n`;
907
+ prompt += `[info:pass] overall:0 Code review passed, no issues found.\n\n`;
908
+ prompt += `--- Files ---\n\n`;
909
+ for (const [filePath, content] of files) {
910
+ prompt += `### ${filePath}\n\`\`\`typescript\n${content.substring(0, 8000)}\n\`\`\`\n\n`;
911
+ }
912
+ return prompt;
913
+ }
914
+ /**
915
+ * LLM 리뷰 응답 파싱.
916
+ */
917
+ parseReviewResponse(response) {
918
+ const checks = [];
919
+ const linePattern = /\[(critical|high|medium|low|info):(fail|warn|pass)\]\s+([^:]+):(\d+)\s+(.+)/gi;
920
+ let match;
921
+ while ((match = linePattern.exec(response)) !== null) {
922
+ const severity = match[1].toLowerCase();
923
+ const status = match[2].toLowerCase();
924
+ const file = match[3].trim();
925
+ const line = parseInt(match[4], 10);
926
+ const message = match[5].trim();
927
+ checks.push({
928
+ name: "LLM Code Review",
929
+ status,
930
+ message,
931
+ file: file === "overall" ? undefined : file,
932
+ line: line > 0 ? line : undefined,
933
+ fixable: false,
934
+ severity,
935
+ });
936
+ }
937
+ // If no structured output found, treat whole response as a single pass
938
+ if (checks.length === 0) {
939
+ checks.push({
940
+ name: "LLM Code Review",
941
+ status: "pass",
942
+ message: response.substring(0, 200).trim() || "Review completed",
943
+ fixable: false,
944
+ severity: "info",
945
+ });
946
+ }
947
+ return checks;
948
+ }
949
+ // ─── Stage 5: Decision ──────────────────────────────────────────────
950
+ /**
951
+ * Stage 5 — 전체 결과 기반 자동 판정.
952
+ *
953
+ * - Critical failure → "escalate"
954
+ * - Only fixable failures → "fix_and_retry"
955
+ * - All pass or only warnings → "approve"
956
+ */
957
+ makeDecision(stages) {
958
+ const allChecks = stages.flatMap((s) => s.checks);
959
+ const failures = allChecks.filter((c) => c.status === "fail");
960
+ const criticalIssues = failures.filter((c) => c.severity === "critical");
961
+ const suggestions = [];
962
+ // Collect warnings as suggestions
963
+ const warnings = allChecks.filter((c) => c.status === "warn");
964
+ for (const w of warnings.slice(0, 5)) {
965
+ suggestions.push(`[${w.severity}] ${w.name}: ${w.message}`);
966
+ }
967
+ // Any critical failure → escalate
968
+ if (criticalIssues.length > 0) {
969
+ return {
970
+ action: "escalate",
971
+ reason: `${criticalIssues.length} critical issue(s) require human review`,
972
+ criticalIssues,
973
+ suggestions,
974
+ };
975
+ }
976
+ // Only fixable failures → fix_and_retry
977
+ if (failures.length > 0) {
978
+ const allFixable = failures.every((f) => f.fixable);
979
+ if (allFixable) {
980
+ return {
981
+ action: "fix_and_retry",
982
+ reason: `${failures.length} fixable issue(s) detected`,
983
+ criticalIssues: [],
984
+ suggestions: [
985
+ ...failures.map((f) => `Fix: ${f.name} — ${f.message}`),
986
+ ...suggestions,
987
+ ],
988
+ };
989
+ }
990
+ // Non-fixable failures → escalate
991
+ return {
992
+ action: "escalate",
993
+ reason: `${failures.length} failure(s), some not auto-fixable`,
994
+ criticalIssues: failures.filter((f) => !f.fixable),
995
+ suggestions,
996
+ };
997
+ }
998
+ // All pass or only warnings → approve
999
+ return {
1000
+ action: "approve",
1001
+ reason: warnings.length > 0
1002
+ ? `All checks passed with ${warnings.length} warning(s)`
1003
+ : "All checks passed",
1004
+ criticalIssues: [],
1005
+ suggestions,
1006
+ };
1007
+ }
1008
+ // ─── Auto-Fix ───────────────────────────────────────────────────────
1009
+ /**
1010
+ * 실패한 검사를 자동 수정 시도.
1011
+ *
1012
+ * @param check 실패한 검사 결과
1013
+ * @param attempt 시도 번호 (1-based)
1014
+ */
1015
+ async attemptFix(check, attempt) {
1016
+ const base = {
1017
+ check: check.name,
1018
+ attempt,
1019
+ success: false,
1020
+ description: "",
1021
+ };
1022
+ try {
1023
+ switch (check.name) {
1024
+ case "ESLint": {
1025
+ const fixed = await this.fixLint();
1026
+ return { ...base, success: fixed, description: fixed ? "Ran eslint --fix" : "eslint --fix failed" };
1027
+ }
1028
+ case "TypeScript Compilation": {
1029
+ // TypeScript errors can't be auto-fixed easily, but we can try fixing imports
1030
+ const fixed = await this.fixImports();
1031
+ return { ...base, success: fixed, description: fixed ? "Fixed import issues" : "Could not auto-fix TS errors" };
1032
+ }
1033
+ default:
1034
+ return { ...base, success: false, description: `No auto-fix available for ${check.name}` };
1035
+ }
1036
+ }
1037
+ catch (err) {
1038
+ return {
1039
+ ...base,
1040
+ success: false,
1041
+ description: `Fix error: ${err instanceof Error ? err.message : String(err)}`,
1042
+ };
1043
+ }
1044
+ }
1045
+ /**
1046
+ * ESLint --fix 실행.
1047
+ */
1048
+ async fixLint() {
1049
+ const hasEslint = await this.fileExists(path.join(this.config.projectPath, "node_modules/.bin/eslint"));
1050
+ if (!hasEslint)
1051
+ return false;
1052
+ const result = await this.exec("npx", ["eslint", ".", "--fix", "--quiet"], this.config.buildTimeout);
1053
+ return result.exitCode === 0;
1054
+ }
1055
+ /**
1056
+ * Import 에러 수정 시도 (간이: 미사용 import 제거).
1057
+ */
1058
+ async fixImports() {
1059
+ // TypeScript 에러를 직접 수정하기는 어려우므로,
1060
+ // tsc --noEmit 재실행하여 변화 확인만 수행
1061
+ const result = await this.exec("npx", ["tsc", "--noEmit"], this.config.buildTimeout);
1062
+ return result.exitCode === 0;
1063
+ }
1064
+ // ─── Helpers ────────────────────────────────────────────────────────
1065
+ /**
1066
+ * 셸 명령 실행 (타임아웃 포함).
1067
+ */
1068
+ exec(command, args, timeout) {
1069
+ return new Promise((resolve) => {
1070
+ execFile(command, args, {
1071
+ cwd: this.config.projectPath,
1072
+ timeout,
1073
+ maxBuffer: 2 * 1024 * 1024,
1074
+ env: { ...process.env, FORCE_COLOR: "0", NODE_ENV: "test" },
1075
+ }, (error, stdout, stderr) => {
1076
+ let exitCode = 0;
1077
+ if (error) {
1078
+ exitCode = typeof error.code === "number"
1079
+ ? error.code
1080
+ : error.status ?? 1;
1081
+ }
1082
+ resolve({
1083
+ stdout: (stdout ?? "").toString(),
1084
+ stderr: (stderr ?? "").toString(),
1085
+ exitCode,
1086
+ });
1087
+ });
1088
+ });
1089
+ }
1090
+ /**
1091
+ * 파일 목록을 읽어 Map<경로, 내용>으로 반환.
1092
+ */
1093
+ async readFiles(files) {
1094
+ const result = new Map();
1095
+ for (const file of files) {
1096
+ try {
1097
+ const absPath = path.isAbsolute(file)
1098
+ ? file
1099
+ : path.join(this.config.projectPath, file);
1100
+ const content = await readFile(absPath, "utf-8");
1101
+ result.set(path.relative(this.config.projectPath, absPath), content);
1102
+ }
1103
+ catch {
1104
+ // Skip unreadable files
1105
+ }
1106
+ }
1107
+ return result;
1108
+ }
1109
+ /**
1110
+ * src/ 디렉토리 내 모든 .ts/.tsx 소스 파일 수집.
1111
+ */
1112
+ async collectSourceFiles() {
1113
+ const srcDir = path.join(this.config.projectPath, "src");
1114
+ const hasSrc = await this.fileExists(srcDir);
1115
+ const baseDir = hasSrc ? srcDir : this.config.projectPath;
1116
+ const files = [];
1117
+ await this.walkDir(baseDir, files);
1118
+ return files;
1119
+ }
1120
+ /**
1121
+ * 디렉토리 재귀 탐색.
1122
+ */
1123
+ async walkDir(dir, out) {
1124
+ try {
1125
+ const entries = await readdir(dir, { withFileTypes: true });
1126
+ for (const entry of entries) {
1127
+ const fullPath = path.join(dir, entry.name);
1128
+ if (entry.isDirectory()) {
1129
+ // Skip known non-source directories
1130
+ if (entry.name === "node_modules" ||
1131
+ entry.name === "dist" ||
1132
+ entry.name === ".git" ||
1133
+ entry.name === "coverage" ||
1134
+ entry.name === ".next") {
1135
+ continue;
1136
+ }
1137
+ await this.walkDir(fullPath, out);
1138
+ }
1139
+ else if (entry.isFile() &&
1140
+ (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx") ||
1141
+ entry.name.endsWith(".js") || entry.name.endsWith(".jsx"))) {
1142
+ out.push(fullPath);
1143
+ }
1144
+ }
1145
+ }
1146
+ catch {
1147
+ // Skip inaccessible directories
1148
+ }
1149
+ }
1150
+ /**
1151
+ * 파일 메트릭 분석 — 순환 복잡도, 인지 복잡도, LOC, 함수 목록.
1152
+ */
1153
+ analyzeFileMetrics(content) {
1154
+ const lines = content.split("\n");
1155
+ const loc = lines.length;
1156
+ const functions = [];
1157
+ // Simple function boundary detection by brace counting
1158
+ let currentFunction = null;
1159
+ let braceDepth = 0;
1160
+ let inBlockComment = false;
1161
+ for (let i = 0; i < lines.length; i++) {
1162
+ const line = lines[i];
1163
+ const trimmed = line.trim();
1164
+ // Handle block comments
1165
+ if (inBlockComment) {
1166
+ if (trimmed.includes("*/"))
1167
+ inBlockComment = false;
1168
+ continue;
1169
+ }
1170
+ if (trimmed.startsWith("/*")) {
1171
+ if (!trimmed.includes("*/"))
1172
+ inBlockComment = true;
1173
+ continue;
1174
+ }
1175
+ // Skip single-line comments
1176
+ if (trimmed.startsWith("//"))
1177
+ continue;
1178
+ // Detect function start
1179
+ const funcMatch = trimmed.match(/(?:async\s+)?function\s+(\w+)/) ||
1180
+ trimmed.match(/(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\(/) ||
1181
+ trimmed.match(/(\w+)\s*\([^)]*\)\s*(?::\s*\S+\s*)?\{/) ||
1182
+ trimmed.match(/(\w+)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>/);
1183
+ if (funcMatch && !currentFunction && trimmed.includes("{")) {
1184
+ currentFunction = {
1185
+ name: funcMatch[1],
1186
+ startLine: i,
1187
+ braceDepth,
1188
+ complexity: 1, // Base complexity
1189
+ };
1190
+ }
1191
+ // Count braces
1192
+ for (const ch of line) {
1193
+ if (ch === "{")
1194
+ braceDepth++;
1195
+ else if (ch === "}")
1196
+ braceDepth--;
1197
+ }
1198
+ // Count complexity within current function
1199
+ if (currentFunction) {
1200
+ COMPLEXITY_KEYWORDS.lastIndex = 0;
1201
+ const kwMatches = trimmed.match(COMPLEXITY_KEYWORDS);
1202
+ if (kwMatches)
1203
+ currentFunction.complexity += kwMatches.length;
1204
+ LOGICAL_OPERATORS.lastIndex = 0;
1205
+ const logicalMatches = trimmed.match(LOGICAL_OPERATORS);
1206
+ if (logicalMatches)
1207
+ currentFunction.complexity += logicalMatches.length;
1208
+ TERNARY_OPERATOR.lastIndex = 0;
1209
+ const ternaryMatches = trimmed.match(TERNARY_OPERATOR);
1210
+ if (ternaryMatches)
1211
+ currentFunction.complexity += ternaryMatches.length;
1212
+ // Check function end
1213
+ if (braceDepth <= currentFunction.braceDepth) {
1214
+ functions.push({
1215
+ name: currentFunction.name,
1216
+ length: i - currentFunction.startLine + 1,
1217
+ complexity: currentFunction.complexity,
1218
+ });
1219
+ currentFunction = null;
1220
+ }
1221
+ }
1222
+ }
1223
+ // If function never closed (incomplete parse), record it
1224
+ if (currentFunction) {
1225
+ functions.push({
1226
+ name: currentFunction.name,
1227
+ length: lines.length - currentFunction.startLine,
1228
+ complexity: currentFunction.complexity,
1229
+ });
1230
+ }
1231
+ // Calculate file-level complexity
1232
+ let cyclomatic = 1;
1233
+ let cognitive = 0;
1234
+ COMPLEXITY_KEYWORDS.lastIndex = 0;
1235
+ const allKeywords = content.match(COMPLEXITY_KEYWORDS);
1236
+ if (allKeywords)
1237
+ cyclomatic += allKeywords.length;
1238
+ LOGICAL_OPERATORS.lastIndex = 0;
1239
+ const allLogical = content.match(LOGICAL_OPERATORS);
1240
+ if (allLogical)
1241
+ cyclomatic += allLogical.length;
1242
+ // Cognitive: nesting awareness (simplified)
1243
+ let nestLevel = 0;
1244
+ for (const line of lines) {
1245
+ const trimmed = line.trim();
1246
+ if (/\b(if|for|while|switch)\b/.test(trimmed)) {
1247
+ cognitive += 1 + nestLevel;
1248
+ }
1249
+ for (const ch of line) {
1250
+ if (ch === "{")
1251
+ nestLevel++;
1252
+ else if (ch === "}")
1253
+ nestLevel = Math.max(0, nestLevel - 1);
1254
+ }
1255
+ }
1256
+ return { cyclomatic, cognitive, loc, functions };
1257
+ }
1258
+ /**
1259
+ * stage 결과 생성 헬퍼.
1260
+ */
1261
+ buildStageResult(stage, checks, autoFixed, startTime, forceStatus) {
1262
+ const duration = Date.now() - startTime;
1263
+ let status;
1264
+ if (forceStatus) {
1265
+ status = forceStatus;
1266
+ }
1267
+ else if (checks.length === 0) {
1268
+ status = "pass";
1269
+ }
1270
+ else if (checks.some((c) => c.status === "fail")) {
1271
+ status = "fail";
1272
+ }
1273
+ else if (checks.some((c) => c.status === "warn")) {
1274
+ status = "warn";
1275
+ }
1276
+ else {
1277
+ status = "pass";
1278
+ }
1279
+ const passCount = checks.filter((c) => c.status === "pass").length;
1280
+ const warnCount = checks.filter((c) => c.status === "warn").length;
1281
+ const failCount = checks.filter((c) => c.status === "fail").length;
1282
+ const summary = `${stage}: ${passCount} passed, ${warnCount} warnings, ${failCount} failed (${duration}ms)`;
1283
+ return { stage, status, duration, checks, autoFixed, summary };
1284
+ }
1285
+ /**
1286
+ * 전체 판정 결정.
1287
+ */
1288
+ determineOverall(stages) {
1289
+ if (stages.some((s) => s.status === "fail"))
1290
+ return "fail";
1291
+ if (stages.some((s) => s.status === "warn"))
1292
+ return "warn";
1293
+ return "pass";
1294
+ }
1295
+ /**
1296
+ * Critical failure 존재 여부 확인.
1297
+ */
1298
+ hasCriticalFailure(stages) {
1299
+ return stages.some((s) => s.status === "fail" &&
1300
+ s.checks.some((c) => c.status === "fail" && c.severity === "critical"));
1301
+ }
1302
+ /**
1303
+ * Quality stage 결과에서 GateResult 추출.
1304
+ */
1305
+ collectGateResults(stageResult) {
1306
+ const gates = [];
1307
+ for (const check of stageResult.checks) {
1308
+ if (check.name === "Cyclomatic Complexity") {
1309
+ // Parse max complexity from message
1310
+ const match = check.message.match(/Max complexity:\s*(\d+)/);
1311
+ if (match) {
1312
+ gates.push({
1313
+ gate: "maxCyclomaticComplexity",
1314
+ threshold: this.config.qualityGates.maxCyclomaticComplexity,
1315
+ actual: parseInt(match[1], 10),
1316
+ passed: check.status !== "fail",
1317
+ });
1318
+ }
1319
+ }
1320
+ }
1321
+ return gates;
1322
+ }
1323
+ /**
1324
+ * Import 경로 해석 (상대 경로 → 절대 경로).
1325
+ */
1326
+ resolveImportPath(fromFile, importPath) {
1327
+ const dir = path.dirname(fromFile);
1328
+ let resolved = path.resolve(dir, importPath);
1329
+ // Add .ts extension if missing
1330
+ if (!path.extname(resolved)) {
1331
+ resolved += ".ts";
1332
+ }
1333
+ // Remove .js and try .ts
1334
+ if (resolved.endsWith(".js")) {
1335
+ resolved = resolved.slice(0, -3) + ".ts";
1336
+ }
1337
+ return resolved;
1338
+ }
1339
+ /**
1340
+ * 파일 존재 확인.
1341
+ */
1342
+ async fileExists(filePath) {
1343
+ try {
1344
+ await access(filePath, constants.F_OK);
1345
+ return true;
1346
+ }
1347
+ catch {
1348
+ return false;
1349
+ }
1350
+ }
1351
+ }
1352
+ //# sourceMappingURL=qa-pipeline.js.map