leerness 1.9.141 → 1.9.143
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 +48 -0
- package/README.md +2 -2
- package/bin/harness.js +141 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.143 — 2026-05-20
|
|
4
|
+
|
|
5
|
+
**JSON 4종 featureGraph 통합 완성 + drift check feature 신호** — 1.9.142 session close --json 패턴을 handoff/health에 확장.
|
|
6
|
+
|
|
7
|
+
### Added — handoff --json featureGraph 통합
|
|
8
|
+
- `result.featureGraph = { total, edges, isolated, summary: "F<n>/E<n>[/iso<n>]" }`
|
|
9
|
+
- 외부 AI가 handoff 한 번에 컨텍스트 + memory surface + featureGraph 동시 회수
|
|
10
|
+
|
|
11
|
+
### Added — health --json featureGraph 통합
|
|
12
|
+
- `out.featureGraph = { total, edges, isolated, summary }`
|
|
13
|
+
- JSON 4종 (handoff/memory status/session close/health) featureGraph 일관성 완성 (memorySurface 1.9.123 패턴과 동형)
|
|
14
|
+
|
|
15
|
+
### Added — drift check feature graph 신호 (6번째 신호)
|
|
16
|
+
- 노드 ≥ 3개 + edges == 0 → weight 25
|
|
17
|
+
- 노드 ≥ 3개 + isolated 비율 ≥ 50% → weight 15
|
|
18
|
+
- `drift check --json` `fired` 배열에 노출
|
|
19
|
+
- 사용자 cascade 방지 의지 + 실제 사용 사이 gap 자동 감지
|
|
20
|
+
|
|
21
|
+
### Validation
|
|
22
|
+
- stress-v88: PASS (handoff/health featureGraph + drift 신호 + 누적 회귀)
|
|
23
|
+
- e2e: 219/219 PASS
|
|
24
|
+
|
|
25
|
+
## 1.9.142 — 2026-05-20
|
|
26
|
+
|
|
27
|
+
**Feature Graph 통합 라운드** — 1.9.141 인과관계 시스템을 audit / MCP CRUD / session close 에 통합.
|
|
28
|
+
|
|
29
|
+
### Added — MCP feature CRUD 완성
|
|
30
|
+
- `leerness_feature_add` (MCP 45) — 외부 AI가 코드 작성 중 feature 등록
|
|
31
|
+
- `leerness_feature_link` (MCP 46) — 의존/영향/공변경 엣지 추가
|
|
32
|
+
- 이제 외부 AI (Claude Code/Cursor 등) 가 leerness 워크플로 밖에서도 인과관계를 직접 갱신
|
|
33
|
+
- 인자: `title/dependsOn/affects/coChangesWith/files/path`
|
|
34
|
+
|
|
35
|
+
### Added — audit Feature Graph 무결성 검증 (kind 13종으로 확장)
|
|
36
|
+
- `feature_graph_orphan` — 다른 노드가 참조하는데 정의 없는 ID (예: F-0001 → F-0099 missing)
|
|
37
|
+
- `feature_graph_cycle` — affects/depends-on 그래프 순환 감지 (DFS 3-color)
|
|
38
|
+
- 둘 다 warning (--strict 시 failures 승격)
|
|
39
|
+
- `--no-feature-check` 옵션으로 끄기
|
|
40
|
+
- `audit --json` findings 에 `count/orphans[]/cycles[]` 상세 포함
|
|
41
|
+
|
|
42
|
+
### Added — session close --json featureGraph 통합
|
|
43
|
+
- `{ total, edges, isolated, summary: "F<n>/E<n>[/iso<n>]" }` 추가
|
|
44
|
+
- 마감 시 Feature Graph 통계 자동 보고
|
|
45
|
+
|
|
46
|
+
### Validation
|
|
47
|
+
- stress-v87: PASS (MCP feature_add/link + audit orphan/cycle + session close featureGraph + 누적 회귀)
|
|
48
|
+
- e2e: 219/219 PASS
|
|
49
|
+
- MCP tools: **46** (+2 from 44)
|
|
50
|
+
|
|
3
51
|
## 1.9.141 — 2026-05-20
|
|
4
52
|
|
|
5
53
|
**ASCII 배너 모션 + Feature Causality Graph (인과관계 추적) — 사용자 요청 2종.**
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **AI 코딩 에이전트의 거짓 완료·중복·망각·충돌을 막아주는 검수·기억·협업 CLI 하네스.**
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/leerness) [](https://www.npmjs.com/package/leerness) []() []() []() []() []() []() []() []()
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
╔══════════════════════════════════════════════════════════════╗
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
║ ██║ ██╔══╝ ██╔══╝ ██╔══██╗██║╚██╗██║██╔══╝ ╚════██║ ║
|
|
13
13
|
║ ███████╗███████╗███████╗██║ ██║██║ ╚████║███████╗███████║ ║
|
|
14
14
|
║ ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝ ║
|
|
15
|
-
║ v1.9.
|
|
15
|
+
║ v1.9.143 AI Agent Reliability Harness ║
|
|
16
16
|
║ verify · remember · orchestrate · audit · prevent drift ║
|
|
17
17
|
╚══════════════════════════════════════════════════════════════╝
|
|
18
18
|
```
|
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.143';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
11
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
12
|
const README_END = '<!-- leerness:project-readme:end -->';
|
|
@@ -2320,6 +2320,62 @@ function audit(root, opts = {}) {
|
|
|
2320
2320
|
}
|
|
2321
2321
|
} catch {}
|
|
2322
2322
|
}
|
|
2323
|
+
// 1.9.142: Feature Graph 무결성 검증 — orphan/cycle 자동 감지 (--no-feature-check로 끄기)
|
|
2324
|
+
if (!has('--no-feature-check')) {
|
|
2325
|
+
try {
|
|
2326
|
+
const { nodes: fNodes } = _readFeatureGraph(root);
|
|
2327
|
+
if (fNodes.length > 0) {
|
|
2328
|
+
const ids = new Set(fNodes.map(n => n.id));
|
|
2329
|
+
// (1) orphan: 다른 노드가 참조하는데 정의가 없는 ID
|
|
2330
|
+
const orphans = [];
|
|
2331
|
+
for (const n of fNodes) {
|
|
2332
|
+
for (const ref of [...(n.dependsOn || []), ...(n.affects || []), ...(n.coChangesWith || [])]) {
|
|
2333
|
+
if (!ids.has(ref)) orphans.push({ from: n.id, missingRef: ref });
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
if (orphans.length) {
|
|
2337
|
+
warnings++;
|
|
2338
|
+
warn(`Feature Graph: orphan 참조 ${orphans.length}건 — ${orphans.slice(0, 3).map(o => `${o.from}→${o.missingRef}`).join(', ')}${orphans.length > 3 ? ' …' : ''}`);
|
|
2339
|
+
_finding('feature_graph_orphan', 'warn', 'Feature Graph 에 정의되지 않은 ID 참조', { count: orphans.length, orphans: orphans.slice(0, 10) });
|
|
2340
|
+
log(` → 수정: leerness feature add 또는 link 제거`);
|
|
2341
|
+
}
|
|
2342
|
+
// (2) cycle: affects 그래프에서 순환 의존성 감지 (DFS)
|
|
2343
|
+
const cycles = [];
|
|
2344
|
+
const WHITE = 0, GRAY = 1, BLACK = 2;
|
|
2345
|
+
const color = new Map();
|
|
2346
|
+
for (const n of fNodes) color.set(n.id, WHITE);
|
|
2347
|
+
const byId = new Map(fNodes.map(n => [n.id, n]));
|
|
2348
|
+
const dfs = (nodeId, path) => {
|
|
2349
|
+
color.set(nodeId, GRAY);
|
|
2350
|
+
const node = byId.get(nodeId);
|
|
2351
|
+
if (!node) { color.set(nodeId, BLACK); return; }
|
|
2352
|
+
for (const next of [...(node.affects || []), ...(node.dependsOn || [])]) {
|
|
2353
|
+
if (!byId.has(next)) continue;
|
|
2354
|
+
const c = color.get(next);
|
|
2355
|
+
if (c === GRAY) {
|
|
2356
|
+
// 순환 발견 — path 에 next 까지 자르기
|
|
2357
|
+
const idx = path.indexOf(next);
|
|
2358
|
+
const cyc = idx >= 0 ? path.slice(idx).concat([next]) : [...path, next];
|
|
2359
|
+
if (!cycles.some(existing => existing.join() === cyc.join())) cycles.push(cyc);
|
|
2360
|
+
} else if (c === WHITE) {
|
|
2361
|
+
dfs(next, [...path, next]);
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
color.set(nodeId, BLACK);
|
|
2365
|
+
};
|
|
2366
|
+
for (const n of fNodes) if (color.get(n.id) === WHITE) dfs(n.id, [n.id]);
|
|
2367
|
+
if (cycles.length) {
|
|
2368
|
+
warnings++;
|
|
2369
|
+
warn(`Feature Graph: 순환 의존 ${cycles.length}건 — ${cycles[0].join(' → ')}${cycles.length > 1 ? ` (외 ${cycles.length-1}건)` : ''}`);
|
|
2370
|
+
_finding('feature_graph_cycle', 'warn', 'Feature Graph 에 순환 의존', { count: cycles.length, cycles: cycles.slice(0, 5) });
|
|
2371
|
+
log(` → 수정: feature link 재구성 (affects/depends-on 방향 정리)`);
|
|
2372
|
+
}
|
|
2373
|
+
if (!orphans.length && !cycles.length) {
|
|
2374
|
+
ok(`Feature Graph OK (${fNodes.length} 노드, orphan/cycle 없음, 1.9.142)`);
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
} catch {}
|
|
2378
|
+
}
|
|
2323
2379
|
// 1.9.63: --strict — warnings ≥ threshold 시 failures로 승격 (CI 친화)
|
|
2324
2380
|
if (has('--strict')) {
|
|
2325
2381
|
const threshold = parseInt(arg('--threshold', '1'), 10);
|
|
@@ -2692,6 +2748,22 @@ function handoff(root) {
|
|
|
2692
2748
|
archive: archiveCountsH, // 1.9.130
|
|
2693
2749
|
summary: `T${tasksInProgress}/D${decisionsCount}/R${rulesActive}/P${milestones}/L${lessonsCount}`,
|
|
2694
2750
|
};
|
|
2751
|
+
// 1.9.143: handoff --json featureGraph 통합 (session close 1.9.142 와 동일 패턴)
|
|
2752
|
+
try {
|
|
2753
|
+
const { nodes: fNodesH } = _readFeatureGraph(root);
|
|
2754
|
+
const edgeCount = fNodesH.reduce((s, n) => s + (n.dependsOn?.length || 0) + (n.affects?.length || 0) + (n.coChangesWith?.length || 0), 0);
|
|
2755
|
+
const linkedSet = new Set();
|
|
2756
|
+
for (const n of fNodesH) {
|
|
2757
|
+
for (const x of [...(n.dependsOn||[]), ...(n.affects||[]), ...(n.coChangesWith||[])]) { linkedSet.add(n.id); linkedSet.add(x); }
|
|
2758
|
+
}
|
|
2759
|
+
const isolated = fNodesH.length ? (fNodesH.length - linkedSet.size) : 0;
|
|
2760
|
+
result.featureGraph = {
|
|
2761
|
+
total: fNodesH.length,
|
|
2762
|
+
edges: edgeCount,
|
|
2763
|
+
isolated: Math.max(0, isolated),
|
|
2764
|
+
summary: `F${fNodesH.length}/E${edgeCount}${isolated > 0 ? `/iso${isolated}` : ''}`
|
|
2765
|
+
};
|
|
2766
|
+
} catch {}
|
|
2695
2767
|
} catch {}
|
|
2696
2768
|
log(JSON.stringify(result, null, 2));
|
|
2697
2769
|
return;
|
|
@@ -4526,7 +4598,7 @@ function _banner(opts = {}) {
|
|
|
4526
4598
|
for (const ln of lines) log(ln);
|
|
4527
4599
|
}
|
|
4528
4600
|
if (opts.quickStart) {
|
|
4529
|
-
log(C.bold(C.cyan(' ✨ 빠른 시작 (1.9.
|
|
4601
|
+
log(C.bold(C.cyan(' ✨ 빠른 시작 (1.9.143+ JSON 4종 featureGraph 통합 완성 + drift 신호 — 73 라운드 자율 누적)')));
|
|
4530
4602
|
log(' ' + C.green('npx leerness@latest init .') + C.dim(' # 신규 프로젝트 + 외부 AI CLI 설정'));
|
|
4531
4603
|
log(' ' + C.green('npx leerness handoff .') + C.dim(' # 컨텍스트 + lessons + 매칭 skill + history hit + brainstorm hits + 헤드라인'));
|
|
4532
4604
|
log(' ' + C.green('npx leerness handoff . --quiet') + C.dim(' # 자동화/CI 모드 (1.9.99) — 자동 회수 라인 비활성'));
|
|
@@ -5486,6 +5558,22 @@ function sessionClose(root, opts = {}) {
|
|
|
5486
5558
|
archive: archiveCountsS, // 1.9.130
|
|
5487
5559
|
summary: `T${tasksInProgress0}/D${decisionsCount0}/R${rulesActive0}/P${milestones0}/L${lessonsCount0}`,
|
|
5488
5560
|
};
|
|
5561
|
+
// 1.9.142: featureCounts 통합 — session close JSON에 Feature Graph 통계
|
|
5562
|
+
try {
|
|
5563
|
+
const { nodes: fNodesC } = _readFeatureGraph(root);
|
|
5564
|
+
const edgeCount = fNodesC.reduce((s, n) => s + (n.dependsOn?.length || 0) + (n.affects?.length || 0) + (n.coChangesWith?.length || 0), 0);
|
|
5565
|
+
const linkedIds = new Set();
|
|
5566
|
+
for (const n of fNodesC) {
|
|
5567
|
+
for (const x of [...(n.dependsOn||[]), ...(n.affects||[]), ...(n.coChangesWith||[])]) { linkedIds.add(n.id); linkedIds.add(x); }
|
|
5568
|
+
}
|
|
5569
|
+
const isolated = fNodesC.length ? (fNodesC.length - linkedIds.size) : 0;
|
|
5570
|
+
jsonResult.featureGraph = {
|
|
5571
|
+
total: fNodesC.length,
|
|
5572
|
+
edges: edgeCount,
|
|
5573
|
+
isolated: Math.max(0, isolated),
|
|
5574
|
+
summary: `F${fNodesC.length}/E${edgeCount}${isolated > 0 ? `/iso${isolated}` : ''}`
|
|
5575
|
+
};
|
|
5576
|
+
} catch {}
|
|
5489
5577
|
} catch {}
|
|
5490
5578
|
process.stdout.write(JSON.stringify(jsonResult, null, 2) + '\n');
|
|
5491
5579
|
}
|
|
@@ -8128,6 +8216,24 @@ function driftCheckCmd(root, opts = {}) {
|
|
|
8128
8216
|
}
|
|
8129
8217
|
}
|
|
8130
8218
|
} catch {}
|
|
8219
|
+
// 1.9.143: Feature Graph 미사용 신호 — 노드는 있는데 edges 비율 낮으면 인과관계 정리 미진
|
|
8220
|
+
try {
|
|
8221
|
+
const { nodes: fGraphNodes } = _readFeatureGraph(root);
|
|
8222
|
+
if (fGraphNodes.length >= 3) {
|
|
8223
|
+
const edgeCount = fGraphNodes.reduce((s, n) => s + (n.dependsOn?.length || 0) + (n.affects?.length || 0) + (n.coChangesWith?.length || 0), 0);
|
|
8224
|
+
const linkedSet = new Set();
|
|
8225
|
+
for (const n of fGraphNodes) {
|
|
8226
|
+
for (const x of [...(n.dependsOn||[]), ...(n.affects||[]), ...(n.coChangesWith||[])]) { linkedSet.add(n.id); linkedSet.add(x); }
|
|
8227
|
+
}
|
|
8228
|
+
const isolatedCount = Math.max(0, fGraphNodes.length - linkedSet.size);
|
|
8229
|
+
const isolatedRatio = isolatedCount / fGraphNodes.length;
|
|
8230
|
+
if (edgeCount === 0 || isolatedRatio >= 0.5) {
|
|
8231
|
+
const fgScore = edgeCount === 0 ? 25 : 15;
|
|
8232
|
+
totalScore += fgScore;
|
|
8233
|
+
fired.push({ file: '.harness/feature-graph.md', ageDays: null, threshold: 0, weight: fgScore, label: `Feature Graph 미정리 (1.9.143): ${fGraphNodes.length} 노드, edges=${edgeCount}, isolated=${isolatedCount}` });
|
|
8234
|
+
}
|
|
8235
|
+
}
|
|
8236
|
+
} catch {}
|
|
8131
8237
|
// 신규 _apps/* 에서 task 0건도 신호로
|
|
8132
8238
|
const appsDir = path.join(root, '_apps');
|
|
8133
8239
|
let appsZeroTask = [];
|
|
@@ -9151,7 +9257,9 @@ function mcpServeCmd(root) {
|
|
|
9151
9257
|
{ name: 'leerness_task_list', description: '1.9.134 — progress-tracker.md 전체 task 조회 JSON ({ total, tasks: [{ id, status, request, evidence, nextAction, updated }] }). --status 필터 지원 (planned|in-progress|done 등). 외부 AI가 task 상태 회수', inputSchema: { type: 'object', properties: { path: { type: 'string' }, status: { type: 'string' } } } },
|
|
9152
9258
|
{ name: 'leerness_rule_remove', description: '1.9.135 — rules.md 에서 특정 rule 제거 (id: R-XXXX). 제거된 rule 은 .harness/rules.archive.md 에 자동 보존 (복구 가능). Rule surface CRUD MCP 완성 (add/list/remove)', inputSchema: { type: 'object', properties: { id: { type: 'string' }, path: { type: 'string' } }, required: ['id'] } },
|
|
9153
9259
|
{ name: 'leerness_feature_impact', description: '1.9.141 — Feature Causality Graph 인과관계 영향 추적 JSON ({ feature, total, impacted: [{ id, title, depth, via, files, errorModes }] }). 신규 기능 추가/형식 변경 전 호출: id 변경으로 영향받는 다른 feature를 transitive (affects + co-changes + reverse depends-on) 으로 회수. 1+1=20 cascade 방지', inputSchema: { type: 'object', properties: { id: { type: 'string' }, path: { type: 'string' } }, required: ['id'] } },
|
|
9154
|
-
{ name: 'leerness_feature_list', description: '1.9.141 — 전체 Feature Graph 노드 + 엣지 JSON. 외부 AI가 시스템 내 기능 의존성을 한 번에 회수', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } }
|
|
9260
|
+
{ name: 'leerness_feature_list', description: '1.9.141 — 전체 Feature Graph 노드 + 엣지 JSON. 외부 AI가 시스템 내 기능 의존성을 한 번에 회수', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
|
|
9261
|
+
{ name: 'leerness_feature_add', description: '1.9.142 — Feature Graph 에 새 노드 추가 (외부 AI가 코드 작성 중 직접 feature 등록). 인자: { title (required), dependsOn?, affects?, coChangesWith?, files?, path? }. 자동 F-XXXX ID 부여. CRUD 완성에 기여', inputSchema: { type: 'object', properties: { title: { type: 'string' }, dependsOn: { type: 'string' }, affects: { type: 'string' }, coChangesWith: { type: 'string' }, files: { type: 'string' }, path: { type: 'string' } }, required: ['title'] } },
|
|
9262
|
+
{ name: 'leerness_feature_link', description: '1.9.142 — 기존 feature 노드에 의존/영향/공변경 엣지 추가. 인자: { id (required, F-XXXX), dependsOn?, affects?, coChangesWith?, path? }. 외부 AI가 코드 변경 도중 발견한 인과관계를 즉시 그래프에 반영', inputSchema: { type: 'object', properties: { id: { type: 'string' }, dependsOn: { type: 'string' }, affects: { type: 'string' }, coChangesWith: { type: 'string' }, path: { type: 'string' } }, required: ['id'] } }
|
|
9155
9263
|
];
|
|
9156
9264
|
|
|
9157
9265
|
function send(obj) {
|
|
@@ -9228,6 +9336,20 @@ function mcpServeCmd(root) {
|
|
|
9228
9336
|
// 1.9.141: Feature Causality Graph
|
|
9229
9337
|
case 'leerness_feature_impact': cliArgs = ['feature', 'impact', String(args.id || ''), '--path', targetPath, '--json']; break;
|
|
9230
9338
|
case 'leerness_feature_list': cliArgs = ['feature', 'list', '--path', targetPath, '--json']; break;
|
|
9339
|
+
// 1.9.142: Feature Graph WRITE CRUD
|
|
9340
|
+
case 'leerness_feature_add':
|
|
9341
|
+
cliArgs = ['feature', 'add', String(args.title || ''), '--path', targetPath];
|
|
9342
|
+
if (args.dependsOn) cliArgs.push('--depends-on', String(args.dependsOn));
|
|
9343
|
+
if (args.affects) cliArgs.push('--affects', String(args.affects));
|
|
9344
|
+
if (args.coChangesWith) cliArgs.push('--co-changes-with', String(args.coChangesWith));
|
|
9345
|
+
if (args.files) cliArgs.push('--files', String(args.files));
|
|
9346
|
+
break;
|
|
9347
|
+
case 'leerness_feature_link':
|
|
9348
|
+
cliArgs = ['feature', 'link', String(args.id || ''), '--path', targetPath];
|
|
9349
|
+
if (args.dependsOn) cliArgs.push('--depends-on', String(args.dependsOn));
|
|
9350
|
+
if (args.affects) cliArgs.push('--affects', String(args.affects));
|
|
9351
|
+
if (args.coChangesWith) cliArgs.push('--co-changes-with', String(args.coChangesWith));
|
|
9352
|
+
break;
|
|
9231
9353
|
default:
|
|
9232
9354
|
return send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown tool: ${name}` } });
|
|
9233
9355
|
}
|
|
@@ -9512,6 +9634,22 @@ function healthCmd(root) {
|
|
|
9512
9634
|
summary: `T${tasksInProgress}/D${decisionsCount}/R${rulesActive}/P${milestones}/L${lessonsCount}`,
|
|
9513
9635
|
};
|
|
9514
9636
|
} catch { out.memorySurface = { error: 'memorySurface 점검 실패' }; }
|
|
9637
|
+
// 1.9.143: health --json featureGraph 통합 (handoff/session close 와 동일 패턴 — JSON 4종 완성)
|
|
9638
|
+
try {
|
|
9639
|
+
const { nodes: fNodesHe } = _readFeatureGraph(root);
|
|
9640
|
+
const edgeCount = fNodesHe.reduce((s, n) => s + (n.dependsOn?.length || 0) + (n.affects?.length || 0) + (n.coChangesWith?.length || 0), 0);
|
|
9641
|
+
const linkedSet = new Set();
|
|
9642
|
+
for (const n of fNodesHe) {
|
|
9643
|
+
for (const x of [...(n.dependsOn||[]), ...(n.affects||[]), ...(n.coChangesWith||[])]) { linkedSet.add(n.id); linkedSet.add(x); }
|
|
9644
|
+
}
|
|
9645
|
+
const isolated = fNodesHe.length ? (fNodesHe.length - linkedSet.size) : 0;
|
|
9646
|
+
out.featureGraph = {
|
|
9647
|
+
total: fNodesHe.length,
|
|
9648
|
+
edges: edgeCount,
|
|
9649
|
+
isolated: Math.max(0, isolated),
|
|
9650
|
+
summary: `F${fNodesHe.length}/E${edgeCount}${isolated > 0 ? `/iso${isolated}` : ''}`
|
|
9651
|
+
};
|
|
9652
|
+
} catch { out.featureGraph = { error: 'featureGraph 점검 실패' }; }
|
|
9515
9653
|
// 6) issues 요약 (사용자 글로벌 룰 가시화)
|
|
9516
9654
|
const issues = [];
|
|
9517
9655
|
if (out.checks.drift?.level && !/healthy/.test(out.checks.drift.level)) issues.push(`drift ${out.checks.drift.level}`);
|