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 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.4';
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
- for (const r of candidates) upsertProgress(root, { id: r.id, evidence: setAll });
698
- ok(`${candidates.length}개 row의 evidence를 일괄 갱신`);
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
- // 1.9.4 A: strong (import-style, 확신도 높음) vs weak (단순 식별자, 가능성 있음) 구분.
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+['"][^'"]*${escapeRegex(base)}` +
1153
- `|require\\(\\s*['"][^'"]*${escapeRegex(base)}` +
1154
- `|@import\\s+['"][^'"]*${escapeRegex(base)}` +
1155
- `|href=["'][^"']*${escapeRegex(base)}` +
1156
- `|src=["'][^"']*${escapeRegex(base)}` +
1157
- `|url\\(\\s*['"]?[^'")]*${escapeRegex(base)}` +
1158
- `|include\\(\\s*['"][^'"]*${escapeRegex(base)}` +
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
- if (high.length === 0 && (low.length === 0 || !showAll)) ok('영향 범위 없음 (강한 참조 없음)');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.4",
3
+ "version": "1.9.5",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
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(path.join(tmp, '.harness/progress-tracker.md'), 'utf8');
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
  {