@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/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 results = [];
325
- for (const edge of candidates) {
326
- if (query.from !== void 0 && edge.from !== query.from) continue;
327
- if (query.to !== void 0 && edge.to !== query.to) continue;
328
- if (query.type !== void 0 && edge.type !== query.type) continue;
329
- results.push({ ...edge });
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
- return results;
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
- if (TEST_TYPES.has(node.type)) {
635
- tests.push(node);
636
- } else if (DOC_TYPES.has(node.type)) {
637
- docs.push(node);
638
- } else if (CODE_TYPES.has(node.type)) {
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 && node.path.replace(/\\/g, "/").includes("/tests/") ? "verified_by" : "requires";
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: { total, withCode, withTests, fullyTraced, untraceable, coveragePercent }
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.2.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,