@toolbaux/guardian 0.1.0

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.
Files changed (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +366 -0
  3. package/dist/adapters/csharp-adapter.js +149 -0
  4. package/dist/adapters/go-adapter.js +96 -0
  5. package/dist/adapters/index.js +16 -0
  6. package/dist/adapters/java-adapter.js +122 -0
  7. package/dist/adapters/python-adapter.js +183 -0
  8. package/dist/adapters/runner.js +69 -0
  9. package/dist/adapters/types.js +1 -0
  10. package/dist/adapters/typescript-adapter.js +179 -0
  11. package/dist/benchmarking/framework.js +91 -0
  12. package/dist/cli.js +343 -0
  13. package/dist/commands/analyze-depth.js +43 -0
  14. package/dist/commands/api-spec-extractor.js +52 -0
  15. package/dist/commands/breaking-change-analyzer.js +334 -0
  16. package/dist/commands/config-compliance.js +219 -0
  17. package/dist/commands/constraints.js +221 -0
  18. package/dist/commands/context.js +101 -0
  19. package/dist/commands/data-flow-tracer.js +291 -0
  20. package/dist/commands/dependency-impact-analyzer.js +27 -0
  21. package/dist/commands/diff.js +146 -0
  22. package/dist/commands/discrepancy.js +71 -0
  23. package/dist/commands/doc-generate.js +163 -0
  24. package/dist/commands/doc-html.js +120 -0
  25. package/dist/commands/drift.js +88 -0
  26. package/dist/commands/extract.js +16 -0
  27. package/dist/commands/feature-context.js +116 -0
  28. package/dist/commands/generate.js +339 -0
  29. package/dist/commands/guard.js +182 -0
  30. package/dist/commands/init.js +209 -0
  31. package/dist/commands/intel.js +20 -0
  32. package/dist/commands/license-dependency-auditor.js +33 -0
  33. package/dist/commands/performance-hotspot-profiler.js +42 -0
  34. package/dist/commands/search.js +314 -0
  35. package/dist/commands/security-boundary-auditor.js +359 -0
  36. package/dist/commands/simulate.js +294 -0
  37. package/dist/commands/summary.js +27 -0
  38. package/dist/commands/test-coverage-mapper.js +264 -0
  39. package/dist/commands/verify-drift.js +62 -0
  40. package/dist/config.js +441 -0
  41. package/dist/extract/ai-context-hints.js +107 -0
  42. package/dist/extract/analyzers/backend.js +1704 -0
  43. package/dist/extract/analyzers/depth.js +264 -0
  44. package/dist/extract/analyzers/frontend.js +2221 -0
  45. package/dist/extract/api-usage-tracker.js +19 -0
  46. package/dist/extract/cache.js +53 -0
  47. package/dist/extract/codebase-intel.js +190 -0
  48. package/dist/extract/compress.js +452 -0
  49. package/dist/extract/context-block.js +356 -0
  50. package/dist/extract/contracts.js +183 -0
  51. package/dist/extract/discrepancies.js +233 -0
  52. package/dist/extract/docs-loader.js +110 -0
  53. package/dist/extract/docs.js +2379 -0
  54. package/dist/extract/drift.js +1578 -0
  55. package/dist/extract/duplicates.js +435 -0
  56. package/dist/extract/feature-arcs.js +138 -0
  57. package/dist/extract/graph.js +76 -0
  58. package/dist/extract/html-doc.js +1409 -0
  59. package/dist/extract/ignore.js +45 -0
  60. package/dist/extract/index.js +455 -0
  61. package/dist/extract/llm-client.js +159 -0
  62. package/dist/extract/pattern-registry.js +141 -0
  63. package/dist/extract/product-doc.js +497 -0
  64. package/dist/extract/python.js +1202 -0
  65. package/dist/extract/runtime.js +193 -0
  66. package/dist/extract/schema-evolution-validator.js +35 -0
  67. package/dist/extract/test-gap-analyzer.js +20 -0
  68. package/dist/extract/tests.js +74 -0
  69. package/dist/extract/types.js +1 -0
  70. package/dist/extract/validate-backend.js +30 -0
  71. package/dist/extract/writer.js +11 -0
  72. package/dist/output-layout.js +37 -0
  73. package/dist/project-discovery.js +309 -0
  74. package/dist/schema/architecture.js +350 -0
  75. package/dist/schema/feature-spec.js +89 -0
  76. package/dist/schema/index.js +8 -0
  77. package/dist/schema/ux.js +46 -0
  78. package/package.json +75 -0
@@ -0,0 +1,1578 @@
1
+ import fs from "node:fs/promises";
2
+ import crypto from "node:crypto";
3
+ import path from "node:path";
4
+ import yaml from "js-yaml";
5
+ import ts from "typescript";
6
+ import Parser from "tree-sitter";
7
+ import Python from "tree-sitter-python";
8
+ import { analyzeBackend } from "./analyzers/backend.js";
9
+ import { analyzeFrontend } from "./analyzers/frontend.js";
10
+ import { loadSpecGuardConfig } from "../config.js";
11
+ const EPSILON = 1e-6;
12
+ export async function computeProjectDrift(options) {
13
+ const resolvedBackendRoot = await resolveBackendRoot(options.backendRoot);
14
+ const resolvedFrontendRoot = path.resolve(options.frontendRoot);
15
+ const config = await loadSpecGuardConfig({
16
+ backendRoot: resolvedBackendRoot,
17
+ frontendRoot: resolvedFrontendRoot,
18
+ configPath: options.configPath
19
+ });
20
+ const backend = await analyzeBackend(resolvedBackendRoot, config);
21
+ await analyzeFrontend(resolvedFrontendRoot, config);
22
+ const projectRoot = findCommonRoot([resolvedBackendRoot, resolvedFrontendRoot]);
23
+ return computeDriftReport({
24
+ backendRoot: resolvedBackendRoot,
25
+ modules: backend.modules,
26
+ moduleGraph: backend.moduleGraph,
27
+ fileGraph: backend.fileGraph,
28
+ circularDependencies: backend.circularDependencies,
29
+ config,
30
+ projectRoot
31
+ });
32
+ }
33
+ export async function computeDriftReport(params) {
34
+ const { modules, moduleGraph, fileGraph, circularDependencies, config, projectRoot, backendRoot } = params;
35
+ const requestedLevel = config.drift?.graphLevel ?? "module";
36
+ const requestedScales = new Set(config.drift?.scales ?? []);
37
+ const moduleGraphData = buildModuleGraph(modules, moduleGraph);
38
+ const fileGraphData = buildFileGraph(modules, fileGraph);
39
+ const shouldBuildFunction = requestedLevel === "function" ||
40
+ requestedLevel === "auto" ||
41
+ requestedScales.has("function");
42
+ const functionGraphData = shouldBuildFunction
43
+ ? await buildFunctionGraph({
44
+ backendRoot,
45
+ modules,
46
+ projectRoot
47
+ })
48
+ : null;
49
+ const domainGraphData = buildDomainGraph(modules, moduleGraph, config);
50
+ const graphs = [];
51
+ if (requestedScales.size === 0 || requestedScales.has("module")) {
52
+ graphs.push(moduleGraphData);
53
+ }
54
+ if ((requestedScales.size === 0 || requestedScales.has("file")) && fileGraphData) {
55
+ graphs.push(fileGraphData);
56
+ }
57
+ if ((requestedScales.size === 0 || requestedScales.has("function")) && functionGraphData) {
58
+ graphs.push(functionGraphData);
59
+ }
60
+ if ((requestedScales.size === 0 || requestedScales.has("domain")) && domainGraphData) {
61
+ graphs.push(domainGraphData);
62
+ }
63
+ if (graphs.length === 0) {
64
+ graphs.push(moduleGraphData);
65
+ }
66
+ const scaleReports = await Promise.all(graphs.map((graph) => computeDriftForGraph({
67
+ graph,
68
+ circularDependencies,
69
+ config,
70
+ projectRoot
71
+ })));
72
+ const primaryLevel = resolvePrimaryLevel(requestedLevel, scaleReports);
73
+ const primary = scaleReports.find((report) => report.level === primaryLevel) ?? scaleReports[0];
74
+ return {
75
+ version: "0.3",
76
+ graph_level: primary.level,
77
+ metrics: primary.metrics,
78
+ D_t: primary.D_t,
79
+ K_t: primary.K_t,
80
+ delta: primary.delta,
81
+ status: primary.status,
82
+ capacity: primary.capacity,
83
+ growth: primary.growth,
84
+ alerts: primary.alerts,
85
+ details: primary.details,
86
+ scales: scaleReports
87
+ };
88
+ }
89
+ function buildModuleGraph(modules, moduleGraph) {
90
+ const isTest = (f) => {
91
+ const lower = f.toLowerCase();
92
+ return lower.includes("test") || lower.includes("spec") || lower.includes("mock");
93
+ };
94
+ const filteredModules = modules.filter(m => !isTest(m.id));
95
+ const nodes = filteredModules.map((module) => module.id);
96
+ const nodeLayers = new Map();
97
+ for (const module of filteredModules) {
98
+ nodeLayers.set(module.id, module.layer);
99
+ }
100
+ const edgeSet = new Set();
101
+ const edges = [];
102
+ for (const edge of moduleGraph) {
103
+ if (isTest(edge.from) || isTest(edge.to)) {
104
+ continue;
105
+ }
106
+ const key = `${edge.from}::${edge.to}`;
107
+ if (edgeSet.has(key)) {
108
+ continue;
109
+ }
110
+ edgeSet.add(key);
111
+ edges.push({ from: edge.from, to: edge.to });
112
+ }
113
+ return {
114
+ level: "module",
115
+ nodes,
116
+ edges,
117
+ nodeLayers
118
+ };
119
+ }
120
+ function buildFileGraph(modules, fileGraph) {
121
+ const nodes = new Set();
122
+ const nodeLayers = new Map();
123
+ const fileToLayer = new Map();
124
+ const isTest = (f) => {
125
+ const lower = f.toLowerCase();
126
+ return lower.includes("test") || lower.includes("spec") || lower.includes("mock");
127
+ };
128
+ for (const module of modules) {
129
+ for (const file of module.files) {
130
+ if (isTest(file))
131
+ continue;
132
+ nodes.add(file);
133
+ fileToLayer.set(file, module.layer);
134
+ nodeLayers.set(file, module.layer);
135
+ }
136
+ }
137
+ const edgeSet = new Set();
138
+ const edges = [];
139
+ for (const edge of fileGraph) {
140
+ if (isTest(edge.from) || isTest(edge.to))
141
+ continue;
142
+ nodes.add(edge.from);
143
+ nodes.add(edge.to);
144
+ if (fileToLayer.has(edge.from) && !nodeLayers.has(edge.from)) {
145
+ nodeLayers.set(edge.from, fileToLayer.get(edge.from));
146
+ }
147
+ if (fileToLayer.has(edge.to) && !nodeLayers.has(edge.to)) {
148
+ nodeLayers.set(edge.to, fileToLayer.get(edge.to));
149
+ }
150
+ const key = `${edge.from}::${edge.to}`;
151
+ if (edgeSet.has(key)) {
152
+ continue;
153
+ }
154
+ edgeSet.add(key);
155
+ edges.push({ from: edge.from, to: edge.to });
156
+ }
157
+ return {
158
+ level: "file",
159
+ nodes: Array.from(nodes),
160
+ edges,
161
+ nodeLayers
162
+ };
163
+ }
164
+ function buildDomainGraph(modules, moduleGraph, config) {
165
+ const domainMap = config.drift?.domains ?? {};
166
+ const domainKeys = Object.keys(domainMap);
167
+ if (domainKeys.length === 0) {
168
+ return null;
169
+ }
170
+ const moduleToDomain = new Map();
171
+ for (const module of modules) {
172
+ const domain = resolveDomainForModule(module.id, domainMap);
173
+ if (domain) {
174
+ moduleToDomain.set(module.id, domain);
175
+ }
176
+ }
177
+ if (moduleToDomain.size === 0) {
178
+ return null;
179
+ }
180
+ const nodes = new Set();
181
+ const nodeLayers = new Map();
182
+ const edgeSet = new Set();
183
+ const edges = [];
184
+ for (const domain of moduleToDomain.values()) {
185
+ nodes.add(domain);
186
+ nodeLayers.set(domain, domain);
187
+ }
188
+ for (const edge of moduleGraph) {
189
+ const fromDomain = moduleToDomain.get(edge.from) ?? "unassigned";
190
+ const toDomain = moduleToDomain.get(edge.to) ?? "unassigned";
191
+ nodes.add(fromDomain);
192
+ nodes.add(toDomain);
193
+ nodeLayers.set(fromDomain, fromDomain);
194
+ nodeLayers.set(toDomain, toDomain);
195
+ const key = `${fromDomain}::${toDomain}`;
196
+ if (edgeSet.has(key)) {
197
+ continue;
198
+ }
199
+ edgeSet.add(key);
200
+ edges.push({ from: fromDomain, to: toDomain });
201
+ }
202
+ return {
203
+ level: "domain",
204
+ nodes: Array.from(nodes),
205
+ edges,
206
+ nodeLayers
207
+ };
208
+ }
209
+ function resolveDomainForModule(moduleId, domainMap) {
210
+ for (const [domain, patterns] of Object.entries(domainMap)) {
211
+ for (const pattern of patterns) {
212
+ if (pattern === moduleId) {
213
+ return domain;
214
+ }
215
+ if (pattern.endsWith("*")) {
216
+ const prefix = pattern.slice(0, -1);
217
+ if (moduleId.startsWith(prefix)) {
218
+ return domain;
219
+ }
220
+ }
221
+ }
222
+ }
223
+ return null;
224
+ }
225
+ async function computeDriftForGraph(params) {
226
+ const { graph, circularDependencies, config, projectRoot } = params;
227
+ const nodeCount = graph.nodes.length;
228
+ const edgeCount = graph.edges.length;
229
+ const layerRules = config.drift?.layers ?? {};
230
+ const layers = Object.keys(layerRules);
231
+ const useLayerRules = layers.length > 0;
232
+ let crossLayerEdges = 0;
233
+ if (useLayerRules) {
234
+ for (const edge of graph.edges) {
235
+ const fromLayer = graph.nodeLayers.get(edge.from);
236
+ const toLayer = graph.nodeLayers.get(edge.to);
237
+ const allowed = fromLayer ? layerRules[fromLayer] ?? [] : [];
238
+ if (!fromLayer || !toLayer || !allowed.includes(toLayer)) {
239
+ crossLayerEdges += 1;
240
+ }
241
+ }
242
+ }
243
+ const crossLayerRatio = edgeCount === 0 ? 0 : crossLayerEdges / edgeCount;
244
+ const degreeMap = new Map();
245
+ for (const node of graph.nodes) {
246
+ degreeMap.set(node, 0);
247
+ }
248
+ for (const edge of graph.edges) {
249
+ degreeMap.set(edge.from, (degreeMap.get(edge.from) ?? 0) + 1);
250
+ degreeMap.set(edge.to, (degreeMap.get(edge.to) ?? 0) + 1);
251
+ }
252
+ const totalDegree = Array.from(degreeMap.values()).reduce((sum, value) => sum + value, 0);
253
+ let entropy = 0;
254
+ if (totalDegree > 0) {
255
+ for (const value of degreeMap.values()) {
256
+ const p = value / totalDegree;
257
+ if (p > 0) {
258
+ entropy -= p * Math.log(p);
259
+ }
260
+ }
261
+ }
262
+ const cycles = graph.level === "module"
263
+ ? circularDependencies.length
264
+ : countCyclesInGraph(graph.edges, graph.nodes);
265
+ const cycleDensity = nodeCount === 0 ? 0 : cycles / nodeCount;
266
+ const modularityGap = computeModularityGap(graph.edges, graph.nodes);
267
+ const weights = {
268
+ entropy: config.drift?.weights?.entropy ?? 0.4,
269
+ crossLayer: config.drift?.weights?.crossLayer ?? 0.3,
270
+ cycles: config.drift?.weights?.cycles ?? 0.2,
271
+ modularity: config.drift?.weights?.modularity ?? 0.1
272
+ };
273
+ const D_t = weights.entropy * entropy +
274
+ weights.crossLayer * crossLayerRatio +
275
+ weights.cycles * cycleDensity +
276
+ weights.modularity * modularityGap;
277
+ const capacity = await resolveCapacity({
278
+ config,
279
+ projectRoot,
280
+ nodeCount,
281
+ layersCount: layers.length
282
+ });
283
+ const delta = capacity - Math.log(D_t + EPSILON);
284
+ const criticalThreshold = config.drift?.criticalDelta ?? 0.25;
285
+ const baseStatus = delta < 0 ? "drift" : delta < criticalThreshold ? "critical" : "stable";
286
+ const capacityReport = computeCapacityReport(graph, config);
287
+ const growthReport = await computeGrowthReport({
288
+ projectRoot,
289
+ config,
290
+ graphLevel: graph.level
291
+ });
292
+ const alerts = buildAlerts({
293
+ baseStatus,
294
+ capacity: capacityReport,
295
+ growth: growthReport
296
+ });
297
+ const fingerprints = computeGraphFingerprints(graph);
298
+ let status = baseStatus;
299
+ if (status !== "drift") {
300
+ if (capacityReport.status === "critical" || growthReport.status === "critical") {
301
+ status = "critical";
302
+ }
303
+ }
304
+ return {
305
+ level: graph.level,
306
+ metrics: {
307
+ entropy,
308
+ cross_layer_ratio: crossLayerRatio,
309
+ cycle_density: cycleDensity,
310
+ modularity_gap: modularityGap
311
+ },
312
+ D_t,
313
+ K_t: capacity,
314
+ delta,
315
+ status,
316
+ capacity: capacityReport,
317
+ growth: growthReport,
318
+ alerts,
319
+ details: {
320
+ nodes: nodeCount,
321
+ edges: edgeCount,
322
+ cycles,
323
+ cross_layer_edges: crossLayerEdges,
324
+ layers,
325
+ fingerprint: fingerprints.fingerprint,
326
+ shape_fingerprint: fingerprints.shape_fingerprint
327
+ }
328
+ };
329
+ }
330
+ function resolvePrimaryLevel(requestedLevel, scales) {
331
+ if (requestedLevel !== "auto") {
332
+ const found = scales.find((scale) => scale.level === requestedLevel);
333
+ if (found) {
334
+ return found.level;
335
+ }
336
+ }
337
+ const preferred = ["function", "module", "file", "domain"];
338
+ for (const level of preferred) {
339
+ if (scales.some((scale) => scale.level === level)) {
340
+ return level;
341
+ }
342
+ }
343
+ return scales[0]?.level ?? "module";
344
+ }
345
+ function computeCapacityReport(graph, config) {
346
+ const thresholds = {
347
+ warning: config.drift?.capacity?.warningRatio ?? 0.85,
348
+ critical: config.drift?.capacity?.criticalRatio ?? 1.0
349
+ };
350
+ const layerBudgets = config.drift?.capacity?.layers ?? {};
351
+ const usage = new Map();
352
+ for (const node of graph.nodes) {
353
+ const layer = graph.nodeLayers.get(node) ?? "unassigned";
354
+ const entry = usage.get(layer) ?? { nodes: 0, edges: 0, crossLayerOut: 0 };
355
+ entry.nodes += 1;
356
+ usage.set(layer, entry);
357
+ }
358
+ for (const edge of graph.edges) {
359
+ const fromLayer = graph.nodeLayers.get(edge.from) ?? "unassigned";
360
+ const toLayer = graph.nodeLayers.get(edge.to) ?? "unassigned";
361
+ const entry = usage.get(fromLayer) ?? { nodes: 0, edges: 0, crossLayerOut: 0 };
362
+ entry.edges += 1;
363
+ if (fromLayer !== toLayer) {
364
+ entry.crossLayerOut += 1;
365
+ }
366
+ usage.set(fromLayer, entry);
367
+ }
368
+ const layers = new Set([...usage.keys(), ...Object.keys(layerBudgets)]);
369
+ const layerReports = [];
370
+ let hasBudget = false;
371
+ let anyMeasured = false;
372
+ let overallStatus = "unbudgeted";
373
+ const severity = (status) => {
374
+ if (status === "critical")
375
+ return 3;
376
+ if (status === "warning")
377
+ return 2;
378
+ if (status === "ok")
379
+ return 1;
380
+ return 0;
381
+ };
382
+ for (const layer of layers) {
383
+ const stats = usage.get(layer) ?? { nodes: 0, edges: 0, crossLayerOut: 0 };
384
+ const budget = layerBudgets[layer];
385
+ const report = computeCapacityStatus(stats.edges, budget, thresholds);
386
+ const entry = {
387
+ layer,
388
+ nodes: stats.nodes,
389
+ edges: stats.edges,
390
+ cross_layer_out: stats.crossLayerOut,
391
+ budget: report.budget,
392
+ ratio: report.ratio,
393
+ remaining: report.remaining,
394
+ status: report.status
395
+ };
396
+ layerReports.push(entry);
397
+ anyMeasured = true;
398
+ if (typeof budget === "number" && budget > 0) {
399
+ hasBudget = true;
400
+ }
401
+ if (severity(entry.status) > severity(overallStatus)) {
402
+ overallStatus = entry.status;
403
+ }
404
+ else if (overallStatus === "unbudgeted" && entry.status !== "unbudgeted") {
405
+ overallStatus = entry.status;
406
+ }
407
+ }
408
+ let totalReport;
409
+ const totalUsed = graph.edges.length;
410
+ const configuredTotal = config.drift?.capacity?.total ?? 0;
411
+ const computedTotal = configuredTotal > 0
412
+ ? configuredTotal
413
+ : hasBudget
414
+ ? Object.values(layerBudgets).reduce((sum, value) => sum + (value || 0), 0)
415
+ : 0;
416
+ if (computedTotal > 0 || anyMeasured) {
417
+ const report = computeCapacityStatus(totalUsed, computedTotal || undefined, thresholds);
418
+ totalReport = {
419
+ budget: report.budget,
420
+ used: totalUsed,
421
+ ratio: report.ratio,
422
+ remaining: report.remaining,
423
+ status: report.status
424
+ };
425
+ if (severity(report.status) > severity(overallStatus)) {
426
+ overallStatus = report.status;
427
+ }
428
+ else if (overallStatus === "unbudgeted" && report.status !== "unbudgeted") {
429
+ overallStatus = report.status;
430
+ }
431
+ }
432
+ return {
433
+ thresholds,
434
+ total: totalReport,
435
+ layers: layerReports.sort((a, b) => a.layer.localeCompare(b.layer)),
436
+ status: overallStatus
437
+ };
438
+ }
439
+ function computeCapacityStatus(used, budget, thresholds) {
440
+ if (typeof budget !== "number" || budget <= 0) {
441
+ return {
442
+ status: "unbudgeted"
443
+ };
444
+ }
445
+ const ratio = budget === 0 ? 0 : used / budget;
446
+ const remaining = Math.max(0, budget - used);
447
+ let status = "ok";
448
+ if (ratio >= thresholds.critical) {
449
+ status = "critical";
450
+ }
451
+ else if (ratio >= thresholds.warning) {
452
+ status = "warning";
453
+ }
454
+ return {
455
+ budget,
456
+ ratio,
457
+ remaining,
458
+ status
459
+ };
460
+ }
461
+ async function computeGrowthReport(params) {
462
+ const { projectRoot, config, graphLevel } = params;
463
+ const entries = await loadDriftHistory(projectRoot, config);
464
+ const filtered = entries
465
+ .map((entry) => ({
466
+ timestamp: entry.timestamp,
467
+ edges: extractEdgesForLevel(entry, graphLevel)
468
+ }))
469
+ .filter((entry) => typeof entry.edges === "number")
470
+ .filter((entry) => typeof entry.timestamp === "string");
471
+ if (filtered.length < 2) {
472
+ return {
473
+ edges_per_hour: 0,
474
+ edges_per_day: 0,
475
+ trend: "insufficient_data",
476
+ window: {},
477
+ status: "insufficient_data"
478
+ };
479
+ }
480
+ const sorted = filtered.sort((a, b) => {
481
+ const timeA = new Date(a.timestamp ?? "").getTime();
482
+ const timeB = new Date(b.timestamp ?? "").getTime();
483
+ return timeA - timeB;
484
+ });
485
+ const last = sorted[sorted.length - 1];
486
+ const prev = sorted[sorted.length - 2];
487
+ const lastTime = new Date(last.timestamp ?? "").getTime();
488
+ const prevTime = new Date(prev.timestamp ?? "").getTime();
489
+ const deltaMs = lastTime - prevTime;
490
+ if (!Number.isFinite(deltaMs) || deltaMs <= 0) {
491
+ return {
492
+ edges_per_hour: 0,
493
+ edges_per_day: 0,
494
+ trend: "insufficient_data",
495
+ window: {
496
+ from: prev.timestamp,
497
+ to: last.timestamp
498
+ },
499
+ status: "insufficient_data"
500
+ };
501
+ }
502
+ const edgesDelta = (last.edges ?? 0) - (prev.edges ?? 0);
503
+ const hours = deltaMs / (1000 * 60 * 60);
504
+ const edgesPerHour = edgesDelta / hours;
505
+ const edgesPerDay = edgesPerHour * 24;
506
+ const trend = edgesDelta > 0 ? "increasing" : edgesDelta < 0 ? "decreasing" : "stable";
507
+ const maxPerHour = config.drift?.growth?.maxEdgesPerHour ?? 0;
508
+ const maxPerDay = config.drift?.growth?.maxEdgesPerDay ?? 0;
509
+ const shouldAlert = (maxPerHour > 0 && edgesPerHour > maxPerHour) || (maxPerDay > 0 && edgesPerDay > maxPerDay);
510
+ return {
511
+ edges_per_hour: edgesPerHour,
512
+ edges_per_day: edgesPerDay,
513
+ trend,
514
+ window: {
515
+ from: prev.timestamp,
516
+ to: last.timestamp,
517
+ hours
518
+ },
519
+ status: shouldAlert ? "critical" : "ok"
520
+ };
521
+ }
522
+ function extractEdgesForLevel(entry, level) {
523
+ if (Array.isArray(entry.scales)) {
524
+ const scale = entry.scales.find((item) => item.level === level);
525
+ if (scale && typeof scale.details?.edges === "number") {
526
+ return scale.details.edges;
527
+ }
528
+ }
529
+ if (entry.graph_level && entry.graph_level !== level) {
530
+ return null;
531
+ }
532
+ if (typeof entry.details?.edges === "number") {
533
+ return entry.details.edges;
534
+ }
535
+ return null;
536
+ }
537
+ async function loadDriftHistory(projectRoot, config) {
538
+ const historyPath = config.drift?.historyPath && config.drift.historyPath.length > 0
539
+ ? config.drift.historyPath
540
+ : "specs-out/drift.history.jsonl";
541
+ const resolved = path.isAbsolute(historyPath)
542
+ ? historyPath
543
+ : path.resolve(projectRoot, historyPath);
544
+ let raw = "";
545
+ try {
546
+ raw = await fs.readFile(resolved, "utf8");
547
+ }
548
+ catch {
549
+ return [];
550
+ }
551
+ const entries = [];
552
+ for (const line of raw.split("\n")) {
553
+ const trimmed = line.trim();
554
+ if (!trimmed) {
555
+ continue;
556
+ }
557
+ try {
558
+ entries.push(JSON.parse(trimmed));
559
+ }
560
+ catch {
561
+ continue;
562
+ }
563
+ }
564
+ return entries;
565
+ }
566
+ function buildAlerts(params) {
567
+ const alerts = [];
568
+ if (params.baseStatus === "drift") {
569
+ alerts.push("delta:drift");
570
+ }
571
+ else if (params.baseStatus === "critical") {
572
+ alerts.push("delta:critical");
573
+ }
574
+ for (const layer of params.capacity.layers) {
575
+ if (layer.status === "warning" || layer.status === "critical") {
576
+ alerts.push(`capacity:${layer.layer}:${layer.status}`);
577
+ }
578
+ }
579
+ if (params.capacity.total && params.capacity.total.status !== "unbudgeted") {
580
+ if (params.capacity.total.status === "warning" || params.capacity.total.status === "critical") {
581
+ alerts.push(`capacity:total:${params.capacity.total.status}`);
582
+ }
583
+ }
584
+ if (params.growth.status === "critical") {
585
+ alerts.push("growth:edges");
586
+ }
587
+ return alerts;
588
+ }
589
+ export async function buildFunctionGraph(params) {
590
+ const { backendRoot, modules, projectRoot } = params;
591
+ const fileToModule = new Map();
592
+ const moduleLayers = new Map();
593
+ for (const module of modules) {
594
+ moduleLayers.set(module.id, module.layer);
595
+ for (const file of module.files) {
596
+ fileToModule.set(toPosix(file), module.id);
597
+ }
598
+ }
599
+ const tsFiles = [];
600
+ const pyFiles = [];
601
+ for (const file of fileToModule.keys()) {
602
+ if (file.endsWith(".d.ts")) {
603
+ continue;
604
+ }
605
+ const ext = path.extname(file).toLowerCase();
606
+ const absolute = path.resolve(projectRoot, file);
607
+ if (ext === ".py") {
608
+ pyFiles.push(absolute);
609
+ }
610
+ else if (isTsFile(ext)) {
611
+ tsFiles.push(absolute);
612
+ }
613
+ }
614
+ const nodes = new Set();
615
+ const edges = new Set();
616
+ const nodeLayers = new Map();
617
+ if (tsFiles.length > 0) {
618
+ const tsResult = buildTsFunctionGraph({
619
+ backendRoot,
620
+ projectRoot,
621
+ fileToModule,
622
+ moduleLayers,
623
+ filePaths: tsFiles
624
+ });
625
+ for (const node of tsResult.nodes) {
626
+ nodes.add(node);
627
+ }
628
+ for (const edge of tsResult.edges) {
629
+ edges.add(edge);
630
+ }
631
+ for (const [key, value] of tsResult.nodeLayers) {
632
+ nodeLayers.set(key, value);
633
+ }
634
+ }
635
+ if (pyFiles.length > 0) {
636
+ const pyResult = await buildPythonFunctionGraph({
637
+ backendRoot,
638
+ projectRoot,
639
+ fileToModule,
640
+ moduleLayers,
641
+ filePaths: pyFiles
642
+ });
643
+ for (const node of pyResult.nodes) {
644
+ nodes.add(node);
645
+ }
646
+ for (const edge of pyResult.edges) {
647
+ edges.add(edge);
648
+ }
649
+ for (const [key, value] of pyResult.nodeLayers) {
650
+ nodeLayers.set(key, value);
651
+ }
652
+ }
653
+ if (nodes.size === 0) {
654
+ return null;
655
+ }
656
+ const edgeList = Array.from(edges).map((entry) => {
657
+ const [from, to] = entry.split("::");
658
+ return { from, to };
659
+ });
660
+ return {
661
+ level: "function",
662
+ nodes: Array.from(nodes),
663
+ edges: edgeList,
664
+ nodeLayers
665
+ };
666
+ }
667
+ function buildTsFunctionGraph(params) {
668
+ const { backendRoot, projectRoot, fileToModule, moduleLayers, filePaths } = params;
669
+ const program = createTsProgram(filePaths, backendRoot);
670
+ const checker = program.getTypeChecker();
671
+ const nodeLayers = new Map();
672
+ const nodes = new Set();
673
+ const edges = new Set();
674
+ const declarationToId = new Map();
675
+ const functionNodeToId = new Map();
676
+ const registerFunction = (id, declaration, functionNode, moduleId) => {
677
+ nodes.add(id);
678
+ declarationToId.set(declaration, id);
679
+ functionNodeToId.set(functionNode, id);
680
+ const layer = moduleLayers.get(moduleId);
681
+ if (layer) {
682
+ nodeLayers.set(id, layer);
683
+ }
684
+ };
685
+ for (const sourceFile of program.getSourceFiles()) {
686
+ if (!filePaths.includes(sourceFile.fileName)) {
687
+ continue;
688
+ }
689
+ const relative = toPosix(path.relative(projectRoot, sourceFile.fileName));
690
+ const moduleId = fileToModule.get(relative);
691
+ if (!moduleId) {
692
+ continue;
693
+ }
694
+ const visit = (node) => {
695
+ if (ts.isFunctionDeclaration(node) && node.name) {
696
+ const id = `${relative}#${node.name.text}`;
697
+ registerFunction(id, node, node, moduleId);
698
+ }
699
+ if (ts.isMethodDeclaration(node) && node.name) {
700
+ const name = ts.isIdentifier(node.name) || ts.isStringLiteral(node.name)
701
+ ? node.name.text
702
+ : "method";
703
+ const className = ts.isClassDeclaration(node.parent) && node.parent.name
704
+ ? node.parent.name.text
705
+ : "Class";
706
+ const id = `${relative}#${className}.${name}`;
707
+ registerFunction(id, node, node, moduleId);
708
+ }
709
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
710
+ const initializer = node.initializer;
711
+ if (initializer && (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer))) {
712
+ const id = `${relative}#${node.name.text}`;
713
+ registerFunction(id, node, initializer, moduleId);
714
+ }
715
+ }
716
+ ts.forEachChild(node, visit);
717
+ };
718
+ visit(sourceFile);
719
+ }
720
+ for (const sourceFile of program.getSourceFiles()) {
721
+ if (!filePaths.includes(sourceFile.fileName)) {
722
+ continue;
723
+ }
724
+ const relative = toPosix(path.relative(projectRoot, sourceFile.fileName));
725
+ const moduleId = fileToModule.get(relative);
726
+ if (!moduleId) {
727
+ continue;
728
+ }
729
+ const visit = (node, currentFunctionId) => {
730
+ const localFunctionId = functionNodeToId.get(node) ?? currentFunctionId;
731
+ if (localFunctionId && ts.isCallExpression(node)) {
732
+ const calleeId = resolveCallTarget(node.expression, checker, declarationToId);
733
+ if (calleeId) {
734
+ edges.add(`${localFunctionId}::${calleeId}`);
735
+ }
736
+ }
737
+ ts.forEachChild(node, (child) => visit(child, localFunctionId));
738
+ };
739
+ visit(sourceFile);
740
+ }
741
+ return { nodes, edges, nodeLayers };
742
+ }
743
+ function resolveCallTarget(expression, checker, declarationToId) {
744
+ let symbol = checker.getSymbolAtLocation(expression);
745
+ if (!symbol) {
746
+ return null;
747
+ }
748
+ if (symbol.flags & ts.SymbolFlags.Alias) {
749
+ symbol = checker.getAliasedSymbol(symbol);
750
+ }
751
+ const declarations = symbol.getDeclarations() ?? [];
752
+ for (const declaration of declarations) {
753
+ const id = declarationToId.get(declaration);
754
+ if (id) {
755
+ return id;
756
+ }
757
+ }
758
+ return null;
759
+ }
760
+ async function buildPythonFunctionGraph(params) {
761
+ const { backendRoot, projectRoot, fileToModule, moduleLayers, filePaths } = params;
762
+ const parser = new Parser();
763
+ parser.setLanguage(Python);
764
+ const nodes = new Set();
765
+ const edges = new Set();
766
+ const nodeLayers = new Map();
767
+ const parsedFiles = [];
768
+ for (const filePath of filePaths) {
769
+ let source = "";
770
+ try {
771
+ source = await fs.readFile(filePath, "utf8");
772
+ }
773
+ catch {
774
+ continue;
775
+ }
776
+ if (!source) {
777
+ continue;
778
+ }
779
+ let tree;
780
+ try {
781
+ tree = parser.parse(source);
782
+ }
783
+ catch {
784
+ continue;
785
+ }
786
+ const root = tree.rootNode;
787
+ const moduleName = pythonModuleName(filePath, backendRoot);
788
+ const relative = toPosix(path.relative(projectRoot, filePath));
789
+ parsedFiles.push({ filePath, source, root, moduleName, relative });
790
+ }
791
+ const moduleIndex = new Map();
792
+ const moduleFunctionIds = new Map();
793
+ const fileFunctionIds = new Map();
794
+ const fileClassMethodIds = new Map();
795
+ for (const parsed of parsedFiles) {
796
+ if (!parsed.moduleName) {
797
+ continue;
798
+ }
799
+ const moduleName = parsed.moduleName;
800
+ moduleIndex.set(moduleName, parsed.filePath);
801
+ const moduleId = fileToModule.get(parsed.relative);
802
+ const layer = moduleId ? moduleLayers.get(moduleId) : undefined;
803
+ const functionIds = new Map();
804
+ const classMethods = new Map();
805
+ walk(parsed.root, (node) => {
806
+ if (node.type !== "function_definition") {
807
+ return;
808
+ }
809
+ if (!isTopLevel(node)) {
810
+ return;
811
+ }
812
+ const nameNode = node.childForFieldName("name");
813
+ if (!nameNode) {
814
+ return;
815
+ }
816
+ const name = nodeText(nameNode, parsed.source);
817
+ const id = `${parsed.relative}#${name}`;
818
+ functionIds.set(name, id);
819
+ nodes.add(id);
820
+ if (layer) {
821
+ nodeLayers.set(id, layer);
822
+ }
823
+ const moduleMap = moduleFunctionIds.get(moduleName);
824
+ if (moduleMap) {
825
+ if (!moduleMap.has(name)) {
826
+ moduleMap.set(name, id);
827
+ }
828
+ }
829
+ else {
830
+ moduleFunctionIds.set(moduleName, new Map([[name, id]]));
831
+ }
832
+ });
833
+ walk(parsed.root, (node) => {
834
+ if (node.type !== "class_definition") {
835
+ return;
836
+ }
837
+ if (!isTopLevel(node)) {
838
+ return;
839
+ }
840
+ const nameNode = node.childForFieldName("name");
841
+ const bodyNode = node.childForFieldName("body");
842
+ if (!nameNode || !bodyNode) {
843
+ return;
844
+ }
845
+ const className = nodeText(nameNode, parsed.source);
846
+ const methods = new Map();
847
+ for (const child of bodyNode.namedChildren) {
848
+ if (child.type !== "function_definition") {
849
+ continue;
850
+ }
851
+ const methodNameNode = child.childForFieldName("name");
852
+ if (!methodNameNode) {
853
+ continue;
854
+ }
855
+ const methodName = nodeText(methodNameNode, parsed.source);
856
+ const id = `${parsed.relative}#${className}.${methodName}`;
857
+ methods.set(methodName, id);
858
+ nodes.add(id);
859
+ if (layer) {
860
+ nodeLayers.set(id, layer);
861
+ }
862
+ const moduleMap = moduleFunctionIds.get(moduleName);
863
+ const methodKey = `${className}.${methodName}`;
864
+ if (moduleMap) {
865
+ if (!moduleMap.has(methodKey)) {
866
+ moduleMap.set(methodKey, id);
867
+ }
868
+ }
869
+ else {
870
+ moduleFunctionIds.set(moduleName, new Map([[methodKey, id]]));
871
+ }
872
+ }
873
+ if (methods.size > 0) {
874
+ classMethods.set(className, methods);
875
+ }
876
+ });
877
+ fileFunctionIds.set(parsed.filePath, functionIds);
878
+ if (classMethods.size > 0) {
879
+ fileClassMethodIds.set(parsed.filePath, classMethods);
880
+ }
881
+ }
882
+ for (const parsed of parsedFiles) {
883
+ if (!parsed.moduleName) {
884
+ continue;
885
+ }
886
+ const functionIds = fileFunctionIds.get(parsed.filePath) ?? new Map();
887
+ const classMethods = fileClassMethodIds.get(parsed.filePath) ?? new Map();
888
+ if (functionIds.size === 0 && classMethods.size === 0) {
889
+ continue;
890
+ }
891
+ const currentPackage = pythonPackageParts(parsed.filePath, backendRoot);
892
+ const imports = collectPythonImports({
893
+ root: parsed.root,
894
+ source: parsed.source,
895
+ moduleIndex,
896
+ moduleFunctionIds,
897
+ currentPackage
898
+ });
899
+ walk(parsed.root, (node) => {
900
+ if (node.type !== "function_definition") {
901
+ return;
902
+ }
903
+ if (!isTopLevel(node)) {
904
+ return;
905
+ }
906
+ const nameNode = node.childForFieldName("name");
907
+ if (!nameNode) {
908
+ return;
909
+ }
910
+ const functionName = nodeText(nameNode, parsed.source);
911
+ const fromId = functionIds.get(functionName);
912
+ if (!fromId) {
913
+ return;
914
+ }
915
+ const body = node.childForFieldName("body");
916
+ if (!body) {
917
+ return;
918
+ }
919
+ walk(body, (child) => {
920
+ if (child.type !== "call") {
921
+ return;
922
+ }
923
+ const callee = child.childForFieldName("function");
924
+ if (!callee) {
925
+ return;
926
+ }
927
+ const toId = resolvePythonCall({
928
+ callee,
929
+ source: parsed.source,
930
+ localFunctions: functionIds,
931
+ imports,
932
+ moduleFunctionIds,
933
+ classContext: null,
934
+ classMethods
935
+ });
936
+ if (toId) {
937
+ edges.add(`${fromId}::${toId}`);
938
+ }
939
+ });
940
+ });
941
+ walk(parsed.root, (node) => {
942
+ if (node.type !== "class_definition") {
943
+ return;
944
+ }
945
+ if (!isTopLevel(node)) {
946
+ return;
947
+ }
948
+ const nameNode = node.childForFieldName("name");
949
+ const bodyNode = node.childForFieldName("body");
950
+ if (!nameNode || !bodyNode) {
951
+ return;
952
+ }
953
+ const className = nodeText(nameNode, parsed.source);
954
+ const methodMap = classMethods.get(className);
955
+ if (!methodMap || methodMap.size === 0) {
956
+ return;
957
+ }
958
+ for (const child of bodyNode.namedChildren) {
959
+ if (child.type !== "function_definition") {
960
+ continue;
961
+ }
962
+ const methodNameNode = child.childForFieldName("name");
963
+ const methodBody = child.childForFieldName("body");
964
+ if (!methodNameNode || !methodBody) {
965
+ continue;
966
+ }
967
+ const methodName = nodeText(methodNameNode, parsed.source);
968
+ const fromId = methodMap.get(methodName);
969
+ if (!fromId) {
970
+ continue;
971
+ }
972
+ walk(methodBody, (callNode) => {
973
+ if (callNode.type !== "call") {
974
+ return;
975
+ }
976
+ const callee = callNode.childForFieldName("function");
977
+ if (!callee) {
978
+ return;
979
+ }
980
+ const toId = resolvePythonCall({
981
+ callee,
982
+ source: parsed.source,
983
+ localFunctions: functionIds,
984
+ imports,
985
+ moduleFunctionIds,
986
+ classContext: className,
987
+ classMethods
988
+ });
989
+ if (toId) {
990
+ edges.add(`${fromId}::${toId}`);
991
+ }
992
+ });
993
+ }
994
+ });
995
+ }
996
+ return { nodes, edges, nodeLayers };
997
+ }
998
+ function createTsProgram(filePaths, searchRoot) {
999
+ const configPath = ts.findConfigFile(searchRoot, ts.sys.fileExists, "tsconfig.json") ||
1000
+ ts.findConfigFile(searchRoot, ts.sys.fileExists, "jsconfig.json");
1001
+ if (configPath) {
1002
+ const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
1003
+ const parsed = ts.parseJsonConfigFileContent(configFile.config ?? {}, ts.sys, path.dirname(configPath));
1004
+ const options = parsed.options;
1005
+ return ts.createProgram(filePaths, options);
1006
+ }
1007
+ return ts.createProgram(filePaths, {
1008
+ allowJs: true,
1009
+ jsx: ts.JsxEmit.ReactJSX,
1010
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
1011
+ module: ts.ModuleKind.NodeNext,
1012
+ target: ts.ScriptTarget.ES2022,
1013
+ skipLibCheck: true,
1014
+ resolveJsonModule: true
1015
+ });
1016
+ }
1017
+ function isTsFile(ext) {
1018
+ return [
1019
+ ".ts",
1020
+ ".tsx",
1021
+ ".js",
1022
+ ".jsx",
1023
+ ".mjs",
1024
+ ".cjs",
1025
+ ".mts",
1026
+ ".cts"
1027
+ ].includes(ext);
1028
+ }
1029
+ function toPosix(value) {
1030
+ return value.split(path.sep).join("/");
1031
+ }
1032
+ function walk(node, visit) {
1033
+ visit(node);
1034
+ for (const child of node.namedChildren) {
1035
+ walk(child, visit);
1036
+ }
1037
+ }
1038
+ function nodeText(node, source) {
1039
+ return source.slice(node.startIndex, node.endIndex);
1040
+ }
1041
+ function isTopLevel(node) {
1042
+ return node.parent?.type === "module";
1043
+ }
1044
+ function pythonModuleParts(filePath, backendRoot) {
1045
+ const relative = toPosix(path.relative(backendRoot, filePath));
1046
+ if (!relative || relative.startsWith("..")) {
1047
+ return [];
1048
+ }
1049
+ const parts = relative.split("/").filter(Boolean);
1050
+ const fileName = parts.pop();
1051
+ if (!fileName) {
1052
+ return [];
1053
+ }
1054
+ const base = fileName.replace(/\.py$/i, "");
1055
+ if (base && base !== "__init__") {
1056
+ parts.push(base);
1057
+ }
1058
+ return parts;
1059
+ }
1060
+ function pythonModuleName(filePath, backendRoot) {
1061
+ const parts = pythonModuleParts(filePath, backendRoot);
1062
+ return parts.length > 0 ? parts.join(".") : null;
1063
+ }
1064
+ function pythonPackageParts(filePath, backendRoot) {
1065
+ const parts = pythonModuleParts(filePath, backendRoot);
1066
+ const base = path.basename(filePath, ".py");
1067
+ if (base === "__init__") {
1068
+ return parts;
1069
+ }
1070
+ return parts.slice(0, -1);
1071
+ }
1072
+ function collectPythonImports(params) {
1073
+ const { root, source, moduleIndex, moduleFunctionIds, currentPackage } = params;
1074
+ const moduleAliases = new Map();
1075
+ const functionAliases = new Map();
1076
+ const registerFunctionAlias = (alias, moduleName, functionName) => {
1077
+ if (!moduleName || !functionName) {
1078
+ return;
1079
+ }
1080
+ functionAliases.set(alias, { moduleName, functionName });
1081
+ };
1082
+ const registerModuleAlias = (alias, moduleName) => {
1083
+ if (!moduleName) {
1084
+ return;
1085
+ }
1086
+ moduleAliases.set(alias, moduleName);
1087
+ };
1088
+ walk(root, (node) => {
1089
+ if (node.type === "import_statement") {
1090
+ const entries = collectImportEntries(node, source);
1091
+ for (const entry of entries) {
1092
+ if (entry.name === "*") {
1093
+ continue;
1094
+ }
1095
+ const alias = entry.alias ?? entry.name.split(".").pop() ?? entry.name;
1096
+ registerModuleAlias(alias, entry.name);
1097
+ }
1098
+ }
1099
+ if (node.type === "import_from_statement") {
1100
+ const moduleNode = node.childForFieldName("module_name");
1101
+ const moduleText = moduleNode ? nodeText(moduleNode, source) : "";
1102
+ const baseModule = resolveImportModuleName(moduleText, currentPackage);
1103
+ if (!baseModule) {
1104
+ return;
1105
+ }
1106
+ const entries = collectImportEntries(node, source);
1107
+ for (const entry of entries) {
1108
+ if (entry.name === "*" || entry.name === "") {
1109
+ continue;
1110
+ }
1111
+ const alias = entry.alias ?? entry.name.split(".").pop() ?? entry.name;
1112
+ const candidateModule = `${baseModule}.${entry.name}`;
1113
+ if (moduleIndex.has(candidateModule)) {
1114
+ registerModuleAlias(alias, candidateModule);
1115
+ continue;
1116
+ }
1117
+ if (entry.name.includes(".")) {
1118
+ const parts = entry.name.split(".").filter(Boolean);
1119
+ const functionName = parts[parts.length - 1];
1120
+ const moduleCandidate = `${baseModule}.${parts.slice(0, -1).join(".")}`;
1121
+ if (moduleFunctionIds.get(moduleCandidate)?.has(functionName)) {
1122
+ registerFunctionAlias(alias, moduleCandidate, functionName);
1123
+ continue;
1124
+ }
1125
+ if (moduleIndex.has(moduleCandidate)) {
1126
+ registerModuleAlias(alias, moduleCandidate);
1127
+ continue;
1128
+ }
1129
+ registerFunctionAlias(alias, baseModule, functionName);
1130
+ continue;
1131
+ }
1132
+ if (moduleFunctionIds.get(baseModule)?.has(entry.name)) {
1133
+ registerFunctionAlias(alias, baseModule, entry.name);
1134
+ continue;
1135
+ }
1136
+ const nestedModule = `${baseModule}.${entry.name}`;
1137
+ if (moduleIndex.has(nestedModule)) {
1138
+ registerModuleAlias(alias, nestedModule);
1139
+ continue;
1140
+ }
1141
+ registerFunctionAlias(alias, baseModule, entry.name);
1142
+ }
1143
+ }
1144
+ });
1145
+ return { moduleAliases, functionAliases };
1146
+ }
1147
+ function collectImportEntries(node, source) {
1148
+ const entries = [];
1149
+ for (const child of node.namedChildren) {
1150
+ if (child.type === "aliased_import") {
1151
+ const nameNode = child.childForFieldName("name");
1152
+ const aliasNode = child.childForFieldName("alias");
1153
+ if (!nameNode) {
1154
+ continue;
1155
+ }
1156
+ entries.push({
1157
+ name: nodeText(nameNode, source),
1158
+ alias: aliasNode ? nodeText(aliasNode, source) : undefined
1159
+ });
1160
+ }
1161
+ else if (child.type === "dotted_name" || child.type === "identifier") {
1162
+ entries.push({ name: nodeText(child, source) });
1163
+ }
1164
+ else if (child.type === "wildcard_import") {
1165
+ entries.push({ name: "*" });
1166
+ }
1167
+ }
1168
+ return entries;
1169
+ }
1170
+ function resolveImportModuleName(moduleText, currentPackage) {
1171
+ if (!moduleText) {
1172
+ return null;
1173
+ }
1174
+ let level = 0;
1175
+ while (level < moduleText.length && moduleText[level] === ".") {
1176
+ level += 1;
1177
+ }
1178
+ if (level === 0) {
1179
+ return moduleText;
1180
+ }
1181
+ const remainder = moduleText.slice(level);
1182
+ const remove = Math.max(0, level - 1);
1183
+ const baseParts = currentPackage.slice(0, Math.max(0, currentPackage.length - remove));
1184
+ const combined = remainder
1185
+ ? [...baseParts, ...remainder.split(".").filter(Boolean)]
1186
+ : baseParts;
1187
+ return combined.length > 0 ? combined.join(".") : null;
1188
+ }
1189
+ function resolvePythonCall(params) {
1190
+ const { callee, source, localFunctions, imports, moduleFunctionIds, classContext, classMethods } = params;
1191
+ if (callee.type === "identifier") {
1192
+ const name = nodeText(callee, source);
1193
+ const local = localFunctions.get(name);
1194
+ if (local) {
1195
+ return local;
1196
+ }
1197
+ const alias = imports.functionAliases.get(name);
1198
+ if (alias) {
1199
+ return moduleFunctionIds.get(alias.moduleName)?.get(alias.functionName) ?? null;
1200
+ }
1201
+ return null;
1202
+ }
1203
+ if (callee.type === "attribute") {
1204
+ const objectNode = callee.childForFieldName("object");
1205
+ const attrNode = callee.childForFieldName("attribute");
1206
+ const attrName = attrNode ? nodeText(attrNode, source) : null;
1207
+ const objectName = objectNode ? exprName(objectNode, source) : null;
1208
+ if (attrName) {
1209
+ if (classContext && (objectName === "self" || objectName === "cls")) {
1210
+ return classMethods.get(classContext)?.get(attrName) ?? null;
1211
+ }
1212
+ if (objectName && classMethods.has(objectName)) {
1213
+ return classMethods.get(objectName)?.get(attrName) ?? null;
1214
+ }
1215
+ }
1216
+ const dotted = exprName(callee, source);
1217
+ if (!dotted) {
1218
+ return null;
1219
+ }
1220
+ const parts = dotted.split(".").filter(Boolean);
1221
+ if (parts.length < 2) {
1222
+ return null;
1223
+ }
1224
+ const base = parts[0];
1225
+ const rest = parts.slice(1);
1226
+ const moduleAlias = imports.moduleAliases.get(base);
1227
+ if (moduleAlias) {
1228
+ return resolveModuleFunction(moduleAlias, rest, moduleFunctionIds);
1229
+ }
1230
+ const moduleName = parts.slice(0, -1).join(".");
1231
+ const functionName = parts[parts.length - 1];
1232
+ return moduleFunctionIds.get(moduleName)?.get(functionName) ?? null;
1233
+ }
1234
+ return null;
1235
+ }
1236
+ function resolveModuleFunction(moduleBase, parts, moduleFunctionIds) {
1237
+ if (parts.length === 0) {
1238
+ return null;
1239
+ }
1240
+ if (parts.length === 1) {
1241
+ return moduleFunctionIds.get(moduleBase)?.get(parts[0]) ?? null;
1242
+ }
1243
+ const functionName = parts[parts.length - 1];
1244
+ const moduleName = `${moduleBase}.${parts.slice(0, -1).join(".")}`;
1245
+ const qualifiedName = parts.join(".");
1246
+ return (moduleFunctionIds.get(moduleName)?.get(functionName) ??
1247
+ moduleFunctionIds.get(moduleBase)?.get(qualifiedName) ??
1248
+ moduleFunctionIds.get(moduleBase)?.get(functionName) ??
1249
+ null);
1250
+ }
1251
+ function exprName(node, source) {
1252
+ if (node.type === "identifier") {
1253
+ return nodeText(node, source);
1254
+ }
1255
+ if (node.type === "attribute") {
1256
+ const objectNode = node.childForFieldName("object");
1257
+ const attrNode = node.childForFieldName("attribute");
1258
+ const base = objectNode ? exprName(objectNode, source) : null;
1259
+ const attr = attrNode ? nodeText(attrNode, source) : null;
1260
+ if (base && attr) {
1261
+ return `${base}.${attr}`;
1262
+ }
1263
+ return attr ?? base;
1264
+ }
1265
+ return null;
1266
+ }
1267
+ function countCyclesInGraph(edges, nodes) {
1268
+ const adjacency = new Map();
1269
+ const reverse = new Map();
1270
+ for (const node of nodes) {
1271
+ adjacency.set(node, []);
1272
+ reverse.set(node, []);
1273
+ }
1274
+ for (const edge of edges) {
1275
+ adjacency.get(edge.from)?.push(edge.to);
1276
+ reverse.get(edge.to)?.push(edge.from);
1277
+ }
1278
+ const visited = new Set();
1279
+ const order = [];
1280
+ const dfs1 = (node) => {
1281
+ visited.add(node);
1282
+ for (const next of adjacency.get(node) ?? []) {
1283
+ if (!visited.has(next)) {
1284
+ dfs1(next);
1285
+ }
1286
+ }
1287
+ order.push(node);
1288
+ };
1289
+ for (const node of nodes) {
1290
+ if (!visited.has(node)) {
1291
+ dfs1(node);
1292
+ }
1293
+ }
1294
+ const visited2 = new Set();
1295
+ let cycleCount = 0;
1296
+ const dfs2 = (node, component) => {
1297
+ visited2.add(node);
1298
+ component.push(node);
1299
+ for (const next of reverse.get(node) ?? []) {
1300
+ if (!visited2.has(next)) {
1301
+ dfs2(next, component);
1302
+ }
1303
+ }
1304
+ };
1305
+ for (let i = order.length - 1; i >= 0; i -= 1) {
1306
+ const node = order[i];
1307
+ if (visited2.has(node)) {
1308
+ continue;
1309
+ }
1310
+ const component = [];
1311
+ dfs2(node, component);
1312
+ if (component.length > 1) {
1313
+ cycleCount += 1;
1314
+ }
1315
+ else {
1316
+ const neighbors = adjacency.get(node) ?? [];
1317
+ if (neighbors.includes(node)) {
1318
+ cycleCount += 1;
1319
+ }
1320
+ }
1321
+ }
1322
+ return cycleCount;
1323
+ }
1324
+ function computeModularityGap(edges, nodes) {
1325
+ if (nodes.length === 0) {
1326
+ return 0;
1327
+ }
1328
+ const graph = buildUndirectedGraph(edges, nodes);
1329
+ if (graph.totalWeight === 0) {
1330
+ return 0;
1331
+ }
1332
+ const modularity = louvainModularity(graph);
1333
+ const clamped = Math.max(0, Math.min(1, modularity));
1334
+ return 1 - clamped;
1335
+ }
1336
+ function buildUndirectedGraph(edges, nodes) {
1337
+ const adjacency = new Map();
1338
+ for (const node of nodes) {
1339
+ adjacency.set(node, new Map());
1340
+ }
1341
+ for (const edge of edges) {
1342
+ if (edge.from === edge.to) {
1343
+ continue;
1344
+ }
1345
+ const fromMap = adjacency.get(edge.from);
1346
+ const toMap = adjacency.get(edge.to);
1347
+ if (!fromMap || !toMap) {
1348
+ continue;
1349
+ }
1350
+ fromMap.set(edge.to, (fromMap.get(edge.to) ?? 0) + 1);
1351
+ toMap.set(edge.from, (toMap.get(edge.from) ?? 0) + 1);
1352
+ }
1353
+ const degrees = new Map();
1354
+ let totalWeight = 0;
1355
+ for (const [node, neighbors] of adjacency.entries()) {
1356
+ const degree = Array.from(neighbors.values()).reduce((sum, value) => sum + value, 0);
1357
+ degrees.set(node, degree);
1358
+ totalWeight += degree;
1359
+ }
1360
+ return {
1361
+ nodes,
1362
+ adjacency,
1363
+ degrees,
1364
+ totalWeight: totalWeight / 2
1365
+ };
1366
+ }
1367
+ function louvainModularity(graph) {
1368
+ const { nodes, adjacency, degrees, totalWeight } = graph;
1369
+ if (totalWeight === 0) {
1370
+ return 0;
1371
+ }
1372
+ const community = new Map();
1373
+ const stats = new Map();
1374
+ for (const node of nodes) {
1375
+ const degree = degrees.get(node) ?? 0;
1376
+ community.set(node, node);
1377
+ stats.set(node, { sumTot: degree, sumIn: 0 });
1378
+ }
1379
+ let moved = true;
1380
+ let passes = 0;
1381
+ const maxPasses = 10;
1382
+ while (moved && passes < maxPasses) {
1383
+ moved = false;
1384
+ passes += 1;
1385
+ for (const node of nodes) {
1386
+ const nodeComm = community.get(node);
1387
+ const nodeDegree = degrees.get(node) ?? 0;
1388
+ const neighbors = adjacency.get(node) ?? new Map();
1389
+ const currentKIn = sumWeightsToCommunity(node, nodeComm, community, neighbors);
1390
+ const currentStats = stats.get(nodeComm);
1391
+ if (currentStats) {
1392
+ currentStats.sumTot -= nodeDegree;
1393
+ currentStats.sumIn -= 2 * currentKIn;
1394
+ }
1395
+ let bestComm = nodeComm;
1396
+ let bestGain = 0;
1397
+ const candidateComms = new Set();
1398
+ candidateComms.add(nodeComm);
1399
+ for (const neighbor of neighbors.keys()) {
1400
+ candidateComms.add(community.get(neighbor) ?? neighbor);
1401
+ }
1402
+ for (const candidate of candidateComms) {
1403
+ const candidateStats = stats.get(candidate);
1404
+ if (!candidateStats) {
1405
+ continue;
1406
+ }
1407
+ const kIn = sumWeightsToCommunity(node, candidate, community, neighbors);
1408
+ const gain = deltaModularity(candidateStats.sumIn, candidateStats.sumTot, nodeDegree, kIn, totalWeight);
1409
+ if (gain > bestGain) {
1410
+ bestGain = gain;
1411
+ bestComm = candidate;
1412
+ }
1413
+ }
1414
+ const bestStats = stats.get(bestComm);
1415
+ if (bestStats) {
1416
+ const kInBest = sumWeightsToCommunity(node, bestComm, community, neighbors);
1417
+ bestStats.sumTot += nodeDegree;
1418
+ bestStats.sumIn += 2 * kInBest;
1419
+ }
1420
+ if (bestComm !== nodeComm) {
1421
+ community.set(node, bestComm);
1422
+ moved = true;
1423
+ }
1424
+ }
1425
+ }
1426
+ return computeModularityFromStats(stats, totalWeight);
1427
+ }
1428
+ function sumWeightsToCommunity(node, communityId, community, neighbors) {
1429
+ let sum = 0;
1430
+ for (const [neighbor, weight] of neighbors.entries()) {
1431
+ if ((community.get(neighbor) ?? neighbor) === communityId) {
1432
+ sum += weight;
1433
+ }
1434
+ }
1435
+ return sum;
1436
+ }
1437
+ function deltaModularity(sumIn, sumTot, nodeDegree, kIn, totalWeight) {
1438
+ const m2 = 2 * totalWeight;
1439
+ const term1 = (sumIn + 2 * kIn) / m2 - Math.pow((sumTot + nodeDegree) / m2, 2);
1440
+ const term2 = sumIn / m2 - Math.pow(sumTot / m2, 2) - Math.pow(nodeDegree / m2, 2);
1441
+ return term1 - term2;
1442
+ }
1443
+ function computeModularityFromStats(stats, totalWeight) {
1444
+ const m2 = 2 * totalWeight;
1445
+ let modularity = 0;
1446
+ for (const entry of stats.values()) {
1447
+ if (entry.sumTot === 0) {
1448
+ continue;
1449
+ }
1450
+ modularity += entry.sumIn / m2 - Math.pow(entry.sumTot / m2, 2);
1451
+ }
1452
+ return modularity;
1453
+ }
1454
+ async function resolveCapacity(params) {
1455
+ const { config, projectRoot, nodeCount, layersCount } = params;
1456
+ const baselinePath = config.drift?.baselinePath ?? "";
1457
+ if (baselinePath) {
1458
+ const resolved = path.isAbsolute(baselinePath)
1459
+ ? baselinePath
1460
+ : path.resolve(projectRoot, baselinePath);
1461
+ const parsed = await loadBaseline(resolved);
1462
+ if (typeof parsed === "number") {
1463
+ return parsed;
1464
+ }
1465
+ }
1466
+ if (layersCount > 0) {
1467
+ return Math.log(Math.max(2, layersCount));
1468
+ }
1469
+ return Math.log(Math.max(2, nodeCount));
1470
+ }
1471
+ async function loadBaseline(filePath) {
1472
+ try {
1473
+ const raw = await fs.readFile(filePath, "utf8");
1474
+ const ext = path.extname(filePath).toLowerCase();
1475
+ const data = ext === ".yaml" || ext === ".yml"
1476
+ ? yaml.load(raw)
1477
+ : JSON.parse(raw);
1478
+ if (!data) {
1479
+ return null;
1480
+ }
1481
+ const drift = data["drift"];
1482
+ const direct = data["K_t"];
1483
+ const nested = drift ? drift["K_t"] : undefined;
1484
+ if (typeof nested === "number") {
1485
+ return nested;
1486
+ }
1487
+ if (typeof direct === "number") {
1488
+ return direct;
1489
+ }
1490
+ }
1491
+ catch {
1492
+ return null;
1493
+ }
1494
+ return null;
1495
+ }
1496
+ function computeGraphFingerprints(graph) {
1497
+ const nodes = [...graph.nodes].sort();
1498
+ const edges = graph.edges.map((edge) => `${edge.from}→${edge.to}`).sort();
1499
+ const fingerprint = hashObject({ nodes, edges });
1500
+ const inbound = new Map();
1501
+ const outbound = new Map();
1502
+ for (const node of nodes) {
1503
+ inbound.set(node, 0);
1504
+ outbound.set(node, 0);
1505
+ }
1506
+ for (const edge of graph.edges) {
1507
+ outbound.set(edge.from, (outbound.get(edge.from) ?? 0) + 1);
1508
+ inbound.set(edge.to, (inbound.get(edge.to) ?? 0) + 1);
1509
+ }
1510
+ const degrees = nodes
1511
+ .map((node) => ({
1512
+ in: inbound.get(node) ?? 0,
1513
+ out: outbound.get(node) ?? 0
1514
+ }))
1515
+ .sort((a, b) => (a.in + a.out) - (b.in + b.out));
1516
+ const degreeSignature = degrees.map((entry) => `${entry.in}:${entry.out}`).join("|");
1517
+ const edgeSignature = graph.edges
1518
+ .map((edge) => {
1519
+ const from = `${inbound.get(edge.from) ?? 0}:${outbound.get(edge.from) ?? 0}`;
1520
+ const to = `${inbound.get(edge.to) ?? 0}:${outbound.get(edge.to) ?? 0}`;
1521
+ return `${from}->${to}`;
1522
+ })
1523
+ .sort()
1524
+ .join("|");
1525
+ return {
1526
+ fingerprint,
1527
+ shape_fingerprint: hashObject({ degreeSignature, edgeSignature })
1528
+ };
1529
+ }
1530
+ function hashObject(value) {
1531
+ return crypto.createHash("sha1").update(JSON.stringify(value)).digest("hex");
1532
+ }
1533
+ async function resolveBackendRoot(backendRoot) {
1534
+ const resolved = path.resolve(backendRoot);
1535
+ const base = path.basename(resolved).toLowerCase();
1536
+ if (base === "backend" || base === "src") {
1537
+ return resolved;
1538
+ }
1539
+ const backendCandidate = path.join(resolved, "backend");
1540
+ const srcCandidate = path.join(resolved, "src");
1541
+ if (await dirExists(backendCandidate)) {
1542
+ return backendCandidate;
1543
+ }
1544
+ if (await dirExists(srcCandidate)) {
1545
+ return srcCandidate;
1546
+ }
1547
+ return resolved;
1548
+ }
1549
+ async function dirExists(dirPath) {
1550
+ try {
1551
+ const stat = await fs.stat(dirPath);
1552
+ return stat.isDirectory();
1553
+ }
1554
+ catch {
1555
+ return false;
1556
+ }
1557
+ }
1558
+ function findCommonRoot(paths) {
1559
+ if (paths.length === 0) {
1560
+ return process.cwd();
1561
+ }
1562
+ const splitPaths = paths.map((p) => path.resolve(p).split(path.sep));
1563
+ const minLength = Math.min(...splitPaths.map((parts) => parts.length));
1564
+ const shared = [];
1565
+ for (let i = 0; i < minLength; i += 1) {
1566
+ const segment = splitPaths[0][i];
1567
+ if (splitPaths.every((parts) => parts[i] === segment)) {
1568
+ shared.push(segment);
1569
+ }
1570
+ else {
1571
+ break;
1572
+ }
1573
+ }
1574
+ if (shared.length === 0) {
1575
+ return path.parse(paths[0]).root;
1576
+ }
1577
+ return shared.join(path.sep);
1578
+ }