@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.mjs
CHANGED
|
@@ -91,7 +91,7 @@ var EDGE_TYPES = [
|
|
|
91
91
|
"caches"
|
|
92
92
|
];
|
|
93
93
|
var OBSERVABILITY_TYPES = /* @__PURE__ */ new Set(["span", "metric", "log"]);
|
|
94
|
-
var CURRENT_SCHEMA_VERSION =
|
|
94
|
+
var CURRENT_SCHEMA_VERSION = 2;
|
|
95
95
|
var NODE_STABILITY = {
|
|
96
96
|
File: "session",
|
|
97
97
|
Function: "session",
|
|
@@ -129,39 +129,100 @@ var GraphEdgeSchema = z.object({
|
|
|
129
129
|
|
|
130
130
|
// src/store/Serializer.ts
|
|
131
131
|
import { readFile, writeFile, mkdir, access } from "fs/promises";
|
|
132
|
-
import { createWriteStream } from "fs";
|
|
132
|
+
import { createReadStream, createWriteStream } from "fs";
|
|
133
|
+
import { createInterface } from "readline";
|
|
133
134
|
import { join } from "path";
|
|
134
|
-
function
|
|
135
|
+
function streamGraphNdjson(filePath, nodes, edges) {
|
|
135
136
|
return new Promise((resolve, reject) => {
|
|
136
137
|
const stream = createWriteStream(filePath, { encoding: "utf-8" });
|
|
137
138
|
stream.on("error", reject);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
stream.write(JSON.stringify(nodes[i]));
|
|
139
|
+
for (const node of nodes) {
|
|
140
|
+
stream.write(JSON.stringify({ kind: "node", ...node }));
|
|
141
|
+
stream.write("\n");
|
|
142
142
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
stream.write(JSON.stringify(edges[i]));
|
|
143
|
+
for (const edge of edges) {
|
|
144
|
+
stream.write(JSON.stringify({ kind: "edge", ...edge }));
|
|
145
|
+
stream.write("\n");
|
|
147
146
|
}
|
|
148
|
-
stream.write("]}");
|
|
149
147
|
stream.end(() => resolve());
|
|
150
148
|
});
|
|
151
149
|
}
|
|
150
|
+
function countNodesByType(nodes) {
|
|
151
|
+
const counts = {};
|
|
152
|
+
for (const node of nodes) {
|
|
153
|
+
counts[node.type] = (counts[node.type] ?? 0) + 1;
|
|
154
|
+
}
|
|
155
|
+
return counts;
|
|
156
|
+
}
|
|
152
157
|
async function saveGraph(dirPath, nodes, edges) {
|
|
153
158
|
await mkdir(dirPath, { recursive: true });
|
|
154
159
|
const metadata = {
|
|
155
160
|
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
156
161
|
lastScanTimestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
157
162
|
nodeCount: nodes.length,
|
|
158
|
-
edgeCount: edges.length
|
|
163
|
+
edgeCount: edges.length,
|
|
164
|
+
nodesByType: countNodesByType(nodes)
|
|
159
165
|
};
|
|
160
166
|
await Promise.all([
|
|
161
|
-
|
|
167
|
+
streamGraphNdjson(join(dirPath, "graph.json"), nodes, edges),
|
|
162
168
|
writeFile(join(dirPath, "metadata.json"), JSON.stringify(metadata, null, 2), "utf-8")
|
|
163
169
|
]);
|
|
164
170
|
}
|
|
171
|
+
async function loadGraphMetadata(dirPath) {
|
|
172
|
+
const metaPath = join(dirPath, "metadata.json");
|
|
173
|
+
try {
|
|
174
|
+
await access(metaPath);
|
|
175
|
+
} catch {
|
|
176
|
+
return { status: "not_found" };
|
|
177
|
+
}
|
|
178
|
+
let metadata;
|
|
179
|
+
try {
|
|
180
|
+
const content = await readFile(metaPath, "utf-8");
|
|
181
|
+
metadata = JSON.parse(content);
|
|
182
|
+
} catch {
|
|
183
|
+
return { status: "not_found" };
|
|
184
|
+
}
|
|
185
|
+
if (metadata.schemaVersion !== CURRENT_SCHEMA_VERSION) {
|
|
186
|
+
return {
|
|
187
|
+
status: "schema_mismatch",
|
|
188
|
+
found: metadata.schemaVersion,
|
|
189
|
+
expected: CURRENT_SCHEMA_VERSION
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return { status: "loaded", metadata };
|
|
193
|
+
}
|
|
194
|
+
async function streamReadGraph(filePath) {
|
|
195
|
+
const stream = createReadStream(filePath, { encoding: "utf-8" });
|
|
196
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
197
|
+
const nodes = [];
|
|
198
|
+
const edges = [];
|
|
199
|
+
try {
|
|
200
|
+
for await (const rawLine of rl) {
|
|
201
|
+
const line = rawLine.trim();
|
|
202
|
+
if (line === "") continue;
|
|
203
|
+
const record = JSON.parse(line);
|
|
204
|
+
if (record.kind === "node") {
|
|
205
|
+
const { kind: _kind, ...rest } = record;
|
|
206
|
+
nodes.push(rest);
|
|
207
|
+
} else if (record.kind === "edge") {
|
|
208
|
+
const { kind: _kind, ...rest } = record;
|
|
209
|
+
edges.push(rest);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} catch (err) {
|
|
213
|
+
if (err instanceof RangeError && /invalid string length/i.test(err.message)) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`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.`,
|
|
216
|
+
{ cause: err }
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
throw err;
|
|
220
|
+
} finally {
|
|
221
|
+
rl.close();
|
|
222
|
+
stream.close();
|
|
223
|
+
}
|
|
224
|
+
return { nodes, edges };
|
|
225
|
+
}
|
|
165
226
|
async function loadGraph(dirPath) {
|
|
166
227
|
const metaPath = join(dirPath, "metadata.json");
|
|
167
228
|
const graphPath = join(dirPath, "graph.json");
|
|
@@ -180,8 +241,8 @@ async function loadGraph(dirPath) {
|
|
|
180
241
|
expected: CURRENT_SCHEMA_VERSION
|
|
181
242
|
};
|
|
182
243
|
}
|
|
183
|
-
const
|
|
184
|
-
return { status: "loaded", graph
|
|
244
|
+
const graph = await streamReadGraph(graphPath);
|
|
245
|
+
return { status: "loaded", graph };
|
|
185
246
|
}
|
|
186
247
|
|
|
187
248
|
// src/store/GraphStore.ts
|
|
@@ -727,6 +788,87 @@ function groupNodesByImpact(nodes, excludeId) {
|
|
|
727
788
|
// src/ingest/CodeIngestor.ts
|
|
728
789
|
import * as fs from "fs/promises";
|
|
729
790
|
import * as path from "path";
|
|
791
|
+
import { minimatch } from "minimatch";
|
|
792
|
+
|
|
793
|
+
// src/ingest/skip-dirs.ts
|
|
794
|
+
var DEFAULT_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
795
|
+
// VCS
|
|
796
|
+
".git",
|
|
797
|
+
".hg",
|
|
798
|
+
".svn",
|
|
799
|
+
// Package managers
|
|
800
|
+
"node_modules",
|
|
801
|
+
".pnpm-store",
|
|
802
|
+
".yarn",
|
|
803
|
+
"vendor",
|
|
804
|
+
// JS/TS build outputs
|
|
805
|
+
"dist",
|
|
806
|
+
"build",
|
|
807
|
+
"out",
|
|
808
|
+
"_build",
|
|
809
|
+
"bin",
|
|
810
|
+
"obj",
|
|
811
|
+
"target",
|
|
812
|
+
"deps",
|
|
813
|
+
// JS/TS framework / tooling caches
|
|
814
|
+
".next",
|
|
815
|
+
".nuxt",
|
|
816
|
+
".svelte-kit",
|
|
817
|
+
".turbo",
|
|
818
|
+
".vite",
|
|
819
|
+
".cache",
|
|
820
|
+
".parcel-cache",
|
|
821
|
+
".docusaurus",
|
|
822
|
+
".wrangler",
|
|
823
|
+
".astro",
|
|
824
|
+
".remix",
|
|
825
|
+
"storybook-static",
|
|
826
|
+
// Test / coverage / reporter outputs
|
|
827
|
+
"coverage",
|
|
828
|
+
".nyc_output",
|
|
829
|
+
".pytest_cache",
|
|
830
|
+
"playwright-report",
|
|
831
|
+
"test-results",
|
|
832
|
+
".e2e",
|
|
833
|
+
// Python
|
|
834
|
+
"__pycache__",
|
|
835
|
+
".venv",
|
|
836
|
+
"venv",
|
|
837
|
+
".tox",
|
|
838
|
+
".mypy_cache",
|
|
839
|
+
".ruff_cache",
|
|
840
|
+
// JVM
|
|
841
|
+
".gradle",
|
|
842
|
+
".gradle-home",
|
|
843
|
+
// IDE / editor
|
|
844
|
+
".idea",
|
|
845
|
+
".vscode",
|
|
846
|
+
".vs",
|
|
847
|
+
// Harness self
|
|
848
|
+
".harness",
|
|
849
|
+
// AI agent sandboxes (these often contain full repo clones — skipping them
|
|
850
|
+
// prevents the walker from re-ingesting nested copies of the project)
|
|
851
|
+
".claude",
|
|
852
|
+
".cursor",
|
|
853
|
+
".codex",
|
|
854
|
+
".gemini",
|
|
855
|
+
".aider",
|
|
856
|
+
".agents",
|
|
857
|
+
".agentastic",
|
|
858
|
+
".playwright-mcp"
|
|
859
|
+
]);
|
|
860
|
+
function resolveSkipDirs(options) {
|
|
861
|
+
const base = options?.skipDirs ? new Set(options.skipDirs) : new Set(DEFAULT_SKIP_DIRS);
|
|
862
|
+
if (options?.additionalSkipDirs) {
|
|
863
|
+
for (const name of options.additionalSkipDirs) base.add(name);
|
|
864
|
+
}
|
|
865
|
+
return base;
|
|
866
|
+
}
|
|
867
|
+
function skipDirGlobs(skipDirs = DEFAULT_SKIP_DIRS) {
|
|
868
|
+
return Array.from(skipDirs, (name) => `**/${name}/**`);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// src/ingest/CodeIngestor.ts
|
|
730
872
|
var SKIP_METHOD_NAMES = /* @__PURE__ */ new Set(["constructor", "if", "for", "while", "switch"]);
|
|
731
873
|
var FUNCTION_PATTERNS = {
|
|
732
874
|
python: /^def\s+(\w+)\s*\(/,
|
|
@@ -796,11 +938,55 @@ function isSupportedSourceFile(name) {
|
|
|
796
938
|
const ext = name.slice(name.lastIndexOf("."));
|
|
797
939
|
return SUPPORTED_EXTENSIONS.has(ext);
|
|
798
940
|
}
|
|
941
|
+
function isExcluded(relPath, patterns) {
|
|
942
|
+
if (patterns.length === 0) return false;
|
|
943
|
+
for (const pattern of patterns) {
|
|
944
|
+
if (minimatch(relPath, pattern, { dot: true, matchBase: false })) return true;
|
|
945
|
+
if (!pattern.includes("/") && minimatch(relPath, pattern, { dot: true, matchBase: true })) {
|
|
946
|
+
return true;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
return false;
|
|
950
|
+
}
|
|
951
|
+
async function readGitignorePatterns(rootDir) {
|
|
952
|
+
let content;
|
|
953
|
+
try {
|
|
954
|
+
content = await fs.readFile(path.join(rootDir, ".gitignore"), "utf-8");
|
|
955
|
+
} catch {
|
|
956
|
+
return [];
|
|
957
|
+
}
|
|
958
|
+
const patterns = [];
|
|
959
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
960
|
+
const line = rawLine.trim();
|
|
961
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
962
|
+
if (line.startsWith("!")) continue;
|
|
963
|
+
const rooted = line.startsWith("/");
|
|
964
|
+
const stripped = rooted ? line.slice(1) : line;
|
|
965
|
+
const isDirOnly = stripped.endsWith("/");
|
|
966
|
+
const core = isDirOnly ? stripped.slice(0, -1) : stripped;
|
|
967
|
+
if (core === "") continue;
|
|
968
|
+
if (rooted) {
|
|
969
|
+
patterns.push(isDirOnly ? `${core}/**` : core);
|
|
970
|
+
if (isDirOnly) patterns.push(`${core}/`);
|
|
971
|
+
} else {
|
|
972
|
+
patterns.push(`**/${core}`);
|
|
973
|
+
patterns.push(`**/${core}/**`);
|
|
974
|
+
if (isDirOnly) patterns.push(`**/${core}/`);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
return patterns;
|
|
978
|
+
}
|
|
799
979
|
var CodeIngestor = class {
|
|
800
|
-
constructor(store) {
|
|
980
|
+
constructor(store, options = {}) {
|
|
801
981
|
this.store = store;
|
|
982
|
+
this.skipDirs = resolveSkipDirs(options);
|
|
983
|
+
this.excludePatterns = options.excludePatterns ?? [];
|
|
984
|
+
this.respectGitignore = options.respectGitignore ?? true;
|
|
802
985
|
}
|
|
803
986
|
store;
|
|
987
|
+
skipDirs;
|
|
988
|
+
excludePatterns;
|
|
989
|
+
respectGitignore;
|
|
804
990
|
async ingest(rootDir) {
|
|
805
991
|
const start = Date.now();
|
|
806
992
|
const errors = [];
|
|
@@ -890,19 +1076,55 @@ var CodeIngestor = class {
|
|
|
890
1076
|
}
|
|
891
1077
|
files.add(relativePath);
|
|
892
1078
|
}
|
|
893
|
-
|
|
1079
|
+
/**
|
|
1080
|
+
* Walk `rootDir` iteratively (BFS via an explicit queue) and return absolute paths
|
|
1081
|
+
* of every supported source file that is not excluded by the configured filters.
|
|
1082
|
+
*
|
|
1083
|
+
* Iterative on purpose: a recursive walker burns one stack frame per nested
|
|
1084
|
+
* directory and crashes with `Maximum call stack size exceeded` on deeply nested
|
|
1085
|
+
* monorepos (issue #274). The queue-based form keeps memory bounded by the
|
|
1086
|
+
* frontier size rather than the deepest path.
|
|
1087
|
+
*/
|
|
1088
|
+
async findSourceFiles(rootDir) {
|
|
894
1089
|
const results = [];
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1090
|
+
const matchers = await this.buildExcludeMatchers(rootDir);
|
|
1091
|
+
const queue = [rootDir];
|
|
1092
|
+
while (queue.length > 0) {
|
|
1093
|
+
const dir = queue.pop();
|
|
1094
|
+
let entries;
|
|
1095
|
+
try {
|
|
1096
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1097
|
+
} catch {
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
for (const entry of entries) {
|
|
1101
|
+
const fullPath = path.join(dir, entry.name);
|
|
1102
|
+
const relPath = path.relative(rootDir, fullPath).replaceAll("\\", "/");
|
|
1103
|
+
if (entry.isDirectory()) {
|
|
1104
|
+
if (this.skipDirs.has(entry.name)) continue;
|
|
1105
|
+
if (isExcluded(`${relPath}/`, matchers)) continue;
|
|
1106
|
+
queue.push(fullPath);
|
|
1107
|
+
} else if (entry.isFile()) {
|
|
1108
|
+
if (!isSupportedSourceFile(entry.name)) continue;
|
|
1109
|
+
if (isExcluded(relPath, matchers)) continue;
|
|
1110
|
+
results.push(fullPath);
|
|
1111
|
+
}
|
|
902
1112
|
}
|
|
903
1113
|
}
|
|
904
1114
|
return results;
|
|
905
1115
|
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Compose the exclude matchers from `excludePatterns` and (optionally) `.gitignore`.
|
|
1118
|
+
* Returns a list of minimatch-compatible patterns; an empty list means "no excludes".
|
|
1119
|
+
*/
|
|
1120
|
+
async buildExcludeMatchers(rootDir) {
|
|
1121
|
+
const patterns = [...this.excludePatterns];
|
|
1122
|
+
if (this.respectGitignore) {
|
|
1123
|
+
const gitignorePatterns = await readGitignorePatterns(rootDir);
|
|
1124
|
+
patterns.push(...gitignorePatterns);
|
|
1125
|
+
}
|
|
1126
|
+
return patterns;
|
|
1127
|
+
}
|
|
906
1128
|
extractSymbols(content, fileId, relativePath) {
|
|
907
1129
|
const results = [];
|
|
908
1130
|
const lines = content.split("\n");
|
|
@@ -1672,6 +1894,7 @@ function emptyResult(durationMs = 0) {
|
|
|
1672
1894
|
|
|
1673
1895
|
// src/ingest/KnowledgeIngestor.ts
|
|
1674
1896
|
var CODE_NODE_TYPES = ["file", "function", "class", "method", "interface", "variable"];
|
|
1897
|
+
var DOCS_OWNED_BY_OTHER_INGESTORS = /* @__PURE__ */ new Set(["adr", "knowledge", "changes", "solutions"]);
|
|
1675
1898
|
var KnowledgeIngestor = class {
|
|
1676
1899
|
constructor(store) {
|
|
1677
1900
|
this.store = store;
|
|
@@ -1743,15 +1966,53 @@ var KnowledgeIngestor = class {
|
|
|
1743
1966
|
}
|
|
1744
1967
|
return buildResult(nodesAdded, edgesAdded, [], start);
|
|
1745
1968
|
}
|
|
1969
|
+
async ingestGeneralDocs(projectPath) {
|
|
1970
|
+
const start = Date.now();
|
|
1971
|
+
const errors = [];
|
|
1972
|
+
let nodesAdded = 0;
|
|
1973
|
+
let edgesAdded = 0;
|
|
1974
|
+
const files = /* @__PURE__ */ new Set();
|
|
1975
|
+
try {
|
|
1976
|
+
const entries = await fs2.readdir(projectPath, { withFileTypes: true });
|
|
1977
|
+
for (const entry of entries) {
|
|
1978
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1979
|
+
files.add(path3.join(projectPath, entry.name));
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
} catch {
|
|
1983
|
+
return emptyResult(Date.now() - start);
|
|
1984
|
+
}
|
|
1985
|
+
const docsRoot = path3.join(projectPath, "docs");
|
|
1986
|
+
try {
|
|
1987
|
+
const scanned = await this.scanDocsDir(docsRoot);
|
|
1988
|
+
for (const f of scanned) files.add(f);
|
|
1989
|
+
} catch {
|
|
1990
|
+
}
|
|
1991
|
+
for (const filePath of files) {
|
|
1992
|
+
try {
|
|
1993
|
+
const content = await fs2.readFile(filePath, "utf-8");
|
|
1994
|
+
const relPath = path3.relative(projectPath, filePath).replaceAll("\\", "/");
|
|
1995
|
+
const nodeId = `doc:${relPath}`;
|
|
1996
|
+
if (this.store.getNode(nodeId)) continue;
|
|
1997
|
+
this.store.addNode(parseDocumentNode(nodeId, filePath, relPath, content));
|
|
1998
|
+
nodesAdded++;
|
|
1999
|
+
edgesAdded += this.linkToCode(content, nodeId, "documents");
|
|
2000
|
+
} catch (err) {
|
|
2001
|
+
errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
return buildResult(nodesAdded, edgesAdded, errors, start);
|
|
2005
|
+
}
|
|
1746
2006
|
async ingestAll(projectPath, opts) {
|
|
1747
2007
|
const start = Date.now();
|
|
1748
2008
|
const adrDir = opts?.adrDir ?? path3.join(projectPath, "docs", "adr");
|
|
1749
|
-
const [adrResult, learningsResult, failuresResult] = await Promise.all([
|
|
2009
|
+
const [adrResult, learningsResult, failuresResult, docsResult] = await Promise.all([
|
|
1750
2010
|
this.ingestADRs(adrDir),
|
|
1751
2011
|
this.ingestLearnings(projectPath),
|
|
1752
|
-
this.ingestFailures(projectPath)
|
|
2012
|
+
this.ingestFailures(projectPath),
|
|
2013
|
+
this.ingestGeneralDocs(projectPath)
|
|
1753
2014
|
]);
|
|
1754
|
-
const merged = mergeResults(adrResult, learningsResult, failuresResult);
|
|
2015
|
+
const merged = mergeResults(adrResult, learningsResult, failuresResult, docsResult);
|
|
1755
2016
|
return { ...merged, durationMs: Date.now() - start };
|
|
1756
2017
|
}
|
|
1757
2018
|
linkToCode(content, sourceNodeId, edgeType) {
|
|
@@ -1786,7 +2047,22 @@ var KnowledgeIngestor = class {
|
|
|
1786
2047
|
const entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
1787
2048
|
for (const entry of entries) {
|
|
1788
2049
|
const fullPath = path3.join(dir, entry.name);
|
|
1789
|
-
if (entry.isDirectory() &&
|
|
2050
|
+
if (entry.isDirectory() && !DEFAULT_SKIP_DIRS.has(entry.name)) {
|
|
2051
|
+
results.push(...await this.findMarkdownFiles(fullPath));
|
|
2052
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
2053
|
+
results.push(fullPath);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
return results;
|
|
2057
|
+
}
|
|
2058
|
+
async scanDocsDir(docsRoot) {
|
|
2059
|
+
const results = [];
|
|
2060
|
+
const rootEntries = await fs2.readdir(docsRoot, { withFileTypes: true });
|
|
2061
|
+
for (const entry of rootEntries) {
|
|
2062
|
+
const fullPath = path3.join(docsRoot, entry.name);
|
|
2063
|
+
if (entry.isDirectory()) {
|
|
2064
|
+
if (DEFAULT_SKIP_DIRS.has(entry.name)) continue;
|
|
2065
|
+
if (DOCS_OWNED_BY_OTHER_INGESTORS.has(entry.name)) continue;
|
|
1790
2066
|
results.push(...await this.findMarkdownFiles(fullPath));
|
|
1791
2067
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1792
2068
|
results.push(fullPath);
|
|
@@ -1812,6 +2088,17 @@ function buildResult(nodesAdded, edgesAdded, errors, start) {
|
|
|
1812
2088
|
durationMs: Date.now() - start
|
|
1813
2089
|
};
|
|
1814
2090
|
}
|
|
2091
|
+
function parseDocumentNode(nodeId, filePath, relPath, content) {
|
|
2092
|
+
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
2093
|
+
const title = titleMatch ? titleMatch[1].trim() : path3.basename(filePath, ".md");
|
|
2094
|
+
return {
|
|
2095
|
+
id: nodeId,
|
|
2096
|
+
type: "document",
|
|
2097
|
+
name: title,
|
|
2098
|
+
path: filePath,
|
|
2099
|
+
metadata: { relPath }
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
1815
2102
|
function parseADRNode(nodeId, filePath, filename, content) {
|
|
1816
2103
|
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
1817
2104
|
const title = titleMatch ? titleMatch[1].trim() : filename;
|
|
@@ -1867,6 +2154,47 @@ function parseFailureSection(section) {
|
|
|
1867
2154
|
// src/ingest/BusinessKnowledgeIngestor.ts
|
|
1868
2155
|
import * as fs3 from "fs/promises";
|
|
1869
2156
|
import * as path4 from "path";
|
|
2157
|
+
import { z as z2 } from "zod";
|
|
2158
|
+
var BUG_TRACK_CATEGORIES = [
|
|
2159
|
+
"build-errors",
|
|
2160
|
+
"test-failures",
|
|
2161
|
+
"runtime-errors",
|
|
2162
|
+
"performance-issues",
|
|
2163
|
+
"database-issues",
|
|
2164
|
+
"security-issues",
|
|
2165
|
+
"ui-bugs",
|
|
2166
|
+
"integration-issues",
|
|
2167
|
+
"logic-errors"
|
|
2168
|
+
];
|
|
2169
|
+
var KNOWLEDGE_TRACK_CATEGORIES = [
|
|
2170
|
+
"architecture-patterns",
|
|
2171
|
+
"design-patterns",
|
|
2172
|
+
"tooling-decisions",
|
|
2173
|
+
"conventions",
|
|
2174
|
+
"dx",
|
|
2175
|
+
"best-practices"
|
|
2176
|
+
];
|
|
2177
|
+
var ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
2178
|
+
var SolutionBaseSchema = z2.object({
|
|
2179
|
+
module: z2.string().min(1),
|
|
2180
|
+
tags: z2.array(z2.string()),
|
|
2181
|
+
problem_type: z2.string().min(1),
|
|
2182
|
+
last_updated: z2.string().regex(ISO_DATE, "last_updated must be ISO date YYYY-MM-DD")
|
|
2183
|
+
});
|
|
2184
|
+
var SolutionDocFrontmatterSchema = z2.discriminatedUnion("track", [
|
|
2185
|
+
SolutionBaseSchema.merge(
|
|
2186
|
+
z2.object({
|
|
2187
|
+
track: z2.literal("bug-track"),
|
|
2188
|
+
category: z2.enum(BUG_TRACK_CATEGORIES)
|
|
2189
|
+
})
|
|
2190
|
+
),
|
|
2191
|
+
SolutionBaseSchema.merge(
|
|
2192
|
+
z2.object({
|
|
2193
|
+
track: z2.literal("knowledge-track"),
|
|
2194
|
+
category: z2.enum(KNOWLEDGE_TRACK_CATEGORIES)
|
|
2195
|
+
})
|
|
2196
|
+
)
|
|
2197
|
+
]);
|
|
1870
2198
|
var BUSINESS_KNOWLEDGE_TYPES = /* @__PURE__ */ new Set([
|
|
1871
2199
|
"business_rule",
|
|
1872
2200
|
"business_process",
|
|
@@ -1909,6 +2237,65 @@ var BusinessKnowledgeIngestor = class {
|
|
|
1909
2237
|
durationMs: Date.now() - start
|
|
1910
2238
|
};
|
|
1911
2239
|
}
|
|
2240
|
+
async ingestSolutions(solutionsDir) {
|
|
2241
|
+
const start = Date.now();
|
|
2242
|
+
const errors = [];
|
|
2243
|
+
let files;
|
|
2244
|
+
try {
|
|
2245
|
+
files = await this.findMarkdownFiles(solutionsDir);
|
|
2246
|
+
} catch {
|
|
2247
|
+
return emptyResult(Date.now() - start);
|
|
2248
|
+
}
|
|
2249
|
+
let nodesAdded = 0;
|
|
2250
|
+
for (const filePath of files) {
|
|
2251
|
+
try {
|
|
2252
|
+
const raw = await fs3.readFile(filePath, "utf-8");
|
|
2253
|
+
const parsed = parseSolutionFrontmatter(raw);
|
|
2254
|
+
if (!parsed) {
|
|
2255
|
+
errors.push(`${filePath}: no frontmatter found`);
|
|
2256
|
+
continue;
|
|
2257
|
+
}
|
|
2258
|
+
const validation = SolutionDocFrontmatterSchema.safeParse(parsed.frontmatter);
|
|
2259
|
+
if (!validation.success) {
|
|
2260
|
+
errors.push(`${filePath}: ${validation.error.message}`);
|
|
2261
|
+
continue;
|
|
2262
|
+
}
|
|
2263
|
+
if (validation.data.track !== "knowledge-track") continue;
|
|
2264
|
+
const relPath = path4.relative(solutionsDir, filePath).replaceAll("\\", "/");
|
|
2265
|
+
const filename = path4.basename(filePath, ".md");
|
|
2266
|
+
const nodeId = `bk:solutions:${validation.data.module}:${filename}`;
|
|
2267
|
+
const titleMatch = parsed.body.match(/^#\s+(.+)$/m);
|
|
2268
|
+
const name = titleMatch ? titleMatch[1].trim() : filename;
|
|
2269
|
+
const node = {
|
|
2270
|
+
id: nodeId,
|
|
2271
|
+
type: "business_concept",
|
|
2272
|
+
name,
|
|
2273
|
+
path: relPath,
|
|
2274
|
+
content: parsed.body.trim(),
|
|
2275
|
+
metadata: {
|
|
2276
|
+
domain: validation.data.module,
|
|
2277
|
+
tags: validation.data.tags,
|
|
2278
|
+
problem_type: validation.data.problem_type,
|
|
2279
|
+
last_updated: validation.data.last_updated,
|
|
2280
|
+
source: "solutions",
|
|
2281
|
+
category: validation.data.category
|
|
2282
|
+
}
|
|
2283
|
+
};
|
|
2284
|
+
this.store.addNode(node);
|
|
2285
|
+
nodesAdded++;
|
|
2286
|
+
} catch (err) {
|
|
2287
|
+
errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
return {
|
|
2291
|
+
nodesAdded,
|
|
2292
|
+
nodesUpdated: 0,
|
|
2293
|
+
edgesAdded: 0,
|
|
2294
|
+
edgesUpdated: 0,
|
|
2295
|
+
errors,
|
|
2296
|
+
durationMs: Date.now() - start
|
|
2297
|
+
};
|
|
2298
|
+
}
|
|
1912
2299
|
async createNodes(files, knowledgeDir, errors) {
|
|
1913
2300
|
const entries = [];
|
|
1914
2301
|
for (const filePath of files) {
|
|
@@ -1998,7 +2385,7 @@ var BusinessKnowledgeIngestor = class {
|
|
|
1998
2385
|
const entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
1999
2386
|
for (const entry of entries) {
|
|
2000
2387
|
const fullPath = path4.join(dir, entry.name);
|
|
2001
|
-
if (entry.isDirectory() &&
|
|
2388
|
+
if (entry.isDirectory() && !DEFAULT_SKIP_DIRS.has(entry.name)) {
|
|
2002
2389
|
results.push(...await this.findMarkdownFiles(fullPath));
|
|
2003
2390
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
2004
2391
|
results.push(fullPath);
|
|
@@ -2031,6 +2418,25 @@ function parseFrontmatter(raw) {
|
|
|
2031
2418
|
body
|
|
2032
2419
|
};
|
|
2033
2420
|
}
|
|
2421
|
+
function parseSolutionFrontmatter(raw) {
|
|
2422
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
2423
|
+
if (!match) return null;
|
|
2424
|
+
const yamlBlock = match[1];
|
|
2425
|
+
const body = match[2];
|
|
2426
|
+
const frontmatter = {};
|
|
2427
|
+
for (const line of yamlBlock.split(/\r?\n/)) {
|
|
2428
|
+
const kvMatch = line.match(/^(\w+):\s*(.+)$/);
|
|
2429
|
+
if (!kvMatch) continue;
|
|
2430
|
+
const key = kvMatch[1];
|
|
2431
|
+
const value = kvMatch[2].trim();
|
|
2432
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
2433
|
+
frontmatter[key] = value.slice(1, -1).split(",").map((s) => s.trim());
|
|
2434
|
+
} else {
|
|
2435
|
+
frontmatter[key] = value;
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
return { frontmatter, body };
|
|
2439
|
+
}
|
|
2034
2440
|
|
|
2035
2441
|
// src/ingest/DecisionIngestor.ts
|
|
2036
2442
|
import * as fs4 from "fs/promises";
|
|
@@ -2823,7 +3229,6 @@ var PlantUmlParser = class {
|
|
|
2823
3229
|
|
|
2824
3230
|
// src/ingest/DiagramParser.ts
|
|
2825
3231
|
var DIAGRAM_EXTENSIONS = /* @__PURE__ */ new Set([".mmd", ".mermaid", ".d2", ".puml", ".plantuml"]);
|
|
2826
|
-
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".harness"]);
|
|
2827
3232
|
var DiagramParser = class {
|
|
2828
3233
|
constructor(store) {
|
|
2829
3234
|
this.store = store;
|
|
@@ -2928,7 +3333,7 @@ var DiagramParser = class {
|
|
|
2928
3333
|
}
|
|
2929
3334
|
for (const entry of entries) {
|
|
2930
3335
|
if (entry.isDirectory()) {
|
|
2931
|
-
if (!
|
|
3336
|
+
if (!DEFAULT_SKIP_DIRS.has(entry.name)) {
|
|
2932
3337
|
await walk(path7.join(currentDir, entry.name));
|
|
2933
3338
|
}
|
|
2934
3339
|
} else if (entry.isFile()) {
|
|
@@ -3452,31 +3857,6 @@ var KnowledgeStagingAggregator = class {
|
|
|
3452
3857
|
// src/ingest/extractors/ExtractionRunner.ts
|
|
3453
3858
|
import * as fs9 from "fs/promises";
|
|
3454
3859
|
import * as path10 from "path";
|
|
3455
|
-
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
3456
|
-
"node_modules",
|
|
3457
|
-
"dist",
|
|
3458
|
-
"target",
|
|
3459
|
-
"build",
|
|
3460
|
-
".git",
|
|
3461
|
-
"__pycache__",
|
|
3462
|
-
".venv",
|
|
3463
|
-
".turbo",
|
|
3464
|
-
".harness",
|
|
3465
|
-
".next",
|
|
3466
|
-
".vscode",
|
|
3467
|
-
".idea",
|
|
3468
|
-
"vendor",
|
|
3469
|
-
"out",
|
|
3470
|
-
".gradle",
|
|
3471
|
-
".gradle-home",
|
|
3472
|
-
"bin",
|
|
3473
|
-
"obj",
|
|
3474
|
-
"venv",
|
|
3475
|
-
"_build",
|
|
3476
|
-
"deps",
|
|
3477
|
-
"coverage",
|
|
3478
|
-
".nyc_output"
|
|
3479
|
-
]);
|
|
3480
3860
|
var EXT_TO_LANGUAGE = {
|
|
3481
3861
|
".ts": "typescript",
|
|
3482
3862
|
".tsx": "typescript",
|
|
@@ -3655,7 +4035,7 @@ var ExtractionRunner = class {
|
|
|
3655
4035
|
}
|
|
3656
4036
|
for (const entry of entries) {
|
|
3657
4037
|
const fullPath = path10.join(dir, entry.name);
|
|
3658
|
-
if (entry.isDirectory() && !
|
|
4038
|
+
if (entry.isDirectory() && !DEFAULT_SKIP_DIRS.has(entry.name)) {
|
|
3659
4039
|
results.push(...await this.findSourceFiles(fullPath));
|
|
3660
4040
|
} else if (entry.isFile() && detectLanguage(fullPath) !== void 0) {
|
|
3661
4041
|
results.push(fullPath);
|
|
@@ -5417,6 +5797,25 @@ var KnowledgePipelineRunner = class {
|
|
|
5417
5797
|
durationMs: 0
|
|
5418
5798
|
};
|
|
5419
5799
|
}
|
|
5800
|
+
const solutionsDir = path12.join(options.projectDir, "docs", "solutions");
|
|
5801
|
+
let solutionsResult;
|
|
5802
|
+
try {
|
|
5803
|
+
solutionsResult = await bkIngestor.ingestSolutions(solutionsDir);
|
|
5804
|
+
} catch {
|
|
5805
|
+
solutionsResult = {
|
|
5806
|
+
nodesAdded: 0,
|
|
5807
|
+
nodesUpdated: 0,
|
|
5808
|
+
edgesAdded: 0,
|
|
5809
|
+
edgesUpdated: 0,
|
|
5810
|
+
errors: [],
|
|
5811
|
+
durationMs: 0
|
|
5812
|
+
};
|
|
5813
|
+
}
|
|
5814
|
+
bkResult = {
|
|
5815
|
+
...bkResult,
|
|
5816
|
+
nodesAdded: bkResult.nodesAdded + solutionsResult.nodesAdded,
|
|
5817
|
+
errors: [...bkResult.errors, ...solutionsResult.errors]
|
|
5818
|
+
};
|
|
5420
5819
|
const decisionsDir = path12.join(options.projectDir, "docs", "knowledge", "decisions");
|
|
5421
5820
|
const decisionIngestor = new DecisionIngestor(this.store);
|
|
5422
5821
|
let decisionResult;
|
|
@@ -8645,8 +9044,8 @@ function queryTraceability(store, options) {
|
|
|
8645
9044
|
}
|
|
8646
9045
|
|
|
8647
9046
|
// src/constraints/GraphConstraintAdapter.ts
|
|
8648
|
-
import { minimatch } from "minimatch";
|
|
8649
|
-
import { relative as
|
|
9047
|
+
import { minimatch as minimatch2 } from "minimatch";
|
|
9048
|
+
import { relative as relative6 } from "path";
|
|
8650
9049
|
var GraphConstraintAdapter = class {
|
|
8651
9050
|
constructor(store) {
|
|
8652
9051
|
this.store = store;
|
|
@@ -8675,8 +9074,8 @@ var GraphConstraintAdapter = class {
|
|
|
8675
9074
|
const { edges } = this.computeDependencyGraph();
|
|
8676
9075
|
const violations = [];
|
|
8677
9076
|
for (const edge of edges) {
|
|
8678
|
-
const fromRelative =
|
|
8679
|
-
const toRelative =
|
|
9077
|
+
const fromRelative = relative6(rootDir, edge.from).replaceAll("\\", "/");
|
|
9078
|
+
const toRelative = relative6(rootDir, edge.to).replaceAll("\\", "/");
|
|
8680
9079
|
const fromLayer = this.resolveLayer(fromRelative, layers);
|
|
8681
9080
|
const toLayer = this.resolveLayer(toRelative, layers);
|
|
8682
9081
|
if (!fromLayer || !toLayer) continue;
|
|
@@ -8697,7 +9096,7 @@ var GraphConstraintAdapter = class {
|
|
|
8697
9096
|
resolveLayer(filePath, layers) {
|
|
8698
9097
|
for (const layer of layers) {
|
|
8699
9098
|
for (const pattern of layer.patterns) {
|
|
8700
|
-
if (
|
|
9099
|
+
if (minimatch2(filePath, pattern)) {
|
|
8701
9100
|
return layer;
|
|
8702
9101
|
}
|
|
8703
9102
|
}
|
|
@@ -9496,6 +9895,7 @@ export {
|
|
|
9496
9895
|
D2Parser,
|
|
9497
9896
|
DEFAULT_BLOCKLIST,
|
|
9498
9897
|
DEFAULT_PATTERNS,
|
|
9898
|
+
DEFAULT_SKIP_DIRS,
|
|
9499
9899
|
DecisionIngestor,
|
|
9500
9900
|
DesignConstraintAdapter,
|
|
9501
9901
|
DesignIngestor,
|
|
@@ -9551,8 +9951,11 @@ export {
|
|
|
9551
9951
|
inferDomain,
|
|
9552
9952
|
linkToCode,
|
|
9553
9953
|
loadGraph,
|
|
9954
|
+
loadGraphMetadata,
|
|
9554
9955
|
normalizeIntent,
|
|
9555
9956
|
project,
|
|
9556
9957
|
queryTraceability,
|
|
9557
|
-
|
|
9958
|
+
resolveSkipDirs,
|
|
9959
|
+
saveGraph,
|
|
9960
|
+
skipDirGlobs
|
|
9558
9961
|
};
|