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/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})\.\s*(.+?)$/gm)];
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;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.17.0",
3
+ "version": "1.19.0",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
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