leerness 1.9.3 → 1.9.5
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/CHANGELOG.md +32 -0
- package/bin/harness.js +139 -24
- package/package.json +1 -1
- package/scripts/e2e.js +119 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.5 — 2026-05-08
|
|
4
|
+
|
|
5
|
+
1.9.4 운영 중 발견된 한계 2건 + 추가 디버그 사항을 패치합니다.
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **F. `task fix-evidence --set` link 보존**: 기존 evidence의 `plan:M-XXXX` 링크를 새 텍스트에 자동으로 `(plan:M-XXXX)` 형태로 부착. `--no-preserve-link`로 끌 수 있음. 이전엔 링크가 사라져 audit이 milestone 미연결로 잡았음.
|
|
10
|
+
- **G. `impact` 동적 참조 (medium)**: `path.join`, `path.resolve`, `readFile`, `writeFile`, `fs.*`, `new URL` 등이 base 파일명을 인자로 받는 패턴을 별도 카테고리(medium)로 분리. default 출력에 strong + medium 모두 표시. site-cli의 `build.js`처럼 동적으로 컴포넌트를 읽는 빌더가 더 이상 weak로만 잡히지 않음.
|
|
11
|
+
|
|
12
|
+
### Migration
|
|
13
|
+
|
|
14
|
+
기존 1.9.x 사용자는 `npx leerness@latest update . --yes`로 즉시 자동 마이그레이션됩니다.
|
|
15
|
+
|
|
16
|
+
## 1.9.4 — 2026-05-08
|
|
17
|
+
|
|
18
|
+
1.9.3 운영 중 발견된 5개 한계점을 모두 패치합니다.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **A. impact 정확도**: 강한 참조(`import / require / @import / href / src / url / include`)와 약한 참조(식별자 등장)를 분리해 default는 강한 참조만 출력. word boundary 추가로 `cards` 안의 `card`가 false positive로 잡히던 문제 해결. `--all`로 약한 참조까지 표시.
|
|
23
|
+
- **B. cross-platform 종료 코드**: main이 끝난 뒤 `process.exit(process.exitCode)`을 명시. 셸 wrapper나 npx 파이프라인에서 `$?`이 0으로 보이던 문제 해결. `ui consistency --fail-on-violation`은 `--strict-exit`로 즉시 `process.exit(1)`도 가능.
|
|
24
|
+
- **C. lazy detect string literal 휴리스틱**: 매치 위치가 `'…'`/`"…"`/`` `…` `` 안이면 카운트에서 제외. leerness CLI 자기 자신(bin/harness.js)도 자동 skip. 메인 디렉토리에서 30개 잡히던 false positive 사실상 0.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **D. `leerness task fix-evidence`** — `done` 상태이면서 evidence가 비어있거나 `user-request` / `plan:M-XXXX` 단독인 row를 일괄 점검. `--set "<텍스트>"`로 일괄 갱신, 또는 row별 `task update` 명령을 출력해 가이드.
|
|
29
|
+
- **E. `.leerness-skip-dirs` 파일** — 프로젝트 루트에 두면 추가 skip 디렉토리(예: `_apps/`, `leerness-pkg/`)가 모든 walk에서 적용됨. 1줄당 1개 디렉토리, `#` 주석 지원. 기본 skip 셋에도 `out`, `tmp`, `temp`, `.svelte-kit`, `.parcel-cache` 추가.
|
|
30
|
+
|
|
31
|
+
### Migration
|
|
32
|
+
|
|
33
|
+
기존 1.9.x 사용자는 `npx leerness@latest update . --yes`로 즉시 자동 마이그레이션됩니다.
|
|
34
|
+
|
|
3
35
|
## 1.9.3 — 2026-05-08
|
|
4
36
|
|
|
5
37
|
이번 릴리스는 "이전 작업과 새 작업의 인과관계·재귀 안내·디자인 일관성"을 자동화합니다.
|
package/bin/harness.js
CHANGED
|
@@ -6,7 +6,7 @@ const path = require('path');
|
|
|
6
6
|
const cp = require('child_process');
|
|
7
7
|
const readline = require('readline');
|
|
8
8
|
|
|
9
|
-
const VERSION = '1.9.
|
|
9
|
+
const VERSION = '1.9.5';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
11
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
12
|
const README_END = '<!-- leerness:project-readme:end -->';
|
|
@@ -679,6 +679,50 @@ function taskDrop(root, id) {
|
|
|
679
679
|
ok(`task dropped: ${id}`);
|
|
680
680
|
}
|
|
681
681
|
|
|
682
|
+
// 1.9.4 D: evidence가 placeholder인 done row를 일괄 점검.
|
|
683
|
+
function taskFixEvidence(root) {
|
|
684
|
+
root = absRoot(root);
|
|
685
|
+
const rows = readProgressRows(root);
|
|
686
|
+
const candidates = rows.filter(r =>
|
|
687
|
+
r.status === 'done' && (
|
|
688
|
+
!r.evidence ||
|
|
689
|
+
/^\s*$/.test(r.evidence) ||
|
|
690
|
+
/^(user-request|-)$/.test(r.evidence) ||
|
|
691
|
+
/^plan:M-\d{4}\s*$/.test(r.evidence)
|
|
692
|
+
)
|
|
693
|
+
);
|
|
694
|
+
if (!candidates.length) return ok('갱신 후보 없음 (모든 done row가 검증 키워드 보유)');
|
|
695
|
+
const setAll = arg('--set', null);
|
|
696
|
+
if (setAll) {
|
|
697
|
+
// 1.9.5 F: 기존 evidence의 plan:M-XXXX 링크를 새 텍스트에 자동 보존 (--no-preserve-link로 끄기)
|
|
698
|
+
const preserveLink = !has('--no-preserve-link');
|
|
699
|
+
let preserved = 0;
|
|
700
|
+
for (const r of candidates) {
|
|
701
|
+
let newEv = setAll;
|
|
702
|
+
if (preserveLink) {
|
|
703
|
+
const m = (r.evidence || '').match(/plan:M-\d{4}/);
|
|
704
|
+
if (m && !newEv.includes(m[0])) {
|
|
705
|
+
newEv = `${setAll} (${m[0]})`;
|
|
706
|
+
preserved++;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
upsertProgress(root, { id: r.id, evidence: newEv });
|
|
710
|
+
}
|
|
711
|
+
ok(`${candidates.length}개 row의 evidence를 일괄 갱신${preserveLink ? ` (link 보존: ${preserved}건)` : ''}`);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
log(`# task fix-evidence — ${candidates.length}개 후보`);
|
|
715
|
+
log(`아래 row들은 evidence가 검증 키워드(테스트/명령/결과)를 포함하지 않습니다.`);
|
|
716
|
+
log(`각각 다음 명령으로 갱신하거나, --set "<공통 텍스트>"로 일괄 갱신하세요.\n`);
|
|
717
|
+
for (const r of candidates) {
|
|
718
|
+
log(`leerness task update ${r.id} --evidence "검증 결과 (e.g., npm test 통과)"`);
|
|
719
|
+
log(` 요청: ${r.request}`);
|
|
720
|
+
log(` 현재 evidence: "${r.evidence || ''}"`);
|
|
721
|
+
log('');
|
|
722
|
+
}
|
|
723
|
+
if (has('--fail-on-candidates')) process.exit(1);
|
|
724
|
+
}
|
|
725
|
+
|
|
682
726
|
function route(name) {
|
|
683
727
|
const r = routes[name];
|
|
684
728
|
if (!r) { fail('Unknown route'); log('Available: ' + Object.keys(routes).join(', ')); return; }
|
|
@@ -771,15 +815,26 @@ const SECRET_PATTERNS = [
|
|
|
771
815
|
{ name: 'Generic private key', re: /-----BEGIN (?:RSA |OPENSSH |EC |DSA |PGP )?PRIVATE KEY-----/g },
|
|
772
816
|
{ name: 'Hardcoded password assignment', re: /\b(?:password|passwd|pwd|secret|api_key|apikey)\s*[:=]\s*["'][^"'\s]{6,}["']/gi },
|
|
773
817
|
];
|
|
774
|
-
const SCAN_SKIP_DIRS = new Set(['.git','node_modules','.harness/archive','.viewwork','dist','build','.next','.turbo','.cache','coverage','_pkg-source']);
|
|
818
|
+
const SCAN_SKIP_DIRS = new Set(['.git','node_modules','.harness/archive','.viewwork','dist','build','.next','.turbo','.cache','coverage','_pkg-source','out','tmp','temp','.svelte-kit','.parcel-cache']);
|
|
819
|
+
// 1.9.4 E: .leerness-skip-dirs 파일에서 추가 skip 디렉토리 읽기
|
|
820
|
+
function getExtraSkipDirs(root) {
|
|
821
|
+
const f = path.join(absRoot(root || '.'), '.leerness-skip-dirs');
|
|
822
|
+
if (!exists(f)) return [];
|
|
823
|
+
return read(f).split('\n').map(s => s.trim().replace(/\/+$/, '')).filter(s => s && !s.startsWith('#'));
|
|
824
|
+
}
|
|
825
|
+
function isSkippedRel(rel, extras = []) {
|
|
826
|
+
const all = [...SCAN_SKIP_DIRS, ...extras];
|
|
827
|
+
return all.some(d => rel === d || rel.startsWith(d + '/'));
|
|
828
|
+
}
|
|
775
829
|
const SCAN_TEXT_EXT = new Set(['.js','.ts','.jsx','.tsx','.mjs','.cjs','.json','.md','.txt','.env','.bash','.sh','.yml','.yaml','.toml','.ini','.cfg','.py','.rb','.go','.rs','.java','.kt','.swift','.cs','.php','.sql','.html','.css','.scss','.less','.xml','.bat','.ps1','']);
|
|
776
|
-
function* walk(root, base = root, depth = 0) {
|
|
830
|
+
function* walk(root, base = root, depth = 0, extras = null) {
|
|
777
831
|
if (depth > 12) return;
|
|
832
|
+
if (extras === null) extras = getExtraSkipDirs(root);
|
|
778
833
|
for (const e of fs.readdirSync(base, { withFileTypes: true })) {
|
|
779
834
|
const p = path.join(base, e.name);
|
|
780
835
|
const r = path.relative(root, p).replace(/\\/g, '/');
|
|
781
|
-
if (
|
|
782
|
-
if (e.isDirectory()) yield* walk(root, p, depth + 1);
|
|
836
|
+
if (isSkippedRel(r, extras)) continue;
|
|
837
|
+
if (e.isDirectory()) yield* walk(root, p, depth + 1, extras);
|
|
783
838
|
else yield p;
|
|
784
839
|
}
|
|
785
840
|
}
|
|
@@ -871,12 +926,32 @@ function lazyDetect(root) {
|
|
|
871
926
|
const ev = exists(evidencePath(root)) ? read(evidencePath(root)) : '';
|
|
872
927
|
const hasTestRun = /\b(npm test|pnpm test|yarn test|pytest|jest|vitest|tsc|eslint|playwright|cypress)\b/i.test(ev);
|
|
873
928
|
if (!hasTestRun) { issues++; warn('review-evidence.md has no recorded test/typecheck/lint run'); }
|
|
929
|
+
// 1.9.4 C: TODO/FIXME가 string literal 안에 있으면 제외 (정규식 패턴 자체 등 false positive).
|
|
930
|
+
function isInsideQuote(line, idx) {
|
|
931
|
+
const pre = line.slice(0, idx);
|
|
932
|
+
const sq = (pre.match(/(?<!\\)'/g) || []).length;
|
|
933
|
+
const dq = (pre.match(/(?<!\\)"/g) || []).length;
|
|
934
|
+
const bq = (pre.match(/(?<!\\)`/g) || []).length;
|
|
935
|
+
return (sq % 2 === 1) || (dq % 2 === 1) || (bq % 2 === 1);
|
|
936
|
+
}
|
|
874
937
|
let todoCount = 0;
|
|
938
|
+
const cliSelf = path.resolve(__filename);
|
|
875
939
|
for (const file of walk(root)) {
|
|
876
940
|
const ext = path.extname(file).toLowerCase();
|
|
877
|
-
if (!SCAN_TEXT_EXT.has(ext)
|
|
941
|
+
if (!SCAN_TEXT_EXT.has(ext)) continue;
|
|
942
|
+
if (file.includes('.harness')) continue;
|
|
943
|
+
if (path.resolve(file) === cliSelf) continue;
|
|
944
|
+
if (/[\\/]bin[\\/]harness\.js$/.test(file)) continue;
|
|
878
945
|
let text; try { text = read(file); } catch { continue; }
|
|
879
|
-
|
|
946
|
+
const lines = text.split('\n');
|
|
947
|
+
const tre = /\bTODO\b|\bFIXME\b|\bXXX\b/g;
|
|
948
|
+
for (const line of lines) {
|
|
949
|
+
tre.lastIndex = 0;
|
|
950
|
+
let m;
|
|
951
|
+
while ((m = tre.exec(line))) {
|
|
952
|
+
if (!isInsideQuote(line, m.index)) todoCount++;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
880
955
|
}
|
|
881
956
|
if (todoCount > 0) {
|
|
882
957
|
const hasTodoTask = rows.some(r => /TODO|FIXME|XXX/.test(r.request) || /TODO|FIXME|XXX/i.test(r.evidence));
|
|
@@ -1064,13 +1139,14 @@ function gate(root) {
|
|
|
1064
1139
|
|
|
1065
1140
|
// ===== 1.9.3: Causal / reuse / consistency =====
|
|
1066
1141
|
const CODE_EXT = new Set(['.js','.ts','.jsx','.tsx','.mjs','.cjs','.css','.scss','.sass','.less','.html','.htm','.vue','.svelte','.md','.json','.py','.rb','.go','.rs','.java','.kt','.swift','.cs','.php']);
|
|
1067
|
-
function* walkCode(root, base = root, depth = 0) {
|
|
1142
|
+
function* walkCode(root, base = root, depth = 0, extras = null) {
|
|
1068
1143
|
if (depth > 12) return;
|
|
1144
|
+
if (extras === null) extras = getExtraSkipDirs(root);
|
|
1069
1145
|
for (const e of fs.readdirSync(base, { withFileTypes: true })) {
|
|
1070
1146
|
const p = path.join(base, e.name);
|
|
1071
1147
|
const r = path.relative(root, p).replace(/\\/g, '/');
|
|
1072
|
-
if (
|
|
1073
|
-
if (e.isDirectory()) yield* walkCode(root, p, depth + 1);
|
|
1148
|
+
if (isSkippedRel(r, extras)) continue;
|
|
1149
|
+
if (e.isDirectory()) yield* walkCode(root, p, depth + 1, extras);
|
|
1074
1150
|
else if (CODE_EXT.has(path.extname(p).toLowerCase())) yield p;
|
|
1075
1151
|
}
|
|
1076
1152
|
}
|
|
@@ -1082,25 +1158,59 @@ function impactCmd(root, target) {
|
|
|
1082
1158
|
const abs = path.isAbsolute(target) ? target : path.resolve(root, target);
|
|
1083
1159
|
const base = path.basename(target);
|
|
1084
1160
|
const noext = path.basename(target, path.extname(target));
|
|
1085
|
-
const direct = [];
|
|
1086
1161
|
const targetRel = rel(root, abs);
|
|
1162
|
+
const eb = escapeRegex(base);
|
|
1163
|
+
// 1.9.5 G: strong (정적 import) / medium (동적 path 함수) / weak (식별자 등장) 3단계.
|
|
1164
|
+
const strongRe = new RegExp(
|
|
1165
|
+
`(?:` +
|
|
1166
|
+
`import\\s+[^;\\n]*?from\\s+['"][^'"]*${eb}` +
|
|
1167
|
+
`|require\\(\\s*['"][^'"]*${eb}` +
|
|
1168
|
+
`|@import\\s+['"][^'"]*${eb}` +
|
|
1169
|
+
`|href=["'][^"']*${eb}` +
|
|
1170
|
+
`|src=["'][^"']*${eb}` +
|
|
1171
|
+
`|url\\(\\s*['"]?[^'")]*${eb}` +
|
|
1172
|
+
`|include\\(\\s*['"][^'"]*${eb}` +
|
|
1173
|
+
`)`
|
|
1174
|
+
);
|
|
1175
|
+
// 동적 path 조합 / 파일 시스템 호출과 함께 base 파일명이 등장하는 경우.
|
|
1176
|
+
const mediumRe = new RegExp(
|
|
1177
|
+
`(?:` +
|
|
1178
|
+
`path\\.(?:join|resolve|relative|parse|format|normalize)\\s*\\([^)]*['"][^'"]*${eb}[^'"]*['"]` +
|
|
1179
|
+
`|(?:readFile|writeFile|stat|access|open|createReadStream|createWriteStream|readFileSync|writeFileSync|statSync|accessSync|openSync)\\s*\\([^)]*['"][^'"]*${eb}[^'"]*['"]` +
|
|
1180
|
+
`|fs\\.[a-zA-Z]+\\s*\\([^)]*['"][^'"]*${eb}[^'"]*['"]` +
|
|
1181
|
+
`|new\\s+URL\\s*\\([^)]*['"][^'"]*${eb}[^'"]*['"]` +
|
|
1182
|
+
`)`
|
|
1183
|
+
);
|
|
1184
|
+
const weakRe = new RegExp(`(?<![A-Za-z0-9_])${escapeRegex(noext)}(?![A-Za-z0-9_])`);
|
|
1185
|
+
const high = []; const medium = []; const low = [];
|
|
1087
1186
|
for (const f of walkCode(root)) {
|
|
1088
1187
|
if (path.resolve(f) === path.resolve(abs)) continue;
|
|
1089
1188
|
let text; try { text = read(f); } catch { continue; }
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
new RegExp(`['"][^'"\\n]*${escapeRegex(noext)}\\b[^'"\\n]*['"]`)
|
|
1094
|
-
];
|
|
1095
|
-
if (patterns.some(re => re.test(text))) direct.push(rel(root, f));
|
|
1189
|
+
if (strongRe.test(text)) high.push(rel(root, f));
|
|
1190
|
+
else if (mediumRe.test(text)) medium.push(rel(root, f));
|
|
1191
|
+
else if (weakRe.test(text)) low.push(rel(root, f));
|
|
1096
1192
|
}
|
|
1097
1193
|
log(`# impact: ${targetRel}`);
|
|
1098
|
-
|
|
1194
|
+
const showAll = has('--all');
|
|
1195
|
+
const totalEffective = high.length + medium.length;
|
|
1196
|
+
if (totalEffective === 0 && (low.length === 0 || !showAll)) ok('영향 범위 없음 (강한/중간 참조 없음)');
|
|
1099
1197
|
else {
|
|
1100
|
-
|
|
1101
|
-
|
|
1198
|
+
if (high.length) {
|
|
1199
|
+
log(`강한 참조 ${high.length}개 (import/require/href/src/@import/url/include):`);
|
|
1200
|
+
high.forEach(d => log(' - ' + d));
|
|
1201
|
+
} else log('강한 참조: 없음');
|
|
1202
|
+
if (medium.length) {
|
|
1203
|
+
log(`\n중간 참조 ${medium.length}개 (path.join/readFile/fs 등 동적 path):`);
|
|
1204
|
+
medium.forEach(d => log(' ~ ' + d));
|
|
1205
|
+
}
|
|
1206
|
+
if (showAll && low.length) {
|
|
1207
|
+
log(`\n약한 참조 ${low.length}개 (식별자 등장 — false positive 가능):`);
|
|
1208
|
+
low.forEach(d => log(' · ' + d));
|
|
1209
|
+
} else if (low.length && !showAll) {
|
|
1210
|
+
log(`\n💡 약한 참조 ${low.length}개 (--all 로 표시)`);
|
|
1211
|
+
}
|
|
1102
1212
|
}
|
|
1103
|
-
return { target: targetRel,
|
|
1213
|
+
return { target: targetRel, high, medium, low };
|
|
1104
1214
|
}
|
|
1105
1215
|
|
|
1106
1216
|
function reuseMapPath(root) { return path.join(root, '.harness/reuse-map.md'); }
|
|
@@ -1206,7 +1316,8 @@ function uiConsistency(root) {
|
|
|
1206
1316
|
warn(`토큰 외 값 ${findings.length}개:`);
|
|
1207
1317
|
for (const f of findings.slice(0, 30)) log(` ${f.file}:${f.line} ${f.value} (${f.type})`);
|
|
1208
1318
|
if (findings.length > 30) log(` ... +${findings.length - 30}개`);
|
|
1209
|
-
|
|
1319
|
+
// 1.9.4 B: cross-platform 종료 코드 명시
|
|
1320
|
+
if (has('--fail-on-violation')) { process.exitCode = 1; if (has('--strict-exit')) process.exit(1); }
|
|
1210
1321
|
}
|
|
1211
1322
|
|
|
1212
1323
|
function graphCmd(root) {
|
|
@@ -1428,7 +1539,7 @@ function viewworkInstall(root) {
|
|
|
1428
1539
|
}
|
|
1429
1540
|
|
|
1430
1541
|
function help() {
|
|
1431
|
-
log(`Leerness v${VERSION}\n\nUsage:\n leerness init [path] [--language auto|ko|en] [--skills recommended|all|a,b]\n leerness migrate [path] [--dry-run] [--force]\n leerness update [path] [--check|--yes|--force|--from <tarball>]\n leerness auto-update install [path]\n leerness status [path]\n leerness verify [path]\n leerness debug [path]\n leerness audit [path]\n leerness check [path]\n leerness scan secrets [path]\n leerness encoding check [path]\n leerness lazy detect [path]\n leerness memory search "query" [--limit 5]\n leerness handoff [path]\n leerness session close [path]\n leerness viewwork install [path]\n leerness viewwork emit [path] [--action a] [--note n] [--agent x] [--tool t]\n leerness route <task-type>\n leerness self check [path]\n leerness readme sync [path]\n leerness consistency check [path]\n leerness consistency merge-design-guide [path]\n leerness plan show|init|add|drop|progress|sync [args]\n leerness task list|add|update|drop [args]\n leerness skill list|info <name>\n leerness skill learn <id> --doc <url> --command "..." --capability "..." [--note ...]\n leerness skill use <id> [--note ...]\n leerness skill optimize <id> --before "..." --after "..." [--note ...]\n leerness skill remove <id>\n leerness skill consolidate [--threshold 0.3]\n leerness gate [path] # verify+audit+scan+encoding+lazy\n leerness impact <target>
|
|
1542
|
+
log(`Leerness v${VERSION}\n\nUsage:\n leerness init [path] [--language auto|ko|en] [--skills recommended|all|a,b]\n leerness migrate [path] [--dry-run] [--force]\n leerness update [path] [--check|--yes|--force|--from <tarball>]\n leerness auto-update install [path]\n leerness status [path]\n leerness verify [path]\n leerness debug [path]\n leerness audit [path]\n leerness check [path]\n leerness scan secrets [path]\n leerness encoding check [path]\n leerness lazy detect [path]\n leerness memory search "query" [--limit 5]\n leerness handoff [path]\n leerness session close [path]\n leerness viewwork install [path]\n leerness viewwork emit [path] [--action a] [--note n] [--agent x] [--tool t]\n leerness route <task-type>\n leerness self check [path]\n leerness readme sync [path]\n leerness consistency check [path]\n leerness consistency merge-design-guide [path]\n leerness plan show|init|add|drop|progress|sync [args]\n leerness task list|add|update|drop|fix-evidence [args]\n leerness skill list|info <name>\n leerness skill learn <id> --doc <url> --command "..." --capability "..." [--note ...]\n leerness skill use <id> [--note ...]\n leerness skill optimize <id> --before "..." --after "..." [--note ...]\n leerness skill remove <id>\n leerness skill consolidate [--threshold 0.3]\n leerness gate [path] # verify+audit+scan+encoding+lazy\n leerness impact <target> [--all] # 변경 전 영향 분석 (기본 strong, --all로 weak 포함)\n leerness reuse find <query> # 기존 자원 검색 (재귀 안내)\n leerness reuse register <name> --where <p> --kind component|hook|util|api [--note ...]\n leerness ui consistency [path] [--strict] [--fail-on-violation]\n leerness graph [path] [--out <file>] # mermaid 의존성 그래프\n leerness guide [target] # impact + reuse + ui consistency 통합 가이드\n`);
|
|
1432
1543
|
}
|
|
1433
1544
|
|
|
1434
1545
|
async function main() {
|
|
@@ -1491,8 +1602,12 @@ async function main() {
|
|
|
1491
1602
|
if (sub==='add') return taskAdd(root, args.slice(2).join(' ') || '새 작업');
|
|
1492
1603
|
if (sub==='update') return taskUpdate(root, args[2]);
|
|
1493
1604
|
if (sub==='drop') return taskDrop(root, args[2]);
|
|
1605
|
+
if (sub==='fix-evidence') return taskFixEvidence(root);
|
|
1494
1606
|
}
|
|
1495
1607
|
return help();
|
|
1496
1608
|
}
|
|
1497
1609
|
|
|
1498
|
-
|
|
1610
|
+
// 1.9.4 B: main 종료 후 exitCode를 명시적으로 process.exit으로 강제 (셸/wrapper 차 무시).
|
|
1611
|
+
main()
|
|
1612
|
+
.then(() => { if (process.exitCode && process.exitCode !== 0) process.exit(process.exitCode); })
|
|
1613
|
+
.catch(err => { fail(err && err.message ? err.message : String(err)); process.exit(1); });
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -223,6 +223,125 @@ total++;
|
|
|
223
223
|
run('graph: mermaid 출력', ['graph', tmp]);
|
|
224
224
|
run('guide: 통합 가이드', ['guide', 'src/components/Card.html', '--path', tmp]);
|
|
225
225
|
|
|
226
|
+
// 1.9.4 회귀: impact strong/weak 구분
|
|
227
|
+
total++;
|
|
228
|
+
{
|
|
229
|
+
// home.html에 "Card"라는 식별자가 plain text로 들어가도록 (false positive 시드)
|
|
230
|
+
fs.appendFileSync(path.join(tmp, 'src/pages/home.html'), '\n<!-- 카드 콘텐츠는 Cards 배열로 -->\n');
|
|
231
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'impact', 'src/components/Card.html', '--path', tmp], { encoding: 'utf8' });
|
|
232
|
+
const strongOK = /강한 참조 \d+개/.test(r.stdout);
|
|
233
|
+
const weakHint = /약한 참조|영향 범위 없음/.test(r.stdout);
|
|
234
|
+
console.log(strongOK && weakHint ? '✓ B(A) impact: strong/weak 구분 출력' : '✗ B(A) impact 구분 실패');
|
|
235
|
+
if (!(strongOK && weakHint)) failed++;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 1.9.5 G 회귀: impact medium 카테고리 — 동적 path 패턴
|
|
239
|
+
total++;
|
|
240
|
+
{
|
|
241
|
+
// builder.js에서 path.join + readFileSync + Card.html을 동적 사용
|
|
242
|
+
fs.writeFileSync(path.join(tmp, 'src/builder.js'),
|
|
243
|
+
`const fs = require('fs'); const path = require('path');\n` +
|
|
244
|
+
`const tpl = fs.readFileSync(path.join(__dirname, 'components', 'Card.html'), 'utf8');\n` +
|
|
245
|
+
`module.exports = { tpl };\n`);
|
|
246
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'impact', 'src/components/Card.html', '--path', tmp], { encoding: 'utf8' });
|
|
247
|
+
const ok = /중간 참조 \d+개/.test(r.stdout) && /src\/builder\.js/.test(r.stdout);
|
|
248
|
+
console.log(ok ? '✓ B(G) impact medium: builder.js (path.join + readFileSync) 검출' : `✗ B(G) medium 검출 실패\n${r.stdout}`);
|
|
249
|
+
if (!ok) failed++;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 1.9.4 회귀: --fail-on-violation cross-platform 종료
|
|
253
|
+
total++;
|
|
254
|
+
{
|
|
255
|
+
fs.appendFileSync(path.join(tmp, 'src/pages/home.html'), '\n<style>.x{color:#cafe00;}</style>\n');
|
|
256
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'ui', 'consistency', tmp, '--fail-on-violation'], { encoding: 'utf8' });
|
|
257
|
+
const ok = r.status === 1;
|
|
258
|
+
console.log(ok ? '✓ B(B) ui consistency --fail-on-violation: exit=1' : `✗ B(B) exit=${r.status}`);
|
|
259
|
+
if (!ok) failed++;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 1.9.4 회귀: lazy detect string literal 무시
|
|
263
|
+
total++;
|
|
264
|
+
{
|
|
265
|
+
// false positive 시드: TODO 단어가 string literal 안에 있는 코드
|
|
266
|
+
fs.writeFileSync(path.join(tmp, 'src/regex-helper.js'), `module.exports = { TODO_RE: /\\bTODO\\b/g, label: 'TODO list' };\n`);
|
|
267
|
+
// 다른 한편 진짜 TODO 주석
|
|
268
|
+
fs.writeFileSync(path.join(tmp, 'src/real-todo.js'), `// TODO: 실제 미완료 작업\nconst x = 1;\n`);
|
|
269
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'lazy', 'detect', tmp], { encoding: 'utf8' });
|
|
270
|
+
// string literal 안의 TODO는 무시되고, 주석 안의 진짜 TODO만 카운트되어야 함
|
|
271
|
+
const todosLine = (r.stdout.match(/code has (\d+) TODO/) || [0,'-'])[1];
|
|
272
|
+
const ok = todosLine === '1' || todosLine === '-' || /lazy detect passed/.test(r.stdout);
|
|
273
|
+
console.log(ok ? `✓ B(C) lazy detect: string literal 무시 (count=${todosLine})` : `✗ B(C) count=${todosLine}`);
|
|
274
|
+
if (!ok) failed++;
|
|
275
|
+
fs.unlinkSync(path.join(tmp, 'src/regex-helper.js'));
|
|
276
|
+
fs.unlinkSync(path.join(tmp, 'src/real-todo.js'));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 1.9.4 회귀: task fix-evidence 표시
|
|
280
|
+
total++;
|
|
281
|
+
{
|
|
282
|
+
// T-0001 evidence를 placeholder로 (test before가 'review-evidence:e2e' 였음 → 'user-request'로 바꿈)
|
|
283
|
+
const trackerPath = path.join(tmp, '.harness/progress-tracker.md');
|
|
284
|
+
let cur = fs.readFileSync(trackerPath, 'utf8');
|
|
285
|
+
cur = cur.replace(/^\| T-0001 \|.*$/m, '| T-0001 | done | mile A | user-request | next | 2026-05-08 |');
|
|
286
|
+
fs.writeFileSync(trackerPath, cur);
|
|
287
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'task', 'fix-evidence', '--path', tmp], { encoding: 'utf8' });
|
|
288
|
+
const ok = r.status === 0 && /T-0001/.test(r.stdout) && /후보/.test(r.stdout);
|
|
289
|
+
console.log(ok ? '✓ B(D) task fix-evidence: 후보 표시' : '✗ B(D) 후보 표시 실패');
|
|
290
|
+
if (!ok) { failed++; console.log(r.stdout); }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 1.9.4 회귀: --set 일괄 갱신
|
|
294
|
+
total++;
|
|
295
|
+
{
|
|
296
|
+
// T-0001 evidence를 plan:M-0002 링크 포함한 placeholder로 (1.9.5 link 보존 검증용 시드)
|
|
297
|
+
const trackerPath2 = path.join(tmp, '.harness/progress-tracker.md');
|
|
298
|
+
let cur = fs.readFileSync(trackerPath2, 'utf8');
|
|
299
|
+
cur = cur.replace(/^\| T-0001 \|.*$/m, '| T-0001 | done | mile A | plan:M-0002 | next | 2026-05-08 |');
|
|
300
|
+
fs.writeFileSync(trackerPath2, cur);
|
|
301
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'task', 'fix-evidence', '--set', 'npm test 통과 (e2e)', '--path', tmp], { encoding: 'utf8' });
|
|
302
|
+
const tracker = fs.readFileSync(trackerPath2, 'utf8');
|
|
303
|
+
const ok = r.status === 0 && /npm test 통과 \(e2e\)/.test(tracker);
|
|
304
|
+
console.log(ok ? '✓ B(D2) task fix-evidence --set: 일괄 갱신' : '✗ B(D2) 일괄 갱신 실패');
|
|
305
|
+
if (!ok) failed++;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// 1.9.5 F 회귀: --set 시 plan:M-XXXX 링크 자동 보존
|
|
309
|
+
total++;
|
|
310
|
+
{
|
|
311
|
+
const tracker = fs.readFileSync(path.join(tmp, '.harness/progress-tracker.md'), 'utf8');
|
|
312
|
+
const ok = /npm test 통과 \(e2e\) \(plan:M-0002\)/.test(tracker);
|
|
313
|
+
console.log(ok ? '✓ B(F) fix-evidence --set: link 자동 보존' : `✗ B(F) link 손실\n${tracker}`);
|
|
314
|
+
if (!ok) failed++;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 1.9.5 F neg: --no-preserve-link
|
|
318
|
+
total++;
|
|
319
|
+
{
|
|
320
|
+
// T-0002 같은 row를 placeholder로 시드
|
|
321
|
+
const trackerPath3 = path.join(tmp, '.harness/progress-tracker.md');
|
|
322
|
+
let cur = fs.readFileSync(trackerPath3, 'utf8');
|
|
323
|
+
// T-0001을 다시 plan:M-0099로 교체
|
|
324
|
+
cur = cur.replace(/^\| T-0001 \|.*$/m, '| T-0001 | done | mile A | plan:M-0099 | next | 2026-05-08 |');
|
|
325
|
+
fs.writeFileSync(trackerPath3, cur);
|
|
326
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'fix-evidence', '--set', 'npm test only', '--no-preserve-link', '--path', tmp], { encoding: 'utf8' });
|
|
327
|
+
const tracker = fs.readFileSync(trackerPath3, 'utf8');
|
|
328
|
+
const ok = /\| T-0001 \| done \| mile A \| npm test only \|/.test(tracker) && !/M-0099/.test(tracker);
|
|
329
|
+
console.log(ok ? '✓ B(F neg) fix-evidence --no-preserve-link: 링크 제거' : `✗ B(F neg) 동작 이상`);
|
|
330
|
+
if (!ok) failed++;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 1.9.4 회귀: .leerness-skip-dirs 적용
|
|
334
|
+
total++;
|
|
335
|
+
{
|
|
336
|
+
fs.mkdirSync(path.join(tmp, '_devspace'), { recursive: true });
|
|
337
|
+
fs.writeFileSync(path.join(tmp, '_devspace/secret-config.js'), `const k = "ghp_${'a'.repeat(36)}";\n`);
|
|
338
|
+
fs.writeFileSync(path.join(tmp, '.leerness-skip-dirs'), '_devspace/\n# 주석은 무시\n');
|
|
339
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'scan', 'secrets', tmp], { encoding: 'utf8' });
|
|
340
|
+
const ok = r.status === 0 && /no obvious secret patterns/.test(r.stdout);
|
|
341
|
+
console.log(ok ? '✓ B(E) .leerness-skip-dirs: _devspace 자동 skip' : `✗ B(E) skip 실패\n${r.stdout}`);
|
|
342
|
+
if (!ok) failed++;
|
|
343
|
+
}
|
|
344
|
+
|
|
226
345
|
run('gate (all checks)', ['gate', tmp]);
|
|
227
346
|
|
|
228
347
|
run('self check (= update --check)', ['self', 'check', tmp], { });
|