leerness 1.9.18 → 1.9.20

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 CHANGED
@@ -1,5 +1,48 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.20 — 2026-05-14
4
+
5
+ **verify-claim 정확도 + 도메인 확장 — Godot/jest/mocha 지원, verify-code --bench**.
6
+
7
+ 1.9.19를 실전 RPG 워크스페이스에 쓰면서 발견한 3가지 한계를 모두 보완. **Round 4 (rpg-godot) 작업에서 실제로 false negative 발생**한 케이스가 동기.
8
+
9
+ ### Added / Changed
10
+
11
+ - **`verify-claim` file path 인식 확장**: 1.9.19까지는 `src/bin/tests/public/lib` prefix만 인식 → Godot의 `scenes/*.tscn`, `scripts/*.gd`, 루트 `project.godot` 등 미검출. 1.9.20부터 **확장자 화이트리스트 기반**으로 변경. dir prefix는 optional, 확장자는 길이 내림차순 정렬로 `.ts` vs `.tscn` 정확히 구분.
12
+ - 신규 지원 확장자: `tscn / tres / godot / gd / cs / py / rb / go / rs / kt / sh / mdx / json5 / yaml / scss / sass / less / gltf / dockerfile / webmanifest` 등
13
+ - **`verify-claim --run-tests` stdout 파싱 확장**: 1.9.19까지는 `X/Y 통과/passed/pass` 만 인식. 1.9.20부터 **jest** (`Tests: 12 passed, 12 total`), **mocha** (`7 passing`), **tap** (`# pass 5`) 형식도 자동 인식. evidence 컬럼 파싱에도 동일 패턴 적용.
14
+ - **`verify-code --bench`**: `package.json#scripts.bench`가 있으면 추가 실행. 성능 metric을 `.harness/review-evidence.md`에 자동 누적. 1.9.19에서 별도 `perf record` 명령 추가 대신 기존 verify-code 확장으로 통합 — 의존성 0, 워크플로 일관.
15
+
16
+ ### Why
17
+ - 1.9.19를 사용한 RPG 워크스페이스에서 `verify-claim T-0002 --path _apps/rpg-godot` 실행 시 evidence "project.godot + scenes/main.tscn + scripts/network.gd + scripts/main.gd"가 0건 검출. 1.9.20에서 **4/4 모두 정확 검출** 확인.
18
+ - 외부 npm 패키지가 jest/mocha를 쓰는 경우 evidence나 stdout이 한국어가 아니어도 자동 인식.
19
+ - 부하 측정 같은 동적 metric을 회고에서 추적 가능하도록 evidence 누적 채널 확장.
20
+
21
+ ### Migration
22
+ ```bash
23
+ npx leerness@latest update . --yes
24
+ ```
25
+
26
+ ## 1.9.19 — 2026-05-14
27
+
28
+ **1.9.18 후속 다듬기 — verify-claim에 동적 실행, --strict-elements 정확도 강화**.
29
+
30
+ 1.9.18을 실전 sub-agent 검수에 쓰면서 발견한 두 가지 가공할 점을 마저 보완.
31
+
32
+ ### Added
33
+
34
+ - **`leerness verify-claim --run-tests`**: 정적 점검(파일 존재 + 테스트 카운트)에 더해 `npm test`를 동적으로 실행. stdout에서 `X/Y passed` 패턴을 파싱해 evidence 주장과 비교. 주장이 `5/5 통과`인데 실제 `3/5`면 exit 1. `--json`에 `run.parsed`, `verdict.declaredPassMatches` 포함.
35
+ - **`--strict-elements` 출력 강화**: 같은 함수명이라도 (a) 같은 파일이면 `⚠ 진짜 중복 가능`, (b) 다른 파일이면 `ℹ 의도 분리 가능` (예: 모듈 함수 vs CLI 명령)으로 분류. 1.9.18의 평면 출력보다 false positive 식별이 쉬워짐.
36
+
37
+ ### Why
38
+ - `verify-claim`만으로는 "파일이 있고 check() 호출이 많다" 정도까지만 보장. `--run-tests`가 추가되면 메인 에이전트가 sub-agent의 evidence를 **한 번의 명령으로 정적+동적 모두 검증**.
39
+ - 1.9.18 `--strict-elements`가 city-insights의 `MemoStats`/`StatsCli`(둘 다 `stats()` 함수, 다른 파일) 같은 의도된 분리를 잠재 중복으로 평면 표시 → 사용자가 직접 판별해야 했음. 1.9.19에선 정보를 더 줘서 즉시 분류 가능.
40
+
41
+ ### Migration
42
+ ```bash
43
+ npx leerness@latest update . --yes
44
+ ```
45
+
3
46
  ## 1.9.18 — 2026-05-14
4
47
 
5
48
  **오케스트레이션 검수 패키지 — `--since` 시간 필터 + `--strict-elements` 잠재 중복 + `depends-on` 그래프 + `verify-claim` 자동 검증**.
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.18';
9
+ const VERSION = '1.9.20';
10
10
  const MARK = '<!-- leerness:managed -->';
11
11
  const README_START = '<!-- leerness:project-readme:start -->';
12
12
  const README_END = '<!-- leerness:project-readme:end -->';
@@ -1445,18 +1445,25 @@ function reuseMapCmd(root) {
1445
1445
  log(` 💡 권장: 가장 안정적인 1개 구현을 추출해 ${dupes.length}건 중복을 공통 모듈로 통합 검토`);
1446
1446
  }
1447
1447
 
1448
- // 1.9.18: --strict-elements 결과
1448
+ // 1.9.18→1.9.19: --strict-elements 결과 (false-positive 줄이기 위해 same-file vs diff-file 구분)
1449
1449
  if (strictElements) {
1450
1450
  log('');
1451
1451
  log(`## 🔍 잠재 중복 (--strict-elements: 함수명 동일 / capability 이름 다름)`);
1452
1452
  if (!funcDupes.length) log(' (없음) — 동일 함수명을 다른 capability로 등록한 경우 없음');
1453
1453
  else {
1454
+ let exactMatches = 0; // 같은 파일 + 같은 함수 (진짜 중복 가능성 ↑)
1455
+ let intentionalSplits = 0; // 같은 함수 / 다른 파일 (의도 분리 가능성 ↑)
1454
1456
  for (const [fn, occ] of funcDupes) {
1455
- log(` - 함수 "${fn}()" ${occ.length}건 (이름 다름)`);
1457
+ const files = new Set(occ.map(o => o.entry.filePath));
1458
+ const sameFile = files.size === 1;
1459
+ const tag = sameFile ? '⚠ 진짜 중복 가능' : 'ℹ 의도 분리 가능';
1460
+ if (sameFile) exactMatches++; else intentionalSplits++;
1461
+ log(` - 함수 "${fn}()" — ${occ.length}건 ${tag}`);
1456
1462
  for (const o of occ) log(` · ${o.project}/${o.entry.capability}: ${o.entry.element}`);
1457
1463
  }
1458
1464
  log('');
1459
- log(` 💡 같은 함수를 다른 capability 이름으로 부르고 있을 가능성. 명명 통일 검토.`);
1465
+ if (exactMatches > 0) log(` 같은 파일 + 같은 함수: ${exactMatches}건 명명 통일 또는 실제 통합 검토`);
1466
+ if (intentionalSplits > 0) log(` ℹ 다른 파일 + 같은 함수: ${intentionalSplits}건 — 의도된 분리(예: 모듈 함수 vs CLI 명령)일 가능성. 보고용`);
1460
1467
  }
1461
1468
  }
1462
1469
 
@@ -1485,14 +1492,40 @@ function verifyClaimCmd(root, taskId) {
1485
1492
  if (!row) return fail(`progress-tracker.md에 ${taskId} 없음.`);
1486
1493
 
1487
1494
  const evidence = row.evidence || '';
1488
- // 파일 경로 추출: src/x.js, bin/y.js, tests/z.js
1489
- const filePatterns = evidence.match(/(?:src|bin|tests|public|lib)\/[\w./-]+\.(?:js|ts|html|css|json|md|webmanifest|xml)/g) || [];
1495
+ // 1.9.20: 파일 경로 추출 도메인 폴더 자동 인식 + 루트 메타파일
1496
+ // (1.9.19까지: src|bin|tests|public|lib 하드코딩 → Godot scenes/scripts 미검출)
1497
+ // 변경: 확장자 화이트리스트 기반. 디렉토리는 선택적 (project.godot 같은 루트 파일도 잡음).
1498
+ // 확장자는 길이 내림차순(긴 것 먼저 매치) + \b 종결로 .ts vs .tscn 구분.
1499
+ const FILE_EXTS = 'webmanifest|dockerfile|tscn|tres|godot|json5|jsx|tsx|yaml|html|scss|sass|less|gltf|json|toml|mdx|xml|css|svg|yml|md|js|ts|gd|cs|py|rb|go|rs|kt|sh|h';
1500
+ const FILE_RE = new RegExp(`(?:[A-Za-z][A-Za-z0-9_-]*\\/)?[A-Za-z][\\w./-]*\\.(?:${FILE_EXTS})\\b`, 'g');
1501
+ const filePatterns = evidence.match(FILE_RE) || [];
1502
+ // 중복 제거 + "tests/test.js" 같은 결과를 유지 (이미 `..` 없으니 그대로)
1490
1503
  const files = Array.from(new Set(filePatterns));
1491
- // 테스트 수 / pass 비율: "X/Y 통과" 또는 "X개 테스트"
1492
- const passMatch = evidence.match(/(\d+)\s*\/\s*(\d+)\s*(통과|passed|pass)/);
1493
- const testCountMatch = evidence.match(/(\d+)\s*개\s*테스트/);
1494
- const declaredPass = passMatch ? { num: parseInt(passMatch[1], 10), denom: parseInt(passMatch[2], 10) } : null;
1495
- const declaredTestCount = testCountMatch ? parseInt(testCountMatch[1], 10) : null;
1504
+ // 1.9.20: 테스트 수 파싱 확장 한국어 + jest/mocha/tap/vitest
1505
+ // 우선순위: 명시적 X/Y 비율 > N passing/passed > N개 테스트
1506
+ let declaredPass = null;
1507
+ let declaredTestCount = null;
1508
+ // 1) X/Y 통과·passed·pass (한·영)
1509
+ const m1 = evidence.match(/(\d+)\s*\/\s*(\d+)\s*(?:통과|passed|pass|passing)/i);
1510
+ if (m1) declaredPass = { num: parseInt(m1[1], 10), denom: parseInt(m1[2], 10) };
1511
+ // 2) jest: "Tests: N passed" 또는 "N passed, M failed"
1512
+ if (!declaredPass) {
1513
+ const m2 = evidence.match(/Tests?:\s*(?:\d+\s*failed,\s*)?(\d+)\s*passed(?:,\s*(\d+)\s*total)?/i);
1514
+ if (m2) declaredPass = { num: parseInt(m2[1], 10), denom: parseInt(m2[2] || m2[1], 10) };
1515
+ }
1516
+ // 3) mocha: "N passing" (실패 없을 때 total = passing)
1517
+ if (!declaredPass) {
1518
+ const m3 = evidence.match(/(\d+)\s+passing\b/i);
1519
+ if (m3) declaredPass = { num: parseInt(m3[1], 10), denom: parseInt(m3[1], 10) };
1520
+ }
1521
+ // 4) N개 테스트 (단순 카운트)
1522
+ const m4 = evidence.match(/(\d+)\s*개\s*테스트/);
1523
+ if (m4) declaredTestCount = parseInt(m4[1], 10);
1524
+ // 5) N tests (영문 단순 카운트)
1525
+ if (!declaredTestCount) {
1526
+ const m5 = evidence.match(/(\d+)\s*tests?\b/i);
1527
+ if (m5) declaredTestCount = parseInt(m5[1], 10);
1528
+ }
1496
1529
 
1497
1530
  // 실제 파일 존재 검사
1498
1531
  const fileChecks = files.map(f => ({ file: f, exists: exists(path.join(root, f)) }));
@@ -1508,8 +1541,53 @@ function verifyClaimCmd(root, taskId) {
1508
1541
  }
1509
1542
  }
1510
1543
 
1544
+ // 1.9.19: --run-tests — npm test 자동 실행 + pass/fail 파싱
1545
+ let runResult = null;
1546
+ if (has('--run-tests')) {
1547
+ const pkgPath = path.join(root, 'package.json');
1548
+ if (!exists(pkgPath)) {
1549
+ runResult = { skipped: true, reason: 'package.json 없음' };
1550
+ } else {
1551
+ let pkg = null;
1552
+ try { pkg = JSON.parse(read(pkgPath)); } catch {}
1553
+ const hasTestScript = pkg && pkg.scripts && pkg.scripts.test;
1554
+ if (!hasTestScript) {
1555
+ runResult = { skipped: true, reason: 'scripts.test 없음' };
1556
+ } else {
1557
+ const r = cp.spawnSync('npm test', [], { cwd: root, encoding: 'utf8', shell: true, timeout: 5 * 60 * 1000 });
1558
+ const out = (r.stdout || '') + (r.stderr || '');
1559
+ // 1.9.20: 파싱 패턴 확장 — 한국어 + jest/mocha/tap/vitest
1560
+ let parsed = null;
1561
+ // 1) X/Y passing|passed|pass|통과
1562
+ let m = out.match(/(\d+)\s*\/\s*(\d+)\s*(?:passed|통과|pass|passing)/i);
1563
+ if (m) parsed = { num: parseInt(m[1], 10), denom: parseInt(m[2], 10) };
1564
+ // 2) jest: "Tests: N passed, M total" — 통과 + 총
1565
+ if (!parsed) {
1566
+ const m2 = out.match(/Tests?:\s*(?:\d+\s*failed,\s*)?(\d+)\s*passed(?:,\s*(\d+)\s*total)?/i);
1567
+ if (m2) parsed = { num: parseInt(m2[1], 10), denom: parseInt(m2[2] || m2[1], 10) };
1568
+ }
1569
+ // 3) mocha: "N passing" — 단독 패턴이면 total = passing
1570
+ if (!parsed) {
1571
+ const m3 = out.match(/^\s*(\d+)\s+passing\b/im);
1572
+ if (m3) parsed = { num: parseInt(m3[1], 10), denom: parseInt(m3[1], 10) };
1573
+ }
1574
+ // 4) tap: "# pass N" 또는 "ok N"
1575
+ if (!parsed) {
1576
+ const m4 = out.match(/#\s*pass\s+(\d+)/i);
1577
+ if (m4) parsed = { num: parseInt(m4[1], 10), denom: parseInt(m4[1], 10) };
1578
+ }
1579
+ runResult = {
1580
+ skipped: false,
1581
+ exitCode: r.status,
1582
+ parsed,
1583
+ allPassed: r.status === 0 && (!parsed || (parsed && parsed.num === parsed.denom))
1584
+ };
1585
+ }
1586
+ }
1587
+ }
1588
+
1511
1589
  if (has('--json')) {
1512
- log(JSON.stringify({
1590
+ const out = {
1513
1591
  project: path.basename(root),
1514
1592
  taskId, row,
1515
1593
  declared: { files: files.length, pass: declaredPass, testCount: declaredTestCount },
@@ -1518,7 +1596,18 @@ function verifyClaimCmd(root, taskId) {
1518
1596
  filesAllExist: fileChecks.every(c => c.exists),
1519
1597
  testCountMatch: declaredTestCount == null || actualTestCount == null || actualTestCount >= declaredTestCount
1520
1598
  }
1521
- }, null, 2));
1599
+ };
1600
+ if (runResult) {
1601
+ out.run = runResult;
1602
+ out.verdict.runTests = !!runResult.allPassed;
1603
+ // declared pass와 실제 비교
1604
+ if (declaredPass && runResult.parsed) {
1605
+ out.verdict.declaredPassMatches = (runResult.parsed.num === declaredPass.num && runResult.parsed.denom === declaredPass.denom);
1606
+ }
1607
+ }
1608
+ log(JSON.stringify(out, null, 2));
1609
+ if (runResult && !runResult.skipped && !runResult.allPassed) return process.exit(1);
1610
+ if (!out.verdict.filesAllExist || !out.verdict.testCountMatch) return process.exit(1);
1522
1611
  return;
1523
1612
  }
1524
1613
 
@@ -1538,19 +1627,45 @@ function verifyClaimCmd(root, taskId) {
1538
1627
  if (declaredTestCount) log(` 주장 (개수): ${declaredTestCount}개`);
1539
1628
  if (actualTestCount != null) log(` 실측: tests/test.js에 ${actualTestCount}개 check/test 호출`);
1540
1629
  else log(` 실측: 테스트 파일 못 찾음 (tests/test.js 등)`);
1630
+
1631
+ // 1.9.19: --run-tests 결과
1632
+ let runTestsOk = true;
1633
+ let declaredPassMatchesActual = true;
1634
+ if (runResult) {
1635
+ log('');
1636
+ log(`## 🚦 npm test 실행 (--run-tests)`);
1637
+ if (runResult.skipped) {
1638
+ log(` ⚠ skipped: ${runResult.reason}`);
1639
+ } else {
1640
+ log(` exit: ${runResult.exitCode}`);
1641
+ if (runResult.parsed) log(` 실행 결과: ${runResult.parsed.num}/${runResult.parsed.denom} ${runResult.parsed.num === runResult.parsed.denom ? 'passed' : 'partial'}`);
1642
+ else log(` (pass/fail 비율을 stdout에서 파싱 못함)`);
1643
+ runTestsOk = runResult.allPassed;
1644
+ if (declaredPass && runResult.parsed) {
1645
+ declaredPassMatchesActual = (runResult.parsed.num === declaredPass.num && runResult.parsed.denom === declaredPass.denom);
1646
+ log(` 주장 vs 실행: ${declaredPassMatchesActual ? '✓ 일치' : `⚠ 불일치 (주장 ${declaredPass.num}/${declaredPass.denom} ≠ 실행 ${runResult.parsed.num}/${runResult.parsed.denom})`}`);
1647
+ }
1648
+ }
1649
+ }
1650
+
1541
1651
  log('');
1542
1652
  const allFilesOk = fileChecks.every(c => c.exists);
1543
1653
  const testOk = declaredTestCount == null || actualTestCount == null || actualTestCount >= declaredTestCount;
1544
1654
  log(`## 종합`);
1545
1655
  log(` - 파일 모두 존재: ${allFilesOk ? '✓ pass' : '✗ FAIL (일부 누락)'}`);
1546
1656
  log(` - 테스트 카운트: ${testOk ? '✓ pass (실측 ≥ 주장)' : '⚠ 주장보다 적음'}`);
1547
- if (!allFilesOk || !testOk) {
1657
+ if (runResult && !runResult.skipped) {
1658
+ log(` - npm test 실행: ${runTestsOk ? '✓ all passed' : '✗ FAIL'}`);
1659
+ if (declaredPass) log(` - 주장과 실행 결과 일치: ${declaredPassMatchesActual ? '✓ pass' : '⚠ 다름'}`);
1660
+ }
1661
+ const overallFail = !allFilesOk || !testOk || (runResult && !runResult.skipped && !runTestsOk);
1662
+ if (overallFail) {
1548
1663
  log('');
1549
1664
  log(` ⚠ evidence 주장과 실제가 일치하지 않음 — task 상태 재검토 권장`);
1550
1665
  return process.exit(1);
1551
1666
  }
1552
1667
  log('');
1553
- log(` ✓ evidence 주장이 실제 파일·테스트와 일치`);
1668
+ log(` ✓ evidence 주장이 실제 파일·테스트${runResult && !runResult.skipped ? '·실행 결과' : ''}와 일치`);
1554
1669
  }
1555
1670
 
1556
1671
  function sessionClose(root) {
@@ -3045,6 +3160,8 @@ function verifyCodeCmd(root) {
3045
3160
  else if (scripts.tsc) tasks.push({ name: 'typecheck', cmd: 'npm run tsc' });
3046
3161
  else if (exists(path.join(root, 'tsconfig.json'))) tasks.push({ name: 'typecheck', cmd: 'npx --yes tsc --noEmit', optional: true });
3047
3162
  if (has('--build') && scripts.build) tasks.push({ name: 'build', cmd: 'npm run build' });
3163
+ // 1.9.20: --bench → scripts.bench 자동 실행 (성능 metric을 evidence에 누적)
3164
+ if (has('--bench') && scripts.bench) tasks.push({ name: 'bench', cmd: 'npm run bench', optional: true });
3048
3165
  if (!tasks.length) {
3049
3166
  warn('실행할 검증 task 없음 (package.json#scripts에 test/lint/typecheck 추가하세요)');
3050
3167
  return;
@@ -3548,7 +3665,7 @@ function viewworkInstall(root) {
3548
3665
  }
3549
3666
 
3550
3667
  function help() {
3551
- 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] [--all-apps] [--include p1,p2] [--since 24h|3d] [--json] # 1.9.17/18 워크스페이스\n leerness reuse-map [path] [--all-apps] [--include p1,p2] [--strict-elements] [--json] # 1.9.18 중복/잠재중복/depends-on\n leerness verify-claim <T-ID> [--path .] [--json] # 1.9.18 progress evidence 자동 검증\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
3668
+ 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] [--all-apps] [--include p1,p2] [--since 24h|3d] [--json] # 1.9.17/18 워크스페이스\n leerness reuse-map [path] [--all-apps] [--include p1,p2] [--strict-elements] [--json] # 1.9.18 중복/잠재중복/depends-on\n leerness verify-claim <T-ID> [--path .] [--run-tests] [--json] # 1.9.18-20 evidence 자동 검증 (1.9.20: scenes/scripts 등 도메인 폴더 + jest/mocha 파싱)\n leerness verify-code [path] [--build] [--bench] # 1.9.20 --bench: scripts.bench 추가 실행 + evidence 누적\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
3552
3669
  leerness retro [path] [--days 7] [--all-apps] [--include p1,p2] [--json] # 회고 (1.9.13~1.9.16)
3553
3670
  leerness insights [path] [--all-apps] [--include p1,p2] [--json] # 누적 통계 (1.9.13~1.9.16)
3554
3671
  leerness brainstorm "<주제>" [--all-apps] [--include p1,p2] [--json] # 브레인스토밍 (1.9.13~1.9.16)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.18",
3
+ "version": "1.9.20",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -467,6 +467,166 @@ total++;
467
467
  if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
468
468
  }
469
469
 
470
+ // 1.9.19 회귀: verify-claim --run-tests + --strict-elements same-file 구분
471
+ total++;
472
+ {
473
+ // verify-claim --run-tests: npm test 자동 실행 + 주장 vs 실행 결과 대조
474
+ const tmpR = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-rt-'));
475
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpR, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
476
+ // 최소 npm 프로젝트 + 단순 tests/test.js (5/5 pass)
477
+ fs.writeFileSync(path.join(tmpR, 'package.json'), JSON.stringify({
478
+ name: 'rt-fixture', version: '0.0.1', scripts: { test: 'node tests/test.js' }
479
+ }));
480
+ fs.mkdirSync(path.join(tmpR, 'src'), { recursive: true });
481
+ fs.mkdirSync(path.join(tmpR, 'tests'), { recursive: true });
482
+ fs.writeFileSync(path.join(tmpR, 'src/mod.js'), 'module.exports={};\n');
483
+ // 5 check 호출 + "5/5 passed" 직접 출력 (간단한 fixture)
484
+ fs.writeFileSync(path.join(tmpR, 'tests/test.js'),
485
+ "let p=0;function check(c){if(c)p++;}check(1);check(1);check(1);check(1);check(1);console.log(p+'/5 passed');if(p!==5)process.exit(1);\n");
486
+ fs.appendFileSync(path.join(tmpR, '.harness/progress-tracker.md'),
487
+ '| T-0050 | done | rt 작업 | src/mod.js + tests/test.js (5/5 통과) | next | 2026-05-14 |\n');
488
+ const r = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0050', '--path', tmpR, '--run-tests'], { encoding: 'utf8', timeout: 60000 });
489
+ const ok = r.status === 0
490
+ && /npm test 실행 \(--run-tests\)/.test(r.stdout)
491
+ && /실행 결과: 5\/5 passed/.test(r.stdout)
492
+ && /주장 vs 실행: ✓ 일치/.test(r.stdout)
493
+ && /npm test 실행: ✓ all passed/.test(r.stdout);
494
+ console.log(ok ? '✓ B(1.9.19) verify-claim --run-tests: npm test 실행 + 주장 일치' : '✗ --run-tests 실패');
495
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 600)); }
496
+ }
497
+
498
+ total++;
499
+ {
500
+ // --run-tests 실패 케이스: 5/5 주장 vs 실제 3/5 실행 → 불일치 + exit 1
501
+ const tmpF = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-rtf-'));
502
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpF, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
503
+ fs.writeFileSync(path.join(tmpF, 'package.json'), JSON.stringify({ name: 'rtf', version: '0.0.1', scripts: { test: 'node tests/test.js' } }));
504
+ fs.mkdirSync(path.join(tmpF, 'src'), { recursive: true });
505
+ fs.mkdirSync(path.join(tmpF, 'tests'), { recursive: true });
506
+ fs.writeFileSync(path.join(tmpF, 'src/mod.js'), 'module.exports={};\n');
507
+ fs.writeFileSync(path.join(tmpF, 'tests/test.js'),
508
+ "console.log('3/5 passed'); process.exit(1);\n");
509
+ fs.appendFileSync(path.join(tmpF, '.harness/progress-tracker.md'),
510
+ '| T-0051 | done | 거짓 | src/mod.js + tests/test.js (5/5 통과) | next | 2026-05-14 |\n');
511
+ const r = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0051', '--path', tmpF, '--run-tests'], { encoding: 'utf8', timeout: 60000 });
512
+ const ok = r.status !== 0
513
+ && /불일치/.test(r.stdout)
514
+ && /npm test 실행: ✗ FAIL/.test(r.stdout);
515
+ console.log(ok ? '✓ B(1.9.19) verify-claim --run-tests: 주장/실행 불일치 → exit≠0' : '✗ --run-tests 불일치 검증 실패');
516
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 600)); }
517
+ }
518
+
519
+ total++;
520
+ {
521
+ // --strict-elements: same-file ⚠ vs diff-file ℹ 구분
522
+ const tmpA = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-strict2-a-'));
523
+ const tmpB = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-strict2-b-'));
524
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpA, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
525
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpB, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
526
+ // 같은 파일 + 같은 함수 (다른 capability 이름) — 진짜 중복 가능
527
+ fs.appendFileSync(path.join(tmpA, '.harness/reuse-map.md'),
528
+ '| FormatX | src/util.js (format) | util | A |\n');
529
+ fs.appendFileSync(path.join(tmpB, '.harness/reuse-map.md'),
530
+ '| FormatY | src/util.js (format) | util | B |\n');
531
+ // 다른 파일 + 같은 함수 — 의도 분리 가능
532
+ fs.appendFileSync(path.join(tmpA, '.harness/reuse-map.md'),
533
+ '| Stats1 | src/memo.js (stats) | util | A |\n');
534
+ fs.appendFileSync(path.join(tmpB, '.harness/reuse-map.md'),
535
+ '| Stats2 | bin/cli.js (stats) | command | B |\n');
536
+ const r = cp.spawnSync(process.execPath, [CLI, 'reuse-map', '--include', `${tmpA},${tmpB}`, '--strict-elements'], { encoding: 'utf8', timeout: 15000, cwd: os.tmpdir() });
537
+ const ok = r.status === 0
538
+ && /⚠ 진짜 중복 가능/.test(r.stdout)
539
+ && /ℹ 의도 분리 가능/.test(r.stdout)
540
+ && /같은 파일 \+ 같은 함수: 1건/.test(r.stdout)
541
+ && /다른 파일 \+ 같은 함수: 1건/.test(r.stdout);
542
+ console.log(ok ? '✓ B(1.9.19) --strict-elements: same-file ⚠ vs diff-file ℹ 구분' : '✗ strict-elements 분류 실패');
543
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 600)); }
544
+ }
545
+
546
+ // 1.9.20 회귀: verify-claim file regex 확장 + jest/mocha 파싱 + --bench
547
+ total++;
548
+ {
549
+ // verify-claim regex: 도메인 폴더 (scenes/scripts) + 루트 메타 파일 (project.godot)
550
+ const tmpG = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-godot-'));
551
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpG, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
552
+ fs.writeFileSync(path.join(tmpG, 'project.godot'), 'config_version=5\n');
553
+ fs.mkdirSync(path.join(tmpG, 'scenes'), { recursive: true });
554
+ fs.mkdirSync(path.join(tmpG, 'scripts'), { recursive: true });
555
+ fs.writeFileSync(path.join(tmpG, 'scenes/main.tscn'), '[gd_scene]\n');
556
+ fs.writeFileSync(path.join(tmpG, 'scripts/network.gd'), 'extends Node\n');
557
+ fs.appendFileSync(path.join(tmpG, '.harness/progress-tracker.md'),
558
+ '| T-0020 | done | Godot 클라 | project.godot + scenes/main.tscn + scripts/network.gd | next | 2026-05-14 |\n');
559
+ const r = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0020', '--path', tmpG], { encoding: 'utf8', timeout: 15000 });
560
+ const ok = r.status === 0
561
+ && /✓ project\.godot/.test(r.stdout)
562
+ && /✓ scenes\/main\.tscn/.test(r.stdout)
563
+ && /✓ scripts\/network\.gd/.test(r.stdout)
564
+ && !/scenes\/main\.ts\s/.test(r.stdout); // .tscn 이 .ts 로 잘못 매칭되지 않음
565
+ console.log(ok ? '✓ B(1.9.20) verify-claim regex: 도메인 폴더 + 루트 파일 + .tscn 정확' : '✗ regex 확장 실패');
566
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
567
+ }
568
+
569
+ total++;
570
+ {
571
+ // jest 출력 파싱
572
+ const tmpJ = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-tparse-'));
573
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpJ, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
574
+ fs.writeFileSync(path.join(tmpJ, 'package.json'), JSON.stringify({ name: 'tp', version: '0.0.1', scripts: { test: 'node tests/test.js' } }));
575
+ fs.mkdirSync(path.join(tmpJ, 'src'), { recursive: true });
576
+ fs.mkdirSync(path.join(tmpJ, 'tests'), { recursive: true });
577
+ fs.writeFileSync(path.join(tmpJ, 'src/foo.js'), 'module.exports={};\n');
578
+ fs.writeFileSync(path.join(tmpJ, 'tests/test.js'), "console.log('Tests: 12 passed, 12 total');\n");
579
+ fs.appendFileSync(path.join(tmpJ, '.harness/progress-tracker.md'),
580
+ '| T-0021 | done | jest 스타일 | src/foo.js + Tests: 12 passed, 12 total | next | 2026-05-14 |\n');
581
+ const r = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0021', '--path', tmpJ, '--run-tests'], { encoding: 'utf8', timeout: 60000 });
582
+ const okEv = /주장 \(pass\): 12\/12/.test(r.stdout);
583
+ const okRun = /실행 결과: 12\/12 passed/.test(r.stdout);
584
+ const ok = r.status === 0 && okEv && okRun;
585
+ console.log(ok ? '✓ B(1.9.20) verify-claim: jest "Tests: N passed, N total" 파싱' : `✗ jest 파싱 실패 (ev=${okEv} run=${okRun})`);
586
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
587
+ }
588
+
589
+ total++;
590
+ {
591
+ // mocha "N passing"
592
+ const tmpM = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-mocha-'));
593
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpM, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
594
+ fs.writeFileSync(path.join(tmpM, 'package.json'), JSON.stringify({ name: 'mc', version: '0.0.1', scripts: { test: 'node tests/test.js' } }));
595
+ fs.mkdirSync(path.join(tmpM, 'src'), { recursive: true });
596
+ fs.mkdirSync(path.join(tmpM, 'tests'), { recursive: true });
597
+ fs.writeFileSync(path.join(tmpM, 'src/x.js'), 'module.exports={};\n');
598
+ fs.writeFileSync(path.join(tmpM, 'tests/test.js'), "console.log(' 7 passing (12ms)');\n");
599
+ fs.appendFileSync(path.join(tmpM, '.harness/progress-tracker.md'),
600
+ '| T-0022 | done | mocha | src/x.js + 7 passing | next | 2026-05-14 |\n');
601
+ const r = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0022', '--path', tmpM, '--run-tests'], { encoding: 'utf8', timeout: 60000 });
602
+ const ok = r.status === 0 && /주장 \(pass\): 7\/7/.test(r.stdout) && /실행 결과: 7\/7 passed/.test(r.stdout);
603
+ console.log(ok ? '✓ B(1.9.20) verify-claim: mocha "N passing" 파싱' : '✗ mocha 파싱 실패');
604
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
605
+ }
606
+
607
+ total++;
608
+ {
609
+ // verify-code --bench: scripts.bench 자동 실행 + evidence 누적
610
+ const tmpB = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-bench-'));
611
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpB, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
612
+ fs.writeFileSync(path.join(tmpB, 'package.json'), JSON.stringify({
613
+ name: 'b', version: '0.0.1',
614
+ scripts: { test: 'node tests/test.js', bench: 'node tests/bench.js' }
615
+ }));
616
+ fs.mkdirSync(path.join(tmpB, 'tests'), { recursive: true });
617
+ fs.writeFileSync(path.join(tmpB, 'tests/test.js'), "console.log('1/1 passed');\n");
618
+ fs.writeFileSync(path.join(tmpB, 'tests/bench.js'), "console.log('# bench result 12345 ops/sec');\n");
619
+ const rNoBench = cp.spawnSync(process.execPath, [CLI, 'verify-code', tmpB], { encoding: 'utf8', timeout: 60000 });
620
+ const okBaseline = rNoBench.status === 0 && /verify-code \(1개\)/.test(rNoBench.stdout) && !/^## bench:/m.test(rNoBench.stdout);
621
+ const rWith = cp.spawnSync(process.execPath, [CLI, 'verify-code', tmpB, '--bench'], { encoding: 'utf8', timeout: 60000 });
622
+ const okBench = rWith.status === 0 && /verify-code \(2개\)/.test(rWith.stdout) && /bench passed/.test(rWith.stdout);
623
+ const evidence = fs.readFileSync(path.join(tmpB, '.harness/review-evidence.md'), 'utf8');
624
+ const okEvidence = /bench/.test(evidence) && /node tests\/bench\.js/.test(evidence);
625
+ const ok = okBaseline && okBench && okEvidence;
626
+ console.log(ok ? '✓ B(1.9.20) verify-code --bench: scripts.bench 자동 실행 + evidence 누적' : `✗ --bench 실패 (base=${okBaseline} bench=${okBench} ev=${okEvidence})`);
627
+ if (!ok) { failed++; console.log(rWith.stdout.slice(0, 500)); }
628
+ }
629
+
470
630
  // 1.9.15 회귀: brainstorm 라인번호 / --all-apps / --include
471
631
  total++;
472
632
  {