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