@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 +47 -1
- package/dist/index.d.ts +47 -1
- package/dist/index.js +244 -4
- package/dist/index.mjs +243 -4
- package/package.json +3 -3
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("
|
|
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("
|
|
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.
|
|
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.
|
|
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": "
|
|
55
|
+
"clean": "node ../../scripts/clean.mjs dist",
|
|
56
56
|
"test": "vitest run",
|
|
57
57
|
"test:watch": "vitest",
|
|
58
58
|
"test:coverage": "vitest run --coverage"
|