@triflux/core 10.50.1 → 10.51.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/hooks/agy-session-hook.mjs +11 -56
- package/hooks/codex-session-hook.mjs +11 -101
- package/hub/bridge.mjs +1 -79
- package/hub/cli-adapter-base.mjs +12 -90
- package/hub/codex-adapter.mjs +0 -1
- package/hub/codex-preflight.mjs +5 -28
- package/hub/index.mjs +3 -9
- package/hub/lib/cto-env.mjs +0 -23
- package/hub/lib/known-errors.json +2 -2
- package/hub/lib/process-utils.mjs +0 -123
- package/hub/lib/spawn-trace.mjs +3 -2
- package/hub/lib/timeout-defaults.mjs +0 -1
- package/hub/paths.mjs +0 -6
- package/hub/pipeline/index.mjs +0 -190
- package/hub/pipeline/transitions.mjs +0 -10
- package/hub/reflexion.mjs +1 -84
- package/hub/role-contract.mjs +0 -2
- package/hud/cli-policy.mjs +0 -26
- package/hud/colors.mjs +0 -13
- package/hud/constants.mjs +7 -153
- package/hud/context-monitor.mjs +3 -64
- package/hud/hud-qos-status.mjs +54 -264
- package/hud/providers/claude.mjs +4 -16
- package/hud/providers/gemini.mjs +83 -689
- package/hud/renderers.mjs +59 -367
- package/hud/terminal.mjs +1 -63
- package/hud/utils.mjs +2 -26
- package/package.json +1 -4
- package/scripts/lib/agent-route-policy.mjs +162 -14
- package/scripts/lib/claudemd-scanner.mjs +2 -199
- package/scripts/lib/cli-claude.mjs +0 -2
- package/scripts/lib/context.mjs +3 -19
- package/scripts/lib/env-probe.mjs +5 -64
- package/scripts/lib/gemini-profiles.mjs +2 -69
- package/scripts/lib/legacy-mcp-cleanup.mjs +14 -3
- package/scripts/lib/machine-profile.mjs +3 -17
- package/scripts/lib/mcp-filter.mjs +109 -53
- package/scripts/lib/mcp-health.mjs +2 -2
- package/scripts/lib/mcp-server-catalog.mjs +2 -2
- package/scripts/lib/psmux-info.mjs +25 -104
- package/scripts/lib/session-presence.mjs +1 -1
- package/scripts/lib/skill-template.mjs +0 -271
- package/hub/codex-compat.mjs +0 -12
- package/hub/dynamic-routing-engine.mjs +0 -511
- package/hub/fullcycle.mjs +0 -113
- package/hub/gemini-adapter.mjs +0 -174
- package/hub/intent.mjs +0 -321
- package/hub/lib/mcp-response-cache.mjs +0 -239
- package/hub/lib/path-utils.mjs +0 -165
- package/hub/lib/risk-tier.mjs +0 -53
- package/hub/lib/ssh-command.mjs +0 -231
- package/hub/lib/staged-file-detect.mjs +0 -43
- package/hub/lib/tfx-route-args.mjs +0 -353
- package/hub/lib/timeline-adapter.mjs +0 -190
- package/hub/pipeline/gates/confidence.mjs +0 -75
- package/hub/pipeline/gates/consensus.mjs +0 -106
- package/hub/pipeline/gates/index.mjs +0 -9
- package/hub/pipeline/gates/selfcheck.mjs +0 -120
- package/hub/quality/deslop.mjs +0 -274
- package/hub/research.mjs +0 -155
- package/hub/role-control-events.mjs +0 -113
- package/hub/routing/complexity.mjs +0 -269
- package/hub/routing/index.mjs +0 -122
- package/hub/routing/q-learning.mjs +0 -387
- package/hub/routing-snapshot.mjs +0 -261
- package/hub/token-mode.mjs +0 -228
- package/hud/mission-board.mjs +0 -56
- package/hud/providers/cto.mjs +0 -76
- package/mesh/index.mjs +0 -69
- package/mesh/mesh-budget.mjs +0 -137
- package/mesh/mesh-heartbeat.mjs +0 -100
- package/mesh/mesh-protocol.mjs +0 -96
- package/mesh/mesh-queue.mjs +0 -163
- package/mesh/mesh-registry.mjs +0 -78
- package/mesh/mesh-router.mjs +0 -77
- package/scripts/lib/agent-json.mjs +0 -27
- package/scripts/lib/async.mjs +0 -174
- package/scripts/lib/cli-agy.mjs +0 -85
- package/scripts/lib/cli-codex.mjs +0 -62
- package/scripts/lib/cli-gemini.mjs +0 -37
- package/scripts/lib/dynamic-route-cli.mjs +0 -107
- package/scripts/lib/env.mjs +0 -14
- package/scripts/lib/hub.mjs +0 -48
- package/scripts/lib/pid.mjs +0 -63
- package/scripts/lib/quota.mjs +0 -47
- package/scripts/lib/skill-state.mjs +0 -220
- package/scripts/lib/team.mjs +0 -75
- package/scripts/lib/timeout.mjs +0 -66
- package/scripts/lib/tmp.mjs +0 -26
- package/scripts/lib/toml.mjs +0 -132
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
// hub/pipeline/gates/consensus.mjs — Consensus Quality Gate
|
|
2
|
-
//
|
|
3
|
-
// N개 결과의 합의도를 평가하여 5단계 분기 결정
|
|
4
|
-
// proceed(>=90%) / proceed_warn(>=75%) / retry(<75%+재시도) / escalate(<75%+감독) / abort
|
|
5
|
-
|
|
6
|
-
/** 단계별 합의 임계값 (%) */
|
|
7
|
-
export const STAGE_THRESHOLDS = {
|
|
8
|
-
plan: 50,
|
|
9
|
-
define: 75,
|
|
10
|
-
execute: 75,
|
|
11
|
-
verify: 80,
|
|
12
|
-
security: 100,
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
/** 환경변수 기반 기본 임계값 (기본 75) */
|
|
16
|
-
function getDefaultThreshold() {
|
|
17
|
-
const env =
|
|
18
|
-
typeof process !== "undefined" && process.env?.TRIFLUX_CONSENSUS_THRESHOLD;
|
|
19
|
-
if (env != null && env !== "") {
|
|
20
|
-
const parsed = Number(env);
|
|
21
|
-
if (!Number.isNaN(parsed) && parsed >= 0 && parsed <= 100) return parsed;
|
|
22
|
-
}
|
|
23
|
-
return 75;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* 성공률 기반 5단계 분기 결정
|
|
28
|
-
* @param {number} successRate - 합의 성공률 (0-100)
|
|
29
|
-
* @param {number} retryCount - 현재 재시도 횟수
|
|
30
|
-
* @param {number} maxRetries - 최대 재시도 횟수
|
|
31
|
-
* @param {string} [mode] - 실행 모드 ('supervised' | 기타)
|
|
32
|
-
* @returns {'proceed' | 'proceed_warn' | 'retry' | 'escalate' | 'abort'}
|
|
33
|
-
*/
|
|
34
|
-
export function evaluateQualityBranch(
|
|
35
|
-
successRate,
|
|
36
|
-
retryCount,
|
|
37
|
-
maxRetries,
|
|
38
|
-
mode,
|
|
39
|
-
) {
|
|
40
|
-
if (successRate >= 90) return "proceed";
|
|
41
|
-
if (successRate >= 75) return "proceed_warn";
|
|
42
|
-
|
|
43
|
-
// <75%: 재시도 가능 여부에 따라 분기
|
|
44
|
-
if (retryCount < maxRetries) return "retry";
|
|
45
|
-
if (mode === "supervised") return "escalate";
|
|
46
|
-
return "abort";
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* N개 결과의 합의도 평가
|
|
51
|
-
* @param {Array<{ success: boolean }>} results - 평가 대상 결과 배열
|
|
52
|
-
* @param {object} [options]
|
|
53
|
-
* @param {string} [options.stage] - 파이프라인 단계 (STAGE_THRESHOLDS 키)
|
|
54
|
-
* @param {number} [options.threshold] - 합의 임계값 직접 지정 (stage보다 우선)
|
|
55
|
-
* @param {number} [options.retryCount=0] - 현재 재시도 횟수
|
|
56
|
-
* @param {number} [options.maxRetries=2] - 최대 재시도 횟수
|
|
57
|
-
* @param {string} [options.mode] - 실행 모드 ('supervised' 등)
|
|
58
|
-
* @returns {{ successRate: number, threshold: number, decision: string, reasoning: string, results: Array }}
|
|
59
|
-
*/
|
|
60
|
-
export function evaluateConsensus(results, options = {}) {
|
|
61
|
-
if (!Array.isArray(results) || results.length === 0) {
|
|
62
|
-
return {
|
|
63
|
-
successRate: 0,
|
|
64
|
-
threshold: options.threshold ?? getDefaultThreshold(),
|
|
65
|
-
decision: "abort",
|
|
66
|
-
reasoning: "평가 대상 결과가 없습니다.",
|
|
67
|
-
results: [],
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const retryCount = options.retryCount ?? 0;
|
|
72
|
-
const maxRetries = options.maxRetries ?? 2;
|
|
73
|
-
const mode = options.mode;
|
|
74
|
-
|
|
75
|
-
// 임계값 결정: 직접 지정 > stage별 > 환경변수 > 기본 75
|
|
76
|
-
const threshold =
|
|
77
|
-
options.threshold ??
|
|
78
|
-
(options.stage && STAGE_THRESHOLDS[options.stage]) ??
|
|
79
|
-
getDefaultThreshold();
|
|
80
|
-
|
|
81
|
-
const successCount = results.filter((r) => r.success).length;
|
|
82
|
-
const successRate = Math.round((successCount / results.length) * 100);
|
|
83
|
-
|
|
84
|
-
const decision = evaluateQualityBranch(
|
|
85
|
-
successRate,
|
|
86
|
-
retryCount,
|
|
87
|
-
maxRetries,
|
|
88
|
-
mode,
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
const reasoningMap = {
|
|
92
|
-
proceed: `합의율 ${successRate}% (>= 90%): 전원 합의. 진행.`,
|
|
93
|
-
proceed_warn: `합의율 ${successRate}% (>= 75%): 부분 합의. 경고와 함께 진행.`,
|
|
94
|
-
retry: `합의율 ${successRate}% (< 75%): 재시도 ${retryCount + 1}/${maxRetries} 가능.`,
|
|
95
|
-
escalate: `합의율 ${successRate}% (< 75%): 감독 모드 에스컬레이션.`,
|
|
96
|
-
abort: `합의율 ${successRate}% (< 75%): 합의 실패. 중단.`,
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
successRate,
|
|
101
|
-
threshold,
|
|
102
|
-
decision,
|
|
103
|
-
reasoning: reasoningMap[decision],
|
|
104
|
-
results,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
// hub/pipeline/gates/index.mjs — Quality Gates re-export
|
|
2
|
-
|
|
3
|
-
export { CRITERIA, runConfidenceCheck } from "./confidence.mjs";
|
|
4
|
-
export {
|
|
5
|
-
evaluateConsensus,
|
|
6
|
-
evaluateQualityBranch,
|
|
7
|
-
STAGE_THRESHOLDS,
|
|
8
|
-
} from "./consensus.mjs";
|
|
9
|
-
export { QUESTIONS, RED_FLAGS, runSelfCheck } from "./selfcheck.mjs";
|
|
@@ -1,120 +0,0 @@
|
|
|
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
|
-
{
|
|
8
|
-
id: "test_pass_no_output",
|
|
9
|
-
pattern: /테스트\s*(?:가\s*)?통과/,
|
|
10
|
-
label: '"테스트 통과" (출력 없이)',
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
id: "everything_works",
|
|
14
|
-
pattern: /모든\s*게?\s*작동/,
|
|
15
|
-
label: '"모든게 작동" (증거 없이)',
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
id: "no_changes_with_diff",
|
|
19
|
-
pattern: /변경\s*(?:사항\s*)?없/,
|
|
20
|
-
label: '"변경 없음" (diff 있는데)',
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
id: "backward_compatible",
|
|
24
|
-
pattern: /호환성\s*(?:이\s*)?유지/,
|
|
25
|
-
label: '"호환성 유지" (검증 없이)',
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
id: "performance_improved",
|
|
29
|
-
pattern: /성능\s*(?:이\s*)?개선/,
|
|
30
|
-
label: '"성능 개선" (벤치마크 없이)',
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
id: "security_enhanced",
|
|
34
|
-
pattern: /보안\s*(?:이\s*)?강화/,
|
|
35
|
-
label: '"보안 강화" (증거 없이)',
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
id: "error_handling_done",
|
|
39
|
-
pattern: /에러\s*처리\s*(?:가\s*)?완료/,
|
|
40
|
-
label: '"에러 처리 완료" (catch 블록만)',
|
|
41
|
-
},
|
|
42
|
-
];
|
|
43
|
-
|
|
44
|
-
export const QUESTIONS = [
|
|
45
|
-
{
|
|
46
|
-
id: "tests_passing",
|
|
47
|
-
label: "모든 테스트 통과?",
|
|
48
|
-
evidenceKey: "testOutput",
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
id: "requirements_met",
|
|
52
|
-
label: "모든 요구사항 충족?",
|
|
53
|
-
evidenceKey: "requirementChecklist",
|
|
54
|
-
},
|
|
55
|
-
{ id: "no_assumptions", label: "검증 없는 가정?", evidenceKey: "references" },
|
|
56
|
-
{ id: "evidence_provided", label: "증거 있는가?", evidenceKey: "artifacts" },
|
|
57
|
-
];
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Red Flag 스캔 — 텍스트에서 할루시네이션 패턴 탐지
|
|
61
|
-
* @param {string} text - 스캔 대상 텍스트
|
|
62
|
-
* @param {object} context - { hasDiff?, evidence? }
|
|
63
|
-
* @returns {Array<{ id: string, label: string }>}
|
|
64
|
-
*/
|
|
65
|
-
function detectRedFlags(text, context = {}) {
|
|
66
|
-
const flags = [];
|
|
67
|
-
const evidence = context.evidence || {};
|
|
68
|
-
|
|
69
|
-
for (const rf of RED_FLAGS) {
|
|
70
|
-
if (!rf.pattern.test(text)) continue;
|
|
71
|
-
|
|
72
|
-
// "변경 없음"은 실제 diff가 있을 때만 Red Flag
|
|
73
|
-
if (rf.id === "no_changes_with_diff" && !context.hasDiff) continue;
|
|
74
|
-
|
|
75
|
-
// "테스트 통과"는 testOutput 증거가 없을 때만 Red Flag
|
|
76
|
-
if (rf.id === "test_pass_no_output" && evidence.testOutput) continue;
|
|
77
|
-
|
|
78
|
-
// 기타 Red Flag는 해당 id의 반증이 있으면 스킵
|
|
79
|
-
if (evidence[rf.id]) continue;
|
|
80
|
-
|
|
81
|
-
flags.push({ id: rf.id, label: rf.label });
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return flags;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Self-Check 실행
|
|
89
|
-
* @param {string|object} execResult - 실행 결과 (텍스트 또는 객체)
|
|
90
|
-
* @param {string|object} verifyResult - 검증 결과 (텍스트 또는 객체)
|
|
91
|
-
* @param {object} requirements - { hasDiff?, evidence? }
|
|
92
|
-
* @param {boolean} [requirements.hasDiff] - diff 존재 여부
|
|
93
|
-
* @param {object} [requirements.evidence] - { testOutput, requirementChecklist, references, artifacts }
|
|
94
|
-
* @returns {{ passed: boolean, score: number, flags: Array, checklist: Array }}
|
|
95
|
-
*/
|
|
96
|
-
export function runSelfCheck(execResult, verifyResult, requirements = {}) {
|
|
97
|
-
const normalize = (v) =>
|
|
98
|
-
typeof v === "string" ? v : v != null ? JSON.stringify(v) : "";
|
|
99
|
-
const text = [normalize(execResult), normalize(verifyResult)].join("\n");
|
|
100
|
-
|
|
101
|
-
const flags = detectRedFlags(text, requirements);
|
|
102
|
-
|
|
103
|
-
const evidence = requirements.evidence || {};
|
|
104
|
-
const checklist = QUESTIONS.map((q) => {
|
|
105
|
-
const ev = evidence[q.evidenceKey];
|
|
106
|
-
const passed =
|
|
107
|
-
ev != null && (typeof ev === "string" ? ev.trim().length > 0 : true);
|
|
108
|
-
return { id: q.id, label: q.label, passed, evidence: ev || null };
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
const allQuestionsPassed = checklist.every((q) => q.passed);
|
|
112
|
-
const passed = flags.length === 0 && allQuestionsPassed;
|
|
113
|
-
|
|
114
|
-
// 점수: 기본 100, Red Flag당 -15, 실패 질문당 -20
|
|
115
|
-
const flagPenalty = flags.length * 15;
|
|
116
|
-
const questionPenalty = checklist.filter((q) => !q.passed).length * 20;
|
|
117
|
-
const score = Math.max(0, 100 - flagPenalty - questionPenalty);
|
|
118
|
-
|
|
119
|
-
return { passed, score, flags, checklist };
|
|
120
|
-
}
|
package/hub/quality/deslop.mjs
DELETED
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Anti-Slop Code Pass
|
|
3
|
-
* AI 생성 코드에서 불필요한 요소를 자동 탐지/제거하는 정적 분석 모듈
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
-
|
|
9
|
-
/** @type {ReadonlyArray<{type: string, pattern: RegExp, severity: string, autoFixable: boolean, multiline: boolean}>} */
|
|
10
|
-
export const SLOP_PATTERNS = Object.freeze([
|
|
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:
|
|
49
|
-
/:\s*(string|number|boolean)\s*=\s*('[^']*'|"[^"]*"|\d+|true|false)/,
|
|
50
|
-
severity: "low",
|
|
51
|
-
autoFixable: false,
|
|
52
|
-
multiline: false,
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
type: "commented_code",
|
|
56
|
-
pattern:
|
|
57
|
-
/^\s*\/\/\s*(const |let |var |function |class |if\s*\(|for\s*\(|while\s*\(|return |await )/,
|
|
58
|
-
severity: "low",
|
|
59
|
-
autoFixable: true,
|
|
60
|
-
multiline: false,
|
|
61
|
-
},
|
|
62
|
-
]);
|
|
63
|
-
|
|
64
|
-
const SEVERITY_WEIGHT = { low: 2, med: 5 };
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* 파일 내용에서 slop 패턴 탐지
|
|
68
|
-
* @param {string} content - 파일 내용
|
|
69
|
-
* @param {string} [filePath] - 파일 경로 (보고용)
|
|
70
|
-
* @returns {{ issues: Array<{line: number, type: string, severity: string, suggestion: string, text: string, autoFixable: boolean}>, score: number }}
|
|
71
|
-
*/
|
|
72
|
-
export function detectSlop(content, filePath = "") {
|
|
73
|
-
const lines = content.split("\n");
|
|
74
|
-
const issues = [];
|
|
75
|
-
|
|
76
|
-
for (const sp of SLOP_PATTERNS) {
|
|
77
|
-
if (sp.multiline) continue;
|
|
78
|
-
for (let i = 0; i < lines.length; i++) {
|
|
79
|
-
if (sp.pattern.test(lines[i])) {
|
|
80
|
-
issues.push({
|
|
81
|
-
line: i + 1,
|
|
82
|
-
type: sp.type,
|
|
83
|
-
severity: sp.severity,
|
|
84
|
-
suggestion: `${sp.type} 패턴 감지`,
|
|
85
|
-
text: lines[i].trim(),
|
|
86
|
-
autoFixable: sp.autoFixable,
|
|
87
|
-
file: filePath,
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
for (const sp of SLOP_PATTERNS) {
|
|
94
|
-
if (!sp.multiline) continue;
|
|
95
|
-
const regex = new RegExp(
|
|
96
|
-
sp.pattern.source,
|
|
97
|
-
sp.pattern.flags.replace("g", "") + "g",
|
|
98
|
-
);
|
|
99
|
-
let match;
|
|
100
|
-
while ((match = regex.exec(content)) !== null) {
|
|
101
|
-
const line = content.substring(0, match.index).split("\n").length;
|
|
102
|
-
issues.push({
|
|
103
|
-
line,
|
|
104
|
-
type: sp.type,
|
|
105
|
-
severity: sp.severity,
|
|
106
|
-
suggestion: `${sp.type} 패턴 감지`,
|
|
107
|
-
text: match[0].split("\n")[0].trim(),
|
|
108
|
-
autoFixable: sp.autoFixable,
|
|
109
|
-
file: filePath,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
issues.sort((a, b) => a.line - b.line);
|
|
115
|
-
|
|
116
|
-
const totalPenalty = issues.reduce(
|
|
117
|
-
(sum, i) => sum + (SEVERITY_WEIGHT[i.severity] || 2),
|
|
118
|
-
0,
|
|
119
|
-
);
|
|
120
|
-
const score = Math.max(0, 100 - totalPenalty);
|
|
121
|
-
|
|
122
|
-
return { issues, score };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* 자동 수정 (safe transforms만)
|
|
127
|
-
* @param {string} content - 파일 내용
|
|
128
|
-
* @param {Array<{type: string, autoFixable: boolean}>} issues - detectSlop 결과
|
|
129
|
-
* @returns {{ fixed: string, applied: number, skipped: number }}
|
|
130
|
-
*/
|
|
131
|
-
export function autoFixSlop(content, issues) {
|
|
132
|
-
if (!issues || issues.length === 0)
|
|
133
|
-
return { fixed: content, applied: 0, skipped: 0 };
|
|
134
|
-
|
|
135
|
-
const fixable = issues.filter((i) => i.autoFixable);
|
|
136
|
-
const skipped = issues.length - fixable.length;
|
|
137
|
-
|
|
138
|
-
if (fixable.length === 0) return { fixed: content, applied: 0, skipped };
|
|
139
|
-
|
|
140
|
-
let fixed = content;
|
|
141
|
-
let applied = 0;
|
|
142
|
-
|
|
143
|
-
// Multi-line: useless_jsdoc 제거
|
|
144
|
-
if (fixable.some((i) => i.type === "useless_jsdoc")) {
|
|
145
|
-
const matches = fixed.match(/\/\*\*\s*\n\s*\*\s*\n\s*\*\/\n?/g);
|
|
146
|
-
if (matches) {
|
|
147
|
-
fixed = fixed.replace(/\/\*\*\s*\n\s*\*\s*\n\s*\*\/\n?/g, "");
|
|
148
|
-
applied += matches.length;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Line-level: trivial_comment, console_debug, commented_code 제거
|
|
153
|
-
const lineTypes = new Set(
|
|
154
|
-
fixable
|
|
155
|
-
.filter((i) =>
|
|
156
|
-
["trivial_comment", "console_debug", "commented_code"].includes(i.type),
|
|
157
|
-
)
|
|
158
|
-
.map((i) => i.type),
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
if (lineTypes.size > 0) {
|
|
162
|
-
const linePatterns = SLOP_PATTERNS.filter((p) => lineTypes.has(p.type));
|
|
163
|
-
const lines = fixed.split("\n");
|
|
164
|
-
const result = [];
|
|
165
|
-
for (const line of lines) {
|
|
166
|
-
let remove = false;
|
|
167
|
-
for (const p of linePatterns) {
|
|
168
|
-
if (p.pattern.test(line)) {
|
|
169
|
-
remove = true;
|
|
170
|
-
applied++;
|
|
171
|
-
break;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
if (!remove) result.push(line);
|
|
175
|
-
}
|
|
176
|
-
fixed = result.join("\n");
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return { fixed, applied, skipped };
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function matchesGlob(filePath, pattern) {
|
|
183
|
-
const normalized = "/" + filePath.replace(/\\/g, "/");
|
|
184
|
-
|
|
185
|
-
// **/*.ext → 확장자 매칭
|
|
186
|
-
if (pattern.startsWith("**/*.")) {
|
|
187
|
-
const ext = pattern.slice(4);
|
|
188
|
-
return normalized.endsWith(ext);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// *.ext → 확장자 매칭 (디렉토리 무관)
|
|
192
|
-
if (pattern.startsWith("*.") && !pattern.includes("/")) {
|
|
193
|
-
const ext = pattern.slice(1);
|
|
194
|
-
return normalized.endsWith(ext);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// **/dir/** → 디렉토리 포함 여부
|
|
198
|
-
const dirMatch = pattern.match(/^\*\*\/([^*]+)\/\*\*$/);
|
|
199
|
-
if (dirMatch) {
|
|
200
|
-
return normalized.includes("/" + dirMatch[1] + "/");
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return false;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* 디렉토리 전체 스캔
|
|
208
|
-
* @param {string} dirPath - 스캔 대상 디렉토리
|
|
209
|
-
* @param {object} [opts]
|
|
210
|
-
* @param {string[]} [opts.include] - 포함할 glob 패턴
|
|
211
|
-
* @param {string[]} [opts.exclude] - 제외할 glob 패턴
|
|
212
|
-
* @param {boolean} [opts.autoFix] - 자동 수정 여부
|
|
213
|
-
* @returns {Promise<{ files: Array<{path: string, issues: Array, score: number}>, summary: object }>}
|
|
214
|
-
*/
|
|
215
|
-
export async function scanDirectory(dirPath, opts = {}) {
|
|
216
|
-
const {
|
|
217
|
-
include = ["**/*.mjs", "**/*.js", "**/*.ts"],
|
|
218
|
-
exclude = ["**/node_modules/**", "**/dist/**", "**/.git/**"],
|
|
219
|
-
autoFix = false,
|
|
220
|
-
} = opts;
|
|
221
|
-
|
|
222
|
-
const entries = await readdir(dirPath, { recursive: true });
|
|
223
|
-
const files = [];
|
|
224
|
-
|
|
225
|
-
for (const entry of entries) {
|
|
226
|
-
const normalized = entry.replace(/\\/g, "/");
|
|
227
|
-
const fullPath = join(dirPath, entry);
|
|
228
|
-
|
|
229
|
-
let st;
|
|
230
|
-
try {
|
|
231
|
-
st = await stat(fullPath);
|
|
232
|
-
} catch {
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
if (!st.isFile()) continue;
|
|
236
|
-
|
|
237
|
-
const included = include.some((p) => matchesGlob(normalized, p));
|
|
238
|
-
const excluded = exclude.some((p) => matchesGlob(normalized, p));
|
|
239
|
-
if (!included || excluded) continue;
|
|
240
|
-
|
|
241
|
-
const fileContent = await readFile(fullPath, "utf-8");
|
|
242
|
-
const { issues, score } = detectSlop(fileContent, normalized);
|
|
243
|
-
|
|
244
|
-
if (autoFix && issues.length > 0) {
|
|
245
|
-
const { fixed, applied } = autoFixSlop(fileContent, issues);
|
|
246
|
-
if (applied > 0) await writeFile(fullPath, fixed, "utf-8");
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
files.push({ path: normalized, issues, score });
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const totalIssues = files.reduce((sum, f) => sum + f.issues.length, 0);
|
|
253
|
-
const avgScore =
|
|
254
|
-
files.length > 0
|
|
255
|
-
? Math.round(files.reduce((sum, f) => sum + f.score, 0) / files.length)
|
|
256
|
-
: 100;
|
|
257
|
-
|
|
258
|
-
const byType = {};
|
|
259
|
-
for (const f of files) {
|
|
260
|
-
for (const issue of f.issues) {
|
|
261
|
-
byType[issue.type] = (byType[issue.type] || 0) + 1;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
return {
|
|
266
|
-
files,
|
|
267
|
-
summary: {
|
|
268
|
-
totalFiles: files.length,
|
|
269
|
-
totalIssues,
|
|
270
|
-
averageScore: avgScore,
|
|
271
|
-
byType,
|
|
272
|
-
},
|
|
273
|
-
};
|
|
274
|
-
}
|
package/hub/research.mjs
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
// hub/research.mjs — 자율 웹 리서치 엔진 코어
|
|
2
|
-
// 검색 쿼리 생성 → 결과 정규화 → 보고서 빌드 → 저장
|
|
3
|
-
|
|
4
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { join, resolve } from "node:path";
|
|
6
|
-
import { TFX_REPORTS_DIR } from "./paths.mjs";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* @experimental 런타임 미연결 — 향후 통합 예정
|
|
10
|
-
* @experimental
|
|
11
|
-
* 주제에서 검색 쿼리 3-5개를 자동 생성한다.
|
|
12
|
-
* 한국어 주제 → 한국어 + 영어 혼합, 영어 주제 → 영어 쿼리.
|
|
13
|
-
* @param {string} topic - 리서치 주제
|
|
14
|
-
* @param {'ko'|'en'|'auto'} [lang='auto'] - 언어 힌트
|
|
15
|
-
* @returns {string[]} 검색 쿼리 배열
|
|
16
|
-
*/
|
|
17
|
-
export function generateQueries(topic, lang = "auto") {
|
|
18
|
-
if (!topic || typeof topic !== "string" || !topic.trim()) return [];
|
|
19
|
-
|
|
20
|
-
const t = topic.trim();
|
|
21
|
-
const detectedLang = lang === "auto" ? detectLang(t) : lang;
|
|
22
|
-
|
|
23
|
-
if (detectedLang === "ko") {
|
|
24
|
-
return [
|
|
25
|
-
`${t} 정리`,
|
|
26
|
-
`${t} 비교 분석`,
|
|
27
|
-
`${t} 최신 동향 ${new Date().getFullYear()}`,
|
|
28
|
-
`${toEnglishQuery(t)} overview`,
|
|
29
|
-
`${toEnglishQuery(t)} comparison ${new Date().getFullYear()}`,
|
|
30
|
-
];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return [
|
|
34
|
-
`${t} overview`,
|
|
35
|
-
`${t} comparison`,
|
|
36
|
-
`${t} best practices ${new Date().getFullYear()}`,
|
|
37
|
-
`${t} pros and cons`,
|
|
38
|
-
];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* 검색 원시 결과를 정규화한다. 중복 URL 제거 + 빈/null 필터링.
|
|
43
|
-
* @param {Array<object|null|undefined>} rawResults - 검색 엔진 원시 결과
|
|
44
|
-
* @returns {Array<{title: string, url: string, snippet: string}>}
|
|
45
|
-
*/
|
|
46
|
-
export function normalizeResults(rawResults) {
|
|
47
|
-
if (!Array.isArray(rawResults)) return [];
|
|
48
|
-
|
|
49
|
-
const seen = new Set();
|
|
50
|
-
const out = [];
|
|
51
|
-
|
|
52
|
-
for (const r of rawResults) {
|
|
53
|
-
if (!r || typeof r !== "object") continue;
|
|
54
|
-
const url = (r.url || r.link || "").trim();
|
|
55
|
-
const title = (r.title || r.name || "").trim();
|
|
56
|
-
const snippet = (r.snippet || r.description || r.content || "").trim();
|
|
57
|
-
|
|
58
|
-
if (!url || seen.has(url)) continue;
|
|
59
|
-
seen.add(url);
|
|
60
|
-
out.push({ title, url, snippet });
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return out;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* 리서치 보고서를 마크다운으로 빌드한다.
|
|
68
|
-
* @param {string} topic - 리서치 주제
|
|
69
|
-
* @param {string[]} findings - 핵심 발견 목록
|
|
70
|
-
* @param {Array<{title: string, url: string, snippet: string}>} sources - 출처 목록
|
|
71
|
-
* @returns {string} 마크다운 문자열
|
|
72
|
-
*/
|
|
73
|
-
export function buildReport(topic, findings, sources) {
|
|
74
|
-
const date = new Date().toISOString().split("T")[0];
|
|
75
|
-
const findingsSection = (findings || [])
|
|
76
|
-
.map((f, i) => `${i + 1}. ${f}`)
|
|
77
|
-
.join("\n");
|
|
78
|
-
const sourcesSection = (sources || [])
|
|
79
|
-
.map(
|
|
80
|
-
(s) =>
|
|
81
|
-
`- [${s.title || s.url}](${s.url})${s.snippet ? ` — ${s.snippet}` : ""}`,
|
|
82
|
-
)
|
|
83
|
-
.join("\n");
|
|
84
|
-
|
|
85
|
-
return `# Research: ${topic}
|
|
86
|
-
Date: ${date}
|
|
87
|
-
|
|
88
|
-
## Executive Summary
|
|
89
|
-
${topic}에 대한 자동 리서치 결과입니다.
|
|
90
|
-
|
|
91
|
-
## Key Findings
|
|
92
|
-
${findingsSection || "_발견 없음_"}
|
|
93
|
-
|
|
94
|
-
## Actionable Recommendations
|
|
95
|
-
리서치 결과를 바탕으로 다음 단계를 검토하세요.
|
|
96
|
-
|
|
97
|
-
## Sources
|
|
98
|
-
${sourcesSection || "_출처 없음_"}
|
|
99
|
-
`;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* 보고서를 .tfx/reports/research-{timestamp}.md에 저장한다.
|
|
104
|
-
* @param {string} topic - 리서치 주제 (파일명 생성용)
|
|
105
|
-
* @param {string} content - 마크다운 보고서 내용
|
|
106
|
-
* @param {string} [baseDir=process.cwd()] - 프로젝트 루트 경로
|
|
107
|
-
* @returns {string} 저장된 파일 경로
|
|
108
|
-
*/
|
|
109
|
-
export function saveReport(topic, content, baseDir = process.cwd()) {
|
|
110
|
-
const dir = join(baseDir, TFX_REPORTS_DIR);
|
|
111
|
-
const resolvedDir = resolve(dir);
|
|
112
|
-
const expectedBase = resolve(baseDir || TFX_REPORTS_DIR);
|
|
113
|
-
if (!resolvedDir.startsWith(expectedBase)) {
|
|
114
|
-
throw new Error("Invalid report directory: path traversal detected");
|
|
115
|
-
}
|
|
116
|
-
mkdirSync(dir, { recursive: true });
|
|
117
|
-
|
|
118
|
-
const ts = new Date()
|
|
119
|
-
.toISOString()
|
|
120
|
-
.replace(/[:.]/g, "-")
|
|
121
|
-
.replace("T", "_")
|
|
122
|
-
.slice(0, 19);
|
|
123
|
-
const slug = (topic || "untitled")
|
|
124
|
-
.replace(/[^a-zA-Z0-9가-힣\s-]/g, "")
|
|
125
|
-
.replace(/\s+/g, "-")
|
|
126
|
-
.slice(0, 40)
|
|
127
|
-
.toLowerCase();
|
|
128
|
-
const filename = `research-${ts}-${slug}.md`;
|
|
129
|
-
const filepath = join(dir, filename);
|
|
130
|
-
|
|
131
|
-
writeFileSync(filepath, content, "utf-8");
|
|
132
|
-
return filepath;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ── internal helpers ──
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* 텍스트에 한글이 포함되어 있으면 'ko', 아니면 'en'
|
|
139
|
-
* @param {string} text
|
|
140
|
-
* @returns {'ko'|'en'}
|
|
141
|
-
*/
|
|
142
|
-
function detectLang(text) {
|
|
143
|
-
return /[가-힣]/.test(text) ? "ko" : "en";
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* 한국어 토픽에서 영어 검색 쿼리용 문자열 추출.
|
|
148
|
-
* 영문/숫자만 남기고, 없으면 원문 그대로 반환.
|
|
149
|
-
* @param {string} text
|
|
150
|
-
* @returns {string}
|
|
151
|
-
*/
|
|
152
|
-
function toEnglishQuery(text) {
|
|
153
|
-
const eng = text.replace(/[가-힣\s]+/g, " ").trim();
|
|
154
|
-
return eng || text;
|
|
155
|
-
}
|