@harness-engineering/graph 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -45,6 +45,9 @@ __export(index_exports, {
45
45
  ContradictionDetector: () => ContradictionDetector,
46
46
  CoverageScorer: () => CoverageScorer,
47
47
  D2Parser: () => D2Parser,
48
+ DEFAULT_BLOCKLIST: () => DEFAULT_BLOCKLIST,
49
+ DEFAULT_PATTERNS: () => DEFAULT_PATTERNS,
50
+ DecisionIngestor: () => DecisionIngestor,
48
51
  DesignConstraintAdapter: () => DesignConstraintAdapter,
49
52
  DesignIngestor: () => DesignIngestor,
50
53
  DiagramParser: () => DiagramParser,
@@ -69,6 +72,7 @@ __export(index_exports, {
69
72
  ImageAnalysisExtractor: () => ImageAnalysisExtractor,
70
73
  IntentClassifier: () => IntentClassifier,
71
74
  JiraConnector: () => JiraConnector,
75
+ KnowledgeDocMaterializer: () => KnowledgeDocMaterializer,
72
76
  KnowledgeIngestor: () => KnowledgeIngestor,
73
77
  KnowledgePipelineRunner: () => KnowledgePipelineRunner,
74
78
  KnowledgeStagingAggregator: () => KnowledgeStagingAggregator,
@@ -95,6 +99,7 @@ __export(index_exports, {
95
99
  createExtractionRunner: () => createExtractionRunner,
96
100
  detectLanguage: () => detectLanguage,
97
101
  groupNodesByImpact: () => groupNodesByImpact,
102
+ inferDomain: () => inferDomain,
98
103
  linkToCode: () => linkToCode,
99
104
  loadGraph: () => loadGraph,
100
105
  normalizeIntent: () => normalizeIntent,
@@ -130,7 +135,7 @@ var NODE_TYPES = [
130
135
  "build",
131
136
  "test_result",
132
137
  "execution_outcome",
133
- // Observability (future)
138
+ // Observability — reserved for future tracing/metrics integration
134
139
  "span",
135
140
  "metric",
136
141
  "log",
@@ -177,7 +182,7 @@ var EDGE_TYPES = [
177
182
  "triggered_by",
178
183
  "failed_in",
179
184
  "outcome_of",
180
- // Execution relationships (future)
185
+ // Execution relationships — reserved for future observability integration
181
186
  "executed_by",
182
187
  "measured_by",
183
188
  // Design relationships
@@ -252,7 +257,7 @@ function streamGraphJson(filePath, nodes, edges) {
252
257
  stream.write(JSON.stringify(edges[i]));
253
258
  }
254
259
  stream.write("]}");
255
- stream.end(resolve);
260
+ stream.end(() => resolve());
256
261
  });
257
262
  }
258
263
  async function saveGraph(dirPath, nodes, edges) {
@@ -275,15 +280,19 @@ async function loadGraph(dirPath) {
275
280
  await (0, import_promises.access)(metaPath);
276
281
  await (0, import_promises.access)(graphPath);
277
282
  } catch {
278
- return null;
283
+ return { status: "not_found" };
279
284
  }
280
285
  const metaContent = await (0, import_promises.readFile)(metaPath, "utf-8");
281
286
  const metadata = JSON.parse(metaContent);
282
287
  if (metadata.schemaVersion !== CURRENT_SCHEMA_VERSION) {
283
- return null;
288
+ return {
289
+ status: "schema_mismatch",
290
+ found: metadata.schemaVersion,
291
+ expected: CURRENT_SCHEMA_VERSION
292
+ };
284
293
  }
285
294
  const graphContent = await (0, import_promises.readFile)(graphPath, "utf-8");
286
- return JSON.parse(graphContent);
295
+ return { status: "loaded", graph: JSON.parse(graphContent) };
287
296
  }
288
297
 
289
298
  // src/store/GraphStore.ts
@@ -458,8 +467,15 @@ var GraphStore = class {
458
467
  await saveGraph(dirPath, allNodes, allEdges);
459
468
  }
460
469
  async load(dirPath) {
461
- const data = await loadGraph(dirPath);
462
- if (!data) return false;
470
+ const result = await loadGraph(dirPath);
471
+ if (result.status === "not_found") return false;
472
+ if (result.status === "schema_mismatch") {
473
+ console.warn(
474
+ `[graph] Schema version mismatch: graph was saved with v${result.found} but current is v${result.expected}. Graph will be rebuilt from scratch. Run \`harness graph scan\` to repopulate.`
475
+ );
476
+ return false;
477
+ }
478
+ const data = result.graph;
463
479
  this.clear();
464
480
  for (const node of data.nodes) {
465
481
  this.nodeMap.set(node.id, { ...node });
@@ -903,9 +919,10 @@ var CodeIngestor = class {
903
919
  let edgesAdded = 0;
904
920
  const files = await this.findSourceFiles(rootDir);
905
921
  const nameToFiles = /* @__PURE__ */ new Map();
922
+ const contentCache = /* @__PURE__ */ new Map();
906
923
  for (const filePath of files) {
907
924
  try {
908
- const result = await this.processFile(filePath, rootDir, nameToFiles);
925
+ const result = await this.processFile(filePath, rootDir, nameToFiles, contentCache);
909
926
  nodesAdded += result.nodesAdded;
910
927
  edgesAdded += result.edgesAdded;
911
928
  } catch (err) {
@@ -915,7 +932,8 @@ var CodeIngestor = class {
915
932
  for (const filePath of files) {
916
933
  try {
917
934
  const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
918
- const content = await fs.readFile(filePath, "utf-8");
935
+ const content = contentCache.get(filePath);
936
+ if (content === void 0) continue;
919
937
  const callsEdges = this.extractCallsEdgesForFile(relativePath, content, nameToFiles);
920
938
  for (const edge of callsEdges) {
921
939
  this.store.addEdge(edge);
@@ -926,7 +944,8 @@ var CodeIngestor = class {
926
944
  }
927
945
  for (const filePath of files) {
928
946
  try {
929
- const content = await fs.readFile(filePath, "utf-8");
947
+ const content = contentCache.get(filePath);
948
+ if (content === void 0) continue;
930
949
  edgesAdded += this.extractReqAnnotationsForFile(filePath, content, rootDir);
931
950
  } catch {
932
951
  }
@@ -940,11 +959,12 @@ var CodeIngestor = class {
940
959
  durationMs: Date.now() - start
941
960
  };
942
961
  }
943
- async processFile(filePath, rootDir, nameToFiles) {
962
+ async processFile(filePath, rootDir, nameToFiles, contentCache) {
944
963
  let nodesAdded = 0;
945
964
  let edgesAdded = 0;
946
965
  const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
947
966
  const content = await fs.readFile(filePath, "utf-8");
967
+ contentCache.set(filePath, content);
948
968
  const stat2 = await fs.stat(filePath);
949
969
  const fileId = `file:${relativePath}`;
950
970
  const fileNode = {
@@ -2122,9 +2142,119 @@ function parseFrontmatter(raw) {
2122
2142
  };
2123
2143
  }
2124
2144
 
2125
- // src/ingest/RequirementIngestor.ts
2145
+ // src/ingest/DecisionIngestor.ts
2126
2146
  var fs4 = __toESM(require("fs/promises"));
2127
2147
  var path5 = __toESM(require("path"));
2148
+ var CODE_NODE_TYPES3 = [
2149
+ "file",
2150
+ "function",
2151
+ "class",
2152
+ "method",
2153
+ "interface",
2154
+ "variable"
2155
+ ];
2156
+ var DecisionIngestor = class {
2157
+ constructor(store) {
2158
+ this.store = store;
2159
+ }
2160
+ store;
2161
+ async ingest(decisionsDir) {
2162
+ const start = Date.now();
2163
+ const errors = [];
2164
+ let files;
2165
+ try {
2166
+ files = await this.findDecisionFiles(decisionsDir);
2167
+ } catch {
2168
+ return emptyResult(Date.now() - start);
2169
+ }
2170
+ let nodesAdded = 0;
2171
+ let edgesAdded = 0;
2172
+ for (const filePath of files) {
2173
+ try {
2174
+ const raw = await fs4.readFile(filePath, "utf-8");
2175
+ const parsed = this.parseFrontmatter(raw);
2176
+ if (!parsed) continue;
2177
+ const { frontmatter, body } = parsed;
2178
+ if (!frontmatter.number || !frontmatter.title) continue;
2179
+ const filename = path5.basename(filePath, ".md");
2180
+ const nodeId = `decision:${filename}`;
2181
+ const node = {
2182
+ id: nodeId,
2183
+ type: "decision",
2184
+ name: frontmatter.title,
2185
+ path: filePath,
2186
+ content: body.trim(),
2187
+ metadata: {
2188
+ number: frontmatter.number,
2189
+ ...frontmatter.date && { date: frontmatter.date },
2190
+ ...frontmatter.status && { status: frontmatter.status },
2191
+ ...frontmatter.tier && { tier: frontmatter.tier },
2192
+ ...frontmatter.source && { source: frontmatter.source },
2193
+ ...frontmatter.supersedes && { supersedes: frontmatter.supersedes }
2194
+ }
2195
+ };
2196
+ this.store.addNode(node);
2197
+ nodesAdded++;
2198
+ edgesAdded += this.linkToCode(body, nodeId);
2199
+ } catch (err) {
2200
+ errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
2201
+ }
2202
+ }
2203
+ return {
2204
+ nodesAdded,
2205
+ nodesUpdated: 0,
2206
+ edgesAdded,
2207
+ edgesUpdated: 0,
2208
+ errors,
2209
+ durationMs: Date.now() - start
2210
+ };
2211
+ }
2212
+ parseFrontmatter(raw) {
2213
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
2214
+ if (!match) return null;
2215
+ const yamlBlock = match[1];
2216
+ const body = match[2];
2217
+ const frontmatter = {};
2218
+ for (const line of yamlBlock.split("\n")) {
2219
+ const kvMatch = line.match(/^(\w+):\s*(.+)$/);
2220
+ if (!kvMatch) continue;
2221
+ frontmatter[kvMatch[1]] = kvMatch[2].trim();
2222
+ }
2223
+ if (!frontmatter.number || !frontmatter.title) return null;
2224
+ return {
2225
+ frontmatter,
2226
+ body
2227
+ };
2228
+ }
2229
+ linkToCode(content, sourceNodeId) {
2230
+ let count = 0;
2231
+ for (const nodeType of CODE_NODE_TYPES3) {
2232
+ const codeNodes = this.store.findNodes({ type: nodeType });
2233
+ for (const node of codeNodes) {
2234
+ if (node.name.length < 3) continue;
2235
+ const escaped = node.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2236
+ const namePattern = new RegExp(`\\b${escaped}\\b`, "i");
2237
+ if (namePattern.test(content)) {
2238
+ this.store.addEdge({
2239
+ from: sourceNodeId,
2240
+ to: node.id,
2241
+ type: "decided"
2242
+ });
2243
+ count++;
2244
+ }
2245
+ }
2246
+ }
2247
+ return count;
2248
+ }
2249
+ async findDecisionFiles(dir) {
2250
+ const entries = await fs4.readdir(dir, { withFileTypes: true });
2251
+ return entries.filter((e) => e.isFile() && e.name.endsWith(".md") && e.name !== "README.md").map((e) => path5.join(dir, e.name));
2252
+ }
2253
+ };
2254
+
2255
+ // src/ingest/RequirementIngestor.ts
2256
+ var fs5 = __toESM(require("fs/promises"));
2257
+ var path6 = __toESM(require("path"));
2128
2258
  var REQUIREMENT_SECTIONS = [
2129
2259
  "Observable Truths",
2130
2260
  "Success Criteria",
@@ -2141,7 +2271,7 @@ function detectEarsPattern(text) {
2141
2271
  if (/^the\s+\w+\s+shall\b/.test(lower)) return "ubiquitous";
2142
2272
  return void 0;
2143
2273
  }
2144
- var CODE_NODE_TYPES3 = ["file", "function", "class", "method", "interface", "variable"];
2274
+ var CODE_NODE_TYPES4 = ["file", "function", "class", "method", "interface", "variable"];
2145
2275
  var RequirementIngestor = class {
2146
2276
  constructor(store) {
2147
2277
  this.store = store;
@@ -2159,8 +2289,8 @@ var RequirementIngestor = class {
2159
2289
  let edgesAdded = 0;
2160
2290
  let featureDirs;
2161
2291
  try {
2162
- const entries = await fs4.readdir(specsDir, { withFileTypes: true });
2163
- featureDirs = entries.filter((e) => e.isDirectory()).map((e) => path5.join(specsDir, e.name));
2292
+ const entries = await fs5.readdir(specsDir, { withFileTypes: true });
2293
+ featureDirs = entries.filter((e) => e.isDirectory()).map((e) => path6.join(specsDir, e.name));
2164
2294
  } catch {
2165
2295
  return emptyResult(Date.now() - start);
2166
2296
  }
@@ -2179,11 +2309,11 @@ var RequirementIngestor = class {
2179
2309
  };
2180
2310
  }
2181
2311
  async ingestFeatureDir(featureDir, errors) {
2182
- const featureName = path5.basename(featureDir);
2183
- const specPath = path5.join(featureDir, "proposal.md").replaceAll("\\", "/");
2312
+ const featureName = path6.basename(featureDir);
2313
+ const specPath = path6.join(featureDir, "proposal.md").replaceAll("\\", "/");
2184
2314
  let content;
2185
2315
  try {
2186
- content = await fs4.readFile(specPath, "utf-8");
2316
+ content = await fs5.readFile(specPath, "utf-8");
2187
2317
  } catch {
2188
2318
  return { nodesAdded: 0, edgesAdded: 0 };
2189
2319
  }
@@ -2200,7 +2330,7 @@ var RequirementIngestor = class {
2200
2330
  this.store.addNode({
2201
2331
  id: specNodeId,
2202
2332
  type: "document",
2203
- name: path5.basename(specPath),
2333
+ name: path6.basename(specPath),
2204
2334
  path: specPath,
2205
2335
  metadata: { featureName }
2206
2336
  });
@@ -2315,9 +2445,9 @@ var RequirementIngestor = class {
2315
2445
  for (const node of fileNodes) {
2316
2446
  if (!node.path) continue;
2317
2447
  const normalizedPath = node.path.replace(/\\/g, "/");
2318
- const isCodeMatch = normalizedPath.includes("packages/") && path5.basename(normalizedPath).includes(featureName);
2448
+ const isCodeMatch = normalizedPath.includes("packages/") && path6.basename(normalizedPath).includes(featureName);
2319
2449
  const isTestMatch = normalizedPath.includes("/tests/") && // platform-safe
2320
- path5.basename(normalizedPath).includes(featureName);
2450
+ path6.basename(normalizedPath).includes(featureName);
2321
2451
  if (isCodeMatch && !isTestMatch) {
2322
2452
  this.store.addEdge({
2323
2453
  from: reqId,
@@ -2346,7 +2476,7 @@ var RequirementIngestor = class {
2346
2476
  */
2347
2477
  linkByKeywordOverlap(reqId, reqText) {
2348
2478
  let count = 0;
2349
- for (const nodeType of CODE_NODE_TYPES3) {
2479
+ for (const nodeType of CODE_NODE_TYPES4) {
2350
2480
  const codeNodes = this.store.findNodes({ type: nodeType });
2351
2481
  for (const node of codeNodes) {
2352
2482
  if (node.name.length < 3) continue;
@@ -2369,20 +2499,241 @@ var RequirementIngestor = class {
2369
2499
  }
2370
2500
  };
2371
2501
 
2502
+ // src/ingest/domain-inference.ts
2503
+ var DEFAULT_PATTERNS = [
2504
+ "packages/<dir>",
2505
+ "apps/<dir>",
2506
+ "services/<dir>",
2507
+ "src/<dir>",
2508
+ "lib/<dir>"
2509
+ ];
2510
+ var DEFAULT_BLOCKLIST = /* @__PURE__ */ new Set([
2511
+ "node_modules",
2512
+ ".harness",
2513
+ "dist",
2514
+ "build",
2515
+ ".git",
2516
+ "coverage",
2517
+ ".next",
2518
+ ".turbo",
2519
+ ".cache",
2520
+ "out",
2521
+ "tmp"
2522
+ ]);
2523
+ var CODE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
2524
+ function matchPattern(filePath, pattern) {
2525
+ const patternParts = pattern.split("/").filter((s) => s.length > 0);
2526
+ if (patternParts.length !== 2 || patternParts[1] !== "<dir>") {
2527
+ return null;
2528
+ }
2529
+ const prefix = patternParts[0];
2530
+ const pathParts = filePath.split("/").filter((s) => s.length > 0);
2531
+ if (pathParts.length < 2) return null;
2532
+ if (pathParts[0] !== prefix) return null;
2533
+ let dir = pathParts[1];
2534
+ if (dir.length === 0) return null;
2535
+ if (CODE_EXTENSIONS.some((ext) => dir.endsWith(ext))) {
2536
+ const dotIdx = dir.lastIndexOf(".");
2537
+ if (dotIdx > 0) dir = dir.slice(0, dotIdx);
2538
+ }
2539
+ if (dir.length === 0) return null;
2540
+ return dir;
2541
+ }
2542
+ function inferDomain(node, options = {}) {
2543
+ if (node.metadata?.domain && typeof node.metadata.domain === "string" && node.metadata.domain.length > 0) {
2544
+ return node.metadata.domain;
2545
+ }
2546
+ const filePath = typeof node.path === "string" ? node.path : "";
2547
+ const blocklist = new Set(DEFAULT_BLOCKLIST);
2548
+ if (options.extraBlocklist) {
2549
+ for (const seg of options.extraBlocklist) {
2550
+ if (seg && seg.length > 0) blocklist.add(seg);
2551
+ }
2552
+ }
2553
+ if (filePath.length > 0) {
2554
+ const extraPatterns = options.extraPatterns ?? [];
2555
+ for (const pattern of extraPatterns) {
2556
+ const dir = matchPattern(filePath, pattern);
2557
+ if (dir !== null) {
2558
+ if (blocklist.has(dir)) return "unknown";
2559
+ return dir;
2560
+ }
2561
+ }
2562
+ for (const pattern of DEFAULT_PATTERNS) {
2563
+ const dir = matchPattern(filePath, pattern);
2564
+ if (dir !== null) {
2565
+ if (blocklist.has(dir)) return "unknown";
2566
+ return dir;
2567
+ }
2568
+ }
2569
+ const segments = filePath.split("/").filter((s) => s.length > 0);
2570
+ if (segments.length > 0) {
2571
+ const first = segments[0];
2572
+ if (!blocklist.has(first)) return first;
2573
+ }
2574
+ }
2575
+ const source = node.metadata?.source;
2576
+ if (source === "knowledge-linker" || source === "connector") {
2577
+ const connector = node.metadata?.connectorName;
2578
+ if (typeof connector === "string" && connector.length > 0) return connector;
2579
+ return "general";
2580
+ }
2581
+ return "unknown";
2582
+ }
2583
+
2372
2584
  // src/ingest/KnowledgePipelineRunner.ts
2373
- var fs9 = __toESM(require("fs/promises"));
2374
- var path10 = __toESM(require("path"));
2585
+ var fs11 = __toESM(require("fs/promises"));
2586
+ var path12 = __toESM(require("path"));
2375
2587
 
2376
2588
  // src/ingest/DiagramParser.ts
2377
- var fs5 = __toESM(require("fs/promises"));
2378
- var path6 = __toESM(require("path"));
2379
- function emptyMermaidResult(diagramType = "unknown") {
2589
+ var fs6 = __toESM(require("fs/promises"));
2590
+ var path7 = __toESM(require("path"));
2591
+
2592
+ // src/ingest/parsers/mermaid.ts
2593
+ function emptyResult2(diagramType = "unknown") {
2380
2594
  return {
2381
2595
  entities: [],
2382
2596
  relationships: [],
2383
2597
  metadata: { format: "mermaid", diagramType }
2384
2598
  };
2385
2599
  }
2600
+ function detectDiagramType(content) {
2601
+ for (const line of content.split("\n")) {
2602
+ const trimmed = line.trim();
2603
+ if (!trimmed) continue;
2604
+ if (/^(?:graph|flowchart)\b/i.test(trimmed)) return "flowchart";
2605
+ if (/^sequenceDiagram\b/.test(trimmed)) return "sequence";
2606
+ if (/^classDiagram\b/.test(trimmed)) return "class";
2607
+ if (/^erDiagram\b/.test(trimmed)) return "er";
2608
+ break;
2609
+ }
2610
+ return "unknown";
2611
+ }
2612
+ function isDecisionNode(content, nodeId) {
2613
+ const decisionRegex = new RegExp(`${nodeId}\\s*\\{`);
2614
+ return decisionRegex.test(content);
2615
+ }
2616
+ function buildFlowchartEntity(content, match) {
2617
+ const id = match[1] ?? "";
2618
+ const label = (match[2] ?? "").trim();
2619
+ if (!id) return null;
2620
+ const decision = isDecisionNode(content, id);
2621
+ const entity = {
2622
+ id,
2623
+ label,
2624
+ ...decision ? { type: "decision" } : {}
2625
+ };
2626
+ return { id, entity };
2627
+ }
2628
+ function extractFlowchartNodes(content) {
2629
+ const entities = /* @__PURE__ */ new Map();
2630
+ const nodeRegex = /([A-Za-z0-9_]+)\s*[[({]([^\])}]+)[\])}]/g;
2631
+ let match = nodeRegex.exec(content);
2632
+ while (match !== null) {
2633
+ const parsed = buildFlowchartEntity(content, match);
2634
+ if (parsed && !entities.has(parsed.id)) {
2635
+ entities.set(parsed.id, parsed.entity);
2636
+ }
2637
+ match = nodeRegex.exec(content);
2638
+ }
2639
+ return entities;
2640
+ }
2641
+ function parseLabeledEdgeMatch(match) {
2642
+ const from = match[1] ?? "";
2643
+ const label = (match[2] ?? "").trim();
2644
+ const to = match[3] ?? "";
2645
+ if (!from || !to) return null;
2646
+ return { key: `${from}->${to}:${label}`, rel: { from, to, label } };
2647
+ }
2648
+ function extractLabeledEdges(stripped, edgeKeys) {
2649
+ const relationships = [];
2650
+ const labeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\|([^|]+)\|\s*([A-Za-z0-9_]+)/g;
2651
+ let match = labeledEdgeRegex.exec(stripped);
2652
+ while (match !== null) {
2653
+ const parsed = parseLabeledEdgeMatch(match);
2654
+ if (parsed && !edgeKeys.has(parsed.key)) {
2655
+ edgeKeys.add(parsed.key);
2656
+ relationships.push(parsed.rel);
2657
+ }
2658
+ match = labeledEdgeRegex.exec(stripped);
2659
+ }
2660
+ return relationships;
2661
+ }
2662
+ function hasLabeledEdgeBetween(labeledEdges, from, to) {
2663
+ return labeledEdges.some((r) => r.from === from && r.to === to);
2664
+ }
2665
+ function extractUnlabeledEdges(stripped, edgeKeys, labeledEdges) {
2666
+ const relationships = [];
2667
+ const unlabeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\s+([A-Za-z0-9_]+)/g;
2668
+ let match = unlabeledEdgeRegex.exec(stripped);
2669
+ while (match !== null) {
2670
+ const from = match[1] ?? "";
2671
+ const to = match[2] ?? "";
2672
+ const key = `${from}->${to}`;
2673
+ if (from && to && !hasLabeledEdgeBetween(labeledEdges, from, to) && !edgeKeys.has(key)) {
2674
+ edgeKeys.add(key);
2675
+ relationships.push({ from, to });
2676
+ }
2677
+ match = unlabeledEdgeRegex.exec(stripped);
2678
+ }
2679
+ return relationships;
2680
+ }
2681
+ function parseFlowchart(content, diagramType) {
2682
+ const entities = extractFlowchartNodes(content);
2683
+ const stripped = content.replace(/[[({][^\])}]*[\])}]/g, "");
2684
+ const edgeKeys = /* @__PURE__ */ new Set();
2685
+ const labeledEdges = extractLabeledEdges(stripped, edgeKeys);
2686
+ const unlabeledEdges = extractUnlabeledEdges(stripped, edgeKeys, labeledEdges);
2687
+ return {
2688
+ entities: Array.from(entities.values()),
2689
+ relationships: [...labeledEdges, ...unlabeledEdges],
2690
+ metadata: { format: "mermaid", diagramType }
2691
+ };
2692
+ }
2693
+ function extractParticipants(content) {
2694
+ const entities = [];
2695
+ const seenParticipants = /* @__PURE__ */ new Set();
2696
+ const participantRegex = /participant\s+(\w+)(?:\s+as\s+(.+))?/g;
2697
+ let match = participantRegex.exec(content);
2698
+ while (match !== null) {
2699
+ const id = match[1] ?? "";
2700
+ const label = match[2]?.trim() || id;
2701
+ if (id && !seenParticipants.has(id)) {
2702
+ seenParticipants.add(id);
2703
+ entities.push({ id, label });
2704
+ }
2705
+ match = participantRegex.exec(content);
2706
+ }
2707
+ return entities;
2708
+ }
2709
+ function relationshipFromMatch(match) {
2710
+ const from = match[1] ?? "";
2711
+ const to = match[2] ?? "";
2712
+ const label = (match[3] ?? "").trim();
2713
+ return from && to ? { from, to, label } : null;
2714
+ }
2715
+ function collectMessageMatches(content, regex) {
2716
+ const results = [];
2717
+ let match = regex.exec(content);
2718
+ while (match !== null) {
2719
+ const rel = relationshipFromMatch(match);
2720
+ if (rel) results.push(rel);
2721
+ match = regex.exec(content);
2722
+ }
2723
+ return results;
2724
+ }
2725
+ function extractMessages(content) {
2726
+ const forward = collectMessageMatches(content, /(\w+)\s*->>?\+?\s*(\w+)\s*:\s*(.+)/g);
2727
+ const returns = collectMessageMatches(content, /(\w+)\s*-->>?-?\s*(\w+)\s*:\s*(.+)/g);
2728
+ return [...forward, ...returns];
2729
+ }
2730
+ function parseSequence(content, diagramType) {
2731
+ return {
2732
+ entities: extractParticipants(content),
2733
+ relationships: extractMessages(content),
2734
+ metadata: { format: "mermaid", diagramType }
2735
+ };
2736
+ }
2386
2737
  var MermaidParser = class {
2387
2738
  canParse(_content, ext) {
2388
2739
  return ext === ".mmd" || ext === ".mermaid";
@@ -2390,269 +2741,199 @@ var MermaidParser = class {
2390
2741
  parse(content, _filePath) {
2391
2742
  const trimmed = content.trim();
2392
2743
  if (!trimmed) {
2393
- return emptyMermaidResult();
2744
+ return emptyResult2();
2394
2745
  }
2395
- const diagramType = this.detectDiagramType(trimmed);
2746
+ const diagramType = detectDiagramType(trimmed);
2396
2747
  switch (diagramType) {
2397
2748
  case "flowchart":
2398
- return this.parseFlowchart(trimmed, diagramType);
2749
+ return parseFlowchart(trimmed, diagramType);
2399
2750
  case "sequence":
2400
- return this.parseSequence(trimmed, diagramType);
2751
+ return parseSequence(trimmed, diagramType);
2401
2752
  default:
2402
- return emptyMermaidResult(diagramType);
2753
+ return emptyResult2(diagramType);
2403
2754
  }
2404
2755
  }
2405
- detectDiagramType(content) {
2406
- const lines = content.split("\n");
2407
- for (const line of lines) {
2408
- const trimmedLine = line.trim();
2409
- if (!trimmedLine) continue;
2410
- if (/^(?:graph|flowchart)\b/i.test(trimmedLine)) return "flowchart";
2411
- if (/^sequenceDiagram\b/.test(trimmedLine)) return "sequence";
2412
- if (/^classDiagram\b/.test(trimmedLine)) return "class";
2413
- if (/^erDiagram\b/.test(trimmedLine)) return "er";
2414
- break;
2415
- }
2416
- return "unknown";
2756
+ };
2757
+
2758
+ // src/ingest/parsers/d2.ts
2759
+ function emptyResult3() {
2760
+ return {
2761
+ entities: [],
2762
+ relationships: [],
2763
+ metadata: { format: "d2", diagramType: "architecture" }
2764
+ };
2765
+ }
2766
+ function parseBlockShape(stripped) {
2767
+ const match = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+?)\s*\{$/);
2768
+ if (match) {
2769
+ const id = match[1];
2770
+ const label = match[2];
2771
+ if (id && label) return { id, label };
2772
+ }
2773
+ return null;
2774
+ }
2775
+ function parseConnection(stripped) {
2776
+ const match = stripped.match(/^([a-zA-Z0-9_.-]+)\s*->\s*([a-zA-Z0-9_.-]+)(?:\s*:\s*(.+?))?$/);
2777
+ if (match) {
2778
+ const from = match[1] ?? "";
2779
+ const to = match[2] ?? "";
2780
+ const label = match[3]?.trim();
2781
+ if (from && to) return { from, to, ...label ? { label } : {} };
2782
+ }
2783
+ return null;
2784
+ }
2785
+ function parseSimpleShape(stripped) {
2786
+ const match = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+)$/);
2787
+ if (match) {
2788
+ const id = match[1];
2789
+ const label = (match[2] ?? "").trim();
2790
+ if (id && label) return { id, label };
2791
+ }
2792
+ return null;
2793
+ }
2794
+ function handleBlockOpen(stripped, state) {
2795
+ if (state.braceDepth === 0) {
2796
+ const shape = parseBlockShape(stripped);
2797
+ if (shape) state.entities.set(shape.id, { id: shape.id, label: shape.label });
2417
2798
  }
2418
- parseFlowchart(content, diagramType) {
2419
- const entities = /* @__PURE__ */ new Map();
2420
- const relationships = [];
2421
- const nodeRegex = /([A-Za-z0-9_]+)\s*[[({]([^\])}]+)[\])}]/g;
2422
- let match;
2423
- match = nodeRegex.exec(content);
2424
- while (match !== null) {
2425
- const id = match[1] ?? "";
2426
- const label = (match[2] ?? "").trim();
2427
- if (id && !entities.has(id)) {
2428
- const isDecision = this.isDecisionNode(content, id);
2429
- entities.set(id, {
2430
- id,
2431
- label,
2432
- ...isDecision ? { type: "decision" } : {}
2433
- });
2434
- }
2435
- match = nodeRegex.exec(content);
2436
- }
2437
- const stripped = content.replace(/[[({][^\])}]*[\])}]/g, "");
2438
- const labeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\|([^|]+)\|\s*([A-Za-z0-9_]+)/g;
2439
- const unlabeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\s+([A-Za-z0-9_]+)/g;
2440
- const edgeKeys = /* @__PURE__ */ new Set();
2441
- match = labeledEdgeRegex.exec(stripped);
2442
- while (match !== null) {
2443
- const from = match[1] ?? "";
2444
- const label = (match[2] ?? "").trim();
2445
- const to = match[3] ?? "";
2446
- if (from && to) {
2447
- const key = `${from}->${to}:${label}`;
2448
- if (!edgeKeys.has(key)) {
2449
- edgeKeys.add(key);
2450
- relationships.push({ from, to, label });
2451
- }
2452
- }
2453
- match = labeledEdgeRegex.exec(stripped);
2454
- }
2455
- match = unlabeledEdgeRegex.exec(stripped);
2456
- while (match !== null) {
2457
- const from = match[1] ?? "";
2458
- const to = match[2] ?? "";
2459
- if (from && to) {
2460
- const hasLabeled = relationships.some((r) => r.from === from && r.to === to);
2461
- if (!hasLabeled) {
2462
- const key = `${from}->${to}`;
2463
- if (!edgeKeys.has(key)) {
2464
- edgeKeys.add(key);
2465
- relationships.push({ from, to });
2466
- }
2467
- }
2468
- }
2469
- match = unlabeledEdgeRegex.exec(stripped);
2470
- }
2471
- return {
2472
- entities: Array.from(entities.values()),
2473
- relationships,
2474
- metadata: { format: "mermaid", diagramType }
2475
- };
2799
+ state.braceDepth++;
2800
+ }
2801
+ function processTopLevelLine(stripped, state) {
2802
+ const conn = parseConnection(stripped);
2803
+ if (conn) {
2804
+ state.relationships.push(conn);
2805
+ return;
2476
2806
  }
2477
- isDecisionNode(content, nodeId) {
2478
- const decisionRegex = new RegExp(`${nodeId}\\s*\\{`);
2479
- return decisionRegex.test(content);
2807
+ const shape = parseSimpleShape(stripped);
2808
+ if (shape && !state.entities.has(shape.id)) {
2809
+ state.entities.set(shape.id, { id: shape.id, label: shape.label });
2480
2810
  }
2481
- parseSequence(content, diagramType) {
2482
- const entities = [];
2483
- const relationships = [];
2484
- const seenParticipants = /* @__PURE__ */ new Set();
2485
- const participantRegex = /participant\s+(\w+)(?:\s+as\s+(.+))?/g;
2486
- let match;
2487
- match = participantRegex.exec(content);
2488
- while (match !== null) {
2489
- const id = match[1] ?? "";
2490
- const label = match[2]?.trim() || id;
2491
- if (id && !seenParticipants.has(id)) {
2492
- seenParticipants.add(id);
2493
- entities.push({ id, label });
2494
- }
2495
- match = participantRegex.exec(content);
2496
- }
2497
- const forwardMsgRegex = /(\w+)\s*->>?\+?\s*(\w+)\s*:\s*(.+)/g;
2498
- match = forwardMsgRegex.exec(content);
2499
- while (match !== null) {
2500
- const from = match[1] ?? "";
2501
- const to = match[2] ?? "";
2502
- const label = (match[3] ?? "").trim();
2503
- if (from && to) {
2504
- relationships.push({ from, to, label });
2505
- }
2506
- match = forwardMsgRegex.exec(content);
2507
- }
2508
- const returnMsgRegex = /(\w+)\s*-->>?-?\s*(\w+)\s*:\s*(.+)/g;
2509
- match = returnMsgRegex.exec(content);
2510
- while (match !== null) {
2511
- const from = match[1] ?? "";
2512
- const to = match[2] ?? "";
2513
- const label = (match[3] ?? "").trim();
2514
- if (from && to) {
2515
- relationships.push({ from, to, label });
2516
- }
2517
- match = returnMsgRegex.exec(content);
2518
- }
2519
- return {
2520
- entities,
2521
- relationships,
2522
- metadata: { format: "mermaid", diagramType }
2523
- };
2811
+ }
2812
+ function processD2Line(stripped, state) {
2813
+ if (!stripped || stripped.startsWith("#")) return;
2814
+ if (stripped.endsWith("{")) {
2815
+ handleBlockOpen(stripped, state);
2816
+ return;
2524
2817
  }
2525
- };
2818
+ if (stripped === "}") {
2819
+ state.braceDepth = Math.max(0, state.braceDepth - 1);
2820
+ return;
2821
+ }
2822
+ if (state.braceDepth > 0) return;
2823
+ processTopLevelLine(stripped, state);
2824
+ }
2526
2825
  var D2Parser = class {
2527
2826
  canParse(_content, ext) {
2528
2827
  return ext === ".d2";
2529
2828
  }
2530
2829
  parse(content, _filePath) {
2531
2830
  const trimmed = content.trim();
2532
- if (!trimmed) {
2533
- return {
2534
- entities: [],
2535
- relationships: [],
2536
- metadata: { format: "d2", diagramType: "architecture" }
2537
- };
2538
- }
2539
- const entities = /* @__PURE__ */ new Map();
2540
- const relationships = [];
2541
- let braceDepth = 0;
2831
+ if (!trimmed) return emptyResult3();
2832
+ const state = {
2833
+ entities: /* @__PURE__ */ new Map(),
2834
+ relationships: [],
2835
+ braceDepth: 0
2836
+ };
2542
2837
  for (const line of trimmed.split("\n")) {
2543
- const stripped = line.trim();
2544
- if (!stripped || stripped.startsWith("#")) continue;
2545
- if (stripped.endsWith("{")) {
2546
- if (braceDepth === 0) {
2547
- const shapeMatch = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+?)\s*\{$/);
2548
- if (shapeMatch) {
2549
- const id = shapeMatch[1];
2550
- const label = shapeMatch[2];
2551
- if (id && label) {
2552
- entities.set(id, { id, label });
2553
- }
2554
- }
2555
- }
2556
- braceDepth++;
2557
- continue;
2558
- }
2559
- if (stripped === "}") {
2560
- braceDepth = Math.max(0, braceDepth - 1);
2561
- continue;
2562
- }
2563
- if (braceDepth > 0) continue;
2564
- const connMatch = stripped.match(
2565
- /^([a-zA-Z0-9_.-]+)\s*->\s*([a-zA-Z0-9_.-]+)(?:\s*:\s*(.+?))?$/
2566
- );
2567
- if (connMatch) {
2568
- const from = connMatch[1] ?? "";
2569
- const to = connMatch[2] ?? "";
2570
- const label = connMatch[3]?.trim();
2571
- if (from && to) {
2572
- relationships.push({ from, to, ...label ? { label } : {} });
2573
- }
2574
- continue;
2575
- }
2576
- const simpleMatch = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+)$/);
2577
- if (simpleMatch) {
2578
- const id = simpleMatch[1];
2579
- const label = (simpleMatch[2] ?? "").trim();
2580
- if (id && label && !entities.has(id)) {
2581
- entities.set(id, { id, label });
2582
- }
2583
- }
2838
+ processD2Line(line.trim(), state);
2584
2839
  }
2585
2840
  return {
2586
- entities: Array.from(entities.values()),
2587
- relationships,
2841
+ entities: Array.from(state.entities.values()),
2842
+ relationships: state.relationships,
2588
2843
  metadata: { format: "d2", diagramType: "architecture" }
2589
2844
  };
2590
2845
  }
2591
2846
  };
2847
+
2848
+ // src/ingest/parsers/plantuml.ts
2849
+ function emptyResult4() {
2850
+ return {
2851
+ entities: [],
2852
+ relationships: [],
2853
+ metadata: { format: "plantuml", diagramType: "unknown" }
2854
+ };
2855
+ }
2856
+ function detectDiagramType2(content) {
2857
+ if (/class\s+\w+/.test(content)) return "class";
2858
+ if (/\[.+\]/.test(content) || /component\s+/.test(content)) return "component";
2859
+ if (/participant\s+/.test(content) || /actor\s+/.test(content)) return "sequence";
2860
+ return "unknown";
2861
+ }
2862
+ function stripWrappers(content) {
2863
+ return content.replace(/@startuml\b.*\n?/, "").replace(/@enduml\b.*/, "").trim();
2864
+ }
2865
+ function extractClasses(body, entities) {
2866
+ const classRegex = /class\s+(\w+)/g;
2867
+ let match = classRegex.exec(body);
2868
+ while (match !== null) {
2869
+ const id = match[1] ?? "";
2870
+ if (id && !entities.has(id)) {
2871
+ entities.set(id, { id, label: id });
2872
+ }
2873
+ match = classRegex.exec(body);
2874
+ }
2875
+ }
2876
+ function extractComponents(body, entities) {
2877
+ const componentRegex = /\[([^\]]+)\]/g;
2878
+ let match = componentRegex.exec(body);
2879
+ while (match !== null) {
2880
+ const label = (match[1] ?? "").trim();
2881
+ if (label) {
2882
+ const id = label.replace(/\s+/g, "_");
2883
+ if (!entities.has(id)) {
2884
+ entities.set(id, { id, label });
2885
+ }
2886
+ }
2887
+ match = componentRegex.exec(body);
2888
+ }
2889
+ }
2890
+ function parseRelationshipMatch(match) {
2891
+ const from = match[1] ?? "";
2892
+ const to = match[2] ?? "";
2893
+ if (!from || !to) return null;
2894
+ const label = match[3]?.trim();
2895
+ return { from, to, ...label ? { label } : {} };
2896
+ }
2897
+ function collectRelationshipMatches(body, regex) {
2898
+ const results = [];
2899
+ let match = regex.exec(body);
2900
+ while (match !== null) {
2901
+ const rel = parseRelationshipMatch(match);
2902
+ if (rel) results.push(rel);
2903
+ match = regex.exec(body);
2904
+ }
2905
+ return results;
2906
+ }
2907
+ function extractRelationships(body) {
2908
+ return collectRelationshipMatches(
2909
+ body,
2910
+ /(\w+)\s*(?:-->|->|<--|<-|\.\.>|--)\s*(\w+)(?:\s*:\s*(.+))?/g
2911
+ );
2912
+ }
2592
2913
  var PlantUmlParser = class {
2593
2914
  canParse(_content, ext) {
2594
2915
  return ext === ".puml" || ext === ".plantuml";
2595
2916
  }
2596
2917
  parse(content, _filePath) {
2597
2918
  const trimmed = content.trim();
2598
- if (!trimmed) {
2599
- return {
2600
- entities: [],
2601
- relationships: [],
2602
- metadata: { format: "plantuml", diagramType: "unknown" }
2603
- };
2604
- }
2605
- const diagramType = this.detectDiagramType(trimmed);
2919
+ if (!trimmed) return emptyResult4();
2920
+ const diagramType = detectDiagramType2(trimmed);
2606
2921
  const entities = /* @__PURE__ */ new Map();
2607
- const relationships = [];
2608
- const body = trimmed.replace(/@startuml\b.*\n?/, "").replace(/@enduml\b.*/, "").trim();
2609
- const classRegex = /class\s+(\w+)/g;
2610
- let match;
2611
- match = classRegex.exec(body);
2612
- while (match !== null) {
2613
- const id = match[1] ?? "";
2614
- if (id && !entities.has(id)) {
2615
- entities.set(id, { id, label: id });
2616
- }
2617
- match = classRegex.exec(body);
2618
- }
2619
- const componentRegex1 = /\[([^\]]+)\]/g;
2620
- match = componentRegex1.exec(body);
2621
- while (match !== null) {
2622
- const label = (match[1] ?? "").trim();
2623
- if (label) {
2624
- const id = label.replace(/\s+/g, "_");
2625
- if (!entities.has(id)) {
2626
- entities.set(id, { id, label });
2627
- }
2628
- }
2629
- match = componentRegex1.exec(body);
2630
- }
2631
- const relRegex = /(\w+)\s*(?:-->|->|<--|<-|\.\.>|--)\s*(\w+)(?:\s*:\s*(.+))?/g;
2632
- match = relRegex.exec(body);
2633
- while (match !== null) {
2634
- const from = match[1] ?? "";
2635
- const to = match[2] ?? "";
2636
- const label = match[3]?.trim();
2637
- if (from && to) {
2638
- relationships.push({ from, to, ...label ? { label } : {} });
2639
- }
2640
- match = relRegex.exec(body);
2641
- }
2922
+ const body = stripWrappers(trimmed);
2923
+ extractClasses(body, entities);
2924
+ extractComponents(body, entities);
2925
+ const relationships = extractRelationships(body);
2642
2926
  return {
2643
2927
  entities: Array.from(entities.values()),
2644
2928
  relationships,
2645
2929
  metadata: { format: "plantuml", diagramType }
2646
2930
  };
2647
2931
  }
2648
- detectDiagramType(content) {
2649
- if (/class\s+\w+/.test(content)) return "class";
2650
- if (/\[.+\]/.test(content) || /component\s+/.test(content)) return "component";
2651
- if (/participant\s+/.test(content) || /actor\s+/.test(content)) return "sequence";
2652
- return "unknown";
2653
- }
2654
2932
  };
2933
+
2934
+ // src/ingest/DiagramParser.ts
2655
2935
  var DIAGRAM_EXTENSIONS = /* @__PURE__ */ new Set([".mmd", ".mermaid", ".d2", ".puml", ".plantuml"]);
2936
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".harness"]);
2656
2937
  var DiagramParser = class {
2657
2938
  constructor(store) {
2658
2939
  this.store = store;
@@ -2664,7 +2945,7 @@ var DiagramParser = class {
2664
2945
  new PlantUmlParser()
2665
2946
  ];
2666
2947
  parse(content, filePath) {
2667
- const ext = path6.extname(filePath).toLowerCase();
2948
+ const ext = path7.extname(filePath).toLowerCase();
2668
2949
  for (const parser of this.parsers) {
2669
2950
  if (parser.canParse(content, ext)) {
2670
2951
  return parser.parse(content, filePath);
@@ -2684,41 +2965,13 @@ var DiagramParser = class {
2684
2965
  const diagramFiles = await this.findDiagramFiles(projectDir);
2685
2966
  for (const filePath of diagramFiles) {
2686
2967
  try {
2687
- const content = await fs5.readFile(filePath, "utf-8");
2688
- const relPath = path6.relative(projectDir, filePath).replaceAll("\\", "/");
2968
+ const content = await fs6.readFile(filePath, "utf-8");
2969
+ const relPath = path7.relative(projectDir, filePath).replaceAll("\\", "/");
2689
2970
  const result = this.parse(content, filePath);
2690
2971
  if (result.entities.length === 0) continue;
2691
2972
  const pathHash = hash(relPath);
2692
- for (const entity of result.entities) {
2693
- const nodeId = `diagram:${pathHash}:${entity.id}`;
2694
- this.store.addNode({
2695
- id: nodeId,
2696
- type: "business_concept",
2697
- name: entity.label,
2698
- path: relPath,
2699
- metadata: {
2700
- source: "diagram",
2701
- format: result.metadata.format,
2702
- diagramType: result.metadata.diagramType,
2703
- confidence: 0.85,
2704
- ...entity.type ? { entityType: entity.type } : {}
2705
- }
2706
- });
2707
- nodesAdded++;
2708
- }
2709
- for (const rel of result.relationships) {
2710
- const fromId = `diagram:${pathHash}:${rel.from}`;
2711
- const toId = `diagram:${pathHash}:${rel.to}`;
2712
- this.store.addEdge({
2713
- from: fromId,
2714
- to: toId,
2715
- type: "references",
2716
- metadata: {
2717
- ...rel.label ? { label: rel.label } : {}
2718
- }
2719
- });
2720
- edgesAdded++;
2721
- }
2973
+ nodesAdded += this.addEntityNodes(result, relPath, pathHash);
2974
+ edgesAdded += this.addRelationshipEdges(result, pathHash);
2722
2975
  } catch (err) {
2723
2976
  errors.push(
2724
2977
  `Failed to parse ${filePath}: ${err instanceof Error ? err.message : String(err)}`
@@ -2734,25 +2987,64 @@ var DiagramParser = class {
2734
2987
  durationMs: Date.now() - start
2735
2988
  };
2736
2989
  }
2990
+ /** Map diagram entities to business_concept graph nodes. */
2991
+ addEntityNodes(result, relPath, pathHash) {
2992
+ let count = 0;
2993
+ for (const entity of result.entities) {
2994
+ const nodeId = `diagram:${pathHash}:${entity.id}`;
2995
+ this.store.addNode({
2996
+ id: nodeId,
2997
+ type: "business_concept",
2998
+ name: entity.label,
2999
+ path: relPath,
3000
+ metadata: {
3001
+ source: "diagram",
3002
+ format: result.metadata.format,
3003
+ diagramType: result.metadata.diagramType,
3004
+ confidence: 0.85,
3005
+ ...entity.type ? { entityType: entity.type } : {}
3006
+ }
3007
+ });
3008
+ count++;
3009
+ }
3010
+ return count;
3011
+ }
3012
+ /** Map diagram relationships to references graph edges. */
3013
+ addRelationshipEdges(result, pathHash) {
3014
+ let count = 0;
3015
+ for (const rel of result.relationships) {
3016
+ const fromId = `diagram:${pathHash}:${rel.from}`;
3017
+ const toId = `diagram:${pathHash}:${rel.to}`;
3018
+ this.store.addEdge({
3019
+ from: fromId,
3020
+ to: toId,
3021
+ type: "references",
3022
+ metadata: {
3023
+ ...rel.label ? { label: rel.label } : {}
3024
+ }
3025
+ });
3026
+ count++;
3027
+ }
3028
+ return count;
3029
+ }
2737
3030
  async findDiagramFiles(dir) {
2738
3031
  const files = [];
2739
- const SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".harness"]);
2740
3032
  const walk = async (currentDir) => {
2741
3033
  let entries;
2742
3034
  try {
2743
- entries = await fs5.readdir(currentDir, { withFileTypes: true });
3035
+ entries = await fs6.readdir(currentDir, { withFileTypes: true });
2744
3036
  } catch {
2745
3037
  return;
2746
3038
  }
2747
3039
  for (const entry of entries) {
2748
3040
  if (entry.isDirectory()) {
2749
- if (!SKIP_DIRS2.has(entry.name)) {
2750
- await walk(path6.join(currentDir, entry.name));
3041
+ if (!SKIP_DIRS.has(entry.name)) {
3042
+ await walk(path7.join(currentDir, entry.name));
2751
3043
  }
2752
3044
  } else if (entry.isFile()) {
2753
- const ext = path6.extname(entry.name).toLowerCase();
3045
+ const ext = path7.extname(entry.name).toLowerCase();
2754
3046
  if (DIAGRAM_EXTENSIONS.has(ext)) {
2755
- files.push(path6.join(currentDir, entry.name));
3047
+ files.push(path7.join(currentDir, entry.name));
2756
3048
  }
2757
3049
  }
2758
3050
  }
@@ -2763,8 +3055,8 @@ var DiagramParser = class {
2763
3055
  };
2764
3056
 
2765
3057
  // src/ingest/KnowledgeLinker.ts
2766
- var fs6 = __toESM(require("fs/promises"));
2767
- var path7 = __toESM(require("path"));
3058
+ var fs7 = __toESM(require("fs/promises"));
3059
+ var path8 = __toESM(require("path"));
2768
3060
  var HEURISTIC_PATTERNS = [
2769
3061
  {
2770
3062
  name: "business-rule-imperative",
@@ -2911,8 +3203,10 @@ var KnowledgeLinker = class {
2911
3203
  if (!duplicate) return false;
2912
3204
  const existingSources = duplicate.metadata.sources ?? [];
2913
3205
  if (!existingSources.includes(candidate.sourceNodeId)) {
2914
- existingSources.push(candidate.sourceNodeId);
2915
- duplicate.metadata.sources = existingSources;
3206
+ this.store.addNode({
3207
+ ...duplicate,
3208
+ metadata: { ...duplicate.metadata, sources: [...existingSources, candidate.sourceNodeId] }
3209
+ });
2916
3210
  }
2917
3211
  return true;
2918
3212
  }
@@ -2946,8 +3240,8 @@ var KnowledgeLinker = class {
2946
3240
  */
2947
3241
  async writeJsonl(candidates) {
2948
3242
  if (!this.outputDir) return;
2949
- await fs6.mkdir(this.outputDir, { recursive: true });
2950
- const filePath = path7.join(this.outputDir, "linker.jsonl");
3243
+ await fs7.mkdir(this.outputDir, { recursive: true });
3244
+ const filePath = path8.join(this.outputDir, "linker.jsonl");
2951
3245
  const lines = candidates.map(
2952
3246
  (c) => JSON.stringify({
2953
3247
  id: c.id,
@@ -2960,16 +3254,16 @@ var KnowledgeLinker = class {
2960
3254
  nodeType: c.nodeType
2961
3255
  })
2962
3256
  );
2963
- await fs6.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
3257
+ await fs7.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
2964
3258
  }
2965
3259
  /**
2966
3260
  * Write medium-confidence candidates to staged JSONL for human review.
2967
3261
  */
2968
3262
  async writeStagedJsonl(candidates) {
2969
3263
  if (!this.outputDir || candidates.length === 0) return;
2970
- const stagedDir = path7.join(this.outputDir, "staged");
2971
- await fs6.mkdir(stagedDir, { recursive: true });
2972
- const filePath = path7.join(stagedDir, "linker-staged.jsonl");
3264
+ const stagedDir = path8.join(this.outputDir, "staged");
3265
+ await fs7.mkdir(stagedDir, { recursive: true });
3266
+ const filePath = path8.join(stagedDir, "linker-staged.jsonl");
2973
3267
  const lines = candidates.map(
2974
3268
  (c) => JSON.stringify({
2975
3269
  id: c.id,
@@ -2982,7 +3276,7 @@ var KnowledgeLinker = class {
2982
3276
  nodeType: c.nodeType
2983
3277
  })
2984
3278
  );
2985
- await fs6.writeFile(filePath, lines.join("\n") + "\n");
3279
+ await fs7.writeFile(filePath, lines.join("\n") + "\n");
2986
3280
  }
2987
3281
  /**
2988
3282
  * Apply heuristic patterns to content and return candidate extractions.
@@ -3088,13 +3382,23 @@ var StructuralDriftDetector = class {
3088
3382
  };
3089
3383
 
3090
3384
  // src/ingest/KnowledgeStagingAggregator.ts
3091
- var fs7 = __toESM(require("fs/promises"));
3092
- var path8 = __toESM(require("path"));
3385
+ var fs8 = __toESM(require("fs/promises"));
3386
+ var path9 = __toESM(require("path"));
3387
+ var BUSINESS_NODE_TYPES = [
3388
+ "business_concept",
3389
+ "business_rule",
3390
+ "business_process",
3391
+ "business_term",
3392
+ "business_metric",
3393
+ "business_fact"
3394
+ ];
3093
3395
  var KnowledgeStagingAggregator = class {
3094
- constructor(projectDir) {
3396
+ constructor(projectDir, inferenceOptions = {}) {
3095
3397
  this.projectDir = projectDir;
3398
+ this.inferenceOptions = inferenceOptions;
3096
3399
  }
3097
3400
  projectDir;
3401
+ inferenceOptions;
3098
3402
  async aggregate(extractorResults, linkerResults, diagramResults) {
3099
3403
  const all = [...extractorResults, ...linkerResults, ...diagramResults];
3100
3404
  const byHash = /* @__PURE__ */ new Map();
@@ -3108,59 +3412,157 @@ var KnowledgeStagingAggregator = class {
3108
3412
  if (deduplicated.length === 0) {
3109
3413
  return { staged: 0 };
3110
3414
  }
3111
- const stagedDir = path8.join(this.projectDir, ".harness", "knowledge", "staged");
3112
- await fs7.mkdir(stagedDir, { recursive: true });
3415
+ const stagedDir = path9.join(this.projectDir, ".harness", "knowledge", "staged");
3416
+ await fs8.mkdir(stagedDir, { recursive: true });
3113
3417
  const jsonl = deduplicated.map((entry) => JSON.stringify(entry)).join("\n") + "\n";
3114
- await fs7.writeFile(path8.join(stagedDir, "pipeline-staged.jsonl"), jsonl, "utf-8");
3418
+ await fs8.writeFile(path9.join(stagedDir, "pipeline-staged.jsonl"), jsonl, "utf-8");
3115
3419
  return { staged: deduplicated.length };
3116
3420
  }
3117
- async generateGapReport(knowledgeDir) {
3118
- const domains = [];
3421
+ async extractDocName(filePath) {
3422
+ const raw = await fs8.readFile(filePath, "utf-8");
3423
+ const fmMatch = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
3424
+ const body = fmMatch ? fmMatch[1] : raw;
3425
+ const titleMatch = body.match(/^#\s+(.+)$/m);
3426
+ return titleMatch ? titleMatch[1].trim() : path9.basename(filePath, ".md");
3427
+ }
3428
+ async generateGapReport(knowledgeDir, store) {
3429
+ const domainDocNames = /* @__PURE__ */ new Map();
3430
+ const domainEntryCounts = /* @__PURE__ */ new Map();
3119
3431
  let totalEntries = 0;
3120
3432
  try {
3121
- const entries = await fs7.readdir(knowledgeDir, { withFileTypes: true });
3433
+ const entries = await fs8.readdir(knowledgeDir, { withFileTypes: true });
3122
3434
  const domainDirs = entries.filter((e) => e.isDirectory());
3123
3435
  for (const dir of domainDirs) {
3124
- const domainPath = path8.join(knowledgeDir, dir.name);
3125
- const files = await fs7.readdir(domainPath);
3436
+ const domainPath = path9.join(knowledgeDir, dir.name);
3437
+ const files = await fs8.readdir(domainPath);
3126
3438
  const mdFiles = files.filter((f) => f.endsWith(".md"));
3127
3439
  const entryCount = mdFiles.length;
3128
3440
  totalEntries += entryCount;
3129
- domains.push({ domain: dir.name, entryCount });
3441
+ domainEntryCounts.set(dir.name, entryCount);
3442
+ if (store) {
3443
+ const names = [];
3444
+ for (const file of mdFiles) {
3445
+ const name = await this.extractDocName(path9.join(domainPath, file));
3446
+ names.push(name.toLowerCase().trim());
3447
+ }
3448
+ domainDocNames.set(dir.name, names);
3449
+ }
3130
3450
  }
3131
3451
  } catch {
3132
3452
  }
3453
+ if (!store) {
3454
+ const domains2 = [];
3455
+ for (const [domain, entryCount] of domainEntryCounts) {
3456
+ domains2.push({ domain, entryCount, extractedCount: 0, gapCount: 0, gapEntries: [] });
3457
+ }
3458
+ return {
3459
+ domains: domains2,
3460
+ totalEntries,
3461
+ totalExtracted: 0,
3462
+ totalGaps: 0,
3463
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
3464
+ };
3465
+ }
3466
+ const extractedByDomain = /* @__PURE__ */ new Map();
3467
+ for (const nodeType of BUSINESS_NODE_TYPES) {
3468
+ const nodes = store.findNodes({ type: nodeType });
3469
+ for (const node of nodes) {
3470
+ const domain = inferDomain(node, this.inferenceOptions);
3471
+ const list = extractedByDomain.get(domain) ?? [];
3472
+ list.push(node);
3473
+ extractedByDomain.set(domain, list);
3474
+ }
3475
+ }
3476
+ for (const [domain, nodes] of extractedByDomain) {
3477
+ const seen = /* @__PURE__ */ new Set();
3478
+ const deduped = nodes.filter((n) => {
3479
+ const key = n.name.toLowerCase().trim();
3480
+ if (seen.has(key)) return false;
3481
+ seen.add(key);
3482
+ return true;
3483
+ });
3484
+ extractedByDomain.set(domain, deduped);
3485
+ }
3486
+ const allDomains = /* @__PURE__ */ new Set([...domainEntryCounts.keys(), ...extractedByDomain.keys()]);
3487
+ const domains = [];
3488
+ let totalExtracted = 0;
3489
+ let totalGaps = 0;
3490
+ for (const domain of allDomains) {
3491
+ const entryCount = domainEntryCounts.get(domain) ?? 0;
3492
+ const extractedNodes = extractedByDomain.get(domain) ?? [];
3493
+ const extractedCount = extractedNodes.length;
3494
+ totalExtracted += extractedCount;
3495
+ const docNames = domainDocNames.get(domain) ?? [];
3496
+ const gapEntries = [];
3497
+ for (const node of extractedNodes) {
3498
+ const normalizedName = node.name.toLowerCase().trim();
3499
+ if (!docNames.includes(normalizedName)) {
3500
+ gapEntries.push({
3501
+ nodeId: node.id,
3502
+ name: node.name,
3503
+ nodeType: node.type,
3504
+ source: node.metadata?.source ?? "unknown",
3505
+ hasContent: Boolean(node.content && node.content.trim().length >= 10)
3506
+ });
3507
+ }
3508
+ }
3509
+ totalGaps += gapEntries.length;
3510
+ domains.push({ domain, entryCount, extractedCount, gapCount: gapEntries.length, gapEntries });
3511
+ }
3133
3512
  return {
3134
3513
  domains,
3135
3514
  totalEntries,
3515
+ totalExtracted,
3516
+ totalGaps,
3136
3517
  generatedAt: (/* @__PURE__ */ new Date()).toISOString()
3137
3518
  };
3138
3519
  }
3139
3520
  async writeGapReport(report) {
3140
- const gapsDir = path8.join(this.projectDir, ".harness", "knowledge");
3141
- await fs7.mkdir(gapsDir, { recursive: true });
3521
+ const gapsDir = path9.join(this.projectDir, ".harness", "knowledge");
3522
+ await fs8.mkdir(gapsDir, { recursive: true });
3523
+ const hasDifferential = report.totalExtracted > 0;
3142
3524
  const lines = [
3143
3525
  "# Knowledge Gaps Report",
3144
3526
  "",
3145
3527
  `Generated: ${report.generatedAt}`,
3146
3528
  "",
3147
3529
  "## Coverage by Domain",
3148
- "",
3149
- "| Domain | Entries |",
3150
- "| ------ | ------- |"
3530
+ ""
3151
3531
  ];
3152
- for (const domain of report.domains) {
3153
- lines.push(`| ${domain.domain} | ${domain.entryCount} |`);
3532
+ if (hasDifferential) {
3533
+ lines.push(
3534
+ "| Domain | Documented | Extracted | Gaps |",
3535
+ "| ------ | ---------- | --------- | ---- |"
3536
+ );
3537
+ for (const domain of report.domains) {
3538
+ lines.push(
3539
+ `| ${domain.domain} | ${domain.entryCount} | ${domain.extractedCount} | ${domain.gapCount} |`
3540
+ );
3541
+ }
3542
+ lines.push(
3543
+ "",
3544
+ `## Summary`,
3545
+ "",
3546
+ `- **Total Documented:** ${report.totalEntries}`,
3547
+ `- **Total Extracted:** ${report.totalExtracted}`,
3548
+ `- **Total Gaps:** ${report.totalGaps}`,
3549
+ ""
3550
+ );
3551
+ } else {
3552
+ lines.push("| Domain | Entries |", "| ------ | ------- |");
3553
+ for (const domain of report.domains) {
3554
+ lines.push(`| ${domain.domain} | ${domain.entryCount} |`);
3555
+ }
3556
+ lines.push("", `## Total Entries: ${report.totalEntries}`, "");
3154
3557
  }
3155
- lines.push("", `## Total Entries: ${report.totalEntries}`, "");
3156
- await fs7.writeFile(path8.join(gapsDir, "gaps.md"), lines.join("\n"), "utf-8");
3558
+ await fs8.writeFile(path9.join(gapsDir, "gaps.md"), lines.join("\n"), "utf-8");
3157
3559
  }
3158
3560
  };
3159
3561
 
3160
3562
  // src/ingest/extractors/ExtractionRunner.ts
3161
- var fs8 = __toESM(require("fs/promises"));
3162
- var path9 = __toESM(require("path"));
3163
- var SKIP_DIRS = /* @__PURE__ */ new Set([
3563
+ var fs9 = __toESM(require("fs/promises"));
3564
+ var path10 = __toESM(require("path"));
3565
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
3164
3566
  "node_modules",
3165
3567
  "dist",
3166
3568
  "target",
@@ -3197,11 +3599,11 @@ var EXT_TO_LANGUAGE = {
3197
3599
  };
3198
3600
  var SKIP_EXTENSIONS2 = /* @__PURE__ */ new Set([".d.ts"]);
3199
3601
  function detectLanguage(filePath) {
3200
- const name = path9.basename(filePath);
3602
+ const name = path10.basename(filePath);
3201
3603
  for (const skip of SKIP_EXTENSIONS2) {
3202
3604
  if (name.endsWith(skip)) return void 0;
3203
3605
  }
3204
- const ext = path9.extname(filePath);
3606
+ const ext = path10.extname(filePath);
3205
3607
  return EXT_TO_LANGUAGE[ext];
3206
3608
  }
3207
3609
  var EXTRACTOR_EDGE_TYPE = {
@@ -3227,7 +3629,7 @@ var ExtractionRunner = class {
3227
3629
  let nodesAdded = 0;
3228
3630
  let nodesUpdated = 0;
3229
3631
  let edgesAdded = 0;
3230
- await fs8.mkdir(outputDir, { recursive: true });
3632
+ await fs9.mkdir(outputDir, { recursive: true });
3231
3633
  const files = await this.findSourceFiles(projectDir);
3232
3634
  const recordsByExtractor = /* @__PURE__ */ new Map();
3233
3635
  for (const ext of this.extractors) {
@@ -3236,17 +3638,17 @@ var ExtractionRunner = class {
3236
3638
  for (const filePath of files) {
3237
3639
  const language = detectLanguage(filePath);
3238
3640
  if (!language) continue;
3239
- const ext = path9.extname(filePath);
3641
+ const ext = path10.extname(filePath);
3240
3642
  let content;
3241
3643
  try {
3242
- content = await fs8.readFile(filePath, "utf-8");
3644
+ content = await fs9.readFile(filePath, "utf-8");
3243
3645
  } catch (err) {
3244
3646
  errors.push(
3245
3647
  `Failed to read ${filePath}: ${err instanceof Error ? err.message : String(err)}`
3246
3648
  );
3247
3649
  continue;
3248
3650
  }
3249
- const relativePath = path9.relative(projectDir, filePath).replaceAll("\\", "/");
3651
+ const relativePath = path10.relative(projectDir, filePath).replaceAll("\\", "/");
3250
3652
  for (const extractor2 of this.extractors) {
3251
3653
  if (!extractor2.supportedExtensions.includes(ext)) continue;
3252
3654
  try {
@@ -3283,9 +3685,9 @@ var ExtractionRunner = class {
3283
3685
  }
3284
3686
  /** Write extraction records to JSONL file. */
3285
3687
  async writeJsonl(records, outputDir, extractorName) {
3286
- const filePath = path9.join(outputDir, `${extractorName}.jsonl`);
3688
+ const filePath = path10.join(outputDir, `${extractorName}.jsonl`);
3287
3689
  const lines = records.map((r) => JSON.stringify(r));
3288
- await fs8.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
3690
+ await fs9.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
3289
3691
  }
3290
3692
  /** Create or update a graph node from an extraction record. */
3291
3693
  persistRecord(record, store) {
@@ -3357,13 +3759,13 @@ var ExtractionRunner = class {
3357
3759
  const results = [];
3358
3760
  let entries;
3359
3761
  try {
3360
- entries = await fs8.readdir(dir, { withFileTypes: true });
3762
+ entries = await fs9.readdir(dir, { withFileTypes: true });
3361
3763
  } catch {
3362
3764
  return results;
3363
3765
  }
3364
3766
  for (const entry of entries) {
3365
- const fullPath = path9.join(dir, entry.name);
3366
- if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
3767
+ const fullPath = path10.join(dir, entry.name);
3768
+ if (entry.isDirectory() && !SKIP_DIRS2.has(entry.name)) {
3367
3769
  results.push(...await this.findSourceFiles(fullPath));
3368
3770
  } else if (entry.isFile() && detectLanguage(fullPath) !== void 0) {
3369
3771
  results.push(fullPath);
@@ -4489,57 +4891,9 @@ var ImageAnalysisExtractor = class {
4489
4891
  const errors = [];
4490
4892
  for (const imagePath of imagePaths) {
4491
4893
  try {
4492
- const response = await this.provider.analyze({
4493
- prompt: "Analyze this image and provide a structured description of its visual contents.",
4494
- systemPrompt: "You are an image analysis assistant. Describe the image contents, detect UI elements, extract visible text, identify design patterns, and note accessibility concerns.",
4495
- responseSchema: {},
4496
- // Schema handled by provider
4497
- maxTokens: 1e3
4498
- });
4499
- const result = response.result;
4500
- const pathHash = hash(imagePath);
4501
- const annotationId = `img:${pathHash}`;
4502
- store.addNode({
4503
- id: annotationId,
4504
- type: "image_annotation",
4505
- name: result.description.slice(0, 200),
4506
- path: imagePath,
4507
- content: result.description,
4508
- hash: hash(result.description),
4509
- metadata: {
4510
- source: "image-analysis",
4511
- detectedElements: result.detectedElements,
4512
- extractedText: result.extractedText,
4513
- designPatterns: result.designPatterns,
4514
- accessibilityNotes: result.accessibilityNotes,
4515
- model: response.model
4516
- }
4517
- });
4518
- nodesAdded++;
4519
- const fileNodes = store.findNodes({ type: "file" });
4520
- const fileNode = fileNodes.find((n) => n.path === imagePath);
4521
- if (fileNode) {
4522
- store.addEdge({ from: annotationId, to: fileNode.id, type: "annotates" });
4523
- edgesAdded++;
4524
- }
4525
- for (const pattern of result.designPatterns) {
4526
- const conceptId = `img:concept:${hash(pattern + imagePath)}`;
4527
- store.addNode({
4528
- id: conceptId,
4529
- type: "business_concept",
4530
- name: pattern,
4531
- content: `Design pattern detected in ${imagePath}: ${pattern}`,
4532
- hash: hash(pattern),
4533
- metadata: {
4534
- source: "image-analysis",
4535
- sourceImage: imagePath,
4536
- domain: "design"
4537
- }
4538
- });
4539
- nodesAdded++;
4540
- store.addEdge({ from: annotationId, to: conceptId, type: "contains" });
4541
- edgesAdded++;
4542
- }
4894
+ const counts = await this.processImage(store, imagePath);
4895
+ nodesAdded += counts.nodes;
4896
+ edgesAdded += counts.edges;
4543
4897
  } catch (err) {
4544
4898
  errors.push(
4545
4899
  `Image analysis failed for ${imagePath}: ${err instanceof Error ? err.message : String(err)}`
@@ -4555,6 +4909,68 @@ var ImageAnalysisExtractor = class {
4555
4909
  durationMs: Date.now() - start
4556
4910
  };
4557
4911
  }
4912
+ /** Analyze a single image and add annotation + concept nodes to the store. */
4913
+ async processImage(store, imagePath) {
4914
+ const response = await this.provider.analyze({
4915
+ prompt: "Analyze this image and provide a structured description of its visual contents.",
4916
+ systemPrompt: "You are an image analysis assistant. Describe the image contents, detect UI elements, extract visible text, identify design patterns, and note accessibility concerns.",
4917
+ responseSchema: {},
4918
+ // Schema handled by provider
4919
+ maxTokens: 1e3
4920
+ });
4921
+ const result = response.result;
4922
+ const annotationId = `img:${hash(imagePath)}`;
4923
+ let nodes = 0;
4924
+ let edges = 0;
4925
+ store.addNode({
4926
+ id: annotationId,
4927
+ type: "image_annotation",
4928
+ name: result.description.slice(0, 200),
4929
+ path: imagePath,
4930
+ content: result.description,
4931
+ hash: hash(result.description),
4932
+ metadata: {
4933
+ source: "image-analysis",
4934
+ detectedElements: result.detectedElements,
4935
+ extractedText: result.extractedText,
4936
+ designPatterns: result.designPatterns,
4937
+ accessibilityNotes: result.accessibilityNotes,
4938
+ model: response.model
4939
+ }
4940
+ });
4941
+ nodes++;
4942
+ edges += this.linkToFileNode(store, annotationId, imagePath);
4943
+ for (const pattern of result.designPatterns) {
4944
+ edges += this.addDesignPatternConcept(store, annotationId, imagePath, pattern);
4945
+ nodes++;
4946
+ }
4947
+ return { nodes, edges };
4948
+ }
4949
+ /** Link annotation to its source file node if it exists. Returns 1 if linked, 0 otherwise. */
4950
+ linkToFileNode(store, annotationId, imagePath) {
4951
+ const fileNode = store.findNodes({ type: "file" }).find((n) => n.path === imagePath);
4952
+ if (!fileNode) return 0;
4953
+ store.addEdge({ from: annotationId, to: fileNode.id, type: "annotates" });
4954
+ return 1;
4955
+ }
4956
+ /** Create a business_concept node for a design pattern and link it. Returns edges added. */
4957
+ addDesignPatternConcept(store, annotationId, imagePath, pattern) {
4958
+ const conceptId = `img:concept:${hash(pattern + imagePath)}`;
4959
+ store.addNode({
4960
+ id: conceptId,
4961
+ type: "business_concept",
4962
+ name: pattern,
4963
+ content: `Design pattern detected in ${imagePath}: ${pattern}`,
4964
+ hash: hash(pattern),
4965
+ metadata: {
4966
+ source: "image-analysis",
4967
+ sourceImage: imagePath,
4968
+ domain: "design"
4969
+ }
4970
+ });
4971
+ store.addEdge({ from: annotationId, to: conceptId, type: "contains" });
4972
+ return 1;
4973
+ }
4558
4974
  };
4559
4975
 
4560
4976
  // src/ingest/knowledgeTypes.ts
@@ -4565,6 +4981,7 @@ var KNOWLEDGE_NODE_TYPES = [
4565
4981
  "business_term",
4566
4982
  "business_concept",
4567
4983
  "business_metric",
4984
+ "decision",
4568
4985
  "design_token",
4569
4986
  "design_constraint",
4570
4987
  "aesthetic_intent",
@@ -4605,83 +5022,99 @@ function classifyConflict(a, b) {
4605
5022
  return "temporal_conflict";
4606
5023
  return "status_divergence";
4607
5024
  }
4608
- var ContradictionDetector = class {
4609
- detect(store) {
4610
- const nodes = KNOWLEDGE_NODE_TYPES.flatMap((t) => store.findNodes({ type: t }));
4611
- const byName = /* @__PURE__ */ new Map();
4612
- for (const node of nodes) {
4613
- const key = node.name.toLowerCase().trim();
4614
- const group = byName.get(key) ?? [];
4615
- group.push(node);
4616
- byName.set(key, group);
4617
- }
4618
- const contradictions = [];
4619
- const sourcePairCounts = {};
4620
- const seen = /* @__PURE__ */ new Set();
4621
- const addContradiction = (a, b, similarity) => {
4622
- const sourceA = a.metadata.source ?? "unknown";
4623
- const sourceB = b.metadata.source ?? "unknown";
4624
- if (sourceA === sourceB) return;
4625
- const hashA = a.hash ?? a.id;
4626
- const hashB = b.hash ?? b.id;
4627
- if (hashA === hashB) return;
4628
- const pairId = [a.id, b.id].sort().join(":");
4629
- if (seen.has(pairId)) return;
4630
- seen.add(pairId);
4631
- const conflictType = classifyConflict(a, b);
4632
- const severity = conflictType === "value_mismatch" ? "critical" : conflictType === "definition_conflict" ? "high" : "medium";
4633
- const pairKey = [sourceA, sourceB].sort().join("\u2194");
4634
- sourcePairCounts[pairKey] = (sourcePairCounts[pairKey] ?? 0) + 1;
4635
- contradictions.push({
4636
- id: `contradiction:${a.id}:${b.id}`,
4637
- entityA: {
4638
- nodeId: a.id,
4639
- source: sourceA,
4640
- name: a.name,
4641
- content: a.content ?? "",
4642
- lastModified: a.lastModified
4643
- },
4644
- entityB: {
4645
- nodeId: b.id,
4646
- source: sourceB,
4647
- name: b.name,
4648
- content: b.content ?? "",
4649
- lastModified: b.lastModified
4650
- },
4651
- similarity,
4652
- conflictType,
4653
- severity,
4654
- description: `"${a.name}" has conflicting definitions from ${sourceA} and ${sourceB}`
4655
- });
4656
- };
4657
- for (const [, group] of byName) {
4658
- if (group.length < 2) continue;
4659
- for (let i = 0; i < group.length; i++) {
4660
- for (let j = i + 1; j < group.length; j++) {
4661
- addContradiction(group[i], group[j], 1);
4662
- }
5025
+ var SEVERITY_MAP = {
5026
+ value_mismatch: "critical",
5027
+ definition_conflict: "high",
5028
+ status_divergence: "medium",
5029
+ temporal_conflict: "medium"
5030
+ };
5031
+ function buildEntry(node, source) {
5032
+ return {
5033
+ nodeId: node.id,
5034
+ source,
5035
+ name: node.name,
5036
+ content: node.content ?? "",
5037
+ lastModified: node.lastModified
5038
+ };
5039
+ }
5040
+ function groupByName(nodes) {
5041
+ const byName = /* @__PURE__ */ new Map();
5042
+ for (const node of nodes) {
5043
+ const key = node.name.toLowerCase().trim();
5044
+ const group = byName.get(key) ?? [];
5045
+ group.push(node);
5046
+ byName.set(key, group);
5047
+ }
5048
+ return byName;
5049
+ }
5050
+ function tryAddContradiction(acc, a, b, similarity) {
5051
+ const sourceA = a.metadata.source ?? "unknown";
5052
+ const sourceB = b.metadata.source ?? "unknown";
5053
+ if (sourceA === sourceB) return;
5054
+ if ((a.hash ?? a.id) === (b.hash ?? b.id)) return;
5055
+ const pairId = [a.id, b.id].sort().join(":");
5056
+ if (acc.seen.has(pairId)) return;
5057
+ acc.seen.add(pairId);
5058
+ const conflictType = classifyConflict(a, b);
5059
+ const pairKey = [sourceA, sourceB].sort().join("\u2194");
5060
+ acc.sourcePairCounts[pairKey] = (acc.sourcePairCounts[pairKey] ?? 0) + 1;
5061
+ acc.contradictions.push({
5062
+ id: `contradiction:${a.id}:${b.id}`,
5063
+ entityA: buildEntry(a, sourceA),
5064
+ entityB: buildEntry(b, sourceB),
5065
+ similarity,
5066
+ conflictType,
5067
+ severity: SEVERITY_MAP[conflictType],
5068
+ description: `"${a.name}" has conflicting definitions from ${sourceA} and ${sourceB}`
5069
+ });
5070
+ }
5071
+ function collectExactMatches(byName, acc) {
5072
+ for (const [, group] of byName) {
5073
+ if (group.length < 2) continue;
5074
+ for (let i = 0; i < group.length; i++) {
5075
+ for (let j = i + 1; j < group.length; j++) {
5076
+ tryAddContradiction(acc, group[i], group[j], 1);
4663
5077
  }
4664
5078
  }
4665
- const keys = [...byName.keys()];
4666
- for (let i = 0; i < keys.length; i++) {
4667
- for (let j = i + 1; j < keys.length; j++) {
4668
- const keyA = keys[i];
4669
- const keyB = keys[j];
4670
- const maxLen = Math.max(keyA.length, keyB.length);
4671
- const minLen = Math.min(keyA.length, keyB.length);
4672
- if (minLen / maxLen < SIMILARITY_THRESHOLD) continue;
4673
- const ratio = levenshteinRatio(keyA, keyB);
4674
- if (ratio < SIMILARITY_THRESHOLD) continue;
4675
- const groupA = byName.get(keyA);
4676
- const groupB = byName.get(keyB);
4677
- for (const a of groupA) {
4678
- for (const b of groupB) {
4679
- addContradiction(a, b, ratio);
4680
- }
5079
+ }
5080
+ }
5081
+ function collectFuzzyMatches(byName, acc) {
5082
+ const keys = [...byName.keys()];
5083
+ for (let i = 0; i < keys.length; i++) {
5084
+ for (let j = i + 1; j < keys.length; j++) {
5085
+ const keyA = keys[i];
5086
+ const keyB = keys[j];
5087
+ const maxLen = Math.max(keyA.length, keyB.length);
5088
+ const minLen = Math.min(keyA.length, keyB.length);
5089
+ if (minLen / maxLen < SIMILARITY_THRESHOLD) continue;
5090
+ const ratio = levenshteinRatio(keyA, keyB);
5091
+ if (ratio < SIMILARITY_THRESHOLD) continue;
5092
+ const groupA = byName.get(keyA);
5093
+ const groupB = byName.get(keyB);
5094
+ for (const a of groupA) {
5095
+ for (const b of groupB) {
5096
+ tryAddContradiction(acc, a, b, ratio);
4681
5097
  }
4682
5098
  }
4683
5099
  }
4684
- return { contradictions, sourcePairCounts, totalChecked: nodes.length };
5100
+ }
5101
+ }
5102
+ var ContradictionDetector = class {
5103
+ detect(store) {
5104
+ const nodes = KNOWLEDGE_NODE_TYPES.flatMap((t) => store.findNodes({ type: t }));
5105
+ const byName = groupByName(nodes);
5106
+ const acc = {
5107
+ contradictions: [],
5108
+ sourcePairCounts: {},
5109
+ seen: /* @__PURE__ */ new Set()
5110
+ };
5111
+ collectExactMatches(byName, acc);
5112
+ collectFuzzyMatches(byName, acc);
5113
+ return {
5114
+ contradictions: acc.contradictions,
5115
+ sourcePairCounts: acc.sourcePairCounts,
5116
+ totalChecked: nodes.length
5117
+ };
4685
5118
  }
4686
5119
  };
4687
5120
 
@@ -4712,64 +5145,80 @@ function toGrade(score) {
4712
5145
  if (score >= 20) return "D";
4713
5146
  return "F";
4714
5147
  }
5148
+ function groupByDomain(nodes, _fallback, options = {}) {
5149
+ const map = /* @__PURE__ */ new Map();
5150
+ for (const node of nodes) {
5151
+ const domain = inferDomain(node, options);
5152
+ const group = map.get(domain) ?? [];
5153
+ group.push(node);
5154
+ map.set(domain, group);
5155
+ }
5156
+ return map;
5157
+ }
5158
+ function countLinkedEntities(codeEntries, store) {
5159
+ const linkedIds = /* @__PURE__ */ new Set();
5160
+ for (const codeNode of codeEntries) {
5161
+ if (hasKnowledgeEdge(codeNode.id, store)) {
5162
+ linkedIds.add(codeNode.id);
5163
+ }
5164
+ }
5165
+ return linkedIds;
5166
+ }
5167
+ function hasKnowledgeEdge(nodeId, store) {
5168
+ for (const edgeType of KNOWLEDGE_EDGE_TYPES) {
5169
+ if (store.getEdges({ to: nodeId, type: edgeType }).length > 0) return true;
5170
+ }
5171
+ return false;
5172
+ }
5173
+ function computeSourceBreakdown(knEntries) {
5174
+ const breakdown = {};
5175
+ for (const kn of knEntries) {
5176
+ const src = kn.metadata.source ?? "unknown";
5177
+ breakdown[src] = (breakdown[src] ?? 0) + 1;
5178
+ }
5179
+ return breakdown;
5180
+ }
5181
+ function computeDomainScore(knowledgeEntries, codeEntities, linkedEntities, uniqueSources) {
5182
+ const codeCoverageComponent = codeEntities > 0 ? linkedEntities / codeEntities * 60 : 0;
5183
+ const knowledgeDepthComponent = Math.min(knowledgeEntries / 10, 1) * 20;
5184
+ const sourceDiversityComponent = Math.min(uniqueSources / 3, 1) * 20;
5185
+ return Math.round(codeCoverageComponent + knowledgeDepthComponent + sourceDiversityComponent);
5186
+ }
5187
+ function scoreDomain(domain, knEntries, codeEntries, store) {
5188
+ const linkedIds = countLinkedEntities(codeEntries, store);
5189
+ const sourceBreakdown = computeSourceBreakdown(knEntries);
5190
+ const codeEntities = codeEntries.length;
5191
+ const linkedEntities = linkedIds.size;
5192
+ const knowledgeEntries = knEntries.length;
5193
+ const uniqueSources = Object.keys(sourceBreakdown).length;
5194
+ const score = computeDomainScore(knowledgeEntries, codeEntities, linkedEntities, uniqueSources);
5195
+ return {
5196
+ domain,
5197
+ score,
5198
+ knowledgeEntries,
5199
+ codeEntities,
5200
+ linkedEntities,
5201
+ unlinkedEntities: codeEntities - linkedEntities,
5202
+ sourceBreakdown,
5203
+ grade: toGrade(score)
5204
+ };
5205
+ }
4715
5206
  var CoverageScorer = class {
5207
+ constructor(inferenceOptions = {}) {
5208
+ this.inferenceOptions = inferenceOptions;
5209
+ }
5210
+ inferenceOptions;
4716
5211
  score(store) {
4717
5212
  const knowledgeNodes = KNOWLEDGE_TYPES.flatMap((t) => store.findNodes({ type: t }));
4718
- const domainMap = /* @__PURE__ */ new Map();
4719
- for (const node of knowledgeNodes) {
4720
- const domain = node.metadata.domain ?? "unclassified";
4721
- const group = domainMap.get(domain) ?? [];
4722
- group.push(node);
4723
- domainMap.set(domain, group);
4724
- }
5213
+ const domainMap = groupByDomain(knowledgeNodes, void 0, this.inferenceOptions);
4725
5214
  const codeNodes = CODE_TYPES2.flatMap((t) => store.findNodes({ type: t }));
4726
- const codeDomains = /* @__PURE__ */ new Map();
4727
- for (const node of codeNodes) {
4728
- const domain = node.metadata.domain ?? this.domainFromPath(node.path);
4729
- const group = codeDomains.get(domain) ?? [];
4730
- group.push(node);
4731
- codeDomains.set(domain, group);
4732
- }
5215
+ const codeDomains = groupByDomain(codeNodes, void 0, this.inferenceOptions);
4733
5216
  const allDomains = /* @__PURE__ */ new Set([...domainMap.keys(), ...codeDomains.keys()]);
4734
5217
  const domains = [];
4735
5218
  for (const domain of allDomains) {
4736
- const knEntries = domainMap.get(domain) ?? [];
4737
- const codeEntries = codeDomains.get(domain) ?? [];
4738
- const linkedIds = /* @__PURE__ */ new Set();
4739
- for (const codeNode of codeEntries) {
4740
- for (const edgeType of KNOWLEDGE_EDGE_TYPES) {
4741
- const edges = store.getEdges({ to: codeNode.id, type: edgeType });
4742
- if (edges.length > 0) {
4743
- linkedIds.add(codeNode.id);
4744
- break;
4745
- }
4746
- }
4747
- }
4748
- const sourceBreakdown = {};
4749
- for (const kn of knEntries) {
4750
- const src = kn.metadata.source ?? "unknown";
4751
- sourceBreakdown[src] = (sourceBreakdown[src] ?? 0) + 1;
4752
- }
4753
- const codeEntities = codeEntries.length;
4754
- const linkedEntities = linkedIds.size;
4755
- const knowledgeEntries = knEntries.length;
4756
- const uniqueSources = Object.keys(sourceBreakdown).length;
4757
- const codeCoverageComponent = codeEntities > 0 ? linkedEntities / codeEntities * 60 : 0;
4758
- const knowledgeDepthComponent = Math.min(knowledgeEntries / 10, 1) * 20;
4759
- const sourceDiversityComponent = Math.min(uniqueSources / 3, 1) * 20;
4760
- const score = Math.round(
4761
- codeCoverageComponent + knowledgeDepthComponent + sourceDiversityComponent
5219
+ domains.push(
5220
+ scoreDomain(domain, domainMap.get(domain) ?? [], codeDomains.get(domain) ?? [], store)
4762
5221
  );
4763
- domains.push({
4764
- domain,
4765
- score,
4766
- knowledgeEntries,
4767
- codeEntities,
4768
- linkedEntities,
4769
- unlinkedEntities: codeEntities - linkedEntities,
4770
- sourceBreakdown,
4771
- grade: toGrade(score)
4772
- });
4773
5222
  }
4774
5223
  const overallScore = domains.length > 0 ? Math.round(domains.reduce((sum, d) => sum + d.score, 0) / domains.length) : 0;
4775
5224
  return {
@@ -4779,19 +5228,157 @@ var CoverageScorer = class {
4779
5228
  generatedAt: (/* @__PURE__ */ new Date()).toISOString()
4780
5229
  };
4781
5230
  }
4782
- domainFromPath(filePath) {
4783
- if (!filePath) return "unclassified";
4784
- const parts = filePath.split("/");
4785
- const pkgIdx = parts.indexOf("packages");
4786
- if (pkgIdx >= 0 && parts[pkgIdx + 1]) return parts[pkgIdx + 1];
4787
- const srcIdx = parts.indexOf("src");
4788
- if (srcIdx >= 0 && parts[srcIdx + 1]) return parts[srcIdx + 1];
4789
- return parts[0] ?? "unclassified";
5231
+ };
5232
+
5233
+ // src/ingest/KnowledgeDocMaterializer.ts
5234
+ var fs10 = __toESM(require("fs/promises"));
5235
+ var path11 = __toESM(require("path"));
5236
+ var VALID_BUSINESS_TYPES = /* @__PURE__ */ new Set([
5237
+ "business_rule",
5238
+ "business_process",
5239
+ "business_concept",
5240
+ "business_term",
5241
+ "business_metric"
5242
+ ]);
5243
+ var DEFAULT_MAX_DOCS = 50;
5244
+ var MAX_COLLISION_SUFFIX = 10;
5245
+ var KnowledgeDocMaterializer = class {
5246
+ constructor(store, inferenceOptions = {}) {
5247
+ this.store = store;
5248
+ this.inferenceOptions = inferenceOptions;
5249
+ }
5250
+ store;
5251
+ inferenceOptions;
5252
+ async materialize(gapEntries, options) {
5253
+ const maxDocs = options.maxDocs ?? DEFAULT_MAX_DOCS;
5254
+ const created = [];
5255
+ const skipped = [];
5256
+ const createdNames = /* @__PURE__ */ new Set();
5257
+ for (const entry of gapEntries) {
5258
+ const resolved = await this.resolveEntry(
5259
+ entry,
5260
+ created.length,
5261
+ maxDocs,
5262
+ createdNames,
5263
+ options
5264
+ );
5265
+ if ("reason" in resolved) {
5266
+ skipped.push(resolved);
5267
+ continue;
5268
+ }
5269
+ const { node, domain, domainDir, nameKey } = resolved;
5270
+ await fs10.mkdir(domainDir, { recursive: true });
5271
+ const basename10 = this.generateFilename(entry.name);
5272
+ const filename = await this.resolveCollision(domainDir, basename10);
5273
+ const content = this.formatDoc(node, domain);
5274
+ const filePath = ["docs", "knowledge", domain, filename].join("/");
5275
+ await fs10.writeFile(path11.join(options.projectDir, filePath), content, "utf-8");
5276
+ createdNames.add(nameKey);
5277
+ created.push({ filePath, nodeId: entry.nodeId, domain, name: entry.name });
5278
+ }
5279
+ return { created, skipped };
5280
+ }
5281
+ async resolveEntry(entry, createdCount, maxDocs, createdNames, options) {
5282
+ if (!entry.hasContent) {
5283
+ return { nodeId: entry.nodeId, name: entry.name, reason: "no_content" };
5284
+ }
5285
+ const node = this.store.getNode(entry.nodeId);
5286
+ if (!node || !node.content || node.content.trim().length < 10) {
5287
+ return { nodeId: entry.nodeId, name: entry.name, reason: "no_content" };
5288
+ }
5289
+ const domain = this.inferDomain(node);
5290
+ if (!domain || /[/\\]|\.\.|\0/.test(domain)) {
5291
+ return { nodeId: entry.nodeId, name: entry.name, reason: "no_domain" };
5292
+ }
5293
+ if (createdCount >= maxDocs) {
5294
+ return { nodeId: entry.nodeId, name: entry.name, reason: "cap_reached" };
5295
+ }
5296
+ const domainDir = path11.join(options.projectDir, "docs", "knowledge", domain);
5297
+ const nameKey = `${domain}:${entry.name.toLowerCase().trim()}`;
5298
+ if (!createdNames.has(nameKey) && await this.hasExistingDoc(domainDir, entry.name)) {
5299
+ return { nodeId: entry.nodeId, name: entry.name, reason: "already_documented" };
5300
+ }
5301
+ if (options.dryRun) {
5302
+ return { nodeId: entry.nodeId, name: entry.name, reason: "dry_run" };
5303
+ }
5304
+ return { node, domain, domainDir, nameKey };
5305
+ }
5306
+ inferDomain(node) {
5307
+ const result = inferDomain(node, this.inferenceOptions);
5308
+ return result === "unknown" ? null : result;
5309
+ }
5310
+ generateFilename(name) {
5311
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
5312
+ return `${slug}.md`;
5313
+ }
5314
+ async resolveCollision(dir, basename10) {
5315
+ const ext = path11.extname(basename10);
5316
+ const stem = path11.basename(basename10, ext);
5317
+ try {
5318
+ await fs10.access(path11.join(dir, basename10));
5319
+ } catch {
5320
+ return basename10;
5321
+ }
5322
+ for (let i = 2; i <= MAX_COLLISION_SUFFIX; i++) {
5323
+ const candidate = `${stem}-${i}${ext}`;
5324
+ try {
5325
+ await fs10.access(path11.join(dir, candidate));
5326
+ } catch {
5327
+ return candidate;
5328
+ }
5329
+ }
5330
+ throw new Error(
5331
+ `Cannot resolve filename collision for "${basename10}" after ${MAX_COLLISION_SUFFIX} attempts`
5332
+ );
5333
+ }
5334
+ formatDoc(node, domain) {
5335
+ const mappedType = this.mapNodeType(node);
5336
+ const sanitize = (s) => s.replace(/[\n\r]/g, " ").replace(/:/g, "-");
5337
+ const lines = ["---", `type: ${sanitize(mappedType)}`, `domain: ${sanitize(domain)}`];
5338
+ const tags = node.metadata?.tags;
5339
+ if (Array.isArray(tags) && tags.length > 0) {
5340
+ lines.push(`tags: [${tags.map((t) => sanitize(String(t))).join(", ")}]`);
5341
+ }
5342
+ const related = node.metadata?.related;
5343
+ if (Array.isArray(related) && related.length > 0) {
5344
+ lines.push(`related: [${related.map((r) => sanitize(String(r))).join(", ")}]`);
5345
+ }
5346
+ const title = (node.name ?? "").replace(/[\n\r]/g, " ");
5347
+ lines.push("---", "", `# ${title}`, "", node.content ?? "", "");
5348
+ return lines.join("\n");
5349
+ }
5350
+ /** Check if a doc with a matching title already exists in the domain directory. */
5351
+ async hasExistingDoc(domainDir, name) {
5352
+ const normalizedName = name.toLowerCase().trim();
5353
+ try {
5354
+ const files = await fs10.readdir(domainDir);
5355
+ for (const file of files) {
5356
+ if (!file.endsWith(".md")) continue;
5357
+ const raw = await fs10.readFile(path11.join(domainDir, file), "utf-8");
5358
+ const fmMatch = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
5359
+ const body = fmMatch ? fmMatch[1] : raw;
5360
+ const titleMatch = body.match(/^#\s+(.+)$/m);
5361
+ if (titleMatch && titleMatch[1].trim().toLowerCase() === normalizedName) {
5362
+ return true;
5363
+ }
5364
+ }
5365
+ } catch {
5366
+ }
5367
+ return false;
5368
+ }
5369
+ mapNodeType(node) {
5370
+ if (VALID_BUSINESS_TYPES.has(node.type)) {
5371
+ return node.type;
5372
+ }
5373
+ if (node.type === "business_fact") {
5374
+ return "business_rule";
5375
+ }
5376
+ return "business_concept";
4790
5377
  }
4791
5378
  };
4792
5379
 
4793
5380
  // src/ingest/KnowledgePipelineRunner.ts
4794
- var BUSINESS_NODE_TYPES = [
5381
+ var BUSINESS_NODE_TYPES2 = [
4795
5382
  "business_concept",
4796
5383
  "business_rule",
4797
5384
  "business_process",
@@ -4800,7 +5387,8 @@ var BUSINESS_NODE_TYPES = [
4800
5387
  "business_fact"
4801
5388
  ];
4802
5389
  var SNAPSHOT_NODE_TYPES = [
4803
- ...BUSINESS_NODE_TYPES,
5390
+ ...BUSINESS_NODE_TYPES2,
5391
+ "decision",
4804
5392
  "design_token",
4805
5393
  "design_constraint",
4806
5394
  "aesthetic_intent",
@@ -4811,52 +5399,102 @@ var KnowledgePipelineRunner = class {
4811
5399
  this.store = store;
4812
5400
  }
4813
5401
  store;
5402
+ /** Resolved per-`run()` inference options. Set on entry to `run()`. */
5403
+ inferenceOptions = {};
4814
5404
  async run(options) {
4815
- const maxIterations = options.maxIterations ?? 5;
5405
+ this.inferenceOptions = options.inferenceOptions ?? {};
4816
5406
  const remediations = [];
4817
5407
  const preSnapshot = this.buildSnapshot(options.domain);
4818
5408
  const extraction = await this.extract(options);
4819
5409
  const postSnapshot = this.buildSnapshot(options.domain);
4820
5410
  let driftResult = this.reconcile(preSnapshot, postSnapshot);
4821
- const contradictionDetector = new ContradictionDetector();
4822
- const contradictions = contradictionDetector.detect(this.store);
5411
+ const contradictions = new ContradictionDetector().detect(this.store);
4823
5412
  let gapReport = await this.detect(options);
4824
- const coverageScorer = new CoverageScorer();
4825
- const coverage = coverageScorer.score(this.store);
5413
+ const coverage = new CoverageScorer(this.inferenceOptions).score(this.store);
5414
+ let materialization;
4826
5415
  let iterations = 1;
4827
5416
  if (options.fix) {
4828
- let previousFindingCount = driftResult.findings.length;
4829
- while (iterations < maxIterations) {
4830
- if (driftResult.findings.length === 0) break;
4831
- this.remediate(driftResult, remediations, options);
4832
- const rePreSnapshot = this.buildSnapshot(options.domain);
4833
- await this.extract(options);
4834
- const rePostSnapshot = this.buildSnapshot(options.domain);
4835
- driftResult = this.reconcile(rePreSnapshot, rePostSnapshot);
4836
- gapReport = await this.detect(options);
4837
- iterations++;
4838
- const newFindingCount = driftResult.findings.length;
4839
- if (newFindingCount >= previousFindingCount) break;
4840
- previousFindingCount = newFindingCount;
4841
- }
5417
+ const loopResult = await this.runRemediationLoop(
5418
+ options,
5419
+ driftResult,
5420
+ gapReport,
5421
+ remediations
5422
+ );
5423
+ iterations = loopResult.iterations;
5424
+ materialization = loopResult.materialization;
5425
+ }
5426
+ if (options.fix && iterations > 1) {
5427
+ const finalSnapshot = this.buildSnapshot(options.domain);
5428
+ driftResult = this.reconcile(preSnapshot, finalSnapshot);
5429
+ gapReport = await this.detect(options);
4842
5430
  }
4843
5431
  await this.stageNewFindings(driftResult, options);
5432
+ return this.buildResult(
5433
+ driftResult,
5434
+ iterations,
5435
+ extraction,
5436
+ gapReport,
5437
+ remediations,
5438
+ contradictions,
5439
+ coverage,
5440
+ materialization
5441
+ );
5442
+ }
5443
+ /** Run the remediation convergence loop; returns iteration count and accumulated materialization. */
5444
+ async runRemediationLoop(options, driftResult, gapReport, remediations) {
5445
+ const maxIterations = options.maxIterations ?? 5;
5446
+ let iterations = 1;
5447
+ let currentDrift = driftResult;
5448
+ let currentGapReport = gapReport;
5449
+ let previousIssueCount = currentDrift.findings.length + currentGapReport.totalGaps;
5450
+ let accumulatedMaterialization;
5451
+ while (iterations < maxIterations) {
5452
+ if (currentDrift.findings.length === 0 && currentGapReport.totalGaps === 0) break;
5453
+ const matResult = await this.remediate(currentDrift, remediations, options, currentGapReport);
5454
+ if (matResult) {
5455
+ if (!accumulatedMaterialization) {
5456
+ accumulatedMaterialization = matResult;
5457
+ } else {
5458
+ accumulatedMaterialization = {
5459
+ created: [...accumulatedMaterialization.created, ...matResult.created],
5460
+ skipped: [...accumulatedMaterialization.skipped, ...matResult.skipped]
5461
+ };
5462
+ }
5463
+ }
5464
+ const preSnapshot = this.buildSnapshot(options.domain);
5465
+ await this.extract(options);
5466
+ const postSnapshot = this.buildSnapshot(options.domain);
5467
+ currentDrift = this.reconcile(preSnapshot, postSnapshot);
5468
+ currentGapReport = await this.detect(options);
5469
+ iterations++;
5470
+ const currentIssueCount = currentDrift.findings.length + currentGapReport.totalGaps;
5471
+ if (currentIssueCount >= previousIssueCount) break;
5472
+ previousIssueCount = currentIssueCount;
5473
+ }
5474
+ return {
5475
+ iterations,
5476
+ ...accumulatedMaterialization ? { materialization: accumulatedMaterialization } : {}
5477
+ };
5478
+ }
5479
+ /** Assemble the final pipeline result. */
5480
+ buildResult(driftResult, iterations, extraction, gaps, remediations, contradictions, coverage, materialization) {
4844
5481
  return {
4845
5482
  verdict: this.computeVerdict(driftResult),
4846
5483
  driftScore: driftResult.driftScore,
4847
5484
  iterations,
4848
5485
  findings: driftResult.summary,
4849
5486
  extraction,
4850
- gaps: gapReport,
5487
+ gaps,
4851
5488
  remediations,
4852
5489
  contradictions,
4853
- coverage
5490
+ coverage,
5491
+ ...materialization ? { materialization } : {}
4854
5492
  };
4855
5493
  }
4856
5494
  // ── Phase 1: EXTRACT ──────────────────────────────────────────────────────
4857
5495
  async extract(options) {
4858
- const extractedDir = path10.join(options.projectDir, ".harness", "knowledge", "extracted");
4859
- await fs9.mkdir(extractedDir, { recursive: true });
5496
+ const extractedDir = path12.join(options.projectDir, ".harness", "knowledge", "extracted");
5497
+ await fs11.mkdir(extractedDir, { recursive: true });
4860
5498
  const runner = createExtractionRunner();
4861
5499
  const extractionResult = await runner.run(options.projectDir, this.store, extractedDir);
4862
5500
  const diagramParser = new DiagramParser(this.store);
@@ -4870,7 +5508,7 @@ var KnowledgePipelineRunner = class {
4870
5508
  const imageResult = await imageExtractor.analyze(this.store, imagePaths);
4871
5509
  imageCount = imageResult.nodesAdded;
4872
5510
  }
4873
- const knowledgeDir = path10.join(options.projectDir, "docs", "knowledge");
5511
+ const knowledgeDir = path12.join(options.projectDir, "docs", "knowledge");
4874
5512
  const bkIngestor = new BusinessKnowledgeIngestor(this.store);
4875
5513
  let bkResult;
4876
5514
  try {
@@ -4885,6 +5523,21 @@ var KnowledgePipelineRunner = class {
4885
5523
  durationMs: 0
4886
5524
  };
4887
5525
  }
5526
+ const decisionsDir = path12.join(options.projectDir, "docs", "knowledge", "decisions");
5527
+ const decisionIngestor = new DecisionIngestor(this.store);
5528
+ let decisionResult;
5529
+ try {
5530
+ decisionResult = await decisionIngestor.ingest(decisionsDir);
5531
+ } catch {
5532
+ decisionResult = {
5533
+ nodesAdded: 0,
5534
+ nodesUpdated: 0,
5535
+ edgesAdded: 0,
5536
+ edgesUpdated: 0,
5537
+ errors: [],
5538
+ durationMs: 0
5539
+ };
5540
+ }
4888
5541
  const linker = new KnowledgeLinker(this.store, extractedDir);
4889
5542
  const linkResult = await linker.link();
4890
5543
  return {
@@ -4892,6 +5545,7 @@ var KnowledgePipelineRunner = class {
4892
5545
  diagrams: diagramResult.nodesAdded,
4893
5546
  linkerFacts: linkResult.factsCreated,
4894
5547
  businessKnowledge: bkResult.nodesAdded,
5548
+ decisions: decisionResult.nodesAdded,
4895
5549
  images: imageCount
4896
5550
  };
4897
5551
  }
@@ -4918,14 +5572,14 @@ var KnowledgePipelineRunner = class {
4918
5572
  }
4919
5573
  // ── Phase 3: DETECT ───────────────────────────────────────────────────────
4920
5574
  async detect(options) {
4921
- const knowledgeDir = path10.join(options.projectDir, "docs", "knowledge");
4922
- const aggregator = new KnowledgeStagingAggregator(options.projectDir);
4923
- const gapReport = await aggregator.generateGapReport(knowledgeDir);
5575
+ const knowledgeDir = path12.join(options.projectDir, "docs", "knowledge");
5576
+ const aggregator = new KnowledgeStagingAggregator(options.projectDir, this.inferenceOptions);
5577
+ const gapReport = await aggregator.generateGapReport(knowledgeDir, this.store);
4924
5578
  await aggregator.writeGapReport(gapReport);
4925
5579
  return gapReport;
4926
5580
  }
4927
5581
  // ── Phase 4: REMEDIATE ────────────────────────────────────────────────────
4928
- remediate(driftResult, remediations, options) {
5582
+ async remediate(driftResult, remediations, options, gapReport) {
4929
5583
  for (const finding of driftResult.findings) {
4930
5584
  switch (finding.classification) {
4931
5585
  case "stale":
@@ -4943,6 +5597,22 @@ var KnowledgePipelineRunner = class {
4943
5597
  break;
4944
5598
  }
4945
5599
  }
5600
+ if (!options.ci) {
5601
+ const allGapEntries = gapReport.domains.flatMap((d) => d.gapEntries);
5602
+ const materializable = allGapEntries.filter((e) => e.hasContent);
5603
+ if (materializable.length > 0) {
5604
+ const materializer = new KnowledgeDocMaterializer(this.store, this.inferenceOptions);
5605
+ const matResult = await materializer.materialize(materializable, {
5606
+ projectDir: options.projectDir,
5607
+ dryRun: false
5608
+ });
5609
+ for (const doc of matResult.created) {
5610
+ remediations.push(`created doc: ${doc.filePath}`);
5611
+ }
5612
+ return matResult;
5613
+ }
5614
+ }
5615
+ return void 0;
4946
5616
  }
4947
5617
  async stageNewFindings(driftResult, options) {
4948
5618
  const newFindings = driftResult.findings.filter((f) => f.classification === "new");
@@ -4957,7 +5627,7 @@ var KnowledgePipelineRunner = class {
4957
5627
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
4958
5628
  }));
4959
5629
  if (stagedEntries.length > 0) {
4960
- const aggregator = new KnowledgeStagingAggregator(options.projectDir);
5630
+ const aggregator = new KnowledgeStagingAggregator(options.projectDir, this.inferenceOptions);
4961
5631
  await aggregator.aggregate(stagedEntries, [], []);
4962
5632
  }
4963
5633
  }
@@ -4977,7 +5647,7 @@ var KnowledgePipelineRunner = class {
4977
5647
  };
4978
5648
 
4979
5649
  // src/ingest/connectors/ConnectorUtils.ts
4980
- var CODE_NODE_TYPES4 = ["file", "function", "class", "method", "interface", "variable"];
5650
+ var CODE_NODE_TYPES5 = ["file", "function", "class", "method", "interface", "variable"];
4981
5651
  var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
4982
5652
  function withRetry(client, options) {
4983
5653
  const maxRetries = options?.maxRetries ?? 3;
@@ -5042,7 +5712,7 @@ function sanitizeExternalText(text, maxLength = 2e3) {
5042
5712
  }
5043
5713
  function linkToCode(store, content, sourceNodeId, edgeType, options) {
5044
5714
  let edgesCreated = 0;
5045
- for (const type of CODE_NODE_TYPES4) {
5715
+ for (const type of CODE_NODE_TYPES5) {
5046
5716
  const nodes = store.findNodes({ type });
5047
5717
  for (const node of nodes) {
5048
5718
  if (node.name.length < 3) continue;
@@ -5062,12 +5732,12 @@ function linkToCode(store, content, sourceNodeId, edgeType, options) {
5062
5732
  }
5063
5733
 
5064
5734
  // src/ingest/connectors/SyncManager.ts
5065
- var fs10 = __toESM(require("fs/promises"));
5066
- var path11 = __toESM(require("path"));
5735
+ var fs12 = __toESM(require("fs/promises"));
5736
+ var path13 = __toESM(require("path"));
5067
5737
  var SyncManager = class {
5068
5738
  constructor(store, graphDir) {
5069
5739
  this.store = store;
5070
- this.metadataPath = path11.join(graphDir, "sync-metadata.json");
5740
+ this.metadataPath = path13.join(graphDir, "sync-metadata.json");
5071
5741
  }
5072
5742
  store;
5073
5743
  registrations = /* @__PURE__ */ new Map();
@@ -5132,15 +5802,15 @@ var SyncManager = class {
5132
5802
  }
5133
5803
  async loadMetadata() {
5134
5804
  try {
5135
- const raw = await fs10.readFile(this.metadataPath, "utf-8");
5805
+ const raw = await fs12.readFile(this.metadataPath, "utf-8");
5136
5806
  return JSON.parse(raw);
5137
5807
  } catch {
5138
5808
  return { connectors: {} };
5139
5809
  }
5140
5810
  }
5141
5811
  async saveMetadata(metadata) {
5142
- await fs10.mkdir(path11.dirname(this.metadataPath), { recursive: true });
5143
- await fs10.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
5812
+ await fs12.mkdir(path13.dirname(this.metadataPath), { recursive: true });
5813
+ await fs12.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
5144
5814
  }
5145
5815
  };
5146
5816
 
@@ -5245,6 +5915,14 @@ var JiraConnector = class {
5245
5915
  start
5246
5916
  );
5247
5917
  }
5918
+ try {
5919
+ const parsed = new URL(baseUrl);
5920
+ if (parsed.protocol !== "https:" && parsed.hostname !== "localhost") {
5921
+ return buildIngestResult(0, 0, [`${baseUrlEnv} must use HTTPS`], start);
5922
+ }
5923
+ } catch {
5924
+ return buildIngestResult(0, 0, [`${baseUrlEnv} is not a valid URL`], start);
5925
+ }
5248
5926
  const jql = buildJql(config);
5249
5927
  const headers = { Authorization: `Basic ${apiKey}`, "Content-Type": "application/json" };
5250
5928
  try {
@@ -5534,6 +6212,14 @@ var ConfluenceConnector = class {
5534
6212
  }
5535
6213
  const baseUrlEnv = config.baseUrlEnv ?? "CONFLUENCE_BASE_URL";
5536
6214
  const baseUrl = process.env[baseUrlEnv] ?? "";
6215
+ try {
6216
+ const parsed = new URL(baseUrl);
6217
+ if (parsed.protocol !== "https:" && parsed.hostname !== "localhost") {
6218
+ return missingApiKeyResult(`${baseUrlEnv} must use HTTPS`, start);
6219
+ }
6220
+ } catch {
6221
+ return missingApiKeyResult(`${baseUrlEnv} is not a valid URL`, start);
6222
+ }
5537
6223
  const spaceKey = config.spaceKey ?? "";
5538
6224
  const counts = await this.fetchAllPagesHandled(
5539
6225
  store,
@@ -5627,7 +6313,7 @@ var ConfluenceConnector = class {
5627
6313
  };
5628
6314
 
5629
6315
  // src/ingest/connectors/CIConnector.ts
5630
- function emptyResult2(errors, start) {
6316
+ function emptyResult5(errors, start) {
5631
6317
  return {
5632
6318
  nodesAdded: 0,
5633
6319
  nodesUpdated: 0,
@@ -5695,7 +6381,7 @@ var CIConnector = class {
5695
6381
  const apiKeyEnv = config.apiKeyEnv ?? "GITHUB_TOKEN";
5696
6382
  const apiKey = process.env[apiKeyEnv];
5697
6383
  if (!apiKey) {
5698
- return emptyResult2(
6384
+ return emptyResult5(
5699
6385
  [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
5700
6386
  start
5701
6387
  );
@@ -5759,6 +6445,14 @@ function hasConstraintKeyword(text) {
5759
6445
  const lower = text.toLowerCase();
5760
6446
  return CONSTRAINT_KEYWORDS.some((kw) => lower.includes(kw));
5761
6447
  }
6448
+ function buildNodeMetadata(condensed, base) {
6449
+ const metadata = { ...base };
6450
+ if (condensed.method !== "passthrough") {
6451
+ metadata.condensed = condensed.method;
6452
+ metadata.originalLength = condensed.originalLength;
6453
+ }
6454
+ return metadata;
6455
+ }
5762
6456
  function buildIngestResult2(nodesAdded, edgesAdded, errors, start) {
5763
6457
  return {
5764
6458
  nodesAdded,
@@ -5769,6 +6463,24 @@ function buildIngestResult2(nodesAdded, edgesAdded, errors, start) {
5769
6463
  durationMs: Date.now() - start
5770
6464
  };
5771
6465
  }
6466
+ function validateFigmaConfig(config) {
6467
+ const apiKeyEnv = config.apiKeyEnv ?? "FIGMA_API_KEY";
6468
+ const apiKey = process.env[apiKeyEnv];
6469
+ if (!apiKey) return { error: `Missing API key: environment variable "${apiKeyEnv}" is not set` };
6470
+ const baseUrlEnv = config.baseUrlEnv ?? "FIGMA_BASE_URL";
6471
+ const baseUrl = process.env[baseUrlEnv] ?? "https://api.figma.com";
6472
+ try {
6473
+ const parsed = new URL(baseUrl);
6474
+ if (parsed.protocol !== "https:" || !parsed.hostname.endsWith("api.figma.com")) {
6475
+ return { error: `Invalid ${baseUrlEnv}: must be an HTTPS URL on api.figma.com` };
6476
+ }
6477
+ } catch {
6478
+ return { error: `Invalid ${baseUrlEnv}: not a valid URL` };
6479
+ }
6480
+ const fileIds = config.fileIds;
6481
+ if (!fileIds || fileIds.length === 0) return { error: "No fileIds provided in connector config" };
6482
+ return { apiKey, baseUrl, fileIds };
6483
+ }
5772
6484
  var FigmaConnector = class {
5773
6485
  name = "figma";
5774
6486
  source = "figma";
@@ -5778,35 +6490,9 @@ var FigmaConnector = class {
5778
6490
  }
5779
6491
  async ingest(store, config) {
5780
6492
  const start = Date.now();
5781
- const apiKeyEnv = config.apiKeyEnv ?? "FIGMA_API_KEY";
5782
- const apiKey = process.env[apiKeyEnv];
5783
- if (!apiKey) {
5784
- return buildIngestResult2(
5785
- 0,
5786
- 0,
5787
- [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
5788
- start
5789
- );
5790
- }
5791
- const baseUrlEnv = config.baseUrlEnv ?? "FIGMA_BASE_URL";
5792
- const baseUrl = process.env[baseUrlEnv] ?? "https://api.figma.com";
5793
- try {
5794
- const parsed = new URL(baseUrl);
5795
- if (parsed.protocol !== "https:" || !parsed.hostname.endsWith("api.figma.com")) {
5796
- return buildIngestResult2(
5797
- 0,
5798
- 0,
5799
- [`Invalid ${baseUrlEnv}: must be an HTTPS URL on api.figma.com`],
5800
- start
5801
- );
5802
- }
5803
- } catch {
5804
- return buildIngestResult2(0, 0, [`Invalid ${baseUrlEnv}: not a valid URL`], start);
5805
- }
5806
- const fileIds = config.fileIds;
5807
- if (!fileIds || fileIds.length === 0) {
5808
- return buildIngestResult2(0, 0, ["No fileIds provided in connector config"], start);
5809
- }
6493
+ const validated = validateFigmaConfig(config);
6494
+ if ("error" in validated) return buildIngestResult2(0, 0, [validated.error], start);
6495
+ const { apiKey, baseUrl, fileIds } = validated;
5810
6496
  const headers = { "X-FIGMA-TOKEN": apiKey };
5811
6497
  const maxLen = config.maxContentLength ?? 4e3;
5812
6498
  let nodesAdded = 0;
@@ -5826,6 +6512,17 @@ var FigmaConnector = class {
5826
6512
  return buildIngestResult2(nodesAdded, edgesAdded, errors, start);
5827
6513
  }
5828
6514
  async processFile(store, baseUrl, fileId, headers, maxLen) {
6515
+ let nodesAdded = 0;
6516
+ let edgesAdded = 0;
6517
+ const styleCounts = await this.ingestStyles(store, baseUrl, fileId, headers, maxLen);
6518
+ nodesAdded += styleCounts.nodesAdded;
6519
+ edgesAdded += styleCounts.edgesAdded;
6520
+ const componentCounts = await this.ingestComponents(store, baseUrl, fileId, headers, maxLen);
6521
+ nodesAdded += componentCounts.nodesAdded;
6522
+ edgesAdded += componentCounts.edgesAdded;
6523
+ return { nodesAdded, edgesAdded };
6524
+ }
6525
+ async ingestStyles(store, baseUrl, fileId, headers, maxLen) {
5829
6526
  let nodesAdded = 0;
5830
6527
  let edgesAdded = 0;
5831
6528
  const stylesUrl = `${baseUrl}/v1/files/${fileId}/styles`;
@@ -5833,90 +6530,112 @@ var FigmaConnector = class {
5833
6530
  if (!stylesResponse.ok) throw new Error(`Styles request failed for file ${fileId}`);
5834
6531
  const stylesData = await stylesResponse.json();
5835
6532
  for (const style of stylesData.meta.styles) {
5836
- const nodeId = `figma:token:${style.key}`;
5837
- const condensed = await condenseContent(sanitizeExternalText(style.description || "", 2e3), {
5838
- maxLength: maxLen
5839
- });
5840
- const metadata = {
5841
- source: "figma",
5842
- key: style.key,
5843
- styleType: style.style_type,
5844
- fileId
5845
- };
5846
- if (condensed.method !== "passthrough") {
5847
- metadata.condensed = condensed.method;
5848
- metadata.originalLength = condensed.originalLength;
5849
- }
5850
- store.addNode({
5851
- id: nodeId,
5852
- type: "design_token",
5853
- name: sanitizeExternalText(style.name, 500),
5854
- content: condensed.content,
5855
- metadata
5856
- });
5857
- nodesAdded++;
5858
- const searchText = sanitizeExternalText([style.name, style.description].join(" "));
5859
- edgesAdded += linkToCode(store, searchText, nodeId, "references");
6533
+ const counts = await this.processStyle(store, style, fileId, maxLen);
6534
+ nodesAdded += counts.nodesAdded;
6535
+ edgesAdded += counts.edgesAdded;
5860
6536
  }
6537
+ return { nodesAdded, edgesAdded };
6538
+ }
6539
+ async processStyle(store, style, fileId, maxLen) {
6540
+ const nodeId = `figma:token:${style.key}`;
6541
+ const condensed = await condenseContent(sanitizeExternalText(style.description || "", 2e3), {
6542
+ maxLength: maxLen
6543
+ });
6544
+ const metadata = buildNodeMetadata(condensed, {
6545
+ source: "figma",
6546
+ key: style.key,
6547
+ styleType: style.style_type,
6548
+ fileId
6549
+ });
6550
+ store.addNode({
6551
+ id: nodeId,
6552
+ type: "design_token",
6553
+ name: sanitizeExternalText(style.name, 500),
6554
+ content: condensed.content,
6555
+ metadata
6556
+ });
6557
+ const searchText = sanitizeExternalText([style.name, style.description].join(" "));
6558
+ const edgesAdded = linkToCode(store, searchText, nodeId, "references");
6559
+ return { nodesAdded: 1, edgesAdded };
6560
+ }
6561
+ async ingestComponents(store, baseUrl, fileId, headers, maxLen) {
6562
+ let nodesAdded = 0;
6563
+ let edgesAdded = 0;
5861
6564
  const componentsUrl = `${baseUrl}/v1/files/${fileId}/components`;
5862
6565
  const componentsResponse = await this.httpClient(componentsUrl, { headers });
5863
6566
  if (!componentsResponse.ok) throw new Error(`Components request failed for file ${fileId}`);
5864
6567
  const componentsData = await componentsResponse.json();
5865
6568
  for (const component of componentsData.meta.components) {
5866
- const description = component.description || "";
5867
- if (description) {
5868
- const intentId = `figma:intent:${component.key}`;
5869
- const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
5870
- maxLength: maxLen
5871
- });
5872
- const metadata = {
5873
- source: "figma",
5874
- key: component.key,
5875
- fileId
5876
- };
5877
- if (condensed.method !== "passthrough") {
5878
- metadata.condensed = condensed.method;
5879
- metadata.originalLength = condensed.originalLength;
5880
- }
5881
- store.addNode({
5882
- id: intentId,
5883
- type: "aesthetic_intent",
5884
- name: sanitizeExternalText(component.name, 500),
5885
- content: condensed.content,
5886
- metadata
5887
- });
5888
- nodesAdded++;
5889
- const searchText = sanitizeExternalText([component.name, description].join(" "));
5890
- edgesAdded += linkToCode(store, searchText, intentId, "references");
5891
- }
5892
- if (description && hasConstraintKeyword(description)) {
5893
- const constraintId = `figma:constraint:${component.key}`;
5894
- const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
5895
- maxLength: maxLen
5896
- });
5897
- const metadata = {
5898
- source: "figma",
5899
- key: component.key,
5900
- fileId
5901
- };
5902
- if (condensed.method !== "passthrough") {
5903
- metadata.condensed = condensed.method;
5904
- metadata.originalLength = condensed.originalLength;
5905
- }
5906
- store.addNode({
5907
- id: constraintId,
5908
- type: "design_constraint",
5909
- name: sanitizeExternalText(component.name, 500),
5910
- content: condensed.content,
5911
- metadata
5912
- });
5913
- nodesAdded++;
5914
- const searchText = sanitizeExternalText([component.name, description].join(" "));
5915
- edgesAdded += linkToCode(store, searchText, constraintId, "references");
5916
- }
6569
+ const counts = await this.processComponent(store, component, fileId, maxLen);
6570
+ nodesAdded += counts.nodesAdded;
6571
+ edgesAdded += counts.edgesAdded;
5917
6572
  }
5918
6573
  return { nodesAdded, edgesAdded };
5919
6574
  }
6575
+ async processComponent(store, component, fileId, maxLen) {
6576
+ let nodesAdded = 0;
6577
+ let edgesAdded = 0;
6578
+ const description = component.description || "";
6579
+ if (description) {
6580
+ const counts = await this.addComponentIntent(store, component, description, fileId, maxLen);
6581
+ nodesAdded += counts.nodesAdded;
6582
+ edgesAdded += counts.edgesAdded;
6583
+ }
6584
+ if (description && hasConstraintKeyword(description)) {
6585
+ const counts = await this.addComponentConstraint(
6586
+ store,
6587
+ component,
6588
+ description,
6589
+ fileId,
6590
+ maxLen
6591
+ );
6592
+ nodesAdded += counts.nodesAdded;
6593
+ edgesAdded += counts.edgesAdded;
6594
+ }
6595
+ return { nodesAdded, edgesAdded };
6596
+ }
6597
+ async addComponentIntent(store, component, description, fileId, maxLen) {
6598
+ const intentId = `figma:intent:${component.key}`;
6599
+ const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
6600
+ maxLength: maxLen
6601
+ });
6602
+ const metadata = buildNodeMetadata(condensed, {
6603
+ source: "figma",
6604
+ key: component.key,
6605
+ fileId
6606
+ });
6607
+ store.addNode({
6608
+ id: intentId,
6609
+ type: "aesthetic_intent",
6610
+ name: sanitizeExternalText(component.name, 500),
6611
+ content: condensed.content,
6612
+ metadata
6613
+ });
6614
+ const searchText = sanitizeExternalText([component.name, description].join(" "));
6615
+ const edgesAdded = linkToCode(store, searchText, intentId, "references");
6616
+ return { nodesAdded: 1, edgesAdded };
6617
+ }
6618
+ async addComponentConstraint(store, component, description, fileId, maxLen) {
6619
+ const constraintId = `figma:constraint:${component.key}`;
6620
+ const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
6621
+ maxLength: maxLen
6622
+ });
6623
+ const metadata = buildNodeMetadata(condensed, {
6624
+ source: "figma",
6625
+ key: component.key,
6626
+ fileId
6627
+ });
6628
+ store.addNode({
6629
+ id: constraintId,
6630
+ type: "design_constraint",
6631
+ name: sanitizeExternalText(component.name, 500),
6632
+ content: condensed.content,
6633
+ metadata
6634
+ });
6635
+ const searchText = sanitizeExternalText([component.name, description].join(" "));
6636
+ const edgesAdded = linkToCode(store, searchText, constraintId, "references");
6637
+ return { nodesAdded: 1, edgesAdded };
6638
+ }
5920
6639
  };
5921
6640
 
5922
6641
  // src/ingest/connectors/MiroConnector.ts
@@ -5935,6 +6654,46 @@ function buildIngestResult3(nodesAdded, edgesAdded, errors, start) {
5935
6654
  durationMs: Date.now() - start
5936
6655
  };
5937
6656
  }
6657
+ function validateBaseUrl(baseUrl, baseUrlEnv) {
6658
+ try {
6659
+ const parsed = new URL(baseUrl);
6660
+ if (parsed.protocol !== "https:" || parsed.hostname !== "api.miro.com") {
6661
+ return `Invalid ${baseUrlEnv}: must be an HTTPS URL on api.miro.com`;
6662
+ }
6663
+ } catch {
6664
+ return `Invalid ${baseUrlEnv}: not a valid URL`;
6665
+ }
6666
+ return null;
6667
+ }
6668
+ function resolveConfig(config, start) {
6669
+ const apiKeyEnv = config.apiKeyEnv ?? "MIRO_API_KEY";
6670
+ const apiKey = process.env[apiKeyEnv];
6671
+ if (!apiKey) {
6672
+ return {
6673
+ ok: false,
6674
+ result: buildIngestResult3(
6675
+ 0,
6676
+ 0,
6677
+ [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
6678
+ start
6679
+ )
6680
+ };
6681
+ }
6682
+ const baseUrlEnv = config.baseUrlEnv ?? "MIRO_BASE_URL";
6683
+ const baseUrl = process.env[baseUrlEnv] ?? "https://api.miro.com";
6684
+ const urlError = validateBaseUrl(baseUrl, baseUrlEnv);
6685
+ if (urlError) {
6686
+ return { ok: false, result: buildIngestResult3(0, 0, [urlError], start) };
6687
+ }
6688
+ const boardIds = config.boardIds;
6689
+ if (!boardIds || boardIds.length === 0) {
6690
+ return {
6691
+ ok: false,
6692
+ result: buildIngestResult3(0, 0, ["No boardIds provided in config"], start)
6693
+ };
6694
+ }
6695
+ return { ok: true, apiKey, baseUrl, boardIds, headers: { Authorization: `Bearer ${apiKey}` } };
6696
+ }
5938
6697
  var MiroConnector = class {
5939
6698
  name = "miro";
5940
6699
  source = "miro";
@@ -5944,36 +6703,9 @@ var MiroConnector = class {
5944
6703
  }
5945
6704
  async ingest(store, config) {
5946
6705
  const start = Date.now();
5947
- const apiKeyEnv = config.apiKeyEnv ?? "MIRO_API_KEY";
5948
- const apiKey = process.env[apiKeyEnv];
5949
- if (!apiKey) {
5950
- return buildIngestResult3(
5951
- 0,
5952
- 0,
5953
- [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
5954
- start
5955
- );
5956
- }
5957
- const baseUrlEnv = config.baseUrlEnv ?? "MIRO_BASE_URL";
5958
- const baseUrl = process.env[baseUrlEnv] ?? "https://api.miro.com";
5959
- try {
5960
- const parsed = new URL(baseUrl);
5961
- if (parsed.protocol !== "https:" || parsed.hostname !== "api.miro.com") {
5962
- return buildIngestResult3(
5963
- 0,
5964
- 0,
5965
- [`Invalid ${baseUrlEnv}: must be an HTTPS URL on api.miro.com`],
5966
- start
5967
- );
5968
- }
5969
- } catch {
5970
- return buildIngestResult3(0, 0, [`Invalid ${baseUrlEnv}: not a valid URL`], start);
5971
- }
5972
- const boardIds = config.boardIds;
5973
- if (!boardIds || boardIds.length === 0) {
5974
- return buildIngestResult3(0, 0, ["No boardIds provided in config"], start);
5975
- }
5976
- const headers = { Authorization: `Bearer ${apiKey}` };
6706
+ const resolved = resolveConfig(config, start);
6707
+ if (!resolved.ok) return resolved.result;
6708
+ const { baseUrl, boardIds, headers } = resolved;
5977
6709
  let totalNodesAdded = 0;
5978
6710
  let totalEdgesAdded = 0;
5979
6711
  const errors = [];
@@ -6177,7 +6909,7 @@ var FusionLayer = class {
6177
6909
  };
6178
6910
 
6179
6911
  // src/entropy/GraphEntropyAdapter.ts
6180
- var CODE_NODE_TYPES5 = ["file", "function", "class", "method", "interface", "variable"];
6912
+ var CODE_NODE_TYPES6 = ["file", "function", "class", "method", "interface", "variable"];
6181
6913
  var GraphEntropyAdapter = class {
6182
6914
  constructor(store) {
6183
6915
  this.store = store;
@@ -6257,7 +6989,7 @@ var GraphEntropyAdapter = class {
6257
6989
  }
6258
6990
  findEntryPoints() {
6259
6991
  const entryPoints = [];
6260
- for (const nodeType of CODE_NODE_TYPES5) {
6992
+ for (const nodeType of CODE_NODE_TYPES6) {
6261
6993
  const nodes = this.store.findNodes({ type: nodeType });
6262
6994
  for (const node of nodes) {
6263
6995
  const isIndexFile = nodeType === "file" && node.name === "index.ts";
@@ -6293,7 +7025,7 @@ var GraphEntropyAdapter = class {
6293
7025
  }
6294
7026
  collectUnreachableNodes(visited) {
6295
7027
  const unreachableNodes = [];
6296
- for (const nodeType of CODE_NODE_TYPES5) {
7028
+ for (const nodeType of CODE_NODE_TYPES6) {
6297
7029
  const nodes = this.store.findNodes({ type: nodeType });
6298
7030
  for (const node of nodes) {
6299
7031
  if (!visited.has(node.id)) {
@@ -7139,9 +7871,9 @@ var EntityExtractor = class {
7139
7871
  extractPaths(trimmed, add) {
7140
7872
  const consumed = /* @__PURE__ */ new Set();
7141
7873
  for (const match of trimmed.matchAll(FILE_PATH_RE)) {
7142
- const path13 = match[0];
7143
- add(path13);
7144
- consumed.add(path13);
7874
+ const path15 = match[0];
7875
+ add(path15);
7876
+ consumed.add(path15);
7145
7877
  }
7146
7878
  return consumed;
7147
7879
  }
@@ -7213,8 +7945,8 @@ var EntityResolver = class {
7213
7945
  if (isPathLike && node.path.includes(raw)) {
7214
7946
  return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
7215
7947
  }
7216
- const basename7 = node.path.split("/").pop() ?? "";
7217
- if (basename7.includes(raw)) {
7948
+ const basename10 = node.path.split("/").pop() ?? "";
7949
+ if (basename10.includes(raw)) {
7218
7950
  return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
7219
7951
  }
7220
7952
  if (raw.length >= 4 && node.path.includes(raw)) {
@@ -7293,13 +8025,13 @@ var ResponseFormatter = class {
7293
8025
  const context = Array.isArray(d?.context) ? d.context : [];
7294
8026
  const firstEntity = entities[0];
7295
8027
  const nodeType = firstEntity?.node.type ?? "node";
7296
- const path13 = firstEntity?.node.path ?? "unknown";
8028
+ const path15 = firstEntity?.node.path ?? "unknown";
7297
8029
  let neighborCount = 0;
7298
8030
  const firstContext = context[0];
7299
8031
  if (firstContext && Array.isArray(firstContext.nodes)) {
7300
8032
  neighborCount = firstContext.nodes.length;
7301
8033
  }
7302
- return `**${entityName}** is a ${nodeType} at \`${path13}\`. Connected to ${neighborCount} nodes.`;
8034
+ return `**${entityName}** is a ${nodeType} at \`${path15}\`. Connected to ${neighborCount} nodes.`;
7303
8035
  }
7304
8036
  formatAnomaly(data) {
7305
8037
  const d = data;
@@ -7682,7 +8414,7 @@ var PHASE_NODE_TYPES = {
7682
8414
  debug: ["failure", "learning", "function", "method"],
7683
8415
  plan: ["adr", "document", "module", "layer"]
7684
8416
  };
7685
- var CODE_NODE_TYPES6 = /* @__PURE__ */ new Set([
8417
+ var CODE_NODE_TYPES7 = /* @__PURE__ */ new Set([
7686
8418
  "file",
7687
8419
  "function",
7688
8420
  "class",
@@ -7901,7 +8633,7 @@ var Assembler = class {
7901
8633
  */
7902
8634
  checkCoverage() {
7903
8635
  const codeNodes = [];
7904
- for (const type of CODE_NODE_TYPES6) {
8636
+ for (const type of CODE_NODE_TYPES7) {
7905
8637
  codeNodes.push(...this.store.findNodes({ type }));
7906
8638
  }
7907
8639
  const documented = [];
@@ -8081,14 +8813,14 @@ var GraphConstraintAdapter = class {
8081
8813
  };
8082
8814
 
8083
8815
  // src/ingest/DesignIngestor.ts
8084
- var fs11 = __toESM(require("fs/promises"));
8085
- var path12 = __toESM(require("path"));
8816
+ var fs13 = __toESM(require("fs/promises"));
8817
+ var path14 = __toESM(require("path"));
8086
8818
  function isDTCGToken(obj) {
8087
8819
  return typeof obj === "object" && obj !== null && "$value" in obj && "$type" in obj;
8088
8820
  }
8089
8821
  async function readFileOrNull(filePath) {
8090
8822
  try {
8091
- return await fs11.readFile(filePath, "utf-8");
8823
+ return await fs13.readFile(filePath, "utf-8");
8092
8824
  } catch {
8093
8825
  return null;
8094
8826
  }
@@ -8234,8 +8966,8 @@ var DesignIngestor = class {
8234
8966
  async ingestAll(designDir) {
8235
8967
  const start = Date.now();
8236
8968
  const [tokensResult, intentResult] = await Promise.all([
8237
- this.ingestTokens(path12.join(designDir, "tokens.json")),
8238
- this.ingestDesignIntent(path12.join(designDir, "DESIGN.md"))
8969
+ this.ingestTokens(path14.join(designDir, "tokens.json")),
8970
+ this.ingestDesignIntent(path14.join(designDir, "DESIGN.md"))
8239
8971
  ]);
8240
8972
  const merged = mergeResults(tokensResult, intentResult);
8241
8973
  return { ...merged, durationMs: Date.now() - start };
@@ -8471,10 +9203,10 @@ var TaskIndependenceAnalyzer = class {
8471
9203
  includeTypes: ["file"]
8472
9204
  });
8473
9205
  for (const n of queryResult.nodes) {
8474
- const path13 = n.path ?? n.id.replace(/^file:/, "");
8475
- if (!fileSet.has(path13)) {
8476
- if (!result.has(path13)) {
8477
- result.set(path13, file);
9206
+ const path15 = n.path ?? n.id.replace(/^file:/, "");
9207
+ if (!fileSet.has(path15)) {
9208
+ if (!result.has(path15)) {
9209
+ result.set(path15, file);
8478
9210
  }
8479
9211
  }
8480
9212
  }
@@ -8851,7 +9583,7 @@ var ConflictPredictor = class {
8851
9583
  };
8852
9584
 
8853
9585
  // src/index.ts
8854
- var VERSION = "0.4.3";
9586
+ var VERSION = "0.6.0";
8855
9587
  // Annotate the CommonJS export names for ESM import in node:
8856
9588
  0 && (module.exports = {
8857
9589
  ALL_EXTRACTORS,
@@ -8869,6 +9601,9 @@ var VERSION = "0.4.3";
8869
9601
  ContradictionDetector,
8870
9602
  CoverageScorer,
8871
9603
  D2Parser,
9604
+ DEFAULT_BLOCKLIST,
9605
+ DEFAULT_PATTERNS,
9606
+ DecisionIngestor,
8872
9607
  DesignConstraintAdapter,
8873
9608
  DesignIngestor,
8874
9609
  DiagramParser,
@@ -8893,6 +9628,7 @@ var VERSION = "0.4.3";
8893
9628
  ImageAnalysisExtractor,
8894
9629
  IntentClassifier,
8895
9630
  JiraConnector,
9631
+ KnowledgeDocMaterializer,
8896
9632
  KnowledgeIngestor,
8897
9633
  KnowledgePipelineRunner,
8898
9634
  KnowledgeStagingAggregator,
@@ -8919,6 +9655,7 @@ var VERSION = "0.4.3";
8919
9655
  createExtractionRunner,
8920
9656
  detectLanguage,
8921
9657
  groupNodesByImpact,
9658
+ inferDomain,
8922
9659
  linkToCode,
8923
9660
  loadGraph,
8924
9661
  normalizeIntent,