leerness 1.33.0 → 1.35.0
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 +155 -0
- package/README.ko.md +2 -2
- package/README.md +11 -9
- package/bin/leerness.js +140 -15
- package/lib/graph.js +218 -0
- package/lib/mcp-tools.js +1 -0
- package/lib/pure-utils.js +1426 -1422
- package/package.json +1 -1
- package/scripts/e2e.js +138 -1
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -6332,6 +6332,143 @@ total++;
|
|
|
6332
6332
|
if (!ok) failed++;
|
|
6333
6333
|
}
|
|
6334
6334
|
|
|
6335
|
+
// 1.33.1 회귀 (verify-claim+gate 슬라이스 강화): ci init 생성 워크플로가 production-grade — leerness 버전 핀(재현성) + 최소권한 permissions + concurrency 취소.
|
|
6336
|
+
total++;
|
|
6337
|
+
{
|
|
6338
|
+
let ok = false;
|
|
6339
|
+
try {
|
|
6340
|
+
const ver = require(path.resolve(__dirname, '..', 'package.json')).version;
|
|
6341
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-ci-'));
|
|
6342
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes'], { encoding: 'utf8', timeout: 30000 });
|
|
6343
|
+
const ci = cp.spawnSync(process.execPath, [CLI, 'ci', 'init', d], { encoding: 'utf8', timeout: 15000 });
|
|
6344
|
+
const wf = fs.readFileSync(path.join(d, '.github', 'workflows', 'leerness-gate.yml'), 'utf8');
|
|
6345
|
+
// 버전 핀(설치 버전 == package.json) · 미핀 latest 부재 · 최소권한 · concurrency 취소 · gate 호출
|
|
6346
|
+
const pinned = new RegExp('run: npx -y leerness@' + ver.replace(/\./g, '\\.') + ' gate \\.').test(wf);
|
|
6347
|
+
const noUnpinned = !/npx -y leerness gate \./.test(wf);
|
|
6348
|
+
const perms = /permissions:\n\s*contents: read/.test(wf);
|
|
6349
|
+
const conc = /concurrency:\n\s*group: leerness-gate-\$\{\{ github\.ref \}\}\n\s*cancel-in-progress: true/.test(wf);
|
|
6350
|
+
const stillGate = /leerness-gate/.test(wf) && /pull_request:/.test(wf) && /actions\/checkout@v4/.test(wf);
|
|
6351
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
6352
|
+
ok = ci.status === 0 && pinned && noUnpinned && perms && conc && stillGate;
|
|
6353
|
+
} catch {}
|
|
6354
|
+
console.log(ok ? '✓ B(1.33.1) gate 슬라이스 강화: ci init 워크플로 버전핀(leerness@설치버전) + 최소권한 permissions(contents:read) + concurrency cancel + 미핀 latest 부재' : '✗ ci init 워크플로 강화 가드 실패');
|
|
6355
|
+
if (!ok) failed++;
|
|
6356
|
+
}
|
|
6357
|
+
|
|
6358
|
+
// 1.33.2 회귀 (verify-claim+gate 슬라이스 강화): verify-claim --all — 모든 done 주장 일괄 검증. 거짓완료=exit 1(files-missing), 진실완료/빈프로젝트=exit 0, per-task 경로와 verdict 일치(맹신 X).
|
|
6359
|
+
total++;
|
|
6360
|
+
{
|
|
6361
|
+
let ok = false;
|
|
6362
|
+
try {
|
|
6363
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-vca-'));
|
|
6364
|
+
const R = (a) => cp.spawnSync(process.execPath, [CLI, ...a, '--path', d], { encoding: 'utf8', timeout: 30000 });
|
|
6365
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes'], { encoding: 'utf8', timeout: 30000 });
|
|
6366
|
+
// 빈(완료 0) → ok true exit 0
|
|
6367
|
+
const empty = R(['verify-claim', '--all', '--json']);
|
|
6368
|
+
const ej = JSON.parse(empty.stdout);
|
|
6369
|
+
const emptyOk = empty.status === 0 && ej.ok === true && ej.total === 0;
|
|
6370
|
+
// 진실 완료(실제 파일 + 테스트)
|
|
6371
|
+
fs.writeFileSync(path.join(d, 'calc.js'), 'function add(a,b){ return a+b; }\nmodule.exports={add};\n');
|
|
6372
|
+
fs.mkdirSync(path.join(d, 'tests'), { recursive: true });
|
|
6373
|
+
fs.writeFileSync(path.join(d, 'tests', 'calc.test.js'), 'const {add}=require("../calc.js");\ntest("add",()=>{ if(add(1,2)!==3) throw new Error("x"); });\n');
|
|
6374
|
+
const aT = R(['task', 'add', 'Implement calc.js add()']);
|
|
6375
|
+
const idT = (aT.stdout.match(/T-\d{4,}/) || [])[0];
|
|
6376
|
+
R(['task', 'update', idT, '--status', 'done', '--evidence', 'calc.js + tests/calc.test.js — 1 test passing']);
|
|
6377
|
+
// 거짓 완료(존재하지 않는 파일 주장)
|
|
6378
|
+
const aF = R(['task', 'add', 'Implement payment API']);
|
|
6379
|
+
const idF = (aF.stdout.match(/T-\d{4,}/) || [])[0];
|
|
6380
|
+
R(['task', 'update', idF, '--status', 'done', '--evidence', 'payment.js implemented and tested']);
|
|
6381
|
+
const mixed = R(['verify-claim', '--all', '--json']);
|
|
6382
|
+
const mj = JSON.parse(mixed.stdout);
|
|
6383
|
+
const fr = mj.results.find((x) => x.id === idF);
|
|
6384
|
+
const tr = mj.results.find((x) => x.id === idT);
|
|
6385
|
+
const mixedOk = mixed.status === 1 && mj.ok === false && mj.total === 2 && mj.failed === 1 && fr && fr.ok === false && fr.reasons.includes('files-missing') && tr && tr.ok === true;
|
|
6386
|
+
// per-task 경로와 일치(맹신 X): 거짓 개별 exit 1, 진실 개별 exit 0 — 일괄 verdict 가 정밀 검사를 그대로 재사용
|
|
6387
|
+
const consistent = R(['verify-claim', idF]).status === 1 && R(['verify-claim', idT]).status === 0;
|
|
6388
|
+
// human 출력도 exit 1 + 불일치 표기
|
|
6389
|
+
const human = R(['verify-claim', '--all']);
|
|
6390
|
+
const humanOk = human.status === 1 && /불일치|mismatch/.test(human.stdout);
|
|
6391
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
6392
|
+
ok = emptyOk && mixedOk && consistent && humanOk;
|
|
6393
|
+
} catch (e) {}
|
|
6394
|
+
console.log(ok ? '✓ B(1.33.2) verify-claim --all: 거짓완료 일괄 차단(exit 1, files-missing) + 진실완료/빈프로젝트 통과 + per-task verdict 일치' : '✗ verify-claim --all 일괄 검증 가드 실패');
|
|
6395
|
+
if (!ok) failed++;
|
|
6396
|
+
}
|
|
6397
|
+
|
|
6398
|
+
// 1.33.3 회귀 (verify-claim+CI gate 슬라이스 강화): gate --claims opt-in 6번째 체크(기본 5 무변경) + MCP leerness_verify_claim_all 라운드트립.
|
|
6399
|
+
total++;
|
|
6400
|
+
{
|
|
6401
|
+
let ok = false;
|
|
6402
|
+
try {
|
|
6403
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-g3-'));
|
|
6404
|
+
const R = (a) => cp.spawnSync(process.execPath, [CLI, ...a, '--path', d], { encoding: 'utf8', timeout: 30000 });
|
|
6405
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes'], { encoding: 'utf8', timeout: 30000 });
|
|
6406
|
+
// 클린: 기본 gate 5체크, --claims 6체크(verify-claims 추가) 둘 다 통과
|
|
6407
|
+
const cleanDefault = JSON.parse(R(['gate', '.', '--json']).stdout);
|
|
6408
|
+
const cleanClaims = JSON.parse(R(['gate', '.', '--claims', '--json']).stdout);
|
|
6409
|
+
const cleanOk = cleanDefault.total === 5 && cleanDefault.ok === true && cleanClaims.total === 6 && cleanClaims.ok === true && cleanClaims.checks.some((c) => c.name === 'verify-claims');
|
|
6410
|
+
// 거짓 완료 추가
|
|
6411
|
+
const aF = R(['task', 'add', 'Implement payment API']);
|
|
6412
|
+
const idF = (aF.stdout.match(/T-\d{4,}/) || [])[0];
|
|
6413
|
+
R(['task', 'update', idF, '--status', 'done', '--evidence', 'payment.js implemented and tested']);
|
|
6414
|
+
// 기본 gate(5): 기존 동작 유지(lazy/audit 가 거짓완료 차단 → exit 1)
|
|
6415
|
+
const falseDefault = R(['gate', '.', '--json']);
|
|
6416
|
+
const fd = JSON.parse(falseDefault.stdout);
|
|
6417
|
+
const defaultStill = falseDefault.status === 1 && fd.total === 5;
|
|
6418
|
+
// --claims gate(6): verify-claims 체크가 명시적으로 false + exit 1 + human 모드 summary 도달(하드 exit 없음)
|
|
6419
|
+
const falseClaims = R(['gate', '.', '--claims', '--json']);
|
|
6420
|
+
const fc = JSON.parse(falseClaims.stdout);
|
|
6421
|
+
const vcCheck = fc.checks.find((c) => c.name === 'verify-claims');
|
|
6422
|
+
const claimsBlocks = falseClaims.status === 1 && vcCheck && vcCheck.ok === false && fc.total === 6;
|
|
6423
|
+
const humanClaims = R(['gate', '.', '--claims']);
|
|
6424
|
+
const humanOk = humanClaims.status === 1 && /verify-claims/.test(humanClaims.stdout) && /gate summary/.test(humanClaims.stdout);
|
|
6425
|
+
// MCP leerness_verify_claim_all 라운드트립
|
|
6426
|
+
const mcpCall = (req) => { const r = cp.spawnSync(process.execPath, [CLI, 'mcp', 'serve'], { encoding: 'utf8', timeout: 12000, input: JSON.stringify(req) + '\n' }); try { const line = r.stdout.split('\n').filter(Boolean)[0]; const j = JSON.parse(line); return JSON.parse(j.result.content[0].text); } catch { return null; } };
|
|
6427
|
+
const listed = cp.spawnSync(process.execPath, [CLI, 'mcp', 'serve'], { encoding: 'utf8', timeout: 12000, input: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }) + '\n' });
|
|
6428
|
+
let toolListed = false; try { toolListed = JSON.parse(listed.stdout.split('\n').filter(Boolean)[0]).result.tools.some((t) => t.name === 'leerness_verify_claim_all'); } catch {}
|
|
6429
|
+
const mcpRes = mcpCall({ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'leerness_verify_claim_all', arguments: { path: d } } });
|
|
6430
|
+
const mcpOk = toolListed && mcpRes && mcpRes.ok === false && mcpRes.total === 1 && mcpRes.failed === 1 && Array.isArray(mcpRes.results) && mcpRes.results[0].reasons.includes('files-missing');
|
|
6431
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
6432
|
+
ok = cleanOk && defaultStill && claimsBlocks && humanOk && mcpOk;
|
|
6433
|
+
} catch (e) {}
|
|
6434
|
+
console.log(ok ? '✓ B(1.33.3) gate --claims opt-in(기본 5 무변경 + 6번째 verify-claims 거짓완료 차단) + MCP verify_claim_all 라운드트립' : '✗ gate --claims + MCP verify_claim_all 가드 실패');
|
|
6435
|
+
if (!ok) failed++;
|
|
6436
|
+
}
|
|
6437
|
+
|
|
6438
|
+
// 1.34.1 회귀 (16th리뷰 정직화 실증): gate --claims 정밀성 가치 — 워크스페이스가 깨끗(lazy detect 0 finding)한데 콘텐츠-레벨 거짓(부풀린 테스트 카운트)인 경우, 기본 5체크는 통과(exit 0)하고 --claims 만 차단(exit 1). 이 판별 케이스가 깨지면(기본도 잡거나 --claims가 못 잡으면) --claims 의 차별 가치가 사라진 것이므로 가드.
|
|
6439
|
+
total++;
|
|
6440
|
+
{
|
|
6441
|
+
let ok = false;
|
|
6442
|
+
try {
|
|
6443
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-disc-'));
|
|
6444
|
+
const R = (a) => cp.spawnSync(process.execPath, [CLI, ...a, '--path', d], { encoding: 'utf8', timeout: 30000 });
|
|
6445
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes'], { encoding: 'utf8', timeout: 30000 });
|
|
6446
|
+
fs.writeFileSync(path.join(d, 'calc.js'), 'function add(a,b){return a+b}\nmodule.exports={add}\n');
|
|
6447
|
+
fs.mkdirSync(path.join(d, 'tests'));
|
|
6448
|
+
fs.writeFileSync(path.join(d, 'tests', 'calc.test.js'), 'const {add}=require("../calc.js");\ntest("a",()=>{if(add(1,2)!==3)throw 0});\n');
|
|
6449
|
+
// lazy detect 완전 무력화: 유효 handoff(Last generated + 비어있지 않은 섹션) + test-run 기록
|
|
6450
|
+
fs.writeFileSync(path.join(d, '.harness', 'session-handoff.md'), '# Handoff\nLast generated: 2026-06-19T00:00:00Z\n\n## Completed\n- calc.js add() 구현 + 테스트\n\n## Next Exact Step\n- 배포\n');
|
|
6451
|
+
fs.writeFileSync(path.join(d, '.harness', 'review-evidence.md'), '# Evidence\n## Test run\n- npm test: 1/1 passing\n');
|
|
6452
|
+
// 콘텐츠-레벨 거짓: 실파일/실테스트 존재하나 evidence 가 테스트 50개 통과(실제 1개) 부풀림
|
|
6453
|
+
const id = (R(['task', 'add', 'calc 구현']).stdout.match(/T-\d{4,}/) || [])[0];
|
|
6454
|
+
R(['task', 'update', id, '--status', 'done', '--evidence', 'calc.js + tests/calc.test.js 테스트 50개 통과']);
|
|
6455
|
+
// lazy detect 깨끗(blocking 0) 확인
|
|
6456
|
+
const lz = JSON.parse(R(['lazy', 'detect', '.', '--json']).stdout);
|
|
6457
|
+
const lazyClean = R(['lazy', 'detect', '.']).status === 0 && (lz.findings || []).length === 0;
|
|
6458
|
+
// 핵심 판별: 기본 게이트는 통과(exit 0), --claims 만 차단(exit 1)
|
|
6459
|
+
const gDef = R(['gate', '.']);
|
|
6460
|
+
const gClaims = R(['gate', '.', '--claims', '--json']);
|
|
6461
|
+
const gcj = JSON.parse(gClaims.stdout);
|
|
6462
|
+
const vcCheck = gcj.checks.find((c) => c.name === 'verify-claims');
|
|
6463
|
+
const lazyCheck = gcj.checks.find((c) => c.name === 'lazy detect');
|
|
6464
|
+
const discriminates = gDef.status === 0 && gClaims.status === 1 && vcCheck && vcCheck.ok === false && lazyCheck && lazyCheck.ok === true;
|
|
6465
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
6466
|
+
ok = lazyClean && discriminates;
|
|
6467
|
+
} catch (e) {}
|
|
6468
|
+
console.log(ok ? '✓ B(1.34.1) gate --claims 정밀성 REAL: 워크스페이스 깨끗(lazy 0) + 콘텐츠거짓(부풀린카운트) → 기본 5체크 통과(exit 0), --claims 만 차단(exit 1)' : '✗ gate --claims 정밀성 판별 가드 실패');
|
|
6469
|
+
if (!ok) failed++;
|
|
6470
|
+
}
|
|
6471
|
+
|
|
6335
6472
|
// 1.9.430 (10th 외부평가 UR-0130): health 보안 CRITICAL(커밋 시크릿)은 --strict 없이도 exit 1(CI 게이트). 클린은 exit 0.
|
|
6336
6473
|
total++;
|
|
6337
6474
|
{
|
|
@@ -6456,7 +6593,7 @@ total++;
|
|
|
6456
6593
|
const wfPath = path.join(d, '.github', 'workflows', 'leerness-gate.yml');
|
|
6457
6594
|
const created = fs.existsSync(wfPath);
|
|
6458
6595
|
const content = created ? fs.readFileSync(wfPath, 'utf8') : '';
|
|
6459
|
-
const contentOk = /name:\s*leerness-gate/.test(content) && /pull_request:/.test(content) && /leerness gate \./.test(content);
|
|
6596
|
+
const contentOk = /name:\s*leerness-gate/.test(content) && /pull_request:/.test(content) && /leerness@[\d.]+ gate \./.test(content); // 1.33.1: 버전 핀(leerness@x.y.z gate)
|
|
6460
6597
|
// 멱등: 재실행 시 경고(덮어쓰기 X, exit 0)
|
|
6461
6598
|
const r2 = cp.spawnSync(process.execPath, [CLI, 'ci', 'init', d], { encoding: 'utf8', timeout: 15000 });
|
|
6462
6599
|
const idempotent = r2.status === 0 && /이미 존재|exists/.test((r2.stdout || '') + (r2.stderr || ''));
|