leerness 1.9.5 → 1.9.6
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 +13 -0
- package/bin/harness.js +74 -4
- package/package.json +1 -1
- package/scripts/e2e.js +41 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.6 — 2026-05-08
|
|
4
|
+
|
|
5
|
+
1.9.5 후 발견된 한 가지 한계 (옛 link 손실 자동 복구 부재)를 패치.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`leerness task relink [--apply] [--min-score 0.2]`** — `plan.md`의 milestone 텍스트와 `progress-tracker.md`의 task `request` 텍스트를 jaccard 토큰 유사도로 비교해 미연결 milestone을 가장 비슷한 row와 자동 매칭. default는 제안만 출력 (사용자가 명령 복사해 실행), `--apply`로 자동 적용. `--min-score`로 임계 조정 (기본 0.2).
|
|
10
|
+
- **`audit`이 미연결 milestone 발견 시 `leerness task relink` 안내 자동 출력**.
|
|
11
|
+
|
|
12
|
+
### Migration
|
|
13
|
+
|
|
14
|
+
기존 1.9.x 사용자는 `npx leerness@latest update . --yes`로 즉시 자동 마이그레이션됩니다.
|
|
15
|
+
|
|
3
16
|
## 1.9.5 — 2026-05-08
|
|
4
17
|
|
|
5
18
|
1.9.4 운영 중 발견된 한계 2건 + 추가 디버그 사항을 패치합니다.
|
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.6';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
11
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
12
|
const README_END = '<!-- leerness:project-readme:end -->';
|
|
@@ -679,6 +679,67 @@ function taskDrop(root, id) {
|
|
|
679
679
|
ok(`task dropped: ${id}`);
|
|
680
680
|
}
|
|
681
681
|
|
|
682
|
+
// 1.9.6: 옛 link 손실 row를 plan.md milestone과 자동 매칭 제안/복구.
|
|
683
|
+
function _tokenizeForSim(s) {
|
|
684
|
+
// unicode letter/number만 보존 — \W는 ASCII 기준이라 한글이 분리되는 버그 회피
|
|
685
|
+
return new Set(
|
|
686
|
+
String(s || '').toLowerCase()
|
|
687
|
+
.split(/\s+/)
|
|
688
|
+
.map(t => t.replace(/[^\p{L}\p{N}_]+/gu, ''))
|
|
689
|
+
.filter(t => t.length >= 2)
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
function _jaccard(a, b) {
|
|
693
|
+
const inter = new Set([...a].filter(x => b.has(x))).size;
|
|
694
|
+
const uni = new Set([...a, ...b]).size;
|
|
695
|
+
return uni ? inter / uni : 0;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function taskRelink(root) {
|
|
699
|
+
root = absRoot(root);
|
|
700
|
+
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
701
|
+
const milestones = [...planText.matchAll(/^### (M-\d{4})\.\s*(.+?)$/gm)]
|
|
702
|
+
.map(m => ({ id: m[1], text: m[2].trim() }));
|
|
703
|
+
const rows = readProgressRows(root);
|
|
704
|
+
const linkedM = new Set(rows.map(r => (r.evidence.match(/M-\d{4}/) || [])[0]).filter(Boolean));
|
|
705
|
+
const orphanM = milestones.filter(m => !linkedM.has(m.id));
|
|
706
|
+
if (!orphanM.length) return ok('미연결 milestone 없음');
|
|
707
|
+
|
|
708
|
+
const apply = has('--apply');
|
|
709
|
+
const minScore = parseFloat(arg('--min-score', '0.2'));
|
|
710
|
+
log(`# task relink — 미연결 milestone ${orphanM.length}개${apply ? ' (--apply: 자동 적용)' : ' (제안만, --apply로 적용)'}`);
|
|
711
|
+
const suggestions = [];
|
|
712
|
+
for (const m of orphanM) {
|
|
713
|
+
const milestoneTokens = _tokenizeForSim(m.text);
|
|
714
|
+
const candidates = rows
|
|
715
|
+
.map(r => ({ r, score: _jaccard(milestoneTokens, _tokenizeForSim(r.request)) }))
|
|
716
|
+
.filter(x => x.score >= minScore)
|
|
717
|
+
.sort((a, b) => b.score - a.score);
|
|
718
|
+
log(`\n${m.id}: ${m.text}`);
|
|
719
|
+
if (!candidates.length) {
|
|
720
|
+
log(` ⓘ 매칭 후보 없음 (score ≥ ${minScore})`);
|
|
721
|
+
log(` → 새 task: leerness task add "${m.text}" --status planned --evidence "plan:${m.id}"`);
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
const best = candidates[0];
|
|
725
|
+
const newEv = best.r.evidence.includes(`plan:${m.id}`) ? best.r.evidence : `${best.r.evidence} (plan:${m.id})`;
|
|
726
|
+
log(` ✓ 최선 후보: ${best.r.id} (score ${best.score.toFixed(2)}) — ${best.r.request}`);
|
|
727
|
+
log(` 현재 evidence: "${best.r.evidence}"`);
|
|
728
|
+
log(` 제안 evidence: "${newEv}"`);
|
|
729
|
+
log(` 수동: leerness task update ${best.r.id} --evidence "${newEv}"`);
|
|
730
|
+
if (candidates.length > 1) {
|
|
731
|
+
const next = candidates.slice(1, 3).map(c => `${c.r.id}(${c.score.toFixed(2)})`).join(', ');
|
|
732
|
+
log(` 다른 후보: ${next}`);
|
|
733
|
+
}
|
|
734
|
+
suggestions.push({ id: best.r.id, evidence: newEv });
|
|
735
|
+
}
|
|
736
|
+
if (apply && suggestions.length) {
|
|
737
|
+
for (const s of suggestions) upsertProgress(root, { id: s.id, evidence: s.evidence });
|
|
738
|
+
log('');
|
|
739
|
+
ok(`${suggestions.length}개 row 자동 relink 완료`);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
682
743
|
// 1.9.4 D: evidence가 placeholder인 done row를 일괄 점검.
|
|
683
744
|
function taskFixEvidence(root) {
|
|
684
745
|
root = absRoot(root);
|
|
@@ -786,9 +847,17 @@ function audit(root) {
|
|
|
786
847
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
787
848
|
const milestoneIds = Array.from(planText.matchAll(/^### (M-\d{4})\./gm)).map(m => m[1]);
|
|
788
849
|
const rows = readProgressRows(root);
|
|
789
|
-
|
|
850
|
+
// 1.9.6 수정: 한 row에 여러 plan:M-XXXX 링크가 있어도 모두 인식 (matchAll로 전부 추출)
|
|
851
|
+
const linkedMs = new Set(
|
|
852
|
+
rows.flatMap(r => Array.from(String(r.evidence || '').matchAll(/M-\d{4}/g), m => m[0]))
|
|
853
|
+
);
|
|
790
854
|
const missingFromProgress = milestoneIds.filter(m => !linkedMs.has(m));
|
|
791
|
-
if (missingFromProgress.length) {
|
|
855
|
+
if (missingFromProgress.length) {
|
|
856
|
+
warnings++;
|
|
857
|
+
warn(`milestones without progress entry: ${missingFromProgress.join(', ')}`);
|
|
858
|
+
log(` → 자동 매칭 제안: leerness task relink`);
|
|
859
|
+
log(` → 자동 적용: leerness task relink --apply`);
|
|
860
|
+
}
|
|
792
861
|
else if (milestoneIds.length) ok('all milestones linked in progress-tracker');
|
|
793
862
|
const handoff = exists(handoffPath(root)) ? read(handoffPath(root)) : '';
|
|
794
863
|
if (handoff.includes('Last generated: (자동)')) { warnings++; warn('session-handoff.md never auto-generated (run: leerness session close .)'); }
|
|
@@ -1539,7 +1608,7 @@ function viewworkInstall(root) {
|
|
|
1539
1608
|
}
|
|
1540
1609
|
|
|
1541
1610
|
function help() {
|
|
1542
|
-
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]\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 [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\n leerness impact <target> [--all] # 변경 전 영향 분석 (기본 strong, --all로 weak 포함)\n leerness reuse find <query> # 기존 자원 검색 (재귀 안내)\n leerness reuse register <name> --where <p> --kind component|hook|util|api [--note ...]\n leerness ui consistency [path] [--strict] [--fail-on-violation]\n leerness graph [path] [--out <file>] # mermaid 의존성 그래프\n leerness guide [target] # impact + reuse + ui consistency 통합 가이드\n`);
|
|
1611
|
+
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]\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\n leerness impact <target> [--all] # 변경 전 영향 분석 (기본 strong, --all로 weak 포함)\n leerness reuse find <query> # 기존 자원 검색 (재귀 안내)\n leerness reuse register <name> --where <p> --kind component|hook|util|api [--note ...]\n leerness ui consistency [path] [--strict] [--fail-on-violation]\n leerness graph [path] [--out <file>] # mermaid 의존성 그래프\n leerness guide [target] # impact + reuse + ui consistency 통합 가이드\n`);
|
|
1543
1612
|
}
|
|
1544
1613
|
|
|
1545
1614
|
async function main() {
|
|
@@ -1603,6 +1672,7 @@ async function main() {
|
|
|
1603
1672
|
if (sub==='update') return taskUpdate(root, args[2]);
|
|
1604
1673
|
if (sub==='drop') return taskDrop(root, args[2]);
|
|
1605
1674
|
if (sub==='fix-evidence') return taskFixEvidence(root);
|
|
1675
|
+
if (sub==='relink') return taskRelink(root);
|
|
1606
1676
|
}
|
|
1607
1677
|
return help();
|
|
1608
1678
|
}
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -235,6 +235,47 @@ total++;
|
|
|
235
235
|
if (!(strongOK && weakHint)) failed++;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
// 1.9.6 회귀: task relink — 인위적 link 손실 → 자동 매칭 제안 + --apply 자동 복구
|
|
239
|
+
total++;
|
|
240
|
+
{
|
|
241
|
+
// plan.md에 새 milestone 추가, progress엔 link 없는 비슷한 row 추가
|
|
242
|
+
const planPath_ = path.join(tmp, '.harness/plan.md');
|
|
243
|
+
fs.appendFileSync(planPath_,
|
|
244
|
+
`\n### M-9001. 캐시 헬퍼 모듈\nStatus: planned\nProgress: 0%\n\nTasks:\n- [ ] 캐시 helper\n` +
|
|
245
|
+
`\n### M-9002. 인증 헬퍼 모듈\nStatus: planned\nProgress: 0%\n\nTasks:\n- [ ] 인증 helper\n`);
|
|
246
|
+
// progress에 link 없는 비슷한 row 2개 추가
|
|
247
|
+
const trackerPath_ = path.join(tmp, '.harness/progress-tracker.md');
|
|
248
|
+
fs.appendFileSync(trackerPath_,
|
|
249
|
+
`| T-9001 | done | 캐시 helper 구현 | tests:5/5 (link lost) | 다음 단계 | 2026-05-08 |\n` +
|
|
250
|
+
`| T-9002 | done | 인증 helper 구현 | tests:8/8 (link lost) | 다음 단계 | 2026-05-08 |\n`);
|
|
251
|
+
|
|
252
|
+
// 제안 모드
|
|
253
|
+
const r1 = cp.spawnSync(process.execPath, [CLI, 'task', 'relink', '--path', tmp], { encoding: 'utf8' });
|
|
254
|
+
const okSuggest = r1.status === 0 && /M-9001/.test(r1.stdout) && /M-9002/.test(r1.stdout) && /최선 후보/.test(r1.stdout);
|
|
255
|
+
console.log(okSuggest ? '✓ B(1.9.6) task relink 제안: 2개 매칭 발견' : `✗ B(1.9.6) 제안 실패\n${r1.stdout}`);
|
|
256
|
+
if (!okSuggest) failed++;
|
|
257
|
+
}
|
|
258
|
+
total++;
|
|
259
|
+
{
|
|
260
|
+
// --apply
|
|
261
|
+
const r2 = cp.spawnSync(process.execPath, [CLI, 'task', 'relink', '--apply', '--path', tmp], { encoding: 'utf8' });
|
|
262
|
+
const tracker = fs.readFileSync(path.join(tmp, '.harness/progress-tracker.md'), 'utf8');
|
|
263
|
+
const okApply = r2.status === 0 && /M-9001/.test(tracker) && /M-9002/.test(tracker);
|
|
264
|
+
console.log(okApply ? '✓ B(1.9.6) task relink --apply: 자동 복구' : '✗ B(1.9.6) --apply 실패');
|
|
265
|
+
if (!okApply) failed++;
|
|
266
|
+
}
|
|
267
|
+
total++;
|
|
268
|
+
{
|
|
269
|
+
// audit이 task relink 안내를 출력하는지 (이번엔 link 복구 후라 미연결 milestone 없을 것)
|
|
270
|
+
// 일부러 새 milestone 추가 후 audit
|
|
271
|
+
fs.appendFileSync(path.join(tmp, '.harness/plan.md'),
|
|
272
|
+
`\n### M-9999. 매칭 후보 없는 milestone\nStatus: planned\n\nTasks:\n- [ ] x\n`);
|
|
273
|
+
const r3 = cp.spawnSync(process.execPath, [CLI, 'audit', tmp], { encoding: 'utf8' });
|
|
274
|
+
const ok = /milestones without progress entry/.test(r3.stdout) && /M-9999/.test(r3.stdout) && /leerness task relink/.test(r3.stdout);
|
|
275
|
+
console.log(ok ? '✓ B(1.9.6) audit이 task relink 안내 출력' : `✗ B(1.9.6) audit 안내 누락\n${r3.stdout}`);
|
|
276
|
+
if (!ok) failed++;
|
|
277
|
+
}
|
|
278
|
+
|
|
238
279
|
// 1.9.5 G 회귀: impact medium 카테고리 — 동적 path 패턴
|
|
239
280
|
total++;
|
|
240
281
|
{
|