@harness-engineering/graph 0.4.0 → 0.4.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/README.md +31 -25
- package/dist/index.d.mts +92 -2
- package/dist/index.d.ts +92 -2
- package/dist/index.js +350 -73
- package/dist/index.mjs +347 -73
- package/package.json +2 -3
package/dist/index.js
CHANGED
|
@@ -33,7 +33,9 @@ __export(index_exports, {
|
|
|
33
33
|
Assembler: () => Assembler,
|
|
34
34
|
CIConnector: () => CIConnector,
|
|
35
35
|
CURRENT_SCHEMA_VERSION: () => CURRENT_SCHEMA_VERSION,
|
|
36
|
+
CascadeSimulator: () => CascadeSimulator,
|
|
36
37
|
CodeIngestor: () => CodeIngestor,
|
|
38
|
+
CompositeProbabilityStrategy: () => CompositeProbabilityStrategy,
|
|
37
39
|
ConflictPredictor: () => ConflictPredictor,
|
|
38
40
|
ConfluenceConnector: () => ConfluenceConnector,
|
|
39
41
|
ContextQL: () => ContextQL,
|
|
@@ -68,6 +70,7 @@ __export(index_exports, {
|
|
|
68
70
|
VERSION: () => VERSION,
|
|
69
71
|
VectorStore: () => VectorStore,
|
|
70
72
|
askGraph: () => askGraph,
|
|
73
|
+
classifyNodeCategory: () => classifyNodeCategory,
|
|
71
74
|
groupNodesByImpact: () => groupNodesByImpact,
|
|
72
75
|
linkToCode: () => linkToCode,
|
|
73
76
|
loadGraph: () => loadGraph,
|
|
@@ -241,6 +244,16 @@ function removeFromIndex(index, key, edge) {
|
|
|
241
244
|
if (idx !== -1) list.splice(idx, 1);
|
|
242
245
|
if (list.length === 0) index.delete(key);
|
|
243
246
|
}
|
|
247
|
+
function filterEdges(candidates, query) {
|
|
248
|
+
const results = [];
|
|
249
|
+
for (const edge of candidates) {
|
|
250
|
+
if (query.from !== void 0 && edge.from !== query.from) continue;
|
|
251
|
+
if (query.to !== void 0 && edge.to !== query.to) continue;
|
|
252
|
+
if (query.type !== void 0 && edge.type !== query.type) continue;
|
|
253
|
+
results.push({ ...edge });
|
|
254
|
+
}
|
|
255
|
+
return results;
|
|
256
|
+
}
|
|
244
257
|
var GraphStore = class {
|
|
245
258
|
nodeMap = /* @__PURE__ */ new Map();
|
|
246
259
|
edgeMap = /* @__PURE__ */ new Map();
|
|
@@ -308,27 +321,25 @@ var GraphStore = class {
|
|
|
308
321
|
}
|
|
309
322
|
}
|
|
310
323
|
getEdges(query) {
|
|
311
|
-
let candidates;
|
|
312
324
|
if (query.from !== void 0 && query.to !== void 0 && query.type !== void 0) {
|
|
313
325
|
const edge = this.edgeMap.get(edgeKey(query.from, query.to, query.type));
|
|
314
326
|
return edge ? [{ ...edge }] : [];
|
|
315
|
-
} else if (query.from !== void 0) {
|
|
316
|
-
candidates = this.edgesByFrom.get(query.from) ?? [];
|
|
317
|
-
} else if (query.to !== void 0) {
|
|
318
|
-
candidates = this.edgesByTo.get(query.to) ?? [];
|
|
319
|
-
} else if (query.type !== void 0) {
|
|
320
|
-
candidates = this.edgesByType.get(query.type) ?? [];
|
|
321
|
-
} else {
|
|
322
|
-
candidates = this.edgeMap.values();
|
|
323
327
|
}
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
328
|
+
const candidates = this.selectCandidates(query);
|
|
329
|
+
return filterEdges(candidates, query);
|
|
330
|
+
}
|
|
331
|
+
/** Pick the most selective index to start from. */
|
|
332
|
+
selectCandidates(query) {
|
|
333
|
+
if (query.from !== void 0) {
|
|
334
|
+
return this.edgesByFrom.get(query.from) ?? [];
|
|
330
335
|
}
|
|
331
|
-
|
|
336
|
+
if (query.to !== void 0) {
|
|
337
|
+
return this.edgesByTo.get(query.to) ?? [];
|
|
338
|
+
}
|
|
339
|
+
if (query.type !== void 0) {
|
|
340
|
+
return this.edgesByType.get(query.type) ?? [];
|
|
341
|
+
}
|
|
342
|
+
return this.edgeMap.values();
|
|
332
343
|
}
|
|
333
344
|
getNeighbors(nodeId, direction = "both") {
|
|
334
345
|
const neighborIds = /* @__PURE__ */ new Set();
|
|
@@ -624,6 +635,12 @@ var CODE_TYPES = /* @__PURE__ */ new Set([
|
|
|
624
635
|
"method",
|
|
625
636
|
"variable"
|
|
626
637
|
]);
|
|
638
|
+
function classifyNodeCategory(node) {
|
|
639
|
+
if (TEST_TYPES.has(node.type)) return "tests";
|
|
640
|
+
if (DOC_TYPES.has(node.type)) return "docs";
|
|
641
|
+
if (CODE_TYPES.has(node.type)) return "code";
|
|
642
|
+
return "other";
|
|
643
|
+
}
|
|
627
644
|
function groupNodesByImpact(nodes, excludeId) {
|
|
628
645
|
const tests = [];
|
|
629
646
|
const docs = [];
|
|
@@ -631,15 +648,11 @@ function groupNodesByImpact(nodes, excludeId) {
|
|
|
631
648
|
const other = [];
|
|
632
649
|
for (const node of nodes) {
|
|
633
650
|
if (excludeId && node.id === excludeId) continue;
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
code.push(node);
|
|
640
|
-
} else {
|
|
641
|
-
other.push(node);
|
|
642
|
-
}
|
|
651
|
+
const category = classifyNodeCategory(node);
|
|
652
|
+
if (category === "tests") tests.push(node);
|
|
653
|
+
else if (category === "docs") docs.push(node);
|
|
654
|
+
else if (category === "code") code.push(node);
|
|
655
|
+
else other.push(node);
|
|
643
656
|
}
|
|
644
657
|
return { tests, docs, code, other };
|
|
645
658
|
}
|
|
@@ -1604,7 +1617,7 @@ var RequirementIngestor = class {
|
|
|
1604
1617
|
}
|
|
1605
1618
|
for (const featureDir of featureDirs) {
|
|
1606
1619
|
const featureName = path4.basename(featureDir);
|
|
1607
|
-
const specPath = path4.join(featureDir, "proposal.md");
|
|
1620
|
+
const specPath = path4.join(featureDir, "proposal.md").replaceAll("\\", "/");
|
|
1608
1621
|
let content;
|
|
1609
1622
|
try {
|
|
1610
1623
|
content = await fs3.readFile(specPath, "utf-8");
|
|
@@ -1754,7 +1767,7 @@ var RequirementIngestor = class {
|
|
|
1754
1767
|
const escaped = node.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1755
1768
|
const namePattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
1756
1769
|
if (namePattern.test(reqText)) {
|
|
1757
|
-
const edgeType = node.path
|
|
1770
|
+
const edgeType = node.path?.replace(/\\/g, "/").includes("/tests/") ? "verified_by" : "requires";
|
|
1758
1771
|
this.store.addEdge({
|
|
1759
1772
|
from: reqId,
|
|
1760
1773
|
to: node.id,
|
|
@@ -2971,6 +2984,7 @@ var INTENT_SIGNALS = {
|
|
|
2971
2984
|
"depend",
|
|
2972
2985
|
"blast",
|
|
2973
2986
|
"radius",
|
|
2987
|
+
"cascade",
|
|
2974
2988
|
"risk",
|
|
2975
2989
|
"delete",
|
|
2976
2990
|
"remove"
|
|
@@ -2980,6 +2994,7 @@ var INTENT_SIGNALS = {
|
|
|
2980
2994
|
/what\s+(breaks|happens|is affected)/,
|
|
2981
2995
|
/if\s+i\s+(change|modify|remove|delete)/,
|
|
2982
2996
|
/blast\s+radius/,
|
|
2997
|
+
/cascad/,
|
|
2983
2998
|
/what\s+(depend|relies)/
|
|
2984
2999
|
]
|
|
2985
3000
|
},
|
|
@@ -3465,6 +3480,10 @@ var ResponseFormatter = class {
|
|
|
3465
3480
|
}
|
|
3466
3481
|
formatImpact(entityName, data) {
|
|
3467
3482
|
const d = data;
|
|
3483
|
+
if ("sourceNodeId" in d && "summary" in d) {
|
|
3484
|
+
const summary = d.summary;
|
|
3485
|
+
return `Blast radius of **${entityName}**: ${summary.totalAffected} affected nodes (${summary.highRisk} high risk, ${summary.mediumRisk} medium, ${summary.lowRisk} low).`;
|
|
3486
|
+
}
|
|
3468
3487
|
const code = this.safeArrayLength(d?.code);
|
|
3469
3488
|
const tests = this.safeArrayLength(d?.tests);
|
|
3470
3489
|
const docs = this.safeArrayLength(d?.docs);
|
|
@@ -3526,6 +3545,246 @@ var ResponseFormatter = class {
|
|
|
3526
3545
|
}
|
|
3527
3546
|
};
|
|
3528
3547
|
|
|
3548
|
+
// src/blast-radius/CompositeProbabilityStrategy.ts
|
|
3549
|
+
var CompositeProbabilityStrategy = class _CompositeProbabilityStrategy {
|
|
3550
|
+
constructor(changeFreqMap, couplingMap) {
|
|
3551
|
+
this.changeFreqMap = changeFreqMap;
|
|
3552
|
+
this.couplingMap = couplingMap;
|
|
3553
|
+
}
|
|
3554
|
+
changeFreqMap;
|
|
3555
|
+
couplingMap;
|
|
3556
|
+
static BASE_WEIGHTS = {
|
|
3557
|
+
imports: 0.7,
|
|
3558
|
+
calls: 0.5,
|
|
3559
|
+
implements: 0.6,
|
|
3560
|
+
inherits: 0.6,
|
|
3561
|
+
co_changes_with: 0.4,
|
|
3562
|
+
references: 0.2,
|
|
3563
|
+
contains: 0.3
|
|
3564
|
+
};
|
|
3565
|
+
static FALLBACK_WEIGHT = 0.1;
|
|
3566
|
+
static EDGE_TYPE_BLEND = 0.5;
|
|
3567
|
+
static CHANGE_FREQ_BLEND = 0.3;
|
|
3568
|
+
static COUPLING_BLEND = 0.2;
|
|
3569
|
+
getEdgeProbability(edge, _fromNode, toNode) {
|
|
3570
|
+
const base = _CompositeProbabilityStrategy.BASE_WEIGHTS[edge.type] ?? _CompositeProbabilityStrategy.FALLBACK_WEIGHT;
|
|
3571
|
+
const changeFreq = this.changeFreqMap.get(toNode.id) ?? 0;
|
|
3572
|
+
const coupling = this.couplingMap.get(toNode.id) ?? 0;
|
|
3573
|
+
return Math.min(
|
|
3574
|
+
1,
|
|
3575
|
+
base * _CompositeProbabilityStrategy.EDGE_TYPE_BLEND + changeFreq * _CompositeProbabilityStrategy.CHANGE_FREQ_BLEND + coupling * _CompositeProbabilityStrategy.COUPLING_BLEND
|
|
3576
|
+
);
|
|
3577
|
+
}
|
|
3578
|
+
};
|
|
3579
|
+
|
|
3580
|
+
// src/blast-radius/CascadeSimulator.ts
|
|
3581
|
+
var DEFAULT_PROBABILITY_FLOOR = 0.05;
|
|
3582
|
+
var DEFAULT_MAX_DEPTH = 10;
|
|
3583
|
+
var CascadeSimulator = class {
|
|
3584
|
+
constructor(store) {
|
|
3585
|
+
this.store = store;
|
|
3586
|
+
}
|
|
3587
|
+
store;
|
|
3588
|
+
simulate(sourceNodeId, options = {}) {
|
|
3589
|
+
const sourceNode = this.store.getNode(sourceNodeId);
|
|
3590
|
+
if (!sourceNode) {
|
|
3591
|
+
throw new Error(`Node not found: ${sourceNodeId}. Ensure the file has been ingested.`);
|
|
3592
|
+
}
|
|
3593
|
+
const probabilityFloor = options.probabilityFloor ?? DEFAULT_PROBABILITY_FLOOR;
|
|
3594
|
+
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
3595
|
+
const edgeTypeFilter = options.edgeTypes ? new Set(options.edgeTypes) : null;
|
|
3596
|
+
const strategy = options.strategy ?? this.buildDefaultStrategy();
|
|
3597
|
+
const visited = /* @__PURE__ */ new Map();
|
|
3598
|
+
const queue = [];
|
|
3599
|
+
const fanOutCount = /* @__PURE__ */ new Map();
|
|
3600
|
+
this.seedQueue(
|
|
3601
|
+
sourceNodeId,
|
|
3602
|
+
sourceNode,
|
|
3603
|
+
strategy,
|
|
3604
|
+
edgeTypeFilter,
|
|
3605
|
+
probabilityFloor,
|
|
3606
|
+
queue,
|
|
3607
|
+
fanOutCount
|
|
3608
|
+
);
|
|
3609
|
+
const truncated = this.runBfs(
|
|
3610
|
+
queue,
|
|
3611
|
+
visited,
|
|
3612
|
+
fanOutCount,
|
|
3613
|
+
sourceNodeId,
|
|
3614
|
+
strategy,
|
|
3615
|
+
edgeTypeFilter,
|
|
3616
|
+
probabilityFloor,
|
|
3617
|
+
maxDepth
|
|
3618
|
+
);
|
|
3619
|
+
return this.buildResult(sourceNodeId, sourceNode.name, visited, fanOutCount, truncated);
|
|
3620
|
+
}
|
|
3621
|
+
seedQueue(sourceNodeId, sourceNode, strategy, edgeTypeFilter, probabilityFloor, queue, fanOutCount) {
|
|
3622
|
+
const sourceEdges = this.store.getEdges({ from: sourceNodeId });
|
|
3623
|
+
for (const edge of sourceEdges) {
|
|
3624
|
+
if (edge.to === sourceNodeId) continue;
|
|
3625
|
+
if (edgeTypeFilter && !edgeTypeFilter.has(edge.type)) continue;
|
|
3626
|
+
const targetNode = this.store.getNode(edge.to);
|
|
3627
|
+
if (!targetNode) continue;
|
|
3628
|
+
const cumProb = strategy.getEdgeProbability(edge, sourceNode, targetNode);
|
|
3629
|
+
if (cumProb < probabilityFloor) continue;
|
|
3630
|
+
queue.push({
|
|
3631
|
+
nodeId: edge.to,
|
|
3632
|
+
cumProb,
|
|
3633
|
+
depth: 1,
|
|
3634
|
+
parentId: sourceNodeId,
|
|
3635
|
+
incomingEdge: edge.type
|
|
3636
|
+
});
|
|
3637
|
+
}
|
|
3638
|
+
fanOutCount.set(
|
|
3639
|
+
sourceNodeId,
|
|
3640
|
+
sourceEdges.filter(
|
|
3641
|
+
(e) => e.to !== sourceNodeId && (!edgeTypeFilter || edgeTypeFilter.has(e.type))
|
|
3642
|
+
).length
|
|
3643
|
+
);
|
|
3644
|
+
}
|
|
3645
|
+
runBfs(queue, visited, fanOutCount, sourceNodeId, strategy, edgeTypeFilter, probabilityFloor, maxDepth) {
|
|
3646
|
+
const MAX_QUEUE_SIZE = 1e4;
|
|
3647
|
+
let head = 0;
|
|
3648
|
+
while (head < queue.length) {
|
|
3649
|
+
if (queue.length > MAX_QUEUE_SIZE) return true;
|
|
3650
|
+
const entry = queue[head++];
|
|
3651
|
+
const existing = visited.get(entry.nodeId);
|
|
3652
|
+
if (existing && existing.cumulativeProbability >= entry.cumProb) continue;
|
|
3653
|
+
const targetNode = this.store.getNode(entry.nodeId);
|
|
3654
|
+
if (!targetNode) continue;
|
|
3655
|
+
visited.set(entry.nodeId, {
|
|
3656
|
+
nodeId: entry.nodeId,
|
|
3657
|
+
name: targetNode.name,
|
|
3658
|
+
...targetNode.path !== void 0 && { path: targetNode.path },
|
|
3659
|
+
type: targetNode.type,
|
|
3660
|
+
cumulativeProbability: entry.cumProb,
|
|
3661
|
+
depth: entry.depth,
|
|
3662
|
+
incomingEdge: entry.incomingEdge,
|
|
3663
|
+
parentId: entry.parentId
|
|
3664
|
+
});
|
|
3665
|
+
if (entry.depth < maxDepth) {
|
|
3666
|
+
const childCount = this.expandNode(
|
|
3667
|
+
entry,
|
|
3668
|
+
targetNode,
|
|
3669
|
+
sourceNodeId,
|
|
3670
|
+
strategy,
|
|
3671
|
+
edgeTypeFilter,
|
|
3672
|
+
probabilityFloor,
|
|
3673
|
+
queue
|
|
3674
|
+
);
|
|
3675
|
+
fanOutCount.set(entry.nodeId, (fanOutCount.get(entry.nodeId) ?? 0) + childCount);
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
return false;
|
|
3679
|
+
}
|
|
3680
|
+
expandNode(entry, fromNode, sourceNodeId, strategy, edgeTypeFilter, probabilityFloor, queue) {
|
|
3681
|
+
const outEdges = this.store.getEdges({ from: entry.nodeId });
|
|
3682
|
+
let childCount = 0;
|
|
3683
|
+
for (const edge of outEdges) {
|
|
3684
|
+
if (edgeTypeFilter && !edgeTypeFilter.has(edge.type)) continue;
|
|
3685
|
+
if (edge.to === sourceNodeId) continue;
|
|
3686
|
+
const childNode = this.store.getNode(edge.to);
|
|
3687
|
+
if (!childNode) continue;
|
|
3688
|
+
const newCumProb = entry.cumProb * strategy.getEdgeProbability(edge, fromNode, childNode);
|
|
3689
|
+
if (newCumProb < probabilityFloor) continue;
|
|
3690
|
+
childCount++;
|
|
3691
|
+
queue.push({
|
|
3692
|
+
nodeId: edge.to,
|
|
3693
|
+
cumProb: newCumProb,
|
|
3694
|
+
depth: entry.depth + 1,
|
|
3695
|
+
parentId: entry.nodeId,
|
|
3696
|
+
incomingEdge: edge.type
|
|
3697
|
+
});
|
|
3698
|
+
}
|
|
3699
|
+
return childCount;
|
|
3700
|
+
}
|
|
3701
|
+
buildDefaultStrategy() {
|
|
3702
|
+
return new CompositeProbabilityStrategy(/* @__PURE__ */ new Map(), /* @__PURE__ */ new Map());
|
|
3703
|
+
}
|
|
3704
|
+
buildResult(sourceNodeId, sourceName, visited, fanOutCount, truncated = false) {
|
|
3705
|
+
if (visited.size === 0) {
|
|
3706
|
+
return {
|
|
3707
|
+
sourceNodeId,
|
|
3708
|
+
sourceName,
|
|
3709
|
+
layers: [],
|
|
3710
|
+
flatSummary: [],
|
|
3711
|
+
summary: {
|
|
3712
|
+
totalAffected: 0,
|
|
3713
|
+
maxDepthReached: 0,
|
|
3714
|
+
highRisk: 0,
|
|
3715
|
+
mediumRisk: 0,
|
|
3716
|
+
lowRisk: 0,
|
|
3717
|
+
categoryBreakdown: { code: 0, tests: 0, docs: 0, other: 0 },
|
|
3718
|
+
amplificationPoints: [],
|
|
3719
|
+
truncated
|
|
3720
|
+
}
|
|
3721
|
+
};
|
|
3722
|
+
}
|
|
3723
|
+
const allNodes = Array.from(visited.values());
|
|
3724
|
+
const flatSummary = [...allNodes].sort(
|
|
3725
|
+
(a, b) => b.cumulativeProbability - a.cumulativeProbability
|
|
3726
|
+
);
|
|
3727
|
+
const depthMap = /* @__PURE__ */ new Map();
|
|
3728
|
+
for (const node of allNodes) {
|
|
3729
|
+
let list = depthMap.get(node.depth);
|
|
3730
|
+
if (!list) {
|
|
3731
|
+
list = [];
|
|
3732
|
+
depthMap.set(node.depth, list);
|
|
3733
|
+
}
|
|
3734
|
+
list.push(node);
|
|
3735
|
+
}
|
|
3736
|
+
const layers = [];
|
|
3737
|
+
const depths = Array.from(depthMap.keys()).sort((a, b) => a - b);
|
|
3738
|
+
for (const depth of depths) {
|
|
3739
|
+
const nodes = depthMap.get(depth);
|
|
3740
|
+
const breakdown = { code: 0, tests: 0, docs: 0, other: 0 };
|
|
3741
|
+
for (const n of nodes) {
|
|
3742
|
+
const graphNode = this.store.getNode(n.nodeId);
|
|
3743
|
+
if (graphNode) {
|
|
3744
|
+
breakdown[classifyNodeCategory(graphNode)]++;
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
layers.push({ depth, nodes, categoryBreakdown: breakdown });
|
|
3748
|
+
}
|
|
3749
|
+
let highRisk = 0;
|
|
3750
|
+
let mediumRisk = 0;
|
|
3751
|
+
let lowRisk = 0;
|
|
3752
|
+
const catBreakdown = { code: 0, tests: 0, docs: 0, other: 0 };
|
|
3753
|
+
for (const node of allNodes) {
|
|
3754
|
+
if (node.cumulativeProbability >= 0.5) highRisk++;
|
|
3755
|
+
else if (node.cumulativeProbability >= 0.2) mediumRisk++;
|
|
3756
|
+
else lowRisk++;
|
|
3757
|
+
const graphNode = this.store.getNode(node.nodeId);
|
|
3758
|
+
if (graphNode) {
|
|
3759
|
+
catBreakdown[classifyNodeCategory(graphNode)]++;
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
const amplificationPoints = [];
|
|
3763
|
+
for (const [nodeId, count] of fanOutCount) {
|
|
3764
|
+
if (count > 3) {
|
|
3765
|
+
amplificationPoints.push(nodeId);
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
const maxDepthReached = allNodes.reduce((max, n) => Math.max(max, n.depth), 0);
|
|
3769
|
+
return {
|
|
3770
|
+
sourceNodeId,
|
|
3771
|
+
sourceName,
|
|
3772
|
+
layers,
|
|
3773
|
+
flatSummary,
|
|
3774
|
+
summary: {
|
|
3775
|
+
totalAffected: allNodes.length,
|
|
3776
|
+
maxDepthReached,
|
|
3777
|
+
highRisk,
|
|
3778
|
+
mediumRisk,
|
|
3779
|
+
lowRisk,
|
|
3780
|
+
categoryBreakdown: catBreakdown,
|
|
3781
|
+
amplificationPoints,
|
|
3782
|
+
truncated
|
|
3783
|
+
}
|
|
3784
|
+
};
|
|
3785
|
+
}
|
|
3786
|
+
};
|
|
3787
|
+
|
|
3529
3788
|
// src/nlq/index.ts
|
|
3530
3789
|
var ENTITY_REQUIRED_INTENTS = /* @__PURE__ */ new Set(["impact", "relationships", "explain"]);
|
|
3531
3790
|
var classifier = new IntentClassifier();
|
|
@@ -3588,6 +3847,11 @@ function executeOperation(store, intent, entities, question, fusion) {
|
|
|
3588
3847
|
switch (intent) {
|
|
3589
3848
|
case "impact": {
|
|
3590
3849
|
const rootId = entities[0].nodeId;
|
|
3850
|
+
const lowerQuestion = question.toLowerCase();
|
|
3851
|
+
if (lowerQuestion.includes("blast radius") || lowerQuestion.includes("cascade")) {
|
|
3852
|
+
const simulator = new CascadeSimulator(store);
|
|
3853
|
+
return simulator.simulate(rootId);
|
|
3854
|
+
}
|
|
3591
3855
|
const result = cql.execute({
|
|
3592
3856
|
rootNodeIds: [rootId],
|
|
3593
3857
|
bidirectional: true,
|
|
@@ -3882,6 +4146,59 @@ var Assembler = class {
|
|
|
3882
4146
|
};
|
|
3883
4147
|
|
|
3884
4148
|
// src/query/Traceability.ts
|
|
4149
|
+
function extractConfidence(edge) {
|
|
4150
|
+
return edge.confidence ?? edge.metadata?.confidence ?? 0;
|
|
4151
|
+
}
|
|
4152
|
+
function extractMethod(edge) {
|
|
4153
|
+
return edge.metadata?.method ?? "convention";
|
|
4154
|
+
}
|
|
4155
|
+
function edgesToTracedFiles(store, edges) {
|
|
4156
|
+
return edges.map((edge) => ({
|
|
4157
|
+
path: store.getNode(edge.to)?.path ?? edge.to,
|
|
4158
|
+
confidence: extractConfidence(edge),
|
|
4159
|
+
method: extractMethod(edge)
|
|
4160
|
+
}));
|
|
4161
|
+
}
|
|
4162
|
+
function determineCoverageStatus(hasCode, hasTests) {
|
|
4163
|
+
if (hasCode && hasTests) return "full";
|
|
4164
|
+
if (hasCode) return "code-only";
|
|
4165
|
+
if (hasTests) return "test-only";
|
|
4166
|
+
return "none";
|
|
4167
|
+
}
|
|
4168
|
+
function computeMaxConfidence(codeFiles, testFiles) {
|
|
4169
|
+
const allConfidences = [
|
|
4170
|
+
...codeFiles.map((f) => f.confidence),
|
|
4171
|
+
...testFiles.map((f) => f.confidence)
|
|
4172
|
+
];
|
|
4173
|
+
return allConfidences.length > 0 ? Math.max(...allConfidences) : 0;
|
|
4174
|
+
}
|
|
4175
|
+
function buildRequirementCoverage(store, req) {
|
|
4176
|
+
const codeFiles = edgesToTracedFiles(store, store.getEdges({ from: req.id, type: "requires" }));
|
|
4177
|
+
const testFiles = edgesToTracedFiles(
|
|
4178
|
+
store,
|
|
4179
|
+
store.getEdges({ from: req.id, type: "verified_by" })
|
|
4180
|
+
);
|
|
4181
|
+
const hasCode = codeFiles.length > 0;
|
|
4182
|
+
const hasTests = testFiles.length > 0;
|
|
4183
|
+
return {
|
|
4184
|
+
requirementId: req.id,
|
|
4185
|
+
requirementName: req.name,
|
|
4186
|
+
index: req.metadata?.index ?? 0,
|
|
4187
|
+
codeFiles,
|
|
4188
|
+
testFiles,
|
|
4189
|
+
status: determineCoverageStatus(hasCode, hasTests),
|
|
4190
|
+
maxConfidence: computeMaxConfidence(codeFiles, testFiles)
|
|
4191
|
+
};
|
|
4192
|
+
}
|
|
4193
|
+
function computeSummary(requirements) {
|
|
4194
|
+
const total = requirements.length;
|
|
4195
|
+
const withCode = requirements.filter((r) => r.codeFiles.length > 0).length;
|
|
4196
|
+
const withTests = requirements.filter((r) => r.testFiles.length > 0).length;
|
|
4197
|
+
const fullyTraced = requirements.filter((r) => r.status === "full").length;
|
|
4198
|
+
const untraceable = requirements.filter((r) => r.status === "none").length;
|
|
4199
|
+
const coveragePercent = total > 0 ? Math.round(fullyTraced / total * 100) : 0;
|
|
4200
|
+
return { total, withCode, withTests, fullyTraced, untraceable, coveragePercent };
|
|
4201
|
+
}
|
|
3885
4202
|
function queryTraceability(store, options) {
|
|
3886
4203
|
const allRequirements = store.findNodes({ type: "requirement" });
|
|
3887
4204
|
const filtered = allRequirements.filter((node) => {
|
|
@@ -3909,56 +4226,13 @@ function queryTraceability(store, options) {
|
|
|
3909
4226
|
const firstMeta = firstReq.metadata;
|
|
3910
4227
|
const specPath = firstMeta?.specPath ?? "";
|
|
3911
4228
|
const featureName = firstMeta?.featureName ?? "";
|
|
3912
|
-
const requirements =
|
|
3913
|
-
for (const req of reqs) {
|
|
3914
|
-
const requiresEdges = store.getEdges({ from: req.id, type: "requires" });
|
|
3915
|
-
const codeFiles = requiresEdges.map((edge) => {
|
|
3916
|
-
const targetNode = store.getNode(edge.to);
|
|
3917
|
-
return {
|
|
3918
|
-
path: targetNode?.path ?? edge.to,
|
|
3919
|
-
confidence: edge.confidence ?? edge.metadata?.confidence ?? 0,
|
|
3920
|
-
method: edge.metadata?.method ?? "convention"
|
|
3921
|
-
};
|
|
3922
|
-
});
|
|
3923
|
-
const verifiedByEdges = store.getEdges({ from: req.id, type: "verified_by" });
|
|
3924
|
-
const testFiles = verifiedByEdges.map((edge) => {
|
|
3925
|
-
const targetNode = store.getNode(edge.to);
|
|
3926
|
-
return {
|
|
3927
|
-
path: targetNode?.path ?? edge.to,
|
|
3928
|
-
confidence: edge.confidence ?? edge.metadata?.confidence ?? 0,
|
|
3929
|
-
method: edge.metadata?.method ?? "convention"
|
|
3930
|
-
};
|
|
3931
|
-
});
|
|
3932
|
-
const hasCode = codeFiles.length > 0;
|
|
3933
|
-
const hasTests = testFiles.length > 0;
|
|
3934
|
-
const status = hasCode && hasTests ? "full" : hasCode ? "code-only" : hasTests ? "test-only" : "none";
|
|
3935
|
-
const allConfidences = [
|
|
3936
|
-
...codeFiles.map((f) => f.confidence),
|
|
3937
|
-
...testFiles.map((f) => f.confidence)
|
|
3938
|
-
];
|
|
3939
|
-
const maxConfidence = allConfidences.length > 0 ? Math.max(...allConfidences) : 0;
|
|
3940
|
-
requirements.push({
|
|
3941
|
-
requirementId: req.id,
|
|
3942
|
-
requirementName: req.name,
|
|
3943
|
-
index: req.metadata?.index ?? 0,
|
|
3944
|
-
codeFiles,
|
|
3945
|
-
testFiles,
|
|
3946
|
-
status,
|
|
3947
|
-
maxConfidence
|
|
3948
|
-
});
|
|
3949
|
-
}
|
|
4229
|
+
const requirements = reqs.map((req) => buildRequirementCoverage(store, req));
|
|
3950
4230
|
requirements.sort((a, b) => a.index - b.index);
|
|
3951
|
-
const total = requirements.length;
|
|
3952
|
-
const withCode = requirements.filter((r) => r.codeFiles.length > 0).length;
|
|
3953
|
-
const withTests = requirements.filter((r) => r.testFiles.length > 0).length;
|
|
3954
|
-
const fullyTraced = requirements.filter((r) => r.status === "full").length;
|
|
3955
|
-
const untraceable = requirements.filter((r) => r.status === "none").length;
|
|
3956
|
-
const coveragePercent = total > 0 ? Math.round(fullyTraced / total * 100) : 0;
|
|
3957
4231
|
results.push({
|
|
3958
4232
|
specPath,
|
|
3959
4233
|
featureName,
|
|
3960
4234
|
requirements,
|
|
3961
|
-
summary:
|
|
4235
|
+
summary: computeSummary(requirements)
|
|
3962
4236
|
});
|
|
3963
4237
|
}
|
|
3964
4238
|
return results;
|
|
@@ -4782,13 +5056,15 @@ var ConflictPredictor = class {
|
|
|
4782
5056
|
};
|
|
4783
5057
|
|
|
4784
5058
|
// src/index.ts
|
|
4785
|
-
var VERSION = "0.
|
|
5059
|
+
var VERSION = "0.4.0";
|
|
4786
5060
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4787
5061
|
0 && (module.exports = {
|
|
4788
5062
|
Assembler,
|
|
4789
5063
|
CIConnector,
|
|
4790
5064
|
CURRENT_SCHEMA_VERSION,
|
|
5065
|
+
CascadeSimulator,
|
|
4791
5066
|
CodeIngestor,
|
|
5067
|
+
CompositeProbabilityStrategy,
|
|
4792
5068
|
ConflictPredictor,
|
|
4793
5069
|
ConfluenceConnector,
|
|
4794
5070
|
ContextQL,
|
|
@@ -4823,6 +5099,7 @@ var VERSION = "0.2.0";
|
|
|
4823
5099
|
VERSION,
|
|
4824
5100
|
VectorStore,
|
|
4825
5101
|
askGraph,
|
|
5102
|
+
classifyNodeCategory,
|
|
4826
5103
|
groupNodesByImpact,
|
|
4827
5104
|
linkToCode,
|
|
4828
5105
|
loadGraph,
|