leerness 1.9.6 → 1.9.7
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 +25 -0
- package/bin/harness.js +154 -6
- package/package.json +1 -1
- package/scripts/e2e.js +88 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.7 — 2026-05-08
|
|
4
|
+
|
|
5
|
+
코드 검증 자동 실행 + 과거 결정/실수 자동 회수 + TODO 자동 추적의 3종 자동화.
|
|
6
|
+
|
|
7
|
+
### Added — A. `leerness verify-code [path] [--build]`
|
|
8
|
+
|
|
9
|
+
`package.json#scripts`에서 `test` / `lint` / `typecheck` (또는 `tsc`) / (선택) `build`를 자동 감지해 차례로 실행. 결과는 모두 `review-evidence.md`에 자동 누적 (`Command/Tasks/exit/duration/tail`). 실패 시 `process.exit(1)` + progress의 in-progress row를 `incomplete`로 표시 권장 안내.
|
|
10
|
+
|
|
11
|
+
- `tsconfig.json`이 있고 `typecheck` script가 없으면 `npx tsc --noEmit` 자동 호출.
|
|
12
|
+
- 5분 timeout 내장 (장기 실행 방지).
|
|
13
|
+
|
|
14
|
+
### Added — B. `leerness lessons [--query <키>] [--limit N]`
|
|
15
|
+
|
|
16
|
+
`decisions.md`의 모든 `### 블록`, `review-evidence.md`의 실패 표지(`✗ / fail / 롤백 / incomplete / bug / 버그 / warning`) 블록, `task-log.md`의 실패 키워드 라인, `session-handoff.md`의 Incomplete 섹션을 통합 추출. `--query`로 키워드 필터.
|
|
17
|
+
|
|
18
|
+
- `leerness guide [target]`이 자동으로 lessons 섹션을 추가 (target 이름을 query로 사용).
|
|
19
|
+
|
|
20
|
+
### Added — C. `lazy detect --auto-track` + `.harness/known-todos.json`
|
|
21
|
+
|
|
22
|
+
새 TODO/FIXME/XXX의 `(file, line, text)` 위치 캡처. `known-todos.json`에 acknowledged 기록을 비교해 매번 같은 false positive를 줄이고, 새로 발견된 것만 노출. `--auto-track`으로 `progress-tracker.md`에 `T-XXXX requested`로 자동 등록 + known-todos.json에도 자동 추가.
|
|
23
|
+
|
|
24
|
+
### Migration
|
|
25
|
+
|
|
26
|
+
기존 1.9.x 사용자는 `npx leerness@latest update . --yes`로 즉시 자동 마이그레이션됩니다.
|
|
27
|
+
|
|
3
28
|
## 1.9.6 — 2026-05-08
|
|
4
29
|
|
|
5
30
|
1.9.5 후 발견된 한 가지 한계 (옛 link 손실 자동 복구 부재)를 패치.
|
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.7';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
11
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
12
|
const README_END = '<!-- leerness:project-readme:end -->';
|
|
@@ -1003,7 +1003,13 @@ function lazyDetect(root) {
|
|
|
1003
1003
|
const bq = (pre.match(/(?<!\\)`/g) || []).length;
|
|
1004
1004
|
return (sq % 2 === 1) || (dq % 2 === 1) || (bq % 2 === 1);
|
|
1005
1005
|
}
|
|
1006
|
+
// 1.9.7 C: TODO 자동 추적 강화 — 위치+텍스트 캡처, known-todos 비교, --auto-track 등록
|
|
1007
|
+
const knownPath = path.join(root, '.harness/known-todos.json');
|
|
1008
|
+
let knownList = [];
|
|
1009
|
+
if (exists(knownPath)) { try { knownList = JSON.parse(read(knownPath)); } catch {} }
|
|
1010
|
+
const knownSet = new Set(knownList.map(k => `${k.file}:${k.line}:${k.text}`));
|
|
1006
1011
|
let todoCount = 0;
|
|
1012
|
+
const newTodos = [];
|
|
1007
1013
|
const cliSelf = path.resolve(__filename);
|
|
1008
1014
|
for (const file of walk(root)) {
|
|
1009
1015
|
const ext = path.extname(file).toLowerCase();
|
|
@@ -1014,17 +1020,39 @@ function lazyDetect(root) {
|
|
|
1014
1020
|
let text; try { text = read(file); } catch { continue; }
|
|
1015
1021
|
const lines = text.split('\n');
|
|
1016
1022
|
const tre = /\bTODO\b|\bFIXME\b|\bXXX\b/g;
|
|
1017
|
-
for (
|
|
1023
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1018
1024
|
tre.lastIndex = 0;
|
|
1019
1025
|
let m;
|
|
1020
|
-
while ((m = tre.exec(
|
|
1021
|
-
if (
|
|
1026
|
+
while ((m = tre.exec(lines[i]))) {
|
|
1027
|
+
if (isInsideQuote(lines[i], m.index)) continue;
|
|
1028
|
+
todoCount++;
|
|
1029
|
+
const txt = lines[i].trim().slice(0, 120);
|
|
1030
|
+
const fileRel = rel(root, file);
|
|
1031
|
+
const key = `${fileRel}:${i + 1}:${txt}`;
|
|
1032
|
+
if (!knownSet.has(key)) newTodos.push({ file: fileRel, line: i + 1, text: txt });
|
|
1022
1033
|
}
|
|
1023
1034
|
}
|
|
1024
1035
|
}
|
|
1025
1036
|
if (todoCount > 0) {
|
|
1026
1037
|
const hasTodoTask = rows.some(r => /TODO|FIXME|XXX/.test(r.request) || /TODO|FIXME|XXX/i.test(r.evidence));
|
|
1027
|
-
if (!hasTodoTask) {
|
|
1038
|
+
if (!hasTodoTask) {
|
|
1039
|
+
issues++;
|
|
1040
|
+
warn(`code has ${todoCount} TODO/FIXME/XXX (new: ${newTodos.length}) but no progress-tracker entry tracks them`);
|
|
1041
|
+
// 새 TODO 처음 5개 표시
|
|
1042
|
+
newTodos.slice(0, 5).forEach(t => log(` ${t.file}:${t.line} ${t.text}`));
|
|
1043
|
+
if (has('--auto-track') && newTodos.length) {
|
|
1044
|
+
for (const t of newTodos) {
|
|
1045
|
+
const id = nextId(root, 'T');
|
|
1046
|
+
upsertProgress(root, { id, status: 'requested', request: `TODO ${t.file}:${t.line}`, evidence: 'auto-tracked', nextAction: t.text.slice(0, 80) });
|
|
1047
|
+
}
|
|
1048
|
+
// known-todos에 추가 — 다음 detect에서 재카운트 안 하도록
|
|
1049
|
+
const merged = [...knownList, ...newTodos.map(t => ({ ...t, ackAt: now() }))];
|
|
1050
|
+
writeUtf8(knownPath, JSON.stringify(merged, null, 2) + '\n');
|
|
1051
|
+
ok(`${newTodos.length}개 TODO를 progress-tracker에 자동 등록 + known-todos.json 갱신`);
|
|
1052
|
+
} else if (newTodos.length) {
|
|
1053
|
+
log(` 💡 자동 등록: leerness lazy detect --auto-track`);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1028
1056
|
}
|
|
1029
1057
|
const blockers = rows.filter(r => r.status === 'blocked');
|
|
1030
1058
|
for (const b of blockers) if (b.nextAction === '없음' || /다음 액션 작성/.test(b.nextAction)) { issues++; warn(`blocker without nextAction: ${b.id}`); }
|
|
@@ -1206,6 +1234,114 @@ function gate(root) {
|
|
|
1206
1234
|
else ok('all gates passed');
|
|
1207
1235
|
}
|
|
1208
1236
|
|
|
1237
|
+
// ===== 1.9.7 A: verify-code — npm scripts 자동 감지 + evidence 자동 기록 =====
|
|
1238
|
+
function verifyCodeCmd(root) {
|
|
1239
|
+
root = absRoot(root);
|
|
1240
|
+
const pkgFile = path.join(root, 'package.json');
|
|
1241
|
+
if (!exists(pkgFile)) return fail('package.json 없음 — Node 프로젝트 위치에서 실행하세요.');
|
|
1242
|
+
let pkg;
|
|
1243
|
+
try { pkg = JSON.parse(read(pkgFile)); } catch (e) { return fail('package.json 파싱 실패: ' + e.message); }
|
|
1244
|
+
const scripts = pkg.scripts || {};
|
|
1245
|
+
const tasks = [];
|
|
1246
|
+
if (scripts.test) tasks.push({ name: 'test', cmd: 'npm test' });
|
|
1247
|
+
else if (scripts['test:smoke']) tasks.push({ name: 'test', cmd: 'npm run test:smoke' });
|
|
1248
|
+
if (scripts.lint) tasks.push({ name: 'lint', cmd: 'npm run lint' });
|
|
1249
|
+
if (scripts.typecheck) tasks.push({ name: 'typecheck', cmd: 'npm run typecheck' });
|
|
1250
|
+
else if (scripts.tsc) tasks.push({ name: 'typecheck', cmd: 'npm run tsc' });
|
|
1251
|
+
else if (exists(path.join(root, 'tsconfig.json'))) tasks.push({ name: 'typecheck', cmd: 'npx --yes tsc --noEmit', optional: true });
|
|
1252
|
+
if (has('--build') && scripts.build) tasks.push({ name: 'build', cmd: 'npm run build' });
|
|
1253
|
+
if (!tasks.length) {
|
|
1254
|
+
warn('실행할 검증 task 없음 (package.json#scripts에 test/lint/typecheck 추가하세요)');
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
log(`# verify-code (${tasks.length}개)`);
|
|
1258
|
+
let failedCnt = 0;
|
|
1259
|
+
const results = [];
|
|
1260
|
+
for (const t of tasks) {
|
|
1261
|
+
log(`\n## ${t.name}: ${t.cmd}`);
|
|
1262
|
+
const start = Date.now();
|
|
1263
|
+
const r = cp.spawnSync(t.cmd, [], { cwd: root, encoding: 'utf8', shell: true, timeout: 5 * 60 * 1000 });
|
|
1264
|
+
const dur = Date.now() - start;
|
|
1265
|
+
if (r.status === 0) ok(`${t.name} passed (${dur}ms)`);
|
|
1266
|
+
else if (t.optional && r.status === 127) warn(`${t.name} 스킵 (${t.cmd} 없음)`);
|
|
1267
|
+
else { fail(`${t.name} failed (exit ${r.status}, ${dur}ms)`); failedCnt++; }
|
|
1268
|
+
const tail = (r.stdout || '').split('\n').slice(-8).join('\n').slice(0, 400);
|
|
1269
|
+
results.push({ name: t.name, cmd: t.cmd, exit: r.status, durMs: dur, tail });
|
|
1270
|
+
}
|
|
1271
|
+
const evBlock = [
|
|
1272
|
+
``,
|
|
1273
|
+
`## ${now().slice(0, 16)} verify-code (자동)`,
|
|
1274
|
+
`Command: leerness verify-code`,
|
|
1275
|
+
`Tasks: ${tasks.map(t => t.name).join(', ')}`,
|
|
1276
|
+
...results.map(r => `- ${r.name}: exit=${r.exit} (${r.durMs}ms) — \`${r.cmd}\``),
|
|
1277
|
+
`Tail:`,
|
|
1278
|
+
'```',
|
|
1279
|
+
results.map(r => `[${r.name}]\n${r.tail}`).join('\n---\n').slice(0, 1500),
|
|
1280
|
+
'```'
|
|
1281
|
+
].join('\n');
|
|
1282
|
+
append(evidencePath(root), evBlock + '\n');
|
|
1283
|
+
ok(`evidence 기록: .harness/review-evidence.md`);
|
|
1284
|
+
if (failedCnt) { process.exitCode = 1; warn(`${failedCnt}개 task 실패 — progress의 해당 row를 incomplete로 표시하세요.`); }
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// ===== 1.9.7 B: lessons — 과거 결정/실수 자동 회수 =====
|
|
1288
|
+
function lessonsCmd(root) {
|
|
1289
|
+
root = absRoot(root);
|
|
1290
|
+
const query = arg('--query', null);
|
|
1291
|
+
const limit = parseInt(arg('--limit', '10'), 10);
|
|
1292
|
+
const decisions = exists(decisionsPath(root)) ? read(decisionsPath(root)) : '';
|
|
1293
|
+
const evidence = exists(evidencePath(root)) ? read(evidencePath(root)) : '';
|
|
1294
|
+
const tlog = exists(taskLogPath(root)) ? read(taskLogPath(root)) : '';
|
|
1295
|
+
const handoff = exists(handoffPath(root)) ? read(handoffPath(root)) : '';
|
|
1296
|
+
const lessons = [];
|
|
1297
|
+
// decisions: ### 블록 전체
|
|
1298
|
+
for (const block of decisions.split(/\n(?=### )/)) {
|
|
1299
|
+
if (!block.startsWith('### ')) continue;
|
|
1300
|
+
const m = block.match(/^### (.+)$/m);
|
|
1301
|
+
if (!m) continue;
|
|
1302
|
+
lessons.push({ source: 'decisions.md', title: m[1].trim(), block });
|
|
1303
|
+
}
|
|
1304
|
+
// evidence: ## 블록 중 실패/롤백/버그 표지가 있는 것
|
|
1305
|
+
for (const block of evidence.split(/\n(?=## )/)) {
|
|
1306
|
+
if (!block.startsWith('## ')) continue;
|
|
1307
|
+
if (/✗|\bfail(ed)?\b|롤백|재발|incomplete|\bbug\b|버그|warning/i.test(block)) {
|
|
1308
|
+
const m = block.match(/^## (.+)$/m);
|
|
1309
|
+
if (m) lessons.push({ source: 'review-evidence.md', title: m[1].trim(), block });
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
// task-log: 실패 키워드 라인
|
|
1313
|
+
for (const line of tlog.split('\n')) {
|
|
1314
|
+
if (line.length > 4 && /✗|\bfail|롤백|재발|incomplete|버그/i.test(line)) {
|
|
1315
|
+
lessons.push({ source: 'task-log.md', title: line.replace(/^[-*]\s*/, '').slice(0, 80), block: line });
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
// handoff: 미완료/블로커 항목
|
|
1319
|
+
if (handoff) {
|
|
1320
|
+
const incompleteSec = handoff.match(/## Incomplete[\s\S]*?(?=\n## |$)/);
|
|
1321
|
+
if (incompleteSec && incompleteSec[0].split('\n').slice(1).some(l => /^- (?!없음)/.test(l))) {
|
|
1322
|
+
lessons.push({ source: 'session-handoff.md', title: 'Incomplete / Blocked from last session', block: incompleteSec[0] });
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
let filtered = lessons;
|
|
1326
|
+
if (query) {
|
|
1327
|
+
const q = new RegExp(escapeRegex(query), 'i');
|
|
1328
|
+
filtered = lessons.filter(l => q.test(l.title) || q.test(l.block));
|
|
1329
|
+
}
|
|
1330
|
+
log(`# Lessons${query ? ` — query="${query}"` : ''}`);
|
|
1331
|
+
if (!filtered.length) {
|
|
1332
|
+
if (query) ok(`"${query}" 관련 과거 lessons 없음`);
|
|
1333
|
+
else ok('과거 lessons 없음 (decisions/evidence가 비어있거나 실패 표지 없음)');
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
log(`총 ${filtered.length}건 발견:`);
|
|
1337
|
+
for (const l of filtered.slice(0, limit)) {
|
|
1338
|
+
log(`\n[${l.source}] ${l.title}`);
|
|
1339
|
+
const preview = l.block.replace(/\n+/g, ' ').replace(/\s{2,}/g, ' ').slice(0, 240);
|
|
1340
|
+
log(` ${preview}${l.block.length > 240 ? '…' : ''}`);
|
|
1341
|
+
}
|
|
1342
|
+
if (filtered.length > limit) log(`\n💡 ${filtered.length - limit}개 더 있음 — --limit ${filtered.length}`);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1209
1345
|
// ===== 1.9.3: Causal / reuse / consistency =====
|
|
1210
1346
|
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']);
|
|
1211
1347
|
function* walkCode(root, base = root, depth = 0, extras = null) {
|
|
@@ -1432,6 +1568,13 @@ function guideCmd(root, target) {
|
|
|
1432
1568
|
log('');
|
|
1433
1569
|
log('## 3. UI consistency — 디자인 토큰 일치');
|
|
1434
1570
|
uiConsistency(root);
|
|
1571
|
+
log('');
|
|
1572
|
+
log('## 4. Lessons — 과거 결정/실수 회수 (1.9.7)');
|
|
1573
|
+
if (q) {
|
|
1574
|
+
// lessonsCmd가 arg('--query')를 읽으므로 임시로 push
|
|
1575
|
+
if (!process.argv.includes('--query')) { process.argv.push('--query', q); }
|
|
1576
|
+
lessonsCmd(root);
|
|
1577
|
+
} else log('(target/--query 없음 — lessons 검색 스킵)');
|
|
1435
1578
|
log('\n💡 다음 단계: 위 결과를 바탕으로 작업 계획을 plan/progress에 기록 후 진행하세요.');
|
|
1436
1579
|
}
|
|
1437
1580
|
|
|
@@ -1608,7 +1751,10 @@ function viewworkInstall(root) {
|
|
|
1608
1751
|
}
|
|
1609
1752
|
|
|
1610
1753
|
function help() {
|
|
1611
|
-
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|relink [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
|
|
1754
|
+
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|relink [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
|
|
1755
|
+
leerness verify-code [path] [--build] # npm test/lint/typecheck 자동 실행 + evidence 자동 기록 (1.9.7)
|
|
1756
|
+
leerness lessons [--query <키>] [--limit N] # 과거 결정/실수 자동 회수 (1.9.7)
|
|
1757
|
+
leerness lazy detect [path] [--auto-track] # --auto-track으로 새 TODO를 progress에 자동 등록 (1.9.7)\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`);
|
|
1612
1758
|
}
|
|
1613
1759
|
|
|
1614
1760
|
async function main() {
|
|
@@ -1647,6 +1793,8 @@ async function main() {
|
|
|
1647
1793
|
if (cmd === 'skill' && args[1] === 'remove') return skillRemove(absRoot(arg('--path', process.cwd())), args[2]);
|
|
1648
1794
|
if (cmd === 'skill' && args[1] === 'consolidate') return skillConsolidate(absRoot(arg('--path', process.cwd())));
|
|
1649
1795
|
if (cmd === 'gate') return gate(args[1] || process.cwd());
|
|
1796
|
+
if (cmd === 'verify-code') return verifyCodeCmd(args[1] || process.cwd());
|
|
1797
|
+
if (cmd === 'lessons') return lessonsCmd(arg('--path', process.cwd()));
|
|
1650
1798
|
if (cmd === 'impact') return impactCmd(arg('--path', process.cwd()), args[1]);
|
|
1651
1799
|
if (cmd === 'reuse' && args[1] === 'find') return reuseFind(arg('--path', process.cwd()), args.slice(2).filter(x => !x.startsWith('-')).join(' '));
|
|
1652
1800
|
if (cmd === 'reuse' && args[1] === 'register') return reuseRegister(arg('--path', process.cwd()), args[2]);
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -235,6 +235,94 @@ total++;
|
|
|
235
235
|
if (!(strongOK && weakHint)) failed++;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
// 1.9.7 A: verify-code — 가짜 package.json + 통과 시나리오
|
|
239
|
+
total++;
|
|
240
|
+
{
|
|
241
|
+
const tmpV = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-vc-'));
|
|
242
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpV, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore' });
|
|
243
|
+
fs.writeFileSync(path.join(tmpV, 'package.json'), JSON.stringify({ name: 't', version: '0.0.1', scripts: { test: 'node -e "console.log(\\"OK\\");process.exit(0)"' } }));
|
|
244
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'verify-code', tmpV], { encoding: 'utf8' });
|
|
245
|
+
const ev = fs.readFileSync(path.join(tmpV, '.harness/review-evidence.md'), 'utf8');
|
|
246
|
+
const ok = r.status === 0 && /test passed/.test(r.stdout) && /verify-code \(자동\)/.test(ev);
|
|
247
|
+
console.log(ok ? '✓ B(1.9.7-A) verify-code: 통과 + evidence 자동 기록' : `✗ A 실패\n${r.stdout}`);
|
|
248
|
+
if (!ok) failed++;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 1.9.7 A: verify-code — 실패 시나리오
|
|
252
|
+
total++;
|
|
253
|
+
{
|
|
254
|
+
const tmpV2 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-vc2-'));
|
|
255
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpV2, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore' });
|
|
256
|
+
fs.writeFileSync(path.join(tmpV2, 'package.json'), JSON.stringify({ name: 't', version: '0.0.1', scripts: { test: 'node -e "process.exit(2)"' } }));
|
|
257
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'verify-code', tmpV2], { encoding: 'utf8' });
|
|
258
|
+
const ok = r.status === 1 && /test failed/.test(r.stdout);
|
|
259
|
+
console.log(ok ? '✓ B(1.9.7-A) verify-code: 실패 시 exit=1' : `✗ A2 실패 status=${r.status}`);
|
|
260
|
+
if (!ok) failed++;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 1.9.7 B: lessons — decisions/evidence에 시드 후 query로 회수
|
|
264
|
+
total++;
|
|
265
|
+
{
|
|
266
|
+
fs.appendFileSync(path.join(tmp, '.harness/decisions.md'), `\n### 2026-05-08 — Decision: 캐시 차등 TTL 도입\n- Reason: 단일 5분 TTL이 daily 데이터에 비효율\n- Impact: open-meteo 응답 캐시 적중률 ↑\n`);
|
|
267
|
+
fs.appendFileSync(path.join(tmp, '.harness/review-evidence.md'), `\n## 2026-05-08 e2e\n✗ 캐시 키 불안정 — 좌표 정규화 부재 (롤백 후 fix)\n`);
|
|
268
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'lessons', '--query', '캐시', '--path', tmp], { encoding: 'utf8' });
|
|
269
|
+
const ok = r.status === 0 && /Lessons.*query="캐시"/.test(r.stdout) && /decisions\.md/.test(r.stdout) && /review-evidence\.md/.test(r.stdout);
|
|
270
|
+
console.log(ok ? '✓ B(1.9.7-B) lessons: decisions+evidence 회수' : `✗ B 실패\n${r.stdout}`);
|
|
271
|
+
if (!ok) failed++;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// 1.9.7 B: guide가 lessons를 자동 통합
|
|
275
|
+
total++;
|
|
276
|
+
{
|
|
277
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'guide', 'src/components/Card.html', '--path', tmp], { encoding: 'utf8' });
|
|
278
|
+
const ok = r.status === 0 && /## 4\. Lessons/.test(r.stdout);
|
|
279
|
+
console.log(ok ? '✓ B(1.9.7-B) guide: lessons 섹션 자동 추가' : '✗ B guide 통합 실패');
|
|
280
|
+
if (!ok) failed++;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 1.9.7 C: lazy detect --auto-track
|
|
284
|
+
total++;
|
|
285
|
+
{
|
|
286
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-c-'));
|
|
287
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore' });
|
|
288
|
+
// 의도된 진짜 TODO (주석)
|
|
289
|
+
fs.mkdirSync(path.join(tmpC, 'src'), { recursive: true });
|
|
290
|
+
fs.writeFileSync(path.join(tmpC, 'src/a.js'), `// TODO: 추적해야 할 미완료 작업\nfunction foo() {}\n`);
|
|
291
|
+
// review-evidence에 npm test 키워드 추가 (lazy detect의 다른 신호 우회)
|
|
292
|
+
fs.appendFileSync(path.join(tmpC, '.harness/review-evidence.md'), '\n## seed\nCommand: npm test\n');
|
|
293
|
+
// session close로 handoff 채우기
|
|
294
|
+
cp.spawnSync(process.execPath, [CLI, 'session', 'close', tmpC], { stdio: 'ignore' });
|
|
295
|
+
// --auto-track 실행
|
|
296
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'lazy', 'detect', tmpC, '--auto-track'], { encoding: 'utf8' });
|
|
297
|
+
const tracker = fs.readFileSync(path.join(tmpC, '.harness/progress-tracker.md'), 'utf8');
|
|
298
|
+
const known = fs.existsSync(path.join(tmpC, '.harness/known-todos.json')) ? JSON.parse(fs.readFileSync(path.join(tmpC, '.harness/known-todos.json'), 'utf8')) : [];
|
|
299
|
+
const ok = /TODO src\/a\.js:1/.test(tracker) && /auto-tracked/.test(tracker) && known.length === 1;
|
|
300
|
+
console.log(ok ? '✓ B(1.9.7-C) lazy detect --auto-track: 자동 등록 + known-todos.json' : `✗ C 실패\nTracker:\n${tracker.split('\n').filter(l=>l.startsWith('| T-')).slice(-3).join('\n')}\nKnown: ${JSON.stringify(known)}`);
|
|
301
|
+
if (!ok) failed++;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 1.9.7 C: 같은 TODO 재실행 시 known-todos가 적용되어 newTodos 0
|
|
305
|
+
total++;
|
|
306
|
+
{
|
|
307
|
+
// 위 시나리오 이어서 — known-todos가 있으므로 새 TODO=0이어야 함
|
|
308
|
+
// 별도 새 tmp로 재현 (tmpC는 위에서 자동 등록됐으니 same dir에서 다시 호출)
|
|
309
|
+
// 위 tmpC는 already auto-tracked
|
|
310
|
+
const tmpC2 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-c2-'));
|
|
311
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC2, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore' });
|
|
312
|
+
fs.mkdirSync(path.join(tmpC2, 'src'), { recursive: true });
|
|
313
|
+
fs.writeFileSync(path.join(tmpC2, 'src/a.js'), `// TODO: 추적된 항목\n`);
|
|
314
|
+
// known-todos.json에 미리 등록
|
|
315
|
+
fs.writeFileSync(path.join(tmpC2, '.harness/known-todos.json'), JSON.stringify([{ file: 'src/a.js', line: 1, text: '// TODO: 추적된 항목', ackAt: '2026-05-08T00:00:00Z' }]));
|
|
316
|
+
fs.appendFileSync(path.join(tmpC2, '.harness/review-evidence.md'), '\n## seed\nCommand: npm test\n');
|
|
317
|
+
cp.spawnSync(process.execPath, [CLI, 'session', 'close', tmpC2], { stdio: 'ignore' });
|
|
318
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'lazy', 'detect', tmpC2], { encoding: 'utf8' });
|
|
319
|
+
// newTodos가 0이므로 "new: 0" 또는 TODO 카운트가 1이지만 progress 추적에 자동 등록 안 됨
|
|
320
|
+
// 핵심: TODO 1개 잡혀도 known이라 새 항목 노출 X
|
|
321
|
+
const ok = /new: 0\b/.test(r.stdout) || !/💡 자동 등록/.test(r.stdout);
|
|
322
|
+
console.log(ok ? '✓ B(1.9.7-C) known-todos: 재카운트 회피' : `✗ C2 실패\n${r.stdout}`);
|
|
323
|
+
if (!ok) failed++;
|
|
324
|
+
}
|
|
325
|
+
|
|
238
326
|
// 1.9.6 회귀: task relink — 인위적 link 손실 → 자동 매칭 제안 + --apply 자동 복구
|
|
239
327
|
total++;
|
|
240
328
|
{
|