@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.d.mts +120 -3
- package/dist/index.d.ts +120 -3
- package/dist/index.js +468 -61
- package/dist/index.mjs +467 -64
- package/package.json +1 -1
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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
|
|
295
|
-
return { status: "loaded", graph
|
|
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
|
-
|
|
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
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
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() &&
|
|
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() &&
|
|
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 (!
|
|
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() && !
|
|
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
|
|
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,
|
|
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
|
-
|
|
10074
|
+
resolveSkipDirs,
|
|
10075
|
+
saveGraph,
|
|
10076
|
+
skipDirGlobs
|
|
9670
10077
|
});
|