leerness 1.17.0 → 1.19.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 +172 -0
- package/README.ko.md +187 -0
- package/README.md +50 -134
- package/SECURITY.md +66 -56
- package/bin/leerness.js +417 -84
- package/lib/audit.js +336 -322
- package/lib/pure-utils.js +1 -1
- package/lib/session-close.js +17 -1
- package/package.json +1 -1
- package/scripts/e2e.js +15 -13
package/lib/pure-utils.js
CHANGED
|
@@ -322,7 +322,7 @@ function _roadmapParseMilestones(text) {
|
|
|
322
322
|
const s = String(text || '');
|
|
323
323
|
const out = [];
|
|
324
324
|
// 1.9.352 (UR-0068 외부리뷰): 다음 milestone 직전까지 block 한정 — 이전 구현은 slice(m.index) 로 다음 milestone 의 Status/Progress 를 누출했음
|
|
325
|
-
const matches = [...s.matchAll(/^### (M-\d{4})
|
|
325
|
+
const matches = [...s.matchAll(/^### (M-\d{4})\.[ \t]*(.+?)$/gm)]; // 17th 버그헌트 P2: \s* 가 개행 흡수해 빈 제목 milestone 이 다음 줄(Status:)을 제목으로 먹던 것 차단
|
|
326
326
|
for (let i = 0; i < matches.length; i++) {
|
|
327
327
|
const m = matches[i];
|
|
328
328
|
const end = i + 1 < matches.length ? matches[i + 1].index : s.length;
|
package/lib/session-close.js
CHANGED
|
@@ -11,7 +11,7 @@ const { log, ok, warn, fail, failJson, today, now, absRoot, exists, read, readBu
|
|
|
11
11
|
const { _sanitizeFences, _parseArchiveBlocks } = require('./pure-utils');
|
|
12
12
|
|
|
13
13
|
function sessionClose(root, opts = {}, deps = {}) {
|
|
14
|
-
const { VERSION, STATUSES, MARK, has, arg, harnessPath, readProgressRows, evidencePath, handoffPath, currentStatePath, taskLogPath, verifyRules, _autoRoadmap, _readUsageStats, readSessionCounter, writeSessionCounter, _retroAggregate, _retroOneLine, retroCmd, _loadDecisions, readRules, planPath, _loadLessons, _readFeatureGraph, _auditUserRequests, _detectDeliveredRequests, _computeRoundHistory, _computeMilestones, _computeRecentChanges, _collectPyFiles, _analyzePyFile, _collectRuntimeEnv, _scanShellScriptsEncoding, _listAPISkills, _matchAPISkills, _loadShellFailures, _shellEnvDrift, _runPreWakeAudit, _saveAndAppendPreWakeReport, _runIdempotencyAudit, _detectAbnormalShutdown, _updateUserRequest } = deps;
|
|
14
|
+
const { VERSION, STATUSES, MARK, has, arg, harnessPath, readProgressRows, evidencePath, handoffPath, currentStatePath, taskLogPath, verifyRules, _autoRoadmap, _readUsageStats, readSessionCounter, writeSessionCounter, _retroAggregate, _retroOneLine, retroCmd, _loadDecisions, readRules, planPath, _loadLessons, _readFeatureGraph, _auditUserRequests, _detectDeliveredRequests, _computeRoundHistory, _computeMilestones, _computeRecentChanges, _collectPyFiles, _analyzePyFile, _collectRuntimeEnv, _scanShellScriptsEncoding, _listAPISkills, _matchAPISkills, _loadShellFailures, _shellEnvDrift, _runPreWakeAudit, _saveAndAppendPreWakeReport, _runIdempotencyAudit, _detectAbnormalShutdown, _updateUserRequest, _detectOptimism, _scanCodeForPatterns, _collectSecretFindings } = deps;
|
|
15
15
|
root = absRoot(root);
|
|
16
16
|
// 1.10.4 (13th 버그헌트 P2, UR-0167): 경로 없음/디렉토리 아님 → 구조화 에러 + exit 1. mkdir <path>/.harness ENOTDIR 크래시 & 실패를 성공(exit 0)으로 오판하던 문제 차단.
|
|
17
17
|
if (!exists(root) || !fs.statSync(root).isDirectory()) { failJson(!!opts.json || has('--json'), 'path_not_found', `경로 없음 또는 디렉토리 아님: ${root}`); return; }
|
|
@@ -34,6 +34,22 @@ function sessionClose(root, opts = {}, deps = {}) {
|
|
|
34
34
|
const _doneNoEvidence = (buckets['done'] || []).filter(r => !r.evidence || /^(\s*|user-request|-)$/.test(r.evidence) || /^plan:M-\d{4}\s*$/.test(r.evidence));
|
|
35
35
|
jsonResult.completionHonesty = { doneTotal: (buckets['done'] || []).length, doneWithoutEvidence: _doneNoEvidence.length, ids: _doneNoEvidence.slice(0, 5).map(r => r.id) };
|
|
36
36
|
if (_doneNoEvidence.length) log(` ⚠ 완료 정직성: done ${_doneNoEvidence.length}건 evidence 없음/placeholder (${_doneNoEvidence.slice(0, 3).map(r => r.id).join(', ')}) — verify-claim 권장 (advisory)`);
|
|
37
|
+
// 1.17.6 (UR-0049 마감 정합): done 의 미해소 낙관 의심 재확인 — verify-claim 을 건너뛴 거짓 주장(evidence 에 API/DB 주장 있는데 코드 흔적 없음)이
|
|
38
|
+
// 평범한 'done' 으로 마감을 무사 통과하던 것(5축 실증 P2: 거짓 DB 주장이 done 으로 마감, gate 실패 중 'clean' 선언) — 마감이 마지막 관문 역할을 하도록 재확인. advisory.
|
|
39
|
+
let _doneOptimism = [];
|
|
40
|
+
try {
|
|
41
|
+
if (_detectOptimism && _scanCodeForPatterns && (buckets['done'] || []).length) {
|
|
42
|
+
const _codeText = _scanCodeForPatterns(root);
|
|
43
|
+
_doneOptimism = (buckets['done'] || []).map(r => ({ id: r.id, suspects: _detectOptimism(r.evidence || '', _codeText) || [] })).filter(x => x.suspects.length);
|
|
44
|
+
}
|
|
45
|
+
} catch {}
|
|
46
|
+
jsonResult.completionHonesty.optimismUnresolved = _doneOptimism.map(x => ({ id: x.id, kinds: x.suspects.map(s => s.kind) }));
|
|
47
|
+
if (_doneOptimism.length) log(` ⚠ 완료 정직성: done ${_doneOptimism.length}건 낙관 의심 미해소 (${_doneOptimism.slice(0, 3).map(x => x.id).join(', ')}) — evidence 주장 vs 코드 흔적 불일치, 마감 전 verify-claim 재확인 권장 (advisory)`);
|
|
48
|
+
// 1.17.6 (UR-0049): 마감 보안 재확인 — 커밋 대상 시크릿이 살아있으면 'clean' 으로 마감하지 않도록 표면화. advisory(차단 X).
|
|
49
|
+
let _closeSecrets = 0;
|
|
50
|
+
try { if (_collectSecretFindings) _closeSecrets = ((_collectSecretFindings(root) || {}).committed || []).length; } catch {}
|
|
51
|
+
jsonResult.closeSecurity = { committedSecrets: _closeSecrets };
|
|
52
|
+
if (_closeSecrets) log(` 🚨 마감 보안: 커밋 대상 시크릿 ${_closeSecrets}건 미해소 — clean 아님, leerness scan secrets 확인 후 마감 권장`);
|
|
37
53
|
|
|
38
54
|
function rowsToList(arr) {
|
|
39
55
|
if (!arr || !arr.length) return '- 없음';
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -448,12 +448,14 @@ total++;
|
|
|
448
448
|
// 실제 src 파일 + 테스트 파일 생성 (5개 check)
|
|
449
449
|
fs.mkdirSync(path.join(tmpV, 'src'), { recursive: true });
|
|
450
450
|
fs.mkdirSync(path.join(tmpV, 'tests'), { recursive: true });
|
|
451
|
-
fs.writeFileSync(path.join(tmpV, 'src/myMod.js'), 'module.exports = {};\n');
|
|
451
|
+
fs.writeFileSync(path.join(tmpV, 'src/myMod.js'), 'module.exports = { ok: true };\n');
|
|
452
452
|
fs.writeFileSync(path.join(tmpV, 'tests/test.js'), 'check(1); check(2); check(3); check(4); check(5);\n');
|
|
453
453
|
// T-row를 evidence와 함께 추가
|
|
454
|
+
// 1.17.4 (UR-0047): evidence 에 명시적 개수 주장(테스트 5개) 포함 — 카운트 검증이 실제로 수행되는 경로를 테스트.
|
|
455
|
+
// 이전 evidence 는 "(5/5 통과)"(pass 비율)만 있어 개수 주장이 없었는데도 옛 코드가 "✓ pass (실측 ≥ 주장)" 으로 표기(측정실패=통과 모순의 일부) — 정직화로 "⊘ (주장 없음)" 이 되므로 의도(카운트 검증)에 맞게 주장을 명시.
|
|
454
456
|
fs.appendFileSync(path.join(tmpV, '.harness/progress-tracker.md'),
|
|
455
|
-
'| T-0099 | done | 신모듈 | src/myMod.js + tests/test.js (5/5 통과) | next | 2026-05-14 |\n');
|
|
456
|
-
// 정상: 파일 존재 + 테스트 5개
|
|
457
|
+
'| T-0099 | done | 신모듈 | src/myMod.js + tests/test.js 테스트 5개 (5/5 통과) | next | 2026-05-14 |\n');
|
|
458
|
+
// 정상: 파일 존재 + 테스트 5개 (주장 5 = 실측 5)
|
|
457
459
|
const r = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0099', '--path', tmpV], { encoding: 'utf8', timeout: 15000 });
|
|
458
460
|
const okPass = r.status === 0 && /✓ src\/myMod\.js/.test(r.stdout) && /✓ tests\/test\.js/.test(r.stdout) && /pass \(실측 ≥ 주장\)/.test(r.stdout);
|
|
459
461
|
// 파일 없는 케이스 → exit ≠ 0
|
|
@@ -482,7 +484,7 @@ total++;
|
|
|
482
484
|
}));
|
|
483
485
|
fs.mkdirSync(path.join(tmpR, 'src'), { recursive: true });
|
|
484
486
|
fs.mkdirSync(path.join(tmpR, 'tests'), { recursive: true });
|
|
485
|
-
fs.writeFileSync(path.join(tmpR, 'src/mod.js'), 'module.exports={};\n');
|
|
487
|
+
fs.writeFileSync(path.join(tmpR, 'src/mod.js'), 'module.exports = { ok: true };\n');
|
|
486
488
|
// 5 check 호출 + "5/5 passed" 직접 출력 (간단한 fixture)
|
|
487
489
|
fs.writeFileSync(path.join(tmpR, 'tests/test.js'),
|
|
488
490
|
"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");
|
|
@@ -506,7 +508,7 @@ total++;
|
|
|
506
508
|
fs.writeFileSync(path.join(tmpF, 'package.json'), JSON.stringify({ name: 'rtf', version: '0.0.1', scripts: { test: 'node tests/test.js' } }));
|
|
507
509
|
fs.mkdirSync(path.join(tmpF, 'src'), { recursive: true });
|
|
508
510
|
fs.mkdirSync(path.join(tmpF, 'tests'), { recursive: true });
|
|
509
|
-
fs.writeFileSync(path.join(tmpF, 'src/mod.js'), 'module.exports={};\n');
|
|
511
|
+
fs.writeFileSync(path.join(tmpF, 'src/mod.js'), 'module.exports = { ok: true };\n');
|
|
510
512
|
fs.writeFileSync(path.join(tmpF, 'tests/test.js'),
|
|
511
513
|
"console.log('3/5 passed'); process.exit(1);\n");
|
|
512
514
|
fs.appendFileSync(path.join(tmpF, '.harness/progress-tracker.md'),
|
|
@@ -650,7 +652,7 @@ total++;
|
|
|
650
652
|
const tmpS = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-stct-'));
|
|
651
653
|
cp.spawnSync(process.execPath, [CLI, 'init', tmpS, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
652
654
|
fs.mkdirSync(path.join(tmpS, 'src'), { recursive: true });
|
|
653
|
-
fs.writeFileSync(path.join(tmpS, 'src/x.js'), 'module.exports={};\n');
|
|
655
|
+
fs.writeFileSync(path.join(tmpS, 'src/x.js'), 'module.exports = { ok: true };\n');
|
|
654
656
|
fs.appendFileSync(path.join(tmpS, '.harness/progress-tracker.md'),
|
|
655
657
|
'| T-0050 | done | DB 마이그레이션 | 사용자 데이터 DB에 저장, 1000건 insert 성공 | (완료) | 2026-05-15 |\n');
|
|
656
658
|
const r = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0050', '--path', tmpS, '--strict-claims'], { encoding: 'utf8', timeout: 10000 });
|
|
@@ -686,7 +688,7 @@ total++;
|
|
|
686
688
|
const tmpN = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-notify-'));
|
|
687
689
|
cp.spawnSync(process.execPath, [CLI, 'init', tmpN, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
688
690
|
fs.mkdirSync(path.join(tmpN, 'src'), { recursive: true });
|
|
689
|
-
fs.writeFileSync(path.join(tmpN, 'src/x.js'), 'module.exports={};\n');
|
|
691
|
+
fs.writeFileSync(path.join(tmpN, 'src/x.js'), 'module.exports = { ok: true };\n');
|
|
690
692
|
fs.appendFileSync(path.join(tmpN, '.harness/progress-tracker.md'),
|
|
691
693
|
'| T-9100 | done | Slack 알림 | 슬랙 알림 발송 완료, #general 채널에 통보 | next | 2026-05-15 |\n');
|
|
692
694
|
const r = cp.spawnSync(process.execPath, [CLI, 'optimism-check', 'T-9100', '--path', tmpN], { encoding: 'utf8', timeout: 10000 });
|
|
@@ -701,7 +703,7 @@ total++;
|
|
|
701
703
|
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-conf-'));
|
|
702
704
|
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
703
705
|
fs.mkdirSync(path.join(tmpC, 'src'), { recursive: true });
|
|
704
|
-
fs.writeFileSync(path.join(tmpC, 'src/x.js'), 'module.exports={};\n');
|
|
706
|
+
fs.writeFileSync(path.join(tmpC, 'src/x.js'), 'module.exports = { ok: true };\n');
|
|
705
707
|
fs.appendFileSync(path.join(tmpC, '.harness/progress-tracker.md'),
|
|
706
708
|
'| T-9200 | done | pure compute | src/x.js 모듈 추가 | next | 2026-05-15 |\n');
|
|
707
709
|
const r = cp.spawnSync(process.execPath, [CLI, 'optimism-check', 'T-9200', '--path', tmpC, '--json'], { encoding: 'utf8', timeout: 10000 });
|
|
@@ -718,7 +720,7 @@ total++;
|
|
|
718
720
|
const tmpK = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-kpay-'));
|
|
719
721
|
cp.spawnSync(process.execPath, [CLI, 'init', tmpK, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
720
722
|
fs.mkdirSync(path.join(tmpK, 'src'), { recursive: true });
|
|
721
|
-
fs.writeFileSync(path.join(tmpK, 'src/x.js'), 'module.exports={};\n');
|
|
723
|
+
fs.writeFileSync(path.join(tmpK, 'src/x.js'), 'module.exports = { ok: true };\n');
|
|
722
724
|
fs.appendFileSync(path.join(tmpK, '.harness/progress-tracker.md'),
|
|
723
725
|
'| T-9100 | done | 결제 | 카카오페이 결제 승인 완료 | next | 2026-05-15 |\n');
|
|
724
726
|
const r = cp.spawnSync(process.execPath, [CLI, 'optimism-check', 'T-9100', '--path', tmpK, '--json'], { encoding: 'utf8', timeout: 10000 });
|
|
@@ -2005,7 +2007,7 @@ total++;
|
|
|
2005
2007
|
fs.writeFileSync(path.join(tmpJ, 'package.json'), JSON.stringify({ name: 'tp', version: '0.0.1', scripts: { test: 'node tests/test.js' } }));
|
|
2006
2008
|
fs.mkdirSync(path.join(tmpJ, 'src'), { recursive: true });
|
|
2007
2009
|
fs.mkdirSync(path.join(tmpJ, 'tests'), { recursive: true });
|
|
2008
|
-
fs.writeFileSync(path.join(tmpJ, 'src/foo.js'), 'module.exports={};\n');
|
|
2010
|
+
fs.writeFileSync(path.join(tmpJ, 'src/foo.js'), 'module.exports = { ok: true };\n');
|
|
2009
2011
|
fs.writeFileSync(path.join(tmpJ, 'tests/test.js'), "console.log('Tests: 12 passed, 12 total');\n");
|
|
2010
2012
|
fs.appendFileSync(path.join(tmpJ, '.harness/progress-tracker.md'),
|
|
2011
2013
|
'| T-0021 | done | jest 스타일 | src/foo.js + Tests: 12 passed, 12 total | next | 2026-05-14 |\n');
|
|
@@ -2025,7 +2027,7 @@ total++;
|
|
|
2025
2027
|
fs.writeFileSync(path.join(tmpM, 'package.json'), JSON.stringify({ name: 'mc', version: '0.0.1', scripts: { test: 'node tests/test.js' } }));
|
|
2026
2028
|
fs.mkdirSync(path.join(tmpM, 'src'), { recursive: true });
|
|
2027
2029
|
fs.mkdirSync(path.join(tmpM, 'tests'), { recursive: true });
|
|
2028
|
-
fs.writeFileSync(path.join(tmpM, 'src/x.js'), 'module.exports={};\n');
|
|
2030
|
+
fs.writeFileSync(path.join(tmpM, 'src/x.js'), 'module.exports = { ok: true };\n');
|
|
2029
2031
|
fs.writeFileSync(path.join(tmpM, 'tests/test.js'), "console.log(' 7 passing (12ms)');\n");
|
|
2030
2032
|
fs.appendFileSync(path.join(tmpM, '.harness/progress-tracker.md'),
|
|
2031
2033
|
'| T-0022 | done | mocha | src/x.js + 7 passing | next | 2026-05-14 |\n');
|
|
@@ -3017,7 +3019,7 @@ total++;
|
|
|
3017
3019
|
const fakeBlocked = rFake.status === 1 && /evidence 완전성.*FAIL/.test(rFake.stdout);
|
|
3018
3020
|
// 완전한 evidence(파일+테스트) + 실제 파일 존재 → pass (exit 0)
|
|
3019
3021
|
fs.mkdirSync(path.join(cDir, 'src'), { recursive: true });
|
|
3020
|
-
fs.writeFileSync(path.join(cDir, 'src', 'api.js'), 'module.exports = {};\n');
|
|
3022
|
+
fs.writeFileSync(path.join(cDir, 'src', 'api.js'), 'module.exports = { ok: true };\n');
|
|
3021
3023
|
cp.spawnSync(process.execPath, [CLI, 'task', 'update', 'T-0002', '--evidence', 'src/api.js 구현, npm test 5/5 통과 (Exit: 0)', '--path', cDir], { encoding: 'utf8', timeout: 15000, env });
|
|
3022
3024
|
const rGood = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0002', '--require-evidence', '--path', cDir], { encoding: 'utf8', timeout: 20000, env });
|
|
3023
3025
|
const goodPass = rGood.status === 0 && /evidence 완전성.*pass/.test(rGood.stdout);
|
|
@@ -3573,7 +3575,7 @@ total++;
|
|
|
3573
3575
|
try {
|
|
3574
3576
|
const vc = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-vc48-'));
|
|
3575
3577
|
cp.spawnSync(process.execPath, [CLI, 'init', vc, '--yes', '--language', 'ko', '--skills', 'recommended'], { encoding: 'utf8', timeout: 30000 });
|
|
3576
|
-
fs.mkdirSync(path.join(vc, 'src'), { recursive: true }); fs.writeFileSync(path.join(vc, 'src', 'api.js'), 'module.exports={};');
|
|
3578
|
+
fs.mkdirSync(path.join(vc, 'src'), { recursive: true }); fs.writeFileSync(path.join(vc, 'src', 'api.js'), 'module.exports = { ok: true };');
|
|
3577
3579
|
const ex = (...a) => cp.spawnSync(process.execPath, [CLI, ...a], { cwd: vc, encoding: 'utf8', timeout: 20000 });
|
|
3578
3580
|
ex('task', 'add', '증거없는완료', '--path', vc, '--status', 'done'); // T-0002: 증거 0
|
|
3579
3581
|
ex('task', 'add', '증거있는완료', '--path', vc, '--status', 'done', '--evidence', 'src/api.js 수정, 8 tests 통과 (Exit: 0)'); // T-0003
|