@harness-engineering/graph 0.7.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -47,6 +47,7 @@ __export(index_exports, {
47
47
  D2Parser: () => D2Parser,
48
48
  DEFAULT_BLOCKLIST: () => DEFAULT_BLOCKLIST,
49
49
  DEFAULT_PATTERNS: () => DEFAULT_PATTERNS,
50
+ DEFAULT_SKIP_DIRS: () => DEFAULT_SKIP_DIRS,
50
51
  DecisionIngestor: () => DecisionIngestor,
51
52
  DesignConstraintAdapter: () => DesignConstraintAdapter,
52
53
  DesignIngestor: () => DesignIngestor,
@@ -102,10 +103,13 @@ __export(index_exports, {
102
103
  inferDomain: () => inferDomain,
103
104
  linkToCode: () => linkToCode,
104
105
  loadGraph: () => loadGraph,
106
+ loadGraphMetadata: () => loadGraphMetadata,
105
107
  normalizeIntent: () => normalizeIntent,
106
108
  project: () => project,
107
109
  queryTraceability: () => queryTraceability,
108
- saveGraph: () => saveGraph
110
+ resolveSkipDirs: () => resolveSkipDirs,
111
+ saveGraph: () => saveGraph,
112
+ skipDirGlobs: () => skipDirGlobs
109
113
  });
110
114
  module.exports = __toCommonJS(index_exports);
111
115
 
@@ -202,7 +206,7 @@ var EDGE_TYPES = [
202
206
  "caches"
203
207
  ];
204
208
  var OBSERVABILITY_TYPES = /* @__PURE__ */ new Set(["span", "metric", "log"]);
205
- var CURRENT_SCHEMA_VERSION = 1;
209
+ var CURRENT_SCHEMA_VERSION = 2;
206
210
  var NODE_STABILITY = {
207
211
  File: "session",
208
212
  Function: "session",
@@ -241,38 +245,99 @@ var GraphEdgeSchema = import_zod.z.object({
241
245
  // src/store/Serializer.ts
242
246
  var import_promises = require("fs/promises");
243
247
  var import_node_fs = require("fs");
248
+ var import_node_readline = require("readline");
244
249
  var import_node_path = require("path");
245
- function streamGraphJson(filePath, nodes, edges) {
250
+ function streamGraphNdjson(filePath, nodes, edges) {
246
251
  return new Promise((resolve, reject) => {
247
252
  const stream = (0, import_node_fs.createWriteStream)(filePath, { encoding: "utf-8" });
248
253
  stream.on("error", reject);
249
- stream.write('{"nodes":[');
250
- for (let i = 0; i < nodes.length; i++) {
251
- if (i > 0) stream.write(",");
252
- stream.write(JSON.stringify(nodes[i]));
254
+ for (const node of nodes) {
255
+ stream.write(JSON.stringify({ kind: "node", ...node }));
256
+ stream.write("\n");
253
257
  }
254
- stream.write('],"edges":[');
255
- for (let i = 0; i < edges.length; i++) {
256
- if (i > 0) stream.write(",");
257
- stream.write(JSON.stringify(edges[i]));
258
+ for (const edge of edges) {
259
+ stream.write(JSON.stringify({ kind: "edge", ...edge }));
260
+ stream.write("\n");
258
261
  }
259
- stream.write("]}");
260
262
  stream.end(() => resolve());
261
263
  });
262
264
  }
265
+ function countNodesByType(nodes) {
266
+ const counts = {};
267
+ for (const node of nodes) {
268
+ counts[node.type] = (counts[node.type] ?? 0) + 1;
269
+ }
270
+ return counts;
271
+ }
263
272
  async function saveGraph(dirPath, nodes, edges) {
264
273
  await (0, import_promises.mkdir)(dirPath, { recursive: true });
265
274
  const metadata = {
266
275
  schemaVersion: CURRENT_SCHEMA_VERSION,
267
276
  lastScanTimestamp: (/* @__PURE__ */ new Date()).toISOString(),
268
277
  nodeCount: nodes.length,
269
- edgeCount: edges.length
278
+ edgeCount: edges.length,
279
+ nodesByType: countNodesByType(nodes)
270
280
  };
271
281
  await Promise.all([
272
- streamGraphJson((0, import_node_path.join)(dirPath, "graph.json"), nodes, edges),
282
+ streamGraphNdjson((0, import_node_path.join)(dirPath, "graph.json"), nodes, edges),
273
283
  (0, import_promises.writeFile)((0, import_node_path.join)(dirPath, "metadata.json"), JSON.stringify(metadata, null, 2), "utf-8")
274
284
  ]);
275
285
  }
286
+ async function loadGraphMetadata(dirPath) {
287
+ const metaPath = (0, import_node_path.join)(dirPath, "metadata.json");
288
+ try {
289
+ await (0, import_promises.access)(metaPath);
290
+ } catch {
291
+ return { status: "not_found" };
292
+ }
293
+ let metadata;
294
+ try {
295
+ const content = await (0, import_promises.readFile)(metaPath, "utf-8");
296
+ metadata = JSON.parse(content);
297
+ } catch {
298
+ return { status: "not_found" };
299
+ }
300
+ if (metadata.schemaVersion !== CURRENT_SCHEMA_VERSION) {
301
+ return {
302
+ status: "schema_mismatch",
303
+ found: metadata.schemaVersion,
304
+ expected: CURRENT_SCHEMA_VERSION
305
+ };
306
+ }
307
+ return { status: "loaded", metadata };
308
+ }
309
+ async function streamReadGraph(filePath) {
310
+ const stream = (0, import_node_fs.createReadStream)(filePath, { encoding: "utf-8" });
311
+ const rl = (0, import_node_readline.createInterface)({ input: stream, crlfDelay: Infinity });
312
+ const nodes = [];
313
+ const edges = [];
314
+ try {
315
+ for await (const rawLine of rl) {
316
+ const line = rawLine.trim();
317
+ if (line === "") continue;
318
+ const record = JSON.parse(line);
319
+ if (record.kind === "node") {
320
+ const { kind: _kind, ...rest } = record;
321
+ nodes.push(rest);
322
+ } else if (record.kind === "edge") {
323
+ const { kind: _kind, ...rest } = record;
324
+ edges.push(rest);
325
+ }
326
+ }
327
+ } catch (err) {
328
+ if (err instanceof RangeError && /invalid string length/i.test(err.message)) {
329
+ throw new Error(
330
+ `Graph contains a record larger than V8 can hold in a single string (~512 MB). This usually means a node or edge has a multi-hundred-MB content/embedding field. Inspect ${filePath} for the offending record.`,
331
+ { cause: err }
332
+ );
333
+ }
334
+ throw err;
335
+ } finally {
336
+ rl.close();
337
+ stream.close();
338
+ }
339
+ return { nodes, edges };
340
+ }
276
341
  async function loadGraph(dirPath) {
277
342
  const metaPath = (0, import_node_path.join)(dirPath, "metadata.json");
278
343
  const graphPath = (0, import_node_path.join)(dirPath, "graph.json");
@@ -291,8 +356,8 @@ async function loadGraph(dirPath) {
291
356
  expected: CURRENT_SCHEMA_VERSION
292
357
  };
293
358
  }
294
- const graphContent = await (0, import_promises.readFile)(graphPath, "utf-8");
295
- return { status: "loaded", graph: JSON.parse(graphContent) };
359
+ const graph = await streamReadGraph(graphPath);
360
+ return { status: "loaded", graph };
296
361
  }
297
362
 
298
363
  // src/store/GraphStore.ts
@@ -838,6 +903,87 @@ function groupNodesByImpact(nodes, excludeId) {
838
903
  // src/ingest/CodeIngestor.ts
839
904
  var fs = __toESM(require("fs/promises"));
840
905
  var path = __toESM(require("path"));
906
+ var import_minimatch = require("minimatch");
907
+
908
+ // src/ingest/skip-dirs.ts
909
+ var DEFAULT_SKIP_DIRS = /* @__PURE__ */ new Set([
910
+ // VCS
911
+ ".git",
912
+ ".hg",
913
+ ".svn",
914
+ // Package managers
915
+ "node_modules",
916
+ ".pnpm-store",
917
+ ".yarn",
918
+ "vendor",
919
+ // JS/TS build outputs
920
+ "dist",
921
+ "build",
922
+ "out",
923
+ "_build",
924
+ "bin",
925
+ "obj",
926
+ "target",
927
+ "deps",
928
+ // JS/TS framework / tooling caches
929
+ ".next",
930
+ ".nuxt",
931
+ ".svelte-kit",
932
+ ".turbo",
933
+ ".vite",
934
+ ".cache",
935
+ ".parcel-cache",
936
+ ".docusaurus",
937
+ ".wrangler",
938
+ ".astro",
939
+ ".remix",
940
+ "storybook-static",
941
+ // Test / coverage / reporter outputs
942
+ "coverage",
943
+ ".nyc_output",
944
+ ".pytest_cache",
945
+ "playwright-report",
946
+ "test-results",
947
+ ".e2e",
948
+ // Python
949
+ "__pycache__",
950
+ ".venv",
951
+ "venv",
952
+ ".tox",
953
+ ".mypy_cache",
954
+ ".ruff_cache",
955
+ // JVM
956
+ ".gradle",
957
+ ".gradle-home",
958
+ // IDE / editor
959
+ ".idea",
960
+ ".vscode",
961
+ ".vs",
962
+ // Harness self
963
+ ".harness",
964
+ // AI agent sandboxes (these often contain full repo clones — skipping them
965
+ // prevents the walker from re-ingesting nested copies of the project)
966
+ ".claude",
967
+ ".cursor",
968
+ ".codex",
969
+ ".gemini",
970
+ ".aider",
971
+ ".agents",
972
+ ".agentastic",
973
+ ".playwright-mcp"
974
+ ]);
975
+ function resolveSkipDirs(options) {
976
+ const base = options?.skipDirs ? new Set(options.skipDirs) : new Set(DEFAULT_SKIP_DIRS);
977
+ if (options?.additionalSkipDirs) {
978
+ for (const name of options.additionalSkipDirs) base.add(name);
979
+ }
980
+ return base;
981
+ }
982
+ function skipDirGlobs(skipDirs = DEFAULT_SKIP_DIRS) {
983
+ return Array.from(skipDirs, (name) => `**/${name}/**`);
984
+ }
985
+
986
+ // src/ingest/CodeIngestor.ts
841
987
  var SKIP_METHOD_NAMES = /* @__PURE__ */ new Set(["constructor", "if", "for", "while", "switch"]);
842
988
  var FUNCTION_PATTERNS = {
843
989
  python: /^def\s+(\w+)\s*\(/,
@@ -907,11 +1053,55 @@ function isSupportedSourceFile(name) {
907
1053
  const ext = name.slice(name.lastIndexOf("."));
908
1054
  return SUPPORTED_EXTENSIONS.has(ext);
909
1055
  }
1056
+ function isExcluded(relPath, patterns) {
1057
+ if (patterns.length === 0) return false;
1058
+ for (const pattern of patterns) {
1059
+ if ((0, import_minimatch.minimatch)(relPath, pattern, { dot: true, matchBase: false })) return true;
1060
+ if (!pattern.includes("/") && (0, import_minimatch.minimatch)(relPath, pattern, { dot: true, matchBase: true })) {
1061
+ return true;
1062
+ }
1063
+ }
1064
+ return false;
1065
+ }
1066
+ async function readGitignorePatterns(rootDir) {
1067
+ let content;
1068
+ try {
1069
+ content = await fs.readFile(path.join(rootDir, ".gitignore"), "utf-8");
1070
+ } catch {
1071
+ return [];
1072
+ }
1073
+ const patterns = [];
1074
+ for (const rawLine of content.split(/\r?\n/)) {
1075
+ const line = rawLine.trim();
1076
+ if (line === "" || line.startsWith("#")) continue;
1077
+ if (line.startsWith("!")) continue;
1078
+ const rooted = line.startsWith("/");
1079
+ const stripped = rooted ? line.slice(1) : line;
1080
+ const isDirOnly = stripped.endsWith("/");
1081
+ const core = isDirOnly ? stripped.slice(0, -1) : stripped;
1082
+ if (core === "") continue;
1083
+ if (rooted) {
1084
+ patterns.push(isDirOnly ? `${core}/**` : core);
1085
+ if (isDirOnly) patterns.push(`${core}/`);
1086
+ } else {
1087
+ patterns.push(`**/${core}`);
1088
+ patterns.push(`**/${core}/**`);
1089
+ if (isDirOnly) patterns.push(`**/${core}/`);
1090
+ }
1091
+ }
1092
+ return patterns;
1093
+ }
910
1094
  var CodeIngestor = class {
911
- constructor(store) {
1095
+ constructor(store, options = {}) {
912
1096
  this.store = store;
1097
+ this.skipDirs = resolveSkipDirs(options);
1098
+ this.excludePatterns = options.excludePatterns ?? [];
1099
+ this.respectGitignore = options.respectGitignore ?? true;
913
1100
  }
914
1101
  store;
1102
+ skipDirs;
1103
+ excludePatterns;
1104
+ respectGitignore;
915
1105
  async ingest(rootDir) {
916
1106
  const start = Date.now();
917
1107
  const errors = [];
@@ -1001,19 +1191,55 @@ var CodeIngestor = class {
1001
1191
  }
1002
1192
  files.add(relativePath);
1003
1193
  }
1004
- async findSourceFiles(dir) {
1194
+ /**
1195
+ * Walk `rootDir` iteratively (BFS via an explicit queue) and return absolute paths
1196
+ * of every supported source file that is not excluded by the configured filters.
1197
+ *
1198
+ * Iterative on purpose: a recursive walker burns one stack frame per nested
1199
+ * directory and crashes with `Maximum call stack size exceeded` on deeply nested
1200
+ * monorepos (issue #274). The queue-based form keeps memory bounded by the
1201
+ * frontier size rather than the deepest path.
1202
+ */
1203
+ async findSourceFiles(rootDir) {
1005
1204
  const results = [];
1006
- const entries = await fs.readdir(dir, { withFileTypes: true });
1007
- for (const entry of entries) {
1008
- const fullPath = path.join(dir, entry.name);
1009
- if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "dist" && entry.name !== "target" && entry.name !== "build" && entry.name !== ".git" && entry.name !== "__pycache__" && entry.name !== ".venv" && entry.name !== ".turbo" && entry.name !== ".harness" && entry.name !== ".next" && entry.name !== ".vscode" && entry.name !== ".idea" && entry.name !== "vendor" && entry.name !== "out" && entry.name !== ".gradle" && entry.name !== ".gradle-home" && entry.name !== "bin" && entry.name !== "obj" && entry.name !== "venv" && entry.name !== "_build" && entry.name !== "deps" && entry.name !== "coverage" && entry.name !== ".nyc_output") {
1010
- results.push(...await this.findSourceFiles(fullPath));
1011
- } else if (entry.isFile() && isSupportedSourceFile(entry.name)) {
1012
- results.push(fullPath);
1205
+ const matchers = await this.buildExcludeMatchers(rootDir);
1206
+ const queue = [rootDir];
1207
+ while (queue.length > 0) {
1208
+ const dir = queue.pop();
1209
+ let entries;
1210
+ try {
1211
+ entries = await fs.readdir(dir, { withFileTypes: true });
1212
+ } catch {
1213
+ continue;
1214
+ }
1215
+ for (const entry of entries) {
1216
+ const fullPath = path.join(dir, entry.name);
1217
+ const relPath = path.relative(rootDir, fullPath).replaceAll("\\", "/");
1218
+ if (entry.isDirectory()) {
1219
+ if (this.skipDirs.has(entry.name)) continue;
1220
+ if (isExcluded(`${relPath}/`, matchers)) continue;
1221
+ queue.push(fullPath);
1222
+ } else if (entry.isFile()) {
1223
+ if (!isSupportedSourceFile(entry.name)) continue;
1224
+ if (isExcluded(relPath, matchers)) continue;
1225
+ results.push(fullPath);
1226
+ }
1013
1227
  }
1014
1228
  }
1015
1229
  return results;
1016
1230
  }
1231
+ /**
1232
+ * Compose the exclude matchers from `excludePatterns` and (optionally) `.gitignore`.
1233
+ * Returns a list of minimatch-compatible patterns; an empty list means "no excludes".
1234
+ */
1235
+ async buildExcludeMatchers(rootDir) {
1236
+ const patterns = [...this.excludePatterns];
1237
+ if (this.respectGitignore) {
1238
+ const gitignorePatterns = await readGitignorePatterns(rootDir);
1239
+ patterns.push(...gitignorePatterns);
1240
+ }
1241
+ return patterns;
1242
+ }
1017
1243
  extractSymbols(content, fileId, relativePath) {
1018
1244
  const results = [];
1019
1245
  const lines = content.split("\n");
@@ -1783,6 +2009,7 @@ function emptyResult(durationMs = 0) {
1783
2009
 
1784
2010
  // src/ingest/KnowledgeIngestor.ts
1785
2011
  var CODE_NODE_TYPES = ["file", "function", "class", "method", "interface", "variable"];
2012
+ var DOCS_OWNED_BY_OTHER_INGESTORS = /* @__PURE__ */ new Set(["adr", "knowledge", "changes", "solutions"]);
1786
2013
  var KnowledgeIngestor = class {
1787
2014
  constructor(store) {
1788
2015
  this.store = store;
@@ -1854,15 +2081,53 @@ var KnowledgeIngestor = class {
1854
2081
  }
1855
2082
  return buildResult(nodesAdded, edgesAdded, [], start);
1856
2083
  }
2084
+ async ingestGeneralDocs(projectPath) {
2085
+ const start = Date.now();
2086
+ const errors = [];
2087
+ let nodesAdded = 0;
2088
+ let edgesAdded = 0;
2089
+ const files = /* @__PURE__ */ new Set();
2090
+ try {
2091
+ const entries = await fs2.readdir(projectPath, { withFileTypes: true });
2092
+ for (const entry of entries) {
2093
+ if (entry.isFile() && entry.name.endsWith(".md")) {
2094
+ files.add(path3.join(projectPath, entry.name));
2095
+ }
2096
+ }
2097
+ } catch {
2098
+ return emptyResult(Date.now() - start);
2099
+ }
2100
+ const docsRoot = path3.join(projectPath, "docs");
2101
+ try {
2102
+ const scanned = await this.scanDocsDir(docsRoot);
2103
+ for (const f of scanned) files.add(f);
2104
+ } catch {
2105
+ }
2106
+ for (const filePath of files) {
2107
+ try {
2108
+ const content = await fs2.readFile(filePath, "utf-8");
2109
+ const relPath = path3.relative(projectPath, filePath).replaceAll("\\", "/");
2110
+ const nodeId = `doc:${relPath}`;
2111
+ if (this.store.getNode(nodeId)) continue;
2112
+ this.store.addNode(parseDocumentNode(nodeId, filePath, relPath, content));
2113
+ nodesAdded++;
2114
+ edgesAdded += this.linkToCode(content, nodeId, "documents");
2115
+ } catch (err) {
2116
+ errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
2117
+ }
2118
+ }
2119
+ return buildResult(nodesAdded, edgesAdded, errors, start);
2120
+ }
1857
2121
  async ingestAll(projectPath, opts) {
1858
2122
  const start = Date.now();
1859
2123
  const adrDir = opts?.adrDir ?? path3.join(projectPath, "docs", "adr");
1860
- const [adrResult, learningsResult, failuresResult] = await Promise.all([
2124
+ const [adrResult, learningsResult, failuresResult, docsResult] = await Promise.all([
1861
2125
  this.ingestADRs(adrDir),
1862
2126
  this.ingestLearnings(projectPath),
1863
- this.ingestFailures(projectPath)
2127
+ this.ingestFailures(projectPath),
2128
+ this.ingestGeneralDocs(projectPath)
1864
2129
  ]);
1865
- const merged = mergeResults(adrResult, learningsResult, failuresResult);
2130
+ const merged = mergeResults(adrResult, learningsResult, failuresResult, docsResult);
1866
2131
  return { ...merged, durationMs: Date.now() - start };
1867
2132
  }
1868
2133
  linkToCode(content, sourceNodeId, edgeType) {
@@ -1897,7 +2162,22 @@ var KnowledgeIngestor = class {
1897
2162
  const entries = await fs2.readdir(dir, { withFileTypes: true });
1898
2163
  for (const entry of entries) {
1899
2164
  const fullPath = path3.join(dir, entry.name);
1900
- if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "dist" && entry.name !== "target" && entry.name !== "build" && entry.name !== ".git" && entry.name !== ".gradle" && entry.name !== ".harness" && entry.name !== "vendor" && entry.name !== "bin" && entry.name !== "obj" && entry.name !== "venv" && entry.name !== "_build" && entry.name !== "deps" && entry.name !== "coverage") {
2165
+ if (entry.isDirectory() && !DEFAULT_SKIP_DIRS.has(entry.name)) {
2166
+ results.push(...await this.findMarkdownFiles(fullPath));
2167
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
2168
+ results.push(fullPath);
2169
+ }
2170
+ }
2171
+ return results;
2172
+ }
2173
+ async scanDocsDir(docsRoot) {
2174
+ const results = [];
2175
+ const rootEntries = await fs2.readdir(docsRoot, { withFileTypes: true });
2176
+ for (const entry of rootEntries) {
2177
+ const fullPath = path3.join(docsRoot, entry.name);
2178
+ if (entry.isDirectory()) {
2179
+ if (DEFAULT_SKIP_DIRS.has(entry.name)) continue;
2180
+ if (DOCS_OWNED_BY_OTHER_INGESTORS.has(entry.name)) continue;
1901
2181
  results.push(...await this.findMarkdownFiles(fullPath));
1902
2182
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
1903
2183
  results.push(fullPath);
@@ -1923,6 +2203,17 @@ function buildResult(nodesAdded, edgesAdded, errors, start) {
1923
2203
  durationMs: Date.now() - start
1924
2204
  };
1925
2205
  }
2206
+ function parseDocumentNode(nodeId, filePath, relPath, content) {
2207
+ const titleMatch = content.match(/^#\s+(.+)$/m);
2208
+ const title = titleMatch ? titleMatch[1].trim() : path3.basename(filePath, ".md");
2209
+ return {
2210
+ id: nodeId,
2211
+ type: "document",
2212
+ name: title,
2213
+ path: filePath,
2214
+ metadata: { relPath }
2215
+ };
2216
+ }
1926
2217
  function parseADRNode(nodeId, filePath, filename, content) {
1927
2218
  const titleMatch = content.match(/^#\s+(.+)$/m);
1928
2219
  const title = titleMatch ? titleMatch[1].trim() : filename;
@@ -1978,6 +2269,47 @@ function parseFailureSection(section) {
1978
2269
  // src/ingest/BusinessKnowledgeIngestor.ts
1979
2270
  var fs3 = __toESM(require("fs/promises"));
1980
2271
  var path4 = __toESM(require("path"));
2272
+ var import_zod2 = require("zod");
2273
+ var BUG_TRACK_CATEGORIES = [
2274
+ "build-errors",
2275
+ "test-failures",
2276
+ "runtime-errors",
2277
+ "performance-issues",
2278
+ "database-issues",
2279
+ "security-issues",
2280
+ "ui-bugs",
2281
+ "integration-issues",
2282
+ "logic-errors"
2283
+ ];
2284
+ var KNOWLEDGE_TRACK_CATEGORIES = [
2285
+ "architecture-patterns",
2286
+ "design-patterns",
2287
+ "tooling-decisions",
2288
+ "conventions",
2289
+ "dx",
2290
+ "best-practices"
2291
+ ];
2292
+ var ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
2293
+ var SolutionBaseSchema = import_zod2.z.object({
2294
+ module: import_zod2.z.string().min(1),
2295
+ tags: import_zod2.z.array(import_zod2.z.string()),
2296
+ problem_type: import_zod2.z.string().min(1),
2297
+ last_updated: import_zod2.z.string().regex(ISO_DATE, "last_updated must be ISO date YYYY-MM-DD")
2298
+ });
2299
+ var SolutionDocFrontmatterSchema = import_zod2.z.discriminatedUnion("track", [
2300
+ SolutionBaseSchema.merge(
2301
+ import_zod2.z.object({
2302
+ track: import_zod2.z.literal("bug-track"),
2303
+ category: import_zod2.z.enum(BUG_TRACK_CATEGORIES)
2304
+ })
2305
+ ),
2306
+ SolutionBaseSchema.merge(
2307
+ import_zod2.z.object({
2308
+ track: import_zod2.z.literal("knowledge-track"),
2309
+ category: import_zod2.z.enum(KNOWLEDGE_TRACK_CATEGORIES)
2310
+ })
2311
+ )
2312
+ ]);
1981
2313
  var BUSINESS_KNOWLEDGE_TYPES = /* @__PURE__ */ new Set([
1982
2314
  "business_rule",
1983
2315
  "business_process",
@@ -2020,6 +2352,65 @@ var BusinessKnowledgeIngestor = class {
2020
2352
  durationMs: Date.now() - start
2021
2353
  };
2022
2354
  }
2355
+ async ingestSolutions(solutionsDir) {
2356
+ const start = Date.now();
2357
+ const errors = [];
2358
+ let files;
2359
+ try {
2360
+ files = await this.findMarkdownFiles(solutionsDir);
2361
+ } catch {
2362
+ return emptyResult(Date.now() - start);
2363
+ }
2364
+ let nodesAdded = 0;
2365
+ for (const filePath of files) {
2366
+ try {
2367
+ const raw = await fs3.readFile(filePath, "utf-8");
2368
+ const parsed = parseSolutionFrontmatter(raw);
2369
+ if (!parsed) {
2370
+ errors.push(`${filePath}: no frontmatter found`);
2371
+ continue;
2372
+ }
2373
+ const validation = SolutionDocFrontmatterSchema.safeParse(parsed.frontmatter);
2374
+ if (!validation.success) {
2375
+ errors.push(`${filePath}: ${validation.error.message}`);
2376
+ continue;
2377
+ }
2378
+ if (validation.data.track !== "knowledge-track") continue;
2379
+ const relPath = path4.relative(solutionsDir, filePath).replaceAll("\\", "/");
2380
+ const filename = path4.basename(filePath, ".md");
2381
+ const nodeId = `bk:solutions:${validation.data.module}:${filename}`;
2382
+ const titleMatch = parsed.body.match(/^#\s+(.+)$/m);
2383
+ const name = titleMatch ? titleMatch[1].trim() : filename;
2384
+ const node = {
2385
+ id: nodeId,
2386
+ type: "business_concept",
2387
+ name,
2388
+ path: relPath,
2389
+ content: parsed.body.trim(),
2390
+ metadata: {
2391
+ domain: validation.data.module,
2392
+ tags: validation.data.tags,
2393
+ problem_type: validation.data.problem_type,
2394
+ last_updated: validation.data.last_updated,
2395
+ source: "solutions",
2396
+ category: validation.data.category
2397
+ }
2398
+ };
2399
+ this.store.addNode(node);
2400
+ nodesAdded++;
2401
+ } catch (err) {
2402
+ errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
2403
+ }
2404
+ }
2405
+ return {
2406
+ nodesAdded,
2407
+ nodesUpdated: 0,
2408
+ edgesAdded: 0,
2409
+ edgesUpdated: 0,
2410
+ errors,
2411
+ durationMs: Date.now() - start
2412
+ };
2413
+ }
2023
2414
  async createNodes(files, knowledgeDir, errors) {
2024
2415
  const entries = [];
2025
2416
  for (const filePath of files) {
@@ -2109,7 +2500,7 @@ var BusinessKnowledgeIngestor = class {
2109
2500
  const entries = await fs3.readdir(dir, { withFileTypes: true });
2110
2501
  for (const entry of entries) {
2111
2502
  const fullPath = path4.join(dir, entry.name);
2112
- if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "dist" && entry.name !== "target" && entry.name !== "build" && entry.name !== ".git" && entry.name !== ".gradle" && entry.name !== ".harness" && entry.name !== "vendor" && entry.name !== "bin" && entry.name !== "obj" && entry.name !== "venv" && entry.name !== "_build" && entry.name !== "deps" && entry.name !== "coverage") {
2503
+ if (entry.isDirectory() && !DEFAULT_SKIP_DIRS.has(entry.name)) {
2113
2504
  results.push(...await this.findMarkdownFiles(fullPath));
2114
2505
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
2115
2506
  results.push(fullPath);
@@ -2142,6 +2533,25 @@ function parseFrontmatter(raw) {
2142
2533
  body
2143
2534
  };
2144
2535
  }
2536
+ function parseSolutionFrontmatter(raw) {
2537
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
2538
+ if (!match) return null;
2539
+ const yamlBlock = match[1];
2540
+ const body = match[2];
2541
+ const frontmatter = {};
2542
+ for (const line of yamlBlock.split(/\r?\n/)) {
2543
+ const kvMatch = line.match(/^(\w+):\s*(.+)$/);
2544
+ if (!kvMatch) continue;
2545
+ const key = kvMatch[1];
2546
+ const value = kvMatch[2].trim();
2547
+ if (value.startsWith("[") && value.endsWith("]")) {
2548
+ frontmatter[key] = value.slice(1, -1).split(",").map((s) => s.trim());
2549
+ } else {
2550
+ frontmatter[key] = value;
2551
+ }
2552
+ }
2553
+ return { frontmatter, body };
2554
+ }
2145
2555
 
2146
2556
  // src/ingest/DecisionIngestor.ts
2147
2557
  var fs4 = __toESM(require("fs/promises"));
@@ -2934,7 +3344,6 @@ var PlantUmlParser = class {
2934
3344
 
2935
3345
  // src/ingest/DiagramParser.ts
2936
3346
  var DIAGRAM_EXTENSIONS = /* @__PURE__ */ new Set([".mmd", ".mermaid", ".d2", ".puml", ".plantuml"]);
2937
- var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".harness"]);
2938
3347
  var DiagramParser = class {
2939
3348
  constructor(store) {
2940
3349
  this.store = store;
@@ -3039,7 +3448,7 @@ var DiagramParser = class {
3039
3448
  }
3040
3449
  for (const entry of entries) {
3041
3450
  if (entry.isDirectory()) {
3042
- if (!SKIP_DIRS.has(entry.name)) {
3451
+ if (!DEFAULT_SKIP_DIRS.has(entry.name)) {
3043
3452
  await walk(path7.join(currentDir, entry.name));
3044
3453
  }
3045
3454
  } else if (entry.isFile()) {
@@ -3563,31 +3972,6 @@ var KnowledgeStagingAggregator = class {
3563
3972
  // src/ingest/extractors/ExtractionRunner.ts
3564
3973
  var fs9 = __toESM(require("fs/promises"));
3565
3974
  var path10 = __toESM(require("path"));
3566
- var SKIP_DIRS2 = /* @__PURE__ */ new Set([
3567
- "node_modules",
3568
- "dist",
3569
- "target",
3570
- "build",
3571
- ".git",
3572
- "__pycache__",
3573
- ".venv",
3574
- ".turbo",
3575
- ".harness",
3576
- ".next",
3577
- ".vscode",
3578
- ".idea",
3579
- "vendor",
3580
- "out",
3581
- ".gradle",
3582
- ".gradle-home",
3583
- "bin",
3584
- "obj",
3585
- "venv",
3586
- "_build",
3587
- "deps",
3588
- "coverage",
3589
- ".nyc_output"
3590
- ]);
3591
3975
  var EXT_TO_LANGUAGE = {
3592
3976
  ".ts": "typescript",
3593
3977
  ".tsx": "typescript",
@@ -3766,7 +4150,7 @@ var ExtractionRunner = class {
3766
4150
  }
3767
4151
  for (const entry of entries) {
3768
4152
  const fullPath = path10.join(dir, entry.name);
3769
- if (entry.isDirectory() && !SKIP_DIRS2.has(entry.name)) {
4153
+ if (entry.isDirectory() && !DEFAULT_SKIP_DIRS.has(entry.name)) {
3770
4154
  results.push(...await this.findSourceFiles(fullPath));
3771
4155
  } else if (entry.isFile() && detectLanguage(fullPath) !== void 0) {
3772
4156
  results.push(fullPath);
@@ -5528,6 +5912,25 @@ var KnowledgePipelineRunner = class {
5528
5912
  durationMs: 0
5529
5913
  };
5530
5914
  }
5915
+ const solutionsDir = path12.join(options.projectDir, "docs", "solutions");
5916
+ let solutionsResult;
5917
+ try {
5918
+ solutionsResult = await bkIngestor.ingestSolutions(solutionsDir);
5919
+ } catch {
5920
+ solutionsResult = {
5921
+ nodesAdded: 0,
5922
+ nodesUpdated: 0,
5923
+ edgesAdded: 0,
5924
+ edgesUpdated: 0,
5925
+ errors: [],
5926
+ durationMs: 0
5927
+ };
5928
+ }
5929
+ bkResult = {
5930
+ ...bkResult,
5931
+ nodesAdded: bkResult.nodesAdded + solutionsResult.nodesAdded,
5932
+ errors: [...bkResult.errors, ...solutionsResult.errors]
5933
+ };
5531
5934
  const decisionsDir = path12.join(options.projectDir, "docs", "knowledge", "decisions");
5532
5935
  const decisionIngestor = new DecisionIngestor(this.store);
5533
5936
  let decisionResult;
@@ -8756,7 +9159,7 @@ function queryTraceability(store, options) {
8756
9159
  }
8757
9160
 
8758
9161
  // src/constraints/GraphConstraintAdapter.ts
8759
- var import_minimatch = require("minimatch");
9162
+ var import_minimatch2 = require("minimatch");
8760
9163
  var import_node_path2 = require("path");
8761
9164
  var GraphConstraintAdapter = class {
8762
9165
  constructor(store) {
@@ -8808,7 +9211,7 @@ var GraphConstraintAdapter = class {
8808
9211
  resolveLayer(filePath, layers) {
8809
9212
  for (const layer of layers) {
8810
9213
  for (const pattern of layer.patterns) {
8811
- if ((0, import_minimatch.minimatch)(filePath, pattern)) {
9214
+ if ((0, import_minimatch2.minimatch)(filePath, pattern)) {
8812
9215
  return layer;
8813
9216
  }
8814
9217
  }
@@ -9608,6 +10011,7 @@ var VERSION = "0.6.0";
9608
10011
  D2Parser,
9609
10012
  DEFAULT_BLOCKLIST,
9610
10013
  DEFAULT_PATTERNS,
10014
+ DEFAULT_SKIP_DIRS,
9611
10015
  DecisionIngestor,
9612
10016
  DesignConstraintAdapter,
9613
10017
  DesignIngestor,
@@ -9663,8 +10067,11 @@ var VERSION = "0.6.0";
9663
10067
  inferDomain,
9664
10068
  linkToCode,
9665
10069
  loadGraph,
10070
+ loadGraphMetadata,
9666
10071
  normalizeIntent,
9667
10072
  project,
9668
10073
  queryTraceability,
9669
- saveGraph
10074
+ resolveSkipDirs,
10075
+ saveGraph,
10076
+ skipDirGlobs
9670
10077
  });