triflux 6.0.21 → 6.1.1

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/hub/intent.mjs ADDED
@@ -0,0 +1,100 @@
1
+ // hub/intent.mjs — Intent Classification Engine
2
+ // 사용자 요청의 "진짜 의도"를 분석 → 카테고리 분류 → 최적 에이전트/모델 자동 선택
3
+
4
+ /** triflux 특화 의도 카테고리 (10개) */
5
+ export const INTENT_CATEGORIES = {
6
+ implement: { agent: 'executor', mcp: 'implement', effort: 'high' },
7
+ debug: { agent: 'debugger', mcp: 'implement', effort: 'high' },
8
+ analyze: { agent: 'analyst', mcp: 'analyze', effort: 'xhigh' },
9
+ design: { agent: 'architect', mcp: 'analyze', effort: 'xhigh' },
10
+ review: { agent: 'code-reviewer', mcp: 'review', effort: 'thorough' },
11
+ document: { agent: 'writer', mcp: 'docs', effort: 'pro' },
12
+ research: { agent: 'scientist', mcp: 'analyze', effort: 'high' },
13
+ 'quick-fix':{ agent: 'build-fixer', mcp: 'implement', effort: 'fast' },
14
+ explain: { agent: 'writer', mcp: 'docs', effort: 'flash' },
15
+ test: { agent: 'test-engineer', mcp: null, effort: null },
16
+ };
17
+
18
+ /** @internal 키워드 → 카테고리 매핑 패턴 */
19
+ const KEYWORD_PATTERNS = [
20
+ { category: 'implement', keywords: ['구현', '만들', '추가', '생성', '작성', '빌드', 'implement', 'create', 'add', 'build', 'make', 'develop'], weight: 1.0 },
21
+ { category: 'debug', keywords: ['버그', '에러', '오류', '고쳐', '수정', '디버그', 'fix', 'bug', 'error', 'debug', 'troubleshoot', 'crash', 'broken'], weight: 1.0 },
22
+ { category: 'analyze', keywords: ['분석', '조사', '파악', 'analyze', 'investigate', 'examine', 'inspect'], weight: 0.9 },
23
+ { category: 'design', keywords: ['설계', '아키텍처', '디자인', '구조', 'design', 'architect', 'structure'], weight: 0.9 },
24
+ { category: 'review', keywords: ['리뷰', '검토', '코드리뷰', 'review', 'code review', 'audit'], weight: 1.0 },
25
+ { category: 'document', keywords: ['문서', '도큐먼트', '문서화', 'document', 'docs', 'documentation', 'readme'], weight: 0.9 },
26
+ { category: 'research', keywords: ['리서치', '연구', '탐색', 'research', 'explore', 'study'], weight: 0.8 },
27
+ { category: 'quick-fix', keywords: ['빠르게', '간단히', '급한', 'quick fix', 'hotfix', 'quick'], weight: 0.85 },
28
+ { category: 'explain', keywords: ['설명', '뭐야', '알려', '이해', 'explain', 'what is', 'how does', 'tell me', 'describe'], weight: 1.0 },
29
+ { category: 'test', keywords: ['테스트', '테스팅', '시험', '검증', 'test', 'testing', 'spec', 'unit test'], weight: 1.0 },
30
+ ];
31
+
32
+ /**
33
+ * 키워드 기반 빠른 분류 (0-cost, Codex 호출 없이)
34
+ * 고신뢰(>0.8) 시 Codex triage 건너뜀
35
+ * @param {string} prompt - 사용자 프롬프트
36
+ * @returns {{ category: string, confidence: number }}
37
+ */
38
+ export function quickClassify(prompt) {
39
+ if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
40
+ return { category: 'implement', confidence: 0.1 };
41
+ }
42
+
43
+ const lower = prompt.toLowerCase().trim();
44
+ let bestCategory = null;
45
+ let bestScore = 0;
46
+
47
+ for (const { category, keywords, weight } of KEYWORD_PATTERNS) {
48
+ let matchCount = 0;
49
+ for (const kw of keywords) {
50
+ if (lower.includes(kw)) matchCount++;
51
+ }
52
+ if (matchCount > 0) {
53
+ const score = (matchCount / keywords.length) * weight;
54
+ if (score > bestScore) {
55
+ bestScore = score;
56
+ bestCategory = category;
57
+ }
58
+ }
59
+ }
60
+
61
+ if (!bestCategory) {
62
+ return { category: 'implement', confidence: 0.3 };
63
+ }
64
+
65
+ // 매칭 품질 기반 신뢰도 (0.5~0.95 범위)
66
+ const confidence = Math.min(0.95, 0.5 + bestScore * 0.5);
67
+ return { category: bestCategory, confidence };
68
+ }
69
+
70
+ /**
71
+ * 전체 의도 분류 — routing 정보 포함
72
+ * @param {string} prompt
73
+ * @returns {{ category: string, confidence: number, reasoning: string, routing: { agent: string, mcp: string|null, effort: string|null } }}
74
+ */
75
+ export function classifyIntent(prompt) {
76
+ const quick = quickClassify(prompt);
77
+ const routing = INTENT_CATEGORIES[quick.category] || INTENT_CATEGORIES.implement;
78
+
79
+ return {
80
+ category: quick.category,
81
+ confidence: quick.confidence,
82
+ reasoning: `keyword-match: ${quick.category} (${quick.confidence.toFixed(2)})`,
83
+ routing: {
84
+ agent: routing.agent,
85
+ mcp: routing.mcp,
86
+ effort: routing.effort,
87
+ },
88
+ };
89
+ }
90
+
91
+ /**
92
+ * 분류 히스토리 기반 학습 (reflexion 연동 가능)
93
+ * @param {string} prompt
94
+ * @param {string} actualCategory - 실제 카테고리
95
+ */
96
+ export function refineClassification(prompt, actualCategory) {
97
+ // reflexion 연동 시 store에 오분류 기록 저장 예정
98
+ void prompt;
99
+ void actualCategory;
100
+ }
@@ -0,0 +1,56 @@
1
+ // hub/pipeline/gates/confidence.mjs — Pre-Execution Confidence Gate
2
+ //
3
+ // plan → prd → [confidence] → exec
4
+ // 5단계 확신도 검증: >=90% proceed / 70-89% alternative / <70% abort
5
+
6
+ export const CRITERIA = [
7
+ { id: 'no_duplicate', label: '중복 구현 없는지?', weight: 0.25 },
8
+ { id: 'architecture', label: '아키텍처 준수?', weight: 0.25 },
9
+ { id: 'docs_verified', label: '공식 문서 확인?', weight: 0.20 },
10
+ { id: 'oss_reference', label: 'OSS 레퍼런스?', weight: 0.15 },
11
+ { id: 'root_cause', label: '근본 원인 파악?', weight: 0.15 },
12
+ ];
13
+
14
+ /**
15
+ * 확신도 검증 실행
16
+ * @param {string|object} planArtifact - plan 단계에서 생성된 구현 계획
17
+ * @param {object} context - { checks?, codebaseFiles?, existingTests? }
18
+ * @param {object} [context.checks] - 각 기준별 점수 (boolean 또는 0-1 숫자)
19
+ * @returns {{ score: number, breakdown: Array, decision: string, reasoning: string }}
20
+ */
21
+ export function runConfidenceCheck(planArtifact, context = {}) {
22
+ if (!planArtifact) {
23
+ return {
24
+ score: 0,
25
+ breakdown: CRITERIA.map(c => ({ id: c.id, label: c.label, weight: c.weight, score: 0, passed: false })),
26
+ decision: 'abort',
27
+ reasoning: 'planArtifact가 제공되지 않았습니다.',
28
+ };
29
+ }
30
+
31
+ const checks = context.checks || {};
32
+
33
+ const breakdown = CRITERIA.map(c => {
34
+ const raw = checks[c.id];
35
+ const score = typeof raw === 'number' ? Math.max(0, Math.min(1, raw)) : (raw ? 1 : 0);
36
+ return { id: c.id, label: c.label, weight: c.weight, score, passed: score >= 0.7 };
37
+ });
38
+
39
+ const totalScore = Math.round(
40
+ breakdown.reduce((sum, b) => sum + b.score * b.weight, 0) * 100,
41
+ );
42
+
43
+ let decision, reasoning;
44
+ if (totalScore >= 90) {
45
+ decision = 'proceed';
46
+ reasoning = `확신도 ${totalScore}%: 모든 기준 충족. 실행 진행.`;
47
+ } else if (totalScore >= 70) {
48
+ decision = 'alternative';
49
+ reasoning = `확신도 ${totalScore}%: 일부 기준 미달. 대안 검토 필요.`;
50
+ } else {
51
+ decision = 'abort';
52
+ reasoning = `확신도 ${totalScore}%: 기준 미달. 실행 중단.`;
53
+ }
54
+
55
+ return { score: totalScore, breakdown, decision, reasoning };
56
+ }
@@ -0,0 +1,4 @@
1
+ // hub/pipeline/gates/index.mjs — Quality Gates re-export
2
+
3
+ export { CRITERIA, runConfidenceCheck } from './confidence.mjs';
4
+ export { RED_FLAGS, QUESTIONS, runSelfCheck } from './selfcheck.mjs';
@@ -0,0 +1,84 @@
1
+ // hub/pipeline/gates/selfcheck.mjs — Post-Execution Self-Check (Hallucination Detection)
2
+ //
3
+ // exec → verify → [selfcheck] → complete/fix
4
+ // 4대 필수 질문 + 7대 할루시네이션 Red Flag 탐지
5
+
6
+ export const RED_FLAGS = [
7
+ { id: 'test_pass_no_output', pattern: /테스트\s*(?:가\s*)?통과/, label: '"테스트 통과" (출력 없이)' },
8
+ { id: 'everything_works', pattern: /모든\s*게?\s*작동/, label: '"모든게 작동" (증거 없이)' },
9
+ { id: 'no_changes_with_diff', pattern: /변경\s*(?:사항\s*)?없/, label: '"변경 없음" (diff 있는데)' },
10
+ { id: 'backward_compatible', pattern: /호환성\s*(?:이\s*)?유지/, label: '"호환성 유지" (검증 없이)' },
11
+ { id: 'performance_improved', pattern: /성능\s*(?:이\s*)?개선/, label: '"성능 개선" (벤치마크 없이)' },
12
+ { id: 'security_enhanced', pattern: /보안\s*(?:이\s*)?강화/, label: '"보안 강화" (증거 없이)' },
13
+ { id: 'error_handling_done', pattern: /에러\s*처리\s*(?:가\s*)?완료/, label: '"에러 처리 완료" (catch 블록만)' },
14
+ ];
15
+
16
+ export const QUESTIONS = [
17
+ { id: 'tests_passing', label: '모든 테스트 통과?', evidenceKey: 'testOutput' },
18
+ { id: 'requirements_met', label: '모든 요구사항 충족?', evidenceKey: 'requirementChecklist' },
19
+ { id: 'no_assumptions', label: '검증 없는 가정?', evidenceKey: 'references' },
20
+ { id: 'evidence_provided', label: '증거 있는가?', evidenceKey: 'artifacts' },
21
+ ];
22
+
23
+ /**
24
+ * Red Flag 스캔 — 텍스트에서 할루시네이션 패턴 탐지
25
+ * @param {string} text - 스캔 대상 텍스트
26
+ * @param {object} context - { hasDiff?, evidence? }
27
+ * @returns {Array<{ id: string, label: string }>}
28
+ */
29
+ function detectRedFlags(text, context = {}) {
30
+ const flags = [];
31
+ const evidence = context.evidence || {};
32
+
33
+ for (const rf of RED_FLAGS) {
34
+ if (!rf.pattern.test(text)) continue;
35
+
36
+ // "변경 없음"은 실제 diff가 있을 때만 Red Flag
37
+ if (rf.id === 'no_changes_with_diff' && !context.hasDiff) continue;
38
+
39
+ // "테스트 통과"는 testOutput 증거가 없을 때만 Red Flag
40
+ if (rf.id === 'test_pass_no_output' && evidence.testOutput) continue;
41
+
42
+ // 기타 Red Flag는 해당 id의 반증이 있으면 스킵
43
+ if (evidence[rf.id]) continue;
44
+
45
+ flags.push({ id: rf.id, label: rf.label });
46
+ }
47
+
48
+ return flags;
49
+ }
50
+
51
+ /**
52
+ * Self-Check 실행
53
+ * @param {string|object} execResult - 실행 결과 (텍스트 또는 객체)
54
+ * @param {string|object} verifyResult - 검증 결과 (텍스트 또는 객체)
55
+ * @param {object} requirements - { hasDiff?, evidence? }
56
+ * @param {boolean} [requirements.hasDiff] - diff 존재 여부
57
+ * @param {object} [requirements.evidence] - { testOutput, requirementChecklist, references, artifacts }
58
+ * @returns {{ passed: boolean, score: number, flags: Array, checklist: Array }}
59
+ */
60
+ export function runSelfCheck(execResult, verifyResult, requirements = {}) {
61
+ const text = [
62
+ typeof execResult === 'string' ? execResult : JSON.stringify(execResult || ''),
63
+ typeof verifyResult === 'string' ? verifyResult : JSON.stringify(verifyResult || ''),
64
+ ].join('\n');
65
+
66
+ const flags = detectRedFlags(text, requirements);
67
+
68
+ const evidence = requirements.evidence || {};
69
+ const checklist = QUESTIONS.map(q => {
70
+ const ev = evidence[q.evidenceKey];
71
+ const passed = ev != null && (typeof ev === 'string' ? ev.trim().length > 0 : true);
72
+ return { id: q.id, label: q.label, passed, evidence: ev || null };
73
+ });
74
+
75
+ const allQuestionsPassed = checklist.every(q => q.passed);
76
+ const passed = flags.length === 0 && allQuestionsPassed;
77
+
78
+ // 점수: 기본 100, Red Flag당 -15, 실패 질문당 -20
79
+ const flagPenalty = flags.length * 15;
80
+ const questionPenalty = checklist.filter(q => !q.passed).length * 20;
81
+ const score = Math.max(0, 100 - flagPenalty - questionPenalty);
82
+
83
+ return { passed, score, flags, checklist };
84
+ }
@@ -14,6 +14,10 @@ import {
14
14
  updatePipelineState,
15
15
  removePipelineState,
16
16
  } from './state.mjs';
17
+ import { runConfidenceCheck } from './gates/confidence.mjs';
18
+ import { runSelfCheck } from './gates/selfcheck.mjs';
19
+ import { classifyIntent as _classifyIntent } from '../intent.mjs';
20
+ // deslop gate: 호출자가 scanDirectory/detectSlop 결과를 전달
17
21
 
18
22
  /**
19
23
  * 파이프라인 매니저 생성
@@ -154,6 +158,86 @@ export function createPipeline(db, teamName, opts = {}) {
154
158
  remove() {
155
159
  return removePipelineState(db, teamName);
156
160
  },
161
+
162
+ /**
163
+ * Confidence Gate 실행 + 자동 전이
164
+ * prd → confidence → exec/failed
165
+ * @param {string|object} planArtifact
166
+ * @param {object} context - { checks?, codebaseFiles?, existingTests? }
167
+ * @returns {{ ok: boolean, gate: object, state?: object, error?: string }}
168
+ */
169
+ runConfidenceGate(planArtifact, context = {}) {
170
+ const current = readPipelineState(db, teamName);
171
+ if (!current) return { ok: false, error: `파이프라인 없음: ${teamName}` };
172
+
173
+ if (current.phase !== 'confidence') {
174
+ return { ok: false, error: `confidence gate는 confidence 단계에서만 실행 가능 (현재: ${current.phase})` };
175
+ }
176
+
177
+ const gate = runConfidenceCheck(planArtifact, context);
178
+ this.setArtifact('confidence_result', gate);
179
+
180
+ if (gate.decision === 'abort') {
181
+ const result = this.advance('failed');
182
+ return { ok: true, gate, state: result.state };
183
+ }
184
+
185
+ // proceed 또는 alternative → exec로 전이
186
+ const result = this.advance('exec');
187
+ return { ok: result.ok, gate, state: result.state, error: result.error };
188
+ },
189
+
190
+ /**
191
+ * Deslop Gate 실행 + 자동 전이
192
+ * exec → deslop → verify
193
+ * 호출자가 미리 deslop 결과를 생성하여 전달.
194
+ * @param {object} [deslopResult] - scanDirectory() 또는 detectSlop() 결과
195
+ * @returns {{ ok: boolean, gate: object, state?: object, error?: string }}
196
+ */
197
+ runDeslopGate(deslopResult = null) {
198
+ const current = readPipelineState(db, teamName);
199
+ if (!current) return { ok: false, error: `파이프라인 없음: ${teamName}` };
200
+
201
+ if (current.phase !== 'deslop') {
202
+ return { ok: false, error: `deslop gate는 deslop 단계에서만 실행 가능 (현재: ${current.phase})` };
203
+ }
204
+
205
+ const gate = deslopResult || { files: [], summary: { total: 0, clean: 0 } };
206
+ this.setArtifact('deslop_result', gate);
207
+
208
+ // deslop은 항상 verify로 전이 (정보 제공 게이트, 차단 없음)
209
+ const result = this.advance('verify');
210
+ return { ok: result.ok, gate, state: result.state, error: result.error };
211
+ },
212
+
213
+ /**
214
+ * Self-Check Gate 실행 + 자동 전이
215
+ * verify → selfcheck → complete/fix
216
+ * @param {string|object} execResult
217
+ * @param {string|object} verifyResult
218
+ * @param {object} requirements - { hasDiff?, evidence? }
219
+ * @returns {{ ok: boolean, gate: object, state?: object, error?: string }}
220
+ */
221
+ runSelfCheckGate(execResult, verifyResult, requirements = {}) {
222
+ const current = readPipelineState(db, teamName);
223
+ if (!current) return { ok: false, error: `파이프라인 없음: ${teamName}` };
224
+
225
+ if (current.phase !== 'selfcheck') {
226
+ return { ok: false, error: `selfcheck gate는 selfcheck 단계에서만 실행 가능 (현재: ${current.phase})` };
227
+ }
228
+
229
+ const gate = runSelfCheck(execResult, verifyResult, requirements);
230
+ this.setArtifact('selfcheck_result', gate);
231
+
232
+ if (gate.passed) {
233
+ const result = this.advance('complete');
234
+ return { ok: result.ok, gate, state: result.state, error: result.error };
235
+ }
236
+
237
+ // Red Flag 탐지 또는 필수 질문 실패 → fix
238
+ const result = this.advance('fix');
239
+ return { ok: result.ok, gate, state: result.state, error: result.error };
240
+ },
157
241
  };
158
242
  }
159
243
 
@@ -212,5 +296,23 @@ export async function benchmarkEnd(preLabel, postLabel, options = {}) {
212
296
  } catch { return null; }
213
297
  }
214
298
 
299
+ /**
300
+ * 트리아지 통합: quickClassify 고신뢰 시 Codex 분류 스킵 판정
301
+ * @param {string} prompt
302
+ * @param {number} [threshold=0.8]
303
+ * @returns {{ skip: boolean, routing: object|null, classification: object }}
304
+ */
305
+ export function triageWithIntent(prompt, threshold = 0.8) {
306
+ const classification = _classifyIntent(prompt);
307
+ if (classification.confidence >= threshold) {
308
+ return { skip: true, routing: classification.routing, classification };
309
+ }
310
+ return { skip: false, routing: null, classification };
311
+ }
312
+
215
313
  export { ensurePipelineTable } from './state.mjs';
216
314
  export { PHASES, TERMINAL, ALLOWED, canTransition } from './transitions.mjs';
315
+ export { CRITERIA, runConfidenceCheck } from './gates/confidence.mjs';
316
+ export { RED_FLAGS, QUESTIONS, runSelfCheck } from './gates/selfcheck.mjs';
317
+ export { detectSlop, autoFixSlop, scanDirectory } from '../quality/deslop.mjs';
318
+ export { quickClassify, classifyIntent, INTENT_CATEGORIES } from '../intent.mjs';
@@ -1,21 +1,27 @@
1
1
  // hub/pipeline/transitions.mjs — 파이프라인 단계 전이 규칙
2
2
  //
3
- // plan → prd → exec → verify → complete/fix
3
+ // plan → prd → confidence → exec → deslop → verify → selfcheck → complete/fix
4
4
  // fix → exec/verify/complete/failed
5
5
  // complete, failed = 터미널 상태
6
6
 
7
- export const PHASES = ['plan', 'prd', 'exec', 'verify', 'fix', 'complete', 'failed'];
7
+ export const PHASES = [
8
+ 'plan', 'prd', 'confidence', 'exec', 'deslop', 'verify', 'selfcheck',
9
+ 'fix', 'complete', 'failed',
10
+ ];
8
11
 
9
12
  export const TERMINAL = new Set(['complete', 'failed']);
10
13
 
11
14
  export const ALLOWED = {
12
- 'plan': ['prd'],
13
- 'prd': ['exec'],
14
- 'exec': ['verify'],
15
- 'verify': ['fix', 'complete', 'failed'],
16
- 'fix': ['exec', 'verify', 'complete', 'failed'],
17
- 'complete': [],
18
- 'failed': [],
15
+ 'plan': ['prd'],
16
+ 'prd': ['confidence'],
17
+ 'confidence': ['exec', 'failed'],
18
+ 'exec': ['deslop'],
19
+ 'deslop': ['verify'],
20
+ 'verify': ['selfcheck', 'fix', 'failed'],
21
+ 'selfcheck': ['complete', 'fix'],
22
+ 'fix': ['exec', 'verify', 'complete', 'failed'],
23
+ 'complete': [],
24
+ 'failed': [],
19
25
  };
20
26
 
21
27
  /**
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Anti-Slop Code Pass
3
+ * AI 생성 코드에서 불필요한 요소를 자동 탐지/제거하는 정적 분석 모듈
4
+ */
5
+
6
+ import { readdir, readFile, writeFile, stat } from 'node:fs/promises';
7
+ import { join, relative } from 'node:path';
8
+
9
+ /** @type {Array<{type: string, pattern: RegExp, severity: string, autoFixable: boolean, multiline: boolean}>} */
10
+ export const SLOP_PATTERNS = [
11
+ {
12
+ type: 'trivial_comment',
13
+ pattern: /^\s*\/\/\s*(import|define|set|get|return|export)\s/i,
14
+ severity: 'low',
15
+ autoFixable: true,
16
+ multiline: false,
17
+ },
18
+ {
19
+ type: 'empty_catch',
20
+ pattern: /catch\s*\([^)]*\)\s*\{\s*\}/,
21
+ severity: 'med',
22
+ autoFixable: false,
23
+ multiline: true,
24
+ },
25
+ {
26
+ type: 'console_debug',
27
+ pattern: /^\s*console\.(log|debug|info)\(/,
28
+ severity: 'low',
29
+ autoFixable: true,
30
+ multiline: false,
31
+ },
32
+ {
33
+ type: 'useless_jsdoc',
34
+ pattern: /\/\*\*\s*\n\s*\*\s*\n\s*\*\//,
35
+ severity: 'low',
36
+ autoFixable: true,
37
+ multiline: true,
38
+ },
39
+ {
40
+ type: 'rethrow_only',
41
+ pattern: /catch\s*\((\w+)\)\s*\{\s*throw\s+\1\s*;?\s*\}/,
42
+ severity: 'med',
43
+ autoFixable: false,
44
+ multiline: true,
45
+ },
46
+ {
47
+ type: 'redundant_type',
48
+ pattern: /:\s*(string|number|boolean)\s*=\s*('[^']*'|"[^"]*"|\d+|true|false)/,
49
+ severity: 'low',
50
+ autoFixable: false,
51
+ multiline: false,
52
+ },
53
+ {
54
+ type: 'commented_code',
55
+ pattern: /^\s*\/\/\s*(const |let |var |function |class |if\s*\(|for\s*\(|while\s*\(|return |await )/,
56
+ severity: 'low',
57
+ autoFixable: true,
58
+ multiline: false,
59
+ },
60
+ ];
61
+
62
+ const SEVERITY_WEIGHT = { low: 2, med: 5 };
63
+
64
+ /**
65
+ * 파일 내용에서 slop 패턴 탐지
66
+ * @param {string} content - 파일 내용
67
+ * @param {string} [filePath] - 파일 경로 (보고용)
68
+ * @returns {{ issues: Array<{line: number, type: string, severity: string, suggestion: string, text: string, autoFixable: boolean}>, score: number }}
69
+ */
70
+ export function detectSlop(content, filePath = '') {
71
+ const lines = content.split('\n');
72
+ const issues = [];
73
+
74
+ for (const sp of SLOP_PATTERNS) {
75
+ if (sp.multiline) continue;
76
+ for (let i = 0; i < lines.length; i++) {
77
+ if (sp.pattern.test(lines[i])) {
78
+ issues.push({
79
+ line: i + 1,
80
+ type: sp.type,
81
+ severity: sp.severity,
82
+ suggestion: `${sp.type} 패턴 감지`,
83
+ text: lines[i].trim(),
84
+ autoFixable: sp.autoFixable,
85
+ });
86
+ }
87
+ }
88
+ }
89
+
90
+ for (const sp of SLOP_PATTERNS) {
91
+ if (!sp.multiline) continue;
92
+ const regex = new RegExp(sp.pattern.source, sp.pattern.flags.replace('g', '') + 'g');
93
+ let match;
94
+ while ((match = regex.exec(content)) !== null) {
95
+ const line = content.substring(0, match.index).split('\n').length;
96
+ issues.push({
97
+ line,
98
+ type: sp.type,
99
+ severity: sp.severity,
100
+ suggestion: `${sp.type} 패턴 감지`,
101
+ text: match[0].split('\n')[0].trim(),
102
+ autoFixable: sp.autoFixable,
103
+ });
104
+ }
105
+ }
106
+
107
+ issues.sort((a, b) => a.line - b.line);
108
+
109
+ const totalPenalty = issues.reduce((sum, i) => sum + (SEVERITY_WEIGHT[i.severity] || 2), 0);
110
+ const score = Math.max(0, 100 - totalPenalty);
111
+
112
+ return { issues, score };
113
+ }
114
+
115
+ /**
116
+ * 자동 수정 (safe transforms만)
117
+ * @param {string} content - 파일 내용
118
+ * @param {Array<{type: string, autoFixable: boolean}>} issues - detectSlop 결과
119
+ * @returns {{ fixed: string, applied: number, skipped: number }}
120
+ */
121
+ export function autoFixSlop(content, issues) {
122
+ if (!issues || issues.length === 0) return { fixed: content, applied: 0, skipped: 0 };
123
+
124
+ const fixable = issues.filter(i => i.autoFixable);
125
+ const skipped = issues.length - fixable.length;
126
+
127
+ if (fixable.length === 0) return { fixed: content, applied: 0, skipped };
128
+
129
+ let fixed = content;
130
+ let applied = 0;
131
+
132
+ // Multi-line: useless_jsdoc 제거
133
+ if (fixable.some(i => i.type === 'useless_jsdoc')) {
134
+ const matches = fixed.match(/\/\*\*\s*\n\s*\*\s*\n\s*\*\/\n?/g);
135
+ if (matches) {
136
+ fixed = fixed.replace(/\/\*\*\s*\n\s*\*\s*\n\s*\*\/\n?/g, '');
137
+ applied += matches.length;
138
+ }
139
+ }
140
+
141
+ // Line-level: trivial_comment, console_debug, commented_code 제거
142
+ const lineTypes = new Set(
143
+ fixable
144
+ .filter(i => ['trivial_comment', 'console_debug', 'commented_code'].includes(i.type))
145
+ .map(i => i.type),
146
+ );
147
+
148
+ if (lineTypes.size > 0) {
149
+ const linePatterns = SLOP_PATTERNS.filter(p => lineTypes.has(p.type));
150
+ const lines = fixed.split('\n');
151
+ const result = [];
152
+ for (const line of lines) {
153
+ let remove = false;
154
+ for (const p of linePatterns) {
155
+ if (p.pattern.test(line)) {
156
+ remove = true;
157
+ applied++;
158
+ break;
159
+ }
160
+ }
161
+ if (!remove) result.push(line);
162
+ }
163
+ fixed = result.join('\n');
164
+ }
165
+
166
+ return { fixed, applied, skipped };
167
+ }
168
+
169
+ function matchesGlob(filePath, pattern) {
170
+ const normalized = '/' + filePath.replace(/\\/g, '/');
171
+
172
+ // **/*.ext → 확장자 매칭
173
+ if (pattern.startsWith('**/*.')) {
174
+ const ext = pattern.slice(4);
175
+ return normalized.endsWith(ext);
176
+ }
177
+
178
+ // *.ext → 확장자 매칭 (디렉토리 무관)
179
+ if (pattern.startsWith('*.') && !pattern.includes('/')) {
180
+ const ext = pattern.slice(1);
181
+ return normalized.endsWith(ext);
182
+ }
183
+
184
+ // **/dir/** → 디렉토리 포함 여부
185
+ const dirMatch = pattern.match(/^\*\*\/([^*]+)\/\*\*$/);
186
+ if (dirMatch) {
187
+ return normalized.includes('/' + dirMatch[1] + '/');
188
+ }
189
+
190
+ return false;
191
+ }
192
+
193
+ /**
194
+ * 디렉토리 전체 스캔
195
+ * @param {string} dirPath - 스캔 대상 디렉토리
196
+ * @param {object} [opts]
197
+ * @param {string[]} [opts.include] - 포함할 glob 패턴
198
+ * @param {string[]} [opts.exclude] - 제외할 glob 패턴
199
+ * @param {boolean} [opts.autoFix] - 자동 수정 여부
200
+ * @returns {Promise<{ files: Array<{path: string, issues: Array, score: number}>, summary: object }>}
201
+ */
202
+ export async function scanDirectory(dirPath, opts = {}) {
203
+ const {
204
+ include = ['**/*.mjs', '**/*.js', '**/*.ts'],
205
+ exclude = ['**/node_modules/**', '**/dist/**', '**/.git/**'],
206
+ autoFix = false,
207
+ } = opts;
208
+
209
+ const entries = await readdir(dirPath, { recursive: true });
210
+ const files = [];
211
+
212
+ for (const entry of entries) {
213
+ const normalized = entry.replace(/\\/g, '/');
214
+ const fullPath = join(dirPath, entry);
215
+
216
+ let st;
217
+ try { st = await stat(fullPath); } catch { continue; }
218
+ if (!st.isFile()) continue;
219
+
220
+ const included = include.some(p => matchesGlob(normalized, p));
221
+ const excluded = exclude.some(p => matchesGlob(normalized, p));
222
+ if (!included || excluded) continue;
223
+
224
+ const fileContent = await readFile(fullPath, 'utf-8');
225
+ const { issues, score } = detectSlop(fileContent, normalized);
226
+
227
+ if (autoFix && issues.length > 0) {
228
+ const { fixed, applied } = autoFixSlop(fileContent, issues);
229
+ if (applied > 0) await writeFile(fullPath, fixed, 'utf-8');
230
+ }
231
+
232
+ files.push({ path: normalized, issues, score });
233
+ }
234
+
235
+ const totalIssues = files.reduce((sum, f) => sum + f.issues.length, 0);
236
+ const avgScore = files.length > 0
237
+ ? Math.round(files.reduce((sum, f) => sum + f.score, 0) / files.length)
238
+ : 100;
239
+
240
+ const byType = {};
241
+ for (const f of files) {
242
+ for (const issue of f.issues) {
243
+ byType[issue.type] = (byType[issue.type] || 0) + 1;
244
+ }
245
+ }
246
+
247
+ return {
248
+ files,
249
+ summary: { totalFiles: files.length, totalIssues, averageScore: avgScore, byType },
250
+ };
251
+ }