@harness-engineering/graph 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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;
@@ -2263,20 +2388,241 @@ var RequirementIngestor = class {
2263
2388
  }
2264
2389
  };
2265
2390
 
2391
+ // src/ingest/domain-inference.ts
2392
+ var DEFAULT_PATTERNS = [
2393
+ "packages/<dir>",
2394
+ "apps/<dir>",
2395
+ "services/<dir>",
2396
+ "src/<dir>",
2397
+ "lib/<dir>"
2398
+ ];
2399
+ var DEFAULT_BLOCKLIST = /* @__PURE__ */ new Set([
2400
+ "node_modules",
2401
+ ".harness",
2402
+ "dist",
2403
+ "build",
2404
+ ".git",
2405
+ "coverage",
2406
+ ".next",
2407
+ ".turbo",
2408
+ ".cache",
2409
+ "out",
2410
+ "tmp"
2411
+ ]);
2412
+ var CODE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
2413
+ function matchPattern(filePath, pattern) {
2414
+ const patternParts = pattern.split("/").filter((s) => s.length > 0);
2415
+ if (patternParts.length !== 2 || patternParts[1] !== "<dir>") {
2416
+ return null;
2417
+ }
2418
+ const prefix = patternParts[0];
2419
+ const pathParts = filePath.split("/").filter((s) => s.length > 0);
2420
+ if (pathParts.length < 2) return null;
2421
+ if (pathParts[0] !== prefix) return null;
2422
+ let dir = pathParts[1];
2423
+ if (dir.length === 0) return null;
2424
+ if (CODE_EXTENSIONS.some((ext) => dir.endsWith(ext))) {
2425
+ const dotIdx = dir.lastIndexOf(".");
2426
+ if (dotIdx > 0) dir = dir.slice(0, dotIdx);
2427
+ }
2428
+ if (dir.length === 0) return null;
2429
+ return dir;
2430
+ }
2431
+ function inferDomain(node, options = {}) {
2432
+ if (node.metadata?.domain && typeof node.metadata.domain === "string" && node.metadata.domain.length > 0) {
2433
+ return node.metadata.domain;
2434
+ }
2435
+ const filePath = typeof node.path === "string" ? node.path : "";
2436
+ const blocklist = new Set(DEFAULT_BLOCKLIST);
2437
+ if (options.extraBlocklist) {
2438
+ for (const seg of options.extraBlocklist) {
2439
+ if (seg && seg.length > 0) blocklist.add(seg);
2440
+ }
2441
+ }
2442
+ if (filePath.length > 0) {
2443
+ const extraPatterns = options.extraPatterns ?? [];
2444
+ for (const pattern of extraPatterns) {
2445
+ const dir = matchPattern(filePath, pattern);
2446
+ if (dir !== null) {
2447
+ if (blocklist.has(dir)) return "unknown";
2448
+ return dir;
2449
+ }
2450
+ }
2451
+ for (const pattern of DEFAULT_PATTERNS) {
2452
+ const dir = matchPattern(filePath, pattern);
2453
+ if (dir !== null) {
2454
+ if (blocklist.has(dir)) return "unknown";
2455
+ return dir;
2456
+ }
2457
+ }
2458
+ const segments = filePath.split("/").filter((s) => s.length > 0);
2459
+ if (segments.length > 0) {
2460
+ const first = segments[0];
2461
+ if (!blocklist.has(first)) return first;
2462
+ }
2463
+ }
2464
+ const source = node.metadata?.source;
2465
+ if (source === "knowledge-linker" || source === "connector") {
2466
+ const connector = node.metadata?.connectorName;
2467
+ if (typeof connector === "string" && connector.length > 0) return connector;
2468
+ return "general";
2469
+ }
2470
+ return "unknown";
2471
+ }
2472
+
2266
2473
  // src/ingest/KnowledgePipelineRunner.ts
2267
- import * as fs9 from "fs/promises";
2268
- import * as path10 from "path";
2474
+ import * as fs11 from "fs/promises";
2475
+ import * as path12 from "path";
2269
2476
 
2270
2477
  // src/ingest/DiagramParser.ts
2271
- import * as fs5 from "fs/promises";
2272
- import * as path6 from "path";
2273
- function emptyMermaidResult(diagramType = "unknown") {
2478
+ import * as fs6 from "fs/promises";
2479
+ import * as path7 from "path";
2480
+
2481
+ // src/ingest/parsers/mermaid.ts
2482
+ function emptyResult2(diagramType = "unknown") {
2274
2483
  return {
2275
2484
  entities: [],
2276
2485
  relationships: [],
2277
2486
  metadata: { format: "mermaid", diagramType }
2278
2487
  };
2279
2488
  }
2489
+ function detectDiagramType(content) {
2490
+ for (const line of content.split("\n")) {
2491
+ const trimmed = line.trim();
2492
+ if (!trimmed) continue;
2493
+ if (/^(?:graph|flowchart)\b/i.test(trimmed)) return "flowchart";
2494
+ if (/^sequenceDiagram\b/.test(trimmed)) return "sequence";
2495
+ if (/^classDiagram\b/.test(trimmed)) return "class";
2496
+ if (/^erDiagram\b/.test(trimmed)) return "er";
2497
+ break;
2498
+ }
2499
+ return "unknown";
2500
+ }
2501
+ function isDecisionNode(content, nodeId) {
2502
+ const decisionRegex = new RegExp(`${nodeId}\\s*\\{`);
2503
+ return decisionRegex.test(content);
2504
+ }
2505
+ function buildFlowchartEntity(content, match) {
2506
+ const id = match[1] ?? "";
2507
+ const label = (match[2] ?? "").trim();
2508
+ if (!id) return null;
2509
+ const decision = isDecisionNode(content, id);
2510
+ const entity = {
2511
+ id,
2512
+ label,
2513
+ ...decision ? { type: "decision" } : {}
2514
+ };
2515
+ return { id, entity };
2516
+ }
2517
+ function extractFlowchartNodes(content) {
2518
+ const entities = /* @__PURE__ */ new Map();
2519
+ const nodeRegex = /([A-Za-z0-9_]+)\s*[[({]([^\])}]+)[\])}]/g;
2520
+ let match = nodeRegex.exec(content);
2521
+ while (match !== null) {
2522
+ const parsed = buildFlowchartEntity(content, match);
2523
+ if (parsed && !entities.has(parsed.id)) {
2524
+ entities.set(parsed.id, parsed.entity);
2525
+ }
2526
+ match = nodeRegex.exec(content);
2527
+ }
2528
+ return entities;
2529
+ }
2530
+ function parseLabeledEdgeMatch(match) {
2531
+ const from = match[1] ?? "";
2532
+ const label = (match[2] ?? "").trim();
2533
+ const to = match[3] ?? "";
2534
+ if (!from || !to) return null;
2535
+ return { key: `${from}->${to}:${label}`, rel: { from, to, label } };
2536
+ }
2537
+ function extractLabeledEdges(stripped, edgeKeys) {
2538
+ const relationships = [];
2539
+ const labeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\|([^|]+)\|\s*([A-Za-z0-9_]+)/g;
2540
+ let match = labeledEdgeRegex.exec(stripped);
2541
+ while (match !== null) {
2542
+ const parsed = parseLabeledEdgeMatch(match);
2543
+ if (parsed && !edgeKeys.has(parsed.key)) {
2544
+ edgeKeys.add(parsed.key);
2545
+ relationships.push(parsed.rel);
2546
+ }
2547
+ match = labeledEdgeRegex.exec(stripped);
2548
+ }
2549
+ return relationships;
2550
+ }
2551
+ function hasLabeledEdgeBetween(labeledEdges, from, to) {
2552
+ return labeledEdges.some((r) => r.from === from && r.to === to);
2553
+ }
2554
+ function extractUnlabeledEdges(stripped, edgeKeys, labeledEdges) {
2555
+ const relationships = [];
2556
+ const unlabeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\s+([A-Za-z0-9_]+)/g;
2557
+ let match = unlabeledEdgeRegex.exec(stripped);
2558
+ while (match !== null) {
2559
+ const from = match[1] ?? "";
2560
+ const to = match[2] ?? "";
2561
+ const key = `${from}->${to}`;
2562
+ if (from && to && !hasLabeledEdgeBetween(labeledEdges, from, to) && !edgeKeys.has(key)) {
2563
+ edgeKeys.add(key);
2564
+ relationships.push({ from, to });
2565
+ }
2566
+ match = unlabeledEdgeRegex.exec(stripped);
2567
+ }
2568
+ return relationships;
2569
+ }
2570
+ function parseFlowchart(content, diagramType) {
2571
+ const entities = extractFlowchartNodes(content);
2572
+ const stripped = content.replace(/[[({][^\])}]*[\])}]/g, "");
2573
+ const edgeKeys = /* @__PURE__ */ new Set();
2574
+ const labeledEdges = extractLabeledEdges(stripped, edgeKeys);
2575
+ const unlabeledEdges = extractUnlabeledEdges(stripped, edgeKeys, labeledEdges);
2576
+ return {
2577
+ entities: Array.from(entities.values()),
2578
+ relationships: [...labeledEdges, ...unlabeledEdges],
2579
+ metadata: { format: "mermaid", diagramType }
2580
+ };
2581
+ }
2582
+ function extractParticipants(content) {
2583
+ const entities = [];
2584
+ const seenParticipants = /* @__PURE__ */ new Set();
2585
+ const participantRegex = /participant\s+(\w+)(?:\s+as\s+(.+))?/g;
2586
+ let match = participantRegex.exec(content);
2587
+ while (match !== null) {
2588
+ const id = match[1] ?? "";
2589
+ const label = match[2]?.trim() || id;
2590
+ if (id && !seenParticipants.has(id)) {
2591
+ seenParticipants.add(id);
2592
+ entities.push({ id, label });
2593
+ }
2594
+ match = participantRegex.exec(content);
2595
+ }
2596
+ return entities;
2597
+ }
2598
+ function relationshipFromMatch(match) {
2599
+ const from = match[1] ?? "";
2600
+ const to = match[2] ?? "";
2601
+ const label = (match[3] ?? "").trim();
2602
+ return from && to ? { from, to, label } : null;
2603
+ }
2604
+ function collectMessageMatches(content, regex) {
2605
+ const results = [];
2606
+ let match = regex.exec(content);
2607
+ while (match !== null) {
2608
+ const rel = relationshipFromMatch(match);
2609
+ if (rel) results.push(rel);
2610
+ match = regex.exec(content);
2611
+ }
2612
+ return results;
2613
+ }
2614
+ function extractMessages(content) {
2615
+ const forward = collectMessageMatches(content, /(\w+)\s*->>?\+?\s*(\w+)\s*:\s*(.+)/g);
2616
+ const returns = collectMessageMatches(content, /(\w+)\s*-->>?-?\s*(\w+)\s*:\s*(.+)/g);
2617
+ return [...forward, ...returns];
2618
+ }
2619
+ function parseSequence(content, diagramType) {
2620
+ return {
2621
+ entities: extractParticipants(content),
2622
+ relationships: extractMessages(content),
2623
+ metadata: { format: "mermaid", diagramType }
2624
+ };
2625
+ }
2280
2626
  var MermaidParser = class {
2281
2627
  canParse(_content, ext) {
2282
2628
  return ext === ".mmd" || ext === ".mermaid";
@@ -2284,269 +2630,199 @@ var MermaidParser = class {
2284
2630
  parse(content, _filePath) {
2285
2631
  const trimmed = content.trim();
2286
2632
  if (!trimmed) {
2287
- return emptyMermaidResult();
2633
+ return emptyResult2();
2288
2634
  }
2289
- const diagramType = this.detectDiagramType(trimmed);
2635
+ const diagramType = detectDiagramType(trimmed);
2290
2636
  switch (diagramType) {
2291
2637
  case "flowchart":
2292
- return this.parseFlowchart(trimmed, diagramType);
2638
+ return parseFlowchart(trimmed, diagramType);
2293
2639
  case "sequence":
2294
- return this.parseSequence(trimmed, diagramType);
2640
+ return parseSequence(trimmed, diagramType);
2295
2641
  default:
2296
- return emptyMermaidResult(diagramType);
2642
+ return emptyResult2(diagramType);
2297
2643
  }
2298
2644
  }
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";
2645
+ };
2646
+
2647
+ // src/ingest/parsers/d2.ts
2648
+ function emptyResult3() {
2649
+ return {
2650
+ entities: [],
2651
+ relationships: [],
2652
+ metadata: { format: "d2", diagramType: "architecture" }
2653
+ };
2654
+ }
2655
+ function parseBlockShape(stripped) {
2656
+ const match = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+?)\s*\{$/);
2657
+ if (match) {
2658
+ const id = match[1];
2659
+ const label = match[2];
2660
+ if (id && label) return { id, label };
2661
+ }
2662
+ return null;
2663
+ }
2664
+ function parseConnection(stripped) {
2665
+ const match = stripped.match(/^([a-zA-Z0-9_.-]+)\s*->\s*([a-zA-Z0-9_.-]+)(?:\s*:\s*(.+?))?$/);
2666
+ if (match) {
2667
+ const from = match[1] ?? "";
2668
+ const to = match[2] ?? "";
2669
+ const label = match[3]?.trim();
2670
+ if (from && to) return { from, to, ...label ? { label } : {} };
2671
+ }
2672
+ return null;
2673
+ }
2674
+ function parseSimpleShape(stripped) {
2675
+ const match = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+)$/);
2676
+ if (match) {
2677
+ const id = match[1];
2678
+ const label = (match[2] ?? "").trim();
2679
+ if (id && label) return { id, label };
2680
+ }
2681
+ return null;
2682
+ }
2683
+ function handleBlockOpen(stripped, state) {
2684
+ if (state.braceDepth === 0) {
2685
+ const shape = parseBlockShape(stripped);
2686
+ if (shape) state.entities.set(shape.id, { id: shape.id, label: shape.label });
2311
2687
  }
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
- };
2688
+ state.braceDepth++;
2689
+ }
2690
+ function processTopLevelLine(stripped, state) {
2691
+ const conn = parseConnection(stripped);
2692
+ if (conn) {
2693
+ state.relationships.push(conn);
2694
+ return;
2370
2695
  }
2371
- isDecisionNode(content, nodeId) {
2372
- const decisionRegex = new RegExp(`${nodeId}\\s*\\{`);
2373
- return decisionRegex.test(content);
2696
+ const shape = parseSimpleShape(stripped);
2697
+ if (shape && !state.entities.has(shape.id)) {
2698
+ state.entities.set(shape.id, { id: shape.id, label: shape.label });
2374
2699
  }
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
- };
2700
+ }
2701
+ function processD2Line(stripped, state) {
2702
+ if (!stripped || stripped.startsWith("#")) return;
2703
+ if (stripped.endsWith("{")) {
2704
+ handleBlockOpen(stripped, state);
2705
+ return;
2418
2706
  }
2419
- };
2707
+ if (stripped === "}") {
2708
+ state.braceDepth = Math.max(0, state.braceDepth - 1);
2709
+ return;
2710
+ }
2711
+ if (state.braceDepth > 0) return;
2712
+ processTopLevelLine(stripped, state);
2713
+ }
2420
2714
  var D2Parser = class {
2421
2715
  canParse(_content, ext) {
2422
2716
  return ext === ".d2";
2423
2717
  }
2424
2718
  parse(content, _filePath) {
2425
2719
  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;
2720
+ if (!trimmed) return emptyResult3();
2721
+ const state = {
2722
+ entities: /* @__PURE__ */ new Map(),
2723
+ relationships: [],
2724
+ braceDepth: 0
2725
+ };
2436
2726
  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
- }
2727
+ processD2Line(line.trim(), state);
2478
2728
  }
2479
2729
  return {
2480
- entities: Array.from(entities.values()),
2481
- relationships,
2730
+ entities: Array.from(state.entities.values()),
2731
+ relationships: state.relationships,
2482
2732
  metadata: { format: "d2", diagramType: "architecture" }
2483
2733
  };
2484
2734
  }
2485
2735
  };
2736
+
2737
+ // src/ingest/parsers/plantuml.ts
2738
+ function emptyResult4() {
2739
+ return {
2740
+ entities: [],
2741
+ relationships: [],
2742
+ metadata: { format: "plantuml", diagramType: "unknown" }
2743
+ };
2744
+ }
2745
+ function detectDiagramType2(content) {
2746
+ if (/class\s+\w+/.test(content)) return "class";
2747
+ if (/\[.+\]/.test(content) || /component\s+/.test(content)) return "component";
2748
+ if (/participant\s+/.test(content) || /actor\s+/.test(content)) return "sequence";
2749
+ return "unknown";
2750
+ }
2751
+ function stripWrappers(content) {
2752
+ return content.replace(/@startuml\b.*\n?/, "").replace(/@enduml\b.*/, "").trim();
2753
+ }
2754
+ function extractClasses(body, entities) {
2755
+ const classRegex = /class\s+(\w+)/g;
2756
+ let match = classRegex.exec(body);
2757
+ while (match !== null) {
2758
+ const id = match[1] ?? "";
2759
+ if (id && !entities.has(id)) {
2760
+ entities.set(id, { id, label: id });
2761
+ }
2762
+ match = classRegex.exec(body);
2763
+ }
2764
+ }
2765
+ function extractComponents(body, entities) {
2766
+ const componentRegex = /\[([^\]]+)\]/g;
2767
+ let match = componentRegex.exec(body);
2768
+ while (match !== null) {
2769
+ const label = (match[1] ?? "").trim();
2770
+ if (label) {
2771
+ const id = label.replace(/\s+/g, "_");
2772
+ if (!entities.has(id)) {
2773
+ entities.set(id, { id, label });
2774
+ }
2775
+ }
2776
+ match = componentRegex.exec(body);
2777
+ }
2778
+ }
2779
+ function parseRelationshipMatch(match) {
2780
+ const from = match[1] ?? "";
2781
+ const to = match[2] ?? "";
2782
+ if (!from || !to) return null;
2783
+ const label = match[3]?.trim();
2784
+ return { from, to, ...label ? { label } : {} };
2785
+ }
2786
+ function collectRelationshipMatches(body, regex) {
2787
+ const results = [];
2788
+ let match = regex.exec(body);
2789
+ while (match !== null) {
2790
+ const rel = parseRelationshipMatch(match);
2791
+ if (rel) results.push(rel);
2792
+ match = regex.exec(body);
2793
+ }
2794
+ return results;
2795
+ }
2796
+ function extractRelationships(body) {
2797
+ return collectRelationshipMatches(
2798
+ body,
2799
+ /(\w+)\s*(?:-->|->|<--|<-|\.\.>|--)\s*(\w+)(?:\s*:\s*(.+))?/g
2800
+ );
2801
+ }
2486
2802
  var PlantUmlParser = class {
2487
2803
  canParse(_content, ext) {
2488
2804
  return ext === ".puml" || ext === ".plantuml";
2489
2805
  }
2490
2806
  parse(content, _filePath) {
2491
2807
  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);
2808
+ if (!trimmed) return emptyResult4();
2809
+ const diagramType = detectDiagramType2(trimmed);
2500
2810
  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
- }
2811
+ const body = stripWrappers(trimmed);
2812
+ extractClasses(body, entities);
2813
+ extractComponents(body, entities);
2814
+ const relationships = extractRelationships(body);
2536
2815
  return {
2537
2816
  entities: Array.from(entities.values()),
2538
2817
  relationships,
2539
2818
  metadata: { format: "plantuml", diagramType }
2540
2819
  };
2541
2820
  }
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
2821
  };
2822
+
2823
+ // src/ingest/DiagramParser.ts
2549
2824
  var DIAGRAM_EXTENSIONS = /* @__PURE__ */ new Set([".mmd", ".mermaid", ".d2", ".puml", ".plantuml"]);
2825
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".harness"]);
2550
2826
  var DiagramParser = class {
2551
2827
  constructor(store) {
2552
2828
  this.store = store;
@@ -2558,7 +2834,7 @@ var DiagramParser = class {
2558
2834
  new PlantUmlParser()
2559
2835
  ];
2560
2836
  parse(content, filePath) {
2561
- const ext = path6.extname(filePath).toLowerCase();
2837
+ const ext = path7.extname(filePath).toLowerCase();
2562
2838
  for (const parser of this.parsers) {
2563
2839
  if (parser.canParse(content, ext)) {
2564
2840
  return parser.parse(content, filePath);
@@ -2578,41 +2854,13 @@ var DiagramParser = class {
2578
2854
  const diagramFiles = await this.findDiagramFiles(projectDir);
2579
2855
  for (const filePath of diagramFiles) {
2580
2856
  try {
2581
- const content = await fs5.readFile(filePath, "utf-8");
2582
- const relPath = path6.relative(projectDir, filePath).replaceAll("\\", "/");
2857
+ const content = await fs6.readFile(filePath, "utf-8");
2858
+ const relPath = path7.relative(projectDir, filePath).replaceAll("\\", "/");
2583
2859
  const result = this.parse(content, filePath);
2584
2860
  if (result.entities.length === 0) continue;
2585
2861
  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
- }
2862
+ nodesAdded += this.addEntityNodes(result, relPath, pathHash);
2863
+ edgesAdded += this.addRelationshipEdges(result, pathHash);
2616
2864
  } catch (err) {
2617
2865
  errors.push(
2618
2866
  `Failed to parse ${filePath}: ${err instanceof Error ? err.message : String(err)}`
@@ -2628,25 +2876,64 @@ var DiagramParser = class {
2628
2876
  durationMs: Date.now() - start
2629
2877
  };
2630
2878
  }
2879
+ /** Map diagram entities to business_concept graph nodes. */
2880
+ addEntityNodes(result, relPath, pathHash) {
2881
+ let count = 0;
2882
+ for (const entity of result.entities) {
2883
+ const nodeId = `diagram:${pathHash}:${entity.id}`;
2884
+ this.store.addNode({
2885
+ id: nodeId,
2886
+ type: "business_concept",
2887
+ name: entity.label,
2888
+ path: relPath,
2889
+ metadata: {
2890
+ source: "diagram",
2891
+ format: result.metadata.format,
2892
+ diagramType: result.metadata.diagramType,
2893
+ confidence: 0.85,
2894
+ ...entity.type ? { entityType: entity.type } : {}
2895
+ }
2896
+ });
2897
+ count++;
2898
+ }
2899
+ return count;
2900
+ }
2901
+ /** Map diagram relationships to references graph edges. */
2902
+ addRelationshipEdges(result, pathHash) {
2903
+ let count = 0;
2904
+ for (const rel of result.relationships) {
2905
+ const fromId = `diagram:${pathHash}:${rel.from}`;
2906
+ const toId = `diagram:${pathHash}:${rel.to}`;
2907
+ this.store.addEdge({
2908
+ from: fromId,
2909
+ to: toId,
2910
+ type: "references",
2911
+ metadata: {
2912
+ ...rel.label ? { label: rel.label } : {}
2913
+ }
2914
+ });
2915
+ count++;
2916
+ }
2917
+ return count;
2918
+ }
2631
2919
  async findDiagramFiles(dir) {
2632
2920
  const files = [];
2633
- const SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".harness"]);
2634
2921
  const walk = async (currentDir) => {
2635
2922
  let entries;
2636
2923
  try {
2637
- entries = await fs5.readdir(currentDir, { withFileTypes: true });
2924
+ entries = await fs6.readdir(currentDir, { withFileTypes: true });
2638
2925
  } catch {
2639
2926
  return;
2640
2927
  }
2641
2928
  for (const entry of entries) {
2642
2929
  if (entry.isDirectory()) {
2643
- if (!SKIP_DIRS2.has(entry.name)) {
2644
- await walk(path6.join(currentDir, entry.name));
2930
+ if (!SKIP_DIRS.has(entry.name)) {
2931
+ await walk(path7.join(currentDir, entry.name));
2645
2932
  }
2646
2933
  } else if (entry.isFile()) {
2647
- const ext = path6.extname(entry.name).toLowerCase();
2934
+ const ext = path7.extname(entry.name).toLowerCase();
2648
2935
  if (DIAGRAM_EXTENSIONS.has(ext)) {
2649
- files.push(path6.join(currentDir, entry.name));
2936
+ files.push(path7.join(currentDir, entry.name));
2650
2937
  }
2651
2938
  }
2652
2939
  }
@@ -2657,8 +2944,8 @@ var DiagramParser = class {
2657
2944
  };
2658
2945
 
2659
2946
  // src/ingest/KnowledgeLinker.ts
2660
- import * as fs6 from "fs/promises";
2661
- import * as path7 from "path";
2947
+ import * as fs7 from "fs/promises";
2948
+ import * as path8 from "path";
2662
2949
  var HEURISTIC_PATTERNS = [
2663
2950
  {
2664
2951
  name: "business-rule-imperative",
@@ -2805,8 +3092,10 @@ var KnowledgeLinker = class {
2805
3092
  if (!duplicate) return false;
2806
3093
  const existingSources = duplicate.metadata.sources ?? [];
2807
3094
  if (!existingSources.includes(candidate.sourceNodeId)) {
2808
- existingSources.push(candidate.sourceNodeId);
2809
- duplicate.metadata.sources = existingSources;
3095
+ this.store.addNode({
3096
+ ...duplicate,
3097
+ metadata: { ...duplicate.metadata, sources: [...existingSources, candidate.sourceNodeId] }
3098
+ });
2810
3099
  }
2811
3100
  return true;
2812
3101
  }
@@ -2840,8 +3129,8 @@ var KnowledgeLinker = class {
2840
3129
  */
2841
3130
  async writeJsonl(candidates) {
2842
3131
  if (!this.outputDir) return;
2843
- await fs6.mkdir(this.outputDir, { recursive: true });
2844
- const filePath = path7.join(this.outputDir, "linker.jsonl");
3132
+ await fs7.mkdir(this.outputDir, { recursive: true });
3133
+ const filePath = path8.join(this.outputDir, "linker.jsonl");
2845
3134
  const lines = candidates.map(
2846
3135
  (c) => JSON.stringify({
2847
3136
  id: c.id,
@@ -2854,16 +3143,16 @@ var KnowledgeLinker = class {
2854
3143
  nodeType: c.nodeType
2855
3144
  })
2856
3145
  );
2857
- await fs6.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
3146
+ await fs7.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
2858
3147
  }
2859
3148
  /**
2860
3149
  * Write medium-confidence candidates to staged JSONL for human review.
2861
3150
  */
2862
3151
  async writeStagedJsonl(candidates) {
2863
3152
  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");
3153
+ const stagedDir = path8.join(this.outputDir, "staged");
3154
+ await fs7.mkdir(stagedDir, { recursive: true });
3155
+ const filePath = path8.join(stagedDir, "linker-staged.jsonl");
2867
3156
  const lines = candidates.map(
2868
3157
  (c) => JSON.stringify({
2869
3158
  id: c.id,
@@ -2876,7 +3165,7 @@ var KnowledgeLinker = class {
2876
3165
  nodeType: c.nodeType
2877
3166
  })
2878
3167
  );
2879
- await fs6.writeFile(filePath, lines.join("\n") + "\n");
3168
+ await fs7.writeFile(filePath, lines.join("\n") + "\n");
2880
3169
  }
2881
3170
  /**
2882
3171
  * Apply heuristic patterns to content and return candidate extractions.
@@ -2982,13 +3271,23 @@ var StructuralDriftDetector = class {
2982
3271
  };
2983
3272
 
2984
3273
  // src/ingest/KnowledgeStagingAggregator.ts
2985
- import * as fs7 from "fs/promises";
2986
- import * as path8 from "path";
3274
+ import * as fs8 from "fs/promises";
3275
+ import * as path9 from "path";
3276
+ var BUSINESS_NODE_TYPES = [
3277
+ "business_concept",
3278
+ "business_rule",
3279
+ "business_process",
3280
+ "business_term",
3281
+ "business_metric",
3282
+ "business_fact"
3283
+ ];
2987
3284
  var KnowledgeStagingAggregator = class {
2988
- constructor(projectDir) {
3285
+ constructor(projectDir, inferenceOptions = {}) {
2989
3286
  this.projectDir = projectDir;
3287
+ this.inferenceOptions = inferenceOptions;
2990
3288
  }
2991
3289
  projectDir;
3290
+ inferenceOptions;
2992
3291
  async aggregate(extractorResults, linkerResults, diagramResults) {
2993
3292
  const all = [...extractorResults, ...linkerResults, ...diagramResults];
2994
3293
  const byHash = /* @__PURE__ */ new Map();
@@ -3002,59 +3301,157 @@ var KnowledgeStagingAggregator = class {
3002
3301
  if (deduplicated.length === 0) {
3003
3302
  return { staged: 0 };
3004
3303
  }
3005
- const stagedDir = path8.join(this.projectDir, ".harness", "knowledge", "staged");
3006
- await fs7.mkdir(stagedDir, { recursive: true });
3304
+ const stagedDir = path9.join(this.projectDir, ".harness", "knowledge", "staged");
3305
+ await fs8.mkdir(stagedDir, { recursive: true });
3007
3306
  const jsonl = deduplicated.map((entry) => JSON.stringify(entry)).join("\n") + "\n";
3008
- await fs7.writeFile(path8.join(stagedDir, "pipeline-staged.jsonl"), jsonl, "utf-8");
3307
+ await fs8.writeFile(path9.join(stagedDir, "pipeline-staged.jsonl"), jsonl, "utf-8");
3009
3308
  return { staged: deduplicated.length };
3010
3309
  }
3011
- async generateGapReport(knowledgeDir) {
3012
- const domains = [];
3310
+ async extractDocName(filePath) {
3311
+ const raw = await fs8.readFile(filePath, "utf-8");
3312
+ const fmMatch = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
3313
+ const body = fmMatch ? fmMatch[1] : raw;
3314
+ const titleMatch = body.match(/^#\s+(.+)$/m);
3315
+ return titleMatch ? titleMatch[1].trim() : path9.basename(filePath, ".md");
3316
+ }
3317
+ async generateGapReport(knowledgeDir, store) {
3318
+ const domainDocNames = /* @__PURE__ */ new Map();
3319
+ const domainEntryCounts = /* @__PURE__ */ new Map();
3013
3320
  let totalEntries = 0;
3014
3321
  try {
3015
- const entries = await fs7.readdir(knowledgeDir, { withFileTypes: true });
3322
+ const entries = await fs8.readdir(knowledgeDir, { withFileTypes: true });
3016
3323
  const domainDirs = entries.filter((e) => e.isDirectory());
3017
3324
  for (const dir of domainDirs) {
3018
- const domainPath = path8.join(knowledgeDir, dir.name);
3019
- const files = await fs7.readdir(domainPath);
3325
+ const domainPath = path9.join(knowledgeDir, dir.name);
3326
+ const files = await fs8.readdir(domainPath);
3020
3327
  const mdFiles = files.filter((f) => f.endsWith(".md"));
3021
3328
  const entryCount = mdFiles.length;
3022
3329
  totalEntries += entryCount;
3023
- domains.push({ domain: dir.name, entryCount });
3330
+ domainEntryCounts.set(dir.name, entryCount);
3331
+ if (store) {
3332
+ const names = [];
3333
+ for (const file of mdFiles) {
3334
+ const name = await this.extractDocName(path9.join(domainPath, file));
3335
+ names.push(name.toLowerCase().trim());
3336
+ }
3337
+ domainDocNames.set(dir.name, names);
3338
+ }
3024
3339
  }
3025
3340
  } catch {
3026
3341
  }
3342
+ if (!store) {
3343
+ const domains2 = [];
3344
+ for (const [domain, entryCount] of domainEntryCounts) {
3345
+ domains2.push({ domain, entryCount, extractedCount: 0, gapCount: 0, gapEntries: [] });
3346
+ }
3347
+ return {
3348
+ domains: domains2,
3349
+ totalEntries,
3350
+ totalExtracted: 0,
3351
+ totalGaps: 0,
3352
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
3353
+ };
3354
+ }
3355
+ const extractedByDomain = /* @__PURE__ */ new Map();
3356
+ for (const nodeType of BUSINESS_NODE_TYPES) {
3357
+ const nodes = store.findNodes({ type: nodeType });
3358
+ for (const node of nodes) {
3359
+ const domain = inferDomain(node, this.inferenceOptions);
3360
+ const list = extractedByDomain.get(domain) ?? [];
3361
+ list.push(node);
3362
+ extractedByDomain.set(domain, list);
3363
+ }
3364
+ }
3365
+ for (const [domain, nodes] of extractedByDomain) {
3366
+ const seen = /* @__PURE__ */ new Set();
3367
+ const deduped = nodes.filter((n) => {
3368
+ const key = n.name.toLowerCase().trim();
3369
+ if (seen.has(key)) return false;
3370
+ seen.add(key);
3371
+ return true;
3372
+ });
3373
+ extractedByDomain.set(domain, deduped);
3374
+ }
3375
+ const allDomains = /* @__PURE__ */ new Set([...domainEntryCounts.keys(), ...extractedByDomain.keys()]);
3376
+ const domains = [];
3377
+ let totalExtracted = 0;
3378
+ let totalGaps = 0;
3379
+ for (const domain of allDomains) {
3380
+ const entryCount = domainEntryCounts.get(domain) ?? 0;
3381
+ const extractedNodes = extractedByDomain.get(domain) ?? [];
3382
+ const extractedCount = extractedNodes.length;
3383
+ totalExtracted += extractedCount;
3384
+ const docNames = domainDocNames.get(domain) ?? [];
3385
+ const gapEntries = [];
3386
+ for (const node of extractedNodes) {
3387
+ const normalizedName = node.name.toLowerCase().trim();
3388
+ if (!docNames.includes(normalizedName)) {
3389
+ gapEntries.push({
3390
+ nodeId: node.id,
3391
+ name: node.name,
3392
+ nodeType: node.type,
3393
+ source: node.metadata?.source ?? "unknown",
3394
+ hasContent: Boolean(node.content && node.content.trim().length >= 10)
3395
+ });
3396
+ }
3397
+ }
3398
+ totalGaps += gapEntries.length;
3399
+ domains.push({ domain, entryCount, extractedCount, gapCount: gapEntries.length, gapEntries });
3400
+ }
3027
3401
  return {
3028
3402
  domains,
3029
3403
  totalEntries,
3404
+ totalExtracted,
3405
+ totalGaps,
3030
3406
  generatedAt: (/* @__PURE__ */ new Date()).toISOString()
3031
3407
  };
3032
3408
  }
3033
3409
  async writeGapReport(report) {
3034
- const gapsDir = path8.join(this.projectDir, ".harness", "knowledge");
3035
- await fs7.mkdir(gapsDir, { recursive: true });
3410
+ const gapsDir = path9.join(this.projectDir, ".harness", "knowledge");
3411
+ await fs8.mkdir(gapsDir, { recursive: true });
3412
+ const hasDifferential = report.totalExtracted > 0;
3036
3413
  const lines = [
3037
3414
  "# Knowledge Gaps Report",
3038
3415
  "",
3039
3416
  `Generated: ${report.generatedAt}`,
3040
3417
  "",
3041
3418
  "## Coverage by Domain",
3042
- "",
3043
- "| Domain | Entries |",
3044
- "| ------ | ------- |"
3419
+ ""
3045
3420
  ];
3046
- for (const domain of report.domains) {
3047
- lines.push(`| ${domain.domain} | ${domain.entryCount} |`);
3421
+ if (hasDifferential) {
3422
+ lines.push(
3423
+ "| Domain | Documented | Extracted | Gaps |",
3424
+ "| ------ | ---------- | --------- | ---- |"
3425
+ );
3426
+ for (const domain of report.domains) {
3427
+ lines.push(
3428
+ `| ${domain.domain} | ${domain.entryCount} | ${domain.extractedCount} | ${domain.gapCount} |`
3429
+ );
3430
+ }
3431
+ lines.push(
3432
+ "",
3433
+ `## Summary`,
3434
+ "",
3435
+ `- **Total Documented:** ${report.totalEntries}`,
3436
+ `- **Total Extracted:** ${report.totalExtracted}`,
3437
+ `- **Total Gaps:** ${report.totalGaps}`,
3438
+ ""
3439
+ );
3440
+ } else {
3441
+ lines.push("| Domain | Entries |", "| ------ | ------- |");
3442
+ for (const domain of report.domains) {
3443
+ lines.push(`| ${domain.domain} | ${domain.entryCount} |`);
3444
+ }
3445
+ lines.push("", `## Total Entries: ${report.totalEntries}`, "");
3048
3446
  }
3049
- lines.push("", `## Total Entries: ${report.totalEntries}`, "");
3050
- await fs7.writeFile(path8.join(gapsDir, "gaps.md"), lines.join("\n"), "utf-8");
3447
+ await fs8.writeFile(path9.join(gapsDir, "gaps.md"), lines.join("\n"), "utf-8");
3051
3448
  }
3052
3449
  };
3053
3450
 
3054
3451
  // 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([
3452
+ import * as fs9 from "fs/promises";
3453
+ import * as path10 from "path";
3454
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
3058
3455
  "node_modules",
3059
3456
  "dist",
3060
3457
  "target",
@@ -3091,11 +3488,11 @@ var EXT_TO_LANGUAGE = {
3091
3488
  };
3092
3489
  var SKIP_EXTENSIONS2 = /* @__PURE__ */ new Set([".d.ts"]);
3093
3490
  function detectLanguage(filePath) {
3094
- const name = path9.basename(filePath);
3491
+ const name = path10.basename(filePath);
3095
3492
  for (const skip of SKIP_EXTENSIONS2) {
3096
3493
  if (name.endsWith(skip)) return void 0;
3097
3494
  }
3098
- const ext = path9.extname(filePath);
3495
+ const ext = path10.extname(filePath);
3099
3496
  return EXT_TO_LANGUAGE[ext];
3100
3497
  }
3101
3498
  var EXTRACTOR_EDGE_TYPE = {
@@ -3121,7 +3518,7 @@ var ExtractionRunner = class {
3121
3518
  let nodesAdded = 0;
3122
3519
  let nodesUpdated = 0;
3123
3520
  let edgesAdded = 0;
3124
- await fs8.mkdir(outputDir, { recursive: true });
3521
+ await fs9.mkdir(outputDir, { recursive: true });
3125
3522
  const files = await this.findSourceFiles(projectDir);
3126
3523
  const recordsByExtractor = /* @__PURE__ */ new Map();
3127
3524
  for (const ext of this.extractors) {
@@ -3130,17 +3527,17 @@ var ExtractionRunner = class {
3130
3527
  for (const filePath of files) {
3131
3528
  const language = detectLanguage(filePath);
3132
3529
  if (!language) continue;
3133
- const ext = path9.extname(filePath);
3530
+ const ext = path10.extname(filePath);
3134
3531
  let content;
3135
3532
  try {
3136
- content = await fs8.readFile(filePath, "utf-8");
3533
+ content = await fs9.readFile(filePath, "utf-8");
3137
3534
  } catch (err) {
3138
3535
  errors.push(
3139
3536
  `Failed to read ${filePath}: ${err instanceof Error ? err.message : String(err)}`
3140
3537
  );
3141
3538
  continue;
3142
3539
  }
3143
- const relativePath = path9.relative(projectDir, filePath).replaceAll("\\", "/");
3540
+ const relativePath = path10.relative(projectDir, filePath).replaceAll("\\", "/");
3144
3541
  for (const extractor2 of this.extractors) {
3145
3542
  if (!extractor2.supportedExtensions.includes(ext)) continue;
3146
3543
  try {
@@ -3177,9 +3574,9 @@ var ExtractionRunner = class {
3177
3574
  }
3178
3575
  /** Write extraction records to JSONL file. */
3179
3576
  async writeJsonl(records, outputDir, extractorName) {
3180
- const filePath = path9.join(outputDir, `${extractorName}.jsonl`);
3577
+ const filePath = path10.join(outputDir, `${extractorName}.jsonl`);
3181
3578
  const lines = records.map((r) => JSON.stringify(r));
3182
- await fs8.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
3579
+ await fs9.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
3183
3580
  }
3184
3581
  /** Create or update a graph node from an extraction record. */
3185
3582
  persistRecord(record, store) {
@@ -3251,13 +3648,13 @@ var ExtractionRunner = class {
3251
3648
  const results = [];
3252
3649
  let entries;
3253
3650
  try {
3254
- entries = await fs8.readdir(dir, { withFileTypes: true });
3651
+ entries = await fs9.readdir(dir, { withFileTypes: true });
3255
3652
  } catch {
3256
3653
  return results;
3257
3654
  }
3258
3655
  for (const entry of entries) {
3259
- const fullPath = path9.join(dir, entry.name);
3260
- if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
3656
+ const fullPath = path10.join(dir, entry.name);
3657
+ if (entry.isDirectory() && !SKIP_DIRS2.has(entry.name)) {
3261
3658
  results.push(...await this.findSourceFiles(fullPath));
3262
3659
  } else if (entry.isFile() && detectLanguage(fullPath) !== void 0) {
3263
3660
  results.push(fullPath);
@@ -4383,57 +4780,9 @@ var ImageAnalysisExtractor = class {
4383
4780
  const errors = [];
4384
4781
  for (const imagePath of imagePaths) {
4385
4782
  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
- }
4783
+ const counts = await this.processImage(store, imagePath);
4784
+ nodesAdded += counts.nodes;
4785
+ edgesAdded += counts.edges;
4437
4786
  } catch (err) {
4438
4787
  errors.push(
4439
4788
  `Image analysis failed for ${imagePath}: ${err instanceof Error ? err.message : String(err)}`
@@ -4449,6 +4798,68 @@ var ImageAnalysisExtractor = class {
4449
4798
  durationMs: Date.now() - start
4450
4799
  };
4451
4800
  }
4801
+ /** Analyze a single image and add annotation + concept nodes to the store. */
4802
+ async processImage(store, imagePath) {
4803
+ const response = await this.provider.analyze({
4804
+ prompt: "Analyze this image and provide a structured description of its visual contents.",
4805
+ systemPrompt: "You are an image analysis assistant. Describe the image contents, detect UI elements, extract visible text, identify design patterns, and note accessibility concerns.",
4806
+ responseSchema: {},
4807
+ // Schema handled by provider
4808
+ maxTokens: 1e3
4809
+ });
4810
+ const result = response.result;
4811
+ const annotationId = `img:${hash(imagePath)}`;
4812
+ let nodes = 0;
4813
+ let edges = 0;
4814
+ store.addNode({
4815
+ id: annotationId,
4816
+ type: "image_annotation",
4817
+ name: result.description.slice(0, 200),
4818
+ path: imagePath,
4819
+ content: result.description,
4820
+ hash: hash(result.description),
4821
+ metadata: {
4822
+ source: "image-analysis",
4823
+ detectedElements: result.detectedElements,
4824
+ extractedText: result.extractedText,
4825
+ designPatterns: result.designPatterns,
4826
+ accessibilityNotes: result.accessibilityNotes,
4827
+ model: response.model
4828
+ }
4829
+ });
4830
+ nodes++;
4831
+ edges += this.linkToFileNode(store, annotationId, imagePath);
4832
+ for (const pattern of result.designPatterns) {
4833
+ edges += this.addDesignPatternConcept(store, annotationId, imagePath, pattern);
4834
+ nodes++;
4835
+ }
4836
+ return { nodes, edges };
4837
+ }
4838
+ /** Link annotation to its source file node if it exists. Returns 1 if linked, 0 otherwise. */
4839
+ linkToFileNode(store, annotationId, imagePath) {
4840
+ const fileNode = store.findNodes({ type: "file" }).find((n) => n.path === imagePath);
4841
+ if (!fileNode) return 0;
4842
+ store.addEdge({ from: annotationId, to: fileNode.id, type: "annotates" });
4843
+ return 1;
4844
+ }
4845
+ /** Create a business_concept node for a design pattern and link it. Returns edges added. */
4846
+ addDesignPatternConcept(store, annotationId, imagePath, pattern) {
4847
+ const conceptId = `img:concept:${hash(pattern + imagePath)}`;
4848
+ store.addNode({
4849
+ id: conceptId,
4850
+ type: "business_concept",
4851
+ name: pattern,
4852
+ content: `Design pattern detected in ${imagePath}: ${pattern}`,
4853
+ hash: hash(pattern),
4854
+ metadata: {
4855
+ source: "image-analysis",
4856
+ sourceImage: imagePath,
4857
+ domain: "design"
4858
+ }
4859
+ });
4860
+ store.addEdge({ from: annotationId, to: conceptId, type: "contains" });
4861
+ return 1;
4862
+ }
4452
4863
  };
4453
4864
 
4454
4865
  // src/ingest/knowledgeTypes.ts
@@ -4459,6 +4870,7 @@ var KNOWLEDGE_NODE_TYPES = [
4459
4870
  "business_term",
4460
4871
  "business_concept",
4461
4872
  "business_metric",
4873
+ "decision",
4462
4874
  "design_token",
4463
4875
  "design_constraint",
4464
4876
  "aesthetic_intent",
@@ -4499,83 +4911,99 @@ function classifyConflict(a, b) {
4499
4911
  return "temporal_conflict";
4500
4912
  return "status_divergence";
4501
4913
  }
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
- }
4914
+ var SEVERITY_MAP = {
4915
+ value_mismatch: "critical",
4916
+ definition_conflict: "high",
4917
+ status_divergence: "medium",
4918
+ temporal_conflict: "medium"
4919
+ };
4920
+ function buildEntry(node, source) {
4921
+ return {
4922
+ nodeId: node.id,
4923
+ source,
4924
+ name: node.name,
4925
+ content: node.content ?? "",
4926
+ lastModified: node.lastModified
4927
+ };
4928
+ }
4929
+ function groupByName(nodes) {
4930
+ const byName = /* @__PURE__ */ new Map();
4931
+ for (const node of nodes) {
4932
+ const key = node.name.toLowerCase().trim();
4933
+ const group = byName.get(key) ?? [];
4934
+ group.push(node);
4935
+ byName.set(key, group);
4936
+ }
4937
+ return byName;
4938
+ }
4939
+ function tryAddContradiction(acc, a, b, similarity) {
4940
+ const sourceA = a.metadata.source ?? "unknown";
4941
+ const sourceB = b.metadata.source ?? "unknown";
4942
+ if (sourceA === sourceB) return;
4943
+ if ((a.hash ?? a.id) === (b.hash ?? b.id)) return;
4944
+ const pairId = [a.id, b.id].sort().join(":");
4945
+ if (acc.seen.has(pairId)) return;
4946
+ acc.seen.add(pairId);
4947
+ const conflictType = classifyConflict(a, b);
4948
+ const pairKey = [sourceA, sourceB].sort().join("\u2194");
4949
+ acc.sourcePairCounts[pairKey] = (acc.sourcePairCounts[pairKey] ?? 0) + 1;
4950
+ acc.contradictions.push({
4951
+ id: `contradiction:${a.id}:${b.id}`,
4952
+ entityA: buildEntry(a, sourceA),
4953
+ entityB: buildEntry(b, sourceB),
4954
+ similarity,
4955
+ conflictType,
4956
+ severity: SEVERITY_MAP[conflictType],
4957
+ description: `"${a.name}" has conflicting definitions from ${sourceA} and ${sourceB}`
4958
+ });
4959
+ }
4960
+ function collectExactMatches(byName, acc) {
4961
+ for (const [, group] of byName) {
4962
+ if (group.length < 2) continue;
4963
+ for (let i = 0; i < group.length; i++) {
4964
+ for (let j = i + 1; j < group.length; j++) {
4965
+ tryAddContradiction(acc, group[i], group[j], 1);
4557
4966
  }
4558
4967
  }
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
- }
4968
+ }
4969
+ }
4970
+ function collectFuzzyMatches(byName, acc) {
4971
+ const keys = [...byName.keys()];
4972
+ for (let i = 0; i < keys.length; i++) {
4973
+ for (let j = i + 1; j < keys.length; j++) {
4974
+ const keyA = keys[i];
4975
+ const keyB = keys[j];
4976
+ const maxLen = Math.max(keyA.length, keyB.length);
4977
+ const minLen = Math.min(keyA.length, keyB.length);
4978
+ if (minLen / maxLen < SIMILARITY_THRESHOLD) continue;
4979
+ const ratio = levenshteinRatio(keyA, keyB);
4980
+ if (ratio < SIMILARITY_THRESHOLD) continue;
4981
+ const groupA = byName.get(keyA);
4982
+ const groupB = byName.get(keyB);
4983
+ for (const a of groupA) {
4984
+ for (const b of groupB) {
4985
+ tryAddContradiction(acc, a, b, ratio);
4575
4986
  }
4576
4987
  }
4577
4988
  }
4578
- return { contradictions, sourcePairCounts, totalChecked: nodes.length };
4989
+ }
4990
+ }
4991
+ var ContradictionDetector = class {
4992
+ detect(store) {
4993
+ const nodes = KNOWLEDGE_NODE_TYPES.flatMap((t) => store.findNodes({ type: t }));
4994
+ const byName = groupByName(nodes);
4995
+ const acc = {
4996
+ contradictions: [],
4997
+ sourcePairCounts: {},
4998
+ seen: /* @__PURE__ */ new Set()
4999
+ };
5000
+ collectExactMatches(byName, acc);
5001
+ collectFuzzyMatches(byName, acc);
5002
+ return {
5003
+ contradictions: acc.contradictions,
5004
+ sourcePairCounts: acc.sourcePairCounts,
5005
+ totalChecked: nodes.length
5006
+ };
4579
5007
  }
4580
5008
  };
4581
5009
 
@@ -4606,64 +5034,80 @@ function toGrade(score) {
4606
5034
  if (score >= 20) return "D";
4607
5035
  return "F";
4608
5036
  }
5037
+ function groupByDomain(nodes, _fallback, options = {}) {
5038
+ const map = /* @__PURE__ */ new Map();
5039
+ for (const node of nodes) {
5040
+ const domain = inferDomain(node, options);
5041
+ const group = map.get(domain) ?? [];
5042
+ group.push(node);
5043
+ map.set(domain, group);
5044
+ }
5045
+ return map;
5046
+ }
5047
+ function countLinkedEntities(codeEntries, store) {
5048
+ const linkedIds = /* @__PURE__ */ new Set();
5049
+ for (const codeNode of codeEntries) {
5050
+ if (hasKnowledgeEdge(codeNode.id, store)) {
5051
+ linkedIds.add(codeNode.id);
5052
+ }
5053
+ }
5054
+ return linkedIds;
5055
+ }
5056
+ function hasKnowledgeEdge(nodeId, store) {
5057
+ for (const edgeType of KNOWLEDGE_EDGE_TYPES) {
5058
+ if (store.getEdges({ to: nodeId, type: edgeType }).length > 0) return true;
5059
+ }
5060
+ return false;
5061
+ }
5062
+ function computeSourceBreakdown(knEntries) {
5063
+ const breakdown = {};
5064
+ for (const kn of knEntries) {
5065
+ const src = kn.metadata.source ?? "unknown";
5066
+ breakdown[src] = (breakdown[src] ?? 0) + 1;
5067
+ }
5068
+ return breakdown;
5069
+ }
5070
+ function computeDomainScore(knowledgeEntries, codeEntities, linkedEntities, uniqueSources) {
5071
+ const codeCoverageComponent = codeEntities > 0 ? linkedEntities / codeEntities * 60 : 0;
5072
+ const knowledgeDepthComponent = Math.min(knowledgeEntries / 10, 1) * 20;
5073
+ const sourceDiversityComponent = Math.min(uniqueSources / 3, 1) * 20;
5074
+ return Math.round(codeCoverageComponent + knowledgeDepthComponent + sourceDiversityComponent);
5075
+ }
5076
+ function scoreDomain(domain, knEntries, codeEntries, store) {
5077
+ const linkedIds = countLinkedEntities(codeEntries, store);
5078
+ const sourceBreakdown = computeSourceBreakdown(knEntries);
5079
+ const codeEntities = codeEntries.length;
5080
+ const linkedEntities = linkedIds.size;
5081
+ const knowledgeEntries = knEntries.length;
5082
+ const uniqueSources = Object.keys(sourceBreakdown).length;
5083
+ const score = computeDomainScore(knowledgeEntries, codeEntities, linkedEntities, uniqueSources);
5084
+ return {
5085
+ domain,
5086
+ score,
5087
+ knowledgeEntries,
5088
+ codeEntities,
5089
+ linkedEntities,
5090
+ unlinkedEntities: codeEntities - linkedEntities,
5091
+ sourceBreakdown,
5092
+ grade: toGrade(score)
5093
+ };
5094
+ }
4609
5095
  var CoverageScorer = class {
5096
+ constructor(inferenceOptions = {}) {
5097
+ this.inferenceOptions = inferenceOptions;
5098
+ }
5099
+ inferenceOptions;
4610
5100
  score(store) {
4611
5101
  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
- }
5102
+ const domainMap = groupByDomain(knowledgeNodes, void 0, this.inferenceOptions);
4619
5103
  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
- }
5104
+ const codeDomains = groupByDomain(codeNodes, void 0, this.inferenceOptions);
4627
5105
  const allDomains = /* @__PURE__ */ new Set([...domainMap.keys(), ...codeDomains.keys()]);
4628
5106
  const domains = [];
4629
5107
  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
5108
+ domains.push(
5109
+ scoreDomain(domain, domainMap.get(domain) ?? [], codeDomains.get(domain) ?? [], store)
4656
5110
  );
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
5111
  }
4668
5112
  const overallScore = domains.length > 0 ? Math.round(domains.reduce((sum, d) => sum + d.score, 0) / domains.length) : 0;
4669
5113
  return {
@@ -4673,19 +5117,157 @@ var CoverageScorer = class {
4673
5117
  generatedAt: (/* @__PURE__ */ new Date()).toISOString()
4674
5118
  };
4675
5119
  }
4676
- domainFromPath(filePath) {
4677
- if (!filePath) return "unclassified";
4678
- const parts = filePath.split("/");
4679
- const pkgIdx = parts.indexOf("packages");
4680
- if (pkgIdx >= 0 && parts[pkgIdx + 1]) return parts[pkgIdx + 1];
4681
- const srcIdx = parts.indexOf("src");
4682
- if (srcIdx >= 0 && parts[srcIdx + 1]) return parts[srcIdx + 1];
4683
- return parts[0] ?? "unclassified";
5120
+ };
5121
+
5122
+ // src/ingest/KnowledgeDocMaterializer.ts
5123
+ import * as fs10 from "fs/promises";
5124
+ import * as path11 from "path";
5125
+ var VALID_BUSINESS_TYPES = /* @__PURE__ */ new Set([
5126
+ "business_rule",
5127
+ "business_process",
5128
+ "business_concept",
5129
+ "business_term",
5130
+ "business_metric"
5131
+ ]);
5132
+ var DEFAULT_MAX_DOCS = 50;
5133
+ var MAX_COLLISION_SUFFIX = 10;
5134
+ var KnowledgeDocMaterializer = class {
5135
+ constructor(store, inferenceOptions = {}) {
5136
+ this.store = store;
5137
+ this.inferenceOptions = inferenceOptions;
5138
+ }
5139
+ store;
5140
+ inferenceOptions;
5141
+ async materialize(gapEntries, options) {
5142
+ const maxDocs = options.maxDocs ?? DEFAULT_MAX_DOCS;
5143
+ const created = [];
5144
+ const skipped = [];
5145
+ const createdNames = /* @__PURE__ */ new Set();
5146
+ for (const entry of gapEntries) {
5147
+ const resolved = await this.resolveEntry(
5148
+ entry,
5149
+ created.length,
5150
+ maxDocs,
5151
+ createdNames,
5152
+ options
5153
+ );
5154
+ if ("reason" in resolved) {
5155
+ skipped.push(resolved);
5156
+ continue;
5157
+ }
5158
+ const { node, domain, domainDir, nameKey } = resolved;
5159
+ await fs10.mkdir(domainDir, { recursive: true });
5160
+ const basename10 = this.generateFilename(entry.name);
5161
+ const filename = await this.resolveCollision(domainDir, basename10);
5162
+ const content = this.formatDoc(node, domain);
5163
+ const filePath = ["docs", "knowledge", domain, filename].join("/");
5164
+ await fs10.writeFile(path11.join(options.projectDir, filePath), content, "utf-8");
5165
+ createdNames.add(nameKey);
5166
+ created.push({ filePath, nodeId: entry.nodeId, domain, name: entry.name });
5167
+ }
5168
+ return { created, skipped };
5169
+ }
5170
+ async resolveEntry(entry, createdCount, maxDocs, createdNames, options) {
5171
+ if (!entry.hasContent) {
5172
+ return { nodeId: entry.nodeId, name: entry.name, reason: "no_content" };
5173
+ }
5174
+ const node = this.store.getNode(entry.nodeId);
5175
+ if (!node || !node.content || node.content.trim().length < 10) {
5176
+ return { nodeId: entry.nodeId, name: entry.name, reason: "no_content" };
5177
+ }
5178
+ const domain = this.inferDomain(node);
5179
+ if (!domain || /[/\\]|\.\.|\0/.test(domain)) {
5180
+ return { nodeId: entry.nodeId, name: entry.name, reason: "no_domain" };
5181
+ }
5182
+ if (createdCount >= maxDocs) {
5183
+ return { nodeId: entry.nodeId, name: entry.name, reason: "cap_reached" };
5184
+ }
5185
+ const domainDir = path11.join(options.projectDir, "docs", "knowledge", domain);
5186
+ const nameKey = `${domain}:${entry.name.toLowerCase().trim()}`;
5187
+ if (!createdNames.has(nameKey) && await this.hasExistingDoc(domainDir, entry.name)) {
5188
+ return { nodeId: entry.nodeId, name: entry.name, reason: "already_documented" };
5189
+ }
5190
+ if (options.dryRun) {
5191
+ return { nodeId: entry.nodeId, name: entry.name, reason: "dry_run" };
5192
+ }
5193
+ return { node, domain, domainDir, nameKey };
5194
+ }
5195
+ inferDomain(node) {
5196
+ const result = inferDomain(node, this.inferenceOptions);
5197
+ return result === "unknown" ? null : result;
5198
+ }
5199
+ generateFilename(name) {
5200
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
5201
+ return `${slug}.md`;
5202
+ }
5203
+ async resolveCollision(dir, basename10) {
5204
+ const ext = path11.extname(basename10);
5205
+ const stem = path11.basename(basename10, ext);
5206
+ try {
5207
+ await fs10.access(path11.join(dir, basename10));
5208
+ } catch {
5209
+ return basename10;
5210
+ }
5211
+ for (let i = 2; i <= MAX_COLLISION_SUFFIX; i++) {
5212
+ const candidate = `${stem}-${i}${ext}`;
5213
+ try {
5214
+ await fs10.access(path11.join(dir, candidate));
5215
+ } catch {
5216
+ return candidate;
5217
+ }
5218
+ }
5219
+ throw new Error(
5220
+ `Cannot resolve filename collision for "${basename10}" after ${MAX_COLLISION_SUFFIX} attempts`
5221
+ );
5222
+ }
5223
+ formatDoc(node, domain) {
5224
+ const mappedType = this.mapNodeType(node);
5225
+ const sanitize = (s) => s.replace(/[\n\r]/g, " ").replace(/:/g, "-");
5226
+ const lines = ["---", `type: ${sanitize(mappedType)}`, `domain: ${sanitize(domain)}`];
5227
+ const tags = node.metadata?.tags;
5228
+ if (Array.isArray(tags) && tags.length > 0) {
5229
+ lines.push(`tags: [${tags.map((t) => sanitize(String(t))).join(", ")}]`);
5230
+ }
5231
+ const related = node.metadata?.related;
5232
+ if (Array.isArray(related) && related.length > 0) {
5233
+ lines.push(`related: [${related.map((r) => sanitize(String(r))).join(", ")}]`);
5234
+ }
5235
+ const title = (node.name ?? "").replace(/[\n\r]/g, " ");
5236
+ lines.push("---", "", `# ${title}`, "", node.content ?? "", "");
5237
+ return lines.join("\n");
5238
+ }
5239
+ /** Check if a doc with a matching title already exists in the domain directory. */
5240
+ async hasExistingDoc(domainDir, name) {
5241
+ const normalizedName = name.toLowerCase().trim();
5242
+ try {
5243
+ const files = await fs10.readdir(domainDir);
5244
+ for (const file of files) {
5245
+ if (!file.endsWith(".md")) continue;
5246
+ const raw = await fs10.readFile(path11.join(domainDir, file), "utf-8");
5247
+ const fmMatch = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
5248
+ const body = fmMatch ? fmMatch[1] : raw;
5249
+ const titleMatch = body.match(/^#\s+(.+)$/m);
5250
+ if (titleMatch && titleMatch[1].trim().toLowerCase() === normalizedName) {
5251
+ return true;
5252
+ }
5253
+ }
5254
+ } catch {
5255
+ }
5256
+ return false;
5257
+ }
5258
+ mapNodeType(node) {
5259
+ if (VALID_BUSINESS_TYPES.has(node.type)) {
5260
+ return node.type;
5261
+ }
5262
+ if (node.type === "business_fact") {
5263
+ return "business_rule";
5264
+ }
5265
+ return "business_concept";
4684
5266
  }
4685
5267
  };
4686
5268
 
4687
5269
  // src/ingest/KnowledgePipelineRunner.ts
4688
- var BUSINESS_NODE_TYPES = [
5270
+ var BUSINESS_NODE_TYPES2 = [
4689
5271
  "business_concept",
4690
5272
  "business_rule",
4691
5273
  "business_process",
@@ -4694,7 +5276,8 @@ var BUSINESS_NODE_TYPES = [
4694
5276
  "business_fact"
4695
5277
  ];
4696
5278
  var SNAPSHOT_NODE_TYPES = [
4697
- ...BUSINESS_NODE_TYPES,
5279
+ ...BUSINESS_NODE_TYPES2,
5280
+ "decision",
4698
5281
  "design_token",
4699
5282
  "design_constraint",
4700
5283
  "aesthetic_intent",
@@ -4705,52 +5288,102 @@ var KnowledgePipelineRunner = class {
4705
5288
  this.store = store;
4706
5289
  }
4707
5290
  store;
5291
+ /** Resolved per-`run()` inference options. Set on entry to `run()`. */
5292
+ inferenceOptions = {};
4708
5293
  async run(options) {
4709
- const maxIterations = options.maxIterations ?? 5;
5294
+ this.inferenceOptions = options.inferenceOptions ?? {};
4710
5295
  const remediations = [];
4711
5296
  const preSnapshot = this.buildSnapshot(options.domain);
4712
5297
  const extraction = await this.extract(options);
4713
5298
  const postSnapshot = this.buildSnapshot(options.domain);
4714
5299
  let driftResult = this.reconcile(preSnapshot, postSnapshot);
4715
- const contradictionDetector = new ContradictionDetector();
4716
- const contradictions = contradictionDetector.detect(this.store);
5300
+ const contradictions = new ContradictionDetector().detect(this.store);
4717
5301
  let gapReport = await this.detect(options);
4718
- const coverageScorer = new CoverageScorer();
4719
- const coverage = coverageScorer.score(this.store);
5302
+ const coverage = new CoverageScorer(this.inferenceOptions).score(this.store);
5303
+ let materialization;
4720
5304
  let iterations = 1;
4721
5305
  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
- }
5306
+ const loopResult = await this.runRemediationLoop(
5307
+ options,
5308
+ driftResult,
5309
+ gapReport,
5310
+ remediations
5311
+ );
5312
+ iterations = loopResult.iterations;
5313
+ materialization = loopResult.materialization;
5314
+ }
5315
+ if (options.fix && iterations > 1) {
5316
+ const finalSnapshot = this.buildSnapshot(options.domain);
5317
+ driftResult = this.reconcile(preSnapshot, finalSnapshot);
5318
+ gapReport = await this.detect(options);
4736
5319
  }
4737
5320
  await this.stageNewFindings(driftResult, options);
5321
+ return this.buildResult(
5322
+ driftResult,
5323
+ iterations,
5324
+ extraction,
5325
+ gapReport,
5326
+ remediations,
5327
+ contradictions,
5328
+ coverage,
5329
+ materialization
5330
+ );
5331
+ }
5332
+ /** Run the remediation convergence loop; returns iteration count and accumulated materialization. */
5333
+ async runRemediationLoop(options, driftResult, gapReport, remediations) {
5334
+ const maxIterations = options.maxIterations ?? 5;
5335
+ let iterations = 1;
5336
+ let currentDrift = driftResult;
5337
+ let currentGapReport = gapReport;
5338
+ let previousIssueCount = currentDrift.findings.length + currentGapReport.totalGaps;
5339
+ let accumulatedMaterialization;
5340
+ while (iterations < maxIterations) {
5341
+ if (currentDrift.findings.length === 0 && currentGapReport.totalGaps === 0) break;
5342
+ const matResult = await this.remediate(currentDrift, remediations, options, currentGapReport);
5343
+ if (matResult) {
5344
+ if (!accumulatedMaterialization) {
5345
+ accumulatedMaterialization = matResult;
5346
+ } else {
5347
+ accumulatedMaterialization = {
5348
+ created: [...accumulatedMaterialization.created, ...matResult.created],
5349
+ skipped: [...accumulatedMaterialization.skipped, ...matResult.skipped]
5350
+ };
5351
+ }
5352
+ }
5353
+ const preSnapshot = this.buildSnapshot(options.domain);
5354
+ await this.extract(options);
5355
+ const postSnapshot = this.buildSnapshot(options.domain);
5356
+ currentDrift = this.reconcile(preSnapshot, postSnapshot);
5357
+ currentGapReport = await this.detect(options);
5358
+ iterations++;
5359
+ const currentIssueCount = currentDrift.findings.length + currentGapReport.totalGaps;
5360
+ if (currentIssueCount >= previousIssueCount) break;
5361
+ previousIssueCount = currentIssueCount;
5362
+ }
5363
+ return {
5364
+ iterations,
5365
+ ...accumulatedMaterialization ? { materialization: accumulatedMaterialization } : {}
5366
+ };
5367
+ }
5368
+ /** Assemble the final pipeline result. */
5369
+ buildResult(driftResult, iterations, extraction, gaps, remediations, contradictions, coverage, materialization) {
4738
5370
  return {
4739
5371
  verdict: this.computeVerdict(driftResult),
4740
5372
  driftScore: driftResult.driftScore,
4741
5373
  iterations,
4742
5374
  findings: driftResult.summary,
4743
5375
  extraction,
4744
- gaps: gapReport,
5376
+ gaps,
4745
5377
  remediations,
4746
5378
  contradictions,
4747
- coverage
5379
+ coverage,
5380
+ ...materialization ? { materialization } : {}
4748
5381
  };
4749
5382
  }
4750
5383
  // ── Phase 1: EXTRACT ──────────────────────────────────────────────────────
4751
5384
  async extract(options) {
4752
- const extractedDir = path10.join(options.projectDir, ".harness", "knowledge", "extracted");
4753
- await fs9.mkdir(extractedDir, { recursive: true });
5385
+ const extractedDir = path12.join(options.projectDir, ".harness", "knowledge", "extracted");
5386
+ await fs11.mkdir(extractedDir, { recursive: true });
4754
5387
  const runner = createExtractionRunner();
4755
5388
  const extractionResult = await runner.run(options.projectDir, this.store, extractedDir);
4756
5389
  const diagramParser = new DiagramParser(this.store);
@@ -4764,7 +5397,7 @@ var KnowledgePipelineRunner = class {
4764
5397
  const imageResult = await imageExtractor.analyze(this.store, imagePaths);
4765
5398
  imageCount = imageResult.nodesAdded;
4766
5399
  }
4767
- const knowledgeDir = path10.join(options.projectDir, "docs", "knowledge");
5400
+ const knowledgeDir = path12.join(options.projectDir, "docs", "knowledge");
4768
5401
  const bkIngestor = new BusinessKnowledgeIngestor(this.store);
4769
5402
  let bkResult;
4770
5403
  try {
@@ -4779,6 +5412,21 @@ var KnowledgePipelineRunner = class {
4779
5412
  durationMs: 0
4780
5413
  };
4781
5414
  }
5415
+ const decisionsDir = path12.join(options.projectDir, "docs", "knowledge", "decisions");
5416
+ const decisionIngestor = new DecisionIngestor(this.store);
5417
+ let decisionResult;
5418
+ try {
5419
+ decisionResult = await decisionIngestor.ingest(decisionsDir);
5420
+ } catch {
5421
+ decisionResult = {
5422
+ nodesAdded: 0,
5423
+ nodesUpdated: 0,
5424
+ edgesAdded: 0,
5425
+ edgesUpdated: 0,
5426
+ errors: [],
5427
+ durationMs: 0
5428
+ };
5429
+ }
4782
5430
  const linker = new KnowledgeLinker(this.store, extractedDir);
4783
5431
  const linkResult = await linker.link();
4784
5432
  return {
@@ -4786,6 +5434,7 @@ var KnowledgePipelineRunner = class {
4786
5434
  diagrams: diagramResult.nodesAdded,
4787
5435
  linkerFacts: linkResult.factsCreated,
4788
5436
  businessKnowledge: bkResult.nodesAdded,
5437
+ decisions: decisionResult.nodesAdded,
4789
5438
  images: imageCount
4790
5439
  };
4791
5440
  }
@@ -4812,14 +5461,14 @@ var KnowledgePipelineRunner = class {
4812
5461
  }
4813
5462
  // ── Phase 3: DETECT ───────────────────────────────────────────────────────
4814
5463
  async detect(options) {
4815
- const knowledgeDir = path10.join(options.projectDir, "docs", "knowledge");
4816
- const aggregator = new KnowledgeStagingAggregator(options.projectDir);
4817
- const gapReport = await aggregator.generateGapReport(knowledgeDir);
5464
+ const knowledgeDir = path12.join(options.projectDir, "docs", "knowledge");
5465
+ const aggregator = new KnowledgeStagingAggregator(options.projectDir, this.inferenceOptions);
5466
+ const gapReport = await aggregator.generateGapReport(knowledgeDir, this.store);
4818
5467
  await aggregator.writeGapReport(gapReport);
4819
5468
  return gapReport;
4820
5469
  }
4821
5470
  // ── Phase 4: REMEDIATE ────────────────────────────────────────────────────
4822
- remediate(driftResult, remediations, options) {
5471
+ async remediate(driftResult, remediations, options, gapReport) {
4823
5472
  for (const finding of driftResult.findings) {
4824
5473
  switch (finding.classification) {
4825
5474
  case "stale":
@@ -4837,6 +5486,22 @@ var KnowledgePipelineRunner = class {
4837
5486
  break;
4838
5487
  }
4839
5488
  }
5489
+ if (!options.ci) {
5490
+ const allGapEntries = gapReport.domains.flatMap((d) => d.gapEntries);
5491
+ const materializable = allGapEntries.filter((e) => e.hasContent);
5492
+ if (materializable.length > 0) {
5493
+ const materializer = new KnowledgeDocMaterializer(this.store, this.inferenceOptions);
5494
+ const matResult = await materializer.materialize(materializable, {
5495
+ projectDir: options.projectDir,
5496
+ dryRun: false
5497
+ });
5498
+ for (const doc of matResult.created) {
5499
+ remediations.push(`created doc: ${doc.filePath}`);
5500
+ }
5501
+ return matResult;
5502
+ }
5503
+ }
5504
+ return void 0;
4840
5505
  }
4841
5506
  async stageNewFindings(driftResult, options) {
4842
5507
  const newFindings = driftResult.findings.filter((f) => f.classification === "new");
@@ -4851,7 +5516,7 @@ var KnowledgePipelineRunner = class {
4851
5516
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
4852
5517
  }));
4853
5518
  if (stagedEntries.length > 0) {
4854
- const aggregator = new KnowledgeStagingAggregator(options.projectDir);
5519
+ const aggregator = new KnowledgeStagingAggregator(options.projectDir, this.inferenceOptions);
4855
5520
  await aggregator.aggregate(stagedEntries, [], []);
4856
5521
  }
4857
5522
  }
@@ -4871,7 +5536,7 @@ var KnowledgePipelineRunner = class {
4871
5536
  };
4872
5537
 
4873
5538
  // src/ingest/connectors/ConnectorUtils.ts
4874
- var CODE_NODE_TYPES4 = ["file", "function", "class", "method", "interface", "variable"];
5539
+ var CODE_NODE_TYPES5 = ["file", "function", "class", "method", "interface", "variable"];
4875
5540
  var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
4876
5541
  function withRetry(client, options) {
4877
5542
  const maxRetries = options?.maxRetries ?? 3;
@@ -4936,7 +5601,7 @@ function sanitizeExternalText(text, maxLength = 2e3) {
4936
5601
  }
4937
5602
  function linkToCode(store, content, sourceNodeId, edgeType, options) {
4938
5603
  let edgesCreated = 0;
4939
- for (const type of CODE_NODE_TYPES4) {
5604
+ for (const type of CODE_NODE_TYPES5) {
4940
5605
  const nodes = store.findNodes({ type });
4941
5606
  for (const node of nodes) {
4942
5607
  if (node.name.length < 3) continue;
@@ -4956,12 +5621,12 @@ function linkToCode(store, content, sourceNodeId, edgeType, options) {
4956
5621
  }
4957
5622
 
4958
5623
  // src/ingest/connectors/SyncManager.ts
4959
- import * as fs10 from "fs/promises";
4960
- import * as path11 from "path";
5624
+ import * as fs12 from "fs/promises";
5625
+ import * as path13 from "path";
4961
5626
  var SyncManager = class {
4962
5627
  constructor(store, graphDir) {
4963
5628
  this.store = store;
4964
- this.metadataPath = path11.join(graphDir, "sync-metadata.json");
5629
+ this.metadataPath = path13.join(graphDir, "sync-metadata.json");
4965
5630
  }
4966
5631
  store;
4967
5632
  registrations = /* @__PURE__ */ new Map();
@@ -5026,15 +5691,15 @@ var SyncManager = class {
5026
5691
  }
5027
5692
  async loadMetadata() {
5028
5693
  try {
5029
- const raw = await fs10.readFile(this.metadataPath, "utf-8");
5694
+ const raw = await fs12.readFile(this.metadataPath, "utf-8");
5030
5695
  return JSON.parse(raw);
5031
5696
  } catch {
5032
5697
  return { connectors: {} };
5033
5698
  }
5034
5699
  }
5035
5700
  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");
5701
+ await fs12.mkdir(path13.dirname(this.metadataPath), { recursive: true });
5702
+ await fs12.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
5038
5703
  }
5039
5704
  };
5040
5705
 
@@ -5139,6 +5804,14 @@ var JiraConnector = class {
5139
5804
  start
5140
5805
  );
5141
5806
  }
5807
+ try {
5808
+ const parsed = new URL(baseUrl);
5809
+ if (parsed.protocol !== "https:" && parsed.hostname !== "localhost") {
5810
+ return buildIngestResult(0, 0, [`${baseUrlEnv} must use HTTPS`], start);
5811
+ }
5812
+ } catch {
5813
+ return buildIngestResult(0, 0, [`${baseUrlEnv} is not a valid URL`], start);
5814
+ }
5142
5815
  const jql = buildJql(config);
5143
5816
  const headers = { Authorization: `Basic ${apiKey}`, "Content-Type": "application/json" };
5144
5817
  try {
@@ -5428,6 +6101,14 @@ var ConfluenceConnector = class {
5428
6101
  }
5429
6102
  const baseUrlEnv = config.baseUrlEnv ?? "CONFLUENCE_BASE_URL";
5430
6103
  const baseUrl = process.env[baseUrlEnv] ?? "";
6104
+ try {
6105
+ const parsed = new URL(baseUrl);
6106
+ if (parsed.protocol !== "https:" && parsed.hostname !== "localhost") {
6107
+ return missingApiKeyResult(`${baseUrlEnv} must use HTTPS`, start);
6108
+ }
6109
+ } catch {
6110
+ return missingApiKeyResult(`${baseUrlEnv} is not a valid URL`, start);
6111
+ }
5431
6112
  const spaceKey = config.spaceKey ?? "";
5432
6113
  const counts = await this.fetchAllPagesHandled(
5433
6114
  store,
@@ -5521,7 +6202,7 @@ var ConfluenceConnector = class {
5521
6202
  };
5522
6203
 
5523
6204
  // src/ingest/connectors/CIConnector.ts
5524
- function emptyResult2(errors, start) {
6205
+ function emptyResult5(errors, start) {
5525
6206
  return {
5526
6207
  nodesAdded: 0,
5527
6208
  nodesUpdated: 0,
@@ -5589,7 +6270,7 @@ var CIConnector = class {
5589
6270
  const apiKeyEnv = config.apiKeyEnv ?? "GITHUB_TOKEN";
5590
6271
  const apiKey = process.env[apiKeyEnv];
5591
6272
  if (!apiKey) {
5592
- return emptyResult2(
6273
+ return emptyResult5(
5593
6274
  [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
5594
6275
  start
5595
6276
  );
@@ -5653,6 +6334,14 @@ function hasConstraintKeyword(text) {
5653
6334
  const lower = text.toLowerCase();
5654
6335
  return CONSTRAINT_KEYWORDS.some((kw) => lower.includes(kw));
5655
6336
  }
6337
+ function buildNodeMetadata(condensed, base) {
6338
+ const metadata = { ...base };
6339
+ if (condensed.method !== "passthrough") {
6340
+ metadata.condensed = condensed.method;
6341
+ metadata.originalLength = condensed.originalLength;
6342
+ }
6343
+ return metadata;
6344
+ }
5656
6345
  function buildIngestResult2(nodesAdded, edgesAdded, errors, start) {
5657
6346
  return {
5658
6347
  nodesAdded,
@@ -5663,6 +6352,24 @@ function buildIngestResult2(nodesAdded, edgesAdded, errors, start) {
5663
6352
  durationMs: Date.now() - start
5664
6353
  };
5665
6354
  }
6355
+ function validateFigmaConfig(config) {
6356
+ const apiKeyEnv = config.apiKeyEnv ?? "FIGMA_API_KEY";
6357
+ const apiKey = process.env[apiKeyEnv];
6358
+ if (!apiKey) return { error: `Missing API key: environment variable "${apiKeyEnv}" is not set` };
6359
+ const baseUrlEnv = config.baseUrlEnv ?? "FIGMA_BASE_URL";
6360
+ const baseUrl = process.env[baseUrlEnv] ?? "https://api.figma.com";
6361
+ try {
6362
+ const parsed = new URL(baseUrl);
6363
+ if (parsed.protocol !== "https:" || !parsed.hostname.endsWith("api.figma.com")) {
6364
+ return { error: `Invalid ${baseUrlEnv}: must be an HTTPS URL on api.figma.com` };
6365
+ }
6366
+ } catch {
6367
+ return { error: `Invalid ${baseUrlEnv}: not a valid URL` };
6368
+ }
6369
+ const fileIds = config.fileIds;
6370
+ if (!fileIds || fileIds.length === 0) return { error: "No fileIds provided in connector config" };
6371
+ return { apiKey, baseUrl, fileIds };
6372
+ }
5666
6373
  var FigmaConnector = class {
5667
6374
  name = "figma";
5668
6375
  source = "figma";
@@ -5672,35 +6379,9 @@ var FigmaConnector = class {
5672
6379
  }
5673
6380
  async ingest(store, config) {
5674
6381
  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
- }
6382
+ const validated = validateFigmaConfig(config);
6383
+ if ("error" in validated) return buildIngestResult2(0, 0, [validated.error], start);
6384
+ const { apiKey, baseUrl, fileIds } = validated;
5704
6385
  const headers = { "X-FIGMA-TOKEN": apiKey };
5705
6386
  const maxLen = config.maxContentLength ?? 4e3;
5706
6387
  let nodesAdded = 0;
@@ -5720,6 +6401,17 @@ var FigmaConnector = class {
5720
6401
  return buildIngestResult2(nodesAdded, edgesAdded, errors, start);
5721
6402
  }
5722
6403
  async processFile(store, baseUrl, fileId, headers, maxLen) {
6404
+ let nodesAdded = 0;
6405
+ let edgesAdded = 0;
6406
+ const styleCounts = await this.ingestStyles(store, baseUrl, fileId, headers, maxLen);
6407
+ nodesAdded += styleCounts.nodesAdded;
6408
+ edgesAdded += styleCounts.edgesAdded;
6409
+ const componentCounts = await this.ingestComponents(store, baseUrl, fileId, headers, maxLen);
6410
+ nodesAdded += componentCounts.nodesAdded;
6411
+ edgesAdded += componentCounts.edgesAdded;
6412
+ return { nodesAdded, edgesAdded };
6413
+ }
6414
+ async ingestStyles(store, baseUrl, fileId, headers, maxLen) {
5723
6415
  let nodesAdded = 0;
5724
6416
  let edgesAdded = 0;
5725
6417
  const stylesUrl = `${baseUrl}/v1/files/${fileId}/styles`;
@@ -5727,90 +6419,112 @@ var FigmaConnector = class {
5727
6419
  if (!stylesResponse.ok) throw new Error(`Styles request failed for file ${fileId}`);
5728
6420
  const stylesData = await stylesResponse.json();
5729
6421
  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");
6422
+ const counts = await this.processStyle(store, style, fileId, maxLen);
6423
+ nodesAdded += counts.nodesAdded;
6424
+ edgesAdded += counts.edgesAdded;
5754
6425
  }
6426
+ return { nodesAdded, edgesAdded };
6427
+ }
6428
+ async processStyle(store, style, fileId, maxLen) {
6429
+ const nodeId = `figma:token:${style.key}`;
6430
+ const condensed = await condenseContent(sanitizeExternalText(style.description || "", 2e3), {
6431
+ maxLength: maxLen
6432
+ });
6433
+ const metadata = buildNodeMetadata(condensed, {
6434
+ source: "figma",
6435
+ key: style.key,
6436
+ styleType: style.style_type,
6437
+ fileId
6438
+ });
6439
+ store.addNode({
6440
+ id: nodeId,
6441
+ type: "design_token",
6442
+ name: sanitizeExternalText(style.name, 500),
6443
+ content: condensed.content,
6444
+ metadata
6445
+ });
6446
+ const searchText = sanitizeExternalText([style.name, style.description].join(" "));
6447
+ const edgesAdded = linkToCode(store, searchText, nodeId, "references");
6448
+ return { nodesAdded: 1, edgesAdded };
6449
+ }
6450
+ async ingestComponents(store, baseUrl, fileId, headers, maxLen) {
6451
+ let nodesAdded = 0;
6452
+ let edgesAdded = 0;
5755
6453
  const componentsUrl = `${baseUrl}/v1/files/${fileId}/components`;
5756
6454
  const componentsResponse = await this.httpClient(componentsUrl, { headers });
5757
6455
  if (!componentsResponse.ok) throw new Error(`Components request failed for file ${fileId}`);
5758
6456
  const componentsData = await componentsResponse.json();
5759
6457
  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
- }
6458
+ const counts = await this.processComponent(store, component, fileId, maxLen);
6459
+ nodesAdded += counts.nodesAdded;
6460
+ edgesAdded += counts.edgesAdded;
5811
6461
  }
5812
6462
  return { nodesAdded, edgesAdded };
5813
6463
  }
6464
+ async processComponent(store, component, fileId, maxLen) {
6465
+ let nodesAdded = 0;
6466
+ let edgesAdded = 0;
6467
+ const description = component.description || "";
6468
+ if (description) {
6469
+ const counts = await this.addComponentIntent(store, component, description, fileId, maxLen);
6470
+ nodesAdded += counts.nodesAdded;
6471
+ edgesAdded += counts.edgesAdded;
6472
+ }
6473
+ if (description && hasConstraintKeyword(description)) {
6474
+ const counts = await this.addComponentConstraint(
6475
+ store,
6476
+ component,
6477
+ description,
6478
+ fileId,
6479
+ maxLen
6480
+ );
6481
+ nodesAdded += counts.nodesAdded;
6482
+ edgesAdded += counts.edgesAdded;
6483
+ }
6484
+ return { nodesAdded, edgesAdded };
6485
+ }
6486
+ async addComponentIntent(store, component, description, fileId, maxLen) {
6487
+ const intentId = `figma:intent:${component.key}`;
6488
+ const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
6489
+ maxLength: maxLen
6490
+ });
6491
+ const metadata = buildNodeMetadata(condensed, {
6492
+ source: "figma",
6493
+ key: component.key,
6494
+ fileId
6495
+ });
6496
+ store.addNode({
6497
+ id: intentId,
6498
+ type: "aesthetic_intent",
6499
+ name: sanitizeExternalText(component.name, 500),
6500
+ content: condensed.content,
6501
+ metadata
6502
+ });
6503
+ const searchText = sanitizeExternalText([component.name, description].join(" "));
6504
+ const edgesAdded = linkToCode(store, searchText, intentId, "references");
6505
+ return { nodesAdded: 1, edgesAdded };
6506
+ }
6507
+ async addComponentConstraint(store, component, description, fileId, maxLen) {
6508
+ const constraintId = `figma:constraint:${component.key}`;
6509
+ const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
6510
+ maxLength: maxLen
6511
+ });
6512
+ const metadata = buildNodeMetadata(condensed, {
6513
+ source: "figma",
6514
+ key: component.key,
6515
+ fileId
6516
+ });
6517
+ store.addNode({
6518
+ id: constraintId,
6519
+ type: "design_constraint",
6520
+ name: sanitizeExternalText(component.name, 500),
6521
+ content: condensed.content,
6522
+ metadata
6523
+ });
6524
+ const searchText = sanitizeExternalText([component.name, description].join(" "));
6525
+ const edgesAdded = linkToCode(store, searchText, constraintId, "references");
6526
+ return { nodesAdded: 1, edgesAdded };
6527
+ }
5814
6528
  };
5815
6529
 
5816
6530
  // src/ingest/connectors/MiroConnector.ts
@@ -5829,6 +6543,46 @@ function buildIngestResult3(nodesAdded, edgesAdded, errors, start) {
5829
6543
  durationMs: Date.now() - start
5830
6544
  };
5831
6545
  }
6546
+ function validateBaseUrl(baseUrl, baseUrlEnv) {
6547
+ try {
6548
+ const parsed = new URL(baseUrl);
6549
+ if (parsed.protocol !== "https:" || parsed.hostname !== "api.miro.com") {
6550
+ return `Invalid ${baseUrlEnv}: must be an HTTPS URL on api.miro.com`;
6551
+ }
6552
+ } catch {
6553
+ return `Invalid ${baseUrlEnv}: not a valid URL`;
6554
+ }
6555
+ return null;
6556
+ }
6557
+ function resolveConfig(config, start) {
6558
+ const apiKeyEnv = config.apiKeyEnv ?? "MIRO_API_KEY";
6559
+ const apiKey = process.env[apiKeyEnv];
6560
+ if (!apiKey) {
6561
+ return {
6562
+ ok: false,
6563
+ result: buildIngestResult3(
6564
+ 0,
6565
+ 0,
6566
+ [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
6567
+ start
6568
+ )
6569
+ };
6570
+ }
6571
+ const baseUrlEnv = config.baseUrlEnv ?? "MIRO_BASE_URL";
6572
+ const baseUrl = process.env[baseUrlEnv] ?? "https://api.miro.com";
6573
+ const urlError = validateBaseUrl(baseUrl, baseUrlEnv);
6574
+ if (urlError) {
6575
+ return { ok: false, result: buildIngestResult3(0, 0, [urlError], start) };
6576
+ }
6577
+ const boardIds = config.boardIds;
6578
+ if (!boardIds || boardIds.length === 0) {
6579
+ return {
6580
+ ok: false,
6581
+ result: buildIngestResult3(0, 0, ["No boardIds provided in config"], start)
6582
+ };
6583
+ }
6584
+ return { ok: true, apiKey, baseUrl, boardIds, headers: { Authorization: `Bearer ${apiKey}` } };
6585
+ }
5832
6586
  var MiroConnector = class {
5833
6587
  name = "miro";
5834
6588
  source = "miro";
@@ -5838,36 +6592,9 @@ var MiroConnector = class {
5838
6592
  }
5839
6593
  async ingest(store, config) {
5840
6594
  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}` };
6595
+ const resolved = resolveConfig(config, start);
6596
+ if (!resolved.ok) return resolved.result;
6597
+ const { baseUrl, boardIds, headers } = resolved;
5871
6598
  let totalNodesAdded = 0;
5872
6599
  let totalEdgesAdded = 0;
5873
6600
  const errors = [];
@@ -6071,7 +6798,7 @@ var FusionLayer = class {
6071
6798
  };
6072
6799
 
6073
6800
  // src/entropy/GraphEntropyAdapter.ts
6074
- var CODE_NODE_TYPES5 = ["file", "function", "class", "method", "interface", "variable"];
6801
+ var CODE_NODE_TYPES6 = ["file", "function", "class", "method", "interface", "variable"];
6075
6802
  var GraphEntropyAdapter = class {
6076
6803
  constructor(store) {
6077
6804
  this.store = store;
@@ -6151,7 +6878,7 @@ var GraphEntropyAdapter = class {
6151
6878
  }
6152
6879
  findEntryPoints() {
6153
6880
  const entryPoints = [];
6154
- for (const nodeType of CODE_NODE_TYPES5) {
6881
+ for (const nodeType of CODE_NODE_TYPES6) {
6155
6882
  const nodes = this.store.findNodes({ type: nodeType });
6156
6883
  for (const node of nodes) {
6157
6884
  const isIndexFile = nodeType === "file" && node.name === "index.ts";
@@ -6187,7 +6914,7 @@ var GraphEntropyAdapter = class {
6187
6914
  }
6188
6915
  collectUnreachableNodes(visited) {
6189
6916
  const unreachableNodes = [];
6190
- for (const nodeType of CODE_NODE_TYPES5) {
6917
+ for (const nodeType of CODE_NODE_TYPES6) {
6191
6918
  const nodes = this.store.findNodes({ type: nodeType });
6192
6919
  for (const node of nodes) {
6193
6920
  if (!visited.has(node.id)) {
@@ -7033,9 +7760,9 @@ var EntityExtractor = class {
7033
7760
  extractPaths(trimmed, add) {
7034
7761
  const consumed = /* @__PURE__ */ new Set();
7035
7762
  for (const match of trimmed.matchAll(FILE_PATH_RE)) {
7036
- const path13 = match[0];
7037
- add(path13);
7038
- consumed.add(path13);
7763
+ const path15 = match[0];
7764
+ add(path15);
7765
+ consumed.add(path15);
7039
7766
  }
7040
7767
  return consumed;
7041
7768
  }
@@ -7107,8 +7834,8 @@ var EntityResolver = class {
7107
7834
  if (isPathLike && node.path.includes(raw)) {
7108
7835
  return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
7109
7836
  }
7110
- const basename7 = node.path.split("/").pop() ?? "";
7111
- if (basename7.includes(raw)) {
7837
+ const basename10 = node.path.split("/").pop() ?? "";
7838
+ if (basename10.includes(raw)) {
7112
7839
  return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
7113
7840
  }
7114
7841
  if (raw.length >= 4 && node.path.includes(raw)) {
@@ -7187,13 +7914,13 @@ var ResponseFormatter = class {
7187
7914
  const context = Array.isArray(d?.context) ? d.context : [];
7188
7915
  const firstEntity = entities[0];
7189
7916
  const nodeType = firstEntity?.node.type ?? "node";
7190
- const path13 = firstEntity?.node.path ?? "unknown";
7917
+ const path15 = firstEntity?.node.path ?? "unknown";
7191
7918
  let neighborCount = 0;
7192
7919
  const firstContext = context[0];
7193
7920
  if (firstContext && Array.isArray(firstContext.nodes)) {
7194
7921
  neighborCount = firstContext.nodes.length;
7195
7922
  }
7196
- return `**${entityName}** is a ${nodeType} at \`${path13}\`. Connected to ${neighborCount} nodes.`;
7923
+ return `**${entityName}** is a ${nodeType} at \`${path15}\`. Connected to ${neighborCount} nodes.`;
7197
7924
  }
7198
7925
  formatAnomaly(data) {
7199
7926
  const d = data;
@@ -7576,7 +8303,7 @@ var PHASE_NODE_TYPES = {
7576
8303
  debug: ["failure", "learning", "function", "method"],
7577
8304
  plan: ["adr", "document", "module", "layer"]
7578
8305
  };
7579
- var CODE_NODE_TYPES6 = /* @__PURE__ */ new Set([
8306
+ var CODE_NODE_TYPES7 = /* @__PURE__ */ new Set([
7580
8307
  "file",
7581
8308
  "function",
7582
8309
  "class",
@@ -7795,7 +8522,7 @@ var Assembler = class {
7795
8522
  */
7796
8523
  checkCoverage() {
7797
8524
  const codeNodes = [];
7798
- for (const type of CODE_NODE_TYPES6) {
8525
+ for (const type of CODE_NODE_TYPES7) {
7799
8526
  codeNodes.push(...this.store.findNodes({ type }));
7800
8527
  }
7801
8528
  const documented = [];
@@ -7975,14 +8702,14 @@ var GraphConstraintAdapter = class {
7975
8702
  };
7976
8703
 
7977
8704
  // src/ingest/DesignIngestor.ts
7978
- import * as fs11 from "fs/promises";
7979
- import * as path12 from "path";
8705
+ import * as fs13 from "fs/promises";
8706
+ import * as path14 from "path";
7980
8707
  function isDTCGToken(obj) {
7981
8708
  return typeof obj === "object" && obj !== null && "$value" in obj && "$type" in obj;
7982
8709
  }
7983
8710
  async function readFileOrNull(filePath) {
7984
8711
  try {
7985
- return await fs11.readFile(filePath, "utf-8");
8712
+ return await fs13.readFile(filePath, "utf-8");
7986
8713
  } catch {
7987
8714
  return null;
7988
8715
  }
@@ -8128,8 +8855,8 @@ var DesignIngestor = class {
8128
8855
  async ingestAll(designDir) {
8129
8856
  const start = Date.now();
8130
8857
  const [tokensResult, intentResult] = await Promise.all([
8131
- this.ingestTokens(path12.join(designDir, "tokens.json")),
8132
- this.ingestDesignIntent(path12.join(designDir, "DESIGN.md"))
8858
+ this.ingestTokens(path14.join(designDir, "tokens.json")),
8859
+ this.ingestDesignIntent(path14.join(designDir, "DESIGN.md"))
8133
8860
  ]);
8134
8861
  const merged = mergeResults(tokensResult, intentResult);
8135
8862
  return { ...merged, durationMs: Date.now() - start };
@@ -8365,10 +9092,10 @@ var TaskIndependenceAnalyzer = class {
8365
9092
  includeTypes: ["file"]
8366
9093
  });
8367
9094
  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);
9095
+ const path15 = n.path ?? n.id.replace(/^file:/, "");
9096
+ if (!fileSet.has(path15)) {
9097
+ if (!result.has(path15)) {
9098
+ result.set(path15, file);
8372
9099
  }
8373
9100
  }
8374
9101
  }
@@ -8745,7 +9472,7 @@ var ConflictPredictor = class {
8745
9472
  };
8746
9473
 
8747
9474
  // src/index.ts
8748
- var VERSION = "0.4.3";
9475
+ var VERSION = "0.6.0";
8749
9476
  export {
8750
9477
  ALL_EXTRACTORS,
8751
9478
  ApiPathExtractor,
@@ -8762,6 +9489,9 @@ export {
8762
9489
  ContradictionDetector,
8763
9490
  CoverageScorer,
8764
9491
  D2Parser,
9492
+ DEFAULT_BLOCKLIST,
9493
+ DEFAULT_PATTERNS,
9494
+ DecisionIngestor,
8765
9495
  DesignConstraintAdapter,
8766
9496
  DesignIngestor,
8767
9497
  DiagramParser,
@@ -8786,6 +9516,7 @@ export {
8786
9516
  ImageAnalysisExtractor,
8787
9517
  IntentClassifier,
8788
9518
  JiraConnector,
9519
+ KnowledgeDocMaterializer,
8789
9520
  KnowledgeIngestor,
8790
9521
  KnowledgePipelineRunner,
8791
9522
  KnowledgeStagingAggregator,
@@ -8812,6 +9543,7 @@ export {
8812
9543
  createExtractionRunner,
8813
9544
  detectLanguage,
8814
9545
  groupNodesByImpact,
9546
+ inferDomain,
8815
9547
  linkToCode,
8816
9548
  loadGraph,
8817
9549
  normalizeIntent,