@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.
Files changed (90) hide show
  1. package/hooks/agy-session-hook.mjs +11 -56
  2. package/hooks/codex-session-hook.mjs +11 -101
  3. package/hub/bridge.mjs +1 -79
  4. package/hub/cli-adapter-base.mjs +12 -90
  5. package/hub/codex-adapter.mjs +0 -1
  6. package/hub/codex-preflight.mjs +5 -28
  7. package/hub/index.mjs +3 -9
  8. package/hub/lib/cto-env.mjs +0 -23
  9. package/hub/lib/known-errors.json +2 -2
  10. package/hub/lib/process-utils.mjs +0 -123
  11. package/hub/lib/spawn-trace.mjs +3 -2
  12. package/hub/lib/timeout-defaults.mjs +0 -1
  13. package/hub/paths.mjs +0 -6
  14. package/hub/pipeline/index.mjs +0 -190
  15. package/hub/pipeline/transitions.mjs +0 -10
  16. package/hub/reflexion.mjs +1 -84
  17. package/hub/role-contract.mjs +0 -2
  18. package/hud/cli-policy.mjs +0 -26
  19. package/hud/colors.mjs +0 -13
  20. package/hud/constants.mjs +7 -153
  21. package/hud/context-monitor.mjs +3 -64
  22. package/hud/hud-qos-status.mjs +54 -264
  23. package/hud/providers/claude.mjs +4 -16
  24. package/hud/providers/gemini.mjs +83 -689
  25. package/hud/renderers.mjs +59 -367
  26. package/hud/terminal.mjs +1 -63
  27. package/hud/utils.mjs +2 -26
  28. package/package.json +1 -4
  29. package/scripts/lib/agent-route-policy.mjs +162 -14
  30. package/scripts/lib/claudemd-scanner.mjs +2 -199
  31. package/scripts/lib/cli-claude.mjs +0 -2
  32. package/scripts/lib/context.mjs +3 -19
  33. package/scripts/lib/env-probe.mjs +5 -64
  34. package/scripts/lib/gemini-profiles.mjs +2 -69
  35. package/scripts/lib/legacy-mcp-cleanup.mjs +14 -3
  36. package/scripts/lib/machine-profile.mjs +3 -17
  37. package/scripts/lib/mcp-filter.mjs +109 -53
  38. package/scripts/lib/mcp-health.mjs +2 -2
  39. package/scripts/lib/mcp-server-catalog.mjs +2 -2
  40. package/scripts/lib/psmux-info.mjs +25 -104
  41. package/scripts/lib/session-presence.mjs +1 -1
  42. package/scripts/lib/skill-template.mjs +0 -271
  43. package/hub/codex-compat.mjs +0 -12
  44. package/hub/dynamic-routing-engine.mjs +0 -511
  45. package/hub/fullcycle.mjs +0 -113
  46. package/hub/gemini-adapter.mjs +0 -174
  47. package/hub/intent.mjs +0 -321
  48. package/hub/lib/mcp-response-cache.mjs +0 -239
  49. package/hub/lib/path-utils.mjs +0 -165
  50. package/hub/lib/risk-tier.mjs +0 -53
  51. package/hub/lib/ssh-command.mjs +0 -231
  52. package/hub/lib/staged-file-detect.mjs +0 -43
  53. package/hub/lib/tfx-route-args.mjs +0 -353
  54. package/hub/lib/timeline-adapter.mjs +0 -190
  55. package/hub/pipeline/gates/confidence.mjs +0 -75
  56. package/hub/pipeline/gates/consensus.mjs +0 -106
  57. package/hub/pipeline/gates/index.mjs +0 -9
  58. package/hub/pipeline/gates/selfcheck.mjs +0 -120
  59. package/hub/quality/deslop.mjs +0 -274
  60. package/hub/research.mjs +0 -155
  61. package/hub/role-control-events.mjs +0 -113
  62. package/hub/routing/complexity.mjs +0 -269
  63. package/hub/routing/index.mjs +0 -122
  64. package/hub/routing/q-learning.mjs +0 -387
  65. package/hub/routing-snapshot.mjs +0 -261
  66. package/hub/token-mode.mjs +0 -228
  67. package/hud/mission-board.mjs +0 -56
  68. package/hud/providers/cto.mjs +0 -76
  69. package/mesh/index.mjs +0 -69
  70. package/mesh/mesh-budget.mjs +0 -137
  71. package/mesh/mesh-heartbeat.mjs +0 -100
  72. package/mesh/mesh-protocol.mjs +0 -96
  73. package/mesh/mesh-queue.mjs +0 -163
  74. package/mesh/mesh-registry.mjs +0 -78
  75. package/mesh/mesh-router.mjs +0 -77
  76. package/scripts/lib/agent-json.mjs +0 -27
  77. package/scripts/lib/async.mjs +0 -174
  78. package/scripts/lib/cli-agy.mjs +0 -85
  79. package/scripts/lib/cli-codex.mjs +0 -62
  80. package/scripts/lib/cli-gemini.mjs +0 -37
  81. package/scripts/lib/dynamic-route-cli.mjs +0 -107
  82. package/scripts/lib/env.mjs +0 -14
  83. package/scripts/lib/hub.mjs +0 -48
  84. package/scripts/lib/pid.mjs +0 -63
  85. package/scripts/lib/quota.mjs +0 -47
  86. package/scripts/lib/skill-state.mjs +0 -220
  87. package/scripts/lib/team.mjs +0 -75
  88. package/scripts/lib/timeout.mjs +0 -66
  89. package/scripts/lib/tmp.mjs +0 -26
  90. 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
- }
@@ -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
- }