@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.js
CHANGED
|
@@ -30,7 +30,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
ALL_EXTRACTORS: () => ALL_EXTRACTORS,
|
|
34
|
+
ApiPathExtractor: () => ApiPathExtractor,
|
|
33
35
|
Assembler: () => Assembler,
|
|
36
|
+
BusinessKnowledgeIngestor: () => BusinessKnowledgeIngestor,
|
|
34
37
|
CIConnector: () => CIConnector,
|
|
35
38
|
CURRENT_SCHEMA_VERSION: () => CURRENT_SCHEMA_VERSION,
|
|
36
39
|
CascadeSimulator: () => CascadeSimulator,
|
|
@@ -39,11 +42,18 @@ __export(index_exports, {
|
|
|
39
42
|
ConflictPredictor: () => ConflictPredictor,
|
|
40
43
|
ConfluenceConnector: () => ConfluenceConnector,
|
|
41
44
|
ContextQL: () => ContextQL,
|
|
45
|
+
ContradictionDetector: () => ContradictionDetector,
|
|
46
|
+
CoverageScorer: () => CoverageScorer,
|
|
47
|
+
D2Parser: () => D2Parser,
|
|
42
48
|
DesignConstraintAdapter: () => DesignConstraintAdapter,
|
|
43
49
|
DesignIngestor: () => DesignIngestor,
|
|
50
|
+
DiagramParser: () => DiagramParser,
|
|
44
51
|
EDGE_TYPES: () => EDGE_TYPES,
|
|
45
52
|
EntityExtractor: () => EntityExtractor,
|
|
46
53
|
EntityResolver: () => EntityResolver,
|
|
54
|
+
EnumConstantExtractor: () => EnumConstantExtractor,
|
|
55
|
+
ExtractionRunner: () => ExtractionRunner,
|
|
56
|
+
FigmaConnector: () => FigmaConnector,
|
|
47
57
|
FusionLayer: () => FusionLayer,
|
|
48
58
|
GitIngestor: () => GitIngestor,
|
|
49
59
|
GraphAnomalyAdapter: () => GraphAnomalyAdapter,
|
|
@@ -56,23 +66,34 @@ __export(index_exports, {
|
|
|
56
66
|
GraphNodeSchema: () => GraphNodeSchema,
|
|
57
67
|
GraphStore: () => GraphStore,
|
|
58
68
|
INTENTS: () => INTENTS,
|
|
69
|
+
ImageAnalysisExtractor: () => ImageAnalysisExtractor,
|
|
59
70
|
IntentClassifier: () => IntentClassifier,
|
|
60
71
|
JiraConnector: () => JiraConnector,
|
|
61
72
|
KnowledgeIngestor: () => KnowledgeIngestor,
|
|
73
|
+
KnowledgePipelineRunner: () => KnowledgePipelineRunner,
|
|
74
|
+
KnowledgeStagingAggregator: () => KnowledgeStagingAggregator,
|
|
75
|
+
MermaidParser: () => MermaidParser,
|
|
76
|
+
MiroConnector: () => MiroConnector,
|
|
62
77
|
NODE_STABILITY: () => NODE_STABILITY,
|
|
63
78
|
NODE_TYPES: () => NODE_TYPES,
|
|
64
79
|
OBSERVABILITY_TYPES: () => OBSERVABILITY_TYPES,
|
|
65
80
|
PackedSummaryCache: () => PackedSummaryCache,
|
|
81
|
+
PlantUmlParser: () => PlantUmlParser,
|
|
66
82
|
RequirementIngestor: () => RequirementIngestor,
|
|
67
83
|
ResponseFormatter: () => ResponseFormatter,
|
|
68
84
|
SlackConnector: () => SlackConnector,
|
|
85
|
+
StructuralDriftDetector: () => StructuralDriftDetector,
|
|
69
86
|
SyncManager: () => SyncManager,
|
|
70
87
|
TaskIndependenceAnalyzer: () => TaskIndependenceAnalyzer,
|
|
88
|
+
TestDescriptionExtractor: () => TestDescriptionExtractor,
|
|
71
89
|
TopologicalLinker: () => TopologicalLinker,
|
|
72
90
|
VERSION: () => VERSION,
|
|
91
|
+
ValidationRuleExtractor: () => ValidationRuleExtractor,
|
|
73
92
|
VectorStore: () => VectorStore,
|
|
74
93
|
askGraph: () => askGraph,
|
|
75
94
|
classifyNodeCategory: () => classifyNodeCategory,
|
|
95
|
+
createExtractionRunner: () => createExtractionRunner,
|
|
96
|
+
detectLanguage: () => detectLanguage,
|
|
76
97
|
groupNodesByImpact: () => groupNodesByImpact,
|
|
77
98
|
linkToCode: () => linkToCode,
|
|
78
99
|
loadGraph: () => loadGraph,
|
|
@@ -108,6 +129,7 @@ var NODE_TYPES = [
|
|
|
108
129
|
"commit",
|
|
109
130
|
"build",
|
|
110
131
|
"test_result",
|
|
132
|
+
"execution_outcome",
|
|
111
133
|
// Observability (future)
|
|
112
134
|
"span",
|
|
113
135
|
"metric",
|
|
@@ -121,8 +143,16 @@ var NODE_TYPES = [
|
|
|
121
143
|
"design_token",
|
|
122
144
|
"aesthetic_intent",
|
|
123
145
|
"design_constraint",
|
|
146
|
+
"image_annotation",
|
|
124
147
|
// Traceability
|
|
125
148
|
"requirement",
|
|
149
|
+
// Business Knowledge
|
|
150
|
+
"business_rule",
|
|
151
|
+
"business_process",
|
|
152
|
+
"business_concept",
|
|
153
|
+
"business_term",
|
|
154
|
+
"business_metric",
|
|
155
|
+
"business_fact",
|
|
126
156
|
// Cache
|
|
127
157
|
"packed_summary"
|
|
128
158
|
];
|
|
@@ -146,6 +176,7 @@ var EDGE_TYPES = [
|
|
|
146
176
|
"co_changes_with",
|
|
147
177
|
"triggered_by",
|
|
148
178
|
"failed_in",
|
|
179
|
+
"outcome_of",
|
|
149
180
|
// Execution relationships (future)
|
|
150
181
|
"executed_by",
|
|
151
182
|
"measured_by",
|
|
@@ -158,6 +189,10 @@ var EDGE_TYPES = [
|
|
|
158
189
|
"requires",
|
|
159
190
|
"verified_by",
|
|
160
191
|
"tested_by",
|
|
192
|
+
// Business Knowledge relationships
|
|
193
|
+
"governs",
|
|
194
|
+
"measures",
|
|
195
|
+
"annotates",
|
|
161
196
|
// Cache relationships
|
|
162
197
|
"caches"
|
|
163
198
|
];
|
|
@@ -200,10 +235,28 @@ var GraphEdgeSchema = import_zod.z.object({
|
|
|
200
235
|
|
|
201
236
|
// src/store/Serializer.ts
|
|
202
237
|
var import_promises = require("fs/promises");
|
|
238
|
+
var import_node_fs = require("fs");
|
|
203
239
|
var import_node_path = require("path");
|
|
240
|
+
function streamGraphJson(filePath, nodes, edges) {
|
|
241
|
+
return new Promise((resolve, reject) => {
|
|
242
|
+
const stream = (0, import_node_fs.createWriteStream)(filePath, { encoding: "utf-8" });
|
|
243
|
+
stream.on("error", reject);
|
|
244
|
+
stream.write('{"nodes":[');
|
|
245
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
246
|
+
if (i > 0) stream.write(",");
|
|
247
|
+
stream.write(JSON.stringify(nodes[i]));
|
|
248
|
+
}
|
|
249
|
+
stream.write('],"edges":[');
|
|
250
|
+
for (let i = 0; i < edges.length; i++) {
|
|
251
|
+
if (i > 0) stream.write(",");
|
|
252
|
+
stream.write(JSON.stringify(edges[i]));
|
|
253
|
+
}
|
|
254
|
+
stream.write("]}");
|
|
255
|
+
stream.end(resolve);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
204
258
|
async function saveGraph(dirPath, nodes, edges) {
|
|
205
259
|
await (0, import_promises.mkdir)(dirPath, { recursive: true });
|
|
206
|
-
const graphData = { nodes, edges };
|
|
207
260
|
const metadata = {
|
|
208
261
|
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
209
262
|
lastScanTimestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -211,7 +264,7 @@ async function saveGraph(dirPath, nodes, edges) {
|
|
|
211
264
|
edgeCount: edges.length
|
|
212
265
|
};
|
|
213
266
|
await Promise.all([
|
|
214
|
-
(
|
|
267
|
+
streamGraphJson((0, import_node_path.join)(dirPath, "graph.json"), nodes, edges),
|
|
215
268
|
(0, import_promises.writeFile)((0, import_node_path.join)(dirPath, "metadata.json"), JSON.stringify(metadata, null, 2), "utf-8")
|
|
216
269
|
]);
|
|
217
270
|
}
|
|
@@ -400,8 +453,8 @@ var GraphStore = class {
|
|
|
400
453
|
}
|
|
401
454
|
// --- Persistence ---
|
|
402
455
|
async save(dirPath) {
|
|
403
|
-
const allNodes = Array.from(this.nodeMap.values())
|
|
404
|
-
const allEdges = Array.from(this.edgeMap.values())
|
|
456
|
+
const allNodes = Array.from(this.nodeMap.values());
|
|
457
|
+
const allEdges = Array.from(this.edgeMap.values());
|
|
405
458
|
await saveGraph(dirPath, allNodes, allEdges);
|
|
406
459
|
}
|
|
407
460
|
async load(dirPath) {
|
|
@@ -770,6 +823,49 @@ function groupNodesByImpact(nodes, excludeId) {
|
|
|
770
823
|
var fs = __toESM(require("fs/promises"));
|
|
771
824
|
var path = __toESM(require("path"));
|
|
772
825
|
var SKIP_METHOD_NAMES = /* @__PURE__ */ new Set(["constructor", "if", "for", "while", "switch"]);
|
|
826
|
+
var FUNCTION_PATTERNS = {
|
|
827
|
+
python: /^def\s+(\w+)\s*\(/,
|
|
828
|
+
go: /^func\s+(\w+)\s*[[(]/,
|
|
829
|
+
rust: /^(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/,
|
|
830
|
+
typescript: /(?:export\s+)?(?:async\s+)?function\s+(\w+)/,
|
|
831
|
+
javascript: /(?:export\s+)?(?:async\s+)?function\s+(\w+)/
|
|
832
|
+
};
|
|
833
|
+
var CLASS_PATTERNS = {
|
|
834
|
+
python: /^class\s+(\w+)/,
|
|
835
|
+
go: /^type\s+(\w+)\s+struct\b/,
|
|
836
|
+
rust: /^(?:pub\s+)?struct\s+(\w+)/,
|
|
837
|
+
java: /(?:public|private|protected)?\s*(?:abstract\s+|final\s+)?class\s+(\w+)/,
|
|
838
|
+
typescript: /(?:export\s+)?class\s+(\w+)/,
|
|
839
|
+
javascript: /(?:export\s+)?class\s+(\w+)/
|
|
840
|
+
};
|
|
841
|
+
var INTERFACE_PATTERNS = {
|
|
842
|
+
go: /^type\s+(\w+)\s+interface\b/,
|
|
843
|
+
rust: /^(?:pub\s+)?trait\s+(\w+)/,
|
|
844
|
+
java: /(?:public\s+)?interface\s+(\w+)/,
|
|
845
|
+
typescript: /(?:export\s+)?interface\s+(\w+)/,
|
|
846
|
+
javascript: /(?:export\s+)?interface\s+(\w+)/
|
|
847
|
+
};
|
|
848
|
+
var METHOD_PATTERNS = {
|
|
849
|
+
python: /^\s+def\s+(\w+)\s*\(/,
|
|
850
|
+
rust: /^\s+(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/,
|
|
851
|
+
java: /^\s+(?:(?:public|private|protected|static|final|abstract|synchronized)\s+)*(?:\w+(?:<[^>]*>)?)\s+(\w+)\s*\(/,
|
|
852
|
+
typescript: /^\s+(?:(?:public|private|protected|readonly|static|abstract)\s+)*(?:async\s+)?(\w+)\s*\(/,
|
|
853
|
+
javascript: /^\s+(?:(?:public|private|protected|readonly|static|abstract)\s+)*(?:async\s+)?(\w+)\s*\(/
|
|
854
|
+
};
|
|
855
|
+
var GO_METHOD_PATTERN = /^func\s+\(\w+\s+\*?(\w+)\)\s+(\w+)\s*\(/;
|
|
856
|
+
var VARIABLE_PATTERNS = {
|
|
857
|
+
python: /^([A-Z]\w*)\s*=/,
|
|
858
|
+
go: /^(?:var|const)\s+(\w+)/,
|
|
859
|
+
rust: /^(?:pub\s+)?(?:const|static)\s+(\w+)/,
|
|
860
|
+
typescript: /(?:export\s+)?(?:const|let|var)\s+(\w+)/,
|
|
861
|
+
javascript: /(?:export\s+)?(?:const|let|var)\s+(\w+)/
|
|
862
|
+
};
|
|
863
|
+
function isExported(lang, line, name) {
|
|
864
|
+
if (lang === "go") return /^[A-Z]/.test(name);
|
|
865
|
+
if (lang === "rust") return /^\s*pub\b/.test(line);
|
|
866
|
+
if (lang === "java") return /\bpublic\b/.test(line);
|
|
867
|
+
return line.includes("export");
|
|
868
|
+
}
|
|
773
869
|
function countBraces(line) {
|
|
774
870
|
let net = 0;
|
|
775
871
|
for (const ch of line) {
|
|
@@ -778,6 +874,23 @@ function countBraces(line) {
|
|
|
778
874
|
}
|
|
779
875
|
return net;
|
|
780
876
|
}
|
|
877
|
+
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
878
|
+
".ts",
|
|
879
|
+
".tsx",
|
|
880
|
+
".js",
|
|
881
|
+
".jsx",
|
|
882
|
+
".py",
|
|
883
|
+
".go",
|
|
884
|
+
".rs",
|
|
885
|
+
".java",
|
|
886
|
+
".groovy"
|
|
887
|
+
]);
|
|
888
|
+
var SKIP_EXTENSIONS = /* @__PURE__ */ new Set([".d.ts"]);
|
|
889
|
+
function isSupportedSourceFile(name) {
|
|
890
|
+
if (SKIP_EXTENSIONS.has(name.slice(name.lastIndexOf(".")))) return false;
|
|
891
|
+
const ext = name.slice(name.lastIndexOf("."));
|
|
892
|
+
return SUPPORTED_EXTENSIONS.has(ext);
|
|
893
|
+
}
|
|
781
894
|
var CodeIngestor = class {
|
|
782
895
|
constructor(store) {
|
|
783
896
|
this.store = store;
|
|
@@ -790,22 +903,34 @@ var CodeIngestor = class {
|
|
|
790
903
|
let edgesAdded = 0;
|
|
791
904
|
const files = await this.findSourceFiles(rootDir);
|
|
792
905
|
const nameToFiles = /* @__PURE__ */ new Map();
|
|
793
|
-
const fileContents = /* @__PURE__ */ new Map();
|
|
794
906
|
for (const filePath of files) {
|
|
795
907
|
try {
|
|
796
|
-
const result = await this.processFile(filePath, rootDir, nameToFiles
|
|
908
|
+
const result = await this.processFile(filePath, rootDir, nameToFiles);
|
|
797
909
|
nodesAdded += result.nodesAdded;
|
|
798
910
|
edgesAdded += result.edgesAdded;
|
|
799
911
|
} catch (err) {
|
|
800
912
|
errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
801
913
|
}
|
|
802
914
|
}
|
|
803
|
-
const
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
915
|
+
for (const filePath of files) {
|
|
916
|
+
try {
|
|
917
|
+
const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
918
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
919
|
+
const callsEdges = this.extractCallsEdgesForFile(relativePath, content, nameToFiles);
|
|
920
|
+
for (const edge of callsEdges) {
|
|
921
|
+
this.store.addEdge(edge);
|
|
922
|
+
edgesAdded++;
|
|
923
|
+
}
|
|
924
|
+
} catch {
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
for (const filePath of files) {
|
|
928
|
+
try {
|
|
929
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
930
|
+
edgesAdded += this.extractReqAnnotationsForFile(filePath, content, rootDir);
|
|
931
|
+
} catch {
|
|
932
|
+
}
|
|
807
933
|
}
|
|
808
|
-
edgesAdded += this.extractReqAnnotations(fileContents, rootDir);
|
|
809
934
|
return {
|
|
810
935
|
nodesAdded,
|
|
811
936
|
nodesUpdated: 0,
|
|
@@ -815,14 +940,13 @@ var CodeIngestor = class {
|
|
|
815
940
|
durationMs: Date.now() - start
|
|
816
941
|
};
|
|
817
942
|
}
|
|
818
|
-
async processFile(filePath, rootDir, nameToFiles
|
|
943
|
+
async processFile(filePath, rootDir, nameToFiles) {
|
|
819
944
|
let nodesAdded = 0;
|
|
820
945
|
let edgesAdded = 0;
|
|
821
946
|
const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
822
947
|
const content = await fs.readFile(filePath, "utf-8");
|
|
823
948
|
const stat2 = await fs.stat(filePath);
|
|
824
949
|
const fileId = `file:${relativePath}`;
|
|
825
|
-
fileContents.set(relativePath, content);
|
|
826
950
|
const fileNode = {
|
|
827
951
|
id: fileId,
|
|
828
952
|
type: "file",
|
|
@@ -862,9 +986,9 @@ var CodeIngestor = class {
|
|
|
862
986
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
863
987
|
for (const entry of entries) {
|
|
864
988
|
const fullPath = path.join(dir, entry.name);
|
|
865
|
-
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "dist") {
|
|
989
|
+
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") {
|
|
866
990
|
results.push(...await this.findSourceFiles(fullPath));
|
|
867
|
-
} else if (entry.isFile() &&
|
|
991
|
+
} else if (entry.isFile() && isSupportedSourceFile(entry.name)) {
|
|
868
992
|
results.push(fullPath);
|
|
869
993
|
}
|
|
870
994
|
}
|
|
@@ -873,25 +997,40 @@ var CodeIngestor = class {
|
|
|
873
997
|
extractSymbols(content, fileId, relativePath) {
|
|
874
998
|
const results = [];
|
|
875
999
|
const lines = content.split("\n");
|
|
1000
|
+
const lang = this.detectLanguage(relativePath);
|
|
876
1001
|
const ctx = { className: null, classId: null, insideClass: false, braceDepth: 0 };
|
|
877
1002
|
for (let i = 0; i < lines.length; i++) {
|
|
878
1003
|
const line = lines[i];
|
|
879
|
-
|
|
880
|
-
if (this.tryExtractClass(line, lines, i, fileId, relativePath, ctx, results)) continue;
|
|
881
|
-
if (this.tryExtractInterface(line, lines, i, fileId, relativePath, ctx, results)) continue;
|
|
882
|
-
if (this.updateClassContext(line, ctx)) continue;
|
|
883
|
-
if (this.tryExtractMethod(line, lines, i, fileId, relativePath, ctx, results)) continue;
|
|
884
|
-
if (ctx.insideClass) continue;
|
|
885
|
-
this.tryExtractVariable(line, i, fileId, relativePath, results);
|
|
1004
|
+
this.processSymbolLine(line, lines, i, fileId, relativePath, ctx, results, lang);
|
|
886
1005
|
}
|
|
887
1006
|
return results;
|
|
888
1007
|
}
|
|
889
|
-
|
|
890
|
-
|
|
1008
|
+
processSymbolLine(line, lines, i, fileId, relativePath, ctx, results, lang) {
|
|
1009
|
+
if (lang === "python") {
|
|
1010
|
+
this.updatePythonClassContext(lines, i, ctx);
|
|
1011
|
+
}
|
|
1012
|
+
if (this.tryExtractFunction(line, lines, i, fileId, relativePath, ctx, results, lang)) return;
|
|
1013
|
+
if (this.tryExtractClass(line, lines, i, fileId, relativePath, ctx, results, lang)) return;
|
|
1014
|
+
if (this.tryExtractInterface(line, lines, i, fileId, relativePath, ctx, results, lang)) return;
|
|
1015
|
+
if (this.tryEnterImplBlock(line, lang, relativePath, ctx)) return;
|
|
1016
|
+
if (lang !== "python" && this.updateClassContext(line, ctx)) return;
|
|
1017
|
+
if (this.tryExtractMethod(line, lines, i, fileId, relativePath, ctx, results, lang)) return;
|
|
1018
|
+
if (ctx.insideClass) return;
|
|
1019
|
+
this.tryExtractVariable(line, i, fileId, relativePath, results, lang);
|
|
1020
|
+
}
|
|
1021
|
+
matchFunction(line, lang) {
|
|
1022
|
+
if (lang === "java") return null;
|
|
1023
|
+
const pattern = FUNCTION_PATTERNS[lang];
|
|
1024
|
+
return pattern ? line.match(pattern) : null;
|
|
1025
|
+
}
|
|
1026
|
+
tryExtractFunction(line, lines, i, fileId, relativePath, ctx, results, lang) {
|
|
1027
|
+
if (ctx.insideClass) return false;
|
|
1028
|
+
const fnMatch = this.matchFunction(line, lang);
|
|
891
1029
|
if (!fnMatch) return false;
|
|
892
1030
|
const name = fnMatch[1];
|
|
893
1031
|
const id = `function:${relativePath}:${name}`;
|
|
894
|
-
const endLine = this.findClosingBrace(lines, i);
|
|
1032
|
+
const endLine = lang === "python" ? this.findEndOfIndentBlock(lines, i) : this.findClosingBrace(lines, i);
|
|
1033
|
+
const exported = isExported(lang, line, name);
|
|
895
1034
|
results.push({
|
|
896
1035
|
node: {
|
|
897
1036
|
id,
|
|
@@ -900,7 +1039,7 @@ var CodeIngestor = class {
|
|
|
900
1039
|
path: relativePath,
|
|
901
1040
|
location: { fileId, startLine: i + 1, endLine },
|
|
902
1041
|
metadata: {
|
|
903
|
-
exported
|
|
1042
|
+
exported,
|
|
904
1043
|
cyclomaticComplexity: this.computeCyclomaticComplexity(lines.slice(i, endLine)),
|
|
905
1044
|
nestingDepth: this.computeMaxNesting(lines.slice(i, endLine)),
|
|
906
1045
|
lineCount: endLine - i,
|
|
@@ -915,12 +1054,17 @@ var CodeIngestor = class {
|
|
|
915
1054
|
}
|
|
916
1055
|
return true;
|
|
917
1056
|
}
|
|
918
|
-
|
|
919
|
-
const
|
|
1057
|
+
matchClass(line, lang) {
|
|
1058
|
+
const pattern = CLASS_PATTERNS[lang];
|
|
1059
|
+
return pattern ? line.match(pattern) : null;
|
|
1060
|
+
}
|
|
1061
|
+
tryExtractClass(line, lines, i, fileId, relativePath, ctx, results, lang) {
|
|
1062
|
+
const classMatch = this.matchClass(line, lang);
|
|
920
1063
|
if (!classMatch) return false;
|
|
921
1064
|
const name = classMatch[1];
|
|
922
1065
|
const id = `class:${relativePath}:${name}`;
|
|
923
|
-
const endLine = this.findClosingBrace(lines, i);
|
|
1066
|
+
const endLine = lang === "python" ? this.findEndOfIndentBlock(lines, i) : this.findClosingBrace(lines, i);
|
|
1067
|
+
const exported = isExported(lang, line, name);
|
|
924
1068
|
results.push({
|
|
925
1069
|
node: {
|
|
926
1070
|
id,
|
|
@@ -928,22 +1072,27 @@ var CodeIngestor = class {
|
|
|
928
1072
|
name,
|
|
929
1073
|
path: relativePath,
|
|
930
1074
|
location: { fileId, startLine: i + 1, endLine },
|
|
931
|
-
metadata: { exported
|
|
1075
|
+
metadata: { exported }
|
|
932
1076
|
},
|
|
933
1077
|
edge: { from: fileId, to: id, type: "contains" }
|
|
934
1078
|
});
|
|
935
1079
|
ctx.className = name;
|
|
936
1080
|
ctx.classId = id;
|
|
937
1081
|
ctx.insideClass = true;
|
|
938
|
-
ctx.braceDepth = countBraces(line);
|
|
1082
|
+
ctx.braceDepth = lang === "python" ? 1 : countBraces(line);
|
|
939
1083
|
return true;
|
|
940
1084
|
}
|
|
941
|
-
|
|
942
|
-
const
|
|
1085
|
+
matchInterface(line, lang) {
|
|
1086
|
+
const pattern = INTERFACE_PATTERNS[lang];
|
|
1087
|
+
return pattern ? line.match(pattern) : null;
|
|
1088
|
+
}
|
|
1089
|
+
tryExtractInterface(line, lines, i, fileId, relativePath, ctx, results, lang) {
|
|
1090
|
+
const ifaceMatch = this.matchInterface(line, lang);
|
|
943
1091
|
if (!ifaceMatch) return false;
|
|
944
1092
|
const name = ifaceMatch[1];
|
|
945
1093
|
const id = `interface:${relativePath}:${name}`;
|
|
946
1094
|
const endLine = this.findClosingBrace(lines, i);
|
|
1095
|
+
const exported = isExported(lang, line, name);
|
|
947
1096
|
results.push({
|
|
948
1097
|
node: {
|
|
949
1098
|
id,
|
|
@@ -951,7 +1100,7 @@ var CodeIngestor = class {
|
|
|
951
1100
|
name,
|
|
952
1101
|
path: relativePath,
|
|
953
1102
|
location: { fileId, startLine: i + 1, endLine },
|
|
954
|
-
metadata: { exported
|
|
1103
|
+
metadata: { exported }
|
|
955
1104
|
},
|
|
956
1105
|
edge: { from: fileId, to: id, type: "contains" }
|
|
957
1106
|
});
|
|
@@ -960,6 +1109,23 @@ var CodeIngestor = class {
|
|
|
960
1109
|
ctx.insideClass = false;
|
|
961
1110
|
return true;
|
|
962
1111
|
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Enter an impl block (Rust) or Java class body — sets class context without creating a node.
|
|
1114
|
+
*/
|
|
1115
|
+
tryEnterImplBlock(line, lang, relativePath, ctx) {
|
|
1116
|
+
if (lang === "rust") {
|
|
1117
|
+
const implMatch = line.match(/^impl(?:<[^>]*>)?\s+(\w+)/);
|
|
1118
|
+
if (implMatch) {
|
|
1119
|
+
const name = implMatch[1];
|
|
1120
|
+
ctx.className = name;
|
|
1121
|
+
ctx.classId = `class:${relativePath}:${name}`;
|
|
1122
|
+
ctx.insideClass = true;
|
|
1123
|
+
ctx.braceDepth = countBraces(line);
|
|
1124
|
+
return true;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
return false;
|
|
1128
|
+
}
|
|
963
1129
|
/** Update brace tracking; returns true when line is consumed (class ended or tracked). */
|
|
964
1130
|
updateClassContext(line, ctx) {
|
|
965
1131
|
if (!ctx.insideClass) return false;
|
|
@@ -972,16 +1138,32 @@ var CodeIngestor = class {
|
|
|
972
1138
|
}
|
|
973
1139
|
return false;
|
|
974
1140
|
}
|
|
975
|
-
|
|
976
|
-
if (
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
);
|
|
1141
|
+
matchMethod(line, lang, insideClass) {
|
|
1142
|
+
if (lang === "go") return line.match(GO_METHOD_PATTERN);
|
|
1143
|
+
if (!insideClass) return null;
|
|
1144
|
+
const pattern = METHOD_PATTERNS[lang];
|
|
1145
|
+
return pattern ? line.match(pattern) : null;
|
|
1146
|
+
}
|
|
1147
|
+
tryExtractMethod(line, lines, i, fileId, relativePath, ctx, results, lang) {
|
|
1148
|
+
const methodMatch = this.matchMethod(line, lang, ctx.insideClass);
|
|
980
1149
|
if (!methodMatch) return false;
|
|
981
|
-
|
|
1150
|
+
let methodName;
|
|
1151
|
+
let className;
|
|
1152
|
+
let classId;
|
|
1153
|
+
if (lang === "go") {
|
|
1154
|
+
const receiverType = methodMatch[1];
|
|
1155
|
+
methodName = methodMatch[2];
|
|
1156
|
+
className = receiverType;
|
|
1157
|
+
classId = `class:${relativePath}:${receiverType}`;
|
|
1158
|
+
} else {
|
|
1159
|
+
if (!ctx.insideClass || !ctx.className || !ctx.classId) return false;
|
|
1160
|
+
methodName = methodMatch[1];
|
|
1161
|
+
className = ctx.className;
|
|
1162
|
+
classId = ctx.classId;
|
|
1163
|
+
}
|
|
982
1164
|
if (SKIP_METHOD_NAMES.has(methodName)) return false;
|
|
983
|
-
const id = `method:${relativePath}:${
|
|
984
|
-
const endLine = this.findClosingBrace(lines, i);
|
|
1165
|
+
const id = `method:${relativePath}:${className}.${methodName}`;
|
|
1166
|
+
const endLine = lang === "python" ? this.findEndOfIndentBlock(lines, i) : this.findClosingBrace(lines, i);
|
|
985
1167
|
results.push({
|
|
986
1168
|
node: {
|
|
987
1169
|
id,
|
|
@@ -990,23 +1172,28 @@ var CodeIngestor = class {
|
|
|
990
1172
|
path: relativePath,
|
|
991
1173
|
location: { fileId, startLine: i + 1, endLine },
|
|
992
1174
|
metadata: {
|
|
993
|
-
className
|
|
994
|
-
exported:
|
|
1175
|
+
className,
|
|
1176
|
+
exported: isExported(lang, line, methodName),
|
|
995
1177
|
cyclomaticComplexity: this.computeCyclomaticComplexity(lines.slice(i, endLine)),
|
|
996
1178
|
nestingDepth: this.computeMaxNesting(lines.slice(i, endLine)),
|
|
997
1179
|
lineCount: endLine - i,
|
|
998
1180
|
parameterCount: this.countParameters(line)
|
|
999
1181
|
}
|
|
1000
1182
|
},
|
|
1001
|
-
edge: { from:
|
|
1183
|
+
edge: { from: classId, to: id, type: "contains" }
|
|
1002
1184
|
});
|
|
1003
1185
|
return true;
|
|
1004
1186
|
}
|
|
1005
|
-
|
|
1006
|
-
const
|
|
1187
|
+
matchVariable(line, lang) {
|
|
1188
|
+
const pattern = VARIABLE_PATTERNS[lang];
|
|
1189
|
+
return pattern ? line.match(pattern) : null;
|
|
1190
|
+
}
|
|
1191
|
+
tryExtractVariable(line, i, fileId, relativePath, results, lang) {
|
|
1192
|
+
const varMatch = this.matchVariable(line, lang);
|
|
1007
1193
|
if (!varMatch) return;
|
|
1008
1194
|
const name = varMatch[1];
|
|
1009
1195
|
const id = `variable:${relativePath}:${name}`;
|
|
1196
|
+
const exported = isExported(lang, line, name);
|
|
1010
1197
|
results.push({
|
|
1011
1198
|
node: {
|
|
1012
1199
|
id,
|
|
@@ -1014,7 +1201,7 @@ var CodeIngestor = class {
|
|
|
1014
1201
|
name,
|
|
1015
1202
|
path: relativePath,
|
|
1016
1203
|
location: { fileId, startLine: i + 1, endLine: i + 1 },
|
|
1017
|
-
metadata: { exported
|
|
1204
|
+
metadata: { exported }
|
|
1018
1205
|
},
|
|
1019
1206
|
edge: { from: fileId, to: id, type: "contains" }
|
|
1020
1207
|
});
|
|
@@ -1043,48 +1230,79 @@ var CodeIngestor = class {
|
|
|
1043
1230
|
return startIndex + 1;
|
|
1044
1231
|
}
|
|
1045
1232
|
/**
|
|
1046
|
-
*
|
|
1233
|
+
* Find the end of an indentation-based block (Python).
|
|
1234
|
+
* The block ends when a subsequent non-empty line has indentation <= the starting line.
|
|
1235
|
+
*/
|
|
1236
|
+
findEndOfIndentBlock(lines, startIndex) {
|
|
1237
|
+
const startLine = lines[startIndex] ?? "";
|
|
1238
|
+
const baseIndent = startLine.search(/\S/);
|
|
1239
|
+
let lastNonEmpty = startIndex;
|
|
1240
|
+
for (let i = startIndex + 1; i < lines.length; i++) {
|
|
1241
|
+
const line = lines[i];
|
|
1242
|
+
if (line.trim() === "") continue;
|
|
1243
|
+
const indent = line.search(/\S/);
|
|
1244
|
+
if (indent <= baseIndent) {
|
|
1245
|
+
return lastNonEmpty + 1;
|
|
1246
|
+
}
|
|
1247
|
+
lastNonEmpty = i;
|
|
1248
|
+
}
|
|
1249
|
+
return lastNonEmpty + 1;
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Update class context for Python using indentation tracking.
|
|
1253
|
+
*/
|
|
1254
|
+
updatePythonClassContext(lines, i, ctx) {
|
|
1255
|
+
if (!ctx.insideClass) return false;
|
|
1256
|
+
const line = lines[i];
|
|
1257
|
+
if (line.trim() === "") return false;
|
|
1258
|
+
const indent = line.search(/\S/);
|
|
1259
|
+
if (indent <= 0) {
|
|
1260
|
+
ctx.className = null;
|
|
1261
|
+
ctx.classId = null;
|
|
1262
|
+
ctx.insideClass = false;
|
|
1263
|
+
return false;
|
|
1264
|
+
}
|
|
1265
|
+
return false;
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Scan a file for identifiers matching known callable names,
|
|
1047
1269
|
* then create file-to-file "calls" edges. Uses regex heuristic (not AST).
|
|
1048
1270
|
*/
|
|
1049
|
-
|
|
1271
|
+
extractCallsEdgesForFile(relativePath, content, nameToFiles) {
|
|
1050
1272
|
const edges = [];
|
|
1051
1273
|
const seen = /* @__PURE__ */ new Set();
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
});
|
|
1072
|
-
}
|
|
1274
|
+
const callerFileId = `file:${relativePath}`;
|
|
1275
|
+
const callPattern = /\b([a-zA-Z_$][\w$]*)\s*\(/g;
|
|
1276
|
+
let match;
|
|
1277
|
+
while ((match = callPattern.exec(content)) !== null) {
|
|
1278
|
+
const name = match[1];
|
|
1279
|
+
const targetFiles = nameToFiles.get(name);
|
|
1280
|
+
if (!targetFiles) continue;
|
|
1281
|
+
for (const targetFile of targetFiles) {
|
|
1282
|
+
if (targetFile === relativePath) continue;
|
|
1283
|
+
const targetFileId = `file:${targetFile}`;
|
|
1284
|
+
const key = `${callerFileId}|${targetFileId}`;
|
|
1285
|
+
if (seen.has(key)) continue;
|
|
1286
|
+
seen.add(key);
|
|
1287
|
+
edges.push({
|
|
1288
|
+
from: callerFileId,
|
|
1289
|
+
to: targetFileId,
|
|
1290
|
+
type: "calls",
|
|
1291
|
+
metadata: { confidence: "regex" }
|
|
1292
|
+
});
|
|
1073
1293
|
}
|
|
1074
1294
|
}
|
|
1075
1295
|
return edges;
|
|
1076
1296
|
}
|
|
1077
1297
|
async extractImports(content, fileId, relativePath, rootDir) {
|
|
1298
|
+
const lang = this.detectLanguage(relativePath);
|
|
1078
1299
|
const edges = [];
|
|
1079
|
-
const
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
const importPath = match[1];
|
|
1083
|
-
if (!importPath.startsWith(".")) continue;
|
|
1300
|
+
const importPaths = this.extractImportPaths(content, lang);
|
|
1301
|
+
for (const { importPath, isTypeOnly } of importPaths) {
|
|
1302
|
+
if ((lang === "typescript" || lang === "javascript") && !importPath.startsWith(".")) continue;
|
|
1084
1303
|
const resolvedPath = await this.resolveImportPath(relativePath, importPath, rootDir);
|
|
1085
1304
|
if (resolvedPath) {
|
|
1086
1305
|
const targetId = `file:${resolvedPath}`;
|
|
1087
|
-
const isTypeOnly = match[0].includes("import type");
|
|
1088
1306
|
edges.push({
|
|
1089
1307
|
from: fileId,
|
|
1090
1308
|
to: targetId,
|
|
@@ -1095,10 +1313,53 @@ var CodeIngestor = class {
|
|
|
1095
1313
|
}
|
|
1096
1314
|
return edges;
|
|
1097
1315
|
}
|
|
1316
|
+
extractImportPaths(content, lang) {
|
|
1317
|
+
const results = [];
|
|
1318
|
+
if (lang === "typescript" || lang === "javascript") {
|
|
1319
|
+
const importRegex = /import\s+(?:type\s+)?(?:\{[^}]*\}|[\w*]+)\s+from\s+['"]([^'"]+)['"]/g;
|
|
1320
|
+
let match;
|
|
1321
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
1322
|
+
results.push({
|
|
1323
|
+
importPath: match[1],
|
|
1324
|
+
isTypeOnly: match[0].includes("import type")
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
} else if (lang === "python") {
|
|
1328
|
+
const fromImport = /(?:from\s+([\w.]+)\s+import|import\s+([\w.]+))/g;
|
|
1329
|
+
let match;
|
|
1330
|
+
while ((match = fromImport.exec(content)) !== null) {
|
|
1331
|
+
const importPath = match[1] ?? match[2];
|
|
1332
|
+
if (importPath.startsWith(".")) {
|
|
1333
|
+
results.push({ importPath, isTypeOnly: false });
|
|
1334
|
+
} else {
|
|
1335
|
+
results.push({ importPath: importPath.replace(/\./g, "/"), isTypeOnly: false });
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
} else if (lang === "go") {
|
|
1339
|
+
const goImport = /"([^"]+)"/g;
|
|
1340
|
+
let match;
|
|
1341
|
+
while ((match = goImport.exec(content)) !== null) {
|
|
1342
|
+
results.push({ importPath: match[1], isTypeOnly: false });
|
|
1343
|
+
}
|
|
1344
|
+
} else if (lang === "rust") {
|
|
1345
|
+
const useDecl = /use\s+((?:crate|super|self)(?:::\w+)+)/g;
|
|
1346
|
+
let match;
|
|
1347
|
+
while ((match = useDecl.exec(content)) !== null) {
|
|
1348
|
+
results.push({ importPath: match[1], isTypeOnly: false });
|
|
1349
|
+
}
|
|
1350
|
+
} else if (lang === "java") {
|
|
1351
|
+
const javaImport = /import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;/g;
|
|
1352
|
+
let match;
|
|
1353
|
+
while ((match = javaImport.exec(content)) !== null) {
|
|
1354
|
+
results.push({ importPath: match[1], isTypeOnly: false });
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
return results;
|
|
1358
|
+
}
|
|
1098
1359
|
async resolveImportPath(fromFile, importPath, rootDir) {
|
|
1099
1360
|
const fromDir = path.dirname(fromFile);
|
|
1100
1361
|
const resolved = path.normalize(path.join(fromDir, importPath)).replace(/\\/g, "/");
|
|
1101
|
-
const extensions = [".ts", ".tsx", ".js", ".jsx"];
|
|
1362
|
+
const extensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
|
|
1102
1363
|
for (const ext of extensions) {
|
|
1103
1364
|
const candidate = resolved.replace(/\.js$/, "") + ext;
|
|
1104
1365
|
const fullPath = path.join(rootDir, candidate);
|
|
@@ -1108,7 +1369,8 @@ var CodeIngestor = class {
|
|
|
1108
1369
|
} catch {
|
|
1109
1370
|
}
|
|
1110
1371
|
}
|
|
1111
|
-
|
|
1372
|
+
const indexExtensions = [".ts", ".tsx", ".js", ".jsx"];
|
|
1373
|
+
for (const ext of indexExtensions) {
|
|
1112
1374
|
const candidate = path.join(resolved, `index${ext}`).replace(/\\/g, "/");
|
|
1113
1375
|
const fullPath = path.join(rootDir, candidate);
|
|
1114
1376
|
try {
|
|
@@ -1117,6 +1379,12 @@ var CodeIngestor = class {
|
|
|
1117
1379
|
} catch {
|
|
1118
1380
|
}
|
|
1119
1381
|
}
|
|
1382
|
+
const pyInit = path.join(resolved, "__init__.py").replace(/\\/g, "/");
|
|
1383
|
+
try {
|
|
1384
|
+
await fs.access(path.join(rootDir, pyInit));
|
|
1385
|
+
return pyInit;
|
|
1386
|
+
} catch {
|
|
1387
|
+
}
|
|
1120
1388
|
return null;
|
|
1121
1389
|
}
|
|
1122
1390
|
computeCyclomaticComplexity(lines) {
|
|
@@ -1162,6 +1430,11 @@ var CodeIngestor = class {
|
|
|
1162
1430
|
detectLanguage(filePath) {
|
|
1163
1431
|
if (/\.tsx?$/.test(filePath)) return "typescript";
|
|
1164
1432
|
if (/\.jsx?$/.test(filePath)) return "javascript";
|
|
1433
|
+
if (/\.py$/.test(filePath)) return "python";
|
|
1434
|
+
if (/\.go$/.test(filePath)) return "go";
|
|
1435
|
+
if (/\.rs$/.test(filePath)) return "rust";
|
|
1436
|
+
if (/\.java$/.test(filePath)) return "java";
|
|
1437
|
+
if (/\.groovy$/.test(filePath)) return "java";
|
|
1165
1438
|
return "unknown";
|
|
1166
1439
|
}
|
|
1167
1440
|
/**
|
|
@@ -1169,40 +1442,38 @@ var CodeIngestor = class {
|
|
|
1169
1442
|
* linking requirement nodes to the annotated files.
|
|
1170
1443
|
* Format: // @req <feature-name>#<index>
|
|
1171
1444
|
*/
|
|
1172
|
-
|
|
1173
|
-
const REQ_TAG =
|
|
1445
|
+
extractReqAnnotationsForFile(filePath, content, rootDir) {
|
|
1446
|
+
const REQ_TAG = /(?:\/\/|#|\/\*)\s*@req\s+([\w-]+)#(\d+)/g;
|
|
1174
1447
|
const reqNodes = this.store.findNodes({ type: "requirement" });
|
|
1175
1448
|
let edgesAdded = 0;
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1449
|
+
let match;
|
|
1450
|
+
REQ_TAG.lastIndex = 0;
|
|
1451
|
+
while ((match = REQ_TAG.exec(content)) !== null) {
|
|
1452
|
+
const featureName = match[1];
|
|
1453
|
+
const reqIndex = parseInt(match[2], 10);
|
|
1454
|
+
const reqNode = reqNodes.find(
|
|
1455
|
+
(n) => n.metadata.featureName === featureName && n.metadata.index === reqIndex
|
|
1456
|
+
);
|
|
1457
|
+
if (!reqNode) {
|
|
1458
|
+
console.warn(
|
|
1459
|
+
`@req annotation references non-existent requirement: ${featureName}#${reqIndex} in ${filePath}`
|
|
1184
1460
|
);
|
|
1185
|
-
|
|
1186
|
-
console.warn(
|
|
1187
|
-
`@req annotation references non-existent requirement: ${featureName}#${reqIndex} in ${filePath}`
|
|
1188
|
-
);
|
|
1189
|
-
continue;
|
|
1190
|
-
}
|
|
1191
|
-
const relPath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
1192
|
-
const fileNodeId = `file:${relPath}`;
|
|
1193
|
-
this.store.addEdge({
|
|
1194
|
-
from: reqNode.id,
|
|
1195
|
-
to: fileNodeId,
|
|
1196
|
-
type: "verified_by",
|
|
1197
|
-
confidence: 1,
|
|
1198
|
-
metadata: {
|
|
1199
|
-
method: "annotation",
|
|
1200
|
-
tag: `@req ${featureName}#${reqIndex}`,
|
|
1201
|
-
confidence: 1
|
|
1202
|
-
}
|
|
1203
|
-
});
|
|
1204
|
-
edgesAdded++;
|
|
1461
|
+
continue;
|
|
1205
1462
|
}
|
|
1463
|
+
const relPath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
1464
|
+
const fileNodeId = `file:${relPath}`;
|
|
1465
|
+
this.store.addEdge({
|
|
1466
|
+
from: reqNode.id,
|
|
1467
|
+
to: fileNodeId,
|
|
1468
|
+
type: "verified_by",
|
|
1469
|
+
confidence: 1,
|
|
1470
|
+
metadata: {
|
|
1471
|
+
method: "annotation",
|
|
1472
|
+
tag: `@req ${featureName}#${reqIndex}`,
|
|
1473
|
+
confidence: 1
|
|
1474
|
+
}
|
|
1475
|
+
});
|
|
1476
|
+
edgesAdded++;
|
|
1206
1477
|
}
|
|
1207
1478
|
return edgesAdded;
|
|
1208
1479
|
}
|
|
@@ -1606,7 +1877,7 @@ var KnowledgeIngestor = class {
|
|
|
1606
1877
|
const entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
1607
1878
|
for (const entry of entries) {
|
|
1608
1879
|
const fullPath = path3.join(dir, entry.name);
|
|
1609
|
-
if (entry.isDirectory()) {
|
|
1880
|
+
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") {
|
|
1610
1881
|
results.push(...await this.findMarkdownFiles(fullPath));
|
|
1611
1882
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1612
1883
|
results.push(fullPath);
|
|
@@ -1684,9 +1955,176 @@ function parseFailureSection(section) {
|
|
|
1684
1955
|
};
|
|
1685
1956
|
}
|
|
1686
1957
|
|
|
1687
|
-
// src/ingest/
|
|
1958
|
+
// src/ingest/BusinessKnowledgeIngestor.ts
|
|
1688
1959
|
var fs3 = __toESM(require("fs/promises"));
|
|
1689
1960
|
var path4 = __toESM(require("path"));
|
|
1961
|
+
var BUSINESS_KNOWLEDGE_TYPES = /* @__PURE__ */ new Set([
|
|
1962
|
+
"business_rule",
|
|
1963
|
+
"business_process",
|
|
1964
|
+
"business_concept",
|
|
1965
|
+
"business_term",
|
|
1966
|
+
"business_metric"
|
|
1967
|
+
]);
|
|
1968
|
+
var GOVERNS_SOURCE_TYPES = /* @__PURE__ */ new Set(["business_rule", "business_process"]);
|
|
1969
|
+
var CODE_NODE_TYPES2 = [
|
|
1970
|
+
"file",
|
|
1971
|
+
"function",
|
|
1972
|
+
"class",
|
|
1973
|
+
"method",
|
|
1974
|
+
"interface",
|
|
1975
|
+
"variable"
|
|
1976
|
+
];
|
|
1977
|
+
var MEASURABLE_TYPES = /* @__PURE__ */ new Set(["business_process", "business_concept"]);
|
|
1978
|
+
var BusinessKnowledgeIngestor = class {
|
|
1979
|
+
constructor(store) {
|
|
1980
|
+
this.store = store;
|
|
1981
|
+
}
|
|
1982
|
+
store;
|
|
1983
|
+
async ingest(knowledgeDir) {
|
|
1984
|
+
const start = Date.now();
|
|
1985
|
+
const errors = [];
|
|
1986
|
+
let files;
|
|
1987
|
+
try {
|
|
1988
|
+
files = await this.findMarkdownFiles(knowledgeDir);
|
|
1989
|
+
} catch {
|
|
1990
|
+
return emptyResult(Date.now() - start);
|
|
1991
|
+
}
|
|
1992
|
+
const nodeEntries = await this.createNodes(files, knowledgeDir, errors);
|
|
1993
|
+
const edgesAdded = this.createEdges(nodeEntries);
|
|
1994
|
+
return {
|
|
1995
|
+
nodesAdded: nodeEntries.length,
|
|
1996
|
+
nodesUpdated: 0,
|
|
1997
|
+
edgesAdded,
|
|
1998
|
+
edgesUpdated: 0,
|
|
1999
|
+
errors,
|
|
2000
|
+
durationMs: Date.now() - start
|
|
2001
|
+
};
|
|
2002
|
+
}
|
|
2003
|
+
async createNodes(files, knowledgeDir, errors) {
|
|
2004
|
+
const entries = [];
|
|
2005
|
+
for (const filePath of files) {
|
|
2006
|
+
try {
|
|
2007
|
+
const entry = await this.parseAndAddNode(filePath, knowledgeDir);
|
|
2008
|
+
if (entry) entries.push(entry);
|
|
2009
|
+
} catch (err) {
|
|
2010
|
+
errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
return entries;
|
|
2014
|
+
}
|
|
2015
|
+
async parseAndAddNode(filePath, knowledgeDir) {
|
|
2016
|
+
const raw = await fs3.readFile(filePath, "utf-8");
|
|
2017
|
+
const parsed = parseFrontmatter(raw);
|
|
2018
|
+
if (!parsed) return null;
|
|
2019
|
+
const { frontmatter, body } = parsed;
|
|
2020
|
+
if (!BUSINESS_KNOWLEDGE_TYPES.has(frontmatter.type)) return null;
|
|
2021
|
+
const relPath = path4.relative(knowledgeDir, filePath).replaceAll("\\", "/");
|
|
2022
|
+
const domain = frontmatter.domain ?? relPath.split("/")[0] ?? "unknown";
|
|
2023
|
+
const filename = path4.basename(filePath, ".md");
|
|
2024
|
+
const nodeId = `bk:${domain}:${filename}`;
|
|
2025
|
+
const titleMatch = body.match(/^#\s+(.+)$/m);
|
|
2026
|
+
const name = titleMatch ? titleMatch[1].trim() : filename;
|
|
2027
|
+
const node = {
|
|
2028
|
+
id: nodeId,
|
|
2029
|
+
type: frontmatter.type,
|
|
2030
|
+
name,
|
|
2031
|
+
path: relPath,
|
|
2032
|
+
content: body.trim(),
|
|
2033
|
+
metadata: {
|
|
2034
|
+
domain,
|
|
2035
|
+
...frontmatter.tags && { tags: frontmatter.tags },
|
|
2036
|
+
...frontmatter.related && { related: frontmatter.related }
|
|
2037
|
+
}
|
|
2038
|
+
};
|
|
2039
|
+
this.store.addNode(node);
|
|
2040
|
+
return { nodeId, node, content: body };
|
|
2041
|
+
}
|
|
2042
|
+
createEdges(nodeEntries) {
|
|
2043
|
+
let edgesAdded = 0;
|
|
2044
|
+
for (const { nodeId, node, content } of nodeEntries) {
|
|
2045
|
+
if (GOVERNS_SOURCE_TYPES.has(node.type)) {
|
|
2046
|
+
edgesAdded += this.linkToNodes(content, nodeId, "governs", CODE_NODE_TYPES2);
|
|
2047
|
+
} else {
|
|
2048
|
+
edgesAdded += this.linkToNodes(content, nodeId, "documents", CODE_NODE_TYPES2);
|
|
2049
|
+
}
|
|
2050
|
+
if (node.type === "business_metric") {
|
|
2051
|
+
edgesAdded += this.linkToBusinessNodes(content, nodeId, "measures", MEASURABLE_TYPES);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
return edgesAdded;
|
|
2055
|
+
}
|
|
2056
|
+
linkToNodes(content, sourceNodeId, edgeType, targetTypes) {
|
|
2057
|
+
let count = 0;
|
|
2058
|
+
for (const nodeType of targetTypes) {
|
|
2059
|
+
const nodes = this.store.findNodes({ type: nodeType });
|
|
2060
|
+
for (const node of nodes) {
|
|
2061
|
+
if (node.name.length < 3) continue;
|
|
2062
|
+
const escaped = node.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2063
|
+
const namePattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
2064
|
+
if (namePattern.test(content)) {
|
|
2065
|
+
this.store.addEdge({ from: sourceNodeId, to: node.id, type: edgeType });
|
|
2066
|
+
count++;
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
return count;
|
|
2071
|
+
}
|
|
2072
|
+
linkToBusinessNodes(content, sourceNodeId, edgeType, targetTypes) {
|
|
2073
|
+
let count = 0;
|
|
2074
|
+
for (const node of this.store.findNodes({})) {
|
|
2075
|
+
if (!targetTypes.has(node.type)) continue;
|
|
2076
|
+
if (node.name.length < 3) continue;
|
|
2077
|
+
const escaped = node.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2078
|
+
const namePattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
2079
|
+
if (namePattern.test(content)) {
|
|
2080
|
+
this.store.addEdge({ from: sourceNodeId, to: node.id, type: edgeType });
|
|
2081
|
+
count++;
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
return count;
|
|
2085
|
+
}
|
|
2086
|
+
async findMarkdownFiles(dir) {
|
|
2087
|
+
const results = [];
|
|
2088
|
+
const entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
2089
|
+
for (const entry of entries) {
|
|
2090
|
+
const fullPath = path4.join(dir, entry.name);
|
|
2091
|
+
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") {
|
|
2092
|
+
results.push(...await this.findMarkdownFiles(fullPath));
|
|
2093
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
2094
|
+
results.push(fullPath);
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
return results;
|
|
2098
|
+
}
|
|
2099
|
+
};
|
|
2100
|
+
function parseFrontmatter(raw) {
|
|
2101
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
2102
|
+
if (!match) return null;
|
|
2103
|
+
const yamlBlock = match[1];
|
|
2104
|
+
const body = match[2];
|
|
2105
|
+
const frontmatter = {};
|
|
2106
|
+
for (const line of yamlBlock.split("\n")) {
|
|
2107
|
+
const kvMatch = line.match(/^(\w+):\s*(.+)$/);
|
|
2108
|
+
if (!kvMatch) continue;
|
|
2109
|
+
const key = kvMatch[1];
|
|
2110
|
+
const value = kvMatch[2].trim();
|
|
2111
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
2112
|
+
frontmatter[key] = value.slice(1, -1).split(",").map((s) => s.trim());
|
|
2113
|
+
} else {
|
|
2114
|
+
frontmatter[key] = value;
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
if (!frontmatter.type || typeof frontmatter.type !== "string") return null;
|
|
2118
|
+
if (!frontmatter.domain || typeof frontmatter.domain !== "string") return null;
|
|
2119
|
+
return {
|
|
2120
|
+
frontmatter,
|
|
2121
|
+
body
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
// src/ingest/RequirementIngestor.ts
|
|
2126
|
+
var fs4 = __toESM(require("fs/promises"));
|
|
2127
|
+
var path5 = __toESM(require("path"));
|
|
1690
2128
|
var REQUIREMENT_SECTIONS = [
|
|
1691
2129
|
"Observable Truths",
|
|
1692
2130
|
"Success Criteria",
|
|
@@ -1703,7 +2141,7 @@ function detectEarsPattern(text) {
|
|
|
1703
2141
|
if (/^the\s+\w+\s+shall\b/.test(lower)) return "ubiquitous";
|
|
1704
2142
|
return void 0;
|
|
1705
2143
|
}
|
|
1706
|
-
var
|
|
2144
|
+
var CODE_NODE_TYPES3 = ["file", "function", "class", "method", "interface", "variable"];
|
|
1707
2145
|
var RequirementIngestor = class {
|
|
1708
2146
|
constructor(store) {
|
|
1709
2147
|
this.store = store;
|
|
@@ -1721,8 +2159,8 @@ var RequirementIngestor = class {
|
|
|
1721
2159
|
let edgesAdded = 0;
|
|
1722
2160
|
let featureDirs;
|
|
1723
2161
|
try {
|
|
1724
|
-
const entries = await
|
|
1725
|
-
featureDirs = entries.filter((e) => e.isDirectory()).map((e) =>
|
|
2162
|
+
const entries = await fs4.readdir(specsDir, { withFileTypes: true });
|
|
2163
|
+
featureDirs = entries.filter((e) => e.isDirectory()).map((e) => path5.join(specsDir, e.name));
|
|
1726
2164
|
} catch {
|
|
1727
2165
|
return emptyResult(Date.now() - start);
|
|
1728
2166
|
}
|
|
@@ -1741,11 +2179,11 @@ var RequirementIngestor = class {
|
|
|
1741
2179
|
};
|
|
1742
2180
|
}
|
|
1743
2181
|
async ingestFeatureDir(featureDir, errors) {
|
|
1744
|
-
const featureName =
|
|
1745
|
-
const specPath =
|
|
2182
|
+
const featureName = path5.basename(featureDir);
|
|
2183
|
+
const specPath = path5.join(featureDir, "proposal.md").replaceAll("\\", "/");
|
|
1746
2184
|
let content;
|
|
1747
2185
|
try {
|
|
1748
|
-
content = await
|
|
2186
|
+
content = await fs4.readFile(specPath, "utf-8");
|
|
1749
2187
|
} catch {
|
|
1750
2188
|
return { nodesAdded: 0, edgesAdded: 0 };
|
|
1751
2189
|
}
|
|
@@ -1762,7 +2200,7 @@ var RequirementIngestor = class {
|
|
|
1762
2200
|
this.store.addNode({
|
|
1763
2201
|
id: specNodeId,
|
|
1764
2202
|
type: "document",
|
|
1765
|
-
name:
|
|
2203
|
+
name: path5.basename(specPath),
|
|
1766
2204
|
path: specPath,
|
|
1767
2205
|
metadata: { featureName }
|
|
1768
2206
|
});
|
|
@@ -1877,9 +2315,9 @@ var RequirementIngestor = class {
|
|
|
1877
2315
|
for (const node of fileNodes) {
|
|
1878
2316
|
if (!node.path) continue;
|
|
1879
2317
|
const normalizedPath = node.path.replace(/\\/g, "/");
|
|
1880
|
-
const isCodeMatch = normalizedPath.includes("packages/") &&
|
|
2318
|
+
const isCodeMatch = normalizedPath.includes("packages/") && path5.basename(normalizedPath).includes(featureName);
|
|
1881
2319
|
const isTestMatch = normalizedPath.includes("/tests/") && // platform-safe
|
|
1882
|
-
|
|
2320
|
+
path5.basename(normalizedPath).includes(featureName);
|
|
1883
2321
|
if (isCodeMatch && !isTestMatch) {
|
|
1884
2322
|
this.store.addEdge({
|
|
1885
2323
|
from: reqId,
|
|
@@ -1908,7 +2346,7 @@ var RequirementIngestor = class {
|
|
|
1908
2346
|
*/
|
|
1909
2347
|
linkByKeywordOverlap(reqId, reqText) {
|
|
1910
2348
|
let count = 0;
|
|
1911
|
-
for (const nodeType of
|
|
2349
|
+
for (const nodeType of CODE_NODE_TYPES3) {
|
|
1912
2350
|
const codeNodes = this.store.findNodes({ type: nodeType });
|
|
1913
2351
|
for (const node of codeNodes) {
|
|
1914
2352
|
if (node.name.length < 3) continue;
|
|
@@ -1927,12 +2365,2649 @@ var RequirementIngestor = class {
|
|
|
1927
2365
|
}
|
|
1928
2366
|
}
|
|
1929
2367
|
}
|
|
1930
|
-
return count;
|
|
2368
|
+
return count;
|
|
2369
|
+
}
|
|
2370
|
+
};
|
|
2371
|
+
|
|
2372
|
+
// src/ingest/KnowledgePipelineRunner.ts
|
|
2373
|
+
var fs9 = __toESM(require("fs/promises"));
|
|
2374
|
+
var path10 = __toESM(require("path"));
|
|
2375
|
+
|
|
2376
|
+
// src/ingest/DiagramParser.ts
|
|
2377
|
+
var fs5 = __toESM(require("fs/promises"));
|
|
2378
|
+
var path6 = __toESM(require("path"));
|
|
2379
|
+
function emptyMermaidResult(diagramType = "unknown") {
|
|
2380
|
+
return {
|
|
2381
|
+
entities: [],
|
|
2382
|
+
relationships: [],
|
|
2383
|
+
metadata: { format: "mermaid", diagramType }
|
|
2384
|
+
};
|
|
2385
|
+
}
|
|
2386
|
+
var MermaidParser = class {
|
|
2387
|
+
canParse(_content, ext) {
|
|
2388
|
+
return ext === ".mmd" || ext === ".mermaid";
|
|
2389
|
+
}
|
|
2390
|
+
parse(content, _filePath) {
|
|
2391
|
+
const trimmed = content.trim();
|
|
2392
|
+
if (!trimmed) {
|
|
2393
|
+
return emptyMermaidResult();
|
|
2394
|
+
}
|
|
2395
|
+
const diagramType = this.detectDiagramType(trimmed);
|
|
2396
|
+
switch (diagramType) {
|
|
2397
|
+
case "flowchart":
|
|
2398
|
+
return this.parseFlowchart(trimmed, diagramType);
|
|
2399
|
+
case "sequence":
|
|
2400
|
+
return this.parseSequence(trimmed, diagramType);
|
|
2401
|
+
default:
|
|
2402
|
+
return emptyMermaidResult(diagramType);
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
detectDiagramType(content) {
|
|
2406
|
+
const lines = content.split("\n");
|
|
2407
|
+
for (const line of lines) {
|
|
2408
|
+
const trimmedLine = line.trim();
|
|
2409
|
+
if (!trimmedLine) continue;
|
|
2410
|
+
if (/^(?:graph|flowchart)\b/i.test(trimmedLine)) return "flowchart";
|
|
2411
|
+
if (/^sequenceDiagram\b/.test(trimmedLine)) return "sequence";
|
|
2412
|
+
if (/^classDiagram\b/.test(trimmedLine)) return "class";
|
|
2413
|
+
if (/^erDiagram\b/.test(trimmedLine)) return "er";
|
|
2414
|
+
break;
|
|
2415
|
+
}
|
|
2416
|
+
return "unknown";
|
|
2417
|
+
}
|
|
2418
|
+
parseFlowchart(content, diagramType) {
|
|
2419
|
+
const entities = /* @__PURE__ */ new Map();
|
|
2420
|
+
const relationships = [];
|
|
2421
|
+
const nodeRegex = /([A-Za-z0-9_]+)\s*[[({]([^\])}]+)[\])}]/g;
|
|
2422
|
+
let match;
|
|
2423
|
+
match = nodeRegex.exec(content);
|
|
2424
|
+
while (match !== null) {
|
|
2425
|
+
const id = match[1] ?? "";
|
|
2426
|
+
const label = (match[2] ?? "").trim();
|
|
2427
|
+
if (id && !entities.has(id)) {
|
|
2428
|
+
const isDecision = this.isDecisionNode(content, id);
|
|
2429
|
+
entities.set(id, {
|
|
2430
|
+
id,
|
|
2431
|
+
label,
|
|
2432
|
+
...isDecision ? { type: "decision" } : {}
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
match = nodeRegex.exec(content);
|
|
2436
|
+
}
|
|
2437
|
+
const stripped = content.replace(/[[({][^\])}]*[\])}]/g, "");
|
|
2438
|
+
const labeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\|([^|]+)\|\s*([A-Za-z0-9_]+)/g;
|
|
2439
|
+
const unlabeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\s+([A-Za-z0-9_]+)/g;
|
|
2440
|
+
const edgeKeys = /* @__PURE__ */ new Set();
|
|
2441
|
+
match = labeledEdgeRegex.exec(stripped);
|
|
2442
|
+
while (match !== null) {
|
|
2443
|
+
const from = match[1] ?? "";
|
|
2444
|
+
const label = (match[2] ?? "").trim();
|
|
2445
|
+
const to = match[3] ?? "";
|
|
2446
|
+
if (from && to) {
|
|
2447
|
+
const key = `${from}->${to}:${label}`;
|
|
2448
|
+
if (!edgeKeys.has(key)) {
|
|
2449
|
+
edgeKeys.add(key);
|
|
2450
|
+
relationships.push({ from, to, label });
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
match = labeledEdgeRegex.exec(stripped);
|
|
2454
|
+
}
|
|
2455
|
+
match = unlabeledEdgeRegex.exec(stripped);
|
|
2456
|
+
while (match !== null) {
|
|
2457
|
+
const from = match[1] ?? "";
|
|
2458
|
+
const to = match[2] ?? "";
|
|
2459
|
+
if (from && to) {
|
|
2460
|
+
const hasLabeled = relationships.some((r) => r.from === from && r.to === to);
|
|
2461
|
+
if (!hasLabeled) {
|
|
2462
|
+
const key = `${from}->${to}`;
|
|
2463
|
+
if (!edgeKeys.has(key)) {
|
|
2464
|
+
edgeKeys.add(key);
|
|
2465
|
+
relationships.push({ from, to });
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
match = unlabeledEdgeRegex.exec(stripped);
|
|
2470
|
+
}
|
|
2471
|
+
return {
|
|
2472
|
+
entities: Array.from(entities.values()),
|
|
2473
|
+
relationships,
|
|
2474
|
+
metadata: { format: "mermaid", diagramType }
|
|
2475
|
+
};
|
|
2476
|
+
}
|
|
2477
|
+
isDecisionNode(content, nodeId) {
|
|
2478
|
+
const decisionRegex = new RegExp(`${nodeId}\\s*\\{`);
|
|
2479
|
+
return decisionRegex.test(content);
|
|
2480
|
+
}
|
|
2481
|
+
parseSequence(content, diagramType) {
|
|
2482
|
+
const entities = [];
|
|
2483
|
+
const relationships = [];
|
|
2484
|
+
const seenParticipants = /* @__PURE__ */ new Set();
|
|
2485
|
+
const participantRegex = /participant\s+(\w+)(?:\s+as\s+(.+))?/g;
|
|
2486
|
+
let match;
|
|
2487
|
+
match = participantRegex.exec(content);
|
|
2488
|
+
while (match !== null) {
|
|
2489
|
+
const id = match[1] ?? "";
|
|
2490
|
+
const label = match[2]?.trim() || id;
|
|
2491
|
+
if (id && !seenParticipants.has(id)) {
|
|
2492
|
+
seenParticipants.add(id);
|
|
2493
|
+
entities.push({ id, label });
|
|
2494
|
+
}
|
|
2495
|
+
match = participantRegex.exec(content);
|
|
2496
|
+
}
|
|
2497
|
+
const forwardMsgRegex = /(\w+)\s*->>?\+?\s*(\w+)\s*:\s*(.+)/g;
|
|
2498
|
+
match = forwardMsgRegex.exec(content);
|
|
2499
|
+
while (match !== null) {
|
|
2500
|
+
const from = match[1] ?? "";
|
|
2501
|
+
const to = match[2] ?? "";
|
|
2502
|
+
const label = (match[3] ?? "").trim();
|
|
2503
|
+
if (from && to) {
|
|
2504
|
+
relationships.push({ from, to, label });
|
|
2505
|
+
}
|
|
2506
|
+
match = forwardMsgRegex.exec(content);
|
|
2507
|
+
}
|
|
2508
|
+
const returnMsgRegex = /(\w+)\s*-->>?-?\s*(\w+)\s*:\s*(.+)/g;
|
|
2509
|
+
match = returnMsgRegex.exec(content);
|
|
2510
|
+
while (match !== null) {
|
|
2511
|
+
const from = match[1] ?? "";
|
|
2512
|
+
const to = match[2] ?? "";
|
|
2513
|
+
const label = (match[3] ?? "").trim();
|
|
2514
|
+
if (from && to) {
|
|
2515
|
+
relationships.push({ from, to, label });
|
|
2516
|
+
}
|
|
2517
|
+
match = returnMsgRegex.exec(content);
|
|
2518
|
+
}
|
|
2519
|
+
return {
|
|
2520
|
+
entities,
|
|
2521
|
+
relationships,
|
|
2522
|
+
metadata: { format: "mermaid", diagramType }
|
|
2523
|
+
};
|
|
2524
|
+
}
|
|
2525
|
+
};
|
|
2526
|
+
var D2Parser = class {
|
|
2527
|
+
canParse(_content, ext) {
|
|
2528
|
+
return ext === ".d2";
|
|
2529
|
+
}
|
|
2530
|
+
parse(content, _filePath) {
|
|
2531
|
+
const trimmed = content.trim();
|
|
2532
|
+
if (!trimmed) {
|
|
2533
|
+
return {
|
|
2534
|
+
entities: [],
|
|
2535
|
+
relationships: [],
|
|
2536
|
+
metadata: { format: "d2", diagramType: "architecture" }
|
|
2537
|
+
};
|
|
2538
|
+
}
|
|
2539
|
+
const entities = /* @__PURE__ */ new Map();
|
|
2540
|
+
const relationships = [];
|
|
2541
|
+
let braceDepth = 0;
|
|
2542
|
+
for (const line of trimmed.split("\n")) {
|
|
2543
|
+
const stripped = line.trim();
|
|
2544
|
+
if (!stripped || stripped.startsWith("#")) continue;
|
|
2545
|
+
if (stripped.endsWith("{")) {
|
|
2546
|
+
if (braceDepth === 0) {
|
|
2547
|
+
const shapeMatch = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+?)\s*\{$/);
|
|
2548
|
+
if (shapeMatch) {
|
|
2549
|
+
const id = shapeMatch[1];
|
|
2550
|
+
const label = shapeMatch[2];
|
|
2551
|
+
if (id && label) {
|
|
2552
|
+
entities.set(id, { id, label });
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
braceDepth++;
|
|
2557
|
+
continue;
|
|
2558
|
+
}
|
|
2559
|
+
if (stripped === "}") {
|
|
2560
|
+
braceDepth = Math.max(0, braceDepth - 1);
|
|
2561
|
+
continue;
|
|
2562
|
+
}
|
|
2563
|
+
if (braceDepth > 0) continue;
|
|
2564
|
+
const connMatch = stripped.match(
|
|
2565
|
+
/^([a-zA-Z0-9_.-]+)\s*->\s*([a-zA-Z0-9_.-]+)(?:\s*:\s*(.+?))?$/
|
|
2566
|
+
);
|
|
2567
|
+
if (connMatch) {
|
|
2568
|
+
const from = connMatch[1] ?? "";
|
|
2569
|
+
const to = connMatch[2] ?? "";
|
|
2570
|
+
const label = connMatch[3]?.trim();
|
|
2571
|
+
if (from && to) {
|
|
2572
|
+
relationships.push({ from, to, ...label ? { label } : {} });
|
|
2573
|
+
}
|
|
2574
|
+
continue;
|
|
2575
|
+
}
|
|
2576
|
+
const simpleMatch = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+)$/);
|
|
2577
|
+
if (simpleMatch) {
|
|
2578
|
+
const id = simpleMatch[1];
|
|
2579
|
+
const label = (simpleMatch[2] ?? "").trim();
|
|
2580
|
+
if (id && label && !entities.has(id)) {
|
|
2581
|
+
entities.set(id, { id, label });
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
return {
|
|
2586
|
+
entities: Array.from(entities.values()),
|
|
2587
|
+
relationships,
|
|
2588
|
+
metadata: { format: "d2", diagramType: "architecture" }
|
|
2589
|
+
};
|
|
2590
|
+
}
|
|
2591
|
+
};
|
|
2592
|
+
var PlantUmlParser = class {
|
|
2593
|
+
canParse(_content, ext) {
|
|
2594
|
+
return ext === ".puml" || ext === ".plantuml";
|
|
2595
|
+
}
|
|
2596
|
+
parse(content, _filePath) {
|
|
2597
|
+
const trimmed = content.trim();
|
|
2598
|
+
if (!trimmed) {
|
|
2599
|
+
return {
|
|
2600
|
+
entities: [],
|
|
2601
|
+
relationships: [],
|
|
2602
|
+
metadata: { format: "plantuml", diagramType: "unknown" }
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2605
|
+
const diagramType = this.detectDiagramType(trimmed);
|
|
2606
|
+
const entities = /* @__PURE__ */ new Map();
|
|
2607
|
+
const relationships = [];
|
|
2608
|
+
const body = trimmed.replace(/@startuml\b.*\n?/, "").replace(/@enduml\b.*/, "").trim();
|
|
2609
|
+
const classRegex = /class\s+(\w+)/g;
|
|
2610
|
+
let match;
|
|
2611
|
+
match = classRegex.exec(body);
|
|
2612
|
+
while (match !== null) {
|
|
2613
|
+
const id = match[1] ?? "";
|
|
2614
|
+
if (id && !entities.has(id)) {
|
|
2615
|
+
entities.set(id, { id, label: id });
|
|
2616
|
+
}
|
|
2617
|
+
match = classRegex.exec(body);
|
|
2618
|
+
}
|
|
2619
|
+
const componentRegex1 = /\[([^\]]+)\]/g;
|
|
2620
|
+
match = componentRegex1.exec(body);
|
|
2621
|
+
while (match !== null) {
|
|
2622
|
+
const label = (match[1] ?? "").trim();
|
|
2623
|
+
if (label) {
|
|
2624
|
+
const id = label.replace(/\s+/g, "_");
|
|
2625
|
+
if (!entities.has(id)) {
|
|
2626
|
+
entities.set(id, { id, label });
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
match = componentRegex1.exec(body);
|
|
2630
|
+
}
|
|
2631
|
+
const relRegex = /(\w+)\s*(?:-->|->|<--|<-|\.\.>|--)\s*(\w+)(?:\s*:\s*(.+))?/g;
|
|
2632
|
+
match = relRegex.exec(body);
|
|
2633
|
+
while (match !== null) {
|
|
2634
|
+
const from = match[1] ?? "";
|
|
2635
|
+
const to = match[2] ?? "";
|
|
2636
|
+
const label = match[3]?.trim();
|
|
2637
|
+
if (from && to) {
|
|
2638
|
+
relationships.push({ from, to, ...label ? { label } : {} });
|
|
2639
|
+
}
|
|
2640
|
+
match = relRegex.exec(body);
|
|
2641
|
+
}
|
|
2642
|
+
return {
|
|
2643
|
+
entities: Array.from(entities.values()),
|
|
2644
|
+
relationships,
|
|
2645
|
+
metadata: { format: "plantuml", diagramType }
|
|
2646
|
+
};
|
|
2647
|
+
}
|
|
2648
|
+
detectDiagramType(content) {
|
|
2649
|
+
if (/class\s+\w+/.test(content)) return "class";
|
|
2650
|
+
if (/\[.+\]/.test(content) || /component\s+/.test(content)) return "component";
|
|
2651
|
+
if (/participant\s+/.test(content) || /actor\s+/.test(content)) return "sequence";
|
|
2652
|
+
return "unknown";
|
|
2653
|
+
}
|
|
2654
|
+
};
|
|
2655
|
+
var DIAGRAM_EXTENSIONS = /* @__PURE__ */ new Set([".mmd", ".mermaid", ".d2", ".puml", ".plantuml"]);
|
|
2656
|
+
var DiagramParser = class {
|
|
2657
|
+
constructor(store) {
|
|
2658
|
+
this.store = store;
|
|
2659
|
+
}
|
|
2660
|
+
store;
|
|
2661
|
+
parsers = [
|
|
2662
|
+
new MermaidParser(),
|
|
2663
|
+
new D2Parser(),
|
|
2664
|
+
new PlantUmlParser()
|
|
2665
|
+
];
|
|
2666
|
+
parse(content, filePath) {
|
|
2667
|
+
const ext = path6.extname(filePath).toLowerCase();
|
|
2668
|
+
for (const parser of this.parsers) {
|
|
2669
|
+
if (parser.canParse(content, ext)) {
|
|
2670
|
+
return parser.parse(content, filePath);
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
return {
|
|
2674
|
+
entities: [],
|
|
2675
|
+
relationships: [],
|
|
2676
|
+
metadata: { format: "mermaid", diagramType: "unknown" }
|
|
2677
|
+
};
|
|
2678
|
+
}
|
|
2679
|
+
async ingest(projectDir) {
|
|
2680
|
+
const start = Date.now();
|
|
2681
|
+
let nodesAdded = 0;
|
|
2682
|
+
let edgesAdded = 0;
|
|
2683
|
+
const errors = [];
|
|
2684
|
+
const diagramFiles = await this.findDiagramFiles(projectDir);
|
|
2685
|
+
for (const filePath of diagramFiles) {
|
|
2686
|
+
try {
|
|
2687
|
+
const content = await fs5.readFile(filePath, "utf-8");
|
|
2688
|
+
const relPath = path6.relative(projectDir, filePath).replaceAll("\\", "/");
|
|
2689
|
+
const result = this.parse(content, filePath);
|
|
2690
|
+
if (result.entities.length === 0) continue;
|
|
2691
|
+
const pathHash = hash(relPath);
|
|
2692
|
+
for (const entity of result.entities) {
|
|
2693
|
+
const nodeId = `diagram:${pathHash}:${entity.id}`;
|
|
2694
|
+
this.store.addNode({
|
|
2695
|
+
id: nodeId,
|
|
2696
|
+
type: "business_concept",
|
|
2697
|
+
name: entity.label,
|
|
2698
|
+
path: relPath,
|
|
2699
|
+
metadata: {
|
|
2700
|
+
source: "diagram",
|
|
2701
|
+
format: result.metadata.format,
|
|
2702
|
+
diagramType: result.metadata.diagramType,
|
|
2703
|
+
confidence: 0.85,
|
|
2704
|
+
...entity.type ? { entityType: entity.type } : {}
|
|
2705
|
+
}
|
|
2706
|
+
});
|
|
2707
|
+
nodesAdded++;
|
|
2708
|
+
}
|
|
2709
|
+
for (const rel of result.relationships) {
|
|
2710
|
+
const fromId = `diagram:${pathHash}:${rel.from}`;
|
|
2711
|
+
const toId = `diagram:${pathHash}:${rel.to}`;
|
|
2712
|
+
this.store.addEdge({
|
|
2713
|
+
from: fromId,
|
|
2714
|
+
to: toId,
|
|
2715
|
+
type: "references",
|
|
2716
|
+
metadata: {
|
|
2717
|
+
...rel.label ? { label: rel.label } : {}
|
|
2718
|
+
}
|
|
2719
|
+
});
|
|
2720
|
+
edgesAdded++;
|
|
2721
|
+
}
|
|
2722
|
+
} catch (err) {
|
|
2723
|
+
errors.push(
|
|
2724
|
+
`Failed to parse ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
2725
|
+
);
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
return {
|
|
2729
|
+
nodesAdded,
|
|
2730
|
+
nodesUpdated: 0,
|
|
2731
|
+
edgesAdded,
|
|
2732
|
+
edgesUpdated: 0,
|
|
2733
|
+
errors,
|
|
2734
|
+
durationMs: Date.now() - start
|
|
2735
|
+
};
|
|
2736
|
+
}
|
|
2737
|
+
async findDiagramFiles(dir) {
|
|
2738
|
+
const files = [];
|
|
2739
|
+
const SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".harness"]);
|
|
2740
|
+
const walk = async (currentDir) => {
|
|
2741
|
+
let entries;
|
|
2742
|
+
try {
|
|
2743
|
+
entries = await fs5.readdir(currentDir, { withFileTypes: true });
|
|
2744
|
+
} catch {
|
|
2745
|
+
return;
|
|
2746
|
+
}
|
|
2747
|
+
for (const entry of entries) {
|
|
2748
|
+
if (entry.isDirectory()) {
|
|
2749
|
+
if (!SKIP_DIRS2.has(entry.name)) {
|
|
2750
|
+
await walk(path6.join(currentDir, entry.name));
|
|
2751
|
+
}
|
|
2752
|
+
} else if (entry.isFile()) {
|
|
2753
|
+
const ext = path6.extname(entry.name).toLowerCase();
|
|
2754
|
+
if (DIAGRAM_EXTENSIONS.has(ext)) {
|
|
2755
|
+
files.push(path6.join(currentDir, entry.name));
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
};
|
|
2760
|
+
await walk(dir);
|
|
2761
|
+
return files;
|
|
2762
|
+
}
|
|
2763
|
+
};
|
|
2764
|
+
|
|
2765
|
+
// src/ingest/KnowledgeLinker.ts
|
|
2766
|
+
var fs6 = __toESM(require("fs/promises"));
|
|
2767
|
+
var path7 = __toESM(require("path"));
|
|
2768
|
+
var HEURISTIC_PATTERNS = [
|
|
2769
|
+
{
|
|
2770
|
+
name: "business-rule-imperative",
|
|
2771
|
+
pattern: /\b(?:must|shall|required)\b.*\b(?:system|service|api|user|data|app|application|client|server|process|handler|module)\b/i,
|
|
2772
|
+
signal: "Business rule",
|
|
2773
|
+
confidence: 0.7,
|
|
2774
|
+
nodeType: "business_fact"
|
|
2775
|
+
},
|
|
2776
|
+
{
|
|
2777
|
+
name: "sla-slo-pattern",
|
|
2778
|
+
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,
|
|
2779
|
+
signal: "Business constraint (SLA/SLO)",
|
|
2780
|
+
confidence: 0.8,
|
|
2781
|
+
nodeType: "business_fact"
|
|
2782
|
+
},
|
|
2783
|
+
{
|
|
2784
|
+
name: "monetary-amount",
|
|
2785
|
+
pattern: /\$[\d,]+(?:\.\d{2})?\s*(?:\b(?:cost|revenue|budget|price|fee|license|subscription|annual|monthly)\b)?/i,
|
|
2786
|
+
signal: "Business fact (monetary)",
|
|
2787
|
+
confidence: 0.6,
|
|
2788
|
+
nodeType: "business_fact"
|
|
2789
|
+
},
|
|
2790
|
+
{
|
|
2791
|
+
name: "acceptance-criteria",
|
|
2792
|
+
pattern: /\b(?:Given\b.*\bWhen\b.*\bThen\b|\[[ x]\])/i,
|
|
2793
|
+
signal: "Business rule (acceptance criteria)",
|
|
2794
|
+
confidence: 0.8,
|
|
2795
|
+
nodeType: "business_fact"
|
|
2796
|
+
},
|
|
2797
|
+
{
|
|
2798
|
+
name: "regulatory-reference",
|
|
2799
|
+
pattern: /\b(?:GDPR|SOC\s*2|PCI(?:\s*-?\s*DSS)?|HIPAA|CCPA|FERPA|SOX)\b/i,
|
|
2800
|
+
signal: "Business rule (regulatory)",
|
|
2801
|
+
confidence: 0.9,
|
|
2802
|
+
nodeType: "business_fact"
|
|
2803
|
+
}
|
|
2804
|
+
];
|
|
2805
|
+
var SCANNABLE_TYPES = ["issue", "conversation", "document"];
|
|
2806
|
+
function applyReactionBoost(candidate, nodeType, metadata) {
|
|
2807
|
+
if (nodeType !== "conversation" || !metadata.reactions) return;
|
|
2808
|
+
const reactions = metadata.reactions;
|
|
2809
|
+
const totalReactions = Object.values(reactions).reduce((sum, count) => sum + count, 0);
|
|
2810
|
+
if (totalReactions > 0) {
|
|
2811
|
+
candidate.confidence = Math.min(1, Math.round((candidate.confidence + 0.1) * 100) / 100);
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
var KnowledgeLinker = class {
|
|
2815
|
+
constructor(store, outputDir) {
|
|
2816
|
+
this.store = store;
|
|
2817
|
+
this.outputDir = outputDir;
|
|
2818
|
+
}
|
|
2819
|
+
store;
|
|
2820
|
+
outputDir;
|
|
2821
|
+
async link() {
|
|
2822
|
+
const errors = [];
|
|
2823
|
+
const candidates = this.scanAllNodes(errors);
|
|
2824
|
+
await this.writeJsonl(candidates);
|
|
2825
|
+
const conceptsClustered = this.clusterBySource(candidates);
|
|
2826
|
+
const promoted = this.promoteAndDeduplicate(candidates);
|
|
2827
|
+
await this.writeStagedJsonl(promoted.staged);
|
|
2828
|
+
return {
|
|
2829
|
+
factsCreated: promoted.factsCreated,
|
|
2830
|
+
conceptsClustered,
|
|
2831
|
+
duplicatesMerged: promoted.duplicatesMerged,
|
|
2832
|
+
stagedForReview: promoted.staged.length,
|
|
2833
|
+
errors
|
|
2834
|
+
};
|
|
2835
|
+
}
|
|
2836
|
+
/** Stage 1: Scan all scannable node types for heuristic pattern matches. */
|
|
2837
|
+
scanAllNodes(errors) {
|
|
2838
|
+
const candidates = [];
|
|
2839
|
+
for (const type of SCANNABLE_TYPES) {
|
|
2840
|
+
const nodes = this.store.findNodes({ type });
|
|
2841
|
+
for (const node of nodes) {
|
|
2842
|
+
if (!node.content) continue;
|
|
2843
|
+
try {
|
|
2844
|
+
const matches = this.scanPatterns(node.content, node.id, type);
|
|
2845
|
+
for (const match of matches) {
|
|
2846
|
+
applyReactionBoost(match, type, node.metadata);
|
|
2847
|
+
candidates.push(match);
|
|
2848
|
+
}
|
|
2849
|
+
} catch (err) {
|
|
2850
|
+
errors.push(
|
|
2851
|
+
`Scan failed for ${node.id}: ${err instanceof Error ? err.message : String(err)}`
|
|
2852
|
+
);
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
return candidates;
|
|
2857
|
+
}
|
|
2858
|
+
/** Stage 2: Group candidates by source node and create business_concept for 3+. */
|
|
2859
|
+
clusterBySource(candidates) {
|
|
2860
|
+
const sourceGroups = /* @__PURE__ */ new Map();
|
|
2861
|
+
for (const candidate of candidates) {
|
|
2862
|
+
const group = sourceGroups.get(candidate.sourceNodeId) ?? [];
|
|
2863
|
+
group.push(candidate);
|
|
2864
|
+
sourceGroups.set(candidate.sourceNodeId, group);
|
|
2865
|
+
}
|
|
2866
|
+
let clustered = 0;
|
|
2867
|
+
for (const [sourceNodeId, group] of sourceGroups) {
|
|
2868
|
+
if (group.length >= 3) {
|
|
2869
|
+
this.store.addNode({
|
|
2870
|
+
id: `concept:linker:${hash(sourceNodeId)}`,
|
|
2871
|
+
type: "business_concept",
|
|
2872
|
+
name: `Business concept cluster from ${sourceNodeId}`,
|
|
2873
|
+
metadata: {
|
|
2874
|
+
source: "knowledge-linker",
|
|
2875
|
+
sources: [sourceNodeId],
|
|
2876
|
+
factCount: group.length
|
|
2877
|
+
}
|
|
2878
|
+
});
|
|
2879
|
+
clustered++;
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
return clustered;
|
|
2883
|
+
}
|
|
2884
|
+
/** Stage 3: Promote high-confidence candidates, stage medium, discard low. */
|
|
2885
|
+
promoteAndDeduplicate(candidates) {
|
|
2886
|
+
let factsCreated = 0;
|
|
2887
|
+
let duplicatesMerged = 0;
|
|
2888
|
+
const staged = [];
|
|
2889
|
+
for (const candidate of candidates) {
|
|
2890
|
+
if (candidate.confidence >= 0.8) {
|
|
2891
|
+
const merged = this.tryMergeDuplicate(candidate);
|
|
2892
|
+
if (merged) {
|
|
2893
|
+
duplicatesMerged++;
|
|
2894
|
+
continue;
|
|
2895
|
+
}
|
|
2896
|
+
this.createFactNode(candidate);
|
|
2897
|
+
factsCreated++;
|
|
2898
|
+
} else if (candidate.confidence >= 0.5) {
|
|
2899
|
+
staged.push(candidate);
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
return { factsCreated, duplicatesMerged, staged };
|
|
2903
|
+
}
|
|
2904
|
+
/** Check for existing duplicate fact and merge sources if found. Returns true if merged. */
|
|
2905
|
+
tryMergeDuplicate(candidate) {
|
|
2906
|
+
if (this.store.getNode(candidate.id)) return true;
|
|
2907
|
+
const existingFacts = this.store.findNodes({ type: "business_fact" });
|
|
2908
|
+
const duplicate = existingFacts.find(
|
|
2909
|
+
(f) => f.content === candidate.content && f.id !== candidate.id
|
|
2910
|
+
);
|
|
2911
|
+
if (!duplicate) return false;
|
|
2912
|
+
const existingSources = duplicate.metadata.sources ?? [];
|
|
2913
|
+
if (!existingSources.includes(candidate.sourceNodeId)) {
|
|
2914
|
+
existingSources.push(candidate.sourceNodeId);
|
|
2915
|
+
duplicate.metadata.sources = existingSources;
|
|
2916
|
+
}
|
|
2917
|
+
return true;
|
|
2918
|
+
}
|
|
2919
|
+
/** Create a business_fact node and governs edge for a promoted candidate. */
|
|
2920
|
+
createFactNode(candidate) {
|
|
2921
|
+
this.store.addNode({
|
|
2922
|
+
id: candidate.id,
|
|
2923
|
+
type: candidate.nodeType,
|
|
2924
|
+
name: candidate.name,
|
|
2925
|
+
content: candidate.content,
|
|
2926
|
+
metadata: {
|
|
2927
|
+
source: "knowledge-linker",
|
|
2928
|
+
pattern: candidate.pattern,
|
|
2929
|
+
signal: candidate.signal,
|
|
2930
|
+
confidence: candidate.confidence,
|
|
2931
|
+
sourceNodeId: candidate.sourceNodeId,
|
|
2932
|
+
sourceNodeType: candidate.sourceNodeType,
|
|
2933
|
+
sources: [candidate.sourceNodeId]
|
|
2934
|
+
}
|
|
2935
|
+
});
|
|
2936
|
+
this.store.addEdge({
|
|
2937
|
+
from: candidate.id,
|
|
2938
|
+
to: candidate.sourceNodeId,
|
|
2939
|
+
type: "governs",
|
|
2940
|
+
confidence: candidate.confidence,
|
|
2941
|
+
metadata: { source: "knowledge-linker" }
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
/**
|
|
2945
|
+
* Write candidates to JSONL file for audit trail.
|
|
2946
|
+
*/
|
|
2947
|
+
async writeJsonl(candidates) {
|
|
2948
|
+
if (!this.outputDir) return;
|
|
2949
|
+
await fs6.mkdir(this.outputDir, { recursive: true });
|
|
2950
|
+
const filePath = path7.join(this.outputDir, "linker.jsonl");
|
|
2951
|
+
const lines = candidates.map(
|
|
2952
|
+
(c) => JSON.stringify({
|
|
2953
|
+
id: c.id,
|
|
2954
|
+
sourceNodeId: c.sourceNodeId,
|
|
2955
|
+
sourceNodeType: c.sourceNodeType,
|
|
2956
|
+
name: c.name,
|
|
2957
|
+
confidence: c.confidence,
|
|
2958
|
+
pattern: c.pattern,
|
|
2959
|
+
signal: c.signal,
|
|
2960
|
+
nodeType: c.nodeType
|
|
2961
|
+
})
|
|
2962
|
+
);
|
|
2963
|
+
await fs6.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
|
|
2964
|
+
}
|
|
2965
|
+
/**
|
|
2966
|
+
* Write medium-confidence candidates to staged JSONL for human review.
|
|
2967
|
+
*/
|
|
2968
|
+
async writeStagedJsonl(candidates) {
|
|
2969
|
+
if (!this.outputDir || candidates.length === 0) return;
|
|
2970
|
+
const stagedDir = path7.join(this.outputDir, "staged");
|
|
2971
|
+
await fs6.mkdir(stagedDir, { recursive: true });
|
|
2972
|
+
const filePath = path7.join(stagedDir, "linker-staged.jsonl");
|
|
2973
|
+
const lines = candidates.map(
|
|
2974
|
+
(c) => JSON.stringify({
|
|
2975
|
+
id: c.id,
|
|
2976
|
+
sourceNodeId: c.sourceNodeId,
|
|
2977
|
+
sourceNodeType: c.sourceNodeType,
|
|
2978
|
+
name: c.name,
|
|
2979
|
+
confidence: c.confidence,
|
|
2980
|
+
pattern: c.pattern,
|
|
2981
|
+
signal: c.signal,
|
|
2982
|
+
nodeType: c.nodeType
|
|
2983
|
+
})
|
|
2984
|
+
);
|
|
2985
|
+
await fs6.writeFile(filePath, lines.join("\n") + "\n");
|
|
2986
|
+
}
|
|
2987
|
+
/**
|
|
2988
|
+
* Apply heuristic patterns to content and return candidate extractions.
|
|
2989
|
+
*/
|
|
2990
|
+
scanPatterns(content, nodeId, nodeType) {
|
|
2991
|
+
const candidates = [];
|
|
2992
|
+
for (const heuristic of HEURISTIC_PATTERNS) {
|
|
2993
|
+
if (heuristic.pattern.test(content)) {
|
|
2994
|
+
const candidateId = `extracted:linker:${hash(nodeId + ":" + heuristic.name)}`;
|
|
2995
|
+
candidates.push({
|
|
2996
|
+
id: candidateId,
|
|
2997
|
+
sourceNodeId: nodeId,
|
|
2998
|
+
sourceNodeType: nodeType,
|
|
2999
|
+
name: `${heuristic.signal} from ${nodeId}`,
|
|
3000
|
+
content,
|
|
3001
|
+
confidence: heuristic.confidence,
|
|
3002
|
+
pattern: heuristic.name,
|
|
3003
|
+
signal: heuristic.signal,
|
|
3004
|
+
nodeType: heuristic.nodeType
|
|
3005
|
+
});
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
return candidates;
|
|
3009
|
+
}
|
|
3010
|
+
};
|
|
3011
|
+
|
|
3012
|
+
// src/ingest/StructuralDriftDetector.ts
|
|
3013
|
+
var StructuralDriftDetector = class {
|
|
3014
|
+
detect(current, fresh) {
|
|
3015
|
+
const findings = [];
|
|
3016
|
+
const currentById = new Map(current.entries.map((e) => [e.id, e]));
|
|
3017
|
+
const freshById = new Map(fresh.entries.map((e) => [e.id, e]));
|
|
3018
|
+
for (const [id, entry] of freshById) {
|
|
3019
|
+
if (!currentById.has(id)) {
|
|
3020
|
+
findings.push({
|
|
3021
|
+
entryId: id,
|
|
3022
|
+
classification: "new",
|
|
3023
|
+
fresh: entry,
|
|
3024
|
+
severity: "low"
|
|
3025
|
+
});
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
for (const [id, entry] of currentById) {
|
|
3029
|
+
if (!freshById.has(id)) {
|
|
3030
|
+
findings.push({
|
|
3031
|
+
entryId: id,
|
|
3032
|
+
classification: "stale",
|
|
3033
|
+
current: entry,
|
|
3034
|
+
severity: "high"
|
|
3035
|
+
});
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
for (const [id, freshEntry] of freshById) {
|
|
3039
|
+
const currentEntry = currentById.get(id);
|
|
3040
|
+
if (currentEntry && currentEntry.contentHash !== freshEntry.contentHash) {
|
|
3041
|
+
findings.push({
|
|
3042
|
+
entryId: id,
|
|
3043
|
+
classification: "drifted",
|
|
3044
|
+
current: currentEntry,
|
|
3045
|
+
fresh: freshEntry,
|
|
3046
|
+
severity: "medium"
|
|
3047
|
+
});
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
const byName = /* @__PURE__ */ new Map();
|
|
3051
|
+
for (const entry of fresh.entries) {
|
|
3052
|
+
const group = byName.get(entry.name) ?? [];
|
|
3053
|
+
group.push(entry);
|
|
3054
|
+
byName.set(entry.name, group);
|
|
3055
|
+
}
|
|
3056
|
+
for (const [, group] of byName) {
|
|
3057
|
+
if (group.length > 1) {
|
|
3058
|
+
const sources = new Set(group.map((e) => e.source));
|
|
3059
|
+
const hashes = new Set(group.map((e) => e.contentHash));
|
|
3060
|
+
if (sources.size > 1 && hashes.size > 1) {
|
|
3061
|
+
const alreadyContradicting = new Set(
|
|
3062
|
+
findings.filter((f) => f.classification === "contradicting").map((f) => f.entryId)
|
|
3063
|
+
);
|
|
3064
|
+
for (const entry of group) {
|
|
3065
|
+
if (!alreadyContradicting.has(entry.id)) {
|
|
3066
|
+
findings.push({
|
|
3067
|
+
entryId: entry.id,
|
|
3068
|
+
classification: "contradicting",
|
|
3069
|
+
fresh: entry,
|
|
3070
|
+
severity: "critical"
|
|
3071
|
+
});
|
|
3072
|
+
break;
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
const totalEntries = (/* @__PURE__ */ new Set([...currentById.keys(), ...freshById.keys()])).size;
|
|
3079
|
+
const driftScore = totalEntries > 0 ? findings.length / totalEntries : 0;
|
|
3080
|
+
const summary = {
|
|
3081
|
+
new: findings.filter((f) => f.classification === "new").length,
|
|
3082
|
+
drifted: findings.filter((f) => f.classification === "drifted").length,
|
|
3083
|
+
stale: findings.filter((f) => f.classification === "stale").length,
|
|
3084
|
+
contradicting: findings.filter((f) => f.classification === "contradicting").length
|
|
3085
|
+
};
|
|
3086
|
+
return { findings, driftScore, summary };
|
|
3087
|
+
}
|
|
3088
|
+
};
|
|
3089
|
+
|
|
3090
|
+
// src/ingest/KnowledgeStagingAggregator.ts
|
|
3091
|
+
var fs7 = __toESM(require("fs/promises"));
|
|
3092
|
+
var path8 = __toESM(require("path"));
|
|
3093
|
+
var KnowledgeStagingAggregator = class {
|
|
3094
|
+
constructor(projectDir) {
|
|
3095
|
+
this.projectDir = projectDir;
|
|
3096
|
+
}
|
|
3097
|
+
projectDir;
|
|
3098
|
+
async aggregate(extractorResults, linkerResults, diagramResults) {
|
|
3099
|
+
const all = [...extractorResults, ...linkerResults, ...diagramResults];
|
|
3100
|
+
const byHash = /* @__PURE__ */ new Map();
|
|
3101
|
+
for (const entry of all) {
|
|
3102
|
+
const existing = byHash.get(entry.contentHash);
|
|
3103
|
+
if (!existing || entry.confidence > existing.confidence) {
|
|
3104
|
+
byHash.set(entry.contentHash, entry);
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
const deduplicated = Array.from(byHash.values());
|
|
3108
|
+
if (deduplicated.length === 0) {
|
|
3109
|
+
return { staged: 0 };
|
|
3110
|
+
}
|
|
3111
|
+
const stagedDir = path8.join(this.projectDir, ".harness", "knowledge", "staged");
|
|
3112
|
+
await fs7.mkdir(stagedDir, { recursive: true });
|
|
3113
|
+
const jsonl = deduplicated.map((entry) => JSON.stringify(entry)).join("\n") + "\n";
|
|
3114
|
+
await fs7.writeFile(path8.join(stagedDir, "pipeline-staged.jsonl"), jsonl, "utf-8");
|
|
3115
|
+
return { staged: deduplicated.length };
|
|
3116
|
+
}
|
|
3117
|
+
async generateGapReport(knowledgeDir) {
|
|
3118
|
+
const domains = [];
|
|
3119
|
+
let totalEntries = 0;
|
|
3120
|
+
try {
|
|
3121
|
+
const entries = await fs7.readdir(knowledgeDir, { withFileTypes: true });
|
|
3122
|
+
const domainDirs = entries.filter((e) => e.isDirectory());
|
|
3123
|
+
for (const dir of domainDirs) {
|
|
3124
|
+
const domainPath = path8.join(knowledgeDir, dir.name);
|
|
3125
|
+
const files = await fs7.readdir(domainPath);
|
|
3126
|
+
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
3127
|
+
const entryCount = mdFiles.length;
|
|
3128
|
+
totalEntries += entryCount;
|
|
3129
|
+
domains.push({ domain: dir.name, entryCount });
|
|
3130
|
+
}
|
|
3131
|
+
} catch {
|
|
3132
|
+
}
|
|
3133
|
+
return {
|
|
3134
|
+
domains,
|
|
3135
|
+
totalEntries,
|
|
3136
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3137
|
+
};
|
|
3138
|
+
}
|
|
3139
|
+
async writeGapReport(report) {
|
|
3140
|
+
const gapsDir = path8.join(this.projectDir, ".harness", "knowledge");
|
|
3141
|
+
await fs7.mkdir(gapsDir, { recursive: true });
|
|
3142
|
+
const lines = [
|
|
3143
|
+
"# Knowledge Gaps Report",
|
|
3144
|
+
"",
|
|
3145
|
+
`Generated: ${report.generatedAt}`,
|
|
3146
|
+
"",
|
|
3147
|
+
"## Coverage by Domain",
|
|
3148
|
+
"",
|
|
3149
|
+
"| Domain | Entries |",
|
|
3150
|
+
"| ------ | ------- |"
|
|
3151
|
+
];
|
|
3152
|
+
for (const domain of report.domains) {
|
|
3153
|
+
lines.push(`| ${domain.domain} | ${domain.entryCount} |`);
|
|
3154
|
+
}
|
|
3155
|
+
lines.push("", `## Total Entries: ${report.totalEntries}`, "");
|
|
3156
|
+
await fs7.writeFile(path8.join(gapsDir, "gaps.md"), lines.join("\n"), "utf-8");
|
|
3157
|
+
}
|
|
3158
|
+
};
|
|
3159
|
+
|
|
3160
|
+
// src/ingest/extractors/ExtractionRunner.ts
|
|
3161
|
+
var fs8 = __toESM(require("fs/promises"));
|
|
3162
|
+
var path9 = __toESM(require("path"));
|
|
3163
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
3164
|
+
"node_modules",
|
|
3165
|
+
"dist",
|
|
3166
|
+
"target",
|
|
3167
|
+
"build",
|
|
3168
|
+
".git",
|
|
3169
|
+
"__pycache__",
|
|
3170
|
+
".venv",
|
|
3171
|
+
".turbo",
|
|
3172
|
+
".harness",
|
|
3173
|
+
".next",
|
|
3174
|
+
".vscode",
|
|
3175
|
+
".idea",
|
|
3176
|
+
"vendor",
|
|
3177
|
+
"out",
|
|
3178
|
+
".gradle",
|
|
3179
|
+
".gradle-home",
|
|
3180
|
+
"bin",
|
|
3181
|
+
"obj",
|
|
3182
|
+
"venv",
|
|
3183
|
+
"_build",
|
|
3184
|
+
"deps",
|
|
3185
|
+
"coverage",
|
|
3186
|
+
".nyc_output"
|
|
3187
|
+
]);
|
|
3188
|
+
var EXT_TO_LANGUAGE = {
|
|
3189
|
+
".ts": "typescript",
|
|
3190
|
+
".tsx": "typescript",
|
|
3191
|
+
".js": "javascript",
|
|
3192
|
+
".jsx": "javascript",
|
|
3193
|
+
".py": "python",
|
|
3194
|
+
".go": "go",
|
|
3195
|
+
".rs": "rust",
|
|
3196
|
+
".java": "java"
|
|
3197
|
+
};
|
|
3198
|
+
var SKIP_EXTENSIONS2 = /* @__PURE__ */ new Set([".d.ts"]);
|
|
3199
|
+
function detectLanguage(filePath) {
|
|
3200
|
+
const name = path9.basename(filePath);
|
|
3201
|
+
for (const skip of SKIP_EXTENSIONS2) {
|
|
3202
|
+
if (name.endsWith(skip)) return void 0;
|
|
3203
|
+
}
|
|
3204
|
+
const ext = path9.extname(filePath);
|
|
3205
|
+
return EXT_TO_LANGUAGE[ext];
|
|
3206
|
+
}
|
|
3207
|
+
var EXTRACTOR_EDGE_TYPE = {
|
|
3208
|
+
"test-descriptions": "governs",
|
|
3209
|
+
"enum-constants": "documents",
|
|
3210
|
+
"validation-rules": "governs",
|
|
3211
|
+
"api-paths": "documents"
|
|
3212
|
+
};
|
|
3213
|
+
var ExtractionRunner = class {
|
|
3214
|
+
constructor(extractors) {
|
|
3215
|
+
this.extractors = extractors;
|
|
3216
|
+
}
|
|
3217
|
+
extractors;
|
|
3218
|
+
/**
|
|
3219
|
+
* Run all extractors against a project directory.
|
|
3220
|
+
* @param projectDir - Project root directory
|
|
3221
|
+
* @param store - GraphStore for node/edge persistence
|
|
3222
|
+
* @param outputDir - Directory for JSONL output (e.g. .harness/knowledge/extracted/)
|
|
3223
|
+
*/
|
|
3224
|
+
async run(projectDir, store, outputDir) {
|
|
3225
|
+
const start = Date.now();
|
|
3226
|
+
const errors = [];
|
|
3227
|
+
let nodesAdded = 0;
|
|
3228
|
+
let nodesUpdated = 0;
|
|
3229
|
+
let edgesAdded = 0;
|
|
3230
|
+
await fs8.mkdir(outputDir, { recursive: true });
|
|
3231
|
+
const files = await this.findSourceFiles(projectDir);
|
|
3232
|
+
const recordsByExtractor = /* @__PURE__ */ new Map();
|
|
3233
|
+
for (const ext of this.extractors) {
|
|
3234
|
+
recordsByExtractor.set(ext.name, []);
|
|
3235
|
+
}
|
|
3236
|
+
for (const filePath of files) {
|
|
3237
|
+
const language = detectLanguage(filePath);
|
|
3238
|
+
if (!language) continue;
|
|
3239
|
+
const ext = path9.extname(filePath);
|
|
3240
|
+
let content;
|
|
3241
|
+
try {
|
|
3242
|
+
content = await fs8.readFile(filePath, "utf-8");
|
|
3243
|
+
} catch (err) {
|
|
3244
|
+
errors.push(
|
|
3245
|
+
`Failed to read ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
3246
|
+
);
|
|
3247
|
+
continue;
|
|
3248
|
+
}
|
|
3249
|
+
const relativePath = path9.relative(projectDir, filePath).replaceAll("\\", "/");
|
|
3250
|
+
for (const extractor2 of this.extractors) {
|
|
3251
|
+
if (!extractor2.supportedExtensions.includes(ext)) continue;
|
|
3252
|
+
try {
|
|
3253
|
+
const records = extractor2.extract(content, relativePath, language);
|
|
3254
|
+
recordsByExtractor.get(extractor2.name).push(...records);
|
|
3255
|
+
} catch (err) {
|
|
3256
|
+
errors.push(
|
|
3257
|
+
`Extractor ${extractor2.name} failed on ${relativePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
3258
|
+
);
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
const allCurrentIds = /* @__PURE__ */ new Set();
|
|
3263
|
+
for (const [extractorName, records] of recordsByExtractor) {
|
|
3264
|
+
await this.writeJsonl(records, outputDir, extractorName);
|
|
3265
|
+
for (const record of records) {
|
|
3266
|
+
allCurrentIds.add(record.id);
|
|
3267
|
+
const result = this.persistRecord(record, store);
|
|
3268
|
+
nodesAdded += result.nodesAdded;
|
|
3269
|
+
nodesUpdated += result.nodesUpdated;
|
|
3270
|
+
edgesAdded += result.edgesAdded;
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
const staleCount = this.markStale(store, allCurrentIds);
|
|
3274
|
+
nodesUpdated += staleCount;
|
|
3275
|
+
return {
|
|
3276
|
+
nodesAdded,
|
|
3277
|
+
nodesUpdated,
|
|
3278
|
+
edgesAdded,
|
|
3279
|
+
edgesUpdated: 0,
|
|
3280
|
+
errors,
|
|
3281
|
+
durationMs: Date.now() - start
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
/** Write extraction records to JSONL file. */
|
|
3285
|
+
async writeJsonl(records, outputDir, extractorName) {
|
|
3286
|
+
const filePath = path9.join(outputDir, `${extractorName}.jsonl`);
|
|
3287
|
+
const lines = records.map((r) => JSON.stringify(r));
|
|
3288
|
+
await fs8.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
|
|
3289
|
+
}
|
|
3290
|
+
/** Create or update a graph node from an extraction record. */
|
|
3291
|
+
persistRecord(record, store) {
|
|
3292
|
+
const existing = store.getNode(record.id);
|
|
3293
|
+
const node = {
|
|
3294
|
+
id: record.id,
|
|
3295
|
+
type: record.nodeType,
|
|
3296
|
+
name: record.name,
|
|
3297
|
+
path: record.filePath,
|
|
3298
|
+
location: {
|
|
3299
|
+
fileId: `file:${hash(record.filePath)}`,
|
|
3300
|
+
startLine: record.line,
|
|
3301
|
+
endLine: record.line
|
|
3302
|
+
},
|
|
3303
|
+
content: record.content,
|
|
3304
|
+
metadata: {
|
|
3305
|
+
...record.metadata,
|
|
3306
|
+
source: "code-extractor",
|
|
3307
|
+
extractor: record.extractor,
|
|
3308
|
+
confidence: record.confidence,
|
|
3309
|
+
language: record.language,
|
|
3310
|
+
stale: false
|
|
3311
|
+
}
|
|
3312
|
+
};
|
|
3313
|
+
store.addNode(node);
|
|
3314
|
+
const fileNodeId = `file:${hash(record.filePath)}`;
|
|
3315
|
+
const edgeType = EXTRACTOR_EDGE_TYPE[record.extractor] ?? "documents";
|
|
3316
|
+
const edge = {
|
|
3317
|
+
from: record.id,
|
|
3318
|
+
to: fileNodeId,
|
|
3319
|
+
type: edgeType,
|
|
3320
|
+
confidence: record.confidence,
|
|
3321
|
+
metadata: { source: "code-extractor" }
|
|
3322
|
+
};
|
|
3323
|
+
store.addEdge(edge);
|
|
3324
|
+
return {
|
|
3325
|
+
nodesAdded: existing ? 0 : 1,
|
|
3326
|
+
nodesUpdated: existing ? 1 : 0,
|
|
3327
|
+
edgesAdded: existing ? 0 : 1
|
|
3328
|
+
};
|
|
3329
|
+
}
|
|
3330
|
+
/**
|
|
3331
|
+
* Mark nodes from previous extractions that are no longer present as stale.
|
|
3332
|
+
* Returns the number of nodes marked stale.
|
|
3333
|
+
*/
|
|
3334
|
+
markStale(store, currentIds) {
|
|
3335
|
+
let count = 0;
|
|
3336
|
+
const businessTypes = ["business_rule", "business_process", "business_term"];
|
|
3337
|
+
for (const type of businessTypes) {
|
|
3338
|
+
const nodes = store.findNodes({ type });
|
|
3339
|
+
for (const node of nodes) {
|
|
3340
|
+
if (node.metadata.source === "code-extractor" && !node.metadata.stale && !currentIds.has(node.id)) {
|
|
3341
|
+
store.addNode({
|
|
3342
|
+
...node,
|
|
3343
|
+
metadata: {
|
|
3344
|
+
...node.metadata,
|
|
3345
|
+
stale: true,
|
|
3346
|
+
staleAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3347
|
+
}
|
|
3348
|
+
});
|
|
3349
|
+
count++;
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
return count;
|
|
3354
|
+
}
|
|
3355
|
+
/** Recursively find source files, skipping common non-source directories. */
|
|
3356
|
+
async findSourceFiles(dir) {
|
|
3357
|
+
const results = [];
|
|
3358
|
+
let entries;
|
|
3359
|
+
try {
|
|
3360
|
+
entries = await fs8.readdir(dir, { withFileTypes: true });
|
|
3361
|
+
} catch {
|
|
3362
|
+
return results;
|
|
3363
|
+
}
|
|
3364
|
+
for (const entry of entries) {
|
|
3365
|
+
const fullPath = path9.join(dir, entry.name);
|
|
3366
|
+
if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
|
|
3367
|
+
results.push(...await this.findSourceFiles(fullPath));
|
|
3368
|
+
} else if (entry.isFile() && detectLanguage(fullPath) !== void 0) {
|
|
3369
|
+
results.push(fullPath);
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
return results;
|
|
3373
|
+
}
|
|
3374
|
+
};
|
|
3375
|
+
|
|
3376
|
+
// src/ingest/extractors/TestDescriptionExtractor.ts
|
|
3377
|
+
var PATTERNS = {
|
|
3378
|
+
typescript: [/(?:describe|it|test)\s*\(\s*(['"`])((?:(?!\1).)*)\1/g],
|
|
3379
|
+
javascript: [/(?:describe|it|test)\s*\(\s*(['"`])((?:(?!\1).)*)\1/g],
|
|
3380
|
+
python: [/def\s+(test_\w+)\s*\(/g, /"""((?:(?!""").)*?)"""/gs, /class\s+(Test\w+)/g],
|
|
3381
|
+
go: [/func\s+(Test\w+)\s*\(/g, /t\.Run\(\s*"([^"]+)"/g],
|
|
3382
|
+
rust: [/#\[test\]\s*\n\s*(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/g, /\/\/\/\s*(.+)/g],
|
|
3383
|
+
java: [
|
|
3384
|
+
/@DisplayName\s*\(\s*"([^"]+)"\s*\)/g,
|
|
3385
|
+
/@Test\s*\n\s*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:void|[\w<>]+)\s+(\w+)\s*\(/g
|
|
3386
|
+
]
|
|
3387
|
+
};
|
|
3388
|
+
var TestDescriptionExtractor = class {
|
|
3389
|
+
name = "test-descriptions";
|
|
3390
|
+
supportedExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
|
|
3391
|
+
extract(content, filePath, language) {
|
|
3392
|
+
const records = [];
|
|
3393
|
+
const patterns = PATTERNS[language];
|
|
3394
|
+
if (!patterns) return records;
|
|
3395
|
+
const lines = content.split("\n");
|
|
3396
|
+
if (language === "typescript" || language === "javascript") {
|
|
3397
|
+
return this.extractJsTs(content, filePath, language, lines);
|
|
3398
|
+
}
|
|
3399
|
+
if (language === "python") {
|
|
3400
|
+
return this.extractPython(content, filePath, language, lines);
|
|
3401
|
+
}
|
|
3402
|
+
if (language === "go") {
|
|
3403
|
+
return this.extractGo(content, filePath, language, lines);
|
|
3404
|
+
}
|
|
3405
|
+
if (language === "rust") {
|
|
3406
|
+
return this.extractRust(content, filePath, language, lines);
|
|
3407
|
+
}
|
|
3408
|
+
if (language === "java") {
|
|
3409
|
+
return this.extractJava(content, filePath, language, lines);
|
|
3410
|
+
}
|
|
3411
|
+
return records;
|
|
3412
|
+
}
|
|
3413
|
+
extractJsTs(_content, filePath, language, lines) {
|
|
3414
|
+
const records = [];
|
|
3415
|
+
const describeStack = [];
|
|
3416
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3417
|
+
const line = lines[i];
|
|
3418
|
+
const describeMatch = line.match(/describe\s*\(\s*(['"`])((?:(?!\1).)*)\1/);
|
|
3419
|
+
if (describeMatch) {
|
|
3420
|
+
describeStack.push(describeMatch[2]);
|
|
3421
|
+
}
|
|
3422
|
+
const itMatch = line.match(/(?:it|test)\s*\(\s*(['"`])((?:(?!\1).)*)\1/);
|
|
3423
|
+
if (itMatch) {
|
|
3424
|
+
const testName = itMatch[2];
|
|
3425
|
+
const fullPath = [...describeStack, testName].join(" > ");
|
|
3426
|
+
const patternKey = fullPath;
|
|
3427
|
+
records.push({
|
|
3428
|
+
id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
|
|
3429
|
+
extractor: "test-descriptions",
|
|
3430
|
+
language,
|
|
3431
|
+
filePath,
|
|
3432
|
+
line: i + 1,
|
|
3433
|
+
nodeType: "business_rule",
|
|
3434
|
+
name: testName,
|
|
3435
|
+
content: fullPath,
|
|
3436
|
+
confidence: 0.7,
|
|
3437
|
+
metadata: {
|
|
3438
|
+
suite: describeStack.length > 0 ? describeStack[describeStack.length - 1] : void 0,
|
|
3439
|
+
framework: "vitest"
|
|
3440
|
+
}
|
|
3441
|
+
});
|
|
3442
|
+
}
|
|
3443
|
+
if (line.match(/^\s*\}\s*\)\s*;?\s*$/) && describeStack.length > 0) {
|
|
3444
|
+
describeStack.pop();
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
return records;
|
|
3448
|
+
}
|
|
3449
|
+
extractPython(_content, filePath, language, lines) {
|
|
3450
|
+
const records = [];
|
|
3451
|
+
let currentClass;
|
|
3452
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3453
|
+
const line = lines[i];
|
|
3454
|
+
const classMatch = line.match(/^class\s+(Test\w+)/);
|
|
3455
|
+
if (classMatch) {
|
|
3456
|
+
currentClass = classMatch[1];
|
|
3457
|
+
}
|
|
3458
|
+
const funcMatch = line.match(/^\s*def\s+(test_\w+)\s*\(/);
|
|
3459
|
+
if (funcMatch) {
|
|
3460
|
+
const testName = funcMatch[1];
|
|
3461
|
+
const humanName = testName.replace(/^test_/, "").replace(/_/g, " ");
|
|
3462
|
+
let docstring;
|
|
3463
|
+
if (i + 1 < lines.length) {
|
|
3464
|
+
const nextLine = lines[i + 1].trim();
|
|
3465
|
+
const docMatch = nextLine.match(/^"""(.+?)"""/);
|
|
3466
|
+
if (docMatch) {
|
|
3467
|
+
docstring = docMatch[1];
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
const patternKey = currentClass ? `${currentClass}.${testName}` : testName;
|
|
3471
|
+
records.push({
|
|
3472
|
+
id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
|
|
3473
|
+
extractor: "test-descriptions",
|
|
3474
|
+
language,
|
|
3475
|
+
filePath,
|
|
3476
|
+
line: i + 1,
|
|
3477
|
+
nodeType: "business_rule",
|
|
3478
|
+
name: docstring ?? humanName,
|
|
3479
|
+
content: patternKey,
|
|
3480
|
+
confidence: docstring ? 0.7 : 0.5,
|
|
3481
|
+
metadata: {
|
|
3482
|
+
suite: currentClass,
|
|
3483
|
+
framework: "pytest",
|
|
3484
|
+
functionName: testName
|
|
3485
|
+
}
|
|
3486
|
+
});
|
|
3487
|
+
}
|
|
3488
|
+
if (currentClass && /^\S/.test(line) && !line.startsWith("class ") && !line.startsWith("#") && line.trim() !== "") {
|
|
3489
|
+
currentClass = void 0;
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
return records;
|
|
3493
|
+
}
|
|
3494
|
+
extractGo(_content, filePath, language, lines) {
|
|
3495
|
+
const records = [];
|
|
3496
|
+
let currentTest;
|
|
3497
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3498
|
+
const line = lines[i];
|
|
3499
|
+
const funcMatch = line.match(/^func\s+(Test\w+)\s*\(/);
|
|
3500
|
+
if (funcMatch) {
|
|
3501
|
+
currentTest = funcMatch[1];
|
|
3502
|
+
const humanName = currentTest.replace(/^Test/, "").replace(/([A-Z])/g, " $1").trim();
|
|
3503
|
+
records.push({
|
|
3504
|
+
id: `extracted:test-descriptions:${hash(filePath + ":" + currentTest)}`,
|
|
3505
|
+
extractor: "test-descriptions",
|
|
3506
|
+
language,
|
|
3507
|
+
filePath,
|
|
3508
|
+
line: i + 1,
|
|
3509
|
+
nodeType: "business_rule",
|
|
3510
|
+
name: humanName,
|
|
3511
|
+
content: currentTest,
|
|
3512
|
+
confidence: 0.5,
|
|
3513
|
+
metadata: { framework: "testing" }
|
|
3514
|
+
});
|
|
3515
|
+
}
|
|
3516
|
+
const runMatch = line.match(/t\.Run\(\s*"([^"]+)"/);
|
|
3517
|
+
if (runMatch && currentTest) {
|
|
3518
|
+
const subtestName = runMatch[1];
|
|
3519
|
+
const patternKey = `${currentTest} > ${subtestName}`;
|
|
3520
|
+
records.push({
|
|
3521
|
+
id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
|
|
3522
|
+
extractor: "test-descriptions",
|
|
3523
|
+
language,
|
|
3524
|
+
filePath,
|
|
3525
|
+
line: i + 1,
|
|
3526
|
+
nodeType: "business_rule",
|
|
3527
|
+
name: subtestName,
|
|
3528
|
+
content: patternKey,
|
|
3529
|
+
confidence: 0.7,
|
|
3530
|
+
metadata: {
|
|
3531
|
+
suite: currentTest,
|
|
3532
|
+
framework: "testing"
|
|
3533
|
+
}
|
|
3534
|
+
});
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
return records;
|
|
3538
|
+
}
|
|
3539
|
+
extractRust(_content, filePath, language, lines) {
|
|
3540
|
+
const records = [];
|
|
3541
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3542
|
+
const line = lines[i];
|
|
3543
|
+
if (line.trim() === "#[test]") {
|
|
3544
|
+
for (let j = i + 1; j < lines.length && j <= i + 3; j++) {
|
|
3545
|
+
const fnLine = lines[j];
|
|
3546
|
+
const fnMatch = fnLine.match(/(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/);
|
|
3547
|
+
if (fnMatch) {
|
|
3548
|
+
const testName = fnMatch[1];
|
|
3549
|
+
const humanName = testName.replace(/^test_/, "").replace(/_/g, " ");
|
|
3550
|
+
let docComment;
|
|
3551
|
+
if (i > 0) {
|
|
3552
|
+
const prevLine = lines[i - 1].trim();
|
|
3553
|
+
const docMatch = prevLine.match(/^\/\/\/\s*(.+)/);
|
|
3554
|
+
if (docMatch) {
|
|
3555
|
+
docComment = docMatch[1];
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
records.push({
|
|
3559
|
+
id: `extracted:test-descriptions:${hash(filePath + ":" + testName)}`,
|
|
3560
|
+
extractor: "test-descriptions",
|
|
3561
|
+
language,
|
|
3562
|
+
filePath,
|
|
3563
|
+
line: j + 1,
|
|
3564
|
+
nodeType: "business_rule",
|
|
3565
|
+
name: docComment ?? humanName,
|
|
3566
|
+
content: testName,
|
|
3567
|
+
confidence: docComment ? 0.7 : 0.5,
|
|
3568
|
+
metadata: { framework: "rust-test" }
|
|
3569
|
+
});
|
|
3570
|
+
break;
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
return records;
|
|
3576
|
+
}
|
|
3577
|
+
extractJava(_content, filePath, language, lines) {
|
|
3578
|
+
const records = [];
|
|
3579
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3580
|
+
const line = lines[i];
|
|
3581
|
+
const testMatch = line.match(/@Test\s*$/);
|
|
3582
|
+
if (testMatch) {
|
|
3583
|
+
let displayName;
|
|
3584
|
+
for (let k = Math.max(0, i - 3); k < i; k++) {
|
|
3585
|
+
const prevLine = lines[k];
|
|
3586
|
+
const dm = prevLine.match(/@DisplayName\s*\(\s*"([^"]+)"\s*\)/);
|
|
3587
|
+
if (dm) displayName = dm[1];
|
|
3588
|
+
}
|
|
3589
|
+
for (let j = i + 1; j < lines.length && j <= i + 5; j++) {
|
|
3590
|
+
const scanLine = lines[j];
|
|
3591
|
+
const adjacentDisplay = scanLine.match(/@DisplayName\s*\(\s*"([^"]+)"\s*\)/);
|
|
3592
|
+
if (adjacentDisplay) {
|
|
3593
|
+
displayName = adjacentDisplay[1];
|
|
3594
|
+
continue;
|
|
3595
|
+
}
|
|
3596
|
+
const methodMatch = scanLine.match(
|
|
3597
|
+
/^\s*(?:(?:public|private|protected)\s+)?(?:static\s+)?(?:void|[\w<>[\]]+)\s+(\w+)\s*\(/
|
|
3598
|
+
);
|
|
3599
|
+
if (methodMatch) {
|
|
3600
|
+
const methodName = methodMatch[1];
|
|
3601
|
+
const name = displayName ?? methodName;
|
|
3602
|
+
const patternKey = displayName ?? methodName;
|
|
3603
|
+
records.push({
|
|
3604
|
+
id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
|
|
3605
|
+
extractor: "test-descriptions",
|
|
3606
|
+
language,
|
|
3607
|
+
filePath,
|
|
3608
|
+
line: j + 1,
|
|
3609
|
+
nodeType: "business_rule",
|
|
3610
|
+
name,
|
|
3611
|
+
content: patternKey,
|
|
3612
|
+
confidence: displayName ? 0.7 : 0.5,
|
|
3613
|
+
metadata: { framework: "junit5" }
|
|
3614
|
+
});
|
|
3615
|
+
break;
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
return records;
|
|
3621
|
+
}
|
|
3622
|
+
};
|
|
3623
|
+
|
|
3624
|
+
// src/ingest/extractors/EnumConstantExtractor.ts
|
|
3625
|
+
var EnumConstantExtractor = class {
|
|
3626
|
+
name = "enum-constants";
|
|
3627
|
+
supportedExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
|
|
3628
|
+
extract(content, filePath, language) {
|
|
3629
|
+
switch (language) {
|
|
3630
|
+
case "typescript":
|
|
3631
|
+
return this.extractTypeScript(content, filePath, language);
|
|
3632
|
+
case "javascript":
|
|
3633
|
+
return this.extractJavaScript(content, filePath, language);
|
|
3634
|
+
case "python":
|
|
3635
|
+
return this.extractPython(content, filePath, language);
|
|
3636
|
+
case "go":
|
|
3637
|
+
return this.extractGo(content, filePath, language);
|
|
3638
|
+
case "rust":
|
|
3639
|
+
return this.extractRust(content, filePath, language);
|
|
3640
|
+
case "java":
|
|
3641
|
+
return this.extractJava(content, filePath, language);
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
extractTypeScript(content, filePath, language) {
|
|
3645
|
+
const records = [];
|
|
3646
|
+
const lines = content.split("\n");
|
|
3647
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3648
|
+
const line = lines[i];
|
|
3649
|
+
const enumMatch = line.match(/(?:export\s+)?enum\s+(\w+)/);
|
|
3650
|
+
if (enumMatch) {
|
|
3651
|
+
const enumName = enumMatch[1];
|
|
3652
|
+
const members = this.collectEnumMembers(lines, i + 1);
|
|
3653
|
+
records.push({
|
|
3654
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + enumName)}`,
|
|
3655
|
+
extractor: "enum-constants",
|
|
3656
|
+
language,
|
|
3657
|
+
filePath,
|
|
3658
|
+
line: i + 1,
|
|
3659
|
+
nodeType: "business_term",
|
|
3660
|
+
name: enumName,
|
|
3661
|
+
content: `enum ${enumName} { ${members.join(", ")} }`,
|
|
3662
|
+
confidence: 0.8,
|
|
3663
|
+
metadata: { kind: "enum", members }
|
|
3664
|
+
});
|
|
3665
|
+
}
|
|
3666
|
+
const constMatch = line.match(/(?:export\s+)?const\s+(\w+)\s*=\s*\{/);
|
|
3667
|
+
if (constMatch && content.includes("as const")) {
|
|
3668
|
+
const blockEnd = this.findClosingBrace(lines, i);
|
|
3669
|
+
const block = lines.slice(i, blockEnd + 1).join("\n");
|
|
3670
|
+
if (block.includes("as const")) {
|
|
3671
|
+
const constName = constMatch[1];
|
|
3672
|
+
const members = this.collectObjectKeys(lines, i + 1);
|
|
3673
|
+
records.push({
|
|
3674
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + constName)}`,
|
|
3675
|
+
extractor: "enum-constants",
|
|
3676
|
+
language,
|
|
3677
|
+
filePath,
|
|
3678
|
+
line: i + 1,
|
|
3679
|
+
nodeType: "business_term",
|
|
3680
|
+
name: constName,
|
|
3681
|
+
content: `const ${constName} = { ${members.join(", ")} } as const`,
|
|
3682
|
+
confidence: 0.8,
|
|
3683
|
+
metadata: { kind: "as-const", members }
|
|
3684
|
+
});
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
const unionMatch = line.match(
|
|
3688
|
+
/(?:export\s+)?type\s+(\w+)\s*=\s*(['"`][\w]+['"`](?:\s*\|\s*['"`][\w]+['"`])+)/
|
|
3689
|
+
);
|
|
3690
|
+
if (unionMatch) {
|
|
3691
|
+
const typeName = unionMatch[1];
|
|
3692
|
+
const values = unionMatch[2].split("|").map((v) => v.trim().replace(/['"`]/g, ""));
|
|
3693
|
+
records.push({
|
|
3694
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + typeName)}`,
|
|
3695
|
+
extractor: "enum-constants",
|
|
3696
|
+
language,
|
|
3697
|
+
filePath,
|
|
3698
|
+
line: i + 1,
|
|
3699
|
+
nodeType: "business_term",
|
|
3700
|
+
name: typeName,
|
|
3701
|
+
content: `type ${typeName} = ${values.map((v) => `'${v}'`).join(" | ")}`,
|
|
3702
|
+
confidence: 0.7,
|
|
3703
|
+
metadata: { kind: "union-type", members: values }
|
|
3704
|
+
});
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
return records;
|
|
3708
|
+
}
|
|
3709
|
+
extractJavaScript(content, filePath, language) {
|
|
3710
|
+
const records = [];
|
|
3711
|
+
const lines = content.split("\n");
|
|
3712
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3713
|
+
const line = lines[i];
|
|
3714
|
+
const freezeMatch = line.match(/(?:export\s+)?const\s+(\w+)\s*=\s*Object\.freeze\s*\(\s*\{/);
|
|
3715
|
+
if (freezeMatch) {
|
|
3716
|
+
const name = freezeMatch[1];
|
|
3717
|
+
const members = this.collectObjectKeys(lines, i + 1);
|
|
3718
|
+
records.push({
|
|
3719
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + name)}`,
|
|
3720
|
+
extractor: "enum-constants",
|
|
3721
|
+
language,
|
|
3722
|
+
filePath,
|
|
3723
|
+
line: i + 1,
|
|
3724
|
+
nodeType: "business_term",
|
|
3725
|
+
name,
|
|
3726
|
+
content: `const ${name} = Object.freeze({ ${members.join(", ")} })`,
|
|
3727
|
+
confidence: 0.8,
|
|
3728
|
+
metadata: { kind: "frozen-object", members }
|
|
3729
|
+
});
|
|
3730
|
+
}
|
|
3731
|
+
const constMatch = line.match(/(?:export\s+)?const\s+([A-Z][A-Z_\d]*)\s*=\s*\{/);
|
|
3732
|
+
if (constMatch && !freezeMatch) {
|
|
3733
|
+
const name = constMatch[1];
|
|
3734
|
+
const members = this.collectObjectKeys(lines, i + 1);
|
|
3735
|
+
if (members.length > 0 && members.every((m) => /^[A-Z][A-Z_\d]*$/.test(m))) {
|
|
3736
|
+
records.push({
|
|
3737
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + name)}`,
|
|
3738
|
+
extractor: "enum-constants",
|
|
3739
|
+
language,
|
|
3740
|
+
filePath,
|
|
3741
|
+
line: i + 1,
|
|
3742
|
+
nodeType: "business_term",
|
|
3743
|
+
name,
|
|
3744
|
+
content: `const ${name} = { ${members.join(", ")} }`,
|
|
3745
|
+
confidence: 0.6,
|
|
3746
|
+
metadata: { kind: "const-object", members }
|
|
3747
|
+
});
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
return records;
|
|
3752
|
+
}
|
|
3753
|
+
extractPython(content, filePath, language) {
|
|
3754
|
+
const records = [];
|
|
3755
|
+
const lines = content.split("\n");
|
|
3756
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3757
|
+
const line = lines[i];
|
|
3758
|
+
const enumMatch = line.match(
|
|
3759
|
+
/^class\s+(\w+)\s*\(\s*(?:str\s*,\s*)?(?:Enum|StrEnum|IntEnum|Flag|IntFlag)\s*\)/
|
|
3760
|
+
);
|
|
3761
|
+
if (enumMatch) {
|
|
3762
|
+
const enumName = enumMatch[1];
|
|
3763
|
+
const members = this.collectPythonEnumMembers(lines, i + 1);
|
|
3764
|
+
records.push({
|
|
3765
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + enumName)}`,
|
|
3766
|
+
extractor: "enum-constants",
|
|
3767
|
+
language,
|
|
3768
|
+
filePath,
|
|
3769
|
+
line: i + 1,
|
|
3770
|
+
nodeType: "business_term",
|
|
3771
|
+
name: enumName,
|
|
3772
|
+
content: `class ${enumName}(Enum) { ${members.join(", ")} }`,
|
|
3773
|
+
confidence: 0.8,
|
|
3774
|
+
metadata: { kind: "enum", members }
|
|
3775
|
+
});
|
|
3776
|
+
}
|
|
3777
|
+
const literalMatch = line.match(/(\w+)\s*=\s*Literal\s*\[([^\]]+)\]/);
|
|
3778
|
+
if (literalMatch) {
|
|
3779
|
+
const typeName = literalMatch[1];
|
|
3780
|
+
const values = literalMatch[2].split(",").map((v) => v.trim().replace(/["']/g, ""));
|
|
3781
|
+
records.push({
|
|
3782
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + typeName)}`,
|
|
3783
|
+
extractor: "enum-constants",
|
|
3784
|
+
language,
|
|
3785
|
+
filePath,
|
|
3786
|
+
line: i + 1,
|
|
3787
|
+
nodeType: "business_term",
|
|
3788
|
+
name: typeName,
|
|
3789
|
+
content: `${typeName} = Literal[${values.map((v) => `"${v}"`).join(", ")}]`,
|
|
3790
|
+
confidence: 0.7,
|
|
3791
|
+
metadata: { kind: "literal-type", members: values }
|
|
3792
|
+
});
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
return records;
|
|
3796
|
+
}
|
|
3797
|
+
extractGo(content, filePath, language) {
|
|
3798
|
+
const records = [];
|
|
3799
|
+
const lines = content.split("\n");
|
|
3800
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3801
|
+
const line = lines[i];
|
|
3802
|
+
const typeMatch = line.match(/^type\s+(\w+)\s+(?:int|string|uint|int32|int64|uint32|uint64)/);
|
|
3803
|
+
if (typeMatch) {
|
|
3804
|
+
}
|
|
3805
|
+
const constBlockMatch = line.match(/^const\s*\(/);
|
|
3806
|
+
if (constBlockMatch) {
|
|
3807
|
+
const consts = [];
|
|
3808
|
+
let typeName;
|
|
3809
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
3810
|
+
const constLine = lines[j].trim();
|
|
3811
|
+
if (constLine === ")") break;
|
|
3812
|
+
if (constLine === "" || constLine.startsWith("//")) continue;
|
|
3813
|
+
const iotaMatch = constLine.match(/^(\w+)\s+(\w+)\s*=\s*iota/);
|
|
3814
|
+
if (iotaMatch) {
|
|
3815
|
+
typeName = iotaMatch[2];
|
|
3816
|
+
consts.push(iotaMatch[1]);
|
|
3817
|
+
continue;
|
|
3818
|
+
}
|
|
3819
|
+
const typedMatch = constLine.match(/^(\w+)\s+(\w+)\s*=\s*"[^"]*"/);
|
|
3820
|
+
if (typedMatch) {
|
|
3821
|
+
typeName = typeName ?? typedMatch[2];
|
|
3822
|
+
consts.push(typedMatch[1]);
|
|
3823
|
+
continue;
|
|
3824
|
+
}
|
|
3825
|
+
const bareMatch = constLine.match(/^(\w+)\s*$/);
|
|
3826
|
+
if (bareMatch) {
|
|
3827
|
+
consts.push(bareMatch[1]);
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3830
|
+
if (consts.length > 0) {
|
|
3831
|
+
const name = typeName ?? consts[0];
|
|
3832
|
+
records.push({
|
|
3833
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + name)}`,
|
|
3834
|
+
extractor: "enum-constants",
|
|
3835
|
+
language,
|
|
3836
|
+
filePath,
|
|
3837
|
+
line: i + 1,
|
|
3838
|
+
nodeType: "business_term",
|
|
3839
|
+
name,
|
|
3840
|
+
content: `const ( ${consts.join(", ")} )`,
|
|
3841
|
+
confidence: typeName ? 0.8 : 0.6,
|
|
3842
|
+
metadata: { kind: typeName ? "typed-const" : "const-block", members: consts }
|
|
3843
|
+
});
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
return records;
|
|
3848
|
+
}
|
|
3849
|
+
extractRust(content, filePath, language) {
|
|
3850
|
+
const records = [];
|
|
3851
|
+
const lines = content.split("\n");
|
|
3852
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3853
|
+
const line = lines[i];
|
|
3854
|
+
const enumMatch = line.match(/^(?:pub\s+)?enum\s+(\w+)/);
|
|
3855
|
+
if (enumMatch) {
|
|
3856
|
+
const enumName = enumMatch[1];
|
|
3857
|
+
const members = this.collectRustEnumVariants(lines, i + 1);
|
|
3858
|
+
records.push({
|
|
3859
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + enumName)}`,
|
|
3860
|
+
extractor: "enum-constants",
|
|
3861
|
+
language,
|
|
3862
|
+
filePath,
|
|
3863
|
+
line: i + 1,
|
|
3864
|
+
nodeType: "business_term",
|
|
3865
|
+
name: enumName,
|
|
3866
|
+
content: `enum ${enumName} { ${members.join(", ")} }`,
|
|
3867
|
+
confidence: 0.8,
|
|
3868
|
+
metadata: { kind: "enum", members }
|
|
3869
|
+
});
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
return records;
|
|
3873
|
+
}
|
|
3874
|
+
extractJava(content, filePath, language) {
|
|
3875
|
+
const records = [];
|
|
3876
|
+
const lines = content.split("\n");
|
|
3877
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3878
|
+
const line = lines[i];
|
|
3879
|
+
const enumMatch = line.match(/(?:public\s+|private\s+|protected\s+)?enum\s+(\w+)/);
|
|
3880
|
+
if (enumMatch) {
|
|
3881
|
+
const enumName = enumMatch[1];
|
|
3882
|
+
const members = this.collectJavaEnumConstants(lines, i + 1);
|
|
3883
|
+
records.push({
|
|
3884
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + enumName)}`,
|
|
3885
|
+
extractor: "enum-constants",
|
|
3886
|
+
language,
|
|
3887
|
+
filePath,
|
|
3888
|
+
line: i + 1,
|
|
3889
|
+
nodeType: "business_term",
|
|
3890
|
+
name: enumName,
|
|
3891
|
+
content: `enum ${enumName} { ${members.join(", ")} }`,
|
|
3892
|
+
confidence: 0.8,
|
|
3893
|
+
metadata: { kind: "enum", members }
|
|
3894
|
+
});
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
3897
|
+
return records;
|
|
3898
|
+
}
|
|
3899
|
+
// --- Helper methods ---
|
|
3900
|
+
collectEnumMembers(lines, startLine) {
|
|
3901
|
+
const members = [];
|
|
3902
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3903
|
+
const line = lines[i].trim();
|
|
3904
|
+
if (line === "}") break;
|
|
3905
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
3906
|
+
const match = line.match(/^(\w+)/);
|
|
3907
|
+
if (match) members.push(match[1]);
|
|
3908
|
+
}
|
|
3909
|
+
return members;
|
|
3910
|
+
}
|
|
3911
|
+
collectObjectKeys(lines, startLine) {
|
|
3912
|
+
const keys = [];
|
|
3913
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3914
|
+
const line = lines[i].trim();
|
|
3915
|
+
if (line.startsWith("}")) break;
|
|
3916
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
3917
|
+
const match = line.match(/^(\w+)\s*:/);
|
|
3918
|
+
if (match) keys.push(match[1]);
|
|
3919
|
+
}
|
|
3920
|
+
return keys;
|
|
3921
|
+
}
|
|
3922
|
+
findClosingBrace(lines, startLine) {
|
|
3923
|
+
let depth = 0;
|
|
3924
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3925
|
+
for (const ch of lines[i]) {
|
|
3926
|
+
if (ch === "{") depth++;
|
|
3927
|
+
if (ch === "}") {
|
|
3928
|
+
depth--;
|
|
3929
|
+
if (depth === 0) return i;
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
return lines.length - 1;
|
|
3934
|
+
}
|
|
3935
|
+
collectPythonEnumMembers(lines, startLine) {
|
|
3936
|
+
const members = [];
|
|
3937
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3938
|
+
const line = lines[i];
|
|
3939
|
+
if (/^\S/.test(line) && line.trim() !== "") break;
|
|
3940
|
+
const match = line.match(/^\s+(\w+)\s*=/);
|
|
3941
|
+
if (match) members.push(match[1]);
|
|
3942
|
+
}
|
|
3943
|
+
return members;
|
|
3944
|
+
}
|
|
3945
|
+
collectRustEnumVariants(lines, startLine) {
|
|
3946
|
+
const variants = [];
|
|
3947
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3948
|
+
const line = lines[i].trim();
|
|
3949
|
+
if (line === "}") break;
|
|
3950
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
3951
|
+
const match = line.match(/^(\w+)/);
|
|
3952
|
+
if (match) variants.push(match[1]);
|
|
3953
|
+
}
|
|
3954
|
+
return variants;
|
|
3955
|
+
}
|
|
3956
|
+
collectJavaEnumConstants(lines, startLine) {
|
|
3957
|
+
const constants = [];
|
|
3958
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3959
|
+
const line = lines[i].trim();
|
|
3960
|
+
if (line === "}") break;
|
|
3961
|
+
if (line === "" || line.startsWith("//") || line.startsWith("/*")) continue;
|
|
3962
|
+
if (line.match(/^\s*(?:private|public|protected|static)/)) break;
|
|
3963
|
+
const match = line.match(/^(\w+)[\s(,;]/);
|
|
3964
|
+
if (match) constants.push(match[1]);
|
|
3965
|
+
const singleMatch = line.match(/^(\w+)$/);
|
|
3966
|
+
if (singleMatch) constants.push(singleMatch[1]);
|
|
3967
|
+
}
|
|
3968
|
+
return constants;
|
|
3969
|
+
}
|
|
3970
|
+
};
|
|
3971
|
+
|
|
3972
|
+
// src/ingest/extractors/ValidationRuleExtractor.ts
|
|
3973
|
+
var ValidationRuleExtractor = class {
|
|
3974
|
+
name = "validation-rules";
|
|
3975
|
+
supportedExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
|
|
3976
|
+
extract(content, filePath, language) {
|
|
3977
|
+
switch (language) {
|
|
3978
|
+
case "typescript":
|
|
3979
|
+
case "javascript":
|
|
3980
|
+
return this.extractZod(content, filePath, language);
|
|
3981
|
+
case "python":
|
|
3982
|
+
return this.extractPydantic(content, filePath, language);
|
|
3983
|
+
case "go":
|
|
3984
|
+
return this.extractGoValidate(content, filePath, language);
|
|
3985
|
+
case "rust":
|
|
3986
|
+
return this.extractRustValidate(content, filePath, language);
|
|
3987
|
+
case "java":
|
|
3988
|
+
return this.extractBeanValidation(content, filePath, language);
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
extractZod(content, filePath, language) {
|
|
3992
|
+
const records = [];
|
|
3993
|
+
const lines = content.split("\n");
|
|
3994
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3995
|
+
const line = lines[i];
|
|
3996
|
+
const schemaMatch = line.match(/(?:export\s+)?const\s+(\w+)\s*=\s*z\.object\s*\(\s*\{/);
|
|
3997
|
+
if (schemaMatch) {
|
|
3998
|
+
const schemaName = schemaMatch[1];
|
|
3999
|
+
const constraints = this.collectZodConstraints(lines, i + 1);
|
|
4000
|
+
records.push({
|
|
4001
|
+
id: `extracted:validation-rules:${hash(filePath + ":" + schemaName)}`,
|
|
4002
|
+
extractor: "validation-rules",
|
|
4003
|
+
language,
|
|
4004
|
+
filePath,
|
|
4005
|
+
line: i + 1,
|
|
4006
|
+
nodeType: "business_rule",
|
|
4007
|
+
name: schemaName,
|
|
4008
|
+
content: `${schemaName}: ${constraints.join("; ")}`,
|
|
4009
|
+
confidence: 0.8,
|
|
4010
|
+
metadata: { kind: "zod-schema", constraints, framework: "zod" }
|
|
4011
|
+
});
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
return records;
|
|
4015
|
+
}
|
|
4016
|
+
collectZodConstraints(lines, startLine) {
|
|
4017
|
+
const constraints = [];
|
|
4018
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
4019
|
+
const line = lines[i].trim();
|
|
4020
|
+
if (line.startsWith("}") || line.startsWith(")")) break;
|
|
4021
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
4022
|
+
const fieldMatch = line.match(/(\w+)\s*:\s*z\.(.+?)(?:,?\s*$)/);
|
|
4023
|
+
if (fieldMatch) {
|
|
4024
|
+
const fieldName = fieldMatch[1];
|
|
4025
|
+
const chain = fieldMatch[2];
|
|
4026
|
+
constraints.push(`${fieldName}: ${chain}`);
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
return constraints;
|
|
4030
|
+
}
|
|
4031
|
+
extractPydantic(content, filePath, language) {
|
|
4032
|
+
const records = [];
|
|
4033
|
+
const lines = content.split("\n");
|
|
4034
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4035
|
+
const line = lines[i];
|
|
4036
|
+
const modelMatch = line.match(/^class\s+(\w+)\s*\(\s*BaseModel\s*\)/);
|
|
4037
|
+
if (modelMatch) {
|
|
4038
|
+
const modelName = modelMatch[1];
|
|
4039
|
+
const constraints = this.collectPydanticConstraints(lines, i + 1);
|
|
4040
|
+
records.push({
|
|
4041
|
+
id: `extracted:validation-rules:${hash(filePath + ":" + modelName)}`,
|
|
4042
|
+
extractor: "validation-rules",
|
|
4043
|
+
language,
|
|
4044
|
+
filePath,
|
|
4045
|
+
line: i + 1,
|
|
4046
|
+
nodeType: "business_rule",
|
|
4047
|
+
name: modelName,
|
|
4048
|
+
content: `${modelName}: ${constraints.join("; ")}`,
|
|
4049
|
+
confidence: 0.8,
|
|
4050
|
+
metadata: { kind: "pydantic-model", constraints, framework: "pydantic" }
|
|
4051
|
+
});
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
return records;
|
|
4055
|
+
}
|
|
4056
|
+
collectPydanticConstraints(lines, startLine) {
|
|
4057
|
+
const constraints = [];
|
|
4058
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
4059
|
+
const line = lines[i];
|
|
4060
|
+
if (/^\S/.test(line) && line.trim() !== "") break;
|
|
4061
|
+
if (line.trim() === "" || line.trim().startsWith("#")) continue;
|
|
4062
|
+
const fieldMatch = line.match(/^\s+(\w+)\s*:\s*(.+?)(?:\s*$)/);
|
|
4063
|
+
if (fieldMatch) {
|
|
4064
|
+
constraints.push(`${fieldMatch[1]}: ${fieldMatch[2]}`);
|
|
4065
|
+
}
|
|
4066
|
+
}
|
|
4067
|
+
return constraints;
|
|
4068
|
+
}
|
|
4069
|
+
extractGoValidate(content, filePath, language) {
|
|
4070
|
+
const records = [];
|
|
4071
|
+
const lines = content.split("\n");
|
|
4072
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4073
|
+
const line = lines[i];
|
|
4074
|
+
const structMatch = line.match(/^type\s+(\w+)\s+struct\s*\{/);
|
|
4075
|
+
if (structMatch) {
|
|
4076
|
+
const structName = structMatch[1];
|
|
4077
|
+
const constraints = this.collectGoValidateTags(lines, i + 1);
|
|
4078
|
+
if (constraints.length > 0) {
|
|
4079
|
+
records.push({
|
|
4080
|
+
id: `extracted:validation-rules:${hash(filePath + ":" + structName)}`,
|
|
4081
|
+
extractor: "validation-rules",
|
|
4082
|
+
language,
|
|
4083
|
+
filePath,
|
|
4084
|
+
line: i + 1,
|
|
4085
|
+
nodeType: "business_rule",
|
|
4086
|
+
name: structName,
|
|
4087
|
+
content: `${structName}: ${constraints.join("; ")}`,
|
|
4088
|
+
confidence: 0.8,
|
|
4089
|
+
metadata: { kind: "struct-tags", constraints, framework: "go-playground/validator" }
|
|
4090
|
+
});
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
return records;
|
|
4095
|
+
}
|
|
4096
|
+
collectGoValidateTags(lines, startLine) {
|
|
4097
|
+
const constraints = [];
|
|
4098
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
4099
|
+
const line = lines[i].trim();
|
|
4100
|
+
if (line === "}") break;
|
|
4101
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
4102
|
+
const tagMatch = line.match(/(\w+)\s+\S+\s+`[^`]*validate:"([^"]+)"[^`]*`/);
|
|
4103
|
+
if (tagMatch) {
|
|
4104
|
+
constraints.push(`${tagMatch[1]}: ${tagMatch[2]}`);
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
return constraints;
|
|
4108
|
+
}
|
|
4109
|
+
extractRustValidate(content, filePath, language) {
|
|
4110
|
+
const records = [];
|
|
4111
|
+
const lines = content.split("\n");
|
|
4112
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4113
|
+
const line = lines[i];
|
|
4114
|
+
const deriveMatch = line.match(/#\[derive\([^)]*Validate[^)]*\)\]/);
|
|
4115
|
+
if (deriveMatch) {
|
|
4116
|
+
for (let j = i + 1; j < lines.length && j <= i + 3; j++) {
|
|
4117
|
+
const structLine = lines[j];
|
|
4118
|
+
const structMatch = structLine.match(/^(?:pub\s+)?struct\s+(\w+)/);
|
|
4119
|
+
if (structMatch) {
|
|
4120
|
+
const structName = structMatch[1];
|
|
4121
|
+
const constraints = this.collectRustValidateAttrs(lines, j + 1);
|
|
4122
|
+
if (constraints.length > 0) {
|
|
4123
|
+
records.push({
|
|
4124
|
+
id: `extracted:validation-rules:${hash(filePath + ":" + structName)}`,
|
|
4125
|
+
extractor: "validation-rules",
|
|
4126
|
+
language,
|
|
4127
|
+
filePath,
|
|
4128
|
+
line: j + 1,
|
|
4129
|
+
nodeType: "business_rule",
|
|
4130
|
+
name: structName,
|
|
4131
|
+
content: `${structName}: ${constraints.join("; ")}`,
|
|
4132
|
+
confidence: 0.8,
|
|
4133
|
+
metadata: { kind: "validate-derive", constraints, framework: "validator" }
|
|
4134
|
+
});
|
|
4135
|
+
}
|
|
4136
|
+
break;
|
|
4137
|
+
}
|
|
4138
|
+
}
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
return records;
|
|
4142
|
+
}
|
|
4143
|
+
collectRustValidateAttrs(lines, startLine) {
|
|
4144
|
+
const constraints = [];
|
|
4145
|
+
let pendingValidate;
|
|
4146
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
4147
|
+
const line = lines[i].trim();
|
|
4148
|
+
if (line === "}") break;
|
|
4149
|
+
const attrMatch = line.match(/#\[validate\((.+?)\)\]/);
|
|
4150
|
+
if (attrMatch) {
|
|
4151
|
+
pendingValidate = attrMatch[1];
|
|
4152
|
+
continue;
|
|
4153
|
+
}
|
|
4154
|
+
if (pendingValidate) {
|
|
4155
|
+
const fieldMatch = line.match(/(?:pub\s+)?(\w+)\s*:/);
|
|
4156
|
+
if (fieldMatch) {
|
|
4157
|
+
constraints.push(`${fieldMatch[1]}: ${pendingValidate}`);
|
|
4158
|
+
pendingValidate = void 0;
|
|
4159
|
+
}
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
return constraints;
|
|
4163
|
+
}
|
|
4164
|
+
extractBeanValidation(content, filePath, language) {
|
|
4165
|
+
const records = [];
|
|
4166
|
+
const lines = content.split("\n");
|
|
4167
|
+
const classRanges = this.findJavaClasses(lines);
|
|
4168
|
+
for (const { name: className, startLine, endLine } of classRanges) {
|
|
4169
|
+
const constraints = this.collectBeanConstraints(lines, startLine, endLine);
|
|
4170
|
+
if (constraints.length > 0) {
|
|
4171
|
+
records.push({
|
|
4172
|
+
id: `extracted:validation-rules:${hash(filePath + ":" + className)}`,
|
|
4173
|
+
extractor: "validation-rules",
|
|
4174
|
+
language,
|
|
4175
|
+
filePath,
|
|
4176
|
+
line: startLine + 1,
|
|
4177
|
+
nodeType: "business_rule",
|
|
4178
|
+
name: className,
|
|
4179
|
+
content: `${className}: ${constraints.join("; ")}`,
|
|
4180
|
+
confidence: 0.8,
|
|
4181
|
+
metadata: { kind: "bean-validation", constraints, framework: "javax.validation" }
|
|
4182
|
+
});
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
return records;
|
|
4186
|
+
}
|
|
4187
|
+
findJavaClasses(lines) {
|
|
4188
|
+
const classes = [];
|
|
4189
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4190
|
+
const line = lines[i];
|
|
4191
|
+
const match = line.match(/(?:public\s+|private\s+|protected\s+)?class\s+(\w+)/);
|
|
4192
|
+
if (match) {
|
|
4193
|
+
const endLine = this.findClosingBrace(lines, i);
|
|
4194
|
+
classes.push({ name: match[1], startLine: i, endLine });
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
return classes;
|
|
4198
|
+
}
|
|
4199
|
+
findClosingBrace(lines, startLine) {
|
|
4200
|
+
let depth = 0;
|
|
4201
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
4202
|
+
for (const ch of lines[i]) {
|
|
4203
|
+
if (ch === "{") depth++;
|
|
4204
|
+
if (ch === "}") {
|
|
4205
|
+
depth--;
|
|
4206
|
+
if (depth === 0) return i;
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
}
|
|
4210
|
+
return lines.length - 1;
|
|
4211
|
+
}
|
|
4212
|
+
collectBeanConstraints(lines, startLine, endLine) {
|
|
4213
|
+
const constraints = [];
|
|
4214
|
+
const validationAnnotations = [
|
|
4215
|
+
"@NotNull",
|
|
4216
|
+
"@NotBlank",
|
|
4217
|
+
"@NotEmpty",
|
|
4218
|
+
"@Size",
|
|
4219
|
+
"@Min",
|
|
4220
|
+
"@Max",
|
|
4221
|
+
"@DecimalMin",
|
|
4222
|
+
"@DecimalMax",
|
|
4223
|
+
"@Email",
|
|
4224
|
+
"@Pattern",
|
|
4225
|
+
"@Positive",
|
|
4226
|
+
"@Negative",
|
|
4227
|
+
"@Past",
|
|
4228
|
+
"@Future"
|
|
4229
|
+
];
|
|
4230
|
+
let pendingAnnotations = [];
|
|
4231
|
+
for (let i = startLine + 1; i < endLine; i++) {
|
|
4232
|
+
const line = lines[i].trim();
|
|
4233
|
+
for (const anno of validationAnnotations) {
|
|
4234
|
+
if (line.startsWith(anno)) {
|
|
4235
|
+
pendingAnnotations.push(line.replace(/;?\s*$/, ""));
|
|
4236
|
+
}
|
|
4237
|
+
}
|
|
4238
|
+
if (pendingAnnotations.length > 0) {
|
|
4239
|
+
const fieldMatch = line.match(
|
|
4240
|
+
/(?:private|public|protected)\s+(?:[\w<>[\]]+)\s+(\w+)\s*[;=]/
|
|
4241
|
+
);
|
|
4242
|
+
if (fieldMatch) {
|
|
4243
|
+
constraints.push(`${fieldMatch[1]}: ${pendingAnnotations.join(", ")}`);
|
|
4244
|
+
pendingAnnotations = [];
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
return constraints;
|
|
4249
|
+
}
|
|
4250
|
+
};
|
|
4251
|
+
|
|
4252
|
+
// src/ingest/extractors/ApiPathExtractor.ts
|
|
4253
|
+
var ApiPathExtractor = class {
|
|
4254
|
+
name = "api-paths";
|
|
4255
|
+
supportedExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
|
|
4256
|
+
extract(content, filePath, language) {
|
|
4257
|
+
switch (language) {
|
|
4258
|
+
case "typescript":
|
|
4259
|
+
case "javascript":
|
|
4260
|
+
return this.extractJsTs(content, filePath, language);
|
|
4261
|
+
case "python":
|
|
4262
|
+
return this.extractPython(content, filePath, language);
|
|
4263
|
+
case "go":
|
|
4264
|
+
return this.extractGo(content, filePath, language);
|
|
4265
|
+
case "rust":
|
|
4266
|
+
return this.extractRust(content, filePath, language);
|
|
4267
|
+
case "java":
|
|
4268
|
+
return this.extractJava(content, filePath, language);
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4271
|
+
extractJsTs(content, filePath, language) {
|
|
4272
|
+
const records = [];
|
|
4273
|
+
const lines = content.split("\n");
|
|
4274
|
+
const routePattern = /(?:app|router|server|fastify)\.(get|post|put|patch|delete|head|options)\s*\(\s*(['"`])([^'"`]+)\2/i;
|
|
4275
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4276
|
+
const line = lines[i];
|
|
4277
|
+
const match = line.match(routePattern);
|
|
4278
|
+
if (match) {
|
|
4279
|
+
const method = match[1].toUpperCase();
|
|
4280
|
+
const routePath = match[3];
|
|
4281
|
+
const patternKey = `${method} ${routePath}`;
|
|
4282
|
+
records.push({
|
|
4283
|
+
id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
|
|
4284
|
+
extractor: "api-paths",
|
|
4285
|
+
language,
|
|
4286
|
+
filePath,
|
|
4287
|
+
line: i + 1,
|
|
4288
|
+
nodeType: "business_process",
|
|
4289
|
+
name: patternKey,
|
|
4290
|
+
content: `${method} ${routePath}`,
|
|
4291
|
+
confidence: 0.9,
|
|
4292
|
+
metadata: { method, path: routePath, framework: "express" }
|
|
4293
|
+
});
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
return records;
|
|
4297
|
+
}
|
|
4298
|
+
extractPython(content, filePath, language) {
|
|
4299
|
+
const records = [];
|
|
4300
|
+
const lines = content.split("\n");
|
|
4301
|
+
const decoratorPattern = /@(?:app|router)\.(get|post|put|patch|delete|head|options)\s*\(\s*["']([^"']+)["']/i;
|
|
4302
|
+
const flaskPattern = /@(?:app|blueprint)\.route\s*\(\s*["']([^"']+)["'](?:\s*,\s*methods\s*=\s*\[([^\]]+)\])?/i;
|
|
4303
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4304
|
+
const line = lines[i];
|
|
4305
|
+
const fastApiMatch = line.match(decoratorPattern);
|
|
4306
|
+
if (fastApiMatch) {
|
|
4307
|
+
const method = fastApiMatch[1].toUpperCase();
|
|
4308
|
+
const routePath = fastApiMatch[2];
|
|
4309
|
+
const patternKey = `${method} ${routePath}`;
|
|
4310
|
+
records.push({
|
|
4311
|
+
id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
|
|
4312
|
+
extractor: "api-paths",
|
|
4313
|
+
language,
|
|
4314
|
+
filePath,
|
|
4315
|
+
line: i + 1,
|
|
4316
|
+
nodeType: "business_process",
|
|
4317
|
+
name: patternKey,
|
|
4318
|
+
content: `${method} ${routePath}`,
|
|
4319
|
+
confidence: 0.9,
|
|
4320
|
+
metadata: { method, path: routePath, framework: "fastapi" }
|
|
4321
|
+
});
|
|
4322
|
+
continue;
|
|
4323
|
+
}
|
|
4324
|
+
const flaskMatch = line.match(flaskPattern);
|
|
4325
|
+
if (flaskMatch) {
|
|
4326
|
+
const routePath = flaskMatch[1];
|
|
4327
|
+
const methods = flaskMatch[2] ? flaskMatch[2].split(",").map((m) => m.trim().replace(/["']/g, "")) : ["GET"];
|
|
4328
|
+
for (const method of methods) {
|
|
4329
|
+
const patternKey = `${method} ${routePath}`;
|
|
4330
|
+
records.push({
|
|
4331
|
+
id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
|
|
4332
|
+
extractor: "api-paths",
|
|
4333
|
+
language,
|
|
4334
|
+
filePath,
|
|
4335
|
+
line: i + 1,
|
|
4336
|
+
nodeType: "business_process",
|
|
4337
|
+
name: patternKey,
|
|
4338
|
+
content: `${method} ${routePath}`,
|
|
4339
|
+
confidence: 0.9,
|
|
4340
|
+
metadata: { method, path: routePath, framework: "flask" }
|
|
4341
|
+
});
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
4345
|
+
return records;
|
|
4346
|
+
}
|
|
4347
|
+
extractGo(content, filePath, language) {
|
|
4348
|
+
const records = [];
|
|
4349
|
+
const lines = content.split("\n");
|
|
4350
|
+
const ginPattern = /(?:\w+)\.(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\(\s*"([^"]+)"/i;
|
|
4351
|
+
const httpPattern = /http\.HandleFunc\s*\(\s*"([^"]+)"/;
|
|
4352
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4353
|
+
const line = lines[i];
|
|
4354
|
+
const ginMatch = line.match(ginPattern);
|
|
4355
|
+
if (ginMatch) {
|
|
4356
|
+
const method = ginMatch[1].toUpperCase();
|
|
4357
|
+
const routePath = ginMatch[2];
|
|
4358
|
+
const patternKey = `${method} ${routePath}`;
|
|
4359
|
+
records.push({
|
|
4360
|
+
id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
|
|
4361
|
+
extractor: "api-paths",
|
|
4362
|
+
language,
|
|
4363
|
+
filePath,
|
|
4364
|
+
line: i + 1,
|
|
4365
|
+
nodeType: "business_process",
|
|
4366
|
+
name: patternKey,
|
|
4367
|
+
content: `${method} ${routePath}`,
|
|
4368
|
+
confidence: 0.9,
|
|
4369
|
+
metadata: { method, path: routePath, framework: "gin" }
|
|
4370
|
+
});
|
|
4371
|
+
continue;
|
|
4372
|
+
}
|
|
4373
|
+
const httpMatch = line.match(httpPattern);
|
|
4374
|
+
if (httpMatch) {
|
|
4375
|
+
const routePath = httpMatch[1];
|
|
4376
|
+
const patternKey = `ANY ${routePath}`;
|
|
4377
|
+
records.push({
|
|
4378
|
+
id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
|
|
4379
|
+
extractor: "api-paths",
|
|
4380
|
+
language,
|
|
4381
|
+
filePath,
|
|
4382
|
+
line: i + 1,
|
|
4383
|
+
nodeType: "business_process",
|
|
4384
|
+
name: patternKey,
|
|
4385
|
+
content: `ANY ${routePath}`,
|
|
4386
|
+
confidence: 0.6,
|
|
4387
|
+
metadata: { method: "ANY", path: routePath, framework: "net/http" }
|
|
4388
|
+
});
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
return records;
|
|
4392
|
+
}
|
|
4393
|
+
extractRust(content, filePath, language) {
|
|
4394
|
+
const records = [];
|
|
4395
|
+
const lines = content.split("\n");
|
|
4396
|
+
const actixPattern = /#\[(get|post|put|patch|delete|head|options)\s*\(\s*"([^"]+)"\s*\)\s*\]/i;
|
|
4397
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4398
|
+
const line = lines[i];
|
|
4399
|
+
const match = line.match(actixPattern);
|
|
4400
|
+
if (match) {
|
|
4401
|
+
const method = match[1].toUpperCase();
|
|
4402
|
+
const routePath = match[2];
|
|
4403
|
+
const patternKey = `${method} ${routePath}`;
|
|
4404
|
+
records.push({
|
|
4405
|
+
id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
|
|
4406
|
+
extractor: "api-paths",
|
|
4407
|
+
language,
|
|
4408
|
+
filePath,
|
|
4409
|
+
line: i + 1,
|
|
4410
|
+
nodeType: "business_process",
|
|
4411
|
+
name: patternKey,
|
|
4412
|
+
content: `${method} ${routePath}`,
|
|
4413
|
+
confidence: 0.9,
|
|
4414
|
+
metadata: { method, path: routePath, framework: "actix" }
|
|
4415
|
+
});
|
|
4416
|
+
}
|
|
4417
|
+
}
|
|
4418
|
+
return records;
|
|
4419
|
+
}
|
|
4420
|
+
extractJava(content, filePath, language) {
|
|
4421
|
+
const records = [];
|
|
4422
|
+
const lines = content.split("\n");
|
|
4423
|
+
const springPattern = /@(GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping|RequestMapping)\s*\(\s*(?:value\s*=\s*)?["']([^"']+)["']/;
|
|
4424
|
+
let basePath = "";
|
|
4425
|
+
for (const line of lines) {
|
|
4426
|
+
const baseMatch = line.match(/@RequestMapping\s*\(\s*(?:value\s*=\s*)?["']([^"']+)["']\s*\)/);
|
|
4427
|
+
if (baseMatch && line.match(/class\s/) === null) {
|
|
4428
|
+
basePath = baseMatch[1];
|
|
4429
|
+
}
|
|
4430
|
+
}
|
|
4431
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4432
|
+
const line = lines[i];
|
|
4433
|
+
const match = line.match(springPattern);
|
|
4434
|
+
if (match) {
|
|
4435
|
+
const annotation = match[1];
|
|
4436
|
+
const routePath = basePath + match[2];
|
|
4437
|
+
const methodMap = {
|
|
4438
|
+
GetMapping: "GET",
|
|
4439
|
+
PostMapping: "POST",
|
|
4440
|
+
PutMapping: "PUT",
|
|
4441
|
+
PatchMapping: "PATCH",
|
|
4442
|
+
DeleteMapping: "DELETE",
|
|
4443
|
+
RequestMapping: "ANY"
|
|
4444
|
+
};
|
|
4445
|
+
const method = methodMap[annotation] ?? "ANY";
|
|
4446
|
+
const patternKey = `${method} ${routePath}`;
|
|
4447
|
+
records.push({
|
|
4448
|
+
id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
|
|
4449
|
+
extractor: "api-paths",
|
|
4450
|
+
language,
|
|
4451
|
+
filePath,
|
|
4452
|
+
line: i + 1,
|
|
4453
|
+
nodeType: "business_process",
|
|
4454
|
+
name: patternKey,
|
|
4455
|
+
content: `${method} ${routePath}`,
|
|
4456
|
+
confidence: 0.9,
|
|
4457
|
+
metadata: { method, path: routePath, framework: "spring" }
|
|
4458
|
+
});
|
|
4459
|
+
}
|
|
4460
|
+
}
|
|
4461
|
+
return records;
|
|
4462
|
+
}
|
|
4463
|
+
};
|
|
4464
|
+
|
|
4465
|
+
// src/ingest/extractors/index.ts
|
|
4466
|
+
var ALL_EXTRACTORS = [
|
|
4467
|
+
new TestDescriptionExtractor(),
|
|
4468
|
+
new EnumConstantExtractor(),
|
|
4469
|
+
new ValidationRuleExtractor(),
|
|
4470
|
+
new ApiPathExtractor()
|
|
4471
|
+
];
|
|
4472
|
+
function createExtractionRunner() {
|
|
4473
|
+
return new ExtractionRunner(ALL_EXTRACTORS);
|
|
4474
|
+
}
|
|
4475
|
+
|
|
4476
|
+
// src/ingest/ImageAnalysisExtractor.ts
|
|
4477
|
+
var ImageAnalysisExtractor = class {
|
|
4478
|
+
provider;
|
|
4479
|
+
/** Maximum file size in bytes. Reserved for future file-size filtering. */
|
|
4480
|
+
maxFileSizeBytes;
|
|
4481
|
+
constructor(options) {
|
|
4482
|
+
this.provider = options.analysisProvider;
|
|
4483
|
+
this.maxFileSizeBytes = (options.maxFileSizeMB ?? 10) * 1024 * 1024;
|
|
4484
|
+
}
|
|
4485
|
+
async analyze(store, imagePaths) {
|
|
4486
|
+
const start = Date.now();
|
|
4487
|
+
let nodesAdded = 0;
|
|
4488
|
+
let edgesAdded = 0;
|
|
4489
|
+
const errors = [];
|
|
4490
|
+
for (const imagePath of imagePaths) {
|
|
4491
|
+
try {
|
|
4492
|
+
const response = await this.provider.analyze({
|
|
4493
|
+
prompt: "Analyze this image and provide a structured description of its visual contents.",
|
|
4494
|
+
systemPrompt: "You are an image analysis assistant. Describe the image contents, detect UI elements, extract visible text, identify design patterns, and note accessibility concerns.",
|
|
4495
|
+
responseSchema: {},
|
|
4496
|
+
// Schema handled by provider
|
|
4497
|
+
maxTokens: 1e3
|
|
4498
|
+
});
|
|
4499
|
+
const result = response.result;
|
|
4500
|
+
const pathHash = hash(imagePath);
|
|
4501
|
+
const annotationId = `img:${pathHash}`;
|
|
4502
|
+
store.addNode({
|
|
4503
|
+
id: annotationId,
|
|
4504
|
+
type: "image_annotation",
|
|
4505
|
+
name: result.description.slice(0, 200),
|
|
4506
|
+
path: imagePath,
|
|
4507
|
+
content: result.description,
|
|
4508
|
+
hash: hash(result.description),
|
|
4509
|
+
metadata: {
|
|
4510
|
+
source: "image-analysis",
|
|
4511
|
+
detectedElements: result.detectedElements,
|
|
4512
|
+
extractedText: result.extractedText,
|
|
4513
|
+
designPatterns: result.designPatterns,
|
|
4514
|
+
accessibilityNotes: result.accessibilityNotes,
|
|
4515
|
+
model: response.model
|
|
4516
|
+
}
|
|
4517
|
+
});
|
|
4518
|
+
nodesAdded++;
|
|
4519
|
+
const fileNodes = store.findNodes({ type: "file" });
|
|
4520
|
+
const fileNode = fileNodes.find((n) => n.path === imagePath);
|
|
4521
|
+
if (fileNode) {
|
|
4522
|
+
store.addEdge({ from: annotationId, to: fileNode.id, type: "annotates" });
|
|
4523
|
+
edgesAdded++;
|
|
4524
|
+
}
|
|
4525
|
+
for (const pattern of result.designPatterns) {
|
|
4526
|
+
const conceptId = `img:concept:${hash(pattern + imagePath)}`;
|
|
4527
|
+
store.addNode({
|
|
4528
|
+
id: conceptId,
|
|
4529
|
+
type: "business_concept",
|
|
4530
|
+
name: pattern,
|
|
4531
|
+
content: `Design pattern detected in ${imagePath}: ${pattern}`,
|
|
4532
|
+
hash: hash(pattern),
|
|
4533
|
+
metadata: {
|
|
4534
|
+
source: "image-analysis",
|
|
4535
|
+
sourceImage: imagePath,
|
|
4536
|
+
domain: "design"
|
|
4537
|
+
}
|
|
4538
|
+
});
|
|
4539
|
+
nodesAdded++;
|
|
4540
|
+
store.addEdge({ from: annotationId, to: conceptId, type: "contains" });
|
|
4541
|
+
edgesAdded++;
|
|
4542
|
+
}
|
|
4543
|
+
} catch (err) {
|
|
4544
|
+
errors.push(
|
|
4545
|
+
`Image analysis failed for ${imagePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
4546
|
+
);
|
|
4547
|
+
}
|
|
4548
|
+
}
|
|
4549
|
+
return {
|
|
4550
|
+
nodesAdded,
|
|
4551
|
+
nodesUpdated: 0,
|
|
4552
|
+
edgesAdded,
|
|
4553
|
+
edgesUpdated: 0,
|
|
4554
|
+
errors,
|
|
4555
|
+
durationMs: Date.now() - start
|
|
4556
|
+
};
|
|
4557
|
+
}
|
|
4558
|
+
};
|
|
4559
|
+
|
|
4560
|
+
// src/ingest/knowledgeTypes.ts
|
|
4561
|
+
var KNOWLEDGE_NODE_TYPES = [
|
|
4562
|
+
"business_fact",
|
|
4563
|
+
"business_rule",
|
|
4564
|
+
"business_process",
|
|
4565
|
+
"business_term",
|
|
4566
|
+
"business_concept",
|
|
4567
|
+
"business_metric",
|
|
4568
|
+
"design_token",
|
|
4569
|
+
"design_constraint",
|
|
4570
|
+
"aesthetic_intent",
|
|
4571
|
+
"image_annotation"
|
|
4572
|
+
];
|
|
4573
|
+
|
|
4574
|
+
// src/ingest/ContradictionDetector.ts
|
|
4575
|
+
var SIMILARITY_THRESHOLD = 0.8;
|
|
4576
|
+
function levenshteinDistance(a, b) {
|
|
4577
|
+
const m = a.length;
|
|
4578
|
+
const n = b.length;
|
|
4579
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
4580
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
4581
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
4582
|
+
for (let i = 1; i <= m; i++) {
|
|
4583
|
+
for (let j = 1; j <= n; j++) {
|
|
4584
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
4585
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
|
|
4586
|
+
}
|
|
4587
|
+
}
|
|
4588
|
+
return dp[m][n];
|
|
4589
|
+
}
|
|
4590
|
+
function levenshteinRatio(a, b) {
|
|
4591
|
+
const maxLen = Math.max(a.length, b.length);
|
|
4592
|
+
if (maxLen === 0) return 1;
|
|
4593
|
+
return 1 - levenshteinDistance(a, b) / maxLen;
|
|
4594
|
+
}
|
|
4595
|
+
function classifyConflict(a, b) {
|
|
4596
|
+
const factTypes = /* @__PURE__ */ new Set([
|
|
4597
|
+
"business_fact",
|
|
4598
|
+
"business_metric",
|
|
4599
|
+
"design_token"
|
|
4600
|
+
]);
|
|
4601
|
+
const termTypes = /* @__PURE__ */ new Set(["business_term", "business_concept"]);
|
|
4602
|
+
if (factTypes.has(a.type) || factTypes.has(b.type)) return "value_mismatch";
|
|
4603
|
+
if (termTypes.has(a.type) || termTypes.has(b.type)) return "definition_conflict";
|
|
4604
|
+
if (a.lastModified && b.lastModified && a.lastModified !== b.lastModified)
|
|
4605
|
+
return "temporal_conflict";
|
|
4606
|
+
return "status_divergence";
|
|
4607
|
+
}
|
|
4608
|
+
var ContradictionDetector = class {
|
|
4609
|
+
detect(store) {
|
|
4610
|
+
const nodes = KNOWLEDGE_NODE_TYPES.flatMap((t) => store.findNodes({ type: t }));
|
|
4611
|
+
const byName = /* @__PURE__ */ new Map();
|
|
4612
|
+
for (const node of nodes) {
|
|
4613
|
+
const key = node.name.toLowerCase().trim();
|
|
4614
|
+
const group = byName.get(key) ?? [];
|
|
4615
|
+
group.push(node);
|
|
4616
|
+
byName.set(key, group);
|
|
4617
|
+
}
|
|
4618
|
+
const contradictions = [];
|
|
4619
|
+
const sourcePairCounts = {};
|
|
4620
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4621
|
+
const addContradiction = (a, b, similarity) => {
|
|
4622
|
+
const sourceA = a.metadata.source ?? "unknown";
|
|
4623
|
+
const sourceB = b.metadata.source ?? "unknown";
|
|
4624
|
+
if (sourceA === sourceB) return;
|
|
4625
|
+
const hashA = a.hash ?? a.id;
|
|
4626
|
+
const hashB = b.hash ?? b.id;
|
|
4627
|
+
if (hashA === hashB) return;
|
|
4628
|
+
const pairId = [a.id, b.id].sort().join(":");
|
|
4629
|
+
if (seen.has(pairId)) return;
|
|
4630
|
+
seen.add(pairId);
|
|
4631
|
+
const conflictType = classifyConflict(a, b);
|
|
4632
|
+
const severity = conflictType === "value_mismatch" ? "critical" : conflictType === "definition_conflict" ? "high" : "medium";
|
|
4633
|
+
const pairKey = [sourceA, sourceB].sort().join("\u2194");
|
|
4634
|
+
sourcePairCounts[pairKey] = (sourcePairCounts[pairKey] ?? 0) + 1;
|
|
4635
|
+
contradictions.push({
|
|
4636
|
+
id: `contradiction:${a.id}:${b.id}`,
|
|
4637
|
+
entityA: {
|
|
4638
|
+
nodeId: a.id,
|
|
4639
|
+
source: sourceA,
|
|
4640
|
+
name: a.name,
|
|
4641
|
+
content: a.content ?? "",
|
|
4642
|
+
lastModified: a.lastModified
|
|
4643
|
+
},
|
|
4644
|
+
entityB: {
|
|
4645
|
+
nodeId: b.id,
|
|
4646
|
+
source: sourceB,
|
|
4647
|
+
name: b.name,
|
|
4648
|
+
content: b.content ?? "",
|
|
4649
|
+
lastModified: b.lastModified
|
|
4650
|
+
},
|
|
4651
|
+
similarity,
|
|
4652
|
+
conflictType,
|
|
4653
|
+
severity,
|
|
4654
|
+
description: `"${a.name}" has conflicting definitions from ${sourceA} and ${sourceB}`
|
|
4655
|
+
});
|
|
4656
|
+
};
|
|
4657
|
+
for (const [, group] of byName) {
|
|
4658
|
+
if (group.length < 2) continue;
|
|
4659
|
+
for (let i = 0; i < group.length; i++) {
|
|
4660
|
+
for (let j = i + 1; j < group.length; j++) {
|
|
4661
|
+
addContradiction(group[i], group[j], 1);
|
|
4662
|
+
}
|
|
4663
|
+
}
|
|
4664
|
+
}
|
|
4665
|
+
const keys = [...byName.keys()];
|
|
4666
|
+
for (let i = 0; i < keys.length; i++) {
|
|
4667
|
+
for (let j = i + 1; j < keys.length; j++) {
|
|
4668
|
+
const keyA = keys[i];
|
|
4669
|
+
const keyB = keys[j];
|
|
4670
|
+
const maxLen = Math.max(keyA.length, keyB.length);
|
|
4671
|
+
const minLen = Math.min(keyA.length, keyB.length);
|
|
4672
|
+
if (minLen / maxLen < SIMILARITY_THRESHOLD) continue;
|
|
4673
|
+
const ratio = levenshteinRatio(keyA, keyB);
|
|
4674
|
+
if (ratio < SIMILARITY_THRESHOLD) continue;
|
|
4675
|
+
const groupA = byName.get(keyA);
|
|
4676
|
+
const groupB = byName.get(keyB);
|
|
4677
|
+
for (const a of groupA) {
|
|
4678
|
+
for (const b of groupB) {
|
|
4679
|
+
addContradiction(a, b, ratio);
|
|
4680
|
+
}
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
4683
|
+
}
|
|
4684
|
+
return { contradictions, sourcePairCounts, totalChecked: nodes.length };
|
|
4685
|
+
}
|
|
4686
|
+
};
|
|
4687
|
+
|
|
4688
|
+
// src/ingest/CoverageScorer.ts
|
|
4689
|
+
var KNOWLEDGE_TYPES = KNOWLEDGE_NODE_TYPES;
|
|
4690
|
+
var CODE_TYPES2 = [
|
|
4691
|
+
"file",
|
|
4692
|
+
"function",
|
|
4693
|
+
"class",
|
|
4694
|
+
"method",
|
|
4695
|
+
"interface",
|
|
4696
|
+
"variable"
|
|
4697
|
+
];
|
|
4698
|
+
var KNOWLEDGE_EDGE_TYPES = [
|
|
4699
|
+
"governs",
|
|
4700
|
+
"documents",
|
|
4701
|
+
"measures",
|
|
4702
|
+
"applies_to",
|
|
4703
|
+
"references",
|
|
4704
|
+
"uses_token",
|
|
4705
|
+
"declares_intent",
|
|
4706
|
+
"annotates"
|
|
4707
|
+
];
|
|
4708
|
+
function toGrade(score) {
|
|
4709
|
+
if (score >= 80) return "A";
|
|
4710
|
+
if (score >= 60) return "B";
|
|
4711
|
+
if (score >= 40) return "C";
|
|
4712
|
+
if (score >= 20) return "D";
|
|
4713
|
+
return "F";
|
|
4714
|
+
}
|
|
4715
|
+
var CoverageScorer = class {
|
|
4716
|
+
score(store) {
|
|
4717
|
+
const knowledgeNodes = KNOWLEDGE_TYPES.flatMap((t) => store.findNodes({ type: t }));
|
|
4718
|
+
const domainMap = /* @__PURE__ */ new Map();
|
|
4719
|
+
for (const node of knowledgeNodes) {
|
|
4720
|
+
const domain = node.metadata.domain ?? "unclassified";
|
|
4721
|
+
const group = domainMap.get(domain) ?? [];
|
|
4722
|
+
group.push(node);
|
|
4723
|
+
domainMap.set(domain, group);
|
|
4724
|
+
}
|
|
4725
|
+
const codeNodes = CODE_TYPES2.flatMap((t) => store.findNodes({ type: t }));
|
|
4726
|
+
const codeDomains = /* @__PURE__ */ new Map();
|
|
4727
|
+
for (const node of codeNodes) {
|
|
4728
|
+
const domain = node.metadata.domain ?? this.domainFromPath(node.path);
|
|
4729
|
+
const group = codeDomains.get(domain) ?? [];
|
|
4730
|
+
group.push(node);
|
|
4731
|
+
codeDomains.set(domain, group);
|
|
4732
|
+
}
|
|
4733
|
+
const allDomains = /* @__PURE__ */ new Set([...domainMap.keys(), ...codeDomains.keys()]);
|
|
4734
|
+
const domains = [];
|
|
4735
|
+
for (const domain of allDomains) {
|
|
4736
|
+
const knEntries = domainMap.get(domain) ?? [];
|
|
4737
|
+
const codeEntries = codeDomains.get(domain) ?? [];
|
|
4738
|
+
const linkedIds = /* @__PURE__ */ new Set();
|
|
4739
|
+
for (const codeNode of codeEntries) {
|
|
4740
|
+
for (const edgeType of KNOWLEDGE_EDGE_TYPES) {
|
|
4741
|
+
const edges = store.getEdges({ to: codeNode.id, type: edgeType });
|
|
4742
|
+
if (edges.length > 0) {
|
|
4743
|
+
linkedIds.add(codeNode.id);
|
|
4744
|
+
break;
|
|
4745
|
+
}
|
|
4746
|
+
}
|
|
4747
|
+
}
|
|
4748
|
+
const sourceBreakdown = {};
|
|
4749
|
+
for (const kn of knEntries) {
|
|
4750
|
+
const src = kn.metadata.source ?? "unknown";
|
|
4751
|
+
sourceBreakdown[src] = (sourceBreakdown[src] ?? 0) + 1;
|
|
4752
|
+
}
|
|
4753
|
+
const codeEntities = codeEntries.length;
|
|
4754
|
+
const linkedEntities = linkedIds.size;
|
|
4755
|
+
const knowledgeEntries = knEntries.length;
|
|
4756
|
+
const uniqueSources = Object.keys(sourceBreakdown).length;
|
|
4757
|
+
const codeCoverageComponent = codeEntities > 0 ? linkedEntities / codeEntities * 60 : 0;
|
|
4758
|
+
const knowledgeDepthComponent = Math.min(knowledgeEntries / 10, 1) * 20;
|
|
4759
|
+
const sourceDiversityComponent = Math.min(uniqueSources / 3, 1) * 20;
|
|
4760
|
+
const score = Math.round(
|
|
4761
|
+
codeCoverageComponent + knowledgeDepthComponent + sourceDiversityComponent
|
|
4762
|
+
);
|
|
4763
|
+
domains.push({
|
|
4764
|
+
domain,
|
|
4765
|
+
score,
|
|
4766
|
+
knowledgeEntries,
|
|
4767
|
+
codeEntities,
|
|
4768
|
+
linkedEntities,
|
|
4769
|
+
unlinkedEntities: codeEntities - linkedEntities,
|
|
4770
|
+
sourceBreakdown,
|
|
4771
|
+
grade: toGrade(score)
|
|
4772
|
+
});
|
|
4773
|
+
}
|
|
4774
|
+
const overallScore = domains.length > 0 ? Math.round(domains.reduce((sum, d) => sum + d.score, 0) / domains.length) : 0;
|
|
4775
|
+
return {
|
|
4776
|
+
domains,
|
|
4777
|
+
overallScore,
|
|
4778
|
+
overallGrade: toGrade(overallScore),
|
|
4779
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4780
|
+
};
|
|
4781
|
+
}
|
|
4782
|
+
domainFromPath(filePath) {
|
|
4783
|
+
if (!filePath) return "unclassified";
|
|
4784
|
+
const parts = filePath.split("/");
|
|
4785
|
+
const pkgIdx = parts.indexOf("packages");
|
|
4786
|
+
if (pkgIdx >= 0 && parts[pkgIdx + 1]) return parts[pkgIdx + 1];
|
|
4787
|
+
const srcIdx = parts.indexOf("src");
|
|
4788
|
+
if (srcIdx >= 0 && parts[srcIdx + 1]) return parts[srcIdx + 1];
|
|
4789
|
+
return parts[0] ?? "unclassified";
|
|
4790
|
+
}
|
|
4791
|
+
};
|
|
4792
|
+
|
|
4793
|
+
// src/ingest/KnowledgePipelineRunner.ts
|
|
4794
|
+
var BUSINESS_NODE_TYPES = [
|
|
4795
|
+
"business_concept",
|
|
4796
|
+
"business_rule",
|
|
4797
|
+
"business_process",
|
|
4798
|
+
"business_term",
|
|
4799
|
+
"business_metric",
|
|
4800
|
+
"business_fact"
|
|
4801
|
+
];
|
|
4802
|
+
var SNAPSHOT_NODE_TYPES = [
|
|
4803
|
+
...BUSINESS_NODE_TYPES,
|
|
4804
|
+
"design_token",
|
|
4805
|
+
"design_constraint",
|
|
4806
|
+
"aesthetic_intent",
|
|
4807
|
+
"image_annotation"
|
|
4808
|
+
];
|
|
4809
|
+
var KnowledgePipelineRunner = class {
|
|
4810
|
+
constructor(store) {
|
|
4811
|
+
this.store = store;
|
|
4812
|
+
}
|
|
4813
|
+
store;
|
|
4814
|
+
async run(options) {
|
|
4815
|
+
const maxIterations = options.maxIterations ?? 5;
|
|
4816
|
+
const remediations = [];
|
|
4817
|
+
const preSnapshot = this.buildSnapshot(options.domain);
|
|
4818
|
+
const extraction = await this.extract(options);
|
|
4819
|
+
const postSnapshot = this.buildSnapshot(options.domain);
|
|
4820
|
+
let driftResult = this.reconcile(preSnapshot, postSnapshot);
|
|
4821
|
+
const contradictionDetector = new ContradictionDetector();
|
|
4822
|
+
const contradictions = contradictionDetector.detect(this.store);
|
|
4823
|
+
let gapReport = await this.detect(options);
|
|
4824
|
+
const coverageScorer = new CoverageScorer();
|
|
4825
|
+
const coverage = coverageScorer.score(this.store);
|
|
4826
|
+
let iterations = 1;
|
|
4827
|
+
if (options.fix) {
|
|
4828
|
+
let previousFindingCount = driftResult.findings.length;
|
|
4829
|
+
while (iterations < maxIterations) {
|
|
4830
|
+
if (driftResult.findings.length === 0) break;
|
|
4831
|
+
this.remediate(driftResult, remediations, options);
|
|
4832
|
+
const rePreSnapshot = this.buildSnapshot(options.domain);
|
|
4833
|
+
await this.extract(options);
|
|
4834
|
+
const rePostSnapshot = this.buildSnapshot(options.domain);
|
|
4835
|
+
driftResult = this.reconcile(rePreSnapshot, rePostSnapshot);
|
|
4836
|
+
gapReport = await this.detect(options);
|
|
4837
|
+
iterations++;
|
|
4838
|
+
const newFindingCount = driftResult.findings.length;
|
|
4839
|
+
if (newFindingCount >= previousFindingCount) break;
|
|
4840
|
+
previousFindingCount = newFindingCount;
|
|
4841
|
+
}
|
|
4842
|
+
}
|
|
4843
|
+
await this.stageNewFindings(driftResult, options);
|
|
4844
|
+
return {
|
|
4845
|
+
verdict: this.computeVerdict(driftResult),
|
|
4846
|
+
driftScore: driftResult.driftScore,
|
|
4847
|
+
iterations,
|
|
4848
|
+
findings: driftResult.summary,
|
|
4849
|
+
extraction,
|
|
4850
|
+
gaps: gapReport,
|
|
4851
|
+
remediations,
|
|
4852
|
+
contradictions,
|
|
4853
|
+
coverage
|
|
4854
|
+
};
|
|
4855
|
+
}
|
|
4856
|
+
// ── Phase 1: EXTRACT ──────────────────────────────────────────────────────
|
|
4857
|
+
async extract(options) {
|
|
4858
|
+
const extractedDir = path10.join(options.projectDir, ".harness", "knowledge", "extracted");
|
|
4859
|
+
await fs9.mkdir(extractedDir, { recursive: true });
|
|
4860
|
+
const runner = createExtractionRunner();
|
|
4861
|
+
const extractionResult = await runner.run(options.projectDir, this.store, extractedDir);
|
|
4862
|
+
const diagramParser = new DiagramParser(this.store);
|
|
4863
|
+
const diagramResult = await diagramParser.ingest(options.projectDir);
|
|
4864
|
+
let imageCount = 0;
|
|
4865
|
+
const imagePaths = options.imagePaths ?? [];
|
|
4866
|
+
if (options.analyzeImages && options.analysisProvider && imagePaths.length > 0) {
|
|
4867
|
+
const imageExtractor = new ImageAnalysisExtractor({
|
|
4868
|
+
analysisProvider: options.analysisProvider
|
|
4869
|
+
});
|
|
4870
|
+
const imageResult = await imageExtractor.analyze(this.store, imagePaths);
|
|
4871
|
+
imageCount = imageResult.nodesAdded;
|
|
4872
|
+
}
|
|
4873
|
+
const knowledgeDir = path10.join(options.projectDir, "docs", "knowledge");
|
|
4874
|
+
const bkIngestor = new BusinessKnowledgeIngestor(this.store);
|
|
4875
|
+
let bkResult;
|
|
4876
|
+
try {
|
|
4877
|
+
bkResult = await bkIngestor.ingest(knowledgeDir);
|
|
4878
|
+
} catch {
|
|
4879
|
+
bkResult = {
|
|
4880
|
+
nodesAdded: 0,
|
|
4881
|
+
nodesUpdated: 0,
|
|
4882
|
+
edgesAdded: 0,
|
|
4883
|
+
edgesUpdated: 0,
|
|
4884
|
+
errors: [],
|
|
4885
|
+
durationMs: 0
|
|
4886
|
+
};
|
|
4887
|
+
}
|
|
4888
|
+
const linker = new KnowledgeLinker(this.store, extractedDir);
|
|
4889
|
+
const linkResult = await linker.link();
|
|
4890
|
+
return {
|
|
4891
|
+
codeSignals: extractionResult.nodesAdded,
|
|
4892
|
+
diagrams: diagramResult.nodesAdded,
|
|
4893
|
+
linkerFacts: linkResult.factsCreated,
|
|
4894
|
+
businessKnowledge: bkResult.nodesAdded,
|
|
4895
|
+
images: imageCount
|
|
4896
|
+
};
|
|
4897
|
+
}
|
|
4898
|
+
// ── Phase 2: RECONCILE ────────────────────────────────────────────────────
|
|
4899
|
+
buildSnapshot(domain) {
|
|
4900
|
+
let nodes = SNAPSHOT_NODE_TYPES.flatMap((type) => this.store.findNodes({ type }));
|
|
4901
|
+
if (domain) {
|
|
4902
|
+
nodes = nodes.filter((n) => n.metadata?.domain === domain);
|
|
4903
|
+
}
|
|
4904
|
+
return {
|
|
4905
|
+
entries: nodes.map((n) => ({
|
|
4906
|
+
id: n.id,
|
|
4907
|
+
type: n.type,
|
|
4908
|
+
contentHash: n.hash ?? n.id,
|
|
4909
|
+
source: n.metadata?.source ?? "unknown",
|
|
4910
|
+
name: n.name
|
|
4911
|
+
})),
|
|
4912
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4913
|
+
};
|
|
4914
|
+
}
|
|
4915
|
+
reconcile(current, fresh) {
|
|
4916
|
+
const detector = new StructuralDriftDetector();
|
|
4917
|
+
return detector.detect(current, fresh);
|
|
4918
|
+
}
|
|
4919
|
+
// ── Phase 3: DETECT ───────────────────────────────────────────────────────
|
|
4920
|
+
async detect(options) {
|
|
4921
|
+
const knowledgeDir = path10.join(options.projectDir, "docs", "knowledge");
|
|
4922
|
+
const aggregator = new KnowledgeStagingAggregator(options.projectDir);
|
|
4923
|
+
const gapReport = await aggregator.generateGapReport(knowledgeDir);
|
|
4924
|
+
await aggregator.writeGapReport(gapReport);
|
|
4925
|
+
return gapReport;
|
|
4926
|
+
}
|
|
4927
|
+
// ── Phase 4: REMEDIATE ────────────────────────────────────────────────────
|
|
4928
|
+
remediate(driftResult, remediations, options) {
|
|
4929
|
+
for (const finding of driftResult.findings) {
|
|
4930
|
+
switch (finding.classification) {
|
|
4931
|
+
case "stale":
|
|
4932
|
+
this.store.removeNode(finding.entryId);
|
|
4933
|
+
remediations.push(`removed stale: ${finding.entryId}`);
|
|
4934
|
+
break;
|
|
4935
|
+
case "new":
|
|
4936
|
+
break;
|
|
4937
|
+
case "drifted":
|
|
4938
|
+
if (!options.ci) {
|
|
4939
|
+
remediations.push(`flagged drifted: ${finding.entryId}`);
|
|
4940
|
+
}
|
|
4941
|
+
break;
|
|
4942
|
+
case "contradicting":
|
|
4943
|
+
break;
|
|
4944
|
+
}
|
|
4945
|
+
}
|
|
4946
|
+
}
|
|
4947
|
+
async stageNewFindings(driftResult, options) {
|
|
4948
|
+
const newFindings = driftResult.findings.filter((f) => f.classification === "new");
|
|
4949
|
+
if (newFindings.length === 0) return;
|
|
4950
|
+
const stagedEntries = newFindings.filter((f) => f.fresh != null).map((f) => ({
|
|
4951
|
+
id: f.fresh.id,
|
|
4952
|
+
source: this.classifySource(f.fresh.source),
|
|
4953
|
+
nodeType: f.fresh.type,
|
|
4954
|
+
name: f.fresh.name,
|
|
4955
|
+
confidence: 0.7,
|
|
4956
|
+
contentHash: f.fresh.contentHash,
|
|
4957
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4958
|
+
}));
|
|
4959
|
+
if (stagedEntries.length > 0) {
|
|
4960
|
+
const aggregator = new KnowledgeStagingAggregator(options.projectDir);
|
|
4961
|
+
await aggregator.aggregate(stagedEntries, [], []);
|
|
4962
|
+
}
|
|
4963
|
+
}
|
|
4964
|
+
classifySource(source) {
|
|
4965
|
+
if (source === "linker" || source === "knowledge-linker") return "linker";
|
|
4966
|
+
if (source === "diagram") return "diagram";
|
|
4967
|
+
return "extractor";
|
|
4968
|
+
}
|
|
4969
|
+
// ── Verdict ───────────────────────────────────────────────────────────────
|
|
4970
|
+
computeVerdict(driftResult) {
|
|
4971
|
+
const { summary } = driftResult;
|
|
4972
|
+
const unresolved = summary.drifted + summary.stale + summary.contradicting;
|
|
4973
|
+
if (unresolved === 0 && summary.new === 0) return "pass";
|
|
4974
|
+
if (unresolved === 0) return "warn";
|
|
4975
|
+
return "fail";
|
|
1931
4976
|
}
|
|
1932
4977
|
};
|
|
1933
4978
|
|
|
1934
4979
|
// src/ingest/connectors/ConnectorUtils.ts
|
|
1935
|
-
var
|
|
4980
|
+
var CODE_NODE_TYPES4 = ["file", "function", "class", "method", "interface", "variable"];
|
|
4981
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
4982
|
+
function withRetry(client, options) {
|
|
4983
|
+
const maxRetries = options?.maxRetries ?? 3;
|
|
4984
|
+
const baseDelayMs = options?.baseDelayMs ?? 1e3;
|
|
4985
|
+
const maxDelayMs = options?.maxDelayMs ?? 3e4;
|
|
4986
|
+
return async (url, requestOptions) => {
|
|
4987
|
+
let lastError;
|
|
4988
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
4989
|
+
try {
|
|
4990
|
+
const response = await client(url, requestOptions);
|
|
4991
|
+
if (response.ok || !RETRYABLE_STATUSES.has(response.status ?? 0)) {
|
|
4992
|
+
return response;
|
|
4993
|
+
}
|
|
4994
|
+
if (attempt === maxRetries) {
|
|
4995
|
+
return response;
|
|
4996
|
+
}
|
|
4997
|
+
} catch (err) {
|
|
4998
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
4999
|
+
if (attempt === maxRetries) {
|
|
5000
|
+
throw lastError;
|
|
5001
|
+
}
|
|
5002
|
+
}
|
|
5003
|
+
const exponentialDelay = baseDelayMs * 2 ** attempt;
|
|
5004
|
+
const cappedDelay = Math.min(exponentialDelay, maxDelayMs);
|
|
5005
|
+
const jitteredDelay = cappedDelay * (0.5 + Math.random() * 0.5);
|
|
5006
|
+
await new Promise((resolve) => setTimeout(resolve, jitteredDelay));
|
|
5007
|
+
}
|
|
5008
|
+
throw lastError ?? new Error("Request failed after retries");
|
|
5009
|
+
};
|
|
5010
|
+
}
|
|
1936
5011
|
var SANITIZE_RULES = [
|
|
1937
5012
|
// Strip XML/HTML-like instruction tags that could be interpreted as system prompts
|
|
1938
5013
|
{
|
|
@@ -1967,7 +5042,7 @@ function sanitizeExternalText(text, maxLength = 2e3) {
|
|
|
1967
5042
|
}
|
|
1968
5043
|
function linkToCode(store, content, sourceNodeId, edgeType, options) {
|
|
1969
5044
|
let edgesCreated = 0;
|
|
1970
|
-
for (const type of
|
|
5045
|
+
for (const type of CODE_NODE_TYPES4) {
|
|
1971
5046
|
const nodes = store.findNodes({ type });
|
|
1972
5047
|
for (const node of nodes) {
|
|
1973
5048
|
if (node.name.length < 3) continue;
|
|
@@ -1987,12 +5062,12 @@ function linkToCode(store, content, sourceNodeId, edgeType, options) {
|
|
|
1987
5062
|
}
|
|
1988
5063
|
|
|
1989
5064
|
// src/ingest/connectors/SyncManager.ts
|
|
1990
|
-
var
|
|
1991
|
-
var
|
|
5065
|
+
var fs10 = __toESM(require("fs/promises"));
|
|
5066
|
+
var path11 = __toESM(require("path"));
|
|
1992
5067
|
var SyncManager = class {
|
|
1993
5068
|
constructor(store, graphDir) {
|
|
1994
5069
|
this.store = store;
|
|
1995
|
-
this.metadataPath =
|
|
5070
|
+
this.metadataPath = path11.join(graphDir, "sync-metadata.json");
|
|
1996
5071
|
}
|
|
1997
5072
|
store;
|
|
1998
5073
|
registrations = /* @__PURE__ */ new Map();
|
|
@@ -2040,6 +5115,16 @@ var SyncManager = class {
|
|
|
2040
5115
|
combined.errors.push(...result.errors);
|
|
2041
5116
|
combined.durationMs += result.durationMs;
|
|
2042
5117
|
}
|
|
5118
|
+
try {
|
|
5119
|
+
const linker = new KnowledgeLinker(this.store);
|
|
5120
|
+
const linkResult = await linker.link();
|
|
5121
|
+
combined.nodesAdded += linkResult.factsCreated + linkResult.conceptsClustered;
|
|
5122
|
+
combined.errors.push(...linkResult.errors);
|
|
5123
|
+
} catch (err) {
|
|
5124
|
+
combined.errors.push(
|
|
5125
|
+
`KnowledgeLinker error: ${err instanceof Error ? err.message : String(err)}`
|
|
5126
|
+
);
|
|
5127
|
+
}
|
|
2043
5128
|
return combined;
|
|
2044
5129
|
}
|
|
2045
5130
|
async getMetadata() {
|
|
@@ -2047,18 +5132,64 @@ var SyncManager = class {
|
|
|
2047
5132
|
}
|
|
2048
5133
|
async loadMetadata() {
|
|
2049
5134
|
try {
|
|
2050
|
-
const raw = await
|
|
5135
|
+
const raw = await fs10.readFile(this.metadataPath, "utf-8");
|
|
2051
5136
|
return JSON.parse(raw);
|
|
2052
5137
|
} catch {
|
|
2053
5138
|
return { connectors: {} };
|
|
2054
5139
|
}
|
|
2055
5140
|
}
|
|
2056
5141
|
async saveMetadata(metadata) {
|
|
2057
|
-
await
|
|
2058
|
-
await
|
|
5142
|
+
await fs10.mkdir(path11.dirname(this.metadataPath), { recursive: true });
|
|
5143
|
+
await fs10.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
2059
5144
|
}
|
|
2060
5145
|
};
|
|
2061
5146
|
|
|
5147
|
+
// src/ingest/connectors/ContentCondenser.ts
|
|
5148
|
+
var SUMMARIZE_PROMPT = `Summarize the following content to fit within the specified length.
|
|
5149
|
+
Preserve all business rules, SLAs, requirements, decisions, and regulatory references.
|
|
5150
|
+
Remove redundant details and boilerplate while keeping all actionable information.
|
|
5151
|
+
|
|
5152
|
+
Content to summarize:
|
|
5153
|
+
`;
|
|
5154
|
+
async function condenseContent(raw, options, summarizeFn) {
|
|
5155
|
+
const originalLength = raw.length;
|
|
5156
|
+
const { maxLength } = options;
|
|
5157
|
+
const summarizationThreshold = options.summarizationThreshold ?? maxLength * 2;
|
|
5158
|
+
if (raw.length <= maxLength) {
|
|
5159
|
+
return { content: raw, method: "passthrough", originalLength };
|
|
5160
|
+
}
|
|
5161
|
+
if (raw.length < summarizationThreshold) {
|
|
5162
|
+
return {
|
|
5163
|
+
content: sanitizeExternalText(raw, maxLength),
|
|
5164
|
+
method: "truncated",
|
|
5165
|
+
originalLength
|
|
5166
|
+
};
|
|
5167
|
+
}
|
|
5168
|
+
if (options.modelEndpoint && summarizeFn) {
|
|
5169
|
+
try {
|
|
5170
|
+
const summarized = await summarizeFn(SUMMARIZE_PROMPT + raw, {
|
|
5171
|
+
endpoint: options.modelEndpoint,
|
|
5172
|
+
model: options.modelName ?? "default",
|
|
5173
|
+
maxTokens: Math.ceil(maxLength / 4)
|
|
5174
|
+
// rough token estimate
|
|
5175
|
+
});
|
|
5176
|
+
const finalContent = summarized.length > maxLength ? sanitizeExternalText(summarized, maxLength) : summarized;
|
|
5177
|
+
return { content: finalContent, method: "summarized", originalLength };
|
|
5178
|
+
} catch {
|
|
5179
|
+
return {
|
|
5180
|
+
content: sanitizeExternalText(raw, maxLength),
|
|
5181
|
+
method: "truncated",
|
|
5182
|
+
originalLength
|
|
5183
|
+
};
|
|
5184
|
+
}
|
|
5185
|
+
}
|
|
5186
|
+
return {
|
|
5187
|
+
content: sanitizeExternalText(raw, maxLength),
|
|
5188
|
+
method: "truncated",
|
|
5189
|
+
originalLength
|
|
5190
|
+
};
|
|
5191
|
+
}
|
|
5192
|
+
|
|
2062
5193
|
// src/ingest/connectors/JiraConnector.ts
|
|
2063
5194
|
function buildIngestResult(nodesAdded, edgesAdded, errors, start) {
|
|
2064
5195
|
return {
|
|
@@ -2090,7 +5221,7 @@ var JiraConnector = class {
|
|
|
2090
5221
|
source = "jira";
|
|
2091
5222
|
httpClient;
|
|
2092
5223
|
constructor(httpClient) {
|
|
2093
|
-
this.httpClient = httpClient ?? ((url, options) => fetch(url, options));
|
|
5224
|
+
this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
|
|
2094
5225
|
}
|
|
2095
5226
|
async ingest(store, config) {
|
|
2096
5227
|
const start = Date.now();
|
|
@@ -2117,7 +5248,7 @@ var JiraConnector = class {
|
|
|
2117
5248
|
const jql = buildJql(config);
|
|
2118
5249
|
const headers = { Authorization: `Basic ${apiKey}`, "Content-Type": "application/json" };
|
|
2119
5250
|
try {
|
|
2120
|
-
const counts = await this.fetchAllIssues(store, baseUrl, jql, headers);
|
|
5251
|
+
const counts = await this.fetchAllIssues(store, baseUrl, jql, headers, config);
|
|
2121
5252
|
return buildIngestResult(counts.nodesAdded, counts.edgesAdded, [], start);
|
|
2122
5253
|
} catch (err) {
|
|
2123
5254
|
return buildIngestResult(
|
|
@@ -2128,7 +5259,7 @@ var JiraConnector = class {
|
|
|
2128
5259
|
);
|
|
2129
5260
|
}
|
|
2130
5261
|
}
|
|
2131
|
-
async fetchAllIssues(store, baseUrl, jql, headers) {
|
|
5262
|
+
async fetchAllIssues(store, baseUrl, jql, headers, config) {
|
|
2132
5263
|
let nodesAdded = 0;
|
|
2133
5264
|
let edgesAdded = 0;
|
|
2134
5265
|
let startAt = 0;
|
|
@@ -2141,7 +5272,7 @@ var JiraConnector = class {
|
|
|
2141
5272
|
const data = await response.json();
|
|
2142
5273
|
total = data.total;
|
|
2143
5274
|
for (const issue of data.issues) {
|
|
2144
|
-
const counts = this.processIssue(store, issue);
|
|
5275
|
+
const counts = await this.processIssue(store, issue, baseUrl, headers, config);
|
|
2145
5276
|
nodesAdded += counts.nodesAdded;
|
|
2146
5277
|
edgesAdded += counts.edgesAdded;
|
|
2147
5278
|
}
|
|
@@ -2149,19 +5280,47 @@ var JiraConnector = class {
|
|
|
2149
5280
|
}
|
|
2150
5281
|
return { nodesAdded, edgesAdded };
|
|
2151
5282
|
}
|
|
2152
|
-
processIssue(store, issue) {
|
|
5283
|
+
async processIssue(store, issue, baseUrl, headers, config) {
|
|
2153
5284
|
const nodeId = `issue:jira:${issue.key}`;
|
|
5285
|
+
const comments = await this.fetchComments(baseUrl, issue.key, headers);
|
|
5286
|
+
const parts = [issue.fields.summary];
|
|
5287
|
+
if (issue.fields.description) {
|
|
5288
|
+
parts.push(issue.fields.description);
|
|
5289
|
+
}
|
|
5290
|
+
if (comments.length > 0) {
|
|
5291
|
+
for (const comment of comments) {
|
|
5292
|
+
parts.push(`${comment.author.displayName} (${comment.created}): ${comment.body}`);
|
|
5293
|
+
}
|
|
5294
|
+
}
|
|
5295
|
+
const rawContent = parts.join("\n");
|
|
5296
|
+
const acceptanceCriteria = parseAcceptanceCriteria(issue.fields.description ?? "");
|
|
5297
|
+
const customFields = extractCustomFields(issue.fields);
|
|
5298
|
+
const maxLen = config.maxContentLength ?? 4e3;
|
|
5299
|
+
const condensed = await condenseContent(rawContent, { maxLength: maxLen });
|
|
5300
|
+
const metadata = {
|
|
5301
|
+
key: issue.key,
|
|
5302
|
+
status: issue.fields.status?.name,
|
|
5303
|
+
priority: issue.fields.priority?.name,
|
|
5304
|
+
assignee: issue.fields.assignee?.displayName,
|
|
5305
|
+
labels: issue.fields.labels ?? [],
|
|
5306
|
+
commentCount: comments.length
|
|
5307
|
+
};
|
|
5308
|
+
if (acceptanceCriteria.length > 0) {
|
|
5309
|
+
metadata.acceptanceCriteria = acceptanceCriteria;
|
|
5310
|
+
}
|
|
5311
|
+
if (Object.keys(customFields).length > 0) {
|
|
5312
|
+
metadata.customFields = customFields;
|
|
5313
|
+
}
|
|
5314
|
+
if (condensed.method !== "passthrough") {
|
|
5315
|
+
metadata.condensed = condensed.method;
|
|
5316
|
+
metadata.originalLength = condensed.originalLength;
|
|
5317
|
+
}
|
|
2154
5318
|
store.addNode({
|
|
2155
5319
|
id: nodeId,
|
|
2156
5320
|
type: "issue",
|
|
2157
5321
|
name: sanitizeExternalText(issue.fields.summary, 500),
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
status: issue.fields.status?.name,
|
|
2161
|
-
priority: issue.fields.priority?.name,
|
|
2162
|
-
assignee: issue.fields.assignee?.displayName,
|
|
2163
|
-
labels: issue.fields.labels ?? []
|
|
2164
|
-
}
|
|
5322
|
+
content: condensed.content,
|
|
5323
|
+
metadata
|
|
2165
5324
|
});
|
|
2166
5325
|
const searchText = sanitizeExternalText(
|
|
2167
5326
|
[issue.fields.summary, issue.fields.description ?? ""].join(" ")
|
|
@@ -2169,7 +5328,40 @@ var JiraConnector = class {
|
|
|
2169
5328
|
const edgesAdded = linkToCode(store, searchText, nodeId, "applies_to");
|
|
2170
5329
|
return { nodesAdded: 1, edgesAdded };
|
|
2171
5330
|
}
|
|
5331
|
+
async fetchComments(baseUrl, issueKey, headers) {
|
|
5332
|
+
try {
|
|
5333
|
+
const url = `${baseUrl}/rest/api/2/issue/${issueKey}/comment`;
|
|
5334
|
+
const response = await this.httpClient(url, { headers });
|
|
5335
|
+
if (!response.ok) return [];
|
|
5336
|
+
const data = await response.json();
|
|
5337
|
+
return data.comments ?? [];
|
|
5338
|
+
} catch {
|
|
5339
|
+
return [];
|
|
5340
|
+
}
|
|
5341
|
+
}
|
|
2172
5342
|
};
|
|
5343
|
+
function parseAcceptanceCriteria(description) {
|
|
5344
|
+
const criteria = [];
|
|
5345
|
+
const checkboxRegex = /[-*]\s*\[[ x]\]\s*(.+)/gi;
|
|
5346
|
+
let match;
|
|
5347
|
+
while ((match = checkboxRegex.exec(description)) !== null) {
|
|
5348
|
+
criteria.push(match[1].trim());
|
|
5349
|
+
}
|
|
5350
|
+
const gwtRegex = /Given\b.+?When\b.+?Then\b.+/gi;
|
|
5351
|
+
while ((match = gwtRegex.exec(description)) !== null) {
|
|
5352
|
+
criteria.push(match[0].trim());
|
|
5353
|
+
}
|
|
5354
|
+
return criteria;
|
|
5355
|
+
}
|
|
5356
|
+
function extractCustomFields(fields) {
|
|
5357
|
+
const result = {};
|
|
5358
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
5359
|
+
if (key.startsWith("customfield_") && value != null && typeof value === "string") {
|
|
5360
|
+
result[key] = value;
|
|
5361
|
+
}
|
|
5362
|
+
}
|
|
5363
|
+
return result;
|
|
5364
|
+
}
|
|
2173
5365
|
|
|
2174
5366
|
// src/ingest/connectors/SlackConnector.ts
|
|
2175
5367
|
var SlackConnector = class {
|
|
@@ -2177,7 +5369,7 @@ var SlackConnector = class {
|
|
|
2177
5369
|
source = "slack";
|
|
2178
5370
|
httpClient;
|
|
2179
5371
|
constructor(httpClient) {
|
|
2180
|
-
this.httpClient = httpClient ?? ((url, options) => fetch(url, options));
|
|
5372
|
+
this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
|
|
2181
5373
|
}
|
|
2182
5374
|
async ingest(store, config) {
|
|
2183
5375
|
const start = Date.now();
|
|
@@ -2200,7 +5392,7 @@ var SlackConnector = class {
|
|
|
2200
5392
|
const oldest = config.lookbackDays ? String(Math.floor((Date.now() - Number(config.lookbackDays) * 864e5) / 1e3)) : void 0;
|
|
2201
5393
|
for (const channel of channels) {
|
|
2202
5394
|
try {
|
|
2203
|
-
const result = await this.processChannel(store, channel, apiKey, oldest);
|
|
5395
|
+
const result = await this.processChannel(store, channel, apiKey, oldest, config);
|
|
2204
5396
|
nodesAdded += result.nodesAdded;
|
|
2205
5397
|
edgesAdded += result.edgesAdded;
|
|
2206
5398
|
errors.push(...result.errors);
|
|
@@ -2219,7 +5411,7 @@ var SlackConnector = class {
|
|
|
2219
5411
|
durationMs: Date.now() - start
|
|
2220
5412
|
};
|
|
2221
5413
|
}
|
|
2222
|
-
async processChannel(store, channel, apiKey, oldest) {
|
|
5414
|
+
async processChannel(store, channel, apiKey, oldest, config) {
|
|
2223
5415
|
const errors = [];
|
|
2224
5416
|
let nodesAdded = 0;
|
|
2225
5417
|
let edgesAdded = 0;
|
|
@@ -2244,27 +5436,74 @@ var SlackConnector = class {
|
|
|
2244
5436
|
if (!data.ok) {
|
|
2245
5437
|
return { nodesAdded: 0, edgesAdded: 0, errors: [`Slack API error for channel ${channel}`] };
|
|
2246
5438
|
}
|
|
5439
|
+
const maxLen = config.maxContentLength ?? 2e3;
|
|
2247
5440
|
for (const message of data.messages) {
|
|
2248
5441
|
const nodeId = `conversation:slack:${channel}:${message.ts}`;
|
|
2249
|
-
|
|
2250
|
-
|
|
5442
|
+
let assembledText = message.text;
|
|
5443
|
+
let threadReplyCount;
|
|
5444
|
+
if (message.reply_count && message.reply_count > 0 && message.thread_ts) {
|
|
5445
|
+
const replies = await this.fetchThreadReplies(channel, message.thread_ts, apiKey);
|
|
5446
|
+
if (replies.length > 0) {
|
|
5447
|
+
const threadReplies = replies.slice(1);
|
|
5448
|
+
threadReplyCount = threadReplies.length;
|
|
5449
|
+
if (threadReplies.length > 0) {
|
|
5450
|
+
const replyLines = threadReplies.map((r) => `${r.user} (${r.ts}): ${r.text}`);
|
|
5451
|
+
assembledText = `${message.text}
|
|
5452
|
+
${replyLines.join("\n")}`;
|
|
5453
|
+
}
|
|
5454
|
+
}
|
|
5455
|
+
}
|
|
5456
|
+
const condensed = await condenseContent(assembledText, { maxLength: maxLen });
|
|
5457
|
+
const snippet = condensed.content.length > 100 ? condensed.content.slice(0, 100) : condensed.content;
|
|
5458
|
+
const reactions = message.reactions ? message.reactions.reduce(
|
|
5459
|
+
(acc, r) => ({ ...acc, [r.name]: r.count }),
|
|
5460
|
+
{}
|
|
5461
|
+
) : void 0;
|
|
5462
|
+
const metadata = {
|
|
5463
|
+
author: message.user,
|
|
5464
|
+
channel,
|
|
5465
|
+
timestamp: message.ts
|
|
5466
|
+
};
|
|
5467
|
+
if (threadReplyCount !== void 0) {
|
|
5468
|
+
metadata.threadReplyCount = threadReplyCount;
|
|
5469
|
+
}
|
|
5470
|
+
if (reactions) {
|
|
5471
|
+
metadata.reactions = reactions;
|
|
5472
|
+
}
|
|
5473
|
+
if (condensed.method !== "passthrough") {
|
|
5474
|
+
metadata.condensed = condensed.method;
|
|
5475
|
+
metadata.originalLength = condensed.originalLength;
|
|
5476
|
+
}
|
|
2251
5477
|
store.addNode({
|
|
2252
5478
|
id: nodeId,
|
|
2253
5479
|
type: "conversation",
|
|
2254
5480
|
name: snippet,
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
channel,
|
|
2258
|
-
timestamp: message.ts
|
|
2259
|
-
}
|
|
5481
|
+
content: condensed.content,
|
|
5482
|
+
metadata
|
|
2260
5483
|
});
|
|
2261
5484
|
nodesAdded++;
|
|
2262
|
-
edgesAdded += linkToCode(store,
|
|
5485
|
+
edgesAdded += linkToCode(store, condensed.content, nodeId, "references", {
|
|
2263
5486
|
checkPaths: true
|
|
2264
5487
|
});
|
|
2265
5488
|
}
|
|
2266
5489
|
return { nodesAdded, edgesAdded, errors };
|
|
2267
5490
|
}
|
|
5491
|
+
async fetchThreadReplies(channel, threadTs, apiKey) {
|
|
5492
|
+
try {
|
|
5493
|
+
const url = `https://slack.com/api/conversations.replies?channel=${encodeURIComponent(channel)}&ts=${threadTs}`;
|
|
5494
|
+
const response = await this.httpClient(url, {
|
|
5495
|
+
headers: {
|
|
5496
|
+
Authorization: `Bearer ${apiKey}`,
|
|
5497
|
+
"Content-Type": "application/json"
|
|
5498
|
+
}
|
|
5499
|
+
});
|
|
5500
|
+
if (!response.ok) return [];
|
|
5501
|
+
const data = await response.json();
|
|
5502
|
+
return data.ok ? data.messages : [];
|
|
5503
|
+
} catch {
|
|
5504
|
+
return [];
|
|
5505
|
+
}
|
|
5506
|
+
}
|
|
2268
5507
|
};
|
|
2269
5508
|
|
|
2270
5509
|
// src/ingest/connectors/ConfluenceConnector.ts
|
|
@@ -2283,7 +5522,7 @@ var ConfluenceConnector = class {
|
|
|
2283
5522
|
source = "confluence";
|
|
2284
5523
|
httpClient;
|
|
2285
5524
|
constructor(httpClient) {
|
|
2286
|
-
this.httpClient = httpClient ?? ((url, options) => fetch(url, options));
|
|
5525
|
+
this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
|
|
2287
5526
|
}
|
|
2288
5527
|
async ingest(store, config) {
|
|
2289
5528
|
const start = Date.now();
|
|
@@ -2296,7 +5535,14 @@ var ConfluenceConnector = class {
|
|
|
2296
5535
|
const baseUrlEnv = config.baseUrlEnv ?? "CONFLUENCE_BASE_URL";
|
|
2297
5536
|
const baseUrl = process.env[baseUrlEnv] ?? "";
|
|
2298
5537
|
const spaceKey = config.spaceKey ?? "";
|
|
2299
|
-
const counts = await this.fetchAllPagesHandled(
|
|
5538
|
+
const counts = await this.fetchAllPagesHandled(
|
|
5539
|
+
store,
|
|
5540
|
+
baseUrl,
|
|
5541
|
+
apiKey,
|
|
5542
|
+
spaceKey,
|
|
5543
|
+
errors,
|
|
5544
|
+
config
|
|
5545
|
+
);
|
|
2300
5546
|
return {
|
|
2301
5547
|
nodesAdded: counts.nodesAdded,
|
|
2302
5548
|
nodesUpdated: 0,
|
|
@@ -2306,9 +5552,9 @@ var ConfluenceConnector = class {
|
|
|
2306
5552
|
durationMs: Date.now() - start
|
|
2307
5553
|
};
|
|
2308
5554
|
}
|
|
2309
|
-
async fetchAllPagesHandled(store, baseUrl, apiKey, spaceKey, errors) {
|
|
5555
|
+
async fetchAllPagesHandled(store, baseUrl, apiKey, spaceKey, errors, config) {
|
|
2310
5556
|
try {
|
|
2311
|
-
const result = await this.fetchAllPages(store, baseUrl, apiKey, spaceKey);
|
|
5557
|
+
const result = await this.fetchAllPages(store, baseUrl, apiKey, spaceKey, config);
|
|
2312
5558
|
errors.push(...result.errors);
|
|
2313
5559
|
return { nodesAdded: result.nodesAdded, edgesAdded: result.edgesAdded };
|
|
2314
5560
|
} catch (err) {
|
|
@@ -2316,7 +5562,7 @@ var ConfluenceConnector = class {
|
|
|
2316
5562
|
return { nodesAdded: 0, edgesAdded: 0 };
|
|
2317
5563
|
}
|
|
2318
5564
|
}
|
|
2319
|
-
async fetchAllPages(store, baseUrl, apiKey, spaceKey) {
|
|
5565
|
+
async fetchAllPages(store, baseUrl, apiKey, spaceKey, config) {
|
|
2320
5566
|
const errors = [];
|
|
2321
5567
|
let nodesAdded = 0;
|
|
2322
5568
|
let edgesAdded = 0;
|
|
@@ -2331,7 +5577,7 @@ var ConfluenceConnector = class {
|
|
|
2331
5577
|
}
|
|
2332
5578
|
const data = await response.json();
|
|
2333
5579
|
for (const page of data.results) {
|
|
2334
|
-
const counts = this.processPage(store, page, spaceKey);
|
|
5580
|
+
const counts = await this.processPage(store, page, spaceKey, config);
|
|
2335
5581
|
nodesAdded += counts.nodesAdded;
|
|
2336
5582
|
edgesAdded += counts.edgesAdded;
|
|
2337
5583
|
}
|
|
@@ -2339,22 +5585,43 @@ var ConfluenceConnector = class {
|
|
|
2339
5585
|
}
|
|
2340
5586
|
return { nodesAdded, edgesAdded, errors };
|
|
2341
5587
|
}
|
|
2342
|
-
processPage(store, page, spaceKey) {
|
|
5588
|
+
async processPage(store, page, spaceKey, config) {
|
|
2343
5589
|
const nodeId = `confluence:${page.id}`;
|
|
5590
|
+
let edgesAdded = 0;
|
|
5591
|
+
const labels = page.metadata?.labels?.results?.map((l) => l.name) ?? [];
|
|
5592
|
+
const parentPageId = page.ancestors && page.ancestors.length > 0 ? page.ancestors[page.ancestors.length - 1].id : void 0;
|
|
5593
|
+
const rawContent = `${page.title} ${page.body?.storage?.value ?? ""}`;
|
|
5594
|
+
const maxLen = config.maxContentLength ?? 8e3;
|
|
5595
|
+
const condensed = await condenseContent(rawContent, { maxLength: maxLen });
|
|
5596
|
+
const metadata = {
|
|
5597
|
+
source: "confluence",
|
|
5598
|
+
spaceKey,
|
|
5599
|
+
pageId: page.id,
|
|
5600
|
+
status: page.status,
|
|
5601
|
+
url: page._links?.webui ?? "",
|
|
5602
|
+
labels
|
|
5603
|
+
};
|
|
5604
|
+
if (parentPageId) {
|
|
5605
|
+
metadata.parentPageId = parentPageId;
|
|
5606
|
+
}
|
|
5607
|
+
if (condensed.method !== "passthrough") {
|
|
5608
|
+
metadata.condensed = condensed.method;
|
|
5609
|
+
metadata.originalLength = condensed.originalLength;
|
|
5610
|
+
}
|
|
2344
5611
|
store.addNode({
|
|
2345
5612
|
id: nodeId,
|
|
2346
5613
|
type: "document",
|
|
2347
5614
|
name: sanitizeExternalText(page.title, 500),
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
spaceKey,
|
|
2351
|
-
pageId: page.id,
|
|
2352
|
-
status: page.status,
|
|
2353
|
-
url: page._links?.webui ?? ""
|
|
2354
|
-
}
|
|
5615
|
+
content: condensed.content,
|
|
5616
|
+
metadata
|
|
2355
5617
|
});
|
|
5618
|
+
if (parentPageId) {
|
|
5619
|
+
const parentNodeId = `confluence:${parentPageId}`;
|
|
5620
|
+
store.addEdge({ from: parentNodeId, to: nodeId, type: "contains" });
|
|
5621
|
+
edgesAdded++;
|
|
5622
|
+
}
|
|
2356
5623
|
const text = sanitizeExternalText(`${page.title} ${page.body?.storage?.value ?? ""}`);
|
|
2357
|
-
|
|
5624
|
+
edgesAdded += linkToCode(store, text, nodeId, "documents");
|
|
2358
5625
|
return { nodesAdded: 1, edgesAdded };
|
|
2359
5626
|
}
|
|
2360
5627
|
};
|
|
@@ -2420,7 +5687,7 @@ var CIConnector = class {
|
|
|
2420
5687
|
source = "github-actions";
|
|
2421
5688
|
httpClient;
|
|
2422
5689
|
constructor(httpClient) {
|
|
2423
|
-
this.httpClient = httpClient ?? ((url, options) => fetch(url, options));
|
|
5690
|
+
this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
|
|
2424
5691
|
}
|
|
2425
5692
|
async ingest(store, config) {
|
|
2426
5693
|
const start = Date.now();
|
|
@@ -2472,6 +5739,306 @@ var CIConnector = class {
|
|
|
2472
5739
|
}
|
|
2473
5740
|
};
|
|
2474
5741
|
|
|
5742
|
+
// src/ingest/connectors/FigmaConnector.ts
|
|
5743
|
+
var CONSTRAINT_KEYWORDS = [
|
|
5744
|
+
"constraint",
|
|
5745
|
+
"must",
|
|
5746
|
+
"required",
|
|
5747
|
+
"minimum",
|
|
5748
|
+
"maximum",
|
|
5749
|
+
"spacing",
|
|
5750
|
+
"padding",
|
|
5751
|
+
"margin",
|
|
5752
|
+
"breakpoint",
|
|
5753
|
+
"responsive",
|
|
5754
|
+
"accessible",
|
|
5755
|
+
"a11y",
|
|
5756
|
+
"wcag"
|
|
5757
|
+
];
|
|
5758
|
+
function hasConstraintKeyword(text) {
|
|
5759
|
+
const lower = text.toLowerCase();
|
|
5760
|
+
return CONSTRAINT_KEYWORDS.some((kw) => lower.includes(kw));
|
|
5761
|
+
}
|
|
5762
|
+
function buildIngestResult2(nodesAdded, edgesAdded, errors, start) {
|
|
5763
|
+
return {
|
|
5764
|
+
nodesAdded,
|
|
5765
|
+
nodesUpdated: 0,
|
|
5766
|
+
edgesAdded,
|
|
5767
|
+
edgesUpdated: 0,
|
|
5768
|
+
errors,
|
|
5769
|
+
durationMs: Date.now() - start
|
|
5770
|
+
};
|
|
5771
|
+
}
|
|
5772
|
+
var FigmaConnector = class {
|
|
5773
|
+
name = "figma";
|
|
5774
|
+
source = "figma";
|
|
5775
|
+
httpClient;
|
|
5776
|
+
constructor(httpClient) {
|
|
5777
|
+
this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
|
|
5778
|
+
}
|
|
5779
|
+
async ingest(store, config) {
|
|
5780
|
+
const start = Date.now();
|
|
5781
|
+
const apiKeyEnv = config.apiKeyEnv ?? "FIGMA_API_KEY";
|
|
5782
|
+
const apiKey = process.env[apiKeyEnv];
|
|
5783
|
+
if (!apiKey) {
|
|
5784
|
+
return buildIngestResult2(
|
|
5785
|
+
0,
|
|
5786
|
+
0,
|
|
5787
|
+
[`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
5788
|
+
start
|
|
5789
|
+
);
|
|
5790
|
+
}
|
|
5791
|
+
const baseUrlEnv = config.baseUrlEnv ?? "FIGMA_BASE_URL";
|
|
5792
|
+
const baseUrl = process.env[baseUrlEnv] ?? "https://api.figma.com";
|
|
5793
|
+
try {
|
|
5794
|
+
const parsed = new URL(baseUrl);
|
|
5795
|
+
if (parsed.protocol !== "https:" || !parsed.hostname.endsWith("api.figma.com")) {
|
|
5796
|
+
return buildIngestResult2(
|
|
5797
|
+
0,
|
|
5798
|
+
0,
|
|
5799
|
+
[`Invalid ${baseUrlEnv}: must be an HTTPS URL on api.figma.com`],
|
|
5800
|
+
start
|
|
5801
|
+
);
|
|
5802
|
+
}
|
|
5803
|
+
} catch {
|
|
5804
|
+
return buildIngestResult2(0, 0, [`Invalid ${baseUrlEnv}: not a valid URL`], start);
|
|
5805
|
+
}
|
|
5806
|
+
const fileIds = config.fileIds;
|
|
5807
|
+
if (!fileIds || fileIds.length === 0) {
|
|
5808
|
+
return buildIngestResult2(0, 0, ["No fileIds provided in connector config"], start);
|
|
5809
|
+
}
|
|
5810
|
+
const headers = { "X-FIGMA-TOKEN": apiKey };
|
|
5811
|
+
const maxLen = config.maxContentLength ?? 4e3;
|
|
5812
|
+
let nodesAdded = 0;
|
|
5813
|
+
let edgesAdded = 0;
|
|
5814
|
+
const errors = [];
|
|
5815
|
+
for (const fileId of fileIds) {
|
|
5816
|
+
try {
|
|
5817
|
+
const counts = await this.processFile(store, baseUrl, fileId, headers, maxLen);
|
|
5818
|
+
nodesAdded += counts.nodesAdded;
|
|
5819
|
+
edgesAdded += counts.edgesAdded;
|
|
5820
|
+
} catch (err) {
|
|
5821
|
+
errors.push(
|
|
5822
|
+
`Figma API error for file ${fileId}: ${err instanceof Error ? err.message : String(err)}`
|
|
5823
|
+
);
|
|
5824
|
+
}
|
|
5825
|
+
}
|
|
5826
|
+
return buildIngestResult2(nodesAdded, edgesAdded, errors, start);
|
|
5827
|
+
}
|
|
5828
|
+
async processFile(store, baseUrl, fileId, headers, maxLen) {
|
|
5829
|
+
let nodesAdded = 0;
|
|
5830
|
+
let edgesAdded = 0;
|
|
5831
|
+
const stylesUrl = `${baseUrl}/v1/files/${fileId}/styles`;
|
|
5832
|
+
const stylesResponse = await this.httpClient(stylesUrl, { headers });
|
|
5833
|
+
if (!stylesResponse.ok) throw new Error(`Styles request failed for file ${fileId}`);
|
|
5834
|
+
const stylesData = await stylesResponse.json();
|
|
5835
|
+
for (const style of stylesData.meta.styles) {
|
|
5836
|
+
const nodeId = `figma:token:${style.key}`;
|
|
5837
|
+
const condensed = await condenseContent(sanitizeExternalText(style.description || "", 2e3), {
|
|
5838
|
+
maxLength: maxLen
|
|
5839
|
+
});
|
|
5840
|
+
const metadata = {
|
|
5841
|
+
source: "figma",
|
|
5842
|
+
key: style.key,
|
|
5843
|
+
styleType: style.style_type,
|
|
5844
|
+
fileId
|
|
5845
|
+
};
|
|
5846
|
+
if (condensed.method !== "passthrough") {
|
|
5847
|
+
metadata.condensed = condensed.method;
|
|
5848
|
+
metadata.originalLength = condensed.originalLength;
|
|
5849
|
+
}
|
|
5850
|
+
store.addNode({
|
|
5851
|
+
id: nodeId,
|
|
5852
|
+
type: "design_token",
|
|
5853
|
+
name: sanitizeExternalText(style.name, 500),
|
|
5854
|
+
content: condensed.content,
|
|
5855
|
+
metadata
|
|
5856
|
+
});
|
|
5857
|
+
nodesAdded++;
|
|
5858
|
+
const searchText = sanitizeExternalText([style.name, style.description].join(" "));
|
|
5859
|
+
edgesAdded += linkToCode(store, searchText, nodeId, "references");
|
|
5860
|
+
}
|
|
5861
|
+
const componentsUrl = `${baseUrl}/v1/files/${fileId}/components`;
|
|
5862
|
+
const componentsResponse = await this.httpClient(componentsUrl, { headers });
|
|
5863
|
+
if (!componentsResponse.ok) throw new Error(`Components request failed for file ${fileId}`);
|
|
5864
|
+
const componentsData = await componentsResponse.json();
|
|
5865
|
+
for (const component of componentsData.meta.components) {
|
|
5866
|
+
const description = component.description || "";
|
|
5867
|
+
if (description) {
|
|
5868
|
+
const intentId = `figma:intent:${component.key}`;
|
|
5869
|
+
const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
|
|
5870
|
+
maxLength: maxLen
|
|
5871
|
+
});
|
|
5872
|
+
const metadata = {
|
|
5873
|
+
source: "figma",
|
|
5874
|
+
key: component.key,
|
|
5875
|
+
fileId
|
|
5876
|
+
};
|
|
5877
|
+
if (condensed.method !== "passthrough") {
|
|
5878
|
+
metadata.condensed = condensed.method;
|
|
5879
|
+
metadata.originalLength = condensed.originalLength;
|
|
5880
|
+
}
|
|
5881
|
+
store.addNode({
|
|
5882
|
+
id: intentId,
|
|
5883
|
+
type: "aesthetic_intent",
|
|
5884
|
+
name: sanitizeExternalText(component.name, 500),
|
|
5885
|
+
content: condensed.content,
|
|
5886
|
+
metadata
|
|
5887
|
+
});
|
|
5888
|
+
nodesAdded++;
|
|
5889
|
+
const searchText = sanitizeExternalText([component.name, description].join(" "));
|
|
5890
|
+
edgesAdded += linkToCode(store, searchText, intentId, "references");
|
|
5891
|
+
}
|
|
5892
|
+
if (description && hasConstraintKeyword(description)) {
|
|
5893
|
+
const constraintId = `figma:constraint:${component.key}`;
|
|
5894
|
+
const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
|
|
5895
|
+
maxLength: maxLen
|
|
5896
|
+
});
|
|
5897
|
+
const metadata = {
|
|
5898
|
+
source: "figma",
|
|
5899
|
+
key: component.key,
|
|
5900
|
+
fileId
|
|
5901
|
+
};
|
|
5902
|
+
if (condensed.method !== "passthrough") {
|
|
5903
|
+
metadata.condensed = condensed.method;
|
|
5904
|
+
metadata.originalLength = condensed.originalLength;
|
|
5905
|
+
}
|
|
5906
|
+
store.addNode({
|
|
5907
|
+
id: constraintId,
|
|
5908
|
+
type: "design_constraint",
|
|
5909
|
+
name: sanitizeExternalText(component.name, 500),
|
|
5910
|
+
content: condensed.content,
|
|
5911
|
+
metadata
|
|
5912
|
+
});
|
|
5913
|
+
nodesAdded++;
|
|
5914
|
+
const searchText = sanitizeExternalText([component.name, description].join(" "));
|
|
5915
|
+
edgesAdded += linkToCode(store, searchText, constraintId, "references");
|
|
5916
|
+
}
|
|
5917
|
+
}
|
|
5918
|
+
return { nodesAdded, edgesAdded };
|
|
5919
|
+
}
|
|
5920
|
+
};
|
|
5921
|
+
|
|
5922
|
+
// src/ingest/connectors/MiroConnector.ts
|
|
5923
|
+
var MIN_CONTENT_LENGTH = 10;
|
|
5924
|
+
var CONCEPT_ITEM_TYPES = /* @__PURE__ */ new Set(["sticky_note", "text"]);
|
|
5925
|
+
function stripHtml(html) {
|
|
5926
|
+
return html.replace(/<[^>]*>/g, "");
|
|
5927
|
+
}
|
|
5928
|
+
function buildIngestResult3(nodesAdded, edgesAdded, errors, start) {
|
|
5929
|
+
return {
|
|
5930
|
+
nodesAdded,
|
|
5931
|
+
nodesUpdated: 0,
|
|
5932
|
+
edgesAdded,
|
|
5933
|
+
edgesUpdated: 0,
|
|
5934
|
+
errors,
|
|
5935
|
+
durationMs: Date.now() - start
|
|
5936
|
+
};
|
|
5937
|
+
}
|
|
5938
|
+
var MiroConnector = class {
|
|
5939
|
+
name = "miro";
|
|
5940
|
+
source = "miro";
|
|
5941
|
+
httpClient;
|
|
5942
|
+
constructor(httpClient) {
|
|
5943
|
+
this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
|
|
5944
|
+
}
|
|
5945
|
+
async ingest(store, config) {
|
|
5946
|
+
const start = Date.now();
|
|
5947
|
+
const apiKeyEnv = config.apiKeyEnv ?? "MIRO_API_KEY";
|
|
5948
|
+
const apiKey = process.env[apiKeyEnv];
|
|
5949
|
+
if (!apiKey) {
|
|
5950
|
+
return buildIngestResult3(
|
|
5951
|
+
0,
|
|
5952
|
+
0,
|
|
5953
|
+
[`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
5954
|
+
start
|
|
5955
|
+
);
|
|
5956
|
+
}
|
|
5957
|
+
const baseUrlEnv = config.baseUrlEnv ?? "MIRO_BASE_URL";
|
|
5958
|
+
const baseUrl = process.env[baseUrlEnv] ?? "https://api.miro.com";
|
|
5959
|
+
try {
|
|
5960
|
+
const parsed = new URL(baseUrl);
|
|
5961
|
+
if (parsed.protocol !== "https:" || parsed.hostname !== "api.miro.com") {
|
|
5962
|
+
return buildIngestResult3(
|
|
5963
|
+
0,
|
|
5964
|
+
0,
|
|
5965
|
+
[`Invalid ${baseUrlEnv}: must be an HTTPS URL on api.miro.com`],
|
|
5966
|
+
start
|
|
5967
|
+
);
|
|
5968
|
+
}
|
|
5969
|
+
} catch {
|
|
5970
|
+
return buildIngestResult3(0, 0, [`Invalid ${baseUrlEnv}: not a valid URL`], start);
|
|
5971
|
+
}
|
|
5972
|
+
const boardIds = config.boardIds;
|
|
5973
|
+
if (!boardIds || boardIds.length === 0) {
|
|
5974
|
+
return buildIngestResult3(0, 0, ["No boardIds provided in config"], start);
|
|
5975
|
+
}
|
|
5976
|
+
const headers = { Authorization: `Bearer ${apiKey}` };
|
|
5977
|
+
let totalNodesAdded = 0;
|
|
5978
|
+
let totalEdgesAdded = 0;
|
|
5979
|
+
const errors = [];
|
|
5980
|
+
for (const boardId of boardIds) {
|
|
5981
|
+
try {
|
|
5982
|
+
const counts = await this.processBoard(store, baseUrl, boardId, headers);
|
|
5983
|
+
totalNodesAdded += counts.nodesAdded;
|
|
5984
|
+
totalEdgesAdded += counts.edgesAdded;
|
|
5985
|
+
} catch (err) {
|
|
5986
|
+
errors.push(
|
|
5987
|
+
`Miro API error for board ${boardId}: ${err instanceof Error ? err.message : String(err)}`
|
|
5988
|
+
);
|
|
5989
|
+
}
|
|
5990
|
+
}
|
|
5991
|
+
return buildIngestResult3(totalNodesAdded, totalEdgesAdded, errors, start);
|
|
5992
|
+
}
|
|
5993
|
+
async processBoard(store, baseUrl, boardId, headers) {
|
|
5994
|
+
let nodesAdded = 0;
|
|
5995
|
+
let edgesAdded = 0;
|
|
5996
|
+
const boardResponse = await this.httpClient(`${baseUrl}/v2/boards/${boardId}`, { headers });
|
|
5997
|
+
if (!boardResponse.ok) throw new Error(`Failed to fetch board ${boardId}`);
|
|
5998
|
+
const board = await boardResponse.json();
|
|
5999
|
+
const boardNodeId = `miro:board:${boardId}`;
|
|
6000
|
+
store.addNode({
|
|
6001
|
+
id: boardNodeId,
|
|
6002
|
+
type: "document",
|
|
6003
|
+
name: sanitizeExternalText(board.name, 500),
|
|
6004
|
+
content: sanitizeExternalText(board.description ?? "", 2e3),
|
|
6005
|
+
metadata: {
|
|
6006
|
+
source: "miro",
|
|
6007
|
+
boardId: board.id
|
|
6008
|
+
}
|
|
6009
|
+
});
|
|
6010
|
+
nodesAdded++;
|
|
6011
|
+
const itemsResponse = await this.httpClient(`${baseUrl}/v2/boards/${boardId}/items`, {
|
|
6012
|
+
headers
|
|
6013
|
+
});
|
|
6014
|
+
if (!itemsResponse.ok) throw new Error(`Failed to fetch items for board ${boardId}`);
|
|
6015
|
+
const itemsData = await itemsResponse.json();
|
|
6016
|
+
for (const item of itemsData.data) {
|
|
6017
|
+
const rawContent = item.data?.content ?? "";
|
|
6018
|
+
const plainContent = stripHtml(rawContent);
|
|
6019
|
+
if (plainContent.length < MIN_CONTENT_LENGTH) continue;
|
|
6020
|
+
if (!CONCEPT_ITEM_TYPES.has(item.type)) continue;
|
|
6021
|
+
const itemNodeId = `miro:item:${item.id}`;
|
|
6022
|
+
store.addNode({
|
|
6023
|
+
id: itemNodeId,
|
|
6024
|
+
type: "business_concept",
|
|
6025
|
+
name: sanitizeExternalText(plainContent, 500),
|
|
6026
|
+
content: sanitizeExternalText(plainContent, 2e3),
|
|
6027
|
+
metadata: {
|
|
6028
|
+
source: "miro",
|
|
6029
|
+
boardId: board.id,
|
|
6030
|
+
itemType: item.type
|
|
6031
|
+
}
|
|
6032
|
+
});
|
|
6033
|
+
nodesAdded++;
|
|
6034
|
+
store.addEdge({ from: boardNodeId, to: itemNodeId, type: "contains" });
|
|
6035
|
+
edgesAdded++;
|
|
6036
|
+
edgesAdded += linkToCode(store, plainContent, itemNodeId, "documents");
|
|
6037
|
+
}
|
|
6038
|
+
return { nodesAdded, edgesAdded };
|
|
6039
|
+
}
|
|
6040
|
+
};
|
|
6041
|
+
|
|
2475
6042
|
// src/search/FusionLayer.ts
|
|
2476
6043
|
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
2477
6044
|
"the",
|
|
@@ -2610,7 +6177,7 @@ var FusionLayer = class {
|
|
|
2610
6177
|
};
|
|
2611
6178
|
|
|
2612
6179
|
// src/entropy/GraphEntropyAdapter.ts
|
|
2613
|
-
var
|
|
6180
|
+
var CODE_NODE_TYPES5 = ["file", "function", "class", "method", "interface", "variable"];
|
|
2614
6181
|
var GraphEntropyAdapter = class {
|
|
2615
6182
|
constructor(store) {
|
|
2616
6183
|
this.store = store;
|
|
@@ -2690,7 +6257,7 @@ var GraphEntropyAdapter = class {
|
|
|
2690
6257
|
}
|
|
2691
6258
|
findEntryPoints() {
|
|
2692
6259
|
const entryPoints = [];
|
|
2693
|
-
for (const nodeType of
|
|
6260
|
+
for (const nodeType of CODE_NODE_TYPES5) {
|
|
2694
6261
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
2695
6262
|
for (const node of nodes) {
|
|
2696
6263
|
const isIndexFile = nodeType === "file" && node.name === "index.ts";
|
|
@@ -2726,7 +6293,7 @@ var GraphEntropyAdapter = class {
|
|
|
2726
6293
|
}
|
|
2727
6294
|
collectUnreachableNodes(visited) {
|
|
2728
6295
|
const unreachableNodes = [];
|
|
2729
|
-
for (const nodeType of
|
|
6296
|
+
for (const nodeType of CODE_NODE_TYPES5) {
|
|
2730
6297
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
2731
6298
|
for (const node of nodes) {
|
|
2732
6299
|
if (!visited.has(node.id)) {
|
|
@@ -3572,9 +7139,9 @@ var EntityExtractor = class {
|
|
|
3572
7139
|
extractPaths(trimmed, add) {
|
|
3573
7140
|
const consumed = /* @__PURE__ */ new Set();
|
|
3574
7141
|
for (const match of trimmed.matchAll(FILE_PATH_RE)) {
|
|
3575
|
-
const
|
|
3576
|
-
add(
|
|
3577
|
-
consumed.add(
|
|
7142
|
+
const path13 = match[0];
|
|
7143
|
+
add(path13);
|
|
7144
|
+
consumed.add(path13);
|
|
3578
7145
|
}
|
|
3579
7146
|
return consumed;
|
|
3580
7147
|
}
|
|
@@ -3646,8 +7213,8 @@ var EntityResolver = class {
|
|
|
3646
7213
|
if (isPathLike && node.path.includes(raw)) {
|
|
3647
7214
|
return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
|
|
3648
7215
|
}
|
|
3649
|
-
const
|
|
3650
|
-
if (
|
|
7216
|
+
const basename7 = node.path.split("/").pop() ?? "";
|
|
7217
|
+
if (basename7.includes(raw)) {
|
|
3651
7218
|
return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
|
|
3652
7219
|
}
|
|
3653
7220
|
if (raw.length >= 4 && node.path.includes(raw)) {
|
|
@@ -3726,13 +7293,13 @@ var ResponseFormatter = class {
|
|
|
3726
7293
|
const context = Array.isArray(d?.context) ? d.context : [];
|
|
3727
7294
|
const firstEntity = entities[0];
|
|
3728
7295
|
const nodeType = firstEntity?.node.type ?? "node";
|
|
3729
|
-
const
|
|
7296
|
+
const path13 = firstEntity?.node.path ?? "unknown";
|
|
3730
7297
|
let neighborCount = 0;
|
|
3731
7298
|
const firstContext = context[0];
|
|
3732
7299
|
if (firstContext && Array.isArray(firstContext.nodes)) {
|
|
3733
7300
|
neighborCount = firstContext.nodes.length;
|
|
3734
7301
|
}
|
|
3735
|
-
return `**${entityName}** is a ${nodeType} at \`${
|
|
7302
|
+
return `**${entityName}** is a ${nodeType} at \`${path13}\`. Connected to ${neighborCount} nodes.`;
|
|
3736
7303
|
}
|
|
3737
7304
|
formatAnomaly(data) {
|
|
3738
7305
|
const d = data;
|
|
@@ -4115,7 +7682,7 @@ var PHASE_NODE_TYPES = {
|
|
|
4115
7682
|
debug: ["failure", "learning", "function", "method"],
|
|
4116
7683
|
plan: ["adr", "document", "module", "layer"]
|
|
4117
7684
|
};
|
|
4118
|
-
var
|
|
7685
|
+
var CODE_NODE_TYPES6 = /* @__PURE__ */ new Set([
|
|
4119
7686
|
"file",
|
|
4120
7687
|
"function",
|
|
4121
7688
|
"class",
|
|
@@ -4334,7 +7901,7 @@ var Assembler = class {
|
|
|
4334
7901
|
*/
|
|
4335
7902
|
checkCoverage() {
|
|
4336
7903
|
const codeNodes = [];
|
|
4337
|
-
for (const type of
|
|
7904
|
+
for (const type of CODE_NODE_TYPES6) {
|
|
4338
7905
|
codeNodes.push(...this.store.findNodes({ type }));
|
|
4339
7906
|
}
|
|
4340
7907
|
const documented = [];
|
|
@@ -4514,14 +8081,14 @@ var GraphConstraintAdapter = class {
|
|
|
4514
8081
|
};
|
|
4515
8082
|
|
|
4516
8083
|
// src/ingest/DesignIngestor.ts
|
|
4517
|
-
var
|
|
4518
|
-
var
|
|
8084
|
+
var fs11 = __toESM(require("fs/promises"));
|
|
8085
|
+
var path12 = __toESM(require("path"));
|
|
4519
8086
|
function isDTCGToken(obj) {
|
|
4520
8087
|
return typeof obj === "object" && obj !== null && "$value" in obj && "$type" in obj;
|
|
4521
8088
|
}
|
|
4522
8089
|
async function readFileOrNull(filePath) {
|
|
4523
8090
|
try {
|
|
4524
|
-
return await
|
|
8091
|
+
return await fs11.readFile(filePath, "utf-8");
|
|
4525
8092
|
} catch {
|
|
4526
8093
|
return null;
|
|
4527
8094
|
}
|
|
@@ -4667,8 +8234,8 @@ var DesignIngestor = class {
|
|
|
4667
8234
|
async ingestAll(designDir) {
|
|
4668
8235
|
const start = Date.now();
|
|
4669
8236
|
const [tokensResult, intentResult] = await Promise.all([
|
|
4670
|
-
this.ingestTokens(
|
|
4671
|
-
this.ingestDesignIntent(
|
|
8237
|
+
this.ingestTokens(path12.join(designDir, "tokens.json")),
|
|
8238
|
+
this.ingestDesignIntent(path12.join(designDir, "DESIGN.md"))
|
|
4672
8239
|
]);
|
|
4673
8240
|
const merged = mergeResults(tokensResult, intentResult);
|
|
4674
8241
|
return { ...merged, durationMs: Date.now() - start };
|
|
@@ -4904,10 +8471,10 @@ var TaskIndependenceAnalyzer = class {
|
|
|
4904
8471
|
includeTypes: ["file"]
|
|
4905
8472
|
});
|
|
4906
8473
|
for (const n of queryResult.nodes) {
|
|
4907
|
-
const
|
|
4908
|
-
if (!fileSet.has(
|
|
4909
|
-
if (!result.has(
|
|
4910
|
-
result.set(
|
|
8474
|
+
const path13 = n.path ?? n.id.replace(/^file:/, "");
|
|
8475
|
+
if (!fileSet.has(path13)) {
|
|
8476
|
+
if (!result.has(path13)) {
|
|
8477
|
+
result.set(path13, file);
|
|
4911
8478
|
}
|
|
4912
8479
|
}
|
|
4913
8480
|
}
|
|
@@ -5287,7 +8854,10 @@ var ConflictPredictor = class {
|
|
|
5287
8854
|
var VERSION = "0.4.3";
|
|
5288
8855
|
// Annotate the CommonJS export names for ESM import in node:
|
|
5289
8856
|
0 && (module.exports = {
|
|
8857
|
+
ALL_EXTRACTORS,
|
|
8858
|
+
ApiPathExtractor,
|
|
5290
8859
|
Assembler,
|
|
8860
|
+
BusinessKnowledgeIngestor,
|
|
5291
8861
|
CIConnector,
|
|
5292
8862
|
CURRENT_SCHEMA_VERSION,
|
|
5293
8863
|
CascadeSimulator,
|
|
@@ -5296,11 +8866,18 @@ var VERSION = "0.4.3";
|
|
|
5296
8866
|
ConflictPredictor,
|
|
5297
8867
|
ConfluenceConnector,
|
|
5298
8868
|
ContextQL,
|
|
8869
|
+
ContradictionDetector,
|
|
8870
|
+
CoverageScorer,
|
|
8871
|
+
D2Parser,
|
|
5299
8872
|
DesignConstraintAdapter,
|
|
5300
8873
|
DesignIngestor,
|
|
8874
|
+
DiagramParser,
|
|
5301
8875
|
EDGE_TYPES,
|
|
5302
8876
|
EntityExtractor,
|
|
5303
8877
|
EntityResolver,
|
|
8878
|
+
EnumConstantExtractor,
|
|
8879
|
+
ExtractionRunner,
|
|
8880
|
+
FigmaConnector,
|
|
5304
8881
|
FusionLayer,
|
|
5305
8882
|
GitIngestor,
|
|
5306
8883
|
GraphAnomalyAdapter,
|
|
@@ -5313,23 +8890,34 @@ var VERSION = "0.4.3";
|
|
|
5313
8890
|
GraphNodeSchema,
|
|
5314
8891
|
GraphStore,
|
|
5315
8892
|
INTENTS,
|
|
8893
|
+
ImageAnalysisExtractor,
|
|
5316
8894
|
IntentClassifier,
|
|
5317
8895
|
JiraConnector,
|
|
5318
8896
|
KnowledgeIngestor,
|
|
8897
|
+
KnowledgePipelineRunner,
|
|
8898
|
+
KnowledgeStagingAggregator,
|
|
8899
|
+
MermaidParser,
|
|
8900
|
+
MiroConnector,
|
|
5319
8901
|
NODE_STABILITY,
|
|
5320
8902
|
NODE_TYPES,
|
|
5321
8903
|
OBSERVABILITY_TYPES,
|
|
5322
8904
|
PackedSummaryCache,
|
|
8905
|
+
PlantUmlParser,
|
|
5323
8906
|
RequirementIngestor,
|
|
5324
8907
|
ResponseFormatter,
|
|
5325
8908
|
SlackConnector,
|
|
8909
|
+
StructuralDriftDetector,
|
|
5326
8910
|
SyncManager,
|
|
5327
8911
|
TaskIndependenceAnalyzer,
|
|
8912
|
+
TestDescriptionExtractor,
|
|
5328
8913
|
TopologicalLinker,
|
|
5329
8914
|
VERSION,
|
|
8915
|
+
ValidationRuleExtractor,
|
|
5330
8916
|
VectorStore,
|
|
5331
8917
|
askGraph,
|
|
5332
8918
|
classifyNodeCategory,
|
|
8919
|
+
createExtractionRunner,
|
|
8920
|
+
detectLanguage,
|
|
5333
8921
|
groupNodesByImpact,
|
|
5334
8922
|
linkToCode,
|
|
5335
8923
|
loadGraph,
|