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