@harness-engineering/graph 0.7.1 → 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 +389 -26
- package/dist/index.mjs +387 -27
- 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,84 @@ 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
|
+
|
|
868
|
+
// src/ingest/CodeIngestor.ts
|
|
730
869
|
var SKIP_METHOD_NAMES = /* @__PURE__ */ new Set(["constructor", "if", "for", "while", "switch"]);
|
|
731
870
|
var FUNCTION_PATTERNS = {
|
|
732
871
|
python: /^def\s+(\w+)\s*\(/,
|
|
@@ -796,11 +935,55 @@ function isSupportedSourceFile(name) {
|
|
|
796
935
|
const ext = name.slice(name.lastIndexOf("."));
|
|
797
936
|
return SUPPORTED_EXTENSIONS.has(ext);
|
|
798
937
|
}
|
|
938
|
+
function isExcluded(relPath, patterns) {
|
|
939
|
+
if (patterns.length === 0) return false;
|
|
940
|
+
for (const pattern of patterns) {
|
|
941
|
+
if (minimatch(relPath, pattern, { dot: true, matchBase: false })) return true;
|
|
942
|
+
if (!pattern.includes("/") && minimatch(relPath, pattern, { dot: true, matchBase: true })) {
|
|
943
|
+
return true;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return false;
|
|
947
|
+
}
|
|
948
|
+
async function readGitignorePatterns(rootDir) {
|
|
949
|
+
let content;
|
|
950
|
+
try {
|
|
951
|
+
content = await fs.readFile(path.join(rootDir, ".gitignore"), "utf-8");
|
|
952
|
+
} catch {
|
|
953
|
+
return [];
|
|
954
|
+
}
|
|
955
|
+
const patterns = [];
|
|
956
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
957
|
+
const line = rawLine.trim();
|
|
958
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
959
|
+
if (line.startsWith("!")) continue;
|
|
960
|
+
const rooted = line.startsWith("/");
|
|
961
|
+
const stripped = rooted ? line.slice(1) : line;
|
|
962
|
+
const isDirOnly = stripped.endsWith("/");
|
|
963
|
+
const core = isDirOnly ? stripped.slice(0, -1) : stripped;
|
|
964
|
+
if (core === "") continue;
|
|
965
|
+
if (rooted) {
|
|
966
|
+
patterns.push(isDirOnly ? `${core}/**` : core);
|
|
967
|
+
if (isDirOnly) patterns.push(`${core}/`);
|
|
968
|
+
} else {
|
|
969
|
+
patterns.push(`**/${core}`);
|
|
970
|
+
patterns.push(`**/${core}/**`);
|
|
971
|
+
if (isDirOnly) patterns.push(`**/${core}/`);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return patterns;
|
|
975
|
+
}
|
|
799
976
|
var CodeIngestor = class {
|
|
800
|
-
constructor(store) {
|
|
977
|
+
constructor(store, options = {}) {
|
|
801
978
|
this.store = store;
|
|
979
|
+
this.skipDirs = resolveSkipDirs(options);
|
|
980
|
+
this.excludePatterns = options.excludePatterns ?? [];
|
|
981
|
+
this.respectGitignore = options.respectGitignore ?? true;
|
|
802
982
|
}
|
|
803
983
|
store;
|
|
984
|
+
skipDirs;
|
|
985
|
+
excludePatterns;
|
|
986
|
+
respectGitignore;
|
|
804
987
|
async ingest(rootDir) {
|
|
805
988
|
const start = Date.now();
|
|
806
989
|
const errors = [];
|
|
@@ -890,19 +1073,55 @@ var CodeIngestor = class {
|
|
|
890
1073
|
}
|
|
891
1074
|
files.add(relativePath);
|
|
892
1075
|
}
|
|
893
|
-
|
|
1076
|
+
/**
|
|
1077
|
+
* Walk `rootDir` iteratively (BFS via an explicit queue) and return absolute paths
|
|
1078
|
+
* of every supported source file that is not excluded by the configured filters.
|
|
1079
|
+
*
|
|
1080
|
+
* Iterative on purpose: a recursive walker burns one stack frame per nested
|
|
1081
|
+
* directory and crashes with `Maximum call stack size exceeded` on deeply nested
|
|
1082
|
+
* monorepos (issue #274). The queue-based form keeps memory bounded by the
|
|
1083
|
+
* frontier size rather than the deepest path.
|
|
1084
|
+
*/
|
|
1085
|
+
async findSourceFiles(rootDir) {
|
|
894
1086
|
const results = [];
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1087
|
+
const matchers = await this.buildExcludeMatchers(rootDir);
|
|
1088
|
+
const queue = [rootDir];
|
|
1089
|
+
while (queue.length > 0) {
|
|
1090
|
+
const dir = queue.pop();
|
|
1091
|
+
let entries;
|
|
1092
|
+
try {
|
|
1093
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1094
|
+
} catch {
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
1097
|
+
for (const entry of entries) {
|
|
1098
|
+
const fullPath = path.join(dir, entry.name);
|
|
1099
|
+
const relPath = path.relative(rootDir, fullPath).replaceAll("\\", "/");
|
|
1100
|
+
if (entry.isDirectory()) {
|
|
1101
|
+
if (this.skipDirs.has(entry.name)) continue;
|
|
1102
|
+
if (isExcluded(`${relPath}/`, matchers)) continue;
|
|
1103
|
+
queue.push(fullPath);
|
|
1104
|
+
} else if (entry.isFile()) {
|
|
1105
|
+
if (!isSupportedSourceFile(entry.name)) continue;
|
|
1106
|
+
if (isExcluded(relPath, matchers)) continue;
|
|
1107
|
+
results.push(fullPath);
|
|
1108
|
+
}
|
|
902
1109
|
}
|
|
903
1110
|
}
|
|
904
1111
|
return results;
|
|
905
1112
|
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Compose the exclude matchers from `excludePatterns` and (optionally) `.gitignore`.
|
|
1115
|
+
* Returns a list of minimatch-compatible patterns; an empty list means "no excludes".
|
|
1116
|
+
*/
|
|
1117
|
+
async buildExcludeMatchers(rootDir) {
|
|
1118
|
+
const patterns = [...this.excludePatterns];
|
|
1119
|
+
if (this.respectGitignore) {
|
|
1120
|
+
const gitignorePatterns = await readGitignorePatterns(rootDir);
|
|
1121
|
+
patterns.push(...gitignorePatterns);
|
|
1122
|
+
}
|
|
1123
|
+
return patterns;
|
|
1124
|
+
}
|
|
906
1125
|
extractSymbols(content, fileId, relativePath) {
|
|
907
1126
|
const results = [];
|
|
908
1127
|
const lines = content.split("\n");
|
|
@@ -1867,6 +2086,47 @@ function parseFailureSection(section) {
|
|
|
1867
2086
|
// src/ingest/BusinessKnowledgeIngestor.ts
|
|
1868
2087
|
import * as fs3 from "fs/promises";
|
|
1869
2088
|
import * as path4 from "path";
|
|
2089
|
+
import { z as z2 } from "zod";
|
|
2090
|
+
var BUG_TRACK_CATEGORIES = [
|
|
2091
|
+
"build-errors",
|
|
2092
|
+
"test-failures",
|
|
2093
|
+
"runtime-errors",
|
|
2094
|
+
"performance-issues",
|
|
2095
|
+
"database-issues",
|
|
2096
|
+
"security-issues",
|
|
2097
|
+
"ui-bugs",
|
|
2098
|
+
"integration-issues",
|
|
2099
|
+
"logic-errors"
|
|
2100
|
+
];
|
|
2101
|
+
var KNOWLEDGE_TRACK_CATEGORIES = [
|
|
2102
|
+
"architecture-patterns",
|
|
2103
|
+
"design-patterns",
|
|
2104
|
+
"tooling-decisions",
|
|
2105
|
+
"conventions",
|
|
2106
|
+
"dx",
|
|
2107
|
+
"best-practices"
|
|
2108
|
+
];
|
|
2109
|
+
var ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
2110
|
+
var SolutionBaseSchema = z2.object({
|
|
2111
|
+
module: z2.string().min(1),
|
|
2112
|
+
tags: z2.array(z2.string()),
|
|
2113
|
+
problem_type: z2.string().min(1),
|
|
2114
|
+
last_updated: z2.string().regex(ISO_DATE, "last_updated must be ISO date YYYY-MM-DD")
|
|
2115
|
+
});
|
|
2116
|
+
var SolutionDocFrontmatterSchema = z2.discriminatedUnion("track", [
|
|
2117
|
+
SolutionBaseSchema.merge(
|
|
2118
|
+
z2.object({
|
|
2119
|
+
track: z2.literal("bug-track"),
|
|
2120
|
+
category: z2.enum(BUG_TRACK_CATEGORIES)
|
|
2121
|
+
})
|
|
2122
|
+
),
|
|
2123
|
+
SolutionBaseSchema.merge(
|
|
2124
|
+
z2.object({
|
|
2125
|
+
track: z2.literal("knowledge-track"),
|
|
2126
|
+
category: z2.enum(KNOWLEDGE_TRACK_CATEGORIES)
|
|
2127
|
+
})
|
|
2128
|
+
)
|
|
2129
|
+
]);
|
|
1870
2130
|
var BUSINESS_KNOWLEDGE_TYPES = /* @__PURE__ */ new Set([
|
|
1871
2131
|
"business_rule",
|
|
1872
2132
|
"business_process",
|
|
@@ -1909,6 +2169,65 @@ var BusinessKnowledgeIngestor = class {
|
|
|
1909
2169
|
durationMs: Date.now() - start
|
|
1910
2170
|
};
|
|
1911
2171
|
}
|
|
2172
|
+
async ingestSolutions(solutionsDir) {
|
|
2173
|
+
const start = Date.now();
|
|
2174
|
+
const errors = [];
|
|
2175
|
+
let files;
|
|
2176
|
+
try {
|
|
2177
|
+
files = await this.findMarkdownFiles(solutionsDir);
|
|
2178
|
+
} catch {
|
|
2179
|
+
return emptyResult(Date.now() - start);
|
|
2180
|
+
}
|
|
2181
|
+
let nodesAdded = 0;
|
|
2182
|
+
for (const filePath of files) {
|
|
2183
|
+
try {
|
|
2184
|
+
const raw = await fs3.readFile(filePath, "utf-8");
|
|
2185
|
+
const parsed = parseSolutionFrontmatter(raw);
|
|
2186
|
+
if (!parsed) {
|
|
2187
|
+
errors.push(`${filePath}: no frontmatter found`);
|
|
2188
|
+
continue;
|
|
2189
|
+
}
|
|
2190
|
+
const validation = SolutionDocFrontmatterSchema.safeParse(parsed.frontmatter);
|
|
2191
|
+
if (!validation.success) {
|
|
2192
|
+
errors.push(`${filePath}: ${validation.error.message}`);
|
|
2193
|
+
continue;
|
|
2194
|
+
}
|
|
2195
|
+
if (validation.data.track !== "knowledge-track") continue;
|
|
2196
|
+
const relPath = path4.relative(solutionsDir, filePath).replaceAll("\\", "/");
|
|
2197
|
+
const filename = path4.basename(filePath, ".md");
|
|
2198
|
+
const nodeId = `bk:solutions:${validation.data.module}:${filename}`;
|
|
2199
|
+
const titleMatch = parsed.body.match(/^#\s+(.+)$/m);
|
|
2200
|
+
const name = titleMatch ? titleMatch[1].trim() : filename;
|
|
2201
|
+
const node = {
|
|
2202
|
+
id: nodeId,
|
|
2203
|
+
type: "business_concept",
|
|
2204
|
+
name,
|
|
2205
|
+
path: relPath,
|
|
2206
|
+
content: parsed.body.trim(),
|
|
2207
|
+
metadata: {
|
|
2208
|
+
domain: validation.data.module,
|
|
2209
|
+
tags: validation.data.tags,
|
|
2210
|
+
problem_type: validation.data.problem_type,
|
|
2211
|
+
last_updated: validation.data.last_updated,
|
|
2212
|
+
source: "solutions",
|
|
2213
|
+
category: validation.data.category
|
|
2214
|
+
}
|
|
2215
|
+
};
|
|
2216
|
+
this.store.addNode(node);
|
|
2217
|
+
nodesAdded++;
|
|
2218
|
+
} catch (err) {
|
|
2219
|
+
errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
return {
|
|
2223
|
+
nodesAdded,
|
|
2224
|
+
nodesUpdated: 0,
|
|
2225
|
+
edgesAdded: 0,
|
|
2226
|
+
edgesUpdated: 0,
|
|
2227
|
+
errors,
|
|
2228
|
+
durationMs: Date.now() - start
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
1912
2231
|
async createNodes(files, knowledgeDir, errors) {
|
|
1913
2232
|
const entries = [];
|
|
1914
2233
|
for (const filePath of files) {
|
|
@@ -2031,6 +2350,25 @@ function parseFrontmatter(raw) {
|
|
|
2031
2350
|
body
|
|
2032
2351
|
};
|
|
2033
2352
|
}
|
|
2353
|
+
function parseSolutionFrontmatter(raw) {
|
|
2354
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
2355
|
+
if (!match) return null;
|
|
2356
|
+
const yamlBlock = match[1];
|
|
2357
|
+
const body = match[2];
|
|
2358
|
+
const frontmatter = {};
|
|
2359
|
+
for (const line of yamlBlock.split(/\r?\n/)) {
|
|
2360
|
+
const kvMatch = line.match(/^(\w+):\s*(.+)$/);
|
|
2361
|
+
if (!kvMatch) continue;
|
|
2362
|
+
const key = kvMatch[1];
|
|
2363
|
+
const value = kvMatch[2].trim();
|
|
2364
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
2365
|
+
frontmatter[key] = value.slice(1, -1).split(",").map((s) => s.trim());
|
|
2366
|
+
} else {
|
|
2367
|
+
frontmatter[key] = value;
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
return { frontmatter, body };
|
|
2371
|
+
}
|
|
2034
2372
|
|
|
2035
2373
|
// src/ingest/DecisionIngestor.ts
|
|
2036
2374
|
import * as fs4 from "fs/promises";
|
|
@@ -5417,6 +5755,25 @@ var KnowledgePipelineRunner = class {
|
|
|
5417
5755
|
durationMs: 0
|
|
5418
5756
|
};
|
|
5419
5757
|
}
|
|
5758
|
+
const solutionsDir = path12.join(options.projectDir, "docs", "solutions");
|
|
5759
|
+
let solutionsResult;
|
|
5760
|
+
try {
|
|
5761
|
+
solutionsResult = await bkIngestor.ingestSolutions(solutionsDir);
|
|
5762
|
+
} catch {
|
|
5763
|
+
solutionsResult = {
|
|
5764
|
+
nodesAdded: 0,
|
|
5765
|
+
nodesUpdated: 0,
|
|
5766
|
+
edgesAdded: 0,
|
|
5767
|
+
edgesUpdated: 0,
|
|
5768
|
+
errors: [],
|
|
5769
|
+
durationMs: 0
|
|
5770
|
+
};
|
|
5771
|
+
}
|
|
5772
|
+
bkResult = {
|
|
5773
|
+
...bkResult,
|
|
5774
|
+
nodesAdded: bkResult.nodesAdded + solutionsResult.nodesAdded,
|
|
5775
|
+
errors: [...bkResult.errors, ...solutionsResult.errors]
|
|
5776
|
+
};
|
|
5420
5777
|
const decisionsDir = path12.join(options.projectDir, "docs", "knowledge", "decisions");
|
|
5421
5778
|
const decisionIngestor = new DecisionIngestor(this.store);
|
|
5422
5779
|
let decisionResult;
|
|
@@ -8645,7 +9002,7 @@ function queryTraceability(store, options) {
|
|
|
8645
9002
|
}
|
|
8646
9003
|
|
|
8647
9004
|
// src/constraints/GraphConstraintAdapter.ts
|
|
8648
|
-
import { minimatch } from "minimatch";
|
|
9005
|
+
import { minimatch as minimatch2 } from "minimatch";
|
|
8649
9006
|
import { relative as relative5 } from "path";
|
|
8650
9007
|
var GraphConstraintAdapter = class {
|
|
8651
9008
|
constructor(store) {
|
|
@@ -8697,7 +9054,7 @@ var GraphConstraintAdapter = class {
|
|
|
8697
9054
|
resolveLayer(filePath, layers) {
|
|
8698
9055
|
for (const layer of layers) {
|
|
8699
9056
|
for (const pattern of layer.patterns) {
|
|
8700
|
-
if (
|
|
9057
|
+
if (minimatch2(filePath, pattern)) {
|
|
8701
9058
|
return layer;
|
|
8702
9059
|
}
|
|
8703
9060
|
}
|
|
@@ -9496,6 +9853,7 @@ export {
|
|
|
9496
9853
|
D2Parser,
|
|
9497
9854
|
DEFAULT_BLOCKLIST,
|
|
9498
9855
|
DEFAULT_PATTERNS,
|
|
9856
|
+
DEFAULT_SKIP_DIRS,
|
|
9499
9857
|
DecisionIngestor,
|
|
9500
9858
|
DesignConstraintAdapter,
|
|
9501
9859
|
DesignIngestor,
|
|
@@ -9551,8 +9909,10 @@ export {
|
|
|
9551
9909
|
inferDomain,
|
|
9552
9910
|
linkToCode,
|
|
9553
9911
|
loadGraph,
|
|
9912
|
+
loadGraphMetadata,
|
|
9554
9913
|
normalizeIntent,
|
|
9555
9914
|
project,
|
|
9556
9915
|
queryTraceability,
|
|
9916
|
+
resolveSkipDirs,
|
|
9557
9917
|
saveGraph
|
|
9558
9918
|
};
|