@harness-engineering/graph 0.2.2 → 0.3.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/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,
@@ -550,7 +551,7 @@ var CodeIngestor = class {
550
551
  const fileContents = /* @__PURE__ */ new Map();
551
552
  for (const filePath of files) {
552
553
  try {
553
- const relativePath = path.relative(rootDir, filePath);
554
+ const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
554
555
  const content = await fs.readFile(filePath, "utf-8");
555
556
  const stat2 = await fs.stat(filePath);
556
557
  const fileId = `file:${relativePath}`;
@@ -840,7 +841,7 @@ var CodeIngestor = class {
840
841
  }
841
842
  async resolveImportPath(fromFile, importPath, rootDir) {
842
843
  const fromDir = path.dirname(fromFile);
843
- const resolved = path.normalize(path.join(fromDir, importPath));
844
+ const resolved = path.normalize(path.join(fromDir, importPath)).replace(/\\/g, "/");
844
845
  const extensions = [".ts", ".tsx", ".js", ".jsx"];
845
846
  for (const ext of extensions) {
846
847
  const candidate = resolved.replace(/\.js$/, "") + ext;
@@ -852,7 +853,7 @@ var CodeIngestor = class {
852
853
  }
853
854
  }
854
855
  for (const ext of extensions) {
855
- const candidate = path.join(resolved, `index${ext}`);
856
+ const candidate = path.join(resolved, `index${ext}`).replace(/\\/g, "/");
856
857
  const fullPath = path.join(rootDir, candidate);
857
858
  try {
858
859
  await fs.access(fullPath);
@@ -1172,7 +1173,7 @@ var path3 = __toESM(require("path"));
1172
1173
  // src/ingest/ingestUtils.ts
1173
1174
  var crypto = __toESM(require("crypto"));
1174
1175
  function hash(text) {
1175
- return crypto.createHash("md5").update(text).digest("hex").slice(0, 8);
1176
+ return crypto.createHash("sha256").update(text).digest("hex").slice(0, 8);
1176
1177
  }
1177
1178
  function mergeResults(...results) {
1178
1179
  return {
@@ -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
@@ -483,7 +483,7 @@ var CodeIngestor = class {
483
483
  const fileContents = /* @__PURE__ */ new Map();
484
484
  for (const filePath of files) {
485
485
  try {
486
- const relativePath = path.relative(rootDir, filePath);
486
+ const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
487
487
  const content = await fs.readFile(filePath, "utf-8");
488
488
  const stat2 = await fs.stat(filePath);
489
489
  const fileId = `file:${relativePath}`;
@@ -773,7 +773,7 @@ var CodeIngestor = class {
773
773
  }
774
774
  async resolveImportPath(fromFile, importPath, rootDir) {
775
775
  const fromDir = path.dirname(fromFile);
776
- const resolved = path.normalize(path.join(fromDir, importPath));
776
+ const resolved = path.normalize(path.join(fromDir, importPath)).replace(/\\/g, "/");
777
777
  const extensions = [".ts", ".tsx", ".js", ".jsx"];
778
778
  for (const ext of extensions) {
779
779
  const candidate = resolved.replace(/\.js$/, "") + ext;
@@ -785,7 +785,7 @@ var CodeIngestor = class {
785
785
  }
786
786
  }
787
787
  for (const ext of extensions) {
788
- const candidate = path.join(resolved, `index${ext}`);
788
+ const candidate = path.join(resolved, `index${ext}`).replace(/\\/g, "/");
789
789
  const fullPath = path.join(rootDir, candidate);
790
790
  try {
791
791
  await fs.access(fullPath);
@@ -1105,7 +1105,7 @@ import * as path3 from "path";
1105
1105
  // src/ingest/ingestUtils.ts
1106
1106
  import * as crypto from "crypto";
1107
1107
  function hash(text) {
1108
- return crypto.createHash("md5").update(text).digest("hex").slice(0, 8);
1108
+ return crypto.createHash("sha256").update(text).digest("hex").slice(0, 8);
1109
1109
  }
1110
1110
  function mergeResults(...results) {
1111
1111
  return {
@@ -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.2",
3
+ "version": "0.3.0",
4
4
  "license": "MIT",
5
5
  "description": "Knowledge graph for context assembly in Harness Engineering",
6
6
  "main": "./dist/index.js",
@@ -21,7 +21,7 @@
21
21
  "lokijs": "^1.5.12",
22
22
  "minimatch": "^10.2.4",
23
23
  "zod": "^3.24.1",
24
- "@harness-engineering/types": "0.1.0"
24
+ "@harness-engineering/types": "0.2.0"
25
25
  },
26
26
  "optionalDependencies": {
27
27
  "hnswlib-node": "^3.0.0",
@@ -52,7 +52,7 @@
52
52
  "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
53
53
  "lint": "eslint src",
54
54
  "typecheck": "tsc --noEmit",
55
- "clean": "rm -rf dist",
55
+ "clean": "node ../../scripts/clean.mjs dist",
56
56
  "test": "vitest run",
57
57
  "test:watch": "vitest",
58
58
  "test:coverage": "vitest run --coverage"