@harness-engineering/graph 0.4.3 → 0.5.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/README.md +50 -36
- package/dist/index.d.mts +495 -14
- package/dist/index.d.ts +495 -14
- package/dist/index.js +3778 -190
- package/dist/index.mjs +3760 -193
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -23,6 +23,7 @@ var NODE_TYPES = [
|
|
|
23
23
|
"commit",
|
|
24
24
|
"build",
|
|
25
25
|
"test_result",
|
|
26
|
+
"execution_outcome",
|
|
26
27
|
// Observability (future)
|
|
27
28
|
"span",
|
|
28
29
|
"metric",
|
|
@@ -36,8 +37,16 @@ var NODE_TYPES = [
|
|
|
36
37
|
"design_token",
|
|
37
38
|
"aesthetic_intent",
|
|
38
39
|
"design_constraint",
|
|
40
|
+
"image_annotation",
|
|
39
41
|
// Traceability
|
|
40
42
|
"requirement",
|
|
43
|
+
// Business Knowledge
|
|
44
|
+
"business_rule",
|
|
45
|
+
"business_process",
|
|
46
|
+
"business_concept",
|
|
47
|
+
"business_term",
|
|
48
|
+
"business_metric",
|
|
49
|
+
"business_fact",
|
|
41
50
|
// Cache
|
|
42
51
|
"packed_summary"
|
|
43
52
|
];
|
|
@@ -61,6 +70,7 @@ var EDGE_TYPES = [
|
|
|
61
70
|
"co_changes_with",
|
|
62
71
|
"triggered_by",
|
|
63
72
|
"failed_in",
|
|
73
|
+
"outcome_of",
|
|
64
74
|
// Execution relationships (future)
|
|
65
75
|
"executed_by",
|
|
66
76
|
"measured_by",
|
|
@@ -73,6 +83,10 @@ var EDGE_TYPES = [
|
|
|
73
83
|
"requires",
|
|
74
84
|
"verified_by",
|
|
75
85
|
"tested_by",
|
|
86
|
+
// Business Knowledge relationships
|
|
87
|
+
"governs",
|
|
88
|
+
"measures",
|
|
89
|
+
"annotates",
|
|
76
90
|
// Cache relationships
|
|
77
91
|
"caches"
|
|
78
92
|
];
|
|
@@ -115,10 +129,28 @@ var GraphEdgeSchema = z.object({
|
|
|
115
129
|
|
|
116
130
|
// src/store/Serializer.ts
|
|
117
131
|
import { readFile, writeFile, mkdir, access } from "fs/promises";
|
|
132
|
+
import { createWriteStream } from "fs";
|
|
118
133
|
import { join } from "path";
|
|
134
|
+
function streamGraphJson(filePath, nodes, edges) {
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
const stream = createWriteStream(filePath, { encoding: "utf-8" });
|
|
137
|
+
stream.on("error", reject);
|
|
138
|
+
stream.write('{"nodes":[');
|
|
139
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
140
|
+
if (i > 0) stream.write(",");
|
|
141
|
+
stream.write(JSON.stringify(nodes[i]));
|
|
142
|
+
}
|
|
143
|
+
stream.write('],"edges":[');
|
|
144
|
+
for (let i = 0; i < edges.length; i++) {
|
|
145
|
+
if (i > 0) stream.write(",");
|
|
146
|
+
stream.write(JSON.stringify(edges[i]));
|
|
147
|
+
}
|
|
148
|
+
stream.write("]}");
|
|
149
|
+
stream.end(resolve);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
119
152
|
async function saveGraph(dirPath, nodes, edges) {
|
|
120
153
|
await mkdir(dirPath, { recursive: true });
|
|
121
|
-
const graphData = { nodes, edges };
|
|
122
154
|
const metadata = {
|
|
123
155
|
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
124
156
|
lastScanTimestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -126,7 +158,7 @@ async function saveGraph(dirPath, nodes, edges) {
|
|
|
126
158
|
edgeCount: edges.length
|
|
127
159
|
};
|
|
128
160
|
await Promise.all([
|
|
129
|
-
|
|
161
|
+
streamGraphJson(join(dirPath, "graph.json"), nodes, edges),
|
|
130
162
|
writeFile(join(dirPath, "metadata.json"), JSON.stringify(metadata, null, 2), "utf-8")
|
|
131
163
|
]);
|
|
132
164
|
}
|
|
@@ -315,8 +347,8 @@ var GraphStore = class {
|
|
|
315
347
|
}
|
|
316
348
|
// --- Persistence ---
|
|
317
349
|
async save(dirPath) {
|
|
318
|
-
const allNodes = Array.from(this.nodeMap.values())
|
|
319
|
-
const allEdges = Array.from(this.edgeMap.values())
|
|
350
|
+
const allNodes = Array.from(this.nodeMap.values());
|
|
351
|
+
const allEdges = Array.from(this.edgeMap.values());
|
|
320
352
|
await saveGraph(dirPath, allNodes, allEdges);
|
|
321
353
|
}
|
|
322
354
|
async load(dirPath) {
|
|
@@ -685,6 +717,49 @@ function groupNodesByImpact(nodes, excludeId) {
|
|
|
685
717
|
import * as fs from "fs/promises";
|
|
686
718
|
import * as path from "path";
|
|
687
719
|
var SKIP_METHOD_NAMES = /* @__PURE__ */ new Set(["constructor", "if", "for", "while", "switch"]);
|
|
720
|
+
var FUNCTION_PATTERNS = {
|
|
721
|
+
python: /^def\s+(\w+)\s*\(/,
|
|
722
|
+
go: /^func\s+(\w+)\s*[[(]/,
|
|
723
|
+
rust: /^(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/,
|
|
724
|
+
typescript: /(?:export\s+)?(?:async\s+)?function\s+(\w+)/,
|
|
725
|
+
javascript: /(?:export\s+)?(?:async\s+)?function\s+(\w+)/
|
|
726
|
+
};
|
|
727
|
+
var CLASS_PATTERNS = {
|
|
728
|
+
python: /^class\s+(\w+)/,
|
|
729
|
+
go: /^type\s+(\w+)\s+struct\b/,
|
|
730
|
+
rust: /^(?:pub\s+)?struct\s+(\w+)/,
|
|
731
|
+
java: /(?:public|private|protected)?\s*(?:abstract\s+|final\s+)?class\s+(\w+)/,
|
|
732
|
+
typescript: /(?:export\s+)?class\s+(\w+)/,
|
|
733
|
+
javascript: /(?:export\s+)?class\s+(\w+)/
|
|
734
|
+
};
|
|
735
|
+
var INTERFACE_PATTERNS = {
|
|
736
|
+
go: /^type\s+(\w+)\s+interface\b/,
|
|
737
|
+
rust: /^(?:pub\s+)?trait\s+(\w+)/,
|
|
738
|
+
java: /(?:public\s+)?interface\s+(\w+)/,
|
|
739
|
+
typescript: /(?:export\s+)?interface\s+(\w+)/,
|
|
740
|
+
javascript: /(?:export\s+)?interface\s+(\w+)/
|
|
741
|
+
};
|
|
742
|
+
var METHOD_PATTERNS = {
|
|
743
|
+
python: /^\s+def\s+(\w+)\s*\(/,
|
|
744
|
+
rust: /^\s+(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/,
|
|
745
|
+
java: /^\s+(?:(?:public|private|protected|static|final|abstract|synchronized)\s+)*(?:\w+(?:<[^>]*>)?)\s+(\w+)\s*\(/,
|
|
746
|
+
typescript: /^\s+(?:(?:public|private|protected|readonly|static|abstract)\s+)*(?:async\s+)?(\w+)\s*\(/,
|
|
747
|
+
javascript: /^\s+(?:(?:public|private|protected|readonly|static|abstract)\s+)*(?:async\s+)?(\w+)\s*\(/
|
|
748
|
+
};
|
|
749
|
+
var GO_METHOD_PATTERN = /^func\s+\(\w+\s+\*?(\w+)\)\s+(\w+)\s*\(/;
|
|
750
|
+
var VARIABLE_PATTERNS = {
|
|
751
|
+
python: /^([A-Z]\w*)\s*=/,
|
|
752
|
+
go: /^(?:var|const)\s+(\w+)/,
|
|
753
|
+
rust: /^(?:pub\s+)?(?:const|static)\s+(\w+)/,
|
|
754
|
+
typescript: /(?:export\s+)?(?:const|let|var)\s+(\w+)/,
|
|
755
|
+
javascript: /(?:export\s+)?(?:const|let|var)\s+(\w+)/
|
|
756
|
+
};
|
|
757
|
+
function isExported(lang, line, name) {
|
|
758
|
+
if (lang === "go") return /^[A-Z]/.test(name);
|
|
759
|
+
if (lang === "rust") return /^\s*pub\b/.test(line);
|
|
760
|
+
if (lang === "java") return /\bpublic\b/.test(line);
|
|
761
|
+
return line.includes("export");
|
|
762
|
+
}
|
|
688
763
|
function countBraces(line) {
|
|
689
764
|
let net = 0;
|
|
690
765
|
for (const ch of line) {
|
|
@@ -693,6 +768,23 @@ function countBraces(line) {
|
|
|
693
768
|
}
|
|
694
769
|
return net;
|
|
695
770
|
}
|
|
771
|
+
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
772
|
+
".ts",
|
|
773
|
+
".tsx",
|
|
774
|
+
".js",
|
|
775
|
+
".jsx",
|
|
776
|
+
".py",
|
|
777
|
+
".go",
|
|
778
|
+
".rs",
|
|
779
|
+
".java",
|
|
780
|
+
".groovy"
|
|
781
|
+
]);
|
|
782
|
+
var SKIP_EXTENSIONS = /* @__PURE__ */ new Set([".d.ts"]);
|
|
783
|
+
function isSupportedSourceFile(name) {
|
|
784
|
+
if (SKIP_EXTENSIONS.has(name.slice(name.lastIndexOf(".")))) return false;
|
|
785
|
+
const ext = name.slice(name.lastIndexOf("."));
|
|
786
|
+
return SUPPORTED_EXTENSIONS.has(ext);
|
|
787
|
+
}
|
|
696
788
|
var CodeIngestor = class {
|
|
697
789
|
constructor(store) {
|
|
698
790
|
this.store = store;
|
|
@@ -705,22 +797,34 @@ var CodeIngestor = class {
|
|
|
705
797
|
let edgesAdded = 0;
|
|
706
798
|
const files = await this.findSourceFiles(rootDir);
|
|
707
799
|
const nameToFiles = /* @__PURE__ */ new Map();
|
|
708
|
-
const fileContents = /* @__PURE__ */ new Map();
|
|
709
800
|
for (const filePath of files) {
|
|
710
801
|
try {
|
|
711
|
-
const result = await this.processFile(filePath, rootDir, nameToFiles
|
|
802
|
+
const result = await this.processFile(filePath, rootDir, nameToFiles);
|
|
712
803
|
nodesAdded += result.nodesAdded;
|
|
713
804
|
edgesAdded += result.edgesAdded;
|
|
714
805
|
} catch (err) {
|
|
715
806
|
errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
716
807
|
}
|
|
717
808
|
}
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
809
|
+
for (const filePath of files) {
|
|
810
|
+
try {
|
|
811
|
+
const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
812
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
813
|
+
const callsEdges = this.extractCallsEdgesForFile(relativePath, content, nameToFiles);
|
|
814
|
+
for (const edge of callsEdges) {
|
|
815
|
+
this.store.addEdge(edge);
|
|
816
|
+
edgesAdded++;
|
|
817
|
+
}
|
|
818
|
+
} catch {
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
for (const filePath of files) {
|
|
822
|
+
try {
|
|
823
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
824
|
+
edgesAdded += this.extractReqAnnotationsForFile(filePath, content, rootDir);
|
|
825
|
+
} catch {
|
|
826
|
+
}
|
|
722
827
|
}
|
|
723
|
-
edgesAdded += this.extractReqAnnotations(fileContents, rootDir);
|
|
724
828
|
return {
|
|
725
829
|
nodesAdded,
|
|
726
830
|
nodesUpdated: 0,
|
|
@@ -730,14 +834,13 @@ var CodeIngestor = class {
|
|
|
730
834
|
durationMs: Date.now() - start
|
|
731
835
|
};
|
|
732
836
|
}
|
|
733
|
-
async processFile(filePath, rootDir, nameToFiles
|
|
837
|
+
async processFile(filePath, rootDir, nameToFiles) {
|
|
734
838
|
let nodesAdded = 0;
|
|
735
839
|
let edgesAdded = 0;
|
|
736
840
|
const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
737
841
|
const content = await fs.readFile(filePath, "utf-8");
|
|
738
842
|
const stat2 = await fs.stat(filePath);
|
|
739
843
|
const fileId = `file:${relativePath}`;
|
|
740
|
-
fileContents.set(relativePath, content);
|
|
741
844
|
const fileNode = {
|
|
742
845
|
id: fileId,
|
|
743
846
|
type: "file",
|
|
@@ -777,9 +880,9 @@ var CodeIngestor = class {
|
|
|
777
880
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
778
881
|
for (const entry of entries) {
|
|
779
882
|
const fullPath = path.join(dir, entry.name);
|
|
780
|
-
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "dist") {
|
|
883
|
+
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "dist" && entry.name !== "target" && entry.name !== "build" && entry.name !== ".git" && entry.name !== "__pycache__" && entry.name !== ".venv" && entry.name !== ".turbo" && entry.name !== ".harness" && entry.name !== ".next" && entry.name !== ".vscode" && entry.name !== ".idea" && entry.name !== "vendor" && entry.name !== "out" && entry.name !== ".gradle" && entry.name !== ".gradle-home" && entry.name !== "bin" && entry.name !== "obj" && entry.name !== "venv" && entry.name !== "_build" && entry.name !== "deps" && entry.name !== "coverage" && entry.name !== ".nyc_output") {
|
|
781
884
|
results.push(...await this.findSourceFiles(fullPath));
|
|
782
|
-
} else if (entry.isFile() &&
|
|
885
|
+
} else if (entry.isFile() && isSupportedSourceFile(entry.name)) {
|
|
783
886
|
results.push(fullPath);
|
|
784
887
|
}
|
|
785
888
|
}
|
|
@@ -788,25 +891,40 @@ var CodeIngestor = class {
|
|
|
788
891
|
extractSymbols(content, fileId, relativePath) {
|
|
789
892
|
const results = [];
|
|
790
893
|
const lines = content.split("\n");
|
|
894
|
+
const lang = this.detectLanguage(relativePath);
|
|
791
895
|
const ctx = { className: null, classId: null, insideClass: false, braceDepth: 0 };
|
|
792
896
|
for (let i = 0; i < lines.length; i++) {
|
|
793
897
|
const line = lines[i];
|
|
794
|
-
|
|
795
|
-
if (this.tryExtractClass(line, lines, i, fileId, relativePath, ctx, results)) continue;
|
|
796
|
-
if (this.tryExtractInterface(line, lines, i, fileId, relativePath, ctx, results)) continue;
|
|
797
|
-
if (this.updateClassContext(line, ctx)) continue;
|
|
798
|
-
if (this.tryExtractMethod(line, lines, i, fileId, relativePath, ctx, results)) continue;
|
|
799
|
-
if (ctx.insideClass) continue;
|
|
800
|
-
this.tryExtractVariable(line, i, fileId, relativePath, results);
|
|
898
|
+
this.processSymbolLine(line, lines, i, fileId, relativePath, ctx, results, lang);
|
|
801
899
|
}
|
|
802
900
|
return results;
|
|
803
901
|
}
|
|
804
|
-
|
|
805
|
-
|
|
902
|
+
processSymbolLine(line, lines, i, fileId, relativePath, ctx, results, lang) {
|
|
903
|
+
if (lang === "python") {
|
|
904
|
+
this.updatePythonClassContext(lines, i, ctx);
|
|
905
|
+
}
|
|
906
|
+
if (this.tryExtractFunction(line, lines, i, fileId, relativePath, ctx, results, lang)) return;
|
|
907
|
+
if (this.tryExtractClass(line, lines, i, fileId, relativePath, ctx, results, lang)) return;
|
|
908
|
+
if (this.tryExtractInterface(line, lines, i, fileId, relativePath, ctx, results, lang)) return;
|
|
909
|
+
if (this.tryEnterImplBlock(line, lang, relativePath, ctx)) return;
|
|
910
|
+
if (lang !== "python" && this.updateClassContext(line, ctx)) return;
|
|
911
|
+
if (this.tryExtractMethod(line, lines, i, fileId, relativePath, ctx, results, lang)) return;
|
|
912
|
+
if (ctx.insideClass) return;
|
|
913
|
+
this.tryExtractVariable(line, i, fileId, relativePath, results, lang);
|
|
914
|
+
}
|
|
915
|
+
matchFunction(line, lang) {
|
|
916
|
+
if (lang === "java") return null;
|
|
917
|
+
const pattern = FUNCTION_PATTERNS[lang];
|
|
918
|
+
return pattern ? line.match(pattern) : null;
|
|
919
|
+
}
|
|
920
|
+
tryExtractFunction(line, lines, i, fileId, relativePath, ctx, results, lang) {
|
|
921
|
+
if (ctx.insideClass) return false;
|
|
922
|
+
const fnMatch = this.matchFunction(line, lang);
|
|
806
923
|
if (!fnMatch) return false;
|
|
807
924
|
const name = fnMatch[1];
|
|
808
925
|
const id = `function:${relativePath}:${name}`;
|
|
809
|
-
const endLine = this.findClosingBrace(lines, i);
|
|
926
|
+
const endLine = lang === "python" ? this.findEndOfIndentBlock(lines, i) : this.findClosingBrace(lines, i);
|
|
927
|
+
const exported = isExported(lang, line, name);
|
|
810
928
|
results.push({
|
|
811
929
|
node: {
|
|
812
930
|
id,
|
|
@@ -815,7 +933,7 @@ var CodeIngestor = class {
|
|
|
815
933
|
path: relativePath,
|
|
816
934
|
location: { fileId, startLine: i + 1, endLine },
|
|
817
935
|
metadata: {
|
|
818
|
-
exported
|
|
936
|
+
exported,
|
|
819
937
|
cyclomaticComplexity: this.computeCyclomaticComplexity(lines.slice(i, endLine)),
|
|
820
938
|
nestingDepth: this.computeMaxNesting(lines.slice(i, endLine)),
|
|
821
939
|
lineCount: endLine - i,
|
|
@@ -830,12 +948,17 @@ var CodeIngestor = class {
|
|
|
830
948
|
}
|
|
831
949
|
return true;
|
|
832
950
|
}
|
|
833
|
-
|
|
834
|
-
const
|
|
951
|
+
matchClass(line, lang) {
|
|
952
|
+
const pattern = CLASS_PATTERNS[lang];
|
|
953
|
+
return pattern ? line.match(pattern) : null;
|
|
954
|
+
}
|
|
955
|
+
tryExtractClass(line, lines, i, fileId, relativePath, ctx, results, lang) {
|
|
956
|
+
const classMatch = this.matchClass(line, lang);
|
|
835
957
|
if (!classMatch) return false;
|
|
836
958
|
const name = classMatch[1];
|
|
837
959
|
const id = `class:${relativePath}:${name}`;
|
|
838
|
-
const endLine = this.findClosingBrace(lines, i);
|
|
960
|
+
const endLine = lang === "python" ? this.findEndOfIndentBlock(lines, i) : this.findClosingBrace(lines, i);
|
|
961
|
+
const exported = isExported(lang, line, name);
|
|
839
962
|
results.push({
|
|
840
963
|
node: {
|
|
841
964
|
id,
|
|
@@ -843,22 +966,27 @@ var CodeIngestor = class {
|
|
|
843
966
|
name,
|
|
844
967
|
path: relativePath,
|
|
845
968
|
location: { fileId, startLine: i + 1, endLine },
|
|
846
|
-
metadata: { exported
|
|
969
|
+
metadata: { exported }
|
|
847
970
|
},
|
|
848
971
|
edge: { from: fileId, to: id, type: "contains" }
|
|
849
972
|
});
|
|
850
973
|
ctx.className = name;
|
|
851
974
|
ctx.classId = id;
|
|
852
975
|
ctx.insideClass = true;
|
|
853
|
-
ctx.braceDepth = countBraces(line);
|
|
976
|
+
ctx.braceDepth = lang === "python" ? 1 : countBraces(line);
|
|
854
977
|
return true;
|
|
855
978
|
}
|
|
856
|
-
|
|
857
|
-
const
|
|
979
|
+
matchInterface(line, lang) {
|
|
980
|
+
const pattern = INTERFACE_PATTERNS[lang];
|
|
981
|
+
return pattern ? line.match(pattern) : null;
|
|
982
|
+
}
|
|
983
|
+
tryExtractInterface(line, lines, i, fileId, relativePath, ctx, results, lang) {
|
|
984
|
+
const ifaceMatch = this.matchInterface(line, lang);
|
|
858
985
|
if (!ifaceMatch) return false;
|
|
859
986
|
const name = ifaceMatch[1];
|
|
860
987
|
const id = `interface:${relativePath}:${name}`;
|
|
861
988
|
const endLine = this.findClosingBrace(lines, i);
|
|
989
|
+
const exported = isExported(lang, line, name);
|
|
862
990
|
results.push({
|
|
863
991
|
node: {
|
|
864
992
|
id,
|
|
@@ -866,7 +994,7 @@ var CodeIngestor = class {
|
|
|
866
994
|
name,
|
|
867
995
|
path: relativePath,
|
|
868
996
|
location: { fileId, startLine: i + 1, endLine },
|
|
869
|
-
metadata: { exported
|
|
997
|
+
metadata: { exported }
|
|
870
998
|
},
|
|
871
999
|
edge: { from: fileId, to: id, type: "contains" }
|
|
872
1000
|
});
|
|
@@ -875,6 +1003,23 @@ var CodeIngestor = class {
|
|
|
875
1003
|
ctx.insideClass = false;
|
|
876
1004
|
return true;
|
|
877
1005
|
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Enter an impl block (Rust) or Java class body — sets class context without creating a node.
|
|
1008
|
+
*/
|
|
1009
|
+
tryEnterImplBlock(line, lang, relativePath, ctx) {
|
|
1010
|
+
if (lang === "rust") {
|
|
1011
|
+
const implMatch = line.match(/^impl(?:<[^>]*>)?\s+(\w+)/);
|
|
1012
|
+
if (implMatch) {
|
|
1013
|
+
const name = implMatch[1];
|
|
1014
|
+
ctx.className = name;
|
|
1015
|
+
ctx.classId = `class:${relativePath}:${name}`;
|
|
1016
|
+
ctx.insideClass = true;
|
|
1017
|
+
ctx.braceDepth = countBraces(line);
|
|
1018
|
+
return true;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
return false;
|
|
1022
|
+
}
|
|
878
1023
|
/** Update brace tracking; returns true when line is consumed (class ended or tracked). */
|
|
879
1024
|
updateClassContext(line, ctx) {
|
|
880
1025
|
if (!ctx.insideClass) return false;
|
|
@@ -887,16 +1032,32 @@ var CodeIngestor = class {
|
|
|
887
1032
|
}
|
|
888
1033
|
return false;
|
|
889
1034
|
}
|
|
890
|
-
|
|
891
|
-
if (
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
);
|
|
1035
|
+
matchMethod(line, lang, insideClass) {
|
|
1036
|
+
if (lang === "go") return line.match(GO_METHOD_PATTERN);
|
|
1037
|
+
if (!insideClass) return null;
|
|
1038
|
+
const pattern = METHOD_PATTERNS[lang];
|
|
1039
|
+
return pattern ? line.match(pattern) : null;
|
|
1040
|
+
}
|
|
1041
|
+
tryExtractMethod(line, lines, i, fileId, relativePath, ctx, results, lang) {
|
|
1042
|
+
const methodMatch = this.matchMethod(line, lang, ctx.insideClass);
|
|
895
1043
|
if (!methodMatch) return false;
|
|
896
|
-
|
|
1044
|
+
let methodName;
|
|
1045
|
+
let className;
|
|
1046
|
+
let classId;
|
|
1047
|
+
if (lang === "go") {
|
|
1048
|
+
const receiverType = methodMatch[1];
|
|
1049
|
+
methodName = methodMatch[2];
|
|
1050
|
+
className = receiverType;
|
|
1051
|
+
classId = `class:${relativePath}:${receiverType}`;
|
|
1052
|
+
} else {
|
|
1053
|
+
if (!ctx.insideClass || !ctx.className || !ctx.classId) return false;
|
|
1054
|
+
methodName = methodMatch[1];
|
|
1055
|
+
className = ctx.className;
|
|
1056
|
+
classId = ctx.classId;
|
|
1057
|
+
}
|
|
897
1058
|
if (SKIP_METHOD_NAMES.has(methodName)) return false;
|
|
898
|
-
const id = `method:${relativePath}:${
|
|
899
|
-
const endLine = this.findClosingBrace(lines, i);
|
|
1059
|
+
const id = `method:${relativePath}:${className}.${methodName}`;
|
|
1060
|
+
const endLine = lang === "python" ? this.findEndOfIndentBlock(lines, i) : this.findClosingBrace(lines, i);
|
|
900
1061
|
results.push({
|
|
901
1062
|
node: {
|
|
902
1063
|
id,
|
|
@@ -905,23 +1066,28 @@ var CodeIngestor = class {
|
|
|
905
1066
|
path: relativePath,
|
|
906
1067
|
location: { fileId, startLine: i + 1, endLine },
|
|
907
1068
|
metadata: {
|
|
908
|
-
className
|
|
909
|
-
exported:
|
|
1069
|
+
className,
|
|
1070
|
+
exported: isExported(lang, line, methodName),
|
|
910
1071
|
cyclomaticComplexity: this.computeCyclomaticComplexity(lines.slice(i, endLine)),
|
|
911
1072
|
nestingDepth: this.computeMaxNesting(lines.slice(i, endLine)),
|
|
912
1073
|
lineCount: endLine - i,
|
|
913
1074
|
parameterCount: this.countParameters(line)
|
|
914
1075
|
}
|
|
915
1076
|
},
|
|
916
|
-
edge: { from:
|
|
1077
|
+
edge: { from: classId, to: id, type: "contains" }
|
|
917
1078
|
});
|
|
918
1079
|
return true;
|
|
919
1080
|
}
|
|
920
|
-
|
|
921
|
-
const
|
|
1081
|
+
matchVariable(line, lang) {
|
|
1082
|
+
const pattern = VARIABLE_PATTERNS[lang];
|
|
1083
|
+
return pattern ? line.match(pattern) : null;
|
|
1084
|
+
}
|
|
1085
|
+
tryExtractVariable(line, i, fileId, relativePath, results, lang) {
|
|
1086
|
+
const varMatch = this.matchVariable(line, lang);
|
|
922
1087
|
if (!varMatch) return;
|
|
923
1088
|
const name = varMatch[1];
|
|
924
1089
|
const id = `variable:${relativePath}:${name}`;
|
|
1090
|
+
const exported = isExported(lang, line, name);
|
|
925
1091
|
results.push({
|
|
926
1092
|
node: {
|
|
927
1093
|
id,
|
|
@@ -929,7 +1095,7 @@ var CodeIngestor = class {
|
|
|
929
1095
|
name,
|
|
930
1096
|
path: relativePath,
|
|
931
1097
|
location: { fileId, startLine: i + 1, endLine: i + 1 },
|
|
932
|
-
metadata: { exported
|
|
1098
|
+
metadata: { exported }
|
|
933
1099
|
},
|
|
934
1100
|
edge: { from: fileId, to: id, type: "contains" }
|
|
935
1101
|
});
|
|
@@ -958,48 +1124,79 @@ var CodeIngestor = class {
|
|
|
958
1124
|
return startIndex + 1;
|
|
959
1125
|
}
|
|
960
1126
|
/**
|
|
961
|
-
*
|
|
1127
|
+
* Find the end of an indentation-based block (Python).
|
|
1128
|
+
* The block ends when a subsequent non-empty line has indentation <= the starting line.
|
|
1129
|
+
*/
|
|
1130
|
+
findEndOfIndentBlock(lines, startIndex) {
|
|
1131
|
+
const startLine = lines[startIndex] ?? "";
|
|
1132
|
+
const baseIndent = startLine.search(/\S/);
|
|
1133
|
+
let lastNonEmpty = startIndex;
|
|
1134
|
+
for (let i = startIndex + 1; i < lines.length; i++) {
|
|
1135
|
+
const line = lines[i];
|
|
1136
|
+
if (line.trim() === "") continue;
|
|
1137
|
+
const indent = line.search(/\S/);
|
|
1138
|
+
if (indent <= baseIndent) {
|
|
1139
|
+
return lastNonEmpty + 1;
|
|
1140
|
+
}
|
|
1141
|
+
lastNonEmpty = i;
|
|
1142
|
+
}
|
|
1143
|
+
return lastNonEmpty + 1;
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* Update class context for Python using indentation tracking.
|
|
1147
|
+
*/
|
|
1148
|
+
updatePythonClassContext(lines, i, ctx) {
|
|
1149
|
+
if (!ctx.insideClass) return false;
|
|
1150
|
+
const line = lines[i];
|
|
1151
|
+
if (line.trim() === "") return false;
|
|
1152
|
+
const indent = line.search(/\S/);
|
|
1153
|
+
if (indent <= 0) {
|
|
1154
|
+
ctx.className = null;
|
|
1155
|
+
ctx.classId = null;
|
|
1156
|
+
ctx.insideClass = false;
|
|
1157
|
+
return false;
|
|
1158
|
+
}
|
|
1159
|
+
return false;
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Scan a file for identifiers matching known callable names,
|
|
962
1163
|
* then create file-to-file "calls" edges. Uses regex heuristic (not AST).
|
|
963
1164
|
*/
|
|
964
|
-
|
|
1165
|
+
extractCallsEdgesForFile(relativePath, content, nameToFiles) {
|
|
965
1166
|
const edges = [];
|
|
966
1167
|
const seen = /* @__PURE__ */ new Set();
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
});
|
|
987
|
-
}
|
|
1168
|
+
const callerFileId = `file:${relativePath}`;
|
|
1169
|
+
const callPattern = /\b([a-zA-Z_$][\w$]*)\s*\(/g;
|
|
1170
|
+
let match;
|
|
1171
|
+
while ((match = callPattern.exec(content)) !== null) {
|
|
1172
|
+
const name = match[1];
|
|
1173
|
+
const targetFiles = nameToFiles.get(name);
|
|
1174
|
+
if (!targetFiles) continue;
|
|
1175
|
+
for (const targetFile of targetFiles) {
|
|
1176
|
+
if (targetFile === relativePath) continue;
|
|
1177
|
+
const targetFileId = `file:${targetFile}`;
|
|
1178
|
+
const key = `${callerFileId}|${targetFileId}`;
|
|
1179
|
+
if (seen.has(key)) continue;
|
|
1180
|
+
seen.add(key);
|
|
1181
|
+
edges.push({
|
|
1182
|
+
from: callerFileId,
|
|
1183
|
+
to: targetFileId,
|
|
1184
|
+
type: "calls",
|
|
1185
|
+
metadata: { confidence: "regex" }
|
|
1186
|
+
});
|
|
988
1187
|
}
|
|
989
1188
|
}
|
|
990
1189
|
return edges;
|
|
991
1190
|
}
|
|
992
1191
|
async extractImports(content, fileId, relativePath, rootDir) {
|
|
1192
|
+
const lang = this.detectLanguage(relativePath);
|
|
993
1193
|
const edges = [];
|
|
994
|
-
const
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
const importPath = match[1];
|
|
998
|
-
if (!importPath.startsWith(".")) continue;
|
|
1194
|
+
const importPaths = this.extractImportPaths(content, lang);
|
|
1195
|
+
for (const { importPath, isTypeOnly } of importPaths) {
|
|
1196
|
+
if ((lang === "typescript" || lang === "javascript") && !importPath.startsWith(".")) continue;
|
|
999
1197
|
const resolvedPath = await this.resolveImportPath(relativePath, importPath, rootDir);
|
|
1000
1198
|
if (resolvedPath) {
|
|
1001
1199
|
const targetId = `file:${resolvedPath}`;
|
|
1002
|
-
const isTypeOnly = match[0].includes("import type");
|
|
1003
1200
|
edges.push({
|
|
1004
1201
|
from: fileId,
|
|
1005
1202
|
to: targetId,
|
|
@@ -1010,10 +1207,53 @@ var CodeIngestor = class {
|
|
|
1010
1207
|
}
|
|
1011
1208
|
return edges;
|
|
1012
1209
|
}
|
|
1210
|
+
extractImportPaths(content, lang) {
|
|
1211
|
+
const results = [];
|
|
1212
|
+
if (lang === "typescript" || lang === "javascript") {
|
|
1213
|
+
const importRegex = /import\s+(?:type\s+)?(?:\{[^}]*\}|[\w*]+)\s+from\s+['"]([^'"]+)['"]/g;
|
|
1214
|
+
let match;
|
|
1215
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
1216
|
+
results.push({
|
|
1217
|
+
importPath: match[1],
|
|
1218
|
+
isTypeOnly: match[0].includes("import type")
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
} else if (lang === "python") {
|
|
1222
|
+
const fromImport = /(?:from\s+([\w.]+)\s+import|import\s+([\w.]+))/g;
|
|
1223
|
+
let match;
|
|
1224
|
+
while ((match = fromImport.exec(content)) !== null) {
|
|
1225
|
+
const importPath = match[1] ?? match[2];
|
|
1226
|
+
if (importPath.startsWith(".")) {
|
|
1227
|
+
results.push({ importPath, isTypeOnly: false });
|
|
1228
|
+
} else {
|
|
1229
|
+
results.push({ importPath: importPath.replace(/\./g, "/"), isTypeOnly: false });
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
} else if (lang === "go") {
|
|
1233
|
+
const goImport = /"([^"]+)"/g;
|
|
1234
|
+
let match;
|
|
1235
|
+
while ((match = goImport.exec(content)) !== null) {
|
|
1236
|
+
results.push({ importPath: match[1], isTypeOnly: false });
|
|
1237
|
+
}
|
|
1238
|
+
} else if (lang === "rust") {
|
|
1239
|
+
const useDecl = /use\s+((?:crate|super|self)(?:::\w+)+)/g;
|
|
1240
|
+
let match;
|
|
1241
|
+
while ((match = useDecl.exec(content)) !== null) {
|
|
1242
|
+
results.push({ importPath: match[1], isTypeOnly: false });
|
|
1243
|
+
}
|
|
1244
|
+
} else if (lang === "java") {
|
|
1245
|
+
const javaImport = /import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;/g;
|
|
1246
|
+
let match;
|
|
1247
|
+
while ((match = javaImport.exec(content)) !== null) {
|
|
1248
|
+
results.push({ importPath: match[1], isTypeOnly: false });
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
return results;
|
|
1252
|
+
}
|
|
1013
1253
|
async resolveImportPath(fromFile, importPath, rootDir) {
|
|
1014
1254
|
const fromDir = path.dirname(fromFile);
|
|
1015
1255
|
const resolved = path.normalize(path.join(fromDir, importPath)).replace(/\\/g, "/");
|
|
1016
|
-
const extensions = [".ts", ".tsx", ".js", ".jsx"];
|
|
1256
|
+
const extensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
|
|
1017
1257
|
for (const ext of extensions) {
|
|
1018
1258
|
const candidate = resolved.replace(/\.js$/, "") + ext;
|
|
1019
1259
|
const fullPath = path.join(rootDir, candidate);
|
|
@@ -1023,7 +1263,8 @@ var CodeIngestor = class {
|
|
|
1023
1263
|
} catch {
|
|
1024
1264
|
}
|
|
1025
1265
|
}
|
|
1026
|
-
|
|
1266
|
+
const indexExtensions = [".ts", ".tsx", ".js", ".jsx"];
|
|
1267
|
+
for (const ext of indexExtensions) {
|
|
1027
1268
|
const candidate = path.join(resolved, `index${ext}`).replace(/\\/g, "/");
|
|
1028
1269
|
const fullPath = path.join(rootDir, candidate);
|
|
1029
1270
|
try {
|
|
@@ -1032,6 +1273,12 @@ var CodeIngestor = class {
|
|
|
1032
1273
|
} catch {
|
|
1033
1274
|
}
|
|
1034
1275
|
}
|
|
1276
|
+
const pyInit = path.join(resolved, "__init__.py").replace(/\\/g, "/");
|
|
1277
|
+
try {
|
|
1278
|
+
await fs.access(path.join(rootDir, pyInit));
|
|
1279
|
+
return pyInit;
|
|
1280
|
+
} catch {
|
|
1281
|
+
}
|
|
1035
1282
|
return null;
|
|
1036
1283
|
}
|
|
1037
1284
|
computeCyclomaticComplexity(lines) {
|
|
@@ -1077,6 +1324,11 @@ var CodeIngestor = class {
|
|
|
1077
1324
|
detectLanguage(filePath) {
|
|
1078
1325
|
if (/\.tsx?$/.test(filePath)) return "typescript";
|
|
1079
1326
|
if (/\.jsx?$/.test(filePath)) return "javascript";
|
|
1327
|
+
if (/\.py$/.test(filePath)) return "python";
|
|
1328
|
+
if (/\.go$/.test(filePath)) return "go";
|
|
1329
|
+
if (/\.rs$/.test(filePath)) return "rust";
|
|
1330
|
+
if (/\.java$/.test(filePath)) return "java";
|
|
1331
|
+
if (/\.groovy$/.test(filePath)) return "java";
|
|
1080
1332
|
return "unknown";
|
|
1081
1333
|
}
|
|
1082
1334
|
/**
|
|
@@ -1084,40 +1336,38 @@ var CodeIngestor = class {
|
|
|
1084
1336
|
* linking requirement nodes to the annotated files.
|
|
1085
1337
|
* Format: // @req <feature-name>#<index>
|
|
1086
1338
|
*/
|
|
1087
|
-
|
|
1088
|
-
const REQ_TAG =
|
|
1339
|
+
extractReqAnnotationsForFile(filePath, content, rootDir) {
|
|
1340
|
+
const REQ_TAG = /(?:\/\/|#|\/\*)\s*@req\s+([\w-]+)#(\d+)/g;
|
|
1089
1341
|
const reqNodes = this.store.findNodes({ type: "requirement" });
|
|
1090
1342
|
let edgesAdded = 0;
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1343
|
+
let match;
|
|
1344
|
+
REQ_TAG.lastIndex = 0;
|
|
1345
|
+
while ((match = REQ_TAG.exec(content)) !== null) {
|
|
1346
|
+
const featureName = match[1];
|
|
1347
|
+
const reqIndex = parseInt(match[2], 10);
|
|
1348
|
+
const reqNode = reqNodes.find(
|
|
1349
|
+
(n) => n.metadata.featureName === featureName && n.metadata.index === reqIndex
|
|
1350
|
+
);
|
|
1351
|
+
if (!reqNode) {
|
|
1352
|
+
console.warn(
|
|
1353
|
+
`@req annotation references non-existent requirement: ${featureName}#${reqIndex} in ${filePath}`
|
|
1099
1354
|
);
|
|
1100
|
-
|
|
1101
|
-
console.warn(
|
|
1102
|
-
`@req annotation references non-existent requirement: ${featureName}#${reqIndex} in ${filePath}`
|
|
1103
|
-
);
|
|
1104
|
-
continue;
|
|
1105
|
-
}
|
|
1106
|
-
const relPath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
1107
|
-
const fileNodeId = `file:${relPath}`;
|
|
1108
|
-
this.store.addEdge({
|
|
1109
|
-
from: reqNode.id,
|
|
1110
|
-
to: fileNodeId,
|
|
1111
|
-
type: "verified_by",
|
|
1112
|
-
confidence: 1,
|
|
1113
|
-
metadata: {
|
|
1114
|
-
method: "annotation",
|
|
1115
|
-
tag: `@req ${featureName}#${reqIndex}`,
|
|
1116
|
-
confidence: 1
|
|
1117
|
-
}
|
|
1118
|
-
});
|
|
1119
|
-
edgesAdded++;
|
|
1355
|
+
continue;
|
|
1120
1356
|
}
|
|
1357
|
+
const relPath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
1358
|
+
const fileNodeId = `file:${relPath}`;
|
|
1359
|
+
this.store.addEdge({
|
|
1360
|
+
from: reqNode.id,
|
|
1361
|
+
to: fileNodeId,
|
|
1362
|
+
type: "verified_by",
|
|
1363
|
+
confidence: 1,
|
|
1364
|
+
metadata: {
|
|
1365
|
+
method: "annotation",
|
|
1366
|
+
tag: `@req ${featureName}#${reqIndex}`,
|
|
1367
|
+
confidence: 1
|
|
1368
|
+
}
|
|
1369
|
+
});
|
|
1370
|
+
edgesAdded++;
|
|
1121
1371
|
}
|
|
1122
1372
|
return edgesAdded;
|
|
1123
1373
|
}
|
|
@@ -1521,7 +1771,7 @@ var KnowledgeIngestor = class {
|
|
|
1521
1771
|
const entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
1522
1772
|
for (const entry of entries) {
|
|
1523
1773
|
const fullPath = path3.join(dir, entry.name);
|
|
1524
|
-
if (entry.isDirectory()) {
|
|
1774
|
+
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "dist" && entry.name !== "target" && entry.name !== "build" && entry.name !== ".git" && entry.name !== ".gradle" && entry.name !== ".harness" && entry.name !== "vendor" && entry.name !== "bin" && entry.name !== "obj" && entry.name !== "venv" && entry.name !== "_build" && entry.name !== "deps" && entry.name !== "coverage") {
|
|
1525
1775
|
results.push(...await this.findMarkdownFiles(fullPath));
|
|
1526
1776
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1527
1777
|
results.push(fullPath);
|
|
@@ -1599,9 +1849,176 @@ function parseFailureSection(section) {
|
|
|
1599
1849
|
};
|
|
1600
1850
|
}
|
|
1601
1851
|
|
|
1602
|
-
// src/ingest/
|
|
1852
|
+
// src/ingest/BusinessKnowledgeIngestor.ts
|
|
1603
1853
|
import * as fs3 from "fs/promises";
|
|
1604
1854
|
import * as path4 from "path";
|
|
1855
|
+
var BUSINESS_KNOWLEDGE_TYPES = /* @__PURE__ */ new Set([
|
|
1856
|
+
"business_rule",
|
|
1857
|
+
"business_process",
|
|
1858
|
+
"business_concept",
|
|
1859
|
+
"business_term",
|
|
1860
|
+
"business_metric"
|
|
1861
|
+
]);
|
|
1862
|
+
var GOVERNS_SOURCE_TYPES = /* @__PURE__ */ new Set(["business_rule", "business_process"]);
|
|
1863
|
+
var CODE_NODE_TYPES2 = [
|
|
1864
|
+
"file",
|
|
1865
|
+
"function",
|
|
1866
|
+
"class",
|
|
1867
|
+
"method",
|
|
1868
|
+
"interface",
|
|
1869
|
+
"variable"
|
|
1870
|
+
];
|
|
1871
|
+
var MEASURABLE_TYPES = /* @__PURE__ */ new Set(["business_process", "business_concept"]);
|
|
1872
|
+
var BusinessKnowledgeIngestor = class {
|
|
1873
|
+
constructor(store) {
|
|
1874
|
+
this.store = store;
|
|
1875
|
+
}
|
|
1876
|
+
store;
|
|
1877
|
+
async ingest(knowledgeDir) {
|
|
1878
|
+
const start = Date.now();
|
|
1879
|
+
const errors = [];
|
|
1880
|
+
let files;
|
|
1881
|
+
try {
|
|
1882
|
+
files = await this.findMarkdownFiles(knowledgeDir);
|
|
1883
|
+
} catch {
|
|
1884
|
+
return emptyResult(Date.now() - start);
|
|
1885
|
+
}
|
|
1886
|
+
const nodeEntries = await this.createNodes(files, knowledgeDir, errors);
|
|
1887
|
+
const edgesAdded = this.createEdges(nodeEntries);
|
|
1888
|
+
return {
|
|
1889
|
+
nodesAdded: nodeEntries.length,
|
|
1890
|
+
nodesUpdated: 0,
|
|
1891
|
+
edgesAdded,
|
|
1892
|
+
edgesUpdated: 0,
|
|
1893
|
+
errors,
|
|
1894
|
+
durationMs: Date.now() - start
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
async createNodes(files, knowledgeDir, errors) {
|
|
1898
|
+
const entries = [];
|
|
1899
|
+
for (const filePath of files) {
|
|
1900
|
+
try {
|
|
1901
|
+
const entry = await this.parseAndAddNode(filePath, knowledgeDir);
|
|
1902
|
+
if (entry) entries.push(entry);
|
|
1903
|
+
} catch (err) {
|
|
1904
|
+
errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
return entries;
|
|
1908
|
+
}
|
|
1909
|
+
async parseAndAddNode(filePath, knowledgeDir) {
|
|
1910
|
+
const raw = await fs3.readFile(filePath, "utf-8");
|
|
1911
|
+
const parsed = parseFrontmatter(raw);
|
|
1912
|
+
if (!parsed) return null;
|
|
1913
|
+
const { frontmatter, body } = parsed;
|
|
1914
|
+
if (!BUSINESS_KNOWLEDGE_TYPES.has(frontmatter.type)) return null;
|
|
1915
|
+
const relPath = path4.relative(knowledgeDir, filePath).replaceAll("\\", "/");
|
|
1916
|
+
const domain = frontmatter.domain ?? relPath.split("/")[0] ?? "unknown";
|
|
1917
|
+
const filename = path4.basename(filePath, ".md");
|
|
1918
|
+
const nodeId = `bk:${domain}:${filename}`;
|
|
1919
|
+
const titleMatch = body.match(/^#\s+(.+)$/m);
|
|
1920
|
+
const name = titleMatch ? titleMatch[1].trim() : filename;
|
|
1921
|
+
const node = {
|
|
1922
|
+
id: nodeId,
|
|
1923
|
+
type: frontmatter.type,
|
|
1924
|
+
name,
|
|
1925
|
+
path: relPath,
|
|
1926
|
+
content: body.trim(),
|
|
1927
|
+
metadata: {
|
|
1928
|
+
domain,
|
|
1929
|
+
...frontmatter.tags && { tags: frontmatter.tags },
|
|
1930
|
+
...frontmatter.related && { related: frontmatter.related }
|
|
1931
|
+
}
|
|
1932
|
+
};
|
|
1933
|
+
this.store.addNode(node);
|
|
1934
|
+
return { nodeId, node, content: body };
|
|
1935
|
+
}
|
|
1936
|
+
createEdges(nodeEntries) {
|
|
1937
|
+
let edgesAdded = 0;
|
|
1938
|
+
for (const { nodeId, node, content } of nodeEntries) {
|
|
1939
|
+
if (GOVERNS_SOURCE_TYPES.has(node.type)) {
|
|
1940
|
+
edgesAdded += this.linkToNodes(content, nodeId, "governs", CODE_NODE_TYPES2);
|
|
1941
|
+
} else {
|
|
1942
|
+
edgesAdded += this.linkToNodes(content, nodeId, "documents", CODE_NODE_TYPES2);
|
|
1943
|
+
}
|
|
1944
|
+
if (node.type === "business_metric") {
|
|
1945
|
+
edgesAdded += this.linkToBusinessNodes(content, nodeId, "measures", MEASURABLE_TYPES);
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
return edgesAdded;
|
|
1949
|
+
}
|
|
1950
|
+
linkToNodes(content, sourceNodeId, edgeType, targetTypes) {
|
|
1951
|
+
let count = 0;
|
|
1952
|
+
for (const nodeType of targetTypes) {
|
|
1953
|
+
const nodes = this.store.findNodes({ type: nodeType });
|
|
1954
|
+
for (const node of nodes) {
|
|
1955
|
+
if (node.name.length < 3) continue;
|
|
1956
|
+
const escaped = node.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1957
|
+
const namePattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
1958
|
+
if (namePattern.test(content)) {
|
|
1959
|
+
this.store.addEdge({ from: sourceNodeId, to: node.id, type: edgeType });
|
|
1960
|
+
count++;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
return count;
|
|
1965
|
+
}
|
|
1966
|
+
linkToBusinessNodes(content, sourceNodeId, edgeType, targetTypes) {
|
|
1967
|
+
let count = 0;
|
|
1968
|
+
for (const node of this.store.findNodes({})) {
|
|
1969
|
+
if (!targetTypes.has(node.type)) continue;
|
|
1970
|
+
if (node.name.length < 3) continue;
|
|
1971
|
+
const escaped = node.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1972
|
+
const namePattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
1973
|
+
if (namePattern.test(content)) {
|
|
1974
|
+
this.store.addEdge({ from: sourceNodeId, to: node.id, type: edgeType });
|
|
1975
|
+
count++;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
return count;
|
|
1979
|
+
}
|
|
1980
|
+
async findMarkdownFiles(dir) {
|
|
1981
|
+
const results = [];
|
|
1982
|
+
const entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
1983
|
+
for (const entry of entries) {
|
|
1984
|
+
const fullPath = path4.join(dir, entry.name);
|
|
1985
|
+
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "dist" && entry.name !== "target" && entry.name !== "build" && entry.name !== ".git" && entry.name !== ".gradle" && entry.name !== ".harness" && entry.name !== "vendor" && entry.name !== "bin" && entry.name !== "obj" && entry.name !== "venv" && entry.name !== "_build" && entry.name !== "deps" && entry.name !== "coverage") {
|
|
1986
|
+
results.push(...await this.findMarkdownFiles(fullPath));
|
|
1987
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1988
|
+
results.push(fullPath);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
return results;
|
|
1992
|
+
}
|
|
1993
|
+
};
|
|
1994
|
+
function parseFrontmatter(raw) {
|
|
1995
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
1996
|
+
if (!match) return null;
|
|
1997
|
+
const yamlBlock = match[1];
|
|
1998
|
+
const body = match[2];
|
|
1999
|
+
const frontmatter = {};
|
|
2000
|
+
for (const line of yamlBlock.split("\n")) {
|
|
2001
|
+
const kvMatch = line.match(/^(\w+):\s*(.+)$/);
|
|
2002
|
+
if (!kvMatch) continue;
|
|
2003
|
+
const key = kvMatch[1];
|
|
2004
|
+
const value = kvMatch[2].trim();
|
|
2005
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
2006
|
+
frontmatter[key] = value.slice(1, -1).split(",").map((s) => s.trim());
|
|
2007
|
+
} else {
|
|
2008
|
+
frontmatter[key] = value;
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
if (!frontmatter.type || typeof frontmatter.type !== "string") return null;
|
|
2012
|
+
if (!frontmatter.domain || typeof frontmatter.domain !== "string") return null;
|
|
2013
|
+
return {
|
|
2014
|
+
frontmatter,
|
|
2015
|
+
body
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// src/ingest/RequirementIngestor.ts
|
|
2020
|
+
import * as fs4 from "fs/promises";
|
|
2021
|
+
import * as path5 from "path";
|
|
1605
2022
|
var REQUIREMENT_SECTIONS = [
|
|
1606
2023
|
"Observable Truths",
|
|
1607
2024
|
"Success Criteria",
|
|
@@ -1618,7 +2035,7 @@ function detectEarsPattern(text) {
|
|
|
1618
2035
|
if (/^the\s+\w+\s+shall\b/.test(lower)) return "ubiquitous";
|
|
1619
2036
|
return void 0;
|
|
1620
2037
|
}
|
|
1621
|
-
var
|
|
2038
|
+
var CODE_NODE_TYPES3 = ["file", "function", "class", "method", "interface", "variable"];
|
|
1622
2039
|
var RequirementIngestor = class {
|
|
1623
2040
|
constructor(store) {
|
|
1624
2041
|
this.store = store;
|
|
@@ -1636,8 +2053,8 @@ var RequirementIngestor = class {
|
|
|
1636
2053
|
let edgesAdded = 0;
|
|
1637
2054
|
let featureDirs;
|
|
1638
2055
|
try {
|
|
1639
|
-
const entries = await
|
|
1640
|
-
featureDirs = entries.filter((e) => e.isDirectory()).map((e) =>
|
|
2056
|
+
const entries = await fs4.readdir(specsDir, { withFileTypes: true });
|
|
2057
|
+
featureDirs = entries.filter((e) => e.isDirectory()).map((e) => path5.join(specsDir, e.name));
|
|
1641
2058
|
} catch {
|
|
1642
2059
|
return emptyResult(Date.now() - start);
|
|
1643
2060
|
}
|
|
@@ -1656,11 +2073,11 @@ var RequirementIngestor = class {
|
|
|
1656
2073
|
};
|
|
1657
2074
|
}
|
|
1658
2075
|
async ingestFeatureDir(featureDir, errors) {
|
|
1659
|
-
const featureName =
|
|
1660
|
-
const specPath =
|
|
2076
|
+
const featureName = path5.basename(featureDir);
|
|
2077
|
+
const specPath = path5.join(featureDir, "proposal.md").replaceAll("\\", "/");
|
|
1661
2078
|
let content;
|
|
1662
2079
|
try {
|
|
1663
|
-
content = await
|
|
2080
|
+
content = await fs4.readFile(specPath, "utf-8");
|
|
1664
2081
|
} catch {
|
|
1665
2082
|
return { nodesAdded: 0, edgesAdded: 0 };
|
|
1666
2083
|
}
|
|
@@ -1677,7 +2094,7 @@ var RequirementIngestor = class {
|
|
|
1677
2094
|
this.store.addNode({
|
|
1678
2095
|
id: specNodeId,
|
|
1679
2096
|
type: "document",
|
|
1680
|
-
name:
|
|
2097
|
+
name: path5.basename(specPath),
|
|
1681
2098
|
path: specPath,
|
|
1682
2099
|
metadata: { featureName }
|
|
1683
2100
|
});
|
|
@@ -1792,9 +2209,9 @@ var RequirementIngestor = class {
|
|
|
1792
2209
|
for (const node of fileNodes) {
|
|
1793
2210
|
if (!node.path) continue;
|
|
1794
2211
|
const normalizedPath = node.path.replace(/\\/g, "/");
|
|
1795
|
-
const isCodeMatch = normalizedPath.includes("packages/") &&
|
|
2212
|
+
const isCodeMatch = normalizedPath.includes("packages/") && path5.basename(normalizedPath).includes(featureName);
|
|
1796
2213
|
const isTestMatch = normalizedPath.includes("/tests/") && // platform-safe
|
|
1797
|
-
|
|
2214
|
+
path5.basename(normalizedPath).includes(featureName);
|
|
1798
2215
|
if (isCodeMatch && !isTestMatch) {
|
|
1799
2216
|
this.store.addEdge({
|
|
1800
2217
|
from: reqId,
|
|
@@ -1823,7 +2240,7 @@ var RequirementIngestor = class {
|
|
|
1823
2240
|
*/
|
|
1824
2241
|
linkByKeywordOverlap(reqId, reqText) {
|
|
1825
2242
|
let count = 0;
|
|
1826
|
-
for (const nodeType of
|
|
2243
|
+
for (const nodeType of CODE_NODE_TYPES3) {
|
|
1827
2244
|
const codeNodes = this.store.findNodes({ type: nodeType });
|
|
1828
2245
|
for (const node of codeNodes) {
|
|
1829
2246
|
if (node.name.length < 3) continue;
|
|
@@ -1842,12 +2259,2649 @@ var RequirementIngestor = class {
|
|
|
1842
2259
|
}
|
|
1843
2260
|
}
|
|
1844
2261
|
}
|
|
1845
|
-
return count;
|
|
2262
|
+
return count;
|
|
2263
|
+
}
|
|
2264
|
+
};
|
|
2265
|
+
|
|
2266
|
+
// src/ingest/KnowledgePipelineRunner.ts
|
|
2267
|
+
import * as fs9 from "fs/promises";
|
|
2268
|
+
import * as path10 from "path";
|
|
2269
|
+
|
|
2270
|
+
// src/ingest/DiagramParser.ts
|
|
2271
|
+
import * as fs5 from "fs/promises";
|
|
2272
|
+
import * as path6 from "path";
|
|
2273
|
+
function emptyMermaidResult(diagramType = "unknown") {
|
|
2274
|
+
return {
|
|
2275
|
+
entities: [],
|
|
2276
|
+
relationships: [],
|
|
2277
|
+
metadata: { format: "mermaid", diagramType }
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
var MermaidParser = class {
|
|
2281
|
+
canParse(_content, ext) {
|
|
2282
|
+
return ext === ".mmd" || ext === ".mermaid";
|
|
2283
|
+
}
|
|
2284
|
+
parse(content, _filePath) {
|
|
2285
|
+
const trimmed = content.trim();
|
|
2286
|
+
if (!trimmed) {
|
|
2287
|
+
return emptyMermaidResult();
|
|
2288
|
+
}
|
|
2289
|
+
const diagramType = this.detectDiagramType(trimmed);
|
|
2290
|
+
switch (diagramType) {
|
|
2291
|
+
case "flowchart":
|
|
2292
|
+
return this.parseFlowchart(trimmed, diagramType);
|
|
2293
|
+
case "sequence":
|
|
2294
|
+
return this.parseSequence(trimmed, diagramType);
|
|
2295
|
+
default:
|
|
2296
|
+
return emptyMermaidResult(diagramType);
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
detectDiagramType(content) {
|
|
2300
|
+
const lines = content.split("\n");
|
|
2301
|
+
for (const line of lines) {
|
|
2302
|
+
const trimmedLine = line.trim();
|
|
2303
|
+
if (!trimmedLine) continue;
|
|
2304
|
+
if (/^(?:graph|flowchart)\b/i.test(trimmedLine)) return "flowchart";
|
|
2305
|
+
if (/^sequenceDiagram\b/.test(trimmedLine)) return "sequence";
|
|
2306
|
+
if (/^classDiagram\b/.test(trimmedLine)) return "class";
|
|
2307
|
+
if (/^erDiagram\b/.test(trimmedLine)) return "er";
|
|
2308
|
+
break;
|
|
2309
|
+
}
|
|
2310
|
+
return "unknown";
|
|
2311
|
+
}
|
|
2312
|
+
parseFlowchart(content, diagramType) {
|
|
2313
|
+
const entities = /* @__PURE__ */ new Map();
|
|
2314
|
+
const relationships = [];
|
|
2315
|
+
const nodeRegex = /([A-Za-z0-9_]+)\s*[[({]([^\])}]+)[\])}]/g;
|
|
2316
|
+
let match;
|
|
2317
|
+
match = nodeRegex.exec(content);
|
|
2318
|
+
while (match !== null) {
|
|
2319
|
+
const id = match[1] ?? "";
|
|
2320
|
+
const label = (match[2] ?? "").trim();
|
|
2321
|
+
if (id && !entities.has(id)) {
|
|
2322
|
+
const isDecision = this.isDecisionNode(content, id);
|
|
2323
|
+
entities.set(id, {
|
|
2324
|
+
id,
|
|
2325
|
+
label,
|
|
2326
|
+
...isDecision ? { type: "decision" } : {}
|
|
2327
|
+
});
|
|
2328
|
+
}
|
|
2329
|
+
match = nodeRegex.exec(content);
|
|
2330
|
+
}
|
|
2331
|
+
const stripped = content.replace(/[[({][^\])}]*[\])}]/g, "");
|
|
2332
|
+
const labeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\|([^|]+)\|\s*([A-Za-z0-9_]+)/g;
|
|
2333
|
+
const unlabeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\s+([A-Za-z0-9_]+)/g;
|
|
2334
|
+
const edgeKeys = /* @__PURE__ */ new Set();
|
|
2335
|
+
match = labeledEdgeRegex.exec(stripped);
|
|
2336
|
+
while (match !== null) {
|
|
2337
|
+
const from = match[1] ?? "";
|
|
2338
|
+
const label = (match[2] ?? "").trim();
|
|
2339
|
+
const to = match[3] ?? "";
|
|
2340
|
+
if (from && to) {
|
|
2341
|
+
const key = `${from}->${to}:${label}`;
|
|
2342
|
+
if (!edgeKeys.has(key)) {
|
|
2343
|
+
edgeKeys.add(key);
|
|
2344
|
+
relationships.push({ from, to, label });
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
match = labeledEdgeRegex.exec(stripped);
|
|
2348
|
+
}
|
|
2349
|
+
match = unlabeledEdgeRegex.exec(stripped);
|
|
2350
|
+
while (match !== null) {
|
|
2351
|
+
const from = match[1] ?? "";
|
|
2352
|
+
const to = match[2] ?? "";
|
|
2353
|
+
if (from && to) {
|
|
2354
|
+
const hasLabeled = relationships.some((r) => r.from === from && r.to === to);
|
|
2355
|
+
if (!hasLabeled) {
|
|
2356
|
+
const key = `${from}->${to}`;
|
|
2357
|
+
if (!edgeKeys.has(key)) {
|
|
2358
|
+
edgeKeys.add(key);
|
|
2359
|
+
relationships.push({ from, to });
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
match = unlabeledEdgeRegex.exec(stripped);
|
|
2364
|
+
}
|
|
2365
|
+
return {
|
|
2366
|
+
entities: Array.from(entities.values()),
|
|
2367
|
+
relationships,
|
|
2368
|
+
metadata: { format: "mermaid", diagramType }
|
|
2369
|
+
};
|
|
2370
|
+
}
|
|
2371
|
+
isDecisionNode(content, nodeId) {
|
|
2372
|
+
const decisionRegex = new RegExp(`${nodeId}\\s*\\{`);
|
|
2373
|
+
return decisionRegex.test(content);
|
|
2374
|
+
}
|
|
2375
|
+
parseSequence(content, diagramType) {
|
|
2376
|
+
const entities = [];
|
|
2377
|
+
const relationships = [];
|
|
2378
|
+
const seenParticipants = /* @__PURE__ */ new Set();
|
|
2379
|
+
const participantRegex = /participant\s+(\w+)(?:\s+as\s+(.+))?/g;
|
|
2380
|
+
let match;
|
|
2381
|
+
match = participantRegex.exec(content);
|
|
2382
|
+
while (match !== null) {
|
|
2383
|
+
const id = match[1] ?? "";
|
|
2384
|
+
const label = match[2]?.trim() || id;
|
|
2385
|
+
if (id && !seenParticipants.has(id)) {
|
|
2386
|
+
seenParticipants.add(id);
|
|
2387
|
+
entities.push({ id, label });
|
|
2388
|
+
}
|
|
2389
|
+
match = participantRegex.exec(content);
|
|
2390
|
+
}
|
|
2391
|
+
const forwardMsgRegex = /(\w+)\s*->>?\+?\s*(\w+)\s*:\s*(.+)/g;
|
|
2392
|
+
match = forwardMsgRegex.exec(content);
|
|
2393
|
+
while (match !== null) {
|
|
2394
|
+
const from = match[1] ?? "";
|
|
2395
|
+
const to = match[2] ?? "";
|
|
2396
|
+
const label = (match[3] ?? "").trim();
|
|
2397
|
+
if (from && to) {
|
|
2398
|
+
relationships.push({ from, to, label });
|
|
2399
|
+
}
|
|
2400
|
+
match = forwardMsgRegex.exec(content);
|
|
2401
|
+
}
|
|
2402
|
+
const returnMsgRegex = /(\w+)\s*-->>?-?\s*(\w+)\s*:\s*(.+)/g;
|
|
2403
|
+
match = returnMsgRegex.exec(content);
|
|
2404
|
+
while (match !== null) {
|
|
2405
|
+
const from = match[1] ?? "";
|
|
2406
|
+
const to = match[2] ?? "";
|
|
2407
|
+
const label = (match[3] ?? "").trim();
|
|
2408
|
+
if (from && to) {
|
|
2409
|
+
relationships.push({ from, to, label });
|
|
2410
|
+
}
|
|
2411
|
+
match = returnMsgRegex.exec(content);
|
|
2412
|
+
}
|
|
2413
|
+
return {
|
|
2414
|
+
entities,
|
|
2415
|
+
relationships,
|
|
2416
|
+
metadata: { format: "mermaid", diagramType }
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
};
|
|
2420
|
+
var D2Parser = class {
|
|
2421
|
+
canParse(_content, ext) {
|
|
2422
|
+
return ext === ".d2";
|
|
2423
|
+
}
|
|
2424
|
+
parse(content, _filePath) {
|
|
2425
|
+
const trimmed = content.trim();
|
|
2426
|
+
if (!trimmed) {
|
|
2427
|
+
return {
|
|
2428
|
+
entities: [],
|
|
2429
|
+
relationships: [],
|
|
2430
|
+
metadata: { format: "d2", diagramType: "architecture" }
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2433
|
+
const entities = /* @__PURE__ */ new Map();
|
|
2434
|
+
const relationships = [];
|
|
2435
|
+
let braceDepth = 0;
|
|
2436
|
+
for (const line of trimmed.split("\n")) {
|
|
2437
|
+
const stripped = line.trim();
|
|
2438
|
+
if (!stripped || stripped.startsWith("#")) continue;
|
|
2439
|
+
if (stripped.endsWith("{")) {
|
|
2440
|
+
if (braceDepth === 0) {
|
|
2441
|
+
const shapeMatch = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+?)\s*\{$/);
|
|
2442
|
+
if (shapeMatch) {
|
|
2443
|
+
const id = shapeMatch[1];
|
|
2444
|
+
const label = shapeMatch[2];
|
|
2445
|
+
if (id && label) {
|
|
2446
|
+
entities.set(id, { id, label });
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
braceDepth++;
|
|
2451
|
+
continue;
|
|
2452
|
+
}
|
|
2453
|
+
if (stripped === "}") {
|
|
2454
|
+
braceDepth = Math.max(0, braceDepth - 1);
|
|
2455
|
+
continue;
|
|
2456
|
+
}
|
|
2457
|
+
if (braceDepth > 0) continue;
|
|
2458
|
+
const connMatch = stripped.match(
|
|
2459
|
+
/^([a-zA-Z0-9_.-]+)\s*->\s*([a-zA-Z0-9_.-]+)(?:\s*:\s*(.+?))?$/
|
|
2460
|
+
);
|
|
2461
|
+
if (connMatch) {
|
|
2462
|
+
const from = connMatch[1] ?? "";
|
|
2463
|
+
const to = connMatch[2] ?? "";
|
|
2464
|
+
const label = connMatch[3]?.trim();
|
|
2465
|
+
if (from && to) {
|
|
2466
|
+
relationships.push({ from, to, ...label ? { label } : {} });
|
|
2467
|
+
}
|
|
2468
|
+
continue;
|
|
2469
|
+
}
|
|
2470
|
+
const simpleMatch = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+)$/);
|
|
2471
|
+
if (simpleMatch) {
|
|
2472
|
+
const id = simpleMatch[1];
|
|
2473
|
+
const label = (simpleMatch[2] ?? "").trim();
|
|
2474
|
+
if (id && label && !entities.has(id)) {
|
|
2475
|
+
entities.set(id, { id, label });
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
return {
|
|
2480
|
+
entities: Array.from(entities.values()),
|
|
2481
|
+
relationships,
|
|
2482
|
+
metadata: { format: "d2", diagramType: "architecture" }
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
};
|
|
2486
|
+
var PlantUmlParser = class {
|
|
2487
|
+
canParse(_content, ext) {
|
|
2488
|
+
return ext === ".puml" || ext === ".plantuml";
|
|
2489
|
+
}
|
|
2490
|
+
parse(content, _filePath) {
|
|
2491
|
+
const trimmed = content.trim();
|
|
2492
|
+
if (!trimmed) {
|
|
2493
|
+
return {
|
|
2494
|
+
entities: [],
|
|
2495
|
+
relationships: [],
|
|
2496
|
+
metadata: { format: "plantuml", diagramType: "unknown" }
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
const diagramType = this.detectDiagramType(trimmed);
|
|
2500
|
+
const entities = /* @__PURE__ */ new Map();
|
|
2501
|
+
const relationships = [];
|
|
2502
|
+
const body = trimmed.replace(/@startuml\b.*\n?/, "").replace(/@enduml\b.*/, "").trim();
|
|
2503
|
+
const classRegex = /class\s+(\w+)/g;
|
|
2504
|
+
let match;
|
|
2505
|
+
match = classRegex.exec(body);
|
|
2506
|
+
while (match !== null) {
|
|
2507
|
+
const id = match[1] ?? "";
|
|
2508
|
+
if (id && !entities.has(id)) {
|
|
2509
|
+
entities.set(id, { id, label: id });
|
|
2510
|
+
}
|
|
2511
|
+
match = classRegex.exec(body);
|
|
2512
|
+
}
|
|
2513
|
+
const componentRegex1 = /\[([^\]]+)\]/g;
|
|
2514
|
+
match = componentRegex1.exec(body);
|
|
2515
|
+
while (match !== null) {
|
|
2516
|
+
const label = (match[1] ?? "").trim();
|
|
2517
|
+
if (label) {
|
|
2518
|
+
const id = label.replace(/\s+/g, "_");
|
|
2519
|
+
if (!entities.has(id)) {
|
|
2520
|
+
entities.set(id, { id, label });
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
match = componentRegex1.exec(body);
|
|
2524
|
+
}
|
|
2525
|
+
const relRegex = /(\w+)\s*(?:-->|->|<--|<-|\.\.>|--)\s*(\w+)(?:\s*:\s*(.+))?/g;
|
|
2526
|
+
match = relRegex.exec(body);
|
|
2527
|
+
while (match !== null) {
|
|
2528
|
+
const from = match[1] ?? "";
|
|
2529
|
+
const to = match[2] ?? "";
|
|
2530
|
+
const label = match[3]?.trim();
|
|
2531
|
+
if (from && to) {
|
|
2532
|
+
relationships.push({ from, to, ...label ? { label } : {} });
|
|
2533
|
+
}
|
|
2534
|
+
match = relRegex.exec(body);
|
|
2535
|
+
}
|
|
2536
|
+
return {
|
|
2537
|
+
entities: Array.from(entities.values()),
|
|
2538
|
+
relationships,
|
|
2539
|
+
metadata: { format: "plantuml", diagramType }
|
|
2540
|
+
};
|
|
2541
|
+
}
|
|
2542
|
+
detectDiagramType(content) {
|
|
2543
|
+
if (/class\s+\w+/.test(content)) return "class";
|
|
2544
|
+
if (/\[.+\]/.test(content) || /component\s+/.test(content)) return "component";
|
|
2545
|
+
if (/participant\s+/.test(content) || /actor\s+/.test(content)) return "sequence";
|
|
2546
|
+
return "unknown";
|
|
2547
|
+
}
|
|
2548
|
+
};
|
|
2549
|
+
var DIAGRAM_EXTENSIONS = /* @__PURE__ */ new Set([".mmd", ".mermaid", ".d2", ".puml", ".plantuml"]);
|
|
2550
|
+
var DiagramParser = class {
|
|
2551
|
+
constructor(store) {
|
|
2552
|
+
this.store = store;
|
|
2553
|
+
}
|
|
2554
|
+
store;
|
|
2555
|
+
parsers = [
|
|
2556
|
+
new MermaidParser(),
|
|
2557
|
+
new D2Parser(),
|
|
2558
|
+
new PlantUmlParser()
|
|
2559
|
+
];
|
|
2560
|
+
parse(content, filePath) {
|
|
2561
|
+
const ext = path6.extname(filePath).toLowerCase();
|
|
2562
|
+
for (const parser of this.parsers) {
|
|
2563
|
+
if (parser.canParse(content, ext)) {
|
|
2564
|
+
return parser.parse(content, filePath);
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
return {
|
|
2568
|
+
entities: [],
|
|
2569
|
+
relationships: [],
|
|
2570
|
+
metadata: { format: "mermaid", diagramType: "unknown" }
|
|
2571
|
+
};
|
|
2572
|
+
}
|
|
2573
|
+
async ingest(projectDir) {
|
|
2574
|
+
const start = Date.now();
|
|
2575
|
+
let nodesAdded = 0;
|
|
2576
|
+
let edgesAdded = 0;
|
|
2577
|
+
const errors = [];
|
|
2578
|
+
const diagramFiles = await this.findDiagramFiles(projectDir);
|
|
2579
|
+
for (const filePath of diagramFiles) {
|
|
2580
|
+
try {
|
|
2581
|
+
const content = await fs5.readFile(filePath, "utf-8");
|
|
2582
|
+
const relPath = path6.relative(projectDir, filePath).replaceAll("\\", "/");
|
|
2583
|
+
const result = this.parse(content, filePath);
|
|
2584
|
+
if (result.entities.length === 0) continue;
|
|
2585
|
+
const pathHash = hash(relPath);
|
|
2586
|
+
for (const entity of result.entities) {
|
|
2587
|
+
const nodeId = `diagram:${pathHash}:${entity.id}`;
|
|
2588
|
+
this.store.addNode({
|
|
2589
|
+
id: nodeId,
|
|
2590
|
+
type: "business_concept",
|
|
2591
|
+
name: entity.label,
|
|
2592
|
+
path: relPath,
|
|
2593
|
+
metadata: {
|
|
2594
|
+
source: "diagram",
|
|
2595
|
+
format: result.metadata.format,
|
|
2596
|
+
diagramType: result.metadata.diagramType,
|
|
2597
|
+
confidence: 0.85,
|
|
2598
|
+
...entity.type ? { entityType: entity.type } : {}
|
|
2599
|
+
}
|
|
2600
|
+
});
|
|
2601
|
+
nodesAdded++;
|
|
2602
|
+
}
|
|
2603
|
+
for (const rel of result.relationships) {
|
|
2604
|
+
const fromId = `diagram:${pathHash}:${rel.from}`;
|
|
2605
|
+
const toId = `diagram:${pathHash}:${rel.to}`;
|
|
2606
|
+
this.store.addEdge({
|
|
2607
|
+
from: fromId,
|
|
2608
|
+
to: toId,
|
|
2609
|
+
type: "references",
|
|
2610
|
+
metadata: {
|
|
2611
|
+
...rel.label ? { label: rel.label } : {}
|
|
2612
|
+
}
|
|
2613
|
+
});
|
|
2614
|
+
edgesAdded++;
|
|
2615
|
+
}
|
|
2616
|
+
} catch (err) {
|
|
2617
|
+
errors.push(
|
|
2618
|
+
`Failed to parse ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
2619
|
+
);
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
return {
|
|
2623
|
+
nodesAdded,
|
|
2624
|
+
nodesUpdated: 0,
|
|
2625
|
+
edgesAdded,
|
|
2626
|
+
edgesUpdated: 0,
|
|
2627
|
+
errors,
|
|
2628
|
+
durationMs: Date.now() - start
|
|
2629
|
+
};
|
|
2630
|
+
}
|
|
2631
|
+
async findDiagramFiles(dir) {
|
|
2632
|
+
const files = [];
|
|
2633
|
+
const SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".harness"]);
|
|
2634
|
+
const walk = async (currentDir) => {
|
|
2635
|
+
let entries;
|
|
2636
|
+
try {
|
|
2637
|
+
entries = await fs5.readdir(currentDir, { withFileTypes: true });
|
|
2638
|
+
} catch {
|
|
2639
|
+
return;
|
|
2640
|
+
}
|
|
2641
|
+
for (const entry of entries) {
|
|
2642
|
+
if (entry.isDirectory()) {
|
|
2643
|
+
if (!SKIP_DIRS2.has(entry.name)) {
|
|
2644
|
+
await walk(path6.join(currentDir, entry.name));
|
|
2645
|
+
}
|
|
2646
|
+
} else if (entry.isFile()) {
|
|
2647
|
+
const ext = path6.extname(entry.name).toLowerCase();
|
|
2648
|
+
if (DIAGRAM_EXTENSIONS.has(ext)) {
|
|
2649
|
+
files.push(path6.join(currentDir, entry.name));
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
};
|
|
2654
|
+
await walk(dir);
|
|
2655
|
+
return files;
|
|
2656
|
+
}
|
|
2657
|
+
};
|
|
2658
|
+
|
|
2659
|
+
// src/ingest/KnowledgeLinker.ts
|
|
2660
|
+
import * as fs6 from "fs/promises";
|
|
2661
|
+
import * as path7 from "path";
|
|
2662
|
+
var HEURISTIC_PATTERNS = [
|
|
2663
|
+
{
|
|
2664
|
+
name: "business-rule-imperative",
|
|
2665
|
+
pattern: /\b(?:must|shall|required)\b.*\b(?:system|service|api|user|data|app|application|client|server|process|handler|module)\b/i,
|
|
2666
|
+
signal: "Business rule",
|
|
2667
|
+
confidence: 0.7,
|
|
2668
|
+
nodeType: "business_fact"
|
|
2669
|
+
},
|
|
2670
|
+
{
|
|
2671
|
+
name: "sla-slo-pattern",
|
|
2672
|
+
pattern: /(?:\b\d+(?:\.\d+)?%\s*(?:availability|uptime|success\s*rate)|\bunder\s+\d+\s*(?:ms|seconds?|minutes?|hours?)\b|\b(?:SLA|SLO)\b|\b99(?:\.\d+)?%\b)/i,
|
|
2673
|
+
signal: "Business constraint (SLA/SLO)",
|
|
2674
|
+
confidence: 0.8,
|
|
2675
|
+
nodeType: "business_fact"
|
|
2676
|
+
},
|
|
2677
|
+
{
|
|
2678
|
+
name: "monetary-amount",
|
|
2679
|
+
pattern: /\$[\d,]+(?:\.\d{2})?\s*(?:\b(?:cost|revenue|budget|price|fee|license|subscription|annual|monthly)\b)?/i,
|
|
2680
|
+
signal: "Business fact (monetary)",
|
|
2681
|
+
confidence: 0.6,
|
|
2682
|
+
nodeType: "business_fact"
|
|
2683
|
+
},
|
|
2684
|
+
{
|
|
2685
|
+
name: "acceptance-criteria",
|
|
2686
|
+
pattern: /\b(?:Given\b.*\bWhen\b.*\bThen\b|\[[ x]\])/i,
|
|
2687
|
+
signal: "Business rule (acceptance criteria)",
|
|
2688
|
+
confidence: 0.8,
|
|
2689
|
+
nodeType: "business_fact"
|
|
2690
|
+
},
|
|
2691
|
+
{
|
|
2692
|
+
name: "regulatory-reference",
|
|
2693
|
+
pattern: /\b(?:GDPR|SOC\s*2|PCI(?:\s*-?\s*DSS)?|HIPAA|CCPA|FERPA|SOX)\b/i,
|
|
2694
|
+
signal: "Business rule (regulatory)",
|
|
2695
|
+
confidence: 0.9,
|
|
2696
|
+
nodeType: "business_fact"
|
|
2697
|
+
}
|
|
2698
|
+
];
|
|
2699
|
+
var SCANNABLE_TYPES = ["issue", "conversation", "document"];
|
|
2700
|
+
function applyReactionBoost(candidate, nodeType, metadata) {
|
|
2701
|
+
if (nodeType !== "conversation" || !metadata.reactions) return;
|
|
2702
|
+
const reactions = metadata.reactions;
|
|
2703
|
+
const totalReactions = Object.values(reactions).reduce((sum, count) => sum + count, 0);
|
|
2704
|
+
if (totalReactions > 0) {
|
|
2705
|
+
candidate.confidence = Math.min(1, Math.round((candidate.confidence + 0.1) * 100) / 100);
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
var KnowledgeLinker = class {
|
|
2709
|
+
constructor(store, outputDir) {
|
|
2710
|
+
this.store = store;
|
|
2711
|
+
this.outputDir = outputDir;
|
|
2712
|
+
}
|
|
2713
|
+
store;
|
|
2714
|
+
outputDir;
|
|
2715
|
+
async link() {
|
|
2716
|
+
const errors = [];
|
|
2717
|
+
const candidates = this.scanAllNodes(errors);
|
|
2718
|
+
await this.writeJsonl(candidates);
|
|
2719
|
+
const conceptsClustered = this.clusterBySource(candidates);
|
|
2720
|
+
const promoted = this.promoteAndDeduplicate(candidates);
|
|
2721
|
+
await this.writeStagedJsonl(promoted.staged);
|
|
2722
|
+
return {
|
|
2723
|
+
factsCreated: promoted.factsCreated,
|
|
2724
|
+
conceptsClustered,
|
|
2725
|
+
duplicatesMerged: promoted.duplicatesMerged,
|
|
2726
|
+
stagedForReview: promoted.staged.length,
|
|
2727
|
+
errors
|
|
2728
|
+
};
|
|
2729
|
+
}
|
|
2730
|
+
/** Stage 1: Scan all scannable node types for heuristic pattern matches. */
|
|
2731
|
+
scanAllNodes(errors) {
|
|
2732
|
+
const candidates = [];
|
|
2733
|
+
for (const type of SCANNABLE_TYPES) {
|
|
2734
|
+
const nodes = this.store.findNodes({ type });
|
|
2735
|
+
for (const node of nodes) {
|
|
2736
|
+
if (!node.content) continue;
|
|
2737
|
+
try {
|
|
2738
|
+
const matches = this.scanPatterns(node.content, node.id, type);
|
|
2739
|
+
for (const match of matches) {
|
|
2740
|
+
applyReactionBoost(match, type, node.metadata);
|
|
2741
|
+
candidates.push(match);
|
|
2742
|
+
}
|
|
2743
|
+
} catch (err) {
|
|
2744
|
+
errors.push(
|
|
2745
|
+
`Scan failed for ${node.id}: ${err instanceof Error ? err.message : String(err)}`
|
|
2746
|
+
);
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
return candidates;
|
|
2751
|
+
}
|
|
2752
|
+
/** Stage 2: Group candidates by source node and create business_concept for 3+. */
|
|
2753
|
+
clusterBySource(candidates) {
|
|
2754
|
+
const sourceGroups = /* @__PURE__ */ new Map();
|
|
2755
|
+
for (const candidate of candidates) {
|
|
2756
|
+
const group = sourceGroups.get(candidate.sourceNodeId) ?? [];
|
|
2757
|
+
group.push(candidate);
|
|
2758
|
+
sourceGroups.set(candidate.sourceNodeId, group);
|
|
2759
|
+
}
|
|
2760
|
+
let clustered = 0;
|
|
2761
|
+
for (const [sourceNodeId, group] of sourceGroups) {
|
|
2762
|
+
if (group.length >= 3) {
|
|
2763
|
+
this.store.addNode({
|
|
2764
|
+
id: `concept:linker:${hash(sourceNodeId)}`,
|
|
2765
|
+
type: "business_concept",
|
|
2766
|
+
name: `Business concept cluster from ${sourceNodeId}`,
|
|
2767
|
+
metadata: {
|
|
2768
|
+
source: "knowledge-linker",
|
|
2769
|
+
sources: [sourceNodeId],
|
|
2770
|
+
factCount: group.length
|
|
2771
|
+
}
|
|
2772
|
+
});
|
|
2773
|
+
clustered++;
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
return clustered;
|
|
2777
|
+
}
|
|
2778
|
+
/** Stage 3: Promote high-confidence candidates, stage medium, discard low. */
|
|
2779
|
+
promoteAndDeduplicate(candidates) {
|
|
2780
|
+
let factsCreated = 0;
|
|
2781
|
+
let duplicatesMerged = 0;
|
|
2782
|
+
const staged = [];
|
|
2783
|
+
for (const candidate of candidates) {
|
|
2784
|
+
if (candidate.confidence >= 0.8) {
|
|
2785
|
+
const merged = this.tryMergeDuplicate(candidate);
|
|
2786
|
+
if (merged) {
|
|
2787
|
+
duplicatesMerged++;
|
|
2788
|
+
continue;
|
|
2789
|
+
}
|
|
2790
|
+
this.createFactNode(candidate);
|
|
2791
|
+
factsCreated++;
|
|
2792
|
+
} else if (candidate.confidence >= 0.5) {
|
|
2793
|
+
staged.push(candidate);
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
return { factsCreated, duplicatesMerged, staged };
|
|
2797
|
+
}
|
|
2798
|
+
/** Check for existing duplicate fact and merge sources if found. Returns true if merged. */
|
|
2799
|
+
tryMergeDuplicate(candidate) {
|
|
2800
|
+
if (this.store.getNode(candidate.id)) return true;
|
|
2801
|
+
const existingFacts = this.store.findNodes({ type: "business_fact" });
|
|
2802
|
+
const duplicate = existingFacts.find(
|
|
2803
|
+
(f) => f.content === candidate.content && f.id !== candidate.id
|
|
2804
|
+
);
|
|
2805
|
+
if (!duplicate) return false;
|
|
2806
|
+
const existingSources = duplicate.metadata.sources ?? [];
|
|
2807
|
+
if (!existingSources.includes(candidate.sourceNodeId)) {
|
|
2808
|
+
existingSources.push(candidate.sourceNodeId);
|
|
2809
|
+
duplicate.metadata.sources = existingSources;
|
|
2810
|
+
}
|
|
2811
|
+
return true;
|
|
2812
|
+
}
|
|
2813
|
+
/** Create a business_fact node and governs edge for a promoted candidate. */
|
|
2814
|
+
createFactNode(candidate) {
|
|
2815
|
+
this.store.addNode({
|
|
2816
|
+
id: candidate.id,
|
|
2817
|
+
type: candidate.nodeType,
|
|
2818
|
+
name: candidate.name,
|
|
2819
|
+
content: candidate.content,
|
|
2820
|
+
metadata: {
|
|
2821
|
+
source: "knowledge-linker",
|
|
2822
|
+
pattern: candidate.pattern,
|
|
2823
|
+
signal: candidate.signal,
|
|
2824
|
+
confidence: candidate.confidence,
|
|
2825
|
+
sourceNodeId: candidate.sourceNodeId,
|
|
2826
|
+
sourceNodeType: candidate.sourceNodeType,
|
|
2827
|
+
sources: [candidate.sourceNodeId]
|
|
2828
|
+
}
|
|
2829
|
+
});
|
|
2830
|
+
this.store.addEdge({
|
|
2831
|
+
from: candidate.id,
|
|
2832
|
+
to: candidate.sourceNodeId,
|
|
2833
|
+
type: "governs",
|
|
2834
|
+
confidence: candidate.confidence,
|
|
2835
|
+
metadata: { source: "knowledge-linker" }
|
|
2836
|
+
});
|
|
2837
|
+
}
|
|
2838
|
+
/**
|
|
2839
|
+
* Write candidates to JSONL file for audit trail.
|
|
2840
|
+
*/
|
|
2841
|
+
async writeJsonl(candidates) {
|
|
2842
|
+
if (!this.outputDir) return;
|
|
2843
|
+
await fs6.mkdir(this.outputDir, { recursive: true });
|
|
2844
|
+
const filePath = path7.join(this.outputDir, "linker.jsonl");
|
|
2845
|
+
const lines = candidates.map(
|
|
2846
|
+
(c) => JSON.stringify({
|
|
2847
|
+
id: c.id,
|
|
2848
|
+
sourceNodeId: c.sourceNodeId,
|
|
2849
|
+
sourceNodeType: c.sourceNodeType,
|
|
2850
|
+
name: c.name,
|
|
2851
|
+
confidence: c.confidence,
|
|
2852
|
+
pattern: c.pattern,
|
|
2853
|
+
signal: c.signal,
|
|
2854
|
+
nodeType: c.nodeType
|
|
2855
|
+
})
|
|
2856
|
+
);
|
|
2857
|
+
await fs6.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
|
|
2858
|
+
}
|
|
2859
|
+
/**
|
|
2860
|
+
* Write medium-confidence candidates to staged JSONL for human review.
|
|
2861
|
+
*/
|
|
2862
|
+
async writeStagedJsonl(candidates) {
|
|
2863
|
+
if (!this.outputDir || candidates.length === 0) return;
|
|
2864
|
+
const stagedDir = path7.join(this.outputDir, "staged");
|
|
2865
|
+
await fs6.mkdir(stagedDir, { recursive: true });
|
|
2866
|
+
const filePath = path7.join(stagedDir, "linker-staged.jsonl");
|
|
2867
|
+
const lines = candidates.map(
|
|
2868
|
+
(c) => JSON.stringify({
|
|
2869
|
+
id: c.id,
|
|
2870
|
+
sourceNodeId: c.sourceNodeId,
|
|
2871
|
+
sourceNodeType: c.sourceNodeType,
|
|
2872
|
+
name: c.name,
|
|
2873
|
+
confidence: c.confidence,
|
|
2874
|
+
pattern: c.pattern,
|
|
2875
|
+
signal: c.signal,
|
|
2876
|
+
nodeType: c.nodeType
|
|
2877
|
+
})
|
|
2878
|
+
);
|
|
2879
|
+
await fs6.writeFile(filePath, lines.join("\n") + "\n");
|
|
2880
|
+
}
|
|
2881
|
+
/**
|
|
2882
|
+
* Apply heuristic patterns to content and return candidate extractions.
|
|
2883
|
+
*/
|
|
2884
|
+
scanPatterns(content, nodeId, nodeType) {
|
|
2885
|
+
const candidates = [];
|
|
2886
|
+
for (const heuristic of HEURISTIC_PATTERNS) {
|
|
2887
|
+
if (heuristic.pattern.test(content)) {
|
|
2888
|
+
const candidateId = `extracted:linker:${hash(nodeId + ":" + heuristic.name)}`;
|
|
2889
|
+
candidates.push({
|
|
2890
|
+
id: candidateId,
|
|
2891
|
+
sourceNodeId: nodeId,
|
|
2892
|
+
sourceNodeType: nodeType,
|
|
2893
|
+
name: `${heuristic.signal} from ${nodeId}`,
|
|
2894
|
+
content,
|
|
2895
|
+
confidence: heuristic.confidence,
|
|
2896
|
+
pattern: heuristic.name,
|
|
2897
|
+
signal: heuristic.signal,
|
|
2898
|
+
nodeType: heuristic.nodeType
|
|
2899
|
+
});
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
return candidates;
|
|
2903
|
+
}
|
|
2904
|
+
};
|
|
2905
|
+
|
|
2906
|
+
// src/ingest/StructuralDriftDetector.ts
|
|
2907
|
+
var StructuralDriftDetector = class {
|
|
2908
|
+
detect(current, fresh) {
|
|
2909
|
+
const findings = [];
|
|
2910
|
+
const currentById = new Map(current.entries.map((e) => [e.id, e]));
|
|
2911
|
+
const freshById = new Map(fresh.entries.map((e) => [e.id, e]));
|
|
2912
|
+
for (const [id, entry] of freshById) {
|
|
2913
|
+
if (!currentById.has(id)) {
|
|
2914
|
+
findings.push({
|
|
2915
|
+
entryId: id,
|
|
2916
|
+
classification: "new",
|
|
2917
|
+
fresh: entry,
|
|
2918
|
+
severity: "low"
|
|
2919
|
+
});
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
for (const [id, entry] of currentById) {
|
|
2923
|
+
if (!freshById.has(id)) {
|
|
2924
|
+
findings.push({
|
|
2925
|
+
entryId: id,
|
|
2926
|
+
classification: "stale",
|
|
2927
|
+
current: entry,
|
|
2928
|
+
severity: "high"
|
|
2929
|
+
});
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
for (const [id, freshEntry] of freshById) {
|
|
2933
|
+
const currentEntry = currentById.get(id);
|
|
2934
|
+
if (currentEntry && currentEntry.contentHash !== freshEntry.contentHash) {
|
|
2935
|
+
findings.push({
|
|
2936
|
+
entryId: id,
|
|
2937
|
+
classification: "drifted",
|
|
2938
|
+
current: currentEntry,
|
|
2939
|
+
fresh: freshEntry,
|
|
2940
|
+
severity: "medium"
|
|
2941
|
+
});
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2945
|
+
for (const entry of fresh.entries) {
|
|
2946
|
+
const group = byName.get(entry.name) ?? [];
|
|
2947
|
+
group.push(entry);
|
|
2948
|
+
byName.set(entry.name, group);
|
|
2949
|
+
}
|
|
2950
|
+
for (const [, group] of byName) {
|
|
2951
|
+
if (group.length > 1) {
|
|
2952
|
+
const sources = new Set(group.map((e) => e.source));
|
|
2953
|
+
const hashes = new Set(group.map((e) => e.contentHash));
|
|
2954
|
+
if (sources.size > 1 && hashes.size > 1) {
|
|
2955
|
+
const alreadyContradicting = new Set(
|
|
2956
|
+
findings.filter((f) => f.classification === "contradicting").map((f) => f.entryId)
|
|
2957
|
+
);
|
|
2958
|
+
for (const entry of group) {
|
|
2959
|
+
if (!alreadyContradicting.has(entry.id)) {
|
|
2960
|
+
findings.push({
|
|
2961
|
+
entryId: entry.id,
|
|
2962
|
+
classification: "contradicting",
|
|
2963
|
+
fresh: entry,
|
|
2964
|
+
severity: "critical"
|
|
2965
|
+
});
|
|
2966
|
+
break;
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
const totalEntries = (/* @__PURE__ */ new Set([...currentById.keys(), ...freshById.keys()])).size;
|
|
2973
|
+
const driftScore = totalEntries > 0 ? findings.length / totalEntries : 0;
|
|
2974
|
+
const summary = {
|
|
2975
|
+
new: findings.filter((f) => f.classification === "new").length,
|
|
2976
|
+
drifted: findings.filter((f) => f.classification === "drifted").length,
|
|
2977
|
+
stale: findings.filter((f) => f.classification === "stale").length,
|
|
2978
|
+
contradicting: findings.filter((f) => f.classification === "contradicting").length
|
|
2979
|
+
};
|
|
2980
|
+
return { findings, driftScore, summary };
|
|
2981
|
+
}
|
|
2982
|
+
};
|
|
2983
|
+
|
|
2984
|
+
// src/ingest/KnowledgeStagingAggregator.ts
|
|
2985
|
+
import * as fs7 from "fs/promises";
|
|
2986
|
+
import * as path8 from "path";
|
|
2987
|
+
var KnowledgeStagingAggregator = class {
|
|
2988
|
+
constructor(projectDir) {
|
|
2989
|
+
this.projectDir = projectDir;
|
|
2990
|
+
}
|
|
2991
|
+
projectDir;
|
|
2992
|
+
async aggregate(extractorResults, linkerResults, diagramResults) {
|
|
2993
|
+
const all = [...extractorResults, ...linkerResults, ...diagramResults];
|
|
2994
|
+
const byHash = /* @__PURE__ */ new Map();
|
|
2995
|
+
for (const entry of all) {
|
|
2996
|
+
const existing = byHash.get(entry.contentHash);
|
|
2997
|
+
if (!existing || entry.confidence > existing.confidence) {
|
|
2998
|
+
byHash.set(entry.contentHash, entry);
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
const deduplicated = Array.from(byHash.values());
|
|
3002
|
+
if (deduplicated.length === 0) {
|
|
3003
|
+
return { staged: 0 };
|
|
3004
|
+
}
|
|
3005
|
+
const stagedDir = path8.join(this.projectDir, ".harness", "knowledge", "staged");
|
|
3006
|
+
await fs7.mkdir(stagedDir, { recursive: true });
|
|
3007
|
+
const jsonl = deduplicated.map((entry) => JSON.stringify(entry)).join("\n") + "\n";
|
|
3008
|
+
await fs7.writeFile(path8.join(stagedDir, "pipeline-staged.jsonl"), jsonl, "utf-8");
|
|
3009
|
+
return { staged: deduplicated.length };
|
|
3010
|
+
}
|
|
3011
|
+
async generateGapReport(knowledgeDir) {
|
|
3012
|
+
const domains = [];
|
|
3013
|
+
let totalEntries = 0;
|
|
3014
|
+
try {
|
|
3015
|
+
const entries = await fs7.readdir(knowledgeDir, { withFileTypes: true });
|
|
3016
|
+
const domainDirs = entries.filter((e) => e.isDirectory());
|
|
3017
|
+
for (const dir of domainDirs) {
|
|
3018
|
+
const domainPath = path8.join(knowledgeDir, dir.name);
|
|
3019
|
+
const files = await fs7.readdir(domainPath);
|
|
3020
|
+
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
3021
|
+
const entryCount = mdFiles.length;
|
|
3022
|
+
totalEntries += entryCount;
|
|
3023
|
+
domains.push({ domain: dir.name, entryCount });
|
|
3024
|
+
}
|
|
3025
|
+
} catch {
|
|
3026
|
+
}
|
|
3027
|
+
return {
|
|
3028
|
+
domains,
|
|
3029
|
+
totalEntries,
|
|
3030
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3031
|
+
};
|
|
3032
|
+
}
|
|
3033
|
+
async writeGapReport(report) {
|
|
3034
|
+
const gapsDir = path8.join(this.projectDir, ".harness", "knowledge");
|
|
3035
|
+
await fs7.mkdir(gapsDir, { recursive: true });
|
|
3036
|
+
const lines = [
|
|
3037
|
+
"# Knowledge Gaps Report",
|
|
3038
|
+
"",
|
|
3039
|
+
`Generated: ${report.generatedAt}`,
|
|
3040
|
+
"",
|
|
3041
|
+
"## Coverage by Domain",
|
|
3042
|
+
"",
|
|
3043
|
+
"| Domain | Entries |",
|
|
3044
|
+
"| ------ | ------- |"
|
|
3045
|
+
];
|
|
3046
|
+
for (const domain of report.domains) {
|
|
3047
|
+
lines.push(`| ${domain.domain} | ${domain.entryCount} |`);
|
|
3048
|
+
}
|
|
3049
|
+
lines.push("", `## Total Entries: ${report.totalEntries}`, "");
|
|
3050
|
+
await fs7.writeFile(path8.join(gapsDir, "gaps.md"), lines.join("\n"), "utf-8");
|
|
3051
|
+
}
|
|
3052
|
+
};
|
|
3053
|
+
|
|
3054
|
+
// src/ingest/extractors/ExtractionRunner.ts
|
|
3055
|
+
import * as fs8 from "fs/promises";
|
|
3056
|
+
import * as path9 from "path";
|
|
3057
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
3058
|
+
"node_modules",
|
|
3059
|
+
"dist",
|
|
3060
|
+
"target",
|
|
3061
|
+
"build",
|
|
3062
|
+
".git",
|
|
3063
|
+
"__pycache__",
|
|
3064
|
+
".venv",
|
|
3065
|
+
".turbo",
|
|
3066
|
+
".harness",
|
|
3067
|
+
".next",
|
|
3068
|
+
".vscode",
|
|
3069
|
+
".idea",
|
|
3070
|
+
"vendor",
|
|
3071
|
+
"out",
|
|
3072
|
+
".gradle",
|
|
3073
|
+
".gradle-home",
|
|
3074
|
+
"bin",
|
|
3075
|
+
"obj",
|
|
3076
|
+
"venv",
|
|
3077
|
+
"_build",
|
|
3078
|
+
"deps",
|
|
3079
|
+
"coverage",
|
|
3080
|
+
".nyc_output"
|
|
3081
|
+
]);
|
|
3082
|
+
var EXT_TO_LANGUAGE = {
|
|
3083
|
+
".ts": "typescript",
|
|
3084
|
+
".tsx": "typescript",
|
|
3085
|
+
".js": "javascript",
|
|
3086
|
+
".jsx": "javascript",
|
|
3087
|
+
".py": "python",
|
|
3088
|
+
".go": "go",
|
|
3089
|
+
".rs": "rust",
|
|
3090
|
+
".java": "java"
|
|
3091
|
+
};
|
|
3092
|
+
var SKIP_EXTENSIONS2 = /* @__PURE__ */ new Set([".d.ts"]);
|
|
3093
|
+
function detectLanguage(filePath) {
|
|
3094
|
+
const name = path9.basename(filePath);
|
|
3095
|
+
for (const skip of SKIP_EXTENSIONS2) {
|
|
3096
|
+
if (name.endsWith(skip)) return void 0;
|
|
3097
|
+
}
|
|
3098
|
+
const ext = path9.extname(filePath);
|
|
3099
|
+
return EXT_TO_LANGUAGE[ext];
|
|
3100
|
+
}
|
|
3101
|
+
var EXTRACTOR_EDGE_TYPE = {
|
|
3102
|
+
"test-descriptions": "governs",
|
|
3103
|
+
"enum-constants": "documents",
|
|
3104
|
+
"validation-rules": "governs",
|
|
3105
|
+
"api-paths": "documents"
|
|
3106
|
+
};
|
|
3107
|
+
var ExtractionRunner = class {
|
|
3108
|
+
constructor(extractors) {
|
|
3109
|
+
this.extractors = extractors;
|
|
3110
|
+
}
|
|
3111
|
+
extractors;
|
|
3112
|
+
/**
|
|
3113
|
+
* Run all extractors against a project directory.
|
|
3114
|
+
* @param projectDir - Project root directory
|
|
3115
|
+
* @param store - GraphStore for node/edge persistence
|
|
3116
|
+
* @param outputDir - Directory for JSONL output (e.g. .harness/knowledge/extracted/)
|
|
3117
|
+
*/
|
|
3118
|
+
async run(projectDir, store, outputDir) {
|
|
3119
|
+
const start = Date.now();
|
|
3120
|
+
const errors = [];
|
|
3121
|
+
let nodesAdded = 0;
|
|
3122
|
+
let nodesUpdated = 0;
|
|
3123
|
+
let edgesAdded = 0;
|
|
3124
|
+
await fs8.mkdir(outputDir, { recursive: true });
|
|
3125
|
+
const files = await this.findSourceFiles(projectDir);
|
|
3126
|
+
const recordsByExtractor = /* @__PURE__ */ new Map();
|
|
3127
|
+
for (const ext of this.extractors) {
|
|
3128
|
+
recordsByExtractor.set(ext.name, []);
|
|
3129
|
+
}
|
|
3130
|
+
for (const filePath of files) {
|
|
3131
|
+
const language = detectLanguage(filePath);
|
|
3132
|
+
if (!language) continue;
|
|
3133
|
+
const ext = path9.extname(filePath);
|
|
3134
|
+
let content;
|
|
3135
|
+
try {
|
|
3136
|
+
content = await fs8.readFile(filePath, "utf-8");
|
|
3137
|
+
} catch (err) {
|
|
3138
|
+
errors.push(
|
|
3139
|
+
`Failed to read ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
3140
|
+
);
|
|
3141
|
+
continue;
|
|
3142
|
+
}
|
|
3143
|
+
const relativePath = path9.relative(projectDir, filePath).replaceAll("\\", "/");
|
|
3144
|
+
for (const extractor2 of this.extractors) {
|
|
3145
|
+
if (!extractor2.supportedExtensions.includes(ext)) continue;
|
|
3146
|
+
try {
|
|
3147
|
+
const records = extractor2.extract(content, relativePath, language);
|
|
3148
|
+
recordsByExtractor.get(extractor2.name).push(...records);
|
|
3149
|
+
} catch (err) {
|
|
3150
|
+
errors.push(
|
|
3151
|
+
`Extractor ${extractor2.name} failed on ${relativePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
3152
|
+
);
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
const allCurrentIds = /* @__PURE__ */ new Set();
|
|
3157
|
+
for (const [extractorName, records] of recordsByExtractor) {
|
|
3158
|
+
await this.writeJsonl(records, outputDir, extractorName);
|
|
3159
|
+
for (const record of records) {
|
|
3160
|
+
allCurrentIds.add(record.id);
|
|
3161
|
+
const result = this.persistRecord(record, store);
|
|
3162
|
+
nodesAdded += result.nodesAdded;
|
|
3163
|
+
nodesUpdated += result.nodesUpdated;
|
|
3164
|
+
edgesAdded += result.edgesAdded;
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
const staleCount = this.markStale(store, allCurrentIds);
|
|
3168
|
+
nodesUpdated += staleCount;
|
|
3169
|
+
return {
|
|
3170
|
+
nodesAdded,
|
|
3171
|
+
nodesUpdated,
|
|
3172
|
+
edgesAdded,
|
|
3173
|
+
edgesUpdated: 0,
|
|
3174
|
+
errors,
|
|
3175
|
+
durationMs: Date.now() - start
|
|
3176
|
+
};
|
|
3177
|
+
}
|
|
3178
|
+
/** Write extraction records to JSONL file. */
|
|
3179
|
+
async writeJsonl(records, outputDir, extractorName) {
|
|
3180
|
+
const filePath = path9.join(outputDir, `${extractorName}.jsonl`);
|
|
3181
|
+
const lines = records.map((r) => JSON.stringify(r));
|
|
3182
|
+
await fs8.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
|
|
3183
|
+
}
|
|
3184
|
+
/** Create or update a graph node from an extraction record. */
|
|
3185
|
+
persistRecord(record, store) {
|
|
3186
|
+
const existing = store.getNode(record.id);
|
|
3187
|
+
const node = {
|
|
3188
|
+
id: record.id,
|
|
3189
|
+
type: record.nodeType,
|
|
3190
|
+
name: record.name,
|
|
3191
|
+
path: record.filePath,
|
|
3192
|
+
location: {
|
|
3193
|
+
fileId: `file:${hash(record.filePath)}`,
|
|
3194
|
+
startLine: record.line,
|
|
3195
|
+
endLine: record.line
|
|
3196
|
+
},
|
|
3197
|
+
content: record.content,
|
|
3198
|
+
metadata: {
|
|
3199
|
+
...record.metadata,
|
|
3200
|
+
source: "code-extractor",
|
|
3201
|
+
extractor: record.extractor,
|
|
3202
|
+
confidence: record.confidence,
|
|
3203
|
+
language: record.language,
|
|
3204
|
+
stale: false
|
|
3205
|
+
}
|
|
3206
|
+
};
|
|
3207
|
+
store.addNode(node);
|
|
3208
|
+
const fileNodeId = `file:${hash(record.filePath)}`;
|
|
3209
|
+
const edgeType = EXTRACTOR_EDGE_TYPE[record.extractor] ?? "documents";
|
|
3210
|
+
const edge = {
|
|
3211
|
+
from: record.id,
|
|
3212
|
+
to: fileNodeId,
|
|
3213
|
+
type: edgeType,
|
|
3214
|
+
confidence: record.confidence,
|
|
3215
|
+
metadata: { source: "code-extractor" }
|
|
3216
|
+
};
|
|
3217
|
+
store.addEdge(edge);
|
|
3218
|
+
return {
|
|
3219
|
+
nodesAdded: existing ? 0 : 1,
|
|
3220
|
+
nodesUpdated: existing ? 1 : 0,
|
|
3221
|
+
edgesAdded: existing ? 0 : 1
|
|
3222
|
+
};
|
|
3223
|
+
}
|
|
3224
|
+
/**
|
|
3225
|
+
* Mark nodes from previous extractions that are no longer present as stale.
|
|
3226
|
+
* Returns the number of nodes marked stale.
|
|
3227
|
+
*/
|
|
3228
|
+
markStale(store, currentIds) {
|
|
3229
|
+
let count = 0;
|
|
3230
|
+
const businessTypes = ["business_rule", "business_process", "business_term"];
|
|
3231
|
+
for (const type of businessTypes) {
|
|
3232
|
+
const nodes = store.findNodes({ type });
|
|
3233
|
+
for (const node of nodes) {
|
|
3234
|
+
if (node.metadata.source === "code-extractor" && !node.metadata.stale && !currentIds.has(node.id)) {
|
|
3235
|
+
store.addNode({
|
|
3236
|
+
...node,
|
|
3237
|
+
metadata: {
|
|
3238
|
+
...node.metadata,
|
|
3239
|
+
stale: true,
|
|
3240
|
+
staleAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3241
|
+
}
|
|
3242
|
+
});
|
|
3243
|
+
count++;
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
return count;
|
|
3248
|
+
}
|
|
3249
|
+
/** Recursively find source files, skipping common non-source directories. */
|
|
3250
|
+
async findSourceFiles(dir) {
|
|
3251
|
+
const results = [];
|
|
3252
|
+
let entries;
|
|
3253
|
+
try {
|
|
3254
|
+
entries = await fs8.readdir(dir, { withFileTypes: true });
|
|
3255
|
+
} catch {
|
|
3256
|
+
return results;
|
|
3257
|
+
}
|
|
3258
|
+
for (const entry of entries) {
|
|
3259
|
+
const fullPath = path9.join(dir, entry.name);
|
|
3260
|
+
if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
|
|
3261
|
+
results.push(...await this.findSourceFiles(fullPath));
|
|
3262
|
+
} else if (entry.isFile() && detectLanguage(fullPath) !== void 0) {
|
|
3263
|
+
results.push(fullPath);
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
return results;
|
|
3267
|
+
}
|
|
3268
|
+
};
|
|
3269
|
+
|
|
3270
|
+
// src/ingest/extractors/TestDescriptionExtractor.ts
|
|
3271
|
+
var PATTERNS = {
|
|
3272
|
+
typescript: [/(?:describe|it|test)\s*\(\s*(['"`])((?:(?!\1).)*)\1/g],
|
|
3273
|
+
javascript: [/(?:describe|it|test)\s*\(\s*(['"`])((?:(?!\1).)*)\1/g],
|
|
3274
|
+
python: [/def\s+(test_\w+)\s*\(/g, /"""((?:(?!""").)*?)"""/gs, /class\s+(Test\w+)/g],
|
|
3275
|
+
go: [/func\s+(Test\w+)\s*\(/g, /t\.Run\(\s*"([^"]+)"/g],
|
|
3276
|
+
rust: [/#\[test\]\s*\n\s*(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/g, /\/\/\/\s*(.+)/g],
|
|
3277
|
+
java: [
|
|
3278
|
+
/@DisplayName\s*\(\s*"([^"]+)"\s*\)/g,
|
|
3279
|
+
/@Test\s*\n\s*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:void|[\w<>]+)\s+(\w+)\s*\(/g
|
|
3280
|
+
]
|
|
3281
|
+
};
|
|
3282
|
+
var TestDescriptionExtractor = class {
|
|
3283
|
+
name = "test-descriptions";
|
|
3284
|
+
supportedExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
|
|
3285
|
+
extract(content, filePath, language) {
|
|
3286
|
+
const records = [];
|
|
3287
|
+
const patterns = PATTERNS[language];
|
|
3288
|
+
if (!patterns) return records;
|
|
3289
|
+
const lines = content.split("\n");
|
|
3290
|
+
if (language === "typescript" || language === "javascript") {
|
|
3291
|
+
return this.extractJsTs(content, filePath, language, lines);
|
|
3292
|
+
}
|
|
3293
|
+
if (language === "python") {
|
|
3294
|
+
return this.extractPython(content, filePath, language, lines);
|
|
3295
|
+
}
|
|
3296
|
+
if (language === "go") {
|
|
3297
|
+
return this.extractGo(content, filePath, language, lines);
|
|
3298
|
+
}
|
|
3299
|
+
if (language === "rust") {
|
|
3300
|
+
return this.extractRust(content, filePath, language, lines);
|
|
3301
|
+
}
|
|
3302
|
+
if (language === "java") {
|
|
3303
|
+
return this.extractJava(content, filePath, language, lines);
|
|
3304
|
+
}
|
|
3305
|
+
return records;
|
|
3306
|
+
}
|
|
3307
|
+
extractJsTs(_content, filePath, language, lines) {
|
|
3308
|
+
const records = [];
|
|
3309
|
+
const describeStack = [];
|
|
3310
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3311
|
+
const line = lines[i];
|
|
3312
|
+
const describeMatch = line.match(/describe\s*\(\s*(['"`])((?:(?!\1).)*)\1/);
|
|
3313
|
+
if (describeMatch) {
|
|
3314
|
+
describeStack.push(describeMatch[2]);
|
|
3315
|
+
}
|
|
3316
|
+
const itMatch = line.match(/(?:it|test)\s*\(\s*(['"`])((?:(?!\1).)*)\1/);
|
|
3317
|
+
if (itMatch) {
|
|
3318
|
+
const testName = itMatch[2];
|
|
3319
|
+
const fullPath = [...describeStack, testName].join(" > ");
|
|
3320
|
+
const patternKey = fullPath;
|
|
3321
|
+
records.push({
|
|
3322
|
+
id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
|
|
3323
|
+
extractor: "test-descriptions",
|
|
3324
|
+
language,
|
|
3325
|
+
filePath,
|
|
3326
|
+
line: i + 1,
|
|
3327
|
+
nodeType: "business_rule",
|
|
3328
|
+
name: testName,
|
|
3329
|
+
content: fullPath,
|
|
3330
|
+
confidence: 0.7,
|
|
3331
|
+
metadata: {
|
|
3332
|
+
suite: describeStack.length > 0 ? describeStack[describeStack.length - 1] : void 0,
|
|
3333
|
+
framework: "vitest"
|
|
3334
|
+
}
|
|
3335
|
+
});
|
|
3336
|
+
}
|
|
3337
|
+
if (line.match(/^\s*\}\s*\)\s*;?\s*$/) && describeStack.length > 0) {
|
|
3338
|
+
describeStack.pop();
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
return records;
|
|
3342
|
+
}
|
|
3343
|
+
extractPython(_content, filePath, language, lines) {
|
|
3344
|
+
const records = [];
|
|
3345
|
+
let currentClass;
|
|
3346
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3347
|
+
const line = lines[i];
|
|
3348
|
+
const classMatch = line.match(/^class\s+(Test\w+)/);
|
|
3349
|
+
if (classMatch) {
|
|
3350
|
+
currentClass = classMatch[1];
|
|
3351
|
+
}
|
|
3352
|
+
const funcMatch = line.match(/^\s*def\s+(test_\w+)\s*\(/);
|
|
3353
|
+
if (funcMatch) {
|
|
3354
|
+
const testName = funcMatch[1];
|
|
3355
|
+
const humanName = testName.replace(/^test_/, "").replace(/_/g, " ");
|
|
3356
|
+
let docstring;
|
|
3357
|
+
if (i + 1 < lines.length) {
|
|
3358
|
+
const nextLine = lines[i + 1].trim();
|
|
3359
|
+
const docMatch = nextLine.match(/^"""(.+?)"""/);
|
|
3360
|
+
if (docMatch) {
|
|
3361
|
+
docstring = docMatch[1];
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
const patternKey = currentClass ? `${currentClass}.${testName}` : testName;
|
|
3365
|
+
records.push({
|
|
3366
|
+
id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
|
|
3367
|
+
extractor: "test-descriptions",
|
|
3368
|
+
language,
|
|
3369
|
+
filePath,
|
|
3370
|
+
line: i + 1,
|
|
3371
|
+
nodeType: "business_rule",
|
|
3372
|
+
name: docstring ?? humanName,
|
|
3373
|
+
content: patternKey,
|
|
3374
|
+
confidence: docstring ? 0.7 : 0.5,
|
|
3375
|
+
metadata: {
|
|
3376
|
+
suite: currentClass,
|
|
3377
|
+
framework: "pytest",
|
|
3378
|
+
functionName: testName
|
|
3379
|
+
}
|
|
3380
|
+
});
|
|
3381
|
+
}
|
|
3382
|
+
if (currentClass && /^\S/.test(line) && !line.startsWith("class ") && !line.startsWith("#") && line.trim() !== "") {
|
|
3383
|
+
currentClass = void 0;
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
return records;
|
|
3387
|
+
}
|
|
3388
|
+
extractGo(_content, filePath, language, lines) {
|
|
3389
|
+
const records = [];
|
|
3390
|
+
let currentTest;
|
|
3391
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3392
|
+
const line = lines[i];
|
|
3393
|
+
const funcMatch = line.match(/^func\s+(Test\w+)\s*\(/);
|
|
3394
|
+
if (funcMatch) {
|
|
3395
|
+
currentTest = funcMatch[1];
|
|
3396
|
+
const humanName = currentTest.replace(/^Test/, "").replace(/([A-Z])/g, " $1").trim();
|
|
3397
|
+
records.push({
|
|
3398
|
+
id: `extracted:test-descriptions:${hash(filePath + ":" + currentTest)}`,
|
|
3399
|
+
extractor: "test-descriptions",
|
|
3400
|
+
language,
|
|
3401
|
+
filePath,
|
|
3402
|
+
line: i + 1,
|
|
3403
|
+
nodeType: "business_rule",
|
|
3404
|
+
name: humanName,
|
|
3405
|
+
content: currentTest,
|
|
3406
|
+
confidence: 0.5,
|
|
3407
|
+
metadata: { framework: "testing" }
|
|
3408
|
+
});
|
|
3409
|
+
}
|
|
3410
|
+
const runMatch = line.match(/t\.Run\(\s*"([^"]+)"/);
|
|
3411
|
+
if (runMatch && currentTest) {
|
|
3412
|
+
const subtestName = runMatch[1];
|
|
3413
|
+
const patternKey = `${currentTest} > ${subtestName}`;
|
|
3414
|
+
records.push({
|
|
3415
|
+
id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
|
|
3416
|
+
extractor: "test-descriptions",
|
|
3417
|
+
language,
|
|
3418
|
+
filePath,
|
|
3419
|
+
line: i + 1,
|
|
3420
|
+
nodeType: "business_rule",
|
|
3421
|
+
name: subtestName,
|
|
3422
|
+
content: patternKey,
|
|
3423
|
+
confidence: 0.7,
|
|
3424
|
+
metadata: {
|
|
3425
|
+
suite: currentTest,
|
|
3426
|
+
framework: "testing"
|
|
3427
|
+
}
|
|
3428
|
+
});
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
return records;
|
|
3432
|
+
}
|
|
3433
|
+
extractRust(_content, filePath, language, lines) {
|
|
3434
|
+
const records = [];
|
|
3435
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3436
|
+
const line = lines[i];
|
|
3437
|
+
if (line.trim() === "#[test]") {
|
|
3438
|
+
for (let j = i + 1; j < lines.length && j <= i + 3; j++) {
|
|
3439
|
+
const fnLine = lines[j];
|
|
3440
|
+
const fnMatch = fnLine.match(/(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/);
|
|
3441
|
+
if (fnMatch) {
|
|
3442
|
+
const testName = fnMatch[1];
|
|
3443
|
+
const humanName = testName.replace(/^test_/, "").replace(/_/g, " ");
|
|
3444
|
+
let docComment;
|
|
3445
|
+
if (i > 0) {
|
|
3446
|
+
const prevLine = lines[i - 1].trim();
|
|
3447
|
+
const docMatch = prevLine.match(/^\/\/\/\s*(.+)/);
|
|
3448
|
+
if (docMatch) {
|
|
3449
|
+
docComment = docMatch[1];
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
records.push({
|
|
3453
|
+
id: `extracted:test-descriptions:${hash(filePath + ":" + testName)}`,
|
|
3454
|
+
extractor: "test-descriptions",
|
|
3455
|
+
language,
|
|
3456
|
+
filePath,
|
|
3457
|
+
line: j + 1,
|
|
3458
|
+
nodeType: "business_rule",
|
|
3459
|
+
name: docComment ?? humanName,
|
|
3460
|
+
content: testName,
|
|
3461
|
+
confidence: docComment ? 0.7 : 0.5,
|
|
3462
|
+
metadata: { framework: "rust-test" }
|
|
3463
|
+
});
|
|
3464
|
+
break;
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3469
|
+
return records;
|
|
3470
|
+
}
|
|
3471
|
+
extractJava(_content, filePath, language, lines) {
|
|
3472
|
+
const records = [];
|
|
3473
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3474
|
+
const line = lines[i];
|
|
3475
|
+
const testMatch = line.match(/@Test\s*$/);
|
|
3476
|
+
if (testMatch) {
|
|
3477
|
+
let displayName;
|
|
3478
|
+
for (let k = Math.max(0, i - 3); k < i; k++) {
|
|
3479
|
+
const prevLine = lines[k];
|
|
3480
|
+
const dm = prevLine.match(/@DisplayName\s*\(\s*"([^"]+)"\s*\)/);
|
|
3481
|
+
if (dm) displayName = dm[1];
|
|
3482
|
+
}
|
|
3483
|
+
for (let j = i + 1; j < lines.length && j <= i + 5; j++) {
|
|
3484
|
+
const scanLine = lines[j];
|
|
3485
|
+
const adjacentDisplay = scanLine.match(/@DisplayName\s*\(\s*"([^"]+)"\s*\)/);
|
|
3486
|
+
if (adjacentDisplay) {
|
|
3487
|
+
displayName = adjacentDisplay[1];
|
|
3488
|
+
continue;
|
|
3489
|
+
}
|
|
3490
|
+
const methodMatch = scanLine.match(
|
|
3491
|
+
/^\s*(?:(?:public|private|protected)\s+)?(?:static\s+)?(?:void|[\w<>[\]]+)\s+(\w+)\s*\(/
|
|
3492
|
+
);
|
|
3493
|
+
if (methodMatch) {
|
|
3494
|
+
const methodName = methodMatch[1];
|
|
3495
|
+
const name = displayName ?? methodName;
|
|
3496
|
+
const patternKey = displayName ?? methodName;
|
|
3497
|
+
records.push({
|
|
3498
|
+
id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
|
|
3499
|
+
extractor: "test-descriptions",
|
|
3500
|
+
language,
|
|
3501
|
+
filePath,
|
|
3502
|
+
line: j + 1,
|
|
3503
|
+
nodeType: "business_rule",
|
|
3504
|
+
name,
|
|
3505
|
+
content: patternKey,
|
|
3506
|
+
confidence: displayName ? 0.7 : 0.5,
|
|
3507
|
+
metadata: { framework: "junit5" }
|
|
3508
|
+
});
|
|
3509
|
+
break;
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
return records;
|
|
3515
|
+
}
|
|
3516
|
+
};
|
|
3517
|
+
|
|
3518
|
+
// src/ingest/extractors/EnumConstantExtractor.ts
|
|
3519
|
+
var EnumConstantExtractor = class {
|
|
3520
|
+
name = "enum-constants";
|
|
3521
|
+
supportedExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
|
|
3522
|
+
extract(content, filePath, language) {
|
|
3523
|
+
switch (language) {
|
|
3524
|
+
case "typescript":
|
|
3525
|
+
return this.extractTypeScript(content, filePath, language);
|
|
3526
|
+
case "javascript":
|
|
3527
|
+
return this.extractJavaScript(content, filePath, language);
|
|
3528
|
+
case "python":
|
|
3529
|
+
return this.extractPython(content, filePath, language);
|
|
3530
|
+
case "go":
|
|
3531
|
+
return this.extractGo(content, filePath, language);
|
|
3532
|
+
case "rust":
|
|
3533
|
+
return this.extractRust(content, filePath, language);
|
|
3534
|
+
case "java":
|
|
3535
|
+
return this.extractJava(content, filePath, language);
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
extractTypeScript(content, filePath, language) {
|
|
3539
|
+
const records = [];
|
|
3540
|
+
const lines = content.split("\n");
|
|
3541
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3542
|
+
const line = lines[i];
|
|
3543
|
+
const enumMatch = line.match(/(?:export\s+)?enum\s+(\w+)/);
|
|
3544
|
+
if (enumMatch) {
|
|
3545
|
+
const enumName = enumMatch[1];
|
|
3546
|
+
const members = this.collectEnumMembers(lines, i + 1);
|
|
3547
|
+
records.push({
|
|
3548
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + enumName)}`,
|
|
3549
|
+
extractor: "enum-constants",
|
|
3550
|
+
language,
|
|
3551
|
+
filePath,
|
|
3552
|
+
line: i + 1,
|
|
3553
|
+
nodeType: "business_term",
|
|
3554
|
+
name: enumName,
|
|
3555
|
+
content: `enum ${enumName} { ${members.join(", ")} }`,
|
|
3556
|
+
confidence: 0.8,
|
|
3557
|
+
metadata: { kind: "enum", members }
|
|
3558
|
+
});
|
|
3559
|
+
}
|
|
3560
|
+
const constMatch = line.match(/(?:export\s+)?const\s+(\w+)\s*=\s*\{/);
|
|
3561
|
+
if (constMatch && content.includes("as const")) {
|
|
3562
|
+
const blockEnd = this.findClosingBrace(lines, i);
|
|
3563
|
+
const block = lines.slice(i, blockEnd + 1).join("\n");
|
|
3564
|
+
if (block.includes("as const")) {
|
|
3565
|
+
const constName = constMatch[1];
|
|
3566
|
+
const members = this.collectObjectKeys(lines, i + 1);
|
|
3567
|
+
records.push({
|
|
3568
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + constName)}`,
|
|
3569
|
+
extractor: "enum-constants",
|
|
3570
|
+
language,
|
|
3571
|
+
filePath,
|
|
3572
|
+
line: i + 1,
|
|
3573
|
+
nodeType: "business_term",
|
|
3574
|
+
name: constName,
|
|
3575
|
+
content: `const ${constName} = { ${members.join(", ")} } as const`,
|
|
3576
|
+
confidence: 0.8,
|
|
3577
|
+
metadata: { kind: "as-const", members }
|
|
3578
|
+
});
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
const unionMatch = line.match(
|
|
3582
|
+
/(?:export\s+)?type\s+(\w+)\s*=\s*(['"`][\w]+['"`](?:\s*\|\s*['"`][\w]+['"`])+)/
|
|
3583
|
+
);
|
|
3584
|
+
if (unionMatch) {
|
|
3585
|
+
const typeName = unionMatch[1];
|
|
3586
|
+
const values = unionMatch[2].split("|").map((v) => v.trim().replace(/['"`]/g, ""));
|
|
3587
|
+
records.push({
|
|
3588
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + typeName)}`,
|
|
3589
|
+
extractor: "enum-constants",
|
|
3590
|
+
language,
|
|
3591
|
+
filePath,
|
|
3592
|
+
line: i + 1,
|
|
3593
|
+
nodeType: "business_term",
|
|
3594
|
+
name: typeName,
|
|
3595
|
+
content: `type ${typeName} = ${values.map((v) => `'${v}'`).join(" | ")}`,
|
|
3596
|
+
confidence: 0.7,
|
|
3597
|
+
metadata: { kind: "union-type", members: values }
|
|
3598
|
+
});
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
return records;
|
|
3602
|
+
}
|
|
3603
|
+
extractJavaScript(content, filePath, language) {
|
|
3604
|
+
const records = [];
|
|
3605
|
+
const lines = content.split("\n");
|
|
3606
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3607
|
+
const line = lines[i];
|
|
3608
|
+
const freezeMatch = line.match(/(?:export\s+)?const\s+(\w+)\s*=\s*Object\.freeze\s*\(\s*\{/);
|
|
3609
|
+
if (freezeMatch) {
|
|
3610
|
+
const name = freezeMatch[1];
|
|
3611
|
+
const members = this.collectObjectKeys(lines, i + 1);
|
|
3612
|
+
records.push({
|
|
3613
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + name)}`,
|
|
3614
|
+
extractor: "enum-constants",
|
|
3615
|
+
language,
|
|
3616
|
+
filePath,
|
|
3617
|
+
line: i + 1,
|
|
3618
|
+
nodeType: "business_term",
|
|
3619
|
+
name,
|
|
3620
|
+
content: `const ${name} = Object.freeze({ ${members.join(", ")} })`,
|
|
3621
|
+
confidence: 0.8,
|
|
3622
|
+
metadata: { kind: "frozen-object", members }
|
|
3623
|
+
});
|
|
3624
|
+
}
|
|
3625
|
+
const constMatch = line.match(/(?:export\s+)?const\s+([A-Z][A-Z_\d]*)\s*=\s*\{/);
|
|
3626
|
+
if (constMatch && !freezeMatch) {
|
|
3627
|
+
const name = constMatch[1];
|
|
3628
|
+
const members = this.collectObjectKeys(lines, i + 1);
|
|
3629
|
+
if (members.length > 0 && members.every((m) => /^[A-Z][A-Z_\d]*$/.test(m))) {
|
|
3630
|
+
records.push({
|
|
3631
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + name)}`,
|
|
3632
|
+
extractor: "enum-constants",
|
|
3633
|
+
language,
|
|
3634
|
+
filePath,
|
|
3635
|
+
line: i + 1,
|
|
3636
|
+
nodeType: "business_term",
|
|
3637
|
+
name,
|
|
3638
|
+
content: `const ${name} = { ${members.join(", ")} }`,
|
|
3639
|
+
confidence: 0.6,
|
|
3640
|
+
metadata: { kind: "const-object", members }
|
|
3641
|
+
});
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3645
|
+
return records;
|
|
3646
|
+
}
|
|
3647
|
+
extractPython(content, filePath, language) {
|
|
3648
|
+
const records = [];
|
|
3649
|
+
const lines = content.split("\n");
|
|
3650
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3651
|
+
const line = lines[i];
|
|
3652
|
+
const enumMatch = line.match(
|
|
3653
|
+
/^class\s+(\w+)\s*\(\s*(?:str\s*,\s*)?(?:Enum|StrEnum|IntEnum|Flag|IntFlag)\s*\)/
|
|
3654
|
+
);
|
|
3655
|
+
if (enumMatch) {
|
|
3656
|
+
const enumName = enumMatch[1];
|
|
3657
|
+
const members = this.collectPythonEnumMembers(lines, i + 1);
|
|
3658
|
+
records.push({
|
|
3659
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + enumName)}`,
|
|
3660
|
+
extractor: "enum-constants",
|
|
3661
|
+
language,
|
|
3662
|
+
filePath,
|
|
3663
|
+
line: i + 1,
|
|
3664
|
+
nodeType: "business_term",
|
|
3665
|
+
name: enumName,
|
|
3666
|
+
content: `class ${enumName}(Enum) { ${members.join(", ")} }`,
|
|
3667
|
+
confidence: 0.8,
|
|
3668
|
+
metadata: { kind: "enum", members }
|
|
3669
|
+
});
|
|
3670
|
+
}
|
|
3671
|
+
const literalMatch = line.match(/(\w+)\s*=\s*Literal\s*\[([^\]]+)\]/);
|
|
3672
|
+
if (literalMatch) {
|
|
3673
|
+
const typeName = literalMatch[1];
|
|
3674
|
+
const values = literalMatch[2].split(",").map((v) => v.trim().replace(/["']/g, ""));
|
|
3675
|
+
records.push({
|
|
3676
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + typeName)}`,
|
|
3677
|
+
extractor: "enum-constants",
|
|
3678
|
+
language,
|
|
3679
|
+
filePath,
|
|
3680
|
+
line: i + 1,
|
|
3681
|
+
nodeType: "business_term",
|
|
3682
|
+
name: typeName,
|
|
3683
|
+
content: `${typeName} = Literal[${values.map((v) => `"${v}"`).join(", ")}]`,
|
|
3684
|
+
confidence: 0.7,
|
|
3685
|
+
metadata: { kind: "literal-type", members: values }
|
|
3686
|
+
});
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
return records;
|
|
3690
|
+
}
|
|
3691
|
+
extractGo(content, filePath, language) {
|
|
3692
|
+
const records = [];
|
|
3693
|
+
const lines = content.split("\n");
|
|
3694
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3695
|
+
const line = lines[i];
|
|
3696
|
+
const typeMatch = line.match(/^type\s+(\w+)\s+(?:int|string|uint|int32|int64|uint32|uint64)/);
|
|
3697
|
+
if (typeMatch) {
|
|
3698
|
+
}
|
|
3699
|
+
const constBlockMatch = line.match(/^const\s*\(/);
|
|
3700
|
+
if (constBlockMatch) {
|
|
3701
|
+
const consts = [];
|
|
3702
|
+
let typeName;
|
|
3703
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
3704
|
+
const constLine = lines[j].trim();
|
|
3705
|
+
if (constLine === ")") break;
|
|
3706
|
+
if (constLine === "" || constLine.startsWith("//")) continue;
|
|
3707
|
+
const iotaMatch = constLine.match(/^(\w+)\s+(\w+)\s*=\s*iota/);
|
|
3708
|
+
if (iotaMatch) {
|
|
3709
|
+
typeName = iotaMatch[2];
|
|
3710
|
+
consts.push(iotaMatch[1]);
|
|
3711
|
+
continue;
|
|
3712
|
+
}
|
|
3713
|
+
const typedMatch = constLine.match(/^(\w+)\s+(\w+)\s*=\s*"[^"]*"/);
|
|
3714
|
+
if (typedMatch) {
|
|
3715
|
+
typeName = typeName ?? typedMatch[2];
|
|
3716
|
+
consts.push(typedMatch[1]);
|
|
3717
|
+
continue;
|
|
3718
|
+
}
|
|
3719
|
+
const bareMatch = constLine.match(/^(\w+)\s*$/);
|
|
3720
|
+
if (bareMatch) {
|
|
3721
|
+
consts.push(bareMatch[1]);
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
if (consts.length > 0) {
|
|
3725
|
+
const name = typeName ?? consts[0];
|
|
3726
|
+
records.push({
|
|
3727
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + name)}`,
|
|
3728
|
+
extractor: "enum-constants",
|
|
3729
|
+
language,
|
|
3730
|
+
filePath,
|
|
3731
|
+
line: i + 1,
|
|
3732
|
+
nodeType: "business_term",
|
|
3733
|
+
name,
|
|
3734
|
+
content: `const ( ${consts.join(", ")} )`,
|
|
3735
|
+
confidence: typeName ? 0.8 : 0.6,
|
|
3736
|
+
metadata: { kind: typeName ? "typed-const" : "const-block", members: consts }
|
|
3737
|
+
});
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
return records;
|
|
3742
|
+
}
|
|
3743
|
+
extractRust(content, filePath, language) {
|
|
3744
|
+
const records = [];
|
|
3745
|
+
const lines = content.split("\n");
|
|
3746
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3747
|
+
const line = lines[i];
|
|
3748
|
+
const enumMatch = line.match(/^(?:pub\s+)?enum\s+(\w+)/);
|
|
3749
|
+
if (enumMatch) {
|
|
3750
|
+
const enumName = enumMatch[1];
|
|
3751
|
+
const members = this.collectRustEnumVariants(lines, i + 1);
|
|
3752
|
+
records.push({
|
|
3753
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + enumName)}`,
|
|
3754
|
+
extractor: "enum-constants",
|
|
3755
|
+
language,
|
|
3756
|
+
filePath,
|
|
3757
|
+
line: i + 1,
|
|
3758
|
+
nodeType: "business_term",
|
|
3759
|
+
name: enumName,
|
|
3760
|
+
content: `enum ${enumName} { ${members.join(", ")} }`,
|
|
3761
|
+
confidence: 0.8,
|
|
3762
|
+
metadata: { kind: "enum", members }
|
|
3763
|
+
});
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
return records;
|
|
3767
|
+
}
|
|
3768
|
+
extractJava(content, filePath, language) {
|
|
3769
|
+
const records = [];
|
|
3770
|
+
const lines = content.split("\n");
|
|
3771
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3772
|
+
const line = lines[i];
|
|
3773
|
+
const enumMatch = line.match(/(?:public\s+|private\s+|protected\s+)?enum\s+(\w+)/);
|
|
3774
|
+
if (enumMatch) {
|
|
3775
|
+
const enumName = enumMatch[1];
|
|
3776
|
+
const members = this.collectJavaEnumConstants(lines, i + 1);
|
|
3777
|
+
records.push({
|
|
3778
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + enumName)}`,
|
|
3779
|
+
extractor: "enum-constants",
|
|
3780
|
+
language,
|
|
3781
|
+
filePath,
|
|
3782
|
+
line: i + 1,
|
|
3783
|
+
nodeType: "business_term",
|
|
3784
|
+
name: enumName,
|
|
3785
|
+
content: `enum ${enumName} { ${members.join(", ")} }`,
|
|
3786
|
+
confidence: 0.8,
|
|
3787
|
+
metadata: { kind: "enum", members }
|
|
3788
|
+
});
|
|
3789
|
+
}
|
|
3790
|
+
}
|
|
3791
|
+
return records;
|
|
3792
|
+
}
|
|
3793
|
+
// --- Helper methods ---
|
|
3794
|
+
collectEnumMembers(lines, startLine) {
|
|
3795
|
+
const members = [];
|
|
3796
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3797
|
+
const line = lines[i].trim();
|
|
3798
|
+
if (line === "}") break;
|
|
3799
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
3800
|
+
const match = line.match(/^(\w+)/);
|
|
3801
|
+
if (match) members.push(match[1]);
|
|
3802
|
+
}
|
|
3803
|
+
return members;
|
|
3804
|
+
}
|
|
3805
|
+
collectObjectKeys(lines, startLine) {
|
|
3806
|
+
const keys = [];
|
|
3807
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3808
|
+
const line = lines[i].trim();
|
|
3809
|
+
if (line.startsWith("}")) break;
|
|
3810
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
3811
|
+
const match = line.match(/^(\w+)\s*:/);
|
|
3812
|
+
if (match) keys.push(match[1]);
|
|
3813
|
+
}
|
|
3814
|
+
return keys;
|
|
3815
|
+
}
|
|
3816
|
+
findClosingBrace(lines, startLine) {
|
|
3817
|
+
let depth = 0;
|
|
3818
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3819
|
+
for (const ch of lines[i]) {
|
|
3820
|
+
if (ch === "{") depth++;
|
|
3821
|
+
if (ch === "}") {
|
|
3822
|
+
depth--;
|
|
3823
|
+
if (depth === 0) return i;
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
return lines.length - 1;
|
|
3828
|
+
}
|
|
3829
|
+
collectPythonEnumMembers(lines, startLine) {
|
|
3830
|
+
const members = [];
|
|
3831
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3832
|
+
const line = lines[i];
|
|
3833
|
+
if (/^\S/.test(line) && line.trim() !== "") break;
|
|
3834
|
+
const match = line.match(/^\s+(\w+)\s*=/);
|
|
3835
|
+
if (match) members.push(match[1]);
|
|
3836
|
+
}
|
|
3837
|
+
return members;
|
|
3838
|
+
}
|
|
3839
|
+
collectRustEnumVariants(lines, startLine) {
|
|
3840
|
+
const variants = [];
|
|
3841
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3842
|
+
const line = lines[i].trim();
|
|
3843
|
+
if (line === "}") break;
|
|
3844
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
3845
|
+
const match = line.match(/^(\w+)/);
|
|
3846
|
+
if (match) variants.push(match[1]);
|
|
3847
|
+
}
|
|
3848
|
+
return variants;
|
|
3849
|
+
}
|
|
3850
|
+
collectJavaEnumConstants(lines, startLine) {
|
|
3851
|
+
const constants = [];
|
|
3852
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3853
|
+
const line = lines[i].trim();
|
|
3854
|
+
if (line === "}") break;
|
|
3855
|
+
if (line === "" || line.startsWith("//") || line.startsWith("/*")) continue;
|
|
3856
|
+
if (line.match(/^\s*(?:private|public|protected|static)/)) break;
|
|
3857
|
+
const match = line.match(/^(\w+)[\s(,;]/);
|
|
3858
|
+
if (match) constants.push(match[1]);
|
|
3859
|
+
const singleMatch = line.match(/^(\w+)$/);
|
|
3860
|
+
if (singleMatch) constants.push(singleMatch[1]);
|
|
3861
|
+
}
|
|
3862
|
+
return constants;
|
|
3863
|
+
}
|
|
3864
|
+
};
|
|
3865
|
+
|
|
3866
|
+
// src/ingest/extractors/ValidationRuleExtractor.ts
|
|
3867
|
+
var ValidationRuleExtractor = class {
|
|
3868
|
+
name = "validation-rules";
|
|
3869
|
+
supportedExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
|
|
3870
|
+
extract(content, filePath, language) {
|
|
3871
|
+
switch (language) {
|
|
3872
|
+
case "typescript":
|
|
3873
|
+
case "javascript":
|
|
3874
|
+
return this.extractZod(content, filePath, language);
|
|
3875
|
+
case "python":
|
|
3876
|
+
return this.extractPydantic(content, filePath, language);
|
|
3877
|
+
case "go":
|
|
3878
|
+
return this.extractGoValidate(content, filePath, language);
|
|
3879
|
+
case "rust":
|
|
3880
|
+
return this.extractRustValidate(content, filePath, language);
|
|
3881
|
+
case "java":
|
|
3882
|
+
return this.extractBeanValidation(content, filePath, language);
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
extractZod(content, filePath, language) {
|
|
3886
|
+
const records = [];
|
|
3887
|
+
const lines = content.split("\n");
|
|
3888
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3889
|
+
const line = lines[i];
|
|
3890
|
+
const schemaMatch = line.match(/(?:export\s+)?const\s+(\w+)\s*=\s*z\.object\s*\(\s*\{/);
|
|
3891
|
+
if (schemaMatch) {
|
|
3892
|
+
const schemaName = schemaMatch[1];
|
|
3893
|
+
const constraints = this.collectZodConstraints(lines, i + 1);
|
|
3894
|
+
records.push({
|
|
3895
|
+
id: `extracted:validation-rules:${hash(filePath + ":" + schemaName)}`,
|
|
3896
|
+
extractor: "validation-rules",
|
|
3897
|
+
language,
|
|
3898
|
+
filePath,
|
|
3899
|
+
line: i + 1,
|
|
3900
|
+
nodeType: "business_rule",
|
|
3901
|
+
name: schemaName,
|
|
3902
|
+
content: `${schemaName}: ${constraints.join("; ")}`,
|
|
3903
|
+
confidence: 0.8,
|
|
3904
|
+
metadata: { kind: "zod-schema", constraints, framework: "zod" }
|
|
3905
|
+
});
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
return records;
|
|
3909
|
+
}
|
|
3910
|
+
collectZodConstraints(lines, startLine) {
|
|
3911
|
+
const constraints = [];
|
|
3912
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3913
|
+
const line = lines[i].trim();
|
|
3914
|
+
if (line.startsWith("}") || line.startsWith(")")) break;
|
|
3915
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
3916
|
+
const fieldMatch = line.match(/(\w+)\s*:\s*z\.(.+?)(?:,?\s*$)/);
|
|
3917
|
+
if (fieldMatch) {
|
|
3918
|
+
const fieldName = fieldMatch[1];
|
|
3919
|
+
const chain = fieldMatch[2];
|
|
3920
|
+
constraints.push(`${fieldName}: ${chain}`);
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
return constraints;
|
|
3924
|
+
}
|
|
3925
|
+
extractPydantic(content, filePath, language) {
|
|
3926
|
+
const records = [];
|
|
3927
|
+
const lines = content.split("\n");
|
|
3928
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3929
|
+
const line = lines[i];
|
|
3930
|
+
const modelMatch = line.match(/^class\s+(\w+)\s*\(\s*BaseModel\s*\)/);
|
|
3931
|
+
if (modelMatch) {
|
|
3932
|
+
const modelName = modelMatch[1];
|
|
3933
|
+
const constraints = this.collectPydanticConstraints(lines, i + 1);
|
|
3934
|
+
records.push({
|
|
3935
|
+
id: `extracted:validation-rules:${hash(filePath + ":" + modelName)}`,
|
|
3936
|
+
extractor: "validation-rules",
|
|
3937
|
+
language,
|
|
3938
|
+
filePath,
|
|
3939
|
+
line: i + 1,
|
|
3940
|
+
nodeType: "business_rule",
|
|
3941
|
+
name: modelName,
|
|
3942
|
+
content: `${modelName}: ${constraints.join("; ")}`,
|
|
3943
|
+
confidence: 0.8,
|
|
3944
|
+
metadata: { kind: "pydantic-model", constraints, framework: "pydantic" }
|
|
3945
|
+
});
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
return records;
|
|
3949
|
+
}
|
|
3950
|
+
collectPydanticConstraints(lines, startLine) {
|
|
3951
|
+
const constraints = [];
|
|
3952
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3953
|
+
const line = lines[i];
|
|
3954
|
+
if (/^\S/.test(line) && line.trim() !== "") break;
|
|
3955
|
+
if (line.trim() === "" || line.trim().startsWith("#")) continue;
|
|
3956
|
+
const fieldMatch = line.match(/^\s+(\w+)\s*:\s*(.+?)(?:\s*$)/);
|
|
3957
|
+
if (fieldMatch) {
|
|
3958
|
+
constraints.push(`${fieldMatch[1]}: ${fieldMatch[2]}`);
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
return constraints;
|
|
3962
|
+
}
|
|
3963
|
+
extractGoValidate(content, filePath, language) {
|
|
3964
|
+
const records = [];
|
|
3965
|
+
const lines = content.split("\n");
|
|
3966
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3967
|
+
const line = lines[i];
|
|
3968
|
+
const structMatch = line.match(/^type\s+(\w+)\s+struct\s*\{/);
|
|
3969
|
+
if (structMatch) {
|
|
3970
|
+
const structName = structMatch[1];
|
|
3971
|
+
const constraints = this.collectGoValidateTags(lines, i + 1);
|
|
3972
|
+
if (constraints.length > 0) {
|
|
3973
|
+
records.push({
|
|
3974
|
+
id: `extracted:validation-rules:${hash(filePath + ":" + structName)}`,
|
|
3975
|
+
extractor: "validation-rules",
|
|
3976
|
+
language,
|
|
3977
|
+
filePath,
|
|
3978
|
+
line: i + 1,
|
|
3979
|
+
nodeType: "business_rule",
|
|
3980
|
+
name: structName,
|
|
3981
|
+
content: `${structName}: ${constraints.join("; ")}`,
|
|
3982
|
+
confidence: 0.8,
|
|
3983
|
+
metadata: { kind: "struct-tags", constraints, framework: "go-playground/validator" }
|
|
3984
|
+
});
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3987
|
+
}
|
|
3988
|
+
return records;
|
|
3989
|
+
}
|
|
3990
|
+
collectGoValidateTags(lines, startLine) {
|
|
3991
|
+
const constraints = [];
|
|
3992
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3993
|
+
const line = lines[i].trim();
|
|
3994
|
+
if (line === "}") break;
|
|
3995
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
3996
|
+
const tagMatch = line.match(/(\w+)\s+\S+\s+`[^`]*validate:"([^"]+)"[^`]*`/);
|
|
3997
|
+
if (tagMatch) {
|
|
3998
|
+
constraints.push(`${tagMatch[1]}: ${tagMatch[2]}`);
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
return constraints;
|
|
4002
|
+
}
|
|
4003
|
+
extractRustValidate(content, filePath, language) {
|
|
4004
|
+
const records = [];
|
|
4005
|
+
const lines = content.split("\n");
|
|
4006
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4007
|
+
const line = lines[i];
|
|
4008
|
+
const deriveMatch = line.match(/#\[derive\([^)]*Validate[^)]*\)\]/);
|
|
4009
|
+
if (deriveMatch) {
|
|
4010
|
+
for (let j = i + 1; j < lines.length && j <= i + 3; j++) {
|
|
4011
|
+
const structLine = lines[j];
|
|
4012
|
+
const structMatch = structLine.match(/^(?:pub\s+)?struct\s+(\w+)/);
|
|
4013
|
+
if (structMatch) {
|
|
4014
|
+
const structName = structMatch[1];
|
|
4015
|
+
const constraints = this.collectRustValidateAttrs(lines, j + 1);
|
|
4016
|
+
if (constraints.length > 0) {
|
|
4017
|
+
records.push({
|
|
4018
|
+
id: `extracted:validation-rules:${hash(filePath + ":" + structName)}`,
|
|
4019
|
+
extractor: "validation-rules",
|
|
4020
|
+
language,
|
|
4021
|
+
filePath,
|
|
4022
|
+
line: j + 1,
|
|
4023
|
+
nodeType: "business_rule",
|
|
4024
|
+
name: structName,
|
|
4025
|
+
content: `${structName}: ${constraints.join("; ")}`,
|
|
4026
|
+
confidence: 0.8,
|
|
4027
|
+
metadata: { kind: "validate-derive", constraints, framework: "validator" }
|
|
4028
|
+
});
|
|
4029
|
+
}
|
|
4030
|
+
break;
|
|
4031
|
+
}
|
|
4032
|
+
}
|
|
4033
|
+
}
|
|
4034
|
+
}
|
|
4035
|
+
return records;
|
|
4036
|
+
}
|
|
4037
|
+
collectRustValidateAttrs(lines, startLine) {
|
|
4038
|
+
const constraints = [];
|
|
4039
|
+
let pendingValidate;
|
|
4040
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
4041
|
+
const line = lines[i].trim();
|
|
4042
|
+
if (line === "}") break;
|
|
4043
|
+
const attrMatch = line.match(/#\[validate\((.+?)\)\]/);
|
|
4044
|
+
if (attrMatch) {
|
|
4045
|
+
pendingValidate = attrMatch[1];
|
|
4046
|
+
continue;
|
|
4047
|
+
}
|
|
4048
|
+
if (pendingValidate) {
|
|
4049
|
+
const fieldMatch = line.match(/(?:pub\s+)?(\w+)\s*:/);
|
|
4050
|
+
if (fieldMatch) {
|
|
4051
|
+
constraints.push(`${fieldMatch[1]}: ${pendingValidate}`);
|
|
4052
|
+
pendingValidate = void 0;
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
return constraints;
|
|
4057
|
+
}
|
|
4058
|
+
extractBeanValidation(content, filePath, language) {
|
|
4059
|
+
const records = [];
|
|
4060
|
+
const lines = content.split("\n");
|
|
4061
|
+
const classRanges = this.findJavaClasses(lines);
|
|
4062
|
+
for (const { name: className, startLine, endLine } of classRanges) {
|
|
4063
|
+
const constraints = this.collectBeanConstraints(lines, startLine, endLine);
|
|
4064
|
+
if (constraints.length > 0) {
|
|
4065
|
+
records.push({
|
|
4066
|
+
id: `extracted:validation-rules:${hash(filePath + ":" + className)}`,
|
|
4067
|
+
extractor: "validation-rules",
|
|
4068
|
+
language,
|
|
4069
|
+
filePath,
|
|
4070
|
+
line: startLine + 1,
|
|
4071
|
+
nodeType: "business_rule",
|
|
4072
|
+
name: className,
|
|
4073
|
+
content: `${className}: ${constraints.join("; ")}`,
|
|
4074
|
+
confidence: 0.8,
|
|
4075
|
+
metadata: { kind: "bean-validation", constraints, framework: "javax.validation" }
|
|
4076
|
+
});
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
return records;
|
|
4080
|
+
}
|
|
4081
|
+
findJavaClasses(lines) {
|
|
4082
|
+
const classes = [];
|
|
4083
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4084
|
+
const line = lines[i];
|
|
4085
|
+
const match = line.match(/(?:public\s+|private\s+|protected\s+)?class\s+(\w+)/);
|
|
4086
|
+
if (match) {
|
|
4087
|
+
const endLine = this.findClosingBrace(lines, i);
|
|
4088
|
+
classes.push({ name: match[1], startLine: i, endLine });
|
|
4089
|
+
}
|
|
4090
|
+
}
|
|
4091
|
+
return classes;
|
|
4092
|
+
}
|
|
4093
|
+
findClosingBrace(lines, startLine) {
|
|
4094
|
+
let depth = 0;
|
|
4095
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
4096
|
+
for (const ch of lines[i]) {
|
|
4097
|
+
if (ch === "{") depth++;
|
|
4098
|
+
if (ch === "}") {
|
|
4099
|
+
depth--;
|
|
4100
|
+
if (depth === 0) return i;
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
return lines.length - 1;
|
|
4105
|
+
}
|
|
4106
|
+
collectBeanConstraints(lines, startLine, endLine) {
|
|
4107
|
+
const constraints = [];
|
|
4108
|
+
const validationAnnotations = [
|
|
4109
|
+
"@NotNull",
|
|
4110
|
+
"@NotBlank",
|
|
4111
|
+
"@NotEmpty",
|
|
4112
|
+
"@Size",
|
|
4113
|
+
"@Min",
|
|
4114
|
+
"@Max",
|
|
4115
|
+
"@DecimalMin",
|
|
4116
|
+
"@DecimalMax",
|
|
4117
|
+
"@Email",
|
|
4118
|
+
"@Pattern",
|
|
4119
|
+
"@Positive",
|
|
4120
|
+
"@Negative",
|
|
4121
|
+
"@Past",
|
|
4122
|
+
"@Future"
|
|
4123
|
+
];
|
|
4124
|
+
let pendingAnnotations = [];
|
|
4125
|
+
for (let i = startLine + 1; i < endLine; i++) {
|
|
4126
|
+
const line = lines[i].trim();
|
|
4127
|
+
for (const anno of validationAnnotations) {
|
|
4128
|
+
if (line.startsWith(anno)) {
|
|
4129
|
+
pendingAnnotations.push(line.replace(/;?\s*$/, ""));
|
|
4130
|
+
}
|
|
4131
|
+
}
|
|
4132
|
+
if (pendingAnnotations.length > 0) {
|
|
4133
|
+
const fieldMatch = line.match(
|
|
4134
|
+
/(?:private|public|protected)\s+(?:[\w<>[\]]+)\s+(\w+)\s*[;=]/
|
|
4135
|
+
);
|
|
4136
|
+
if (fieldMatch) {
|
|
4137
|
+
constraints.push(`${fieldMatch[1]}: ${pendingAnnotations.join(", ")}`);
|
|
4138
|
+
pendingAnnotations = [];
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4142
|
+
return constraints;
|
|
4143
|
+
}
|
|
4144
|
+
};
|
|
4145
|
+
|
|
4146
|
+
// src/ingest/extractors/ApiPathExtractor.ts
|
|
4147
|
+
var ApiPathExtractor = class {
|
|
4148
|
+
name = "api-paths";
|
|
4149
|
+
supportedExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
|
|
4150
|
+
extract(content, filePath, language) {
|
|
4151
|
+
switch (language) {
|
|
4152
|
+
case "typescript":
|
|
4153
|
+
case "javascript":
|
|
4154
|
+
return this.extractJsTs(content, filePath, language);
|
|
4155
|
+
case "python":
|
|
4156
|
+
return this.extractPython(content, filePath, language);
|
|
4157
|
+
case "go":
|
|
4158
|
+
return this.extractGo(content, filePath, language);
|
|
4159
|
+
case "rust":
|
|
4160
|
+
return this.extractRust(content, filePath, language);
|
|
4161
|
+
case "java":
|
|
4162
|
+
return this.extractJava(content, filePath, language);
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
4165
|
+
extractJsTs(content, filePath, language) {
|
|
4166
|
+
const records = [];
|
|
4167
|
+
const lines = content.split("\n");
|
|
4168
|
+
const routePattern = /(?:app|router|server|fastify)\.(get|post|put|patch|delete|head|options)\s*\(\s*(['"`])([^'"`]+)\2/i;
|
|
4169
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4170
|
+
const line = lines[i];
|
|
4171
|
+
const match = line.match(routePattern);
|
|
4172
|
+
if (match) {
|
|
4173
|
+
const method = match[1].toUpperCase();
|
|
4174
|
+
const routePath = match[3];
|
|
4175
|
+
const patternKey = `${method} ${routePath}`;
|
|
4176
|
+
records.push({
|
|
4177
|
+
id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
|
|
4178
|
+
extractor: "api-paths",
|
|
4179
|
+
language,
|
|
4180
|
+
filePath,
|
|
4181
|
+
line: i + 1,
|
|
4182
|
+
nodeType: "business_process",
|
|
4183
|
+
name: patternKey,
|
|
4184
|
+
content: `${method} ${routePath}`,
|
|
4185
|
+
confidence: 0.9,
|
|
4186
|
+
metadata: { method, path: routePath, framework: "express" }
|
|
4187
|
+
});
|
|
4188
|
+
}
|
|
4189
|
+
}
|
|
4190
|
+
return records;
|
|
4191
|
+
}
|
|
4192
|
+
extractPython(content, filePath, language) {
|
|
4193
|
+
const records = [];
|
|
4194
|
+
const lines = content.split("\n");
|
|
4195
|
+
const decoratorPattern = /@(?:app|router)\.(get|post|put|patch|delete|head|options)\s*\(\s*["']([^"']+)["']/i;
|
|
4196
|
+
const flaskPattern = /@(?:app|blueprint)\.route\s*\(\s*["']([^"']+)["'](?:\s*,\s*methods\s*=\s*\[([^\]]+)\])?/i;
|
|
4197
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4198
|
+
const line = lines[i];
|
|
4199
|
+
const fastApiMatch = line.match(decoratorPattern);
|
|
4200
|
+
if (fastApiMatch) {
|
|
4201
|
+
const method = fastApiMatch[1].toUpperCase();
|
|
4202
|
+
const routePath = fastApiMatch[2];
|
|
4203
|
+
const patternKey = `${method} ${routePath}`;
|
|
4204
|
+
records.push({
|
|
4205
|
+
id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
|
|
4206
|
+
extractor: "api-paths",
|
|
4207
|
+
language,
|
|
4208
|
+
filePath,
|
|
4209
|
+
line: i + 1,
|
|
4210
|
+
nodeType: "business_process",
|
|
4211
|
+
name: patternKey,
|
|
4212
|
+
content: `${method} ${routePath}`,
|
|
4213
|
+
confidence: 0.9,
|
|
4214
|
+
metadata: { method, path: routePath, framework: "fastapi" }
|
|
4215
|
+
});
|
|
4216
|
+
continue;
|
|
4217
|
+
}
|
|
4218
|
+
const flaskMatch = line.match(flaskPattern);
|
|
4219
|
+
if (flaskMatch) {
|
|
4220
|
+
const routePath = flaskMatch[1];
|
|
4221
|
+
const methods = flaskMatch[2] ? flaskMatch[2].split(",").map((m) => m.trim().replace(/["']/g, "")) : ["GET"];
|
|
4222
|
+
for (const method of methods) {
|
|
4223
|
+
const patternKey = `${method} ${routePath}`;
|
|
4224
|
+
records.push({
|
|
4225
|
+
id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
|
|
4226
|
+
extractor: "api-paths",
|
|
4227
|
+
language,
|
|
4228
|
+
filePath,
|
|
4229
|
+
line: i + 1,
|
|
4230
|
+
nodeType: "business_process",
|
|
4231
|
+
name: patternKey,
|
|
4232
|
+
content: `${method} ${routePath}`,
|
|
4233
|
+
confidence: 0.9,
|
|
4234
|
+
metadata: { method, path: routePath, framework: "flask" }
|
|
4235
|
+
});
|
|
4236
|
+
}
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
return records;
|
|
4240
|
+
}
|
|
4241
|
+
extractGo(content, filePath, language) {
|
|
4242
|
+
const records = [];
|
|
4243
|
+
const lines = content.split("\n");
|
|
4244
|
+
const ginPattern = /(?:\w+)\.(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\(\s*"([^"]+)"/i;
|
|
4245
|
+
const httpPattern = /http\.HandleFunc\s*\(\s*"([^"]+)"/;
|
|
4246
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4247
|
+
const line = lines[i];
|
|
4248
|
+
const ginMatch = line.match(ginPattern);
|
|
4249
|
+
if (ginMatch) {
|
|
4250
|
+
const method = ginMatch[1].toUpperCase();
|
|
4251
|
+
const routePath = ginMatch[2];
|
|
4252
|
+
const patternKey = `${method} ${routePath}`;
|
|
4253
|
+
records.push({
|
|
4254
|
+
id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
|
|
4255
|
+
extractor: "api-paths",
|
|
4256
|
+
language,
|
|
4257
|
+
filePath,
|
|
4258
|
+
line: i + 1,
|
|
4259
|
+
nodeType: "business_process",
|
|
4260
|
+
name: patternKey,
|
|
4261
|
+
content: `${method} ${routePath}`,
|
|
4262
|
+
confidence: 0.9,
|
|
4263
|
+
metadata: { method, path: routePath, framework: "gin" }
|
|
4264
|
+
});
|
|
4265
|
+
continue;
|
|
4266
|
+
}
|
|
4267
|
+
const httpMatch = line.match(httpPattern);
|
|
4268
|
+
if (httpMatch) {
|
|
4269
|
+
const routePath = httpMatch[1];
|
|
4270
|
+
const patternKey = `ANY ${routePath}`;
|
|
4271
|
+
records.push({
|
|
4272
|
+
id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
|
|
4273
|
+
extractor: "api-paths",
|
|
4274
|
+
language,
|
|
4275
|
+
filePath,
|
|
4276
|
+
line: i + 1,
|
|
4277
|
+
nodeType: "business_process",
|
|
4278
|
+
name: patternKey,
|
|
4279
|
+
content: `ANY ${routePath}`,
|
|
4280
|
+
confidence: 0.6,
|
|
4281
|
+
metadata: { method: "ANY", path: routePath, framework: "net/http" }
|
|
4282
|
+
});
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
return records;
|
|
4286
|
+
}
|
|
4287
|
+
extractRust(content, filePath, language) {
|
|
4288
|
+
const records = [];
|
|
4289
|
+
const lines = content.split("\n");
|
|
4290
|
+
const actixPattern = /#\[(get|post|put|patch|delete|head|options)\s*\(\s*"([^"]+)"\s*\)\s*\]/i;
|
|
4291
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4292
|
+
const line = lines[i];
|
|
4293
|
+
const match = line.match(actixPattern);
|
|
4294
|
+
if (match) {
|
|
4295
|
+
const method = match[1].toUpperCase();
|
|
4296
|
+
const routePath = match[2];
|
|
4297
|
+
const patternKey = `${method} ${routePath}`;
|
|
4298
|
+
records.push({
|
|
4299
|
+
id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
|
|
4300
|
+
extractor: "api-paths",
|
|
4301
|
+
language,
|
|
4302
|
+
filePath,
|
|
4303
|
+
line: i + 1,
|
|
4304
|
+
nodeType: "business_process",
|
|
4305
|
+
name: patternKey,
|
|
4306
|
+
content: `${method} ${routePath}`,
|
|
4307
|
+
confidence: 0.9,
|
|
4308
|
+
metadata: { method, path: routePath, framework: "actix" }
|
|
4309
|
+
});
|
|
4310
|
+
}
|
|
4311
|
+
}
|
|
4312
|
+
return records;
|
|
4313
|
+
}
|
|
4314
|
+
extractJava(content, filePath, language) {
|
|
4315
|
+
const records = [];
|
|
4316
|
+
const lines = content.split("\n");
|
|
4317
|
+
const springPattern = /@(GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping|RequestMapping)\s*\(\s*(?:value\s*=\s*)?["']([^"']+)["']/;
|
|
4318
|
+
let basePath = "";
|
|
4319
|
+
for (const line of lines) {
|
|
4320
|
+
const baseMatch = line.match(/@RequestMapping\s*\(\s*(?:value\s*=\s*)?["']([^"']+)["']\s*\)/);
|
|
4321
|
+
if (baseMatch && line.match(/class\s/) === null) {
|
|
4322
|
+
basePath = baseMatch[1];
|
|
4323
|
+
}
|
|
4324
|
+
}
|
|
4325
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4326
|
+
const line = lines[i];
|
|
4327
|
+
const match = line.match(springPattern);
|
|
4328
|
+
if (match) {
|
|
4329
|
+
const annotation = match[1];
|
|
4330
|
+
const routePath = basePath + match[2];
|
|
4331
|
+
const methodMap = {
|
|
4332
|
+
GetMapping: "GET",
|
|
4333
|
+
PostMapping: "POST",
|
|
4334
|
+
PutMapping: "PUT",
|
|
4335
|
+
PatchMapping: "PATCH",
|
|
4336
|
+
DeleteMapping: "DELETE",
|
|
4337
|
+
RequestMapping: "ANY"
|
|
4338
|
+
};
|
|
4339
|
+
const method = methodMap[annotation] ?? "ANY";
|
|
4340
|
+
const patternKey = `${method} ${routePath}`;
|
|
4341
|
+
records.push({
|
|
4342
|
+
id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
|
|
4343
|
+
extractor: "api-paths",
|
|
4344
|
+
language,
|
|
4345
|
+
filePath,
|
|
4346
|
+
line: i + 1,
|
|
4347
|
+
nodeType: "business_process",
|
|
4348
|
+
name: patternKey,
|
|
4349
|
+
content: `${method} ${routePath}`,
|
|
4350
|
+
confidence: 0.9,
|
|
4351
|
+
metadata: { method, path: routePath, framework: "spring" }
|
|
4352
|
+
});
|
|
4353
|
+
}
|
|
4354
|
+
}
|
|
4355
|
+
return records;
|
|
4356
|
+
}
|
|
4357
|
+
};
|
|
4358
|
+
|
|
4359
|
+
// src/ingest/extractors/index.ts
|
|
4360
|
+
var ALL_EXTRACTORS = [
|
|
4361
|
+
new TestDescriptionExtractor(),
|
|
4362
|
+
new EnumConstantExtractor(),
|
|
4363
|
+
new ValidationRuleExtractor(),
|
|
4364
|
+
new ApiPathExtractor()
|
|
4365
|
+
];
|
|
4366
|
+
function createExtractionRunner() {
|
|
4367
|
+
return new ExtractionRunner(ALL_EXTRACTORS);
|
|
4368
|
+
}
|
|
4369
|
+
|
|
4370
|
+
// src/ingest/ImageAnalysisExtractor.ts
|
|
4371
|
+
var ImageAnalysisExtractor = class {
|
|
4372
|
+
provider;
|
|
4373
|
+
/** Maximum file size in bytes. Reserved for future file-size filtering. */
|
|
4374
|
+
maxFileSizeBytes;
|
|
4375
|
+
constructor(options) {
|
|
4376
|
+
this.provider = options.analysisProvider;
|
|
4377
|
+
this.maxFileSizeBytes = (options.maxFileSizeMB ?? 10) * 1024 * 1024;
|
|
4378
|
+
}
|
|
4379
|
+
async analyze(store, imagePaths) {
|
|
4380
|
+
const start = Date.now();
|
|
4381
|
+
let nodesAdded = 0;
|
|
4382
|
+
let edgesAdded = 0;
|
|
4383
|
+
const errors = [];
|
|
4384
|
+
for (const imagePath of imagePaths) {
|
|
4385
|
+
try {
|
|
4386
|
+
const response = await this.provider.analyze({
|
|
4387
|
+
prompt: "Analyze this image and provide a structured description of its visual contents.",
|
|
4388
|
+
systemPrompt: "You are an image analysis assistant. Describe the image contents, detect UI elements, extract visible text, identify design patterns, and note accessibility concerns.",
|
|
4389
|
+
responseSchema: {},
|
|
4390
|
+
// Schema handled by provider
|
|
4391
|
+
maxTokens: 1e3
|
|
4392
|
+
});
|
|
4393
|
+
const result = response.result;
|
|
4394
|
+
const pathHash = hash(imagePath);
|
|
4395
|
+
const annotationId = `img:${pathHash}`;
|
|
4396
|
+
store.addNode({
|
|
4397
|
+
id: annotationId,
|
|
4398
|
+
type: "image_annotation",
|
|
4399
|
+
name: result.description.slice(0, 200),
|
|
4400
|
+
path: imagePath,
|
|
4401
|
+
content: result.description,
|
|
4402
|
+
hash: hash(result.description),
|
|
4403
|
+
metadata: {
|
|
4404
|
+
source: "image-analysis",
|
|
4405
|
+
detectedElements: result.detectedElements,
|
|
4406
|
+
extractedText: result.extractedText,
|
|
4407
|
+
designPatterns: result.designPatterns,
|
|
4408
|
+
accessibilityNotes: result.accessibilityNotes,
|
|
4409
|
+
model: response.model
|
|
4410
|
+
}
|
|
4411
|
+
});
|
|
4412
|
+
nodesAdded++;
|
|
4413
|
+
const fileNodes = store.findNodes({ type: "file" });
|
|
4414
|
+
const fileNode = fileNodes.find((n) => n.path === imagePath);
|
|
4415
|
+
if (fileNode) {
|
|
4416
|
+
store.addEdge({ from: annotationId, to: fileNode.id, type: "annotates" });
|
|
4417
|
+
edgesAdded++;
|
|
4418
|
+
}
|
|
4419
|
+
for (const pattern of result.designPatterns) {
|
|
4420
|
+
const conceptId = `img:concept:${hash(pattern + imagePath)}`;
|
|
4421
|
+
store.addNode({
|
|
4422
|
+
id: conceptId,
|
|
4423
|
+
type: "business_concept",
|
|
4424
|
+
name: pattern,
|
|
4425
|
+
content: `Design pattern detected in ${imagePath}: ${pattern}`,
|
|
4426
|
+
hash: hash(pattern),
|
|
4427
|
+
metadata: {
|
|
4428
|
+
source: "image-analysis",
|
|
4429
|
+
sourceImage: imagePath,
|
|
4430
|
+
domain: "design"
|
|
4431
|
+
}
|
|
4432
|
+
});
|
|
4433
|
+
nodesAdded++;
|
|
4434
|
+
store.addEdge({ from: annotationId, to: conceptId, type: "contains" });
|
|
4435
|
+
edgesAdded++;
|
|
4436
|
+
}
|
|
4437
|
+
} catch (err) {
|
|
4438
|
+
errors.push(
|
|
4439
|
+
`Image analysis failed for ${imagePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
4440
|
+
);
|
|
4441
|
+
}
|
|
4442
|
+
}
|
|
4443
|
+
return {
|
|
4444
|
+
nodesAdded,
|
|
4445
|
+
nodesUpdated: 0,
|
|
4446
|
+
edgesAdded,
|
|
4447
|
+
edgesUpdated: 0,
|
|
4448
|
+
errors,
|
|
4449
|
+
durationMs: Date.now() - start
|
|
4450
|
+
};
|
|
4451
|
+
}
|
|
4452
|
+
};
|
|
4453
|
+
|
|
4454
|
+
// src/ingest/knowledgeTypes.ts
|
|
4455
|
+
var KNOWLEDGE_NODE_TYPES = [
|
|
4456
|
+
"business_fact",
|
|
4457
|
+
"business_rule",
|
|
4458
|
+
"business_process",
|
|
4459
|
+
"business_term",
|
|
4460
|
+
"business_concept",
|
|
4461
|
+
"business_metric",
|
|
4462
|
+
"design_token",
|
|
4463
|
+
"design_constraint",
|
|
4464
|
+
"aesthetic_intent",
|
|
4465
|
+
"image_annotation"
|
|
4466
|
+
];
|
|
4467
|
+
|
|
4468
|
+
// src/ingest/ContradictionDetector.ts
|
|
4469
|
+
var SIMILARITY_THRESHOLD = 0.8;
|
|
4470
|
+
function levenshteinDistance(a, b) {
|
|
4471
|
+
const m = a.length;
|
|
4472
|
+
const n = b.length;
|
|
4473
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
4474
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
4475
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
4476
|
+
for (let i = 1; i <= m; i++) {
|
|
4477
|
+
for (let j = 1; j <= n; j++) {
|
|
4478
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
4479
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
|
|
4480
|
+
}
|
|
4481
|
+
}
|
|
4482
|
+
return dp[m][n];
|
|
4483
|
+
}
|
|
4484
|
+
function levenshteinRatio(a, b) {
|
|
4485
|
+
const maxLen = Math.max(a.length, b.length);
|
|
4486
|
+
if (maxLen === 0) return 1;
|
|
4487
|
+
return 1 - levenshteinDistance(a, b) / maxLen;
|
|
4488
|
+
}
|
|
4489
|
+
function classifyConflict(a, b) {
|
|
4490
|
+
const factTypes = /* @__PURE__ */ new Set([
|
|
4491
|
+
"business_fact",
|
|
4492
|
+
"business_metric",
|
|
4493
|
+
"design_token"
|
|
4494
|
+
]);
|
|
4495
|
+
const termTypes = /* @__PURE__ */ new Set(["business_term", "business_concept"]);
|
|
4496
|
+
if (factTypes.has(a.type) || factTypes.has(b.type)) return "value_mismatch";
|
|
4497
|
+
if (termTypes.has(a.type) || termTypes.has(b.type)) return "definition_conflict";
|
|
4498
|
+
if (a.lastModified && b.lastModified && a.lastModified !== b.lastModified)
|
|
4499
|
+
return "temporal_conflict";
|
|
4500
|
+
return "status_divergence";
|
|
4501
|
+
}
|
|
4502
|
+
var ContradictionDetector = class {
|
|
4503
|
+
detect(store) {
|
|
4504
|
+
const nodes = KNOWLEDGE_NODE_TYPES.flatMap((t) => store.findNodes({ type: t }));
|
|
4505
|
+
const byName = /* @__PURE__ */ new Map();
|
|
4506
|
+
for (const node of nodes) {
|
|
4507
|
+
const key = node.name.toLowerCase().trim();
|
|
4508
|
+
const group = byName.get(key) ?? [];
|
|
4509
|
+
group.push(node);
|
|
4510
|
+
byName.set(key, group);
|
|
4511
|
+
}
|
|
4512
|
+
const contradictions = [];
|
|
4513
|
+
const sourcePairCounts = {};
|
|
4514
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4515
|
+
const addContradiction = (a, b, similarity) => {
|
|
4516
|
+
const sourceA = a.metadata.source ?? "unknown";
|
|
4517
|
+
const sourceB = b.metadata.source ?? "unknown";
|
|
4518
|
+
if (sourceA === sourceB) return;
|
|
4519
|
+
const hashA = a.hash ?? a.id;
|
|
4520
|
+
const hashB = b.hash ?? b.id;
|
|
4521
|
+
if (hashA === hashB) return;
|
|
4522
|
+
const pairId = [a.id, b.id].sort().join(":");
|
|
4523
|
+
if (seen.has(pairId)) return;
|
|
4524
|
+
seen.add(pairId);
|
|
4525
|
+
const conflictType = classifyConflict(a, b);
|
|
4526
|
+
const severity = conflictType === "value_mismatch" ? "critical" : conflictType === "definition_conflict" ? "high" : "medium";
|
|
4527
|
+
const pairKey = [sourceA, sourceB].sort().join("\u2194");
|
|
4528
|
+
sourcePairCounts[pairKey] = (sourcePairCounts[pairKey] ?? 0) + 1;
|
|
4529
|
+
contradictions.push({
|
|
4530
|
+
id: `contradiction:${a.id}:${b.id}`,
|
|
4531
|
+
entityA: {
|
|
4532
|
+
nodeId: a.id,
|
|
4533
|
+
source: sourceA,
|
|
4534
|
+
name: a.name,
|
|
4535
|
+
content: a.content ?? "",
|
|
4536
|
+
lastModified: a.lastModified
|
|
4537
|
+
},
|
|
4538
|
+
entityB: {
|
|
4539
|
+
nodeId: b.id,
|
|
4540
|
+
source: sourceB,
|
|
4541
|
+
name: b.name,
|
|
4542
|
+
content: b.content ?? "",
|
|
4543
|
+
lastModified: b.lastModified
|
|
4544
|
+
},
|
|
4545
|
+
similarity,
|
|
4546
|
+
conflictType,
|
|
4547
|
+
severity,
|
|
4548
|
+
description: `"${a.name}" has conflicting definitions from ${sourceA} and ${sourceB}`
|
|
4549
|
+
});
|
|
4550
|
+
};
|
|
4551
|
+
for (const [, group] of byName) {
|
|
4552
|
+
if (group.length < 2) continue;
|
|
4553
|
+
for (let i = 0; i < group.length; i++) {
|
|
4554
|
+
for (let j = i + 1; j < group.length; j++) {
|
|
4555
|
+
addContradiction(group[i], group[j], 1);
|
|
4556
|
+
}
|
|
4557
|
+
}
|
|
4558
|
+
}
|
|
4559
|
+
const keys = [...byName.keys()];
|
|
4560
|
+
for (let i = 0; i < keys.length; i++) {
|
|
4561
|
+
for (let j = i + 1; j < keys.length; j++) {
|
|
4562
|
+
const keyA = keys[i];
|
|
4563
|
+
const keyB = keys[j];
|
|
4564
|
+
const maxLen = Math.max(keyA.length, keyB.length);
|
|
4565
|
+
const minLen = Math.min(keyA.length, keyB.length);
|
|
4566
|
+
if (minLen / maxLen < SIMILARITY_THRESHOLD) continue;
|
|
4567
|
+
const ratio = levenshteinRatio(keyA, keyB);
|
|
4568
|
+
if (ratio < SIMILARITY_THRESHOLD) continue;
|
|
4569
|
+
const groupA = byName.get(keyA);
|
|
4570
|
+
const groupB = byName.get(keyB);
|
|
4571
|
+
for (const a of groupA) {
|
|
4572
|
+
for (const b of groupB) {
|
|
4573
|
+
addContradiction(a, b, ratio);
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
return { contradictions, sourcePairCounts, totalChecked: nodes.length };
|
|
4579
|
+
}
|
|
4580
|
+
};
|
|
4581
|
+
|
|
4582
|
+
// src/ingest/CoverageScorer.ts
|
|
4583
|
+
var KNOWLEDGE_TYPES = KNOWLEDGE_NODE_TYPES;
|
|
4584
|
+
var CODE_TYPES2 = [
|
|
4585
|
+
"file",
|
|
4586
|
+
"function",
|
|
4587
|
+
"class",
|
|
4588
|
+
"method",
|
|
4589
|
+
"interface",
|
|
4590
|
+
"variable"
|
|
4591
|
+
];
|
|
4592
|
+
var KNOWLEDGE_EDGE_TYPES = [
|
|
4593
|
+
"governs",
|
|
4594
|
+
"documents",
|
|
4595
|
+
"measures",
|
|
4596
|
+
"applies_to",
|
|
4597
|
+
"references",
|
|
4598
|
+
"uses_token",
|
|
4599
|
+
"declares_intent",
|
|
4600
|
+
"annotates"
|
|
4601
|
+
];
|
|
4602
|
+
function toGrade(score) {
|
|
4603
|
+
if (score >= 80) return "A";
|
|
4604
|
+
if (score >= 60) return "B";
|
|
4605
|
+
if (score >= 40) return "C";
|
|
4606
|
+
if (score >= 20) return "D";
|
|
4607
|
+
return "F";
|
|
4608
|
+
}
|
|
4609
|
+
var CoverageScorer = class {
|
|
4610
|
+
score(store) {
|
|
4611
|
+
const knowledgeNodes = KNOWLEDGE_TYPES.flatMap((t) => store.findNodes({ type: t }));
|
|
4612
|
+
const domainMap = /* @__PURE__ */ new Map();
|
|
4613
|
+
for (const node of knowledgeNodes) {
|
|
4614
|
+
const domain = node.metadata.domain ?? "unclassified";
|
|
4615
|
+
const group = domainMap.get(domain) ?? [];
|
|
4616
|
+
group.push(node);
|
|
4617
|
+
domainMap.set(domain, group);
|
|
4618
|
+
}
|
|
4619
|
+
const codeNodes = CODE_TYPES2.flatMap((t) => store.findNodes({ type: t }));
|
|
4620
|
+
const codeDomains = /* @__PURE__ */ new Map();
|
|
4621
|
+
for (const node of codeNodes) {
|
|
4622
|
+
const domain = node.metadata.domain ?? this.domainFromPath(node.path);
|
|
4623
|
+
const group = codeDomains.get(domain) ?? [];
|
|
4624
|
+
group.push(node);
|
|
4625
|
+
codeDomains.set(domain, group);
|
|
4626
|
+
}
|
|
4627
|
+
const allDomains = /* @__PURE__ */ new Set([...domainMap.keys(), ...codeDomains.keys()]);
|
|
4628
|
+
const domains = [];
|
|
4629
|
+
for (const domain of allDomains) {
|
|
4630
|
+
const knEntries = domainMap.get(domain) ?? [];
|
|
4631
|
+
const codeEntries = codeDomains.get(domain) ?? [];
|
|
4632
|
+
const linkedIds = /* @__PURE__ */ new Set();
|
|
4633
|
+
for (const codeNode of codeEntries) {
|
|
4634
|
+
for (const edgeType of KNOWLEDGE_EDGE_TYPES) {
|
|
4635
|
+
const edges = store.getEdges({ to: codeNode.id, type: edgeType });
|
|
4636
|
+
if (edges.length > 0) {
|
|
4637
|
+
linkedIds.add(codeNode.id);
|
|
4638
|
+
break;
|
|
4639
|
+
}
|
|
4640
|
+
}
|
|
4641
|
+
}
|
|
4642
|
+
const sourceBreakdown = {};
|
|
4643
|
+
for (const kn of knEntries) {
|
|
4644
|
+
const src = kn.metadata.source ?? "unknown";
|
|
4645
|
+
sourceBreakdown[src] = (sourceBreakdown[src] ?? 0) + 1;
|
|
4646
|
+
}
|
|
4647
|
+
const codeEntities = codeEntries.length;
|
|
4648
|
+
const linkedEntities = linkedIds.size;
|
|
4649
|
+
const knowledgeEntries = knEntries.length;
|
|
4650
|
+
const uniqueSources = Object.keys(sourceBreakdown).length;
|
|
4651
|
+
const codeCoverageComponent = codeEntities > 0 ? linkedEntities / codeEntities * 60 : 0;
|
|
4652
|
+
const knowledgeDepthComponent = Math.min(knowledgeEntries / 10, 1) * 20;
|
|
4653
|
+
const sourceDiversityComponent = Math.min(uniqueSources / 3, 1) * 20;
|
|
4654
|
+
const score = Math.round(
|
|
4655
|
+
codeCoverageComponent + knowledgeDepthComponent + sourceDiversityComponent
|
|
4656
|
+
);
|
|
4657
|
+
domains.push({
|
|
4658
|
+
domain,
|
|
4659
|
+
score,
|
|
4660
|
+
knowledgeEntries,
|
|
4661
|
+
codeEntities,
|
|
4662
|
+
linkedEntities,
|
|
4663
|
+
unlinkedEntities: codeEntities - linkedEntities,
|
|
4664
|
+
sourceBreakdown,
|
|
4665
|
+
grade: toGrade(score)
|
|
4666
|
+
});
|
|
4667
|
+
}
|
|
4668
|
+
const overallScore = domains.length > 0 ? Math.round(domains.reduce((sum, d) => sum + d.score, 0) / domains.length) : 0;
|
|
4669
|
+
return {
|
|
4670
|
+
domains,
|
|
4671
|
+
overallScore,
|
|
4672
|
+
overallGrade: toGrade(overallScore),
|
|
4673
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4674
|
+
};
|
|
4675
|
+
}
|
|
4676
|
+
domainFromPath(filePath) {
|
|
4677
|
+
if (!filePath) return "unclassified";
|
|
4678
|
+
const parts = filePath.split("/");
|
|
4679
|
+
const pkgIdx = parts.indexOf("packages");
|
|
4680
|
+
if (pkgIdx >= 0 && parts[pkgIdx + 1]) return parts[pkgIdx + 1];
|
|
4681
|
+
const srcIdx = parts.indexOf("src");
|
|
4682
|
+
if (srcIdx >= 0 && parts[srcIdx + 1]) return parts[srcIdx + 1];
|
|
4683
|
+
return parts[0] ?? "unclassified";
|
|
4684
|
+
}
|
|
4685
|
+
};
|
|
4686
|
+
|
|
4687
|
+
// src/ingest/KnowledgePipelineRunner.ts
|
|
4688
|
+
var BUSINESS_NODE_TYPES = [
|
|
4689
|
+
"business_concept",
|
|
4690
|
+
"business_rule",
|
|
4691
|
+
"business_process",
|
|
4692
|
+
"business_term",
|
|
4693
|
+
"business_metric",
|
|
4694
|
+
"business_fact"
|
|
4695
|
+
];
|
|
4696
|
+
var SNAPSHOT_NODE_TYPES = [
|
|
4697
|
+
...BUSINESS_NODE_TYPES,
|
|
4698
|
+
"design_token",
|
|
4699
|
+
"design_constraint",
|
|
4700
|
+
"aesthetic_intent",
|
|
4701
|
+
"image_annotation"
|
|
4702
|
+
];
|
|
4703
|
+
var KnowledgePipelineRunner = class {
|
|
4704
|
+
constructor(store) {
|
|
4705
|
+
this.store = store;
|
|
4706
|
+
}
|
|
4707
|
+
store;
|
|
4708
|
+
async run(options) {
|
|
4709
|
+
const maxIterations = options.maxIterations ?? 5;
|
|
4710
|
+
const remediations = [];
|
|
4711
|
+
const preSnapshot = this.buildSnapshot(options.domain);
|
|
4712
|
+
const extraction = await this.extract(options);
|
|
4713
|
+
const postSnapshot = this.buildSnapshot(options.domain);
|
|
4714
|
+
let driftResult = this.reconcile(preSnapshot, postSnapshot);
|
|
4715
|
+
const contradictionDetector = new ContradictionDetector();
|
|
4716
|
+
const contradictions = contradictionDetector.detect(this.store);
|
|
4717
|
+
let gapReport = await this.detect(options);
|
|
4718
|
+
const coverageScorer = new CoverageScorer();
|
|
4719
|
+
const coverage = coverageScorer.score(this.store);
|
|
4720
|
+
let iterations = 1;
|
|
4721
|
+
if (options.fix) {
|
|
4722
|
+
let previousFindingCount = driftResult.findings.length;
|
|
4723
|
+
while (iterations < maxIterations) {
|
|
4724
|
+
if (driftResult.findings.length === 0) break;
|
|
4725
|
+
this.remediate(driftResult, remediations, options);
|
|
4726
|
+
const rePreSnapshot = this.buildSnapshot(options.domain);
|
|
4727
|
+
await this.extract(options);
|
|
4728
|
+
const rePostSnapshot = this.buildSnapshot(options.domain);
|
|
4729
|
+
driftResult = this.reconcile(rePreSnapshot, rePostSnapshot);
|
|
4730
|
+
gapReport = await this.detect(options);
|
|
4731
|
+
iterations++;
|
|
4732
|
+
const newFindingCount = driftResult.findings.length;
|
|
4733
|
+
if (newFindingCount >= previousFindingCount) break;
|
|
4734
|
+
previousFindingCount = newFindingCount;
|
|
4735
|
+
}
|
|
4736
|
+
}
|
|
4737
|
+
await this.stageNewFindings(driftResult, options);
|
|
4738
|
+
return {
|
|
4739
|
+
verdict: this.computeVerdict(driftResult),
|
|
4740
|
+
driftScore: driftResult.driftScore,
|
|
4741
|
+
iterations,
|
|
4742
|
+
findings: driftResult.summary,
|
|
4743
|
+
extraction,
|
|
4744
|
+
gaps: gapReport,
|
|
4745
|
+
remediations,
|
|
4746
|
+
contradictions,
|
|
4747
|
+
coverage
|
|
4748
|
+
};
|
|
4749
|
+
}
|
|
4750
|
+
// ── Phase 1: EXTRACT ──────────────────────────────────────────────────────
|
|
4751
|
+
async extract(options) {
|
|
4752
|
+
const extractedDir = path10.join(options.projectDir, ".harness", "knowledge", "extracted");
|
|
4753
|
+
await fs9.mkdir(extractedDir, { recursive: true });
|
|
4754
|
+
const runner = createExtractionRunner();
|
|
4755
|
+
const extractionResult = await runner.run(options.projectDir, this.store, extractedDir);
|
|
4756
|
+
const diagramParser = new DiagramParser(this.store);
|
|
4757
|
+
const diagramResult = await diagramParser.ingest(options.projectDir);
|
|
4758
|
+
let imageCount = 0;
|
|
4759
|
+
const imagePaths = options.imagePaths ?? [];
|
|
4760
|
+
if (options.analyzeImages && options.analysisProvider && imagePaths.length > 0) {
|
|
4761
|
+
const imageExtractor = new ImageAnalysisExtractor({
|
|
4762
|
+
analysisProvider: options.analysisProvider
|
|
4763
|
+
});
|
|
4764
|
+
const imageResult = await imageExtractor.analyze(this.store, imagePaths);
|
|
4765
|
+
imageCount = imageResult.nodesAdded;
|
|
4766
|
+
}
|
|
4767
|
+
const knowledgeDir = path10.join(options.projectDir, "docs", "knowledge");
|
|
4768
|
+
const bkIngestor = new BusinessKnowledgeIngestor(this.store);
|
|
4769
|
+
let bkResult;
|
|
4770
|
+
try {
|
|
4771
|
+
bkResult = await bkIngestor.ingest(knowledgeDir);
|
|
4772
|
+
} catch {
|
|
4773
|
+
bkResult = {
|
|
4774
|
+
nodesAdded: 0,
|
|
4775
|
+
nodesUpdated: 0,
|
|
4776
|
+
edgesAdded: 0,
|
|
4777
|
+
edgesUpdated: 0,
|
|
4778
|
+
errors: [],
|
|
4779
|
+
durationMs: 0
|
|
4780
|
+
};
|
|
4781
|
+
}
|
|
4782
|
+
const linker = new KnowledgeLinker(this.store, extractedDir);
|
|
4783
|
+
const linkResult = await linker.link();
|
|
4784
|
+
return {
|
|
4785
|
+
codeSignals: extractionResult.nodesAdded,
|
|
4786
|
+
diagrams: diagramResult.nodesAdded,
|
|
4787
|
+
linkerFacts: linkResult.factsCreated,
|
|
4788
|
+
businessKnowledge: bkResult.nodesAdded,
|
|
4789
|
+
images: imageCount
|
|
4790
|
+
};
|
|
4791
|
+
}
|
|
4792
|
+
// ── Phase 2: RECONCILE ────────────────────────────────────────────────────
|
|
4793
|
+
buildSnapshot(domain) {
|
|
4794
|
+
let nodes = SNAPSHOT_NODE_TYPES.flatMap((type) => this.store.findNodes({ type }));
|
|
4795
|
+
if (domain) {
|
|
4796
|
+
nodes = nodes.filter((n) => n.metadata?.domain === domain);
|
|
4797
|
+
}
|
|
4798
|
+
return {
|
|
4799
|
+
entries: nodes.map((n) => ({
|
|
4800
|
+
id: n.id,
|
|
4801
|
+
type: n.type,
|
|
4802
|
+
contentHash: n.hash ?? n.id,
|
|
4803
|
+
source: n.metadata?.source ?? "unknown",
|
|
4804
|
+
name: n.name
|
|
4805
|
+
})),
|
|
4806
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4807
|
+
};
|
|
4808
|
+
}
|
|
4809
|
+
reconcile(current, fresh) {
|
|
4810
|
+
const detector = new StructuralDriftDetector();
|
|
4811
|
+
return detector.detect(current, fresh);
|
|
4812
|
+
}
|
|
4813
|
+
// ── Phase 3: DETECT ───────────────────────────────────────────────────────
|
|
4814
|
+
async detect(options) {
|
|
4815
|
+
const knowledgeDir = path10.join(options.projectDir, "docs", "knowledge");
|
|
4816
|
+
const aggregator = new KnowledgeStagingAggregator(options.projectDir);
|
|
4817
|
+
const gapReport = await aggregator.generateGapReport(knowledgeDir);
|
|
4818
|
+
await aggregator.writeGapReport(gapReport);
|
|
4819
|
+
return gapReport;
|
|
4820
|
+
}
|
|
4821
|
+
// ── Phase 4: REMEDIATE ────────────────────────────────────────────────────
|
|
4822
|
+
remediate(driftResult, remediations, options) {
|
|
4823
|
+
for (const finding of driftResult.findings) {
|
|
4824
|
+
switch (finding.classification) {
|
|
4825
|
+
case "stale":
|
|
4826
|
+
this.store.removeNode(finding.entryId);
|
|
4827
|
+
remediations.push(`removed stale: ${finding.entryId}`);
|
|
4828
|
+
break;
|
|
4829
|
+
case "new":
|
|
4830
|
+
break;
|
|
4831
|
+
case "drifted":
|
|
4832
|
+
if (!options.ci) {
|
|
4833
|
+
remediations.push(`flagged drifted: ${finding.entryId}`);
|
|
4834
|
+
}
|
|
4835
|
+
break;
|
|
4836
|
+
case "contradicting":
|
|
4837
|
+
break;
|
|
4838
|
+
}
|
|
4839
|
+
}
|
|
4840
|
+
}
|
|
4841
|
+
async stageNewFindings(driftResult, options) {
|
|
4842
|
+
const newFindings = driftResult.findings.filter((f) => f.classification === "new");
|
|
4843
|
+
if (newFindings.length === 0) return;
|
|
4844
|
+
const stagedEntries = newFindings.filter((f) => f.fresh != null).map((f) => ({
|
|
4845
|
+
id: f.fresh.id,
|
|
4846
|
+
source: this.classifySource(f.fresh.source),
|
|
4847
|
+
nodeType: f.fresh.type,
|
|
4848
|
+
name: f.fresh.name,
|
|
4849
|
+
confidence: 0.7,
|
|
4850
|
+
contentHash: f.fresh.contentHash,
|
|
4851
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4852
|
+
}));
|
|
4853
|
+
if (stagedEntries.length > 0) {
|
|
4854
|
+
const aggregator = new KnowledgeStagingAggregator(options.projectDir);
|
|
4855
|
+
await aggregator.aggregate(stagedEntries, [], []);
|
|
4856
|
+
}
|
|
4857
|
+
}
|
|
4858
|
+
classifySource(source) {
|
|
4859
|
+
if (source === "linker" || source === "knowledge-linker") return "linker";
|
|
4860
|
+
if (source === "diagram") return "diagram";
|
|
4861
|
+
return "extractor";
|
|
4862
|
+
}
|
|
4863
|
+
// ── Verdict ───────────────────────────────────────────────────────────────
|
|
4864
|
+
computeVerdict(driftResult) {
|
|
4865
|
+
const { summary } = driftResult;
|
|
4866
|
+
const unresolved = summary.drifted + summary.stale + summary.contradicting;
|
|
4867
|
+
if (unresolved === 0 && summary.new === 0) return "pass";
|
|
4868
|
+
if (unresolved === 0) return "warn";
|
|
4869
|
+
return "fail";
|
|
1846
4870
|
}
|
|
1847
4871
|
};
|
|
1848
4872
|
|
|
1849
4873
|
// src/ingest/connectors/ConnectorUtils.ts
|
|
1850
|
-
var
|
|
4874
|
+
var CODE_NODE_TYPES4 = ["file", "function", "class", "method", "interface", "variable"];
|
|
4875
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
4876
|
+
function withRetry(client, options) {
|
|
4877
|
+
const maxRetries = options?.maxRetries ?? 3;
|
|
4878
|
+
const baseDelayMs = options?.baseDelayMs ?? 1e3;
|
|
4879
|
+
const maxDelayMs = options?.maxDelayMs ?? 3e4;
|
|
4880
|
+
return async (url, requestOptions) => {
|
|
4881
|
+
let lastError;
|
|
4882
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
4883
|
+
try {
|
|
4884
|
+
const response = await client(url, requestOptions);
|
|
4885
|
+
if (response.ok || !RETRYABLE_STATUSES.has(response.status ?? 0)) {
|
|
4886
|
+
return response;
|
|
4887
|
+
}
|
|
4888
|
+
if (attempt === maxRetries) {
|
|
4889
|
+
return response;
|
|
4890
|
+
}
|
|
4891
|
+
} catch (err) {
|
|
4892
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
4893
|
+
if (attempt === maxRetries) {
|
|
4894
|
+
throw lastError;
|
|
4895
|
+
}
|
|
4896
|
+
}
|
|
4897
|
+
const exponentialDelay = baseDelayMs * 2 ** attempt;
|
|
4898
|
+
const cappedDelay = Math.min(exponentialDelay, maxDelayMs);
|
|
4899
|
+
const jitteredDelay = cappedDelay * (0.5 + Math.random() * 0.5);
|
|
4900
|
+
await new Promise((resolve) => setTimeout(resolve, jitteredDelay));
|
|
4901
|
+
}
|
|
4902
|
+
throw lastError ?? new Error("Request failed after retries");
|
|
4903
|
+
};
|
|
4904
|
+
}
|
|
1851
4905
|
var SANITIZE_RULES = [
|
|
1852
4906
|
// Strip XML/HTML-like instruction tags that could be interpreted as system prompts
|
|
1853
4907
|
{
|
|
@@ -1882,7 +4936,7 @@ function sanitizeExternalText(text, maxLength = 2e3) {
|
|
|
1882
4936
|
}
|
|
1883
4937
|
function linkToCode(store, content, sourceNodeId, edgeType, options) {
|
|
1884
4938
|
let edgesCreated = 0;
|
|
1885
|
-
for (const type of
|
|
4939
|
+
for (const type of CODE_NODE_TYPES4) {
|
|
1886
4940
|
const nodes = store.findNodes({ type });
|
|
1887
4941
|
for (const node of nodes) {
|
|
1888
4942
|
if (node.name.length < 3) continue;
|
|
@@ -1902,12 +4956,12 @@ function linkToCode(store, content, sourceNodeId, edgeType, options) {
|
|
|
1902
4956
|
}
|
|
1903
4957
|
|
|
1904
4958
|
// src/ingest/connectors/SyncManager.ts
|
|
1905
|
-
import * as
|
|
1906
|
-
import * as
|
|
4959
|
+
import * as fs10 from "fs/promises";
|
|
4960
|
+
import * as path11 from "path";
|
|
1907
4961
|
var SyncManager = class {
|
|
1908
4962
|
constructor(store, graphDir) {
|
|
1909
4963
|
this.store = store;
|
|
1910
|
-
this.metadataPath =
|
|
4964
|
+
this.metadataPath = path11.join(graphDir, "sync-metadata.json");
|
|
1911
4965
|
}
|
|
1912
4966
|
store;
|
|
1913
4967
|
registrations = /* @__PURE__ */ new Map();
|
|
@@ -1955,6 +5009,16 @@ var SyncManager = class {
|
|
|
1955
5009
|
combined.errors.push(...result.errors);
|
|
1956
5010
|
combined.durationMs += result.durationMs;
|
|
1957
5011
|
}
|
|
5012
|
+
try {
|
|
5013
|
+
const linker = new KnowledgeLinker(this.store);
|
|
5014
|
+
const linkResult = await linker.link();
|
|
5015
|
+
combined.nodesAdded += linkResult.factsCreated + linkResult.conceptsClustered;
|
|
5016
|
+
combined.errors.push(...linkResult.errors);
|
|
5017
|
+
} catch (err) {
|
|
5018
|
+
combined.errors.push(
|
|
5019
|
+
`KnowledgeLinker error: ${err instanceof Error ? err.message : String(err)}`
|
|
5020
|
+
);
|
|
5021
|
+
}
|
|
1958
5022
|
return combined;
|
|
1959
5023
|
}
|
|
1960
5024
|
async getMetadata() {
|
|
@@ -1962,18 +5026,64 @@ var SyncManager = class {
|
|
|
1962
5026
|
}
|
|
1963
5027
|
async loadMetadata() {
|
|
1964
5028
|
try {
|
|
1965
|
-
const raw = await
|
|
5029
|
+
const raw = await fs10.readFile(this.metadataPath, "utf-8");
|
|
1966
5030
|
return JSON.parse(raw);
|
|
1967
5031
|
} catch {
|
|
1968
5032
|
return { connectors: {} };
|
|
1969
5033
|
}
|
|
1970
5034
|
}
|
|
1971
5035
|
async saveMetadata(metadata) {
|
|
1972
|
-
await
|
|
1973
|
-
await
|
|
5036
|
+
await fs10.mkdir(path11.dirname(this.metadataPath), { recursive: true });
|
|
5037
|
+
await fs10.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
1974
5038
|
}
|
|
1975
5039
|
};
|
|
1976
5040
|
|
|
5041
|
+
// src/ingest/connectors/ContentCondenser.ts
|
|
5042
|
+
var SUMMARIZE_PROMPT = `Summarize the following content to fit within the specified length.
|
|
5043
|
+
Preserve all business rules, SLAs, requirements, decisions, and regulatory references.
|
|
5044
|
+
Remove redundant details and boilerplate while keeping all actionable information.
|
|
5045
|
+
|
|
5046
|
+
Content to summarize:
|
|
5047
|
+
`;
|
|
5048
|
+
async function condenseContent(raw, options, summarizeFn) {
|
|
5049
|
+
const originalLength = raw.length;
|
|
5050
|
+
const { maxLength } = options;
|
|
5051
|
+
const summarizationThreshold = options.summarizationThreshold ?? maxLength * 2;
|
|
5052
|
+
if (raw.length <= maxLength) {
|
|
5053
|
+
return { content: raw, method: "passthrough", originalLength };
|
|
5054
|
+
}
|
|
5055
|
+
if (raw.length < summarizationThreshold) {
|
|
5056
|
+
return {
|
|
5057
|
+
content: sanitizeExternalText(raw, maxLength),
|
|
5058
|
+
method: "truncated",
|
|
5059
|
+
originalLength
|
|
5060
|
+
};
|
|
5061
|
+
}
|
|
5062
|
+
if (options.modelEndpoint && summarizeFn) {
|
|
5063
|
+
try {
|
|
5064
|
+
const summarized = await summarizeFn(SUMMARIZE_PROMPT + raw, {
|
|
5065
|
+
endpoint: options.modelEndpoint,
|
|
5066
|
+
model: options.modelName ?? "default",
|
|
5067
|
+
maxTokens: Math.ceil(maxLength / 4)
|
|
5068
|
+
// rough token estimate
|
|
5069
|
+
});
|
|
5070
|
+
const finalContent = summarized.length > maxLength ? sanitizeExternalText(summarized, maxLength) : summarized;
|
|
5071
|
+
return { content: finalContent, method: "summarized", originalLength };
|
|
5072
|
+
} catch {
|
|
5073
|
+
return {
|
|
5074
|
+
content: sanitizeExternalText(raw, maxLength),
|
|
5075
|
+
method: "truncated",
|
|
5076
|
+
originalLength
|
|
5077
|
+
};
|
|
5078
|
+
}
|
|
5079
|
+
}
|
|
5080
|
+
return {
|
|
5081
|
+
content: sanitizeExternalText(raw, maxLength),
|
|
5082
|
+
method: "truncated",
|
|
5083
|
+
originalLength
|
|
5084
|
+
};
|
|
5085
|
+
}
|
|
5086
|
+
|
|
1977
5087
|
// src/ingest/connectors/JiraConnector.ts
|
|
1978
5088
|
function buildIngestResult(nodesAdded, edgesAdded, errors, start) {
|
|
1979
5089
|
return {
|
|
@@ -2005,7 +5115,7 @@ var JiraConnector = class {
|
|
|
2005
5115
|
source = "jira";
|
|
2006
5116
|
httpClient;
|
|
2007
5117
|
constructor(httpClient) {
|
|
2008
|
-
this.httpClient = httpClient ?? ((url, options) => fetch(url, options));
|
|
5118
|
+
this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
|
|
2009
5119
|
}
|
|
2010
5120
|
async ingest(store, config) {
|
|
2011
5121
|
const start = Date.now();
|
|
@@ -2032,7 +5142,7 @@ var JiraConnector = class {
|
|
|
2032
5142
|
const jql = buildJql(config);
|
|
2033
5143
|
const headers = { Authorization: `Basic ${apiKey}`, "Content-Type": "application/json" };
|
|
2034
5144
|
try {
|
|
2035
|
-
const counts = await this.fetchAllIssues(store, baseUrl, jql, headers);
|
|
5145
|
+
const counts = await this.fetchAllIssues(store, baseUrl, jql, headers, config);
|
|
2036
5146
|
return buildIngestResult(counts.nodesAdded, counts.edgesAdded, [], start);
|
|
2037
5147
|
} catch (err) {
|
|
2038
5148
|
return buildIngestResult(
|
|
@@ -2043,7 +5153,7 @@ var JiraConnector = class {
|
|
|
2043
5153
|
);
|
|
2044
5154
|
}
|
|
2045
5155
|
}
|
|
2046
|
-
async fetchAllIssues(store, baseUrl, jql, headers) {
|
|
5156
|
+
async fetchAllIssues(store, baseUrl, jql, headers, config) {
|
|
2047
5157
|
let nodesAdded = 0;
|
|
2048
5158
|
let edgesAdded = 0;
|
|
2049
5159
|
let startAt = 0;
|
|
@@ -2056,7 +5166,7 @@ var JiraConnector = class {
|
|
|
2056
5166
|
const data = await response.json();
|
|
2057
5167
|
total = data.total;
|
|
2058
5168
|
for (const issue of data.issues) {
|
|
2059
|
-
const counts = this.processIssue(store, issue);
|
|
5169
|
+
const counts = await this.processIssue(store, issue, baseUrl, headers, config);
|
|
2060
5170
|
nodesAdded += counts.nodesAdded;
|
|
2061
5171
|
edgesAdded += counts.edgesAdded;
|
|
2062
5172
|
}
|
|
@@ -2064,19 +5174,47 @@ var JiraConnector = class {
|
|
|
2064
5174
|
}
|
|
2065
5175
|
return { nodesAdded, edgesAdded };
|
|
2066
5176
|
}
|
|
2067
|
-
processIssue(store, issue) {
|
|
5177
|
+
async processIssue(store, issue, baseUrl, headers, config) {
|
|
2068
5178
|
const nodeId = `issue:jira:${issue.key}`;
|
|
5179
|
+
const comments = await this.fetchComments(baseUrl, issue.key, headers);
|
|
5180
|
+
const parts = [issue.fields.summary];
|
|
5181
|
+
if (issue.fields.description) {
|
|
5182
|
+
parts.push(issue.fields.description);
|
|
5183
|
+
}
|
|
5184
|
+
if (comments.length > 0) {
|
|
5185
|
+
for (const comment of comments) {
|
|
5186
|
+
parts.push(`${comment.author.displayName} (${comment.created}): ${comment.body}`);
|
|
5187
|
+
}
|
|
5188
|
+
}
|
|
5189
|
+
const rawContent = parts.join("\n");
|
|
5190
|
+
const acceptanceCriteria = parseAcceptanceCriteria(issue.fields.description ?? "");
|
|
5191
|
+
const customFields = extractCustomFields(issue.fields);
|
|
5192
|
+
const maxLen = config.maxContentLength ?? 4e3;
|
|
5193
|
+
const condensed = await condenseContent(rawContent, { maxLength: maxLen });
|
|
5194
|
+
const metadata = {
|
|
5195
|
+
key: issue.key,
|
|
5196
|
+
status: issue.fields.status?.name,
|
|
5197
|
+
priority: issue.fields.priority?.name,
|
|
5198
|
+
assignee: issue.fields.assignee?.displayName,
|
|
5199
|
+
labels: issue.fields.labels ?? [],
|
|
5200
|
+
commentCount: comments.length
|
|
5201
|
+
};
|
|
5202
|
+
if (acceptanceCriteria.length > 0) {
|
|
5203
|
+
metadata.acceptanceCriteria = acceptanceCriteria;
|
|
5204
|
+
}
|
|
5205
|
+
if (Object.keys(customFields).length > 0) {
|
|
5206
|
+
metadata.customFields = customFields;
|
|
5207
|
+
}
|
|
5208
|
+
if (condensed.method !== "passthrough") {
|
|
5209
|
+
metadata.condensed = condensed.method;
|
|
5210
|
+
metadata.originalLength = condensed.originalLength;
|
|
5211
|
+
}
|
|
2069
5212
|
store.addNode({
|
|
2070
5213
|
id: nodeId,
|
|
2071
5214
|
type: "issue",
|
|
2072
5215
|
name: sanitizeExternalText(issue.fields.summary, 500),
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
status: issue.fields.status?.name,
|
|
2076
|
-
priority: issue.fields.priority?.name,
|
|
2077
|
-
assignee: issue.fields.assignee?.displayName,
|
|
2078
|
-
labels: issue.fields.labels ?? []
|
|
2079
|
-
}
|
|
5216
|
+
content: condensed.content,
|
|
5217
|
+
metadata
|
|
2080
5218
|
});
|
|
2081
5219
|
const searchText = sanitizeExternalText(
|
|
2082
5220
|
[issue.fields.summary, issue.fields.description ?? ""].join(" ")
|
|
@@ -2084,7 +5222,40 @@ var JiraConnector = class {
|
|
|
2084
5222
|
const edgesAdded = linkToCode(store, searchText, nodeId, "applies_to");
|
|
2085
5223
|
return { nodesAdded: 1, edgesAdded };
|
|
2086
5224
|
}
|
|
5225
|
+
async fetchComments(baseUrl, issueKey, headers) {
|
|
5226
|
+
try {
|
|
5227
|
+
const url = `${baseUrl}/rest/api/2/issue/${issueKey}/comment`;
|
|
5228
|
+
const response = await this.httpClient(url, { headers });
|
|
5229
|
+
if (!response.ok) return [];
|
|
5230
|
+
const data = await response.json();
|
|
5231
|
+
return data.comments ?? [];
|
|
5232
|
+
} catch {
|
|
5233
|
+
return [];
|
|
5234
|
+
}
|
|
5235
|
+
}
|
|
2087
5236
|
};
|
|
5237
|
+
function parseAcceptanceCriteria(description) {
|
|
5238
|
+
const criteria = [];
|
|
5239
|
+
const checkboxRegex = /[-*]\s*\[[ x]\]\s*(.+)/gi;
|
|
5240
|
+
let match;
|
|
5241
|
+
while ((match = checkboxRegex.exec(description)) !== null) {
|
|
5242
|
+
criteria.push(match[1].trim());
|
|
5243
|
+
}
|
|
5244
|
+
const gwtRegex = /Given\b.+?When\b.+?Then\b.+/gi;
|
|
5245
|
+
while ((match = gwtRegex.exec(description)) !== null) {
|
|
5246
|
+
criteria.push(match[0].trim());
|
|
5247
|
+
}
|
|
5248
|
+
return criteria;
|
|
5249
|
+
}
|
|
5250
|
+
function extractCustomFields(fields) {
|
|
5251
|
+
const result = {};
|
|
5252
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
5253
|
+
if (key.startsWith("customfield_") && value != null && typeof value === "string") {
|
|
5254
|
+
result[key] = value;
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5257
|
+
return result;
|
|
5258
|
+
}
|
|
2088
5259
|
|
|
2089
5260
|
// src/ingest/connectors/SlackConnector.ts
|
|
2090
5261
|
var SlackConnector = class {
|
|
@@ -2092,7 +5263,7 @@ var SlackConnector = class {
|
|
|
2092
5263
|
source = "slack";
|
|
2093
5264
|
httpClient;
|
|
2094
5265
|
constructor(httpClient) {
|
|
2095
|
-
this.httpClient = httpClient ?? ((url, options) => fetch(url, options));
|
|
5266
|
+
this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
|
|
2096
5267
|
}
|
|
2097
5268
|
async ingest(store, config) {
|
|
2098
5269
|
const start = Date.now();
|
|
@@ -2115,7 +5286,7 @@ var SlackConnector = class {
|
|
|
2115
5286
|
const oldest = config.lookbackDays ? String(Math.floor((Date.now() - Number(config.lookbackDays) * 864e5) / 1e3)) : void 0;
|
|
2116
5287
|
for (const channel of channels) {
|
|
2117
5288
|
try {
|
|
2118
|
-
const result = await this.processChannel(store, channel, apiKey, oldest);
|
|
5289
|
+
const result = await this.processChannel(store, channel, apiKey, oldest, config);
|
|
2119
5290
|
nodesAdded += result.nodesAdded;
|
|
2120
5291
|
edgesAdded += result.edgesAdded;
|
|
2121
5292
|
errors.push(...result.errors);
|
|
@@ -2134,7 +5305,7 @@ var SlackConnector = class {
|
|
|
2134
5305
|
durationMs: Date.now() - start
|
|
2135
5306
|
};
|
|
2136
5307
|
}
|
|
2137
|
-
async processChannel(store, channel, apiKey, oldest) {
|
|
5308
|
+
async processChannel(store, channel, apiKey, oldest, config) {
|
|
2138
5309
|
const errors = [];
|
|
2139
5310
|
let nodesAdded = 0;
|
|
2140
5311
|
let edgesAdded = 0;
|
|
@@ -2159,27 +5330,74 @@ var SlackConnector = class {
|
|
|
2159
5330
|
if (!data.ok) {
|
|
2160
5331
|
return { nodesAdded: 0, edgesAdded: 0, errors: [`Slack API error for channel ${channel}`] };
|
|
2161
5332
|
}
|
|
5333
|
+
const maxLen = config.maxContentLength ?? 2e3;
|
|
2162
5334
|
for (const message of data.messages) {
|
|
2163
5335
|
const nodeId = `conversation:slack:${channel}:${message.ts}`;
|
|
2164
|
-
|
|
2165
|
-
|
|
5336
|
+
let assembledText = message.text;
|
|
5337
|
+
let threadReplyCount;
|
|
5338
|
+
if (message.reply_count && message.reply_count > 0 && message.thread_ts) {
|
|
5339
|
+
const replies = await this.fetchThreadReplies(channel, message.thread_ts, apiKey);
|
|
5340
|
+
if (replies.length > 0) {
|
|
5341
|
+
const threadReplies = replies.slice(1);
|
|
5342
|
+
threadReplyCount = threadReplies.length;
|
|
5343
|
+
if (threadReplies.length > 0) {
|
|
5344
|
+
const replyLines = threadReplies.map((r) => `${r.user} (${r.ts}): ${r.text}`);
|
|
5345
|
+
assembledText = `${message.text}
|
|
5346
|
+
${replyLines.join("\n")}`;
|
|
5347
|
+
}
|
|
5348
|
+
}
|
|
5349
|
+
}
|
|
5350
|
+
const condensed = await condenseContent(assembledText, { maxLength: maxLen });
|
|
5351
|
+
const snippet = condensed.content.length > 100 ? condensed.content.slice(0, 100) : condensed.content;
|
|
5352
|
+
const reactions = message.reactions ? message.reactions.reduce(
|
|
5353
|
+
(acc, r) => ({ ...acc, [r.name]: r.count }),
|
|
5354
|
+
{}
|
|
5355
|
+
) : void 0;
|
|
5356
|
+
const metadata = {
|
|
5357
|
+
author: message.user,
|
|
5358
|
+
channel,
|
|
5359
|
+
timestamp: message.ts
|
|
5360
|
+
};
|
|
5361
|
+
if (threadReplyCount !== void 0) {
|
|
5362
|
+
metadata.threadReplyCount = threadReplyCount;
|
|
5363
|
+
}
|
|
5364
|
+
if (reactions) {
|
|
5365
|
+
metadata.reactions = reactions;
|
|
5366
|
+
}
|
|
5367
|
+
if (condensed.method !== "passthrough") {
|
|
5368
|
+
metadata.condensed = condensed.method;
|
|
5369
|
+
metadata.originalLength = condensed.originalLength;
|
|
5370
|
+
}
|
|
2166
5371
|
store.addNode({
|
|
2167
5372
|
id: nodeId,
|
|
2168
5373
|
type: "conversation",
|
|
2169
5374
|
name: snippet,
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
channel,
|
|
2173
|
-
timestamp: message.ts
|
|
2174
|
-
}
|
|
5375
|
+
content: condensed.content,
|
|
5376
|
+
metadata
|
|
2175
5377
|
});
|
|
2176
5378
|
nodesAdded++;
|
|
2177
|
-
edgesAdded += linkToCode(store,
|
|
5379
|
+
edgesAdded += linkToCode(store, condensed.content, nodeId, "references", {
|
|
2178
5380
|
checkPaths: true
|
|
2179
5381
|
});
|
|
2180
5382
|
}
|
|
2181
5383
|
return { nodesAdded, edgesAdded, errors };
|
|
2182
5384
|
}
|
|
5385
|
+
async fetchThreadReplies(channel, threadTs, apiKey) {
|
|
5386
|
+
try {
|
|
5387
|
+
const url = `https://slack.com/api/conversations.replies?channel=${encodeURIComponent(channel)}&ts=${threadTs}`;
|
|
5388
|
+
const response = await this.httpClient(url, {
|
|
5389
|
+
headers: {
|
|
5390
|
+
Authorization: `Bearer ${apiKey}`,
|
|
5391
|
+
"Content-Type": "application/json"
|
|
5392
|
+
}
|
|
5393
|
+
});
|
|
5394
|
+
if (!response.ok) return [];
|
|
5395
|
+
const data = await response.json();
|
|
5396
|
+
return data.ok ? data.messages : [];
|
|
5397
|
+
} catch {
|
|
5398
|
+
return [];
|
|
5399
|
+
}
|
|
5400
|
+
}
|
|
2183
5401
|
};
|
|
2184
5402
|
|
|
2185
5403
|
// src/ingest/connectors/ConfluenceConnector.ts
|
|
@@ -2198,7 +5416,7 @@ var ConfluenceConnector = class {
|
|
|
2198
5416
|
source = "confluence";
|
|
2199
5417
|
httpClient;
|
|
2200
5418
|
constructor(httpClient) {
|
|
2201
|
-
this.httpClient = httpClient ?? ((url, options) => fetch(url, options));
|
|
5419
|
+
this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
|
|
2202
5420
|
}
|
|
2203
5421
|
async ingest(store, config) {
|
|
2204
5422
|
const start = Date.now();
|
|
@@ -2211,7 +5429,14 @@ var ConfluenceConnector = class {
|
|
|
2211
5429
|
const baseUrlEnv = config.baseUrlEnv ?? "CONFLUENCE_BASE_URL";
|
|
2212
5430
|
const baseUrl = process.env[baseUrlEnv] ?? "";
|
|
2213
5431
|
const spaceKey = config.spaceKey ?? "";
|
|
2214
|
-
const counts = await this.fetchAllPagesHandled(
|
|
5432
|
+
const counts = await this.fetchAllPagesHandled(
|
|
5433
|
+
store,
|
|
5434
|
+
baseUrl,
|
|
5435
|
+
apiKey,
|
|
5436
|
+
spaceKey,
|
|
5437
|
+
errors,
|
|
5438
|
+
config
|
|
5439
|
+
);
|
|
2215
5440
|
return {
|
|
2216
5441
|
nodesAdded: counts.nodesAdded,
|
|
2217
5442
|
nodesUpdated: 0,
|
|
@@ -2221,9 +5446,9 @@ var ConfluenceConnector = class {
|
|
|
2221
5446
|
durationMs: Date.now() - start
|
|
2222
5447
|
};
|
|
2223
5448
|
}
|
|
2224
|
-
async fetchAllPagesHandled(store, baseUrl, apiKey, spaceKey, errors) {
|
|
5449
|
+
async fetchAllPagesHandled(store, baseUrl, apiKey, spaceKey, errors, config) {
|
|
2225
5450
|
try {
|
|
2226
|
-
const result = await this.fetchAllPages(store, baseUrl, apiKey, spaceKey);
|
|
5451
|
+
const result = await this.fetchAllPages(store, baseUrl, apiKey, spaceKey, config);
|
|
2227
5452
|
errors.push(...result.errors);
|
|
2228
5453
|
return { nodesAdded: result.nodesAdded, edgesAdded: result.edgesAdded };
|
|
2229
5454
|
} catch (err) {
|
|
@@ -2231,7 +5456,7 @@ var ConfluenceConnector = class {
|
|
|
2231
5456
|
return { nodesAdded: 0, edgesAdded: 0 };
|
|
2232
5457
|
}
|
|
2233
5458
|
}
|
|
2234
|
-
async fetchAllPages(store, baseUrl, apiKey, spaceKey) {
|
|
5459
|
+
async fetchAllPages(store, baseUrl, apiKey, spaceKey, config) {
|
|
2235
5460
|
const errors = [];
|
|
2236
5461
|
let nodesAdded = 0;
|
|
2237
5462
|
let edgesAdded = 0;
|
|
@@ -2246,7 +5471,7 @@ var ConfluenceConnector = class {
|
|
|
2246
5471
|
}
|
|
2247
5472
|
const data = await response.json();
|
|
2248
5473
|
for (const page of data.results) {
|
|
2249
|
-
const counts = this.processPage(store, page, spaceKey);
|
|
5474
|
+
const counts = await this.processPage(store, page, spaceKey, config);
|
|
2250
5475
|
nodesAdded += counts.nodesAdded;
|
|
2251
5476
|
edgesAdded += counts.edgesAdded;
|
|
2252
5477
|
}
|
|
@@ -2254,22 +5479,43 @@ var ConfluenceConnector = class {
|
|
|
2254
5479
|
}
|
|
2255
5480
|
return { nodesAdded, edgesAdded, errors };
|
|
2256
5481
|
}
|
|
2257
|
-
processPage(store, page, spaceKey) {
|
|
5482
|
+
async processPage(store, page, spaceKey, config) {
|
|
2258
5483
|
const nodeId = `confluence:${page.id}`;
|
|
5484
|
+
let edgesAdded = 0;
|
|
5485
|
+
const labels = page.metadata?.labels?.results?.map((l) => l.name) ?? [];
|
|
5486
|
+
const parentPageId = page.ancestors && page.ancestors.length > 0 ? page.ancestors[page.ancestors.length - 1].id : void 0;
|
|
5487
|
+
const rawContent = `${page.title} ${page.body?.storage?.value ?? ""}`;
|
|
5488
|
+
const maxLen = config.maxContentLength ?? 8e3;
|
|
5489
|
+
const condensed = await condenseContent(rawContent, { maxLength: maxLen });
|
|
5490
|
+
const metadata = {
|
|
5491
|
+
source: "confluence",
|
|
5492
|
+
spaceKey,
|
|
5493
|
+
pageId: page.id,
|
|
5494
|
+
status: page.status,
|
|
5495
|
+
url: page._links?.webui ?? "",
|
|
5496
|
+
labels
|
|
5497
|
+
};
|
|
5498
|
+
if (parentPageId) {
|
|
5499
|
+
metadata.parentPageId = parentPageId;
|
|
5500
|
+
}
|
|
5501
|
+
if (condensed.method !== "passthrough") {
|
|
5502
|
+
metadata.condensed = condensed.method;
|
|
5503
|
+
metadata.originalLength = condensed.originalLength;
|
|
5504
|
+
}
|
|
2259
5505
|
store.addNode({
|
|
2260
5506
|
id: nodeId,
|
|
2261
5507
|
type: "document",
|
|
2262
5508
|
name: sanitizeExternalText(page.title, 500),
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
spaceKey,
|
|
2266
|
-
pageId: page.id,
|
|
2267
|
-
status: page.status,
|
|
2268
|
-
url: page._links?.webui ?? ""
|
|
2269
|
-
}
|
|
5509
|
+
content: condensed.content,
|
|
5510
|
+
metadata
|
|
2270
5511
|
});
|
|
5512
|
+
if (parentPageId) {
|
|
5513
|
+
const parentNodeId = `confluence:${parentPageId}`;
|
|
5514
|
+
store.addEdge({ from: parentNodeId, to: nodeId, type: "contains" });
|
|
5515
|
+
edgesAdded++;
|
|
5516
|
+
}
|
|
2271
5517
|
const text = sanitizeExternalText(`${page.title} ${page.body?.storage?.value ?? ""}`);
|
|
2272
|
-
|
|
5518
|
+
edgesAdded += linkToCode(store, text, nodeId, "documents");
|
|
2273
5519
|
return { nodesAdded: 1, edgesAdded };
|
|
2274
5520
|
}
|
|
2275
5521
|
};
|
|
@@ -2335,7 +5581,7 @@ var CIConnector = class {
|
|
|
2335
5581
|
source = "github-actions";
|
|
2336
5582
|
httpClient;
|
|
2337
5583
|
constructor(httpClient) {
|
|
2338
|
-
this.httpClient = httpClient ?? ((url, options) => fetch(url, options));
|
|
5584
|
+
this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
|
|
2339
5585
|
}
|
|
2340
5586
|
async ingest(store, config) {
|
|
2341
5587
|
const start = Date.now();
|
|
@@ -2387,6 +5633,306 @@ var CIConnector = class {
|
|
|
2387
5633
|
}
|
|
2388
5634
|
};
|
|
2389
5635
|
|
|
5636
|
+
// src/ingest/connectors/FigmaConnector.ts
|
|
5637
|
+
var CONSTRAINT_KEYWORDS = [
|
|
5638
|
+
"constraint",
|
|
5639
|
+
"must",
|
|
5640
|
+
"required",
|
|
5641
|
+
"minimum",
|
|
5642
|
+
"maximum",
|
|
5643
|
+
"spacing",
|
|
5644
|
+
"padding",
|
|
5645
|
+
"margin",
|
|
5646
|
+
"breakpoint",
|
|
5647
|
+
"responsive",
|
|
5648
|
+
"accessible",
|
|
5649
|
+
"a11y",
|
|
5650
|
+
"wcag"
|
|
5651
|
+
];
|
|
5652
|
+
function hasConstraintKeyword(text) {
|
|
5653
|
+
const lower = text.toLowerCase();
|
|
5654
|
+
return CONSTRAINT_KEYWORDS.some((kw) => lower.includes(kw));
|
|
5655
|
+
}
|
|
5656
|
+
function buildIngestResult2(nodesAdded, edgesAdded, errors, start) {
|
|
5657
|
+
return {
|
|
5658
|
+
nodesAdded,
|
|
5659
|
+
nodesUpdated: 0,
|
|
5660
|
+
edgesAdded,
|
|
5661
|
+
edgesUpdated: 0,
|
|
5662
|
+
errors,
|
|
5663
|
+
durationMs: Date.now() - start
|
|
5664
|
+
};
|
|
5665
|
+
}
|
|
5666
|
+
var FigmaConnector = class {
|
|
5667
|
+
name = "figma";
|
|
5668
|
+
source = "figma";
|
|
5669
|
+
httpClient;
|
|
5670
|
+
constructor(httpClient) {
|
|
5671
|
+
this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
|
|
5672
|
+
}
|
|
5673
|
+
async ingest(store, config) {
|
|
5674
|
+
const start = Date.now();
|
|
5675
|
+
const apiKeyEnv = config.apiKeyEnv ?? "FIGMA_API_KEY";
|
|
5676
|
+
const apiKey = process.env[apiKeyEnv];
|
|
5677
|
+
if (!apiKey) {
|
|
5678
|
+
return buildIngestResult2(
|
|
5679
|
+
0,
|
|
5680
|
+
0,
|
|
5681
|
+
[`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
5682
|
+
start
|
|
5683
|
+
);
|
|
5684
|
+
}
|
|
5685
|
+
const baseUrlEnv = config.baseUrlEnv ?? "FIGMA_BASE_URL";
|
|
5686
|
+
const baseUrl = process.env[baseUrlEnv] ?? "https://api.figma.com";
|
|
5687
|
+
try {
|
|
5688
|
+
const parsed = new URL(baseUrl);
|
|
5689
|
+
if (parsed.protocol !== "https:" || !parsed.hostname.endsWith("api.figma.com")) {
|
|
5690
|
+
return buildIngestResult2(
|
|
5691
|
+
0,
|
|
5692
|
+
0,
|
|
5693
|
+
[`Invalid ${baseUrlEnv}: must be an HTTPS URL on api.figma.com`],
|
|
5694
|
+
start
|
|
5695
|
+
);
|
|
5696
|
+
}
|
|
5697
|
+
} catch {
|
|
5698
|
+
return buildIngestResult2(0, 0, [`Invalid ${baseUrlEnv}: not a valid URL`], start);
|
|
5699
|
+
}
|
|
5700
|
+
const fileIds = config.fileIds;
|
|
5701
|
+
if (!fileIds || fileIds.length === 0) {
|
|
5702
|
+
return buildIngestResult2(0, 0, ["No fileIds provided in connector config"], start);
|
|
5703
|
+
}
|
|
5704
|
+
const headers = { "X-FIGMA-TOKEN": apiKey };
|
|
5705
|
+
const maxLen = config.maxContentLength ?? 4e3;
|
|
5706
|
+
let nodesAdded = 0;
|
|
5707
|
+
let edgesAdded = 0;
|
|
5708
|
+
const errors = [];
|
|
5709
|
+
for (const fileId of fileIds) {
|
|
5710
|
+
try {
|
|
5711
|
+
const counts = await this.processFile(store, baseUrl, fileId, headers, maxLen);
|
|
5712
|
+
nodesAdded += counts.nodesAdded;
|
|
5713
|
+
edgesAdded += counts.edgesAdded;
|
|
5714
|
+
} catch (err) {
|
|
5715
|
+
errors.push(
|
|
5716
|
+
`Figma API error for file ${fileId}: ${err instanceof Error ? err.message : String(err)}`
|
|
5717
|
+
);
|
|
5718
|
+
}
|
|
5719
|
+
}
|
|
5720
|
+
return buildIngestResult2(nodesAdded, edgesAdded, errors, start);
|
|
5721
|
+
}
|
|
5722
|
+
async processFile(store, baseUrl, fileId, headers, maxLen) {
|
|
5723
|
+
let nodesAdded = 0;
|
|
5724
|
+
let edgesAdded = 0;
|
|
5725
|
+
const stylesUrl = `${baseUrl}/v1/files/${fileId}/styles`;
|
|
5726
|
+
const stylesResponse = await this.httpClient(stylesUrl, { headers });
|
|
5727
|
+
if (!stylesResponse.ok) throw new Error(`Styles request failed for file ${fileId}`);
|
|
5728
|
+
const stylesData = await stylesResponse.json();
|
|
5729
|
+
for (const style of stylesData.meta.styles) {
|
|
5730
|
+
const nodeId = `figma:token:${style.key}`;
|
|
5731
|
+
const condensed = await condenseContent(sanitizeExternalText(style.description || "", 2e3), {
|
|
5732
|
+
maxLength: maxLen
|
|
5733
|
+
});
|
|
5734
|
+
const metadata = {
|
|
5735
|
+
source: "figma",
|
|
5736
|
+
key: style.key,
|
|
5737
|
+
styleType: style.style_type,
|
|
5738
|
+
fileId
|
|
5739
|
+
};
|
|
5740
|
+
if (condensed.method !== "passthrough") {
|
|
5741
|
+
metadata.condensed = condensed.method;
|
|
5742
|
+
metadata.originalLength = condensed.originalLength;
|
|
5743
|
+
}
|
|
5744
|
+
store.addNode({
|
|
5745
|
+
id: nodeId,
|
|
5746
|
+
type: "design_token",
|
|
5747
|
+
name: sanitizeExternalText(style.name, 500),
|
|
5748
|
+
content: condensed.content,
|
|
5749
|
+
metadata
|
|
5750
|
+
});
|
|
5751
|
+
nodesAdded++;
|
|
5752
|
+
const searchText = sanitizeExternalText([style.name, style.description].join(" "));
|
|
5753
|
+
edgesAdded += linkToCode(store, searchText, nodeId, "references");
|
|
5754
|
+
}
|
|
5755
|
+
const componentsUrl = `${baseUrl}/v1/files/${fileId}/components`;
|
|
5756
|
+
const componentsResponse = await this.httpClient(componentsUrl, { headers });
|
|
5757
|
+
if (!componentsResponse.ok) throw new Error(`Components request failed for file ${fileId}`);
|
|
5758
|
+
const componentsData = await componentsResponse.json();
|
|
5759
|
+
for (const component of componentsData.meta.components) {
|
|
5760
|
+
const description = component.description || "";
|
|
5761
|
+
if (description) {
|
|
5762
|
+
const intentId = `figma:intent:${component.key}`;
|
|
5763
|
+
const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
|
|
5764
|
+
maxLength: maxLen
|
|
5765
|
+
});
|
|
5766
|
+
const metadata = {
|
|
5767
|
+
source: "figma",
|
|
5768
|
+
key: component.key,
|
|
5769
|
+
fileId
|
|
5770
|
+
};
|
|
5771
|
+
if (condensed.method !== "passthrough") {
|
|
5772
|
+
metadata.condensed = condensed.method;
|
|
5773
|
+
metadata.originalLength = condensed.originalLength;
|
|
5774
|
+
}
|
|
5775
|
+
store.addNode({
|
|
5776
|
+
id: intentId,
|
|
5777
|
+
type: "aesthetic_intent",
|
|
5778
|
+
name: sanitizeExternalText(component.name, 500),
|
|
5779
|
+
content: condensed.content,
|
|
5780
|
+
metadata
|
|
5781
|
+
});
|
|
5782
|
+
nodesAdded++;
|
|
5783
|
+
const searchText = sanitizeExternalText([component.name, description].join(" "));
|
|
5784
|
+
edgesAdded += linkToCode(store, searchText, intentId, "references");
|
|
5785
|
+
}
|
|
5786
|
+
if (description && hasConstraintKeyword(description)) {
|
|
5787
|
+
const constraintId = `figma:constraint:${component.key}`;
|
|
5788
|
+
const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
|
|
5789
|
+
maxLength: maxLen
|
|
5790
|
+
});
|
|
5791
|
+
const metadata = {
|
|
5792
|
+
source: "figma",
|
|
5793
|
+
key: component.key,
|
|
5794
|
+
fileId
|
|
5795
|
+
};
|
|
5796
|
+
if (condensed.method !== "passthrough") {
|
|
5797
|
+
metadata.condensed = condensed.method;
|
|
5798
|
+
metadata.originalLength = condensed.originalLength;
|
|
5799
|
+
}
|
|
5800
|
+
store.addNode({
|
|
5801
|
+
id: constraintId,
|
|
5802
|
+
type: "design_constraint",
|
|
5803
|
+
name: sanitizeExternalText(component.name, 500),
|
|
5804
|
+
content: condensed.content,
|
|
5805
|
+
metadata
|
|
5806
|
+
});
|
|
5807
|
+
nodesAdded++;
|
|
5808
|
+
const searchText = sanitizeExternalText([component.name, description].join(" "));
|
|
5809
|
+
edgesAdded += linkToCode(store, searchText, constraintId, "references");
|
|
5810
|
+
}
|
|
5811
|
+
}
|
|
5812
|
+
return { nodesAdded, edgesAdded };
|
|
5813
|
+
}
|
|
5814
|
+
};
|
|
5815
|
+
|
|
5816
|
+
// src/ingest/connectors/MiroConnector.ts
|
|
5817
|
+
var MIN_CONTENT_LENGTH = 10;
|
|
5818
|
+
var CONCEPT_ITEM_TYPES = /* @__PURE__ */ new Set(["sticky_note", "text"]);
|
|
5819
|
+
function stripHtml(html) {
|
|
5820
|
+
return html.replace(/<[^>]*>/g, "");
|
|
5821
|
+
}
|
|
5822
|
+
function buildIngestResult3(nodesAdded, edgesAdded, errors, start) {
|
|
5823
|
+
return {
|
|
5824
|
+
nodesAdded,
|
|
5825
|
+
nodesUpdated: 0,
|
|
5826
|
+
edgesAdded,
|
|
5827
|
+
edgesUpdated: 0,
|
|
5828
|
+
errors,
|
|
5829
|
+
durationMs: Date.now() - start
|
|
5830
|
+
};
|
|
5831
|
+
}
|
|
5832
|
+
var MiroConnector = class {
|
|
5833
|
+
name = "miro";
|
|
5834
|
+
source = "miro";
|
|
5835
|
+
httpClient;
|
|
5836
|
+
constructor(httpClient) {
|
|
5837
|
+
this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
|
|
5838
|
+
}
|
|
5839
|
+
async ingest(store, config) {
|
|
5840
|
+
const start = Date.now();
|
|
5841
|
+
const apiKeyEnv = config.apiKeyEnv ?? "MIRO_API_KEY";
|
|
5842
|
+
const apiKey = process.env[apiKeyEnv];
|
|
5843
|
+
if (!apiKey) {
|
|
5844
|
+
return buildIngestResult3(
|
|
5845
|
+
0,
|
|
5846
|
+
0,
|
|
5847
|
+
[`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
5848
|
+
start
|
|
5849
|
+
);
|
|
5850
|
+
}
|
|
5851
|
+
const baseUrlEnv = config.baseUrlEnv ?? "MIRO_BASE_URL";
|
|
5852
|
+
const baseUrl = process.env[baseUrlEnv] ?? "https://api.miro.com";
|
|
5853
|
+
try {
|
|
5854
|
+
const parsed = new URL(baseUrl);
|
|
5855
|
+
if (parsed.protocol !== "https:" || parsed.hostname !== "api.miro.com") {
|
|
5856
|
+
return buildIngestResult3(
|
|
5857
|
+
0,
|
|
5858
|
+
0,
|
|
5859
|
+
[`Invalid ${baseUrlEnv}: must be an HTTPS URL on api.miro.com`],
|
|
5860
|
+
start
|
|
5861
|
+
);
|
|
5862
|
+
}
|
|
5863
|
+
} catch {
|
|
5864
|
+
return buildIngestResult3(0, 0, [`Invalid ${baseUrlEnv}: not a valid URL`], start);
|
|
5865
|
+
}
|
|
5866
|
+
const boardIds = config.boardIds;
|
|
5867
|
+
if (!boardIds || boardIds.length === 0) {
|
|
5868
|
+
return buildIngestResult3(0, 0, ["No boardIds provided in config"], start);
|
|
5869
|
+
}
|
|
5870
|
+
const headers = { Authorization: `Bearer ${apiKey}` };
|
|
5871
|
+
let totalNodesAdded = 0;
|
|
5872
|
+
let totalEdgesAdded = 0;
|
|
5873
|
+
const errors = [];
|
|
5874
|
+
for (const boardId of boardIds) {
|
|
5875
|
+
try {
|
|
5876
|
+
const counts = await this.processBoard(store, baseUrl, boardId, headers);
|
|
5877
|
+
totalNodesAdded += counts.nodesAdded;
|
|
5878
|
+
totalEdgesAdded += counts.edgesAdded;
|
|
5879
|
+
} catch (err) {
|
|
5880
|
+
errors.push(
|
|
5881
|
+
`Miro API error for board ${boardId}: ${err instanceof Error ? err.message : String(err)}`
|
|
5882
|
+
);
|
|
5883
|
+
}
|
|
5884
|
+
}
|
|
5885
|
+
return buildIngestResult3(totalNodesAdded, totalEdgesAdded, errors, start);
|
|
5886
|
+
}
|
|
5887
|
+
async processBoard(store, baseUrl, boardId, headers) {
|
|
5888
|
+
let nodesAdded = 0;
|
|
5889
|
+
let edgesAdded = 0;
|
|
5890
|
+
const boardResponse = await this.httpClient(`${baseUrl}/v2/boards/${boardId}`, { headers });
|
|
5891
|
+
if (!boardResponse.ok) throw new Error(`Failed to fetch board ${boardId}`);
|
|
5892
|
+
const board = await boardResponse.json();
|
|
5893
|
+
const boardNodeId = `miro:board:${boardId}`;
|
|
5894
|
+
store.addNode({
|
|
5895
|
+
id: boardNodeId,
|
|
5896
|
+
type: "document",
|
|
5897
|
+
name: sanitizeExternalText(board.name, 500),
|
|
5898
|
+
content: sanitizeExternalText(board.description ?? "", 2e3),
|
|
5899
|
+
metadata: {
|
|
5900
|
+
source: "miro",
|
|
5901
|
+
boardId: board.id
|
|
5902
|
+
}
|
|
5903
|
+
});
|
|
5904
|
+
nodesAdded++;
|
|
5905
|
+
const itemsResponse = await this.httpClient(`${baseUrl}/v2/boards/${boardId}/items`, {
|
|
5906
|
+
headers
|
|
5907
|
+
});
|
|
5908
|
+
if (!itemsResponse.ok) throw new Error(`Failed to fetch items for board ${boardId}`);
|
|
5909
|
+
const itemsData = await itemsResponse.json();
|
|
5910
|
+
for (const item of itemsData.data) {
|
|
5911
|
+
const rawContent = item.data?.content ?? "";
|
|
5912
|
+
const plainContent = stripHtml(rawContent);
|
|
5913
|
+
if (plainContent.length < MIN_CONTENT_LENGTH) continue;
|
|
5914
|
+
if (!CONCEPT_ITEM_TYPES.has(item.type)) continue;
|
|
5915
|
+
const itemNodeId = `miro:item:${item.id}`;
|
|
5916
|
+
store.addNode({
|
|
5917
|
+
id: itemNodeId,
|
|
5918
|
+
type: "business_concept",
|
|
5919
|
+
name: sanitizeExternalText(plainContent, 500),
|
|
5920
|
+
content: sanitizeExternalText(plainContent, 2e3),
|
|
5921
|
+
metadata: {
|
|
5922
|
+
source: "miro",
|
|
5923
|
+
boardId: board.id,
|
|
5924
|
+
itemType: item.type
|
|
5925
|
+
}
|
|
5926
|
+
});
|
|
5927
|
+
nodesAdded++;
|
|
5928
|
+
store.addEdge({ from: boardNodeId, to: itemNodeId, type: "contains" });
|
|
5929
|
+
edgesAdded++;
|
|
5930
|
+
edgesAdded += linkToCode(store, plainContent, itemNodeId, "documents");
|
|
5931
|
+
}
|
|
5932
|
+
return { nodesAdded, edgesAdded };
|
|
5933
|
+
}
|
|
5934
|
+
};
|
|
5935
|
+
|
|
2390
5936
|
// src/search/FusionLayer.ts
|
|
2391
5937
|
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
2392
5938
|
"the",
|
|
@@ -2525,7 +6071,7 @@ var FusionLayer = class {
|
|
|
2525
6071
|
};
|
|
2526
6072
|
|
|
2527
6073
|
// src/entropy/GraphEntropyAdapter.ts
|
|
2528
|
-
var
|
|
6074
|
+
var CODE_NODE_TYPES5 = ["file", "function", "class", "method", "interface", "variable"];
|
|
2529
6075
|
var GraphEntropyAdapter = class {
|
|
2530
6076
|
constructor(store) {
|
|
2531
6077
|
this.store = store;
|
|
@@ -2605,7 +6151,7 @@ var GraphEntropyAdapter = class {
|
|
|
2605
6151
|
}
|
|
2606
6152
|
findEntryPoints() {
|
|
2607
6153
|
const entryPoints = [];
|
|
2608
|
-
for (const nodeType of
|
|
6154
|
+
for (const nodeType of CODE_NODE_TYPES5) {
|
|
2609
6155
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
2610
6156
|
for (const node of nodes) {
|
|
2611
6157
|
const isIndexFile = nodeType === "file" && node.name === "index.ts";
|
|
@@ -2641,7 +6187,7 @@ var GraphEntropyAdapter = class {
|
|
|
2641
6187
|
}
|
|
2642
6188
|
collectUnreachableNodes(visited) {
|
|
2643
6189
|
const unreachableNodes = [];
|
|
2644
|
-
for (const nodeType of
|
|
6190
|
+
for (const nodeType of CODE_NODE_TYPES5) {
|
|
2645
6191
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
2646
6192
|
for (const node of nodes) {
|
|
2647
6193
|
if (!visited.has(node.id)) {
|
|
@@ -3487,9 +7033,9 @@ var EntityExtractor = class {
|
|
|
3487
7033
|
extractPaths(trimmed, add) {
|
|
3488
7034
|
const consumed = /* @__PURE__ */ new Set();
|
|
3489
7035
|
for (const match of trimmed.matchAll(FILE_PATH_RE)) {
|
|
3490
|
-
const
|
|
3491
|
-
add(
|
|
3492
|
-
consumed.add(
|
|
7036
|
+
const path13 = match[0];
|
|
7037
|
+
add(path13);
|
|
7038
|
+
consumed.add(path13);
|
|
3493
7039
|
}
|
|
3494
7040
|
return consumed;
|
|
3495
7041
|
}
|
|
@@ -3561,8 +7107,8 @@ var EntityResolver = class {
|
|
|
3561
7107
|
if (isPathLike && node.path.includes(raw)) {
|
|
3562
7108
|
return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
|
|
3563
7109
|
}
|
|
3564
|
-
const
|
|
3565
|
-
if (
|
|
7110
|
+
const basename7 = node.path.split("/").pop() ?? "";
|
|
7111
|
+
if (basename7.includes(raw)) {
|
|
3566
7112
|
return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
|
|
3567
7113
|
}
|
|
3568
7114
|
if (raw.length >= 4 && node.path.includes(raw)) {
|
|
@@ -3641,13 +7187,13 @@ var ResponseFormatter = class {
|
|
|
3641
7187
|
const context = Array.isArray(d?.context) ? d.context : [];
|
|
3642
7188
|
const firstEntity = entities[0];
|
|
3643
7189
|
const nodeType = firstEntity?.node.type ?? "node";
|
|
3644
|
-
const
|
|
7190
|
+
const path13 = firstEntity?.node.path ?? "unknown";
|
|
3645
7191
|
let neighborCount = 0;
|
|
3646
7192
|
const firstContext = context[0];
|
|
3647
7193
|
if (firstContext && Array.isArray(firstContext.nodes)) {
|
|
3648
7194
|
neighborCount = firstContext.nodes.length;
|
|
3649
7195
|
}
|
|
3650
|
-
return `**${entityName}** is a ${nodeType} at \`${
|
|
7196
|
+
return `**${entityName}** is a ${nodeType} at \`${path13}\`. Connected to ${neighborCount} nodes.`;
|
|
3651
7197
|
}
|
|
3652
7198
|
formatAnomaly(data) {
|
|
3653
7199
|
const d = data;
|
|
@@ -4030,7 +7576,7 @@ var PHASE_NODE_TYPES = {
|
|
|
4030
7576
|
debug: ["failure", "learning", "function", "method"],
|
|
4031
7577
|
plan: ["adr", "document", "module", "layer"]
|
|
4032
7578
|
};
|
|
4033
|
-
var
|
|
7579
|
+
var CODE_NODE_TYPES6 = /* @__PURE__ */ new Set([
|
|
4034
7580
|
"file",
|
|
4035
7581
|
"function",
|
|
4036
7582
|
"class",
|
|
@@ -4249,7 +7795,7 @@ var Assembler = class {
|
|
|
4249
7795
|
*/
|
|
4250
7796
|
checkCoverage() {
|
|
4251
7797
|
const codeNodes = [];
|
|
4252
|
-
for (const type of
|
|
7798
|
+
for (const type of CODE_NODE_TYPES6) {
|
|
4253
7799
|
codeNodes.push(...this.store.findNodes({ type }));
|
|
4254
7800
|
}
|
|
4255
7801
|
const documented = [];
|
|
@@ -4368,7 +7914,7 @@ function queryTraceability(store, options) {
|
|
|
4368
7914
|
|
|
4369
7915
|
// src/constraints/GraphConstraintAdapter.ts
|
|
4370
7916
|
import { minimatch } from "minimatch";
|
|
4371
|
-
import { relative as
|
|
7917
|
+
import { relative as relative5 } from "path";
|
|
4372
7918
|
var GraphConstraintAdapter = class {
|
|
4373
7919
|
constructor(store) {
|
|
4374
7920
|
this.store = store;
|
|
@@ -4397,8 +7943,8 @@ var GraphConstraintAdapter = class {
|
|
|
4397
7943
|
const { edges } = this.computeDependencyGraph();
|
|
4398
7944
|
const violations = [];
|
|
4399
7945
|
for (const edge of edges) {
|
|
4400
|
-
const fromRelative =
|
|
4401
|
-
const toRelative =
|
|
7946
|
+
const fromRelative = relative5(rootDir, edge.from).replaceAll("\\", "/");
|
|
7947
|
+
const toRelative = relative5(rootDir, edge.to).replaceAll("\\", "/");
|
|
4402
7948
|
const fromLayer = this.resolveLayer(fromRelative, layers);
|
|
4403
7949
|
const toLayer = this.resolveLayer(toRelative, layers);
|
|
4404
7950
|
if (!fromLayer || !toLayer) continue;
|
|
@@ -4429,14 +7975,14 @@ var GraphConstraintAdapter = class {
|
|
|
4429
7975
|
};
|
|
4430
7976
|
|
|
4431
7977
|
// src/ingest/DesignIngestor.ts
|
|
4432
|
-
import * as
|
|
4433
|
-
import * as
|
|
7978
|
+
import * as fs11 from "fs/promises";
|
|
7979
|
+
import * as path12 from "path";
|
|
4434
7980
|
function isDTCGToken(obj) {
|
|
4435
7981
|
return typeof obj === "object" && obj !== null && "$value" in obj && "$type" in obj;
|
|
4436
7982
|
}
|
|
4437
7983
|
async function readFileOrNull(filePath) {
|
|
4438
7984
|
try {
|
|
4439
|
-
return await
|
|
7985
|
+
return await fs11.readFile(filePath, "utf-8");
|
|
4440
7986
|
} catch {
|
|
4441
7987
|
return null;
|
|
4442
7988
|
}
|
|
@@ -4582,8 +8128,8 @@ var DesignIngestor = class {
|
|
|
4582
8128
|
async ingestAll(designDir) {
|
|
4583
8129
|
const start = Date.now();
|
|
4584
8130
|
const [tokensResult, intentResult] = await Promise.all([
|
|
4585
|
-
this.ingestTokens(
|
|
4586
|
-
this.ingestDesignIntent(
|
|
8131
|
+
this.ingestTokens(path12.join(designDir, "tokens.json")),
|
|
8132
|
+
this.ingestDesignIntent(path12.join(designDir, "DESIGN.md"))
|
|
4587
8133
|
]);
|
|
4588
8134
|
const merged = mergeResults(tokensResult, intentResult);
|
|
4589
8135
|
return { ...merged, durationMs: Date.now() - start };
|
|
@@ -4819,10 +8365,10 @@ var TaskIndependenceAnalyzer = class {
|
|
|
4819
8365
|
includeTypes: ["file"]
|
|
4820
8366
|
});
|
|
4821
8367
|
for (const n of queryResult.nodes) {
|
|
4822
|
-
const
|
|
4823
|
-
if (!fileSet.has(
|
|
4824
|
-
if (!result.has(
|
|
4825
|
-
result.set(
|
|
8368
|
+
const path13 = n.path ?? n.id.replace(/^file:/, "");
|
|
8369
|
+
if (!fileSet.has(path13)) {
|
|
8370
|
+
if (!result.has(path13)) {
|
|
8371
|
+
result.set(path13, file);
|
|
4826
8372
|
}
|
|
4827
8373
|
}
|
|
4828
8374
|
}
|
|
@@ -5201,7 +8747,10 @@ var ConflictPredictor = class {
|
|
|
5201
8747
|
// src/index.ts
|
|
5202
8748
|
var VERSION = "0.4.3";
|
|
5203
8749
|
export {
|
|
8750
|
+
ALL_EXTRACTORS,
|
|
8751
|
+
ApiPathExtractor,
|
|
5204
8752
|
Assembler,
|
|
8753
|
+
BusinessKnowledgeIngestor,
|
|
5205
8754
|
CIConnector,
|
|
5206
8755
|
CURRENT_SCHEMA_VERSION,
|
|
5207
8756
|
CascadeSimulator,
|
|
@@ -5210,11 +8759,18 @@ export {
|
|
|
5210
8759
|
ConflictPredictor,
|
|
5211
8760
|
ConfluenceConnector,
|
|
5212
8761
|
ContextQL,
|
|
8762
|
+
ContradictionDetector,
|
|
8763
|
+
CoverageScorer,
|
|
8764
|
+
D2Parser,
|
|
5213
8765
|
DesignConstraintAdapter,
|
|
5214
8766
|
DesignIngestor,
|
|
8767
|
+
DiagramParser,
|
|
5215
8768
|
EDGE_TYPES,
|
|
5216
8769
|
EntityExtractor,
|
|
5217
8770
|
EntityResolver,
|
|
8771
|
+
EnumConstantExtractor,
|
|
8772
|
+
ExtractionRunner,
|
|
8773
|
+
FigmaConnector,
|
|
5218
8774
|
FusionLayer,
|
|
5219
8775
|
GitIngestor,
|
|
5220
8776
|
GraphAnomalyAdapter,
|
|
@@ -5227,23 +8783,34 @@ export {
|
|
|
5227
8783
|
GraphNodeSchema,
|
|
5228
8784
|
GraphStore,
|
|
5229
8785
|
INTENTS,
|
|
8786
|
+
ImageAnalysisExtractor,
|
|
5230
8787
|
IntentClassifier,
|
|
5231
8788
|
JiraConnector,
|
|
5232
8789
|
KnowledgeIngestor,
|
|
8790
|
+
KnowledgePipelineRunner,
|
|
8791
|
+
KnowledgeStagingAggregator,
|
|
8792
|
+
MermaidParser,
|
|
8793
|
+
MiroConnector,
|
|
5233
8794
|
NODE_STABILITY,
|
|
5234
8795
|
NODE_TYPES,
|
|
5235
8796
|
OBSERVABILITY_TYPES,
|
|
5236
8797
|
PackedSummaryCache,
|
|
8798
|
+
PlantUmlParser,
|
|
5237
8799
|
RequirementIngestor,
|
|
5238
8800
|
ResponseFormatter,
|
|
5239
8801
|
SlackConnector,
|
|
8802
|
+
StructuralDriftDetector,
|
|
5240
8803
|
SyncManager,
|
|
5241
8804
|
TaskIndependenceAnalyzer,
|
|
8805
|
+
TestDescriptionExtractor,
|
|
5242
8806
|
TopologicalLinker,
|
|
5243
8807
|
VERSION,
|
|
8808
|
+
ValidationRuleExtractor,
|
|
5244
8809
|
VectorStore,
|
|
5245
8810
|
askGraph,
|
|
5246
8811
|
classifyNodeCategory,
|
|
8812
|
+
createExtractionRunner,
|
|
8813
|
+
detectLanguage,
|
|
5247
8814
|
groupNodesByImpact,
|
|
5248
8815
|
linkToCode,
|
|
5249
8816
|
loadGraph,
|