@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.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;
|
|
@@ -598,6 +604,7 @@ var CodeIngestor = class {
|
|
|
598
604
|
this.store.addEdge(edge);
|
|
599
605
|
edgesAdded++;
|
|
600
606
|
}
|
|
607
|
+
edgesAdded += this.extractReqAnnotations(fileContents, rootDir);
|
|
601
608
|
return {
|
|
602
609
|
nodesAdded,
|
|
603
610
|
nodesUpdated: 0,
|
|
@@ -956,6 +963,48 @@ var CodeIngestor = class {
|
|
|
956
963
|
if (/\.jsx?$/.test(filePath)) return "javascript";
|
|
957
964
|
return "unknown";
|
|
958
965
|
}
|
|
966
|
+
/**
|
|
967
|
+
* Scan file contents for @req annotations and create verified_by edges
|
|
968
|
+
* linking requirement nodes to the annotated files.
|
|
969
|
+
* Format: // @req <feature-name>#<index>
|
|
970
|
+
*/
|
|
971
|
+
extractReqAnnotations(fileContents, rootDir) {
|
|
972
|
+
const REQ_TAG = /\/\/\s*@req\s+([\w-]+)#(\d+)/g;
|
|
973
|
+
const reqNodes = this.store.findNodes({ type: "requirement" });
|
|
974
|
+
let edgesAdded = 0;
|
|
975
|
+
for (const [filePath, content] of fileContents) {
|
|
976
|
+
let match;
|
|
977
|
+
REQ_TAG.lastIndex = 0;
|
|
978
|
+
while ((match = REQ_TAG.exec(content)) !== null) {
|
|
979
|
+
const featureName = match[1];
|
|
980
|
+
const reqIndex = parseInt(match[2], 10);
|
|
981
|
+
const reqNode = reqNodes.find(
|
|
982
|
+
(n) => n.metadata.featureName === featureName && n.metadata.index === reqIndex
|
|
983
|
+
);
|
|
984
|
+
if (!reqNode) {
|
|
985
|
+
console.warn(
|
|
986
|
+
`@req annotation references non-existent requirement: ${featureName}#${reqIndex} in ${filePath}`
|
|
987
|
+
);
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
const relPath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
991
|
+
const fileNodeId = `file:${relPath}`;
|
|
992
|
+
this.store.addEdge({
|
|
993
|
+
from: reqNode.id,
|
|
994
|
+
to: fileNodeId,
|
|
995
|
+
type: "verified_by",
|
|
996
|
+
confidence: 1,
|
|
997
|
+
metadata: {
|
|
998
|
+
method: "annotation",
|
|
999
|
+
tag: `@req ${featureName}#${reqIndex}`,
|
|
1000
|
+
confidence: 1
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
edgesAdded++;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return edgesAdded;
|
|
1007
|
+
}
|
|
959
1008
|
};
|
|
960
1009
|
|
|
961
1010
|
// src/ingest/GitIngestor.ts
|
|
@@ -1432,8 +1481,218 @@ var KnowledgeIngestor = class {
|
|
|
1432
1481
|
}
|
|
1433
1482
|
};
|
|
1434
1483
|
|
|
1435
|
-
// src/ingest/
|
|
1484
|
+
// src/ingest/RequirementIngestor.ts
|
|
1485
|
+
import * as fs3 from "fs/promises";
|
|
1486
|
+
import * as path4 from "path";
|
|
1487
|
+
var REQUIREMENT_SECTIONS = [
|
|
1488
|
+
"Observable Truths",
|
|
1489
|
+
"Success Criteria",
|
|
1490
|
+
"Acceptance Criteria"
|
|
1491
|
+
];
|
|
1492
|
+
var SECTION_HEADING_RE = /^#{2,3}\s+(.+)$/;
|
|
1493
|
+
var NUMBERED_ITEM_RE = /^\s*(\d+)\.\s+(.+)$/;
|
|
1494
|
+
function detectEarsPattern(text) {
|
|
1495
|
+
const lower = text.toLowerCase();
|
|
1496
|
+
if (/^if\b.+\bthen\b.+\bshall not\b/.test(lower)) return "unwanted";
|
|
1497
|
+
if (/^when\b/.test(lower)) return "event-driven";
|
|
1498
|
+
if (/^while\b/.test(lower)) return "state-driven";
|
|
1499
|
+
if (/^where\b/.test(lower)) return "optional";
|
|
1500
|
+
if (/^the\s+\w+\s+shall\b/.test(lower)) return "ubiquitous";
|
|
1501
|
+
return void 0;
|
|
1502
|
+
}
|
|
1436
1503
|
var CODE_NODE_TYPES2 = ["file", "function", "class", "method", "interface", "variable"];
|
|
1504
|
+
var RequirementIngestor = class {
|
|
1505
|
+
constructor(store) {
|
|
1506
|
+
this.store = store;
|
|
1507
|
+
}
|
|
1508
|
+
store;
|
|
1509
|
+
/**
|
|
1510
|
+
* Scan a specs directory for `<feature>/proposal.md` files,
|
|
1511
|
+
* extract numbered requirements from recognized sections,
|
|
1512
|
+
* and create requirement nodes with convention-based edges.
|
|
1513
|
+
*/
|
|
1514
|
+
async ingestSpecs(specsDir) {
|
|
1515
|
+
const start = Date.now();
|
|
1516
|
+
const errors = [];
|
|
1517
|
+
let nodesAdded = 0;
|
|
1518
|
+
let edgesAdded = 0;
|
|
1519
|
+
let featureDirs;
|
|
1520
|
+
try {
|
|
1521
|
+
const entries = await fs3.readdir(specsDir, { withFileTypes: true });
|
|
1522
|
+
featureDirs = entries.filter((e) => e.isDirectory()).map((e) => path4.join(specsDir, e.name));
|
|
1523
|
+
} catch {
|
|
1524
|
+
return emptyResult(Date.now() - start);
|
|
1525
|
+
}
|
|
1526
|
+
for (const featureDir of featureDirs) {
|
|
1527
|
+
const featureName = path4.basename(featureDir);
|
|
1528
|
+
const specPath = path4.join(featureDir, "proposal.md");
|
|
1529
|
+
let content;
|
|
1530
|
+
try {
|
|
1531
|
+
content = await fs3.readFile(specPath, "utf-8");
|
|
1532
|
+
} catch {
|
|
1533
|
+
continue;
|
|
1534
|
+
}
|
|
1535
|
+
try {
|
|
1536
|
+
const specHash = hash(specPath);
|
|
1537
|
+
const specNodeId = `file:${specPath}`;
|
|
1538
|
+
this.store.addNode({
|
|
1539
|
+
id: specNodeId,
|
|
1540
|
+
type: "document",
|
|
1541
|
+
name: path4.basename(specPath),
|
|
1542
|
+
path: specPath,
|
|
1543
|
+
metadata: { featureName }
|
|
1544
|
+
});
|
|
1545
|
+
const requirements = this.extractRequirements(content, specPath, specHash, featureName);
|
|
1546
|
+
for (const req of requirements) {
|
|
1547
|
+
this.store.addNode(req.node);
|
|
1548
|
+
nodesAdded++;
|
|
1549
|
+
this.store.addEdge({
|
|
1550
|
+
from: req.node.id,
|
|
1551
|
+
to: specNodeId,
|
|
1552
|
+
type: "specifies"
|
|
1553
|
+
});
|
|
1554
|
+
edgesAdded++;
|
|
1555
|
+
edgesAdded += this.linkByPathPattern(req.node.id, featureName);
|
|
1556
|
+
edgesAdded += this.linkByKeywordOverlap(req.node.id, req.node.name);
|
|
1557
|
+
}
|
|
1558
|
+
} catch (err) {
|
|
1559
|
+
errors.push(`${specPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
return {
|
|
1563
|
+
nodesAdded,
|
|
1564
|
+
nodesUpdated: 0,
|
|
1565
|
+
edgesAdded,
|
|
1566
|
+
edgesUpdated: 0,
|
|
1567
|
+
errors,
|
|
1568
|
+
durationMs: Date.now() - start
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
/**
|
|
1572
|
+
* Parse markdown content and extract numbered items from recognized sections.
|
|
1573
|
+
*/
|
|
1574
|
+
extractRequirements(content, specPath, specHash, featureName) {
|
|
1575
|
+
const lines = content.split("\n");
|
|
1576
|
+
const results = [];
|
|
1577
|
+
let currentSection;
|
|
1578
|
+
let inRequirementSection = false;
|
|
1579
|
+
let globalIndex = 0;
|
|
1580
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1581
|
+
const line = lines[i];
|
|
1582
|
+
const headingMatch = line.match(SECTION_HEADING_RE);
|
|
1583
|
+
if (headingMatch) {
|
|
1584
|
+
const heading = headingMatch[1].trim();
|
|
1585
|
+
const isReqSection = REQUIREMENT_SECTIONS.some(
|
|
1586
|
+
(s) => heading.toLowerCase() === s.toLowerCase()
|
|
1587
|
+
);
|
|
1588
|
+
if (isReqSection) {
|
|
1589
|
+
currentSection = heading;
|
|
1590
|
+
inRequirementSection = true;
|
|
1591
|
+
} else {
|
|
1592
|
+
inRequirementSection = false;
|
|
1593
|
+
}
|
|
1594
|
+
continue;
|
|
1595
|
+
}
|
|
1596
|
+
if (!inRequirementSection) continue;
|
|
1597
|
+
const itemMatch = line.match(NUMBERED_ITEM_RE);
|
|
1598
|
+
if (!itemMatch) continue;
|
|
1599
|
+
const index = parseInt(itemMatch[1], 10);
|
|
1600
|
+
const text = itemMatch[2].trim();
|
|
1601
|
+
const rawText = line.trim();
|
|
1602
|
+
const lineNumber = i + 1;
|
|
1603
|
+
globalIndex++;
|
|
1604
|
+
const nodeId = `req:${specHash}:${globalIndex}`;
|
|
1605
|
+
const earsPattern = detectEarsPattern(text);
|
|
1606
|
+
results.push({
|
|
1607
|
+
node: {
|
|
1608
|
+
id: nodeId,
|
|
1609
|
+
type: "requirement",
|
|
1610
|
+
name: text,
|
|
1611
|
+
path: specPath,
|
|
1612
|
+
location: {
|
|
1613
|
+
fileId: `file:${specPath}`,
|
|
1614
|
+
startLine: lineNumber,
|
|
1615
|
+
endLine: lineNumber
|
|
1616
|
+
},
|
|
1617
|
+
metadata: {
|
|
1618
|
+
specPath,
|
|
1619
|
+
index,
|
|
1620
|
+
section: currentSection,
|
|
1621
|
+
rawText,
|
|
1622
|
+
earsPattern,
|
|
1623
|
+
featureName
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
return results;
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* Convention-based linking: match requirement to code/test files
|
|
1632
|
+
* by feature name in their path.
|
|
1633
|
+
*/
|
|
1634
|
+
linkByPathPattern(reqId, featureName) {
|
|
1635
|
+
let count = 0;
|
|
1636
|
+
const fileNodes = this.store.findNodes({ type: "file" });
|
|
1637
|
+
for (const node of fileNodes) {
|
|
1638
|
+
if (!node.path) continue;
|
|
1639
|
+
const normalizedPath = node.path.replace(/\\/g, "/");
|
|
1640
|
+
const isCodeMatch = normalizedPath.includes("packages/") && path4.basename(normalizedPath).includes(featureName);
|
|
1641
|
+
const isTestMatch = normalizedPath.includes("/tests/") && // platform-safe
|
|
1642
|
+
path4.basename(normalizedPath).includes(featureName);
|
|
1643
|
+
if (isCodeMatch && !isTestMatch) {
|
|
1644
|
+
this.store.addEdge({
|
|
1645
|
+
from: reqId,
|
|
1646
|
+
to: node.id,
|
|
1647
|
+
type: "requires",
|
|
1648
|
+
confidence: 0.5,
|
|
1649
|
+
metadata: { method: "convention", matchReason: "path-pattern" }
|
|
1650
|
+
});
|
|
1651
|
+
count++;
|
|
1652
|
+
} else if (isTestMatch) {
|
|
1653
|
+
this.store.addEdge({
|
|
1654
|
+
from: reqId,
|
|
1655
|
+
to: node.id,
|
|
1656
|
+
type: "verified_by",
|
|
1657
|
+
confidence: 0.5,
|
|
1658
|
+
metadata: { method: "convention", matchReason: "path-pattern" }
|
|
1659
|
+
});
|
|
1660
|
+
count++;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
return count;
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1666
|
+
* Convention-based linking: match requirement text to code nodes
|
|
1667
|
+
* by keyword overlap (function/class names appearing in requirement text).
|
|
1668
|
+
*/
|
|
1669
|
+
linkByKeywordOverlap(reqId, reqText) {
|
|
1670
|
+
let count = 0;
|
|
1671
|
+
for (const nodeType of CODE_NODE_TYPES2) {
|
|
1672
|
+
const codeNodes = this.store.findNodes({ type: nodeType });
|
|
1673
|
+
for (const node of codeNodes) {
|
|
1674
|
+
if (node.name.length < 3) continue;
|
|
1675
|
+
const escaped = node.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1676
|
+
const namePattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
1677
|
+
if (namePattern.test(reqText)) {
|
|
1678
|
+
const edgeType = node.path && node.path.replace(/\\/g, "/").includes("/tests/") ? "verified_by" : "requires";
|
|
1679
|
+
this.store.addEdge({
|
|
1680
|
+
from: reqId,
|
|
1681
|
+
to: node.id,
|
|
1682
|
+
type: edgeType,
|
|
1683
|
+
confidence: 0.6,
|
|
1684
|
+
metadata: { method: "convention", matchReason: "keyword-overlap" }
|
|
1685
|
+
});
|
|
1686
|
+
count++;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
return count;
|
|
1691
|
+
}
|
|
1692
|
+
};
|
|
1693
|
+
|
|
1694
|
+
// src/ingest/connectors/ConnectorUtils.ts
|
|
1695
|
+
var CODE_NODE_TYPES3 = ["file", "function", "class", "method", "interface", "variable"];
|
|
1437
1696
|
var SANITIZE_RULES = [
|
|
1438
1697
|
// Strip XML/HTML-like instruction tags that could be interpreted as system prompts
|
|
1439
1698
|
{
|
|
@@ -1468,7 +1727,7 @@ function sanitizeExternalText(text, maxLength = 2e3) {
|
|
|
1468
1727
|
}
|
|
1469
1728
|
function linkToCode(store, content, sourceNodeId, edgeType, options) {
|
|
1470
1729
|
let edgesCreated = 0;
|
|
1471
|
-
for (const type of
|
|
1730
|
+
for (const type of CODE_NODE_TYPES3) {
|
|
1472
1731
|
const nodes = store.findNodes({ type });
|
|
1473
1732
|
for (const node of nodes) {
|
|
1474
1733
|
if (node.name.length < 3) continue;
|
|
@@ -1488,12 +1747,12 @@ function linkToCode(store, content, sourceNodeId, edgeType, options) {
|
|
|
1488
1747
|
}
|
|
1489
1748
|
|
|
1490
1749
|
// src/ingest/connectors/SyncManager.ts
|
|
1491
|
-
import * as
|
|
1492
|
-
import * as
|
|
1750
|
+
import * as fs4 from "fs/promises";
|
|
1751
|
+
import * as path5 from "path";
|
|
1493
1752
|
var SyncManager = class {
|
|
1494
1753
|
constructor(store, graphDir) {
|
|
1495
1754
|
this.store = store;
|
|
1496
|
-
this.metadataPath =
|
|
1755
|
+
this.metadataPath = path5.join(graphDir, "sync-metadata.json");
|
|
1497
1756
|
}
|
|
1498
1757
|
store;
|
|
1499
1758
|
registrations = /* @__PURE__ */ new Map();
|
|
@@ -1548,15 +1807,15 @@ var SyncManager = class {
|
|
|
1548
1807
|
}
|
|
1549
1808
|
async loadMetadata() {
|
|
1550
1809
|
try {
|
|
1551
|
-
const raw = await
|
|
1810
|
+
const raw = await fs4.readFile(this.metadataPath, "utf-8");
|
|
1552
1811
|
return JSON.parse(raw);
|
|
1553
1812
|
} catch {
|
|
1554
1813
|
return { connectors: {} };
|
|
1555
1814
|
}
|
|
1556
1815
|
}
|
|
1557
1816
|
async saveMetadata(metadata) {
|
|
1558
|
-
await
|
|
1559
|
-
await
|
|
1817
|
+
await fs4.mkdir(path5.dirname(this.metadataPath), { recursive: true });
|
|
1818
|
+
await fs4.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
1560
1819
|
}
|
|
1561
1820
|
};
|
|
1562
1821
|
|
|
@@ -2085,7 +2344,7 @@ var FusionLayer = class {
|
|
|
2085
2344
|
};
|
|
2086
2345
|
|
|
2087
2346
|
// src/entropy/GraphEntropyAdapter.ts
|
|
2088
|
-
var
|
|
2347
|
+
var CODE_NODE_TYPES4 = ["file", "function", "class", "method", "interface", "variable"];
|
|
2089
2348
|
var GraphEntropyAdapter = class {
|
|
2090
2349
|
constructor(store) {
|
|
2091
2350
|
this.store = store;
|
|
@@ -2152,7 +2411,7 @@ var GraphEntropyAdapter = class {
|
|
|
2152
2411
|
}
|
|
2153
2412
|
findEntryPoints() {
|
|
2154
2413
|
const entryPoints = [];
|
|
2155
|
-
for (const nodeType of
|
|
2414
|
+
for (const nodeType of CODE_NODE_TYPES4) {
|
|
2156
2415
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
2157
2416
|
for (const node of nodes) {
|
|
2158
2417
|
const isIndexFile = nodeType === "file" && node.name === "index.ts";
|
|
@@ -2188,7 +2447,7 @@ var GraphEntropyAdapter = class {
|
|
|
2188
2447
|
}
|
|
2189
2448
|
collectUnreachableNodes(visited) {
|
|
2190
2449
|
const unreachableNodes = [];
|
|
2191
|
-
for (const nodeType of
|
|
2450
|
+
for (const nodeType of CODE_NODE_TYPES4) {
|
|
2192
2451
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
2193
2452
|
for (const node of nodes) {
|
|
2194
2453
|
if (!visited.has(node.id)) {
|
|
@@ -3008,9 +3267,9 @@ var EntityExtractor = class {
|
|
|
3008
3267
|
}
|
|
3009
3268
|
const pathConsumed = /* @__PURE__ */ new Set();
|
|
3010
3269
|
for (const match of trimmed.matchAll(FILE_PATH_RE)) {
|
|
3011
|
-
const
|
|
3012
|
-
add(
|
|
3013
|
-
pathConsumed.add(
|
|
3270
|
+
const path7 = match[0];
|
|
3271
|
+
add(path7);
|
|
3272
|
+
pathConsumed.add(path7);
|
|
3014
3273
|
}
|
|
3015
3274
|
const allConsumed = buildConsumedSet(quotedConsumed, casingConsumed, pathConsumed);
|
|
3016
3275
|
const words = trimmed.split(/\s+/);
|
|
@@ -3081,8 +3340,8 @@ var EntityResolver = class {
|
|
|
3081
3340
|
if (isPathLike && node.path.includes(raw)) {
|
|
3082
3341
|
return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
|
|
3083
3342
|
}
|
|
3084
|
-
const
|
|
3085
|
-
if (
|
|
3343
|
+
const basename5 = node.path.split("/").pop() ?? "";
|
|
3344
|
+
if (basename5.includes(raw)) {
|
|
3086
3345
|
return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
|
|
3087
3346
|
}
|
|
3088
3347
|
if (raw.length >= 4 && node.path.includes(raw)) {
|
|
@@ -3157,13 +3416,13 @@ var ResponseFormatter = class {
|
|
|
3157
3416
|
const context = Array.isArray(d?.context) ? d.context : [];
|
|
3158
3417
|
const firstEntity = entities[0];
|
|
3159
3418
|
const nodeType = firstEntity?.node.type ?? "node";
|
|
3160
|
-
const
|
|
3419
|
+
const path7 = firstEntity?.node.path ?? "unknown";
|
|
3161
3420
|
let neighborCount = 0;
|
|
3162
3421
|
const firstContext = context[0];
|
|
3163
3422
|
if (firstContext && Array.isArray(firstContext.nodes)) {
|
|
3164
3423
|
neighborCount = firstContext.nodes.length;
|
|
3165
3424
|
}
|
|
3166
|
-
return `**${entityName}** is a ${nodeType} at \`${
|
|
3425
|
+
return `**${entityName}** is a ${nodeType} at \`${path7}\`. Connected to ${neighborCount} nodes.`;
|
|
3167
3426
|
}
|
|
3168
3427
|
formatAnomaly(data) {
|
|
3169
3428
|
const d = data;
|
|
@@ -3304,7 +3563,7 @@ var PHASE_NODE_TYPES = {
|
|
|
3304
3563
|
debug: ["failure", "learning", "function", "method"],
|
|
3305
3564
|
plan: ["adr", "document", "module", "layer"]
|
|
3306
3565
|
};
|
|
3307
|
-
var
|
|
3566
|
+
var CODE_NODE_TYPES5 = /* @__PURE__ */ new Set([
|
|
3308
3567
|
"file",
|
|
3309
3568
|
"function",
|
|
3310
3569
|
"class",
|
|
@@ -3519,7 +3778,7 @@ var Assembler = class {
|
|
|
3519
3778
|
*/
|
|
3520
3779
|
checkCoverage() {
|
|
3521
3780
|
const codeNodes = [];
|
|
3522
|
-
for (const type of
|
|
3781
|
+
for (const type of CODE_NODE_TYPES5) {
|
|
3523
3782
|
codeNodes.push(...this.store.findNodes({ type }));
|
|
3524
3783
|
}
|
|
3525
3784
|
const documented = [];
|
|
@@ -3543,6 +3802,89 @@ var Assembler = class {
|
|
|
3543
3802
|
}
|
|
3544
3803
|
};
|
|
3545
3804
|
|
|
3805
|
+
// src/query/Traceability.ts
|
|
3806
|
+
function queryTraceability(store, options) {
|
|
3807
|
+
const allRequirements = store.findNodes({ type: "requirement" });
|
|
3808
|
+
const filtered = allRequirements.filter((node) => {
|
|
3809
|
+
if (options?.specPath && node.metadata?.specPath !== options.specPath) return false;
|
|
3810
|
+
if (options?.featureName && node.metadata?.featureName !== options.featureName) return false;
|
|
3811
|
+
return true;
|
|
3812
|
+
});
|
|
3813
|
+
if (filtered.length === 0) return [];
|
|
3814
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3815
|
+
for (const req of filtered) {
|
|
3816
|
+
const meta = req.metadata;
|
|
3817
|
+
const specPath = meta?.specPath ?? "";
|
|
3818
|
+
const featureName = meta?.featureName ?? "";
|
|
3819
|
+
const key = `${specPath}\0${featureName}`;
|
|
3820
|
+
const list = groups.get(key);
|
|
3821
|
+
if (list) {
|
|
3822
|
+
list.push(req);
|
|
3823
|
+
} else {
|
|
3824
|
+
groups.set(key, [req]);
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
const results = [];
|
|
3828
|
+
for (const [, reqs] of groups) {
|
|
3829
|
+
const firstReq = reqs[0];
|
|
3830
|
+
const firstMeta = firstReq.metadata;
|
|
3831
|
+
const specPath = firstMeta?.specPath ?? "";
|
|
3832
|
+
const featureName = firstMeta?.featureName ?? "";
|
|
3833
|
+
const requirements = [];
|
|
3834
|
+
for (const req of reqs) {
|
|
3835
|
+
const requiresEdges = store.getEdges({ from: req.id, type: "requires" });
|
|
3836
|
+
const codeFiles = requiresEdges.map((edge) => {
|
|
3837
|
+
const targetNode = store.getNode(edge.to);
|
|
3838
|
+
return {
|
|
3839
|
+
path: targetNode?.path ?? edge.to,
|
|
3840
|
+
confidence: edge.confidence ?? edge.metadata?.confidence ?? 0,
|
|
3841
|
+
method: edge.metadata?.method ?? "convention"
|
|
3842
|
+
};
|
|
3843
|
+
});
|
|
3844
|
+
const verifiedByEdges = store.getEdges({ from: req.id, type: "verified_by" });
|
|
3845
|
+
const testFiles = verifiedByEdges.map((edge) => {
|
|
3846
|
+
const targetNode = store.getNode(edge.to);
|
|
3847
|
+
return {
|
|
3848
|
+
path: targetNode?.path ?? edge.to,
|
|
3849
|
+
confidence: edge.confidence ?? edge.metadata?.confidence ?? 0,
|
|
3850
|
+
method: edge.metadata?.method ?? "convention"
|
|
3851
|
+
};
|
|
3852
|
+
});
|
|
3853
|
+
const hasCode = codeFiles.length > 0;
|
|
3854
|
+
const hasTests = testFiles.length > 0;
|
|
3855
|
+
const status = hasCode && hasTests ? "full" : hasCode ? "code-only" : hasTests ? "test-only" : "none";
|
|
3856
|
+
const allConfidences = [
|
|
3857
|
+
...codeFiles.map((f) => f.confidence),
|
|
3858
|
+
...testFiles.map((f) => f.confidence)
|
|
3859
|
+
];
|
|
3860
|
+
const maxConfidence = allConfidences.length > 0 ? Math.max(...allConfidences) : 0;
|
|
3861
|
+
requirements.push({
|
|
3862
|
+
requirementId: req.id,
|
|
3863
|
+
requirementName: req.name,
|
|
3864
|
+
index: req.metadata?.index ?? 0,
|
|
3865
|
+
codeFiles,
|
|
3866
|
+
testFiles,
|
|
3867
|
+
status,
|
|
3868
|
+
maxConfidence
|
|
3869
|
+
});
|
|
3870
|
+
}
|
|
3871
|
+
requirements.sort((a, b) => a.index - b.index);
|
|
3872
|
+
const total = requirements.length;
|
|
3873
|
+
const withCode = requirements.filter((r) => r.codeFiles.length > 0).length;
|
|
3874
|
+
const withTests = requirements.filter((r) => r.testFiles.length > 0).length;
|
|
3875
|
+
const fullyTraced = requirements.filter((r) => r.status === "full").length;
|
|
3876
|
+
const untraceable = requirements.filter((r) => r.status === "none").length;
|
|
3877
|
+
const coveragePercent = total > 0 ? Math.round(fullyTraced / total * 100) : 0;
|
|
3878
|
+
results.push({
|
|
3879
|
+
specPath,
|
|
3880
|
+
featureName,
|
|
3881
|
+
requirements,
|
|
3882
|
+
summary: { total, withCode, withTests, fullyTraced, untraceable, coveragePercent }
|
|
3883
|
+
});
|
|
3884
|
+
}
|
|
3885
|
+
return results;
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3546
3888
|
// src/constraints/GraphConstraintAdapter.ts
|
|
3547
3889
|
import { minimatch } from "minimatch";
|
|
3548
3890
|
import { relative as relative2 } from "path";
|
|
@@ -3602,14 +3944,14 @@ var GraphConstraintAdapter = class {
|
|
|
3602
3944
|
};
|
|
3603
3945
|
|
|
3604
3946
|
// src/ingest/DesignIngestor.ts
|
|
3605
|
-
import * as
|
|
3606
|
-
import * as
|
|
3947
|
+
import * as fs5 from "fs/promises";
|
|
3948
|
+
import * as path6 from "path";
|
|
3607
3949
|
function isDTCGToken(obj) {
|
|
3608
3950
|
return typeof obj === "object" && obj !== null && "$value" in obj && "$type" in obj;
|
|
3609
3951
|
}
|
|
3610
3952
|
async function readFileOrNull(filePath) {
|
|
3611
3953
|
try {
|
|
3612
|
-
return await
|
|
3954
|
+
return await fs5.readFile(filePath, "utf-8");
|
|
3613
3955
|
} catch {
|
|
3614
3956
|
return null;
|
|
3615
3957
|
}
|
|
@@ -3755,8 +4097,8 @@ var DesignIngestor = class {
|
|
|
3755
4097
|
async ingestAll(designDir) {
|
|
3756
4098
|
const start = Date.now();
|
|
3757
4099
|
const [tokensResult, intentResult] = await Promise.all([
|
|
3758
|
-
this.ingestTokens(
|
|
3759
|
-
this.ingestDesignIntent(
|
|
4100
|
+
this.ingestTokens(path6.join(designDir, "tokens.json")),
|
|
4101
|
+
this.ingestDesignIntent(path6.join(designDir, "DESIGN.md"))
|
|
3760
4102
|
]);
|
|
3761
4103
|
const merged = mergeResults(tokensResult, intentResult);
|
|
3762
4104
|
return { ...merged, durationMs: Date.now() - start };
|
|
@@ -4005,10 +4347,10 @@ var TaskIndependenceAnalyzer = class {
|
|
|
4005
4347
|
includeTypes: ["file"]
|
|
4006
4348
|
});
|
|
4007
4349
|
for (const n of queryResult.nodes) {
|
|
4008
|
-
const
|
|
4009
|
-
if (!fileSet.has(
|
|
4010
|
-
if (!result.has(
|
|
4011
|
-
result.set(
|
|
4350
|
+
const path7 = n.path ?? n.id.replace(/^file:/, "");
|
|
4351
|
+
if (!fileSet.has(path7)) {
|
|
4352
|
+
if (!result.has(path7)) {
|
|
4353
|
+
result.set(path7, file);
|
|
4012
4354
|
}
|
|
4013
4355
|
}
|
|
4014
4356
|
}
|
|
@@ -4392,6 +4734,7 @@ export {
|
|
|
4392
4734
|
KnowledgeIngestor,
|
|
4393
4735
|
NODE_TYPES,
|
|
4394
4736
|
OBSERVABILITY_TYPES,
|
|
4737
|
+
RequirementIngestor,
|
|
4395
4738
|
ResponseFormatter,
|
|
4396
4739
|
SlackConnector,
|
|
4397
4740
|
SyncManager,
|
|
@@ -4404,5 +4747,6 @@ export {
|
|
|
4404
4747
|
linkToCode,
|
|
4405
4748
|
loadGraph,
|
|
4406
4749
|
project,
|
|
4750
|
+
queryTraceability,
|
|
4407
4751
|
saveGraph
|
|
4408
4752
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harness-engineering/graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Knowledge graph for context assembly in Harness Engineering",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"minimatch": "^10.2.5",
|
|
22
22
|
"zod": "^3.25.76",
|
|
23
|
-
"@harness-engineering/types": "0.
|
|
23
|
+
"@harness-engineering/types": "0.8.1"
|
|
24
24
|
},
|
|
25
25
|
"optionalDependencies": {
|
|
26
26
|
"tree-sitter": "^0.22.4",
|