@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.
- package/LICENSE +21 -0
- package/README.md +366 -0
- package/dist/adapters/csharp-adapter.js +149 -0
- package/dist/adapters/go-adapter.js +96 -0
- package/dist/adapters/index.js +16 -0
- package/dist/adapters/java-adapter.js +122 -0
- package/dist/adapters/python-adapter.js +183 -0
- package/dist/adapters/runner.js +69 -0
- package/dist/adapters/types.js +1 -0
- package/dist/adapters/typescript-adapter.js +179 -0
- package/dist/benchmarking/framework.js +91 -0
- package/dist/cli.js +343 -0
- package/dist/commands/analyze-depth.js +43 -0
- package/dist/commands/api-spec-extractor.js +52 -0
- package/dist/commands/breaking-change-analyzer.js +334 -0
- package/dist/commands/config-compliance.js +219 -0
- package/dist/commands/constraints.js +221 -0
- package/dist/commands/context.js +101 -0
- package/dist/commands/data-flow-tracer.js +291 -0
- package/dist/commands/dependency-impact-analyzer.js +27 -0
- package/dist/commands/diff.js +146 -0
- package/dist/commands/discrepancy.js +71 -0
- package/dist/commands/doc-generate.js +163 -0
- package/dist/commands/doc-html.js +120 -0
- package/dist/commands/drift.js +88 -0
- package/dist/commands/extract.js +16 -0
- package/dist/commands/feature-context.js +116 -0
- package/dist/commands/generate.js +339 -0
- package/dist/commands/guard.js +182 -0
- package/dist/commands/init.js +209 -0
- package/dist/commands/intel.js +20 -0
- package/dist/commands/license-dependency-auditor.js +33 -0
- package/dist/commands/performance-hotspot-profiler.js +42 -0
- package/dist/commands/search.js +314 -0
- package/dist/commands/security-boundary-auditor.js +359 -0
- package/dist/commands/simulate.js +294 -0
- package/dist/commands/summary.js +27 -0
- package/dist/commands/test-coverage-mapper.js +264 -0
- package/dist/commands/verify-drift.js +62 -0
- package/dist/config.js +441 -0
- package/dist/extract/ai-context-hints.js +107 -0
- package/dist/extract/analyzers/backend.js +1704 -0
- package/dist/extract/analyzers/depth.js +264 -0
- package/dist/extract/analyzers/frontend.js +2221 -0
- package/dist/extract/api-usage-tracker.js +19 -0
- package/dist/extract/cache.js +53 -0
- package/dist/extract/codebase-intel.js +190 -0
- package/dist/extract/compress.js +452 -0
- package/dist/extract/context-block.js +356 -0
- package/dist/extract/contracts.js +183 -0
- package/dist/extract/discrepancies.js +233 -0
- package/dist/extract/docs-loader.js +110 -0
- package/dist/extract/docs.js +2379 -0
- package/dist/extract/drift.js +1578 -0
- package/dist/extract/duplicates.js +435 -0
- package/dist/extract/feature-arcs.js +138 -0
- package/dist/extract/graph.js +76 -0
- package/dist/extract/html-doc.js +1409 -0
- package/dist/extract/ignore.js +45 -0
- package/dist/extract/index.js +455 -0
- package/dist/extract/llm-client.js +159 -0
- package/dist/extract/pattern-registry.js +141 -0
- package/dist/extract/product-doc.js +497 -0
- package/dist/extract/python.js +1202 -0
- package/dist/extract/runtime.js +193 -0
- package/dist/extract/schema-evolution-validator.js +35 -0
- package/dist/extract/test-gap-analyzer.js +20 -0
- package/dist/extract/tests.js +74 -0
- package/dist/extract/types.js +1 -0
- package/dist/extract/validate-backend.js +30 -0
- package/dist/extract/writer.js +11 -0
- package/dist/output-layout.js +37 -0
- package/dist/project-discovery.js +309 -0
- package/dist/schema/architecture.js +350 -0
- package/dist/schema/feature-spec.js +89 -0
- package/dist/schema/index.js +8 -0
- package/dist/schema/ux.js +46 -0
- package/package.json +75 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { buildFunctionGraph } from "./drift.js";
|
|
5
|
+
export async function writeCompressionOutputs(params) {
|
|
6
|
+
const { outputDir, architecture, ux, context } = params;
|
|
7
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
8
|
+
const previous = await loadArchitectureSummary(outputDir);
|
|
9
|
+
const summary = buildArchitectureSummary(architecture, ux);
|
|
10
|
+
const diff = previous ? buildArchitectureDiff(previous, summary) : null;
|
|
11
|
+
const heatmap = await buildHeatmapBundle(architecture, context);
|
|
12
|
+
await fs.writeFile(path.join(outputDir, "architecture.summary.json"), JSON.stringify(summary, null, 2));
|
|
13
|
+
if (diff) {
|
|
14
|
+
await fs.writeFile(path.join(outputDir, "architecture.diff.summary.json"), JSON.stringify(diff, null, 2));
|
|
15
|
+
}
|
|
16
|
+
await fs.writeFile(path.join(outputDir, "drift.heatmap.json"), JSON.stringify(heatmap, null, 2));
|
|
17
|
+
return { summary, diff, heatmap };
|
|
18
|
+
}
|
|
19
|
+
export async function loadArchitectureSummary(outputDir) {
|
|
20
|
+
try {
|
|
21
|
+
const raw = await fs.readFile(path.join(outputDir, "architecture.summary.json"), "utf8");
|
|
22
|
+
return JSON.parse(raw);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export async function loadArchitectureDiff(outputDir) {
|
|
29
|
+
try {
|
|
30
|
+
const raw = await fs.readFile(path.join(outputDir, "architecture.diff.summary.json"), "utf8");
|
|
31
|
+
return JSON.parse(raw);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function loadHeatmap(outputDir) {
|
|
38
|
+
try {
|
|
39
|
+
const raw = await fs.readFile(path.join(outputDir, "drift.heatmap.json"), "utf8");
|
|
40
|
+
const parsed = JSON.parse(raw);
|
|
41
|
+
if ("levels" in parsed) {
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
if ("entries" in parsed) {
|
|
45
|
+
return {
|
|
46
|
+
version: "0.2",
|
|
47
|
+
generated_at: new Date().toISOString(),
|
|
48
|
+
levels: [
|
|
49
|
+
{
|
|
50
|
+
level: "module",
|
|
51
|
+
entries: parsed.entries
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function buildArchitectureSummary(architecture, ux) {
|
|
63
|
+
const modules = architecture.modules.map((module) => module.id).sort();
|
|
64
|
+
const moduleEdges = architecture.dependencies.module_graph
|
|
65
|
+
.map((edge) => `${edge.from}→${edge.to}`)
|
|
66
|
+
.sort();
|
|
67
|
+
const endpoints = architecture.endpoints
|
|
68
|
+
.map((endpoint) => `${endpoint.method} ${endpoint.path}`)
|
|
69
|
+
.sort();
|
|
70
|
+
const models = architecture.data_models.map((model) => model.name).sort();
|
|
71
|
+
const pages = ux.pages.map((page) => page.path).sort();
|
|
72
|
+
const components = ux.components.map((component) => component.id).sort();
|
|
73
|
+
const tasks = architecture.tasks.map((task) => task.name).sort();
|
|
74
|
+
const services = architecture.runtime.services.map((service) => service.name).sort();
|
|
75
|
+
const fileEdges = architecture.dependencies.file_graph ?? [];
|
|
76
|
+
const normalized = {
|
|
77
|
+
modules,
|
|
78
|
+
module_edges: moduleEdges,
|
|
79
|
+
endpoints,
|
|
80
|
+
models,
|
|
81
|
+
pages,
|
|
82
|
+
components,
|
|
83
|
+
tasks,
|
|
84
|
+
runtime_services: services
|
|
85
|
+
};
|
|
86
|
+
const fingerprint = hashObject(normalized);
|
|
87
|
+
const shapeFingerprint = computeShapeFingerprint(architecture);
|
|
88
|
+
const moduleCounts = new Map();
|
|
89
|
+
for (const module of architecture.modules) {
|
|
90
|
+
moduleCounts.set(module.id, { inbound: 0, outbound: 0, layer: module.layer });
|
|
91
|
+
}
|
|
92
|
+
for (const edge of architecture.dependencies.module_graph) {
|
|
93
|
+
const from = moduleCounts.get(edge.from);
|
|
94
|
+
if (from) {
|
|
95
|
+
from.outbound += 1;
|
|
96
|
+
}
|
|
97
|
+
const to = moduleCounts.get(edge.to);
|
|
98
|
+
if (to) {
|
|
99
|
+
to.inbound += 1;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const topModules = Array.from(moduleCounts.entries())
|
|
103
|
+
.map(([id, stats]) => ({
|
|
104
|
+
id,
|
|
105
|
+
inbound: stats.inbound,
|
|
106
|
+
outbound: stats.outbound,
|
|
107
|
+
total: stats.inbound + stats.outbound,
|
|
108
|
+
layer: stats.layer
|
|
109
|
+
}))
|
|
110
|
+
.sort((a, b) => b.total - a.total)
|
|
111
|
+
.slice(0, 10);
|
|
112
|
+
return {
|
|
113
|
+
version: "0.1",
|
|
114
|
+
generated_at: new Date().toISOString(),
|
|
115
|
+
fingerprint,
|
|
116
|
+
shape_fingerprint: shapeFingerprint,
|
|
117
|
+
normalized,
|
|
118
|
+
counts: {
|
|
119
|
+
modules: modules.length,
|
|
120
|
+
module_edges: moduleEdges.length,
|
|
121
|
+
file_edges: fileEdges.length,
|
|
122
|
+
files: countUniqueFiles(architecture),
|
|
123
|
+
endpoints: endpoints.length,
|
|
124
|
+
models: models.length,
|
|
125
|
+
pages: pages.length,
|
|
126
|
+
components: components.length,
|
|
127
|
+
tasks: tasks.length,
|
|
128
|
+
services: services.length
|
|
129
|
+
},
|
|
130
|
+
top_modules: topModules
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export function buildArchitectureDiff(previous, next) {
|
|
134
|
+
const diff = {
|
|
135
|
+
version: "0.1",
|
|
136
|
+
generated_at: new Date().toISOString(),
|
|
137
|
+
from_fingerprint: previous.fingerprint,
|
|
138
|
+
to_fingerprint: next.fingerprint,
|
|
139
|
+
structural_change: previous.fingerprint !== next.fingerprint,
|
|
140
|
+
shape_equivalent: previous.shape_fingerprint === next.shape_fingerprint,
|
|
141
|
+
added: {},
|
|
142
|
+
removed: {},
|
|
143
|
+
counts_delta: {}
|
|
144
|
+
};
|
|
145
|
+
const keys = [
|
|
146
|
+
"modules",
|
|
147
|
+
"module_edges",
|
|
148
|
+
"endpoints",
|
|
149
|
+
"models",
|
|
150
|
+
"pages",
|
|
151
|
+
"components",
|
|
152
|
+
"tasks",
|
|
153
|
+
"runtime_services"
|
|
154
|
+
];
|
|
155
|
+
for (const key of keys) {
|
|
156
|
+
const prev = new Set(previous.normalized[key]);
|
|
157
|
+
const nextSet = new Set(next.normalized[key]);
|
|
158
|
+
diff.added[key] = Array.from(nextSet).filter((value) => !prev.has(value)).sort();
|
|
159
|
+
diff.removed[key] = Array.from(prev).filter((value) => !nextSet.has(value)).sort();
|
|
160
|
+
}
|
|
161
|
+
const countKeys = Object.keys(next.counts);
|
|
162
|
+
for (const key of countKeys) {
|
|
163
|
+
diff.counts_delta[key] = (next.counts[key] ?? 0) - (previous.counts[key] ?? 0);
|
|
164
|
+
}
|
|
165
|
+
return diff;
|
|
166
|
+
}
|
|
167
|
+
function computeShapeFingerprint(architecture) {
|
|
168
|
+
const inbound = new Map();
|
|
169
|
+
const outbound = new Map();
|
|
170
|
+
for (const module of architecture.modules) {
|
|
171
|
+
inbound.set(module.id, 0);
|
|
172
|
+
outbound.set(module.id, 0);
|
|
173
|
+
}
|
|
174
|
+
for (const edge of architecture.dependencies.module_graph) {
|
|
175
|
+
outbound.set(edge.from, (outbound.get(edge.from) ?? 0) + 1);
|
|
176
|
+
inbound.set(edge.to, (inbound.get(edge.to) ?? 0) + 1);
|
|
177
|
+
}
|
|
178
|
+
const degrees = architecture.modules
|
|
179
|
+
.map((module) => ({
|
|
180
|
+
in: inbound.get(module.id) ?? 0,
|
|
181
|
+
out: outbound.get(module.id) ?? 0
|
|
182
|
+
}))
|
|
183
|
+
.sort((a, b) => (a.in + a.out) - (b.in + b.out));
|
|
184
|
+
const degreeSignature = degrees.map((entry) => `${entry.in}:${entry.out}`).join("|");
|
|
185
|
+
const edgeSignature = architecture.dependencies.module_graph
|
|
186
|
+
.map((edge) => {
|
|
187
|
+
const from = `${inbound.get(edge.from) ?? 0}:${outbound.get(edge.from) ?? 0}`;
|
|
188
|
+
const to = `${inbound.get(edge.to) ?? 0}:${outbound.get(edge.to) ?? 0}`;
|
|
189
|
+
return `${from}->${to}`;
|
|
190
|
+
})
|
|
191
|
+
.sort()
|
|
192
|
+
.join("|");
|
|
193
|
+
return hashObject({
|
|
194
|
+
degreeSignature,
|
|
195
|
+
edgeSignature
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
async function buildHeatmapBundle(architecture, context) {
|
|
199
|
+
const levels = [];
|
|
200
|
+
const moduleNodes = architecture.modules.map((module) => module.id);
|
|
201
|
+
const moduleLayers = new Map(architecture.modules.map((module) => [module.id, module.layer]));
|
|
202
|
+
const moduleEdges = architecture.dependencies.module_graph.map((edge) => ({
|
|
203
|
+
from: edge.from,
|
|
204
|
+
to: edge.to
|
|
205
|
+
}));
|
|
206
|
+
levels.push(buildHeatmapFromGraph("module", moduleNodes, moduleEdges, moduleLayers));
|
|
207
|
+
if (architecture.dependencies.file_graph && architecture.dependencies.file_graph.length > 0) {
|
|
208
|
+
const fileNodes = new Set();
|
|
209
|
+
const fileLayers = new Map();
|
|
210
|
+
for (const module of architecture.modules) {
|
|
211
|
+
for (const file of module.files) {
|
|
212
|
+
fileNodes.add(file);
|
|
213
|
+
fileLayers.set(file, module.layer);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
for (const edge of architecture.dependencies.file_graph) {
|
|
217
|
+
fileNodes.add(edge.from);
|
|
218
|
+
fileNodes.add(edge.to);
|
|
219
|
+
}
|
|
220
|
+
const fileEdges = architecture.dependencies.file_graph.map((edge) => ({
|
|
221
|
+
from: edge.from,
|
|
222
|
+
to: edge.to
|
|
223
|
+
}));
|
|
224
|
+
levels.push(buildHeatmapFromGraph("file", Array.from(fileNodes), fileEdges, fileLayers));
|
|
225
|
+
}
|
|
226
|
+
const domainMap = context?.config?.drift?.domains ?? {};
|
|
227
|
+
if (Object.keys(domainMap).length > 0) {
|
|
228
|
+
const moduleToDomain = new Map();
|
|
229
|
+
for (const module of architecture.modules) {
|
|
230
|
+
const domain = resolveDomainForModule(module.id, domainMap);
|
|
231
|
+
if (domain) {
|
|
232
|
+
moduleToDomain.set(module.id, domain);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const domainNodes = new Set();
|
|
236
|
+
const domainLayers = new Map();
|
|
237
|
+
for (const domain of moduleToDomain.values()) {
|
|
238
|
+
domainNodes.add(domain);
|
|
239
|
+
domainLayers.set(domain, domain);
|
|
240
|
+
}
|
|
241
|
+
const domainEdges = [];
|
|
242
|
+
const seen = new Set();
|
|
243
|
+
for (const edge of architecture.dependencies.module_graph) {
|
|
244
|
+
const fromDomain = moduleToDomain.get(edge.from) ?? "unassigned";
|
|
245
|
+
const toDomain = moduleToDomain.get(edge.to) ?? "unassigned";
|
|
246
|
+
const key = `${fromDomain}::${toDomain}`;
|
|
247
|
+
if (seen.has(key)) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
seen.add(key);
|
|
251
|
+
domainNodes.add(fromDomain);
|
|
252
|
+
domainNodes.add(toDomain);
|
|
253
|
+
domainLayers.set(fromDomain, fromDomain);
|
|
254
|
+
domainLayers.set(toDomain, toDomain);
|
|
255
|
+
domainEdges.push({ from: fromDomain, to: toDomain });
|
|
256
|
+
}
|
|
257
|
+
levels.push(buildHeatmapFromGraph("domain", Array.from(domainNodes), domainEdges, domainLayers));
|
|
258
|
+
}
|
|
259
|
+
if (context?.backendRoot && context?.projectRoot) {
|
|
260
|
+
const hasFunctionScale = context.config?.drift?.scales?.includes("function") ?? false;
|
|
261
|
+
const shouldFunction = hasFunctionScale ||
|
|
262
|
+
context.config?.drift?.graphLevel === "function" ||
|
|
263
|
+
context.config?.drift?.graphLevel === "auto";
|
|
264
|
+
if (shouldFunction) {
|
|
265
|
+
const fnGraph = await buildFunctionGraph({
|
|
266
|
+
backendRoot: context.backendRoot,
|
|
267
|
+
modules: architecture.modules,
|
|
268
|
+
projectRoot: context.projectRoot
|
|
269
|
+
});
|
|
270
|
+
if (fnGraph) {
|
|
271
|
+
levels.push(buildHeatmapFromGraph("function", fnGraph.nodes, fnGraph.edges, fnGraph.nodeLayers));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
version: "0.2",
|
|
277
|
+
generated_at: new Date().toISOString(),
|
|
278
|
+
levels
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function buildHeatmapFromGraph(level, nodes, edges, nodeLayers) {
|
|
282
|
+
const adjacency = new Map();
|
|
283
|
+
const reverse = new Map();
|
|
284
|
+
const outbound = new Map();
|
|
285
|
+
const inbound = new Map();
|
|
286
|
+
const crossLayerOut = new Map();
|
|
287
|
+
for (const node of nodes) {
|
|
288
|
+
adjacency.set(node, []);
|
|
289
|
+
reverse.set(node, []);
|
|
290
|
+
outbound.set(node, 0);
|
|
291
|
+
inbound.set(node, 0);
|
|
292
|
+
crossLayerOut.set(node, 0);
|
|
293
|
+
}
|
|
294
|
+
for (const edge of edges) {
|
|
295
|
+
if (!adjacency.has(edge.from)) {
|
|
296
|
+
adjacency.set(edge.from, []);
|
|
297
|
+
reverse.set(edge.from, []);
|
|
298
|
+
outbound.set(edge.from, 0);
|
|
299
|
+
inbound.set(edge.from, 0);
|
|
300
|
+
crossLayerOut.set(edge.from, 0);
|
|
301
|
+
nodes.push(edge.from);
|
|
302
|
+
}
|
|
303
|
+
if (!adjacency.has(edge.to)) {
|
|
304
|
+
adjacency.set(edge.to, []);
|
|
305
|
+
reverse.set(edge.to, []);
|
|
306
|
+
outbound.set(edge.to, 0);
|
|
307
|
+
inbound.set(edge.to, 0);
|
|
308
|
+
crossLayerOut.set(edge.to, 0);
|
|
309
|
+
nodes.push(edge.to);
|
|
310
|
+
}
|
|
311
|
+
adjacency.get(edge.from)?.push(edge.to);
|
|
312
|
+
reverse.get(edge.to)?.push(edge.from);
|
|
313
|
+
outbound.set(edge.from, (outbound.get(edge.from) ?? 0) + 1);
|
|
314
|
+
inbound.set(edge.to, (inbound.get(edge.to) ?? 0) + 1);
|
|
315
|
+
const fromLayer = nodeLayers.get(edge.from);
|
|
316
|
+
const toLayer = nodeLayers.get(edge.to);
|
|
317
|
+
if (fromLayer && toLayer && fromLayer !== toLayer) {
|
|
318
|
+
crossLayerOut.set(edge.from, (crossLayerOut.get(edge.from) ?? 0) + 1);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const cycleNodes = findCycleNodes(nodes, adjacency, reverse);
|
|
322
|
+
const degreeValues = nodes.map((node) => (outbound.get(node) ?? 0) + (inbound.get(node) ?? 0));
|
|
323
|
+
const maxDegree = Math.max(1, ...degreeValues);
|
|
324
|
+
const maxCrossRatio = Math.max(1, ...nodes.map((node) => {
|
|
325
|
+
const out = outbound.get(node) ?? 0;
|
|
326
|
+
const cross = crossLayerOut.get(node) ?? 0;
|
|
327
|
+
return out === 0 ? 0 : cross / out;
|
|
328
|
+
}));
|
|
329
|
+
const entries = nodes.map((node) => {
|
|
330
|
+
const degree = (outbound.get(node) ?? 0) + (inbound.get(node) ?? 0);
|
|
331
|
+
const crossOut = crossLayerOut.get(node) ?? 0;
|
|
332
|
+
const out = outbound.get(node) ?? 0;
|
|
333
|
+
const crossRatio = out === 0 ? 0 : crossOut / out;
|
|
334
|
+
const cycleFlag = cycleNodes.has(node) ? 1 : 0;
|
|
335
|
+
const score = 0.5 * (degree / maxDegree) +
|
|
336
|
+
0.3 * (crossRatio / maxCrossRatio) +
|
|
337
|
+
0.2 * cycleFlag;
|
|
338
|
+
return {
|
|
339
|
+
id: node,
|
|
340
|
+
layer: nodeLayers.get(node) ?? "unknown",
|
|
341
|
+
score: round(score, 4),
|
|
342
|
+
components: {
|
|
343
|
+
degree,
|
|
344
|
+
cross_layer_ratio: round(crossRatio, 4),
|
|
345
|
+
cycle: cycleFlag
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
});
|
|
349
|
+
entries.sort((a, b) => b.score - a.score);
|
|
350
|
+
return {
|
|
351
|
+
level,
|
|
352
|
+
entries
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function resolveDomainForModule(moduleId, domainMap) {
|
|
356
|
+
for (const [domain, patterns] of Object.entries(domainMap)) {
|
|
357
|
+
for (const pattern of patterns) {
|
|
358
|
+
if (pattern === moduleId) {
|
|
359
|
+
return domain;
|
|
360
|
+
}
|
|
361
|
+
if (pattern.endsWith("*")) {
|
|
362
|
+
const prefix = pattern.slice(0, -1);
|
|
363
|
+
if (moduleId.startsWith(prefix)) {
|
|
364
|
+
return domain;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
function findCycleNodes(nodes, adjacency, reverse) {
|
|
372
|
+
const visited = new Set();
|
|
373
|
+
const order = [];
|
|
374
|
+
const dfs1 = (node) => {
|
|
375
|
+
visited.add(node);
|
|
376
|
+
for (const next of adjacency.get(node) ?? []) {
|
|
377
|
+
if (!visited.has(next)) {
|
|
378
|
+
dfs1(next);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
order.push(node);
|
|
382
|
+
};
|
|
383
|
+
for (const node of nodes) {
|
|
384
|
+
if (!visited.has(node)) {
|
|
385
|
+
dfs1(node);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const visited2 = new Set();
|
|
389
|
+
const cycleNodes = new Set();
|
|
390
|
+
const dfs2 = (node, component) => {
|
|
391
|
+
visited2.add(node);
|
|
392
|
+
component.push(node);
|
|
393
|
+
for (const next of reverse.get(node) ?? []) {
|
|
394
|
+
if (!visited2.has(next)) {
|
|
395
|
+
dfs2(next, component);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
for (let i = order.length - 1; i >= 0; i -= 1) {
|
|
400
|
+
const node = order[i];
|
|
401
|
+
if (visited2.has(node)) {
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
const component = [];
|
|
405
|
+
dfs2(node, component);
|
|
406
|
+
if (component.length > 1) {
|
|
407
|
+
for (const entry of component) {
|
|
408
|
+
cycleNodes.add(entry);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
const neighbors = adjacency.get(node) ?? [];
|
|
413
|
+
if (neighbors.includes(node)) {
|
|
414
|
+
cycleNodes.add(node);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return cycleNodes;
|
|
419
|
+
}
|
|
420
|
+
function countUniqueFiles(snapshot) {
|
|
421
|
+
const files = new Set();
|
|
422
|
+
for (const module of snapshot.modules) {
|
|
423
|
+
for (const file of module.files) {
|
|
424
|
+
files.add(file);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
for (const file of snapshot.frontend_files) {
|
|
428
|
+
files.add(file);
|
|
429
|
+
}
|
|
430
|
+
return files.size;
|
|
431
|
+
}
|
|
432
|
+
function hashObject(value) {
|
|
433
|
+
const payload = stableStringify(value);
|
|
434
|
+
return crypto.createHash("sha256").update(payload).digest("hex");
|
|
435
|
+
}
|
|
436
|
+
function stableStringify(value) {
|
|
437
|
+
if (Array.isArray(value)) {
|
|
438
|
+
return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
|
|
439
|
+
}
|
|
440
|
+
if (value && typeof value === "object") {
|
|
441
|
+
const obj = value;
|
|
442
|
+
const keys = Object.keys(obj).sort();
|
|
443
|
+
return `{${keys
|
|
444
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(obj[key])}`)
|
|
445
|
+
.join(",")}}`;
|
|
446
|
+
}
|
|
447
|
+
return JSON.stringify(value);
|
|
448
|
+
}
|
|
449
|
+
function round(value, precision) {
|
|
450
|
+
const factor = 10 ** precision;
|
|
451
|
+
return Math.round(value * factor) / factor;
|
|
452
|
+
}
|