leerness 1.9.4 → 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 +26 -0
- package/bin/harness.js +118 -20
- package/package.json +1 -1
- package/scripts/e2e.js +86 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
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
|
+
|
|
16
|
+
## 1.9.5 — 2026-05-08
|
|
17
|
+
|
|
18
|
+
1.9.4 운영 중 발견된 한계 2건 + 추가 디버그 사항을 패치합니다.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **F. `task fix-evidence --set` link 보존**: 기존 evidence의 `plan:M-XXXX` 링크를 새 텍스트에 자동으로 `(plan:M-XXXX)` 형태로 부착. `--no-preserve-link`로 끌 수 있음. 이전엔 링크가 사라져 audit이 milestone 미연결로 잡았음.
|
|
23
|
+
- **G. `impact` 동적 참조 (medium)**: `path.join`, `path.resolve`, `readFile`, `writeFile`, `fs.*`, `new URL` 등이 base 파일명을 인자로 받는 패턴을 별도 카테고리(medium)로 분리. default 출력에 strong + medium 모두 표시. site-cli의 `build.js`처럼 동적으로 컴포넌트를 읽는 빌더가 더 이상 weak로만 잡히지 않음.
|
|
24
|
+
|
|
25
|
+
### Migration
|
|
26
|
+
|
|
27
|
+
기존 1.9.x 사용자는 `npx leerness@latest update . --yes`로 즉시 자동 마이그레이션됩니다.
|
|
28
|
+
|
|
3
29
|
## 1.9.4 — 2026-05-08
|
|
4
30
|
|
|
5
31
|
1.9.3 운영 중 발견된 5개 한계점을 모두 패치합니다.
|
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);
|
|
@@ -694,8 +755,21 @@ function taskFixEvidence(root) {
|
|
|
694
755
|
if (!candidates.length) return ok('갱신 후보 없음 (모든 done row가 검증 키워드 보유)');
|
|
695
756
|
const setAll = arg('--set', null);
|
|
696
757
|
if (setAll) {
|
|
697
|
-
|
|
698
|
-
|
|
758
|
+
// 1.9.5 F: 기존 evidence의 plan:M-XXXX 링크를 새 텍스트에 자동 보존 (--no-preserve-link로 끄기)
|
|
759
|
+
const preserveLink = !has('--no-preserve-link');
|
|
760
|
+
let preserved = 0;
|
|
761
|
+
for (const r of candidates) {
|
|
762
|
+
let newEv = setAll;
|
|
763
|
+
if (preserveLink) {
|
|
764
|
+
const m = (r.evidence || '').match(/plan:M-\d{4}/);
|
|
765
|
+
if (m && !newEv.includes(m[0])) {
|
|
766
|
+
newEv = `${setAll} (${m[0]})`;
|
|
767
|
+
preserved++;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
upsertProgress(root, { id: r.id, evidence: newEv });
|
|
771
|
+
}
|
|
772
|
+
ok(`${candidates.length}개 row의 evidence를 일괄 갱신${preserveLink ? ` (link 보존: ${preserved}건)` : ''}`);
|
|
699
773
|
return;
|
|
700
774
|
}
|
|
701
775
|
log(`# task fix-evidence — ${candidates.length}개 후보`);
|
|
@@ -773,9 +847,17 @@ function audit(root) {
|
|
|
773
847
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
774
848
|
const milestoneIds = Array.from(planText.matchAll(/^### (M-\d{4})\./gm)).map(m => m[1]);
|
|
775
849
|
const rows = readProgressRows(root);
|
|
776
|
-
|
|
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
|
+
);
|
|
777
854
|
const missingFromProgress = milestoneIds.filter(m => !linkedMs.has(m));
|
|
778
|
-
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
|
+
}
|
|
779
861
|
else if (milestoneIds.length) ok('all milestones linked in progress-tracker');
|
|
780
862
|
const handoff = exists(handoffPath(root)) ? read(handoffPath(root)) : '';
|
|
781
863
|
if (handoff.includes('Last generated: (자동)')) { warnings++; warn('session-handoff.md never auto-generated (run: leerness session close .)'); }
|
|
@@ -1146,43 +1228,58 @@ function impactCmd(root, target) {
|
|
|
1146
1228
|
const base = path.basename(target);
|
|
1147
1229
|
const noext = path.basename(target, path.extname(target));
|
|
1148
1230
|
const targetRel = rel(root, abs);
|
|
1149
|
-
|
|
1231
|
+
const eb = escapeRegex(base);
|
|
1232
|
+
// 1.9.5 G: strong (정적 import) / medium (동적 path 함수) / weak (식별자 등장) 3단계.
|
|
1150
1233
|
const strongRe = new RegExp(
|
|
1151
1234
|
`(?:` +
|
|
1152
|
-
`import\\s+[^;\\n]*?from\\s+['"][^'"]*${
|
|
1153
|
-
`|require\\(\\s*['"][^'"]*${
|
|
1154
|
-
`|@import\\s+['"][^'"]*${
|
|
1155
|
-
`|href=["'][^"']*${
|
|
1156
|
-
`|src=["'][^"']*${
|
|
1157
|
-
`|url\\(\\s*['"]?[^'")]*${
|
|
1158
|
-
`|include\\(\\s*['"][^'"]*${
|
|
1235
|
+
`import\\s+[^;\\n]*?from\\s+['"][^'"]*${eb}` +
|
|
1236
|
+
`|require\\(\\s*['"][^'"]*${eb}` +
|
|
1237
|
+
`|@import\\s+['"][^'"]*${eb}` +
|
|
1238
|
+
`|href=["'][^"']*${eb}` +
|
|
1239
|
+
`|src=["'][^"']*${eb}` +
|
|
1240
|
+
`|url\\(\\s*['"]?[^'")]*${eb}` +
|
|
1241
|
+
`|include\\(\\s*['"][^'"]*${eb}` +
|
|
1242
|
+
`)`
|
|
1243
|
+
);
|
|
1244
|
+
// 동적 path 조합 / 파일 시스템 호출과 함께 base 파일명이 등장하는 경우.
|
|
1245
|
+
const mediumRe = new RegExp(
|
|
1246
|
+
`(?:` +
|
|
1247
|
+
`path\\.(?:join|resolve|relative|parse|format|normalize)\\s*\\([^)]*['"][^'"]*${eb}[^'"]*['"]` +
|
|
1248
|
+
`|(?:readFile|writeFile|stat|access|open|createReadStream|createWriteStream|readFileSync|writeFileSync|statSync|accessSync|openSync)\\s*\\([^)]*['"][^'"]*${eb}[^'"]*['"]` +
|
|
1249
|
+
`|fs\\.[a-zA-Z]+\\s*\\([^)]*['"][^'"]*${eb}[^'"]*['"]` +
|
|
1250
|
+
`|new\\s+URL\\s*\\([^)]*['"][^'"]*${eb}[^'"]*['"]` +
|
|
1159
1251
|
`)`
|
|
1160
1252
|
);
|
|
1161
|
-
// word boundary 강화: cards 안의 card는 매치 안 함.
|
|
1162
1253
|
const weakRe = new RegExp(`(?<![A-Za-z0-9_])${escapeRegex(noext)}(?![A-Za-z0-9_])`);
|
|
1163
|
-
const high = []; const low = [];
|
|
1254
|
+
const high = []; const medium = []; const low = [];
|
|
1164
1255
|
for (const f of walkCode(root)) {
|
|
1165
1256
|
if (path.resolve(f) === path.resolve(abs)) continue;
|
|
1166
1257
|
let text; try { text = read(f); } catch { continue; }
|
|
1167
1258
|
if (strongRe.test(text)) high.push(rel(root, f));
|
|
1259
|
+
else if (mediumRe.test(text)) medium.push(rel(root, f));
|
|
1168
1260
|
else if (weakRe.test(text)) low.push(rel(root, f));
|
|
1169
1261
|
}
|
|
1170
1262
|
log(`# impact: ${targetRel}`);
|
|
1171
1263
|
const showAll = has('--all');
|
|
1172
|
-
|
|
1264
|
+
const totalEffective = high.length + medium.length;
|
|
1265
|
+
if (totalEffective === 0 && (low.length === 0 || !showAll)) ok('영향 범위 없음 (강한/중간 참조 없음)');
|
|
1173
1266
|
else {
|
|
1174
1267
|
if (high.length) {
|
|
1175
1268
|
log(`강한 참조 ${high.length}개 (import/require/href/src/@import/url/include):`);
|
|
1176
1269
|
high.forEach(d => log(' - ' + d));
|
|
1177
1270
|
} else log('강한 참조: 없음');
|
|
1271
|
+
if (medium.length) {
|
|
1272
|
+
log(`\n중간 참조 ${medium.length}개 (path.join/readFile/fs 등 동적 path):`);
|
|
1273
|
+
medium.forEach(d => log(' ~ ' + d));
|
|
1274
|
+
}
|
|
1178
1275
|
if (showAll && low.length) {
|
|
1179
|
-
log(`\n약한 참조 ${low.length}개 (식별자 등장 — false positive
|
|
1276
|
+
log(`\n약한 참조 ${low.length}개 (식별자 등장 — false positive 가능):`);
|
|
1180
1277
|
low.forEach(d => log(' · ' + d));
|
|
1181
1278
|
} else if (low.length && !showAll) {
|
|
1182
|
-
log(`\n💡 약한 참조 ${low.length}개 (--all 로
|
|
1279
|
+
log(`\n💡 약한 참조 ${low.length}개 (--all 로 표시)`);
|
|
1183
1280
|
}
|
|
1184
1281
|
}
|
|
1185
|
-
return { target: targetRel, high, low };
|
|
1282
|
+
return { target: targetRel, high, medium, low };
|
|
1186
1283
|
}
|
|
1187
1284
|
|
|
1188
1285
|
function reuseMapPath(root) { return path.join(root, '.harness/reuse-map.md'); }
|
|
@@ -1511,7 +1608,7 @@ function viewworkInstall(root) {
|
|
|
1511
1608
|
}
|
|
1512
1609
|
|
|
1513
1610
|
function help() {
|
|
1514
|
-
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`);
|
|
1515
1612
|
}
|
|
1516
1613
|
|
|
1517
1614
|
async function main() {
|
|
@@ -1575,6 +1672,7 @@ async function main() {
|
|
|
1575
1672
|
if (sub==='update') return taskUpdate(root, args[2]);
|
|
1576
1673
|
if (sub==='drop') return taskDrop(root, args[2]);
|
|
1577
1674
|
if (sub==='fix-evidence') return taskFixEvidence(root);
|
|
1675
|
+
if (sub==='relink') return taskRelink(root);
|
|
1578
1676
|
}
|
|
1579
1677
|
return help();
|
|
1580
1678
|
}
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -229,14 +229,67 @@ total++;
|
|
|
229
229
|
// home.html에 "Card"라는 식별자가 plain text로 들어가도록 (false positive 시드)
|
|
230
230
|
fs.appendFileSync(path.join(tmp, 'src/pages/home.html'), '\n<!-- 카드 콘텐츠는 Cards 배열로 -->\n');
|
|
231
231
|
const r = cp.spawnSync(process.execPath, [CLI, 'impact', 'src/components/Card.html', '--path', tmp], { encoding: 'utf8' });
|
|
232
|
-
// strong만 잡혀야 하므로 home.html이 강한 참조에 들어가야 함 (이미 <include src=Card.html>)
|
|
233
|
-
// weak에는 about/contact가 들어갈 수 있지만 기본 출력은 strong만
|
|
234
232
|
const strongOK = /강한 참조 \d+개/.test(r.stdout);
|
|
235
233
|
const weakHint = /약한 참조|영향 범위 없음/.test(r.stdout);
|
|
236
234
|
console.log(strongOK && weakHint ? '✓ B(A) impact: strong/weak 구분 출력' : '✗ B(A) impact 구분 실패');
|
|
237
235
|
if (!(strongOK && weakHint)) failed++;
|
|
238
236
|
}
|
|
239
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
|
+
|
|
279
|
+
// 1.9.5 G 회귀: impact medium 카테고리 — 동적 path 패턴
|
|
280
|
+
total++;
|
|
281
|
+
{
|
|
282
|
+
// builder.js에서 path.join + readFileSync + Card.html을 동적 사용
|
|
283
|
+
fs.writeFileSync(path.join(tmp, 'src/builder.js'),
|
|
284
|
+
`const fs = require('fs'); const path = require('path');\n` +
|
|
285
|
+
`const tpl = fs.readFileSync(path.join(__dirname, 'components', 'Card.html'), 'utf8');\n` +
|
|
286
|
+
`module.exports = { tpl };\n`);
|
|
287
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'impact', 'src/components/Card.html', '--path', tmp], { encoding: 'utf8' });
|
|
288
|
+
const ok = /중간 참조 \d+개/.test(r.stdout) && /src\/builder\.js/.test(r.stdout);
|
|
289
|
+
console.log(ok ? '✓ B(G) impact medium: builder.js (path.join + readFileSync) 검출' : `✗ B(G) medium 검출 실패\n${r.stdout}`);
|
|
290
|
+
if (!ok) failed++;
|
|
291
|
+
}
|
|
292
|
+
|
|
240
293
|
// 1.9.4 회귀: --fail-on-violation cross-platform 종료
|
|
241
294
|
total++;
|
|
242
295
|
{
|
|
@@ -281,13 +334,43 @@ total++;
|
|
|
281
334
|
// 1.9.4 회귀: --set 일괄 갱신
|
|
282
335
|
total++;
|
|
283
336
|
{
|
|
337
|
+
// T-0001 evidence를 plan:M-0002 링크 포함한 placeholder로 (1.9.5 link 보존 검증용 시드)
|
|
338
|
+
const trackerPath2 = path.join(tmp, '.harness/progress-tracker.md');
|
|
339
|
+
let cur = fs.readFileSync(trackerPath2, 'utf8');
|
|
340
|
+
cur = cur.replace(/^\| T-0001 \|.*$/m, '| T-0001 | done | mile A | plan:M-0002 | next | 2026-05-08 |');
|
|
341
|
+
fs.writeFileSync(trackerPath2, cur);
|
|
284
342
|
const r = cp.spawnSync(process.execPath, [CLI, 'task', 'fix-evidence', '--set', 'npm test 통과 (e2e)', '--path', tmp], { encoding: 'utf8' });
|
|
285
|
-
const tracker = fs.readFileSync(
|
|
343
|
+
const tracker = fs.readFileSync(trackerPath2, 'utf8');
|
|
286
344
|
const ok = r.status === 0 && /npm test 통과 \(e2e\)/.test(tracker);
|
|
287
345
|
console.log(ok ? '✓ B(D2) task fix-evidence --set: 일괄 갱신' : '✗ B(D2) 일괄 갱신 실패');
|
|
288
346
|
if (!ok) failed++;
|
|
289
347
|
}
|
|
290
348
|
|
|
349
|
+
// 1.9.5 F 회귀: --set 시 plan:M-XXXX 링크 자동 보존
|
|
350
|
+
total++;
|
|
351
|
+
{
|
|
352
|
+
const tracker = fs.readFileSync(path.join(tmp, '.harness/progress-tracker.md'), 'utf8');
|
|
353
|
+
const ok = /npm test 통과 \(e2e\) \(plan:M-0002\)/.test(tracker);
|
|
354
|
+
console.log(ok ? '✓ B(F) fix-evidence --set: link 자동 보존' : `✗ B(F) link 손실\n${tracker}`);
|
|
355
|
+
if (!ok) failed++;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 1.9.5 F neg: --no-preserve-link
|
|
359
|
+
total++;
|
|
360
|
+
{
|
|
361
|
+
// T-0002 같은 row를 placeholder로 시드
|
|
362
|
+
const trackerPath3 = path.join(tmp, '.harness/progress-tracker.md');
|
|
363
|
+
let cur = fs.readFileSync(trackerPath3, 'utf8');
|
|
364
|
+
// T-0001을 다시 plan:M-0099로 교체
|
|
365
|
+
cur = cur.replace(/^\| T-0001 \|.*$/m, '| T-0001 | done | mile A | plan:M-0099 | next | 2026-05-08 |');
|
|
366
|
+
fs.writeFileSync(trackerPath3, cur);
|
|
367
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'fix-evidence', '--set', 'npm test only', '--no-preserve-link', '--path', tmp], { encoding: 'utf8' });
|
|
368
|
+
const tracker = fs.readFileSync(trackerPath3, 'utf8');
|
|
369
|
+
const ok = /\| T-0001 \| done \| mile A \| npm test only \|/.test(tracker) && !/M-0099/.test(tracker);
|
|
370
|
+
console.log(ok ? '✓ B(F neg) fix-evidence --no-preserve-link: 링크 제거' : `✗ B(F neg) 동작 이상`);
|
|
371
|
+
if (!ok) failed++;
|
|
372
|
+
}
|
|
373
|
+
|
|
291
374
|
// 1.9.4 회귀: .leerness-skip-dirs 적용
|
|
292
375
|
total++;
|
|
293
376
|
{
|