@su-record/vibe 2.12.4 → 2.13.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/CLAUDE.md +10 -3
- package/README.en.md +11 -11
- package/README.md +7 -7
- package/dist/cli/commands/init.d.ts +8 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +24 -1
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/postinstall/claude-agents.d.ts +11 -1
- package/dist/cli/postinstall/claude-agents.d.ts.map +1 -1
- package/dist/cli/postinstall/claude-agents.js +22 -2
- package/dist/cli/postinstall/claude-agents.js.map +1 -1
- package/dist/cli/postinstall/constants.d.ts +18 -0
- package/dist/cli/postinstall/constants.d.ts.map +1 -1
- package/dist/cli/postinstall/constants.js +50 -0
- package/dist/cli/postinstall/constants.js.map +1 -1
- package/dist/cli/postinstall/fs-utils.d.ts +23 -0
- package/dist/cli/postinstall/fs-utils.d.ts.map +1 -1
- package/dist/cli/postinstall/fs-utils.js +71 -0
- package/dist/cli/postinstall/fs-utils.js.map +1 -1
- package/dist/cli/postinstall/fs-utils.test.js +69 -1
- package/dist/cli/postinstall/fs-utils.test.js.map +1 -1
- package/dist/cli/postinstall/index.d.ts +1 -1
- package/dist/cli/postinstall/index.d.ts.map +1 -1
- package/dist/cli/postinstall/index.js +1 -1
- package/dist/cli/postinstall/index.js.map +1 -1
- package/dist/cli/postinstall/main.d.ts.map +1 -1
- package/dist/cli/postinstall/main.js +15 -4
- package/dist/cli/postinstall/main.js.map +1 -1
- package/dist/cli/postinstall.d.ts +1 -1
- package/dist/cli/postinstall.d.ts.map +1 -1
- package/dist/cli/postinstall.js +1 -1
- package/dist/cli/postinstall.js.map +1 -1
- package/dist/cli/setup/CodexHooks.test.js +27 -0
- package/dist/cli/setup/CodexHooks.test.js.map +1 -1
- package/dist/cli/setup/ProjectSetup.js +2 -2
- package/dist/cli/setup/ProjectSetup.js.map +1 -1
- package/dist/infra/lib/ContextCompressor.d.ts +11 -2
- package/dist/infra/lib/ContextCompressor.d.ts.map +1 -1
- package/dist/infra/lib/ContextCompressor.js +26 -41
- package/dist/infra/lib/ContextCompressor.js.map +1 -1
- package/dist/infra/lib/ContextCompressor.test.d.ts +2 -0
- package/dist/infra/lib/ContextCompressor.test.d.ts.map +1 -0
- package/dist/infra/lib/ContextCompressor.test.js +25 -0
- package/dist/infra/lib/ContextCompressor.test.js.map +1 -0
- package/dist/infra/lib/DecisionTracer.d.ts +4 -0
- package/dist/infra/lib/DecisionTracer.d.ts.map +1 -1
- package/dist/infra/lib/DecisionTracer.js +4 -0
- package/dist/infra/lib/DecisionTracer.js.map +1 -1
- package/dist/infra/lib/LoopBreaker.d.ts +4 -0
- package/dist/infra/lib/LoopBreaker.d.ts.map +1 -1
- package/dist/infra/lib/LoopBreaker.js +4 -0
- package/dist/infra/lib/LoopBreaker.js.map +1 -1
- package/dist/infra/lib/ReviewRace.d.ts +4 -0
- package/dist/infra/lib/ReviewRace.d.ts.map +1 -1
- package/dist/infra/lib/ReviewRace.js +4 -0
- package/dist/infra/lib/ReviewRace.js.map +1 -1
- package/dist/infra/lib/SkillQualityGate.d.ts +4 -0
- package/dist/infra/lib/SkillQualityGate.d.ts.map +1 -1
- package/dist/infra/lib/SkillQualityGate.js +4 -0
- package/dist/infra/lib/SkillQualityGate.js.map +1 -1
- package/dist/infra/lib/UltraQA.d.ts +4 -0
- package/dist/infra/lib/UltraQA.d.ts.map +1 -1
- package/dist/infra/lib/UltraQA.js +4 -0
- package/dist/infra/lib/UltraQA.js.map +1 -1
- package/dist/infra/lib/VerificationLoop.d.ts +4 -0
- package/dist/infra/lib/VerificationLoop.d.ts.map +1 -1
- package/dist/infra/lib/VerificationLoop.js +4 -0
- package/dist/infra/lib/VerificationLoop.js.map +1 -1
- package/dist/infra/lib/embedding/VectorStore.d.ts +9 -2
- package/dist/infra/lib/embedding/VectorStore.d.ts.map +1 -1
- package/dist/infra/lib/embedding/VectorStore.js +42 -19
- package/dist/infra/lib/embedding/VectorStore.js.map +1 -1
- package/dist/infra/lib/memory/MemoryStorage.d.ts +12 -0
- package/dist/infra/lib/memory/MemoryStorage.d.ts.map +1 -1
- package/dist/infra/lib/memory/MemoryStorage.js +57 -0
- package/dist/infra/lib/memory/MemoryStorage.js.map +1 -1
- package/dist/infra/lib/memory/ReflectionStore.d.ts.map +1 -1
- package/dist/infra/lib/memory/ReflectionStore.js +8 -27
- package/dist/infra/lib/memory/ReflectionStore.js.map +1 -1
- package/dist/infra/orchestrator/LLMCluster.d.ts +4 -0
- package/dist/infra/orchestrator/LLMCluster.d.ts.map +1 -1
- package/dist/infra/orchestrator/LLMCluster.js +35 -8
- package/dist/infra/orchestrator/LLMCluster.js.map +1 -1
- package/dist/infra/orchestrator/index.d.ts.map +1 -1
- package/dist/infra/orchestrator/index.js +1 -3
- package/dist/infra/orchestrator/index.js.map +1 -1
- package/dist/infra/orchestrator/parallelResearch.d.ts.map +1 -1
- package/dist/infra/orchestrator/parallelResearch.js +1 -4
- package/dist/infra/orchestrator/parallelResearch.js.map +1 -1
- package/dist/tools/convention/validateCodeQuality.d.ts.map +1 -1
- package/dist/tools/convention/validateCodeQuality.js +5 -4
- package/dist/tools/convention/validateCodeQuality.js.map +1 -1
- package/dist/tools/spec/traceabilityMatrix.d.ts +2 -0
- package/dist/tools/spec/traceabilityMatrix.d.ts.map +1 -1
- package/dist/tools/spec/traceabilityMatrix.js +50 -1
- package/dist/tools/spec/traceabilityMatrix.js.map +1 -1
- package/dist/tools/spec/traceabilityMatrix.path-resolution.test.d.ts +10 -0
- package/dist/tools/spec/traceabilityMatrix.path-resolution.test.d.ts.map +1 -0
- package/dist/tools/spec/traceabilityMatrix.path-resolution.test.js +89 -0
- package/dist/tools/spec/traceabilityMatrix.path-resolution.test.js.map +1 -0
- package/dist/tools/spec/traceabilityMatrix.test.js +19 -0
- package/dist/tools/spec/traceabilityMatrix.test.js.map +1 -1
- package/hooks/hooks.json +3 -9
- package/hooks/scripts/__tests__/.vibe/command-log.txt +39 -0
- package/hooks/scripts/__tests__/.vibe/memories/memories.db +0 -0
- package/hooks/scripts/__tests__/.vibe/memories/memories.db-shm +0 -0
- package/hooks/scripts/__tests__/.vibe/memories/memories.db-wal +0 -0
- package/hooks/scripts/__tests__/auto-test-debounce.test.js +145 -0
- package/hooks/scripts/__tests__/code-check-detectors.test.js +155 -0
- package/hooks/scripts/__tests__/dispatcher-inprocess.test.js +99 -0
- package/hooks/scripts/__tests__/post-edit-dispatcher.test.js +139 -0
- package/hooks/scripts/__tests__/pre-tool-guard.test.js +115 -1
- package/hooks/scripts/__tests__/run-ledger-verify-required.test.js +146 -0
- package/hooks/scripts/__tests__/run-ledger.test.js +330 -0
- package/hooks/scripts/__tests__/scope-from-spec.test.js +215 -0
- package/hooks/scripts/__tests__/sentinel-guard.test.js +79 -24
- package/hooks/scripts/__tests__/step-counter.test.js +95 -15
- package/hooks/scripts/__tests__/utils-npm-root.test.js +98 -0
- package/hooks/scripts/auto-commit.js +30 -11
- package/hooks/scripts/auto-format.js +96 -26
- package/hooks/scripts/auto-test.js +187 -37
- package/hooks/scripts/code-check.js +292 -99
- package/hooks/scripts/codex-hook-adapter.js +12 -1
- package/hooks/scripts/command-log.js +26 -16
- package/hooks/scripts/lib/dispatcher.js +38 -0
- package/hooks/scripts/lib/hook-context.js +101 -0
- package/hooks/scripts/lib/pr-gate-runner.js +62 -0
- package/hooks/scripts/lib/run-ledger.js +169 -0
- package/hooks/scripts/lib/scope-from-spec.js +40 -7
- package/hooks/scripts/post-edit-dispatcher.js +93 -20
- package/hooks/scripts/post-edit.js +40 -19
- package/hooks/scripts/pr-test-gate.js +8 -37
- package/hooks/scripts/pre-tool-dispatcher.js +18 -16
- package/hooks/scripts/pre-tool-guard.js +55 -52
- package/hooks/scripts/prompt-dispatcher.js +10 -0
- package/hooks/scripts/scope-guard.js +40 -39
- package/hooks/scripts/sentinel-guard.js +41 -41
- package/hooks/scripts/session-start.js +41 -16
- package/hooks/scripts/step-counter.js +100 -7
- package/hooks/scripts/stop-dispatcher.js +26 -0
- package/hooks/scripts/utils.js +96 -30
- package/hooks/scripts/verify-ledger.js +22 -0
- package/package.json +2 -2
- package/skills/arch-guard/SKILL.md +2 -2
- package/skills/characterization-test/SKILL.md +2 -2
- package/skills/exec-plan/SKILL.md +2 -2
- package/skills/spec/SKILL.md +6 -312
- package/skills/spec/references/askuser-examples.md +57 -0
- package/skills/spec/references/example-session.md +87 -0
- package/skills/spec/references/templates.md +194 -0
- package/skills/vibe.run/SKILL.md +145 -1682
- package/skills/vibe.run/references/brand-assets.md +59 -0
- package/skills/vibe.run/references/parallel-agents.md +326 -0
- package/skills/vibe.run/references/race-review.md +272 -0
- package/skills/vibe.run/references/ralph-loop.md +172 -0
- package/skills/vibe.run/references/ultrawork-mode.md +148 -0
- package/skills/vibe.trace/SKILL.md +25 -38
- package/skills/vibe.verify/SKILL.md +15 -0
- package/vibe/templates/claudemd-template.md +1 -1
- package/hooks/scripts/figma-guard.js +0 -219
|
@@ -1,27 +1,115 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PostToolUse Hook - Write/Edit 후 코드 품질 검사 + 관찰 자동 캡처
|
|
3
|
+
*
|
|
4
|
+
* findings를 console.log가 아닌 반환값으로 전달 — 디스패처가 수집해 additionalContext에 주입.
|
|
5
|
+
* P1 이슈 발견 시 run-ledger에 verifyRequired 상태 기록.
|
|
3
6
|
*/
|
|
4
|
-
import { getToolsBaseUrl, PROJECT_DIR } from './utils.js';
|
|
7
|
+
import { getToolsBaseUrl, PROJECT_DIR, readProjectConfig } from './utils.js';
|
|
5
8
|
import { readFileSync, existsSync, writeFileSync } from 'fs';
|
|
6
9
|
import path from 'path';
|
|
7
10
|
import os from 'os';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
process.on('unhandledRejection', () => {});
|
|
11
|
+
import { buildCliCtx, isDirectRun } from './lib/hook-context.js';
|
|
12
|
+
import { recordVerifyRequired } from './lib/run-ledger.js';
|
|
11
13
|
|
|
12
14
|
const BASE_URL = getToolsBaseUrl();
|
|
13
15
|
|
|
16
|
+
// P1 이슈 판단 기준: .ts/.tsx 파일에서만 적용
|
|
17
|
+
const P1_DETECTORS = [
|
|
18
|
+
// `: any` — 타입 어노테이션
|
|
19
|
+
/:\s*any\b/,
|
|
20
|
+
// `as any` — 타입 캐스트
|
|
21
|
+
/\bas\s+any\b/,
|
|
22
|
+
// `<any>` — 제네릭 any (단, JSX 태그 제외 목적으로 뒤에 공백/쉼표/> 허용)
|
|
23
|
+
/<any[\s,>]/,
|
|
24
|
+
// @ts-ignore
|
|
25
|
+
/@ts-ignore\b/,
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const TS_EXT_RE = /\.(ts|tsx)$/;
|
|
29
|
+
const CODE_EXT_RE = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
|
|
30
|
+
|
|
31
|
+
// console.log 기본 허용 경로 (glob 패턴 → 정규식으로 변환)
|
|
32
|
+
const DEFAULT_CONSOLE_ALLOW_GLOBS = [
|
|
33
|
+
'hooks/scripts/**',
|
|
34
|
+
'scripts/**',
|
|
35
|
+
'**/cli/**',
|
|
36
|
+
'**/*.test.*',
|
|
37
|
+
'**/*.spec.*',
|
|
38
|
+
'**/__tests__/**',
|
|
39
|
+
];
|
|
40
|
+
|
|
14
41
|
/**
|
|
15
|
-
*
|
|
42
|
+
* 경량 glob → RegExp 변환 (scope-guard와 동일 알고리즘, 독립 복사본).
|
|
43
|
+
* @param {string} glob
|
|
44
|
+
* @returns {RegExp}
|
|
16
45
|
*/
|
|
17
|
-
function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
46
|
+
function globToRegExp(glob) {
|
|
47
|
+
const normalized = glob.replace(/\\/g, '/');
|
|
48
|
+
let out = '';
|
|
49
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
50
|
+
const c = normalized[i];
|
|
51
|
+
if (c === '*') {
|
|
52
|
+
if (normalized[i + 1] === '*') {
|
|
53
|
+
out += '.*';
|
|
54
|
+
i++;
|
|
55
|
+
if (normalized[i + 1] === '/') i++;
|
|
56
|
+
} else {
|
|
57
|
+
out += '[^/]*';
|
|
58
|
+
}
|
|
59
|
+
} else if (c === '?') {
|
|
60
|
+
out += '[^/]';
|
|
61
|
+
} else if ('.+^$()|{}[]\\'.includes(c)) {
|
|
62
|
+
out += '\\' + c;
|
|
63
|
+
} else {
|
|
64
|
+
out += c;
|
|
24
65
|
}
|
|
66
|
+
}
|
|
67
|
+
return new RegExp('^' + out + '$');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* .vibe/config.json의 qualityCheck.consoleAllow 글로브 목록 로드.
|
|
72
|
+
* 기본 글로브와 병합하여 반환.
|
|
73
|
+
* @returns {RegExp[]}
|
|
74
|
+
*/
|
|
75
|
+
function loadConsoleAllowPatterns() {
|
|
76
|
+
try {
|
|
77
|
+
const cfg = readProjectConfig();
|
|
78
|
+
const extra = cfg?.qualityCheck?.consoleAllow;
|
|
79
|
+
const globs = Array.isArray(extra)
|
|
80
|
+
? [...DEFAULT_CONSOLE_ALLOW_GLOBS, ...extra]
|
|
81
|
+
: DEFAULT_CONSOLE_ALLOW_GLOBS;
|
|
82
|
+
return globs.map(g => globToRegExp(g));
|
|
83
|
+
} catch {
|
|
84
|
+
return DEFAULT_CONSOLE_ALLOW_GLOBS.map(g => globToRegExp(g));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 파일 경로가 console.log 허용 경로인지 판단.
|
|
90
|
+
* @param {string} filePath - 절대 또는 프로젝트 상대 경로
|
|
91
|
+
* @returns {boolean}
|
|
92
|
+
*/
|
|
93
|
+
function isConsoleAllowed(filePath) {
|
|
94
|
+
try {
|
|
95
|
+
const rel = path.relative(PROJECT_DIR, path.resolve(filePath)).replace(/\\/g, '/');
|
|
96
|
+
const patterns = loadConsoleAllowPatterns();
|
|
97
|
+
return patterns.some(re => re.test(rel));
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* hook input에서 수정된 파일 경로 추출.
|
|
105
|
+
* @param {object} ctx
|
|
106
|
+
* @returns {string[]}
|
|
107
|
+
*/
|
|
108
|
+
function getModifiedFiles(ctx) {
|
|
109
|
+
try {
|
|
110
|
+
const parsed = ctx.payload || (ctx.hookInput ? JSON.parse(ctx.hookInput) : null);
|
|
111
|
+
const filePath = parsed?.tool_input?.file_path || parsed?.tool_input?.path;
|
|
112
|
+
return filePath ? [filePath] : [];
|
|
25
113
|
} catch {
|
|
26
114
|
// ignore
|
|
27
115
|
}
|
|
@@ -30,6 +118,8 @@ function getModifiedFiles() {
|
|
|
30
118
|
|
|
31
119
|
/**
|
|
32
120
|
* 파일 확장자/경로로 관찰 타입 분류
|
|
121
|
+
* @param {string[]} files
|
|
122
|
+
* @returns {{ type: string, title: string }}
|
|
33
123
|
*/
|
|
34
124
|
function classifyObservation(files) {
|
|
35
125
|
const hasTest = files.some(f => /\.(test|spec)\.[jt]sx?$/.test(f) || /\/__tests__\//.test(f));
|
|
@@ -41,16 +131,44 @@ function classifyObservation(files) {
|
|
|
41
131
|
}
|
|
42
132
|
|
|
43
133
|
/**
|
|
44
|
-
*
|
|
134
|
+
* P1: any 타입 탐지 — .ts/.tsx 전용, 단어 경계 기반.
|
|
135
|
+
* @param {string[]} lines
|
|
136
|
+
* @returns {Array<{ line: number, match: string, severity: 'P1' }>}
|
|
45
137
|
*/
|
|
46
138
|
function detectAnyType(lines) {
|
|
47
139
|
const findings = [];
|
|
48
140
|
lines.forEach((line, i) => {
|
|
49
|
-
|
|
141
|
+
for (const re of P1_DETECTORS) {
|
|
142
|
+
if (re.test(line)) {
|
|
143
|
+
findings.push({
|
|
144
|
+
line: i + 1,
|
|
145
|
+
match: line.trim(),
|
|
146
|
+
severity: 'P1',
|
|
147
|
+
suggestion: "Replace with: unknown + type guard pattern: if (typeof x === 'string') { ... }",
|
|
148
|
+
});
|
|
149
|
+
break; // 한 줄에 여러 패턴이 있어도 중복 발견 방지
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
return findings;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* P1: console.log 탐지 — 허용 경로가 아닌 곳의 src/ 코드.
|
|
158
|
+
* @param {string[]} lines
|
|
159
|
+
* @param {string} filePath
|
|
160
|
+
* @returns {Array<{ line: number, match: string, severity: 'P1' }>}
|
|
161
|
+
*/
|
|
162
|
+
function detectConsoleLogs(lines, filePath) {
|
|
163
|
+
if (isConsoleAllowed(filePath)) return [];
|
|
164
|
+
const findings = [];
|
|
165
|
+
lines.forEach((line, i) => {
|
|
166
|
+
if (/console\.log\(/.test(line)) {
|
|
50
167
|
findings.push({
|
|
51
168
|
line: i + 1,
|
|
52
169
|
match: line.trim(),
|
|
53
|
-
|
|
170
|
+
severity: 'P1',
|
|
171
|
+
suggestion: 'Remove or replace with debugLog utility',
|
|
54
172
|
});
|
|
55
173
|
}
|
|
56
174
|
});
|
|
@@ -58,7 +176,9 @@ function detectAnyType(lines) {
|
|
|
58
176
|
}
|
|
59
177
|
|
|
60
178
|
/**
|
|
61
|
-
*
|
|
179
|
+
* P2: 함수 길이 초과 탐지.
|
|
180
|
+
* @param {string[]} lines
|
|
181
|
+
* @returns {Array<{ line: number, match: string, severity: 'P2' }>}
|
|
62
182
|
*/
|
|
63
183
|
function detectLongFunctions(lines) {
|
|
64
184
|
const findings = [];
|
|
@@ -80,7 +200,8 @@ function detectLongFunctions(lines) {
|
|
|
80
200
|
findings.push({
|
|
81
201
|
line: fnStart + 1,
|
|
82
202
|
match: `function '${fnName}' is ${length} lines`,
|
|
83
|
-
|
|
203
|
+
severity: 'P2',
|
|
204
|
+
suggestion: `Extract lines ${fnStart + 20}–${i + 1} into a separate helper function`,
|
|
84
205
|
});
|
|
85
206
|
}
|
|
86
207
|
fnStart = -1;
|
|
@@ -91,7 +212,9 @@ function detectLongFunctions(lines) {
|
|
|
91
212
|
}
|
|
92
213
|
|
|
93
214
|
/**
|
|
94
|
-
*
|
|
215
|
+
* P2: 중첩 깊이 초과 탐지.
|
|
216
|
+
* @param {string[]} lines
|
|
217
|
+
* @returns {Array<{ line: number, match: string, severity: 'P2' }>}
|
|
95
218
|
*/
|
|
96
219
|
function detectDeepNesting(lines) {
|
|
97
220
|
const findings = [];
|
|
@@ -104,7 +227,8 @@ function detectDeepNesting(lines) {
|
|
|
104
227
|
findings.push({
|
|
105
228
|
line: i + 1,
|
|
106
229
|
match: `nesting depth ${depth} at line ${i + 1}`,
|
|
107
|
-
|
|
230
|
+
severity: 'P2',
|
|
231
|
+
suggestion: 'Use early return pattern: if (!condition) return; — instead of wrapping in else',
|
|
108
232
|
});
|
|
109
233
|
reported = true;
|
|
110
234
|
}
|
|
@@ -113,70 +237,50 @@ function detectDeepNesting(lines) {
|
|
|
113
237
|
return findings;
|
|
114
238
|
}
|
|
115
239
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
*/
|
|
119
|
-
function detectConsoleLogs(lines) {
|
|
120
|
-
const findings = [];
|
|
121
|
-
lines.forEach((line, i) => {
|
|
122
|
-
if (/console\.log\(/.test(line)) {
|
|
123
|
-
findings.push({
|
|
124
|
-
line: i + 1,
|
|
125
|
-
match: line.trim(),
|
|
126
|
-
suggestion: 'Remove or replace with debugLog utility'
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
return findings;
|
|
131
|
-
}
|
|
240
|
+
// magic number 무시 값 목록 (0, 1, -1, 2, 10, 100, 1000)
|
|
241
|
+
const MAGIC_NUMBER_IGNORE = new Set(['10', '100', '1000']);
|
|
132
242
|
|
|
133
243
|
/**
|
|
134
|
-
*
|
|
244
|
+
* advisory: 매직 넘버 탐지 (P3, 스팸 방지 적용).
|
|
245
|
+
* 무시 조건: 0/1/-1/2 (단일 digit), ALLCAPS const 선언, 테스트 파일, 배열 인덱스
|
|
246
|
+
* @param {string[]} lines
|
|
247
|
+
* @param {string} filePath
|
|
248
|
+
* @returns {Array<{ line: number, match: string, severity: 'P3' }>}
|
|
135
249
|
*/
|
|
136
|
-
function detectMagicNumbers(lines) {
|
|
250
|
+
function detectMagicNumbers(lines, filePath) {
|
|
251
|
+
// 테스트 파일은 스킵
|
|
252
|
+
if (/\.(test|spec)\.[jt]sx?$/.test(filePath) || /\/__tests__\//.test(filePath)) {
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
|
|
137
256
|
const findings = [];
|
|
138
257
|
lines.forEach((line, i) => {
|
|
258
|
+
// ALL_CAPS const 선언 줄은 스킵 (예: const LIMIT = 500)
|
|
259
|
+
if (/^\s*(?:export\s+)?const\s+[A-Z][A-Z0-9_]+\s*=/.test(line)) return;
|
|
260
|
+
|
|
139
261
|
const stripped = line.replace(/\/\/.*$/, '').replace(/(['"`]).*?\1/g, '""');
|
|
140
|
-
const nums = stripped.match(/\b\d{2,}\b/g) || [];
|
|
141
|
-
|
|
262
|
+
const nums = (stripped.match(/\b\d{2,}\b/g) || []).filter(n => MAGIC_NUMBER_IGNORE.has(n));
|
|
263
|
+
// 배열 인덱스: [NN] 패턴 제외
|
|
264
|
+
const nonIndexNums = (stripped.match(/\b\d{2,}\b/g) || []).filter(n => {
|
|
265
|
+
if (MAGIC_NUMBER_IGNORE.has(n)) return false;
|
|
266
|
+
// 배열 인덱스 [NN] 체크
|
|
267
|
+
const idx = stripped.indexOf(n);
|
|
268
|
+
if (idx > 0 && stripped[idx - 1] === '[') return false;
|
|
269
|
+
return true;
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
if (nonIndexNums.length > 0) {
|
|
142
273
|
findings.push({
|
|
143
274
|
line: i + 1,
|
|
144
|
-
match: `magic number(s): ${
|
|
145
|
-
|
|
275
|
+
match: `magic number(s): ${nonIndexNums.join(', ')}`,
|
|
276
|
+
severity: 'P3',
|
|
277
|
+
suggestion: `Extract to named constant: const LIMIT = ${nonIndexNums[0]};`,
|
|
146
278
|
});
|
|
147
279
|
}
|
|
148
280
|
});
|
|
149
281
|
return findings;
|
|
150
282
|
}
|
|
151
283
|
|
|
152
|
-
/**
|
|
153
|
-
* Run all self-heal detectors and emit [SELF-HEAL] messages
|
|
154
|
-
*/
|
|
155
|
-
function emitSelfHealMessages(filePath) {
|
|
156
|
-
let content;
|
|
157
|
-
try {
|
|
158
|
-
content = readFileSync(filePath, 'utf-8');
|
|
159
|
-
} catch {
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const lines = content.split('\n');
|
|
164
|
-
const detectors = [
|
|
165
|
-
{ label: 'any type detected', fn: detectAnyType },
|
|
166
|
-
{ label: 'function too long', fn: detectLongFunctions },
|
|
167
|
-
{ label: 'nesting too deep', fn: detectDeepNesting },
|
|
168
|
-
{ label: 'console.log found', fn: detectConsoleLogs },
|
|
169
|
-
{ label: 'magic number', fn: detectMagicNumbers },
|
|
170
|
-
];
|
|
171
|
-
|
|
172
|
-
for (const { label, fn } of detectors) {
|
|
173
|
-
const findings = fn(lines).slice(0, 2);
|
|
174
|
-
for (const f of findings) {
|
|
175
|
-
console.log(`[SELF-HEAL] ${label} at line ${f.line} → ${f.suggestion}`);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
284
|
// ─── Failure Escalation Tracking ───
|
|
181
285
|
|
|
182
286
|
const ESCALATION_THRESHOLD = 3;
|
|
@@ -197,6 +301,12 @@ function saveFailureTracker(tracker) {
|
|
|
197
301
|
} catch { /* ignore */ }
|
|
198
302
|
}
|
|
199
303
|
|
|
304
|
+
/**
|
|
305
|
+
* 실패 추적 — 동일 파일에 P1 이슈가 반복되면 에스컬레이션 메시지 반환.
|
|
306
|
+
* @param {string} filePath
|
|
307
|
+
* @param {string[]} issues
|
|
308
|
+
* @returns {string|null} 에스컬레이션 메시지 (없으면 null)
|
|
309
|
+
*/
|
|
200
310
|
function trackFailure(filePath, issues) {
|
|
201
311
|
const tracker = loadFailureTracker();
|
|
202
312
|
const key = filePath;
|
|
@@ -208,14 +318,12 @@ function trackFailure(filePath, issues) {
|
|
|
208
318
|
saveFailureTracker(tracker);
|
|
209
319
|
|
|
210
320
|
if (entry.count >= ESCALATION_THRESHOLD) {
|
|
211
|
-
|
|
212
|
-
console.log(` 이슈: ${entry.issues.join(' | ')}`);
|
|
213
|
-
console.log(' → 사용자 개입 필요 — 자동 수정이 수렴하지 않고 있습니다.');
|
|
214
|
-
console.log(' → 방향을 바꾸거나, 접근 방식을 재검토하세요.');
|
|
215
|
-
// 카운터 리셋 (다음 에스컬레이션까지 다시 3회)
|
|
321
|
+
const msg = `[ESCALATION] ${path.basename(filePath)}: 동일 이슈 ${entry.count}회 반복 — 수동 개입 필요`;
|
|
216
322
|
entry.count = 0;
|
|
217
323
|
saveFailureTracker(tracker);
|
|
324
|
+
return msg;
|
|
218
325
|
}
|
|
326
|
+
return null;
|
|
219
327
|
}
|
|
220
328
|
|
|
221
329
|
function clearFailure(filePath) {
|
|
@@ -224,36 +332,112 @@ function clearFailure(filePath) {
|
|
|
224
332
|
saveFailureTracker(tracker);
|
|
225
333
|
}
|
|
226
334
|
|
|
227
|
-
|
|
228
|
-
|
|
335
|
+
/**
|
|
336
|
+
* 모든 self-heal 탐지기 실행. findings 배열 반환.
|
|
337
|
+
* @param {string} filePath
|
|
338
|
+
* @returns {{ p1: string[], advisory: string[], escalation: string|null }}
|
|
339
|
+
*/
|
|
340
|
+
function runDetectors(filePath) {
|
|
341
|
+
let content;
|
|
229
342
|
try {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
|
|
343
|
+
content = readFileSync(filePath, 'utf-8');
|
|
344
|
+
} catch {
|
|
345
|
+
return { p1: [], advisory: [], escalation: null };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const lines = content.split('\n');
|
|
349
|
+
const isTs = TS_EXT_RE.test(filePath);
|
|
350
|
+
|
|
351
|
+
const p1Findings = [];
|
|
352
|
+
const advisoryFindings = [];
|
|
353
|
+
|
|
354
|
+
// P1: any 탐지 — TS 파일만
|
|
355
|
+
if (isTs) {
|
|
356
|
+
const anyHits = detectAnyType(lines).slice(0, 2);
|
|
357
|
+
for (const f of anyHits) {
|
|
358
|
+
p1Findings.push(`P1 any-type line ${f.line}: ${f.match.substring(0, 60)}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// P1: console.log — 허용 경로 제외
|
|
363
|
+
const consoleHits = detectConsoleLogs(lines, filePath).slice(0, 2);
|
|
364
|
+
for (const f of consoleHits) {
|
|
365
|
+
p1Findings.push(`P1 console.log line ${f.line}: ${f.match.substring(0, 60)}`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// P2: 함수 길이
|
|
369
|
+
const fnHits = detectLongFunctions(lines).slice(0, 1);
|
|
370
|
+
for (const f of fnHits) {
|
|
371
|
+
advisoryFindings.push(`P2 ${f.match}`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// P2: 중첩 깊이
|
|
375
|
+
const nestHits = detectDeepNesting(lines).slice(0, 1);
|
|
376
|
+
for (const f of nestHits) {
|
|
377
|
+
advisoryFindings.push(`P2 ${f.match}`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// P3: 매직 넘버 (advisory, 스팸 방지)
|
|
381
|
+
const magicHits = detectMagicNumbers(lines, filePath).slice(0, 1);
|
|
382
|
+
for (const f of magicHits) {
|
|
383
|
+
advisoryFindings.push(`P3 ${f.match}`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
let escalation = null;
|
|
387
|
+
if (p1Findings.length > 0) {
|
|
388
|
+
escalation = trackFailure(filePath, p1Findings);
|
|
389
|
+
} else {
|
|
390
|
+
clearFailure(filePath);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return { p1: p1Findings, advisory: advisoryFindings, escalation };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* in-process 진입점 — 품질 검사 + 관찰 캡처.
|
|
398
|
+
* findings 배열을 반환한다 (디스패처가 수집해 additionalContext에 주입).
|
|
399
|
+
* @param {{ payload: object|null, hookInput: string|null }} ctx
|
|
400
|
+
* @returns {Promise<{ exitCode: number, findings: string[] }>}
|
|
401
|
+
*/
|
|
402
|
+
export async function run(ctx) {
|
|
403
|
+
const findings = [];
|
|
404
|
+
const files = getModifiedFiles(ctx);
|
|
405
|
+
if (files.length === 0) return { exitCode: 0, findings };
|
|
406
|
+
|
|
407
|
+
// 1. Self-heal 탐지기 실행 (changed file only)
|
|
408
|
+
try {
|
|
409
|
+
const { p1, advisory, escalation } = runDetectors(files[0]);
|
|
410
|
+
|
|
411
|
+
for (const msg of p1) findings.push(msg);
|
|
412
|
+
for (const msg of advisory) findings.push(msg);
|
|
413
|
+
if (escalation) findings.push(escalation);
|
|
414
|
+
|
|
415
|
+
// P1 이슈 → run-ledger에 verifyRequired 기록
|
|
416
|
+
if (p1.length > 0) {
|
|
417
|
+
try {
|
|
418
|
+
recordVerifyRequired(PROJECT_DIR, p1[0]);
|
|
419
|
+
} catch { /* fail-open */ }
|
|
247
420
|
}
|
|
248
421
|
} catch {
|
|
249
|
-
//
|
|
422
|
+
// 탐지기 실패 → fail-open, 계속 진행
|
|
250
423
|
}
|
|
251
424
|
|
|
252
|
-
// 2.
|
|
425
|
+
// 2. validateCodeQuality 호출 (P1/P2 필터)
|
|
253
426
|
try {
|
|
254
|
-
const
|
|
255
|
-
|
|
427
|
+
const module = await import(`${BASE_URL}convention/index.js`);
|
|
428
|
+
const result = await module.validateCodeQuality({
|
|
429
|
+
targetPath: files[0],
|
|
430
|
+
projectPath: PROJECT_DIR,
|
|
431
|
+
});
|
|
432
|
+
const text = result.content[0].text;
|
|
433
|
+
const critical = text.split('\n').filter(l => /\b(error|critical|P1|P2)\b/i.test(l)).slice(0, 3);
|
|
434
|
+
for (const line of critical) findings.push(`[CODE CHECK] ${line}`);
|
|
435
|
+
} catch {
|
|
436
|
+
// Silently continue on check failure
|
|
437
|
+
}
|
|
256
438
|
|
|
439
|
+
// 3. 관찰 자동 캡처
|
|
440
|
+
try {
|
|
257
441
|
const memModule = await import(`${BASE_URL}memory/index.js`);
|
|
258
442
|
const { type, title } = classifyObservation(files);
|
|
259
443
|
|
|
@@ -264,8 +448,17 @@ async function main() {
|
|
|
264
448
|
projectPath: PROJECT_DIR,
|
|
265
449
|
});
|
|
266
450
|
} catch {
|
|
267
|
-
// 관찰 캡처 실패해도 무시
|
|
451
|
+
// 관찰 캡처 실패해도 무시
|
|
268
452
|
}
|
|
453
|
+
|
|
454
|
+
return { exitCode: 0, findings };
|
|
269
455
|
}
|
|
270
456
|
|
|
271
|
-
|
|
457
|
+
// standalone CLI 모드 (직접 실행 시 — 디스패처 없이)
|
|
458
|
+
if (isDirectRun(import.meta.url)) {
|
|
459
|
+
process.on('uncaughtException', () => {});
|
|
460
|
+
process.on('unhandledRejection', () => {});
|
|
461
|
+
const { exitCode, findings } = await run(buildCliCtx());
|
|
462
|
+
if (findings.length > 0) process.stdout.write(findings.join('\n') + '\n');
|
|
463
|
+
process.exit(exitCode);
|
|
464
|
+
}
|
|
@@ -98,7 +98,18 @@ function handleUserPromptSubmit() {
|
|
|
98
98
|
|
|
99
99
|
function handlePostToolUse() {
|
|
100
100
|
const result = runScript('post-edit-dispatcher.js');
|
|
101
|
-
|
|
101
|
+
const output = combinedOutput(result);
|
|
102
|
+
if (!output) return;
|
|
103
|
+
// 디스패처가 이미 JSON hookSpecificOutput을 출력한 경우 그대로 전달 (이중 래핑 방지).
|
|
104
|
+
// 그 외(plain text)는 Codex 어댑터 표준 방식으로 래핑.
|
|
105
|
+
try {
|
|
106
|
+
const parsed = JSON.parse(output);
|
|
107
|
+
if (parsed?.hookSpecificOutput) {
|
|
108
|
+
writeJson(parsed);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
} catch { /* not JSON — fall through to text wrap */ }
|
|
112
|
+
writeAdditionalContext(output);
|
|
102
113
|
}
|
|
103
114
|
|
|
104
115
|
function handleStop() {
|
|
@@ -7,26 +7,36 @@
|
|
|
7
7
|
import { appendFileSync, mkdirSync, existsSync } from 'fs';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import { PROJECT_DIR, projectVibeRoot } from './utils.js';
|
|
10
|
+
import { buildCliCtx, isDirectRun } from './lib/hook-context.js';
|
|
10
11
|
|
|
11
|
-
const LOG_DIR = projectVibeRoot(PROJECT_DIR);
|
|
12
|
-
const LOG_FILE = path.join(LOG_DIR, 'command-log.txt');
|
|
13
12
|
const MAX_CMD_LENGTH = 500;
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
/**
|
|
15
|
+
* in-process 진입점 — 로깅만 수행, 항상 0 반환 (차단하지 않음).
|
|
16
|
+
* @param {{ toolInput: string }} ctx
|
|
17
|
+
* @returns {Promise<number>}
|
|
18
|
+
*/
|
|
19
|
+
export async function run(ctx) {
|
|
20
|
+
try {
|
|
21
|
+
const input = JSON.parse(ctx.toolInput || '{}');
|
|
22
|
+
const command = input.command || '';
|
|
23
|
+
if (!command) return 0;
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
const timestamp = new Date().toISOString();
|
|
26
|
+
const truncated = command.length > MAX_CMD_LENGTH
|
|
27
|
+
? command.slice(0, MAX_CMD_LENGTH) + '...(truncated)'
|
|
28
|
+
: command;
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
const logDir = projectVibeRoot(PROJECT_DIR);
|
|
31
|
+
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
|
|
32
|
+
appendFileSync(path.join(logDir, 'command-log.txt'), `[${timestamp}] ${truncated}\n`);
|
|
33
|
+
} catch {
|
|
34
|
+
// Never block on logging failure
|
|
35
|
+
}
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
26
38
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// Never block on logging failure
|
|
39
|
+
// standalone CLI 모드
|
|
40
|
+
if (isDirectRun(import.meta.url)) {
|
|
41
|
+
process.exit(await run(buildCliCtx()));
|
|
31
42
|
}
|
|
32
|
-
process.exit(0);
|
|
@@ -20,6 +20,7 @@ import { spawn } from 'child_process';
|
|
|
20
20
|
import path from 'path';
|
|
21
21
|
import fs from 'fs';
|
|
22
22
|
import { fileURLToPath } from 'url';
|
|
23
|
+
import { readStdinSync, buildCtx } from './hook-context.js';
|
|
23
24
|
|
|
24
25
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
25
26
|
const SCRIPTS_DIR = path.resolve(__dirname, '..');
|
|
@@ -112,3 +113,40 @@ export async function dispatch(steps) {
|
|
|
112
113
|
process.exit(2);
|
|
113
114
|
}
|
|
114
115
|
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* in-process 디스패처 — 자식 spawn 없이 import된 run(ctx)들을 병렬 실행.
|
|
119
|
+
*
|
|
120
|
+
* spawn 대비:
|
|
121
|
+
* - 자식 node VM 기동(~20ms × N)과 stdin 재읽기/재파싱 제거
|
|
122
|
+
* - 크래시 격리는 step별 try/catch로 대체 (throw → exit 1 취급, fail-open)
|
|
123
|
+
* - step별 강제 timeout은 없음 — 무거운 작업(포매터/테스트러너)은 모두
|
|
124
|
+
* 자체 timeout을 가진 비동기 자식 프로세스라 디스패처가 행 걸리지 않는다
|
|
125
|
+
*
|
|
126
|
+
* deny 시맨틱 보존: denyOnExit2 step이 2를 반환하면 process.exit(2)로 상위 전파.
|
|
127
|
+
*
|
|
128
|
+
* @param {Array<{name: string, run: (ctx: object) => Promise<number>, denyOnExit2?: boolean}>} steps
|
|
129
|
+
* @param {{ argvToolName?: string }} [options]
|
|
130
|
+
*/
|
|
131
|
+
export async function dispatchInProcess(steps, { argvToolName = '' } = {}) {
|
|
132
|
+
const { raw, parsed } = readStdinSync();
|
|
133
|
+
const ctx = buildCtx({ rawInput: raw, payload: parsed, argvToolName });
|
|
134
|
+
const hookConfig = loadHookConfig();
|
|
135
|
+
|
|
136
|
+
const enabledSteps = steps.filter(s => isEnabled(hookConfig, s.name));
|
|
137
|
+
const results = await Promise.all(
|
|
138
|
+
enabledSteps.map(async (step) => {
|
|
139
|
+
try {
|
|
140
|
+
return { step, code: await step.run(ctx) };
|
|
141
|
+
} catch {
|
|
142
|
+
// 크래시 격리 — 실패 step은 exit 1 취급, 나머지 step과 디스패처는 계속
|
|
143
|
+
return { step, code: 1 };
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// 하나라도 deny(exit 2) 반환 → 상위에 전파
|
|
149
|
+
if (results.some(({ step, code }) => step.denyOnExit2 && code === 2)) {
|
|
150
|
+
process.exit(2);
|
|
151
|
+
}
|
|
152
|
+
}
|