@harness-engineering/graph 0.5.0 → 0.6.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,7 @@ __export(index_exports, {
45
45
  ContradictionDetector: () => ContradictionDetector,
46
46
  CoverageScorer: () => CoverageScorer,
47
47
  D2Parser: () => D2Parser,
48
+ DecisionIngestor: () => DecisionIngestor,
48
49
  DesignConstraintAdapter: () => DesignConstraintAdapter,
49
50
  DesignIngestor: () => DesignIngestor,
50
51
  DiagramParser: () => DiagramParser,
@@ -69,6 +70,7 @@ __export(index_exports, {
69
70
  ImageAnalysisExtractor: () => ImageAnalysisExtractor,
70
71
  IntentClassifier: () => IntentClassifier,
71
72
  JiraConnector: () => JiraConnector,
73
+ KnowledgeDocMaterializer: () => KnowledgeDocMaterializer,
72
74
  KnowledgeIngestor: () => KnowledgeIngestor,
73
75
  KnowledgePipelineRunner: () => KnowledgePipelineRunner,
74
76
  KnowledgeStagingAggregator: () => KnowledgeStagingAggregator,
@@ -130,7 +132,7 @@ var NODE_TYPES = [
130
132
  "build",
131
133
  "test_result",
132
134
  "execution_outcome",
133
- // Observability (future)
135
+ // Observability — reserved for future tracing/metrics integration
134
136
  "span",
135
137
  "metric",
136
138
  "log",
@@ -177,7 +179,7 @@ var EDGE_TYPES = [
177
179
  "triggered_by",
178
180
  "failed_in",
179
181
  "outcome_of",
180
- // Execution relationships (future)
182
+ // Execution relationships — reserved for future observability integration
181
183
  "executed_by",
182
184
  "measured_by",
183
185
  // Design relationships
@@ -252,7 +254,7 @@ function streamGraphJson(filePath, nodes, edges) {
252
254
  stream.write(JSON.stringify(edges[i]));
253
255
  }
254
256
  stream.write("]}");
255
- stream.end(resolve);
257
+ stream.end(() => resolve());
256
258
  });
257
259
  }
258
260
  async function saveGraph(dirPath, nodes, edges) {
@@ -275,15 +277,19 @@ async function loadGraph(dirPath) {
275
277
  await (0, import_promises.access)(metaPath);
276
278
  await (0, import_promises.access)(graphPath);
277
279
  } catch {
278
- return null;
280
+ return { status: "not_found" };
279
281
  }
280
282
  const metaContent = await (0, import_promises.readFile)(metaPath, "utf-8");
281
283
  const metadata = JSON.parse(metaContent);
282
284
  if (metadata.schemaVersion !== CURRENT_SCHEMA_VERSION) {
283
- return null;
285
+ return {
286
+ status: "schema_mismatch",
287
+ found: metadata.schemaVersion,
288
+ expected: CURRENT_SCHEMA_VERSION
289
+ };
284
290
  }
285
291
  const graphContent = await (0, import_promises.readFile)(graphPath, "utf-8");
286
- return JSON.parse(graphContent);
292
+ return { status: "loaded", graph: JSON.parse(graphContent) };
287
293
  }
288
294
 
289
295
  // src/store/GraphStore.ts
@@ -458,8 +464,15 @@ var GraphStore = class {
458
464
  await saveGraph(dirPath, allNodes, allEdges);
459
465
  }
460
466
  async load(dirPath) {
461
- const data = await loadGraph(dirPath);
462
- if (!data) return false;
467
+ const result = await loadGraph(dirPath);
468
+ if (result.status === "not_found") return false;
469
+ if (result.status === "schema_mismatch") {
470
+ console.warn(
471
+ `[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.`
472
+ );
473
+ return false;
474
+ }
475
+ const data = result.graph;
463
476
  this.clear();
464
477
  for (const node of data.nodes) {
465
478
  this.nodeMap.set(node.id, { ...node });
@@ -903,9 +916,10 @@ var CodeIngestor = class {
903
916
  let edgesAdded = 0;
904
917
  const files = await this.findSourceFiles(rootDir);
905
918
  const nameToFiles = /* @__PURE__ */ new Map();
919
+ const contentCache = /* @__PURE__ */ new Map();
906
920
  for (const filePath of files) {
907
921
  try {
908
- const result = await this.processFile(filePath, rootDir, nameToFiles);
922
+ const result = await this.processFile(filePath, rootDir, nameToFiles, contentCache);
909
923
  nodesAdded += result.nodesAdded;
910
924
  edgesAdded += result.edgesAdded;
911
925
  } catch (err) {
@@ -915,7 +929,8 @@ var CodeIngestor = class {
915
929
  for (const filePath of files) {
916
930
  try {
917
931
  const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
918
- const content = await fs.readFile(filePath, "utf-8");
932
+ const content = contentCache.get(filePath);
933
+ if (content === void 0) continue;
919
934
  const callsEdges = this.extractCallsEdgesForFile(relativePath, content, nameToFiles);
920
935
  for (const edge of callsEdges) {
921
936
  this.store.addEdge(edge);
@@ -926,7 +941,8 @@ var CodeIngestor = class {
926
941
  }
927
942
  for (const filePath of files) {
928
943
  try {
929
- const content = await fs.readFile(filePath, "utf-8");
944
+ const content = contentCache.get(filePath);
945
+ if (content === void 0) continue;
930
946
  edgesAdded += this.extractReqAnnotationsForFile(filePath, content, rootDir);
931
947
  } catch {
932
948
  }
@@ -940,11 +956,12 @@ var CodeIngestor = class {
940
956
  durationMs: Date.now() - start
941
957
  };
942
958
  }
943
- async processFile(filePath, rootDir, nameToFiles) {
959
+ async processFile(filePath, rootDir, nameToFiles, contentCache) {
944
960
  let nodesAdded = 0;
945
961
  let edgesAdded = 0;
946
962
  const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
947
963
  const content = await fs.readFile(filePath, "utf-8");
964
+ contentCache.set(filePath, content);
948
965
  const stat2 = await fs.stat(filePath);
949
966
  const fileId = `file:${relativePath}`;
950
967
  const fileNode = {
@@ -2122,9 +2139,119 @@ function parseFrontmatter(raw) {
2122
2139
  };
2123
2140
  }
2124
2141
 
2125
- // src/ingest/RequirementIngestor.ts
2142
+ // src/ingest/DecisionIngestor.ts
2126
2143
  var fs4 = __toESM(require("fs/promises"));
2127
2144
  var path5 = __toESM(require("path"));
2145
+ var CODE_NODE_TYPES3 = [
2146
+ "file",
2147
+ "function",
2148
+ "class",
2149
+ "method",
2150
+ "interface",
2151
+ "variable"
2152
+ ];
2153
+ var DecisionIngestor = class {
2154
+ constructor(store) {
2155
+ this.store = store;
2156
+ }
2157
+ store;
2158
+ async ingest(decisionsDir) {
2159
+ const start = Date.now();
2160
+ const errors = [];
2161
+ let files;
2162
+ try {
2163
+ files = await this.findDecisionFiles(decisionsDir);
2164
+ } catch {
2165
+ return emptyResult(Date.now() - start);
2166
+ }
2167
+ let nodesAdded = 0;
2168
+ let edgesAdded = 0;
2169
+ for (const filePath of files) {
2170
+ try {
2171
+ const raw = await fs4.readFile(filePath, "utf-8");
2172
+ const parsed = this.parseFrontmatter(raw);
2173
+ if (!parsed) continue;
2174
+ const { frontmatter, body } = parsed;
2175
+ if (!frontmatter.number || !frontmatter.title) continue;
2176
+ const filename = path5.basename(filePath, ".md");
2177
+ const nodeId = `decision:${filename}`;
2178
+ const node = {
2179
+ id: nodeId,
2180
+ type: "decision",
2181
+ name: frontmatter.title,
2182
+ path: filePath,
2183
+ content: body.trim(),
2184
+ metadata: {
2185
+ number: frontmatter.number,
2186
+ ...frontmatter.date && { date: frontmatter.date },
2187
+ ...frontmatter.status && { status: frontmatter.status },
2188
+ ...frontmatter.tier && { tier: frontmatter.tier },
2189
+ ...frontmatter.source && { source: frontmatter.source },
2190
+ ...frontmatter.supersedes && { supersedes: frontmatter.supersedes }
2191
+ }
2192
+ };
2193
+ this.store.addNode(node);
2194
+ nodesAdded++;
2195
+ edgesAdded += this.linkToCode(body, nodeId);
2196
+ } catch (err) {
2197
+ errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
2198
+ }
2199
+ }
2200
+ return {
2201
+ nodesAdded,
2202
+ nodesUpdated: 0,
2203
+ edgesAdded,
2204
+ edgesUpdated: 0,
2205
+ errors,
2206
+ durationMs: Date.now() - start
2207
+ };
2208
+ }
2209
+ parseFrontmatter(raw) {
2210
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
2211
+ if (!match) return null;
2212
+ const yamlBlock = match[1];
2213
+ const body = match[2];
2214
+ const frontmatter = {};
2215
+ for (const line of yamlBlock.split("\n")) {
2216
+ const kvMatch = line.match(/^(\w+):\s*(.+)$/);
2217
+ if (!kvMatch) continue;
2218
+ frontmatter[kvMatch[1]] = kvMatch[2].trim();
2219
+ }
2220
+ if (!frontmatter.number || !frontmatter.title) return null;
2221
+ return {
2222
+ frontmatter,
2223
+ body
2224
+ };
2225
+ }
2226
+ linkToCode(content, sourceNodeId) {
2227
+ let count = 0;
2228
+ for (const nodeType of CODE_NODE_TYPES3) {
2229
+ const codeNodes = this.store.findNodes({ type: nodeType });
2230
+ for (const node of codeNodes) {
2231
+ if (node.name.length < 3) continue;
2232
+ const escaped = node.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2233
+ const namePattern = new RegExp(`\\b${escaped}\\b`, "i");
2234
+ if (namePattern.test(content)) {
2235
+ this.store.addEdge({
2236
+ from: sourceNodeId,
2237
+ to: node.id,
2238
+ type: "decided"
2239
+ });
2240
+ count++;
2241
+ }
2242
+ }
2243
+ }
2244
+ return count;
2245
+ }
2246
+ async findDecisionFiles(dir) {
2247
+ const entries = await fs4.readdir(dir, { withFileTypes: true });
2248
+ return entries.filter((e) => e.isFile() && e.name.endsWith(".md") && e.name !== "README.md").map((e) => path5.join(dir, e.name));
2249
+ }
2250
+ };
2251
+
2252
+ // src/ingest/RequirementIngestor.ts
2253
+ var fs5 = __toESM(require("fs/promises"));
2254
+ var path6 = __toESM(require("path"));
2128
2255
  var REQUIREMENT_SECTIONS = [
2129
2256
  "Observable Truths",
2130
2257
  "Success Criteria",
@@ -2141,7 +2268,7 @@ function detectEarsPattern(text) {
2141
2268
  if (/^the\s+\w+\s+shall\b/.test(lower)) return "ubiquitous";
2142
2269
  return void 0;
2143
2270
  }
2144
- var CODE_NODE_TYPES3 = ["file", "function", "class", "method", "interface", "variable"];
2271
+ var CODE_NODE_TYPES4 = ["file", "function", "class", "method", "interface", "variable"];
2145
2272
  var RequirementIngestor = class {
2146
2273
  constructor(store) {
2147
2274
  this.store = store;
@@ -2159,8 +2286,8 @@ var RequirementIngestor = class {
2159
2286
  let edgesAdded = 0;
2160
2287
  let featureDirs;
2161
2288
  try {
2162
- const entries = await fs4.readdir(specsDir, { withFileTypes: true });
2163
- featureDirs = entries.filter((e) => e.isDirectory()).map((e) => path5.join(specsDir, e.name));
2289
+ const entries = await fs5.readdir(specsDir, { withFileTypes: true });
2290
+ featureDirs = entries.filter((e) => e.isDirectory()).map((e) => path6.join(specsDir, e.name));
2164
2291
  } catch {
2165
2292
  return emptyResult(Date.now() - start);
2166
2293
  }
@@ -2179,11 +2306,11 @@ var RequirementIngestor = class {
2179
2306
  };
2180
2307
  }
2181
2308
  async ingestFeatureDir(featureDir, errors) {
2182
- const featureName = path5.basename(featureDir);
2183
- const specPath = path5.join(featureDir, "proposal.md").replaceAll("\\", "/");
2309
+ const featureName = path6.basename(featureDir);
2310
+ const specPath = path6.join(featureDir, "proposal.md").replaceAll("\\", "/");
2184
2311
  let content;
2185
2312
  try {
2186
- content = await fs4.readFile(specPath, "utf-8");
2313
+ content = await fs5.readFile(specPath, "utf-8");
2187
2314
  } catch {
2188
2315
  return { nodesAdded: 0, edgesAdded: 0 };
2189
2316
  }
@@ -2200,7 +2327,7 @@ var RequirementIngestor = class {
2200
2327
  this.store.addNode({
2201
2328
  id: specNodeId,
2202
2329
  type: "document",
2203
- name: path5.basename(specPath),
2330
+ name: path6.basename(specPath),
2204
2331
  path: specPath,
2205
2332
  metadata: { featureName }
2206
2333
  });
@@ -2315,9 +2442,9 @@ var RequirementIngestor = class {
2315
2442
  for (const node of fileNodes) {
2316
2443
  if (!node.path) continue;
2317
2444
  const normalizedPath = node.path.replace(/\\/g, "/");
2318
- const isCodeMatch = normalizedPath.includes("packages/") && path5.basename(normalizedPath).includes(featureName);
2445
+ const isCodeMatch = normalizedPath.includes("packages/") && path6.basename(normalizedPath).includes(featureName);
2319
2446
  const isTestMatch = normalizedPath.includes("/tests/") && // platform-safe
2320
- path5.basename(normalizedPath).includes(featureName);
2447
+ path6.basename(normalizedPath).includes(featureName);
2321
2448
  if (isCodeMatch && !isTestMatch) {
2322
2449
  this.store.addEdge({
2323
2450
  from: reqId,
@@ -2346,7 +2473,7 @@ var RequirementIngestor = class {
2346
2473
  */
2347
2474
  linkByKeywordOverlap(reqId, reqText) {
2348
2475
  let count = 0;
2349
- for (const nodeType of CODE_NODE_TYPES3) {
2476
+ for (const nodeType of CODE_NODE_TYPES4) {
2350
2477
  const codeNodes = this.store.findNodes({ type: nodeType });
2351
2478
  for (const node of codeNodes) {
2352
2479
  if (node.name.length < 3) continue;
@@ -2370,19 +2497,158 @@ var RequirementIngestor = class {
2370
2497
  };
2371
2498
 
2372
2499
  // src/ingest/KnowledgePipelineRunner.ts
2373
- var fs9 = __toESM(require("fs/promises"));
2374
- var path10 = __toESM(require("path"));
2500
+ var fs11 = __toESM(require("fs/promises"));
2501
+ var path12 = __toESM(require("path"));
2375
2502
 
2376
2503
  // src/ingest/DiagramParser.ts
2377
- var fs5 = __toESM(require("fs/promises"));
2378
- var path6 = __toESM(require("path"));
2379
- function emptyMermaidResult(diagramType = "unknown") {
2504
+ var fs6 = __toESM(require("fs/promises"));
2505
+ var path7 = __toESM(require("path"));
2506
+
2507
+ // src/ingest/parsers/mermaid.ts
2508
+ function emptyResult2(diagramType = "unknown") {
2380
2509
  return {
2381
2510
  entities: [],
2382
2511
  relationships: [],
2383
2512
  metadata: { format: "mermaid", diagramType }
2384
2513
  };
2385
2514
  }
2515
+ function detectDiagramType(content) {
2516
+ for (const line of content.split("\n")) {
2517
+ const trimmed = line.trim();
2518
+ if (!trimmed) continue;
2519
+ if (/^(?:graph|flowchart)\b/i.test(trimmed)) return "flowchart";
2520
+ if (/^sequenceDiagram\b/.test(trimmed)) return "sequence";
2521
+ if (/^classDiagram\b/.test(trimmed)) return "class";
2522
+ if (/^erDiagram\b/.test(trimmed)) return "er";
2523
+ break;
2524
+ }
2525
+ return "unknown";
2526
+ }
2527
+ function isDecisionNode(content, nodeId) {
2528
+ const decisionRegex = new RegExp(`${nodeId}\\s*\\{`);
2529
+ return decisionRegex.test(content);
2530
+ }
2531
+ function buildFlowchartEntity(content, match) {
2532
+ const id = match[1] ?? "";
2533
+ const label = (match[2] ?? "").trim();
2534
+ if (!id) return null;
2535
+ const decision = isDecisionNode(content, id);
2536
+ const entity = {
2537
+ id,
2538
+ label,
2539
+ ...decision ? { type: "decision" } : {}
2540
+ };
2541
+ return { id, entity };
2542
+ }
2543
+ function extractFlowchartNodes(content) {
2544
+ const entities = /* @__PURE__ */ new Map();
2545
+ const nodeRegex = /([A-Za-z0-9_]+)\s*[[({]([^\])}]+)[\])}]/g;
2546
+ let match = nodeRegex.exec(content);
2547
+ while (match !== null) {
2548
+ const parsed = buildFlowchartEntity(content, match);
2549
+ if (parsed && !entities.has(parsed.id)) {
2550
+ entities.set(parsed.id, parsed.entity);
2551
+ }
2552
+ match = nodeRegex.exec(content);
2553
+ }
2554
+ return entities;
2555
+ }
2556
+ function parseLabeledEdgeMatch(match) {
2557
+ const from = match[1] ?? "";
2558
+ const label = (match[2] ?? "").trim();
2559
+ const to = match[3] ?? "";
2560
+ if (!from || !to) return null;
2561
+ return { key: `${from}->${to}:${label}`, rel: { from, to, label } };
2562
+ }
2563
+ function extractLabeledEdges(stripped, edgeKeys) {
2564
+ const relationships = [];
2565
+ const labeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\|([^|]+)\|\s*([A-Za-z0-9_]+)/g;
2566
+ let match = labeledEdgeRegex.exec(stripped);
2567
+ while (match !== null) {
2568
+ const parsed = parseLabeledEdgeMatch(match);
2569
+ if (parsed && !edgeKeys.has(parsed.key)) {
2570
+ edgeKeys.add(parsed.key);
2571
+ relationships.push(parsed.rel);
2572
+ }
2573
+ match = labeledEdgeRegex.exec(stripped);
2574
+ }
2575
+ return relationships;
2576
+ }
2577
+ function hasLabeledEdgeBetween(labeledEdges, from, to) {
2578
+ return labeledEdges.some((r) => r.from === from && r.to === to);
2579
+ }
2580
+ function extractUnlabeledEdges(stripped, edgeKeys, labeledEdges) {
2581
+ const relationships = [];
2582
+ const unlabeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\s+([A-Za-z0-9_]+)/g;
2583
+ let match = unlabeledEdgeRegex.exec(stripped);
2584
+ while (match !== null) {
2585
+ const from = match[1] ?? "";
2586
+ const to = match[2] ?? "";
2587
+ const key = `${from}->${to}`;
2588
+ if (from && to && !hasLabeledEdgeBetween(labeledEdges, from, to) && !edgeKeys.has(key)) {
2589
+ edgeKeys.add(key);
2590
+ relationships.push({ from, to });
2591
+ }
2592
+ match = unlabeledEdgeRegex.exec(stripped);
2593
+ }
2594
+ return relationships;
2595
+ }
2596
+ function parseFlowchart(content, diagramType) {
2597
+ const entities = extractFlowchartNodes(content);
2598
+ const stripped = content.replace(/[[({][^\])}]*[\])}]/g, "");
2599
+ const edgeKeys = /* @__PURE__ */ new Set();
2600
+ const labeledEdges = extractLabeledEdges(stripped, edgeKeys);
2601
+ const unlabeledEdges = extractUnlabeledEdges(stripped, edgeKeys, labeledEdges);
2602
+ return {
2603
+ entities: Array.from(entities.values()),
2604
+ relationships: [...labeledEdges, ...unlabeledEdges],
2605
+ metadata: { format: "mermaid", diagramType }
2606
+ };
2607
+ }
2608
+ function extractParticipants(content) {
2609
+ const entities = [];
2610
+ const seenParticipants = /* @__PURE__ */ new Set();
2611
+ const participantRegex = /participant\s+(\w+)(?:\s+as\s+(.+))?/g;
2612
+ let match = participantRegex.exec(content);
2613
+ while (match !== null) {
2614
+ const id = match[1] ?? "";
2615
+ const label = match[2]?.trim() || id;
2616
+ if (id && !seenParticipants.has(id)) {
2617
+ seenParticipants.add(id);
2618
+ entities.push({ id, label });
2619
+ }
2620
+ match = participantRegex.exec(content);
2621
+ }
2622
+ return entities;
2623
+ }
2624
+ function relationshipFromMatch(match) {
2625
+ const from = match[1] ?? "";
2626
+ const to = match[2] ?? "";
2627
+ const label = (match[3] ?? "").trim();
2628
+ return from && to ? { from, to, label } : null;
2629
+ }
2630
+ function collectMessageMatches(content, regex) {
2631
+ const results = [];
2632
+ let match = regex.exec(content);
2633
+ while (match !== null) {
2634
+ const rel = relationshipFromMatch(match);
2635
+ if (rel) results.push(rel);
2636
+ match = regex.exec(content);
2637
+ }
2638
+ return results;
2639
+ }
2640
+ function extractMessages(content) {
2641
+ const forward = collectMessageMatches(content, /(\w+)\s*->>?\+?\s*(\w+)\s*:\s*(.+)/g);
2642
+ const returns = collectMessageMatches(content, /(\w+)\s*-->>?-?\s*(\w+)\s*:\s*(.+)/g);
2643
+ return [...forward, ...returns];
2644
+ }
2645
+ function parseSequence(content, diagramType) {
2646
+ return {
2647
+ entities: extractParticipants(content),
2648
+ relationships: extractMessages(content),
2649
+ metadata: { format: "mermaid", diagramType }
2650
+ };
2651
+ }
2386
2652
  var MermaidParser = class {
2387
2653
  canParse(_content, ext) {
2388
2654
  return ext === ".mmd" || ext === ".mermaid";
@@ -2390,269 +2656,199 @@ var MermaidParser = class {
2390
2656
  parse(content, _filePath) {
2391
2657
  const trimmed = content.trim();
2392
2658
  if (!trimmed) {
2393
- return emptyMermaidResult();
2659
+ return emptyResult2();
2394
2660
  }
2395
- const diagramType = this.detectDiagramType(trimmed);
2661
+ const diagramType = detectDiagramType(trimmed);
2396
2662
  switch (diagramType) {
2397
2663
  case "flowchart":
2398
- return this.parseFlowchart(trimmed, diagramType);
2664
+ return parseFlowchart(trimmed, diagramType);
2399
2665
  case "sequence":
2400
- return this.parseSequence(trimmed, diagramType);
2666
+ return parseSequence(trimmed, diagramType);
2401
2667
  default:
2402
- return emptyMermaidResult(diagramType);
2668
+ return emptyResult2(diagramType);
2403
2669
  }
2404
2670
  }
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";
2671
+ };
2672
+
2673
+ // src/ingest/parsers/d2.ts
2674
+ function emptyResult3() {
2675
+ return {
2676
+ entities: [],
2677
+ relationships: [],
2678
+ metadata: { format: "d2", diagramType: "architecture" }
2679
+ };
2680
+ }
2681
+ function parseBlockShape(stripped) {
2682
+ const match = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+?)\s*\{$/);
2683
+ if (match) {
2684
+ const id = match[1];
2685
+ const label = match[2];
2686
+ if (id && label) return { id, label };
2687
+ }
2688
+ return null;
2689
+ }
2690
+ function parseConnection(stripped) {
2691
+ const match = stripped.match(/^([a-zA-Z0-9_.-]+)\s*->\s*([a-zA-Z0-9_.-]+)(?:\s*:\s*(.+?))?$/);
2692
+ if (match) {
2693
+ const from = match[1] ?? "";
2694
+ const to = match[2] ?? "";
2695
+ const label = match[3]?.trim();
2696
+ if (from && to) return { from, to, ...label ? { label } : {} };
2697
+ }
2698
+ return null;
2699
+ }
2700
+ function parseSimpleShape(stripped) {
2701
+ const match = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+)$/);
2702
+ if (match) {
2703
+ const id = match[1];
2704
+ const label = (match[2] ?? "").trim();
2705
+ if (id && label) return { id, label };
2706
+ }
2707
+ return null;
2708
+ }
2709
+ function handleBlockOpen(stripped, state) {
2710
+ if (state.braceDepth === 0) {
2711
+ const shape = parseBlockShape(stripped);
2712
+ if (shape) state.entities.set(shape.id, { id: shape.id, label: shape.label });
2417
2713
  }
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
- };
2714
+ state.braceDepth++;
2715
+ }
2716
+ function processTopLevelLine(stripped, state) {
2717
+ const conn = parseConnection(stripped);
2718
+ if (conn) {
2719
+ state.relationships.push(conn);
2720
+ return;
2476
2721
  }
2477
- isDecisionNode(content, nodeId) {
2478
- const decisionRegex = new RegExp(`${nodeId}\\s*\\{`);
2479
- return decisionRegex.test(content);
2722
+ const shape = parseSimpleShape(stripped);
2723
+ if (shape && !state.entities.has(shape.id)) {
2724
+ state.entities.set(shape.id, { id: shape.id, label: shape.label });
2480
2725
  }
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
- };
2726
+ }
2727
+ function processD2Line(stripped, state) {
2728
+ if (!stripped || stripped.startsWith("#")) return;
2729
+ if (stripped.endsWith("{")) {
2730
+ handleBlockOpen(stripped, state);
2731
+ return;
2524
2732
  }
2525
- };
2733
+ if (stripped === "}") {
2734
+ state.braceDepth = Math.max(0, state.braceDepth - 1);
2735
+ return;
2736
+ }
2737
+ if (state.braceDepth > 0) return;
2738
+ processTopLevelLine(stripped, state);
2739
+ }
2526
2740
  var D2Parser = class {
2527
2741
  canParse(_content, ext) {
2528
2742
  return ext === ".d2";
2529
2743
  }
2530
2744
  parse(content, _filePath) {
2531
2745
  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;
2746
+ if (!trimmed) return emptyResult3();
2747
+ const state = {
2748
+ entities: /* @__PURE__ */ new Map(),
2749
+ relationships: [],
2750
+ braceDepth: 0
2751
+ };
2542
2752
  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
- }
2753
+ processD2Line(line.trim(), state);
2584
2754
  }
2585
2755
  return {
2586
- entities: Array.from(entities.values()),
2587
- relationships,
2756
+ entities: Array.from(state.entities.values()),
2757
+ relationships: state.relationships,
2588
2758
  metadata: { format: "d2", diagramType: "architecture" }
2589
2759
  };
2590
2760
  }
2591
2761
  };
2762
+
2763
+ // src/ingest/parsers/plantuml.ts
2764
+ function emptyResult4() {
2765
+ return {
2766
+ entities: [],
2767
+ relationships: [],
2768
+ metadata: { format: "plantuml", diagramType: "unknown" }
2769
+ };
2770
+ }
2771
+ function detectDiagramType2(content) {
2772
+ if (/class\s+\w+/.test(content)) return "class";
2773
+ if (/\[.+\]/.test(content) || /component\s+/.test(content)) return "component";
2774
+ if (/participant\s+/.test(content) || /actor\s+/.test(content)) return "sequence";
2775
+ return "unknown";
2776
+ }
2777
+ function stripWrappers(content) {
2778
+ return content.replace(/@startuml\b.*\n?/, "").replace(/@enduml\b.*/, "").trim();
2779
+ }
2780
+ function extractClasses(body, entities) {
2781
+ const classRegex = /class\s+(\w+)/g;
2782
+ let match = classRegex.exec(body);
2783
+ while (match !== null) {
2784
+ const id = match[1] ?? "";
2785
+ if (id && !entities.has(id)) {
2786
+ entities.set(id, { id, label: id });
2787
+ }
2788
+ match = classRegex.exec(body);
2789
+ }
2790
+ }
2791
+ function extractComponents(body, entities) {
2792
+ const componentRegex = /\[([^\]]+)\]/g;
2793
+ let match = componentRegex.exec(body);
2794
+ while (match !== null) {
2795
+ const label = (match[1] ?? "").trim();
2796
+ if (label) {
2797
+ const id = label.replace(/\s+/g, "_");
2798
+ if (!entities.has(id)) {
2799
+ entities.set(id, { id, label });
2800
+ }
2801
+ }
2802
+ match = componentRegex.exec(body);
2803
+ }
2804
+ }
2805
+ function parseRelationshipMatch(match) {
2806
+ const from = match[1] ?? "";
2807
+ const to = match[2] ?? "";
2808
+ if (!from || !to) return null;
2809
+ const label = match[3]?.trim();
2810
+ return { from, to, ...label ? { label } : {} };
2811
+ }
2812
+ function collectRelationshipMatches(body, regex) {
2813
+ const results = [];
2814
+ let match = regex.exec(body);
2815
+ while (match !== null) {
2816
+ const rel = parseRelationshipMatch(match);
2817
+ if (rel) results.push(rel);
2818
+ match = regex.exec(body);
2819
+ }
2820
+ return results;
2821
+ }
2822
+ function extractRelationships(body) {
2823
+ return collectRelationshipMatches(
2824
+ body,
2825
+ /(\w+)\s*(?:-->|->|<--|<-|\.\.>|--)\s*(\w+)(?:\s*:\s*(.+))?/g
2826
+ );
2827
+ }
2592
2828
  var PlantUmlParser = class {
2593
2829
  canParse(_content, ext) {
2594
2830
  return ext === ".puml" || ext === ".plantuml";
2595
2831
  }
2596
2832
  parse(content, _filePath) {
2597
2833
  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);
2834
+ if (!trimmed) return emptyResult4();
2835
+ const diagramType = detectDiagramType2(trimmed);
2606
2836
  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
- }
2837
+ const body = stripWrappers(trimmed);
2838
+ extractClasses(body, entities);
2839
+ extractComponents(body, entities);
2840
+ const relationships = extractRelationships(body);
2642
2841
  return {
2643
2842
  entities: Array.from(entities.values()),
2644
2843
  relationships,
2645
2844
  metadata: { format: "plantuml", diagramType }
2646
2845
  };
2647
2846
  }
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
2847
  };
2848
+
2849
+ // src/ingest/DiagramParser.ts
2655
2850
  var DIAGRAM_EXTENSIONS = /* @__PURE__ */ new Set([".mmd", ".mermaid", ".d2", ".puml", ".plantuml"]);
2851
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".harness"]);
2656
2852
  var DiagramParser = class {
2657
2853
  constructor(store) {
2658
2854
  this.store = store;
@@ -2664,7 +2860,7 @@ var DiagramParser = class {
2664
2860
  new PlantUmlParser()
2665
2861
  ];
2666
2862
  parse(content, filePath) {
2667
- const ext = path6.extname(filePath).toLowerCase();
2863
+ const ext = path7.extname(filePath).toLowerCase();
2668
2864
  for (const parser of this.parsers) {
2669
2865
  if (parser.canParse(content, ext)) {
2670
2866
  return parser.parse(content, filePath);
@@ -2684,41 +2880,13 @@ var DiagramParser = class {
2684
2880
  const diagramFiles = await this.findDiagramFiles(projectDir);
2685
2881
  for (const filePath of diagramFiles) {
2686
2882
  try {
2687
- const content = await fs5.readFile(filePath, "utf-8");
2688
- const relPath = path6.relative(projectDir, filePath).replaceAll("\\", "/");
2883
+ const content = await fs6.readFile(filePath, "utf-8");
2884
+ const relPath = path7.relative(projectDir, filePath).replaceAll("\\", "/");
2689
2885
  const result = this.parse(content, filePath);
2690
2886
  if (result.entities.length === 0) continue;
2691
2887
  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
- }
2888
+ nodesAdded += this.addEntityNodes(result, relPath, pathHash);
2889
+ edgesAdded += this.addRelationshipEdges(result, pathHash);
2722
2890
  } catch (err) {
2723
2891
  errors.push(
2724
2892
  `Failed to parse ${filePath}: ${err instanceof Error ? err.message : String(err)}`
@@ -2734,25 +2902,64 @@ var DiagramParser = class {
2734
2902
  durationMs: Date.now() - start
2735
2903
  };
2736
2904
  }
2905
+ /** Map diagram entities to business_concept graph nodes. */
2906
+ addEntityNodes(result, relPath, pathHash) {
2907
+ let count = 0;
2908
+ for (const entity of result.entities) {
2909
+ const nodeId = `diagram:${pathHash}:${entity.id}`;
2910
+ this.store.addNode({
2911
+ id: nodeId,
2912
+ type: "business_concept",
2913
+ name: entity.label,
2914
+ path: relPath,
2915
+ metadata: {
2916
+ source: "diagram",
2917
+ format: result.metadata.format,
2918
+ diagramType: result.metadata.diagramType,
2919
+ confidence: 0.85,
2920
+ ...entity.type ? { entityType: entity.type } : {}
2921
+ }
2922
+ });
2923
+ count++;
2924
+ }
2925
+ return count;
2926
+ }
2927
+ /** Map diagram relationships to references graph edges. */
2928
+ addRelationshipEdges(result, pathHash) {
2929
+ let count = 0;
2930
+ for (const rel of result.relationships) {
2931
+ const fromId = `diagram:${pathHash}:${rel.from}`;
2932
+ const toId = `diagram:${pathHash}:${rel.to}`;
2933
+ this.store.addEdge({
2934
+ from: fromId,
2935
+ to: toId,
2936
+ type: "references",
2937
+ metadata: {
2938
+ ...rel.label ? { label: rel.label } : {}
2939
+ }
2940
+ });
2941
+ count++;
2942
+ }
2943
+ return count;
2944
+ }
2737
2945
  async findDiagramFiles(dir) {
2738
2946
  const files = [];
2739
- const SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".harness"]);
2740
2947
  const walk = async (currentDir) => {
2741
2948
  let entries;
2742
2949
  try {
2743
- entries = await fs5.readdir(currentDir, { withFileTypes: true });
2950
+ entries = await fs6.readdir(currentDir, { withFileTypes: true });
2744
2951
  } catch {
2745
2952
  return;
2746
2953
  }
2747
2954
  for (const entry of entries) {
2748
2955
  if (entry.isDirectory()) {
2749
- if (!SKIP_DIRS2.has(entry.name)) {
2750
- await walk(path6.join(currentDir, entry.name));
2956
+ if (!SKIP_DIRS.has(entry.name)) {
2957
+ await walk(path7.join(currentDir, entry.name));
2751
2958
  }
2752
2959
  } else if (entry.isFile()) {
2753
- const ext = path6.extname(entry.name).toLowerCase();
2960
+ const ext = path7.extname(entry.name).toLowerCase();
2754
2961
  if (DIAGRAM_EXTENSIONS.has(ext)) {
2755
- files.push(path6.join(currentDir, entry.name));
2962
+ files.push(path7.join(currentDir, entry.name));
2756
2963
  }
2757
2964
  }
2758
2965
  }
@@ -2763,8 +2970,8 @@ var DiagramParser = class {
2763
2970
  };
2764
2971
 
2765
2972
  // src/ingest/KnowledgeLinker.ts
2766
- var fs6 = __toESM(require("fs/promises"));
2767
- var path7 = __toESM(require("path"));
2973
+ var fs7 = __toESM(require("fs/promises"));
2974
+ var path8 = __toESM(require("path"));
2768
2975
  var HEURISTIC_PATTERNS = [
2769
2976
  {
2770
2977
  name: "business-rule-imperative",
@@ -2911,8 +3118,10 @@ var KnowledgeLinker = class {
2911
3118
  if (!duplicate) return false;
2912
3119
  const existingSources = duplicate.metadata.sources ?? [];
2913
3120
  if (!existingSources.includes(candidate.sourceNodeId)) {
2914
- existingSources.push(candidate.sourceNodeId);
2915
- duplicate.metadata.sources = existingSources;
3121
+ this.store.addNode({
3122
+ ...duplicate,
3123
+ metadata: { ...duplicate.metadata, sources: [...existingSources, candidate.sourceNodeId] }
3124
+ });
2916
3125
  }
2917
3126
  return true;
2918
3127
  }
@@ -2946,8 +3155,8 @@ var KnowledgeLinker = class {
2946
3155
  */
2947
3156
  async writeJsonl(candidates) {
2948
3157
  if (!this.outputDir) return;
2949
- await fs6.mkdir(this.outputDir, { recursive: true });
2950
- const filePath = path7.join(this.outputDir, "linker.jsonl");
3158
+ await fs7.mkdir(this.outputDir, { recursive: true });
3159
+ const filePath = path8.join(this.outputDir, "linker.jsonl");
2951
3160
  const lines = candidates.map(
2952
3161
  (c) => JSON.stringify({
2953
3162
  id: c.id,
@@ -2960,16 +3169,16 @@ var KnowledgeLinker = class {
2960
3169
  nodeType: c.nodeType
2961
3170
  })
2962
3171
  );
2963
- await fs6.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
3172
+ await fs7.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
2964
3173
  }
2965
3174
  /**
2966
3175
  * Write medium-confidence candidates to staged JSONL for human review.
2967
3176
  */
2968
3177
  async writeStagedJsonl(candidates) {
2969
3178
  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");
3179
+ const stagedDir = path8.join(this.outputDir, "staged");
3180
+ await fs7.mkdir(stagedDir, { recursive: true });
3181
+ const filePath = path8.join(stagedDir, "linker-staged.jsonl");
2973
3182
  const lines = candidates.map(
2974
3183
  (c) => JSON.stringify({
2975
3184
  id: c.id,
@@ -2982,7 +3191,7 @@ var KnowledgeLinker = class {
2982
3191
  nodeType: c.nodeType
2983
3192
  })
2984
3193
  );
2985
- await fs6.writeFile(filePath, lines.join("\n") + "\n");
3194
+ await fs7.writeFile(filePath, lines.join("\n") + "\n");
2986
3195
  }
2987
3196
  /**
2988
3197
  * Apply heuristic patterns to content and return candidate extractions.
@@ -3088,8 +3297,16 @@ var StructuralDriftDetector = class {
3088
3297
  };
3089
3298
 
3090
3299
  // src/ingest/KnowledgeStagingAggregator.ts
3091
- var fs7 = __toESM(require("fs/promises"));
3092
- var path8 = __toESM(require("path"));
3300
+ var fs8 = __toESM(require("fs/promises"));
3301
+ var path9 = __toESM(require("path"));
3302
+ var BUSINESS_NODE_TYPES = [
3303
+ "business_concept",
3304
+ "business_rule",
3305
+ "business_process",
3306
+ "business_term",
3307
+ "business_metric",
3308
+ "business_fact"
3309
+ ];
3093
3310
  var KnowledgeStagingAggregator = class {
3094
3311
  constructor(projectDir) {
3095
3312
  this.projectDir = projectDir;
@@ -3108,59 +3325,157 @@ var KnowledgeStagingAggregator = class {
3108
3325
  if (deduplicated.length === 0) {
3109
3326
  return { staged: 0 };
3110
3327
  }
3111
- const stagedDir = path8.join(this.projectDir, ".harness", "knowledge", "staged");
3112
- await fs7.mkdir(stagedDir, { recursive: true });
3328
+ const stagedDir = path9.join(this.projectDir, ".harness", "knowledge", "staged");
3329
+ await fs8.mkdir(stagedDir, { recursive: true });
3113
3330
  const jsonl = deduplicated.map((entry) => JSON.stringify(entry)).join("\n") + "\n";
3114
- await fs7.writeFile(path8.join(stagedDir, "pipeline-staged.jsonl"), jsonl, "utf-8");
3331
+ await fs8.writeFile(path9.join(stagedDir, "pipeline-staged.jsonl"), jsonl, "utf-8");
3115
3332
  return { staged: deduplicated.length };
3116
3333
  }
3117
- async generateGapReport(knowledgeDir) {
3118
- const domains = [];
3334
+ async extractDocName(filePath) {
3335
+ const raw = await fs8.readFile(filePath, "utf-8");
3336
+ const fmMatch = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
3337
+ const body = fmMatch ? fmMatch[1] : raw;
3338
+ const titleMatch = body.match(/^#\s+(.+)$/m);
3339
+ return titleMatch ? titleMatch[1].trim() : path9.basename(filePath, ".md");
3340
+ }
3341
+ async generateGapReport(knowledgeDir, store) {
3342
+ const domainDocNames = /* @__PURE__ */ new Map();
3343
+ const domainEntryCounts = /* @__PURE__ */ new Map();
3119
3344
  let totalEntries = 0;
3120
3345
  try {
3121
- const entries = await fs7.readdir(knowledgeDir, { withFileTypes: true });
3346
+ const entries = await fs8.readdir(knowledgeDir, { withFileTypes: true });
3122
3347
  const domainDirs = entries.filter((e) => e.isDirectory());
3123
3348
  for (const dir of domainDirs) {
3124
- const domainPath = path8.join(knowledgeDir, dir.name);
3125
- const files = await fs7.readdir(domainPath);
3349
+ const domainPath = path9.join(knowledgeDir, dir.name);
3350
+ const files = await fs8.readdir(domainPath);
3126
3351
  const mdFiles = files.filter((f) => f.endsWith(".md"));
3127
3352
  const entryCount = mdFiles.length;
3128
3353
  totalEntries += entryCount;
3129
- domains.push({ domain: dir.name, entryCount });
3354
+ domainEntryCounts.set(dir.name, entryCount);
3355
+ if (store) {
3356
+ const names = [];
3357
+ for (const file of mdFiles) {
3358
+ const name = await this.extractDocName(path9.join(domainPath, file));
3359
+ names.push(name.toLowerCase().trim());
3360
+ }
3361
+ domainDocNames.set(dir.name, names);
3362
+ }
3130
3363
  }
3131
3364
  } catch {
3132
3365
  }
3366
+ if (!store) {
3367
+ const domains2 = [];
3368
+ for (const [domain, entryCount] of domainEntryCounts) {
3369
+ domains2.push({ domain, entryCount, extractedCount: 0, gapCount: 0, gapEntries: [] });
3370
+ }
3371
+ return {
3372
+ domains: domains2,
3373
+ totalEntries,
3374
+ totalExtracted: 0,
3375
+ totalGaps: 0,
3376
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
3377
+ };
3378
+ }
3379
+ const extractedByDomain = /* @__PURE__ */ new Map();
3380
+ for (const nodeType of BUSINESS_NODE_TYPES) {
3381
+ const nodes = store.findNodes({ type: nodeType });
3382
+ for (const node of nodes) {
3383
+ const domain = node.metadata?.domain ?? "unknown";
3384
+ const list = extractedByDomain.get(domain) ?? [];
3385
+ list.push(node);
3386
+ extractedByDomain.set(domain, list);
3387
+ }
3388
+ }
3389
+ for (const [domain, nodes] of extractedByDomain) {
3390
+ const seen = /* @__PURE__ */ new Set();
3391
+ const deduped = nodes.filter((n) => {
3392
+ const key = n.name.toLowerCase().trim();
3393
+ if (seen.has(key)) return false;
3394
+ seen.add(key);
3395
+ return true;
3396
+ });
3397
+ extractedByDomain.set(domain, deduped);
3398
+ }
3399
+ const allDomains = /* @__PURE__ */ new Set([...domainEntryCounts.keys(), ...extractedByDomain.keys()]);
3400
+ const domains = [];
3401
+ let totalExtracted = 0;
3402
+ let totalGaps = 0;
3403
+ for (const domain of allDomains) {
3404
+ const entryCount = domainEntryCounts.get(domain) ?? 0;
3405
+ const extractedNodes = extractedByDomain.get(domain) ?? [];
3406
+ const extractedCount = extractedNodes.length;
3407
+ totalExtracted += extractedCount;
3408
+ const docNames = domainDocNames.get(domain) ?? [];
3409
+ const gapEntries = [];
3410
+ for (const node of extractedNodes) {
3411
+ const normalizedName = node.name.toLowerCase().trim();
3412
+ if (!docNames.includes(normalizedName)) {
3413
+ gapEntries.push({
3414
+ nodeId: node.id,
3415
+ name: node.name,
3416
+ nodeType: node.type,
3417
+ source: node.metadata?.source ?? "unknown",
3418
+ hasContent: Boolean(node.content && node.content.trim().length >= 10)
3419
+ });
3420
+ }
3421
+ }
3422
+ totalGaps += gapEntries.length;
3423
+ domains.push({ domain, entryCount, extractedCount, gapCount: gapEntries.length, gapEntries });
3424
+ }
3133
3425
  return {
3134
3426
  domains,
3135
3427
  totalEntries,
3428
+ totalExtracted,
3429
+ totalGaps,
3136
3430
  generatedAt: (/* @__PURE__ */ new Date()).toISOString()
3137
3431
  };
3138
3432
  }
3139
3433
  async writeGapReport(report) {
3140
- const gapsDir = path8.join(this.projectDir, ".harness", "knowledge");
3141
- await fs7.mkdir(gapsDir, { recursive: true });
3434
+ const gapsDir = path9.join(this.projectDir, ".harness", "knowledge");
3435
+ await fs8.mkdir(gapsDir, { recursive: true });
3436
+ const hasDifferential = report.totalExtracted > 0;
3142
3437
  const lines = [
3143
3438
  "# Knowledge Gaps Report",
3144
3439
  "",
3145
3440
  `Generated: ${report.generatedAt}`,
3146
3441
  "",
3147
3442
  "## Coverage by Domain",
3148
- "",
3149
- "| Domain | Entries |",
3150
- "| ------ | ------- |"
3443
+ ""
3151
3444
  ];
3152
- for (const domain of report.domains) {
3153
- lines.push(`| ${domain.domain} | ${domain.entryCount} |`);
3445
+ if (hasDifferential) {
3446
+ lines.push(
3447
+ "| Domain | Documented | Extracted | Gaps |",
3448
+ "| ------ | ---------- | --------- | ---- |"
3449
+ );
3450
+ for (const domain of report.domains) {
3451
+ lines.push(
3452
+ `| ${domain.domain} | ${domain.entryCount} | ${domain.extractedCount} | ${domain.gapCount} |`
3453
+ );
3454
+ }
3455
+ lines.push(
3456
+ "",
3457
+ `## Summary`,
3458
+ "",
3459
+ `- **Total Documented:** ${report.totalEntries}`,
3460
+ `- **Total Extracted:** ${report.totalExtracted}`,
3461
+ `- **Total Gaps:** ${report.totalGaps}`,
3462
+ ""
3463
+ );
3464
+ } else {
3465
+ lines.push("| Domain | Entries |", "| ------ | ------- |");
3466
+ for (const domain of report.domains) {
3467
+ lines.push(`| ${domain.domain} | ${domain.entryCount} |`);
3468
+ }
3469
+ lines.push("", `## Total Entries: ${report.totalEntries}`, "");
3154
3470
  }
3155
- lines.push("", `## Total Entries: ${report.totalEntries}`, "");
3156
- await fs7.writeFile(path8.join(gapsDir, "gaps.md"), lines.join("\n"), "utf-8");
3471
+ await fs8.writeFile(path9.join(gapsDir, "gaps.md"), lines.join("\n"), "utf-8");
3157
3472
  }
3158
3473
  };
3159
3474
 
3160
3475
  // 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([
3476
+ var fs9 = __toESM(require("fs/promises"));
3477
+ var path10 = __toESM(require("path"));
3478
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
3164
3479
  "node_modules",
3165
3480
  "dist",
3166
3481
  "target",
@@ -3197,11 +3512,11 @@ var EXT_TO_LANGUAGE = {
3197
3512
  };
3198
3513
  var SKIP_EXTENSIONS2 = /* @__PURE__ */ new Set([".d.ts"]);
3199
3514
  function detectLanguage(filePath) {
3200
- const name = path9.basename(filePath);
3515
+ const name = path10.basename(filePath);
3201
3516
  for (const skip of SKIP_EXTENSIONS2) {
3202
3517
  if (name.endsWith(skip)) return void 0;
3203
3518
  }
3204
- const ext = path9.extname(filePath);
3519
+ const ext = path10.extname(filePath);
3205
3520
  return EXT_TO_LANGUAGE[ext];
3206
3521
  }
3207
3522
  var EXTRACTOR_EDGE_TYPE = {
@@ -3227,7 +3542,7 @@ var ExtractionRunner = class {
3227
3542
  let nodesAdded = 0;
3228
3543
  let nodesUpdated = 0;
3229
3544
  let edgesAdded = 0;
3230
- await fs8.mkdir(outputDir, { recursive: true });
3545
+ await fs9.mkdir(outputDir, { recursive: true });
3231
3546
  const files = await this.findSourceFiles(projectDir);
3232
3547
  const recordsByExtractor = /* @__PURE__ */ new Map();
3233
3548
  for (const ext of this.extractors) {
@@ -3236,17 +3551,17 @@ var ExtractionRunner = class {
3236
3551
  for (const filePath of files) {
3237
3552
  const language = detectLanguage(filePath);
3238
3553
  if (!language) continue;
3239
- const ext = path9.extname(filePath);
3554
+ const ext = path10.extname(filePath);
3240
3555
  let content;
3241
3556
  try {
3242
- content = await fs8.readFile(filePath, "utf-8");
3557
+ content = await fs9.readFile(filePath, "utf-8");
3243
3558
  } catch (err) {
3244
3559
  errors.push(
3245
3560
  `Failed to read ${filePath}: ${err instanceof Error ? err.message : String(err)}`
3246
3561
  );
3247
3562
  continue;
3248
3563
  }
3249
- const relativePath = path9.relative(projectDir, filePath).replaceAll("\\", "/");
3564
+ const relativePath = path10.relative(projectDir, filePath).replaceAll("\\", "/");
3250
3565
  for (const extractor2 of this.extractors) {
3251
3566
  if (!extractor2.supportedExtensions.includes(ext)) continue;
3252
3567
  try {
@@ -3283,9 +3598,9 @@ var ExtractionRunner = class {
3283
3598
  }
3284
3599
  /** Write extraction records to JSONL file. */
3285
3600
  async writeJsonl(records, outputDir, extractorName) {
3286
- const filePath = path9.join(outputDir, `${extractorName}.jsonl`);
3601
+ const filePath = path10.join(outputDir, `${extractorName}.jsonl`);
3287
3602
  const lines = records.map((r) => JSON.stringify(r));
3288
- await fs8.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
3603
+ await fs9.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
3289
3604
  }
3290
3605
  /** Create or update a graph node from an extraction record. */
3291
3606
  persistRecord(record, store) {
@@ -3357,13 +3672,13 @@ var ExtractionRunner = class {
3357
3672
  const results = [];
3358
3673
  let entries;
3359
3674
  try {
3360
- entries = await fs8.readdir(dir, { withFileTypes: true });
3675
+ entries = await fs9.readdir(dir, { withFileTypes: true });
3361
3676
  } catch {
3362
3677
  return results;
3363
3678
  }
3364
3679
  for (const entry of entries) {
3365
- const fullPath = path9.join(dir, entry.name);
3366
- if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
3680
+ const fullPath = path10.join(dir, entry.name);
3681
+ if (entry.isDirectory() && !SKIP_DIRS2.has(entry.name)) {
3367
3682
  results.push(...await this.findSourceFiles(fullPath));
3368
3683
  } else if (entry.isFile() && detectLanguage(fullPath) !== void 0) {
3369
3684
  results.push(fullPath);
@@ -4489,57 +4804,9 @@ var ImageAnalysisExtractor = class {
4489
4804
  const errors = [];
4490
4805
  for (const imagePath of imagePaths) {
4491
4806
  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
- }
4807
+ const counts = await this.processImage(store, imagePath);
4808
+ nodesAdded += counts.nodes;
4809
+ edgesAdded += counts.edges;
4543
4810
  } catch (err) {
4544
4811
  errors.push(
4545
4812
  `Image analysis failed for ${imagePath}: ${err instanceof Error ? err.message : String(err)}`
@@ -4555,6 +4822,68 @@ var ImageAnalysisExtractor = class {
4555
4822
  durationMs: Date.now() - start
4556
4823
  };
4557
4824
  }
4825
+ /** Analyze a single image and add annotation + concept nodes to the store. */
4826
+ async processImage(store, imagePath) {
4827
+ const response = await this.provider.analyze({
4828
+ prompt: "Analyze this image and provide a structured description of its visual contents.",
4829
+ systemPrompt: "You are an image analysis assistant. Describe the image contents, detect UI elements, extract visible text, identify design patterns, and note accessibility concerns.",
4830
+ responseSchema: {},
4831
+ // Schema handled by provider
4832
+ maxTokens: 1e3
4833
+ });
4834
+ const result = response.result;
4835
+ const annotationId = `img:${hash(imagePath)}`;
4836
+ let nodes = 0;
4837
+ let edges = 0;
4838
+ store.addNode({
4839
+ id: annotationId,
4840
+ type: "image_annotation",
4841
+ name: result.description.slice(0, 200),
4842
+ path: imagePath,
4843
+ content: result.description,
4844
+ hash: hash(result.description),
4845
+ metadata: {
4846
+ source: "image-analysis",
4847
+ detectedElements: result.detectedElements,
4848
+ extractedText: result.extractedText,
4849
+ designPatterns: result.designPatterns,
4850
+ accessibilityNotes: result.accessibilityNotes,
4851
+ model: response.model
4852
+ }
4853
+ });
4854
+ nodes++;
4855
+ edges += this.linkToFileNode(store, annotationId, imagePath);
4856
+ for (const pattern of result.designPatterns) {
4857
+ edges += this.addDesignPatternConcept(store, annotationId, imagePath, pattern);
4858
+ nodes++;
4859
+ }
4860
+ return { nodes, edges };
4861
+ }
4862
+ /** Link annotation to its source file node if it exists. Returns 1 if linked, 0 otherwise. */
4863
+ linkToFileNode(store, annotationId, imagePath) {
4864
+ const fileNode = store.findNodes({ type: "file" }).find((n) => n.path === imagePath);
4865
+ if (!fileNode) return 0;
4866
+ store.addEdge({ from: annotationId, to: fileNode.id, type: "annotates" });
4867
+ return 1;
4868
+ }
4869
+ /** Create a business_concept node for a design pattern and link it. Returns edges added. */
4870
+ addDesignPatternConcept(store, annotationId, imagePath, pattern) {
4871
+ const conceptId = `img:concept:${hash(pattern + imagePath)}`;
4872
+ store.addNode({
4873
+ id: conceptId,
4874
+ type: "business_concept",
4875
+ name: pattern,
4876
+ content: `Design pattern detected in ${imagePath}: ${pattern}`,
4877
+ hash: hash(pattern),
4878
+ metadata: {
4879
+ source: "image-analysis",
4880
+ sourceImage: imagePath,
4881
+ domain: "design"
4882
+ }
4883
+ });
4884
+ store.addEdge({ from: annotationId, to: conceptId, type: "contains" });
4885
+ return 1;
4886
+ }
4558
4887
  };
4559
4888
 
4560
4889
  // src/ingest/knowledgeTypes.ts
@@ -4565,6 +4894,7 @@ var KNOWLEDGE_NODE_TYPES = [
4565
4894
  "business_term",
4566
4895
  "business_concept",
4567
4896
  "business_metric",
4897
+ "decision",
4568
4898
  "design_token",
4569
4899
  "design_constraint",
4570
4900
  "aesthetic_intent",
@@ -4605,83 +4935,99 @@ function classifyConflict(a, b) {
4605
4935
  return "temporal_conflict";
4606
4936
  return "status_divergence";
4607
4937
  }
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
- }
4938
+ var SEVERITY_MAP = {
4939
+ value_mismatch: "critical",
4940
+ definition_conflict: "high",
4941
+ status_divergence: "medium",
4942
+ temporal_conflict: "medium"
4943
+ };
4944
+ function buildEntry(node, source) {
4945
+ return {
4946
+ nodeId: node.id,
4947
+ source,
4948
+ name: node.name,
4949
+ content: node.content ?? "",
4950
+ lastModified: node.lastModified
4951
+ };
4952
+ }
4953
+ function groupByName(nodes) {
4954
+ const byName = /* @__PURE__ */ new Map();
4955
+ for (const node of nodes) {
4956
+ const key = node.name.toLowerCase().trim();
4957
+ const group = byName.get(key) ?? [];
4958
+ group.push(node);
4959
+ byName.set(key, group);
4960
+ }
4961
+ return byName;
4962
+ }
4963
+ function tryAddContradiction(acc, a, b, similarity) {
4964
+ const sourceA = a.metadata.source ?? "unknown";
4965
+ const sourceB = b.metadata.source ?? "unknown";
4966
+ if (sourceA === sourceB) return;
4967
+ if ((a.hash ?? a.id) === (b.hash ?? b.id)) return;
4968
+ const pairId = [a.id, b.id].sort().join(":");
4969
+ if (acc.seen.has(pairId)) return;
4970
+ acc.seen.add(pairId);
4971
+ const conflictType = classifyConflict(a, b);
4972
+ const pairKey = [sourceA, sourceB].sort().join("\u2194");
4973
+ acc.sourcePairCounts[pairKey] = (acc.sourcePairCounts[pairKey] ?? 0) + 1;
4974
+ acc.contradictions.push({
4975
+ id: `contradiction:${a.id}:${b.id}`,
4976
+ entityA: buildEntry(a, sourceA),
4977
+ entityB: buildEntry(b, sourceB),
4978
+ similarity,
4979
+ conflictType,
4980
+ severity: SEVERITY_MAP[conflictType],
4981
+ description: `"${a.name}" has conflicting definitions from ${sourceA} and ${sourceB}`
4982
+ });
4983
+ }
4984
+ function collectExactMatches(byName, acc) {
4985
+ for (const [, group] of byName) {
4986
+ if (group.length < 2) continue;
4987
+ for (let i = 0; i < group.length; i++) {
4988
+ for (let j = i + 1; j < group.length; j++) {
4989
+ tryAddContradiction(acc, group[i], group[j], 1);
4663
4990
  }
4664
4991
  }
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
- }
4992
+ }
4993
+ }
4994
+ function collectFuzzyMatches(byName, acc) {
4995
+ const keys = [...byName.keys()];
4996
+ for (let i = 0; i < keys.length; i++) {
4997
+ for (let j = i + 1; j < keys.length; j++) {
4998
+ const keyA = keys[i];
4999
+ const keyB = keys[j];
5000
+ const maxLen = Math.max(keyA.length, keyB.length);
5001
+ const minLen = Math.min(keyA.length, keyB.length);
5002
+ if (minLen / maxLen < SIMILARITY_THRESHOLD) continue;
5003
+ const ratio = levenshteinRatio(keyA, keyB);
5004
+ if (ratio < SIMILARITY_THRESHOLD) continue;
5005
+ const groupA = byName.get(keyA);
5006
+ const groupB = byName.get(keyB);
5007
+ for (const a of groupA) {
5008
+ for (const b of groupB) {
5009
+ tryAddContradiction(acc, a, b, ratio);
4681
5010
  }
4682
5011
  }
4683
5012
  }
4684
- return { contradictions, sourcePairCounts, totalChecked: nodes.length };
5013
+ }
5014
+ }
5015
+ var ContradictionDetector = class {
5016
+ detect(store) {
5017
+ const nodes = KNOWLEDGE_NODE_TYPES.flatMap((t) => store.findNodes({ type: t }));
5018
+ const byName = groupByName(nodes);
5019
+ const acc = {
5020
+ contradictions: [],
5021
+ sourcePairCounts: {},
5022
+ seen: /* @__PURE__ */ new Set()
5023
+ };
5024
+ collectExactMatches(byName, acc);
5025
+ collectFuzzyMatches(byName, acc);
5026
+ return {
5027
+ contradictions: acc.contradictions,
5028
+ sourcePairCounts: acc.sourcePairCounts,
5029
+ totalChecked: nodes.length
5030
+ };
4685
5031
  }
4686
5032
  };
4687
5033
 
@@ -4712,64 +5058,76 @@ function toGrade(score) {
4712
5058
  if (score >= 20) return "D";
4713
5059
  return "F";
4714
5060
  }
5061
+ function groupByDomain(nodes, fallback) {
5062
+ const map = /* @__PURE__ */ new Map();
5063
+ for (const node of nodes) {
5064
+ const domain = node.metadata.domain ?? fallback(node);
5065
+ const group = map.get(domain) ?? [];
5066
+ group.push(node);
5067
+ map.set(domain, group);
5068
+ }
5069
+ return map;
5070
+ }
5071
+ function countLinkedEntities(codeEntries, store) {
5072
+ const linkedIds = /* @__PURE__ */ new Set();
5073
+ for (const codeNode of codeEntries) {
5074
+ if (hasKnowledgeEdge(codeNode.id, store)) {
5075
+ linkedIds.add(codeNode.id);
5076
+ }
5077
+ }
5078
+ return linkedIds;
5079
+ }
5080
+ function hasKnowledgeEdge(nodeId, store) {
5081
+ for (const edgeType of KNOWLEDGE_EDGE_TYPES) {
5082
+ if (store.getEdges({ to: nodeId, type: edgeType }).length > 0) return true;
5083
+ }
5084
+ return false;
5085
+ }
5086
+ function computeSourceBreakdown(knEntries) {
5087
+ const breakdown = {};
5088
+ for (const kn of knEntries) {
5089
+ const src = kn.metadata.source ?? "unknown";
5090
+ breakdown[src] = (breakdown[src] ?? 0) + 1;
5091
+ }
5092
+ return breakdown;
5093
+ }
5094
+ function computeDomainScore(knowledgeEntries, codeEntities, linkedEntities, uniqueSources) {
5095
+ const codeCoverageComponent = codeEntities > 0 ? linkedEntities / codeEntities * 60 : 0;
5096
+ const knowledgeDepthComponent = Math.min(knowledgeEntries / 10, 1) * 20;
5097
+ const sourceDiversityComponent = Math.min(uniqueSources / 3, 1) * 20;
5098
+ return Math.round(codeCoverageComponent + knowledgeDepthComponent + sourceDiversityComponent);
5099
+ }
5100
+ function scoreDomain(domain, knEntries, codeEntries, store) {
5101
+ const linkedIds = countLinkedEntities(codeEntries, store);
5102
+ const sourceBreakdown = computeSourceBreakdown(knEntries);
5103
+ const codeEntities = codeEntries.length;
5104
+ const linkedEntities = linkedIds.size;
5105
+ const knowledgeEntries = knEntries.length;
5106
+ const uniqueSources = Object.keys(sourceBreakdown).length;
5107
+ const score = computeDomainScore(knowledgeEntries, codeEntities, linkedEntities, uniqueSources);
5108
+ return {
5109
+ domain,
5110
+ score,
5111
+ knowledgeEntries,
5112
+ codeEntities,
5113
+ linkedEntities,
5114
+ unlinkedEntities: codeEntities - linkedEntities,
5115
+ sourceBreakdown,
5116
+ grade: toGrade(score)
5117
+ };
5118
+ }
4715
5119
  var CoverageScorer = class {
4716
5120
  score(store) {
4717
5121
  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
- }
5122
+ const domainMap = groupByDomain(knowledgeNodes, () => "unclassified");
4725
5123
  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
- }
5124
+ const codeDomains = groupByDomain(codeNodes, (n) => this.domainFromPath(n.path));
4733
5125
  const allDomains = /* @__PURE__ */ new Set([...domainMap.keys(), ...codeDomains.keys()]);
4734
5126
  const domains = [];
4735
5127
  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
5128
+ domains.push(
5129
+ scoreDomain(domain, domainMap.get(domain) ?? [], codeDomains.get(domain) ?? [], store)
4762
5130
  );
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
5131
  }
4774
5132
  const overallScore = domains.length > 0 ? Math.round(domains.reduce((sum, d) => sum + d.score, 0) / domains.length) : 0;
4775
5133
  return {
@@ -4790,8 +5148,166 @@ var CoverageScorer = class {
4790
5148
  }
4791
5149
  };
4792
5150
 
5151
+ // src/ingest/KnowledgeDocMaterializer.ts
5152
+ var fs10 = __toESM(require("fs/promises"));
5153
+ var path11 = __toESM(require("path"));
5154
+ var VALID_BUSINESS_TYPES = /* @__PURE__ */ new Set([
5155
+ "business_rule",
5156
+ "business_process",
5157
+ "business_concept",
5158
+ "business_term",
5159
+ "business_metric"
5160
+ ]);
5161
+ var DEFAULT_MAX_DOCS = 50;
5162
+ var MAX_COLLISION_SUFFIX = 10;
5163
+ var KnowledgeDocMaterializer = class {
5164
+ constructor(store) {
5165
+ this.store = store;
5166
+ }
5167
+ store;
5168
+ async materialize(gapEntries, options) {
5169
+ const maxDocs = options.maxDocs ?? DEFAULT_MAX_DOCS;
5170
+ const created = [];
5171
+ const skipped = [];
5172
+ const createdNames = /* @__PURE__ */ new Set();
5173
+ for (const entry of gapEntries) {
5174
+ const resolved = await this.resolveEntry(
5175
+ entry,
5176
+ created.length,
5177
+ maxDocs,
5178
+ createdNames,
5179
+ options
5180
+ );
5181
+ if ("reason" in resolved) {
5182
+ skipped.push(resolved);
5183
+ continue;
5184
+ }
5185
+ const { node, domain, domainDir, nameKey } = resolved;
5186
+ await fs10.mkdir(domainDir, { recursive: true });
5187
+ const basename10 = this.generateFilename(entry.name);
5188
+ const filename = await this.resolveCollision(domainDir, basename10);
5189
+ const content = this.formatDoc(node, domain);
5190
+ const filePath = ["docs", "knowledge", domain, filename].join("/");
5191
+ await fs10.writeFile(path11.join(options.projectDir, filePath), content, "utf-8");
5192
+ createdNames.add(nameKey);
5193
+ created.push({ filePath, nodeId: entry.nodeId, domain, name: entry.name });
5194
+ }
5195
+ return { created, skipped };
5196
+ }
5197
+ async resolveEntry(entry, createdCount, maxDocs, createdNames, options) {
5198
+ if (!entry.hasContent) {
5199
+ return { nodeId: entry.nodeId, name: entry.name, reason: "no_content" };
5200
+ }
5201
+ const node = this.store.getNode(entry.nodeId);
5202
+ if (!node || !node.content || node.content.trim().length < 10) {
5203
+ return { nodeId: entry.nodeId, name: entry.name, reason: "no_content" };
5204
+ }
5205
+ const domain = this.inferDomain(node);
5206
+ if (!domain || /[/\\]|\.\.|\0/.test(domain)) {
5207
+ return { nodeId: entry.nodeId, name: entry.name, reason: "no_domain" };
5208
+ }
5209
+ if (createdCount >= maxDocs) {
5210
+ return { nodeId: entry.nodeId, name: entry.name, reason: "cap_reached" };
5211
+ }
5212
+ const domainDir = path11.join(options.projectDir, "docs", "knowledge", domain);
5213
+ const nameKey = `${domain}:${entry.name.toLowerCase().trim()}`;
5214
+ if (!createdNames.has(nameKey) && await this.hasExistingDoc(domainDir, entry.name)) {
5215
+ return { nodeId: entry.nodeId, name: entry.name, reason: "already_documented" };
5216
+ }
5217
+ if (options.dryRun) {
5218
+ return { nodeId: entry.nodeId, name: entry.name, reason: "dry_run" };
5219
+ }
5220
+ return { node, domain, domainDir, nameKey };
5221
+ }
5222
+ inferDomain(node) {
5223
+ if (node.metadata?.domain && typeof node.metadata.domain === "string") {
5224
+ return node.metadata.domain;
5225
+ }
5226
+ if (node.path) {
5227
+ const pkgMatch = node.path.match(/^packages\/([^/]+)/);
5228
+ if (pkgMatch) return pkgMatch[1];
5229
+ const srcMatch = node.path.match(/^src\/([^/]+)/);
5230
+ if (srcMatch) return srcMatch[1];
5231
+ }
5232
+ if (node.metadata?.source === "knowledge-linker" || node.metadata?.source === "connector") {
5233
+ const connector = node.metadata.connectorName;
5234
+ if (typeof connector === "string") return connector;
5235
+ return "general";
5236
+ }
5237
+ return null;
5238
+ }
5239
+ generateFilename(name) {
5240
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
5241
+ return `${slug}.md`;
5242
+ }
5243
+ async resolveCollision(dir, basename10) {
5244
+ const ext = path11.extname(basename10);
5245
+ const stem = path11.basename(basename10, ext);
5246
+ try {
5247
+ await fs10.access(path11.join(dir, basename10));
5248
+ } catch {
5249
+ return basename10;
5250
+ }
5251
+ for (let i = 2; i <= MAX_COLLISION_SUFFIX; i++) {
5252
+ const candidate = `${stem}-${i}${ext}`;
5253
+ try {
5254
+ await fs10.access(path11.join(dir, candidate));
5255
+ } catch {
5256
+ return candidate;
5257
+ }
5258
+ }
5259
+ throw new Error(
5260
+ `Cannot resolve filename collision for "${basename10}" after ${MAX_COLLISION_SUFFIX} attempts`
5261
+ );
5262
+ }
5263
+ formatDoc(node, domain) {
5264
+ const mappedType = this.mapNodeType(node);
5265
+ const sanitize = (s) => s.replace(/[\n\r]/g, " ").replace(/:/g, "-");
5266
+ const lines = ["---", `type: ${sanitize(mappedType)}`, `domain: ${sanitize(domain)}`];
5267
+ const tags = node.metadata?.tags;
5268
+ if (Array.isArray(tags) && tags.length > 0) {
5269
+ lines.push(`tags: [${tags.map((t) => sanitize(String(t))).join(", ")}]`);
5270
+ }
5271
+ const related = node.metadata?.related;
5272
+ if (Array.isArray(related) && related.length > 0) {
5273
+ lines.push(`related: [${related.map((r) => sanitize(String(r))).join(", ")}]`);
5274
+ }
5275
+ const title = (node.name ?? "").replace(/[\n\r]/g, " ");
5276
+ lines.push("---", "", `# ${title}`, "", node.content ?? "", "");
5277
+ return lines.join("\n");
5278
+ }
5279
+ /** Check if a doc with a matching title already exists in the domain directory. */
5280
+ async hasExistingDoc(domainDir, name) {
5281
+ const normalizedName = name.toLowerCase().trim();
5282
+ try {
5283
+ const files = await fs10.readdir(domainDir);
5284
+ for (const file of files) {
5285
+ if (!file.endsWith(".md")) continue;
5286
+ const raw = await fs10.readFile(path11.join(domainDir, file), "utf-8");
5287
+ const fmMatch = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
5288
+ const body = fmMatch ? fmMatch[1] : raw;
5289
+ const titleMatch = body.match(/^#\s+(.+)$/m);
5290
+ if (titleMatch && titleMatch[1].trim().toLowerCase() === normalizedName) {
5291
+ return true;
5292
+ }
5293
+ }
5294
+ } catch {
5295
+ }
5296
+ return false;
5297
+ }
5298
+ mapNodeType(node) {
5299
+ if (VALID_BUSINESS_TYPES.has(node.type)) {
5300
+ return node.type;
5301
+ }
5302
+ if (node.type === "business_fact") {
5303
+ return "business_rule";
5304
+ }
5305
+ return "business_concept";
5306
+ }
5307
+ };
5308
+
4793
5309
  // src/ingest/KnowledgePipelineRunner.ts
4794
- var BUSINESS_NODE_TYPES = [
5310
+ var BUSINESS_NODE_TYPES2 = [
4795
5311
  "business_concept",
4796
5312
  "business_rule",
4797
5313
  "business_process",
@@ -4800,7 +5316,8 @@ var BUSINESS_NODE_TYPES = [
4800
5316
  "business_fact"
4801
5317
  ];
4802
5318
  var SNAPSHOT_NODE_TYPES = [
4803
- ...BUSINESS_NODE_TYPES,
5319
+ ...BUSINESS_NODE_TYPES2,
5320
+ "decision",
4804
5321
  "design_token",
4805
5322
  "design_constraint",
4806
5323
  "aesthetic_intent",
@@ -4812,51 +5329,98 @@ var KnowledgePipelineRunner = class {
4812
5329
  }
4813
5330
  store;
4814
5331
  async run(options) {
4815
- const maxIterations = options.maxIterations ?? 5;
4816
5332
  const remediations = [];
4817
5333
  const preSnapshot = this.buildSnapshot(options.domain);
4818
5334
  const extraction = await this.extract(options);
4819
5335
  const postSnapshot = this.buildSnapshot(options.domain);
4820
5336
  let driftResult = this.reconcile(preSnapshot, postSnapshot);
4821
- const contradictionDetector = new ContradictionDetector();
4822
- const contradictions = contradictionDetector.detect(this.store);
5337
+ const contradictions = new ContradictionDetector().detect(this.store);
4823
5338
  let gapReport = await this.detect(options);
4824
- const coverageScorer = new CoverageScorer();
4825
- const coverage = coverageScorer.score(this.store);
5339
+ const coverage = new CoverageScorer().score(this.store);
5340
+ let materialization;
4826
5341
  let iterations = 1;
4827
5342
  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
- }
5343
+ const loopResult = await this.runRemediationLoop(
5344
+ options,
5345
+ driftResult,
5346
+ gapReport,
5347
+ remediations
5348
+ );
5349
+ iterations = loopResult.iterations;
5350
+ materialization = loopResult.materialization;
5351
+ }
5352
+ if (options.fix && iterations > 1) {
5353
+ const finalSnapshot = this.buildSnapshot(options.domain);
5354
+ driftResult = this.reconcile(preSnapshot, finalSnapshot);
5355
+ gapReport = await this.detect(options);
4842
5356
  }
4843
5357
  await this.stageNewFindings(driftResult, options);
5358
+ return this.buildResult(
5359
+ driftResult,
5360
+ iterations,
5361
+ extraction,
5362
+ gapReport,
5363
+ remediations,
5364
+ contradictions,
5365
+ coverage,
5366
+ materialization
5367
+ );
5368
+ }
5369
+ /** Run the remediation convergence loop; returns iteration count and accumulated materialization. */
5370
+ async runRemediationLoop(options, driftResult, gapReport, remediations) {
5371
+ const maxIterations = options.maxIterations ?? 5;
5372
+ let iterations = 1;
5373
+ let currentDrift = driftResult;
5374
+ let currentGapReport = gapReport;
5375
+ let previousIssueCount = currentDrift.findings.length + currentGapReport.totalGaps;
5376
+ let accumulatedMaterialization;
5377
+ while (iterations < maxIterations) {
5378
+ if (currentDrift.findings.length === 0 && currentGapReport.totalGaps === 0) break;
5379
+ const matResult = await this.remediate(currentDrift, remediations, options, currentGapReport);
5380
+ if (matResult) {
5381
+ if (!accumulatedMaterialization) {
5382
+ accumulatedMaterialization = matResult;
5383
+ } else {
5384
+ accumulatedMaterialization = {
5385
+ created: [...accumulatedMaterialization.created, ...matResult.created],
5386
+ skipped: [...accumulatedMaterialization.skipped, ...matResult.skipped]
5387
+ };
5388
+ }
5389
+ }
5390
+ const preSnapshot = this.buildSnapshot(options.domain);
5391
+ await this.extract(options);
5392
+ const postSnapshot = this.buildSnapshot(options.domain);
5393
+ currentDrift = this.reconcile(preSnapshot, postSnapshot);
5394
+ currentGapReport = await this.detect(options);
5395
+ iterations++;
5396
+ const currentIssueCount = currentDrift.findings.length + currentGapReport.totalGaps;
5397
+ if (currentIssueCount >= previousIssueCount) break;
5398
+ previousIssueCount = currentIssueCount;
5399
+ }
5400
+ return {
5401
+ iterations,
5402
+ ...accumulatedMaterialization ? { materialization: accumulatedMaterialization } : {}
5403
+ };
5404
+ }
5405
+ /** Assemble the final pipeline result. */
5406
+ buildResult(driftResult, iterations, extraction, gaps, remediations, contradictions, coverage, materialization) {
4844
5407
  return {
4845
5408
  verdict: this.computeVerdict(driftResult),
4846
5409
  driftScore: driftResult.driftScore,
4847
5410
  iterations,
4848
5411
  findings: driftResult.summary,
4849
5412
  extraction,
4850
- gaps: gapReport,
5413
+ gaps,
4851
5414
  remediations,
4852
5415
  contradictions,
4853
- coverage
5416
+ coverage,
5417
+ ...materialization ? { materialization } : {}
4854
5418
  };
4855
5419
  }
4856
5420
  // ── Phase 1: EXTRACT ──────────────────────────────────────────────────────
4857
5421
  async extract(options) {
4858
- const extractedDir = path10.join(options.projectDir, ".harness", "knowledge", "extracted");
4859
- await fs9.mkdir(extractedDir, { recursive: true });
5422
+ const extractedDir = path12.join(options.projectDir, ".harness", "knowledge", "extracted");
5423
+ await fs11.mkdir(extractedDir, { recursive: true });
4860
5424
  const runner = createExtractionRunner();
4861
5425
  const extractionResult = await runner.run(options.projectDir, this.store, extractedDir);
4862
5426
  const diagramParser = new DiagramParser(this.store);
@@ -4870,7 +5434,7 @@ var KnowledgePipelineRunner = class {
4870
5434
  const imageResult = await imageExtractor.analyze(this.store, imagePaths);
4871
5435
  imageCount = imageResult.nodesAdded;
4872
5436
  }
4873
- const knowledgeDir = path10.join(options.projectDir, "docs", "knowledge");
5437
+ const knowledgeDir = path12.join(options.projectDir, "docs", "knowledge");
4874
5438
  const bkIngestor = new BusinessKnowledgeIngestor(this.store);
4875
5439
  let bkResult;
4876
5440
  try {
@@ -4885,6 +5449,21 @@ var KnowledgePipelineRunner = class {
4885
5449
  durationMs: 0
4886
5450
  };
4887
5451
  }
5452
+ const decisionsDir = path12.join(options.projectDir, "docs", "knowledge", "decisions");
5453
+ const decisionIngestor = new DecisionIngestor(this.store);
5454
+ let decisionResult;
5455
+ try {
5456
+ decisionResult = await decisionIngestor.ingest(decisionsDir);
5457
+ } catch {
5458
+ decisionResult = {
5459
+ nodesAdded: 0,
5460
+ nodesUpdated: 0,
5461
+ edgesAdded: 0,
5462
+ edgesUpdated: 0,
5463
+ errors: [],
5464
+ durationMs: 0
5465
+ };
5466
+ }
4888
5467
  const linker = new KnowledgeLinker(this.store, extractedDir);
4889
5468
  const linkResult = await linker.link();
4890
5469
  return {
@@ -4892,6 +5471,7 @@ var KnowledgePipelineRunner = class {
4892
5471
  diagrams: diagramResult.nodesAdded,
4893
5472
  linkerFacts: linkResult.factsCreated,
4894
5473
  businessKnowledge: bkResult.nodesAdded,
5474
+ decisions: decisionResult.nodesAdded,
4895
5475
  images: imageCount
4896
5476
  };
4897
5477
  }
@@ -4918,14 +5498,14 @@ var KnowledgePipelineRunner = class {
4918
5498
  }
4919
5499
  // ── Phase 3: DETECT ───────────────────────────────────────────────────────
4920
5500
  async detect(options) {
4921
- const knowledgeDir = path10.join(options.projectDir, "docs", "knowledge");
5501
+ const knowledgeDir = path12.join(options.projectDir, "docs", "knowledge");
4922
5502
  const aggregator = new KnowledgeStagingAggregator(options.projectDir);
4923
- const gapReport = await aggregator.generateGapReport(knowledgeDir);
5503
+ const gapReport = await aggregator.generateGapReport(knowledgeDir, this.store);
4924
5504
  await aggregator.writeGapReport(gapReport);
4925
5505
  return gapReport;
4926
5506
  }
4927
5507
  // ── Phase 4: REMEDIATE ────────────────────────────────────────────────────
4928
- remediate(driftResult, remediations, options) {
5508
+ async remediate(driftResult, remediations, options, gapReport) {
4929
5509
  for (const finding of driftResult.findings) {
4930
5510
  switch (finding.classification) {
4931
5511
  case "stale":
@@ -4943,6 +5523,22 @@ var KnowledgePipelineRunner = class {
4943
5523
  break;
4944
5524
  }
4945
5525
  }
5526
+ if (!options.ci) {
5527
+ const allGapEntries = gapReport.domains.flatMap((d) => d.gapEntries);
5528
+ const materializable = allGapEntries.filter((e) => e.hasContent);
5529
+ if (materializable.length > 0) {
5530
+ const materializer = new KnowledgeDocMaterializer(this.store);
5531
+ const matResult = await materializer.materialize(materializable, {
5532
+ projectDir: options.projectDir,
5533
+ dryRun: false
5534
+ });
5535
+ for (const doc of matResult.created) {
5536
+ remediations.push(`created doc: ${doc.filePath}`);
5537
+ }
5538
+ return matResult;
5539
+ }
5540
+ }
5541
+ return void 0;
4946
5542
  }
4947
5543
  async stageNewFindings(driftResult, options) {
4948
5544
  const newFindings = driftResult.findings.filter((f) => f.classification === "new");
@@ -4977,7 +5573,7 @@ var KnowledgePipelineRunner = class {
4977
5573
  };
4978
5574
 
4979
5575
  // src/ingest/connectors/ConnectorUtils.ts
4980
- var CODE_NODE_TYPES4 = ["file", "function", "class", "method", "interface", "variable"];
5576
+ var CODE_NODE_TYPES5 = ["file", "function", "class", "method", "interface", "variable"];
4981
5577
  var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
4982
5578
  function withRetry(client, options) {
4983
5579
  const maxRetries = options?.maxRetries ?? 3;
@@ -5042,7 +5638,7 @@ function sanitizeExternalText(text, maxLength = 2e3) {
5042
5638
  }
5043
5639
  function linkToCode(store, content, sourceNodeId, edgeType, options) {
5044
5640
  let edgesCreated = 0;
5045
- for (const type of CODE_NODE_TYPES4) {
5641
+ for (const type of CODE_NODE_TYPES5) {
5046
5642
  const nodes = store.findNodes({ type });
5047
5643
  for (const node of nodes) {
5048
5644
  if (node.name.length < 3) continue;
@@ -5062,12 +5658,12 @@ function linkToCode(store, content, sourceNodeId, edgeType, options) {
5062
5658
  }
5063
5659
 
5064
5660
  // src/ingest/connectors/SyncManager.ts
5065
- var fs10 = __toESM(require("fs/promises"));
5066
- var path11 = __toESM(require("path"));
5661
+ var fs12 = __toESM(require("fs/promises"));
5662
+ var path13 = __toESM(require("path"));
5067
5663
  var SyncManager = class {
5068
5664
  constructor(store, graphDir) {
5069
5665
  this.store = store;
5070
- this.metadataPath = path11.join(graphDir, "sync-metadata.json");
5666
+ this.metadataPath = path13.join(graphDir, "sync-metadata.json");
5071
5667
  }
5072
5668
  store;
5073
5669
  registrations = /* @__PURE__ */ new Map();
@@ -5132,15 +5728,15 @@ var SyncManager = class {
5132
5728
  }
5133
5729
  async loadMetadata() {
5134
5730
  try {
5135
- const raw = await fs10.readFile(this.metadataPath, "utf-8");
5731
+ const raw = await fs12.readFile(this.metadataPath, "utf-8");
5136
5732
  return JSON.parse(raw);
5137
5733
  } catch {
5138
5734
  return { connectors: {} };
5139
5735
  }
5140
5736
  }
5141
5737
  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");
5738
+ await fs12.mkdir(path13.dirname(this.metadataPath), { recursive: true });
5739
+ await fs12.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
5144
5740
  }
5145
5741
  };
5146
5742
 
@@ -5245,6 +5841,14 @@ var JiraConnector = class {
5245
5841
  start
5246
5842
  );
5247
5843
  }
5844
+ try {
5845
+ const parsed = new URL(baseUrl);
5846
+ if (parsed.protocol !== "https:" && parsed.hostname !== "localhost") {
5847
+ return buildIngestResult(0, 0, [`${baseUrlEnv} must use HTTPS`], start);
5848
+ }
5849
+ } catch {
5850
+ return buildIngestResult(0, 0, [`${baseUrlEnv} is not a valid URL`], start);
5851
+ }
5248
5852
  const jql = buildJql(config);
5249
5853
  const headers = { Authorization: `Basic ${apiKey}`, "Content-Type": "application/json" };
5250
5854
  try {
@@ -5534,6 +6138,14 @@ var ConfluenceConnector = class {
5534
6138
  }
5535
6139
  const baseUrlEnv = config.baseUrlEnv ?? "CONFLUENCE_BASE_URL";
5536
6140
  const baseUrl = process.env[baseUrlEnv] ?? "";
6141
+ try {
6142
+ const parsed = new URL(baseUrl);
6143
+ if (parsed.protocol !== "https:" && parsed.hostname !== "localhost") {
6144
+ return missingApiKeyResult(`${baseUrlEnv} must use HTTPS`, start);
6145
+ }
6146
+ } catch {
6147
+ return missingApiKeyResult(`${baseUrlEnv} is not a valid URL`, start);
6148
+ }
5537
6149
  const spaceKey = config.spaceKey ?? "";
5538
6150
  const counts = await this.fetchAllPagesHandled(
5539
6151
  store,
@@ -5627,7 +6239,7 @@ var ConfluenceConnector = class {
5627
6239
  };
5628
6240
 
5629
6241
  // src/ingest/connectors/CIConnector.ts
5630
- function emptyResult2(errors, start) {
6242
+ function emptyResult5(errors, start) {
5631
6243
  return {
5632
6244
  nodesAdded: 0,
5633
6245
  nodesUpdated: 0,
@@ -5695,7 +6307,7 @@ var CIConnector = class {
5695
6307
  const apiKeyEnv = config.apiKeyEnv ?? "GITHUB_TOKEN";
5696
6308
  const apiKey = process.env[apiKeyEnv];
5697
6309
  if (!apiKey) {
5698
- return emptyResult2(
6310
+ return emptyResult5(
5699
6311
  [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
5700
6312
  start
5701
6313
  );
@@ -5759,6 +6371,14 @@ function hasConstraintKeyword(text) {
5759
6371
  const lower = text.toLowerCase();
5760
6372
  return CONSTRAINT_KEYWORDS.some((kw) => lower.includes(kw));
5761
6373
  }
6374
+ function buildNodeMetadata(condensed, base) {
6375
+ const metadata = { ...base };
6376
+ if (condensed.method !== "passthrough") {
6377
+ metadata.condensed = condensed.method;
6378
+ metadata.originalLength = condensed.originalLength;
6379
+ }
6380
+ return metadata;
6381
+ }
5762
6382
  function buildIngestResult2(nodesAdded, edgesAdded, errors, start) {
5763
6383
  return {
5764
6384
  nodesAdded,
@@ -5769,6 +6389,24 @@ function buildIngestResult2(nodesAdded, edgesAdded, errors, start) {
5769
6389
  durationMs: Date.now() - start
5770
6390
  };
5771
6391
  }
6392
+ function validateFigmaConfig(config) {
6393
+ const apiKeyEnv = config.apiKeyEnv ?? "FIGMA_API_KEY";
6394
+ const apiKey = process.env[apiKeyEnv];
6395
+ if (!apiKey) return { error: `Missing API key: environment variable "${apiKeyEnv}" is not set` };
6396
+ const baseUrlEnv = config.baseUrlEnv ?? "FIGMA_BASE_URL";
6397
+ const baseUrl = process.env[baseUrlEnv] ?? "https://api.figma.com";
6398
+ try {
6399
+ const parsed = new URL(baseUrl);
6400
+ if (parsed.protocol !== "https:" || !parsed.hostname.endsWith("api.figma.com")) {
6401
+ return { error: `Invalid ${baseUrlEnv}: must be an HTTPS URL on api.figma.com` };
6402
+ }
6403
+ } catch {
6404
+ return { error: `Invalid ${baseUrlEnv}: not a valid URL` };
6405
+ }
6406
+ const fileIds = config.fileIds;
6407
+ if (!fileIds || fileIds.length === 0) return { error: "No fileIds provided in connector config" };
6408
+ return { apiKey, baseUrl, fileIds };
6409
+ }
5772
6410
  var FigmaConnector = class {
5773
6411
  name = "figma";
5774
6412
  source = "figma";
@@ -5778,35 +6416,9 @@ var FigmaConnector = class {
5778
6416
  }
5779
6417
  async ingest(store, config) {
5780
6418
  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
- }
6419
+ const validated = validateFigmaConfig(config);
6420
+ if ("error" in validated) return buildIngestResult2(0, 0, [validated.error], start);
6421
+ const { apiKey, baseUrl, fileIds } = validated;
5810
6422
  const headers = { "X-FIGMA-TOKEN": apiKey };
5811
6423
  const maxLen = config.maxContentLength ?? 4e3;
5812
6424
  let nodesAdded = 0;
@@ -5826,6 +6438,17 @@ var FigmaConnector = class {
5826
6438
  return buildIngestResult2(nodesAdded, edgesAdded, errors, start);
5827
6439
  }
5828
6440
  async processFile(store, baseUrl, fileId, headers, maxLen) {
6441
+ let nodesAdded = 0;
6442
+ let edgesAdded = 0;
6443
+ const styleCounts = await this.ingestStyles(store, baseUrl, fileId, headers, maxLen);
6444
+ nodesAdded += styleCounts.nodesAdded;
6445
+ edgesAdded += styleCounts.edgesAdded;
6446
+ const componentCounts = await this.ingestComponents(store, baseUrl, fileId, headers, maxLen);
6447
+ nodesAdded += componentCounts.nodesAdded;
6448
+ edgesAdded += componentCounts.edgesAdded;
6449
+ return { nodesAdded, edgesAdded };
6450
+ }
6451
+ async ingestStyles(store, baseUrl, fileId, headers, maxLen) {
5829
6452
  let nodesAdded = 0;
5830
6453
  let edgesAdded = 0;
5831
6454
  const stylesUrl = `${baseUrl}/v1/files/${fileId}/styles`;
@@ -5833,90 +6456,112 @@ var FigmaConnector = class {
5833
6456
  if (!stylesResponse.ok) throw new Error(`Styles request failed for file ${fileId}`);
5834
6457
  const stylesData = await stylesResponse.json();
5835
6458
  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");
6459
+ const counts = await this.processStyle(store, style, fileId, maxLen);
6460
+ nodesAdded += counts.nodesAdded;
6461
+ edgesAdded += counts.edgesAdded;
5860
6462
  }
6463
+ return { nodesAdded, edgesAdded };
6464
+ }
6465
+ async processStyle(store, style, fileId, maxLen) {
6466
+ const nodeId = `figma:token:${style.key}`;
6467
+ const condensed = await condenseContent(sanitizeExternalText(style.description || "", 2e3), {
6468
+ maxLength: maxLen
6469
+ });
6470
+ const metadata = buildNodeMetadata(condensed, {
6471
+ source: "figma",
6472
+ key: style.key,
6473
+ styleType: style.style_type,
6474
+ fileId
6475
+ });
6476
+ store.addNode({
6477
+ id: nodeId,
6478
+ type: "design_token",
6479
+ name: sanitizeExternalText(style.name, 500),
6480
+ content: condensed.content,
6481
+ metadata
6482
+ });
6483
+ const searchText = sanitizeExternalText([style.name, style.description].join(" "));
6484
+ const edgesAdded = linkToCode(store, searchText, nodeId, "references");
6485
+ return { nodesAdded: 1, edgesAdded };
6486
+ }
6487
+ async ingestComponents(store, baseUrl, fileId, headers, maxLen) {
6488
+ let nodesAdded = 0;
6489
+ let edgesAdded = 0;
5861
6490
  const componentsUrl = `${baseUrl}/v1/files/${fileId}/components`;
5862
6491
  const componentsResponse = await this.httpClient(componentsUrl, { headers });
5863
6492
  if (!componentsResponse.ok) throw new Error(`Components request failed for file ${fileId}`);
5864
6493
  const componentsData = await componentsResponse.json();
5865
6494
  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
- }
6495
+ const counts = await this.processComponent(store, component, fileId, maxLen);
6496
+ nodesAdded += counts.nodesAdded;
6497
+ edgesAdded += counts.edgesAdded;
5917
6498
  }
5918
6499
  return { nodesAdded, edgesAdded };
5919
6500
  }
6501
+ async processComponent(store, component, fileId, maxLen) {
6502
+ let nodesAdded = 0;
6503
+ let edgesAdded = 0;
6504
+ const description = component.description || "";
6505
+ if (description) {
6506
+ const counts = await this.addComponentIntent(store, component, description, fileId, maxLen);
6507
+ nodesAdded += counts.nodesAdded;
6508
+ edgesAdded += counts.edgesAdded;
6509
+ }
6510
+ if (description && hasConstraintKeyword(description)) {
6511
+ const counts = await this.addComponentConstraint(
6512
+ store,
6513
+ component,
6514
+ description,
6515
+ fileId,
6516
+ maxLen
6517
+ );
6518
+ nodesAdded += counts.nodesAdded;
6519
+ edgesAdded += counts.edgesAdded;
6520
+ }
6521
+ return { nodesAdded, edgesAdded };
6522
+ }
6523
+ async addComponentIntent(store, component, description, fileId, maxLen) {
6524
+ const intentId = `figma:intent:${component.key}`;
6525
+ const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
6526
+ maxLength: maxLen
6527
+ });
6528
+ const metadata = buildNodeMetadata(condensed, {
6529
+ source: "figma",
6530
+ key: component.key,
6531
+ fileId
6532
+ });
6533
+ store.addNode({
6534
+ id: intentId,
6535
+ type: "aesthetic_intent",
6536
+ name: sanitizeExternalText(component.name, 500),
6537
+ content: condensed.content,
6538
+ metadata
6539
+ });
6540
+ const searchText = sanitizeExternalText([component.name, description].join(" "));
6541
+ const edgesAdded = linkToCode(store, searchText, intentId, "references");
6542
+ return { nodesAdded: 1, edgesAdded };
6543
+ }
6544
+ async addComponentConstraint(store, component, description, fileId, maxLen) {
6545
+ const constraintId = `figma:constraint:${component.key}`;
6546
+ const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
6547
+ maxLength: maxLen
6548
+ });
6549
+ const metadata = buildNodeMetadata(condensed, {
6550
+ source: "figma",
6551
+ key: component.key,
6552
+ fileId
6553
+ });
6554
+ store.addNode({
6555
+ id: constraintId,
6556
+ type: "design_constraint",
6557
+ name: sanitizeExternalText(component.name, 500),
6558
+ content: condensed.content,
6559
+ metadata
6560
+ });
6561
+ const searchText = sanitizeExternalText([component.name, description].join(" "));
6562
+ const edgesAdded = linkToCode(store, searchText, constraintId, "references");
6563
+ return { nodesAdded: 1, edgesAdded };
6564
+ }
5920
6565
  };
5921
6566
 
5922
6567
  // src/ingest/connectors/MiroConnector.ts
@@ -5935,6 +6580,46 @@ function buildIngestResult3(nodesAdded, edgesAdded, errors, start) {
5935
6580
  durationMs: Date.now() - start
5936
6581
  };
5937
6582
  }
6583
+ function validateBaseUrl(baseUrl, baseUrlEnv) {
6584
+ try {
6585
+ const parsed = new URL(baseUrl);
6586
+ if (parsed.protocol !== "https:" || parsed.hostname !== "api.miro.com") {
6587
+ return `Invalid ${baseUrlEnv}: must be an HTTPS URL on api.miro.com`;
6588
+ }
6589
+ } catch {
6590
+ return `Invalid ${baseUrlEnv}: not a valid URL`;
6591
+ }
6592
+ return null;
6593
+ }
6594
+ function resolveConfig(config, start) {
6595
+ const apiKeyEnv = config.apiKeyEnv ?? "MIRO_API_KEY";
6596
+ const apiKey = process.env[apiKeyEnv];
6597
+ if (!apiKey) {
6598
+ return {
6599
+ ok: false,
6600
+ result: buildIngestResult3(
6601
+ 0,
6602
+ 0,
6603
+ [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
6604
+ start
6605
+ )
6606
+ };
6607
+ }
6608
+ const baseUrlEnv = config.baseUrlEnv ?? "MIRO_BASE_URL";
6609
+ const baseUrl = process.env[baseUrlEnv] ?? "https://api.miro.com";
6610
+ const urlError = validateBaseUrl(baseUrl, baseUrlEnv);
6611
+ if (urlError) {
6612
+ return { ok: false, result: buildIngestResult3(0, 0, [urlError], start) };
6613
+ }
6614
+ const boardIds = config.boardIds;
6615
+ if (!boardIds || boardIds.length === 0) {
6616
+ return {
6617
+ ok: false,
6618
+ result: buildIngestResult3(0, 0, ["No boardIds provided in config"], start)
6619
+ };
6620
+ }
6621
+ return { ok: true, apiKey, baseUrl, boardIds, headers: { Authorization: `Bearer ${apiKey}` } };
6622
+ }
5938
6623
  var MiroConnector = class {
5939
6624
  name = "miro";
5940
6625
  source = "miro";
@@ -5944,36 +6629,9 @@ var MiroConnector = class {
5944
6629
  }
5945
6630
  async ingest(store, config) {
5946
6631
  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}` };
6632
+ const resolved = resolveConfig(config, start);
6633
+ if (!resolved.ok) return resolved.result;
6634
+ const { baseUrl, boardIds, headers } = resolved;
5977
6635
  let totalNodesAdded = 0;
5978
6636
  let totalEdgesAdded = 0;
5979
6637
  const errors = [];
@@ -6177,7 +6835,7 @@ var FusionLayer = class {
6177
6835
  };
6178
6836
 
6179
6837
  // src/entropy/GraphEntropyAdapter.ts
6180
- var CODE_NODE_TYPES5 = ["file", "function", "class", "method", "interface", "variable"];
6838
+ var CODE_NODE_TYPES6 = ["file", "function", "class", "method", "interface", "variable"];
6181
6839
  var GraphEntropyAdapter = class {
6182
6840
  constructor(store) {
6183
6841
  this.store = store;
@@ -6257,7 +6915,7 @@ var GraphEntropyAdapter = class {
6257
6915
  }
6258
6916
  findEntryPoints() {
6259
6917
  const entryPoints = [];
6260
- for (const nodeType of CODE_NODE_TYPES5) {
6918
+ for (const nodeType of CODE_NODE_TYPES6) {
6261
6919
  const nodes = this.store.findNodes({ type: nodeType });
6262
6920
  for (const node of nodes) {
6263
6921
  const isIndexFile = nodeType === "file" && node.name === "index.ts";
@@ -6293,7 +6951,7 @@ var GraphEntropyAdapter = class {
6293
6951
  }
6294
6952
  collectUnreachableNodes(visited) {
6295
6953
  const unreachableNodes = [];
6296
- for (const nodeType of CODE_NODE_TYPES5) {
6954
+ for (const nodeType of CODE_NODE_TYPES6) {
6297
6955
  const nodes = this.store.findNodes({ type: nodeType });
6298
6956
  for (const node of nodes) {
6299
6957
  if (!visited.has(node.id)) {
@@ -7139,9 +7797,9 @@ var EntityExtractor = class {
7139
7797
  extractPaths(trimmed, add) {
7140
7798
  const consumed = /* @__PURE__ */ new Set();
7141
7799
  for (const match of trimmed.matchAll(FILE_PATH_RE)) {
7142
- const path13 = match[0];
7143
- add(path13);
7144
- consumed.add(path13);
7800
+ const path15 = match[0];
7801
+ add(path15);
7802
+ consumed.add(path15);
7145
7803
  }
7146
7804
  return consumed;
7147
7805
  }
@@ -7213,8 +7871,8 @@ var EntityResolver = class {
7213
7871
  if (isPathLike && node.path.includes(raw)) {
7214
7872
  return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
7215
7873
  }
7216
- const basename7 = node.path.split("/").pop() ?? "";
7217
- if (basename7.includes(raw)) {
7874
+ const basename10 = node.path.split("/").pop() ?? "";
7875
+ if (basename10.includes(raw)) {
7218
7876
  return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
7219
7877
  }
7220
7878
  if (raw.length >= 4 && node.path.includes(raw)) {
@@ -7293,13 +7951,13 @@ var ResponseFormatter = class {
7293
7951
  const context = Array.isArray(d?.context) ? d.context : [];
7294
7952
  const firstEntity = entities[0];
7295
7953
  const nodeType = firstEntity?.node.type ?? "node";
7296
- const path13 = firstEntity?.node.path ?? "unknown";
7954
+ const path15 = firstEntity?.node.path ?? "unknown";
7297
7955
  let neighborCount = 0;
7298
7956
  const firstContext = context[0];
7299
7957
  if (firstContext && Array.isArray(firstContext.nodes)) {
7300
7958
  neighborCount = firstContext.nodes.length;
7301
7959
  }
7302
- return `**${entityName}** is a ${nodeType} at \`${path13}\`. Connected to ${neighborCount} nodes.`;
7960
+ return `**${entityName}** is a ${nodeType} at \`${path15}\`. Connected to ${neighborCount} nodes.`;
7303
7961
  }
7304
7962
  formatAnomaly(data) {
7305
7963
  const d = data;
@@ -7682,7 +8340,7 @@ var PHASE_NODE_TYPES = {
7682
8340
  debug: ["failure", "learning", "function", "method"],
7683
8341
  plan: ["adr", "document", "module", "layer"]
7684
8342
  };
7685
- var CODE_NODE_TYPES6 = /* @__PURE__ */ new Set([
8343
+ var CODE_NODE_TYPES7 = /* @__PURE__ */ new Set([
7686
8344
  "file",
7687
8345
  "function",
7688
8346
  "class",
@@ -7901,7 +8559,7 @@ var Assembler = class {
7901
8559
  */
7902
8560
  checkCoverage() {
7903
8561
  const codeNodes = [];
7904
- for (const type of CODE_NODE_TYPES6) {
8562
+ for (const type of CODE_NODE_TYPES7) {
7905
8563
  codeNodes.push(...this.store.findNodes({ type }));
7906
8564
  }
7907
8565
  const documented = [];
@@ -8081,14 +8739,14 @@ var GraphConstraintAdapter = class {
8081
8739
  };
8082
8740
 
8083
8741
  // src/ingest/DesignIngestor.ts
8084
- var fs11 = __toESM(require("fs/promises"));
8085
- var path12 = __toESM(require("path"));
8742
+ var fs13 = __toESM(require("fs/promises"));
8743
+ var path14 = __toESM(require("path"));
8086
8744
  function isDTCGToken(obj) {
8087
8745
  return typeof obj === "object" && obj !== null && "$value" in obj && "$type" in obj;
8088
8746
  }
8089
8747
  async function readFileOrNull(filePath) {
8090
8748
  try {
8091
- return await fs11.readFile(filePath, "utf-8");
8749
+ return await fs13.readFile(filePath, "utf-8");
8092
8750
  } catch {
8093
8751
  return null;
8094
8752
  }
@@ -8234,8 +8892,8 @@ var DesignIngestor = class {
8234
8892
  async ingestAll(designDir) {
8235
8893
  const start = Date.now();
8236
8894
  const [tokensResult, intentResult] = await Promise.all([
8237
- this.ingestTokens(path12.join(designDir, "tokens.json")),
8238
- this.ingestDesignIntent(path12.join(designDir, "DESIGN.md"))
8895
+ this.ingestTokens(path14.join(designDir, "tokens.json")),
8896
+ this.ingestDesignIntent(path14.join(designDir, "DESIGN.md"))
8239
8897
  ]);
8240
8898
  const merged = mergeResults(tokensResult, intentResult);
8241
8899
  return { ...merged, durationMs: Date.now() - start };
@@ -8471,10 +9129,10 @@ var TaskIndependenceAnalyzer = class {
8471
9129
  includeTypes: ["file"]
8472
9130
  });
8473
9131
  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);
9132
+ const path15 = n.path ?? n.id.replace(/^file:/, "");
9133
+ if (!fileSet.has(path15)) {
9134
+ if (!result.has(path15)) {
9135
+ result.set(path15, file);
8478
9136
  }
8479
9137
  }
8480
9138
  }
@@ -8851,7 +9509,7 @@ var ConflictPredictor = class {
8851
9509
  };
8852
9510
 
8853
9511
  // src/index.ts
8854
- var VERSION = "0.4.3";
9512
+ var VERSION = "0.6.0";
8855
9513
  // Annotate the CommonJS export names for ESM import in node:
8856
9514
  0 && (module.exports = {
8857
9515
  ALL_EXTRACTORS,
@@ -8869,6 +9527,7 @@ var VERSION = "0.4.3";
8869
9527
  ContradictionDetector,
8870
9528
  CoverageScorer,
8871
9529
  D2Parser,
9530
+ DecisionIngestor,
8872
9531
  DesignConstraintAdapter,
8873
9532
  DesignIngestor,
8874
9533
  DiagramParser,
@@ -8893,6 +9552,7 @@ var VERSION = "0.4.3";
8893
9552
  ImageAnalysisExtractor,
8894
9553
  IntentClassifier,
8895
9554
  JiraConnector,
9555
+ KnowledgeDocMaterializer,
8896
9556
  KnowledgeIngestor,
8897
9557
  KnowledgePipelineRunner,
8898
9558
  KnowledgeStagingAggregator,