@harness-engineering/graph 0.2.0 → 0.2.2
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 +97 -13
- package/dist/index.d.ts +97 -13
- package/dist/index.js +534 -49
- package/dist/index.mjs +530 -49
- package/package.json +14 -1
- package/dist/.tsbuildinfo +0 -1
package/dist/index.js
CHANGED
|
@@ -36,10 +36,14 @@ __export(index_exports, {
|
|
|
36
36
|
CodeIngestor: () => CodeIngestor,
|
|
37
37
|
ConfluenceConnector: () => ConfluenceConnector,
|
|
38
38
|
ContextQL: () => ContextQL,
|
|
39
|
+
DesignConstraintAdapter: () => DesignConstraintAdapter,
|
|
40
|
+
DesignIngestor: () => DesignIngestor,
|
|
39
41
|
EDGE_TYPES: () => EDGE_TYPES,
|
|
40
42
|
FusionLayer: () => FusionLayer,
|
|
41
43
|
GitIngestor: () => GitIngestor,
|
|
44
|
+
GraphComplexityAdapter: () => GraphComplexityAdapter,
|
|
42
45
|
GraphConstraintAdapter: () => GraphConstraintAdapter,
|
|
46
|
+
GraphCouplingAdapter: () => GraphCouplingAdapter,
|
|
43
47
|
GraphEdgeSchema: () => GraphEdgeSchema,
|
|
44
48
|
GraphEntropyAdapter: () => GraphEntropyAdapter,
|
|
45
49
|
GraphFeedbackAdapter: () => GraphFeedbackAdapter,
|
|
@@ -94,7 +98,11 @@ var NODE_TYPES = [
|
|
|
94
98
|
"layer",
|
|
95
99
|
"pattern",
|
|
96
100
|
"constraint",
|
|
97
|
-
"violation"
|
|
101
|
+
"violation",
|
|
102
|
+
// Design
|
|
103
|
+
"design_token",
|
|
104
|
+
"aesthetic_intent",
|
|
105
|
+
"design_constraint"
|
|
98
106
|
];
|
|
99
107
|
var EDGE_TYPES = [
|
|
100
108
|
// Code relationships
|
|
@@ -118,7 +126,12 @@ var EDGE_TYPES = [
|
|
|
118
126
|
"failed_in",
|
|
119
127
|
// Execution relationships (future)
|
|
120
128
|
"executed_by",
|
|
121
|
-
"measured_by"
|
|
129
|
+
"measured_by",
|
|
130
|
+
// Design relationships
|
|
131
|
+
"uses_token",
|
|
132
|
+
"declares_intent",
|
|
133
|
+
"violates_design",
|
|
134
|
+
"platform_binding"
|
|
122
135
|
];
|
|
123
136
|
var OBSERVABILITY_TYPES = /* @__PURE__ */ new Set(["span", "metric", "log"]);
|
|
124
137
|
var CURRENT_SCHEMA_VERSION = 1;
|
|
@@ -187,6 +200,14 @@ async function loadGraph(dirPath) {
|
|
|
187
200
|
}
|
|
188
201
|
|
|
189
202
|
// src/store/GraphStore.ts
|
|
203
|
+
var POISONED_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
204
|
+
function safeMerge(target, source) {
|
|
205
|
+
for (const key of Object.keys(source)) {
|
|
206
|
+
if (!POISONED_KEYS.has(key)) {
|
|
207
|
+
target[key] = source[key];
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
190
211
|
var GraphStore = class {
|
|
191
212
|
db;
|
|
192
213
|
nodes;
|
|
@@ -205,7 +226,7 @@ var GraphStore = class {
|
|
|
205
226
|
addNode(node) {
|
|
206
227
|
const existing = this.nodes.by("id", node.id);
|
|
207
228
|
if (existing) {
|
|
208
|
-
|
|
229
|
+
safeMerge(existing, node);
|
|
209
230
|
this.nodes.update(existing);
|
|
210
231
|
} else {
|
|
211
232
|
this.nodes.insert({ ...node });
|
|
@@ -249,7 +270,7 @@ var GraphStore = class {
|
|
|
249
270
|
});
|
|
250
271
|
if (existing) {
|
|
251
272
|
if (edge.metadata) {
|
|
252
|
-
|
|
273
|
+
safeMerge(existing, edge);
|
|
253
274
|
this.edges.update(existing);
|
|
254
275
|
}
|
|
255
276
|
return;
|
|
@@ -525,7 +546,7 @@ var CodeIngestor = class {
|
|
|
525
546
|
let nodesAdded = 0;
|
|
526
547
|
let edgesAdded = 0;
|
|
527
548
|
const files = await this.findSourceFiles(rootDir);
|
|
528
|
-
const
|
|
549
|
+
const nameToFiles = /* @__PURE__ */ new Map();
|
|
529
550
|
const fileContents = /* @__PURE__ */ new Map();
|
|
530
551
|
for (const filePath of files) {
|
|
531
552
|
try {
|
|
@@ -551,7 +572,12 @@ var CodeIngestor = class {
|
|
|
551
572
|
nodesAdded++;
|
|
552
573
|
edgesAdded++;
|
|
553
574
|
if (node.type === "function" || node.type === "method") {
|
|
554
|
-
|
|
575
|
+
let files2 = nameToFiles.get(node.name);
|
|
576
|
+
if (!files2) {
|
|
577
|
+
files2 = /* @__PURE__ */ new Set();
|
|
578
|
+
nameToFiles.set(node.name, files2);
|
|
579
|
+
}
|
|
580
|
+
files2.add(relativePath);
|
|
555
581
|
}
|
|
556
582
|
}
|
|
557
583
|
const imports = await this.extractImports(content, fileId, relativePath, rootDir);
|
|
@@ -563,7 +589,7 @@ var CodeIngestor = class {
|
|
|
563
589
|
errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
564
590
|
}
|
|
565
591
|
}
|
|
566
|
-
const callsEdges = this.extractCallsEdges(
|
|
592
|
+
const callsEdges = this.extractCallsEdges(nameToFiles, fileContents);
|
|
567
593
|
for (const edge of callsEdges) {
|
|
568
594
|
this.store.addEdge(edge);
|
|
569
595
|
edgesAdded++;
|
|
@@ -611,7 +637,13 @@ var CodeIngestor = class {
|
|
|
611
637
|
name,
|
|
612
638
|
path: relativePath,
|
|
613
639
|
location: { fileId, startLine: i + 1, endLine },
|
|
614
|
-
metadata: {
|
|
640
|
+
metadata: {
|
|
641
|
+
exported: line.includes("export"),
|
|
642
|
+
cyclomaticComplexity: this.computeCyclomaticComplexity(lines.slice(i, endLine)),
|
|
643
|
+
nestingDepth: this.computeMaxNesting(lines.slice(i, endLine)),
|
|
644
|
+
lineCount: endLine - i,
|
|
645
|
+
parameterCount: this.countParameters(line)
|
|
646
|
+
}
|
|
615
647
|
},
|
|
616
648
|
edge: { from: fileId, to: id, type: "contains" }
|
|
617
649
|
});
|
|
@@ -697,7 +729,14 @@ var CodeIngestor = class {
|
|
|
697
729
|
name: methodName,
|
|
698
730
|
path: relativePath,
|
|
699
731
|
location: { fileId, startLine: i + 1, endLine },
|
|
700
|
-
metadata: {
|
|
732
|
+
metadata: {
|
|
733
|
+
className: currentClassName,
|
|
734
|
+
exported: false,
|
|
735
|
+
cyclomaticComplexity: this.computeCyclomaticComplexity(lines.slice(i, endLine)),
|
|
736
|
+
nestingDepth: this.computeMaxNesting(lines.slice(i, endLine)),
|
|
737
|
+
lineCount: endLine - i,
|
|
738
|
+
parameterCount: this.countParameters(line)
|
|
739
|
+
}
|
|
701
740
|
},
|
|
702
741
|
edge: { from: currentClassId, to: id, type: "contains" }
|
|
703
742
|
});
|
|
@@ -747,43 +786,37 @@ var CodeIngestor = class {
|
|
|
747
786
|
return startIndex + 1;
|
|
748
787
|
}
|
|
749
788
|
/**
|
|
750
|
-
* Second pass:
|
|
751
|
-
*
|
|
789
|
+
* Second pass: scan each file for identifiers matching known callable names,
|
|
790
|
+
* then create file-to-file "calls" edges. Uses regex heuristic (not AST).
|
|
752
791
|
*/
|
|
753
|
-
extractCallsEdges(
|
|
792
|
+
extractCallsEdges(nameToFiles, fileContents) {
|
|
754
793
|
const edges = [];
|
|
755
|
-
const
|
|
756
|
-
for (const
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
metadata: { confidence: "regex" }
|
|
777
|
-
});
|
|
778
|
-
}
|
|
794
|
+
const seen = /* @__PURE__ */ new Set();
|
|
795
|
+
for (const [filePath, content] of fileContents) {
|
|
796
|
+
const callerFileId = `file:${filePath}`;
|
|
797
|
+
const callPattern = /\b([a-zA-Z_$][\w$]*)\s*\(/g;
|
|
798
|
+
let match;
|
|
799
|
+
while ((match = callPattern.exec(content)) !== null) {
|
|
800
|
+
const name = match[1];
|
|
801
|
+
const targetFiles = nameToFiles.get(name);
|
|
802
|
+
if (!targetFiles) continue;
|
|
803
|
+
for (const targetFile of targetFiles) {
|
|
804
|
+
if (targetFile === filePath) continue;
|
|
805
|
+
const targetFileId = `file:${targetFile}`;
|
|
806
|
+
const key = `${callerFileId}|${targetFileId}`;
|
|
807
|
+
if (seen.has(key)) continue;
|
|
808
|
+
seen.add(key);
|
|
809
|
+
edges.push({
|
|
810
|
+
from: callerFileId,
|
|
811
|
+
to: targetFileId,
|
|
812
|
+
type: "calls",
|
|
813
|
+
metadata: { confidence: "regex" }
|
|
814
|
+
});
|
|
779
815
|
}
|
|
780
816
|
}
|
|
781
817
|
}
|
|
782
818
|
return edges;
|
|
783
819
|
}
|
|
784
|
-
escapeRegex(str) {
|
|
785
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
786
|
-
}
|
|
787
820
|
async extractImports(content, fileId, relativePath, rootDir) {
|
|
788
821
|
const edges = [];
|
|
789
822
|
const importRegex = /import\s+(?:type\s+)?(?:\{[^}]*\}|[\w*]+)\s+from\s+['"]([^'"]+)['"]/g;
|
|
@@ -829,6 +862,46 @@ var CodeIngestor = class {
|
|
|
829
862
|
}
|
|
830
863
|
return null;
|
|
831
864
|
}
|
|
865
|
+
computeCyclomaticComplexity(lines) {
|
|
866
|
+
let complexity = 1;
|
|
867
|
+
const decisionPattern = /\b(if|else\s+if|while|for|case)\b|\?\s*[^:?]|&&|\|\||catch\b/g;
|
|
868
|
+
for (const line of lines) {
|
|
869
|
+
const trimmed = line.trim();
|
|
870
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
871
|
+
const matches = trimmed.match(decisionPattern);
|
|
872
|
+
if (matches) complexity += matches.length;
|
|
873
|
+
}
|
|
874
|
+
return complexity;
|
|
875
|
+
}
|
|
876
|
+
computeMaxNesting(lines) {
|
|
877
|
+
let maxDepth = 0;
|
|
878
|
+
let currentDepth = 0;
|
|
879
|
+
for (const line of lines) {
|
|
880
|
+
const trimmed = line.trim();
|
|
881
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
882
|
+
for (const ch of trimmed) {
|
|
883
|
+
if (ch === "{") {
|
|
884
|
+
currentDepth++;
|
|
885
|
+
if (currentDepth > maxDepth) maxDepth = currentDepth;
|
|
886
|
+
} else if (ch === "}") {
|
|
887
|
+
currentDepth--;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return Math.max(0, maxDepth - 1);
|
|
892
|
+
}
|
|
893
|
+
countParameters(declarationLine) {
|
|
894
|
+
const parenMatch = declarationLine.match(/\(([^)]*)\)/);
|
|
895
|
+
if (!parenMatch || !parenMatch[1].trim()) return 0;
|
|
896
|
+
let depth = 0;
|
|
897
|
+
let count = 1;
|
|
898
|
+
for (const ch of parenMatch[1]) {
|
|
899
|
+
if (ch === "<" || ch === "(") depth++;
|
|
900
|
+
else if (ch === ">" || ch === ")") depth--;
|
|
901
|
+
else if (ch === "," && depth === 0) count++;
|
|
902
|
+
}
|
|
903
|
+
return count;
|
|
904
|
+
}
|
|
832
905
|
detectLanguage(filePath) {
|
|
833
906
|
if (/\.tsx?$/.test(filePath)) return "typescript";
|
|
834
907
|
if (/\.jsx?$/.test(filePath)) return "javascript";
|
|
@@ -1095,8 +1168,9 @@ var TopologicalLinker = class {
|
|
|
1095
1168
|
// src/ingest/KnowledgeIngestor.ts
|
|
1096
1169
|
var fs2 = __toESM(require("fs/promises"));
|
|
1097
1170
|
var path3 = __toESM(require("path"));
|
|
1171
|
+
|
|
1172
|
+
// src/ingest/ingestUtils.ts
|
|
1098
1173
|
var crypto = __toESM(require("crypto"));
|
|
1099
|
-
var CODE_NODE_TYPES = ["file", "function", "class", "method", "interface", "variable"];
|
|
1100
1174
|
function hash(text) {
|
|
1101
1175
|
return crypto.createHash("md5").update(text).digest("hex").slice(0, 8);
|
|
1102
1176
|
}
|
|
@@ -1113,6 +1187,9 @@ function mergeResults(...results) {
|
|
|
1113
1187
|
function emptyResult(durationMs = 0) {
|
|
1114
1188
|
return { nodesAdded: 0, nodesUpdated: 0, edgesAdded: 0, edgesUpdated: 0, errors: [], durationMs };
|
|
1115
1189
|
}
|
|
1190
|
+
|
|
1191
|
+
// src/ingest/KnowledgeIngestor.ts
|
|
1192
|
+
var CODE_NODE_TYPES = ["file", "function", "class", "method", "interface", "variable"];
|
|
1116
1193
|
var KnowledgeIngestor = class {
|
|
1117
1194
|
constructor(store) {
|
|
1118
1195
|
this.store = store;
|
|
@@ -1304,6 +1381,22 @@ var KnowledgeIngestor = class {
|
|
|
1304
1381
|
|
|
1305
1382
|
// src/ingest/connectors/ConnectorUtils.ts
|
|
1306
1383
|
var CODE_NODE_TYPES2 = ["file", "function", "class", "method", "interface", "variable"];
|
|
1384
|
+
function sanitizeExternalText(text, maxLength = 2e3) {
|
|
1385
|
+
let sanitized = text.replace(
|
|
1386
|
+
/<\/?(?:system|instruction|prompt|role|context|tool_call|function_call|assistant|human|user)[^>]*>/gi,
|
|
1387
|
+
""
|
|
1388
|
+
).replace(/^#{1,3}\s*(?:system|instruction|prompt)\s*[::]\s*/gim, "").replace(
|
|
1389
|
+
/(?:ignore|disregard|forget)\s+(?:all\s+)?(?:previous|prior|above)\s+(?:instructions?|prompts?|context)/gi,
|
|
1390
|
+
"[filtered]"
|
|
1391
|
+
).replace(
|
|
1392
|
+
/you\s+are\s+now\s+(?:a\s+)?(?:helpful\s+)?(?:an?\s+)?(?:assistant|system|ai|bot|agent|tool)\b/gi,
|
|
1393
|
+
"[filtered]"
|
|
1394
|
+
);
|
|
1395
|
+
if (sanitized.length > maxLength) {
|
|
1396
|
+
sanitized = sanitized.slice(0, maxLength) + "\u2026";
|
|
1397
|
+
}
|
|
1398
|
+
return sanitized;
|
|
1399
|
+
}
|
|
1307
1400
|
function linkToCode(store, content, sourceNodeId, edgeType, options) {
|
|
1308
1401
|
let edgesCreated = 0;
|
|
1309
1402
|
for (const type of CODE_NODE_TYPES2) {
|
|
@@ -1471,7 +1564,7 @@ var JiraConnector = class {
|
|
|
1471
1564
|
store.addNode({
|
|
1472
1565
|
id: nodeId,
|
|
1473
1566
|
type: "issue",
|
|
1474
|
-
name: issue.fields.summary,
|
|
1567
|
+
name: sanitizeExternalText(issue.fields.summary, 500),
|
|
1475
1568
|
metadata: {
|
|
1476
1569
|
key: issue.key,
|
|
1477
1570
|
status: issue.fields.status?.name,
|
|
@@ -1481,7 +1574,9 @@ var JiraConnector = class {
|
|
|
1481
1574
|
}
|
|
1482
1575
|
});
|
|
1483
1576
|
nodesAdded++;
|
|
1484
|
-
const searchText =
|
|
1577
|
+
const searchText = sanitizeExternalText(
|
|
1578
|
+
[issue.fields.summary, issue.fields.description ?? ""].join(" ")
|
|
1579
|
+
);
|
|
1485
1580
|
edgesAdded += linkToCode(store, searchText, nodeId, "applies_to");
|
|
1486
1581
|
}
|
|
1487
1582
|
startAt += maxResults;
|
|
@@ -1557,7 +1652,8 @@ var SlackConnector = class {
|
|
|
1557
1652
|
}
|
|
1558
1653
|
for (const message of data.messages) {
|
|
1559
1654
|
const nodeId = `conversation:slack:${channel}:${message.ts}`;
|
|
1560
|
-
const
|
|
1655
|
+
const sanitizedText = sanitizeExternalText(message.text);
|
|
1656
|
+
const snippet = sanitizedText.length > 100 ? sanitizedText.slice(0, 100) : sanitizedText;
|
|
1561
1657
|
store.addNode({
|
|
1562
1658
|
id: nodeId,
|
|
1563
1659
|
type: "conversation",
|
|
@@ -1569,7 +1665,9 @@ var SlackConnector = class {
|
|
|
1569
1665
|
}
|
|
1570
1666
|
});
|
|
1571
1667
|
nodesAdded++;
|
|
1572
|
-
edgesAdded += linkToCode(store,
|
|
1668
|
+
edgesAdded += linkToCode(store, sanitizedText, nodeId, "references", {
|
|
1669
|
+
checkPaths: true
|
|
1670
|
+
});
|
|
1573
1671
|
}
|
|
1574
1672
|
} catch (err) {
|
|
1575
1673
|
errors.push(
|
|
@@ -1632,7 +1730,7 @@ var ConfluenceConnector = class {
|
|
|
1632
1730
|
store.addNode({
|
|
1633
1731
|
id: nodeId,
|
|
1634
1732
|
type: "document",
|
|
1635
|
-
name: page.title,
|
|
1733
|
+
name: sanitizeExternalText(page.title, 500),
|
|
1636
1734
|
metadata: {
|
|
1637
1735
|
source: "confluence",
|
|
1638
1736
|
spaceKey,
|
|
@@ -1642,7 +1740,7 @@ var ConfluenceConnector = class {
|
|
|
1642
1740
|
}
|
|
1643
1741
|
});
|
|
1644
1742
|
nodesAdded++;
|
|
1645
|
-
const text = `${page.title} ${page.body?.storage?.value ?? ""}
|
|
1743
|
+
const text = sanitizeExternalText(`${page.title} ${page.body?.storage?.value ?? ""}`);
|
|
1646
1744
|
edgesAdded += linkToCode(store, text, nodeId, "documents");
|
|
1647
1745
|
}
|
|
1648
1746
|
nextUrl = data._links?.next ? `${baseUrl}${data._links.next}` : null;
|
|
@@ -1707,10 +1805,11 @@ var CIConnector = class {
|
|
|
1707
1805
|
const data = await response.json();
|
|
1708
1806
|
for (const run of data.workflow_runs) {
|
|
1709
1807
|
const buildId = `build:${run.id}`;
|
|
1808
|
+
const safeName = sanitizeExternalText(run.name, 200);
|
|
1710
1809
|
store.addNode({
|
|
1711
1810
|
id: buildId,
|
|
1712
1811
|
type: "build",
|
|
1713
|
-
name: `${
|
|
1812
|
+
name: `${safeName} #${run.id}`,
|
|
1714
1813
|
metadata: {
|
|
1715
1814
|
source: "github-actions",
|
|
1716
1815
|
status: run.status,
|
|
@@ -1732,7 +1831,7 @@ var CIConnector = class {
|
|
|
1732
1831
|
store.addNode({
|
|
1733
1832
|
id: testResultId,
|
|
1734
1833
|
type: "test_result",
|
|
1735
|
-
name: `Failed: ${
|
|
1834
|
+
name: `Failed: ${safeName} #${run.id}`,
|
|
1736
1835
|
metadata: {
|
|
1737
1836
|
source: "github-actions",
|
|
1738
1837
|
buildId: String(run.id),
|
|
@@ -2035,6 +2134,144 @@ var GraphEntropyAdapter = class {
|
|
|
2035
2134
|
}
|
|
2036
2135
|
};
|
|
2037
2136
|
|
|
2137
|
+
// src/entropy/GraphComplexityAdapter.ts
|
|
2138
|
+
var GraphComplexityAdapter = class {
|
|
2139
|
+
constructor(store) {
|
|
2140
|
+
this.store = store;
|
|
2141
|
+
}
|
|
2142
|
+
/**
|
|
2143
|
+
* Compute complexity hotspots by combining cyclomatic complexity with change frequency.
|
|
2144
|
+
*
|
|
2145
|
+
* 1. Find all function and method nodes
|
|
2146
|
+
* 2. For each, find the containing file and count commit nodes referencing that file
|
|
2147
|
+
* 3. Compute hotspotScore = changeFrequency * cyclomaticComplexity
|
|
2148
|
+
* 4. Sort descending by score
|
|
2149
|
+
* 5. Compute 95th percentile
|
|
2150
|
+
*/
|
|
2151
|
+
computeComplexityHotspots() {
|
|
2152
|
+
const functionNodes = [
|
|
2153
|
+
...this.store.findNodes({ type: "function" }),
|
|
2154
|
+
...this.store.findNodes({ type: "method" })
|
|
2155
|
+
];
|
|
2156
|
+
if (functionNodes.length === 0) {
|
|
2157
|
+
return { hotspots: [], percentile95Score: 0 };
|
|
2158
|
+
}
|
|
2159
|
+
const fileChangeFrequency = /* @__PURE__ */ new Map();
|
|
2160
|
+
const hotspots = [];
|
|
2161
|
+
for (const fnNode of functionNodes) {
|
|
2162
|
+
const complexity = fnNode.metadata?.cyclomaticComplexity ?? 1;
|
|
2163
|
+
const containsEdges = this.store.getEdges({ to: fnNode.id, type: "contains" });
|
|
2164
|
+
let fileId;
|
|
2165
|
+
for (const edge of containsEdges) {
|
|
2166
|
+
const sourceNode = this.store.getNode(edge.from);
|
|
2167
|
+
if (sourceNode?.type === "file") {
|
|
2168
|
+
fileId = sourceNode.id;
|
|
2169
|
+
break;
|
|
2170
|
+
}
|
|
2171
|
+
if (sourceNode?.type === "class") {
|
|
2172
|
+
const classContainsEdges = this.store.getEdges({ to: sourceNode.id, type: "contains" });
|
|
2173
|
+
for (const classEdge of classContainsEdges) {
|
|
2174
|
+
const parentNode = this.store.getNode(classEdge.from);
|
|
2175
|
+
if (parentNode?.type === "file") {
|
|
2176
|
+
fileId = parentNode.id;
|
|
2177
|
+
break;
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
if (fileId) break;
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
if (!fileId) continue;
|
|
2184
|
+
let changeFrequency = fileChangeFrequency.get(fileId);
|
|
2185
|
+
if (changeFrequency === void 0) {
|
|
2186
|
+
const referencesEdges = this.store.getEdges({ to: fileId, type: "references" });
|
|
2187
|
+
changeFrequency = referencesEdges.length;
|
|
2188
|
+
fileChangeFrequency.set(fileId, changeFrequency);
|
|
2189
|
+
}
|
|
2190
|
+
const hotspotScore = changeFrequency * complexity;
|
|
2191
|
+
const filePath = fnNode.path ?? fileId.replace(/^file:/, "");
|
|
2192
|
+
hotspots.push({
|
|
2193
|
+
file: filePath,
|
|
2194
|
+
function: fnNode.name,
|
|
2195
|
+
changeFrequency,
|
|
2196
|
+
complexity,
|
|
2197
|
+
hotspotScore
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
hotspots.sort((a, b) => b.hotspotScore - a.hotspotScore);
|
|
2201
|
+
const percentile95Score = this.computePercentile(
|
|
2202
|
+
hotspots.map((h) => h.hotspotScore),
|
|
2203
|
+
95
|
|
2204
|
+
);
|
|
2205
|
+
return { hotspots, percentile95Score };
|
|
2206
|
+
}
|
|
2207
|
+
computePercentile(descendingScores, percentile) {
|
|
2208
|
+
if (descendingScores.length === 0) return 0;
|
|
2209
|
+
const ascending = [...descendingScores].sort((a, b) => a - b);
|
|
2210
|
+
const index = Math.ceil(percentile / 100 * ascending.length) - 1;
|
|
2211
|
+
return ascending[Math.min(index, ascending.length - 1)];
|
|
2212
|
+
}
|
|
2213
|
+
};
|
|
2214
|
+
|
|
2215
|
+
// src/entropy/GraphCouplingAdapter.ts
|
|
2216
|
+
var GraphCouplingAdapter = class {
|
|
2217
|
+
constructor(store) {
|
|
2218
|
+
this.store = store;
|
|
2219
|
+
}
|
|
2220
|
+
/**
|
|
2221
|
+
* Compute coupling data for all file nodes in the graph.
|
|
2222
|
+
*
|
|
2223
|
+
* For each file:
|
|
2224
|
+
* - fanOut: number of outbound 'imports' edges
|
|
2225
|
+
* - fanIn: number of inbound 'imports' edges from other files
|
|
2226
|
+
* - couplingRatio: fanOut / (fanIn + fanOut), rounded to 2 decimals (0 if both are 0)
|
|
2227
|
+
* - transitiveDepth: longest chain of outbound 'imports' edges via BFS
|
|
2228
|
+
*/
|
|
2229
|
+
computeCouplingData() {
|
|
2230
|
+
const fileNodes = this.store.findNodes({ type: "file" });
|
|
2231
|
+
if (fileNodes.length === 0) {
|
|
2232
|
+
return { files: [] };
|
|
2233
|
+
}
|
|
2234
|
+
const files = [];
|
|
2235
|
+
for (const node of fileNodes) {
|
|
2236
|
+
const fileId = node.id;
|
|
2237
|
+
const filePath = node.path ?? node.name;
|
|
2238
|
+
const outEdges = this.store.getEdges({ from: fileId, type: "imports" });
|
|
2239
|
+
const fanOut = outEdges.length;
|
|
2240
|
+
const inEdges = this.store.getEdges({ to: fileId, type: "imports" });
|
|
2241
|
+
const fanIn = inEdges.length;
|
|
2242
|
+
const total = fanIn + fanOut;
|
|
2243
|
+
const couplingRatio = total === 0 ? 0 : Math.round(fanOut / total * 100) / 100;
|
|
2244
|
+
const transitiveDepth = this.computeTransitiveDepth(fileId);
|
|
2245
|
+
files.push({ file: filePath, fanIn, fanOut, couplingRatio, transitiveDepth });
|
|
2246
|
+
}
|
|
2247
|
+
return { files };
|
|
2248
|
+
}
|
|
2249
|
+
/**
|
|
2250
|
+
* BFS from a node following outbound 'imports' edges to find the maximum depth.
|
|
2251
|
+
*/
|
|
2252
|
+
computeTransitiveDepth(startId) {
|
|
2253
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2254
|
+
const queue = [[startId, 0]];
|
|
2255
|
+
visited.add(startId);
|
|
2256
|
+
let maxDepth = 0;
|
|
2257
|
+
let head = 0;
|
|
2258
|
+
while (head < queue.length) {
|
|
2259
|
+
const [nodeId, depth] = queue[head++];
|
|
2260
|
+
if (depth > maxDepth) {
|
|
2261
|
+
maxDepth = depth;
|
|
2262
|
+
}
|
|
2263
|
+
const outEdges = this.store.getEdges({ from: nodeId, type: "imports" });
|
|
2264
|
+
for (const edge of outEdges) {
|
|
2265
|
+
if (!visited.has(edge.to)) {
|
|
2266
|
+
visited.add(edge.to);
|
|
2267
|
+
queue.push([edge.to, depth + 1]);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
return maxDepth;
|
|
2272
|
+
}
|
|
2273
|
+
};
|
|
2274
|
+
|
|
2038
2275
|
// src/context/Assembler.ts
|
|
2039
2276
|
var PHASE_NODE_TYPES = {
|
|
2040
2277
|
implement: ["file", "function", "class", "method", "interface", "variable"],
|
|
@@ -2342,6 +2579,250 @@ var GraphConstraintAdapter = class {
|
|
|
2342
2579
|
}
|
|
2343
2580
|
};
|
|
2344
2581
|
|
|
2582
|
+
// src/ingest/DesignIngestor.ts
|
|
2583
|
+
var fs4 = __toESM(require("fs/promises"));
|
|
2584
|
+
var path5 = __toESM(require("path"));
|
|
2585
|
+
function isDTCGToken(obj) {
|
|
2586
|
+
return typeof obj === "object" && obj !== null && "$value" in obj && "$type" in obj;
|
|
2587
|
+
}
|
|
2588
|
+
async function readFileOrNull(filePath) {
|
|
2589
|
+
try {
|
|
2590
|
+
return await fs4.readFile(filePath, "utf-8");
|
|
2591
|
+
} catch {
|
|
2592
|
+
return null;
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
function parseJsonOrError(content, filePath) {
|
|
2596
|
+
try {
|
|
2597
|
+
return { data: JSON.parse(content) };
|
|
2598
|
+
} catch (err) {
|
|
2599
|
+
return {
|
|
2600
|
+
error: `Failed to parse ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
2601
|
+
};
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
function walkDTCGTokens(store, obj, groupPath, topGroup, tokensPath) {
|
|
2605
|
+
let count = 0;
|
|
2606
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
2607
|
+
if (key.startsWith("$")) continue;
|
|
2608
|
+
if (isDTCGToken(value)) {
|
|
2609
|
+
const tokenPath = [...groupPath, key].join(".");
|
|
2610
|
+
store.addNode({
|
|
2611
|
+
id: `design_token:${tokenPath}`,
|
|
2612
|
+
type: "design_token",
|
|
2613
|
+
name: tokenPath,
|
|
2614
|
+
path: tokensPath,
|
|
2615
|
+
metadata: {
|
|
2616
|
+
tokenType: value.$type,
|
|
2617
|
+
value: value.$value,
|
|
2618
|
+
group: topGroup || groupPath[0] || key,
|
|
2619
|
+
...value.$description ? { description: value.$description } : {}
|
|
2620
|
+
}
|
|
2621
|
+
});
|
|
2622
|
+
count++;
|
|
2623
|
+
} else if (typeof value === "object" && value !== null) {
|
|
2624
|
+
count += walkDTCGTokens(
|
|
2625
|
+
store,
|
|
2626
|
+
value,
|
|
2627
|
+
[...groupPath, key],
|
|
2628
|
+
topGroup || key,
|
|
2629
|
+
tokensPath
|
|
2630
|
+
);
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
return count;
|
|
2634
|
+
}
|
|
2635
|
+
function parseAestheticDirection(content) {
|
|
2636
|
+
const extract = (pattern) => {
|
|
2637
|
+
const m = content.match(pattern);
|
|
2638
|
+
return m ? m[1].trim() : void 0;
|
|
2639
|
+
};
|
|
2640
|
+
const result = {};
|
|
2641
|
+
const style = extract(/\*\*Style:\*\*\s*(.+)/);
|
|
2642
|
+
if (style !== void 0) result.style = style;
|
|
2643
|
+
const tone = extract(/\*\*Tone:\*\*\s*(.+)/);
|
|
2644
|
+
if (tone !== void 0) result.tone = tone;
|
|
2645
|
+
const differentiator = extract(/\*\*Differentiator:\*\*\s*(.+)/);
|
|
2646
|
+
if (differentiator !== void 0) result.differentiator = differentiator;
|
|
2647
|
+
const strictness = extract(/^level:\s*(strict|standard|permissive)\s*$/m);
|
|
2648
|
+
if (strictness !== void 0) result.strictness = strictness;
|
|
2649
|
+
return result;
|
|
2650
|
+
}
|
|
2651
|
+
function parseAntiPatterns(content) {
|
|
2652
|
+
const lines = content.split("\n");
|
|
2653
|
+
const patterns = [];
|
|
2654
|
+
let inSection = false;
|
|
2655
|
+
for (const line of lines) {
|
|
2656
|
+
if (/^##\s+Anti-Patterns/i.test(line)) {
|
|
2657
|
+
inSection = true;
|
|
2658
|
+
continue;
|
|
2659
|
+
}
|
|
2660
|
+
if (inSection && /^##\s+/.test(line)) {
|
|
2661
|
+
break;
|
|
2662
|
+
}
|
|
2663
|
+
if (inSection) {
|
|
2664
|
+
const bulletMatch = line.match(/^-\s+(.+)/);
|
|
2665
|
+
if (bulletMatch) {
|
|
2666
|
+
patterns.push(bulletMatch[1].trim());
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
return patterns;
|
|
2671
|
+
}
|
|
2672
|
+
var DesignIngestor = class {
|
|
2673
|
+
constructor(store) {
|
|
2674
|
+
this.store = store;
|
|
2675
|
+
}
|
|
2676
|
+
async ingestTokens(tokensPath) {
|
|
2677
|
+
const start = Date.now();
|
|
2678
|
+
const content = await readFileOrNull(tokensPath);
|
|
2679
|
+
if (content === null) return emptyResult(Date.now() - start);
|
|
2680
|
+
const parsed = parseJsonOrError(content, tokensPath);
|
|
2681
|
+
if ("error" in parsed) {
|
|
2682
|
+
return { ...emptyResult(Date.now() - start), errors: [parsed.error] };
|
|
2683
|
+
}
|
|
2684
|
+
const nodesAdded = walkDTCGTokens(this.store, parsed.data, [], "", tokensPath);
|
|
2685
|
+
return {
|
|
2686
|
+
nodesAdded,
|
|
2687
|
+
nodesUpdated: 0,
|
|
2688
|
+
edgesAdded: 0,
|
|
2689
|
+
edgesUpdated: 0,
|
|
2690
|
+
errors: [],
|
|
2691
|
+
durationMs: Date.now() - start
|
|
2692
|
+
};
|
|
2693
|
+
}
|
|
2694
|
+
async ingestDesignIntent(designPath) {
|
|
2695
|
+
const start = Date.now();
|
|
2696
|
+
const content = await readFileOrNull(designPath);
|
|
2697
|
+
if (content === null) return emptyResult(Date.now() - start);
|
|
2698
|
+
let nodesAdded = 0;
|
|
2699
|
+
const direction = parseAestheticDirection(content);
|
|
2700
|
+
const metadata = {};
|
|
2701
|
+
if (direction.style) metadata.style = direction.style;
|
|
2702
|
+
if (direction.tone) metadata.tone = direction.tone;
|
|
2703
|
+
if (direction.differentiator) metadata.differentiator = direction.differentiator;
|
|
2704
|
+
if (direction.strictness) metadata.strictness = direction.strictness;
|
|
2705
|
+
this.store.addNode({
|
|
2706
|
+
id: "aesthetic_intent:project",
|
|
2707
|
+
type: "aesthetic_intent",
|
|
2708
|
+
name: "project",
|
|
2709
|
+
path: designPath,
|
|
2710
|
+
metadata
|
|
2711
|
+
});
|
|
2712
|
+
nodesAdded++;
|
|
2713
|
+
for (const text of parseAntiPatterns(content)) {
|
|
2714
|
+
this.store.addNode({
|
|
2715
|
+
id: `design_constraint:${hash(text)}`,
|
|
2716
|
+
type: "design_constraint",
|
|
2717
|
+
name: text,
|
|
2718
|
+
path: designPath,
|
|
2719
|
+
metadata: { rule: text, severity: "warn", scope: "project" }
|
|
2720
|
+
});
|
|
2721
|
+
nodesAdded++;
|
|
2722
|
+
}
|
|
2723
|
+
return {
|
|
2724
|
+
nodesAdded,
|
|
2725
|
+
nodesUpdated: 0,
|
|
2726
|
+
edgesAdded: 0,
|
|
2727
|
+
edgesUpdated: 0,
|
|
2728
|
+
errors: [],
|
|
2729
|
+
durationMs: Date.now() - start
|
|
2730
|
+
};
|
|
2731
|
+
}
|
|
2732
|
+
async ingestAll(designDir) {
|
|
2733
|
+
const start = Date.now();
|
|
2734
|
+
const [tokensResult, intentResult] = await Promise.all([
|
|
2735
|
+
this.ingestTokens(path5.join(designDir, "tokens.json")),
|
|
2736
|
+
this.ingestDesignIntent(path5.join(designDir, "DESIGN.md"))
|
|
2737
|
+
]);
|
|
2738
|
+
const merged = mergeResults(tokensResult, intentResult);
|
|
2739
|
+
return { ...merged, durationMs: Date.now() - start };
|
|
2740
|
+
}
|
|
2741
|
+
};
|
|
2742
|
+
|
|
2743
|
+
// src/constraints/DesignConstraintAdapter.ts
|
|
2744
|
+
var DesignConstraintAdapter = class {
|
|
2745
|
+
constructor(store) {
|
|
2746
|
+
this.store = store;
|
|
2747
|
+
}
|
|
2748
|
+
checkForHardcodedColors(source, file, strictness) {
|
|
2749
|
+
const severity = this.mapSeverity(strictness);
|
|
2750
|
+
const tokenNodes = this.store.findNodes({ type: "design_token" });
|
|
2751
|
+
const colorValues = /* @__PURE__ */ new Set();
|
|
2752
|
+
for (const node of tokenNodes) {
|
|
2753
|
+
if (node.metadata.tokenType === "color" && typeof node.metadata.value === "string") {
|
|
2754
|
+
colorValues.add(node.metadata.value.toLowerCase());
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
const hexPattern = /#[0-9a-fA-F]{3,8}\b/g;
|
|
2758
|
+
const violations = [];
|
|
2759
|
+
let match;
|
|
2760
|
+
while ((match = hexPattern.exec(source)) !== null) {
|
|
2761
|
+
const hexValue = match[0];
|
|
2762
|
+
if (!colorValues.has(hexValue.toLowerCase())) {
|
|
2763
|
+
violations.push({
|
|
2764
|
+
code: "DESIGN-001",
|
|
2765
|
+
file,
|
|
2766
|
+
message: `Hardcoded color ${hexValue} is not in the design token set`,
|
|
2767
|
+
severity,
|
|
2768
|
+
value: hexValue
|
|
2769
|
+
});
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
return violations;
|
|
2773
|
+
}
|
|
2774
|
+
checkForHardcodedFonts(source, file, strictness) {
|
|
2775
|
+
const severity = this.mapSeverity(strictness);
|
|
2776
|
+
const tokenNodes = this.store.findNodes({ type: "design_token" });
|
|
2777
|
+
const fontFamilies = /* @__PURE__ */ new Set();
|
|
2778
|
+
for (const node of tokenNodes) {
|
|
2779
|
+
if (node.metadata.tokenType === "typography") {
|
|
2780
|
+
const value = node.metadata.value;
|
|
2781
|
+
if (typeof value === "object" && value !== null && "fontFamily" in value) {
|
|
2782
|
+
fontFamilies.add(value.fontFamily.toLowerCase());
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
const fontPatterns = [/fontFamily:\s*['"]([^'"]+)['"]/g, /font-family:\s*['"]([^'"]+)['"]/g];
|
|
2787
|
+
const violations = [];
|
|
2788
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2789
|
+
for (const pattern of fontPatterns) {
|
|
2790
|
+
let match;
|
|
2791
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
2792
|
+
const fontName = match[1];
|
|
2793
|
+
if (seen.has(fontName.toLowerCase())) continue;
|
|
2794
|
+
seen.add(fontName.toLowerCase());
|
|
2795
|
+
if (!fontFamilies.has(fontName.toLowerCase())) {
|
|
2796
|
+
violations.push({
|
|
2797
|
+
code: "DESIGN-002",
|
|
2798
|
+
file,
|
|
2799
|
+
message: `Hardcoded font family "${fontName}" is not in the design token set`,
|
|
2800
|
+
severity,
|
|
2801
|
+
value: fontName
|
|
2802
|
+
});
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
return violations;
|
|
2807
|
+
}
|
|
2808
|
+
checkAll(source, file, strictness) {
|
|
2809
|
+
return [
|
|
2810
|
+
...this.checkForHardcodedColors(source, file, strictness),
|
|
2811
|
+
...this.checkForHardcodedFonts(source, file, strictness)
|
|
2812
|
+
];
|
|
2813
|
+
}
|
|
2814
|
+
mapSeverity(strictness = "standard") {
|
|
2815
|
+
switch (strictness) {
|
|
2816
|
+
case "permissive":
|
|
2817
|
+
return "info";
|
|
2818
|
+
case "standard":
|
|
2819
|
+
return "warn";
|
|
2820
|
+
case "strict":
|
|
2821
|
+
return "error";
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
};
|
|
2825
|
+
|
|
2345
2826
|
// src/feedback/GraphFeedbackAdapter.ts
|
|
2346
2827
|
var GraphFeedbackAdapter = class {
|
|
2347
2828
|
constructor(store) {
|
|
@@ -2414,7 +2895,7 @@ var GraphFeedbackAdapter = class {
|
|
|
2414
2895
|
};
|
|
2415
2896
|
|
|
2416
2897
|
// src/index.ts
|
|
2417
|
-
var VERSION = "0.
|
|
2898
|
+
var VERSION = "0.2.0";
|
|
2418
2899
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2419
2900
|
0 && (module.exports = {
|
|
2420
2901
|
Assembler,
|
|
@@ -2423,10 +2904,14 @@ var VERSION = "0.1.0";
|
|
|
2423
2904
|
CodeIngestor,
|
|
2424
2905
|
ConfluenceConnector,
|
|
2425
2906
|
ContextQL,
|
|
2907
|
+
DesignConstraintAdapter,
|
|
2908
|
+
DesignIngestor,
|
|
2426
2909
|
EDGE_TYPES,
|
|
2427
2910
|
FusionLayer,
|
|
2428
2911
|
GitIngestor,
|
|
2912
|
+
GraphComplexityAdapter,
|
|
2429
2913
|
GraphConstraintAdapter,
|
|
2914
|
+
GraphCouplingAdapter,
|
|
2430
2915
|
GraphEdgeSchema,
|
|
2431
2916
|
GraphEntropyAdapter,
|
|
2432
2917
|
GraphFeedbackAdapter,
|