@harness-engineering/graph 0.3.5 → 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 +164 -10
- package/dist/index.d.ts +164 -10
- package/dist/index.js +680 -57
- package/dist/index.mjs +675 -57
- 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,
|
|
@@ -59,6 +61,7 @@ __export(index_exports, {
|
|
|
59
61
|
KnowledgeIngestor: () => KnowledgeIngestor,
|
|
60
62
|
NODE_TYPES: () => NODE_TYPES,
|
|
61
63
|
OBSERVABILITY_TYPES: () => OBSERVABILITY_TYPES,
|
|
64
|
+
RequirementIngestor: () => RequirementIngestor,
|
|
62
65
|
ResponseFormatter: () => ResponseFormatter,
|
|
63
66
|
SlackConnector: () => SlackConnector,
|
|
64
67
|
SyncManager: () => SyncManager,
|
|
@@ -67,10 +70,12 @@ __export(index_exports, {
|
|
|
67
70
|
VERSION: () => VERSION,
|
|
68
71
|
VectorStore: () => VectorStore,
|
|
69
72
|
askGraph: () => askGraph,
|
|
73
|
+
classifyNodeCategory: () => classifyNodeCategory,
|
|
70
74
|
groupNodesByImpact: () => groupNodesByImpact,
|
|
71
75
|
linkToCode: () => linkToCode,
|
|
72
76
|
loadGraph: () => loadGraph,
|
|
73
77
|
project: () => project,
|
|
78
|
+
queryTraceability: () => queryTraceability,
|
|
74
79
|
saveGraph: () => saveGraph
|
|
75
80
|
});
|
|
76
81
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -112,7 +117,9 @@ var NODE_TYPES = [
|
|
|
112
117
|
// Design
|
|
113
118
|
"design_token",
|
|
114
119
|
"aesthetic_intent",
|
|
115
|
-
"design_constraint"
|
|
120
|
+
"design_constraint",
|
|
121
|
+
// Traceability
|
|
122
|
+
"requirement"
|
|
116
123
|
];
|
|
117
124
|
var EDGE_TYPES = [
|
|
118
125
|
// Code relationships
|
|
@@ -141,7 +148,11 @@ var EDGE_TYPES = [
|
|
|
141
148
|
"uses_token",
|
|
142
149
|
"declares_intent",
|
|
143
150
|
"violates_design",
|
|
144
|
-
"platform_binding"
|
|
151
|
+
"platform_binding",
|
|
152
|
+
// Traceability relationships
|
|
153
|
+
"requires",
|
|
154
|
+
"verified_by",
|
|
155
|
+
"tested_by"
|
|
145
156
|
];
|
|
146
157
|
var OBSERVABILITY_TYPES = /* @__PURE__ */ new Set(["span", "metric", "log"]);
|
|
147
158
|
var CURRENT_SCHEMA_VERSION = 1;
|
|
@@ -233,6 +244,16 @@ function removeFromIndex(index, key, edge) {
|
|
|
233
244
|
if (idx !== -1) list.splice(idx, 1);
|
|
234
245
|
if (list.length === 0) index.delete(key);
|
|
235
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
|
+
}
|
|
236
257
|
var GraphStore = class {
|
|
237
258
|
nodeMap = /* @__PURE__ */ new Map();
|
|
238
259
|
edgeMap = /* @__PURE__ */ new Map();
|
|
@@ -300,27 +321,25 @@ var GraphStore = class {
|
|
|
300
321
|
}
|
|
301
322
|
}
|
|
302
323
|
getEdges(query) {
|
|
303
|
-
let candidates;
|
|
304
324
|
if (query.from !== void 0 && query.to !== void 0 && query.type !== void 0) {
|
|
305
325
|
const edge = this.edgeMap.get(edgeKey(query.from, query.to, query.type));
|
|
306
326
|
return edge ? [{ ...edge }] : [];
|
|
307
|
-
} else if (query.from !== void 0) {
|
|
308
|
-
candidates = this.edgesByFrom.get(query.from) ?? [];
|
|
309
|
-
} else if (query.to !== void 0) {
|
|
310
|
-
candidates = this.edgesByTo.get(query.to) ?? [];
|
|
311
|
-
} else if (query.type !== void 0) {
|
|
312
|
-
candidates = this.edgesByType.get(query.type) ?? [];
|
|
313
|
-
} else {
|
|
314
|
-
candidates = this.edgeMap.values();
|
|
315
327
|
}
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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) ?? [];
|
|
322
335
|
}
|
|
323
|
-
|
|
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();
|
|
324
343
|
}
|
|
325
344
|
getNeighbors(nodeId, direction = "both") {
|
|
326
345
|
const neighborIds = /* @__PURE__ */ new Set();
|
|
@@ -616,6 +635,12 @@ var CODE_TYPES = /* @__PURE__ */ new Set([
|
|
|
616
635
|
"method",
|
|
617
636
|
"variable"
|
|
618
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
|
+
}
|
|
619
644
|
function groupNodesByImpact(nodes, excludeId) {
|
|
620
645
|
const tests = [];
|
|
621
646
|
const docs = [];
|
|
@@ -623,15 +648,11 @@ function groupNodesByImpact(nodes, excludeId) {
|
|
|
623
648
|
const other = [];
|
|
624
649
|
for (const node of nodes) {
|
|
625
650
|
if (excludeId && node.id === excludeId) continue;
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
code.push(node);
|
|
632
|
-
} else {
|
|
633
|
-
other.push(node);
|
|
634
|
-
}
|
|
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);
|
|
635
656
|
}
|
|
636
657
|
return { tests, docs, code, other };
|
|
637
658
|
}
|
|
@@ -675,6 +696,7 @@ var CodeIngestor = class {
|
|
|
675
696
|
this.store.addEdge(edge);
|
|
676
697
|
edgesAdded++;
|
|
677
698
|
}
|
|
699
|
+
edgesAdded += this.extractReqAnnotations(fileContents, rootDir);
|
|
678
700
|
return {
|
|
679
701
|
nodesAdded,
|
|
680
702
|
nodesUpdated: 0,
|
|
@@ -1033,6 +1055,48 @@ var CodeIngestor = class {
|
|
|
1033
1055
|
if (/\.jsx?$/.test(filePath)) return "javascript";
|
|
1034
1056
|
return "unknown";
|
|
1035
1057
|
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Scan file contents for @req annotations and create verified_by edges
|
|
1060
|
+
* linking requirement nodes to the annotated files.
|
|
1061
|
+
* Format: // @req <feature-name>#<index>
|
|
1062
|
+
*/
|
|
1063
|
+
extractReqAnnotations(fileContents, rootDir) {
|
|
1064
|
+
const REQ_TAG = /\/\/\s*@req\s+([\w-]+)#(\d+)/g;
|
|
1065
|
+
const reqNodes = this.store.findNodes({ type: "requirement" });
|
|
1066
|
+
let edgesAdded = 0;
|
|
1067
|
+
for (const [filePath, content] of fileContents) {
|
|
1068
|
+
let match;
|
|
1069
|
+
REQ_TAG.lastIndex = 0;
|
|
1070
|
+
while ((match = REQ_TAG.exec(content)) !== null) {
|
|
1071
|
+
const featureName = match[1];
|
|
1072
|
+
const reqIndex = parseInt(match[2], 10);
|
|
1073
|
+
const reqNode = reqNodes.find(
|
|
1074
|
+
(n) => n.metadata.featureName === featureName && n.metadata.index === reqIndex
|
|
1075
|
+
);
|
|
1076
|
+
if (!reqNode) {
|
|
1077
|
+
console.warn(
|
|
1078
|
+
`@req annotation references non-existent requirement: ${featureName}#${reqIndex} in ${filePath}`
|
|
1079
|
+
);
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
const relPath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
1083
|
+
const fileNodeId = `file:${relPath}`;
|
|
1084
|
+
this.store.addEdge({
|
|
1085
|
+
from: reqNode.id,
|
|
1086
|
+
to: fileNodeId,
|
|
1087
|
+
type: "verified_by",
|
|
1088
|
+
confidence: 1,
|
|
1089
|
+
metadata: {
|
|
1090
|
+
method: "annotation",
|
|
1091
|
+
tag: `@req ${featureName}#${reqIndex}`,
|
|
1092
|
+
confidence: 1
|
|
1093
|
+
}
|
|
1094
|
+
});
|
|
1095
|
+
edgesAdded++;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
return edgesAdded;
|
|
1099
|
+
}
|
|
1036
1100
|
};
|
|
1037
1101
|
|
|
1038
1102
|
// src/ingest/GitIngestor.ts
|
|
@@ -1509,8 +1573,218 @@ var KnowledgeIngestor = class {
|
|
|
1509
1573
|
}
|
|
1510
1574
|
};
|
|
1511
1575
|
|
|
1512
|
-
// src/ingest/
|
|
1576
|
+
// src/ingest/RequirementIngestor.ts
|
|
1577
|
+
var fs3 = __toESM(require("fs/promises"));
|
|
1578
|
+
var path4 = __toESM(require("path"));
|
|
1579
|
+
var REQUIREMENT_SECTIONS = [
|
|
1580
|
+
"Observable Truths",
|
|
1581
|
+
"Success Criteria",
|
|
1582
|
+
"Acceptance Criteria"
|
|
1583
|
+
];
|
|
1584
|
+
var SECTION_HEADING_RE = /^#{2,3}\s+(.+)$/;
|
|
1585
|
+
var NUMBERED_ITEM_RE = /^\s*(\d+)\.\s+(.+)$/;
|
|
1586
|
+
function detectEarsPattern(text) {
|
|
1587
|
+
const lower = text.toLowerCase();
|
|
1588
|
+
if (/^if\b.+\bthen\b.+\bshall not\b/.test(lower)) return "unwanted";
|
|
1589
|
+
if (/^when\b/.test(lower)) return "event-driven";
|
|
1590
|
+
if (/^while\b/.test(lower)) return "state-driven";
|
|
1591
|
+
if (/^where\b/.test(lower)) return "optional";
|
|
1592
|
+
if (/^the\s+\w+\s+shall\b/.test(lower)) return "ubiquitous";
|
|
1593
|
+
return void 0;
|
|
1594
|
+
}
|
|
1513
1595
|
var CODE_NODE_TYPES2 = ["file", "function", "class", "method", "interface", "variable"];
|
|
1596
|
+
var RequirementIngestor = class {
|
|
1597
|
+
constructor(store) {
|
|
1598
|
+
this.store = store;
|
|
1599
|
+
}
|
|
1600
|
+
store;
|
|
1601
|
+
/**
|
|
1602
|
+
* Scan a specs directory for `<feature>/proposal.md` files,
|
|
1603
|
+
* extract numbered requirements from recognized sections,
|
|
1604
|
+
* and create requirement nodes with convention-based edges.
|
|
1605
|
+
*/
|
|
1606
|
+
async ingestSpecs(specsDir) {
|
|
1607
|
+
const start = Date.now();
|
|
1608
|
+
const errors = [];
|
|
1609
|
+
let nodesAdded = 0;
|
|
1610
|
+
let edgesAdded = 0;
|
|
1611
|
+
let featureDirs;
|
|
1612
|
+
try {
|
|
1613
|
+
const entries = await fs3.readdir(specsDir, { withFileTypes: true });
|
|
1614
|
+
featureDirs = entries.filter((e) => e.isDirectory()).map((e) => path4.join(specsDir, e.name));
|
|
1615
|
+
} catch {
|
|
1616
|
+
return emptyResult(Date.now() - start);
|
|
1617
|
+
}
|
|
1618
|
+
for (const featureDir of featureDirs) {
|
|
1619
|
+
const featureName = path4.basename(featureDir);
|
|
1620
|
+
const specPath = path4.join(featureDir, "proposal.md").replaceAll("\\", "/");
|
|
1621
|
+
let content;
|
|
1622
|
+
try {
|
|
1623
|
+
content = await fs3.readFile(specPath, "utf-8");
|
|
1624
|
+
} catch {
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
try {
|
|
1628
|
+
const specHash = hash(specPath);
|
|
1629
|
+
const specNodeId = `file:${specPath}`;
|
|
1630
|
+
this.store.addNode({
|
|
1631
|
+
id: specNodeId,
|
|
1632
|
+
type: "document",
|
|
1633
|
+
name: path4.basename(specPath),
|
|
1634
|
+
path: specPath,
|
|
1635
|
+
metadata: { featureName }
|
|
1636
|
+
});
|
|
1637
|
+
const requirements = this.extractRequirements(content, specPath, specHash, featureName);
|
|
1638
|
+
for (const req of requirements) {
|
|
1639
|
+
this.store.addNode(req.node);
|
|
1640
|
+
nodesAdded++;
|
|
1641
|
+
this.store.addEdge({
|
|
1642
|
+
from: req.node.id,
|
|
1643
|
+
to: specNodeId,
|
|
1644
|
+
type: "specifies"
|
|
1645
|
+
});
|
|
1646
|
+
edgesAdded++;
|
|
1647
|
+
edgesAdded += this.linkByPathPattern(req.node.id, featureName);
|
|
1648
|
+
edgesAdded += this.linkByKeywordOverlap(req.node.id, req.node.name);
|
|
1649
|
+
}
|
|
1650
|
+
} catch (err) {
|
|
1651
|
+
errors.push(`${specPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
return {
|
|
1655
|
+
nodesAdded,
|
|
1656
|
+
nodesUpdated: 0,
|
|
1657
|
+
edgesAdded,
|
|
1658
|
+
edgesUpdated: 0,
|
|
1659
|
+
errors,
|
|
1660
|
+
durationMs: Date.now() - start
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Parse markdown content and extract numbered items from recognized sections.
|
|
1665
|
+
*/
|
|
1666
|
+
extractRequirements(content, specPath, specHash, featureName) {
|
|
1667
|
+
const lines = content.split("\n");
|
|
1668
|
+
const results = [];
|
|
1669
|
+
let currentSection;
|
|
1670
|
+
let inRequirementSection = false;
|
|
1671
|
+
let globalIndex = 0;
|
|
1672
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1673
|
+
const line = lines[i];
|
|
1674
|
+
const headingMatch = line.match(SECTION_HEADING_RE);
|
|
1675
|
+
if (headingMatch) {
|
|
1676
|
+
const heading = headingMatch[1].trim();
|
|
1677
|
+
const isReqSection = REQUIREMENT_SECTIONS.some(
|
|
1678
|
+
(s) => heading.toLowerCase() === s.toLowerCase()
|
|
1679
|
+
);
|
|
1680
|
+
if (isReqSection) {
|
|
1681
|
+
currentSection = heading;
|
|
1682
|
+
inRequirementSection = true;
|
|
1683
|
+
} else {
|
|
1684
|
+
inRequirementSection = false;
|
|
1685
|
+
}
|
|
1686
|
+
continue;
|
|
1687
|
+
}
|
|
1688
|
+
if (!inRequirementSection) continue;
|
|
1689
|
+
const itemMatch = line.match(NUMBERED_ITEM_RE);
|
|
1690
|
+
if (!itemMatch) continue;
|
|
1691
|
+
const index = parseInt(itemMatch[1], 10);
|
|
1692
|
+
const text = itemMatch[2].trim();
|
|
1693
|
+
const rawText = line.trim();
|
|
1694
|
+
const lineNumber = i + 1;
|
|
1695
|
+
globalIndex++;
|
|
1696
|
+
const nodeId = `req:${specHash}:${globalIndex}`;
|
|
1697
|
+
const earsPattern = detectEarsPattern(text);
|
|
1698
|
+
results.push({
|
|
1699
|
+
node: {
|
|
1700
|
+
id: nodeId,
|
|
1701
|
+
type: "requirement",
|
|
1702
|
+
name: text,
|
|
1703
|
+
path: specPath,
|
|
1704
|
+
location: {
|
|
1705
|
+
fileId: `file:${specPath}`,
|
|
1706
|
+
startLine: lineNumber,
|
|
1707
|
+
endLine: lineNumber
|
|
1708
|
+
},
|
|
1709
|
+
metadata: {
|
|
1710
|
+
specPath,
|
|
1711
|
+
index,
|
|
1712
|
+
section: currentSection,
|
|
1713
|
+
rawText,
|
|
1714
|
+
earsPattern,
|
|
1715
|
+
featureName
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
return results;
|
|
1721
|
+
}
|
|
1722
|
+
/**
|
|
1723
|
+
* Convention-based linking: match requirement to code/test files
|
|
1724
|
+
* by feature name in their path.
|
|
1725
|
+
*/
|
|
1726
|
+
linkByPathPattern(reqId, featureName) {
|
|
1727
|
+
let count = 0;
|
|
1728
|
+
const fileNodes = this.store.findNodes({ type: "file" });
|
|
1729
|
+
for (const node of fileNodes) {
|
|
1730
|
+
if (!node.path) continue;
|
|
1731
|
+
const normalizedPath = node.path.replace(/\\/g, "/");
|
|
1732
|
+
const isCodeMatch = normalizedPath.includes("packages/") && path4.basename(normalizedPath).includes(featureName);
|
|
1733
|
+
const isTestMatch = normalizedPath.includes("/tests/") && // platform-safe
|
|
1734
|
+
path4.basename(normalizedPath).includes(featureName);
|
|
1735
|
+
if (isCodeMatch && !isTestMatch) {
|
|
1736
|
+
this.store.addEdge({
|
|
1737
|
+
from: reqId,
|
|
1738
|
+
to: node.id,
|
|
1739
|
+
type: "requires",
|
|
1740
|
+
confidence: 0.5,
|
|
1741
|
+
metadata: { method: "convention", matchReason: "path-pattern" }
|
|
1742
|
+
});
|
|
1743
|
+
count++;
|
|
1744
|
+
} else if (isTestMatch) {
|
|
1745
|
+
this.store.addEdge({
|
|
1746
|
+
from: reqId,
|
|
1747
|
+
to: node.id,
|
|
1748
|
+
type: "verified_by",
|
|
1749
|
+
confidence: 0.5,
|
|
1750
|
+
metadata: { method: "convention", matchReason: "path-pattern" }
|
|
1751
|
+
});
|
|
1752
|
+
count++;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
return count;
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Convention-based linking: match requirement text to code nodes
|
|
1759
|
+
* by keyword overlap (function/class names appearing in requirement text).
|
|
1760
|
+
*/
|
|
1761
|
+
linkByKeywordOverlap(reqId, reqText) {
|
|
1762
|
+
let count = 0;
|
|
1763
|
+
for (const nodeType of CODE_NODE_TYPES2) {
|
|
1764
|
+
const codeNodes = this.store.findNodes({ type: nodeType });
|
|
1765
|
+
for (const node of codeNodes) {
|
|
1766
|
+
if (node.name.length < 3) continue;
|
|
1767
|
+
const escaped = node.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1768
|
+
const namePattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
1769
|
+
if (namePattern.test(reqText)) {
|
|
1770
|
+
const edgeType = node.path?.replace(/\\/g, "/").includes("/tests/") ? "verified_by" : "requires";
|
|
1771
|
+
this.store.addEdge({
|
|
1772
|
+
from: reqId,
|
|
1773
|
+
to: node.id,
|
|
1774
|
+
type: edgeType,
|
|
1775
|
+
confidence: 0.6,
|
|
1776
|
+
metadata: { method: "convention", matchReason: "keyword-overlap" }
|
|
1777
|
+
});
|
|
1778
|
+
count++;
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
return count;
|
|
1783
|
+
}
|
|
1784
|
+
};
|
|
1785
|
+
|
|
1786
|
+
// src/ingest/connectors/ConnectorUtils.ts
|
|
1787
|
+
var CODE_NODE_TYPES3 = ["file", "function", "class", "method", "interface", "variable"];
|
|
1514
1788
|
var SANITIZE_RULES = [
|
|
1515
1789
|
// Strip XML/HTML-like instruction tags that could be interpreted as system prompts
|
|
1516
1790
|
{
|
|
@@ -1545,7 +1819,7 @@ function sanitizeExternalText(text, maxLength = 2e3) {
|
|
|
1545
1819
|
}
|
|
1546
1820
|
function linkToCode(store, content, sourceNodeId, edgeType, options) {
|
|
1547
1821
|
let edgesCreated = 0;
|
|
1548
|
-
for (const type of
|
|
1822
|
+
for (const type of CODE_NODE_TYPES3) {
|
|
1549
1823
|
const nodes = store.findNodes({ type });
|
|
1550
1824
|
for (const node of nodes) {
|
|
1551
1825
|
if (node.name.length < 3) continue;
|
|
@@ -1565,12 +1839,12 @@ function linkToCode(store, content, sourceNodeId, edgeType, options) {
|
|
|
1565
1839
|
}
|
|
1566
1840
|
|
|
1567
1841
|
// src/ingest/connectors/SyncManager.ts
|
|
1568
|
-
var
|
|
1569
|
-
var
|
|
1842
|
+
var fs4 = __toESM(require("fs/promises"));
|
|
1843
|
+
var path5 = __toESM(require("path"));
|
|
1570
1844
|
var SyncManager = class {
|
|
1571
1845
|
constructor(store, graphDir) {
|
|
1572
1846
|
this.store = store;
|
|
1573
|
-
this.metadataPath =
|
|
1847
|
+
this.metadataPath = path5.join(graphDir, "sync-metadata.json");
|
|
1574
1848
|
}
|
|
1575
1849
|
store;
|
|
1576
1850
|
registrations = /* @__PURE__ */ new Map();
|
|
@@ -1625,15 +1899,15 @@ var SyncManager = class {
|
|
|
1625
1899
|
}
|
|
1626
1900
|
async loadMetadata() {
|
|
1627
1901
|
try {
|
|
1628
|
-
const raw = await
|
|
1902
|
+
const raw = await fs4.readFile(this.metadataPath, "utf-8");
|
|
1629
1903
|
return JSON.parse(raw);
|
|
1630
1904
|
} catch {
|
|
1631
1905
|
return { connectors: {} };
|
|
1632
1906
|
}
|
|
1633
1907
|
}
|
|
1634
1908
|
async saveMetadata(metadata) {
|
|
1635
|
-
await
|
|
1636
|
-
await
|
|
1909
|
+
await fs4.mkdir(path5.dirname(this.metadataPath), { recursive: true });
|
|
1910
|
+
await fs4.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
1637
1911
|
}
|
|
1638
1912
|
};
|
|
1639
1913
|
|
|
@@ -2162,7 +2436,7 @@ var FusionLayer = class {
|
|
|
2162
2436
|
};
|
|
2163
2437
|
|
|
2164
2438
|
// src/entropy/GraphEntropyAdapter.ts
|
|
2165
|
-
var
|
|
2439
|
+
var CODE_NODE_TYPES4 = ["file", "function", "class", "method", "interface", "variable"];
|
|
2166
2440
|
var GraphEntropyAdapter = class {
|
|
2167
2441
|
constructor(store) {
|
|
2168
2442
|
this.store = store;
|
|
@@ -2229,7 +2503,7 @@ var GraphEntropyAdapter = class {
|
|
|
2229
2503
|
}
|
|
2230
2504
|
findEntryPoints() {
|
|
2231
2505
|
const entryPoints = [];
|
|
2232
|
-
for (const nodeType of
|
|
2506
|
+
for (const nodeType of CODE_NODE_TYPES4) {
|
|
2233
2507
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
2234
2508
|
for (const node of nodes) {
|
|
2235
2509
|
const isIndexFile = nodeType === "file" && node.name === "index.ts";
|
|
@@ -2265,7 +2539,7 @@ var GraphEntropyAdapter = class {
|
|
|
2265
2539
|
}
|
|
2266
2540
|
collectUnreachableNodes(visited) {
|
|
2267
2541
|
const unreachableNodes = [];
|
|
2268
|
-
for (const nodeType of
|
|
2542
|
+
for (const nodeType of CODE_NODE_TYPES4) {
|
|
2269
2543
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
2270
2544
|
for (const node of nodes) {
|
|
2271
2545
|
if (!visited.has(node.id)) {
|
|
@@ -2710,6 +2984,7 @@ var INTENT_SIGNALS = {
|
|
|
2710
2984
|
"depend",
|
|
2711
2985
|
"blast",
|
|
2712
2986
|
"radius",
|
|
2987
|
+
"cascade",
|
|
2713
2988
|
"risk",
|
|
2714
2989
|
"delete",
|
|
2715
2990
|
"remove"
|
|
@@ -2719,6 +2994,7 @@ var INTENT_SIGNALS = {
|
|
|
2719
2994
|
/what\s+(breaks|happens|is affected)/,
|
|
2720
2995
|
/if\s+i\s+(change|modify|remove|delete)/,
|
|
2721
2996
|
/blast\s+radius/,
|
|
2997
|
+
/cascad/,
|
|
2722
2998
|
/what\s+(depend|relies)/
|
|
2723
2999
|
]
|
|
2724
3000
|
},
|
|
@@ -3085,9 +3361,9 @@ var EntityExtractor = class {
|
|
|
3085
3361
|
}
|
|
3086
3362
|
const pathConsumed = /* @__PURE__ */ new Set();
|
|
3087
3363
|
for (const match of trimmed.matchAll(FILE_PATH_RE)) {
|
|
3088
|
-
const
|
|
3089
|
-
add(
|
|
3090
|
-
pathConsumed.add(
|
|
3364
|
+
const path7 = match[0];
|
|
3365
|
+
add(path7);
|
|
3366
|
+
pathConsumed.add(path7);
|
|
3091
3367
|
}
|
|
3092
3368
|
const allConsumed = buildConsumedSet(quotedConsumed, casingConsumed, pathConsumed);
|
|
3093
3369
|
const words = trimmed.split(/\s+/);
|
|
@@ -3158,8 +3434,8 @@ var EntityResolver = class {
|
|
|
3158
3434
|
if (isPathLike && node.path.includes(raw)) {
|
|
3159
3435
|
return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
|
|
3160
3436
|
}
|
|
3161
|
-
const
|
|
3162
|
-
if (
|
|
3437
|
+
const basename5 = node.path.split("/").pop() ?? "";
|
|
3438
|
+
if (basename5.includes(raw)) {
|
|
3163
3439
|
return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
|
|
3164
3440
|
}
|
|
3165
3441
|
if (raw.length >= 4 && node.path.includes(raw)) {
|
|
@@ -3204,6 +3480,10 @@ var ResponseFormatter = class {
|
|
|
3204
3480
|
}
|
|
3205
3481
|
formatImpact(entityName, data) {
|
|
3206
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
|
+
}
|
|
3207
3487
|
const code = this.safeArrayLength(d?.code);
|
|
3208
3488
|
const tests = this.safeArrayLength(d?.tests);
|
|
3209
3489
|
const docs = this.safeArrayLength(d?.docs);
|
|
@@ -3234,13 +3514,13 @@ var ResponseFormatter = class {
|
|
|
3234
3514
|
const context = Array.isArray(d?.context) ? d.context : [];
|
|
3235
3515
|
const firstEntity = entities[0];
|
|
3236
3516
|
const nodeType = firstEntity?.node.type ?? "node";
|
|
3237
|
-
const
|
|
3517
|
+
const path7 = firstEntity?.node.path ?? "unknown";
|
|
3238
3518
|
let neighborCount = 0;
|
|
3239
3519
|
const firstContext = context[0];
|
|
3240
3520
|
if (firstContext && Array.isArray(firstContext.nodes)) {
|
|
3241
3521
|
neighborCount = firstContext.nodes.length;
|
|
3242
3522
|
}
|
|
3243
|
-
return `**${entityName}** is a ${nodeType} at \`${
|
|
3523
|
+
return `**${entityName}** is a ${nodeType} at \`${path7}\`. Connected to ${neighborCount} nodes.`;
|
|
3244
3524
|
}
|
|
3245
3525
|
formatAnomaly(data) {
|
|
3246
3526
|
const d = data;
|
|
@@ -3265,6 +3545,246 @@ var ResponseFormatter = class {
|
|
|
3265
3545
|
}
|
|
3266
3546
|
};
|
|
3267
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
|
+
|
|
3268
3788
|
// src/nlq/index.ts
|
|
3269
3789
|
var ENTITY_REQUIRED_INTENTS = /* @__PURE__ */ new Set(["impact", "relationships", "explain"]);
|
|
3270
3790
|
var classifier = new IntentClassifier();
|
|
@@ -3327,6 +3847,11 @@ function executeOperation(store, intent, entities, question, fusion) {
|
|
|
3327
3847
|
switch (intent) {
|
|
3328
3848
|
case "impact": {
|
|
3329
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
|
+
}
|
|
3330
3855
|
const result = cql.execute({
|
|
3331
3856
|
rootNodeIds: [rootId],
|
|
3332
3857
|
bidirectional: true,
|
|
@@ -3381,7 +3906,7 @@ var PHASE_NODE_TYPES = {
|
|
|
3381
3906
|
debug: ["failure", "learning", "function", "method"],
|
|
3382
3907
|
plan: ["adr", "document", "module", "layer"]
|
|
3383
3908
|
};
|
|
3384
|
-
var
|
|
3909
|
+
var CODE_NODE_TYPES5 = /* @__PURE__ */ new Set([
|
|
3385
3910
|
"file",
|
|
3386
3911
|
"function",
|
|
3387
3912
|
"class",
|
|
@@ -3596,7 +4121,7 @@ var Assembler = class {
|
|
|
3596
4121
|
*/
|
|
3597
4122
|
checkCoverage() {
|
|
3598
4123
|
const codeNodes = [];
|
|
3599
|
-
for (const type of
|
|
4124
|
+
for (const type of CODE_NODE_TYPES5) {
|
|
3600
4125
|
codeNodes.push(...this.store.findNodes({ type }));
|
|
3601
4126
|
}
|
|
3602
4127
|
const documented = [];
|
|
@@ -3620,6 +4145,99 @@ var Assembler = class {
|
|
|
3620
4145
|
}
|
|
3621
4146
|
};
|
|
3622
4147
|
|
|
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
|
+
}
|
|
4202
|
+
function queryTraceability(store, options) {
|
|
4203
|
+
const allRequirements = store.findNodes({ type: "requirement" });
|
|
4204
|
+
const filtered = allRequirements.filter((node) => {
|
|
4205
|
+
if (options?.specPath && node.metadata?.specPath !== options.specPath) return false;
|
|
4206
|
+
if (options?.featureName && node.metadata?.featureName !== options.featureName) return false;
|
|
4207
|
+
return true;
|
|
4208
|
+
});
|
|
4209
|
+
if (filtered.length === 0) return [];
|
|
4210
|
+
const groups = /* @__PURE__ */ new Map();
|
|
4211
|
+
for (const req of filtered) {
|
|
4212
|
+
const meta = req.metadata;
|
|
4213
|
+
const specPath = meta?.specPath ?? "";
|
|
4214
|
+
const featureName = meta?.featureName ?? "";
|
|
4215
|
+
const key = `${specPath}\0${featureName}`;
|
|
4216
|
+
const list = groups.get(key);
|
|
4217
|
+
if (list) {
|
|
4218
|
+
list.push(req);
|
|
4219
|
+
} else {
|
|
4220
|
+
groups.set(key, [req]);
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
4223
|
+
const results = [];
|
|
4224
|
+
for (const [, reqs] of groups) {
|
|
4225
|
+
const firstReq = reqs[0];
|
|
4226
|
+
const firstMeta = firstReq.metadata;
|
|
4227
|
+
const specPath = firstMeta?.specPath ?? "";
|
|
4228
|
+
const featureName = firstMeta?.featureName ?? "";
|
|
4229
|
+
const requirements = reqs.map((req) => buildRequirementCoverage(store, req));
|
|
4230
|
+
requirements.sort((a, b) => a.index - b.index);
|
|
4231
|
+
results.push({
|
|
4232
|
+
specPath,
|
|
4233
|
+
featureName,
|
|
4234
|
+
requirements,
|
|
4235
|
+
summary: computeSummary(requirements)
|
|
4236
|
+
});
|
|
4237
|
+
}
|
|
4238
|
+
return results;
|
|
4239
|
+
}
|
|
4240
|
+
|
|
3623
4241
|
// src/constraints/GraphConstraintAdapter.ts
|
|
3624
4242
|
var import_minimatch = require("minimatch");
|
|
3625
4243
|
var import_node_path2 = require("path");
|
|
@@ -3679,14 +4297,14 @@ var GraphConstraintAdapter = class {
|
|
|
3679
4297
|
};
|
|
3680
4298
|
|
|
3681
4299
|
// src/ingest/DesignIngestor.ts
|
|
3682
|
-
var
|
|
3683
|
-
var
|
|
4300
|
+
var fs5 = __toESM(require("fs/promises"));
|
|
4301
|
+
var path6 = __toESM(require("path"));
|
|
3684
4302
|
function isDTCGToken(obj) {
|
|
3685
4303
|
return typeof obj === "object" && obj !== null && "$value" in obj && "$type" in obj;
|
|
3686
4304
|
}
|
|
3687
4305
|
async function readFileOrNull(filePath) {
|
|
3688
4306
|
try {
|
|
3689
|
-
return await
|
|
4307
|
+
return await fs5.readFile(filePath, "utf-8");
|
|
3690
4308
|
} catch {
|
|
3691
4309
|
return null;
|
|
3692
4310
|
}
|
|
@@ -3832,8 +4450,8 @@ var DesignIngestor = class {
|
|
|
3832
4450
|
async ingestAll(designDir) {
|
|
3833
4451
|
const start = Date.now();
|
|
3834
4452
|
const [tokensResult, intentResult] = await Promise.all([
|
|
3835
|
-
this.ingestTokens(
|
|
3836
|
-
this.ingestDesignIntent(
|
|
4453
|
+
this.ingestTokens(path6.join(designDir, "tokens.json")),
|
|
4454
|
+
this.ingestDesignIntent(path6.join(designDir, "DESIGN.md"))
|
|
3837
4455
|
]);
|
|
3838
4456
|
const merged = mergeResults(tokensResult, intentResult);
|
|
3839
4457
|
return { ...merged, durationMs: Date.now() - start };
|
|
@@ -4082,10 +4700,10 @@ var TaskIndependenceAnalyzer = class {
|
|
|
4082
4700
|
includeTypes: ["file"]
|
|
4083
4701
|
});
|
|
4084
4702
|
for (const n of queryResult.nodes) {
|
|
4085
|
-
const
|
|
4086
|
-
if (!fileSet.has(
|
|
4087
|
-
if (!result.has(
|
|
4088
|
-
result.set(
|
|
4703
|
+
const path7 = n.path ?? n.id.replace(/^file:/, "");
|
|
4704
|
+
if (!fileSet.has(path7)) {
|
|
4705
|
+
if (!result.has(path7)) {
|
|
4706
|
+
result.set(path7, file);
|
|
4089
4707
|
}
|
|
4090
4708
|
}
|
|
4091
4709
|
}
|
|
@@ -4438,13 +5056,15 @@ var ConflictPredictor = class {
|
|
|
4438
5056
|
};
|
|
4439
5057
|
|
|
4440
5058
|
// src/index.ts
|
|
4441
|
-
var VERSION = "0.
|
|
5059
|
+
var VERSION = "0.4.0";
|
|
4442
5060
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4443
5061
|
0 && (module.exports = {
|
|
4444
5062
|
Assembler,
|
|
4445
5063
|
CIConnector,
|
|
4446
5064
|
CURRENT_SCHEMA_VERSION,
|
|
5065
|
+
CascadeSimulator,
|
|
4447
5066
|
CodeIngestor,
|
|
5067
|
+
CompositeProbabilityStrategy,
|
|
4448
5068
|
ConflictPredictor,
|
|
4449
5069
|
ConfluenceConnector,
|
|
4450
5070
|
ContextQL,
|
|
@@ -4470,6 +5090,7 @@ var VERSION = "0.2.0";
|
|
|
4470
5090
|
KnowledgeIngestor,
|
|
4471
5091
|
NODE_TYPES,
|
|
4472
5092
|
OBSERVABILITY_TYPES,
|
|
5093
|
+
RequirementIngestor,
|
|
4473
5094
|
ResponseFormatter,
|
|
4474
5095
|
SlackConnector,
|
|
4475
5096
|
SyncManager,
|
|
@@ -4478,9 +5099,11 @@ var VERSION = "0.2.0";
|
|
|
4478
5099
|
VERSION,
|
|
4479
5100
|
VectorStore,
|
|
4480
5101
|
askGraph,
|
|
5102
|
+
classifyNodeCategory,
|
|
4481
5103
|
groupNodesByImpact,
|
|
4482
5104
|
linkToCode,
|
|
4483
5105
|
loadGraph,
|
|
4484
5106
|
project,
|
|
5107
|
+
queryTraceability,
|
|
4485
5108
|
saveGraph
|
|
4486
5109
|
});
|