@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/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
- (0, import_promises.writeFile)((0, import_node_path.join)(dirPath, "graph.json"), JSON.stringify(graphData, null, 2), "utf-8"),
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()).map((n) => ({ ...n }));
404
- const allEdges = Array.from(this.edgeMap.values()).map((e) => ({ ...e }));
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, fileContents);
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 callsEdges = this.extractCallsEdges(nameToFiles, fileContents);
804
- for (const edge of callsEdges) {
805
- this.store.addEdge(edge);
806
- edgesAdded++;
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, fileContents) {
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() && /\.(ts|tsx|js|jsx)$/.test(entry.name) && !entry.name.endsWith(".d.ts")) {
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
- if (this.tryExtractFunction(line, lines, i, fileId, relativePath, ctx, results)) continue;
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
- tryExtractFunction(line, lines, i, fileId, relativePath, ctx, results) {
890
- const fnMatch = line.match(/(?:export\s+)?(?:async\s+)?function\s+(\w+)/);
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: line.includes("export"),
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
- tryExtractClass(line, lines, i, fileId, relativePath, ctx, results) {
919
- const classMatch = line.match(/(?:export\s+)?class\s+(\w+)/);
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: line.includes("export") }
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
- tryExtractInterface(line, lines, i, fileId, relativePath, ctx, results) {
942
- const ifaceMatch = line.match(/(?:export\s+)?interface\s+(\w+)/);
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: line.includes("export") }
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
- tryExtractMethod(line, lines, i, fileId, relativePath, ctx, results) {
976
- if (!ctx.insideClass || !ctx.className || !ctx.classId) return false;
977
- const methodMatch = line.match(
978
- /^\s+(?:(?:public|private|protected|readonly|static|abstract)\s+)*(?:async\s+)?(\w+)\s*\(/
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
- const methodName = methodMatch[1];
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}:${ctx.className}.${methodName}`;
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: ctx.className,
994
- exported: false,
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: ctx.classId, to: id, type: "contains" }
1183
+ edge: { from: classId, to: id, type: "contains" }
1002
1184
  });
1003
1185
  return true;
1004
1186
  }
1005
- tryExtractVariable(line, i, fileId, relativePath, results) {
1006
- const varMatch = line.match(/(?:export\s+)?(?:const|let|var)\s+(\w+)/);
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: line.includes("export") }
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
- * Second pass: scan each file for identifiers matching known callable names,
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
- extractCallsEdges(nameToFiles, fileContents) {
1271
+ extractCallsEdgesForFile(relativePath, content, nameToFiles) {
1050
1272
  const edges = [];
1051
1273
  const seen = /* @__PURE__ */ new Set();
1052
- for (const [filePath, content] of fileContents) {
1053
- const callerFileId = `file:${filePath}`;
1054
- const callPattern = /\b([a-zA-Z_$][\w$]*)\s*\(/g;
1055
- let match;
1056
- while ((match = callPattern.exec(content)) !== null) {
1057
- const name = match[1];
1058
- const targetFiles = nameToFiles.get(name);
1059
- if (!targetFiles) continue;
1060
- for (const targetFile of targetFiles) {
1061
- if (targetFile === filePath) continue;
1062
- const targetFileId = `file:${targetFile}`;
1063
- const key = `${callerFileId}|${targetFileId}`;
1064
- if (seen.has(key)) continue;
1065
- seen.add(key);
1066
- edges.push({
1067
- from: callerFileId,
1068
- to: targetFileId,
1069
- type: "calls",
1070
- metadata: { confidence: "regex" }
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 importRegex = /import\s+(?:type\s+)?(?:\{[^}]*\}|[\w*]+)\s+from\s+['"]([^'"]+)['"]/g;
1080
- let match;
1081
- while ((match = importRegex.exec(content)) !== null) {
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
- for (const ext of extensions) {
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
- extractReqAnnotations(fileContents, rootDir) {
1173
- const REQ_TAG = /\/\/\s*@req\s+([\w-]+)#(\d+)/g;
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
- for (const [filePath, content] of fileContents) {
1177
- let match;
1178
- REQ_TAG.lastIndex = 0;
1179
- while ((match = REQ_TAG.exec(content)) !== null) {
1180
- const featureName = match[1];
1181
- const reqIndex = parseInt(match[2], 10);
1182
- const reqNode = reqNodes.find(
1183
- (n) => n.metadata.featureName === featureName && n.metadata.index === reqIndex
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
- if (!reqNode) {
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/RequirementIngestor.ts
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 CODE_NODE_TYPES2 = ["file", "function", "class", "method", "interface", "variable"];
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 fs3.readdir(specsDir, { withFileTypes: true });
1725
- featureDirs = entries.filter((e) => e.isDirectory()).map((e) => path4.join(specsDir, e.name));
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 = path4.basename(featureDir);
1745
- const specPath = path4.join(featureDir, "proposal.md").replaceAll("\\", "/");
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 fs3.readFile(specPath, "utf-8");
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: path4.basename(specPath),
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/") && path4.basename(normalizedPath).includes(featureName);
2318
+ const isCodeMatch = normalizedPath.includes("packages/") && path5.basename(normalizedPath).includes(featureName);
1881
2319
  const isTestMatch = normalizedPath.includes("/tests/") && // platform-safe
1882
- path4.basename(normalizedPath).includes(featureName);
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 CODE_NODE_TYPES2) {
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 CODE_NODE_TYPES3 = ["file", "function", "class", "method", "interface", "variable"];
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 CODE_NODE_TYPES3) {
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 fs4 = __toESM(require("fs/promises"));
1991
- var path5 = __toESM(require("path"));
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 = path5.join(graphDir, "sync-metadata.json");
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 fs4.readFile(this.metadataPath, "utf-8");
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 fs4.mkdir(path5.dirname(this.metadataPath), { recursive: true });
2058
- await fs4.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
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
- metadata: {
2159
- key: issue.key,
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
- const sanitizedText = sanitizeExternalText(message.text);
2250
- const snippet = sanitizedText.length > 100 ? sanitizedText.slice(0, 100) : sanitizedText;
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
- metadata: {
2256
- author: message.user,
2257
- channel,
2258
- timestamp: message.ts
2259
- }
5481
+ content: condensed.content,
5482
+ metadata
2260
5483
  });
2261
5484
  nodesAdded++;
2262
- edgesAdded += linkToCode(store, sanitizedText, nodeId, "references", {
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(store, baseUrl, apiKey, spaceKey, errors);
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
- metadata: {
2349
- source: "confluence",
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
- const edgesAdded = linkToCode(store, text, nodeId, "documents");
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 CODE_NODE_TYPES4 = ["file", "function", "class", "method", "interface", "variable"];
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 CODE_NODE_TYPES4) {
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 CODE_NODE_TYPES4) {
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 path7 = match[0];
3576
- add(path7);
3577
- consumed.add(path7);
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 basename5 = node.path.split("/").pop() ?? "";
3650
- if (basename5.includes(raw)) {
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 path7 = firstEntity?.node.path ?? "unknown";
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 \`${path7}\`. Connected to ${neighborCount} nodes.`;
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 CODE_NODE_TYPES5 = /* @__PURE__ */ new Set([
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 CODE_NODE_TYPES5) {
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 fs5 = __toESM(require("fs/promises"));
4518
- var path6 = __toESM(require("path"));
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 fs5.readFile(filePath, "utf-8");
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(path6.join(designDir, "tokens.json")),
4671
- this.ingestDesignIntent(path6.join(designDir, "DESIGN.md"))
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 path7 = n.path ?? n.id.replace(/^file:/, "");
4908
- if (!fileSet.has(path7)) {
4909
- if (!result.has(path7)) {
4910
- result.set(path7, file);
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,