@aiready/visualizer 0.7.2 → 0.7.4

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/dist/index.js CHANGED
@@ -1,6 +1,79 @@
1
1
  import fs from 'fs';
2
- import path from 'path';
3
- import { UnifiedReportSchema, ToolName, Severity } from '@aiready/core';
2
+ import path2 from 'path';
3
+ import { Severity, UnifiedReportSchema, ToolName, normalizeAnalysisResult } from '@aiready/core';
4
+
5
+ // src/graph/builder.ts
6
+ var GRAPH_CONSTANTS = {
7
+ DEFAULT_NODE_SIZE: 1,
8
+ DEFAULT_REFERENCE_SIZE: 5,
9
+ DEFAULT_DEPENDENCY_SIZE: 2,
10
+ DEFAULT_CONTEXT_SIZE: 10,
11
+ FUZZY_MATCH_THRESHOLD: 50,
12
+ FUZZY_MATCH_HIGH_THRESHOLD: 80,
13
+ COLORS: {
14
+ CRITICAL: "#ff4d4f",
15
+ MAJOR: "#ff9900",
16
+ MINOR: "#ffd666",
17
+ INFO: "#91d5ff",
18
+ DEFAULT: "#97c2fc"
19
+ },
20
+ SEVERITY_ORDER: {
21
+ [Severity.Critical]: 3,
22
+ [Severity.Major]: 2,
23
+ [Severity.Minor]: 1,
24
+ [Severity.Info]: 0
25
+ }
26
+ };
27
+ function normalizeLabel(filePath, rootDir) {
28
+ try {
29
+ return path2.relative(rootDir, filePath);
30
+ } catch {
31
+ return filePath;
32
+ }
33
+ }
34
+ function extractReferencedPaths(message) {
35
+ if (!message || typeof message !== "string") return [];
36
+ const reAbs = /\/(?:[\w\-.]+\/)+[\w\-.]+\.(?:ts|tsx|js|jsx|py|java|go)/g;
37
+ const reRel = /(?:\.\/|\.\.\/)(?:[\w\-.]+\/)+[\w\-.]+\.(?:ts|tsx|js|jsx|py|java|go)/g;
38
+ const abs = message.match(reAbs) ?? [];
39
+ const rel = message.match(reRel) ?? [];
40
+ return abs.concat(rel);
41
+ }
42
+ function getPackageGroup(fp) {
43
+ if (!fp) return void 0;
44
+ const parts = fp.split(path2.sep);
45
+ const pkgIdx = parts.indexOf("packages");
46
+ if (pkgIdx >= 0 && parts.length > pkgIdx + 1)
47
+ return `packages/${parts[pkgIdx + 1]}`;
48
+ const landingIdx = parts.indexOf("landing");
49
+ if (landingIdx >= 0) return "landing";
50
+ const scriptsIdx = parts.indexOf("scripts");
51
+ if (scriptsIdx >= 0) return "scripts";
52
+ return parts.length > 1 ? parts[1] : parts[0];
53
+ }
54
+ function rankSeverity(s) {
55
+ if (!s) return null;
56
+ const ss = String(s).toLowerCase();
57
+ if (ss.includes("critical")) return Severity.Critical;
58
+ if (ss.includes("major")) return Severity.Major;
59
+ if (ss.includes("minor")) return Severity.Minor;
60
+ if (ss.includes("info")) return Severity.Info;
61
+ return null;
62
+ }
63
+ function getColorForSeverity(sev) {
64
+ switch (sev) {
65
+ case Severity.Critical:
66
+ return GRAPH_CONSTANTS.COLORS.CRITICAL;
67
+ case Severity.Major:
68
+ return GRAPH_CONSTANTS.COLORS.MAJOR;
69
+ case Severity.Minor:
70
+ return GRAPH_CONSTANTS.COLORS.MINOR;
71
+ case Severity.Info:
72
+ return GRAPH_CONSTANTS.COLORS.INFO;
73
+ default:
74
+ return GRAPH_CONSTANTS.COLORS.DEFAULT;
75
+ }
76
+ }
4
77
 
5
78
  // src/graph/builder.ts
6
79
  var GraphBuilder = class _GraphBuilder {
@@ -10,97 +83,59 @@ var GraphBuilder = class _GraphBuilder {
10
83
  this.edges = [];
11
84
  this.edgesSet = /* @__PURE__ */ new Set();
12
85
  }
13
- normalizeLabel(filePath) {
14
- try {
15
- return path.relative(this.rootDir, filePath);
16
- } catch {
17
- return filePath;
18
- }
19
- }
20
- extractReferencedPaths(message) {
21
- if (!message || typeof message !== "string") return [];
22
- const reAbs = /\/(?:[\w\-.]+\/)+[\w\-.]+\.(?:ts|tsx|js|jsx|py|java|go)/g;
23
- const reRel = /(?:\.\/|\.\.\/)(?:[\w\-.]+\/)+[\w\-.]+\.(?:ts|tsx|js|jsx|py|java|go)/g;
24
- const abs = message.match(reAbs) ?? [];
25
- const rel = message.match(reRel) ?? [];
26
- return abs.concat(rel);
27
- }
28
- getPackageGroup(fp) {
29
- if (!fp) return null;
30
- const parts = fp.split(path.sep);
31
- const pkgIdx = parts.indexOf("packages");
32
- if (pkgIdx >= 0 && parts.length > pkgIdx + 1)
33
- return `packages/${parts[pkgIdx + 1]}`;
34
- const landingIdx = parts.indexOf("landing");
35
- if (landingIdx >= 0) return "landing";
36
- const scriptsIdx = parts.indexOf("scripts");
37
- if (scriptsIdx >= 0) return "scripts";
38
- return parts.length > 1 ? parts[1] : parts[0];
39
- }
40
86
  /**
41
87
  * Add a new node to the graph or update an existing one.
42
- *
43
- * @param file - Unique identifier for the file (node ID).
44
- * @param title - Optional title or description for the node.
45
- * @param value - Numerical value representing the node size/weight.
46
88
  */
47
- addNode(file, title = "", value = 1) {
89
+ addNode(file, title = "", size = GRAPH_CONSTANTS.DEFAULT_NODE_SIZE) {
48
90
  if (!file) return;
49
- const id = path.resolve(this.rootDir, file);
50
- if (!this.nodesMap.has(id)) {
91
+ const id = path2.resolve(this.rootDir, file);
92
+ const existingNode = this.nodesMap.get(id);
93
+ if (!existingNode) {
51
94
  const node = {
52
95
  id,
53
96
  path: id,
54
- label: this.normalizeLabel(id),
97
+ label: normalizeLabel(id, this.rootDir),
55
98
  title,
56
- size: value ?? 1
99
+ size
57
100
  };
58
101
  this.nodesMap.set(id, node);
59
102
  } else {
60
- const node = this.nodesMap.get(id);
61
- if (title && (!node.title || !node.title.includes(title))) {
62
- node.title = (node.title ? node.title + "\n" : "") + title;
103
+ if (title && (!existingNode.title || !existingNode.title.includes(title))) {
104
+ existingNode.title = (existingNode.title ? existingNode.title + "\n" : "") + title;
105
+ }
106
+ if (size > (existingNode.size ?? 0)) {
107
+ existingNode.size = size;
63
108
  }
64
- if (value > (node.size ?? 0)) node.size = value;
65
109
  }
66
110
  }
67
111
  /**
68
112
  * Add a directed edge between two nodes in the graph.
69
- *
70
- * @param from - Source node ID (file path).
71
- * @param to - Target node ID (file path).
72
- * @param type - Type of relationship (e.g., 'dependency', 'reference').
73
113
  */
74
114
  addEdge(from, to, type = "link") {
75
115
  if (!from || !to) return;
76
- const a = path.resolve(this.rootDir, from);
77
- const b = path.resolve(this.rootDir, to);
78
- if (a === b) return;
79
- const key = `${a}->${b}`;
116
+ const source = path2.resolve(this.rootDir, from);
117
+ const target = path2.resolve(this.rootDir, to);
118
+ if (source === target) return;
119
+ const key = `${source}->${target}`;
80
120
  if (!this.edgesSet.has(key)) {
81
- this.edges.push({ source: a, target: b, type });
121
+ this.edges.push({ source, target, type });
82
122
  this.edgesSet.add(key);
83
123
  }
84
124
  }
85
125
  /**
86
126
  * Build the final GraphData object from collected nodes and edges.
87
- *
88
- * @returns Consolidated graph data structure.
89
127
  */
90
128
  build() {
91
129
  const nodes = Array.from(this.nodesMap.values());
92
- const edges = this.edges.map(
93
- (e) => ({ source: e.source, target: e.target, type: e.type })
94
- );
95
130
  return {
96
131
  nodes,
97
- edges,
132
+ edges: [...this.edges],
98
133
  clusters: [],
99
134
  issues: [],
100
135
  metadata: {
101
136
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
102
137
  totalFiles: nodes.length,
103
- totalDependencies: edges.length,
138
+ totalDependencies: this.edges.length,
104
139
  analysisTypes: [],
105
140
  criticalIssues: 0,
106
141
  majorIssues: 0,
@@ -115,10 +150,6 @@ var GraphBuilder = class _GraphBuilder {
115
150
  }
116
151
  /**
117
152
  * Static helper to build graph from an AIReady report JSON.
118
- *
119
- * @param report - Unified AIReady report object.
120
- * @param rootDir - Root directory for path resolution.
121
- * @returns Fully populated GraphData for visualization.
122
153
  */
123
154
  static buildFromReport(report, rootDir = process.cwd()) {
124
155
  const validation = UnifiedReportSchema.safeParse(report);
@@ -129,31 +160,18 @@ var GraphBuilder = class _GraphBuilder {
129
160
  }
130
161
  const builder = new _GraphBuilder(rootDir);
131
162
  const fileIssues = /* @__PURE__ */ new Map();
132
- const rankSeverity = (s) => {
133
- if (!s) return null;
134
- const ss = String(s).toLowerCase();
135
- if (ss.includes("critical")) return Severity.Critical;
136
- if (ss.includes("major")) return Severity.Major;
137
- if (ss.includes("minor")) return Severity.Minor;
138
- if (ss.includes("info")) return Severity.Info;
139
- return null;
140
- };
141
- const bumpIssue = (file, sev) => {
163
+ const bumpIssue = (file, severity) => {
142
164
  if (!file) return;
143
- const id = path.resolve(rootDir, file);
144
- if (!fileIssues.has(id))
165
+ const id = path2.resolve(rootDir, file);
166
+ if (!fileIssues.has(id)) {
145
167
  fileIssues.set(id, { count: 0, maxSeverity: null, duplicates: 0 });
146
- const rec = fileIssues.get(id);
147
- rec.count += 1;
148
- if (sev) {
149
- const order = {
150
- [Severity.Critical]: 3,
151
- [Severity.Major]: 2,
152
- [Severity.Minor]: 1,
153
- [Severity.Info]: 0
154
- };
155
- if (!rec.maxSeverity || order[sev] > order[rec.maxSeverity])
156
- rec.maxSeverity = sev;
168
+ }
169
+ const record = fileIssues.get(id);
170
+ record.count += 1;
171
+ if (severity) {
172
+ if (!record.maxSeverity || GRAPH_CONSTANTS.SEVERITY_ORDER[severity] > GRAPH_CONSTANTS.SEVERITY_ORDER[record.maxSeverity]) {
173
+ record.maxSeverity = severity;
174
+ }
157
175
  }
158
176
  };
159
177
  const getResults = (toolKey, legacyKey) => {
@@ -163,120 +181,121 @@ var GraphBuilder = class _GraphBuilder {
163
181
  if (Array.isArray(toolData)) return toolData;
164
182
  return toolData.results ?? toolData.issues ?? [];
165
183
  };
184
+ this.processPatterns(builder, getResults(ToolName.PatternDetect, "patterns"), rootDir, bumpIssue);
185
+ this.processDuplicates(builder, report, rootDir, fileIssues);
186
+ this.processContext(builder, getResults(ToolName.ContextAnalyzer, "context"), rootDir, bumpIssue);
187
+ this.processToolResults(builder, ToolName.DocDrift, "docDrift", report, bumpIssue, "Doc-Drift Issue");
188
+ this.processToolResults(builder, ToolName.DependencyHealth, "dependencyHealth", report, bumpIssue, "Dependency Issue");
189
+ this.processToolResults(builder, ToolName.ContractEnforcement, "contractEnforcement", report, bumpIssue, "Contract Gap");
190
+ return this.finalizeGraph(builder, fileIssues, report);
191
+ }
192
+ static processPatterns(builder, results, rootDir, bumpIssue) {
166
193
  const basenameMap = /* @__PURE__ */ new Map();
167
- const patternResults = getResults(ToolName.PatternDetect, "patterns");
168
- patternResults.forEach((p) => {
194
+ results.forEach((p) => {
169
195
  const fileName = p.fileName ?? p.file;
170
196
  if (fileName) {
171
- const base = path.basename(fileName);
197
+ const base = path2.basename(fileName);
172
198
  if (!basenameMap.has(base)) basenameMap.set(base, /* @__PURE__ */ new Set());
173
199
  basenameMap.get(base).add(fileName);
174
200
  }
175
201
  });
176
- patternResults.forEach((entry) => {
177
- const file = entry.fileName ?? entry.file;
202
+ results.forEach((entry) => {
203
+ const normalized = normalizeAnalysisResult(entry);
204
+ const file = normalized.fileName;
178
205
  if (!file) return;
179
206
  builder.addNode(
180
207
  file,
181
- `Issues: ${(entry.issues ?? []).length}`,
182
- entry.metrics?.tokenCost ?? 5
208
+ `Issues: ${normalized.issues.length}`,
209
+ normalized.metrics.tokenCost || GRAPH_CONSTANTS.DEFAULT_REFERENCE_SIZE
183
210
  );
184
- if ((entry.issues ?? []).length > 0) {
185
- (entry.issues ?? []).forEach((issue) => {
186
- const sev = rankSeverity(
187
- issue.severity ?? issue.severityLevel ?? null
188
- );
189
- bumpIssue(file, sev);
211
+ const rawIssues = Array.isArray(entry.issues) ? entry.issues : [];
212
+ if (rawIssues.length > 0) {
213
+ rawIssues.forEach((issue) => {
214
+ bumpIssue(file, rankSeverity(issue.severity));
215
+ });
216
+ } else {
217
+ normalized.issues.forEach((issue) => {
218
+ bumpIssue(file, issue.severity);
190
219
  });
191
220
  }
192
- (entry.issues ?? []).forEach((issue) => {
193
- const message = issue.message || "";
194
- const refs = builder.extractReferencedPaths(message);
221
+ normalized.issues.forEach((issue) => {
222
+ const refs = extractReferencedPaths(issue.message);
195
223
  refs.forEach((ref) => {
196
- let target = ref;
197
- if (!path.isAbsolute(ref)) {
198
- target = path.resolve(path.dirname(file), ref);
199
- }
200
- builder.addNode(target, "Referenced file", 5);
224
+ const target = path2.isAbsolute(ref) ? ref : path2.resolve(path2.dirname(file), ref);
225
+ builder.addNode(target, "Referenced file", GRAPH_CONSTANTS.DEFAULT_REFERENCE_SIZE);
201
226
  builder.addEdge(file, target, "reference");
202
227
  });
203
- const percMatch = (message.match(/(\d+)%/) || [])[1];
228
+ const percMatch = (issue.message.match(/(\d+)%/) || [])[1];
204
229
  const perc = percMatch ? parseInt(percMatch, 10) : null;
205
- const wantFuzzy = issue.type === "duplicate-pattern" || /similar/i.test(message) || perc && perc >= 50;
230
+ const wantFuzzy = issue.type === "duplicate-pattern" || /similar/i.test(issue.message) || perc !== null && perc >= GRAPH_CONSTANTS.FUZZY_MATCH_THRESHOLD;
206
231
  if (wantFuzzy) {
207
- const fileGroup = builder.getPackageGroup(file);
232
+ const fileGroup = getPackageGroup(file);
208
233
  for (const [base, pathsSet] of basenameMap.entries()) {
209
- if (!message.includes(base) || base === path.basename(file))
210
- continue;
234
+ if (!issue.message.includes(base) || base === path2.basename(file)) continue;
211
235
  for (const target of pathsSet) {
212
- const targetGroup = builder.getPackageGroup(target);
213
- if (fileGroup !== targetGroup && !(perc && perc >= 80)) continue;
214
- builder.addNode(target, "Fuzzy match", 5);
236
+ const targetGroup = getPackageGroup(target);
237
+ if (fileGroup !== targetGroup && !(perc !== null && perc >= GRAPH_CONSTANTS.FUZZY_MATCH_HIGH_THRESHOLD)) continue;
238
+ builder.addNode(target, "Fuzzy match", GRAPH_CONSTANTS.DEFAULT_REFERENCE_SIZE);
215
239
  builder.addEdge(file, target, "similarity");
216
240
  }
217
241
  }
218
242
  }
219
243
  });
220
244
  });
245
+ }
246
+ static processDuplicates(builder, report, rootDir, fileIssues) {
221
247
  const patternData = report[ToolName.PatternDetect] || report.patternDetect || report.patterns || {};
222
248
  const duplicates = (Array.isArray(patternData.duplicates) ? patternData.duplicates : null) || (patternData.summary && Array.isArray(patternData.summary.duplicates) ? patternData.summary.duplicates : null) || (Array.isArray(report.duplicates) ? report.duplicates : []);
223
249
  duplicates.forEach((dup) => {
224
- builder.addNode(dup.file1, "Similarity target", 5);
225
- builder.addNode(dup.file2, "Similarity target", 5);
250
+ builder.addNode(dup.file1, "Similarity target", GRAPH_CONSTANTS.DEFAULT_REFERENCE_SIZE);
251
+ builder.addNode(dup.file2, "Similarity target", GRAPH_CONSTANTS.DEFAULT_REFERENCE_SIZE);
226
252
  builder.addEdge(dup.file1, dup.file2, "similarity");
227
- const f1 = path.resolve(rootDir, dup.file1);
228
- const f2 = path.resolve(rootDir, dup.file2);
229
- if (!fileIssues.has(f1))
230
- fileIssues.set(f1, { count: 0, maxSeverity: null, duplicates: 0 });
231
- if (!fileIssues.has(f2))
232
- fileIssues.set(f2, { count: 0, maxSeverity: null, duplicates: 0 });
233
- fileIssues.get(f1).duplicates += 1;
234
- fileIssues.get(f2).duplicates += 1;
253
+ [dup.file1, dup.file2].forEach((file) => {
254
+ const id = path2.resolve(rootDir, file);
255
+ if (!fileIssues.has(id)) {
256
+ fileIssues.set(id, { count: 0, maxSeverity: null, duplicates: 0 });
257
+ }
258
+ fileIssues.get(id).duplicates += 1;
259
+ });
235
260
  });
236
- const contextResults = getResults(ToolName.ContextAnalyzer, "context");
237
- contextResults.forEach((ctx) => {
238
- const file = ctx.fileName ?? ctx.file;
261
+ }
262
+ static processContext(builder, results, rootDir, bumpIssue) {
263
+ results.forEach((ctx) => {
264
+ const normalized = normalizeAnalysisResult(ctx);
265
+ const file = normalized.fileName;
239
266
  if (!file) return;
240
- builder.addNode(file, `Deps: ${ctx.dependencyCount || 0}`, 10);
241
- if (ctx.issues && Array.isArray(ctx.issues)) {
242
- ctx.issues.forEach((issue) => {
243
- const sev = rankSeverity(
244
- typeof issue === "string" ? ctx.severity : issue.severity ?? issue.severityLevel ?? null
245
- );
246
- bumpIssue(file, sev);
247
- });
248
- }
267
+ builder.addNode(file, `Deps: ${ctx.dependencyCount || 0}`, GRAPH_CONSTANTS.DEFAULT_CONTEXT_SIZE);
268
+ normalized.issues.forEach((issue) => {
269
+ bumpIssue(file, issue.severity);
270
+ });
249
271
  (ctx.relatedFiles ?? []).forEach((rel) => {
250
- const resolvedRel = path.isAbsolute(rel) ? rel : path.resolve(path.dirname(file), rel);
251
- const keyA = `${path.resolve(builder.rootDir, file)}->${path.resolve(builder.rootDir, resolvedRel)}`;
252
- const keyB = `${path.resolve(builder.rootDir, resolvedRel)}->${path.resolve(builder.rootDir, file)}`;
253
- if (builder.edgesSet.has(keyA) || builder.edgesSet.has(keyB))
254
- return;
255
- builder.addNode(resolvedRel, "Related file", 5);
256
- const n = builder.nodesMap.get(
257
- path.resolve(builder.rootDir, resolvedRel)
258
- );
259
- if (n) n.size = (n.size || 1) + 2;
260
- try {
261
- builder.addEdge(file, resolvedRel, "related");
262
- } catch {
272
+ const resolvedRel = path2.isAbsolute(rel) ? rel : path2.resolve(path2.dirname(file), rel);
273
+ const resolvedFile = path2.resolve(builder.rootDir, file);
274
+ const resolvedTarget = path2.resolve(builder.rootDir, resolvedRel);
275
+ const keyA = `${resolvedFile}->${resolvedTarget}`;
276
+ const keyB = `${resolvedTarget}->${resolvedFile}`;
277
+ if (builder["edgesSet"].has(keyA) || builder["edgesSet"].has(keyB)) return;
278
+ builder.addNode(resolvedRel, "Related file", GRAPH_CONSTANTS.DEFAULT_REFERENCE_SIZE);
279
+ const node = builder["nodesMap"].get(resolvedTarget);
280
+ if (node) {
281
+ node.size = (node.size || 1) + 2;
263
282
  }
283
+ builder.addEdge(file, resolvedRel, "related");
264
284
  });
265
- const absoluteFile = path.resolve(builder.rootDir, file);
266
- const fileDir = path.dirname(absoluteFile);
285
+ const fileDir = path2.dirname(path2.resolve(builder.rootDir, file));
267
286
  (ctx.dependencyList ?? []).forEach((dep) => {
268
287
  if (dep.startsWith(".") || dep.startsWith("/")) {
269
288
  const possiblePaths = [
270
- path.resolve(fileDir, dep),
271
- path.resolve(fileDir, dep + ".ts"),
272
- path.resolve(fileDir, dep + ".tsx"),
273
- path.resolve(fileDir, dep + ".js"),
274
- path.resolve(fileDir, dep, "index.ts"),
275
- path.resolve(fileDir, dep, "index.tsx")
289
+ path2.resolve(fileDir, dep),
290
+ path2.resolve(fileDir, dep + ".ts"),
291
+ path2.resolve(fileDir, dep + ".tsx"),
292
+ path2.resolve(fileDir, dep + ".js"),
293
+ path2.resolve(fileDir, dep, "index.ts"),
294
+ path2.resolve(fileDir, dep, "index.tsx")
276
295
  ];
277
296
  for (const p of possiblePaths) {
278
297
  if (fs.existsSync(p)) {
279
- builder.addNode(p, "Dependency", 2);
298
+ builder.addNode(p, "Dependency", GRAPH_CONSTANTS.DEFAULT_DEPENDENCY_SIZE);
280
299
  builder.addEdge(file, p, "dependency");
281
300
  break;
282
301
  }
@@ -284,100 +303,60 @@ var GraphBuilder = class _GraphBuilder {
284
303
  }
285
304
  });
286
305
  });
287
- const docDriftResults = getResults(ToolName.DocDrift, "docDrift");
288
- docDriftResults.forEach((issue) => {
289
- const file = issue.fileName ?? issue.location?.file;
290
- if (file) {
291
- builder.addNode(file, "Doc-Drift Issue", 5);
292
- const sev = rankSeverity(issue.severity ?? null);
293
- bumpIssue(file, sev);
294
- }
295
- });
296
- const depsResults = getResults(
297
- ToolName.DependencyHealth,
298
- "dependencyHealth"
299
- );
300
- depsResults.forEach((issue) => {
301
- const file = issue.fileName ?? issue.location?.file;
302
- if (file) {
303
- builder.addNode(file, "Dependency Issue", 5);
304
- const sev = rankSeverity(issue.severity ?? null);
305
- bumpIssue(file, sev);
306
+ }
307
+ static processToolResults(builder, toolName, legacyKey, report, bumpIssue, title) {
308
+ const camelKey = toolName.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
309
+ const toolData = report[toolName] ?? report[camelKey] ?? report[legacyKey];
310
+ if (!toolData) return;
311
+ const results = Array.isArray(toolData) ? toolData : toolData.results ?? toolData.issues ?? [];
312
+ results.forEach((item) => {
313
+ if (!Array.isArray(item.issues) && (item.severity || item.message)) {
314
+ const file2 = item.fileName ?? item.file ?? item.location?.file;
315
+ if (file2) {
316
+ builder.addNode(file2, title, GRAPH_CONSTANTS.DEFAULT_REFERENCE_SIZE);
317
+ bumpIssue(file2, rankSeverity(item.severity));
318
+ }
319
+ return;
306
320
  }
307
- });
308
- const contractEnforcementResults = getResults(
309
- ToolName.ContractEnforcement,
310
- "contractEnforcement"
311
- );
312
- contractEnforcementResults.forEach((issue) => {
313
- const file = issue.fileName ?? issue.location?.file;
321
+ const normalized = normalizeAnalysisResult(item);
322
+ const file = normalized.fileName;
314
323
  if (file) {
315
- builder.addNode(file, "Contract Gap", 5);
316
- const sev = rankSeverity(issue.severity ?? null);
317
- bumpIssue(file, sev);
324
+ builder.addNode(file, title, GRAPH_CONSTANTS.DEFAULT_REFERENCE_SIZE);
325
+ normalized.issues.forEach((issue) => {
326
+ bumpIssue(file, issue.severity);
327
+ });
318
328
  }
319
329
  });
320
- const nodes = Array.from(builder.nodesMap.values());
321
- const edges = builder.edges;
322
- const colorFor = (sev) => {
323
- switch (sev) {
324
- case Severity.Critical:
325
- return "#ff4d4f";
326
- // red
327
- case Severity.Major:
328
- return "#ff9900";
329
- // orange
330
- case Severity.Minor:
331
- return "#ffd666";
332
- // yellow
333
- case Severity.Info:
334
- return "#91d5ff";
335
- // light blue
336
- default:
337
- return "#97c2fc";
338
- }
339
- };
330
+ }
331
+ static finalizeGraph(builder, fileIssues, report) {
332
+ const graph = builder.build();
340
333
  let criticalIssues = 0;
341
334
  let majorIssues = 0;
342
335
  let minorIssues = 0;
343
336
  let infoIssues = 0;
344
- for (const node of nodes) {
345
- const n = node;
346
- const rec = fileIssues.get(n.id);
347
- if (rec) {
348
- n.duplicates = rec.duplicates || 0;
349
- n.color = colorFor(rec.maxSeverity);
350
- n.group = builder.getPackageGroup(n.id) || void 0;
351
- if (rec.maxSeverity === Severity.Critical) criticalIssues += rec.count;
352
- else if (rec.maxSeverity === Severity.Major) majorIssues += rec.count;
353
- else if (rec.maxSeverity === Severity.Minor) minorIssues += rec.count;
354
- else if (rec.maxSeverity === Severity.Info) infoIssues += rec.count;
337
+ graph.nodes.forEach((node) => {
338
+ const record = fileIssues.get(node.id);
339
+ if (record) {
340
+ node.duplicates = record.duplicates || 0;
341
+ node.color = getColorForSeverity(record.maxSeverity);
342
+ node.group = getPackageGroup(node.id);
343
+ if (record.maxSeverity === Severity.Critical) criticalIssues += record.count;
344
+ else if (record.maxSeverity === Severity.Major) majorIssues += record.count;
345
+ else if (record.maxSeverity === Severity.Minor) minorIssues += record.count;
346
+ else if (record.maxSeverity === Severity.Info) infoIssues += record.count;
355
347
  } else {
356
- n.color = colorFor(null);
357
- n.group = builder.getPackageGroup(n.id) || void 0;
358
- n.duplicates = 0;
359
- }
360
- }
361
- const graph = {
362
- nodes,
363
- edges,
364
- clusters: [],
365
- issues: [],
366
- metadata: {
367
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
368
- totalFiles: nodes.length,
369
- totalDependencies: edges.length,
370
- analysisTypes: [],
371
- criticalIssues,
372
- majorIssues,
373
- minorIssues,
374
- infoIssues,
375
- tokenBudget: report.scoring?.tokenBudget
376
- },
377
- truncated: {
378
- nodes: false,
379
- edges: false
348
+ node.color = getColorForSeverity(null);
349
+ node.group = getPackageGroup(node.id);
350
+ node.duplicates = 0;
380
351
  }
352
+ });
353
+ graph.metadata = {
354
+ ...graph.metadata,
355
+ criticalIssues,
356
+ majorIssues,
357
+ minorIssues,
358
+ infoIssues,
359
+ tokenBudget: report.scoring?.tokenBudget
381
360
  };
382
361
  return graph;
383
362
  }
@@ -387,11 +366,7 @@ function createSampleGraph() {
387
366
  builder.addNode("src/components/Button.tsx", "Button", 15);
388
367
  builder.addNode("src/utils/helpers.ts", "helpers", 12);
389
368
  builder.addNode("src/services/api.ts", "api", 18);
390
- builder.addEdge(
391
- "src/components/Button.tsx",
392
- "src/utils/helpers.ts",
393
- "dependency"
394
- );
369
+ builder.addEdge("src/components/Button.tsx", "src/utils/helpers.ts", "dependency");
395
370
  builder.addEdge("src/utils/helpers.ts", "src/services/api.ts", "dependency");
396
371
  return builder.build();
397
372
  }