@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 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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-engineering/graph",
3
- "version": "0.2.3",
3
+ "version": "0.3.1",
4
4
  "license": "MIT",
5
5
  "description": "Knowledge graph for context assembly in Harness Engineering",
6
6
  "main": "./dist/index.js",