leerness 1.9.18 → 1.9.19

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,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.19 — 2026-05-14
4
+
5
+ **1.9.18 후속 다듬기 — verify-claim에 동적 실행, --strict-elements 정확도 강화**.
6
+
7
+ 1.9.18을 실전 sub-agent 검수에 쓰면서 발견한 두 가지 가공할 점을 마저 보완.
8
+
9
+ ### Added
10
+
11
+ - **`leerness verify-claim --run-tests`**: 정적 점검(파일 존재 + 테스트 카운트)에 더해 `npm test`를 동적으로 실행. stdout에서 `X/Y passed` 패턴을 파싱해 evidence 주장과 비교. 주장이 `5/5 통과`인데 실제 `3/5`면 exit 1. `--json`에 `run.parsed`, `verdict.declaredPassMatches` 포함.
12
+ - **`--strict-elements` 출력 강화**: 같은 함수명이라도 (a) 같은 파일이면 `⚠ 진짜 중복 가능`, (b) 다른 파일이면 `ℹ 의도 분리 가능` (예: 모듈 함수 vs CLI 명령)으로 분류. 1.9.18의 평면 출력보다 false positive 식별이 쉬워짐.
13
+
14
+ ### Why
15
+ - `verify-claim`만으로는 "파일이 있고 check() 호출이 많다" 정도까지만 보장. `--run-tests`가 추가되면 메인 에이전트가 sub-agent의 evidence를 **한 번의 명령으로 정적+동적 모두 검증**.
16
+ - 1.9.18 `--strict-elements`가 city-insights의 `MemoStats`/`StatsCli`(둘 다 `stats()` 함수, 다른 파일) 같은 의도된 분리를 잠재 중복으로 평면 표시 → 사용자가 직접 판별해야 했음. 1.9.19에선 정보를 더 줘서 즉시 분류 가능.
17
+
18
+ ### Migration
19
+ ```bash
20
+ npx leerness@latest update . --yes
21
+ ```
22
+
3
23
  ## 1.9.18 — 2026-05-14
4
24
 
5
25
  **오케스트레이션 검수 패키지 — `--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.19';
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
 
@@ -1508,8 +1515,35 @@ function verifyClaimCmd(root, taskId) {
1508
1515
  }
1509
1516
  }
1510
1517
 
1518
+ // 1.9.19: --run-tests — npm test 자동 실행 + pass/fail 파싱
1519
+ let runResult = null;
1520
+ if (has('--run-tests')) {
1521
+ const pkgPath = path.join(root, 'package.json');
1522
+ if (!exists(pkgPath)) {
1523
+ runResult = { skipped: true, reason: 'package.json 없음' };
1524
+ } else {
1525
+ let pkg = null;
1526
+ try { pkg = JSON.parse(read(pkgPath)); } catch {}
1527
+ const hasTestScript = pkg && pkg.scripts && pkg.scripts.test;
1528
+ if (!hasTestScript) {
1529
+ runResult = { skipped: true, reason: 'scripts.test 없음' };
1530
+ } else {
1531
+ const r = cp.spawnSync('npm test', [], { cwd: root, encoding: 'utf8', shell: true, timeout: 5 * 60 * 1000 });
1532
+ const out = (r.stdout || '') + (r.stderr || '');
1533
+ // "54/54 passed" 또는 "54/54 통과" 등을 파싱
1534
+ const m = out.match(/(\d+)\s*\/\s*(\d+)\s*(?:passed|통과|pass)/);
1535
+ runResult = {
1536
+ skipped: false,
1537
+ exitCode: r.status,
1538
+ parsed: m ? { num: parseInt(m[1], 10), denom: parseInt(m[2], 10) } : null,
1539
+ allPassed: r.status === 0 && (!m || (m && m[1] === m[2]))
1540
+ };
1541
+ }
1542
+ }
1543
+ }
1544
+
1511
1545
  if (has('--json')) {
1512
- log(JSON.stringify({
1546
+ const out = {
1513
1547
  project: path.basename(root),
1514
1548
  taskId, row,
1515
1549
  declared: { files: files.length, pass: declaredPass, testCount: declaredTestCount },
@@ -1518,7 +1552,18 @@ function verifyClaimCmd(root, taskId) {
1518
1552
  filesAllExist: fileChecks.every(c => c.exists),
1519
1553
  testCountMatch: declaredTestCount == null || actualTestCount == null || actualTestCount >= declaredTestCount
1520
1554
  }
1521
- }, null, 2));
1555
+ };
1556
+ if (runResult) {
1557
+ out.run = runResult;
1558
+ out.verdict.runTests = !!runResult.allPassed;
1559
+ // declared pass와 실제 비교
1560
+ if (declaredPass && runResult.parsed) {
1561
+ out.verdict.declaredPassMatches = (runResult.parsed.num === declaredPass.num && runResult.parsed.denom === declaredPass.denom);
1562
+ }
1563
+ }
1564
+ log(JSON.stringify(out, null, 2));
1565
+ if (runResult && !runResult.skipped && !runResult.allPassed) return process.exit(1);
1566
+ if (!out.verdict.filesAllExist || !out.verdict.testCountMatch) return process.exit(1);
1522
1567
  return;
1523
1568
  }
1524
1569
 
@@ -1538,19 +1583,45 @@ function verifyClaimCmd(root, taskId) {
1538
1583
  if (declaredTestCount) log(` 주장 (개수): ${declaredTestCount}개`);
1539
1584
  if (actualTestCount != null) log(` 실측: tests/test.js에 ${actualTestCount}개 check/test 호출`);
1540
1585
  else log(` 실측: 테스트 파일 못 찾음 (tests/test.js 등)`);
1586
+
1587
+ // 1.9.19: --run-tests 결과
1588
+ let runTestsOk = true;
1589
+ let declaredPassMatchesActual = true;
1590
+ if (runResult) {
1591
+ log('');
1592
+ log(`## 🚦 npm test 실행 (--run-tests)`);
1593
+ if (runResult.skipped) {
1594
+ log(` ⚠ skipped: ${runResult.reason}`);
1595
+ } else {
1596
+ log(` exit: ${runResult.exitCode}`);
1597
+ if (runResult.parsed) log(` 실행 결과: ${runResult.parsed.num}/${runResult.parsed.denom} ${runResult.parsed.num === runResult.parsed.denom ? 'passed' : 'partial'}`);
1598
+ else log(` (pass/fail 비율을 stdout에서 파싱 못함)`);
1599
+ runTestsOk = runResult.allPassed;
1600
+ if (declaredPass && runResult.parsed) {
1601
+ declaredPassMatchesActual = (runResult.parsed.num === declaredPass.num && runResult.parsed.denom === declaredPass.denom);
1602
+ log(` 주장 vs 실행: ${declaredPassMatchesActual ? '✓ 일치' : `⚠ 불일치 (주장 ${declaredPass.num}/${declaredPass.denom} ≠ 실행 ${runResult.parsed.num}/${runResult.parsed.denom})`}`);
1603
+ }
1604
+ }
1605
+ }
1606
+
1541
1607
  log('');
1542
1608
  const allFilesOk = fileChecks.every(c => c.exists);
1543
1609
  const testOk = declaredTestCount == null || actualTestCount == null || actualTestCount >= declaredTestCount;
1544
1610
  log(`## 종합`);
1545
1611
  log(` - 파일 모두 존재: ${allFilesOk ? '✓ pass' : '✗ FAIL (일부 누락)'}`);
1546
1612
  log(` - 테스트 카운트: ${testOk ? '✓ pass (실측 ≥ 주장)' : '⚠ 주장보다 적음'}`);
1547
- if (!allFilesOk || !testOk) {
1613
+ if (runResult && !runResult.skipped) {
1614
+ log(` - npm test 실행: ${runTestsOk ? '✓ all passed' : '✗ FAIL'}`);
1615
+ if (declaredPass) log(` - 주장과 실행 결과 일치: ${declaredPassMatchesActual ? '✓ pass' : '⚠ 다름'}`);
1616
+ }
1617
+ const overallFail = !allFilesOk || !testOk || (runResult && !runResult.skipped && !runTestsOk);
1618
+ if (overallFail) {
1548
1619
  log('');
1549
1620
  log(` ⚠ evidence 주장과 실제가 일치하지 않음 — task 상태 재검토 권장`);
1550
1621
  return process.exit(1);
1551
1622
  }
1552
1623
  log('');
1553
- log(` ✓ evidence 주장이 실제 파일·테스트와 일치`);
1624
+ log(` ✓ evidence 주장이 실제 파일·테스트${runResult && !runResult.skipped ? '·실행 결과' : ''}와 일치`);
1554
1625
  }
1555
1626
 
1556
1627
  function sessionClose(root) {
@@ -3548,7 +3619,7 @@ function viewworkInstall(root) {
3548
3619
  }
3549
3620
 
3550
3621
  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
3622
+ 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/19 evidence 자동 검증 (+npm test 동적)\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
3623
  leerness retro [path] [--days 7] [--all-apps] [--include p1,p2] [--json] # 회고 (1.9.13~1.9.16)
3553
3624
  leerness insights [path] [--all-apps] [--include p1,p2] [--json] # 누적 통계 (1.9.13~1.9.16)
3554
3625
  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.19",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -467,6 +467,82 @@ 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
+
470
546
  // 1.9.15 회귀: brainstorm 라인번호 / --all-apps / --include
471
547
  total++;
472
548
  {