@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.mjs CHANGED
@@ -24,7 +24,7 @@ var NODE_TYPES = [
24
24
  "build",
25
25
  "test_result",
26
26
  "execution_outcome",
27
- // Observability (future)
27
+ // Observability — reserved for future tracing/metrics integration
28
28
  "span",
29
29
  "metric",
30
30
  "log",
@@ -71,7 +71,7 @@ var EDGE_TYPES = [
71
71
  "triggered_by",
72
72
  "failed_in",
73
73
  "outcome_of",
74
- // Execution relationships (future)
74
+ // Execution relationships — reserved for future observability integration
75
75
  "executed_by",
76
76
  "measured_by",
77
77
  // Design relationships
@@ -146,7 +146,7 @@ function streamGraphJson(filePath, nodes, edges) {
146
146
  stream.write(JSON.stringify(edges[i]));
147
147
  }
148
148
  stream.write("]}");
149
- stream.end(resolve);
149
+ stream.end(() => resolve());
150
150
  });
151
151
  }
152
152
  async function saveGraph(dirPath, nodes, edges) {
@@ -169,15 +169,19 @@ async function loadGraph(dirPath) {
169
169
  await access(metaPath);
170
170
  await access(graphPath);
171
171
  } catch {
172
- return null;
172
+ return { status: "not_found" };
173
173
  }
174
174
  const metaContent = await readFile(metaPath, "utf-8");
175
175
  const metadata = JSON.parse(metaContent);
176
176
  if (metadata.schemaVersion !== CURRENT_SCHEMA_VERSION) {
177
- return null;
177
+ return {
178
+ status: "schema_mismatch",
179
+ found: metadata.schemaVersion,
180
+ expected: CURRENT_SCHEMA_VERSION
181
+ };
178
182
  }
179
183
  const graphContent = await readFile(graphPath, "utf-8");
180
- return JSON.parse(graphContent);
184
+ return { status: "loaded", graph: JSON.parse(graphContent) };
181
185
  }
182
186
 
183
187
  // src/store/GraphStore.ts
@@ -352,8 +356,15 @@ var GraphStore = class {
352
356
  await saveGraph(dirPath, allNodes, allEdges);
353
357
  }
354
358
  async load(dirPath) {
355
- const data = await loadGraph(dirPath);
356
- if (!data) return false;
359
+ const result = await loadGraph(dirPath);
360
+ if (result.status === "not_found") return false;
361
+ if (result.status === "schema_mismatch") {
362
+ console.warn(
363
+ `[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.`
364
+ );
365
+ return false;
366
+ }
367
+ const data = result.graph;
357
368
  this.clear();
358
369
  for (const node of data.nodes) {
359
370
  this.nodeMap.set(node.id, { ...node });
@@ -797,9 +808,10 @@ var CodeIngestor = class {
797
808
  let edgesAdded = 0;
798
809
  const files = await this.findSourceFiles(rootDir);
799
810
  const nameToFiles = /* @__PURE__ */ new Map();
811
+ const contentCache = /* @__PURE__ */ new Map();
800
812
  for (const filePath of files) {
801
813
  try {
802
- const result = await this.processFile(filePath, rootDir, nameToFiles);
814
+ const result = await this.processFile(filePath, rootDir, nameToFiles, contentCache);
803
815
  nodesAdded += result.nodesAdded;
804
816
  edgesAdded += result.edgesAdded;
805
817
  } catch (err) {
@@ -809,7 +821,8 @@ var CodeIngestor = class {
809
821
  for (const filePath of files) {
810
822
  try {
811
823
  const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
812
- const content = await fs.readFile(filePath, "utf-8");
824
+ const content = contentCache.get(filePath);
825
+ if (content === void 0) continue;
813
826
  const callsEdges = this.extractCallsEdgesForFile(relativePath, content, nameToFiles);
814
827
  for (const edge of callsEdges) {
815
828
  this.store.addEdge(edge);
@@ -820,7 +833,8 @@ var CodeIngestor = class {
820
833
  }
821
834
  for (const filePath of files) {
822
835
  try {
823
- const content = await fs.readFile(filePath, "utf-8");
836
+ const content = contentCache.get(filePath);
837
+ if (content === void 0) continue;
824
838
  edgesAdded += this.extractReqAnnotationsForFile(filePath, content, rootDir);
825
839
  } catch {
826
840
  }
@@ -834,11 +848,12 @@ var CodeIngestor = class {
834
848
  durationMs: Date.now() - start
835
849
  };
836
850
  }
837
- async processFile(filePath, rootDir, nameToFiles) {
851
+ async processFile(filePath, rootDir, nameToFiles, contentCache) {
838
852
  let nodesAdded = 0;
839
853
  let edgesAdded = 0;
840
854
  const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
841
855
  const content = await fs.readFile(filePath, "utf-8");
856
+ contentCache.set(filePath, content);
842
857
  const stat2 = await fs.stat(filePath);
843
858
  const fileId = `file:${relativePath}`;
844
859
  const fileNode = {
@@ -2016,9 +2031,119 @@ function parseFrontmatter(raw) {
2016
2031
  };
2017
2032
  }
2018
2033
 
2019
- // src/ingest/RequirementIngestor.ts
2034
+ // src/ingest/DecisionIngestor.ts
2020
2035
  import * as fs4 from "fs/promises";
2021
2036
  import * as path5 from "path";
2037
+ var CODE_NODE_TYPES3 = [
2038
+ "file",
2039
+ "function",
2040
+ "class",
2041
+ "method",
2042
+ "interface",
2043
+ "variable"
2044
+ ];
2045
+ var DecisionIngestor = class {
2046
+ constructor(store) {
2047
+ this.store = store;
2048
+ }
2049
+ store;
2050
+ async ingest(decisionsDir) {
2051
+ const start = Date.now();
2052
+ const errors = [];
2053
+ let files;
2054
+ try {
2055
+ files = await this.findDecisionFiles(decisionsDir);
2056
+ } catch {
2057
+ return emptyResult(Date.now() - start);
2058
+ }
2059
+ let nodesAdded = 0;
2060
+ let edgesAdded = 0;
2061
+ for (const filePath of files) {
2062
+ try {
2063
+ const raw = await fs4.readFile(filePath, "utf-8");
2064
+ const parsed = this.parseFrontmatter(raw);
2065
+ if (!parsed) continue;
2066
+ const { frontmatter, body } = parsed;
2067
+ if (!frontmatter.number || !frontmatter.title) continue;
2068
+ const filename = path5.basename(filePath, ".md");
2069
+ const nodeId = `decision:${filename}`;
2070
+ const node = {
2071
+ id: nodeId,
2072
+ type: "decision",
2073
+ name: frontmatter.title,
2074
+ path: filePath,
2075
+ content: body.trim(),
2076
+ metadata: {
2077
+ number: frontmatter.number,
2078
+ ...frontmatter.date && { date: frontmatter.date },
2079
+ ...frontmatter.status && { status: frontmatter.status },
2080
+ ...frontmatter.tier && { tier: frontmatter.tier },
2081
+ ...frontmatter.source && { source: frontmatter.source },
2082
+ ...frontmatter.supersedes && { supersedes: frontmatter.supersedes }
2083
+ }
2084
+ };
2085
+ this.store.addNode(node);
2086
+ nodesAdded++;
2087
+ edgesAdded += this.linkToCode(body, nodeId);
2088
+ } catch (err) {
2089
+ errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
2090
+ }
2091
+ }
2092
+ return {
2093
+ nodesAdded,
2094
+ nodesUpdated: 0,
2095
+ edgesAdded,
2096
+ edgesUpdated: 0,
2097
+ errors,
2098
+ durationMs: Date.now() - start
2099
+ };
2100
+ }
2101
+ parseFrontmatter(raw) {
2102
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
2103
+ if (!match) return null;
2104
+ const yamlBlock = match[1];
2105
+ const body = match[2];
2106
+ const frontmatter = {};
2107
+ for (const line of yamlBlock.split("\n")) {
2108
+ const kvMatch = line.match(/^(\w+):\s*(.+)$/);
2109
+ if (!kvMatch) continue;
2110
+ frontmatter[kvMatch[1]] = kvMatch[2].trim();
2111
+ }
2112
+ if (!frontmatter.number || !frontmatter.title) return null;
2113
+ return {
2114
+ frontmatter,
2115
+ body
2116
+ };
2117
+ }
2118
+ linkToCode(content, sourceNodeId) {
2119
+ let count = 0;
2120
+ for (const nodeType of CODE_NODE_TYPES3) {
2121
+ const codeNodes = this.store.findNodes({ type: nodeType });
2122
+ for (const node of codeNodes) {
2123
+ if (node.name.length < 3) continue;
2124
+ const escaped = node.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2125
+ const namePattern = new RegExp(`\\b${escaped}\\b`, "i");
2126
+ if (namePattern.test(content)) {
2127
+ this.store.addEdge({
2128
+ from: sourceNodeId,
2129
+ to: node.id,
2130
+ type: "decided"
2131
+ });
2132
+ count++;
2133
+ }
2134
+ }
2135
+ }
2136
+ return count;
2137
+ }
2138
+ async findDecisionFiles(dir) {
2139
+ const entries = await fs4.readdir(dir, { withFileTypes: true });
2140
+ return entries.filter((e) => e.isFile() && e.name.endsWith(".md") && e.name !== "README.md").map((e) => path5.join(dir, e.name));
2141
+ }
2142
+ };
2143
+
2144
+ // src/ingest/RequirementIngestor.ts
2145
+ import * as fs5 from "fs/promises";
2146
+ import * as path6 from "path";
2022
2147
  var REQUIREMENT_SECTIONS = [
2023
2148
  "Observable Truths",
2024
2149
  "Success Criteria",
@@ -2035,7 +2160,7 @@ function detectEarsPattern(text) {
2035
2160
  if (/^the\s+\w+\s+shall\b/.test(lower)) return "ubiquitous";
2036
2161
  return void 0;
2037
2162
  }
2038
- var CODE_NODE_TYPES3 = ["file", "function", "class", "method", "interface", "variable"];
2163
+ var CODE_NODE_TYPES4 = ["file", "function", "class", "method", "interface", "variable"];
2039
2164
  var RequirementIngestor = class {
2040
2165
  constructor(store) {
2041
2166
  this.store = store;
@@ -2053,8 +2178,8 @@ var RequirementIngestor = class {
2053
2178
  let edgesAdded = 0;
2054
2179
  let featureDirs;
2055
2180
  try {
2056
- const entries = await fs4.readdir(specsDir, { withFileTypes: true });
2057
- featureDirs = entries.filter((e) => e.isDirectory()).map((e) => path5.join(specsDir, e.name));
2181
+ const entries = await fs5.readdir(specsDir, { withFileTypes: true });
2182
+ featureDirs = entries.filter((e) => e.isDirectory()).map((e) => path6.join(specsDir, e.name));
2058
2183
  } catch {
2059
2184
  return emptyResult(Date.now() - start);
2060
2185
  }
@@ -2073,11 +2198,11 @@ var RequirementIngestor = class {
2073
2198
  };
2074
2199
  }
2075
2200
  async ingestFeatureDir(featureDir, errors) {
2076
- const featureName = path5.basename(featureDir);
2077
- const specPath = path5.join(featureDir, "proposal.md").replaceAll("\\", "/");
2201
+ const featureName = path6.basename(featureDir);
2202
+ const specPath = path6.join(featureDir, "proposal.md").replaceAll("\\", "/");
2078
2203
  let content;
2079
2204
  try {
2080
- content = await fs4.readFile(specPath, "utf-8");
2205
+ content = await fs5.readFile(specPath, "utf-8");
2081
2206
  } catch {
2082
2207
  return { nodesAdded: 0, edgesAdded: 0 };
2083
2208
  }
@@ -2094,7 +2219,7 @@ var RequirementIngestor = class {
2094
2219
  this.store.addNode({
2095
2220
  id: specNodeId,
2096
2221
  type: "document",
2097
- name: path5.basename(specPath),
2222
+ name: path6.basename(specPath),
2098
2223
  path: specPath,
2099
2224
  metadata: { featureName }
2100
2225
  });
@@ -2209,9 +2334,9 @@ var RequirementIngestor = class {
2209
2334
  for (const node of fileNodes) {
2210
2335
  if (!node.path) continue;
2211
2336
  const normalizedPath = node.path.replace(/\\/g, "/");
2212
- const isCodeMatch = normalizedPath.includes("packages/") && path5.basename(normalizedPath).includes(featureName);
2337
+ const isCodeMatch = normalizedPath.includes("packages/") && path6.basename(normalizedPath).includes(featureName);
2213
2338
  const isTestMatch = normalizedPath.includes("/tests/") && // platform-safe
2214
- path5.basename(normalizedPath).includes(featureName);
2339
+ path6.basename(normalizedPath).includes(featureName);
2215
2340
  if (isCodeMatch && !isTestMatch) {
2216
2341
  this.store.addEdge({
2217
2342
  from: reqId,
@@ -2240,7 +2365,7 @@ var RequirementIngestor = class {
2240
2365
  */
2241
2366
  linkByKeywordOverlap(reqId, reqText) {
2242
2367
  let count = 0;
2243
- for (const nodeType of CODE_NODE_TYPES3) {
2368
+ for (const nodeType of CODE_NODE_TYPES4) {
2244
2369
  const codeNodes = this.store.findNodes({ type: nodeType });
2245
2370
  for (const node of codeNodes) {
2246
2371
  if (node.name.length < 3) continue;
@@ -2264,19 +2389,158 @@ var RequirementIngestor = class {
2264
2389
  };
2265
2390
 
2266
2391
  // src/ingest/KnowledgePipelineRunner.ts
2267
- import * as fs9 from "fs/promises";
2268
- import * as path10 from "path";
2392
+ import * as fs11 from "fs/promises";
2393
+ import * as path12 from "path";
2269
2394
 
2270
2395
  // src/ingest/DiagramParser.ts
2271
- import * as fs5 from "fs/promises";
2272
- import * as path6 from "path";
2273
- function emptyMermaidResult(diagramType = "unknown") {
2396
+ import * as fs6 from "fs/promises";
2397
+ import * as path7 from "path";
2398
+
2399
+ // src/ingest/parsers/mermaid.ts
2400
+ function emptyResult2(diagramType = "unknown") {
2274
2401
  return {
2275
2402
  entities: [],
2276
2403
  relationships: [],
2277
2404
  metadata: { format: "mermaid", diagramType }
2278
2405
  };
2279
2406
  }
2407
+ function detectDiagramType(content) {
2408
+ for (const line of content.split("\n")) {
2409
+ const trimmed = line.trim();
2410
+ if (!trimmed) continue;
2411
+ if (/^(?:graph|flowchart)\b/i.test(trimmed)) return "flowchart";
2412
+ if (/^sequenceDiagram\b/.test(trimmed)) return "sequence";
2413
+ if (/^classDiagram\b/.test(trimmed)) return "class";
2414
+ if (/^erDiagram\b/.test(trimmed)) return "er";
2415
+ break;
2416
+ }
2417
+ return "unknown";
2418
+ }
2419
+ function isDecisionNode(content, nodeId) {
2420
+ const decisionRegex = new RegExp(`${nodeId}\\s*\\{`);
2421
+ return decisionRegex.test(content);
2422
+ }
2423
+ function buildFlowchartEntity(content, match) {
2424
+ const id = match[1] ?? "";
2425
+ const label = (match[2] ?? "").trim();
2426
+ if (!id) return null;
2427
+ const decision = isDecisionNode(content, id);
2428
+ const entity = {
2429
+ id,
2430
+ label,
2431
+ ...decision ? { type: "decision" } : {}
2432
+ };
2433
+ return { id, entity };
2434
+ }
2435
+ function extractFlowchartNodes(content) {
2436
+ const entities = /* @__PURE__ */ new Map();
2437
+ const nodeRegex = /([A-Za-z0-9_]+)\s*[[({]([^\])}]+)[\])}]/g;
2438
+ let match = nodeRegex.exec(content);
2439
+ while (match !== null) {
2440
+ const parsed = buildFlowchartEntity(content, match);
2441
+ if (parsed && !entities.has(parsed.id)) {
2442
+ entities.set(parsed.id, parsed.entity);
2443
+ }
2444
+ match = nodeRegex.exec(content);
2445
+ }
2446
+ return entities;
2447
+ }
2448
+ function parseLabeledEdgeMatch(match) {
2449
+ const from = match[1] ?? "";
2450
+ const label = (match[2] ?? "").trim();
2451
+ const to = match[3] ?? "";
2452
+ if (!from || !to) return null;
2453
+ return { key: `${from}->${to}:${label}`, rel: { from, to, label } };
2454
+ }
2455
+ function extractLabeledEdges(stripped, edgeKeys) {
2456
+ const relationships = [];
2457
+ const labeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\|([^|]+)\|\s*([A-Za-z0-9_]+)/g;
2458
+ let match = labeledEdgeRegex.exec(stripped);
2459
+ while (match !== null) {
2460
+ const parsed = parseLabeledEdgeMatch(match);
2461
+ if (parsed && !edgeKeys.has(parsed.key)) {
2462
+ edgeKeys.add(parsed.key);
2463
+ relationships.push(parsed.rel);
2464
+ }
2465
+ match = labeledEdgeRegex.exec(stripped);
2466
+ }
2467
+ return relationships;
2468
+ }
2469
+ function hasLabeledEdgeBetween(labeledEdges, from, to) {
2470
+ return labeledEdges.some((r) => r.from === from && r.to === to);
2471
+ }
2472
+ function extractUnlabeledEdges(stripped, edgeKeys, labeledEdges) {
2473
+ const relationships = [];
2474
+ const unlabeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\s+([A-Za-z0-9_]+)/g;
2475
+ let match = unlabeledEdgeRegex.exec(stripped);
2476
+ while (match !== null) {
2477
+ const from = match[1] ?? "";
2478
+ const to = match[2] ?? "";
2479
+ const key = `${from}->${to}`;
2480
+ if (from && to && !hasLabeledEdgeBetween(labeledEdges, from, to) && !edgeKeys.has(key)) {
2481
+ edgeKeys.add(key);
2482
+ relationships.push({ from, to });
2483
+ }
2484
+ match = unlabeledEdgeRegex.exec(stripped);
2485
+ }
2486
+ return relationships;
2487
+ }
2488
+ function parseFlowchart(content, diagramType) {
2489
+ const entities = extractFlowchartNodes(content);
2490
+ const stripped = content.replace(/[[({][^\])}]*[\])}]/g, "");
2491
+ const edgeKeys = /* @__PURE__ */ new Set();
2492
+ const labeledEdges = extractLabeledEdges(stripped, edgeKeys);
2493
+ const unlabeledEdges = extractUnlabeledEdges(stripped, edgeKeys, labeledEdges);
2494
+ return {
2495
+ entities: Array.from(entities.values()),
2496
+ relationships: [...labeledEdges, ...unlabeledEdges],
2497
+ metadata: { format: "mermaid", diagramType }
2498
+ };
2499
+ }
2500
+ function extractParticipants(content) {
2501
+ const entities = [];
2502
+ const seenParticipants = /* @__PURE__ */ new Set();
2503
+ const participantRegex = /participant\s+(\w+)(?:\s+as\s+(.+))?/g;
2504
+ let match = participantRegex.exec(content);
2505
+ while (match !== null) {
2506
+ const id = match[1] ?? "";
2507
+ const label = match[2]?.trim() || id;
2508
+ if (id && !seenParticipants.has(id)) {
2509
+ seenParticipants.add(id);
2510
+ entities.push({ id, label });
2511
+ }
2512
+ match = participantRegex.exec(content);
2513
+ }
2514
+ return entities;
2515
+ }
2516
+ function relationshipFromMatch(match) {
2517
+ const from = match[1] ?? "";
2518
+ const to = match[2] ?? "";
2519
+ const label = (match[3] ?? "").trim();
2520
+ return from && to ? { from, to, label } : null;
2521
+ }
2522
+ function collectMessageMatches(content, regex) {
2523
+ const results = [];
2524
+ let match = regex.exec(content);
2525
+ while (match !== null) {
2526
+ const rel = relationshipFromMatch(match);
2527
+ if (rel) results.push(rel);
2528
+ match = regex.exec(content);
2529
+ }
2530
+ return results;
2531
+ }
2532
+ function extractMessages(content) {
2533
+ const forward = collectMessageMatches(content, /(\w+)\s*->>?\+?\s*(\w+)\s*:\s*(.+)/g);
2534
+ const returns = collectMessageMatches(content, /(\w+)\s*-->>?-?\s*(\w+)\s*:\s*(.+)/g);
2535
+ return [...forward, ...returns];
2536
+ }
2537
+ function parseSequence(content, diagramType) {
2538
+ return {
2539
+ entities: extractParticipants(content),
2540
+ relationships: extractMessages(content),
2541
+ metadata: { format: "mermaid", diagramType }
2542
+ };
2543
+ }
2280
2544
  var MermaidParser = class {
2281
2545
  canParse(_content, ext) {
2282
2546
  return ext === ".mmd" || ext === ".mermaid";
@@ -2284,269 +2548,199 @@ var MermaidParser = class {
2284
2548
  parse(content, _filePath) {
2285
2549
  const trimmed = content.trim();
2286
2550
  if (!trimmed) {
2287
- return emptyMermaidResult();
2551
+ return emptyResult2();
2288
2552
  }
2289
- const diagramType = this.detectDiagramType(trimmed);
2553
+ const diagramType = detectDiagramType(trimmed);
2290
2554
  switch (diagramType) {
2291
2555
  case "flowchart":
2292
- return this.parseFlowchart(trimmed, diagramType);
2556
+ return parseFlowchart(trimmed, diagramType);
2293
2557
  case "sequence":
2294
- return this.parseSequence(trimmed, diagramType);
2558
+ return parseSequence(trimmed, diagramType);
2295
2559
  default:
2296
- return emptyMermaidResult(diagramType);
2560
+ return emptyResult2(diagramType);
2297
2561
  }
2298
2562
  }
2299
- detectDiagramType(content) {
2300
- const lines = content.split("\n");
2301
- for (const line of lines) {
2302
- const trimmedLine = line.trim();
2303
- if (!trimmedLine) continue;
2304
- if (/^(?:graph|flowchart)\b/i.test(trimmedLine)) return "flowchart";
2305
- if (/^sequenceDiagram\b/.test(trimmedLine)) return "sequence";
2306
- if (/^classDiagram\b/.test(trimmedLine)) return "class";
2307
- if (/^erDiagram\b/.test(trimmedLine)) return "er";
2308
- break;
2309
- }
2310
- return "unknown";
2563
+ };
2564
+
2565
+ // src/ingest/parsers/d2.ts
2566
+ function emptyResult3() {
2567
+ return {
2568
+ entities: [],
2569
+ relationships: [],
2570
+ metadata: { format: "d2", diagramType: "architecture" }
2571
+ };
2572
+ }
2573
+ function parseBlockShape(stripped) {
2574
+ const match = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+?)\s*\{$/);
2575
+ if (match) {
2576
+ const id = match[1];
2577
+ const label = match[2];
2578
+ if (id && label) return { id, label };
2579
+ }
2580
+ return null;
2581
+ }
2582
+ function parseConnection(stripped) {
2583
+ const match = stripped.match(/^([a-zA-Z0-9_.-]+)\s*->\s*([a-zA-Z0-9_.-]+)(?:\s*:\s*(.+?))?$/);
2584
+ if (match) {
2585
+ const from = match[1] ?? "";
2586
+ const to = match[2] ?? "";
2587
+ const label = match[3]?.trim();
2588
+ if (from && to) return { from, to, ...label ? { label } : {} };
2589
+ }
2590
+ return null;
2591
+ }
2592
+ function parseSimpleShape(stripped) {
2593
+ const match = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+)$/);
2594
+ if (match) {
2595
+ const id = match[1];
2596
+ const label = (match[2] ?? "").trim();
2597
+ if (id && label) return { id, label };
2598
+ }
2599
+ return null;
2600
+ }
2601
+ function handleBlockOpen(stripped, state) {
2602
+ if (state.braceDepth === 0) {
2603
+ const shape = parseBlockShape(stripped);
2604
+ if (shape) state.entities.set(shape.id, { id: shape.id, label: shape.label });
2311
2605
  }
2312
- parseFlowchart(content, diagramType) {
2313
- const entities = /* @__PURE__ */ new Map();
2314
- const relationships = [];
2315
- const nodeRegex = /([A-Za-z0-9_]+)\s*[[({]([^\])}]+)[\])}]/g;
2316
- let match;
2317
- match = nodeRegex.exec(content);
2318
- while (match !== null) {
2319
- const id = match[1] ?? "";
2320
- const label = (match[2] ?? "").trim();
2321
- if (id && !entities.has(id)) {
2322
- const isDecision = this.isDecisionNode(content, id);
2323
- entities.set(id, {
2324
- id,
2325
- label,
2326
- ...isDecision ? { type: "decision" } : {}
2327
- });
2328
- }
2329
- match = nodeRegex.exec(content);
2330
- }
2331
- const stripped = content.replace(/[[({][^\])}]*[\])}]/g, "");
2332
- const labeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\|([^|]+)\|\s*([A-Za-z0-9_]+)/g;
2333
- const unlabeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\s+([A-Za-z0-9_]+)/g;
2334
- const edgeKeys = /* @__PURE__ */ new Set();
2335
- match = labeledEdgeRegex.exec(stripped);
2336
- while (match !== null) {
2337
- const from = match[1] ?? "";
2338
- const label = (match[2] ?? "").trim();
2339
- const to = match[3] ?? "";
2340
- if (from && to) {
2341
- const key = `${from}->${to}:${label}`;
2342
- if (!edgeKeys.has(key)) {
2343
- edgeKeys.add(key);
2344
- relationships.push({ from, to, label });
2345
- }
2346
- }
2347
- match = labeledEdgeRegex.exec(stripped);
2348
- }
2349
- match = unlabeledEdgeRegex.exec(stripped);
2350
- while (match !== null) {
2351
- const from = match[1] ?? "";
2352
- const to = match[2] ?? "";
2353
- if (from && to) {
2354
- const hasLabeled = relationships.some((r) => r.from === from && r.to === to);
2355
- if (!hasLabeled) {
2356
- const key = `${from}->${to}`;
2357
- if (!edgeKeys.has(key)) {
2358
- edgeKeys.add(key);
2359
- relationships.push({ from, to });
2360
- }
2361
- }
2362
- }
2363
- match = unlabeledEdgeRegex.exec(stripped);
2364
- }
2365
- return {
2366
- entities: Array.from(entities.values()),
2367
- relationships,
2368
- metadata: { format: "mermaid", diagramType }
2369
- };
2606
+ state.braceDepth++;
2607
+ }
2608
+ function processTopLevelLine(stripped, state) {
2609
+ const conn = parseConnection(stripped);
2610
+ if (conn) {
2611
+ state.relationships.push(conn);
2612
+ return;
2370
2613
  }
2371
- isDecisionNode(content, nodeId) {
2372
- const decisionRegex = new RegExp(`${nodeId}\\s*\\{`);
2373
- return decisionRegex.test(content);
2614
+ const shape = parseSimpleShape(stripped);
2615
+ if (shape && !state.entities.has(shape.id)) {
2616
+ state.entities.set(shape.id, { id: shape.id, label: shape.label });
2374
2617
  }
2375
- parseSequence(content, diagramType) {
2376
- const entities = [];
2377
- const relationships = [];
2378
- const seenParticipants = /* @__PURE__ */ new Set();
2379
- const participantRegex = /participant\s+(\w+)(?:\s+as\s+(.+))?/g;
2380
- let match;
2381
- match = participantRegex.exec(content);
2382
- while (match !== null) {
2383
- const id = match[1] ?? "";
2384
- const label = match[2]?.trim() || id;
2385
- if (id && !seenParticipants.has(id)) {
2386
- seenParticipants.add(id);
2387
- entities.push({ id, label });
2388
- }
2389
- match = participantRegex.exec(content);
2390
- }
2391
- const forwardMsgRegex = /(\w+)\s*->>?\+?\s*(\w+)\s*:\s*(.+)/g;
2392
- match = forwardMsgRegex.exec(content);
2393
- while (match !== null) {
2394
- const from = match[1] ?? "";
2395
- const to = match[2] ?? "";
2396
- const label = (match[3] ?? "").trim();
2397
- if (from && to) {
2398
- relationships.push({ from, to, label });
2399
- }
2400
- match = forwardMsgRegex.exec(content);
2401
- }
2402
- const returnMsgRegex = /(\w+)\s*-->>?-?\s*(\w+)\s*:\s*(.+)/g;
2403
- match = returnMsgRegex.exec(content);
2404
- while (match !== null) {
2405
- const from = match[1] ?? "";
2406
- const to = match[2] ?? "";
2407
- const label = (match[3] ?? "").trim();
2408
- if (from && to) {
2409
- relationships.push({ from, to, label });
2410
- }
2411
- match = returnMsgRegex.exec(content);
2412
- }
2413
- return {
2414
- entities,
2415
- relationships,
2416
- metadata: { format: "mermaid", diagramType }
2417
- };
2618
+ }
2619
+ function processD2Line(stripped, state) {
2620
+ if (!stripped || stripped.startsWith("#")) return;
2621
+ if (stripped.endsWith("{")) {
2622
+ handleBlockOpen(stripped, state);
2623
+ return;
2418
2624
  }
2419
- };
2625
+ if (stripped === "}") {
2626
+ state.braceDepth = Math.max(0, state.braceDepth - 1);
2627
+ return;
2628
+ }
2629
+ if (state.braceDepth > 0) return;
2630
+ processTopLevelLine(stripped, state);
2631
+ }
2420
2632
  var D2Parser = class {
2421
2633
  canParse(_content, ext) {
2422
2634
  return ext === ".d2";
2423
2635
  }
2424
2636
  parse(content, _filePath) {
2425
2637
  const trimmed = content.trim();
2426
- if (!trimmed) {
2427
- return {
2428
- entities: [],
2429
- relationships: [],
2430
- metadata: { format: "d2", diagramType: "architecture" }
2431
- };
2432
- }
2433
- const entities = /* @__PURE__ */ new Map();
2434
- const relationships = [];
2435
- let braceDepth = 0;
2638
+ if (!trimmed) return emptyResult3();
2639
+ const state = {
2640
+ entities: /* @__PURE__ */ new Map(),
2641
+ relationships: [],
2642
+ braceDepth: 0
2643
+ };
2436
2644
  for (const line of trimmed.split("\n")) {
2437
- const stripped = line.trim();
2438
- if (!stripped || stripped.startsWith("#")) continue;
2439
- if (stripped.endsWith("{")) {
2440
- if (braceDepth === 0) {
2441
- const shapeMatch = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+?)\s*\{$/);
2442
- if (shapeMatch) {
2443
- const id = shapeMatch[1];
2444
- const label = shapeMatch[2];
2445
- if (id && label) {
2446
- entities.set(id, { id, label });
2447
- }
2448
- }
2449
- }
2450
- braceDepth++;
2451
- continue;
2452
- }
2453
- if (stripped === "}") {
2454
- braceDepth = Math.max(0, braceDepth - 1);
2455
- continue;
2456
- }
2457
- if (braceDepth > 0) continue;
2458
- const connMatch = stripped.match(
2459
- /^([a-zA-Z0-9_.-]+)\s*->\s*([a-zA-Z0-9_.-]+)(?:\s*:\s*(.+?))?$/
2460
- );
2461
- if (connMatch) {
2462
- const from = connMatch[1] ?? "";
2463
- const to = connMatch[2] ?? "";
2464
- const label = connMatch[3]?.trim();
2465
- if (from && to) {
2466
- relationships.push({ from, to, ...label ? { label } : {} });
2467
- }
2468
- continue;
2469
- }
2470
- const simpleMatch = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+)$/);
2471
- if (simpleMatch) {
2472
- const id = simpleMatch[1];
2473
- const label = (simpleMatch[2] ?? "").trim();
2474
- if (id && label && !entities.has(id)) {
2475
- entities.set(id, { id, label });
2476
- }
2477
- }
2645
+ processD2Line(line.trim(), state);
2478
2646
  }
2479
2647
  return {
2480
- entities: Array.from(entities.values()),
2481
- relationships,
2648
+ entities: Array.from(state.entities.values()),
2649
+ relationships: state.relationships,
2482
2650
  metadata: { format: "d2", diagramType: "architecture" }
2483
2651
  };
2484
2652
  }
2485
2653
  };
2654
+
2655
+ // src/ingest/parsers/plantuml.ts
2656
+ function emptyResult4() {
2657
+ return {
2658
+ entities: [],
2659
+ relationships: [],
2660
+ metadata: { format: "plantuml", diagramType: "unknown" }
2661
+ };
2662
+ }
2663
+ function detectDiagramType2(content) {
2664
+ if (/class\s+\w+/.test(content)) return "class";
2665
+ if (/\[.+\]/.test(content) || /component\s+/.test(content)) return "component";
2666
+ if (/participant\s+/.test(content) || /actor\s+/.test(content)) return "sequence";
2667
+ return "unknown";
2668
+ }
2669
+ function stripWrappers(content) {
2670
+ return content.replace(/@startuml\b.*\n?/, "").replace(/@enduml\b.*/, "").trim();
2671
+ }
2672
+ function extractClasses(body, entities) {
2673
+ const classRegex = /class\s+(\w+)/g;
2674
+ let match = classRegex.exec(body);
2675
+ while (match !== null) {
2676
+ const id = match[1] ?? "";
2677
+ if (id && !entities.has(id)) {
2678
+ entities.set(id, { id, label: id });
2679
+ }
2680
+ match = classRegex.exec(body);
2681
+ }
2682
+ }
2683
+ function extractComponents(body, entities) {
2684
+ const componentRegex = /\[([^\]]+)\]/g;
2685
+ let match = componentRegex.exec(body);
2686
+ while (match !== null) {
2687
+ const label = (match[1] ?? "").trim();
2688
+ if (label) {
2689
+ const id = label.replace(/\s+/g, "_");
2690
+ if (!entities.has(id)) {
2691
+ entities.set(id, { id, label });
2692
+ }
2693
+ }
2694
+ match = componentRegex.exec(body);
2695
+ }
2696
+ }
2697
+ function parseRelationshipMatch(match) {
2698
+ const from = match[1] ?? "";
2699
+ const to = match[2] ?? "";
2700
+ if (!from || !to) return null;
2701
+ const label = match[3]?.trim();
2702
+ return { from, to, ...label ? { label } : {} };
2703
+ }
2704
+ function collectRelationshipMatches(body, regex) {
2705
+ const results = [];
2706
+ let match = regex.exec(body);
2707
+ while (match !== null) {
2708
+ const rel = parseRelationshipMatch(match);
2709
+ if (rel) results.push(rel);
2710
+ match = regex.exec(body);
2711
+ }
2712
+ return results;
2713
+ }
2714
+ function extractRelationships(body) {
2715
+ return collectRelationshipMatches(
2716
+ body,
2717
+ /(\w+)\s*(?:-->|->|<--|<-|\.\.>|--)\s*(\w+)(?:\s*:\s*(.+))?/g
2718
+ );
2719
+ }
2486
2720
  var PlantUmlParser = class {
2487
2721
  canParse(_content, ext) {
2488
2722
  return ext === ".puml" || ext === ".plantuml";
2489
2723
  }
2490
2724
  parse(content, _filePath) {
2491
2725
  const trimmed = content.trim();
2492
- if (!trimmed) {
2493
- return {
2494
- entities: [],
2495
- relationships: [],
2496
- metadata: { format: "plantuml", diagramType: "unknown" }
2497
- };
2498
- }
2499
- const diagramType = this.detectDiagramType(trimmed);
2726
+ if (!trimmed) return emptyResult4();
2727
+ const diagramType = detectDiagramType2(trimmed);
2500
2728
  const entities = /* @__PURE__ */ new Map();
2501
- const relationships = [];
2502
- const body = trimmed.replace(/@startuml\b.*\n?/, "").replace(/@enduml\b.*/, "").trim();
2503
- const classRegex = /class\s+(\w+)/g;
2504
- let match;
2505
- match = classRegex.exec(body);
2506
- while (match !== null) {
2507
- const id = match[1] ?? "";
2508
- if (id && !entities.has(id)) {
2509
- entities.set(id, { id, label: id });
2510
- }
2511
- match = classRegex.exec(body);
2512
- }
2513
- const componentRegex1 = /\[([^\]]+)\]/g;
2514
- match = componentRegex1.exec(body);
2515
- while (match !== null) {
2516
- const label = (match[1] ?? "").trim();
2517
- if (label) {
2518
- const id = label.replace(/\s+/g, "_");
2519
- if (!entities.has(id)) {
2520
- entities.set(id, { id, label });
2521
- }
2522
- }
2523
- match = componentRegex1.exec(body);
2524
- }
2525
- const relRegex = /(\w+)\s*(?:-->|->|<--|<-|\.\.>|--)\s*(\w+)(?:\s*:\s*(.+))?/g;
2526
- match = relRegex.exec(body);
2527
- while (match !== null) {
2528
- const from = match[1] ?? "";
2529
- const to = match[2] ?? "";
2530
- const label = match[3]?.trim();
2531
- if (from && to) {
2532
- relationships.push({ from, to, ...label ? { label } : {} });
2533
- }
2534
- match = relRegex.exec(body);
2535
- }
2729
+ const body = stripWrappers(trimmed);
2730
+ extractClasses(body, entities);
2731
+ extractComponents(body, entities);
2732
+ const relationships = extractRelationships(body);
2536
2733
  return {
2537
2734
  entities: Array.from(entities.values()),
2538
2735
  relationships,
2539
2736
  metadata: { format: "plantuml", diagramType }
2540
2737
  };
2541
2738
  }
2542
- detectDiagramType(content) {
2543
- if (/class\s+\w+/.test(content)) return "class";
2544
- if (/\[.+\]/.test(content) || /component\s+/.test(content)) return "component";
2545
- if (/participant\s+/.test(content) || /actor\s+/.test(content)) return "sequence";
2546
- return "unknown";
2547
- }
2548
2739
  };
2740
+
2741
+ // src/ingest/DiagramParser.ts
2549
2742
  var DIAGRAM_EXTENSIONS = /* @__PURE__ */ new Set([".mmd", ".mermaid", ".d2", ".puml", ".plantuml"]);
2743
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".harness"]);
2550
2744
  var DiagramParser = class {
2551
2745
  constructor(store) {
2552
2746
  this.store = store;
@@ -2558,7 +2752,7 @@ var DiagramParser = class {
2558
2752
  new PlantUmlParser()
2559
2753
  ];
2560
2754
  parse(content, filePath) {
2561
- const ext = path6.extname(filePath).toLowerCase();
2755
+ const ext = path7.extname(filePath).toLowerCase();
2562
2756
  for (const parser of this.parsers) {
2563
2757
  if (parser.canParse(content, ext)) {
2564
2758
  return parser.parse(content, filePath);
@@ -2578,41 +2772,13 @@ var DiagramParser = class {
2578
2772
  const diagramFiles = await this.findDiagramFiles(projectDir);
2579
2773
  for (const filePath of diagramFiles) {
2580
2774
  try {
2581
- const content = await fs5.readFile(filePath, "utf-8");
2582
- const relPath = path6.relative(projectDir, filePath).replaceAll("\\", "/");
2775
+ const content = await fs6.readFile(filePath, "utf-8");
2776
+ const relPath = path7.relative(projectDir, filePath).replaceAll("\\", "/");
2583
2777
  const result = this.parse(content, filePath);
2584
2778
  if (result.entities.length === 0) continue;
2585
2779
  const pathHash = hash(relPath);
2586
- for (const entity of result.entities) {
2587
- const nodeId = `diagram:${pathHash}:${entity.id}`;
2588
- this.store.addNode({
2589
- id: nodeId,
2590
- type: "business_concept",
2591
- name: entity.label,
2592
- path: relPath,
2593
- metadata: {
2594
- source: "diagram",
2595
- format: result.metadata.format,
2596
- diagramType: result.metadata.diagramType,
2597
- confidence: 0.85,
2598
- ...entity.type ? { entityType: entity.type } : {}
2599
- }
2600
- });
2601
- nodesAdded++;
2602
- }
2603
- for (const rel of result.relationships) {
2604
- const fromId = `diagram:${pathHash}:${rel.from}`;
2605
- const toId = `diagram:${pathHash}:${rel.to}`;
2606
- this.store.addEdge({
2607
- from: fromId,
2608
- to: toId,
2609
- type: "references",
2610
- metadata: {
2611
- ...rel.label ? { label: rel.label } : {}
2612
- }
2613
- });
2614
- edgesAdded++;
2615
- }
2780
+ nodesAdded += this.addEntityNodes(result, relPath, pathHash);
2781
+ edgesAdded += this.addRelationshipEdges(result, pathHash);
2616
2782
  } catch (err) {
2617
2783
  errors.push(
2618
2784
  `Failed to parse ${filePath}: ${err instanceof Error ? err.message : String(err)}`
@@ -2628,25 +2794,64 @@ var DiagramParser = class {
2628
2794
  durationMs: Date.now() - start
2629
2795
  };
2630
2796
  }
2797
+ /** Map diagram entities to business_concept graph nodes. */
2798
+ addEntityNodes(result, relPath, pathHash) {
2799
+ let count = 0;
2800
+ for (const entity of result.entities) {
2801
+ const nodeId = `diagram:${pathHash}:${entity.id}`;
2802
+ this.store.addNode({
2803
+ id: nodeId,
2804
+ type: "business_concept",
2805
+ name: entity.label,
2806
+ path: relPath,
2807
+ metadata: {
2808
+ source: "diagram",
2809
+ format: result.metadata.format,
2810
+ diagramType: result.metadata.diagramType,
2811
+ confidence: 0.85,
2812
+ ...entity.type ? { entityType: entity.type } : {}
2813
+ }
2814
+ });
2815
+ count++;
2816
+ }
2817
+ return count;
2818
+ }
2819
+ /** Map diagram relationships to references graph edges. */
2820
+ addRelationshipEdges(result, pathHash) {
2821
+ let count = 0;
2822
+ for (const rel of result.relationships) {
2823
+ const fromId = `diagram:${pathHash}:${rel.from}`;
2824
+ const toId = `diagram:${pathHash}:${rel.to}`;
2825
+ this.store.addEdge({
2826
+ from: fromId,
2827
+ to: toId,
2828
+ type: "references",
2829
+ metadata: {
2830
+ ...rel.label ? { label: rel.label } : {}
2831
+ }
2832
+ });
2833
+ count++;
2834
+ }
2835
+ return count;
2836
+ }
2631
2837
  async findDiagramFiles(dir) {
2632
2838
  const files = [];
2633
- const SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".harness"]);
2634
2839
  const walk = async (currentDir) => {
2635
2840
  let entries;
2636
2841
  try {
2637
- entries = await fs5.readdir(currentDir, { withFileTypes: true });
2842
+ entries = await fs6.readdir(currentDir, { withFileTypes: true });
2638
2843
  } catch {
2639
2844
  return;
2640
2845
  }
2641
2846
  for (const entry of entries) {
2642
2847
  if (entry.isDirectory()) {
2643
- if (!SKIP_DIRS2.has(entry.name)) {
2644
- await walk(path6.join(currentDir, entry.name));
2848
+ if (!SKIP_DIRS.has(entry.name)) {
2849
+ await walk(path7.join(currentDir, entry.name));
2645
2850
  }
2646
2851
  } else if (entry.isFile()) {
2647
- const ext = path6.extname(entry.name).toLowerCase();
2852
+ const ext = path7.extname(entry.name).toLowerCase();
2648
2853
  if (DIAGRAM_EXTENSIONS.has(ext)) {
2649
- files.push(path6.join(currentDir, entry.name));
2854
+ files.push(path7.join(currentDir, entry.name));
2650
2855
  }
2651
2856
  }
2652
2857
  }
@@ -2657,8 +2862,8 @@ var DiagramParser = class {
2657
2862
  };
2658
2863
 
2659
2864
  // src/ingest/KnowledgeLinker.ts
2660
- import * as fs6 from "fs/promises";
2661
- import * as path7 from "path";
2865
+ import * as fs7 from "fs/promises";
2866
+ import * as path8 from "path";
2662
2867
  var HEURISTIC_PATTERNS = [
2663
2868
  {
2664
2869
  name: "business-rule-imperative",
@@ -2805,8 +3010,10 @@ var KnowledgeLinker = class {
2805
3010
  if (!duplicate) return false;
2806
3011
  const existingSources = duplicate.metadata.sources ?? [];
2807
3012
  if (!existingSources.includes(candidate.sourceNodeId)) {
2808
- existingSources.push(candidate.sourceNodeId);
2809
- duplicate.metadata.sources = existingSources;
3013
+ this.store.addNode({
3014
+ ...duplicate,
3015
+ metadata: { ...duplicate.metadata, sources: [...existingSources, candidate.sourceNodeId] }
3016
+ });
2810
3017
  }
2811
3018
  return true;
2812
3019
  }
@@ -2840,8 +3047,8 @@ var KnowledgeLinker = class {
2840
3047
  */
2841
3048
  async writeJsonl(candidates) {
2842
3049
  if (!this.outputDir) return;
2843
- await fs6.mkdir(this.outputDir, { recursive: true });
2844
- const filePath = path7.join(this.outputDir, "linker.jsonl");
3050
+ await fs7.mkdir(this.outputDir, { recursive: true });
3051
+ const filePath = path8.join(this.outputDir, "linker.jsonl");
2845
3052
  const lines = candidates.map(
2846
3053
  (c) => JSON.stringify({
2847
3054
  id: c.id,
@@ -2854,16 +3061,16 @@ var KnowledgeLinker = class {
2854
3061
  nodeType: c.nodeType
2855
3062
  })
2856
3063
  );
2857
- await fs6.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
3064
+ await fs7.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
2858
3065
  }
2859
3066
  /**
2860
3067
  * Write medium-confidence candidates to staged JSONL for human review.
2861
3068
  */
2862
3069
  async writeStagedJsonl(candidates) {
2863
3070
  if (!this.outputDir || candidates.length === 0) return;
2864
- const stagedDir = path7.join(this.outputDir, "staged");
2865
- await fs6.mkdir(stagedDir, { recursive: true });
2866
- const filePath = path7.join(stagedDir, "linker-staged.jsonl");
3071
+ const stagedDir = path8.join(this.outputDir, "staged");
3072
+ await fs7.mkdir(stagedDir, { recursive: true });
3073
+ const filePath = path8.join(stagedDir, "linker-staged.jsonl");
2867
3074
  const lines = candidates.map(
2868
3075
  (c) => JSON.stringify({
2869
3076
  id: c.id,
@@ -2876,7 +3083,7 @@ var KnowledgeLinker = class {
2876
3083
  nodeType: c.nodeType
2877
3084
  })
2878
3085
  );
2879
- await fs6.writeFile(filePath, lines.join("\n") + "\n");
3086
+ await fs7.writeFile(filePath, lines.join("\n") + "\n");
2880
3087
  }
2881
3088
  /**
2882
3089
  * Apply heuristic patterns to content and return candidate extractions.
@@ -2982,8 +3189,16 @@ var StructuralDriftDetector = class {
2982
3189
  };
2983
3190
 
2984
3191
  // src/ingest/KnowledgeStagingAggregator.ts
2985
- import * as fs7 from "fs/promises";
2986
- import * as path8 from "path";
3192
+ import * as fs8 from "fs/promises";
3193
+ import * as path9 from "path";
3194
+ var BUSINESS_NODE_TYPES = [
3195
+ "business_concept",
3196
+ "business_rule",
3197
+ "business_process",
3198
+ "business_term",
3199
+ "business_metric",
3200
+ "business_fact"
3201
+ ];
2987
3202
  var KnowledgeStagingAggregator = class {
2988
3203
  constructor(projectDir) {
2989
3204
  this.projectDir = projectDir;
@@ -3002,59 +3217,157 @@ var KnowledgeStagingAggregator = class {
3002
3217
  if (deduplicated.length === 0) {
3003
3218
  return { staged: 0 };
3004
3219
  }
3005
- const stagedDir = path8.join(this.projectDir, ".harness", "knowledge", "staged");
3006
- await fs7.mkdir(stagedDir, { recursive: true });
3220
+ const stagedDir = path9.join(this.projectDir, ".harness", "knowledge", "staged");
3221
+ await fs8.mkdir(stagedDir, { recursive: true });
3007
3222
  const jsonl = deduplicated.map((entry) => JSON.stringify(entry)).join("\n") + "\n";
3008
- await fs7.writeFile(path8.join(stagedDir, "pipeline-staged.jsonl"), jsonl, "utf-8");
3223
+ await fs8.writeFile(path9.join(stagedDir, "pipeline-staged.jsonl"), jsonl, "utf-8");
3009
3224
  return { staged: deduplicated.length };
3010
3225
  }
3011
- async generateGapReport(knowledgeDir) {
3012
- const domains = [];
3226
+ async extractDocName(filePath) {
3227
+ const raw = await fs8.readFile(filePath, "utf-8");
3228
+ const fmMatch = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
3229
+ const body = fmMatch ? fmMatch[1] : raw;
3230
+ const titleMatch = body.match(/^#\s+(.+)$/m);
3231
+ return titleMatch ? titleMatch[1].trim() : path9.basename(filePath, ".md");
3232
+ }
3233
+ async generateGapReport(knowledgeDir, store) {
3234
+ const domainDocNames = /* @__PURE__ */ new Map();
3235
+ const domainEntryCounts = /* @__PURE__ */ new Map();
3013
3236
  let totalEntries = 0;
3014
3237
  try {
3015
- const entries = await fs7.readdir(knowledgeDir, { withFileTypes: true });
3238
+ const entries = await fs8.readdir(knowledgeDir, { withFileTypes: true });
3016
3239
  const domainDirs = entries.filter((e) => e.isDirectory());
3017
3240
  for (const dir of domainDirs) {
3018
- const domainPath = path8.join(knowledgeDir, dir.name);
3019
- const files = await fs7.readdir(domainPath);
3241
+ const domainPath = path9.join(knowledgeDir, dir.name);
3242
+ const files = await fs8.readdir(domainPath);
3020
3243
  const mdFiles = files.filter((f) => f.endsWith(".md"));
3021
3244
  const entryCount = mdFiles.length;
3022
3245
  totalEntries += entryCount;
3023
- domains.push({ domain: dir.name, entryCount });
3246
+ domainEntryCounts.set(dir.name, entryCount);
3247
+ if (store) {
3248
+ const names = [];
3249
+ for (const file of mdFiles) {
3250
+ const name = await this.extractDocName(path9.join(domainPath, file));
3251
+ names.push(name.toLowerCase().trim());
3252
+ }
3253
+ domainDocNames.set(dir.name, names);
3254
+ }
3024
3255
  }
3025
3256
  } catch {
3026
3257
  }
3258
+ if (!store) {
3259
+ const domains2 = [];
3260
+ for (const [domain, entryCount] of domainEntryCounts) {
3261
+ domains2.push({ domain, entryCount, extractedCount: 0, gapCount: 0, gapEntries: [] });
3262
+ }
3263
+ return {
3264
+ domains: domains2,
3265
+ totalEntries,
3266
+ totalExtracted: 0,
3267
+ totalGaps: 0,
3268
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
3269
+ };
3270
+ }
3271
+ const extractedByDomain = /* @__PURE__ */ new Map();
3272
+ for (const nodeType of BUSINESS_NODE_TYPES) {
3273
+ const nodes = store.findNodes({ type: nodeType });
3274
+ for (const node of nodes) {
3275
+ const domain = node.metadata?.domain ?? "unknown";
3276
+ const list = extractedByDomain.get(domain) ?? [];
3277
+ list.push(node);
3278
+ extractedByDomain.set(domain, list);
3279
+ }
3280
+ }
3281
+ for (const [domain, nodes] of extractedByDomain) {
3282
+ const seen = /* @__PURE__ */ new Set();
3283
+ const deduped = nodes.filter((n) => {
3284
+ const key = n.name.toLowerCase().trim();
3285
+ if (seen.has(key)) return false;
3286
+ seen.add(key);
3287
+ return true;
3288
+ });
3289
+ extractedByDomain.set(domain, deduped);
3290
+ }
3291
+ const allDomains = /* @__PURE__ */ new Set([...domainEntryCounts.keys(), ...extractedByDomain.keys()]);
3292
+ const domains = [];
3293
+ let totalExtracted = 0;
3294
+ let totalGaps = 0;
3295
+ for (const domain of allDomains) {
3296
+ const entryCount = domainEntryCounts.get(domain) ?? 0;
3297
+ const extractedNodes = extractedByDomain.get(domain) ?? [];
3298
+ const extractedCount = extractedNodes.length;
3299
+ totalExtracted += extractedCount;
3300
+ const docNames = domainDocNames.get(domain) ?? [];
3301
+ const gapEntries = [];
3302
+ for (const node of extractedNodes) {
3303
+ const normalizedName = node.name.toLowerCase().trim();
3304
+ if (!docNames.includes(normalizedName)) {
3305
+ gapEntries.push({
3306
+ nodeId: node.id,
3307
+ name: node.name,
3308
+ nodeType: node.type,
3309
+ source: node.metadata?.source ?? "unknown",
3310
+ hasContent: Boolean(node.content && node.content.trim().length >= 10)
3311
+ });
3312
+ }
3313
+ }
3314
+ totalGaps += gapEntries.length;
3315
+ domains.push({ domain, entryCount, extractedCount, gapCount: gapEntries.length, gapEntries });
3316
+ }
3027
3317
  return {
3028
3318
  domains,
3029
3319
  totalEntries,
3320
+ totalExtracted,
3321
+ totalGaps,
3030
3322
  generatedAt: (/* @__PURE__ */ new Date()).toISOString()
3031
3323
  };
3032
3324
  }
3033
3325
  async writeGapReport(report) {
3034
- const gapsDir = path8.join(this.projectDir, ".harness", "knowledge");
3035
- await fs7.mkdir(gapsDir, { recursive: true });
3326
+ const gapsDir = path9.join(this.projectDir, ".harness", "knowledge");
3327
+ await fs8.mkdir(gapsDir, { recursive: true });
3328
+ const hasDifferential = report.totalExtracted > 0;
3036
3329
  const lines = [
3037
3330
  "# Knowledge Gaps Report",
3038
3331
  "",
3039
3332
  `Generated: ${report.generatedAt}`,
3040
3333
  "",
3041
3334
  "## Coverage by Domain",
3042
- "",
3043
- "| Domain | Entries |",
3044
- "| ------ | ------- |"
3335
+ ""
3045
3336
  ];
3046
- for (const domain of report.domains) {
3047
- lines.push(`| ${domain.domain} | ${domain.entryCount} |`);
3337
+ if (hasDifferential) {
3338
+ lines.push(
3339
+ "| Domain | Documented | Extracted | Gaps |",
3340
+ "| ------ | ---------- | --------- | ---- |"
3341
+ );
3342
+ for (const domain of report.domains) {
3343
+ lines.push(
3344
+ `| ${domain.domain} | ${domain.entryCount} | ${domain.extractedCount} | ${domain.gapCount} |`
3345
+ );
3346
+ }
3347
+ lines.push(
3348
+ "",
3349
+ `## Summary`,
3350
+ "",
3351
+ `- **Total Documented:** ${report.totalEntries}`,
3352
+ `- **Total Extracted:** ${report.totalExtracted}`,
3353
+ `- **Total Gaps:** ${report.totalGaps}`,
3354
+ ""
3355
+ );
3356
+ } else {
3357
+ lines.push("| Domain | Entries |", "| ------ | ------- |");
3358
+ for (const domain of report.domains) {
3359
+ lines.push(`| ${domain.domain} | ${domain.entryCount} |`);
3360
+ }
3361
+ lines.push("", `## Total Entries: ${report.totalEntries}`, "");
3048
3362
  }
3049
- lines.push("", `## Total Entries: ${report.totalEntries}`, "");
3050
- await fs7.writeFile(path8.join(gapsDir, "gaps.md"), lines.join("\n"), "utf-8");
3363
+ await fs8.writeFile(path9.join(gapsDir, "gaps.md"), lines.join("\n"), "utf-8");
3051
3364
  }
3052
3365
  };
3053
3366
 
3054
3367
  // src/ingest/extractors/ExtractionRunner.ts
3055
- import * as fs8 from "fs/promises";
3056
- import * as path9 from "path";
3057
- var SKIP_DIRS = /* @__PURE__ */ new Set([
3368
+ import * as fs9 from "fs/promises";
3369
+ import * as path10 from "path";
3370
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
3058
3371
  "node_modules",
3059
3372
  "dist",
3060
3373
  "target",
@@ -3091,11 +3404,11 @@ var EXT_TO_LANGUAGE = {
3091
3404
  };
3092
3405
  var SKIP_EXTENSIONS2 = /* @__PURE__ */ new Set([".d.ts"]);
3093
3406
  function detectLanguage(filePath) {
3094
- const name = path9.basename(filePath);
3407
+ const name = path10.basename(filePath);
3095
3408
  for (const skip of SKIP_EXTENSIONS2) {
3096
3409
  if (name.endsWith(skip)) return void 0;
3097
3410
  }
3098
- const ext = path9.extname(filePath);
3411
+ const ext = path10.extname(filePath);
3099
3412
  return EXT_TO_LANGUAGE[ext];
3100
3413
  }
3101
3414
  var EXTRACTOR_EDGE_TYPE = {
@@ -3121,7 +3434,7 @@ var ExtractionRunner = class {
3121
3434
  let nodesAdded = 0;
3122
3435
  let nodesUpdated = 0;
3123
3436
  let edgesAdded = 0;
3124
- await fs8.mkdir(outputDir, { recursive: true });
3437
+ await fs9.mkdir(outputDir, { recursive: true });
3125
3438
  const files = await this.findSourceFiles(projectDir);
3126
3439
  const recordsByExtractor = /* @__PURE__ */ new Map();
3127
3440
  for (const ext of this.extractors) {
@@ -3130,17 +3443,17 @@ var ExtractionRunner = class {
3130
3443
  for (const filePath of files) {
3131
3444
  const language = detectLanguage(filePath);
3132
3445
  if (!language) continue;
3133
- const ext = path9.extname(filePath);
3446
+ const ext = path10.extname(filePath);
3134
3447
  let content;
3135
3448
  try {
3136
- content = await fs8.readFile(filePath, "utf-8");
3449
+ content = await fs9.readFile(filePath, "utf-8");
3137
3450
  } catch (err) {
3138
3451
  errors.push(
3139
3452
  `Failed to read ${filePath}: ${err instanceof Error ? err.message : String(err)}`
3140
3453
  );
3141
3454
  continue;
3142
3455
  }
3143
- const relativePath = path9.relative(projectDir, filePath).replaceAll("\\", "/");
3456
+ const relativePath = path10.relative(projectDir, filePath).replaceAll("\\", "/");
3144
3457
  for (const extractor2 of this.extractors) {
3145
3458
  if (!extractor2.supportedExtensions.includes(ext)) continue;
3146
3459
  try {
@@ -3177,9 +3490,9 @@ var ExtractionRunner = class {
3177
3490
  }
3178
3491
  /** Write extraction records to JSONL file. */
3179
3492
  async writeJsonl(records, outputDir, extractorName) {
3180
- const filePath = path9.join(outputDir, `${extractorName}.jsonl`);
3493
+ const filePath = path10.join(outputDir, `${extractorName}.jsonl`);
3181
3494
  const lines = records.map((r) => JSON.stringify(r));
3182
- await fs8.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
3495
+ await fs9.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
3183
3496
  }
3184
3497
  /** Create or update a graph node from an extraction record. */
3185
3498
  persistRecord(record, store) {
@@ -3251,13 +3564,13 @@ var ExtractionRunner = class {
3251
3564
  const results = [];
3252
3565
  let entries;
3253
3566
  try {
3254
- entries = await fs8.readdir(dir, { withFileTypes: true });
3567
+ entries = await fs9.readdir(dir, { withFileTypes: true });
3255
3568
  } catch {
3256
3569
  return results;
3257
3570
  }
3258
3571
  for (const entry of entries) {
3259
- const fullPath = path9.join(dir, entry.name);
3260
- if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
3572
+ const fullPath = path10.join(dir, entry.name);
3573
+ if (entry.isDirectory() && !SKIP_DIRS2.has(entry.name)) {
3261
3574
  results.push(...await this.findSourceFiles(fullPath));
3262
3575
  } else if (entry.isFile() && detectLanguage(fullPath) !== void 0) {
3263
3576
  results.push(fullPath);
@@ -4383,57 +4696,9 @@ var ImageAnalysisExtractor = class {
4383
4696
  const errors = [];
4384
4697
  for (const imagePath of imagePaths) {
4385
4698
  try {
4386
- const response = await this.provider.analyze({
4387
- prompt: "Analyze this image and provide a structured description of its visual contents.",
4388
- systemPrompt: "You are an image analysis assistant. Describe the image contents, detect UI elements, extract visible text, identify design patterns, and note accessibility concerns.",
4389
- responseSchema: {},
4390
- // Schema handled by provider
4391
- maxTokens: 1e3
4392
- });
4393
- const result = response.result;
4394
- const pathHash = hash(imagePath);
4395
- const annotationId = `img:${pathHash}`;
4396
- store.addNode({
4397
- id: annotationId,
4398
- type: "image_annotation",
4399
- name: result.description.slice(0, 200),
4400
- path: imagePath,
4401
- content: result.description,
4402
- hash: hash(result.description),
4403
- metadata: {
4404
- source: "image-analysis",
4405
- detectedElements: result.detectedElements,
4406
- extractedText: result.extractedText,
4407
- designPatterns: result.designPatterns,
4408
- accessibilityNotes: result.accessibilityNotes,
4409
- model: response.model
4410
- }
4411
- });
4412
- nodesAdded++;
4413
- const fileNodes = store.findNodes({ type: "file" });
4414
- const fileNode = fileNodes.find((n) => n.path === imagePath);
4415
- if (fileNode) {
4416
- store.addEdge({ from: annotationId, to: fileNode.id, type: "annotates" });
4417
- edgesAdded++;
4418
- }
4419
- for (const pattern of result.designPatterns) {
4420
- const conceptId = `img:concept:${hash(pattern + imagePath)}`;
4421
- store.addNode({
4422
- id: conceptId,
4423
- type: "business_concept",
4424
- name: pattern,
4425
- content: `Design pattern detected in ${imagePath}: ${pattern}`,
4426
- hash: hash(pattern),
4427
- metadata: {
4428
- source: "image-analysis",
4429
- sourceImage: imagePath,
4430
- domain: "design"
4431
- }
4432
- });
4433
- nodesAdded++;
4434
- store.addEdge({ from: annotationId, to: conceptId, type: "contains" });
4435
- edgesAdded++;
4436
- }
4699
+ const counts = await this.processImage(store, imagePath);
4700
+ nodesAdded += counts.nodes;
4701
+ edgesAdded += counts.edges;
4437
4702
  } catch (err) {
4438
4703
  errors.push(
4439
4704
  `Image analysis failed for ${imagePath}: ${err instanceof Error ? err.message : String(err)}`
@@ -4449,6 +4714,68 @@ var ImageAnalysisExtractor = class {
4449
4714
  durationMs: Date.now() - start
4450
4715
  };
4451
4716
  }
4717
+ /** Analyze a single image and add annotation + concept nodes to the store. */
4718
+ async processImage(store, imagePath) {
4719
+ const response = await this.provider.analyze({
4720
+ prompt: "Analyze this image and provide a structured description of its visual contents.",
4721
+ systemPrompt: "You are an image analysis assistant. Describe the image contents, detect UI elements, extract visible text, identify design patterns, and note accessibility concerns.",
4722
+ responseSchema: {},
4723
+ // Schema handled by provider
4724
+ maxTokens: 1e3
4725
+ });
4726
+ const result = response.result;
4727
+ const annotationId = `img:${hash(imagePath)}`;
4728
+ let nodes = 0;
4729
+ let edges = 0;
4730
+ store.addNode({
4731
+ id: annotationId,
4732
+ type: "image_annotation",
4733
+ name: result.description.slice(0, 200),
4734
+ path: imagePath,
4735
+ content: result.description,
4736
+ hash: hash(result.description),
4737
+ metadata: {
4738
+ source: "image-analysis",
4739
+ detectedElements: result.detectedElements,
4740
+ extractedText: result.extractedText,
4741
+ designPatterns: result.designPatterns,
4742
+ accessibilityNotes: result.accessibilityNotes,
4743
+ model: response.model
4744
+ }
4745
+ });
4746
+ nodes++;
4747
+ edges += this.linkToFileNode(store, annotationId, imagePath);
4748
+ for (const pattern of result.designPatterns) {
4749
+ edges += this.addDesignPatternConcept(store, annotationId, imagePath, pattern);
4750
+ nodes++;
4751
+ }
4752
+ return { nodes, edges };
4753
+ }
4754
+ /** Link annotation to its source file node if it exists. Returns 1 if linked, 0 otherwise. */
4755
+ linkToFileNode(store, annotationId, imagePath) {
4756
+ const fileNode = store.findNodes({ type: "file" }).find((n) => n.path === imagePath);
4757
+ if (!fileNode) return 0;
4758
+ store.addEdge({ from: annotationId, to: fileNode.id, type: "annotates" });
4759
+ return 1;
4760
+ }
4761
+ /** Create a business_concept node for a design pattern and link it. Returns edges added. */
4762
+ addDesignPatternConcept(store, annotationId, imagePath, pattern) {
4763
+ const conceptId = `img:concept:${hash(pattern + imagePath)}`;
4764
+ store.addNode({
4765
+ id: conceptId,
4766
+ type: "business_concept",
4767
+ name: pattern,
4768
+ content: `Design pattern detected in ${imagePath}: ${pattern}`,
4769
+ hash: hash(pattern),
4770
+ metadata: {
4771
+ source: "image-analysis",
4772
+ sourceImage: imagePath,
4773
+ domain: "design"
4774
+ }
4775
+ });
4776
+ store.addEdge({ from: annotationId, to: conceptId, type: "contains" });
4777
+ return 1;
4778
+ }
4452
4779
  };
4453
4780
 
4454
4781
  // src/ingest/knowledgeTypes.ts
@@ -4459,6 +4786,7 @@ var KNOWLEDGE_NODE_TYPES = [
4459
4786
  "business_term",
4460
4787
  "business_concept",
4461
4788
  "business_metric",
4789
+ "decision",
4462
4790
  "design_token",
4463
4791
  "design_constraint",
4464
4792
  "aesthetic_intent",
@@ -4499,83 +4827,99 @@ function classifyConflict(a, b) {
4499
4827
  return "temporal_conflict";
4500
4828
  return "status_divergence";
4501
4829
  }
4502
- var ContradictionDetector = class {
4503
- detect(store) {
4504
- const nodes = KNOWLEDGE_NODE_TYPES.flatMap((t) => store.findNodes({ type: t }));
4505
- const byName = /* @__PURE__ */ new Map();
4506
- for (const node of nodes) {
4507
- const key = node.name.toLowerCase().trim();
4508
- const group = byName.get(key) ?? [];
4509
- group.push(node);
4510
- byName.set(key, group);
4511
- }
4512
- const contradictions = [];
4513
- const sourcePairCounts = {};
4514
- const seen = /* @__PURE__ */ new Set();
4515
- const addContradiction = (a, b, similarity) => {
4516
- const sourceA = a.metadata.source ?? "unknown";
4517
- const sourceB = b.metadata.source ?? "unknown";
4518
- if (sourceA === sourceB) return;
4519
- const hashA = a.hash ?? a.id;
4520
- const hashB = b.hash ?? b.id;
4521
- if (hashA === hashB) return;
4522
- const pairId = [a.id, b.id].sort().join(":");
4523
- if (seen.has(pairId)) return;
4524
- seen.add(pairId);
4525
- const conflictType = classifyConflict(a, b);
4526
- const severity = conflictType === "value_mismatch" ? "critical" : conflictType === "definition_conflict" ? "high" : "medium";
4527
- const pairKey = [sourceA, sourceB].sort().join("\u2194");
4528
- sourcePairCounts[pairKey] = (sourcePairCounts[pairKey] ?? 0) + 1;
4529
- contradictions.push({
4530
- id: `contradiction:${a.id}:${b.id}`,
4531
- entityA: {
4532
- nodeId: a.id,
4533
- source: sourceA,
4534
- name: a.name,
4535
- content: a.content ?? "",
4536
- lastModified: a.lastModified
4537
- },
4538
- entityB: {
4539
- nodeId: b.id,
4540
- source: sourceB,
4541
- name: b.name,
4542
- content: b.content ?? "",
4543
- lastModified: b.lastModified
4544
- },
4545
- similarity,
4546
- conflictType,
4547
- severity,
4548
- description: `"${a.name}" has conflicting definitions from ${sourceA} and ${sourceB}`
4549
- });
4550
- };
4551
- for (const [, group] of byName) {
4552
- if (group.length < 2) continue;
4553
- for (let i = 0; i < group.length; i++) {
4554
- for (let j = i + 1; j < group.length; j++) {
4555
- addContradiction(group[i], group[j], 1);
4556
- }
4830
+ var SEVERITY_MAP = {
4831
+ value_mismatch: "critical",
4832
+ definition_conflict: "high",
4833
+ status_divergence: "medium",
4834
+ temporal_conflict: "medium"
4835
+ };
4836
+ function buildEntry(node, source) {
4837
+ return {
4838
+ nodeId: node.id,
4839
+ source,
4840
+ name: node.name,
4841
+ content: node.content ?? "",
4842
+ lastModified: node.lastModified
4843
+ };
4844
+ }
4845
+ function groupByName(nodes) {
4846
+ const byName = /* @__PURE__ */ new Map();
4847
+ for (const node of nodes) {
4848
+ const key = node.name.toLowerCase().trim();
4849
+ const group = byName.get(key) ?? [];
4850
+ group.push(node);
4851
+ byName.set(key, group);
4852
+ }
4853
+ return byName;
4854
+ }
4855
+ function tryAddContradiction(acc, a, b, similarity) {
4856
+ const sourceA = a.metadata.source ?? "unknown";
4857
+ const sourceB = b.metadata.source ?? "unknown";
4858
+ if (sourceA === sourceB) return;
4859
+ if ((a.hash ?? a.id) === (b.hash ?? b.id)) return;
4860
+ const pairId = [a.id, b.id].sort().join(":");
4861
+ if (acc.seen.has(pairId)) return;
4862
+ acc.seen.add(pairId);
4863
+ const conflictType = classifyConflict(a, b);
4864
+ const pairKey = [sourceA, sourceB].sort().join("\u2194");
4865
+ acc.sourcePairCounts[pairKey] = (acc.sourcePairCounts[pairKey] ?? 0) + 1;
4866
+ acc.contradictions.push({
4867
+ id: `contradiction:${a.id}:${b.id}`,
4868
+ entityA: buildEntry(a, sourceA),
4869
+ entityB: buildEntry(b, sourceB),
4870
+ similarity,
4871
+ conflictType,
4872
+ severity: SEVERITY_MAP[conflictType],
4873
+ description: `"${a.name}" has conflicting definitions from ${sourceA} and ${sourceB}`
4874
+ });
4875
+ }
4876
+ function collectExactMatches(byName, acc) {
4877
+ for (const [, group] of byName) {
4878
+ if (group.length < 2) continue;
4879
+ for (let i = 0; i < group.length; i++) {
4880
+ for (let j = i + 1; j < group.length; j++) {
4881
+ tryAddContradiction(acc, group[i], group[j], 1);
4557
4882
  }
4558
4883
  }
4559
- const keys = [...byName.keys()];
4560
- for (let i = 0; i < keys.length; i++) {
4561
- for (let j = i + 1; j < keys.length; j++) {
4562
- const keyA = keys[i];
4563
- const keyB = keys[j];
4564
- const maxLen = Math.max(keyA.length, keyB.length);
4565
- const minLen = Math.min(keyA.length, keyB.length);
4566
- if (minLen / maxLen < SIMILARITY_THRESHOLD) continue;
4567
- const ratio = levenshteinRatio(keyA, keyB);
4568
- if (ratio < SIMILARITY_THRESHOLD) continue;
4569
- const groupA = byName.get(keyA);
4570
- const groupB = byName.get(keyB);
4571
- for (const a of groupA) {
4572
- for (const b of groupB) {
4573
- addContradiction(a, b, ratio);
4574
- }
4884
+ }
4885
+ }
4886
+ function collectFuzzyMatches(byName, acc) {
4887
+ const keys = [...byName.keys()];
4888
+ for (let i = 0; i < keys.length; i++) {
4889
+ for (let j = i + 1; j < keys.length; j++) {
4890
+ const keyA = keys[i];
4891
+ const keyB = keys[j];
4892
+ const maxLen = Math.max(keyA.length, keyB.length);
4893
+ const minLen = Math.min(keyA.length, keyB.length);
4894
+ if (minLen / maxLen < SIMILARITY_THRESHOLD) continue;
4895
+ const ratio = levenshteinRatio(keyA, keyB);
4896
+ if (ratio < SIMILARITY_THRESHOLD) continue;
4897
+ const groupA = byName.get(keyA);
4898
+ const groupB = byName.get(keyB);
4899
+ for (const a of groupA) {
4900
+ for (const b of groupB) {
4901
+ tryAddContradiction(acc, a, b, ratio);
4575
4902
  }
4576
4903
  }
4577
4904
  }
4578
- return { contradictions, sourcePairCounts, totalChecked: nodes.length };
4905
+ }
4906
+ }
4907
+ var ContradictionDetector = class {
4908
+ detect(store) {
4909
+ const nodes = KNOWLEDGE_NODE_TYPES.flatMap((t) => store.findNodes({ type: t }));
4910
+ const byName = groupByName(nodes);
4911
+ const acc = {
4912
+ contradictions: [],
4913
+ sourcePairCounts: {},
4914
+ seen: /* @__PURE__ */ new Set()
4915
+ };
4916
+ collectExactMatches(byName, acc);
4917
+ collectFuzzyMatches(byName, acc);
4918
+ return {
4919
+ contradictions: acc.contradictions,
4920
+ sourcePairCounts: acc.sourcePairCounts,
4921
+ totalChecked: nodes.length
4922
+ };
4579
4923
  }
4580
4924
  };
4581
4925
 
@@ -4606,64 +4950,76 @@ function toGrade(score) {
4606
4950
  if (score >= 20) return "D";
4607
4951
  return "F";
4608
4952
  }
4953
+ function groupByDomain(nodes, fallback) {
4954
+ const map = /* @__PURE__ */ new Map();
4955
+ for (const node of nodes) {
4956
+ const domain = node.metadata.domain ?? fallback(node);
4957
+ const group = map.get(domain) ?? [];
4958
+ group.push(node);
4959
+ map.set(domain, group);
4960
+ }
4961
+ return map;
4962
+ }
4963
+ function countLinkedEntities(codeEntries, store) {
4964
+ const linkedIds = /* @__PURE__ */ new Set();
4965
+ for (const codeNode of codeEntries) {
4966
+ if (hasKnowledgeEdge(codeNode.id, store)) {
4967
+ linkedIds.add(codeNode.id);
4968
+ }
4969
+ }
4970
+ return linkedIds;
4971
+ }
4972
+ function hasKnowledgeEdge(nodeId, store) {
4973
+ for (const edgeType of KNOWLEDGE_EDGE_TYPES) {
4974
+ if (store.getEdges({ to: nodeId, type: edgeType }).length > 0) return true;
4975
+ }
4976
+ return false;
4977
+ }
4978
+ function computeSourceBreakdown(knEntries) {
4979
+ const breakdown = {};
4980
+ for (const kn of knEntries) {
4981
+ const src = kn.metadata.source ?? "unknown";
4982
+ breakdown[src] = (breakdown[src] ?? 0) + 1;
4983
+ }
4984
+ return breakdown;
4985
+ }
4986
+ function computeDomainScore(knowledgeEntries, codeEntities, linkedEntities, uniqueSources) {
4987
+ const codeCoverageComponent = codeEntities > 0 ? linkedEntities / codeEntities * 60 : 0;
4988
+ const knowledgeDepthComponent = Math.min(knowledgeEntries / 10, 1) * 20;
4989
+ const sourceDiversityComponent = Math.min(uniqueSources / 3, 1) * 20;
4990
+ return Math.round(codeCoverageComponent + knowledgeDepthComponent + sourceDiversityComponent);
4991
+ }
4992
+ function scoreDomain(domain, knEntries, codeEntries, store) {
4993
+ const linkedIds = countLinkedEntities(codeEntries, store);
4994
+ const sourceBreakdown = computeSourceBreakdown(knEntries);
4995
+ const codeEntities = codeEntries.length;
4996
+ const linkedEntities = linkedIds.size;
4997
+ const knowledgeEntries = knEntries.length;
4998
+ const uniqueSources = Object.keys(sourceBreakdown).length;
4999
+ const score = computeDomainScore(knowledgeEntries, codeEntities, linkedEntities, uniqueSources);
5000
+ return {
5001
+ domain,
5002
+ score,
5003
+ knowledgeEntries,
5004
+ codeEntities,
5005
+ linkedEntities,
5006
+ unlinkedEntities: codeEntities - linkedEntities,
5007
+ sourceBreakdown,
5008
+ grade: toGrade(score)
5009
+ };
5010
+ }
4609
5011
  var CoverageScorer = class {
4610
5012
  score(store) {
4611
5013
  const knowledgeNodes = KNOWLEDGE_TYPES.flatMap((t) => store.findNodes({ type: t }));
4612
- const domainMap = /* @__PURE__ */ new Map();
4613
- for (const node of knowledgeNodes) {
4614
- const domain = node.metadata.domain ?? "unclassified";
4615
- const group = domainMap.get(domain) ?? [];
4616
- group.push(node);
4617
- domainMap.set(domain, group);
4618
- }
5014
+ const domainMap = groupByDomain(knowledgeNodes, () => "unclassified");
4619
5015
  const codeNodes = CODE_TYPES2.flatMap((t) => store.findNodes({ type: t }));
4620
- const codeDomains = /* @__PURE__ */ new Map();
4621
- for (const node of codeNodes) {
4622
- const domain = node.metadata.domain ?? this.domainFromPath(node.path);
4623
- const group = codeDomains.get(domain) ?? [];
4624
- group.push(node);
4625
- codeDomains.set(domain, group);
4626
- }
5016
+ const codeDomains = groupByDomain(codeNodes, (n) => this.domainFromPath(n.path));
4627
5017
  const allDomains = /* @__PURE__ */ new Set([...domainMap.keys(), ...codeDomains.keys()]);
4628
5018
  const domains = [];
4629
5019
  for (const domain of allDomains) {
4630
- const knEntries = domainMap.get(domain) ?? [];
4631
- const codeEntries = codeDomains.get(domain) ?? [];
4632
- const linkedIds = /* @__PURE__ */ new Set();
4633
- for (const codeNode of codeEntries) {
4634
- for (const edgeType of KNOWLEDGE_EDGE_TYPES) {
4635
- const edges = store.getEdges({ to: codeNode.id, type: edgeType });
4636
- if (edges.length > 0) {
4637
- linkedIds.add(codeNode.id);
4638
- break;
4639
- }
4640
- }
4641
- }
4642
- const sourceBreakdown = {};
4643
- for (const kn of knEntries) {
4644
- const src = kn.metadata.source ?? "unknown";
4645
- sourceBreakdown[src] = (sourceBreakdown[src] ?? 0) + 1;
4646
- }
4647
- const codeEntities = codeEntries.length;
4648
- const linkedEntities = linkedIds.size;
4649
- const knowledgeEntries = knEntries.length;
4650
- const uniqueSources = Object.keys(sourceBreakdown).length;
4651
- const codeCoverageComponent = codeEntities > 0 ? linkedEntities / codeEntities * 60 : 0;
4652
- const knowledgeDepthComponent = Math.min(knowledgeEntries / 10, 1) * 20;
4653
- const sourceDiversityComponent = Math.min(uniqueSources / 3, 1) * 20;
4654
- const score = Math.round(
4655
- codeCoverageComponent + knowledgeDepthComponent + sourceDiversityComponent
5020
+ domains.push(
5021
+ scoreDomain(domain, domainMap.get(domain) ?? [], codeDomains.get(domain) ?? [], store)
4656
5022
  );
4657
- domains.push({
4658
- domain,
4659
- score,
4660
- knowledgeEntries,
4661
- codeEntities,
4662
- linkedEntities,
4663
- unlinkedEntities: codeEntities - linkedEntities,
4664
- sourceBreakdown,
4665
- grade: toGrade(score)
4666
- });
4667
5023
  }
4668
5024
  const overallScore = domains.length > 0 ? Math.round(domains.reduce((sum, d) => sum + d.score, 0) / domains.length) : 0;
4669
5025
  return {
@@ -4684,8 +5040,166 @@ var CoverageScorer = class {
4684
5040
  }
4685
5041
  };
4686
5042
 
5043
+ // src/ingest/KnowledgeDocMaterializer.ts
5044
+ import * as fs10 from "fs/promises";
5045
+ import * as path11 from "path";
5046
+ var VALID_BUSINESS_TYPES = /* @__PURE__ */ new Set([
5047
+ "business_rule",
5048
+ "business_process",
5049
+ "business_concept",
5050
+ "business_term",
5051
+ "business_metric"
5052
+ ]);
5053
+ var DEFAULT_MAX_DOCS = 50;
5054
+ var MAX_COLLISION_SUFFIX = 10;
5055
+ var KnowledgeDocMaterializer = class {
5056
+ constructor(store) {
5057
+ this.store = store;
5058
+ }
5059
+ store;
5060
+ async materialize(gapEntries, options) {
5061
+ const maxDocs = options.maxDocs ?? DEFAULT_MAX_DOCS;
5062
+ const created = [];
5063
+ const skipped = [];
5064
+ const createdNames = /* @__PURE__ */ new Set();
5065
+ for (const entry of gapEntries) {
5066
+ const resolved = await this.resolveEntry(
5067
+ entry,
5068
+ created.length,
5069
+ maxDocs,
5070
+ createdNames,
5071
+ options
5072
+ );
5073
+ if ("reason" in resolved) {
5074
+ skipped.push(resolved);
5075
+ continue;
5076
+ }
5077
+ const { node, domain, domainDir, nameKey } = resolved;
5078
+ await fs10.mkdir(domainDir, { recursive: true });
5079
+ const basename10 = this.generateFilename(entry.name);
5080
+ const filename = await this.resolveCollision(domainDir, basename10);
5081
+ const content = this.formatDoc(node, domain);
5082
+ const filePath = ["docs", "knowledge", domain, filename].join("/");
5083
+ await fs10.writeFile(path11.join(options.projectDir, filePath), content, "utf-8");
5084
+ createdNames.add(nameKey);
5085
+ created.push({ filePath, nodeId: entry.nodeId, domain, name: entry.name });
5086
+ }
5087
+ return { created, skipped };
5088
+ }
5089
+ async resolveEntry(entry, createdCount, maxDocs, createdNames, options) {
5090
+ if (!entry.hasContent) {
5091
+ return { nodeId: entry.nodeId, name: entry.name, reason: "no_content" };
5092
+ }
5093
+ const node = this.store.getNode(entry.nodeId);
5094
+ if (!node || !node.content || node.content.trim().length < 10) {
5095
+ return { nodeId: entry.nodeId, name: entry.name, reason: "no_content" };
5096
+ }
5097
+ const domain = this.inferDomain(node);
5098
+ if (!domain || /[/\\]|\.\.|\0/.test(domain)) {
5099
+ return { nodeId: entry.nodeId, name: entry.name, reason: "no_domain" };
5100
+ }
5101
+ if (createdCount >= maxDocs) {
5102
+ return { nodeId: entry.nodeId, name: entry.name, reason: "cap_reached" };
5103
+ }
5104
+ const domainDir = path11.join(options.projectDir, "docs", "knowledge", domain);
5105
+ const nameKey = `${domain}:${entry.name.toLowerCase().trim()}`;
5106
+ if (!createdNames.has(nameKey) && await this.hasExistingDoc(domainDir, entry.name)) {
5107
+ return { nodeId: entry.nodeId, name: entry.name, reason: "already_documented" };
5108
+ }
5109
+ if (options.dryRun) {
5110
+ return { nodeId: entry.nodeId, name: entry.name, reason: "dry_run" };
5111
+ }
5112
+ return { node, domain, domainDir, nameKey };
5113
+ }
5114
+ inferDomain(node) {
5115
+ if (node.metadata?.domain && typeof node.metadata.domain === "string") {
5116
+ return node.metadata.domain;
5117
+ }
5118
+ if (node.path) {
5119
+ const pkgMatch = node.path.match(/^packages\/([^/]+)/);
5120
+ if (pkgMatch) return pkgMatch[1];
5121
+ const srcMatch = node.path.match(/^src\/([^/]+)/);
5122
+ if (srcMatch) return srcMatch[1];
5123
+ }
5124
+ if (node.metadata?.source === "knowledge-linker" || node.metadata?.source === "connector") {
5125
+ const connector = node.metadata.connectorName;
5126
+ if (typeof connector === "string") return connector;
5127
+ return "general";
5128
+ }
5129
+ return null;
5130
+ }
5131
+ generateFilename(name) {
5132
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
5133
+ return `${slug}.md`;
5134
+ }
5135
+ async resolveCollision(dir, basename10) {
5136
+ const ext = path11.extname(basename10);
5137
+ const stem = path11.basename(basename10, ext);
5138
+ try {
5139
+ await fs10.access(path11.join(dir, basename10));
5140
+ } catch {
5141
+ return basename10;
5142
+ }
5143
+ for (let i = 2; i <= MAX_COLLISION_SUFFIX; i++) {
5144
+ const candidate = `${stem}-${i}${ext}`;
5145
+ try {
5146
+ await fs10.access(path11.join(dir, candidate));
5147
+ } catch {
5148
+ return candidate;
5149
+ }
5150
+ }
5151
+ throw new Error(
5152
+ `Cannot resolve filename collision for "${basename10}" after ${MAX_COLLISION_SUFFIX} attempts`
5153
+ );
5154
+ }
5155
+ formatDoc(node, domain) {
5156
+ const mappedType = this.mapNodeType(node);
5157
+ const sanitize = (s) => s.replace(/[\n\r]/g, " ").replace(/:/g, "-");
5158
+ const lines = ["---", `type: ${sanitize(mappedType)}`, `domain: ${sanitize(domain)}`];
5159
+ const tags = node.metadata?.tags;
5160
+ if (Array.isArray(tags) && tags.length > 0) {
5161
+ lines.push(`tags: [${tags.map((t) => sanitize(String(t))).join(", ")}]`);
5162
+ }
5163
+ const related = node.metadata?.related;
5164
+ if (Array.isArray(related) && related.length > 0) {
5165
+ lines.push(`related: [${related.map((r) => sanitize(String(r))).join(", ")}]`);
5166
+ }
5167
+ const title = (node.name ?? "").replace(/[\n\r]/g, " ");
5168
+ lines.push("---", "", `# ${title}`, "", node.content ?? "", "");
5169
+ return lines.join("\n");
5170
+ }
5171
+ /** Check if a doc with a matching title already exists in the domain directory. */
5172
+ async hasExistingDoc(domainDir, name) {
5173
+ const normalizedName = name.toLowerCase().trim();
5174
+ try {
5175
+ const files = await fs10.readdir(domainDir);
5176
+ for (const file of files) {
5177
+ if (!file.endsWith(".md")) continue;
5178
+ const raw = await fs10.readFile(path11.join(domainDir, file), "utf-8");
5179
+ const fmMatch = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
5180
+ const body = fmMatch ? fmMatch[1] : raw;
5181
+ const titleMatch = body.match(/^#\s+(.+)$/m);
5182
+ if (titleMatch && titleMatch[1].trim().toLowerCase() === normalizedName) {
5183
+ return true;
5184
+ }
5185
+ }
5186
+ } catch {
5187
+ }
5188
+ return false;
5189
+ }
5190
+ mapNodeType(node) {
5191
+ if (VALID_BUSINESS_TYPES.has(node.type)) {
5192
+ return node.type;
5193
+ }
5194
+ if (node.type === "business_fact") {
5195
+ return "business_rule";
5196
+ }
5197
+ return "business_concept";
5198
+ }
5199
+ };
5200
+
4687
5201
  // src/ingest/KnowledgePipelineRunner.ts
4688
- var BUSINESS_NODE_TYPES = [
5202
+ var BUSINESS_NODE_TYPES2 = [
4689
5203
  "business_concept",
4690
5204
  "business_rule",
4691
5205
  "business_process",
@@ -4694,7 +5208,8 @@ var BUSINESS_NODE_TYPES = [
4694
5208
  "business_fact"
4695
5209
  ];
4696
5210
  var SNAPSHOT_NODE_TYPES = [
4697
- ...BUSINESS_NODE_TYPES,
5211
+ ...BUSINESS_NODE_TYPES2,
5212
+ "decision",
4698
5213
  "design_token",
4699
5214
  "design_constraint",
4700
5215
  "aesthetic_intent",
@@ -4706,51 +5221,98 @@ var KnowledgePipelineRunner = class {
4706
5221
  }
4707
5222
  store;
4708
5223
  async run(options) {
4709
- const maxIterations = options.maxIterations ?? 5;
4710
5224
  const remediations = [];
4711
5225
  const preSnapshot = this.buildSnapshot(options.domain);
4712
5226
  const extraction = await this.extract(options);
4713
5227
  const postSnapshot = this.buildSnapshot(options.domain);
4714
5228
  let driftResult = this.reconcile(preSnapshot, postSnapshot);
4715
- const contradictionDetector = new ContradictionDetector();
4716
- const contradictions = contradictionDetector.detect(this.store);
5229
+ const contradictions = new ContradictionDetector().detect(this.store);
4717
5230
  let gapReport = await this.detect(options);
4718
- const coverageScorer = new CoverageScorer();
4719
- const coverage = coverageScorer.score(this.store);
5231
+ const coverage = new CoverageScorer().score(this.store);
5232
+ let materialization;
4720
5233
  let iterations = 1;
4721
5234
  if (options.fix) {
4722
- let previousFindingCount = driftResult.findings.length;
4723
- while (iterations < maxIterations) {
4724
- if (driftResult.findings.length === 0) break;
4725
- this.remediate(driftResult, remediations, options);
4726
- const rePreSnapshot = this.buildSnapshot(options.domain);
4727
- await this.extract(options);
4728
- const rePostSnapshot = this.buildSnapshot(options.domain);
4729
- driftResult = this.reconcile(rePreSnapshot, rePostSnapshot);
4730
- gapReport = await this.detect(options);
4731
- iterations++;
4732
- const newFindingCount = driftResult.findings.length;
4733
- if (newFindingCount >= previousFindingCount) break;
4734
- previousFindingCount = newFindingCount;
4735
- }
5235
+ const loopResult = await this.runRemediationLoop(
5236
+ options,
5237
+ driftResult,
5238
+ gapReport,
5239
+ remediations
5240
+ );
5241
+ iterations = loopResult.iterations;
5242
+ materialization = loopResult.materialization;
5243
+ }
5244
+ if (options.fix && iterations > 1) {
5245
+ const finalSnapshot = this.buildSnapshot(options.domain);
5246
+ driftResult = this.reconcile(preSnapshot, finalSnapshot);
5247
+ gapReport = await this.detect(options);
4736
5248
  }
4737
5249
  await this.stageNewFindings(driftResult, options);
5250
+ return this.buildResult(
5251
+ driftResult,
5252
+ iterations,
5253
+ extraction,
5254
+ gapReport,
5255
+ remediations,
5256
+ contradictions,
5257
+ coverage,
5258
+ materialization
5259
+ );
5260
+ }
5261
+ /** Run the remediation convergence loop; returns iteration count and accumulated materialization. */
5262
+ async runRemediationLoop(options, driftResult, gapReport, remediations) {
5263
+ const maxIterations = options.maxIterations ?? 5;
5264
+ let iterations = 1;
5265
+ let currentDrift = driftResult;
5266
+ let currentGapReport = gapReport;
5267
+ let previousIssueCount = currentDrift.findings.length + currentGapReport.totalGaps;
5268
+ let accumulatedMaterialization;
5269
+ while (iterations < maxIterations) {
5270
+ if (currentDrift.findings.length === 0 && currentGapReport.totalGaps === 0) break;
5271
+ const matResult = await this.remediate(currentDrift, remediations, options, currentGapReport);
5272
+ if (matResult) {
5273
+ if (!accumulatedMaterialization) {
5274
+ accumulatedMaterialization = matResult;
5275
+ } else {
5276
+ accumulatedMaterialization = {
5277
+ created: [...accumulatedMaterialization.created, ...matResult.created],
5278
+ skipped: [...accumulatedMaterialization.skipped, ...matResult.skipped]
5279
+ };
5280
+ }
5281
+ }
5282
+ const preSnapshot = this.buildSnapshot(options.domain);
5283
+ await this.extract(options);
5284
+ const postSnapshot = this.buildSnapshot(options.domain);
5285
+ currentDrift = this.reconcile(preSnapshot, postSnapshot);
5286
+ currentGapReport = await this.detect(options);
5287
+ iterations++;
5288
+ const currentIssueCount = currentDrift.findings.length + currentGapReport.totalGaps;
5289
+ if (currentIssueCount >= previousIssueCount) break;
5290
+ previousIssueCount = currentIssueCount;
5291
+ }
5292
+ return {
5293
+ iterations,
5294
+ ...accumulatedMaterialization ? { materialization: accumulatedMaterialization } : {}
5295
+ };
5296
+ }
5297
+ /** Assemble the final pipeline result. */
5298
+ buildResult(driftResult, iterations, extraction, gaps, remediations, contradictions, coverage, materialization) {
4738
5299
  return {
4739
5300
  verdict: this.computeVerdict(driftResult),
4740
5301
  driftScore: driftResult.driftScore,
4741
5302
  iterations,
4742
5303
  findings: driftResult.summary,
4743
5304
  extraction,
4744
- gaps: gapReport,
5305
+ gaps,
4745
5306
  remediations,
4746
5307
  contradictions,
4747
- coverage
5308
+ coverage,
5309
+ ...materialization ? { materialization } : {}
4748
5310
  };
4749
5311
  }
4750
5312
  // ── Phase 1: EXTRACT ──────────────────────────────────────────────────────
4751
5313
  async extract(options) {
4752
- const extractedDir = path10.join(options.projectDir, ".harness", "knowledge", "extracted");
4753
- await fs9.mkdir(extractedDir, { recursive: true });
5314
+ const extractedDir = path12.join(options.projectDir, ".harness", "knowledge", "extracted");
5315
+ await fs11.mkdir(extractedDir, { recursive: true });
4754
5316
  const runner = createExtractionRunner();
4755
5317
  const extractionResult = await runner.run(options.projectDir, this.store, extractedDir);
4756
5318
  const diagramParser = new DiagramParser(this.store);
@@ -4764,7 +5326,7 @@ var KnowledgePipelineRunner = class {
4764
5326
  const imageResult = await imageExtractor.analyze(this.store, imagePaths);
4765
5327
  imageCount = imageResult.nodesAdded;
4766
5328
  }
4767
- const knowledgeDir = path10.join(options.projectDir, "docs", "knowledge");
5329
+ const knowledgeDir = path12.join(options.projectDir, "docs", "knowledge");
4768
5330
  const bkIngestor = new BusinessKnowledgeIngestor(this.store);
4769
5331
  let bkResult;
4770
5332
  try {
@@ -4779,6 +5341,21 @@ var KnowledgePipelineRunner = class {
4779
5341
  durationMs: 0
4780
5342
  };
4781
5343
  }
5344
+ const decisionsDir = path12.join(options.projectDir, "docs", "knowledge", "decisions");
5345
+ const decisionIngestor = new DecisionIngestor(this.store);
5346
+ let decisionResult;
5347
+ try {
5348
+ decisionResult = await decisionIngestor.ingest(decisionsDir);
5349
+ } catch {
5350
+ decisionResult = {
5351
+ nodesAdded: 0,
5352
+ nodesUpdated: 0,
5353
+ edgesAdded: 0,
5354
+ edgesUpdated: 0,
5355
+ errors: [],
5356
+ durationMs: 0
5357
+ };
5358
+ }
4782
5359
  const linker = new KnowledgeLinker(this.store, extractedDir);
4783
5360
  const linkResult = await linker.link();
4784
5361
  return {
@@ -4786,6 +5363,7 @@ var KnowledgePipelineRunner = class {
4786
5363
  diagrams: diagramResult.nodesAdded,
4787
5364
  linkerFacts: linkResult.factsCreated,
4788
5365
  businessKnowledge: bkResult.nodesAdded,
5366
+ decisions: decisionResult.nodesAdded,
4789
5367
  images: imageCount
4790
5368
  };
4791
5369
  }
@@ -4812,14 +5390,14 @@ var KnowledgePipelineRunner = class {
4812
5390
  }
4813
5391
  // ── Phase 3: DETECT ───────────────────────────────────────────────────────
4814
5392
  async detect(options) {
4815
- const knowledgeDir = path10.join(options.projectDir, "docs", "knowledge");
5393
+ const knowledgeDir = path12.join(options.projectDir, "docs", "knowledge");
4816
5394
  const aggregator = new KnowledgeStagingAggregator(options.projectDir);
4817
- const gapReport = await aggregator.generateGapReport(knowledgeDir);
5395
+ const gapReport = await aggregator.generateGapReport(knowledgeDir, this.store);
4818
5396
  await aggregator.writeGapReport(gapReport);
4819
5397
  return gapReport;
4820
5398
  }
4821
5399
  // ── Phase 4: REMEDIATE ────────────────────────────────────────────────────
4822
- remediate(driftResult, remediations, options) {
5400
+ async remediate(driftResult, remediations, options, gapReport) {
4823
5401
  for (const finding of driftResult.findings) {
4824
5402
  switch (finding.classification) {
4825
5403
  case "stale":
@@ -4837,6 +5415,22 @@ var KnowledgePipelineRunner = class {
4837
5415
  break;
4838
5416
  }
4839
5417
  }
5418
+ if (!options.ci) {
5419
+ const allGapEntries = gapReport.domains.flatMap((d) => d.gapEntries);
5420
+ const materializable = allGapEntries.filter((e) => e.hasContent);
5421
+ if (materializable.length > 0) {
5422
+ const materializer = new KnowledgeDocMaterializer(this.store);
5423
+ const matResult = await materializer.materialize(materializable, {
5424
+ projectDir: options.projectDir,
5425
+ dryRun: false
5426
+ });
5427
+ for (const doc of matResult.created) {
5428
+ remediations.push(`created doc: ${doc.filePath}`);
5429
+ }
5430
+ return matResult;
5431
+ }
5432
+ }
5433
+ return void 0;
4840
5434
  }
4841
5435
  async stageNewFindings(driftResult, options) {
4842
5436
  const newFindings = driftResult.findings.filter((f) => f.classification === "new");
@@ -4871,7 +5465,7 @@ var KnowledgePipelineRunner = class {
4871
5465
  };
4872
5466
 
4873
5467
  // src/ingest/connectors/ConnectorUtils.ts
4874
- var CODE_NODE_TYPES4 = ["file", "function", "class", "method", "interface", "variable"];
5468
+ var CODE_NODE_TYPES5 = ["file", "function", "class", "method", "interface", "variable"];
4875
5469
  var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
4876
5470
  function withRetry(client, options) {
4877
5471
  const maxRetries = options?.maxRetries ?? 3;
@@ -4936,7 +5530,7 @@ function sanitizeExternalText(text, maxLength = 2e3) {
4936
5530
  }
4937
5531
  function linkToCode(store, content, sourceNodeId, edgeType, options) {
4938
5532
  let edgesCreated = 0;
4939
- for (const type of CODE_NODE_TYPES4) {
5533
+ for (const type of CODE_NODE_TYPES5) {
4940
5534
  const nodes = store.findNodes({ type });
4941
5535
  for (const node of nodes) {
4942
5536
  if (node.name.length < 3) continue;
@@ -4956,12 +5550,12 @@ function linkToCode(store, content, sourceNodeId, edgeType, options) {
4956
5550
  }
4957
5551
 
4958
5552
  // src/ingest/connectors/SyncManager.ts
4959
- import * as fs10 from "fs/promises";
4960
- import * as path11 from "path";
5553
+ import * as fs12 from "fs/promises";
5554
+ import * as path13 from "path";
4961
5555
  var SyncManager = class {
4962
5556
  constructor(store, graphDir) {
4963
5557
  this.store = store;
4964
- this.metadataPath = path11.join(graphDir, "sync-metadata.json");
5558
+ this.metadataPath = path13.join(graphDir, "sync-metadata.json");
4965
5559
  }
4966
5560
  store;
4967
5561
  registrations = /* @__PURE__ */ new Map();
@@ -5026,15 +5620,15 @@ var SyncManager = class {
5026
5620
  }
5027
5621
  async loadMetadata() {
5028
5622
  try {
5029
- const raw = await fs10.readFile(this.metadataPath, "utf-8");
5623
+ const raw = await fs12.readFile(this.metadataPath, "utf-8");
5030
5624
  return JSON.parse(raw);
5031
5625
  } catch {
5032
5626
  return { connectors: {} };
5033
5627
  }
5034
5628
  }
5035
5629
  async saveMetadata(metadata) {
5036
- await fs10.mkdir(path11.dirname(this.metadataPath), { recursive: true });
5037
- await fs10.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
5630
+ await fs12.mkdir(path13.dirname(this.metadataPath), { recursive: true });
5631
+ await fs12.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
5038
5632
  }
5039
5633
  };
5040
5634
 
@@ -5139,6 +5733,14 @@ var JiraConnector = class {
5139
5733
  start
5140
5734
  );
5141
5735
  }
5736
+ try {
5737
+ const parsed = new URL(baseUrl);
5738
+ if (parsed.protocol !== "https:" && parsed.hostname !== "localhost") {
5739
+ return buildIngestResult(0, 0, [`${baseUrlEnv} must use HTTPS`], start);
5740
+ }
5741
+ } catch {
5742
+ return buildIngestResult(0, 0, [`${baseUrlEnv} is not a valid URL`], start);
5743
+ }
5142
5744
  const jql = buildJql(config);
5143
5745
  const headers = { Authorization: `Basic ${apiKey}`, "Content-Type": "application/json" };
5144
5746
  try {
@@ -5428,6 +6030,14 @@ var ConfluenceConnector = class {
5428
6030
  }
5429
6031
  const baseUrlEnv = config.baseUrlEnv ?? "CONFLUENCE_BASE_URL";
5430
6032
  const baseUrl = process.env[baseUrlEnv] ?? "";
6033
+ try {
6034
+ const parsed = new URL(baseUrl);
6035
+ if (parsed.protocol !== "https:" && parsed.hostname !== "localhost") {
6036
+ return missingApiKeyResult(`${baseUrlEnv} must use HTTPS`, start);
6037
+ }
6038
+ } catch {
6039
+ return missingApiKeyResult(`${baseUrlEnv} is not a valid URL`, start);
6040
+ }
5431
6041
  const spaceKey = config.spaceKey ?? "";
5432
6042
  const counts = await this.fetchAllPagesHandled(
5433
6043
  store,
@@ -5521,7 +6131,7 @@ var ConfluenceConnector = class {
5521
6131
  };
5522
6132
 
5523
6133
  // src/ingest/connectors/CIConnector.ts
5524
- function emptyResult2(errors, start) {
6134
+ function emptyResult5(errors, start) {
5525
6135
  return {
5526
6136
  nodesAdded: 0,
5527
6137
  nodesUpdated: 0,
@@ -5589,7 +6199,7 @@ var CIConnector = class {
5589
6199
  const apiKeyEnv = config.apiKeyEnv ?? "GITHUB_TOKEN";
5590
6200
  const apiKey = process.env[apiKeyEnv];
5591
6201
  if (!apiKey) {
5592
- return emptyResult2(
6202
+ return emptyResult5(
5593
6203
  [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
5594
6204
  start
5595
6205
  );
@@ -5653,6 +6263,14 @@ function hasConstraintKeyword(text) {
5653
6263
  const lower = text.toLowerCase();
5654
6264
  return CONSTRAINT_KEYWORDS.some((kw) => lower.includes(kw));
5655
6265
  }
6266
+ function buildNodeMetadata(condensed, base) {
6267
+ const metadata = { ...base };
6268
+ if (condensed.method !== "passthrough") {
6269
+ metadata.condensed = condensed.method;
6270
+ metadata.originalLength = condensed.originalLength;
6271
+ }
6272
+ return metadata;
6273
+ }
5656
6274
  function buildIngestResult2(nodesAdded, edgesAdded, errors, start) {
5657
6275
  return {
5658
6276
  nodesAdded,
@@ -5663,6 +6281,24 @@ function buildIngestResult2(nodesAdded, edgesAdded, errors, start) {
5663
6281
  durationMs: Date.now() - start
5664
6282
  };
5665
6283
  }
6284
+ function validateFigmaConfig(config) {
6285
+ const apiKeyEnv = config.apiKeyEnv ?? "FIGMA_API_KEY";
6286
+ const apiKey = process.env[apiKeyEnv];
6287
+ if (!apiKey) return { error: `Missing API key: environment variable "${apiKeyEnv}" is not set` };
6288
+ const baseUrlEnv = config.baseUrlEnv ?? "FIGMA_BASE_URL";
6289
+ const baseUrl = process.env[baseUrlEnv] ?? "https://api.figma.com";
6290
+ try {
6291
+ const parsed = new URL(baseUrl);
6292
+ if (parsed.protocol !== "https:" || !parsed.hostname.endsWith("api.figma.com")) {
6293
+ return { error: `Invalid ${baseUrlEnv}: must be an HTTPS URL on api.figma.com` };
6294
+ }
6295
+ } catch {
6296
+ return { error: `Invalid ${baseUrlEnv}: not a valid URL` };
6297
+ }
6298
+ const fileIds = config.fileIds;
6299
+ if (!fileIds || fileIds.length === 0) return { error: "No fileIds provided in connector config" };
6300
+ return { apiKey, baseUrl, fileIds };
6301
+ }
5666
6302
  var FigmaConnector = class {
5667
6303
  name = "figma";
5668
6304
  source = "figma";
@@ -5672,35 +6308,9 @@ var FigmaConnector = class {
5672
6308
  }
5673
6309
  async ingest(store, config) {
5674
6310
  const start = Date.now();
5675
- const apiKeyEnv = config.apiKeyEnv ?? "FIGMA_API_KEY";
5676
- const apiKey = process.env[apiKeyEnv];
5677
- if (!apiKey) {
5678
- return buildIngestResult2(
5679
- 0,
5680
- 0,
5681
- [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
5682
- start
5683
- );
5684
- }
5685
- const baseUrlEnv = config.baseUrlEnv ?? "FIGMA_BASE_URL";
5686
- const baseUrl = process.env[baseUrlEnv] ?? "https://api.figma.com";
5687
- try {
5688
- const parsed = new URL(baseUrl);
5689
- if (parsed.protocol !== "https:" || !parsed.hostname.endsWith("api.figma.com")) {
5690
- return buildIngestResult2(
5691
- 0,
5692
- 0,
5693
- [`Invalid ${baseUrlEnv}: must be an HTTPS URL on api.figma.com`],
5694
- start
5695
- );
5696
- }
5697
- } catch {
5698
- return buildIngestResult2(0, 0, [`Invalid ${baseUrlEnv}: not a valid URL`], start);
5699
- }
5700
- const fileIds = config.fileIds;
5701
- if (!fileIds || fileIds.length === 0) {
5702
- return buildIngestResult2(0, 0, ["No fileIds provided in connector config"], start);
5703
- }
6311
+ const validated = validateFigmaConfig(config);
6312
+ if ("error" in validated) return buildIngestResult2(0, 0, [validated.error], start);
6313
+ const { apiKey, baseUrl, fileIds } = validated;
5704
6314
  const headers = { "X-FIGMA-TOKEN": apiKey };
5705
6315
  const maxLen = config.maxContentLength ?? 4e3;
5706
6316
  let nodesAdded = 0;
@@ -5720,6 +6330,17 @@ var FigmaConnector = class {
5720
6330
  return buildIngestResult2(nodesAdded, edgesAdded, errors, start);
5721
6331
  }
5722
6332
  async processFile(store, baseUrl, fileId, headers, maxLen) {
6333
+ let nodesAdded = 0;
6334
+ let edgesAdded = 0;
6335
+ const styleCounts = await this.ingestStyles(store, baseUrl, fileId, headers, maxLen);
6336
+ nodesAdded += styleCounts.nodesAdded;
6337
+ edgesAdded += styleCounts.edgesAdded;
6338
+ const componentCounts = await this.ingestComponents(store, baseUrl, fileId, headers, maxLen);
6339
+ nodesAdded += componentCounts.nodesAdded;
6340
+ edgesAdded += componentCounts.edgesAdded;
6341
+ return { nodesAdded, edgesAdded };
6342
+ }
6343
+ async ingestStyles(store, baseUrl, fileId, headers, maxLen) {
5723
6344
  let nodesAdded = 0;
5724
6345
  let edgesAdded = 0;
5725
6346
  const stylesUrl = `${baseUrl}/v1/files/${fileId}/styles`;
@@ -5727,90 +6348,112 @@ var FigmaConnector = class {
5727
6348
  if (!stylesResponse.ok) throw new Error(`Styles request failed for file ${fileId}`);
5728
6349
  const stylesData = await stylesResponse.json();
5729
6350
  for (const style of stylesData.meta.styles) {
5730
- const nodeId = `figma:token:${style.key}`;
5731
- const condensed = await condenseContent(sanitizeExternalText(style.description || "", 2e3), {
5732
- maxLength: maxLen
5733
- });
5734
- const metadata = {
5735
- source: "figma",
5736
- key: style.key,
5737
- styleType: style.style_type,
5738
- fileId
5739
- };
5740
- if (condensed.method !== "passthrough") {
5741
- metadata.condensed = condensed.method;
5742
- metadata.originalLength = condensed.originalLength;
5743
- }
5744
- store.addNode({
5745
- id: nodeId,
5746
- type: "design_token",
5747
- name: sanitizeExternalText(style.name, 500),
5748
- content: condensed.content,
5749
- metadata
5750
- });
5751
- nodesAdded++;
5752
- const searchText = sanitizeExternalText([style.name, style.description].join(" "));
5753
- edgesAdded += linkToCode(store, searchText, nodeId, "references");
6351
+ const counts = await this.processStyle(store, style, fileId, maxLen);
6352
+ nodesAdded += counts.nodesAdded;
6353
+ edgesAdded += counts.edgesAdded;
5754
6354
  }
6355
+ return { nodesAdded, edgesAdded };
6356
+ }
6357
+ async processStyle(store, style, fileId, maxLen) {
6358
+ const nodeId = `figma:token:${style.key}`;
6359
+ const condensed = await condenseContent(sanitizeExternalText(style.description || "", 2e3), {
6360
+ maxLength: maxLen
6361
+ });
6362
+ const metadata = buildNodeMetadata(condensed, {
6363
+ source: "figma",
6364
+ key: style.key,
6365
+ styleType: style.style_type,
6366
+ fileId
6367
+ });
6368
+ store.addNode({
6369
+ id: nodeId,
6370
+ type: "design_token",
6371
+ name: sanitizeExternalText(style.name, 500),
6372
+ content: condensed.content,
6373
+ metadata
6374
+ });
6375
+ const searchText = sanitizeExternalText([style.name, style.description].join(" "));
6376
+ const edgesAdded = linkToCode(store, searchText, nodeId, "references");
6377
+ return { nodesAdded: 1, edgesAdded };
6378
+ }
6379
+ async ingestComponents(store, baseUrl, fileId, headers, maxLen) {
6380
+ let nodesAdded = 0;
6381
+ let edgesAdded = 0;
5755
6382
  const componentsUrl = `${baseUrl}/v1/files/${fileId}/components`;
5756
6383
  const componentsResponse = await this.httpClient(componentsUrl, { headers });
5757
6384
  if (!componentsResponse.ok) throw new Error(`Components request failed for file ${fileId}`);
5758
6385
  const componentsData = await componentsResponse.json();
5759
6386
  for (const component of componentsData.meta.components) {
5760
- const description = component.description || "";
5761
- if (description) {
5762
- const intentId = `figma:intent:${component.key}`;
5763
- const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
5764
- maxLength: maxLen
5765
- });
5766
- const metadata = {
5767
- source: "figma",
5768
- key: component.key,
5769
- fileId
5770
- };
5771
- if (condensed.method !== "passthrough") {
5772
- metadata.condensed = condensed.method;
5773
- metadata.originalLength = condensed.originalLength;
5774
- }
5775
- store.addNode({
5776
- id: intentId,
5777
- type: "aesthetic_intent",
5778
- name: sanitizeExternalText(component.name, 500),
5779
- content: condensed.content,
5780
- metadata
5781
- });
5782
- nodesAdded++;
5783
- const searchText = sanitizeExternalText([component.name, description].join(" "));
5784
- edgesAdded += linkToCode(store, searchText, intentId, "references");
5785
- }
5786
- if (description && hasConstraintKeyword(description)) {
5787
- const constraintId = `figma:constraint:${component.key}`;
5788
- const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
5789
- maxLength: maxLen
5790
- });
5791
- const metadata = {
5792
- source: "figma",
5793
- key: component.key,
5794
- fileId
5795
- };
5796
- if (condensed.method !== "passthrough") {
5797
- metadata.condensed = condensed.method;
5798
- metadata.originalLength = condensed.originalLength;
5799
- }
5800
- store.addNode({
5801
- id: constraintId,
5802
- type: "design_constraint",
5803
- name: sanitizeExternalText(component.name, 500),
5804
- content: condensed.content,
5805
- metadata
5806
- });
5807
- nodesAdded++;
5808
- const searchText = sanitizeExternalText([component.name, description].join(" "));
5809
- edgesAdded += linkToCode(store, searchText, constraintId, "references");
5810
- }
6387
+ const counts = await this.processComponent(store, component, fileId, maxLen);
6388
+ nodesAdded += counts.nodesAdded;
6389
+ edgesAdded += counts.edgesAdded;
5811
6390
  }
5812
6391
  return { nodesAdded, edgesAdded };
5813
6392
  }
6393
+ async processComponent(store, component, fileId, maxLen) {
6394
+ let nodesAdded = 0;
6395
+ let edgesAdded = 0;
6396
+ const description = component.description || "";
6397
+ if (description) {
6398
+ const counts = await this.addComponentIntent(store, component, description, fileId, maxLen);
6399
+ nodesAdded += counts.nodesAdded;
6400
+ edgesAdded += counts.edgesAdded;
6401
+ }
6402
+ if (description && hasConstraintKeyword(description)) {
6403
+ const counts = await this.addComponentConstraint(
6404
+ store,
6405
+ component,
6406
+ description,
6407
+ fileId,
6408
+ maxLen
6409
+ );
6410
+ nodesAdded += counts.nodesAdded;
6411
+ edgesAdded += counts.edgesAdded;
6412
+ }
6413
+ return { nodesAdded, edgesAdded };
6414
+ }
6415
+ async addComponentIntent(store, component, description, fileId, maxLen) {
6416
+ const intentId = `figma:intent:${component.key}`;
6417
+ const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
6418
+ maxLength: maxLen
6419
+ });
6420
+ const metadata = buildNodeMetadata(condensed, {
6421
+ source: "figma",
6422
+ key: component.key,
6423
+ fileId
6424
+ });
6425
+ store.addNode({
6426
+ id: intentId,
6427
+ type: "aesthetic_intent",
6428
+ name: sanitizeExternalText(component.name, 500),
6429
+ content: condensed.content,
6430
+ metadata
6431
+ });
6432
+ const searchText = sanitizeExternalText([component.name, description].join(" "));
6433
+ const edgesAdded = linkToCode(store, searchText, intentId, "references");
6434
+ return { nodesAdded: 1, edgesAdded };
6435
+ }
6436
+ async addComponentConstraint(store, component, description, fileId, maxLen) {
6437
+ const constraintId = `figma:constraint:${component.key}`;
6438
+ const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
6439
+ maxLength: maxLen
6440
+ });
6441
+ const metadata = buildNodeMetadata(condensed, {
6442
+ source: "figma",
6443
+ key: component.key,
6444
+ fileId
6445
+ });
6446
+ store.addNode({
6447
+ id: constraintId,
6448
+ type: "design_constraint",
6449
+ name: sanitizeExternalText(component.name, 500),
6450
+ content: condensed.content,
6451
+ metadata
6452
+ });
6453
+ const searchText = sanitizeExternalText([component.name, description].join(" "));
6454
+ const edgesAdded = linkToCode(store, searchText, constraintId, "references");
6455
+ return { nodesAdded: 1, edgesAdded };
6456
+ }
5814
6457
  };
5815
6458
 
5816
6459
  // src/ingest/connectors/MiroConnector.ts
@@ -5829,6 +6472,46 @@ function buildIngestResult3(nodesAdded, edgesAdded, errors, start) {
5829
6472
  durationMs: Date.now() - start
5830
6473
  };
5831
6474
  }
6475
+ function validateBaseUrl(baseUrl, baseUrlEnv) {
6476
+ try {
6477
+ const parsed = new URL(baseUrl);
6478
+ if (parsed.protocol !== "https:" || parsed.hostname !== "api.miro.com") {
6479
+ return `Invalid ${baseUrlEnv}: must be an HTTPS URL on api.miro.com`;
6480
+ }
6481
+ } catch {
6482
+ return `Invalid ${baseUrlEnv}: not a valid URL`;
6483
+ }
6484
+ return null;
6485
+ }
6486
+ function resolveConfig(config, start) {
6487
+ const apiKeyEnv = config.apiKeyEnv ?? "MIRO_API_KEY";
6488
+ const apiKey = process.env[apiKeyEnv];
6489
+ if (!apiKey) {
6490
+ return {
6491
+ ok: false,
6492
+ result: buildIngestResult3(
6493
+ 0,
6494
+ 0,
6495
+ [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
6496
+ start
6497
+ )
6498
+ };
6499
+ }
6500
+ const baseUrlEnv = config.baseUrlEnv ?? "MIRO_BASE_URL";
6501
+ const baseUrl = process.env[baseUrlEnv] ?? "https://api.miro.com";
6502
+ const urlError = validateBaseUrl(baseUrl, baseUrlEnv);
6503
+ if (urlError) {
6504
+ return { ok: false, result: buildIngestResult3(0, 0, [urlError], start) };
6505
+ }
6506
+ const boardIds = config.boardIds;
6507
+ if (!boardIds || boardIds.length === 0) {
6508
+ return {
6509
+ ok: false,
6510
+ result: buildIngestResult3(0, 0, ["No boardIds provided in config"], start)
6511
+ };
6512
+ }
6513
+ return { ok: true, apiKey, baseUrl, boardIds, headers: { Authorization: `Bearer ${apiKey}` } };
6514
+ }
5832
6515
  var MiroConnector = class {
5833
6516
  name = "miro";
5834
6517
  source = "miro";
@@ -5838,36 +6521,9 @@ var MiroConnector = class {
5838
6521
  }
5839
6522
  async ingest(store, config) {
5840
6523
  const start = Date.now();
5841
- const apiKeyEnv = config.apiKeyEnv ?? "MIRO_API_KEY";
5842
- const apiKey = process.env[apiKeyEnv];
5843
- if (!apiKey) {
5844
- return buildIngestResult3(
5845
- 0,
5846
- 0,
5847
- [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
5848
- start
5849
- );
5850
- }
5851
- const baseUrlEnv = config.baseUrlEnv ?? "MIRO_BASE_URL";
5852
- const baseUrl = process.env[baseUrlEnv] ?? "https://api.miro.com";
5853
- try {
5854
- const parsed = new URL(baseUrl);
5855
- if (parsed.protocol !== "https:" || parsed.hostname !== "api.miro.com") {
5856
- return buildIngestResult3(
5857
- 0,
5858
- 0,
5859
- [`Invalid ${baseUrlEnv}: must be an HTTPS URL on api.miro.com`],
5860
- start
5861
- );
5862
- }
5863
- } catch {
5864
- return buildIngestResult3(0, 0, [`Invalid ${baseUrlEnv}: not a valid URL`], start);
5865
- }
5866
- const boardIds = config.boardIds;
5867
- if (!boardIds || boardIds.length === 0) {
5868
- return buildIngestResult3(0, 0, ["No boardIds provided in config"], start);
5869
- }
5870
- const headers = { Authorization: `Bearer ${apiKey}` };
6524
+ const resolved = resolveConfig(config, start);
6525
+ if (!resolved.ok) return resolved.result;
6526
+ const { baseUrl, boardIds, headers } = resolved;
5871
6527
  let totalNodesAdded = 0;
5872
6528
  let totalEdgesAdded = 0;
5873
6529
  const errors = [];
@@ -6071,7 +6727,7 @@ var FusionLayer = class {
6071
6727
  };
6072
6728
 
6073
6729
  // src/entropy/GraphEntropyAdapter.ts
6074
- var CODE_NODE_TYPES5 = ["file", "function", "class", "method", "interface", "variable"];
6730
+ var CODE_NODE_TYPES6 = ["file", "function", "class", "method", "interface", "variable"];
6075
6731
  var GraphEntropyAdapter = class {
6076
6732
  constructor(store) {
6077
6733
  this.store = store;
@@ -6151,7 +6807,7 @@ var GraphEntropyAdapter = class {
6151
6807
  }
6152
6808
  findEntryPoints() {
6153
6809
  const entryPoints = [];
6154
- for (const nodeType of CODE_NODE_TYPES5) {
6810
+ for (const nodeType of CODE_NODE_TYPES6) {
6155
6811
  const nodes = this.store.findNodes({ type: nodeType });
6156
6812
  for (const node of nodes) {
6157
6813
  const isIndexFile = nodeType === "file" && node.name === "index.ts";
@@ -6187,7 +6843,7 @@ var GraphEntropyAdapter = class {
6187
6843
  }
6188
6844
  collectUnreachableNodes(visited) {
6189
6845
  const unreachableNodes = [];
6190
- for (const nodeType of CODE_NODE_TYPES5) {
6846
+ for (const nodeType of CODE_NODE_TYPES6) {
6191
6847
  const nodes = this.store.findNodes({ type: nodeType });
6192
6848
  for (const node of nodes) {
6193
6849
  if (!visited.has(node.id)) {
@@ -7033,9 +7689,9 @@ var EntityExtractor = class {
7033
7689
  extractPaths(trimmed, add) {
7034
7690
  const consumed = /* @__PURE__ */ new Set();
7035
7691
  for (const match of trimmed.matchAll(FILE_PATH_RE)) {
7036
- const path13 = match[0];
7037
- add(path13);
7038
- consumed.add(path13);
7692
+ const path15 = match[0];
7693
+ add(path15);
7694
+ consumed.add(path15);
7039
7695
  }
7040
7696
  return consumed;
7041
7697
  }
@@ -7107,8 +7763,8 @@ var EntityResolver = class {
7107
7763
  if (isPathLike && node.path.includes(raw)) {
7108
7764
  return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
7109
7765
  }
7110
- const basename7 = node.path.split("/").pop() ?? "";
7111
- if (basename7.includes(raw)) {
7766
+ const basename10 = node.path.split("/").pop() ?? "";
7767
+ if (basename10.includes(raw)) {
7112
7768
  return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
7113
7769
  }
7114
7770
  if (raw.length >= 4 && node.path.includes(raw)) {
@@ -7187,13 +7843,13 @@ var ResponseFormatter = class {
7187
7843
  const context = Array.isArray(d?.context) ? d.context : [];
7188
7844
  const firstEntity = entities[0];
7189
7845
  const nodeType = firstEntity?.node.type ?? "node";
7190
- const path13 = firstEntity?.node.path ?? "unknown";
7846
+ const path15 = firstEntity?.node.path ?? "unknown";
7191
7847
  let neighborCount = 0;
7192
7848
  const firstContext = context[0];
7193
7849
  if (firstContext && Array.isArray(firstContext.nodes)) {
7194
7850
  neighborCount = firstContext.nodes.length;
7195
7851
  }
7196
- return `**${entityName}** is a ${nodeType} at \`${path13}\`. Connected to ${neighborCount} nodes.`;
7852
+ return `**${entityName}** is a ${nodeType} at \`${path15}\`. Connected to ${neighborCount} nodes.`;
7197
7853
  }
7198
7854
  formatAnomaly(data) {
7199
7855
  const d = data;
@@ -7576,7 +8232,7 @@ var PHASE_NODE_TYPES = {
7576
8232
  debug: ["failure", "learning", "function", "method"],
7577
8233
  plan: ["adr", "document", "module", "layer"]
7578
8234
  };
7579
- var CODE_NODE_TYPES6 = /* @__PURE__ */ new Set([
8235
+ var CODE_NODE_TYPES7 = /* @__PURE__ */ new Set([
7580
8236
  "file",
7581
8237
  "function",
7582
8238
  "class",
@@ -7795,7 +8451,7 @@ var Assembler = class {
7795
8451
  */
7796
8452
  checkCoverage() {
7797
8453
  const codeNodes = [];
7798
- for (const type of CODE_NODE_TYPES6) {
8454
+ for (const type of CODE_NODE_TYPES7) {
7799
8455
  codeNodes.push(...this.store.findNodes({ type }));
7800
8456
  }
7801
8457
  const documented = [];
@@ -7975,14 +8631,14 @@ var GraphConstraintAdapter = class {
7975
8631
  };
7976
8632
 
7977
8633
  // src/ingest/DesignIngestor.ts
7978
- import * as fs11 from "fs/promises";
7979
- import * as path12 from "path";
8634
+ import * as fs13 from "fs/promises";
8635
+ import * as path14 from "path";
7980
8636
  function isDTCGToken(obj) {
7981
8637
  return typeof obj === "object" && obj !== null && "$value" in obj && "$type" in obj;
7982
8638
  }
7983
8639
  async function readFileOrNull(filePath) {
7984
8640
  try {
7985
- return await fs11.readFile(filePath, "utf-8");
8641
+ return await fs13.readFile(filePath, "utf-8");
7986
8642
  } catch {
7987
8643
  return null;
7988
8644
  }
@@ -8128,8 +8784,8 @@ var DesignIngestor = class {
8128
8784
  async ingestAll(designDir) {
8129
8785
  const start = Date.now();
8130
8786
  const [tokensResult, intentResult] = await Promise.all([
8131
- this.ingestTokens(path12.join(designDir, "tokens.json")),
8132
- this.ingestDesignIntent(path12.join(designDir, "DESIGN.md"))
8787
+ this.ingestTokens(path14.join(designDir, "tokens.json")),
8788
+ this.ingestDesignIntent(path14.join(designDir, "DESIGN.md"))
8133
8789
  ]);
8134
8790
  const merged = mergeResults(tokensResult, intentResult);
8135
8791
  return { ...merged, durationMs: Date.now() - start };
@@ -8365,10 +9021,10 @@ var TaskIndependenceAnalyzer = class {
8365
9021
  includeTypes: ["file"]
8366
9022
  });
8367
9023
  for (const n of queryResult.nodes) {
8368
- const path13 = n.path ?? n.id.replace(/^file:/, "");
8369
- if (!fileSet.has(path13)) {
8370
- if (!result.has(path13)) {
8371
- result.set(path13, file);
9024
+ const path15 = n.path ?? n.id.replace(/^file:/, "");
9025
+ if (!fileSet.has(path15)) {
9026
+ if (!result.has(path15)) {
9027
+ result.set(path15, file);
8372
9028
  }
8373
9029
  }
8374
9030
  }
@@ -8745,7 +9401,7 @@ var ConflictPredictor = class {
8745
9401
  };
8746
9402
 
8747
9403
  // src/index.ts
8748
- var VERSION = "0.4.3";
9404
+ var VERSION = "0.6.0";
8749
9405
  export {
8750
9406
  ALL_EXTRACTORS,
8751
9407
  ApiPathExtractor,
@@ -8762,6 +9418,7 @@ export {
8762
9418
  ContradictionDetector,
8763
9419
  CoverageScorer,
8764
9420
  D2Parser,
9421
+ DecisionIngestor,
8765
9422
  DesignConstraintAdapter,
8766
9423
  DesignIngestor,
8767
9424
  DiagramParser,
@@ -8786,6 +9443,7 @@ export {
8786
9443
  ImageAnalysisExtractor,
8787
9444
  IntentClassifier,
8788
9445
  JiraConnector,
9446
+ KnowledgeDocMaterializer,
8789
9447
  KnowledgeIngestor,
8790
9448
  KnowledgePipelineRunner,
8791
9449
  KnowledgeStagingAggregator,