@harness-engineering/graph 0.4.2 → 0.5.0

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