leerness 1.9.4 → 1.9.5
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 +45 -17
- package/package.json +1 -1
- package/scripts/e2e.js +45 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.5 — 2026-05-08
|
|
4
|
+
|
|
5
|
+
1.9.4 운영 중 발견된 한계 2건 + 추가 디버그 사항을 패치합니다.
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **F. `task fix-evidence --set` link 보존**: 기존 evidence의 `plan:M-XXXX` 링크를 새 텍스트에 자동으로 `(plan:M-XXXX)` 형태로 부착. `--no-preserve-link`로 끌 수 있음. 이전엔 링크가 사라져 audit이 milestone 미연결로 잡았음.
|
|
10
|
+
- **G. `impact` 동적 참조 (medium)**: `path.join`, `path.resolve`, `readFile`, `writeFile`, `fs.*`, `new URL` 등이 base 파일명을 인자로 받는 패턴을 별도 카테고리(medium)로 분리. default 출력에 strong + medium 모두 표시. site-cli의 `build.js`처럼 동적으로 컴포넌트를 읽는 빌더가 더 이상 weak로만 잡히지 않음.
|
|
11
|
+
|
|
12
|
+
### Migration
|
|
13
|
+
|
|
14
|
+
기존 1.9.x 사용자는 `npx leerness@latest update . --yes`로 즉시 자동 마이그레이션됩니다.
|
|
15
|
+
|
|
3
16
|
## 1.9.4 — 2026-05-08
|
|
4
17
|
|
|
5
18
|
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.5';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
11
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
12
|
const README_END = '<!-- leerness:project-readme:end -->';
|
|
@@ -694,8 +694,21 @@ function taskFixEvidence(root) {
|
|
|
694
694
|
if (!candidates.length) return ok('갱신 후보 없음 (모든 done row가 검증 키워드 보유)');
|
|
695
695
|
const setAll = arg('--set', null);
|
|
696
696
|
if (setAll) {
|
|
697
|
-
|
|
698
|
-
|
|
697
|
+
// 1.9.5 F: 기존 evidence의 plan:M-XXXX 링크를 새 텍스트에 자동 보존 (--no-preserve-link로 끄기)
|
|
698
|
+
const preserveLink = !has('--no-preserve-link');
|
|
699
|
+
let preserved = 0;
|
|
700
|
+
for (const r of candidates) {
|
|
701
|
+
let newEv = setAll;
|
|
702
|
+
if (preserveLink) {
|
|
703
|
+
const m = (r.evidence || '').match(/plan:M-\d{4}/);
|
|
704
|
+
if (m && !newEv.includes(m[0])) {
|
|
705
|
+
newEv = `${setAll} (${m[0]})`;
|
|
706
|
+
preserved++;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
upsertProgress(root, { id: r.id, evidence: newEv });
|
|
710
|
+
}
|
|
711
|
+
ok(`${candidates.length}개 row의 evidence를 일괄 갱신${preserveLink ? ` (link 보존: ${preserved}건)` : ''}`);
|
|
699
712
|
return;
|
|
700
713
|
}
|
|
701
714
|
log(`# task fix-evidence — ${candidates.length}개 후보`);
|
|
@@ -1146,43 +1159,58 @@ function impactCmd(root, target) {
|
|
|
1146
1159
|
const base = path.basename(target);
|
|
1147
1160
|
const noext = path.basename(target, path.extname(target));
|
|
1148
1161
|
const targetRel = rel(root, abs);
|
|
1149
|
-
|
|
1162
|
+
const eb = escapeRegex(base);
|
|
1163
|
+
// 1.9.5 G: strong (정적 import) / medium (동적 path 함수) / weak (식별자 등장) 3단계.
|
|
1150
1164
|
const strongRe = new RegExp(
|
|
1151
1165
|
`(?:` +
|
|
1152
|
-
`import\\s+[^;\\n]*?from\\s+['"][^'"]*${
|
|
1153
|
-
`|require\\(\\s*['"][^'"]*${
|
|
1154
|
-
`|@import\\s+['"][^'"]*${
|
|
1155
|
-
`|href=["'][^"']*${
|
|
1156
|
-
`|src=["'][^"']*${
|
|
1157
|
-
`|url\\(\\s*['"]?[^'")]*${
|
|
1158
|
-
`|include\\(\\s*['"][^'"]*${
|
|
1166
|
+
`import\\s+[^;\\n]*?from\\s+['"][^'"]*${eb}` +
|
|
1167
|
+
`|require\\(\\s*['"][^'"]*${eb}` +
|
|
1168
|
+
`|@import\\s+['"][^'"]*${eb}` +
|
|
1169
|
+
`|href=["'][^"']*${eb}` +
|
|
1170
|
+
`|src=["'][^"']*${eb}` +
|
|
1171
|
+
`|url\\(\\s*['"]?[^'")]*${eb}` +
|
|
1172
|
+
`|include\\(\\s*['"][^'"]*${eb}` +
|
|
1173
|
+
`)`
|
|
1174
|
+
);
|
|
1175
|
+
// 동적 path 조합 / 파일 시스템 호출과 함께 base 파일명이 등장하는 경우.
|
|
1176
|
+
const mediumRe = new RegExp(
|
|
1177
|
+
`(?:` +
|
|
1178
|
+
`path\\.(?:join|resolve|relative|parse|format|normalize)\\s*\\([^)]*['"][^'"]*${eb}[^'"]*['"]` +
|
|
1179
|
+
`|(?:readFile|writeFile|stat|access|open|createReadStream|createWriteStream|readFileSync|writeFileSync|statSync|accessSync|openSync)\\s*\\([^)]*['"][^'"]*${eb}[^'"]*['"]` +
|
|
1180
|
+
`|fs\\.[a-zA-Z]+\\s*\\([^)]*['"][^'"]*${eb}[^'"]*['"]` +
|
|
1181
|
+
`|new\\s+URL\\s*\\([^)]*['"][^'"]*${eb}[^'"]*['"]` +
|
|
1159
1182
|
`)`
|
|
1160
1183
|
);
|
|
1161
|
-
// word boundary 강화: cards 안의 card는 매치 안 함.
|
|
1162
1184
|
const weakRe = new RegExp(`(?<![A-Za-z0-9_])${escapeRegex(noext)}(?![A-Za-z0-9_])`);
|
|
1163
|
-
const high = []; const low = [];
|
|
1185
|
+
const high = []; const medium = []; const low = [];
|
|
1164
1186
|
for (const f of walkCode(root)) {
|
|
1165
1187
|
if (path.resolve(f) === path.resolve(abs)) continue;
|
|
1166
1188
|
let text; try { text = read(f); } catch { continue; }
|
|
1167
1189
|
if (strongRe.test(text)) high.push(rel(root, f));
|
|
1190
|
+
else if (mediumRe.test(text)) medium.push(rel(root, f));
|
|
1168
1191
|
else if (weakRe.test(text)) low.push(rel(root, f));
|
|
1169
1192
|
}
|
|
1170
1193
|
log(`# impact: ${targetRel}`);
|
|
1171
1194
|
const showAll = has('--all');
|
|
1172
|
-
|
|
1195
|
+
const totalEffective = high.length + medium.length;
|
|
1196
|
+
if (totalEffective === 0 && (low.length === 0 || !showAll)) ok('영향 범위 없음 (강한/중간 참조 없음)');
|
|
1173
1197
|
else {
|
|
1174
1198
|
if (high.length) {
|
|
1175
1199
|
log(`강한 참조 ${high.length}개 (import/require/href/src/@import/url/include):`);
|
|
1176
1200
|
high.forEach(d => log(' - ' + d));
|
|
1177
1201
|
} else log('강한 참조: 없음');
|
|
1202
|
+
if (medium.length) {
|
|
1203
|
+
log(`\n중간 참조 ${medium.length}개 (path.join/readFile/fs 등 동적 path):`);
|
|
1204
|
+
medium.forEach(d => log(' ~ ' + d));
|
|
1205
|
+
}
|
|
1178
1206
|
if (showAll && low.length) {
|
|
1179
|
-
log(`\n약한 참조 ${low.length}개 (식별자 등장 — false positive
|
|
1207
|
+
log(`\n약한 참조 ${low.length}개 (식별자 등장 — false positive 가능):`);
|
|
1180
1208
|
low.forEach(d => log(' · ' + d));
|
|
1181
1209
|
} else if (low.length && !showAll) {
|
|
1182
|
-
log(`\n💡 약한 참조 ${low.length}개 (--all 로
|
|
1210
|
+
log(`\n💡 약한 참조 ${low.length}개 (--all 로 표시)`);
|
|
1183
1211
|
}
|
|
1184
1212
|
}
|
|
1185
|
-
return { target: targetRel, high, low };
|
|
1213
|
+
return { target: targetRel, high, medium, low };
|
|
1186
1214
|
}
|
|
1187
1215
|
|
|
1188
1216
|
function reuseMapPath(root) { return path.join(root, '.harness/reuse-map.md'); }
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -229,14 +229,26 @@ 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.5 G 회귀: impact medium 카테고리 — 동적 path 패턴
|
|
239
|
+
total++;
|
|
240
|
+
{
|
|
241
|
+
// builder.js에서 path.join + readFileSync + Card.html을 동적 사용
|
|
242
|
+
fs.writeFileSync(path.join(tmp, 'src/builder.js'),
|
|
243
|
+
`const fs = require('fs'); const path = require('path');\n` +
|
|
244
|
+
`const tpl = fs.readFileSync(path.join(__dirname, 'components', 'Card.html'), 'utf8');\n` +
|
|
245
|
+
`module.exports = { tpl };\n`);
|
|
246
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'impact', 'src/components/Card.html', '--path', tmp], { encoding: 'utf8' });
|
|
247
|
+
const ok = /중간 참조 \d+개/.test(r.stdout) && /src\/builder\.js/.test(r.stdout);
|
|
248
|
+
console.log(ok ? '✓ B(G) impact medium: builder.js (path.join + readFileSync) 검출' : `✗ B(G) medium 검출 실패\n${r.stdout}`);
|
|
249
|
+
if (!ok) failed++;
|
|
250
|
+
}
|
|
251
|
+
|
|
240
252
|
// 1.9.4 회귀: --fail-on-violation cross-platform 종료
|
|
241
253
|
total++;
|
|
242
254
|
{
|
|
@@ -281,13 +293,43 @@ total++;
|
|
|
281
293
|
// 1.9.4 회귀: --set 일괄 갱신
|
|
282
294
|
total++;
|
|
283
295
|
{
|
|
296
|
+
// T-0001 evidence를 plan:M-0002 링크 포함한 placeholder로 (1.9.5 link 보존 검증용 시드)
|
|
297
|
+
const trackerPath2 = path.join(tmp, '.harness/progress-tracker.md');
|
|
298
|
+
let cur = fs.readFileSync(trackerPath2, 'utf8');
|
|
299
|
+
cur = cur.replace(/^\| T-0001 \|.*$/m, '| T-0001 | done | mile A | plan:M-0002 | next | 2026-05-08 |');
|
|
300
|
+
fs.writeFileSync(trackerPath2, cur);
|
|
284
301
|
const r = cp.spawnSync(process.execPath, [CLI, 'task', 'fix-evidence', '--set', 'npm test 통과 (e2e)', '--path', tmp], { encoding: 'utf8' });
|
|
285
|
-
const tracker = fs.readFileSync(
|
|
302
|
+
const tracker = fs.readFileSync(trackerPath2, 'utf8');
|
|
286
303
|
const ok = r.status === 0 && /npm test 통과 \(e2e\)/.test(tracker);
|
|
287
304
|
console.log(ok ? '✓ B(D2) task fix-evidence --set: 일괄 갱신' : '✗ B(D2) 일괄 갱신 실패');
|
|
288
305
|
if (!ok) failed++;
|
|
289
306
|
}
|
|
290
307
|
|
|
308
|
+
// 1.9.5 F 회귀: --set 시 plan:M-XXXX 링크 자동 보존
|
|
309
|
+
total++;
|
|
310
|
+
{
|
|
311
|
+
const tracker = fs.readFileSync(path.join(tmp, '.harness/progress-tracker.md'), 'utf8');
|
|
312
|
+
const ok = /npm test 통과 \(e2e\) \(plan:M-0002\)/.test(tracker);
|
|
313
|
+
console.log(ok ? '✓ B(F) fix-evidence --set: link 자동 보존' : `✗ B(F) link 손실\n${tracker}`);
|
|
314
|
+
if (!ok) failed++;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 1.9.5 F neg: --no-preserve-link
|
|
318
|
+
total++;
|
|
319
|
+
{
|
|
320
|
+
// T-0002 같은 row를 placeholder로 시드
|
|
321
|
+
const trackerPath3 = path.join(tmp, '.harness/progress-tracker.md');
|
|
322
|
+
let cur = fs.readFileSync(trackerPath3, 'utf8');
|
|
323
|
+
// T-0001을 다시 plan:M-0099로 교체
|
|
324
|
+
cur = cur.replace(/^\| T-0001 \|.*$/m, '| T-0001 | done | mile A | plan:M-0099 | next | 2026-05-08 |');
|
|
325
|
+
fs.writeFileSync(trackerPath3, cur);
|
|
326
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'fix-evidence', '--set', 'npm test only', '--no-preserve-link', '--path', tmp], { encoding: 'utf8' });
|
|
327
|
+
const tracker = fs.readFileSync(trackerPath3, 'utf8');
|
|
328
|
+
const ok = /\| T-0001 \| done \| mile A \| npm test only \|/.test(tracker) && !/M-0099/.test(tracker);
|
|
329
|
+
console.log(ok ? '✓ B(F neg) fix-evidence --no-preserve-link: 링크 제거' : `✗ B(F neg) 동작 이상`);
|
|
330
|
+
if (!ok) failed++;
|
|
331
|
+
}
|
|
332
|
+
|
|
291
333
|
// 1.9.4 회귀: .leerness-skip-dirs 적용
|
|
292
334
|
total++;
|
|
293
335
|
{
|