@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.mjs
CHANGED
|
@@ -31,7 +31,11 @@ var NODE_TYPES = [
|
|
|
31
31
|
"layer",
|
|
32
32
|
"pattern",
|
|
33
33
|
"constraint",
|
|
34
|
-
"violation"
|
|
34
|
+
"violation",
|
|
35
|
+
// Design
|
|
36
|
+
"design_token",
|
|
37
|
+
"aesthetic_intent",
|
|
38
|
+
"design_constraint"
|
|
35
39
|
];
|
|
36
40
|
var EDGE_TYPES = [
|
|
37
41
|
// Code relationships
|
|
@@ -55,7 +59,12 @@ var EDGE_TYPES = [
|
|
|
55
59
|
"failed_in",
|
|
56
60
|
// Execution relationships (future)
|
|
57
61
|
"executed_by",
|
|
58
|
-
"measured_by"
|
|
62
|
+
"measured_by",
|
|
63
|
+
// Design relationships
|
|
64
|
+
"uses_token",
|
|
65
|
+
"declares_intent",
|
|
66
|
+
"violates_design",
|
|
67
|
+
"platform_binding"
|
|
59
68
|
];
|
|
60
69
|
var OBSERVABILITY_TYPES = /* @__PURE__ */ new Set(["span", "metric", "log"]);
|
|
61
70
|
var CURRENT_SCHEMA_VERSION = 1;
|
|
@@ -124,6 +133,14 @@ async function loadGraph(dirPath) {
|
|
|
124
133
|
}
|
|
125
134
|
|
|
126
135
|
// src/store/GraphStore.ts
|
|
136
|
+
var POISONED_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
137
|
+
function safeMerge(target, source) {
|
|
138
|
+
for (const key of Object.keys(source)) {
|
|
139
|
+
if (!POISONED_KEYS.has(key)) {
|
|
140
|
+
target[key] = source[key];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
127
144
|
var GraphStore = class {
|
|
128
145
|
db;
|
|
129
146
|
nodes;
|
|
@@ -142,7 +159,7 @@ var GraphStore = class {
|
|
|
142
159
|
addNode(node) {
|
|
143
160
|
const existing = this.nodes.by("id", node.id);
|
|
144
161
|
if (existing) {
|
|
145
|
-
|
|
162
|
+
safeMerge(existing, node);
|
|
146
163
|
this.nodes.update(existing);
|
|
147
164
|
} else {
|
|
148
165
|
this.nodes.insert({ ...node });
|
|
@@ -186,7 +203,7 @@ var GraphStore = class {
|
|
|
186
203
|
});
|
|
187
204
|
if (existing) {
|
|
188
205
|
if (edge.metadata) {
|
|
189
|
-
|
|
206
|
+
safeMerge(existing, edge);
|
|
190
207
|
this.edges.update(existing);
|
|
191
208
|
}
|
|
192
209
|
return;
|
|
@@ -462,7 +479,7 @@ var CodeIngestor = class {
|
|
|
462
479
|
let nodesAdded = 0;
|
|
463
480
|
let edgesAdded = 0;
|
|
464
481
|
const files = await this.findSourceFiles(rootDir);
|
|
465
|
-
const
|
|
482
|
+
const nameToFiles = /* @__PURE__ */ new Map();
|
|
466
483
|
const fileContents = /* @__PURE__ */ new Map();
|
|
467
484
|
for (const filePath of files) {
|
|
468
485
|
try {
|
|
@@ -488,7 +505,12 @@ var CodeIngestor = class {
|
|
|
488
505
|
nodesAdded++;
|
|
489
506
|
edgesAdded++;
|
|
490
507
|
if (node.type === "function" || node.type === "method") {
|
|
491
|
-
|
|
508
|
+
let files2 = nameToFiles.get(node.name);
|
|
509
|
+
if (!files2) {
|
|
510
|
+
files2 = /* @__PURE__ */ new Set();
|
|
511
|
+
nameToFiles.set(node.name, files2);
|
|
512
|
+
}
|
|
513
|
+
files2.add(relativePath);
|
|
492
514
|
}
|
|
493
515
|
}
|
|
494
516
|
const imports = await this.extractImports(content, fileId, relativePath, rootDir);
|
|
@@ -500,7 +522,7 @@ var CodeIngestor = class {
|
|
|
500
522
|
errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
501
523
|
}
|
|
502
524
|
}
|
|
503
|
-
const callsEdges = this.extractCallsEdges(
|
|
525
|
+
const callsEdges = this.extractCallsEdges(nameToFiles, fileContents);
|
|
504
526
|
for (const edge of callsEdges) {
|
|
505
527
|
this.store.addEdge(edge);
|
|
506
528
|
edgesAdded++;
|
|
@@ -548,7 +570,13 @@ var CodeIngestor = class {
|
|
|
548
570
|
name,
|
|
549
571
|
path: relativePath,
|
|
550
572
|
location: { fileId, startLine: i + 1, endLine },
|
|
551
|
-
metadata: {
|
|
573
|
+
metadata: {
|
|
574
|
+
exported: line.includes("export"),
|
|
575
|
+
cyclomaticComplexity: this.computeCyclomaticComplexity(lines.slice(i, endLine)),
|
|
576
|
+
nestingDepth: this.computeMaxNesting(lines.slice(i, endLine)),
|
|
577
|
+
lineCount: endLine - i,
|
|
578
|
+
parameterCount: this.countParameters(line)
|
|
579
|
+
}
|
|
552
580
|
},
|
|
553
581
|
edge: { from: fileId, to: id, type: "contains" }
|
|
554
582
|
});
|
|
@@ -634,7 +662,14 @@ var CodeIngestor = class {
|
|
|
634
662
|
name: methodName,
|
|
635
663
|
path: relativePath,
|
|
636
664
|
location: { fileId, startLine: i + 1, endLine },
|
|
637
|
-
metadata: {
|
|
665
|
+
metadata: {
|
|
666
|
+
className: currentClassName,
|
|
667
|
+
exported: false,
|
|
668
|
+
cyclomaticComplexity: this.computeCyclomaticComplexity(lines.slice(i, endLine)),
|
|
669
|
+
nestingDepth: this.computeMaxNesting(lines.slice(i, endLine)),
|
|
670
|
+
lineCount: endLine - i,
|
|
671
|
+
parameterCount: this.countParameters(line)
|
|
672
|
+
}
|
|
638
673
|
},
|
|
639
674
|
edge: { from: currentClassId, to: id, type: "contains" }
|
|
640
675
|
});
|
|
@@ -684,43 +719,37 @@ var CodeIngestor = class {
|
|
|
684
719
|
return startIndex + 1;
|
|
685
720
|
}
|
|
686
721
|
/**
|
|
687
|
-
* Second pass:
|
|
688
|
-
*
|
|
722
|
+
* Second pass: scan each file for identifiers matching known callable names,
|
|
723
|
+
* then create file-to-file "calls" edges. Uses regex heuristic (not AST).
|
|
689
724
|
*/
|
|
690
|
-
extractCallsEdges(
|
|
725
|
+
extractCallsEdges(nameToFiles, fileContents) {
|
|
691
726
|
const edges = [];
|
|
692
|
-
const
|
|
693
|
-
for (const
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
metadata: { confidence: "regex" }
|
|
714
|
-
});
|
|
715
|
-
}
|
|
727
|
+
const seen = /* @__PURE__ */ new Set();
|
|
728
|
+
for (const [filePath, content] of fileContents) {
|
|
729
|
+
const callerFileId = `file:${filePath}`;
|
|
730
|
+
const callPattern = /\b([a-zA-Z_$][\w$]*)\s*\(/g;
|
|
731
|
+
let match;
|
|
732
|
+
while ((match = callPattern.exec(content)) !== null) {
|
|
733
|
+
const name = match[1];
|
|
734
|
+
const targetFiles = nameToFiles.get(name);
|
|
735
|
+
if (!targetFiles) continue;
|
|
736
|
+
for (const targetFile of targetFiles) {
|
|
737
|
+
if (targetFile === filePath) continue;
|
|
738
|
+
const targetFileId = `file:${targetFile}`;
|
|
739
|
+
const key = `${callerFileId}|${targetFileId}`;
|
|
740
|
+
if (seen.has(key)) continue;
|
|
741
|
+
seen.add(key);
|
|
742
|
+
edges.push({
|
|
743
|
+
from: callerFileId,
|
|
744
|
+
to: targetFileId,
|
|
745
|
+
type: "calls",
|
|
746
|
+
metadata: { confidence: "regex" }
|
|
747
|
+
});
|
|
716
748
|
}
|
|
717
749
|
}
|
|
718
750
|
}
|
|
719
751
|
return edges;
|
|
720
752
|
}
|
|
721
|
-
escapeRegex(str) {
|
|
722
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
723
|
-
}
|
|
724
753
|
async extractImports(content, fileId, relativePath, rootDir) {
|
|
725
754
|
const edges = [];
|
|
726
755
|
const importRegex = /import\s+(?:type\s+)?(?:\{[^}]*\}|[\w*]+)\s+from\s+['"]([^'"]+)['"]/g;
|
|
@@ -766,6 +795,46 @@ var CodeIngestor = class {
|
|
|
766
795
|
}
|
|
767
796
|
return null;
|
|
768
797
|
}
|
|
798
|
+
computeCyclomaticComplexity(lines) {
|
|
799
|
+
let complexity = 1;
|
|
800
|
+
const decisionPattern = /\b(if|else\s+if|while|for|case)\b|\?\s*[^:?]|&&|\|\||catch\b/g;
|
|
801
|
+
for (const line of lines) {
|
|
802
|
+
const trimmed = line.trim();
|
|
803
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
804
|
+
const matches = trimmed.match(decisionPattern);
|
|
805
|
+
if (matches) complexity += matches.length;
|
|
806
|
+
}
|
|
807
|
+
return complexity;
|
|
808
|
+
}
|
|
809
|
+
computeMaxNesting(lines) {
|
|
810
|
+
let maxDepth = 0;
|
|
811
|
+
let currentDepth = 0;
|
|
812
|
+
for (const line of lines) {
|
|
813
|
+
const trimmed = line.trim();
|
|
814
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
815
|
+
for (const ch of trimmed) {
|
|
816
|
+
if (ch === "{") {
|
|
817
|
+
currentDepth++;
|
|
818
|
+
if (currentDepth > maxDepth) maxDepth = currentDepth;
|
|
819
|
+
} else if (ch === "}") {
|
|
820
|
+
currentDepth--;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return Math.max(0, maxDepth - 1);
|
|
825
|
+
}
|
|
826
|
+
countParameters(declarationLine) {
|
|
827
|
+
const parenMatch = declarationLine.match(/\(([^)]*)\)/);
|
|
828
|
+
if (!parenMatch || !parenMatch[1].trim()) return 0;
|
|
829
|
+
let depth = 0;
|
|
830
|
+
let count = 1;
|
|
831
|
+
for (const ch of parenMatch[1]) {
|
|
832
|
+
if (ch === "<" || ch === "(") depth++;
|
|
833
|
+
else if (ch === ">" || ch === ")") depth--;
|
|
834
|
+
else if (ch === "," && depth === 0) count++;
|
|
835
|
+
}
|
|
836
|
+
return count;
|
|
837
|
+
}
|
|
769
838
|
detectLanguage(filePath) {
|
|
770
839
|
if (/\.tsx?$/.test(filePath)) return "typescript";
|
|
771
840
|
if (/\.jsx?$/.test(filePath)) return "javascript";
|
|
@@ -1032,8 +1101,9 @@ var TopologicalLinker = class {
|
|
|
1032
1101
|
// src/ingest/KnowledgeIngestor.ts
|
|
1033
1102
|
import * as fs2 from "fs/promises";
|
|
1034
1103
|
import * as path3 from "path";
|
|
1104
|
+
|
|
1105
|
+
// src/ingest/ingestUtils.ts
|
|
1035
1106
|
import * as crypto from "crypto";
|
|
1036
|
-
var CODE_NODE_TYPES = ["file", "function", "class", "method", "interface", "variable"];
|
|
1037
1107
|
function hash(text) {
|
|
1038
1108
|
return crypto.createHash("md5").update(text).digest("hex").slice(0, 8);
|
|
1039
1109
|
}
|
|
@@ -1050,6 +1120,9 @@ function mergeResults(...results) {
|
|
|
1050
1120
|
function emptyResult(durationMs = 0) {
|
|
1051
1121
|
return { nodesAdded: 0, nodesUpdated: 0, edgesAdded: 0, edgesUpdated: 0, errors: [], durationMs };
|
|
1052
1122
|
}
|
|
1123
|
+
|
|
1124
|
+
// src/ingest/KnowledgeIngestor.ts
|
|
1125
|
+
var CODE_NODE_TYPES = ["file", "function", "class", "method", "interface", "variable"];
|
|
1053
1126
|
var KnowledgeIngestor = class {
|
|
1054
1127
|
constructor(store) {
|
|
1055
1128
|
this.store = store;
|
|
@@ -1241,6 +1314,22 @@ var KnowledgeIngestor = class {
|
|
|
1241
1314
|
|
|
1242
1315
|
// src/ingest/connectors/ConnectorUtils.ts
|
|
1243
1316
|
var CODE_NODE_TYPES2 = ["file", "function", "class", "method", "interface", "variable"];
|
|
1317
|
+
function sanitizeExternalText(text, maxLength = 2e3) {
|
|
1318
|
+
let sanitized = text.replace(
|
|
1319
|
+
/<\/?(?:system|instruction|prompt|role|context|tool_call|function_call|assistant|human|user)[^>]*>/gi,
|
|
1320
|
+
""
|
|
1321
|
+
).replace(/^#{1,3}\s*(?:system|instruction|prompt)\s*[::]\s*/gim, "").replace(
|
|
1322
|
+
/(?:ignore|disregard|forget)\s+(?:all\s+)?(?:previous|prior|above)\s+(?:instructions?|prompts?|context)/gi,
|
|
1323
|
+
"[filtered]"
|
|
1324
|
+
).replace(
|
|
1325
|
+
/you\s+are\s+now\s+(?:a\s+)?(?:helpful\s+)?(?:an?\s+)?(?:assistant|system|ai|bot|agent|tool)\b/gi,
|
|
1326
|
+
"[filtered]"
|
|
1327
|
+
);
|
|
1328
|
+
if (sanitized.length > maxLength) {
|
|
1329
|
+
sanitized = sanitized.slice(0, maxLength) + "\u2026";
|
|
1330
|
+
}
|
|
1331
|
+
return sanitized;
|
|
1332
|
+
}
|
|
1244
1333
|
function linkToCode(store, content, sourceNodeId, edgeType, options) {
|
|
1245
1334
|
let edgesCreated = 0;
|
|
1246
1335
|
for (const type of CODE_NODE_TYPES2) {
|
|
@@ -1408,7 +1497,7 @@ var JiraConnector = class {
|
|
|
1408
1497
|
store.addNode({
|
|
1409
1498
|
id: nodeId,
|
|
1410
1499
|
type: "issue",
|
|
1411
|
-
name: issue.fields.summary,
|
|
1500
|
+
name: sanitizeExternalText(issue.fields.summary, 500),
|
|
1412
1501
|
metadata: {
|
|
1413
1502
|
key: issue.key,
|
|
1414
1503
|
status: issue.fields.status?.name,
|
|
@@ -1418,7 +1507,9 @@ var JiraConnector = class {
|
|
|
1418
1507
|
}
|
|
1419
1508
|
});
|
|
1420
1509
|
nodesAdded++;
|
|
1421
|
-
const searchText =
|
|
1510
|
+
const searchText = sanitizeExternalText(
|
|
1511
|
+
[issue.fields.summary, issue.fields.description ?? ""].join(" ")
|
|
1512
|
+
);
|
|
1422
1513
|
edgesAdded += linkToCode(store, searchText, nodeId, "applies_to");
|
|
1423
1514
|
}
|
|
1424
1515
|
startAt += maxResults;
|
|
@@ -1494,7 +1585,8 @@ var SlackConnector = class {
|
|
|
1494
1585
|
}
|
|
1495
1586
|
for (const message of data.messages) {
|
|
1496
1587
|
const nodeId = `conversation:slack:${channel}:${message.ts}`;
|
|
1497
|
-
const
|
|
1588
|
+
const sanitizedText = sanitizeExternalText(message.text);
|
|
1589
|
+
const snippet = sanitizedText.length > 100 ? sanitizedText.slice(0, 100) : sanitizedText;
|
|
1498
1590
|
store.addNode({
|
|
1499
1591
|
id: nodeId,
|
|
1500
1592
|
type: "conversation",
|
|
@@ -1506,7 +1598,9 @@ var SlackConnector = class {
|
|
|
1506
1598
|
}
|
|
1507
1599
|
});
|
|
1508
1600
|
nodesAdded++;
|
|
1509
|
-
edgesAdded += linkToCode(store,
|
|
1601
|
+
edgesAdded += linkToCode(store, sanitizedText, nodeId, "references", {
|
|
1602
|
+
checkPaths: true
|
|
1603
|
+
});
|
|
1510
1604
|
}
|
|
1511
1605
|
} catch (err) {
|
|
1512
1606
|
errors.push(
|
|
@@ -1569,7 +1663,7 @@ var ConfluenceConnector = class {
|
|
|
1569
1663
|
store.addNode({
|
|
1570
1664
|
id: nodeId,
|
|
1571
1665
|
type: "document",
|
|
1572
|
-
name: page.title,
|
|
1666
|
+
name: sanitizeExternalText(page.title, 500),
|
|
1573
1667
|
metadata: {
|
|
1574
1668
|
source: "confluence",
|
|
1575
1669
|
spaceKey,
|
|
@@ -1579,7 +1673,7 @@ var ConfluenceConnector = class {
|
|
|
1579
1673
|
}
|
|
1580
1674
|
});
|
|
1581
1675
|
nodesAdded++;
|
|
1582
|
-
const text = `${page.title} ${page.body?.storage?.value ?? ""}
|
|
1676
|
+
const text = sanitizeExternalText(`${page.title} ${page.body?.storage?.value ?? ""}`);
|
|
1583
1677
|
edgesAdded += linkToCode(store, text, nodeId, "documents");
|
|
1584
1678
|
}
|
|
1585
1679
|
nextUrl = data._links?.next ? `${baseUrl}${data._links.next}` : null;
|
|
@@ -1644,10 +1738,11 @@ var CIConnector = class {
|
|
|
1644
1738
|
const data = await response.json();
|
|
1645
1739
|
for (const run of data.workflow_runs) {
|
|
1646
1740
|
const buildId = `build:${run.id}`;
|
|
1741
|
+
const safeName = sanitizeExternalText(run.name, 200);
|
|
1647
1742
|
store.addNode({
|
|
1648
1743
|
id: buildId,
|
|
1649
1744
|
type: "build",
|
|
1650
|
-
name: `${
|
|
1745
|
+
name: `${safeName} #${run.id}`,
|
|
1651
1746
|
metadata: {
|
|
1652
1747
|
source: "github-actions",
|
|
1653
1748
|
status: run.status,
|
|
@@ -1669,7 +1764,7 @@ var CIConnector = class {
|
|
|
1669
1764
|
store.addNode({
|
|
1670
1765
|
id: testResultId,
|
|
1671
1766
|
type: "test_result",
|
|
1672
|
-
name: `Failed: ${
|
|
1767
|
+
name: `Failed: ${safeName} #${run.id}`,
|
|
1673
1768
|
metadata: {
|
|
1674
1769
|
source: "github-actions",
|
|
1675
1770
|
buildId: String(run.id),
|
|
@@ -1972,6 +2067,144 @@ var GraphEntropyAdapter = class {
|
|
|
1972
2067
|
}
|
|
1973
2068
|
};
|
|
1974
2069
|
|
|
2070
|
+
// src/entropy/GraphComplexityAdapter.ts
|
|
2071
|
+
var GraphComplexityAdapter = class {
|
|
2072
|
+
constructor(store) {
|
|
2073
|
+
this.store = store;
|
|
2074
|
+
}
|
|
2075
|
+
/**
|
|
2076
|
+
* Compute complexity hotspots by combining cyclomatic complexity with change frequency.
|
|
2077
|
+
*
|
|
2078
|
+
* 1. Find all function and method nodes
|
|
2079
|
+
* 2. For each, find the containing file and count commit nodes referencing that file
|
|
2080
|
+
* 3. Compute hotspotScore = changeFrequency * cyclomaticComplexity
|
|
2081
|
+
* 4. Sort descending by score
|
|
2082
|
+
* 5. Compute 95th percentile
|
|
2083
|
+
*/
|
|
2084
|
+
computeComplexityHotspots() {
|
|
2085
|
+
const functionNodes = [
|
|
2086
|
+
...this.store.findNodes({ type: "function" }),
|
|
2087
|
+
...this.store.findNodes({ type: "method" })
|
|
2088
|
+
];
|
|
2089
|
+
if (functionNodes.length === 0) {
|
|
2090
|
+
return { hotspots: [], percentile95Score: 0 };
|
|
2091
|
+
}
|
|
2092
|
+
const fileChangeFrequency = /* @__PURE__ */ new Map();
|
|
2093
|
+
const hotspots = [];
|
|
2094
|
+
for (const fnNode of functionNodes) {
|
|
2095
|
+
const complexity = fnNode.metadata?.cyclomaticComplexity ?? 1;
|
|
2096
|
+
const containsEdges = this.store.getEdges({ to: fnNode.id, type: "contains" });
|
|
2097
|
+
let fileId;
|
|
2098
|
+
for (const edge of containsEdges) {
|
|
2099
|
+
const sourceNode = this.store.getNode(edge.from);
|
|
2100
|
+
if (sourceNode?.type === "file") {
|
|
2101
|
+
fileId = sourceNode.id;
|
|
2102
|
+
break;
|
|
2103
|
+
}
|
|
2104
|
+
if (sourceNode?.type === "class") {
|
|
2105
|
+
const classContainsEdges = this.store.getEdges({ to: sourceNode.id, type: "contains" });
|
|
2106
|
+
for (const classEdge of classContainsEdges) {
|
|
2107
|
+
const parentNode = this.store.getNode(classEdge.from);
|
|
2108
|
+
if (parentNode?.type === "file") {
|
|
2109
|
+
fileId = parentNode.id;
|
|
2110
|
+
break;
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
if (fileId) break;
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
if (!fileId) continue;
|
|
2117
|
+
let changeFrequency = fileChangeFrequency.get(fileId);
|
|
2118
|
+
if (changeFrequency === void 0) {
|
|
2119
|
+
const referencesEdges = this.store.getEdges({ to: fileId, type: "references" });
|
|
2120
|
+
changeFrequency = referencesEdges.length;
|
|
2121
|
+
fileChangeFrequency.set(fileId, changeFrequency);
|
|
2122
|
+
}
|
|
2123
|
+
const hotspotScore = changeFrequency * complexity;
|
|
2124
|
+
const filePath = fnNode.path ?? fileId.replace(/^file:/, "");
|
|
2125
|
+
hotspots.push({
|
|
2126
|
+
file: filePath,
|
|
2127
|
+
function: fnNode.name,
|
|
2128
|
+
changeFrequency,
|
|
2129
|
+
complexity,
|
|
2130
|
+
hotspotScore
|
|
2131
|
+
});
|
|
2132
|
+
}
|
|
2133
|
+
hotspots.sort((a, b) => b.hotspotScore - a.hotspotScore);
|
|
2134
|
+
const percentile95Score = this.computePercentile(
|
|
2135
|
+
hotspots.map((h) => h.hotspotScore),
|
|
2136
|
+
95
|
|
2137
|
+
);
|
|
2138
|
+
return { hotspots, percentile95Score };
|
|
2139
|
+
}
|
|
2140
|
+
computePercentile(descendingScores, percentile) {
|
|
2141
|
+
if (descendingScores.length === 0) return 0;
|
|
2142
|
+
const ascending = [...descendingScores].sort((a, b) => a - b);
|
|
2143
|
+
const index = Math.ceil(percentile / 100 * ascending.length) - 1;
|
|
2144
|
+
return ascending[Math.min(index, ascending.length - 1)];
|
|
2145
|
+
}
|
|
2146
|
+
};
|
|
2147
|
+
|
|
2148
|
+
// src/entropy/GraphCouplingAdapter.ts
|
|
2149
|
+
var GraphCouplingAdapter = class {
|
|
2150
|
+
constructor(store) {
|
|
2151
|
+
this.store = store;
|
|
2152
|
+
}
|
|
2153
|
+
/**
|
|
2154
|
+
* Compute coupling data for all file nodes in the graph.
|
|
2155
|
+
*
|
|
2156
|
+
* For each file:
|
|
2157
|
+
* - fanOut: number of outbound 'imports' edges
|
|
2158
|
+
* - fanIn: number of inbound 'imports' edges from other files
|
|
2159
|
+
* - couplingRatio: fanOut / (fanIn + fanOut), rounded to 2 decimals (0 if both are 0)
|
|
2160
|
+
* - transitiveDepth: longest chain of outbound 'imports' edges via BFS
|
|
2161
|
+
*/
|
|
2162
|
+
computeCouplingData() {
|
|
2163
|
+
const fileNodes = this.store.findNodes({ type: "file" });
|
|
2164
|
+
if (fileNodes.length === 0) {
|
|
2165
|
+
return { files: [] };
|
|
2166
|
+
}
|
|
2167
|
+
const files = [];
|
|
2168
|
+
for (const node of fileNodes) {
|
|
2169
|
+
const fileId = node.id;
|
|
2170
|
+
const filePath = node.path ?? node.name;
|
|
2171
|
+
const outEdges = this.store.getEdges({ from: fileId, type: "imports" });
|
|
2172
|
+
const fanOut = outEdges.length;
|
|
2173
|
+
const inEdges = this.store.getEdges({ to: fileId, type: "imports" });
|
|
2174
|
+
const fanIn = inEdges.length;
|
|
2175
|
+
const total = fanIn + fanOut;
|
|
2176
|
+
const couplingRatio = total === 0 ? 0 : Math.round(fanOut / total * 100) / 100;
|
|
2177
|
+
const transitiveDepth = this.computeTransitiveDepth(fileId);
|
|
2178
|
+
files.push({ file: filePath, fanIn, fanOut, couplingRatio, transitiveDepth });
|
|
2179
|
+
}
|
|
2180
|
+
return { files };
|
|
2181
|
+
}
|
|
2182
|
+
/**
|
|
2183
|
+
* BFS from a node following outbound 'imports' edges to find the maximum depth.
|
|
2184
|
+
*/
|
|
2185
|
+
computeTransitiveDepth(startId) {
|
|
2186
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2187
|
+
const queue = [[startId, 0]];
|
|
2188
|
+
visited.add(startId);
|
|
2189
|
+
let maxDepth = 0;
|
|
2190
|
+
let head = 0;
|
|
2191
|
+
while (head < queue.length) {
|
|
2192
|
+
const [nodeId, depth] = queue[head++];
|
|
2193
|
+
if (depth > maxDepth) {
|
|
2194
|
+
maxDepth = depth;
|
|
2195
|
+
}
|
|
2196
|
+
const outEdges = this.store.getEdges({ from: nodeId, type: "imports" });
|
|
2197
|
+
for (const edge of outEdges) {
|
|
2198
|
+
if (!visited.has(edge.to)) {
|
|
2199
|
+
visited.add(edge.to);
|
|
2200
|
+
queue.push([edge.to, depth + 1]);
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
return maxDepth;
|
|
2205
|
+
}
|
|
2206
|
+
};
|
|
2207
|
+
|
|
1975
2208
|
// src/context/Assembler.ts
|
|
1976
2209
|
var PHASE_NODE_TYPES = {
|
|
1977
2210
|
implement: ["file", "function", "class", "method", "interface", "variable"],
|
|
@@ -2279,6 +2512,250 @@ var GraphConstraintAdapter = class {
|
|
|
2279
2512
|
}
|
|
2280
2513
|
};
|
|
2281
2514
|
|
|
2515
|
+
// src/ingest/DesignIngestor.ts
|
|
2516
|
+
import * as fs4 from "fs/promises";
|
|
2517
|
+
import * as path5 from "path";
|
|
2518
|
+
function isDTCGToken(obj) {
|
|
2519
|
+
return typeof obj === "object" && obj !== null && "$value" in obj && "$type" in obj;
|
|
2520
|
+
}
|
|
2521
|
+
async function readFileOrNull(filePath) {
|
|
2522
|
+
try {
|
|
2523
|
+
return await fs4.readFile(filePath, "utf-8");
|
|
2524
|
+
} catch {
|
|
2525
|
+
return null;
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
function parseJsonOrError(content, filePath) {
|
|
2529
|
+
try {
|
|
2530
|
+
return { data: JSON.parse(content) };
|
|
2531
|
+
} catch (err) {
|
|
2532
|
+
return {
|
|
2533
|
+
error: `Failed to parse ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
2534
|
+
};
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
function walkDTCGTokens(store, obj, groupPath, topGroup, tokensPath) {
|
|
2538
|
+
let count = 0;
|
|
2539
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
2540
|
+
if (key.startsWith("$")) continue;
|
|
2541
|
+
if (isDTCGToken(value)) {
|
|
2542
|
+
const tokenPath = [...groupPath, key].join(".");
|
|
2543
|
+
store.addNode({
|
|
2544
|
+
id: `design_token:${tokenPath}`,
|
|
2545
|
+
type: "design_token",
|
|
2546
|
+
name: tokenPath,
|
|
2547
|
+
path: tokensPath,
|
|
2548
|
+
metadata: {
|
|
2549
|
+
tokenType: value.$type,
|
|
2550
|
+
value: value.$value,
|
|
2551
|
+
group: topGroup || groupPath[0] || key,
|
|
2552
|
+
...value.$description ? { description: value.$description } : {}
|
|
2553
|
+
}
|
|
2554
|
+
});
|
|
2555
|
+
count++;
|
|
2556
|
+
} else if (typeof value === "object" && value !== null) {
|
|
2557
|
+
count += walkDTCGTokens(
|
|
2558
|
+
store,
|
|
2559
|
+
value,
|
|
2560
|
+
[...groupPath, key],
|
|
2561
|
+
topGroup || key,
|
|
2562
|
+
tokensPath
|
|
2563
|
+
);
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
return count;
|
|
2567
|
+
}
|
|
2568
|
+
function parseAestheticDirection(content) {
|
|
2569
|
+
const extract = (pattern) => {
|
|
2570
|
+
const m = content.match(pattern);
|
|
2571
|
+
return m ? m[1].trim() : void 0;
|
|
2572
|
+
};
|
|
2573
|
+
const result = {};
|
|
2574
|
+
const style = extract(/\*\*Style:\*\*\s*(.+)/);
|
|
2575
|
+
if (style !== void 0) result.style = style;
|
|
2576
|
+
const tone = extract(/\*\*Tone:\*\*\s*(.+)/);
|
|
2577
|
+
if (tone !== void 0) result.tone = tone;
|
|
2578
|
+
const differentiator = extract(/\*\*Differentiator:\*\*\s*(.+)/);
|
|
2579
|
+
if (differentiator !== void 0) result.differentiator = differentiator;
|
|
2580
|
+
const strictness = extract(/^level:\s*(strict|standard|permissive)\s*$/m);
|
|
2581
|
+
if (strictness !== void 0) result.strictness = strictness;
|
|
2582
|
+
return result;
|
|
2583
|
+
}
|
|
2584
|
+
function parseAntiPatterns(content) {
|
|
2585
|
+
const lines = content.split("\n");
|
|
2586
|
+
const patterns = [];
|
|
2587
|
+
let inSection = false;
|
|
2588
|
+
for (const line of lines) {
|
|
2589
|
+
if (/^##\s+Anti-Patterns/i.test(line)) {
|
|
2590
|
+
inSection = true;
|
|
2591
|
+
continue;
|
|
2592
|
+
}
|
|
2593
|
+
if (inSection && /^##\s+/.test(line)) {
|
|
2594
|
+
break;
|
|
2595
|
+
}
|
|
2596
|
+
if (inSection) {
|
|
2597
|
+
const bulletMatch = line.match(/^-\s+(.+)/);
|
|
2598
|
+
if (bulletMatch) {
|
|
2599
|
+
patterns.push(bulletMatch[1].trim());
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
return patterns;
|
|
2604
|
+
}
|
|
2605
|
+
var DesignIngestor = class {
|
|
2606
|
+
constructor(store) {
|
|
2607
|
+
this.store = store;
|
|
2608
|
+
}
|
|
2609
|
+
async ingestTokens(tokensPath) {
|
|
2610
|
+
const start = Date.now();
|
|
2611
|
+
const content = await readFileOrNull(tokensPath);
|
|
2612
|
+
if (content === null) return emptyResult(Date.now() - start);
|
|
2613
|
+
const parsed = parseJsonOrError(content, tokensPath);
|
|
2614
|
+
if ("error" in parsed) {
|
|
2615
|
+
return { ...emptyResult(Date.now() - start), errors: [parsed.error] };
|
|
2616
|
+
}
|
|
2617
|
+
const nodesAdded = walkDTCGTokens(this.store, parsed.data, [], "", tokensPath);
|
|
2618
|
+
return {
|
|
2619
|
+
nodesAdded,
|
|
2620
|
+
nodesUpdated: 0,
|
|
2621
|
+
edgesAdded: 0,
|
|
2622
|
+
edgesUpdated: 0,
|
|
2623
|
+
errors: [],
|
|
2624
|
+
durationMs: Date.now() - start
|
|
2625
|
+
};
|
|
2626
|
+
}
|
|
2627
|
+
async ingestDesignIntent(designPath) {
|
|
2628
|
+
const start = Date.now();
|
|
2629
|
+
const content = await readFileOrNull(designPath);
|
|
2630
|
+
if (content === null) return emptyResult(Date.now() - start);
|
|
2631
|
+
let nodesAdded = 0;
|
|
2632
|
+
const direction = parseAestheticDirection(content);
|
|
2633
|
+
const metadata = {};
|
|
2634
|
+
if (direction.style) metadata.style = direction.style;
|
|
2635
|
+
if (direction.tone) metadata.tone = direction.tone;
|
|
2636
|
+
if (direction.differentiator) metadata.differentiator = direction.differentiator;
|
|
2637
|
+
if (direction.strictness) metadata.strictness = direction.strictness;
|
|
2638
|
+
this.store.addNode({
|
|
2639
|
+
id: "aesthetic_intent:project",
|
|
2640
|
+
type: "aesthetic_intent",
|
|
2641
|
+
name: "project",
|
|
2642
|
+
path: designPath,
|
|
2643
|
+
metadata
|
|
2644
|
+
});
|
|
2645
|
+
nodesAdded++;
|
|
2646
|
+
for (const text of parseAntiPatterns(content)) {
|
|
2647
|
+
this.store.addNode({
|
|
2648
|
+
id: `design_constraint:${hash(text)}`,
|
|
2649
|
+
type: "design_constraint",
|
|
2650
|
+
name: text,
|
|
2651
|
+
path: designPath,
|
|
2652
|
+
metadata: { rule: text, severity: "warn", scope: "project" }
|
|
2653
|
+
});
|
|
2654
|
+
nodesAdded++;
|
|
2655
|
+
}
|
|
2656
|
+
return {
|
|
2657
|
+
nodesAdded,
|
|
2658
|
+
nodesUpdated: 0,
|
|
2659
|
+
edgesAdded: 0,
|
|
2660
|
+
edgesUpdated: 0,
|
|
2661
|
+
errors: [],
|
|
2662
|
+
durationMs: Date.now() - start
|
|
2663
|
+
};
|
|
2664
|
+
}
|
|
2665
|
+
async ingestAll(designDir) {
|
|
2666
|
+
const start = Date.now();
|
|
2667
|
+
const [tokensResult, intentResult] = await Promise.all([
|
|
2668
|
+
this.ingestTokens(path5.join(designDir, "tokens.json")),
|
|
2669
|
+
this.ingestDesignIntent(path5.join(designDir, "DESIGN.md"))
|
|
2670
|
+
]);
|
|
2671
|
+
const merged = mergeResults(tokensResult, intentResult);
|
|
2672
|
+
return { ...merged, durationMs: Date.now() - start };
|
|
2673
|
+
}
|
|
2674
|
+
};
|
|
2675
|
+
|
|
2676
|
+
// src/constraints/DesignConstraintAdapter.ts
|
|
2677
|
+
var DesignConstraintAdapter = class {
|
|
2678
|
+
constructor(store) {
|
|
2679
|
+
this.store = store;
|
|
2680
|
+
}
|
|
2681
|
+
checkForHardcodedColors(source, file, strictness) {
|
|
2682
|
+
const severity = this.mapSeverity(strictness);
|
|
2683
|
+
const tokenNodes = this.store.findNodes({ type: "design_token" });
|
|
2684
|
+
const colorValues = /* @__PURE__ */ new Set();
|
|
2685
|
+
for (const node of tokenNodes) {
|
|
2686
|
+
if (node.metadata.tokenType === "color" && typeof node.metadata.value === "string") {
|
|
2687
|
+
colorValues.add(node.metadata.value.toLowerCase());
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
const hexPattern = /#[0-9a-fA-F]{3,8}\b/g;
|
|
2691
|
+
const violations = [];
|
|
2692
|
+
let match;
|
|
2693
|
+
while ((match = hexPattern.exec(source)) !== null) {
|
|
2694
|
+
const hexValue = match[0];
|
|
2695
|
+
if (!colorValues.has(hexValue.toLowerCase())) {
|
|
2696
|
+
violations.push({
|
|
2697
|
+
code: "DESIGN-001",
|
|
2698
|
+
file,
|
|
2699
|
+
message: `Hardcoded color ${hexValue} is not in the design token set`,
|
|
2700
|
+
severity,
|
|
2701
|
+
value: hexValue
|
|
2702
|
+
});
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
return violations;
|
|
2706
|
+
}
|
|
2707
|
+
checkForHardcodedFonts(source, file, strictness) {
|
|
2708
|
+
const severity = this.mapSeverity(strictness);
|
|
2709
|
+
const tokenNodes = this.store.findNodes({ type: "design_token" });
|
|
2710
|
+
const fontFamilies = /* @__PURE__ */ new Set();
|
|
2711
|
+
for (const node of tokenNodes) {
|
|
2712
|
+
if (node.metadata.tokenType === "typography") {
|
|
2713
|
+
const value = node.metadata.value;
|
|
2714
|
+
if (typeof value === "object" && value !== null && "fontFamily" in value) {
|
|
2715
|
+
fontFamilies.add(value.fontFamily.toLowerCase());
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
const fontPatterns = [/fontFamily:\s*['"]([^'"]+)['"]/g, /font-family:\s*['"]([^'"]+)['"]/g];
|
|
2720
|
+
const violations = [];
|
|
2721
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2722
|
+
for (const pattern of fontPatterns) {
|
|
2723
|
+
let match;
|
|
2724
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
2725
|
+
const fontName = match[1];
|
|
2726
|
+
if (seen.has(fontName.toLowerCase())) continue;
|
|
2727
|
+
seen.add(fontName.toLowerCase());
|
|
2728
|
+
if (!fontFamilies.has(fontName.toLowerCase())) {
|
|
2729
|
+
violations.push({
|
|
2730
|
+
code: "DESIGN-002",
|
|
2731
|
+
file,
|
|
2732
|
+
message: `Hardcoded font family "${fontName}" is not in the design token set`,
|
|
2733
|
+
severity,
|
|
2734
|
+
value: fontName
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
return violations;
|
|
2740
|
+
}
|
|
2741
|
+
checkAll(source, file, strictness) {
|
|
2742
|
+
return [
|
|
2743
|
+
...this.checkForHardcodedColors(source, file, strictness),
|
|
2744
|
+
...this.checkForHardcodedFonts(source, file, strictness)
|
|
2745
|
+
];
|
|
2746
|
+
}
|
|
2747
|
+
mapSeverity(strictness = "standard") {
|
|
2748
|
+
switch (strictness) {
|
|
2749
|
+
case "permissive":
|
|
2750
|
+
return "info";
|
|
2751
|
+
case "standard":
|
|
2752
|
+
return "warn";
|
|
2753
|
+
case "strict":
|
|
2754
|
+
return "error";
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
};
|
|
2758
|
+
|
|
2282
2759
|
// src/feedback/GraphFeedbackAdapter.ts
|
|
2283
2760
|
var GraphFeedbackAdapter = class {
|
|
2284
2761
|
constructor(store) {
|
|
@@ -2351,7 +2828,7 @@ var GraphFeedbackAdapter = class {
|
|
|
2351
2828
|
};
|
|
2352
2829
|
|
|
2353
2830
|
// src/index.ts
|
|
2354
|
-
var VERSION = "0.
|
|
2831
|
+
var VERSION = "0.2.0";
|
|
2355
2832
|
export {
|
|
2356
2833
|
Assembler,
|
|
2357
2834
|
CIConnector,
|
|
@@ -2359,10 +2836,14 @@ export {
|
|
|
2359
2836
|
CodeIngestor,
|
|
2360
2837
|
ConfluenceConnector,
|
|
2361
2838
|
ContextQL,
|
|
2839
|
+
DesignConstraintAdapter,
|
|
2840
|
+
DesignIngestor,
|
|
2362
2841
|
EDGE_TYPES,
|
|
2363
2842
|
FusionLayer,
|
|
2364
2843
|
GitIngestor,
|
|
2844
|
+
GraphComplexityAdapter,
|
|
2365
2845
|
GraphConstraintAdapter,
|
|
2846
|
+
GraphCouplingAdapter,
|
|
2366
2847
|
GraphEdgeSchema,
|
|
2367
2848
|
GraphEntropyAdapter,
|
|
2368
2849
|
GraphFeedbackAdapter,
|