@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.mjs
CHANGED
|
@@ -162,6 +162,16 @@ function removeFromIndex(index, key, edge) {
|
|
|
162
162
|
if (idx !== -1) list.splice(idx, 1);
|
|
163
163
|
if (list.length === 0) index.delete(key);
|
|
164
164
|
}
|
|
165
|
+
function filterEdges(candidates, query) {
|
|
166
|
+
const results = [];
|
|
167
|
+
for (const edge of candidates) {
|
|
168
|
+
if (query.from !== void 0 && edge.from !== query.from) continue;
|
|
169
|
+
if (query.to !== void 0 && edge.to !== query.to) continue;
|
|
170
|
+
if (query.type !== void 0 && edge.type !== query.type) continue;
|
|
171
|
+
results.push({ ...edge });
|
|
172
|
+
}
|
|
173
|
+
return results;
|
|
174
|
+
}
|
|
165
175
|
var GraphStore = class {
|
|
166
176
|
nodeMap = /* @__PURE__ */ new Map();
|
|
167
177
|
edgeMap = /* @__PURE__ */ new Map();
|
|
@@ -229,27 +239,25 @@ var GraphStore = class {
|
|
|
229
239
|
}
|
|
230
240
|
}
|
|
231
241
|
getEdges(query) {
|
|
232
|
-
let candidates;
|
|
233
242
|
if (query.from !== void 0 && query.to !== void 0 && query.type !== void 0) {
|
|
234
243
|
const edge = this.edgeMap.get(edgeKey(query.from, query.to, query.type));
|
|
235
244
|
return edge ? [{ ...edge }] : [];
|
|
236
|
-
} else if (query.from !== void 0) {
|
|
237
|
-
candidates = this.edgesByFrom.get(query.from) ?? [];
|
|
238
|
-
} else if (query.to !== void 0) {
|
|
239
|
-
candidates = this.edgesByTo.get(query.to) ?? [];
|
|
240
|
-
} else if (query.type !== void 0) {
|
|
241
|
-
candidates = this.edgesByType.get(query.type) ?? [];
|
|
242
|
-
} else {
|
|
243
|
-
candidates = this.edgeMap.values();
|
|
244
245
|
}
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
246
|
+
const candidates = this.selectCandidates(query);
|
|
247
|
+
return filterEdges(candidates, query);
|
|
248
|
+
}
|
|
249
|
+
/** Pick the most selective index to start from. */
|
|
250
|
+
selectCandidates(query) {
|
|
251
|
+
if (query.from !== void 0) {
|
|
252
|
+
return this.edgesByFrom.get(query.from) ?? [];
|
|
251
253
|
}
|
|
252
|
-
|
|
254
|
+
if (query.to !== void 0) {
|
|
255
|
+
return this.edgesByTo.get(query.to) ?? [];
|
|
256
|
+
}
|
|
257
|
+
if (query.type !== void 0) {
|
|
258
|
+
return this.edgesByType.get(query.type) ?? [];
|
|
259
|
+
}
|
|
260
|
+
return this.edgeMap.values();
|
|
253
261
|
}
|
|
254
262
|
getNeighbors(nodeId, direction = "both") {
|
|
255
263
|
const neighborIds = /* @__PURE__ */ new Set();
|
|
@@ -545,6 +553,12 @@ var CODE_TYPES = /* @__PURE__ */ new Set([
|
|
|
545
553
|
"method",
|
|
546
554
|
"variable"
|
|
547
555
|
]);
|
|
556
|
+
function classifyNodeCategory(node) {
|
|
557
|
+
if (TEST_TYPES.has(node.type)) return "tests";
|
|
558
|
+
if (DOC_TYPES.has(node.type)) return "docs";
|
|
559
|
+
if (CODE_TYPES.has(node.type)) return "code";
|
|
560
|
+
return "other";
|
|
561
|
+
}
|
|
548
562
|
function groupNodesByImpact(nodes, excludeId) {
|
|
549
563
|
const tests = [];
|
|
550
564
|
const docs = [];
|
|
@@ -552,15 +566,11 @@ function groupNodesByImpact(nodes, excludeId) {
|
|
|
552
566
|
const other = [];
|
|
553
567
|
for (const node of nodes) {
|
|
554
568
|
if (excludeId && node.id === excludeId) continue;
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
code.push(node);
|
|
561
|
-
} else {
|
|
562
|
-
other.push(node);
|
|
563
|
-
}
|
|
569
|
+
const category = classifyNodeCategory(node);
|
|
570
|
+
if (category === "tests") tests.push(node);
|
|
571
|
+
else if (category === "docs") docs.push(node);
|
|
572
|
+
else if (category === "code") code.push(node);
|
|
573
|
+
else other.push(node);
|
|
564
574
|
}
|
|
565
575
|
return { tests, docs, code, other };
|
|
566
576
|
}
|
|
@@ -1525,7 +1535,7 @@ var RequirementIngestor = class {
|
|
|
1525
1535
|
}
|
|
1526
1536
|
for (const featureDir of featureDirs) {
|
|
1527
1537
|
const featureName = path4.basename(featureDir);
|
|
1528
|
-
const specPath = path4.join(featureDir, "proposal.md");
|
|
1538
|
+
const specPath = path4.join(featureDir, "proposal.md").replaceAll("\\", "/");
|
|
1529
1539
|
let content;
|
|
1530
1540
|
try {
|
|
1531
1541
|
content = await fs3.readFile(specPath, "utf-8");
|
|
@@ -1675,7 +1685,7 @@ var RequirementIngestor = class {
|
|
|
1675
1685
|
const escaped = node.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1676
1686
|
const namePattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
1677
1687
|
if (namePattern.test(reqText)) {
|
|
1678
|
-
const edgeType = node.path
|
|
1688
|
+
const edgeType = node.path?.replace(/\\/g, "/").includes("/tests/") ? "verified_by" : "requires";
|
|
1679
1689
|
this.store.addEdge({
|
|
1680
1690
|
from: reqId,
|
|
1681
1691
|
to: node.id,
|
|
@@ -2892,6 +2902,7 @@ var INTENT_SIGNALS = {
|
|
|
2892
2902
|
"depend",
|
|
2893
2903
|
"blast",
|
|
2894
2904
|
"radius",
|
|
2905
|
+
"cascade",
|
|
2895
2906
|
"risk",
|
|
2896
2907
|
"delete",
|
|
2897
2908
|
"remove"
|
|
@@ -2901,6 +2912,7 @@ var INTENT_SIGNALS = {
|
|
|
2901
2912
|
/what\s+(breaks|happens|is affected)/,
|
|
2902
2913
|
/if\s+i\s+(change|modify|remove|delete)/,
|
|
2903
2914
|
/blast\s+radius/,
|
|
2915
|
+
/cascad/,
|
|
2904
2916
|
/what\s+(depend|relies)/
|
|
2905
2917
|
]
|
|
2906
2918
|
},
|
|
@@ -3386,6 +3398,10 @@ var ResponseFormatter = class {
|
|
|
3386
3398
|
}
|
|
3387
3399
|
formatImpact(entityName, data) {
|
|
3388
3400
|
const d = data;
|
|
3401
|
+
if ("sourceNodeId" in d && "summary" in d) {
|
|
3402
|
+
const summary = d.summary;
|
|
3403
|
+
return `Blast radius of **${entityName}**: ${summary.totalAffected} affected nodes (${summary.highRisk} high risk, ${summary.mediumRisk} medium, ${summary.lowRisk} low).`;
|
|
3404
|
+
}
|
|
3389
3405
|
const code = this.safeArrayLength(d?.code);
|
|
3390
3406
|
const tests = this.safeArrayLength(d?.tests);
|
|
3391
3407
|
const docs = this.safeArrayLength(d?.docs);
|
|
@@ -3447,6 +3463,246 @@ var ResponseFormatter = class {
|
|
|
3447
3463
|
}
|
|
3448
3464
|
};
|
|
3449
3465
|
|
|
3466
|
+
// src/blast-radius/CompositeProbabilityStrategy.ts
|
|
3467
|
+
var CompositeProbabilityStrategy = class _CompositeProbabilityStrategy {
|
|
3468
|
+
constructor(changeFreqMap, couplingMap) {
|
|
3469
|
+
this.changeFreqMap = changeFreqMap;
|
|
3470
|
+
this.couplingMap = couplingMap;
|
|
3471
|
+
}
|
|
3472
|
+
changeFreqMap;
|
|
3473
|
+
couplingMap;
|
|
3474
|
+
static BASE_WEIGHTS = {
|
|
3475
|
+
imports: 0.7,
|
|
3476
|
+
calls: 0.5,
|
|
3477
|
+
implements: 0.6,
|
|
3478
|
+
inherits: 0.6,
|
|
3479
|
+
co_changes_with: 0.4,
|
|
3480
|
+
references: 0.2,
|
|
3481
|
+
contains: 0.3
|
|
3482
|
+
};
|
|
3483
|
+
static FALLBACK_WEIGHT = 0.1;
|
|
3484
|
+
static EDGE_TYPE_BLEND = 0.5;
|
|
3485
|
+
static CHANGE_FREQ_BLEND = 0.3;
|
|
3486
|
+
static COUPLING_BLEND = 0.2;
|
|
3487
|
+
getEdgeProbability(edge, _fromNode, toNode) {
|
|
3488
|
+
const base = _CompositeProbabilityStrategy.BASE_WEIGHTS[edge.type] ?? _CompositeProbabilityStrategy.FALLBACK_WEIGHT;
|
|
3489
|
+
const changeFreq = this.changeFreqMap.get(toNode.id) ?? 0;
|
|
3490
|
+
const coupling = this.couplingMap.get(toNode.id) ?? 0;
|
|
3491
|
+
return Math.min(
|
|
3492
|
+
1,
|
|
3493
|
+
base * _CompositeProbabilityStrategy.EDGE_TYPE_BLEND + changeFreq * _CompositeProbabilityStrategy.CHANGE_FREQ_BLEND + coupling * _CompositeProbabilityStrategy.COUPLING_BLEND
|
|
3494
|
+
);
|
|
3495
|
+
}
|
|
3496
|
+
};
|
|
3497
|
+
|
|
3498
|
+
// src/blast-radius/CascadeSimulator.ts
|
|
3499
|
+
var DEFAULT_PROBABILITY_FLOOR = 0.05;
|
|
3500
|
+
var DEFAULT_MAX_DEPTH = 10;
|
|
3501
|
+
var CascadeSimulator = class {
|
|
3502
|
+
constructor(store) {
|
|
3503
|
+
this.store = store;
|
|
3504
|
+
}
|
|
3505
|
+
store;
|
|
3506
|
+
simulate(sourceNodeId, options = {}) {
|
|
3507
|
+
const sourceNode = this.store.getNode(sourceNodeId);
|
|
3508
|
+
if (!sourceNode) {
|
|
3509
|
+
throw new Error(`Node not found: ${sourceNodeId}. Ensure the file has been ingested.`);
|
|
3510
|
+
}
|
|
3511
|
+
const probabilityFloor = options.probabilityFloor ?? DEFAULT_PROBABILITY_FLOOR;
|
|
3512
|
+
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
3513
|
+
const edgeTypeFilter = options.edgeTypes ? new Set(options.edgeTypes) : null;
|
|
3514
|
+
const strategy = options.strategy ?? this.buildDefaultStrategy();
|
|
3515
|
+
const visited = /* @__PURE__ */ new Map();
|
|
3516
|
+
const queue = [];
|
|
3517
|
+
const fanOutCount = /* @__PURE__ */ new Map();
|
|
3518
|
+
this.seedQueue(
|
|
3519
|
+
sourceNodeId,
|
|
3520
|
+
sourceNode,
|
|
3521
|
+
strategy,
|
|
3522
|
+
edgeTypeFilter,
|
|
3523
|
+
probabilityFloor,
|
|
3524
|
+
queue,
|
|
3525
|
+
fanOutCount
|
|
3526
|
+
);
|
|
3527
|
+
const truncated = this.runBfs(
|
|
3528
|
+
queue,
|
|
3529
|
+
visited,
|
|
3530
|
+
fanOutCount,
|
|
3531
|
+
sourceNodeId,
|
|
3532
|
+
strategy,
|
|
3533
|
+
edgeTypeFilter,
|
|
3534
|
+
probabilityFloor,
|
|
3535
|
+
maxDepth
|
|
3536
|
+
);
|
|
3537
|
+
return this.buildResult(sourceNodeId, sourceNode.name, visited, fanOutCount, truncated);
|
|
3538
|
+
}
|
|
3539
|
+
seedQueue(sourceNodeId, sourceNode, strategy, edgeTypeFilter, probabilityFloor, queue, fanOutCount) {
|
|
3540
|
+
const sourceEdges = this.store.getEdges({ from: sourceNodeId });
|
|
3541
|
+
for (const edge of sourceEdges) {
|
|
3542
|
+
if (edge.to === sourceNodeId) continue;
|
|
3543
|
+
if (edgeTypeFilter && !edgeTypeFilter.has(edge.type)) continue;
|
|
3544
|
+
const targetNode = this.store.getNode(edge.to);
|
|
3545
|
+
if (!targetNode) continue;
|
|
3546
|
+
const cumProb = strategy.getEdgeProbability(edge, sourceNode, targetNode);
|
|
3547
|
+
if (cumProb < probabilityFloor) continue;
|
|
3548
|
+
queue.push({
|
|
3549
|
+
nodeId: edge.to,
|
|
3550
|
+
cumProb,
|
|
3551
|
+
depth: 1,
|
|
3552
|
+
parentId: sourceNodeId,
|
|
3553
|
+
incomingEdge: edge.type
|
|
3554
|
+
});
|
|
3555
|
+
}
|
|
3556
|
+
fanOutCount.set(
|
|
3557
|
+
sourceNodeId,
|
|
3558
|
+
sourceEdges.filter(
|
|
3559
|
+
(e) => e.to !== sourceNodeId && (!edgeTypeFilter || edgeTypeFilter.has(e.type))
|
|
3560
|
+
).length
|
|
3561
|
+
);
|
|
3562
|
+
}
|
|
3563
|
+
runBfs(queue, visited, fanOutCount, sourceNodeId, strategy, edgeTypeFilter, probabilityFloor, maxDepth) {
|
|
3564
|
+
const MAX_QUEUE_SIZE = 1e4;
|
|
3565
|
+
let head = 0;
|
|
3566
|
+
while (head < queue.length) {
|
|
3567
|
+
if (queue.length > MAX_QUEUE_SIZE) return true;
|
|
3568
|
+
const entry = queue[head++];
|
|
3569
|
+
const existing = visited.get(entry.nodeId);
|
|
3570
|
+
if (existing && existing.cumulativeProbability >= entry.cumProb) continue;
|
|
3571
|
+
const targetNode = this.store.getNode(entry.nodeId);
|
|
3572
|
+
if (!targetNode) continue;
|
|
3573
|
+
visited.set(entry.nodeId, {
|
|
3574
|
+
nodeId: entry.nodeId,
|
|
3575
|
+
name: targetNode.name,
|
|
3576
|
+
...targetNode.path !== void 0 && { path: targetNode.path },
|
|
3577
|
+
type: targetNode.type,
|
|
3578
|
+
cumulativeProbability: entry.cumProb,
|
|
3579
|
+
depth: entry.depth,
|
|
3580
|
+
incomingEdge: entry.incomingEdge,
|
|
3581
|
+
parentId: entry.parentId
|
|
3582
|
+
});
|
|
3583
|
+
if (entry.depth < maxDepth) {
|
|
3584
|
+
const childCount = this.expandNode(
|
|
3585
|
+
entry,
|
|
3586
|
+
targetNode,
|
|
3587
|
+
sourceNodeId,
|
|
3588
|
+
strategy,
|
|
3589
|
+
edgeTypeFilter,
|
|
3590
|
+
probabilityFloor,
|
|
3591
|
+
queue
|
|
3592
|
+
);
|
|
3593
|
+
fanOutCount.set(entry.nodeId, (fanOutCount.get(entry.nodeId) ?? 0) + childCount);
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
return false;
|
|
3597
|
+
}
|
|
3598
|
+
expandNode(entry, fromNode, sourceNodeId, strategy, edgeTypeFilter, probabilityFloor, queue) {
|
|
3599
|
+
const outEdges = this.store.getEdges({ from: entry.nodeId });
|
|
3600
|
+
let childCount = 0;
|
|
3601
|
+
for (const edge of outEdges) {
|
|
3602
|
+
if (edgeTypeFilter && !edgeTypeFilter.has(edge.type)) continue;
|
|
3603
|
+
if (edge.to === sourceNodeId) continue;
|
|
3604
|
+
const childNode = this.store.getNode(edge.to);
|
|
3605
|
+
if (!childNode) continue;
|
|
3606
|
+
const newCumProb = entry.cumProb * strategy.getEdgeProbability(edge, fromNode, childNode);
|
|
3607
|
+
if (newCumProb < probabilityFloor) continue;
|
|
3608
|
+
childCount++;
|
|
3609
|
+
queue.push({
|
|
3610
|
+
nodeId: edge.to,
|
|
3611
|
+
cumProb: newCumProb,
|
|
3612
|
+
depth: entry.depth + 1,
|
|
3613
|
+
parentId: entry.nodeId,
|
|
3614
|
+
incomingEdge: edge.type
|
|
3615
|
+
});
|
|
3616
|
+
}
|
|
3617
|
+
return childCount;
|
|
3618
|
+
}
|
|
3619
|
+
buildDefaultStrategy() {
|
|
3620
|
+
return new CompositeProbabilityStrategy(/* @__PURE__ */ new Map(), /* @__PURE__ */ new Map());
|
|
3621
|
+
}
|
|
3622
|
+
buildResult(sourceNodeId, sourceName, visited, fanOutCount, truncated = false) {
|
|
3623
|
+
if (visited.size === 0) {
|
|
3624
|
+
return {
|
|
3625
|
+
sourceNodeId,
|
|
3626
|
+
sourceName,
|
|
3627
|
+
layers: [],
|
|
3628
|
+
flatSummary: [],
|
|
3629
|
+
summary: {
|
|
3630
|
+
totalAffected: 0,
|
|
3631
|
+
maxDepthReached: 0,
|
|
3632
|
+
highRisk: 0,
|
|
3633
|
+
mediumRisk: 0,
|
|
3634
|
+
lowRisk: 0,
|
|
3635
|
+
categoryBreakdown: { code: 0, tests: 0, docs: 0, other: 0 },
|
|
3636
|
+
amplificationPoints: [],
|
|
3637
|
+
truncated
|
|
3638
|
+
}
|
|
3639
|
+
};
|
|
3640
|
+
}
|
|
3641
|
+
const allNodes = Array.from(visited.values());
|
|
3642
|
+
const flatSummary = [...allNodes].sort(
|
|
3643
|
+
(a, b) => b.cumulativeProbability - a.cumulativeProbability
|
|
3644
|
+
);
|
|
3645
|
+
const depthMap = /* @__PURE__ */ new Map();
|
|
3646
|
+
for (const node of allNodes) {
|
|
3647
|
+
let list = depthMap.get(node.depth);
|
|
3648
|
+
if (!list) {
|
|
3649
|
+
list = [];
|
|
3650
|
+
depthMap.set(node.depth, list);
|
|
3651
|
+
}
|
|
3652
|
+
list.push(node);
|
|
3653
|
+
}
|
|
3654
|
+
const layers = [];
|
|
3655
|
+
const depths = Array.from(depthMap.keys()).sort((a, b) => a - b);
|
|
3656
|
+
for (const depth of depths) {
|
|
3657
|
+
const nodes = depthMap.get(depth);
|
|
3658
|
+
const breakdown = { code: 0, tests: 0, docs: 0, other: 0 };
|
|
3659
|
+
for (const n of nodes) {
|
|
3660
|
+
const graphNode = this.store.getNode(n.nodeId);
|
|
3661
|
+
if (graphNode) {
|
|
3662
|
+
breakdown[classifyNodeCategory(graphNode)]++;
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
layers.push({ depth, nodes, categoryBreakdown: breakdown });
|
|
3666
|
+
}
|
|
3667
|
+
let highRisk = 0;
|
|
3668
|
+
let mediumRisk = 0;
|
|
3669
|
+
let lowRisk = 0;
|
|
3670
|
+
const catBreakdown = { code: 0, tests: 0, docs: 0, other: 0 };
|
|
3671
|
+
for (const node of allNodes) {
|
|
3672
|
+
if (node.cumulativeProbability >= 0.5) highRisk++;
|
|
3673
|
+
else if (node.cumulativeProbability >= 0.2) mediumRisk++;
|
|
3674
|
+
else lowRisk++;
|
|
3675
|
+
const graphNode = this.store.getNode(node.nodeId);
|
|
3676
|
+
if (graphNode) {
|
|
3677
|
+
catBreakdown[classifyNodeCategory(graphNode)]++;
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
const amplificationPoints = [];
|
|
3681
|
+
for (const [nodeId, count] of fanOutCount) {
|
|
3682
|
+
if (count > 3) {
|
|
3683
|
+
amplificationPoints.push(nodeId);
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3686
|
+
const maxDepthReached = allNodes.reduce((max, n) => Math.max(max, n.depth), 0);
|
|
3687
|
+
return {
|
|
3688
|
+
sourceNodeId,
|
|
3689
|
+
sourceName,
|
|
3690
|
+
layers,
|
|
3691
|
+
flatSummary,
|
|
3692
|
+
summary: {
|
|
3693
|
+
totalAffected: allNodes.length,
|
|
3694
|
+
maxDepthReached,
|
|
3695
|
+
highRisk,
|
|
3696
|
+
mediumRisk,
|
|
3697
|
+
lowRisk,
|
|
3698
|
+
categoryBreakdown: catBreakdown,
|
|
3699
|
+
amplificationPoints,
|
|
3700
|
+
truncated
|
|
3701
|
+
}
|
|
3702
|
+
};
|
|
3703
|
+
}
|
|
3704
|
+
};
|
|
3705
|
+
|
|
3450
3706
|
// src/nlq/index.ts
|
|
3451
3707
|
var ENTITY_REQUIRED_INTENTS = /* @__PURE__ */ new Set(["impact", "relationships", "explain"]);
|
|
3452
3708
|
var classifier = new IntentClassifier();
|
|
@@ -3509,6 +3765,11 @@ function executeOperation(store, intent, entities, question, fusion) {
|
|
|
3509
3765
|
switch (intent) {
|
|
3510
3766
|
case "impact": {
|
|
3511
3767
|
const rootId = entities[0].nodeId;
|
|
3768
|
+
const lowerQuestion = question.toLowerCase();
|
|
3769
|
+
if (lowerQuestion.includes("blast radius") || lowerQuestion.includes("cascade")) {
|
|
3770
|
+
const simulator = new CascadeSimulator(store);
|
|
3771
|
+
return simulator.simulate(rootId);
|
|
3772
|
+
}
|
|
3512
3773
|
const result = cql.execute({
|
|
3513
3774
|
rootNodeIds: [rootId],
|
|
3514
3775
|
bidirectional: true,
|
|
@@ -3803,6 +4064,59 @@ var Assembler = class {
|
|
|
3803
4064
|
};
|
|
3804
4065
|
|
|
3805
4066
|
// src/query/Traceability.ts
|
|
4067
|
+
function extractConfidence(edge) {
|
|
4068
|
+
return edge.confidence ?? edge.metadata?.confidence ?? 0;
|
|
4069
|
+
}
|
|
4070
|
+
function extractMethod(edge) {
|
|
4071
|
+
return edge.metadata?.method ?? "convention";
|
|
4072
|
+
}
|
|
4073
|
+
function edgesToTracedFiles(store, edges) {
|
|
4074
|
+
return edges.map((edge) => ({
|
|
4075
|
+
path: store.getNode(edge.to)?.path ?? edge.to,
|
|
4076
|
+
confidence: extractConfidence(edge),
|
|
4077
|
+
method: extractMethod(edge)
|
|
4078
|
+
}));
|
|
4079
|
+
}
|
|
4080
|
+
function determineCoverageStatus(hasCode, hasTests) {
|
|
4081
|
+
if (hasCode && hasTests) return "full";
|
|
4082
|
+
if (hasCode) return "code-only";
|
|
4083
|
+
if (hasTests) return "test-only";
|
|
4084
|
+
return "none";
|
|
4085
|
+
}
|
|
4086
|
+
function computeMaxConfidence(codeFiles, testFiles) {
|
|
4087
|
+
const allConfidences = [
|
|
4088
|
+
...codeFiles.map((f) => f.confidence),
|
|
4089
|
+
...testFiles.map((f) => f.confidence)
|
|
4090
|
+
];
|
|
4091
|
+
return allConfidences.length > 0 ? Math.max(...allConfidences) : 0;
|
|
4092
|
+
}
|
|
4093
|
+
function buildRequirementCoverage(store, req) {
|
|
4094
|
+
const codeFiles = edgesToTracedFiles(store, store.getEdges({ from: req.id, type: "requires" }));
|
|
4095
|
+
const testFiles = edgesToTracedFiles(
|
|
4096
|
+
store,
|
|
4097
|
+
store.getEdges({ from: req.id, type: "verified_by" })
|
|
4098
|
+
);
|
|
4099
|
+
const hasCode = codeFiles.length > 0;
|
|
4100
|
+
const hasTests = testFiles.length > 0;
|
|
4101
|
+
return {
|
|
4102
|
+
requirementId: req.id,
|
|
4103
|
+
requirementName: req.name,
|
|
4104
|
+
index: req.metadata?.index ?? 0,
|
|
4105
|
+
codeFiles,
|
|
4106
|
+
testFiles,
|
|
4107
|
+
status: determineCoverageStatus(hasCode, hasTests),
|
|
4108
|
+
maxConfidence: computeMaxConfidence(codeFiles, testFiles)
|
|
4109
|
+
};
|
|
4110
|
+
}
|
|
4111
|
+
function computeSummary(requirements) {
|
|
4112
|
+
const total = requirements.length;
|
|
4113
|
+
const withCode = requirements.filter((r) => r.codeFiles.length > 0).length;
|
|
4114
|
+
const withTests = requirements.filter((r) => r.testFiles.length > 0).length;
|
|
4115
|
+
const fullyTraced = requirements.filter((r) => r.status === "full").length;
|
|
4116
|
+
const untraceable = requirements.filter((r) => r.status === "none").length;
|
|
4117
|
+
const coveragePercent = total > 0 ? Math.round(fullyTraced / total * 100) : 0;
|
|
4118
|
+
return { total, withCode, withTests, fullyTraced, untraceable, coveragePercent };
|
|
4119
|
+
}
|
|
3806
4120
|
function queryTraceability(store, options) {
|
|
3807
4121
|
const allRequirements = store.findNodes({ type: "requirement" });
|
|
3808
4122
|
const filtered = allRequirements.filter((node) => {
|
|
@@ -3830,56 +4144,13 @@ function queryTraceability(store, options) {
|
|
|
3830
4144
|
const firstMeta = firstReq.metadata;
|
|
3831
4145
|
const specPath = firstMeta?.specPath ?? "";
|
|
3832
4146
|
const featureName = firstMeta?.featureName ?? "";
|
|
3833
|
-
const requirements =
|
|
3834
|
-
for (const req of reqs) {
|
|
3835
|
-
const requiresEdges = store.getEdges({ from: req.id, type: "requires" });
|
|
3836
|
-
const codeFiles = requiresEdges.map((edge) => {
|
|
3837
|
-
const targetNode = store.getNode(edge.to);
|
|
3838
|
-
return {
|
|
3839
|
-
path: targetNode?.path ?? edge.to,
|
|
3840
|
-
confidence: edge.confidence ?? edge.metadata?.confidence ?? 0,
|
|
3841
|
-
method: edge.metadata?.method ?? "convention"
|
|
3842
|
-
};
|
|
3843
|
-
});
|
|
3844
|
-
const verifiedByEdges = store.getEdges({ from: req.id, type: "verified_by" });
|
|
3845
|
-
const testFiles = verifiedByEdges.map((edge) => {
|
|
3846
|
-
const targetNode = store.getNode(edge.to);
|
|
3847
|
-
return {
|
|
3848
|
-
path: targetNode?.path ?? edge.to,
|
|
3849
|
-
confidence: edge.confidence ?? edge.metadata?.confidence ?? 0,
|
|
3850
|
-
method: edge.metadata?.method ?? "convention"
|
|
3851
|
-
};
|
|
3852
|
-
});
|
|
3853
|
-
const hasCode = codeFiles.length > 0;
|
|
3854
|
-
const hasTests = testFiles.length > 0;
|
|
3855
|
-
const status = hasCode && hasTests ? "full" : hasCode ? "code-only" : hasTests ? "test-only" : "none";
|
|
3856
|
-
const allConfidences = [
|
|
3857
|
-
...codeFiles.map((f) => f.confidence),
|
|
3858
|
-
...testFiles.map((f) => f.confidence)
|
|
3859
|
-
];
|
|
3860
|
-
const maxConfidence = allConfidences.length > 0 ? Math.max(...allConfidences) : 0;
|
|
3861
|
-
requirements.push({
|
|
3862
|
-
requirementId: req.id,
|
|
3863
|
-
requirementName: req.name,
|
|
3864
|
-
index: req.metadata?.index ?? 0,
|
|
3865
|
-
codeFiles,
|
|
3866
|
-
testFiles,
|
|
3867
|
-
status,
|
|
3868
|
-
maxConfidence
|
|
3869
|
-
});
|
|
3870
|
-
}
|
|
4147
|
+
const requirements = reqs.map((req) => buildRequirementCoverage(store, req));
|
|
3871
4148
|
requirements.sort((a, b) => a.index - b.index);
|
|
3872
|
-
const total = requirements.length;
|
|
3873
|
-
const withCode = requirements.filter((r) => r.codeFiles.length > 0).length;
|
|
3874
|
-
const withTests = requirements.filter((r) => r.testFiles.length > 0).length;
|
|
3875
|
-
const fullyTraced = requirements.filter((r) => r.status === "full").length;
|
|
3876
|
-
const untraceable = requirements.filter((r) => r.status === "none").length;
|
|
3877
|
-
const coveragePercent = total > 0 ? Math.round(fullyTraced / total * 100) : 0;
|
|
3878
4149
|
results.push({
|
|
3879
4150
|
specPath,
|
|
3880
4151
|
featureName,
|
|
3881
4152
|
requirements,
|
|
3882
|
-
summary:
|
|
4153
|
+
summary: computeSummary(requirements)
|
|
3883
4154
|
});
|
|
3884
4155
|
}
|
|
3885
4156
|
return results;
|
|
@@ -4703,12 +4974,14 @@ var ConflictPredictor = class {
|
|
|
4703
4974
|
};
|
|
4704
4975
|
|
|
4705
4976
|
// src/index.ts
|
|
4706
|
-
var VERSION = "0.
|
|
4977
|
+
var VERSION = "0.4.0";
|
|
4707
4978
|
export {
|
|
4708
4979
|
Assembler,
|
|
4709
4980
|
CIConnector,
|
|
4710
4981
|
CURRENT_SCHEMA_VERSION,
|
|
4982
|
+
CascadeSimulator,
|
|
4711
4983
|
CodeIngestor,
|
|
4984
|
+
CompositeProbabilityStrategy,
|
|
4712
4985
|
ConflictPredictor,
|
|
4713
4986
|
ConfluenceConnector,
|
|
4714
4987
|
ContextQL,
|
|
@@ -4743,6 +5016,7 @@ export {
|
|
|
4743
5016
|
VERSION,
|
|
4744
5017
|
VectorStore,
|
|
4745
5018
|
askGraph,
|
|
5019
|
+
classifyNodeCategory,
|
|
4746
5020
|
groupNodesByImpact,
|
|
4747
5021
|
linkToCode,
|
|
4748
5022
|
loadGraph,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harness-engineering/graph",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Knowledge graph for context assembly in Harness Engineering",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -19,8 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"minimatch": "^10.2.5",
|
|
22
|
-
"zod": "^3.25.76"
|
|
23
|
-
"@harness-engineering/types": "0.8.1"
|
|
22
|
+
"zod": "^3.25.76"
|
|
24
23
|
},
|
|
25
24
|
"optionalDependencies": {
|
|
26
25
|
"tree-sitter": "^0.22.4",
|