leerness 1.9.3 → 1.9.4
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 +19 -0
- package/bin/harness.js +111 -24
- package/package.json +1 -1
- package/scripts/e2e.js +77 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.4 — 2026-05-08
|
|
4
|
+
|
|
5
|
+
1.9.3 운영 중 발견된 5개 한계점을 모두 패치합니다.
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **A. impact 정확도**: 강한 참조(`import / require / @import / href / src / url / include`)와 약한 참조(식별자 등장)를 분리해 default는 강한 참조만 출력. word boundary 추가로 `cards` 안의 `card`가 false positive로 잡히던 문제 해결. `--all`로 약한 참조까지 표시.
|
|
10
|
+
- **B. cross-platform 종료 코드**: main이 끝난 뒤 `process.exit(process.exitCode)`을 명시. 셸 wrapper나 npx 파이프라인에서 `$?`이 0으로 보이던 문제 해결. `ui consistency --fail-on-violation`은 `--strict-exit`로 즉시 `process.exit(1)`도 가능.
|
|
11
|
+
- **C. lazy detect string literal 휴리스틱**: 매치 위치가 `'…'`/`"…"`/`` `…` `` 안이면 카운트에서 제외. leerness CLI 자기 자신(bin/harness.js)도 자동 skip. 메인 디렉토리에서 30개 잡히던 false positive 사실상 0.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **D. `leerness task fix-evidence`** — `done` 상태이면서 evidence가 비어있거나 `user-request` / `plan:M-XXXX` 단독인 row를 일괄 점검. `--set "<텍스트>"`로 일괄 갱신, 또는 row별 `task update` 명령을 출력해 가이드.
|
|
16
|
+
- **E. `.leerness-skip-dirs` 파일** — 프로젝트 루트에 두면 추가 skip 디렉토리(예: `_apps/`, `leerness-pkg/`)가 모든 walk에서 적용됨. 1줄당 1개 디렉토리, `#` 주석 지원. 기본 skip 셋에도 `out`, `tmp`, `temp`, `.svelte-kit`, `.parcel-cache` 추가.
|
|
17
|
+
|
|
18
|
+
### Migration
|
|
19
|
+
|
|
20
|
+
기존 1.9.x 사용자는 `npx leerness@latest update . --yes`로 즉시 자동 마이그레이션됩니다.
|
|
21
|
+
|
|
3
22
|
## 1.9.3 — 2026-05-08
|
|
4
23
|
|
|
5
24
|
이번 릴리스는 "이전 작업과 새 작업의 인과관계·재귀 안내·디자인 일관성"을 자동화합니다.
|
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.4';
|
|
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,37 @@ 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
|
+
for (const r of candidates) upsertProgress(root, { id: r.id, evidence: setAll });
|
|
698
|
+
ok(`${candidates.length}개 row의 evidence를 일괄 갱신`);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
log(`# task fix-evidence — ${candidates.length}개 후보`);
|
|
702
|
+
log(`아래 row들은 evidence가 검증 키워드(테스트/명령/결과)를 포함하지 않습니다.`);
|
|
703
|
+
log(`각각 다음 명령으로 갱신하거나, --set "<공통 텍스트>"로 일괄 갱신하세요.\n`);
|
|
704
|
+
for (const r of candidates) {
|
|
705
|
+
log(`leerness task update ${r.id} --evidence "검증 결과 (e.g., npm test 통과)"`);
|
|
706
|
+
log(` 요청: ${r.request}`);
|
|
707
|
+
log(` 현재 evidence: "${r.evidence || ''}"`);
|
|
708
|
+
log('');
|
|
709
|
+
}
|
|
710
|
+
if (has('--fail-on-candidates')) process.exit(1);
|
|
711
|
+
}
|
|
712
|
+
|
|
682
713
|
function route(name) {
|
|
683
714
|
const r = routes[name];
|
|
684
715
|
if (!r) { fail('Unknown route'); log('Available: ' + Object.keys(routes).join(', ')); return; }
|
|
@@ -771,15 +802,26 @@ const SECRET_PATTERNS = [
|
|
|
771
802
|
{ name: 'Generic private key', re: /-----BEGIN (?:RSA |OPENSSH |EC |DSA |PGP )?PRIVATE KEY-----/g },
|
|
772
803
|
{ name: 'Hardcoded password assignment', re: /\b(?:password|passwd|pwd|secret|api_key|apikey)\s*[:=]\s*["'][^"'\s]{6,}["']/gi },
|
|
773
804
|
];
|
|
774
|
-
const SCAN_SKIP_DIRS = new Set(['.git','node_modules','.harness/archive','.viewwork','dist','build','.next','.turbo','.cache','coverage','_pkg-source']);
|
|
805
|
+
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']);
|
|
806
|
+
// 1.9.4 E: .leerness-skip-dirs 파일에서 추가 skip 디렉토리 읽기
|
|
807
|
+
function getExtraSkipDirs(root) {
|
|
808
|
+
const f = path.join(absRoot(root || '.'), '.leerness-skip-dirs');
|
|
809
|
+
if (!exists(f)) return [];
|
|
810
|
+
return read(f).split('\n').map(s => s.trim().replace(/\/+$/, '')).filter(s => s && !s.startsWith('#'));
|
|
811
|
+
}
|
|
812
|
+
function isSkippedRel(rel, extras = []) {
|
|
813
|
+
const all = [...SCAN_SKIP_DIRS, ...extras];
|
|
814
|
+
return all.some(d => rel === d || rel.startsWith(d + '/'));
|
|
815
|
+
}
|
|
775
816
|
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) {
|
|
817
|
+
function* walk(root, base = root, depth = 0, extras = null) {
|
|
777
818
|
if (depth > 12) return;
|
|
819
|
+
if (extras === null) extras = getExtraSkipDirs(root);
|
|
778
820
|
for (const e of fs.readdirSync(base, { withFileTypes: true })) {
|
|
779
821
|
const p = path.join(base, e.name);
|
|
780
822
|
const r = path.relative(root, p).replace(/\\/g, '/');
|
|
781
|
-
if (
|
|
782
|
-
if (e.isDirectory()) yield* walk(root, p, depth + 1);
|
|
823
|
+
if (isSkippedRel(r, extras)) continue;
|
|
824
|
+
if (e.isDirectory()) yield* walk(root, p, depth + 1, extras);
|
|
783
825
|
else yield p;
|
|
784
826
|
}
|
|
785
827
|
}
|
|
@@ -871,12 +913,32 @@ function lazyDetect(root) {
|
|
|
871
913
|
const ev = exists(evidencePath(root)) ? read(evidencePath(root)) : '';
|
|
872
914
|
const hasTestRun = /\b(npm test|pnpm test|yarn test|pytest|jest|vitest|tsc|eslint|playwright|cypress)\b/i.test(ev);
|
|
873
915
|
if (!hasTestRun) { issues++; warn('review-evidence.md has no recorded test/typecheck/lint run'); }
|
|
916
|
+
// 1.9.4 C: TODO/FIXME가 string literal 안에 있으면 제외 (정규식 패턴 자체 등 false positive).
|
|
917
|
+
function isInsideQuote(line, idx) {
|
|
918
|
+
const pre = line.slice(0, idx);
|
|
919
|
+
const sq = (pre.match(/(?<!\\)'/g) || []).length;
|
|
920
|
+
const dq = (pre.match(/(?<!\\)"/g) || []).length;
|
|
921
|
+
const bq = (pre.match(/(?<!\\)`/g) || []).length;
|
|
922
|
+
return (sq % 2 === 1) || (dq % 2 === 1) || (bq % 2 === 1);
|
|
923
|
+
}
|
|
874
924
|
let todoCount = 0;
|
|
925
|
+
const cliSelf = path.resolve(__filename);
|
|
875
926
|
for (const file of walk(root)) {
|
|
876
927
|
const ext = path.extname(file).toLowerCase();
|
|
877
|
-
if (!SCAN_TEXT_EXT.has(ext)
|
|
928
|
+
if (!SCAN_TEXT_EXT.has(ext)) continue;
|
|
929
|
+
if (file.includes('.harness')) continue;
|
|
930
|
+
if (path.resolve(file) === cliSelf) continue;
|
|
931
|
+
if (/[\\/]bin[\\/]harness\.js$/.test(file)) continue;
|
|
878
932
|
let text; try { text = read(file); } catch { continue; }
|
|
879
|
-
|
|
933
|
+
const lines = text.split('\n');
|
|
934
|
+
const tre = /\bTODO\b|\bFIXME\b|\bXXX\b/g;
|
|
935
|
+
for (const line of lines) {
|
|
936
|
+
tre.lastIndex = 0;
|
|
937
|
+
let m;
|
|
938
|
+
while ((m = tre.exec(line))) {
|
|
939
|
+
if (!isInsideQuote(line, m.index)) todoCount++;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
880
942
|
}
|
|
881
943
|
if (todoCount > 0) {
|
|
882
944
|
const hasTodoTask = rows.some(r => /TODO|FIXME|XXX/.test(r.request) || /TODO|FIXME|XXX/i.test(r.evidence));
|
|
@@ -1064,13 +1126,14 @@ function gate(root) {
|
|
|
1064
1126
|
|
|
1065
1127
|
// ===== 1.9.3: Causal / reuse / consistency =====
|
|
1066
1128
|
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) {
|
|
1129
|
+
function* walkCode(root, base = root, depth = 0, extras = null) {
|
|
1068
1130
|
if (depth > 12) return;
|
|
1131
|
+
if (extras === null) extras = getExtraSkipDirs(root);
|
|
1069
1132
|
for (const e of fs.readdirSync(base, { withFileTypes: true })) {
|
|
1070
1133
|
const p = path.join(base, e.name);
|
|
1071
1134
|
const r = path.relative(root, p).replace(/\\/g, '/');
|
|
1072
|
-
if (
|
|
1073
|
-
if (e.isDirectory()) yield* walkCode(root, p, depth + 1);
|
|
1135
|
+
if (isSkippedRel(r, extras)) continue;
|
|
1136
|
+
if (e.isDirectory()) yield* walkCode(root, p, depth + 1, extras);
|
|
1074
1137
|
else if (CODE_EXT.has(path.extname(p).toLowerCase())) yield p;
|
|
1075
1138
|
}
|
|
1076
1139
|
}
|
|
@@ -1082,25 +1145,44 @@ function impactCmd(root, target) {
|
|
|
1082
1145
|
const abs = path.isAbsolute(target) ? target : path.resolve(root, target);
|
|
1083
1146
|
const base = path.basename(target);
|
|
1084
1147
|
const noext = path.basename(target, path.extname(target));
|
|
1085
|
-
const direct = [];
|
|
1086
1148
|
const targetRel = rel(root, abs);
|
|
1149
|
+
// 1.9.4 A: strong (import-style, 확신도 높음) vs weak (단순 식별자, 가능성 있음) 구분.
|
|
1150
|
+
const strongRe = new RegExp(
|
|
1151
|
+
`(?:` +
|
|
1152
|
+
`import\\s+[^;\\n]*?from\\s+['"][^'"]*${escapeRegex(base)}` +
|
|
1153
|
+
`|require\\(\\s*['"][^'"]*${escapeRegex(base)}` +
|
|
1154
|
+
`|@import\\s+['"][^'"]*${escapeRegex(base)}` +
|
|
1155
|
+
`|href=["'][^"']*${escapeRegex(base)}` +
|
|
1156
|
+
`|src=["'][^"']*${escapeRegex(base)}` +
|
|
1157
|
+
`|url\\(\\s*['"]?[^'")]*${escapeRegex(base)}` +
|
|
1158
|
+
`|include\\(\\s*['"][^'"]*${escapeRegex(base)}` +
|
|
1159
|
+
`)`
|
|
1160
|
+
);
|
|
1161
|
+
// word boundary 강화: cards 안의 card는 매치 안 함.
|
|
1162
|
+
const weakRe = new RegExp(`(?<![A-Za-z0-9_])${escapeRegex(noext)}(?![A-Za-z0-9_])`);
|
|
1163
|
+
const high = []; const low = [];
|
|
1087
1164
|
for (const f of walkCode(root)) {
|
|
1088
1165
|
if (path.resolve(f) === path.resolve(abs)) continue;
|
|
1089
1166
|
let text; try { text = read(f); } catch { continue; }
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
// bare reference to filename without extension (e.g., 'card' alias)
|
|
1093
|
-
new RegExp(`['"][^'"\\n]*${escapeRegex(noext)}\\b[^'"\\n]*['"]`)
|
|
1094
|
-
];
|
|
1095
|
-
if (patterns.some(re => re.test(text))) direct.push(rel(root, f));
|
|
1167
|
+
if (strongRe.test(text)) high.push(rel(root, f));
|
|
1168
|
+
else if (weakRe.test(text)) low.push(rel(root, f));
|
|
1096
1169
|
}
|
|
1097
1170
|
log(`# impact: ${targetRel}`);
|
|
1098
|
-
|
|
1171
|
+
const showAll = has('--all');
|
|
1172
|
+
if (high.length === 0 && (low.length === 0 || !showAll)) ok('영향 범위 없음 (강한 참조 없음)');
|
|
1099
1173
|
else {
|
|
1100
|
-
|
|
1101
|
-
|
|
1174
|
+
if (high.length) {
|
|
1175
|
+
log(`강한 참조 ${high.length}개 (import/require/href/src/@import/url/include):`);
|
|
1176
|
+
high.forEach(d => log(' - ' + d));
|
|
1177
|
+
} else log('강한 참조: 없음');
|
|
1178
|
+
if (showAll && low.length) {
|
|
1179
|
+
log(`\n약한 참조 ${low.length}개 (식별자 등장 — false positive 가능, 확인 필요):`);
|
|
1180
|
+
low.forEach(d => log(' · ' + d));
|
|
1181
|
+
} else if (low.length && !showAll) {
|
|
1182
|
+
log(`\n💡 약한 참조 ${low.length}개 (--all 로 표시, 정확도는 떨어짐)`);
|
|
1183
|
+
}
|
|
1102
1184
|
}
|
|
1103
|
-
return { target: targetRel,
|
|
1185
|
+
return { target: targetRel, high, low };
|
|
1104
1186
|
}
|
|
1105
1187
|
|
|
1106
1188
|
function reuseMapPath(root) { return path.join(root, '.harness/reuse-map.md'); }
|
|
@@ -1206,7 +1288,8 @@ function uiConsistency(root) {
|
|
|
1206
1288
|
warn(`토큰 외 값 ${findings.length}개:`);
|
|
1207
1289
|
for (const f of findings.slice(0, 30)) log(` ${f.file}:${f.line} ${f.value} (${f.type})`);
|
|
1208
1290
|
if (findings.length > 30) log(` ... +${findings.length - 30}개`);
|
|
1209
|
-
|
|
1291
|
+
// 1.9.4 B: cross-platform 종료 코드 명시
|
|
1292
|
+
if (has('--fail-on-violation')) { process.exitCode = 1; if (has('--strict-exit')) process.exit(1); }
|
|
1210
1293
|
}
|
|
1211
1294
|
|
|
1212
1295
|
function graphCmd(root) {
|
|
@@ -1428,7 +1511,7 @@ function viewworkInstall(root) {
|
|
|
1428
1511
|
}
|
|
1429
1512
|
|
|
1430
1513
|
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>
|
|
1514
|
+
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
1515
|
}
|
|
1433
1516
|
|
|
1434
1517
|
async function main() {
|
|
@@ -1491,8 +1574,12 @@ async function main() {
|
|
|
1491
1574
|
if (sub==='add') return taskAdd(root, args.slice(2).join(' ') || '새 작업');
|
|
1492
1575
|
if (sub==='update') return taskUpdate(root, args[2]);
|
|
1493
1576
|
if (sub==='drop') return taskDrop(root, args[2]);
|
|
1577
|
+
if (sub==='fix-evidence') return taskFixEvidence(root);
|
|
1494
1578
|
}
|
|
1495
1579
|
return help();
|
|
1496
1580
|
}
|
|
1497
1581
|
|
|
1498
|
-
|
|
1582
|
+
// 1.9.4 B: main 종료 후 exitCode를 명시적으로 process.exit으로 강제 (셸/wrapper 차 무시).
|
|
1583
|
+
main()
|
|
1584
|
+
.then(() => { if (process.exitCode && process.exitCode !== 0) process.exit(process.exitCode); })
|
|
1585
|
+
.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,83 @@ 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
|
+
// strong만 잡혀야 하므로 home.html이 강한 참조에 들어가야 함 (이미 <include src=Card.html>)
|
|
233
|
+
// weak에는 about/contact가 들어갈 수 있지만 기본 출력은 strong만
|
|
234
|
+
const strongOK = /강한 참조 \d+개/.test(r.stdout);
|
|
235
|
+
const weakHint = /약한 참조|영향 범위 없음/.test(r.stdout);
|
|
236
|
+
console.log(strongOK && weakHint ? '✓ B(A) impact: strong/weak 구분 출력' : '✗ B(A) impact 구분 실패');
|
|
237
|
+
if (!(strongOK && weakHint)) failed++;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 1.9.4 회귀: --fail-on-violation cross-platform 종료
|
|
241
|
+
total++;
|
|
242
|
+
{
|
|
243
|
+
fs.appendFileSync(path.join(tmp, 'src/pages/home.html'), '\n<style>.x{color:#cafe00;}</style>\n');
|
|
244
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'ui', 'consistency', tmp, '--fail-on-violation'], { encoding: 'utf8' });
|
|
245
|
+
const ok = r.status === 1;
|
|
246
|
+
console.log(ok ? '✓ B(B) ui consistency --fail-on-violation: exit=1' : `✗ B(B) exit=${r.status}`);
|
|
247
|
+
if (!ok) failed++;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 1.9.4 회귀: lazy detect string literal 무시
|
|
251
|
+
total++;
|
|
252
|
+
{
|
|
253
|
+
// false positive 시드: TODO 단어가 string literal 안에 있는 코드
|
|
254
|
+
fs.writeFileSync(path.join(tmp, 'src/regex-helper.js'), `module.exports = { TODO_RE: /\\bTODO\\b/g, label: 'TODO list' };\n`);
|
|
255
|
+
// 다른 한편 진짜 TODO 주석
|
|
256
|
+
fs.writeFileSync(path.join(tmp, 'src/real-todo.js'), `// TODO: 실제 미완료 작업\nconst x = 1;\n`);
|
|
257
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'lazy', 'detect', tmp], { encoding: 'utf8' });
|
|
258
|
+
// string literal 안의 TODO는 무시되고, 주석 안의 진짜 TODO만 카운트되어야 함
|
|
259
|
+
const todosLine = (r.stdout.match(/code has (\d+) TODO/) || [0,'-'])[1];
|
|
260
|
+
const ok = todosLine === '1' || todosLine === '-' || /lazy detect passed/.test(r.stdout);
|
|
261
|
+
console.log(ok ? `✓ B(C) lazy detect: string literal 무시 (count=${todosLine})` : `✗ B(C) count=${todosLine}`);
|
|
262
|
+
if (!ok) failed++;
|
|
263
|
+
fs.unlinkSync(path.join(tmp, 'src/regex-helper.js'));
|
|
264
|
+
fs.unlinkSync(path.join(tmp, 'src/real-todo.js'));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 1.9.4 회귀: task fix-evidence 표시
|
|
268
|
+
total++;
|
|
269
|
+
{
|
|
270
|
+
// T-0001 evidence를 placeholder로 (test before가 'review-evidence:e2e' 였음 → 'user-request'로 바꿈)
|
|
271
|
+
const trackerPath = path.join(tmp, '.harness/progress-tracker.md');
|
|
272
|
+
let cur = fs.readFileSync(trackerPath, 'utf8');
|
|
273
|
+
cur = cur.replace(/^\| T-0001 \|.*$/m, '| T-0001 | done | mile A | user-request | next | 2026-05-08 |');
|
|
274
|
+
fs.writeFileSync(trackerPath, cur);
|
|
275
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'task', 'fix-evidence', '--path', tmp], { encoding: 'utf8' });
|
|
276
|
+
const ok = r.status === 0 && /T-0001/.test(r.stdout) && /후보/.test(r.stdout);
|
|
277
|
+
console.log(ok ? '✓ B(D) task fix-evidence: 후보 표시' : '✗ B(D) 후보 표시 실패');
|
|
278
|
+
if (!ok) { failed++; console.log(r.stdout); }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// 1.9.4 회귀: --set 일괄 갱신
|
|
282
|
+
total++;
|
|
283
|
+
{
|
|
284
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'task', 'fix-evidence', '--set', 'npm test 통과 (e2e)', '--path', tmp], { encoding: 'utf8' });
|
|
285
|
+
const tracker = fs.readFileSync(path.join(tmp, '.harness/progress-tracker.md'), 'utf8');
|
|
286
|
+
const ok = r.status === 0 && /npm test 통과 \(e2e\)/.test(tracker);
|
|
287
|
+
console.log(ok ? '✓ B(D2) task fix-evidence --set: 일괄 갱신' : '✗ B(D2) 일괄 갱신 실패');
|
|
288
|
+
if (!ok) failed++;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 1.9.4 회귀: .leerness-skip-dirs 적용
|
|
292
|
+
total++;
|
|
293
|
+
{
|
|
294
|
+
fs.mkdirSync(path.join(tmp, '_devspace'), { recursive: true });
|
|
295
|
+
fs.writeFileSync(path.join(tmp, '_devspace/secret-config.js'), `const k = "ghp_${'a'.repeat(36)}";\n`);
|
|
296
|
+
fs.writeFileSync(path.join(tmp, '.leerness-skip-dirs'), '_devspace/\n# 주석은 무시\n');
|
|
297
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'scan', 'secrets', tmp], { encoding: 'utf8' });
|
|
298
|
+
const ok = r.status === 0 && /no obvious secret patterns/.test(r.stdout);
|
|
299
|
+
console.log(ok ? '✓ B(E) .leerness-skip-dirs: _devspace 자동 skip' : `✗ B(E) skip 실패\n${r.stdout}`);
|
|
300
|
+
if (!ok) failed++;
|
|
301
|
+
}
|
|
302
|
+
|
|
226
303
|
run('gate (all checks)', ['gate', tmp]);
|
|
227
304
|
|
|
228
305
|
run('self check (= update --check)', ['self', 'check', tmp], { });
|