@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.mjs
CHANGED
|
@@ -35,7 +35,9 @@ var NODE_TYPES = [
|
|
|
35
35
|
// Design
|
|
36
36
|
"design_token",
|
|
37
37
|
"aesthetic_intent",
|
|
38
|
-
"design_constraint"
|
|
38
|
+
"design_constraint",
|
|
39
|
+
// Traceability
|
|
40
|
+
"requirement"
|
|
39
41
|
];
|
|
40
42
|
var EDGE_TYPES = [
|
|
41
43
|
// Code relationships
|
|
@@ -64,7 +66,11 @@ var EDGE_TYPES = [
|
|
|
64
66
|
"uses_token",
|
|
65
67
|
"declares_intent",
|
|
66
68
|
"violates_design",
|
|
67
|
-
"platform_binding"
|
|
69
|
+
"platform_binding",
|
|
70
|
+
// Traceability relationships
|
|
71
|
+
"requires",
|
|
72
|
+
"verified_by",
|
|
73
|
+
"tested_by"
|
|
68
74
|
];
|
|
69
75
|
var OBSERVABILITY_TYPES = /* @__PURE__ */ new Set(["span", "metric", "log"]);
|
|
70
76
|
var CURRENT_SCHEMA_VERSION = 1;
|
|
@@ -156,6 +162,16 @@ function removeFromIndex(index, key, edge) {
|
|
|
156
162
|
if (idx !== -1) list.splice(idx, 1);
|
|
157
163
|
if (list.length === 0) index.delete(key);
|
|
158
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
|
+
}
|
|
159
175
|
var GraphStore = class {
|
|
160
176
|
nodeMap = /* @__PURE__ */ new Map();
|
|
161
177
|
edgeMap = /* @__PURE__ */ new Map();
|
|
@@ -223,27 +239,25 @@ var GraphStore = class {
|
|
|
223
239
|
}
|
|
224
240
|
}
|
|
225
241
|
getEdges(query) {
|
|
226
|
-
let candidates;
|
|
227
242
|
if (query.from !== void 0 && query.to !== void 0 && query.type !== void 0) {
|
|
228
243
|
const edge = this.edgeMap.get(edgeKey(query.from, query.to, query.type));
|
|
229
244
|
return edge ? [{ ...edge }] : [];
|
|
230
|
-
} else if (query.from !== void 0) {
|
|
231
|
-
candidates = this.edgesByFrom.get(query.from) ?? [];
|
|
232
|
-
} else if (query.to !== void 0) {
|
|
233
|
-
candidates = this.edgesByTo.get(query.to) ?? [];
|
|
234
|
-
} else if (query.type !== void 0) {
|
|
235
|
-
candidates = this.edgesByType.get(query.type) ?? [];
|
|
236
|
-
} else {
|
|
237
|
-
candidates = this.edgeMap.values();
|
|
238
245
|
}
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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) ?? [];
|
|
245
253
|
}
|
|
246
|
-
|
|
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();
|
|
247
261
|
}
|
|
248
262
|
getNeighbors(nodeId, direction = "both") {
|
|
249
263
|
const neighborIds = /* @__PURE__ */ new Set();
|
|
@@ -539,6 +553,12 @@ var CODE_TYPES = /* @__PURE__ */ new Set([
|
|
|
539
553
|
"method",
|
|
540
554
|
"variable"
|
|
541
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
|
+
}
|
|
542
562
|
function groupNodesByImpact(nodes, excludeId) {
|
|
543
563
|
const tests = [];
|
|
544
564
|
const docs = [];
|
|
@@ -546,15 +566,11 @@ function groupNodesByImpact(nodes, excludeId) {
|
|
|
546
566
|
const other = [];
|
|
547
567
|
for (const node of nodes) {
|
|
548
568
|
if (excludeId && node.id === excludeId) continue;
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
code.push(node);
|
|
555
|
-
} else {
|
|
556
|
-
other.push(node);
|
|
557
|
-
}
|
|
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);
|
|
558
574
|
}
|
|
559
575
|
return { tests, docs, code, other };
|
|
560
576
|
}
|
|
@@ -598,6 +614,7 @@ var CodeIngestor = class {
|
|
|
598
614
|
this.store.addEdge(edge);
|
|
599
615
|
edgesAdded++;
|
|
600
616
|
}
|
|
617
|
+
edgesAdded += this.extractReqAnnotations(fileContents, rootDir);
|
|
601
618
|
return {
|
|
602
619
|
nodesAdded,
|
|
603
620
|
nodesUpdated: 0,
|
|
@@ -956,6 +973,48 @@ var CodeIngestor = class {
|
|
|
956
973
|
if (/\.jsx?$/.test(filePath)) return "javascript";
|
|
957
974
|
return "unknown";
|
|
958
975
|
}
|
|
976
|
+
/**
|
|
977
|
+
* Scan file contents for @req annotations and create verified_by edges
|
|
978
|
+
* linking requirement nodes to the annotated files.
|
|
979
|
+
* Format: // @req <feature-name>#<index>
|
|
980
|
+
*/
|
|
981
|
+
extractReqAnnotations(fileContents, rootDir) {
|
|
982
|
+
const REQ_TAG = /\/\/\s*@req\s+([\w-]+)#(\d+)/g;
|
|
983
|
+
const reqNodes = this.store.findNodes({ type: "requirement" });
|
|
984
|
+
let edgesAdded = 0;
|
|
985
|
+
for (const [filePath, content] of fileContents) {
|
|
986
|
+
let match;
|
|
987
|
+
REQ_TAG.lastIndex = 0;
|
|
988
|
+
while ((match = REQ_TAG.exec(content)) !== null) {
|
|
989
|
+
const featureName = match[1];
|
|
990
|
+
const reqIndex = parseInt(match[2], 10);
|
|
991
|
+
const reqNode = reqNodes.find(
|
|
992
|
+
(n) => n.metadata.featureName === featureName && n.metadata.index === reqIndex
|
|
993
|
+
);
|
|
994
|
+
if (!reqNode) {
|
|
995
|
+
console.warn(
|
|
996
|
+
`@req annotation references non-existent requirement: ${featureName}#${reqIndex} in ${filePath}`
|
|
997
|
+
);
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
const relPath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
1001
|
+
const fileNodeId = `file:${relPath}`;
|
|
1002
|
+
this.store.addEdge({
|
|
1003
|
+
from: reqNode.id,
|
|
1004
|
+
to: fileNodeId,
|
|
1005
|
+
type: "verified_by",
|
|
1006
|
+
confidence: 1,
|
|
1007
|
+
metadata: {
|
|
1008
|
+
method: "annotation",
|
|
1009
|
+
tag: `@req ${featureName}#${reqIndex}`,
|
|
1010
|
+
confidence: 1
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
1013
|
+
edgesAdded++;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return edgesAdded;
|
|
1017
|
+
}
|
|
959
1018
|
};
|
|
960
1019
|
|
|
961
1020
|
// src/ingest/GitIngestor.ts
|
|
@@ -1432,8 +1491,218 @@ var KnowledgeIngestor = class {
|
|
|
1432
1491
|
}
|
|
1433
1492
|
};
|
|
1434
1493
|
|
|
1435
|
-
// src/ingest/
|
|
1494
|
+
// src/ingest/RequirementIngestor.ts
|
|
1495
|
+
import * as fs3 from "fs/promises";
|
|
1496
|
+
import * as path4 from "path";
|
|
1497
|
+
var REQUIREMENT_SECTIONS = [
|
|
1498
|
+
"Observable Truths",
|
|
1499
|
+
"Success Criteria",
|
|
1500
|
+
"Acceptance Criteria"
|
|
1501
|
+
];
|
|
1502
|
+
var SECTION_HEADING_RE = /^#{2,3}\s+(.+)$/;
|
|
1503
|
+
var NUMBERED_ITEM_RE = /^\s*(\d+)\.\s+(.+)$/;
|
|
1504
|
+
function detectEarsPattern(text) {
|
|
1505
|
+
const lower = text.toLowerCase();
|
|
1506
|
+
if (/^if\b.+\bthen\b.+\bshall not\b/.test(lower)) return "unwanted";
|
|
1507
|
+
if (/^when\b/.test(lower)) return "event-driven";
|
|
1508
|
+
if (/^while\b/.test(lower)) return "state-driven";
|
|
1509
|
+
if (/^where\b/.test(lower)) return "optional";
|
|
1510
|
+
if (/^the\s+\w+\s+shall\b/.test(lower)) return "ubiquitous";
|
|
1511
|
+
return void 0;
|
|
1512
|
+
}
|
|
1436
1513
|
var CODE_NODE_TYPES2 = ["file", "function", "class", "method", "interface", "variable"];
|
|
1514
|
+
var RequirementIngestor = class {
|
|
1515
|
+
constructor(store) {
|
|
1516
|
+
this.store = store;
|
|
1517
|
+
}
|
|
1518
|
+
store;
|
|
1519
|
+
/**
|
|
1520
|
+
* Scan a specs directory for `<feature>/proposal.md` files,
|
|
1521
|
+
* extract numbered requirements from recognized sections,
|
|
1522
|
+
* and create requirement nodes with convention-based edges.
|
|
1523
|
+
*/
|
|
1524
|
+
async ingestSpecs(specsDir) {
|
|
1525
|
+
const start = Date.now();
|
|
1526
|
+
const errors = [];
|
|
1527
|
+
let nodesAdded = 0;
|
|
1528
|
+
let edgesAdded = 0;
|
|
1529
|
+
let featureDirs;
|
|
1530
|
+
try {
|
|
1531
|
+
const entries = await fs3.readdir(specsDir, { withFileTypes: true });
|
|
1532
|
+
featureDirs = entries.filter((e) => e.isDirectory()).map((e) => path4.join(specsDir, e.name));
|
|
1533
|
+
} catch {
|
|
1534
|
+
return emptyResult(Date.now() - start);
|
|
1535
|
+
}
|
|
1536
|
+
for (const featureDir of featureDirs) {
|
|
1537
|
+
const featureName = path4.basename(featureDir);
|
|
1538
|
+
const specPath = path4.join(featureDir, "proposal.md").replaceAll("\\", "/");
|
|
1539
|
+
let content;
|
|
1540
|
+
try {
|
|
1541
|
+
content = await fs3.readFile(specPath, "utf-8");
|
|
1542
|
+
} catch {
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1545
|
+
try {
|
|
1546
|
+
const specHash = hash(specPath);
|
|
1547
|
+
const specNodeId = `file:${specPath}`;
|
|
1548
|
+
this.store.addNode({
|
|
1549
|
+
id: specNodeId,
|
|
1550
|
+
type: "document",
|
|
1551
|
+
name: path4.basename(specPath),
|
|
1552
|
+
path: specPath,
|
|
1553
|
+
metadata: { featureName }
|
|
1554
|
+
});
|
|
1555
|
+
const requirements = this.extractRequirements(content, specPath, specHash, featureName);
|
|
1556
|
+
for (const req of requirements) {
|
|
1557
|
+
this.store.addNode(req.node);
|
|
1558
|
+
nodesAdded++;
|
|
1559
|
+
this.store.addEdge({
|
|
1560
|
+
from: req.node.id,
|
|
1561
|
+
to: specNodeId,
|
|
1562
|
+
type: "specifies"
|
|
1563
|
+
});
|
|
1564
|
+
edgesAdded++;
|
|
1565
|
+
edgesAdded += this.linkByPathPattern(req.node.id, featureName);
|
|
1566
|
+
edgesAdded += this.linkByKeywordOverlap(req.node.id, req.node.name);
|
|
1567
|
+
}
|
|
1568
|
+
} catch (err) {
|
|
1569
|
+
errors.push(`${specPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
return {
|
|
1573
|
+
nodesAdded,
|
|
1574
|
+
nodesUpdated: 0,
|
|
1575
|
+
edgesAdded,
|
|
1576
|
+
edgesUpdated: 0,
|
|
1577
|
+
errors,
|
|
1578
|
+
durationMs: Date.now() - start
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Parse markdown content and extract numbered items from recognized sections.
|
|
1583
|
+
*/
|
|
1584
|
+
extractRequirements(content, specPath, specHash, featureName) {
|
|
1585
|
+
const lines = content.split("\n");
|
|
1586
|
+
const results = [];
|
|
1587
|
+
let currentSection;
|
|
1588
|
+
let inRequirementSection = false;
|
|
1589
|
+
let globalIndex = 0;
|
|
1590
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1591
|
+
const line = lines[i];
|
|
1592
|
+
const headingMatch = line.match(SECTION_HEADING_RE);
|
|
1593
|
+
if (headingMatch) {
|
|
1594
|
+
const heading = headingMatch[1].trim();
|
|
1595
|
+
const isReqSection = REQUIREMENT_SECTIONS.some(
|
|
1596
|
+
(s) => heading.toLowerCase() === s.toLowerCase()
|
|
1597
|
+
);
|
|
1598
|
+
if (isReqSection) {
|
|
1599
|
+
currentSection = heading;
|
|
1600
|
+
inRequirementSection = true;
|
|
1601
|
+
} else {
|
|
1602
|
+
inRequirementSection = false;
|
|
1603
|
+
}
|
|
1604
|
+
continue;
|
|
1605
|
+
}
|
|
1606
|
+
if (!inRequirementSection) continue;
|
|
1607
|
+
const itemMatch = line.match(NUMBERED_ITEM_RE);
|
|
1608
|
+
if (!itemMatch) continue;
|
|
1609
|
+
const index = parseInt(itemMatch[1], 10);
|
|
1610
|
+
const text = itemMatch[2].trim();
|
|
1611
|
+
const rawText = line.trim();
|
|
1612
|
+
const lineNumber = i + 1;
|
|
1613
|
+
globalIndex++;
|
|
1614
|
+
const nodeId = `req:${specHash}:${globalIndex}`;
|
|
1615
|
+
const earsPattern = detectEarsPattern(text);
|
|
1616
|
+
results.push({
|
|
1617
|
+
node: {
|
|
1618
|
+
id: nodeId,
|
|
1619
|
+
type: "requirement",
|
|
1620
|
+
name: text,
|
|
1621
|
+
path: specPath,
|
|
1622
|
+
location: {
|
|
1623
|
+
fileId: `file:${specPath}`,
|
|
1624
|
+
startLine: lineNumber,
|
|
1625
|
+
endLine: lineNumber
|
|
1626
|
+
},
|
|
1627
|
+
metadata: {
|
|
1628
|
+
specPath,
|
|
1629
|
+
index,
|
|
1630
|
+
section: currentSection,
|
|
1631
|
+
rawText,
|
|
1632
|
+
earsPattern,
|
|
1633
|
+
featureName
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
return results;
|
|
1639
|
+
}
|
|
1640
|
+
/**
|
|
1641
|
+
* Convention-based linking: match requirement to code/test files
|
|
1642
|
+
* by feature name in their path.
|
|
1643
|
+
*/
|
|
1644
|
+
linkByPathPattern(reqId, featureName) {
|
|
1645
|
+
let count = 0;
|
|
1646
|
+
const fileNodes = this.store.findNodes({ type: "file" });
|
|
1647
|
+
for (const node of fileNodes) {
|
|
1648
|
+
if (!node.path) continue;
|
|
1649
|
+
const normalizedPath = node.path.replace(/\\/g, "/");
|
|
1650
|
+
const isCodeMatch = normalizedPath.includes("packages/") && path4.basename(normalizedPath).includes(featureName);
|
|
1651
|
+
const isTestMatch = normalizedPath.includes("/tests/") && // platform-safe
|
|
1652
|
+
path4.basename(normalizedPath).includes(featureName);
|
|
1653
|
+
if (isCodeMatch && !isTestMatch) {
|
|
1654
|
+
this.store.addEdge({
|
|
1655
|
+
from: reqId,
|
|
1656
|
+
to: node.id,
|
|
1657
|
+
type: "requires",
|
|
1658
|
+
confidence: 0.5,
|
|
1659
|
+
metadata: { method: "convention", matchReason: "path-pattern" }
|
|
1660
|
+
});
|
|
1661
|
+
count++;
|
|
1662
|
+
} else if (isTestMatch) {
|
|
1663
|
+
this.store.addEdge({
|
|
1664
|
+
from: reqId,
|
|
1665
|
+
to: node.id,
|
|
1666
|
+
type: "verified_by",
|
|
1667
|
+
confidence: 0.5,
|
|
1668
|
+
metadata: { method: "convention", matchReason: "path-pattern" }
|
|
1669
|
+
});
|
|
1670
|
+
count++;
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
return count;
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* Convention-based linking: match requirement text to code nodes
|
|
1677
|
+
* by keyword overlap (function/class names appearing in requirement text).
|
|
1678
|
+
*/
|
|
1679
|
+
linkByKeywordOverlap(reqId, reqText) {
|
|
1680
|
+
let count = 0;
|
|
1681
|
+
for (const nodeType of CODE_NODE_TYPES2) {
|
|
1682
|
+
const codeNodes = this.store.findNodes({ type: nodeType });
|
|
1683
|
+
for (const node of codeNodes) {
|
|
1684
|
+
if (node.name.length < 3) continue;
|
|
1685
|
+
const escaped = node.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1686
|
+
const namePattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
1687
|
+
if (namePattern.test(reqText)) {
|
|
1688
|
+
const edgeType = node.path?.replace(/\\/g, "/").includes("/tests/") ? "verified_by" : "requires";
|
|
1689
|
+
this.store.addEdge({
|
|
1690
|
+
from: reqId,
|
|
1691
|
+
to: node.id,
|
|
1692
|
+
type: edgeType,
|
|
1693
|
+
confidence: 0.6,
|
|
1694
|
+
metadata: { method: "convention", matchReason: "keyword-overlap" }
|
|
1695
|
+
});
|
|
1696
|
+
count++;
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
return count;
|
|
1701
|
+
}
|
|
1702
|
+
};
|
|
1703
|
+
|
|
1704
|
+
// src/ingest/connectors/ConnectorUtils.ts
|
|
1705
|
+
var CODE_NODE_TYPES3 = ["file", "function", "class", "method", "interface", "variable"];
|
|
1437
1706
|
var SANITIZE_RULES = [
|
|
1438
1707
|
// Strip XML/HTML-like instruction tags that could be interpreted as system prompts
|
|
1439
1708
|
{
|
|
@@ -1468,7 +1737,7 @@ function sanitizeExternalText(text, maxLength = 2e3) {
|
|
|
1468
1737
|
}
|
|
1469
1738
|
function linkToCode(store, content, sourceNodeId, edgeType, options) {
|
|
1470
1739
|
let edgesCreated = 0;
|
|
1471
|
-
for (const type of
|
|
1740
|
+
for (const type of CODE_NODE_TYPES3) {
|
|
1472
1741
|
const nodes = store.findNodes({ type });
|
|
1473
1742
|
for (const node of nodes) {
|
|
1474
1743
|
if (node.name.length < 3) continue;
|
|
@@ -1488,12 +1757,12 @@ function linkToCode(store, content, sourceNodeId, edgeType, options) {
|
|
|
1488
1757
|
}
|
|
1489
1758
|
|
|
1490
1759
|
// src/ingest/connectors/SyncManager.ts
|
|
1491
|
-
import * as
|
|
1492
|
-
import * as
|
|
1760
|
+
import * as fs4 from "fs/promises";
|
|
1761
|
+
import * as path5 from "path";
|
|
1493
1762
|
var SyncManager = class {
|
|
1494
1763
|
constructor(store, graphDir) {
|
|
1495
1764
|
this.store = store;
|
|
1496
|
-
this.metadataPath =
|
|
1765
|
+
this.metadataPath = path5.join(graphDir, "sync-metadata.json");
|
|
1497
1766
|
}
|
|
1498
1767
|
store;
|
|
1499
1768
|
registrations = /* @__PURE__ */ new Map();
|
|
@@ -1548,15 +1817,15 @@ var SyncManager = class {
|
|
|
1548
1817
|
}
|
|
1549
1818
|
async loadMetadata() {
|
|
1550
1819
|
try {
|
|
1551
|
-
const raw = await
|
|
1820
|
+
const raw = await fs4.readFile(this.metadataPath, "utf-8");
|
|
1552
1821
|
return JSON.parse(raw);
|
|
1553
1822
|
} catch {
|
|
1554
1823
|
return { connectors: {} };
|
|
1555
1824
|
}
|
|
1556
1825
|
}
|
|
1557
1826
|
async saveMetadata(metadata) {
|
|
1558
|
-
await
|
|
1559
|
-
await
|
|
1827
|
+
await fs4.mkdir(path5.dirname(this.metadataPath), { recursive: true });
|
|
1828
|
+
await fs4.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
1560
1829
|
}
|
|
1561
1830
|
};
|
|
1562
1831
|
|
|
@@ -2085,7 +2354,7 @@ var FusionLayer = class {
|
|
|
2085
2354
|
};
|
|
2086
2355
|
|
|
2087
2356
|
// src/entropy/GraphEntropyAdapter.ts
|
|
2088
|
-
var
|
|
2357
|
+
var CODE_NODE_TYPES4 = ["file", "function", "class", "method", "interface", "variable"];
|
|
2089
2358
|
var GraphEntropyAdapter = class {
|
|
2090
2359
|
constructor(store) {
|
|
2091
2360
|
this.store = store;
|
|
@@ -2152,7 +2421,7 @@ var GraphEntropyAdapter = class {
|
|
|
2152
2421
|
}
|
|
2153
2422
|
findEntryPoints() {
|
|
2154
2423
|
const entryPoints = [];
|
|
2155
|
-
for (const nodeType of
|
|
2424
|
+
for (const nodeType of CODE_NODE_TYPES4) {
|
|
2156
2425
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
2157
2426
|
for (const node of nodes) {
|
|
2158
2427
|
const isIndexFile = nodeType === "file" && node.name === "index.ts";
|
|
@@ -2188,7 +2457,7 @@ var GraphEntropyAdapter = class {
|
|
|
2188
2457
|
}
|
|
2189
2458
|
collectUnreachableNodes(visited) {
|
|
2190
2459
|
const unreachableNodes = [];
|
|
2191
|
-
for (const nodeType of
|
|
2460
|
+
for (const nodeType of CODE_NODE_TYPES4) {
|
|
2192
2461
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
2193
2462
|
for (const node of nodes) {
|
|
2194
2463
|
if (!visited.has(node.id)) {
|
|
@@ -2633,6 +2902,7 @@ var INTENT_SIGNALS = {
|
|
|
2633
2902
|
"depend",
|
|
2634
2903
|
"blast",
|
|
2635
2904
|
"radius",
|
|
2905
|
+
"cascade",
|
|
2636
2906
|
"risk",
|
|
2637
2907
|
"delete",
|
|
2638
2908
|
"remove"
|
|
@@ -2642,6 +2912,7 @@ var INTENT_SIGNALS = {
|
|
|
2642
2912
|
/what\s+(breaks|happens|is affected)/,
|
|
2643
2913
|
/if\s+i\s+(change|modify|remove|delete)/,
|
|
2644
2914
|
/blast\s+radius/,
|
|
2915
|
+
/cascad/,
|
|
2645
2916
|
/what\s+(depend|relies)/
|
|
2646
2917
|
]
|
|
2647
2918
|
},
|
|
@@ -3008,9 +3279,9 @@ var EntityExtractor = class {
|
|
|
3008
3279
|
}
|
|
3009
3280
|
const pathConsumed = /* @__PURE__ */ new Set();
|
|
3010
3281
|
for (const match of trimmed.matchAll(FILE_PATH_RE)) {
|
|
3011
|
-
const
|
|
3012
|
-
add(
|
|
3013
|
-
pathConsumed.add(
|
|
3282
|
+
const path7 = match[0];
|
|
3283
|
+
add(path7);
|
|
3284
|
+
pathConsumed.add(path7);
|
|
3014
3285
|
}
|
|
3015
3286
|
const allConsumed = buildConsumedSet(quotedConsumed, casingConsumed, pathConsumed);
|
|
3016
3287
|
const words = trimmed.split(/\s+/);
|
|
@@ -3081,8 +3352,8 @@ var EntityResolver = class {
|
|
|
3081
3352
|
if (isPathLike && node.path.includes(raw)) {
|
|
3082
3353
|
return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
|
|
3083
3354
|
}
|
|
3084
|
-
const
|
|
3085
|
-
if (
|
|
3355
|
+
const basename5 = node.path.split("/").pop() ?? "";
|
|
3356
|
+
if (basename5.includes(raw)) {
|
|
3086
3357
|
return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
|
|
3087
3358
|
}
|
|
3088
3359
|
if (raw.length >= 4 && node.path.includes(raw)) {
|
|
@@ -3127,6 +3398,10 @@ var ResponseFormatter = class {
|
|
|
3127
3398
|
}
|
|
3128
3399
|
formatImpact(entityName, data) {
|
|
3129
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
|
+
}
|
|
3130
3405
|
const code = this.safeArrayLength(d?.code);
|
|
3131
3406
|
const tests = this.safeArrayLength(d?.tests);
|
|
3132
3407
|
const docs = this.safeArrayLength(d?.docs);
|
|
@@ -3157,13 +3432,13 @@ var ResponseFormatter = class {
|
|
|
3157
3432
|
const context = Array.isArray(d?.context) ? d.context : [];
|
|
3158
3433
|
const firstEntity = entities[0];
|
|
3159
3434
|
const nodeType = firstEntity?.node.type ?? "node";
|
|
3160
|
-
const
|
|
3435
|
+
const path7 = firstEntity?.node.path ?? "unknown";
|
|
3161
3436
|
let neighborCount = 0;
|
|
3162
3437
|
const firstContext = context[0];
|
|
3163
3438
|
if (firstContext && Array.isArray(firstContext.nodes)) {
|
|
3164
3439
|
neighborCount = firstContext.nodes.length;
|
|
3165
3440
|
}
|
|
3166
|
-
return `**${entityName}** is a ${nodeType} at \`${
|
|
3441
|
+
return `**${entityName}** is a ${nodeType} at \`${path7}\`. Connected to ${neighborCount} nodes.`;
|
|
3167
3442
|
}
|
|
3168
3443
|
formatAnomaly(data) {
|
|
3169
3444
|
const d = data;
|
|
@@ -3188,6 +3463,246 @@ var ResponseFormatter = class {
|
|
|
3188
3463
|
}
|
|
3189
3464
|
};
|
|
3190
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
|
+
|
|
3191
3706
|
// src/nlq/index.ts
|
|
3192
3707
|
var ENTITY_REQUIRED_INTENTS = /* @__PURE__ */ new Set(["impact", "relationships", "explain"]);
|
|
3193
3708
|
var classifier = new IntentClassifier();
|
|
@@ -3250,6 +3765,11 @@ function executeOperation(store, intent, entities, question, fusion) {
|
|
|
3250
3765
|
switch (intent) {
|
|
3251
3766
|
case "impact": {
|
|
3252
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
|
+
}
|
|
3253
3773
|
const result = cql.execute({
|
|
3254
3774
|
rootNodeIds: [rootId],
|
|
3255
3775
|
bidirectional: true,
|
|
@@ -3304,7 +3824,7 @@ var PHASE_NODE_TYPES = {
|
|
|
3304
3824
|
debug: ["failure", "learning", "function", "method"],
|
|
3305
3825
|
plan: ["adr", "document", "module", "layer"]
|
|
3306
3826
|
};
|
|
3307
|
-
var
|
|
3827
|
+
var CODE_NODE_TYPES5 = /* @__PURE__ */ new Set([
|
|
3308
3828
|
"file",
|
|
3309
3829
|
"function",
|
|
3310
3830
|
"class",
|
|
@@ -3519,7 +4039,7 @@ var Assembler = class {
|
|
|
3519
4039
|
*/
|
|
3520
4040
|
checkCoverage() {
|
|
3521
4041
|
const codeNodes = [];
|
|
3522
|
-
for (const type of
|
|
4042
|
+
for (const type of CODE_NODE_TYPES5) {
|
|
3523
4043
|
codeNodes.push(...this.store.findNodes({ type }));
|
|
3524
4044
|
}
|
|
3525
4045
|
const documented = [];
|
|
@@ -3543,6 +4063,99 @@ var Assembler = class {
|
|
|
3543
4063
|
}
|
|
3544
4064
|
};
|
|
3545
4065
|
|
|
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
|
+
}
|
|
4120
|
+
function queryTraceability(store, options) {
|
|
4121
|
+
const allRequirements = store.findNodes({ type: "requirement" });
|
|
4122
|
+
const filtered = allRequirements.filter((node) => {
|
|
4123
|
+
if (options?.specPath && node.metadata?.specPath !== options.specPath) return false;
|
|
4124
|
+
if (options?.featureName && node.metadata?.featureName !== options.featureName) return false;
|
|
4125
|
+
return true;
|
|
4126
|
+
});
|
|
4127
|
+
if (filtered.length === 0) return [];
|
|
4128
|
+
const groups = /* @__PURE__ */ new Map();
|
|
4129
|
+
for (const req of filtered) {
|
|
4130
|
+
const meta = req.metadata;
|
|
4131
|
+
const specPath = meta?.specPath ?? "";
|
|
4132
|
+
const featureName = meta?.featureName ?? "";
|
|
4133
|
+
const key = `${specPath}\0${featureName}`;
|
|
4134
|
+
const list = groups.get(key);
|
|
4135
|
+
if (list) {
|
|
4136
|
+
list.push(req);
|
|
4137
|
+
} else {
|
|
4138
|
+
groups.set(key, [req]);
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
const results = [];
|
|
4142
|
+
for (const [, reqs] of groups) {
|
|
4143
|
+
const firstReq = reqs[0];
|
|
4144
|
+
const firstMeta = firstReq.metadata;
|
|
4145
|
+
const specPath = firstMeta?.specPath ?? "";
|
|
4146
|
+
const featureName = firstMeta?.featureName ?? "";
|
|
4147
|
+
const requirements = reqs.map((req) => buildRequirementCoverage(store, req));
|
|
4148
|
+
requirements.sort((a, b) => a.index - b.index);
|
|
4149
|
+
results.push({
|
|
4150
|
+
specPath,
|
|
4151
|
+
featureName,
|
|
4152
|
+
requirements,
|
|
4153
|
+
summary: computeSummary(requirements)
|
|
4154
|
+
});
|
|
4155
|
+
}
|
|
4156
|
+
return results;
|
|
4157
|
+
}
|
|
4158
|
+
|
|
3546
4159
|
// src/constraints/GraphConstraintAdapter.ts
|
|
3547
4160
|
import { minimatch } from "minimatch";
|
|
3548
4161
|
import { relative as relative2 } from "path";
|
|
@@ -3602,14 +4215,14 @@ var GraphConstraintAdapter = class {
|
|
|
3602
4215
|
};
|
|
3603
4216
|
|
|
3604
4217
|
// src/ingest/DesignIngestor.ts
|
|
3605
|
-
import * as
|
|
3606
|
-
import * as
|
|
4218
|
+
import * as fs5 from "fs/promises";
|
|
4219
|
+
import * as path6 from "path";
|
|
3607
4220
|
function isDTCGToken(obj) {
|
|
3608
4221
|
return typeof obj === "object" && obj !== null && "$value" in obj && "$type" in obj;
|
|
3609
4222
|
}
|
|
3610
4223
|
async function readFileOrNull(filePath) {
|
|
3611
4224
|
try {
|
|
3612
|
-
return await
|
|
4225
|
+
return await fs5.readFile(filePath, "utf-8");
|
|
3613
4226
|
} catch {
|
|
3614
4227
|
return null;
|
|
3615
4228
|
}
|
|
@@ -3755,8 +4368,8 @@ var DesignIngestor = class {
|
|
|
3755
4368
|
async ingestAll(designDir) {
|
|
3756
4369
|
const start = Date.now();
|
|
3757
4370
|
const [tokensResult, intentResult] = await Promise.all([
|
|
3758
|
-
this.ingestTokens(
|
|
3759
|
-
this.ingestDesignIntent(
|
|
4371
|
+
this.ingestTokens(path6.join(designDir, "tokens.json")),
|
|
4372
|
+
this.ingestDesignIntent(path6.join(designDir, "DESIGN.md"))
|
|
3760
4373
|
]);
|
|
3761
4374
|
const merged = mergeResults(tokensResult, intentResult);
|
|
3762
4375
|
return { ...merged, durationMs: Date.now() - start };
|
|
@@ -4005,10 +4618,10 @@ var TaskIndependenceAnalyzer = class {
|
|
|
4005
4618
|
includeTypes: ["file"]
|
|
4006
4619
|
});
|
|
4007
4620
|
for (const n of queryResult.nodes) {
|
|
4008
|
-
const
|
|
4009
|
-
if (!fileSet.has(
|
|
4010
|
-
if (!result.has(
|
|
4011
|
-
result.set(
|
|
4621
|
+
const path7 = n.path ?? n.id.replace(/^file:/, "");
|
|
4622
|
+
if (!fileSet.has(path7)) {
|
|
4623
|
+
if (!result.has(path7)) {
|
|
4624
|
+
result.set(path7, file);
|
|
4012
4625
|
}
|
|
4013
4626
|
}
|
|
4014
4627
|
}
|
|
@@ -4361,12 +4974,14 @@ var ConflictPredictor = class {
|
|
|
4361
4974
|
};
|
|
4362
4975
|
|
|
4363
4976
|
// src/index.ts
|
|
4364
|
-
var VERSION = "0.
|
|
4977
|
+
var VERSION = "0.4.0";
|
|
4365
4978
|
export {
|
|
4366
4979
|
Assembler,
|
|
4367
4980
|
CIConnector,
|
|
4368
4981
|
CURRENT_SCHEMA_VERSION,
|
|
4982
|
+
CascadeSimulator,
|
|
4369
4983
|
CodeIngestor,
|
|
4984
|
+
CompositeProbabilityStrategy,
|
|
4370
4985
|
ConflictPredictor,
|
|
4371
4986
|
ConfluenceConnector,
|
|
4372
4987
|
ContextQL,
|
|
@@ -4392,6 +5007,7 @@ export {
|
|
|
4392
5007
|
KnowledgeIngestor,
|
|
4393
5008
|
NODE_TYPES,
|
|
4394
5009
|
OBSERVABILITY_TYPES,
|
|
5010
|
+
RequirementIngestor,
|
|
4395
5011
|
ResponseFormatter,
|
|
4396
5012
|
SlackConnector,
|
|
4397
5013
|
SyncManager,
|
|
@@ -4400,9 +5016,11 @@ export {
|
|
|
4400
5016
|
VERSION,
|
|
4401
5017
|
VectorStore,
|
|
4402
5018
|
askGraph,
|
|
5019
|
+
classifyNodeCategory,
|
|
4403
5020
|
groupNodesByImpact,
|
|
4404
5021
|
linkToCode,
|
|
4405
5022
|
loadGraph,
|
|
4406
5023
|
project,
|
|
5024
|
+
queryTraceability,
|
|
4407
5025
|
saveGraph
|
|
4408
5026
|
};
|