@harness-engineering/graph 0.7.0 → 0.8.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 +108 -3
- package/dist/index.d.ts +108 -3
- package/dist/index.js +394 -26
- package/dist/index.mjs +392 -27
- 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,9 +103,11 @@ __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,
|
|
110
|
+
resolveSkipDirs: () => resolveSkipDirs,
|
|
108
111
|
saveGraph: () => saveGraph
|
|
109
112
|
});
|
|
110
113
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -202,7 +205,7 @@ var EDGE_TYPES = [
|
|
|
202
205
|
"caches"
|
|
203
206
|
];
|
|
204
207
|
var OBSERVABILITY_TYPES = /* @__PURE__ */ new Set(["span", "metric", "log"]);
|
|
205
|
-
var CURRENT_SCHEMA_VERSION =
|
|
208
|
+
var CURRENT_SCHEMA_VERSION = 2;
|
|
206
209
|
var NODE_STABILITY = {
|
|
207
210
|
File: "session",
|
|
208
211
|
Function: "session",
|
|
@@ -241,38 +244,99 @@ var GraphEdgeSchema = import_zod.z.object({
|
|
|
241
244
|
// src/store/Serializer.ts
|
|
242
245
|
var import_promises = require("fs/promises");
|
|
243
246
|
var import_node_fs = require("fs");
|
|
247
|
+
var import_node_readline = require("readline");
|
|
244
248
|
var import_node_path = require("path");
|
|
245
|
-
function
|
|
249
|
+
function streamGraphNdjson(filePath, nodes, edges) {
|
|
246
250
|
return new Promise((resolve, reject) => {
|
|
247
251
|
const stream = (0, import_node_fs.createWriteStream)(filePath, { encoding: "utf-8" });
|
|
248
252
|
stream.on("error", reject);
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
stream.write(JSON.stringify(nodes[i]));
|
|
253
|
+
for (const node of nodes) {
|
|
254
|
+
stream.write(JSON.stringify({ kind: "node", ...node }));
|
|
255
|
+
stream.write("\n");
|
|
253
256
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
stream.write(JSON.stringify(edges[i]));
|
|
257
|
+
for (const edge of edges) {
|
|
258
|
+
stream.write(JSON.stringify({ kind: "edge", ...edge }));
|
|
259
|
+
stream.write("\n");
|
|
258
260
|
}
|
|
259
|
-
stream.write("]}");
|
|
260
261
|
stream.end(() => resolve());
|
|
261
262
|
});
|
|
262
263
|
}
|
|
264
|
+
function countNodesByType(nodes) {
|
|
265
|
+
const counts = {};
|
|
266
|
+
for (const node of nodes) {
|
|
267
|
+
counts[node.type] = (counts[node.type] ?? 0) + 1;
|
|
268
|
+
}
|
|
269
|
+
return counts;
|
|
270
|
+
}
|
|
263
271
|
async function saveGraph(dirPath, nodes, edges) {
|
|
264
272
|
await (0, import_promises.mkdir)(dirPath, { recursive: true });
|
|
265
273
|
const metadata = {
|
|
266
274
|
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
267
275
|
lastScanTimestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
268
276
|
nodeCount: nodes.length,
|
|
269
|
-
edgeCount: edges.length
|
|
277
|
+
edgeCount: edges.length,
|
|
278
|
+
nodesByType: countNodesByType(nodes)
|
|
270
279
|
};
|
|
271
280
|
await Promise.all([
|
|
272
|
-
|
|
281
|
+
streamGraphNdjson((0, import_node_path.join)(dirPath, "graph.json"), nodes, edges),
|
|
273
282
|
(0, import_promises.writeFile)((0, import_node_path.join)(dirPath, "metadata.json"), JSON.stringify(metadata, null, 2), "utf-8")
|
|
274
283
|
]);
|
|
275
284
|
}
|
|
285
|
+
async function loadGraphMetadata(dirPath) {
|
|
286
|
+
const metaPath = (0, import_node_path.join)(dirPath, "metadata.json");
|
|
287
|
+
try {
|
|
288
|
+
await (0, import_promises.access)(metaPath);
|
|
289
|
+
} catch {
|
|
290
|
+
return { status: "not_found" };
|
|
291
|
+
}
|
|
292
|
+
let metadata;
|
|
293
|
+
try {
|
|
294
|
+
const content = await (0, import_promises.readFile)(metaPath, "utf-8");
|
|
295
|
+
metadata = JSON.parse(content);
|
|
296
|
+
} catch {
|
|
297
|
+
return { status: "not_found" };
|
|
298
|
+
}
|
|
299
|
+
if (metadata.schemaVersion !== CURRENT_SCHEMA_VERSION) {
|
|
300
|
+
return {
|
|
301
|
+
status: "schema_mismatch",
|
|
302
|
+
found: metadata.schemaVersion,
|
|
303
|
+
expected: CURRENT_SCHEMA_VERSION
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
return { status: "loaded", metadata };
|
|
307
|
+
}
|
|
308
|
+
async function streamReadGraph(filePath) {
|
|
309
|
+
const stream = (0, import_node_fs.createReadStream)(filePath, { encoding: "utf-8" });
|
|
310
|
+
const rl = (0, import_node_readline.createInterface)({ input: stream, crlfDelay: Infinity });
|
|
311
|
+
const nodes = [];
|
|
312
|
+
const edges = [];
|
|
313
|
+
try {
|
|
314
|
+
for await (const rawLine of rl) {
|
|
315
|
+
const line = rawLine.trim();
|
|
316
|
+
if (line === "") continue;
|
|
317
|
+
const record = JSON.parse(line);
|
|
318
|
+
if (record.kind === "node") {
|
|
319
|
+
const { kind: _kind, ...rest } = record;
|
|
320
|
+
nodes.push(rest);
|
|
321
|
+
} else if (record.kind === "edge") {
|
|
322
|
+
const { kind: _kind, ...rest } = record;
|
|
323
|
+
edges.push(rest);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
} catch (err) {
|
|
327
|
+
if (err instanceof RangeError && /invalid string length/i.test(err.message)) {
|
|
328
|
+
throw new Error(
|
|
329
|
+
`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.`,
|
|
330
|
+
{ cause: err }
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
throw err;
|
|
334
|
+
} finally {
|
|
335
|
+
rl.close();
|
|
336
|
+
stream.close();
|
|
337
|
+
}
|
|
338
|
+
return { nodes, edges };
|
|
339
|
+
}
|
|
276
340
|
async function loadGraph(dirPath) {
|
|
277
341
|
const metaPath = (0, import_node_path.join)(dirPath, "metadata.json");
|
|
278
342
|
const graphPath = (0, import_node_path.join)(dirPath, "graph.json");
|
|
@@ -291,8 +355,8 @@ async function loadGraph(dirPath) {
|
|
|
291
355
|
expected: CURRENT_SCHEMA_VERSION
|
|
292
356
|
};
|
|
293
357
|
}
|
|
294
|
-
const
|
|
295
|
-
return { status: "loaded", graph
|
|
358
|
+
const graph = await streamReadGraph(graphPath);
|
|
359
|
+
return { status: "loaded", graph };
|
|
296
360
|
}
|
|
297
361
|
|
|
298
362
|
// src/store/GraphStore.ts
|
|
@@ -838,6 +902,84 @@ function groupNodesByImpact(nodes, excludeId) {
|
|
|
838
902
|
// src/ingest/CodeIngestor.ts
|
|
839
903
|
var fs = __toESM(require("fs/promises"));
|
|
840
904
|
var path = __toESM(require("path"));
|
|
905
|
+
var import_minimatch = require("minimatch");
|
|
906
|
+
|
|
907
|
+
// src/ingest/skip-dirs.ts
|
|
908
|
+
var DEFAULT_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
909
|
+
// VCS
|
|
910
|
+
".git",
|
|
911
|
+
".hg",
|
|
912
|
+
".svn",
|
|
913
|
+
// Package managers
|
|
914
|
+
"node_modules",
|
|
915
|
+
".pnpm-store",
|
|
916
|
+
".yarn",
|
|
917
|
+
"vendor",
|
|
918
|
+
// JS/TS build outputs
|
|
919
|
+
"dist",
|
|
920
|
+
"build",
|
|
921
|
+
"out",
|
|
922
|
+
"_build",
|
|
923
|
+
"bin",
|
|
924
|
+
"obj",
|
|
925
|
+
"target",
|
|
926
|
+
"deps",
|
|
927
|
+
// JS/TS framework / tooling caches
|
|
928
|
+
".next",
|
|
929
|
+
".nuxt",
|
|
930
|
+
".svelte-kit",
|
|
931
|
+
".turbo",
|
|
932
|
+
".vite",
|
|
933
|
+
".cache",
|
|
934
|
+
".parcel-cache",
|
|
935
|
+
".docusaurus",
|
|
936
|
+
".wrangler",
|
|
937
|
+
".astro",
|
|
938
|
+
".remix",
|
|
939
|
+
"storybook-static",
|
|
940
|
+
// Test / coverage / reporter outputs
|
|
941
|
+
"coverage",
|
|
942
|
+
".nyc_output",
|
|
943
|
+
".pytest_cache",
|
|
944
|
+
"playwright-report",
|
|
945
|
+
"test-results",
|
|
946
|
+
".e2e",
|
|
947
|
+
// Python
|
|
948
|
+
"__pycache__",
|
|
949
|
+
".venv",
|
|
950
|
+
"venv",
|
|
951
|
+
".tox",
|
|
952
|
+
".mypy_cache",
|
|
953
|
+
".ruff_cache",
|
|
954
|
+
// JVM
|
|
955
|
+
".gradle",
|
|
956
|
+
".gradle-home",
|
|
957
|
+
// IDE / editor
|
|
958
|
+
".idea",
|
|
959
|
+
".vscode",
|
|
960
|
+
".vs",
|
|
961
|
+
// Harness self
|
|
962
|
+
".harness",
|
|
963
|
+
// AI agent sandboxes (these often contain full repo clones — skipping them
|
|
964
|
+
// prevents the walker from re-ingesting nested copies of the project)
|
|
965
|
+
".claude",
|
|
966
|
+
".cursor",
|
|
967
|
+
".codex",
|
|
968
|
+
".gemini",
|
|
969
|
+
".aider",
|
|
970
|
+
".agents",
|
|
971
|
+
".agentastic",
|
|
972
|
+
".playwright-mcp"
|
|
973
|
+
]);
|
|
974
|
+
function resolveSkipDirs(options) {
|
|
975
|
+
const base = options?.skipDirs ? new Set(options.skipDirs) : new Set(DEFAULT_SKIP_DIRS);
|
|
976
|
+
if (options?.additionalSkipDirs) {
|
|
977
|
+
for (const name of options.additionalSkipDirs) base.add(name);
|
|
978
|
+
}
|
|
979
|
+
return base;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// src/ingest/CodeIngestor.ts
|
|
841
983
|
var SKIP_METHOD_NAMES = /* @__PURE__ */ new Set(["constructor", "if", "for", "while", "switch"]);
|
|
842
984
|
var FUNCTION_PATTERNS = {
|
|
843
985
|
python: /^def\s+(\w+)\s*\(/,
|
|
@@ -907,11 +1049,55 @@ function isSupportedSourceFile(name) {
|
|
|
907
1049
|
const ext = name.slice(name.lastIndexOf("."));
|
|
908
1050
|
return SUPPORTED_EXTENSIONS.has(ext);
|
|
909
1051
|
}
|
|
1052
|
+
function isExcluded(relPath, patterns) {
|
|
1053
|
+
if (patterns.length === 0) return false;
|
|
1054
|
+
for (const pattern of patterns) {
|
|
1055
|
+
if ((0, import_minimatch.minimatch)(relPath, pattern, { dot: true, matchBase: false })) return true;
|
|
1056
|
+
if (!pattern.includes("/") && (0, import_minimatch.minimatch)(relPath, pattern, { dot: true, matchBase: true })) {
|
|
1057
|
+
return true;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
return false;
|
|
1061
|
+
}
|
|
1062
|
+
async function readGitignorePatterns(rootDir) {
|
|
1063
|
+
let content;
|
|
1064
|
+
try {
|
|
1065
|
+
content = await fs.readFile(path.join(rootDir, ".gitignore"), "utf-8");
|
|
1066
|
+
} catch {
|
|
1067
|
+
return [];
|
|
1068
|
+
}
|
|
1069
|
+
const patterns = [];
|
|
1070
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
1071
|
+
const line = rawLine.trim();
|
|
1072
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
1073
|
+
if (line.startsWith("!")) continue;
|
|
1074
|
+
const rooted = line.startsWith("/");
|
|
1075
|
+
const stripped = rooted ? line.slice(1) : line;
|
|
1076
|
+
const isDirOnly = stripped.endsWith("/");
|
|
1077
|
+
const core = isDirOnly ? stripped.slice(0, -1) : stripped;
|
|
1078
|
+
if (core === "") continue;
|
|
1079
|
+
if (rooted) {
|
|
1080
|
+
patterns.push(isDirOnly ? `${core}/**` : core);
|
|
1081
|
+
if (isDirOnly) patterns.push(`${core}/`);
|
|
1082
|
+
} else {
|
|
1083
|
+
patterns.push(`**/${core}`);
|
|
1084
|
+
patterns.push(`**/${core}/**`);
|
|
1085
|
+
if (isDirOnly) patterns.push(`**/${core}/`);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
return patterns;
|
|
1089
|
+
}
|
|
910
1090
|
var CodeIngestor = class {
|
|
911
|
-
constructor(store) {
|
|
1091
|
+
constructor(store, options = {}) {
|
|
912
1092
|
this.store = store;
|
|
1093
|
+
this.skipDirs = resolveSkipDirs(options);
|
|
1094
|
+
this.excludePatterns = options.excludePatterns ?? [];
|
|
1095
|
+
this.respectGitignore = options.respectGitignore ?? true;
|
|
913
1096
|
}
|
|
914
1097
|
store;
|
|
1098
|
+
skipDirs;
|
|
1099
|
+
excludePatterns;
|
|
1100
|
+
respectGitignore;
|
|
915
1101
|
async ingest(rootDir) {
|
|
916
1102
|
const start = Date.now();
|
|
917
1103
|
const errors = [];
|
|
@@ -1001,19 +1187,55 @@ var CodeIngestor = class {
|
|
|
1001
1187
|
}
|
|
1002
1188
|
files.add(relativePath);
|
|
1003
1189
|
}
|
|
1004
|
-
|
|
1190
|
+
/**
|
|
1191
|
+
* Walk `rootDir` iteratively (BFS via an explicit queue) and return absolute paths
|
|
1192
|
+
* of every supported source file that is not excluded by the configured filters.
|
|
1193
|
+
*
|
|
1194
|
+
* Iterative on purpose: a recursive walker burns one stack frame per nested
|
|
1195
|
+
* directory and crashes with `Maximum call stack size exceeded` on deeply nested
|
|
1196
|
+
* monorepos (issue #274). The queue-based form keeps memory bounded by the
|
|
1197
|
+
* frontier size rather than the deepest path.
|
|
1198
|
+
*/
|
|
1199
|
+
async findSourceFiles(rootDir) {
|
|
1005
1200
|
const results = [];
|
|
1006
|
-
const
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1201
|
+
const matchers = await this.buildExcludeMatchers(rootDir);
|
|
1202
|
+
const queue = [rootDir];
|
|
1203
|
+
while (queue.length > 0) {
|
|
1204
|
+
const dir = queue.pop();
|
|
1205
|
+
let entries;
|
|
1206
|
+
try {
|
|
1207
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1208
|
+
} catch {
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1211
|
+
for (const entry of entries) {
|
|
1212
|
+
const fullPath = path.join(dir, entry.name);
|
|
1213
|
+
const relPath = path.relative(rootDir, fullPath).replaceAll("\\", "/");
|
|
1214
|
+
if (entry.isDirectory()) {
|
|
1215
|
+
if (this.skipDirs.has(entry.name)) continue;
|
|
1216
|
+
if (isExcluded(`${relPath}/`, matchers)) continue;
|
|
1217
|
+
queue.push(fullPath);
|
|
1218
|
+
} else if (entry.isFile()) {
|
|
1219
|
+
if (!isSupportedSourceFile(entry.name)) continue;
|
|
1220
|
+
if (isExcluded(relPath, matchers)) continue;
|
|
1221
|
+
results.push(fullPath);
|
|
1222
|
+
}
|
|
1013
1223
|
}
|
|
1014
1224
|
}
|
|
1015
1225
|
return results;
|
|
1016
1226
|
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Compose the exclude matchers from `excludePatterns` and (optionally) `.gitignore`.
|
|
1229
|
+
* Returns a list of minimatch-compatible patterns; an empty list means "no excludes".
|
|
1230
|
+
*/
|
|
1231
|
+
async buildExcludeMatchers(rootDir) {
|
|
1232
|
+
const patterns = [...this.excludePatterns];
|
|
1233
|
+
if (this.respectGitignore) {
|
|
1234
|
+
const gitignorePatterns = await readGitignorePatterns(rootDir);
|
|
1235
|
+
patterns.push(...gitignorePatterns);
|
|
1236
|
+
}
|
|
1237
|
+
return patterns;
|
|
1238
|
+
}
|
|
1017
1239
|
extractSymbols(content, fileId, relativePath) {
|
|
1018
1240
|
const results = [];
|
|
1019
1241
|
const lines = content.split("\n");
|
|
@@ -1978,6 +2200,47 @@ function parseFailureSection(section) {
|
|
|
1978
2200
|
// src/ingest/BusinessKnowledgeIngestor.ts
|
|
1979
2201
|
var fs3 = __toESM(require("fs/promises"));
|
|
1980
2202
|
var path4 = __toESM(require("path"));
|
|
2203
|
+
var import_zod2 = require("zod");
|
|
2204
|
+
var BUG_TRACK_CATEGORIES = [
|
|
2205
|
+
"build-errors",
|
|
2206
|
+
"test-failures",
|
|
2207
|
+
"runtime-errors",
|
|
2208
|
+
"performance-issues",
|
|
2209
|
+
"database-issues",
|
|
2210
|
+
"security-issues",
|
|
2211
|
+
"ui-bugs",
|
|
2212
|
+
"integration-issues",
|
|
2213
|
+
"logic-errors"
|
|
2214
|
+
];
|
|
2215
|
+
var KNOWLEDGE_TRACK_CATEGORIES = [
|
|
2216
|
+
"architecture-patterns",
|
|
2217
|
+
"design-patterns",
|
|
2218
|
+
"tooling-decisions",
|
|
2219
|
+
"conventions",
|
|
2220
|
+
"dx",
|
|
2221
|
+
"best-practices"
|
|
2222
|
+
];
|
|
2223
|
+
var ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
2224
|
+
var SolutionBaseSchema = import_zod2.z.object({
|
|
2225
|
+
module: import_zod2.z.string().min(1),
|
|
2226
|
+
tags: import_zod2.z.array(import_zod2.z.string()),
|
|
2227
|
+
problem_type: import_zod2.z.string().min(1),
|
|
2228
|
+
last_updated: import_zod2.z.string().regex(ISO_DATE, "last_updated must be ISO date YYYY-MM-DD")
|
|
2229
|
+
});
|
|
2230
|
+
var SolutionDocFrontmatterSchema = import_zod2.z.discriminatedUnion("track", [
|
|
2231
|
+
SolutionBaseSchema.merge(
|
|
2232
|
+
import_zod2.z.object({
|
|
2233
|
+
track: import_zod2.z.literal("bug-track"),
|
|
2234
|
+
category: import_zod2.z.enum(BUG_TRACK_CATEGORIES)
|
|
2235
|
+
})
|
|
2236
|
+
),
|
|
2237
|
+
SolutionBaseSchema.merge(
|
|
2238
|
+
import_zod2.z.object({
|
|
2239
|
+
track: import_zod2.z.literal("knowledge-track"),
|
|
2240
|
+
category: import_zod2.z.enum(KNOWLEDGE_TRACK_CATEGORIES)
|
|
2241
|
+
})
|
|
2242
|
+
)
|
|
2243
|
+
]);
|
|
1981
2244
|
var BUSINESS_KNOWLEDGE_TYPES = /* @__PURE__ */ new Set([
|
|
1982
2245
|
"business_rule",
|
|
1983
2246
|
"business_process",
|
|
@@ -2020,6 +2283,65 @@ var BusinessKnowledgeIngestor = class {
|
|
|
2020
2283
|
durationMs: Date.now() - start
|
|
2021
2284
|
};
|
|
2022
2285
|
}
|
|
2286
|
+
async ingestSolutions(solutionsDir) {
|
|
2287
|
+
const start = Date.now();
|
|
2288
|
+
const errors = [];
|
|
2289
|
+
let files;
|
|
2290
|
+
try {
|
|
2291
|
+
files = await this.findMarkdownFiles(solutionsDir);
|
|
2292
|
+
} catch {
|
|
2293
|
+
return emptyResult(Date.now() - start);
|
|
2294
|
+
}
|
|
2295
|
+
let nodesAdded = 0;
|
|
2296
|
+
for (const filePath of files) {
|
|
2297
|
+
try {
|
|
2298
|
+
const raw = await fs3.readFile(filePath, "utf-8");
|
|
2299
|
+
const parsed = parseSolutionFrontmatter(raw);
|
|
2300
|
+
if (!parsed) {
|
|
2301
|
+
errors.push(`${filePath}: no frontmatter found`);
|
|
2302
|
+
continue;
|
|
2303
|
+
}
|
|
2304
|
+
const validation = SolutionDocFrontmatterSchema.safeParse(parsed.frontmatter);
|
|
2305
|
+
if (!validation.success) {
|
|
2306
|
+
errors.push(`${filePath}: ${validation.error.message}`);
|
|
2307
|
+
continue;
|
|
2308
|
+
}
|
|
2309
|
+
if (validation.data.track !== "knowledge-track") continue;
|
|
2310
|
+
const relPath = path4.relative(solutionsDir, filePath).replaceAll("\\", "/");
|
|
2311
|
+
const filename = path4.basename(filePath, ".md");
|
|
2312
|
+
const nodeId = `bk:solutions:${validation.data.module}:${filename}`;
|
|
2313
|
+
const titleMatch = parsed.body.match(/^#\s+(.+)$/m);
|
|
2314
|
+
const name = titleMatch ? titleMatch[1].trim() : filename;
|
|
2315
|
+
const node = {
|
|
2316
|
+
id: nodeId,
|
|
2317
|
+
type: "business_concept",
|
|
2318
|
+
name,
|
|
2319
|
+
path: relPath,
|
|
2320
|
+
content: parsed.body.trim(),
|
|
2321
|
+
metadata: {
|
|
2322
|
+
domain: validation.data.module,
|
|
2323
|
+
tags: validation.data.tags,
|
|
2324
|
+
problem_type: validation.data.problem_type,
|
|
2325
|
+
last_updated: validation.data.last_updated,
|
|
2326
|
+
source: "solutions",
|
|
2327
|
+
category: validation.data.category
|
|
2328
|
+
}
|
|
2329
|
+
};
|
|
2330
|
+
this.store.addNode(node);
|
|
2331
|
+
nodesAdded++;
|
|
2332
|
+
} catch (err) {
|
|
2333
|
+
errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
return {
|
|
2337
|
+
nodesAdded,
|
|
2338
|
+
nodesUpdated: 0,
|
|
2339
|
+
edgesAdded: 0,
|
|
2340
|
+
edgesUpdated: 0,
|
|
2341
|
+
errors,
|
|
2342
|
+
durationMs: Date.now() - start
|
|
2343
|
+
};
|
|
2344
|
+
}
|
|
2023
2345
|
async createNodes(files, knowledgeDir, errors) {
|
|
2024
2346
|
const entries = [];
|
|
2025
2347
|
for (const filePath of files) {
|
|
@@ -2052,6 +2374,7 @@ var BusinessKnowledgeIngestor = class {
|
|
|
2052
2374
|
content: body.trim(),
|
|
2053
2375
|
metadata: {
|
|
2054
2376
|
domain,
|
|
2377
|
+
...frontmatter.source && { source: frontmatter.source },
|
|
2055
2378
|
...frontmatter.tags && { tags: frontmatter.tags },
|
|
2056
2379
|
...frontmatter.related && { related: frontmatter.related }
|
|
2057
2380
|
}
|
|
@@ -2141,6 +2464,25 @@ function parseFrontmatter(raw) {
|
|
|
2141
2464
|
body
|
|
2142
2465
|
};
|
|
2143
2466
|
}
|
|
2467
|
+
function parseSolutionFrontmatter(raw) {
|
|
2468
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
2469
|
+
if (!match) return null;
|
|
2470
|
+
const yamlBlock = match[1];
|
|
2471
|
+
const body = match[2];
|
|
2472
|
+
const frontmatter = {};
|
|
2473
|
+
for (const line of yamlBlock.split(/\r?\n/)) {
|
|
2474
|
+
const kvMatch = line.match(/^(\w+):\s*(.+)$/);
|
|
2475
|
+
if (!kvMatch) continue;
|
|
2476
|
+
const key = kvMatch[1];
|
|
2477
|
+
const value = kvMatch[2].trim();
|
|
2478
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
2479
|
+
frontmatter[key] = value.slice(1, -1).split(",").map((s) => s.trim());
|
|
2480
|
+
} else {
|
|
2481
|
+
frontmatter[key] = value;
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
return { frontmatter, body };
|
|
2485
|
+
}
|
|
2144
2486
|
|
|
2145
2487
|
// src/ingest/DecisionIngestor.ts
|
|
2146
2488
|
var fs4 = __toESM(require("fs/promises"));
|
|
@@ -5335,6 +5677,10 @@ var KnowledgeDocMaterializer = class {
|
|
|
5335
5677
|
const mappedType = this.mapNodeType(node);
|
|
5336
5678
|
const sanitize = (s) => s.replace(/[\n\r]/g, " ").replace(/:/g, "-");
|
|
5337
5679
|
const lines = ["---", `type: ${sanitize(mappedType)}`, `domain: ${sanitize(domain)}`];
|
|
5680
|
+
const source = node.metadata?.source;
|
|
5681
|
+
if (typeof source === "string" && source.length > 0) {
|
|
5682
|
+
lines.push(`source: ${sanitize(source)}`);
|
|
5683
|
+
}
|
|
5338
5684
|
const tags = node.metadata?.tags;
|
|
5339
5685
|
if (Array.isArray(tags) && tags.length > 0) {
|
|
5340
5686
|
lines.push(`tags: [${tags.map((t) => sanitize(String(t))).join(", ")}]`);
|
|
@@ -5523,6 +5869,25 @@ var KnowledgePipelineRunner = class {
|
|
|
5523
5869
|
durationMs: 0
|
|
5524
5870
|
};
|
|
5525
5871
|
}
|
|
5872
|
+
const solutionsDir = path12.join(options.projectDir, "docs", "solutions");
|
|
5873
|
+
let solutionsResult;
|
|
5874
|
+
try {
|
|
5875
|
+
solutionsResult = await bkIngestor.ingestSolutions(solutionsDir);
|
|
5876
|
+
} catch {
|
|
5877
|
+
solutionsResult = {
|
|
5878
|
+
nodesAdded: 0,
|
|
5879
|
+
nodesUpdated: 0,
|
|
5880
|
+
edgesAdded: 0,
|
|
5881
|
+
edgesUpdated: 0,
|
|
5882
|
+
errors: [],
|
|
5883
|
+
durationMs: 0
|
|
5884
|
+
};
|
|
5885
|
+
}
|
|
5886
|
+
bkResult = {
|
|
5887
|
+
...bkResult,
|
|
5888
|
+
nodesAdded: bkResult.nodesAdded + solutionsResult.nodesAdded,
|
|
5889
|
+
errors: [...bkResult.errors, ...solutionsResult.errors]
|
|
5890
|
+
};
|
|
5526
5891
|
const decisionsDir = path12.join(options.projectDir, "docs", "knowledge", "decisions");
|
|
5527
5892
|
const decisionIngestor = new DecisionIngestor(this.store);
|
|
5528
5893
|
let decisionResult;
|
|
@@ -8751,7 +9116,7 @@ function queryTraceability(store, options) {
|
|
|
8751
9116
|
}
|
|
8752
9117
|
|
|
8753
9118
|
// src/constraints/GraphConstraintAdapter.ts
|
|
8754
|
-
var
|
|
9119
|
+
var import_minimatch2 = require("minimatch");
|
|
8755
9120
|
var import_node_path2 = require("path");
|
|
8756
9121
|
var GraphConstraintAdapter = class {
|
|
8757
9122
|
constructor(store) {
|
|
@@ -8803,7 +9168,7 @@ var GraphConstraintAdapter = class {
|
|
|
8803
9168
|
resolveLayer(filePath, layers) {
|
|
8804
9169
|
for (const layer of layers) {
|
|
8805
9170
|
for (const pattern of layer.patterns) {
|
|
8806
|
-
if ((0,
|
|
9171
|
+
if ((0, import_minimatch2.minimatch)(filePath, pattern)) {
|
|
8807
9172
|
return layer;
|
|
8808
9173
|
}
|
|
8809
9174
|
}
|
|
@@ -9603,6 +9968,7 @@ var VERSION = "0.6.0";
|
|
|
9603
9968
|
D2Parser,
|
|
9604
9969
|
DEFAULT_BLOCKLIST,
|
|
9605
9970
|
DEFAULT_PATTERNS,
|
|
9971
|
+
DEFAULT_SKIP_DIRS,
|
|
9606
9972
|
DecisionIngestor,
|
|
9607
9973
|
DesignConstraintAdapter,
|
|
9608
9974
|
DesignIngestor,
|
|
@@ -9658,8 +10024,10 @@ var VERSION = "0.6.0";
|
|
|
9658
10024
|
inferDomain,
|
|
9659
10025
|
linkToCode,
|
|
9660
10026
|
loadGraph,
|
|
10027
|
+
loadGraphMetadata,
|
|
9661
10028
|
normalizeIntent,
|
|
9662
10029
|
project,
|
|
9663
10030
|
queryTraceability,
|
|
10031
|
+
resolveSkipDirs,
|
|
9664
10032
|
saveGraph
|
|
9665
10033
|
});
|