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 +43 -0
- package/bin/harness.js +133 -16
- package/package.json +1 -1
- package/scripts/e2e.js +160 -0
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.
|
|
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
|
-
|
|
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(`
|
|
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
|
-
// 파일 경로
|
|
1489
|
-
|
|
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
|
-
// 테스트 수
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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 (
|
|
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]
|
|
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
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
|
{
|