@harness-engineering/graph 0.2.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +47 -1
- package/dist/index.d.ts +47 -1
- package/dist/index.js +240 -0
- package/dist/index.mjs +239 -0
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -527,6 +527,52 @@ declare class GraphCouplingAdapter {
|
|
|
527
527
|
private computeTransitiveDepth;
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
+
interface AnomalyDetectionOptions {
|
|
531
|
+
threshold?: number;
|
|
532
|
+
metrics?: string[];
|
|
533
|
+
}
|
|
534
|
+
interface StatisticalOutlier {
|
|
535
|
+
nodeId: string;
|
|
536
|
+
nodeName: string;
|
|
537
|
+
nodePath?: string | undefined;
|
|
538
|
+
nodeType: string;
|
|
539
|
+
metric: string;
|
|
540
|
+
value: number;
|
|
541
|
+
zScore: number;
|
|
542
|
+
mean: number;
|
|
543
|
+
stdDev: number;
|
|
544
|
+
}
|
|
545
|
+
interface ArticulationPoint {
|
|
546
|
+
nodeId: string;
|
|
547
|
+
nodeName: string;
|
|
548
|
+
nodePath?: string | undefined;
|
|
549
|
+
componentsIfRemoved: number;
|
|
550
|
+
dependentCount: number;
|
|
551
|
+
}
|
|
552
|
+
interface AnomalyReport {
|
|
553
|
+
statisticalOutliers: StatisticalOutlier[];
|
|
554
|
+
articulationPoints: ArticulationPoint[];
|
|
555
|
+
overlapping: string[];
|
|
556
|
+
summary: {
|
|
557
|
+
totalNodesAnalyzed: number;
|
|
558
|
+
outlierCount: number;
|
|
559
|
+
articulationPointCount: number;
|
|
560
|
+
overlapCount: number;
|
|
561
|
+
metricsAnalyzed: string[];
|
|
562
|
+
warnings: string[];
|
|
563
|
+
threshold: number;
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
declare class GraphAnomalyAdapter {
|
|
567
|
+
private readonly store;
|
|
568
|
+
constructor(store: GraphStore);
|
|
569
|
+
detect(options?: AnomalyDetectionOptions): AnomalyReport;
|
|
570
|
+
private collectMetricValues;
|
|
571
|
+
private computeZScoreOutliers;
|
|
572
|
+
private findArticulationPoints;
|
|
573
|
+
private computeRemovalImpact;
|
|
574
|
+
}
|
|
575
|
+
|
|
530
576
|
interface AssembledContext {
|
|
531
577
|
readonly nodes: readonly GraphNode[];
|
|
532
578
|
readonly edges: readonly GraphEdge[];
|
|
@@ -662,4 +708,4 @@ declare class GraphFeedbackAdapter {
|
|
|
662
708
|
|
|
663
709
|
declare const VERSION = "0.2.0";
|
|
664
710
|
|
|
665
|
-
export { type AssembledContext, Assembler, CIConnector, CURRENT_SCHEMA_VERSION, CodeIngestor, ConfluenceConnector, type ConnectorConfig, ContextQL, type ContextQLParams, type ContextQLResult, DesignConstraintAdapter, DesignIngestor, type DesignStrictness, type DesignViolation, EDGE_TYPES, type EdgeQuery, type EdgeType, FusionLayer, type FusionResult, GitIngestor, type GitRunner, type GraphBudget, GraphComplexityAdapter, type GraphComplexityHotspot, type GraphComplexityResult, type GraphConnector, GraphConstraintAdapter, GraphCouplingAdapter, type GraphCouplingFileData, type GraphCouplingResult, type GraphCoverageReport, type GraphDeadCodeData, type GraphDependencyData, type GraphDriftData, type GraphEdge, GraphEdgeSchema, GraphEntropyAdapter, GraphFeedbackAdapter, type GraphFilterResult, type GraphHarnessCheckData, type GraphImpactData, type GraphLayerViolation, type GraphMetadata, type GraphNode, GraphNodeSchema, type GraphSnapshotSummary, GraphStore, type HttpClient, type IngestResult, JiraConnector, KnowledgeIngestor, type LinkResult, NODE_TYPES, type NodeQuery, type NodeType, OBSERVABILITY_TYPES, type ProjectionSpec, SlackConnector, type SourceLocation, SyncManager, type SyncMetadata, TopologicalLinker, VERSION, type VectorSearchResult, VectorStore, linkToCode, loadGraph, project, saveGraph };
|
|
711
|
+
export { type AnomalyDetectionOptions, type AnomalyReport, type ArticulationPoint, type AssembledContext, Assembler, CIConnector, CURRENT_SCHEMA_VERSION, CodeIngestor, ConfluenceConnector, type ConnectorConfig, ContextQL, type ContextQLParams, type ContextQLResult, DesignConstraintAdapter, DesignIngestor, type DesignStrictness, type DesignViolation, EDGE_TYPES, type EdgeQuery, type EdgeType, FusionLayer, type FusionResult, GitIngestor, type GitRunner, GraphAnomalyAdapter, type GraphBudget, GraphComplexityAdapter, type GraphComplexityHotspot, type GraphComplexityResult, type GraphConnector, GraphConstraintAdapter, GraphCouplingAdapter, type GraphCouplingFileData, type GraphCouplingResult, type GraphCoverageReport, type GraphDeadCodeData, type GraphDependencyData, type GraphDriftData, type GraphEdge, GraphEdgeSchema, GraphEntropyAdapter, GraphFeedbackAdapter, type GraphFilterResult, type GraphHarnessCheckData, type GraphImpactData, type GraphLayerViolation, type GraphMetadata, type GraphNode, GraphNodeSchema, type GraphSnapshotSummary, GraphStore, type HttpClient, type IngestResult, JiraConnector, KnowledgeIngestor, type LinkResult, NODE_TYPES, type NodeQuery, type NodeType, OBSERVABILITY_TYPES, type ProjectionSpec, SlackConnector, type SourceLocation, type StatisticalOutlier, SyncManager, type SyncMetadata, TopologicalLinker, VERSION, type VectorSearchResult, VectorStore, linkToCode, loadGraph, project, saveGraph };
|
package/dist/index.d.ts
CHANGED
|
@@ -527,6 +527,52 @@ declare class GraphCouplingAdapter {
|
|
|
527
527
|
private computeTransitiveDepth;
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
+
interface AnomalyDetectionOptions {
|
|
531
|
+
threshold?: number;
|
|
532
|
+
metrics?: string[];
|
|
533
|
+
}
|
|
534
|
+
interface StatisticalOutlier {
|
|
535
|
+
nodeId: string;
|
|
536
|
+
nodeName: string;
|
|
537
|
+
nodePath?: string | undefined;
|
|
538
|
+
nodeType: string;
|
|
539
|
+
metric: string;
|
|
540
|
+
value: number;
|
|
541
|
+
zScore: number;
|
|
542
|
+
mean: number;
|
|
543
|
+
stdDev: number;
|
|
544
|
+
}
|
|
545
|
+
interface ArticulationPoint {
|
|
546
|
+
nodeId: string;
|
|
547
|
+
nodeName: string;
|
|
548
|
+
nodePath?: string | undefined;
|
|
549
|
+
componentsIfRemoved: number;
|
|
550
|
+
dependentCount: number;
|
|
551
|
+
}
|
|
552
|
+
interface AnomalyReport {
|
|
553
|
+
statisticalOutliers: StatisticalOutlier[];
|
|
554
|
+
articulationPoints: ArticulationPoint[];
|
|
555
|
+
overlapping: string[];
|
|
556
|
+
summary: {
|
|
557
|
+
totalNodesAnalyzed: number;
|
|
558
|
+
outlierCount: number;
|
|
559
|
+
articulationPointCount: number;
|
|
560
|
+
overlapCount: number;
|
|
561
|
+
metricsAnalyzed: string[];
|
|
562
|
+
warnings: string[];
|
|
563
|
+
threshold: number;
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
declare class GraphAnomalyAdapter {
|
|
567
|
+
private readonly store;
|
|
568
|
+
constructor(store: GraphStore);
|
|
569
|
+
detect(options?: AnomalyDetectionOptions): AnomalyReport;
|
|
570
|
+
private collectMetricValues;
|
|
571
|
+
private computeZScoreOutliers;
|
|
572
|
+
private findArticulationPoints;
|
|
573
|
+
private computeRemovalImpact;
|
|
574
|
+
}
|
|
575
|
+
|
|
530
576
|
interface AssembledContext {
|
|
531
577
|
readonly nodes: readonly GraphNode[];
|
|
532
578
|
readonly edges: readonly GraphEdge[];
|
|
@@ -662,4 +708,4 @@ declare class GraphFeedbackAdapter {
|
|
|
662
708
|
|
|
663
709
|
declare const VERSION = "0.2.0";
|
|
664
710
|
|
|
665
|
-
export { type AssembledContext, Assembler, CIConnector, CURRENT_SCHEMA_VERSION, CodeIngestor, ConfluenceConnector, type ConnectorConfig, ContextQL, type ContextQLParams, type ContextQLResult, DesignConstraintAdapter, DesignIngestor, type DesignStrictness, type DesignViolation, EDGE_TYPES, type EdgeQuery, type EdgeType, FusionLayer, type FusionResult, GitIngestor, type GitRunner, type GraphBudget, GraphComplexityAdapter, type GraphComplexityHotspot, type GraphComplexityResult, type GraphConnector, GraphConstraintAdapter, GraphCouplingAdapter, type GraphCouplingFileData, type GraphCouplingResult, type GraphCoverageReport, type GraphDeadCodeData, type GraphDependencyData, type GraphDriftData, type GraphEdge, GraphEdgeSchema, GraphEntropyAdapter, GraphFeedbackAdapter, type GraphFilterResult, type GraphHarnessCheckData, type GraphImpactData, type GraphLayerViolation, type GraphMetadata, type GraphNode, GraphNodeSchema, type GraphSnapshotSummary, GraphStore, type HttpClient, type IngestResult, JiraConnector, KnowledgeIngestor, type LinkResult, NODE_TYPES, type NodeQuery, type NodeType, OBSERVABILITY_TYPES, type ProjectionSpec, SlackConnector, type SourceLocation, SyncManager, type SyncMetadata, TopologicalLinker, VERSION, type VectorSearchResult, VectorStore, linkToCode, loadGraph, project, saveGraph };
|
|
711
|
+
export { type AnomalyDetectionOptions, type AnomalyReport, type ArticulationPoint, type AssembledContext, Assembler, CIConnector, CURRENT_SCHEMA_VERSION, CodeIngestor, ConfluenceConnector, type ConnectorConfig, ContextQL, type ContextQLParams, type ContextQLResult, DesignConstraintAdapter, DesignIngestor, type DesignStrictness, type DesignViolation, EDGE_TYPES, type EdgeQuery, type EdgeType, FusionLayer, type FusionResult, GitIngestor, type GitRunner, GraphAnomalyAdapter, type GraphBudget, GraphComplexityAdapter, type GraphComplexityHotspot, type GraphComplexityResult, type GraphConnector, GraphConstraintAdapter, GraphCouplingAdapter, type GraphCouplingFileData, type GraphCouplingResult, type GraphCoverageReport, type GraphDeadCodeData, type GraphDependencyData, type GraphDriftData, type GraphEdge, GraphEdgeSchema, GraphEntropyAdapter, GraphFeedbackAdapter, type GraphFilterResult, type GraphHarnessCheckData, type GraphImpactData, type GraphLayerViolation, type GraphMetadata, type GraphNode, GraphNodeSchema, type GraphSnapshotSummary, GraphStore, type HttpClient, type IngestResult, JiraConnector, KnowledgeIngestor, type LinkResult, NODE_TYPES, type NodeQuery, type NodeType, OBSERVABILITY_TYPES, type ProjectionSpec, SlackConnector, type SourceLocation, type StatisticalOutlier, SyncManager, type SyncMetadata, TopologicalLinker, VERSION, type VectorSearchResult, VectorStore, linkToCode, loadGraph, project, saveGraph };
|
package/dist/index.js
CHANGED
|
@@ -41,6 +41,7 @@ __export(index_exports, {
|
|
|
41
41
|
EDGE_TYPES: () => EDGE_TYPES,
|
|
42
42
|
FusionLayer: () => FusionLayer,
|
|
43
43
|
GitIngestor: () => GitIngestor,
|
|
44
|
+
GraphAnomalyAdapter: () => GraphAnomalyAdapter,
|
|
44
45
|
GraphComplexityAdapter: () => GraphComplexityAdapter,
|
|
45
46
|
GraphConstraintAdapter: () => GraphConstraintAdapter,
|
|
46
47
|
GraphCouplingAdapter: () => GraphCouplingAdapter,
|
|
@@ -2272,6 +2273,244 @@ var GraphCouplingAdapter = class {
|
|
|
2272
2273
|
}
|
|
2273
2274
|
};
|
|
2274
2275
|
|
|
2276
|
+
// src/entropy/GraphAnomalyAdapter.ts
|
|
2277
|
+
var DEFAULT_THRESHOLD = 2;
|
|
2278
|
+
var DEFAULT_METRICS = [
|
|
2279
|
+
"cyclomaticComplexity",
|
|
2280
|
+
"fanIn",
|
|
2281
|
+
"fanOut",
|
|
2282
|
+
"hotspotScore",
|
|
2283
|
+
"transitiveDepth"
|
|
2284
|
+
];
|
|
2285
|
+
var RECOGNIZED_METRICS = new Set(DEFAULT_METRICS);
|
|
2286
|
+
var GraphAnomalyAdapter = class {
|
|
2287
|
+
constructor(store) {
|
|
2288
|
+
this.store = store;
|
|
2289
|
+
}
|
|
2290
|
+
detect(options) {
|
|
2291
|
+
const threshold = options?.threshold != null && options.threshold > 0 ? options.threshold : DEFAULT_THRESHOLD;
|
|
2292
|
+
const requestedMetrics = options?.metrics ?? [...DEFAULT_METRICS];
|
|
2293
|
+
const warnings = [];
|
|
2294
|
+
const metricsToAnalyze = [];
|
|
2295
|
+
for (const m of requestedMetrics) {
|
|
2296
|
+
if (RECOGNIZED_METRICS.has(m)) {
|
|
2297
|
+
metricsToAnalyze.push(m);
|
|
2298
|
+
} else {
|
|
2299
|
+
warnings.push(m);
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
const allOutliers = [];
|
|
2303
|
+
const analyzedNodeIds = /* @__PURE__ */ new Set();
|
|
2304
|
+
const couplingMetrics = ["fanIn", "fanOut", "transitiveDepth"];
|
|
2305
|
+
const needsCoupling = metricsToAnalyze.some((m) => couplingMetrics.includes(m));
|
|
2306
|
+
const needsComplexity = metricsToAnalyze.includes("hotspotScore");
|
|
2307
|
+
const cachedCouplingData = needsCoupling ? new GraphCouplingAdapter(this.store).computeCouplingData() : void 0;
|
|
2308
|
+
const cachedHotspotData = needsComplexity ? new GraphComplexityAdapter(this.store).computeComplexityHotspots() : void 0;
|
|
2309
|
+
for (const metric of metricsToAnalyze) {
|
|
2310
|
+
const entries = this.collectMetricValues(metric, cachedCouplingData, cachedHotspotData);
|
|
2311
|
+
for (const e of entries) {
|
|
2312
|
+
analyzedNodeIds.add(e.nodeId);
|
|
2313
|
+
}
|
|
2314
|
+
const outliers = this.computeZScoreOutliers(entries, metric, threshold);
|
|
2315
|
+
allOutliers.push(...outliers);
|
|
2316
|
+
}
|
|
2317
|
+
allOutliers.sort((a, b) => b.zScore - a.zScore);
|
|
2318
|
+
const articulationPoints = this.findArticulationPoints();
|
|
2319
|
+
const outlierNodeIds = new Set(allOutliers.map((o) => o.nodeId));
|
|
2320
|
+
const apNodeIds = new Set(articulationPoints.map((ap) => ap.nodeId));
|
|
2321
|
+
const overlapping = [...outlierNodeIds].filter((id) => apNodeIds.has(id));
|
|
2322
|
+
return {
|
|
2323
|
+
statisticalOutliers: allOutliers,
|
|
2324
|
+
articulationPoints,
|
|
2325
|
+
overlapping,
|
|
2326
|
+
summary: {
|
|
2327
|
+
totalNodesAnalyzed: analyzedNodeIds.size,
|
|
2328
|
+
outlierCount: allOutliers.length,
|
|
2329
|
+
articulationPointCount: articulationPoints.length,
|
|
2330
|
+
overlapCount: overlapping.length,
|
|
2331
|
+
metricsAnalyzed: metricsToAnalyze,
|
|
2332
|
+
warnings,
|
|
2333
|
+
threshold
|
|
2334
|
+
}
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
collectMetricValues(metric, cachedCouplingData, cachedHotspotData) {
|
|
2338
|
+
const entries = [];
|
|
2339
|
+
if (metric === "cyclomaticComplexity") {
|
|
2340
|
+
const functionNodes = [
|
|
2341
|
+
...this.store.findNodes({ type: "function" }),
|
|
2342
|
+
...this.store.findNodes({ type: "method" })
|
|
2343
|
+
];
|
|
2344
|
+
for (const node of functionNodes) {
|
|
2345
|
+
const cc = node.metadata?.cyclomaticComplexity;
|
|
2346
|
+
if (typeof cc === "number") {
|
|
2347
|
+
entries.push({
|
|
2348
|
+
nodeId: node.id,
|
|
2349
|
+
nodeName: node.name,
|
|
2350
|
+
nodePath: node.path,
|
|
2351
|
+
nodeType: node.type,
|
|
2352
|
+
value: cc
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
} else if (metric === "fanIn" || metric === "fanOut" || metric === "transitiveDepth") {
|
|
2357
|
+
const couplingData = cachedCouplingData ?? new GraphCouplingAdapter(this.store).computeCouplingData();
|
|
2358
|
+
const fileNodes = this.store.findNodes({ type: "file" });
|
|
2359
|
+
for (const fileData of couplingData.files) {
|
|
2360
|
+
const fileNode = fileNodes.find((n) => (n.path ?? n.name) === fileData.file);
|
|
2361
|
+
if (!fileNode) continue;
|
|
2362
|
+
entries.push({
|
|
2363
|
+
nodeId: fileNode.id,
|
|
2364
|
+
nodeName: fileNode.name,
|
|
2365
|
+
nodePath: fileNode.path,
|
|
2366
|
+
nodeType: "file",
|
|
2367
|
+
value: fileData[metric]
|
|
2368
|
+
});
|
|
2369
|
+
}
|
|
2370
|
+
} else if (metric === "hotspotScore") {
|
|
2371
|
+
const hotspots = cachedHotspotData ?? new GraphComplexityAdapter(this.store).computeComplexityHotspots();
|
|
2372
|
+
const functionNodes = [
|
|
2373
|
+
...this.store.findNodes({ type: "function" }),
|
|
2374
|
+
...this.store.findNodes({ type: "method" })
|
|
2375
|
+
];
|
|
2376
|
+
for (const h of hotspots.hotspots) {
|
|
2377
|
+
const fnNode = functionNodes.find(
|
|
2378
|
+
(n) => n.name === h.function && (n.path ?? "") === (h.file ?? "")
|
|
2379
|
+
);
|
|
2380
|
+
if (!fnNode) continue;
|
|
2381
|
+
entries.push({
|
|
2382
|
+
nodeId: fnNode.id,
|
|
2383
|
+
nodeName: fnNode.name,
|
|
2384
|
+
nodePath: fnNode.path,
|
|
2385
|
+
nodeType: fnNode.type,
|
|
2386
|
+
value: h.hotspotScore
|
|
2387
|
+
});
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
return entries;
|
|
2391
|
+
}
|
|
2392
|
+
computeZScoreOutliers(entries, metric, threshold) {
|
|
2393
|
+
if (entries.length === 0) return [];
|
|
2394
|
+
const values = entries.map((e) => e.value);
|
|
2395
|
+
const mean = values.reduce((sum, v) => sum + v, 0) / values.length;
|
|
2396
|
+
const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;
|
|
2397
|
+
const stdDev = Math.sqrt(variance);
|
|
2398
|
+
if (stdDev === 0) return [];
|
|
2399
|
+
const outliers = [];
|
|
2400
|
+
for (const entry of entries) {
|
|
2401
|
+
const zScore = Math.abs(entry.value - mean) / stdDev;
|
|
2402
|
+
if (zScore > threshold) {
|
|
2403
|
+
outliers.push({
|
|
2404
|
+
nodeId: entry.nodeId,
|
|
2405
|
+
nodeName: entry.nodeName,
|
|
2406
|
+
nodePath: entry.nodePath,
|
|
2407
|
+
nodeType: entry.nodeType,
|
|
2408
|
+
metric,
|
|
2409
|
+
value: entry.value,
|
|
2410
|
+
zScore,
|
|
2411
|
+
mean,
|
|
2412
|
+
stdDev
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
return outliers;
|
|
2417
|
+
}
|
|
2418
|
+
findArticulationPoints() {
|
|
2419
|
+
const fileNodes = this.store.findNodes({ type: "file" });
|
|
2420
|
+
if (fileNodes.length === 0) return [];
|
|
2421
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
2422
|
+
const adj = /* @__PURE__ */ new Map();
|
|
2423
|
+
for (const node of fileNodes) {
|
|
2424
|
+
nodeMap.set(node.id, { name: node.name, path: node.path });
|
|
2425
|
+
adj.set(node.id, /* @__PURE__ */ new Set());
|
|
2426
|
+
}
|
|
2427
|
+
const importEdges = this.store.getEdges({ type: "imports" });
|
|
2428
|
+
for (const edge of importEdges) {
|
|
2429
|
+
if (adj.has(edge.from) && adj.has(edge.to)) {
|
|
2430
|
+
adj.get(edge.from).add(edge.to);
|
|
2431
|
+
adj.get(edge.to).add(edge.from);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
const disc = /* @__PURE__ */ new Map();
|
|
2435
|
+
const low = /* @__PURE__ */ new Map();
|
|
2436
|
+
const parent = /* @__PURE__ */ new Map();
|
|
2437
|
+
const apSet = /* @__PURE__ */ new Set();
|
|
2438
|
+
let timer = 0;
|
|
2439
|
+
const dfs = (u) => {
|
|
2440
|
+
disc.set(u, timer);
|
|
2441
|
+
low.set(u, timer);
|
|
2442
|
+
timer++;
|
|
2443
|
+
let children = 0;
|
|
2444
|
+
for (const v of adj.get(u)) {
|
|
2445
|
+
if (!disc.has(v)) {
|
|
2446
|
+
children++;
|
|
2447
|
+
parent.set(v, u);
|
|
2448
|
+
dfs(v);
|
|
2449
|
+
low.set(u, Math.min(low.get(u), low.get(v)));
|
|
2450
|
+
if (parent.get(u) === null && children > 1) {
|
|
2451
|
+
apSet.add(u);
|
|
2452
|
+
}
|
|
2453
|
+
if (parent.get(u) !== null && low.get(v) >= disc.get(u)) {
|
|
2454
|
+
apSet.add(u);
|
|
2455
|
+
}
|
|
2456
|
+
} else if (v !== parent.get(u)) {
|
|
2457
|
+
low.set(u, Math.min(low.get(u), disc.get(v)));
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
};
|
|
2461
|
+
for (const nodeId of adj.keys()) {
|
|
2462
|
+
if (!disc.has(nodeId)) {
|
|
2463
|
+
parent.set(nodeId, null);
|
|
2464
|
+
dfs(nodeId);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
const results = [];
|
|
2468
|
+
for (const apId of apSet) {
|
|
2469
|
+
const { components, dependentCount } = this.computeRemovalImpact(apId, adj);
|
|
2470
|
+
const info = nodeMap.get(apId);
|
|
2471
|
+
results.push({
|
|
2472
|
+
nodeId: apId,
|
|
2473
|
+
nodeName: info.name,
|
|
2474
|
+
nodePath: info.path,
|
|
2475
|
+
componentsIfRemoved: components,
|
|
2476
|
+
dependentCount
|
|
2477
|
+
});
|
|
2478
|
+
}
|
|
2479
|
+
results.sort((a, b) => b.dependentCount - a.dependentCount);
|
|
2480
|
+
return results;
|
|
2481
|
+
}
|
|
2482
|
+
computeRemovalImpact(removedId, adj) {
|
|
2483
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2484
|
+
visited.add(removedId);
|
|
2485
|
+
const componentSizes = [];
|
|
2486
|
+
for (const nodeId of adj.keys()) {
|
|
2487
|
+
if (visited.has(nodeId)) continue;
|
|
2488
|
+
const queue = [nodeId];
|
|
2489
|
+
visited.add(nodeId);
|
|
2490
|
+
let size = 0;
|
|
2491
|
+
let head = 0;
|
|
2492
|
+
while (head < queue.length) {
|
|
2493
|
+
const current = queue[head++];
|
|
2494
|
+
size++;
|
|
2495
|
+
for (const neighbor of adj.get(current)) {
|
|
2496
|
+
if (!visited.has(neighbor)) {
|
|
2497
|
+
visited.add(neighbor);
|
|
2498
|
+
queue.push(neighbor);
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
componentSizes.push(size);
|
|
2503
|
+
}
|
|
2504
|
+
const components = componentSizes.length;
|
|
2505
|
+
if (componentSizes.length <= 1) {
|
|
2506
|
+
return { components, dependentCount: 0 };
|
|
2507
|
+
}
|
|
2508
|
+
const maxSize = Math.max(...componentSizes);
|
|
2509
|
+
const dependentCount = componentSizes.reduce((sum, s) => sum + s, 0) - maxSize;
|
|
2510
|
+
return { components, dependentCount };
|
|
2511
|
+
}
|
|
2512
|
+
};
|
|
2513
|
+
|
|
2275
2514
|
// src/context/Assembler.ts
|
|
2276
2515
|
var PHASE_NODE_TYPES = {
|
|
2277
2516
|
implement: ["file", "function", "class", "method", "interface", "variable"],
|
|
@@ -2909,6 +3148,7 @@ var VERSION = "0.2.0";
|
|
|
2909
3148
|
EDGE_TYPES,
|
|
2910
3149
|
FusionLayer,
|
|
2911
3150
|
GitIngestor,
|
|
3151
|
+
GraphAnomalyAdapter,
|
|
2912
3152
|
GraphComplexityAdapter,
|
|
2913
3153
|
GraphConstraintAdapter,
|
|
2914
3154
|
GraphCouplingAdapter,
|
package/dist/index.mjs
CHANGED
|
@@ -2205,6 +2205,244 @@ var GraphCouplingAdapter = class {
|
|
|
2205
2205
|
}
|
|
2206
2206
|
};
|
|
2207
2207
|
|
|
2208
|
+
// src/entropy/GraphAnomalyAdapter.ts
|
|
2209
|
+
var DEFAULT_THRESHOLD = 2;
|
|
2210
|
+
var DEFAULT_METRICS = [
|
|
2211
|
+
"cyclomaticComplexity",
|
|
2212
|
+
"fanIn",
|
|
2213
|
+
"fanOut",
|
|
2214
|
+
"hotspotScore",
|
|
2215
|
+
"transitiveDepth"
|
|
2216
|
+
];
|
|
2217
|
+
var RECOGNIZED_METRICS = new Set(DEFAULT_METRICS);
|
|
2218
|
+
var GraphAnomalyAdapter = class {
|
|
2219
|
+
constructor(store) {
|
|
2220
|
+
this.store = store;
|
|
2221
|
+
}
|
|
2222
|
+
detect(options) {
|
|
2223
|
+
const threshold = options?.threshold != null && options.threshold > 0 ? options.threshold : DEFAULT_THRESHOLD;
|
|
2224
|
+
const requestedMetrics = options?.metrics ?? [...DEFAULT_METRICS];
|
|
2225
|
+
const warnings = [];
|
|
2226
|
+
const metricsToAnalyze = [];
|
|
2227
|
+
for (const m of requestedMetrics) {
|
|
2228
|
+
if (RECOGNIZED_METRICS.has(m)) {
|
|
2229
|
+
metricsToAnalyze.push(m);
|
|
2230
|
+
} else {
|
|
2231
|
+
warnings.push(m);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
const allOutliers = [];
|
|
2235
|
+
const analyzedNodeIds = /* @__PURE__ */ new Set();
|
|
2236
|
+
const couplingMetrics = ["fanIn", "fanOut", "transitiveDepth"];
|
|
2237
|
+
const needsCoupling = metricsToAnalyze.some((m) => couplingMetrics.includes(m));
|
|
2238
|
+
const needsComplexity = metricsToAnalyze.includes("hotspotScore");
|
|
2239
|
+
const cachedCouplingData = needsCoupling ? new GraphCouplingAdapter(this.store).computeCouplingData() : void 0;
|
|
2240
|
+
const cachedHotspotData = needsComplexity ? new GraphComplexityAdapter(this.store).computeComplexityHotspots() : void 0;
|
|
2241
|
+
for (const metric of metricsToAnalyze) {
|
|
2242
|
+
const entries = this.collectMetricValues(metric, cachedCouplingData, cachedHotspotData);
|
|
2243
|
+
for (const e of entries) {
|
|
2244
|
+
analyzedNodeIds.add(e.nodeId);
|
|
2245
|
+
}
|
|
2246
|
+
const outliers = this.computeZScoreOutliers(entries, metric, threshold);
|
|
2247
|
+
allOutliers.push(...outliers);
|
|
2248
|
+
}
|
|
2249
|
+
allOutliers.sort((a, b) => b.zScore - a.zScore);
|
|
2250
|
+
const articulationPoints = this.findArticulationPoints();
|
|
2251
|
+
const outlierNodeIds = new Set(allOutliers.map((o) => o.nodeId));
|
|
2252
|
+
const apNodeIds = new Set(articulationPoints.map((ap) => ap.nodeId));
|
|
2253
|
+
const overlapping = [...outlierNodeIds].filter((id) => apNodeIds.has(id));
|
|
2254
|
+
return {
|
|
2255
|
+
statisticalOutliers: allOutliers,
|
|
2256
|
+
articulationPoints,
|
|
2257
|
+
overlapping,
|
|
2258
|
+
summary: {
|
|
2259
|
+
totalNodesAnalyzed: analyzedNodeIds.size,
|
|
2260
|
+
outlierCount: allOutliers.length,
|
|
2261
|
+
articulationPointCount: articulationPoints.length,
|
|
2262
|
+
overlapCount: overlapping.length,
|
|
2263
|
+
metricsAnalyzed: metricsToAnalyze,
|
|
2264
|
+
warnings,
|
|
2265
|
+
threshold
|
|
2266
|
+
}
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
collectMetricValues(metric, cachedCouplingData, cachedHotspotData) {
|
|
2270
|
+
const entries = [];
|
|
2271
|
+
if (metric === "cyclomaticComplexity") {
|
|
2272
|
+
const functionNodes = [
|
|
2273
|
+
...this.store.findNodes({ type: "function" }),
|
|
2274
|
+
...this.store.findNodes({ type: "method" })
|
|
2275
|
+
];
|
|
2276
|
+
for (const node of functionNodes) {
|
|
2277
|
+
const cc = node.metadata?.cyclomaticComplexity;
|
|
2278
|
+
if (typeof cc === "number") {
|
|
2279
|
+
entries.push({
|
|
2280
|
+
nodeId: node.id,
|
|
2281
|
+
nodeName: node.name,
|
|
2282
|
+
nodePath: node.path,
|
|
2283
|
+
nodeType: node.type,
|
|
2284
|
+
value: cc
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
} else if (metric === "fanIn" || metric === "fanOut" || metric === "transitiveDepth") {
|
|
2289
|
+
const couplingData = cachedCouplingData ?? new GraphCouplingAdapter(this.store).computeCouplingData();
|
|
2290
|
+
const fileNodes = this.store.findNodes({ type: "file" });
|
|
2291
|
+
for (const fileData of couplingData.files) {
|
|
2292
|
+
const fileNode = fileNodes.find((n) => (n.path ?? n.name) === fileData.file);
|
|
2293
|
+
if (!fileNode) continue;
|
|
2294
|
+
entries.push({
|
|
2295
|
+
nodeId: fileNode.id,
|
|
2296
|
+
nodeName: fileNode.name,
|
|
2297
|
+
nodePath: fileNode.path,
|
|
2298
|
+
nodeType: "file",
|
|
2299
|
+
value: fileData[metric]
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
} else if (metric === "hotspotScore") {
|
|
2303
|
+
const hotspots = cachedHotspotData ?? new GraphComplexityAdapter(this.store).computeComplexityHotspots();
|
|
2304
|
+
const functionNodes = [
|
|
2305
|
+
...this.store.findNodes({ type: "function" }),
|
|
2306
|
+
...this.store.findNodes({ type: "method" })
|
|
2307
|
+
];
|
|
2308
|
+
for (const h of hotspots.hotspots) {
|
|
2309
|
+
const fnNode = functionNodes.find(
|
|
2310
|
+
(n) => n.name === h.function && (n.path ?? "") === (h.file ?? "")
|
|
2311
|
+
);
|
|
2312
|
+
if (!fnNode) continue;
|
|
2313
|
+
entries.push({
|
|
2314
|
+
nodeId: fnNode.id,
|
|
2315
|
+
nodeName: fnNode.name,
|
|
2316
|
+
nodePath: fnNode.path,
|
|
2317
|
+
nodeType: fnNode.type,
|
|
2318
|
+
value: h.hotspotScore
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
return entries;
|
|
2323
|
+
}
|
|
2324
|
+
computeZScoreOutliers(entries, metric, threshold) {
|
|
2325
|
+
if (entries.length === 0) return [];
|
|
2326
|
+
const values = entries.map((e) => e.value);
|
|
2327
|
+
const mean = values.reduce((sum, v) => sum + v, 0) / values.length;
|
|
2328
|
+
const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;
|
|
2329
|
+
const stdDev = Math.sqrt(variance);
|
|
2330
|
+
if (stdDev === 0) return [];
|
|
2331
|
+
const outliers = [];
|
|
2332
|
+
for (const entry of entries) {
|
|
2333
|
+
const zScore = Math.abs(entry.value - mean) / stdDev;
|
|
2334
|
+
if (zScore > threshold) {
|
|
2335
|
+
outliers.push({
|
|
2336
|
+
nodeId: entry.nodeId,
|
|
2337
|
+
nodeName: entry.nodeName,
|
|
2338
|
+
nodePath: entry.nodePath,
|
|
2339
|
+
nodeType: entry.nodeType,
|
|
2340
|
+
metric,
|
|
2341
|
+
value: entry.value,
|
|
2342
|
+
zScore,
|
|
2343
|
+
mean,
|
|
2344
|
+
stdDev
|
|
2345
|
+
});
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
return outliers;
|
|
2349
|
+
}
|
|
2350
|
+
findArticulationPoints() {
|
|
2351
|
+
const fileNodes = this.store.findNodes({ type: "file" });
|
|
2352
|
+
if (fileNodes.length === 0) return [];
|
|
2353
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
2354
|
+
const adj = /* @__PURE__ */ new Map();
|
|
2355
|
+
for (const node of fileNodes) {
|
|
2356
|
+
nodeMap.set(node.id, { name: node.name, path: node.path });
|
|
2357
|
+
adj.set(node.id, /* @__PURE__ */ new Set());
|
|
2358
|
+
}
|
|
2359
|
+
const importEdges = this.store.getEdges({ type: "imports" });
|
|
2360
|
+
for (const edge of importEdges) {
|
|
2361
|
+
if (adj.has(edge.from) && adj.has(edge.to)) {
|
|
2362
|
+
adj.get(edge.from).add(edge.to);
|
|
2363
|
+
adj.get(edge.to).add(edge.from);
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
const disc = /* @__PURE__ */ new Map();
|
|
2367
|
+
const low = /* @__PURE__ */ new Map();
|
|
2368
|
+
const parent = /* @__PURE__ */ new Map();
|
|
2369
|
+
const apSet = /* @__PURE__ */ new Set();
|
|
2370
|
+
let timer = 0;
|
|
2371
|
+
const dfs = (u) => {
|
|
2372
|
+
disc.set(u, timer);
|
|
2373
|
+
low.set(u, timer);
|
|
2374
|
+
timer++;
|
|
2375
|
+
let children = 0;
|
|
2376
|
+
for (const v of adj.get(u)) {
|
|
2377
|
+
if (!disc.has(v)) {
|
|
2378
|
+
children++;
|
|
2379
|
+
parent.set(v, u);
|
|
2380
|
+
dfs(v);
|
|
2381
|
+
low.set(u, Math.min(low.get(u), low.get(v)));
|
|
2382
|
+
if (parent.get(u) === null && children > 1) {
|
|
2383
|
+
apSet.add(u);
|
|
2384
|
+
}
|
|
2385
|
+
if (parent.get(u) !== null && low.get(v) >= disc.get(u)) {
|
|
2386
|
+
apSet.add(u);
|
|
2387
|
+
}
|
|
2388
|
+
} else if (v !== parent.get(u)) {
|
|
2389
|
+
low.set(u, Math.min(low.get(u), disc.get(v)));
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
};
|
|
2393
|
+
for (const nodeId of adj.keys()) {
|
|
2394
|
+
if (!disc.has(nodeId)) {
|
|
2395
|
+
parent.set(nodeId, null);
|
|
2396
|
+
dfs(nodeId);
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
const results = [];
|
|
2400
|
+
for (const apId of apSet) {
|
|
2401
|
+
const { components, dependentCount } = this.computeRemovalImpact(apId, adj);
|
|
2402
|
+
const info = nodeMap.get(apId);
|
|
2403
|
+
results.push({
|
|
2404
|
+
nodeId: apId,
|
|
2405
|
+
nodeName: info.name,
|
|
2406
|
+
nodePath: info.path,
|
|
2407
|
+
componentsIfRemoved: components,
|
|
2408
|
+
dependentCount
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
results.sort((a, b) => b.dependentCount - a.dependentCount);
|
|
2412
|
+
return results;
|
|
2413
|
+
}
|
|
2414
|
+
computeRemovalImpact(removedId, adj) {
|
|
2415
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2416
|
+
visited.add(removedId);
|
|
2417
|
+
const componentSizes = [];
|
|
2418
|
+
for (const nodeId of adj.keys()) {
|
|
2419
|
+
if (visited.has(nodeId)) continue;
|
|
2420
|
+
const queue = [nodeId];
|
|
2421
|
+
visited.add(nodeId);
|
|
2422
|
+
let size = 0;
|
|
2423
|
+
let head = 0;
|
|
2424
|
+
while (head < queue.length) {
|
|
2425
|
+
const current = queue[head++];
|
|
2426
|
+
size++;
|
|
2427
|
+
for (const neighbor of adj.get(current)) {
|
|
2428
|
+
if (!visited.has(neighbor)) {
|
|
2429
|
+
visited.add(neighbor);
|
|
2430
|
+
queue.push(neighbor);
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
componentSizes.push(size);
|
|
2435
|
+
}
|
|
2436
|
+
const components = componentSizes.length;
|
|
2437
|
+
if (componentSizes.length <= 1) {
|
|
2438
|
+
return { components, dependentCount: 0 };
|
|
2439
|
+
}
|
|
2440
|
+
const maxSize = Math.max(...componentSizes);
|
|
2441
|
+
const dependentCount = componentSizes.reduce((sum, s) => sum + s, 0) - maxSize;
|
|
2442
|
+
return { components, dependentCount };
|
|
2443
|
+
}
|
|
2444
|
+
};
|
|
2445
|
+
|
|
2208
2446
|
// src/context/Assembler.ts
|
|
2209
2447
|
var PHASE_NODE_TYPES = {
|
|
2210
2448
|
implement: ["file", "function", "class", "method", "interface", "variable"],
|
|
@@ -2841,6 +3079,7 @@ export {
|
|
|
2841
3079
|
EDGE_TYPES,
|
|
2842
3080
|
FusionLayer,
|
|
2843
3081
|
GitIngestor,
|
|
3082
|
+
GraphAnomalyAdapter,
|
|
2844
3083
|
GraphComplexityAdapter,
|
|
2845
3084
|
GraphConstraintAdapter,
|
|
2846
3085
|
GraphCouplingAdapter,
|