@harness-engineering/graph 0.3.5 → 0.4.0
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.d.mts +73 -9
- package/dist/index.d.ts +73 -9
- package/dist/index.js +377 -31
- package/dist/index.mjs +375 -31
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -59,6 +59,7 @@ __export(index_exports, {
|
|
|
59
59
|
KnowledgeIngestor: () => KnowledgeIngestor,
|
|
60
60
|
NODE_TYPES: () => NODE_TYPES,
|
|
61
61
|
OBSERVABILITY_TYPES: () => OBSERVABILITY_TYPES,
|
|
62
|
+
RequirementIngestor: () => RequirementIngestor,
|
|
62
63
|
ResponseFormatter: () => ResponseFormatter,
|
|
63
64
|
SlackConnector: () => SlackConnector,
|
|
64
65
|
SyncManager: () => SyncManager,
|
|
@@ -71,6 +72,7 @@ __export(index_exports, {
|
|
|
71
72
|
linkToCode: () => linkToCode,
|
|
72
73
|
loadGraph: () => loadGraph,
|
|
73
74
|
project: () => project,
|
|
75
|
+
queryTraceability: () => queryTraceability,
|
|
74
76
|
saveGraph: () => saveGraph
|
|
75
77
|
});
|
|
76
78
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -112,7 +114,9 @@ var NODE_TYPES = [
|
|
|
112
114
|
// Design
|
|
113
115
|
"design_token",
|
|
114
116
|
"aesthetic_intent",
|
|
115
|
-
"design_constraint"
|
|
117
|
+
"design_constraint",
|
|
118
|
+
// Traceability
|
|
119
|
+
"requirement"
|
|
116
120
|
];
|
|
117
121
|
var EDGE_TYPES = [
|
|
118
122
|
// Code relationships
|
|
@@ -141,7 +145,11 @@ var EDGE_TYPES = [
|
|
|
141
145
|
"uses_token",
|
|
142
146
|
"declares_intent",
|
|
143
147
|
"violates_design",
|
|
144
|
-
"platform_binding"
|
|
148
|
+
"platform_binding",
|
|
149
|
+
// Traceability relationships
|
|
150
|
+
"requires",
|
|
151
|
+
"verified_by",
|
|
152
|
+
"tested_by"
|
|
145
153
|
];
|
|
146
154
|
var OBSERVABILITY_TYPES = /* @__PURE__ */ new Set(["span", "metric", "log"]);
|
|
147
155
|
var CURRENT_SCHEMA_VERSION = 1;
|
|
@@ -675,6 +683,7 @@ var CodeIngestor = class {
|
|
|
675
683
|
this.store.addEdge(edge);
|
|
676
684
|
edgesAdded++;
|
|
677
685
|
}
|
|
686
|
+
edgesAdded += this.extractReqAnnotations(fileContents, rootDir);
|
|
678
687
|
return {
|
|
679
688
|
nodesAdded,
|
|
680
689
|
nodesUpdated: 0,
|
|
@@ -1033,6 +1042,48 @@ var CodeIngestor = class {
|
|
|
1033
1042
|
if (/\.jsx?$/.test(filePath)) return "javascript";
|
|
1034
1043
|
return "unknown";
|
|
1035
1044
|
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Scan file contents for @req annotations and create verified_by edges
|
|
1047
|
+
* linking requirement nodes to the annotated files.
|
|
1048
|
+
* Format: // @req <feature-name>#<index>
|
|
1049
|
+
*/
|
|
1050
|
+
extractReqAnnotations(fileContents, rootDir) {
|
|
1051
|
+
const REQ_TAG = /\/\/\s*@req\s+([\w-]+)#(\d+)/g;
|
|
1052
|
+
const reqNodes = this.store.findNodes({ type: "requirement" });
|
|
1053
|
+
let edgesAdded = 0;
|
|
1054
|
+
for (const [filePath, content] of fileContents) {
|
|
1055
|
+
let match;
|
|
1056
|
+
REQ_TAG.lastIndex = 0;
|
|
1057
|
+
while ((match = REQ_TAG.exec(content)) !== null) {
|
|
1058
|
+
const featureName = match[1];
|
|
1059
|
+
const reqIndex = parseInt(match[2], 10);
|
|
1060
|
+
const reqNode = reqNodes.find(
|
|
1061
|
+
(n) => n.metadata.featureName === featureName && n.metadata.index === reqIndex
|
|
1062
|
+
);
|
|
1063
|
+
if (!reqNode) {
|
|
1064
|
+
console.warn(
|
|
1065
|
+
`@req annotation references non-existent requirement: ${featureName}#${reqIndex} in ${filePath}`
|
|
1066
|
+
);
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
const relPath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
1070
|
+
const fileNodeId = `file:${relPath}`;
|
|
1071
|
+
this.store.addEdge({
|
|
1072
|
+
from: reqNode.id,
|
|
1073
|
+
to: fileNodeId,
|
|
1074
|
+
type: "verified_by",
|
|
1075
|
+
confidence: 1,
|
|
1076
|
+
metadata: {
|
|
1077
|
+
method: "annotation",
|
|
1078
|
+
tag: `@req ${featureName}#${reqIndex}`,
|
|
1079
|
+
confidence: 1
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
edgesAdded++;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return edgesAdded;
|
|
1086
|
+
}
|
|
1036
1087
|
};
|
|
1037
1088
|
|
|
1038
1089
|
// src/ingest/GitIngestor.ts
|
|
@@ -1509,8 +1560,218 @@ var KnowledgeIngestor = class {
|
|
|
1509
1560
|
}
|
|
1510
1561
|
};
|
|
1511
1562
|
|
|
1512
|
-
// src/ingest/
|
|
1563
|
+
// src/ingest/RequirementIngestor.ts
|
|
1564
|
+
var fs3 = __toESM(require("fs/promises"));
|
|
1565
|
+
var path4 = __toESM(require("path"));
|
|
1566
|
+
var REQUIREMENT_SECTIONS = [
|
|
1567
|
+
"Observable Truths",
|
|
1568
|
+
"Success Criteria",
|
|
1569
|
+
"Acceptance Criteria"
|
|
1570
|
+
];
|
|
1571
|
+
var SECTION_HEADING_RE = /^#{2,3}\s+(.+)$/;
|
|
1572
|
+
var NUMBERED_ITEM_RE = /^\s*(\d+)\.\s+(.+)$/;
|
|
1573
|
+
function detectEarsPattern(text) {
|
|
1574
|
+
const lower = text.toLowerCase();
|
|
1575
|
+
if (/^if\b.+\bthen\b.+\bshall not\b/.test(lower)) return "unwanted";
|
|
1576
|
+
if (/^when\b/.test(lower)) return "event-driven";
|
|
1577
|
+
if (/^while\b/.test(lower)) return "state-driven";
|
|
1578
|
+
if (/^where\b/.test(lower)) return "optional";
|
|
1579
|
+
if (/^the\s+\w+\s+shall\b/.test(lower)) return "ubiquitous";
|
|
1580
|
+
return void 0;
|
|
1581
|
+
}
|
|
1513
1582
|
var CODE_NODE_TYPES2 = ["file", "function", "class", "method", "interface", "variable"];
|
|
1583
|
+
var RequirementIngestor = class {
|
|
1584
|
+
constructor(store) {
|
|
1585
|
+
this.store = store;
|
|
1586
|
+
}
|
|
1587
|
+
store;
|
|
1588
|
+
/**
|
|
1589
|
+
* Scan a specs directory for `<feature>/proposal.md` files,
|
|
1590
|
+
* extract numbered requirements from recognized sections,
|
|
1591
|
+
* and create requirement nodes with convention-based edges.
|
|
1592
|
+
*/
|
|
1593
|
+
async ingestSpecs(specsDir) {
|
|
1594
|
+
const start = Date.now();
|
|
1595
|
+
const errors = [];
|
|
1596
|
+
let nodesAdded = 0;
|
|
1597
|
+
let edgesAdded = 0;
|
|
1598
|
+
let featureDirs;
|
|
1599
|
+
try {
|
|
1600
|
+
const entries = await fs3.readdir(specsDir, { withFileTypes: true });
|
|
1601
|
+
featureDirs = entries.filter((e) => e.isDirectory()).map((e) => path4.join(specsDir, e.name));
|
|
1602
|
+
} catch {
|
|
1603
|
+
return emptyResult(Date.now() - start);
|
|
1604
|
+
}
|
|
1605
|
+
for (const featureDir of featureDirs) {
|
|
1606
|
+
const featureName = path4.basename(featureDir);
|
|
1607
|
+
const specPath = path4.join(featureDir, "proposal.md");
|
|
1608
|
+
let content;
|
|
1609
|
+
try {
|
|
1610
|
+
content = await fs3.readFile(specPath, "utf-8");
|
|
1611
|
+
} catch {
|
|
1612
|
+
continue;
|
|
1613
|
+
}
|
|
1614
|
+
try {
|
|
1615
|
+
const specHash = hash(specPath);
|
|
1616
|
+
const specNodeId = `file:${specPath}`;
|
|
1617
|
+
this.store.addNode({
|
|
1618
|
+
id: specNodeId,
|
|
1619
|
+
type: "document",
|
|
1620
|
+
name: path4.basename(specPath),
|
|
1621
|
+
path: specPath,
|
|
1622
|
+
metadata: { featureName }
|
|
1623
|
+
});
|
|
1624
|
+
const requirements = this.extractRequirements(content, specPath, specHash, featureName);
|
|
1625
|
+
for (const req of requirements) {
|
|
1626
|
+
this.store.addNode(req.node);
|
|
1627
|
+
nodesAdded++;
|
|
1628
|
+
this.store.addEdge({
|
|
1629
|
+
from: req.node.id,
|
|
1630
|
+
to: specNodeId,
|
|
1631
|
+
type: "specifies"
|
|
1632
|
+
});
|
|
1633
|
+
edgesAdded++;
|
|
1634
|
+
edgesAdded += this.linkByPathPattern(req.node.id, featureName);
|
|
1635
|
+
edgesAdded += this.linkByKeywordOverlap(req.node.id, req.node.name);
|
|
1636
|
+
}
|
|
1637
|
+
} catch (err) {
|
|
1638
|
+
errors.push(`${specPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
return {
|
|
1642
|
+
nodesAdded,
|
|
1643
|
+
nodesUpdated: 0,
|
|
1644
|
+
edgesAdded,
|
|
1645
|
+
edgesUpdated: 0,
|
|
1646
|
+
errors,
|
|
1647
|
+
durationMs: Date.now() - start
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
/**
|
|
1651
|
+
* Parse markdown content and extract numbered items from recognized sections.
|
|
1652
|
+
*/
|
|
1653
|
+
extractRequirements(content, specPath, specHash, featureName) {
|
|
1654
|
+
const lines = content.split("\n");
|
|
1655
|
+
const results = [];
|
|
1656
|
+
let currentSection;
|
|
1657
|
+
let inRequirementSection = false;
|
|
1658
|
+
let globalIndex = 0;
|
|
1659
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1660
|
+
const line = lines[i];
|
|
1661
|
+
const headingMatch = line.match(SECTION_HEADING_RE);
|
|
1662
|
+
if (headingMatch) {
|
|
1663
|
+
const heading = headingMatch[1].trim();
|
|
1664
|
+
const isReqSection = REQUIREMENT_SECTIONS.some(
|
|
1665
|
+
(s) => heading.toLowerCase() === s.toLowerCase()
|
|
1666
|
+
);
|
|
1667
|
+
if (isReqSection) {
|
|
1668
|
+
currentSection = heading;
|
|
1669
|
+
inRequirementSection = true;
|
|
1670
|
+
} else {
|
|
1671
|
+
inRequirementSection = false;
|
|
1672
|
+
}
|
|
1673
|
+
continue;
|
|
1674
|
+
}
|
|
1675
|
+
if (!inRequirementSection) continue;
|
|
1676
|
+
const itemMatch = line.match(NUMBERED_ITEM_RE);
|
|
1677
|
+
if (!itemMatch) continue;
|
|
1678
|
+
const index = parseInt(itemMatch[1], 10);
|
|
1679
|
+
const text = itemMatch[2].trim();
|
|
1680
|
+
const rawText = line.trim();
|
|
1681
|
+
const lineNumber = i + 1;
|
|
1682
|
+
globalIndex++;
|
|
1683
|
+
const nodeId = `req:${specHash}:${globalIndex}`;
|
|
1684
|
+
const earsPattern = detectEarsPattern(text);
|
|
1685
|
+
results.push({
|
|
1686
|
+
node: {
|
|
1687
|
+
id: nodeId,
|
|
1688
|
+
type: "requirement",
|
|
1689
|
+
name: text,
|
|
1690
|
+
path: specPath,
|
|
1691
|
+
location: {
|
|
1692
|
+
fileId: `file:${specPath}`,
|
|
1693
|
+
startLine: lineNumber,
|
|
1694
|
+
endLine: lineNumber
|
|
1695
|
+
},
|
|
1696
|
+
metadata: {
|
|
1697
|
+
specPath,
|
|
1698
|
+
index,
|
|
1699
|
+
section: currentSection,
|
|
1700
|
+
rawText,
|
|
1701
|
+
earsPattern,
|
|
1702
|
+
featureName
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
return results;
|
|
1708
|
+
}
|
|
1709
|
+
/**
|
|
1710
|
+
* Convention-based linking: match requirement to code/test files
|
|
1711
|
+
* by feature name in their path.
|
|
1712
|
+
*/
|
|
1713
|
+
linkByPathPattern(reqId, featureName) {
|
|
1714
|
+
let count = 0;
|
|
1715
|
+
const fileNodes = this.store.findNodes({ type: "file" });
|
|
1716
|
+
for (const node of fileNodes) {
|
|
1717
|
+
if (!node.path) continue;
|
|
1718
|
+
const normalizedPath = node.path.replace(/\\/g, "/");
|
|
1719
|
+
const isCodeMatch = normalizedPath.includes("packages/") && path4.basename(normalizedPath).includes(featureName);
|
|
1720
|
+
const isTestMatch = normalizedPath.includes("/tests/") && // platform-safe
|
|
1721
|
+
path4.basename(normalizedPath).includes(featureName);
|
|
1722
|
+
if (isCodeMatch && !isTestMatch) {
|
|
1723
|
+
this.store.addEdge({
|
|
1724
|
+
from: reqId,
|
|
1725
|
+
to: node.id,
|
|
1726
|
+
type: "requires",
|
|
1727
|
+
confidence: 0.5,
|
|
1728
|
+
metadata: { method: "convention", matchReason: "path-pattern" }
|
|
1729
|
+
});
|
|
1730
|
+
count++;
|
|
1731
|
+
} else if (isTestMatch) {
|
|
1732
|
+
this.store.addEdge({
|
|
1733
|
+
from: reqId,
|
|
1734
|
+
to: node.id,
|
|
1735
|
+
type: "verified_by",
|
|
1736
|
+
confidence: 0.5,
|
|
1737
|
+
metadata: { method: "convention", matchReason: "path-pattern" }
|
|
1738
|
+
});
|
|
1739
|
+
count++;
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
return count;
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Convention-based linking: match requirement text to code nodes
|
|
1746
|
+
* by keyword overlap (function/class names appearing in requirement text).
|
|
1747
|
+
*/
|
|
1748
|
+
linkByKeywordOverlap(reqId, reqText) {
|
|
1749
|
+
let count = 0;
|
|
1750
|
+
for (const nodeType of CODE_NODE_TYPES2) {
|
|
1751
|
+
const codeNodes = this.store.findNodes({ type: nodeType });
|
|
1752
|
+
for (const node of codeNodes) {
|
|
1753
|
+
if (node.name.length < 3) continue;
|
|
1754
|
+
const escaped = node.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1755
|
+
const namePattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
1756
|
+
if (namePattern.test(reqText)) {
|
|
1757
|
+
const edgeType = node.path && node.path.replace(/\\/g, "/").includes("/tests/") ? "verified_by" : "requires";
|
|
1758
|
+
this.store.addEdge({
|
|
1759
|
+
from: reqId,
|
|
1760
|
+
to: node.id,
|
|
1761
|
+
type: edgeType,
|
|
1762
|
+
confidence: 0.6,
|
|
1763
|
+
metadata: { method: "convention", matchReason: "keyword-overlap" }
|
|
1764
|
+
});
|
|
1765
|
+
count++;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
return count;
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
|
|
1773
|
+
// src/ingest/connectors/ConnectorUtils.ts
|
|
1774
|
+
var CODE_NODE_TYPES3 = ["file", "function", "class", "method", "interface", "variable"];
|
|
1514
1775
|
var SANITIZE_RULES = [
|
|
1515
1776
|
// Strip XML/HTML-like instruction tags that could be interpreted as system prompts
|
|
1516
1777
|
{
|
|
@@ -1545,7 +1806,7 @@ function sanitizeExternalText(text, maxLength = 2e3) {
|
|
|
1545
1806
|
}
|
|
1546
1807
|
function linkToCode(store, content, sourceNodeId, edgeType, options) {
|
|
1547
1808
|
let edgesCreated = 0;
|
|
1548
|
-
for (const type of
|
|
1809
|
+
for (const type of CODE_NODE_TYPES3) {
|
|
1549
1810
|
const nodes = store.findNodes({ type });
|
|
1550
1811
|
for (const node of nodes) {
|
|
1551
1812
|
if (node.name.length < 3) continue;
|
|
@@ -1565,12 +1826,12 @@ function linkToCode(store, content, sourceNodeId, edgeType, options) {
|
|
|
1565
1826
|
}
|
|
1566
1827
|
|
|
1567
1828
|
// src/ingest/connectors/SyncManager.ts
|
|
1568
|
-
var
|
|
1569
|
-
var
|
|
1829
|
+
var fs4 = __toESM(require("fs/promises"));
|
|
1830
|
+
var path5 = __toESM(require("path"));
|
|
1570
1831
|
var SyncManager = class {
|
|
1571
1832
|
constructor(store, graphDir) {
|
|
1572
1833
|
this.store = store;
|
|
1573
|
-
this.metadataPath =
|
|
1834
|
+
this.metadataPath = path5.join(graphDir, "sync-metadata.json");
|
|
1574
1835
|
}
|
|
1575
1836
|
store;
|
|
1576
1837
|
registrations = /* @__PURE__ */ new Map();
|
|
@@ -1625,15 +1886,15 @@ var SyncManager = class {
|
|
|
1625
1886
|
}
|
|
1626
1887
|
async loadMetadata() {
|
|
1627
1888
|
try {
|
|
1628
|
-
const raw = await
|
|
1889
|
+
const raw = await fs4.readFile(this.metadataPath, "utf-8");
|
|
1629
1890
|
return JSON.parse(raw);
|
|
1630
1891
|
} catch {
|
|
1631
1892
|
return { connectors: {} };
|
|
1632
1893
|
}
|
|
1633
1894
|
}
|
|
1634
1895
|
async saveMetadata(metadata) {
|
|
1635
|
-
await
|
|
1636
|
-
await
|
|
1896
|
+
await fs4.mkdir(path5.dirname(this.metadataPath), { recursive: true });
|
|
1897
|
+
await fs4.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
1637
1898
|
}
|
|
1638
1899
|
};
|
|
1639
1900
|
|
|
@@ -2162,7 +2423,7 @@ var FusionLayer = class {
|
|
|
2162
2423
|
};
|
|
2163
2424
|
|
|
2164
2425
|
// src/entropy/GraphEntropyAdapter.ts
|
|
2165
|
-
var
|
|
2426
|
+
var CODE_NODE_TYPES4 = ["file", "function", "class", "method", "interface", "variable"];
|
|
2166
2427
|
var GraphEntropyAdapter = class {
|
|
2167
2428
|
constructor(store) {
|
|
2168
2429
|
this.store = store;
|
|
@@ -2229,7 +2490,7 @@ var GraphEntropyAdapter = class {
|
|
|
2229
2490
|
}
|
|
2230
2491
|
findEntryPoints() {
|
|
2231
2492
|
const entryPoints = [];
|
|
2232
|
-
for (const nodeType of
|
|
2493
|
+
for (const nodeType of CODE_NODE_TYPES4) {
|
|
2233
2494
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
2234
2495
|
for (const node of nodes) {
|
|
2235
2496
|
const isIndexFile = nodeType === "file" && node.name === "index.ts";
|
|
@@ -2265,7 +2526,7 @@ var GraphEntropyAdapter = class {
|
|
|
2265
2526
|
}
|
|
2266
2527
|
collectUnreachableNodes(visited) {
|
|
2267
2528
|
const unreachableNodes = [];
|
|
2268
|
-
for (const nodeType of
|
|
2529
|
+
for (const nodeType of CODE_NODE_TYPES4) {
|
|
2269
2530
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
2270
2531
|
for (const node of nodes) {
|
|
2271
2532
|
if (!visited.has(node.id)) {
|
|
@@ -3085,9 +3346,9 @@ var EntityExtractor = class {
|
|
|
3085
3346
|
}
|
|
3086
3347
|
const pathConsumed = /* @__PURE__ */ new Set();
|
|
3087
3348
|
for (const match of trimmed.matchAll(FILE_PATH_RE)) {
|
|
3088
|
-
const
|
|
3089
|
-
add(
|
|
3090
|
-
pathConsumed.add(
|
|
3349
|
+
const path7 = match[0];
|
|
3350
|
+
add(path7);
|
|
3351
|
+
pathConsumed.add(path7);
|
|
3091
3352
|
}
|
|
3092
3353
|
const allConsumed = buildConsumedSet(quotedConsumed, casingConsumed, pathConsumed);
|
|
3093
3354
|
const words = trimmed.split(/\s+/);
|
|
@@ -3158,8 +3419,8 @@ var EntityResolver = class {
|
|
|
3158
3419
|
if (isPathLike && node.path.includes(raw)) {
|
|
3159
3420
|
return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
|
|
3160
3421
|
}
|
|
3161
|
-
const
|
|
3162
|
-
if (
|
|
3422
|
+
const basename5 = node.path.split("/").pop() ?? "";
|
|
3423
|
+
if (basename5.includes(raw)) {
|
|
3163
3424
|
return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
|
|
3164
3425
|
}
|
|
3165
3426
|
if (raw.length >= 4 && node.path.includes(raw)) {
|
|
@@ -3234,13 +3495,13 @@ var ResponseFormatter = class {
|
|
|
3234
3495
|
const context = Array.isArray(d?.context) ? d.context : [];
|
|
3235
3496
|
const firstEntity = entities[0];
|
|
3236
3497
|
const nodeType = firstEntity?.node.type ?? "node";
|
|
3237
|
-
const
|
|
3498
|
+
const path7 = firstEntity?.node.path ?? "unknown";
|
|
3238
3499
|
let neighborCount = 0;
|
|
3239
3500
|
const firstContext = context[0];
|
|
3240
3501
|
if (firstContext && Array.isArray(firstContext.nodes)) {
|
|
3241
3502
|
neighborCount = firstContext.nodes.length;
|
|
3242
3503
|
}
|
|
3243
|
-
return `**${entityName}** is a ${nodeType} at \`${
|
|
3504
|
+
return `**${entityName}** is a ${nodeType} at \`${path7}\`. Connected to ${neighborCount} nodes.`;
|
|
3244
3505
|
}
|
|
3245
3506
|
formatAnomaly(data) {
|
|
3246
3507
|
const d = data;
|
|
@@ -3381,7 +3642,7 @@ var PHASE_NODE_TYPES = {
|
|
|
3381
3642
|
debug: ["failure", "learning", "function", "method"],
|
|
3382
3643
|
plan: ["adr", "document", "module", "layer"]
|
|
3383
3644
|
};
|
|
3384
|
-
var
|
|
3645
|
+
var CODE_NODE_TYPES5 = /* @__PURE__ */ new Set([
|
|
3385
3646
|
"file",
|
|
3386
3647
|
"function",
|
|
3387
3648
|
"class",
|
|
@@ -3596,7 +3857,7 @@ var Assembler = class {
|
|
|
3596
3857
|
*/
|
|
3597
3858
|
checkCoverage() {
|
|
3598
3859
|
const codeNodes = [];
|
|
3599
|
-
for (const type of
|
|
3860
|
+
for (const type of CODE_NODE_TYPES5) {
|
|
3600
3861
|
codeNodes.push(...this.store.findNodes({ type }));
|
|
3601
3862
|
}
|
|
3602
3863
|
const documented = [];
|
|
@@ -3620,6 +3881,89 @@ var Assembler = class {
|
|
|
3620
3881
|
}
|
|
3621
3882
|
};
|
|
3622
3883
|
|
|
3884
|
+
// src/query/Traceability.ts
|
|
3885
|
+
function queryTraceability(store, options) {
|
|
3886
|
+
const allRequirements = store.findNodes({ type: "requirement" });
|
|
3887
|
+
const filtered = allRequirements.filter((node) => {
|
|
3888
|
+
if (options?.specPath && node.metadata?.specPath !== options.specPath) return false;
|
|
3889
|
+
if (options?.featureName && node.metadata?.featureName !== options.featureName) return false;
|
|
3890
|
+
return true;
|
|
3891
|
+
});
|
|
3892
|
+
if (filtered.length === 0) return [];
|
|
3893
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3894
|
+
for (const req of filtered) {
|
|
3895
|
+
const meta = req.metadata;
|
|
3896
|
+
const specPath = meta?.specPath ?? "";
|
|
3897
|
+
const featureName = meta?.featureName ?? "";
|
|
3898
|
+
const key = `${specPath}\0${featureName}`;
|
|
3899
|
+
const list = groups.get(key);
|
|
3900
|
+
if (list) {
|
|
3901
|
+
list.push(req);
|
|
3902
|
+
} else {
|
|
3903
|
+
groups.set(key, [req]);
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
const results = [];
|
|
3907
|
+
for (const [, reqs] of groups) {
|
|
3908
|
+
const firstReq = reqs[0];
|
|
3909
|
+
const firstMeta = firstReq.metadata;
|
|
3910
|
+
const specPath = firstMeta?.specPath ?? "";
|
|
3911
|
+
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
|
+
}
|
|
3950
|
+
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
|
+
results.push({
|
|
3958
|
+
specPath,
|
|
3959
|
+
featureName,
|
|
3960
|
+
requirements,
|
|
3961
|
+
summary: { total, withCode, withTests, fullyTraced, untraceable, coveragePercent }
|
|
3962
|
+
});
|
|
3963
|
+
}
|
|
3964
|
+
return results;
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3623
3967
|
// src/constraints/GraphConstraintAdapter.ts
|
|
3624
3968
|
var import_minimatch = require("minimatch");
|
|
3625
3969
|
var import_node_path2 = require("path");
|
|
@@ -3679,14 +4023,14 @@ var GraphConstraintAdapter = class {
|
|
|
3679
4023
|
};
|
|
3680
4024
|
|
|
3681
4025
|
// src/ingest/DesignIngestor.ts
|
|
3682
|
-
var
|
|
3683
|
-
var
|
|
4026
|
+
var fs5 = __toESM(require("fs/promises"));
|
|
4027
|
+
var path6 = __toESM(require("path"));
|
|
3684
4028
|
function isDTCGToken(obj) {
|
|
3685
4029
|
return typeof obj === "object" && obj !== null && "$value" in obj && "$type" in obj;
|
|
3686
4030
|
}
|
|
3687
4031
|
async function readFileOrNull(filePath) {
|
|
3688
4032
|
try {
|
|
3689
|
-
return await
|
|
4033
|
+
return await fs5.readFile(filePath, "utf-8");
|
|
3690
4034
|
} catch {
|
|
3691
4035
|
return null;
|
|
3692
4036
|
}
|
|
@@ -3832,8 +4176,8 @@ var DesignIngestor = class {
|
|
|
3832
4176
|
async ingestAll(designDir) {
|
|
3833
4177
|
const start = Date.now();
|
|
3834
4178
|
const [tokensResult, intentResult] = await Promise.all([
|
|
3835
|
-
this.ingestTokens(
|
|
3836
|
-
this.ingestDesignIntent(
|
|
4179
|
+
this.ingestTokens(path6.join(designDir, "tokens.json")),
|
|
4180
|
+
this.ingestDesignIntent(path6.join(designDir, "DESIGN.md"))
|
|
3837
4181
|
]);
|
|
3838
4182
|
const merged = mergeResults(tokensResult, intentResult);
|
|
3839
4183
|
return { ...merged, durationMs: Date.now() - start };
|
|
@@ -4082,10 +4426,10 @@ var TaskIndependenceAnalyzer = class {
|
|
|
4082
4426
|
includeTypes: ["file"]
|
|
4083
4427
|
});
|
|
4084
4428
|
for (const n of queryResult.nodes) {
|
|
4085
|
-
const
|
|
4086
|
-
if (!fileSet.has(
|
|
4087
|
-
if (!result.has(
|
|
4088
|
-
result.set(
|
|
4429
|
+
const path7 = n.path ?? n.id.replace(/^file:/, "");
|
|
4430
|
+
if (!fileSet.has(path7)) {
|
|
4431
|
+
if (!result.has(path7)) {
|
|
4432
|
+
result.set(path7, file);
|
|
4089
4433
|
}
|
|
4090
4434
|
}
|
|
4091
4435
|
}
|
|
@@ -4470,6 +4814,7 @@ var VERSION = "0.2.0";
|
|
|
4470
4814
|
KnowledgeIngestor,
|
|
4471
4815
|
NODE_TYPES,
|
|
4472
4816
|
OBSERVABILITY_TYPES,
|
|
4817
|
+
RequirementIngestor,
|
|
4473
4818
|
ResponseFormatter,
|
|
4474
4819
|
SlackConnector,
|
|
4475
4820
|
SyncManager,
|
|
@@ -4482,5 +4827,6 @@ var VERSION = "0.2.0";
|
|
|
4482
4827
|
linkToCode,
|
|
4483
4828
|
loadGraph,
|
|
4484
4829
|
project,
|
|
4830
|
+
queryTraceability,
|
|
4485
4831
|
saveGraph
|
|
4486
4832
|
});
|