@route-intelligence/core 2.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.
- package/dist/chunk-56XSRN7B.js +163 -0
- package/dist/index.d.ts +158 -0
- package/dist/index.js +1495 -0
- package/dist/metrics-SMPFGQCR.js +8 -0
- package/package.json +51 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// src/graph/algorithms.ts
|
|
2
|
+
import { hasCycle, willCreateCycle } from "graphology-dag";
|
|
3
|
+
import { bidirectional } from "graphology-shortest-path";
|
|
4
|
+
import { bfsFromNode } from "graphology-traversal";
|
|
5
|
+
function findCycles(graph) {
|
|
6
|
+
const underlying = graph.getUnderlyingGraph();
|
|
7
|
+
const cycles = [];
|
|
8
|
+
if (!hasCycle(underlying)) return cycles;
|
|
9
|
+
const visited = /* @__PURE__ */ new Set();
|
|
10
|
+
const recursionStack = /* @__PURE__ */ new Set();
|
|
11
|
+
const path = [];
|
|
12
|
+
function dfs(nodeId) {
|
|
13
|
+
visited.add(nodeId);
|
|
14
|
+
recursionStack.add(nodeId);
|
|
15
|
+
path.push(nodeId);
|
|
16
|
+
for (const neighbor of underlying.outNeighbors(nodeId)) {
|
|
17
|
+
if (!visited.has(neighbor)) {
|
|
18
|
+
dfs(neighbor);
|
|
19
|
+
} else if (recursionStack.has(neighbor)) {
|
|
20
|
+
const cycleStart = path.indexOf(neighbor);
|
|
21
|
+
if (cycleStart >= 0) {
|
|
22
|
+
cycles.push({ nodes: path.slice(cycleStart), edges: [] });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
path.pop();
|
|
27
|
+
recursionStack.delete(nodeId);
|
|
28
|
+
}
|
|
29
|
+
for (const nodeId of graph.getAllNodeIds()) {
|
|
30
|
+
if (!visited.has(nodeId)) {
|
|
31
|
+
dfs(nodeId);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return cycles;
|
|
35
|
+
}
|
|
36
|
+
function findDeadRoutes(graph) {
|
|
37
|
+
const entryTypes = ["route", "layout", "middleware"];
|
|
38
|
+
const entryNodes = graph.getAllNodeIds().filter((id) => {
|
|
39
|
+
const node = graph.getNode(id);
|
|
40
|
+
return node && entryTypes.includes(node.type);
|
|
41
|
+
});
|
|
42
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
43
|
+
const underlying = graph.getUnderlyingGraph();
|
|
44
|
+
for (const entry of entryNodes) {
|
|
45
|
+
bfsFromNode(underlying, entry, (node) => {
|
|
46
|
+
reachable.add(node);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return graph.getAllNodeIds().filter((id) => {
|
|
50
|
+
const node = graph.getNode(id);
|
|
51
|
+
if (!node || node.type !== "route") return false;
|
|
52
|
+
const incoming = graph.getIncomingEdges(id);
|
|
53
|
+
const hasNavIncoming = incoming.some(
|
|
54
|
+
(e) => e.attributes.type === "navigation" || e.attributes.type === "redirect" || e.attributes.type === "conditional-navigation"
|
|
55
|
+
);
|
|
56
|
+
return !reachable.has(id) && !hasNavIncoming && node.path !== "/";
|
|
57
|
+
}).map((id) => id);
|
|
58
|
+
}
|
|
59
|
+
function findShortestPath(graph, sourceId, targetId) {
|
|
60
|
+
const underlying = graph.getUnderlyingGraph();
|
|
61
|
+
if (!underlying.hasNode(sourceId) || !underlying.hasNode(targetId)) return null;
|
|
62
|
+
try {
|
|
63
|
+
return bidirectional(underlying, sourceId, targetId);
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function detectInfiniteRedirects(graph) {
|
|
69
|
+
const redirectEdges = graph.getAllEdges().filter((e) => {
|
|
70
|
+
const attrs = graph.getUnderlyingGraph().getEdgeAttributes(e.id);
|
|
71
|
+
return attrs.type === "redirect" || attrs.type === "permanent-redirect" || attrs.type === "rewrite";
|
|
72
|
+
});
|
|
73
|
+
const cycles = [];
|
|
74
|
+
const visited = /* @__PURE__ */ new Set();
|
|
75
|
+
for (const edge of redirectEdges) {
|
|
76
|
+
const key = `${edge.source}->${edge.target}`;
|
|
77
|
+
if (visited.has(key)) continue;
|
|
78
|
+
visited.add(key);
|
|
79
|
+
const path = [edge.source];
|
|
80
|
+
let current = edge.target;
|
|
81
|
+
const seen = /* @__PURE__ */ new Set([edge.source]);
|
|
82
|
+
while (current && !seen.has(current)) {
|
|
83
|
+
seen.add(current);
|
|
84
|
+
path.push(current);
|
|
85
|
+
const nextEdge = redirectEdges.find((e) => e.source === current);
|
|
86
|
+
if (!nextEdge) break;
|
|
87
|
+
current = nextEdge.target;
|
|
88
|
+
}
|
|
89
|
+
if (current && seen.has(current)) {
|
|
90
|
+
const cycleStart = path.indexOf(current);
|
|
91
|
+
cycles.push({ nodes: path.slice(cycleStart), edges: [] });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return cycles;
|
|
95
|
+
}
|
|
96
|
+
function getMostConnected(graph, n = 10) {
|
|
97
|
+
const underlying = graph.getUnderlyingGraph();
|
|
98
|
+
const degrees = graph.getAllNodeIds().map((id) => ({
|
|
99
|
+
id,
|
|
100
|
+
degree: underlying.degree(id)
|
|
101
|
+
}));
|
|
102
|
+
return degrees.sort((a, b) => b.degree - a.degree).slice(0, n);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/graph/metrics.ts
|
|
106
|
+
function computeMetrics(graph) {
|
|
107
|
+
const routeNodes = graph.findNodesByType("route");
|
|
108
|
+
const layoutNodes = graph.findNodesByType("layout");
|
|
109
|
+
const apiNodes = graph.findNodesByType("api-route");
|
|
110
|
+
const deadRoutes = findDeadRoutes(graph);
|
|
111
|
+
const cycles = findCycles(graph);
|
|
112
|
+
const redirectCycles = detectInfiniteRedirects(graph);
|
|
113
|
+
const mostConnected = getMostConnected(graph, 5);
|
|
114
|
+
const depths = routeNodes.map((id) => graph.getNode(id)?.depth ?? 0);
|
|
115
|
+
const maxDepth = depths.length > 0 ? Math.max(...depths) : 0;
|
|
116
|
+
const averageDepth = depths.length > 0 ? depths.reduce((a, b) => a + b, 0) / depths.length : 0;
|
|
117
|
+
const deadRoutePct = routeNodes.length > 0 ? deadRoutes.length / routeNodes.length : 0;
|
|
118
|
+
const cyclePenalty = (cycles.length + redirectCycles.length) * 5;
|
|
119
|
+
const depthPenalty = maxDepth > 10 ? (maxDepth - 10) * 2 : 0;
|
|
120
|
+
const maintainabilityScore = Math.max(
|
|
121
|
+
0,
|
|
122
|
+
Math.min(100, 100 - deadRoutePct * 40 - cyclePenalty - depthPenalty)
|
|
123
|
+
);
|
|
124
|
+
const riskScore = Math.max(0, Math.min(100, deadRoutePct * 30 + cyclePenalty * 2 + depthPenalty));
|
|
125
|
+
return {
|
|
126
|
+
totalRoutes: routeNodes.length,
|
|
127
|
+
totalLayouts: layoutNodes.length,
|
|
128
|
+
totalApiRoutes: apiNodes.length,
|
|
129
|
+
deadRouteCount: deadRoutes.length,
|
|
130
|
+
cycleCount: cycles.length + redirectCycles.length,
|
|
131
|
+
maxDepth,
|
|
132
|
+
averageDepth,
|
|
133
|
+
mostConnected: mostConnected.map((m) => ({
|
|
134
|
+
...m,
|
|
135
|
+
path: graph.getNodePath(m.id) ?? m.id
|
|
136
|
+
})),
|
|
137
|
+
maintainabilityScore,
|
|
138
|
+
riskScore
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function metricsToMetadata(graph, pluginIds) {
|
|
142
|
+
const metrics = computeMetrics(graph);
|
|
143
|
+
return {
|
|
144
|
+
pluginIds,
|
|
145
|
+
totalRoutes: metrics.totalRoutes,
|
|
146
|
+
totalLayouts: metrics.totalLayouts,
|
|
147
|
+
totalApiRoutes: metrics.totalApiRoutes,
|
|
148
|
+
deadRouteCount: metrics.deadRouteCount,
|
|
149
|
+
cycleCount: metrics.cycleCount,
|
|
150
|
+
maintainabilityScore: metrics.maintainabilityScore,
|
|
151
|
+
riskScore: metrics.riskScore
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export {
|
|
156
|
+
findCycles,
|
|
157
|
+
findDeadRoutes,
|
|
158
|
+
findShortestPath,
|
|
159
|
+
detectInfiniteRedirects,
|
|
160
|
+
getMostConnected,
|
|
161
|
+
computeMetrics,
|
|
162
|
+
metricsToMetadata
|
|
163
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import * as _route_intelligence_shared from '@route-intelligence/shared';
|
|
3
|
+
import { RouteGraphLike, NodeAttributes, EdgeAttributes, GraphMetadata, SerializedGraph, SemanticFile, InvalidationSet, GraphPatch, FileChangeEvent, FrameworkPlugin, AnalysisResult, AnalyzerConfig, RawRoute, Diagnostic } from '@route-intelligence/shared';
|
|
4
|
+
export { AnalysisResult, AnalyzerConfig, Diagnostic, FrameworkPlugin, GraphMetadata, GraphPatch, defineConfig } from '@route-intelligence/shared';
|
|
5
|
+
import { SourceFile } from 'ts-morph';
|
|
6
|
+
import { MultiDirectedGraph } from 'graphology';
|
|
7
|
+
|
|
8
|
+
declare class RouteGraph implements RouteGraphLike {
|
|
9
|
+
private readonly graph;
|
|
10
|
+
constructor();
|
|
11
|
+
hasNode(id: string): boolean;
|
|
12
|
+
getNode(id: string): NodeAttributes | undefined;
|
|
13
|
+
getNodePath(id: string): string | undefined;
|
|
14
|
+
findNodeByPath(path: string): string | undefined;
|
|
15
|
+
findNodesByType(type: NodeAttributes['type']): string[];
|
|
16
|
+
addNode(id: string, attributes: NodeAttributes): void;
|
|
17
|
+
removeNode(id: string): void;
|
|
18
|
+
addEdge(id: string, source: string, target: string, attributes: EdgeAttributes): void;
|
|
19
|
+
removeEdge(id: string): void;
|
|
20
|
+
getAllNodeIds(): string[];
|
|
21
|
+
getAllEdges(): Array<{
|
|
22
|
+
id: string;
|
|
23
|
+
source: string;
|
|
24
|
+
target: string;
|
|
25
|
+
}>;
|
|
26
|
+
getIncomingEdges(nodeId: string): Array<{
|
|
27
|
+
id: string;
|
|
28
|
+
source: string;
|
|
29
|
+
attributes: EdgeAttributes;
|
|
30
|
+
}>;
|
|
31
|
+
getOutgoingEdges(nodeId: string): Array<{
|
|
32
|
+
id: string;
|
|
33
|
+
target: string;
|
|
34
|
+
attributes: EdgeAttributes;
|
|
35
|
+
}>;
|
|
36
|
+
getUnderlyingGraph(): MultiDirectedGraph<NodeAttributes, EdgeAttributes>;
|
|
37
|
+
toJSON(root: string, metadata?: Partial<GraphMetadata>): SerializedGraph;
|
|
38
|
+
static fromJSON(data: SerializedGraph): RouteGraph;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
declare function exportDot(graph: RouteGraph): string;
|
|
42
|
+
|
|
43
|
+
declare function exportHtml(graph: RouteGraph, root: string): string;
|
|
44
|
+
declare function exportMarkdown(graph: RouteGraph, root: string): string;
|
|
45
|
+
|
|
46
|
+
declare function exportJson(graph: RouteGraph, root: string): string;
|
|
47
|
+
|
|
48
|
+
declare function exportMermaid(graph: RouteGraph): string;
|
|
49
|
+
|
|
50
|
+
declare function exportPlantUML(graph: RouteGraph): string;
|
|
51
|
+
|
|
52
|
+
interface CycleResult {
|
|
53
|
+
nodes: string[];
|
|
54
|
+
edges: string[];
|
|
55
|
+
}
|
|
56
|
+
declare function findCycles(graph: RouteGraph): CycleResult[];
|
|
57
|
+
declare function findDeadRoutes(graph: RouteGraph): string[];
|
|
58
|
+
declare function findShortestPath(graph: RouteGraph, sourceId: string, targetId: string): string[] | null;
|
|
59
|
+
declare function detectInfiniteRedirects(graph: RouteGraph): CycleResult[];
|
|
60
|
+
declare function getMostConnected(graph: RouteGraph, n?: number): Array<{
|
|
61
|
+
id: string;
|
|
62
|
+
degree: number;
|
|
63
|
+
}>;
|
|
64
|
+
|
|
65
|
+
declare class IncrementalCache {
|
|
66
|
+
private fileHashes;
|
|
67
|
+
private fileDependencies;
|
|
68
|
+
private parsedFiles;
|
|
69
|
+
private graphSnapshot;
|
|
70
|
+
private readonly cacheDir;
|
|
71
|
+
constructor(cacheDir: string);
|
|
72
|
+
hashFile(content: string): string;
|
|
73
|
+
getFileHash(filePath: string): string | undefined;
|
|
74
|
+
setFileHash(filePath: string, hash: string): void;
|
|
75
|
+
getDependencies(filePath: string): Set<string>;
|
|
76
|
+
setDependencies(filePath: string, nodeIds: Set<string>): void;
|
|
77
|
+
getParsedFile(filePath: string): SemanticFile | undefined;
|
|
78
|
+
setParsedFile(filePath: string, file: SemanticFile): void;
|
|
79
|
+
getGraphSnapshot(): SerializedGraph | null;
|
|
80
|
+
setGraphSnapshot(snapshot: SerializedGraph): void;
|
|
81
|
+
invalidate(filePath: string): InvalidationSet;
|
|
82
|
+
save(): void;
|
|
83
|
+
private load;
|
|
84
|
+
}
|
|
85
|
+
declare function computeGraphPatch(before: SerializedGraph, after: SerializedGraph): GraphPatch;
|
|
86
|
+
|
|
87
|
+
declare class Invalidator {
|
|
88
|
+
private readonly cache;
|
|
89
|
+
constructor(cache: IncrementalCache);
|
|
90
|
+
computeInvalidation(event: FileChangeEvent): InvalidationSet;
|
|
91
|
+
getAffectedFiles(invalidation: InvalidationSet): string[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
declare class PluginRegistry {
|
|
95
|
+
private plugins;
|
|
96
|
+
register(plugin: FrameworkPlugin): void;
|
|
97
|
+
registerAll(plugins: FrameworkPlugin[]): void;
|
|
98
|
+
getAll(): FrameworkPlugin[];
|
|
99
|
+
detectActive(root: string, config: _route_intelligence_shared.AnalyzerConfig): Promise<FrameworkPlugin[]>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface Analyzer {
|
|
103
|
+
analyze(): Promise<AnalysisResult>;
|
|
104
|
+
watch(): GraphWatcher;
|
|
105
|
+
}
|
|
106
|
+
interface GraphWatcher extends EventEmitter {
|
|
107
|
+
on(event: 'update', listener: (patch: GraphPatch) => void): this;
|
|
108
|
+
on(event: 'error', listener: (error: Error) => void): this;
|
|
109
|
+
stop(): Promise<void>;
|
|
110
|
+
}
|
|
111
|
+
declare function createAnalyzer(config: AnalyzerConfig): Analyzer;
|
|
112
|
+
|
|
113
|
+
declare function createSemanticFile(sourceFile: SourceFile, filePath: string, customNavigationWrappers?: string[]): SemanticFile;
|
|
114
|
+
|
|
115
|
+
interface PipelineContext {
|
|
116
|
+
root: string;
|
|
117
|
+
config: AnalyzerConfig;
|
|
118
|
+
graph: RouteGraph;
|
|
119
|
+
plugins: FrameworkPlugin[];
|
|
120
|
+
files: string[];
|
|
121
|
+
semanticFiles: Map<string, SemanticFile>;
|
|
122
|
+
routes: Map<string, RawRoute>;
|
|
123
|
+
pluginConfigs: Map<string, Record<string, unknown>>;
|
|
124
|
+
}
|
|
125
|
+
interface PipelineStage {
|
|
126
|
+
readonly name: string;
|
|
127
|
+
run(ctx: PipelineContext): Promise<void>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
declare class Pipeline {
|
|
131
|
+
private readonly stages;
|
|
132
|
+
constructor(stages?: PipelineStage[]);
|
|
133
|
+
run(ctx: PipelineContext): Promise<{
|
|
134
|
+
diagnostics: Diagnostic[];
|
|
135
|
+
metadata: GraphMetadata;
|
|
136
|
+
}>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
interface GraphMetrics {
|
|
140
|
+
totalRoutes: number;
|
|
141
|
+
totalLayouts: number;
|
|
142
|
+
totalApiRoutes: number;
|
|
143
|
+
deadRouteCount: number;
|
|
144
|
+
cycleCount: number;
|
|
145
|
+
maxDepth: number;
|
|
146
|
+
averageDepth: number;
|
|
147
|
+
mostConnected: Array<{
|
|
148
|
+
id: string;
|
|
149
|
+
degree: number;
|
|
150
|
+
path: string;
|
|
151
|
+
}>;
|
|
152
|
+
maintainabilityScore: number;
|
|
153
|
+
riskScore: number;
|
|
154
|
+
}
|
|
155
|
+
declare function computeMetrics(graph: RouteGraph): GraphMetrics;
|
|
156
|
+
declare function metricsToMetadata(graph: RouteGraph, pluginIds: string[]): GraphMetadata;
|
|
157
|
+
|
|
158
|
+
export { type Analyzer, type GraphWatcher, IncrementalCache, Invalidator, Pipeline, PluginRegistry, RouteGraph, computeGraphPatch, computeMetrics, createAnalyzer, createSemanticFile, detectInfiniteRedirects, exportDot, exportHtml, exportJson, exportMarkdown, exportMermaid, exportPlantUML, findCycles, findDeadRoutes, findShortestPath, getMostConnected, metricsToMetadata };
|