@tuzi-ince/hi-loop 0.2.1 → 0.2.2
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/bin/hi-loop.js +25 -3
- package/docs/design.md +12 -1
- package/docs/spec.md +3 -1
- package/package.json +1 -1
- package/src/args.js +18 -5
- package/src/blast.js +42 -0
- package/src/build.js +74 -62
- package/src/check.js +169 -0
- package/src/checkpoint.js +88 -4
- package/src/checks.js +122 -0
- package/src/cli-options.js +13 -0
- package/src/gates.js +104 -0
- package/src/integrity.js +30 -1
- package/src/loop.js +21 -37
- package/src/metrics.js +140 -0
- package/src/outcome.js +76 -0
- package/src/report.js +73 -7
- package/src/resume.js +93 -0
- package/src/seal.js +85 -0
- package/src/stages.js +16 -3
- package/src/state.js +21 -39
- package/src/treekey.js +63 -0
- package/src/verdict.js +54 -0
- package/src/verify.js +6 -5
- package/src/wiring.js +74 -0
package/src/verify.js
CHANGED
|
@@ -40,12 +40,13 @@ export function parseVerdict(text) {
|
|
|
40
40
|
const m = String(text).match(/\{[\s\S]*"verdict"[\s\S]*\}/);
|
|
41
41
|
const obj = JSON.parse(m ? m[0] : text);
|
|
42
42
|
const verdict = obj.verdict === 'reject' ? 'reject' : 'pass';
|
|
43
|
-
|
|
43
|
+
// 여기만 verified: true 다 — 검증자가 실제로 판정문을 냈다는 뜻이다.
|
|
44
|
+
return { verdict, verified: true, reason: typeof obj.reason === 'string' ? obj.reason : '' };
|
|
44
45
|
} catch {
|
|
45
46
|
// 판정 형식을 못 읽으면 기각하지 않는다 — 검증자가 헛소리를 했다고 통과를
|
|
46
47
|
// 막으면, 잘못된 입력이 방어를 "더 약하게"가 아니라 "더 세게" 만드는 게 아니라
|
|
47
48
|
// 반대가 된다. Tier 2 는 하향 전용이고 확신 없으면 통과가 원칙이다.
|
|
48
|
-
return { verdict: 'pass', reason: '검증자 응답을 파싱하지 못해 통과로 처리(하향 전용)' };
|
|
49
|
+
return { verdict: 'pass', verified: false, reason: '검증자 응답을 파싱하지 못해 통과로 처리(하향 전용)' };
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
|
|
@@ -63,7 +64,7 @@ export function makeSpecVerifier({
|
|
|
63
64
|
const specFull = specPath ? join(cwd, specPath) : null;
|
|
64
65
|
if (!specFull || !existsSync(specFull)) {
|
|
65
66
|
// 스펙이 없으면 대조할 오라클이 없다 → 기각할 근거 없음 → 통과.
|
|
66
|
-
resolve({ verdict: 'pass', reason: '스펙 파일이 없어 검증 생략' });
|
|
67
|
+
resolve({ verdict: 'pass', verified: false, reason: '스펙 파일이 없어 검증 생략' });
|
|
67
68
|
return;
|
|
68
69
|
}
|
|
69
70
|
const specText = readFileSync(specFull, 'utf8');
|
|
@@ -81,12 +82,12 @@ export function makeSpecVerifier({
|
|
|
81
82
|
child.on('error', () => {
|
|
82
83
|
clearTimeout(timer);
|
|
83
84
|
// 검증자 실행 자체가 실패하면 통과를 막지 않는다(인프라 장애가 판정을 뒤집으면 안 됨).
|
|
84
|
-
resolve({ verdict: 'pass', reason: `검증자 실행 실패 — 통과로 처리` });
|
|
85
|
+
resolve({ verdict: 'pass', verified: false, reason: `검증자 실행 실패 — 통과로 처리` });
|
|
85
86
|
});
|
|
86
87
|
child.on('close', (code) => {
|
|
87
88
|
clearTimeout(timer);
|
|
88
89
|
if (code !== 0) {
|
|
89
|
-
resolve({ verdict: 'pass', reason: `검증자가 코드 ${code}로 종료 — 통과로 처리` });
|
|
90
|
+
resolve({ verdict: 'pass', verified: false, reason: `검증자가 코드 ${code}로 종료 — 통과로 처리` });
|
|
90
91
|
return;
|
|
91
92
|
}
|
|
92
93
|
const { text } = parseAgentOutput(stdout);
|
package/src/wiring.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 미배선 검출 (L19) — "만들었지만 아무도 안 쓰는" 통과.
|
|
3
|
+
*
|
|
4
|
+
* 이 엔진이 아직 못 보던 false green 이 하나 남아 있다. 에이전트가 `src/foo.js` 를 만들고
|
|
5
|
+
* `tests/foo.test.js` 가 그 모듈을 **직접** import 해서 테스트하면:
|
|
6
|
+
* · exit 0 (테스트가 통과한다)
|
|
7
|
+
* · 무결성 깨끗 (케이스 수가 늘었지 줄지 않았다)
|
|
8
|
+
* · 비회귀 깨끗 (통과 수가 늘었다)
|
|
9
|
+
* · 유령 아님 (git 에 추적된다)
|
|
10
|
+
* 그런데 **제품 코드 어디에서도 foo 를 부르지 않는다.** 기능은 없고 테스트만 있다.
|
|
11
|
+
* 기존 게이트는 전부 테스트를 보는데, 이 결함은 테스트가 아니라 **제품 배선**에 있다.
|
|
12
|
+
*
|
|
13
|
+
* bkit 은 이걸 CI 하드 게이트로 갖고 있다(`scripts/check-deadcode.js`, exit 1). 함수 단위
|
|
14
|
+
* 스캐너의 주석이 이름까지 붙여놨다 — *"Built But Not Wired"*.
|
|
15
|
+
*
|
|
16
|
+
* 판정이 아니라 **공개**로 다룬다. 새 모듈이 아직 안 불리는 정상 상황이 흔하기 때문이다
|
|
17
|
+
* (다음 회차에 배선할 수도 있고, 엔트리포인트일 수도 있다). 차단하면 오탐이 루프를 죽인다.
|
|
18
|
+
* 대신 Gaps 에 실어서 "테스트는 통과했지만 이 파일은 제품 경로에서 안 불린다"를 밝힌다.
|
|
19
|
+
*/
|
|
20
|
+
import { readFileSync } from 'node:fs';
|
|
21
|
+
import { basename, extname, join } from 'node:path';
|
|
22
|
+
|
|
23
|
+
const CODE_RE = /\.[cm]?[jt]sx?$/;
|
|
24
|
+
const TEST_RE = /(?:^|\/)(?:tests?|__tests__)\//;
|
|
25
|
+
const TEST_FILE_RE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
26
|
+
|
|
27
|
+
/** 제품 코드인가 (테스트·설정이 아닌). */
|
|
28
|
+
export function isProductionFile(rel) {
|
|
29
|
+
return CODE_RE.test(rel) && !TEST_RE.test(rel) && !TEST_FILE_RE.test(rel);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 모듈 참조를 뽑는다. import/require/동적 import 를 모두 본다. */
|
|
33
|
+
export function referencedNames(text) {
|
|
34
|
+
const out = new Set();
|
|
35
|
+
const patterns = [
|
|
36
|
+
/(?:from|import)\s*\(?\s*['"]([^'"]+)['"]/g,
|
|
37
|
+
/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
38
|
+
];
|
|
39
|
+
for (const re of patterns) {
|
|
40
|
+
for (const m of String(text ?? '').matchAll(re)) {
|
|
41
|
+
const spec = m[1];
|
|
42
|
+
if (!spec.startsWith('.') && !spec.startsWith('/')) continue; // 패키지 import 는 관심 밖
|
|
43
|
+
out.add(basename(spec, extname(spec)));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* baseline 이후 새로 생긴 제품 파일 중 **제품 코드 어디에서도 참조되지 않는 것**.
|
|
51
|
+
*
|
|
52
|
+
* 기준이 baseline 인 이유: 이 루프가 만든 것만 본다. 원래부터 안 불리던 파일까지 들추면
|
|
53
|
+
* 매 실행이 남의 부채를 보고하는 소음이 된다.
|
|
54
|
+
*
|
|
55
|
+
* 관측 불가(baseline 이 없거나 현재 목록을 못 얻음)면 빈 배열 — 인프라는 fail-open.
|
|
56
|
+
*/
|
|
57
|
+
export function detectUnwired({ cwd, baselineFiles, currentFiles }) {
|
|
58
|
+
if (!Array.isArray(baselineFiles) || !Array.isArray(currentFiles)) return [];
|
|
59
|
+
const was = new Set(baselineFiles);
|
|
60
|
+
const fresh = currentFiles.filter((f) => !was.has(f) && isProductionFile(f));
|
|
61
|
+
if (!fresh.length) return [];
|
|
62
|
+
|
|
63
|
+
// 참조 집합은 **제품 파일에서만** 모은다. 테스트가 부르는 것은 배선이 아니다 —
|
|
64
|
+
// 그게 바로 이 검출기가 겨냥하는 형태다.
|
|
65
|
+
const referenced = new Set();
|
|
66
|
+
for (const rel of currentFiles.filter(isProductionFile)) {
|
|
67
|
+
try {
|
|
68
|
+
for (const name of referencedNames(readFileSync(join(cwd, rel), 'utf8'))) referenced.add(name);
|
|
69
|
+
} catch {
|
|
70
|
+
/* 읽는 사이 사라졌으면 무시 — 가드가 루프를 죽이면 안 된다 */
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return fresh.filter((f) => !referenced.has(basename(f, extname(f))));
|
|
74
|
+
}
|