triflux 6.0.21 → 6.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.
- package/hub/intent.mjs +100 -0
- package/hub/pipeline/gates/confidence.mjs +56 -0
- package/hub/pipeline/gates/index.mjs +4 -0
- package/hub/pipeline/gates/selfcheck.mjs +84 -0
- package/hub/pipeline/index.mjs +102 -0
- package/hub/pipeline/transitions.mjs +15 -9
- package/hub/quality/deslop.mjs +251 -0
- package/hub/reflexion.mjs +103 -0
- package/hub/research.mjs +141 -0
- package/hub/schema.sql +19 -0
- package/hub/store.mjs +73 -1
- package/hub/team/cli/commands/start/index.mjs +2 -2
- package/hub/team/cli/commands/start/parse-args.mjs +4 -0
- package/hub/team/cli/commands/start/start-headless.mjs +14 -7
- package/hub/team/handoff.mjs +291 -0
- package/hub/team/headless.mjs +82 -17
- package/hub/token-mode.mjs +134 -0
- package/package.json +1 -1
- package/scripts/headless-guard.mjs +3 -1
- package/skills/tfx-autoresearch/SKILL.md +129 -0
- package/skills/tfx-deep-interview/SKILL.md +133 -0
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,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
|
+
}
|
package/hub/pipeline/index.mjs
CHANGED
|
@@ -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 = [
|
|
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':
|
|
13
|
-
'prd':
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
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
|
+
}
|