@harness-engineering/graph 0.5.0 → 0.7.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 +180 -16
- package/dist/index.d.ts +180 -16
- package/dist/index.js +1452 -715
- package/dist/index.mjs +1447 -715
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -45,6 +45,9 @@ __export(index_exports, {
|
|
|
45
45
|
ContradictionDetector: () => ContradictionDetector,
|
|
46
46
|
CoverageScorer: () => CoverageScorer,
|
|
47
47
|
D2Parser: () => D2Parser,
|
|
48
|
+
DEFAULT_BLOCKLIST: () => DEFAULT_BLOCKLIST,
|
|
49
|
+
DEFAULT_PATTERNS: () => DEFAULT_PATTERNS,
|
|
50
|
+
DecisionIngestor: () => DecisionIngestor,
|
|
48
51
|
DesignConstraintAdapter: () => DesignConstraintAdapter,
|
|
49
52
|
DesignIngestor: () => DesignIngestor,
|
|
50
53
|
DiagramParser: () => DiagramParser,
|
|
@@ -69,6 +72,7 @@ __export(index_exports, {
|
|
|
69
72
|
ImageAnalysisExtractor: () => ImageAnalysisExtractor,
|
|
70
73
|
IntentClassifier: () => IntentClassifier,
|
|
71
74
|
JiraConnector: () => JiraConnector,
|
|
75
|
+
KnowledgeDocMaterializer: () => KnowledgeDocMaterializer,
|
|
72
76
|
KnowledgeIngestor: () => KnowledgeIngestor,
|
|
73
77
|
KnowledgePipelineRunner: () => KnowledgePipelineRunner,
|
|
74
78
|
KnowledgeStagingAggregator: () => KnowledgeStagingAggregator,
|
|
@@ -95,6 +99,7 @@ __export(index_exports, {
|
|
|
95
99
|
createExtractionRunner: () => createExtractionRunner,
|
|
96
100
|
detectLanguage: () => detectLanguage,
|
|
97
101
|
groupNodesByImpact: () => groupNodesByImpact,
|
|
102
|
+
inferDomain: () => inferDomain,
|
|
98
103
|
linkToCode: () => linkToCode,
|
|
99
104
|
loadGraph: () => loadGraph,
|
|
100
105
|
normalizeIntent: () => normalizeIntent,
|
|
@@ -130,7 +135,7 @@ var NODE_TYPES = [
|
|
|
130
135
|
"build",
|
|
131
136
|
"test_result",
|
|
132
137
|
"execution_outcome",
|
|
133
|
-
// Observability
|
|
138
|
+
// Observability — reserved for future tracing/metrics integration
|
|
134
139
|
"span",
|
|
135
140
|
"metric",
|
|
136
141
|
"log",
|
|
@@ -177,7 +182,7 @@ var EDGE_TYPES = [
|
|
|
177
182
|
"triggered_by",
|
|
178
183
|
"failed_in",
|
|
179
184
|
"outcome_of",
|
|
180
|
-
// Execution relationships
|
|
185
|
+
// Execution relationships — reserved for future observability integration
|
|
181
186
|
"executed_by",
|
|
182
187
|
"measured_by",
|
|
183
188
|
// Design relationships
|
|
@@ -252,7 +257,7 @@ function streamGraphJson(filePath, nodes, edges) {
|
|
|
252
257
|
stream.write(JSON.stringify(edges[i]));
|
|
253
258
|
}
|
|
254
259
|
stream.write("]}");
|
|
255
|
-
stream.end(resolve);
|
|
260
|
+
stream.end(() => resolve());
|
|
256
261
|
});
|
|
257
262
|
}
|
|
258
263
|
async function saveGraph(dirPath, nodes, edges) {
|
|
@@ -275,15 +280,19 @@ async function loadGraph(dirPath) {
|
|
|
275
280
|
await (0, import_promises.access)(metaPath);
|
|
276
281
|
await (0, import_promises.access)(graphPath);
|
|
277
282
|
} catch {
|
|
278
|
-
return
|
|
283
|
+
return { status: "not_found" };
|
|
279
284
|
}
|
|
280
285
|
const metaContent = await (0, import_promises.readFile)(metaPath, "utf-8");
|
|
281
286
|
const metadata = JSON.parse(metaContent);
|
|
282
287
|
if (metadata.schemaVersion !== CURRENT_SCHEMA_VERSION) {
|
|
283
|
-
return
|
|
288
|
+
return {
|
|
289
|
+
status: "schema_mismatch",
|
|
290
|
+
found: metadata.schemaVersion,
|
|
291
|
+
expected: CURRENT_SCHEMA_VERSION
|
|
292
|
+
};
|
|
284
293
|
}
|
|
285
294
|
const graphContent = await (0, import_promises.readFile)(graphPath, "utf-8");
|
|
286
|
-
return JSON.parse(graphContent);
|
|
295
|
+
return { status: "loaded", graph: JSON.parse(graphContent) };
|
|
287
296
|
}
|
|
288
297
|
|
|
289
298
|
// src/store/GraphStore.ts
|
|
@@ -458,8 +467,15 @@ var GraphStore = class {
|
|
|
458
467
|
await saveGraph(dirPath, allNodes, allEdges);
|
|
459
468
|
}
|
|
460
469
|
async load(dirPath) {
|
|
461
|
-
const
|
|
462
|
-
if (
|
|
470
|
+
const result = await loadGraph(dirPath);
|
|
471
|
+
if (result.status === "not_found") return false;
|
|
472
|
+
if (result.status === "schema_mismatch") {
|
|
473
|
+
console.warn(
|
|
474
|
+
`[graph] Schema version mismatch: graph was saved with v${result.found} but current is v${result.expected}. Graph will be rebuilt from scratch. Run \`harness graph scan\` to repopulate.`
|
|
475
|
+
);
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
const data = result.graph;
|
|
463
479
|
this.clear();
|
|
464
480
|
for (const node of data.nodes) {
|
|
465
481
|
this.nodeMap.set(node.id, { ...node });
|
|
@@ -903,9 +919,10 @@ var CodeIngestor = class {
|
|
|
903
919
|
let edgesAdded = 0;
|
|
904
920
|
const files = await this.findSourceFiles(rootDir);
|
|
905
921
|
const nameToFiles = /* @__PURE__ */ new Map();
|
|
922
|
+
const contentCache = /* @__PURE__ */ new Map();
|
|
906
923
|
for (const filePath of files) {
|
|
907
924
|
try {
|
|
908
|
-
const result = await this.processFile(filePath, rootDir, nameToFiles);
|
|
925
|
+
const result = await this.processFile(filePath, rootDir, nameToFiles, contentCache);
|
|
909
926
|
nodesAdded += result.nodesAdded;
|
|
910
927
|
edgesAdded += result.edgesAdded;
|
|
911
928
|
} catch (err) {
|
|
@@ -915,7 +932,8 @@ var CodeIngestor = class {
|
|
|
915
932
|
for (const filePath of files) {
|
|
916
933
|
try {
|
|
917
934
|
const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
918
|
-
const content =
|
|
935
|
+
const content = contentCache.get(filePath);
|
|
936
|
+
if (content === void 0) continue;
|
|
919
937
|
const callsEdges = this.extractCallsEdgesForFile(relativePath, content, nameToFiles);
|
|
920
938
|
for (const edge of callsEdges) {
|
|
921
939
|
this.store.addEdge(edge);
|
|
@@ -926,7 +944,8 @@ var CodeIngestor = class {
|
|
|
926
944
|
}
|
|
927
945
|
for (const filePath of files) {
|
|
928
946
|
try {
|
|
929
|
-
const content =
|
|
947
|
+
const content = contentCache.get(filePath);
|
|
948
|
+
if (content === void 0) continue;
|
|
930
949
|
edgesAdded += this.extractReqAnnotationsForFile(filePath, content, rootDir);
|
|
931
950
|
} catch {
|
|
932
951
|
}
|
|
@@ -940,11 +959,12 @@ var CodeIngestor = class {
|
|
|
940
959
|
durationMs: Date.now() - start
|
|
941
960
|
};
|
|
942
961
|
}
|
|
943
|
-
async processFile(filePath, rootDir, nameToFiles) {
|
|
962
|
+
async processFile(filePath, rootDir, nameToFiles, contentCache) {
|
|
944
963
|
let nodesAdded = 0;
|
|
945
964
|
let edgesAdded = 0;
|
|
946
965
|
const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
947
966
|
const content = await fs.readFile(filePath, "utf-8");
|
|
967
|
+
contentCache.set(filePath, content);
|
|
948
968
|
const stat2 = await fs.stat(filePath);
|
|
949
969
|
const fileId = `file:${relativePath}`;
|
|
950
970
|
const fileNode = {
|
|
@@ -2122,9 +2142,119 @@ function parseFrontmatter(raw) {
|
|
|
2122
2142
|
};
|
|
2123
2143
|
}
|
|
2124
2144
|
|
|
2125
|
-
// src/ingest/
|
|
2145
|
+
// src/ingest/DecisionIngestor.ts
|
|
2126
2146
|
var fs4 = __toESM(require("fs/promises"));
|
|
2127
2147
|
var path5 = __toESM(require("path"));
|
|
2148
|
+
var CODE_NODE_TYPES3 = [
|
|
2149
|
+
"file",
|
|
2150
|
+
"function",
|
|
2151
|
+
"class",
|
|
2152
|
+
"method",
|
|
2153
|
+
"interface",
|
|
2154
|
+
"variable"
|
|
2155
|
+
];
|
|
2156
|
+
var DecisionIngestor = class {
|
|
2157
|
+
constructor(store) {
|
|
2158
|
+
this.store = store;
|
|
2159
|
+
}
|
|
2160
|
+
store;
|
|
2161
|
+
async ingest(decisionsDir) {
|
|
2162
|
+
const start = Date.now();
|
|
2163
|
+
const errors = [];
|
|
2164
|
+
let files;
|
|
2165
|
+
try {
|
|
2166
|
+
files = await this.findDecisionFiles(decisionsDir);
|
|
2167
|
+
} catch {
|
|
2168
|
+
return emptyResult(Date.now() - start);
|
|
2169
|
+
}
|
|
2170
|
+
let nodesAdded = 0;
|
|
2171
|
+
let edgesAdded = 0;
|
|
2172
|
+
for (const filePath of files) {
|
|
2173
|
+
try {
|
|
2174
|
+
const raw = await fs4.readFile(filePath, "utf-8");
|
|
2175
|
+
const parsed = this.parseFrontmatter(raw);
|
|
2176
|
+
if (!parsed) continue;
|
|
2177
|
+
const { frontmatter, body } = parsed;
|
|
2178
|
+
if (!frontmatter.number || !frontmatter.title) continue;
|
|
2179
|
+
const filename = path5.basename(filePath, ".md");
|
|
2180
|
+
const nodeId = `decision:${filename}`;
|
|
2181
|
+
const node = {
|
|
2182
|
+
id: nodeId,
|
|
2183
|
+
type: "decision",
|
|
2184
|
+
name: frontmatter.title,
|
|
2185
|
+
path: filePath,
|
|
2186
|
+
content: body.trim(),
|
|
2187
|
+
metadata: {
|
|
2188
|
+
number: frontmatter.number,
|
|
2189
|
+
...frontmatter.date && { date: frontmatter.date },
|
|
2190
|
+
...frontmatter.status && { status: frontmatter.status },
|
|
2191
|
+
...frontmatter.tier && { tier: frontmatter.tier },
|
|
2192
|
+
...frontmatter.source && { source: frontmatter.source },
|
|
2193
|
+
...frontmatter.supersedes && { supersedes: frontmatter.supersedes }
|
|
2194
|
+
}
|
|
2195
|
+
};
|
|
2196
|
+
this.store.addNode(node);
|
|
2197
|
+
nodesAdded++;
|
|
2198
|
+
edgesAdded += this.linkToCode(body, nodeId);
|
|
2199
|
+
} catch (err) {
|
|
2200
|
+
errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
return {
|
|
2204
|
+
nodesAdded,
|
|
2205
|
+
nodesUpdated: 0,
|
|
2206
|
+
edgesAdded,
|
|
2207
|
+
edgesUpdated: 0,
|
|
2208
|
+
errors,
|
|
2209
|
+
durationMs: Date.now() - start
|
|
2210
|
+
};
|
|
2211
|
+
}
|
|
2212
|
+
parseFrontmatter(raw) {
|
|
2213
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
2214
|
+
if (!match) return null;
|
|
2215
|
+
const yamlBlock = match[1];
|
|
2216
|
+
const body = match[2];
|
|
2217
|
+
const frontmatter = {};
|
|
2218
|
+
for (const line of yamlBlock.split("\n")) {
|
|
2219
|
+
const kvMatch = line.match(/^(\w+):\s*(.+)$/);
|
|
2220
|
+
if (!kvMatch) continue;
|
|
2221
|
+
frontmatter[kvMatch[1]] = kvMatch[2].trim();
|
|
2222
|
+
}
|
|
2223
|
+
if (!frontmatter.number || !frontmatter.title) return null;
|
|
2224
|
+
return {
|
|
2225
|
+
frontmatter,
|
|
2226
|
+
body
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
linkToCode(content, sourceNodeId) {
|
|
2230
|
+
let count = 0;
|
|
2231
|
+
for (const nodeType of CODE_NODE_TYPES3) {
|
|
2232
|
+
const codeNodes = this.store.findNodes({ type: nodeType });
|
|
2233
|
+
for (const node of codeNodes) {
|
|
2234
|
+
if (node.name.length < 3) continue;
|
|
2235
|
+
const escaped = node.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2236
|
+
const namePattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
2237
|
+
if (namePattern.test(content)) {
|
|
2238
|
+
this.store.addEdge({
|
|
2239
|
+
from: sourceNodeId,
|
|
2240
|
+
to: node.id,
|
|
2241
|
+
type: "decided"
|
|
2242
|
+
});
|
|
2243
|
+
count++;
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
return count;
|
|
2248
|
+
}
|
|
2249
|
+
async findDecisionFiles(dir) {
|
|
2250
|
+
const entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
2251
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".md") && e.name !== "README.md").map((e) => path5.join(dir, e.name));
|
|
2252
|
+
}
|
|
2253
|
+
};
|
|
2254
|
+
|
|
2255
|
+
// src/ingest/RequirementIngestor.ts
|
|
2256
|
+
var fs5 = __toESM(require("fs/promises"));
|
|
2257
|
+
var path6 = __toESM(require("path"));
|
|
2128
2258
|
var REQUIREMENT_SECTIONS = [
|
|
2129
2259
|
"Observable Truths",
|
|
2130
2260
|
"Success Criteria",
|
|
@@ -2141,7 +2271,7 @@ function detectEarsPattern(text) {
|
|
|
2141
2271
|
if (/^the\s+\w+\s+shall\b/.test(lower)) return "ubiquitous";
|
|
2142
2272
|
return void 0;
|
|
2143
2273
|
}
|
|
2144
|
-
var
|
|
2274
|
+
var CODE_NODE_TYPES4 = ["file", "function", "class", "method", "interface", "variable"];
|
|
2145
2275
|
var RequirementIngestor = class {
|
|
2146
2276
|
constructor(store) {
|
|
2147
2277
|
this.store = store;
|
|
@@ -2159,8 +2289,8 @@ var RequirementIngestor = class {
|
|
|
2159
2289
|
let edgesAdded = 0;
|
|
2160
2290
|
let featureDirs;
|
|
2161
2291
|
try {
|
|
2162
|
-
const entries = await
|
|
2163
|
-
featureDirs = entries.filter((e) => e.isDirectory()).map((e) =>
|
|
2292
|
+
const entries = await fs5.readdir(specsDir, { withFileTypes: true });
|
|
2293
|
+
featureDirs = entries.filter((e) => e.isDirectory()).map((e) => path6.join(specsDir, e.name));
|
|
2164
2294
|
} catch {
|
|
2165
2295
|
return emptyResult(Date.now() - start);
|
|
2166
2296
|
}
|
|
@@ -2179,11 +2309,11 @@ var RequirementIngestor = class {
|
|
|
2179
2309
|
};
|
|
2180
2310
|
}
|
|
2181
2311
|
async ingestFeatureDir(featureDir, errors) {
|
|
2182
|
-
const featureName =
|
|
2183
|
-
const specPath =
|
|
2312
|
+
const featureName = path6.basename(featureDir);
|
|
2313
|
+
const specPath = path6.join(featureDir, "proposal.md").replaceAll("\\", "/");
|
|
2184
2314
|
let content;
|
|
2185
2315
|
try {
|
|
2186
|
-
content = await
|
|
2316
|
+
content = await fs5.readFile(specPath, "utf-8");
|
|
2187
2317
|
} catch {
|
|
2188
2318
|
return { nodesAdded: 0, edgesAdded: 0 };
|
|
2189
2319
|
}
|
|
@@ -2200,7 +2330,7 @@ var RequirementIngestor = class {
|
|
|
2200
2330
|
this.store.addNode({
|
|
2201
2331
|
id: specNodeId,
|
|
2202
2332
|
type: "document",
|
|
2203
|
-
name:
|
|
2333
|
+
name: path6.basename(specPath),
|
|
2204
2334
|
path: specPath,
|
|
2205
2335
|
metadata: { featureName }
|
|
2206
2336
|
});
|
|
@@ -2315,9 +2445,9 @@ var RequirementIngestor = class {
|
|
|
2315
2445
|
for (const node of fileNodes) {
|
|
2316
2446
|
if (!node.path) continue;
|
|
2317
2447
|
const normalizedPath = node.path.replace(/\\/g, "/");
|
|
2318
|
-
const isCodeMatch = normalizedPath.includes("packages/") &&
|
|
2448
|
+
const isCodeMatch = normalizedPath.includes("packages/") && path6.basename(normalizedPath).includes(featureName);
|
|
2319
2449
|
const isTestMatch = normalizedPath.includes("/tests/") && // platform-safe
|
|
2320
|
-
|
|
2450
|
+
path6.basename(normalizedPath).includes(featureName);
|
|
2321
2451
|
if (isCodeMatch && !isTestMatch) {
|
|
2322
2452
|
this.store.addEdge({
|
|
2323
2453
|
from: reqId,
|
|
@@ -2346,7 +2476,7 @@ var RequirementIngestor = class {
|
|
|
2346
2476
|
*/
|
|
2347
2477
|
linkByKeywordOverlap(reqId, reqText) {
|
|
2348
2478
|
let count = 0;
|
|
2349
|
-
for (const nodeType of
|
|
2479
|
+
for (const nodeType of CODE_NODE_TYPES4) {
|
|
2350
2480
|
const codeNodes = this.store.findNodes({ type: nodeType });
|
|
2351
2481
|
for (const node of codeNodes) {
|
|
2352
2482
|
if (node.name.length < 3) continue;
|
|
@@ -2369,20 +2499,241 @@ var RequirementIngestor = class {
|
|
|
2369
2499
|
}
|
|
2370
2500
|
};
|
|
2371
2501
|
|
|
2502
|
+
// src/ingest/domain-inference.ts
|
|
2503
|
+
var DEFAULT_PATTERNS = [
|
|
2504
|
+
"packages/<dir>",
|
|
2505
|
+
"apps/<dir>",
|
|
2506
|
+
"services/<dir>",
|
|
2507
|
+
"src/<dir>",
|
|
2508
|
+
"lib/<dir>"
|
|
2509
|
+
];
|
|
2510
|
+
var DEFAULT_BLOCKLIST = /* @__PURE__ */ new Set([
|
|
2511
|
+
"node_modules",
|
|
2512
|
+
".harness",
|
|
2513
|
+
"dist",
|
|
2514
|
+
"build",
|
|
2515
|
+
".git",
|
|
2516
|
+
"coverage",
|
|
2517
|
+
".next",
|
|
2518
|
+
".turbo",
|
|
2519
|
+
".cache",
|
|
2520
|
+
"out",
|
|
2521
|
+
"tmp"
|
|
2522
|
+
]);
|
|
2523
|
+
var CODE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
2524
|
+
function matchPattern(filePath, pattern) {
|
|
2525
|
+
const patternParts = pattern.split("/").filter((s) => s.length > 0);
|
|
2526
|
+
if (patternParts.length !== 2 || patternParts[1] !== "<dir>") {
|
|
2527
|
+
return null;
|
|
2528
|
+
}
|
|
2529
|
+
const prefix = patternParts[0];
|
|
2530
|
+
const pathParts = filePath.split("/").filter((s) => s.length > 0);
|
|
2531
|
+
if (pathParts.length < 2) return null;
|
|
2532
|
+
if (pathParts[0] !== prefix) return null;
|
|
2533
|
+
let dir = pathParts[1];
|
|
2534
|
+
if (dir.length === 0) return null;
|
|
2535
|
+
if (CODE_EXTENSIONS.some((ext) => dir.endsWith(ext))) {
|
|
2536
|
+
const dotIdx = dir.lastIndexOf(".");
|
|
2537
|
+
if (dotIdx > 0) dir = dir.slice(0, dotIdx);
|
|
2538
|
+
}
|
|
2539
|
+
if (dir.length === 0) return null;
|
|
2540
|
+
return dir;
|
|
2541
|
+
}
|
|
2542
|
+
function inferDomain(node, options = {}) {
|
|
2543
|
+
if (node.metadata?.domain && typeof node.metadata.domain === "string" && node.metadata.domain.length > 0) {
|
|
2544
|
+
return node.metadata.domain;
|
|
2545
|
+
}
|
|
2546
|
+
const filePath = typeof node.path === "string" ? node.path : "";
|
|
2547
|
+
const blocklist = new Set(DEFAULT_BLOCKLIST);
|
|
2548
|
+
if (options.extraBlocklist) {
|
|
2549
|
+
for (const seg of options.extraBlocklist) {
|
|
2550
|
+
if (seg && seg.length > 0) blocklist.add(seg);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
if (filePath.length > 0) {
|
|
2554
|
+
const extraPatterns = options.extraPatterns ?? [];
|
|
2555
|
+
for (const pattern of extraPatterns) {
|
|
2556
|
+
const dir = matchPattern(filePath, pattern);
|
|
2557
|
+
if (dir !== null) {
|
|
2558
|
+
if (blocklist.has(dir)) return "unknown";
|
|
2559
|
+
return dir;
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
for (const pattern of DEFAULT_PATTERNS) {
|
|
2563
|
+
const dir = matchPattern(filePath, pattern);
|
|
2564
|
+
if (dir !== null) {
|
|
2565
|
+
if (blocklist.has(dir)) return "unknown";
|
|
2566
|
+
return dir;
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
const segments = filePath.split("/").filter((s) => s.length > 0);
|
|
2570
|
+
if (segments.length > 0) {
|
|
2571
|
+
const first = segments[0];
|
|
2572
|
+
if (!blocklist.has(first)) return first;
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
const source = node.metadata?.source;
|
|
2576
|
+
if (source === "knowledge-linker" || source === "connector") {
|
|
2577
|
+
const connector = node.metadata?.connectorName;
|
|
2578
|
+
if (typeof connector === "string" && connector.length > 0) return connector;
|
|
2579
|
+
return "general";
|
|
2580
|
+
}
|
|
2581
|
+
return "unknown";
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2372
2584
|
// src/ingest/KnowledgePipelineRunner.ts
|
|
2373
|
-
var
|
|
2374
|
-
var
|
|
2585
|
+
var fs11 = __toESM(require("fs/promises"));
|
|
2586
|
+
var path12 = __toESM(require("path"));
|
|
2375
2587
|
|
|
2376
2588
|
// src/ingest/DiagramParser.ts
|
|
2377
|
-
var
|
|
2378
|
-
var
|
|
2379
|
-
|
|
2589
|
+
var fs6 = __toESM(require("fs/promises"));
|
|
2590
|
+
var path7 = __toESM(require("path"));
|
|
2591
|
+
|
|
2592
|
+
// src/ingest/parsers/mermaid.ts
|
|
2593
|
+
function emptyResult2(diagramType = "unknown") {
|
|
2380
2594
|
return {
|
|
2381
2595
|
entities: [],
|
|
2382
2596
|
relationships: [],
|
|
2383
2597
|
metadata: { format: "mermaid", diagramType }
|
|
2384
2598
|
};
|
|
2385
2599
|
}
|
|
2600
|
+
function detectDiagramType(content) {
|
|
2601
|
+
for (const line of content.split("\n")) {
|
|
2602
|
+
const trimmed = line.trim();
|
|
2603
|
+
if (!trimmed) continue;
|
|
2604
|
+
if (/^(?:graph|flowchart)\b/i.test(trimmed)) return "flowchart";
|
|
2605
|
+
if (/^sequenceDiagram\b/.test(trimmed)) return "sequence";
|
|
2606
|
+
if (/^classDiagram\b/.test(trimmed)) return "class";
|
|
2607
|
+
if (/^erDiagram\b/.test(trimmed)) return "er";
|
|
2608
|
+
break;
|
|
2609
|
+
}
|
|
2610
|
+
return "unknown";
|
|
2611
|
+
}
|
|
2612
|
+
function isDecisionNode(content, nodeId) {
|
|
2613
|
+
const decisionRegex = new RegExp(`${nodeId}\\s*\\{`);
|
|
2614
|
+
return decisionRegex.test(content);
|
|
2615
|
+
}
|
|
2616
|
+
function buildFlowchartEntity(content, match) {
|
|
2617
|
+
const id = match[1] ?? "";
|
|
2618
|
+
const label = (match[2] ?? "").trim();
|
|
2619
|
+
if (!id) return null;
|
|
2620
|
+
const decision = isDecisionNode(content, id);
|
|
2621
|
+
const entity = {
|
|
2622
|
+
id,
|
|
2623
|
+
label,
|
|
2624
|
+
...decision ? { type: "decision" } : {}
|
|
2625
|
+
};
|
|
2626
|
+
return { id, entity };
|
|
2627
|
+
}
|
|
2628
|
+
function extractFlowchartNodes(content) {
|
|
2629
|
+
const entities = /* @__PURE__ */ new Map();
|
|
2630
|
+
const nodeRegex = /([A-Za-z0-9_]+)\s*[[({]([^\])}]+)[\])}]/g;
|
|
2631
|
+
let match = nodeRegex.exec(content);
|
|
2632
|
+
while (match !== null) {
|
|
2633
|
+
const parsed = buildFlowchartEntity(content, match);
|
|
2634
|
+
if (parsed && !entities.has(parsed.id)) {
|
|
2635
|
+
entities.set(parsed.id, parsed.entity);
|
|
2636
|
+
}
|
|
2637
|
+
match = nodeRegex.exec(content);
|
|
2638
|
+
}
|
|
2639
|
+
return entities;
|
|
2640
|
+
}
|
|
2641
|
+
function parseLabeledEdgeMatch(match) {
|
|
2642
|
+
const from = match[1] ?? "";
|
|
2643
|
+
const label = (match[2] ?? "").trim();
|
|
2644
|
+
const to = match[3] ?? "";
|
|
2645
|
+
if (!from || !to) return null;
|
|
2646
|
+
return { key: `${from}->${to}:${label}`, rel: { from, to, label } };
|
|
2647
|
+
}
|
|
2648
|
+
function extractLabeledEdges(stripped, edgeKeys) {
|
|
2649
|
+
const relationships = [];
|
|
2650
|
+
const labeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\|([^|]+)\|\s*([A-Za-z0-9_]+)/g;
|
|
2651
|
+
let match = labeledEdgeRegex.exec(stripped);
|
|
2652
|
+
while (match !== null) {
|
|
2653
|
+
const parsed = parseLabeledEdgeMatch(match);
|
|
2654
|
+
if (parsed && !edgeKeys.has(parsed.key)) {
|
|
2655
|
+
edgeKeys.add(parsed.key);
|
|
2656
|
+
relationships.push(parsed.rel);
|
|
2657
|
+
}
|
|
2658
|
+
match = labeledEdgeRegex.exec(stripped);
|
|
2659
|
+
}
|
|
2660
|
+
return relationships;
|
|
2661
|
+
}
|
|
2662
|
+
function hasLabeledEdgeBetween(labeledEdges, from, to) {
|
|
2663
|
+
return labeledEdges.some((r) => r.from === from && r.to === to);
|
|
2664
|
+
}
|
|
2665
|
+
function extractUnlabeledEdges(stripped, edgeKeys, labeledEdges) {
|
|
2666
|
+
const relationships = [];
|
|
2667
|
+
const unlabeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\s+([A-Za-z0-9_]+)/g;
|
|
2668
|
+
let match = unlabeledEdgeRegex.exec(stripped);
|
|
2669
|
+
while (match !== null) {
|
|
2670
|
+
const from = match[1] ?? "";
|
|
2671
|
+
const to = match[2] ?? "";
|
|
2672
|
+
const key = `${from}->${to}`;
|
|
2673
|
+
if (from && to && !hasLabeledEdgeBetween(labeledEdges, from, to) && !edgeKeys.has(key)) {
|
|
2674
|
+
edgeKeys.add(key);
|
|
2675
|
+
relationships.push({ from, to });
|
|
2676
|
+
}
|
|
2677
|
+
match = unlabeledEdgeRegex.exec(stripped);
|
|
2678
|
+
}
|
|
2679
|
+
return relationships;
|
|
2680
|
+
}
|
|
2681
|
+
function parseFlowchart(content, diagramType) {
|
|
2682
|
+
const entities = extractFlowchartNodes(content);
|
|
2683
|
+
const stripped = content.replace(/[[({][^\])}]*[\])}]/g, "");
|
|
2684
|
+
const edgeKeys = /* @__PURE__ */ new Set();
|
|
2685
|
+
const labeledEdges = extractLabeledEdges(stripped, edgeKeys);
|
|
2686
|
+
const unlabeledEdges = extractUnlabeledEdges(stripped, edgeKeys, labeledEdges);
|
|
2687
|
+
return {
|
|
2688
|
+
entities: Array.from(entities.values()),
|
|
2689
|
+
relationships: [...labeledEdges, ...unlabeledEdges],
|
|
2690
|
+
metadata: { format: "mermaid", diagramType }
|
|
2691
|
+
};
|
|
2692
|
+
}
|
|
2693
|
+
function extractParticipants(content) {
|
|
2694
|
+
const entities = [];
|
|
2695
|
+
const seenParticipants = /* @__PURE__ */ new Set();
|
|
2696
|
+
const participantRegex = /participant\s+(\w+)(?:\s+as\s+(.+))?/g;
|
|
2697
|
+
let match = participantRegex.exec(content);
|
|
2698
|
+
while (match !== null) {
|
|
2699
|
+
const id = match[1] ?? "";
|
|
2700
|
+
const label = match[2]?.trim() || id;
|
|
2701
|
+
if (id && !seenParticipants.has(id)) {
|
|
2702
|
+
seenParticipants.add(id);
|
|
2703
|
+
entities.push({ id, label });
|
|
2704
|
+
}
|
|
2705
|
+
match = participantRegex.exec(content);
|
|
2706
|
+
}
|
|
2707
|
+
return entities;
|
|
2708
|
+
}
|
|
2709
|
+
function relationshipFromMatch(match) {
|
|
2710
|
+
const from = match[1] ?? "";
|
|
2711
|
+
const to = match[2] ?? "";
|
|
2712
|
+
const label = (match[3] ?? "").trim();
|
|
2713
|
+
return from && to ? { from, to, label } : null;
|
|
2714
|
+
}
|
|
2715
|
+
function collectMessageMatches(content, regex) {
|
|
2716
|
+
const results = [];
|
|
2717
|
+
let match = regex.exec(content);
|
|
2718
|
+
while (match !== null) {
|
|
2719
|
+
const rel = relationshipFromMatch(match);
|
|
2720
|
+
if (rel) results.push(rel);
|
|
2721
|
+
match = regex.exec(content);
|
|
2722
|
+
}
|
|
2723
|
+
return results;
|
|
2724
|
+
}
|
|
2725
|
+
function extractMessages(content) {
|
|
2726
|
+
const forward = collectMessageMatches(content, /(\w+)\s*->>?\+?\s*(\w+)\s*:\s*(.+)/g);
|
|
2727
|
+
const returns = collectMessageMatches(content, /(\w+)\s*-->>?-?\s*(\w+)\s*:\s*(.+)/g);
|
|
2728
|
+
return [...forward, ...returns];
|
|
2729
|
+
}
|
|
2730
|
+
function parseSequence(content, diagramType) {
|
|
2731
|
+
return {
|
|
2732
|
+
entities: extractParticipants(content),
|
|
2733
|
+
relationships: extractMessages(content),
|
|
2734
|
+
metadata: { format: "mermaid", diagramType }
|
|
2735
|
+
};
|
|
2736
|
+
}
|
|
2386
2737
|
var MermaidParser = class {
|
|
2387
2738
|
canParse(_content, ext) {
|
|
2388
2739
|
return ext === ".mmd" || ext === ".mermaid";
|
|
@@ -2390,269 +2741,199 @@ var MermaidParser = class {
|
|
|
2390
2741
|
parse(content, _filePath) {
|
|
2391
2742
|
const trimmed = content.trim();
|
|
2392
2743
|
if (!trimmed) {
|
|
2393
|
-
return
|
|
2744
|
+
return emptyResult2();
|
|
2394
2745
|
}
|
|
2395
|
-
const diagramType =
|
|
2746
|
+
const diagramType = detectDiagramType(trimmed);
|
|
2396
2747
|
switch (diagramType) {
|
|
2397
2748
|
case "flowchart":
|
|
2398
|
-
return
|
|
2749
|
+
return parseFlowchart(trimmed, diagramType);
|
|
2399
2750
|
case "sequence":
|
|
2400
|
-
return
|
|
2751
|
+
return parseSequence(trimmed, diagramType);
|
|
2401
2752
|
default:
|
|
2402
|
-
return
|
|
2753
|
+
return emptyResult2(diagramType);
|
|
2403
2754
|
}
|
|
2404
2755
|
}
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2756
|
+
};
|
|
2757
|
+
|
|
2758
|
+
// src/ingest/parsers/d2.ts
|
|
2759
|
+
function emptyResult3() {
|
|
2760
|
+
return {
|
|
2761
|
+
entities: [],
|
|
2762
|
+
relationships: [],
|
|
2763
|
+
metadata: { format: "d2", diagramType: "architecture" }
|
|
2764
|
+
};
|
|
2765
|
+
}
|
|
2766
|
+
function parseBlockShape(stripped) {
|
|
2767
|
+
const match = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+?)\s*\{$/);
|
|
2768
|
+
if (match) {
|
|
2769
|
+
const id = match[1];
|
|
2770
|
+
const label = match[2];
|
|
2771
|
+
if (id && label) return { id, label };
|
|
2772
|
+
}
|
|
2773
|
+
return null;
|
|
2774
|
+
}
|
|
2775
|
+
function parseConnection(stripped) {
|
|
2776
|
+
const match = stripped.match(/^([a-zA-Z0-9_.-]+)\s*->\s*([a-zA-Z0-9_.-]+)(?:\s*:\s*(.+?))?$/);
|
|
2777
|
+
if (match) {
|
|
2778
|
+
const from = match[1] ?? "";
|
|
2779
|
+
const to = match[2] ?? "";
|
|
2780
|
+
const label = match[3]?.trim();
|
|
2781
|
+
if (from && to) return { from, to, ...label ? { label } : {} };
|
|
2782
|
+
}
|
|
2783
|
+
return null;
|
|
2784
|
+
}
|
|
2785
|
+
function parseSimpleShape(stripped) {
|
|
2786
|
+
const match = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+)$/);
|
|
2787
|
+
if (match) {
|
|
2788
|
+
const id = match[1];
|
|
2789
|
+
const label = (match[2] ?? "").trim();
|
|
2790
|
+
if (id && label) return { id, label };
|
|
2791
|
+
}
|
|
2792
|
+
return null;
|
|
2793
|
+
}
|
|
2794
|
+
function handleBlockOpen(stripped, state) {
|
|
2795
|
+
if (state.braceDepth === 0) {
|
|
2796
|
+
const shape = parseBlockShape(stripped);
|
|
2797
|
+
if (shape) state.entities.set(shape.id, { id: shape.id, label: shape.label });
|
|
2417
2798
|
}
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
const id = match[1] ?? "";
|
|
2426
|
-
const label = (match[2] ?? "").trim();
|
|
2427
|
-
if (id && !entities.has(id)) {
|
|
2428
|
-
const isDecision = this.isDecisionNode(content, id);
|
|
2429
|
-
entities.set(id, {
|
|
2430
|
-
id,
|
|
2431
|
-
label,
|
|
2432
|
-
...isDecision ? { type: "decision" } : {}
|
|
2433
|
-
});
|
|
2434
|
-
}
|
|
2435
|
-
match = nodeRegex.exec(content);
|
|
2436
|
-
}
|
|
2437
|
-
const stripped = content.replace(/[[({][^\])}]*[\])}]/g, "");
|
|
2438
|
-
const labeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\|([^|]+)\|\s*([A-Za-z0-9_]+)/g;
|
|
2439
|
-
const unlabeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\s+([A-Za-z0-9_]+)/g;
|
|
2440
|
-
const edgeKeys = /* @__PURE__ */ new Set();
|
|
2441
|
-
match = labeledEdgeRegex.exec(stripped);
|
|
2442
|
-
while (match !== null) {
|
|
2443
|
-
const from = match[1] ?? "";
|
|
2444
|
-
const label = (match[2] ?? "").trim();
|
|
2445
|
-
const to = match[3] ?? "";
|
|
2446
|
-
if (from && to) {
|
|
2447
|
-
const key = `${from}->${to}:${label}`;
|
|
2448
|
-
if (!edgeKeys.has(key)) {
|
|
2449
|
-
edgeKeys.add(key);
|
|
2450
|
-
relationships.push({ from, to, label });
|
|
2451
|
-
}
|
|
2452
|
-
}
|
|
2453
|
-
match = labeledEdgeRegex.exec(stripped);
|
|
2454
|
-
}
|
|
2455
|
-
match = unlabeledEdgeRegex.exec(stripped);
|
|
2456
|
-
while (match !== null) {
|
|
2457
|
-
const from = match[1] ?? "";
|
|
2458
|
-
const to = match[2] ?? "";
|
|
2459
|
-
if (from && to) {
|
|
2460
|
-
const hasLabeled = relationships.some((r) => r.from === from && r.to === to);
|
|
2461
|
-
if (!hasLabeled) {
|
|
2462
|
-
const key = `${from}->${to}`;
|
|
2463
|
-
if (!edgeKeys.has(key)) {
|
|
2464
|
-
edgeKeys.add(key);
|
|
2465
|
-
relationships.push({ from, to });
|
|
2466
|
-
}
|
|
2467
|
-
}
|
|
2468
|
-
}
|
|
2469
|
-
match = unlabeledEdgeRegex.exec(stripped);
|
|
2470
|
-
}
|
|
2471
|
-
return {
|
|
2472
|
-
entities: Array.from(entities.values()),
|
|
2473
|
-
relationships,
|
|
2474
|
-
metadata: { format: "mermaid", diagramType }
|
|
2475
|
-
};
|
|
2799
|
+
state.braceDepth++;
|
|
2800
|
+
}
|
|
2801
|
+
function processTopLevelLine(stripped, state) {
|
|
2802
|
+
const conn = parseConnection(stripped);
|
|
2803
|
+
if (conn) {
|
|
2804
|
+
state.relationships.push(conn);
|
|
2805
|
+
return;
|
|
2476
2806
|
}
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2807
|
+
const shape = parseSimpleShape(stripped);
|
|
2808
|
+
if (shape && !state.entities.has(shape.id)) {
|
|
2809
|
+
state.entities.set(shape.id, { id: shape.id, label: shape.label });
|
|
2480
2810
|
}
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
match = participantRegex.exec(content);
|
|
2488
|
-
while (match !== null) {
|
|
2489
|
-
const id = match[1] ?? "";
|
|
2490
|
-
const label = match[2]?.trim() || id;
|
|
2491
|
-
if (id && !seenParticipants.has(id)) {
|
|
2492
|
-
seenParticipants.add(id);
|
|
2493
|
-
entities.push({ id, label });
|
|
2494
|
-
}
|
|
2495
|
-
match = participantRegex.exec(content);
|
|
2496
|
-
}
|
|
2497
|
-
const forwardMsgRegex = /(\w+)\s*->>?\+?\s*(\w+)\s*:\s*(.+)/g;
|
|
2498
|
-
match = forwardMsgRegex.exec(content);
|
|
2499
|
-
while (match !== null) {
|
|
2500
|
-
const from = match[1] ?? "";
|
|
2501
|
-
const to = match[2] ?? "";
|
|
2502
|
-
const label = (match[3] ?? "").trim();
|
|
2503
|
-
if (from && to) {
|
|
2504
|
-
relationships.push({ from, to, label });
|
|
2505
|
-
}
|
|
2506
|
-
match = forwardMsgRegex.exec(content);
|
|
2507
|
-
}
|
|
2508
|
-
const returnMsgRegex = /(\w+)\s*-->>?-?\s*(\w+)\s*:\s*(.+)/g;
|
|
2509
|
-
match = returnMsgRegex.exec(content);
|
|
2510
|
-
while (match !== null) {
|
|
2511
|
-
const from = match[1] ?? "";
|
|
2512
|
-
const to = match[2] ?? "";
|
|
2513
|
-
const label = (match[3] ?? "").trim();
|
|
2514
|
-
if (from && to) {
|
|
2515
|
-
relationships.push({ from, to, label });
|
|
2516
|
-
}
|
|
2517
|
-
match = returnMsgRegex.exec(content);
|
|
2518
|
-
}
|
|
2519
|
-
return {
|
|
2520
|
-
entities,
|
|
2521
|
-
relationships,
|
|
2522
|
-
metadata: { format: "mermaid", diagramType }
|
|
2523
|
-
};
|
|
2811
|
+
}
|
|
2812
|
+
function processD2Line(stripped, state) {
|
|
2813
|
+
if (!stripped || stripped.startsWith("#")) return;
|
|
2814
|
+
if (stripped.endsWith("{")) {
|
|
2815
|
+
handleBlockOpen(stripped, state);
|
|
2816
|
+
return;
|
|
2524
2817
|
}
|
|
2525
|
-
}
|
|
2818
|
+
if (stripped === "}") {
|
|
2819
|
+
state.braceDepth = Math.max(0, state.braceDepth - 1);
|
|
2820
|
+
return;
|
|
2821
|
+
}
|
|
2822
|
+
if (state.braceDepth > 0) return;
|
|
2823
|
+
processTopLevelLine(stripped, state);
|
|
2824
|
+
}
|
|
2526
2825
|
var D2Parser = class {
|
|
2527
2826
|
canParse(_content, ext) {
|
|
2528
2827
|
return ext === ".d2";
|
|
2529
2828
|
}
|
|
2530
2829
|
parse(content, _filePath) {
|
|
2531
2830
|
const trimmed = content.trim();
|
|
2532
|
-
if (!trimmed)
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
}
|
|
2539
|
-
const entities = /* @__PURE__ */ new Map();
|
|
2540
|
-
const relationships = [];
|
|
2541
|
-
let braceDepth = 0;
|
|
2831
|
+
if (!trimmed) return emptyResult3();
|
|
2832
|
+
const state = {
|
|
2833
|
+
entities: /* @__PURE__ */ new Map(),
|
|
2834
|
+
relationships: [],
|
|
2835
|
+
braceDepth: 0
|
|
2836
|
+
};
|
|
2542
2837
|
for (const line of trimmed.split("\n")) {
|
|
2543
|
-
|
|
2544
|
-
if (!stripped || stripped.startsWith("#")) continue;
|
|
2545
|
-
if (stripped.endsWith("{")) {
|
|
2546
|
-
if (braceDepth === 0) {
|
|
2547
|
-
const shapeMatch = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+?)\s*\{$/);
|
|
2548
|
-
if (shapeMatch) {
|
|
2549
|
-
const id = shapeMatch[1];
|
|
2550
|
-
const label = shapeMatch[2];
|
|
2551
|
-
if (id && label) {
|
|
2552
|
-
entities.set(id, { id, label });
|
|
2553
|
-
}
|
|
2554
|
-
}
|
|
2555
|
-
}
|
|
2556
|
-
braceDepth++;
|
|
2557
|
-
continue;
|
|
2558
|
-
}
|
|
2559
|
-
if (stripped === "}") {
|
|
2560
|
-
braceDepth = Math.max(0, braceDepth - 1);
|
|
2561
|
-
continue;
|
|
2562
|
-
}
|
|
2563
|
-
if (braceDepth > 0) continue;
|
|
2564
|
-
const connMatch = stripped.match(
|
|
2565
|
-
/^([a-zA-Z0-9_.-]+)\s*->\s*([a-zA-Z0-9_.-]+)(?:\s*:\s*(.+?))?$/
|
|
2566
|
-
);
|
|
2567
|
-
if (connMatch) {
|
|
2568
|
-
const from = connMatch[1] ?? "";
|
|
2569
|
-
const to = connMatch[2] ?? "";
|
|
2570
|
-
const label = connMatch[3]?.trim();
|
|
2571
|
-
if (from && to) {
|
|
2572
|
-
relationships.push({ from, to, ...label ? { label } : {} });
|
|
2573
|
-
}
|
|
2574
|
-
continue;
|
|
2575
|
-
}
|
|
2576
|
-
const simpleMatch = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+)$/);
|
|
2577
|
-
if (simpleMatch) {
|
|
2578
|
-
const id = simpleMatch[1];
|
|
2579
|
-
const label = (simpleMatch[2] ?? "").trim();
|
|
2580
|
-
if (id && label && !entities.has(id)) {
|
|
2581
|
-
entities.set(id, { id, label });
|
|
2582
|
-
}
|
|
2583
|
-
}
|
|
2838
|
+
processD2Line(line.trim(), state);
|
|
2584
2839
|
}
|
|
2585
2840
|
return {
|
|
2586
|
-
entities: Array.from(entities.values()),
|
|
2587
|
-
relationships,
|
|
2841
|
+
entities: Array.from(state.entities.values()),
|
|
2842
|
+
relationships: state.relationships,
|
|
2588
2843
|
metadata: { format: "d2", diagramType: "architecture" }
|
|
2589
2844
|
};
|
|
2590
2845
|
}
|
|
2591
2846
|
};
|
|
2847
|
+
|
|
2848
|
+
// src/ingest/parsers/plantuml.ts
|
|
2849
|
+
function emptyResult4() {
|
|
2850
|
+
return {
|
|
2851
|
+
entities: [],
|
|
2852
|
+
relationships: [],
|
|
2853
|
+
metadata: { format: "plantuml", diagramType: "unknown" }
|
|
2854
|
+
};
|
|
2855
|
+
}
|
|
2856
|
+
function detectDiagramType2(content) {
|
|
2857
|
+
if (/class\s+\w+/.test(content)) return "class";
|
|
2858
|
+
if (/\[.+\]/.test(content) || /component\s+/.test(content)) return "component";
|
|
2859
|
+
if (/participant\s+/.test(content) || /actor\s+/.test(content)) return "sequence";
|
|
2860
|
+
return "unknown";
|
|
2861
|
+
}
|
|
2862
|
+
function stripWrappers(content) {
|
|
2863
|
+
return content.replace(/@startuml\b.*\n?/, "").replace(/@enduml\b.*/, "").trim();
|
|
2864
|
+
}
|
|
2865
|
+
function extractClasses(body, entities) {
|
|
2866
|
+
const classRegex = /class\s+(\w+)/g;
|
|
2867
|
+
let match = classRegex.exec(body);
|
|
2868
|
+
while (match !== null) {
|
|
2869
|
+
const id = match[1] ?? "";
|
|
2870
|
+
if (id && !entities.has(id)) {
|
|
2871
|
+
entities.set(id, { id, label: id });
|
|
2872
|
+
}
|
|
2873
|
+
match = classRegex.exec(body);
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
function extractComponents(body, entities) {
|
|
2877
|
+
const componentRegex = /\[([^\]]+)\]/g;
|
|
2878
|
+
let match = componentRegex.exec(body);
|
|
2879
|
+
while (match !== null) {
|
|
2880
|
+
const label = (match[1] ?? "").trim();
|
|
2881
|
+
if (label) {
|
|
2882
|
+
const id = label.replace(/\s+/g, "_");
|
|
2883
|
+
if (!entities.has(id)) {
|
|
2884
|
+
entities.set(id, { id, label });
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
match = componentRegex.exec(body);
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
function parseRelationshipMatch(match) {
|
|
2891
|
+
const from = match[1] ?? "";
|
|
2892
|
+
const to = match[2] ?? "";
|
|
2893
|
+
if (!from || !to) return null;
|
|
2894
|
+
const label = match[3]?.trim();
|
|
2895
|
+
return { from, to, ...label ? { label } : {} };
|
|
2896
|
+
}
|
|
2897
|
+
function collectRelationshipMatches(body, regex) {
|
|
2898
|
+
const results = [];
|
|
2899
|
+
let match = regex.exec(body);
|
|
2900
|
+
while (match !== null) {
|
|
2901
|
+
const rel = parseRelationshipMatch(match);
|
|
2902
|
+
if (rel) results.push(rel);
|
|
2903
|
+
match = regex.exec(body);
|
|
2904
|
+
}
|
|
2905
|
+
return results;
|
|
2906
|
+
}
|
|
2907
|
+
function extractRelationships(body) {
|
|
2908
|
+
return collectRelationshipMatches(
|
|
2909
|
+
body,
|
|
2910
|
+
/(\w+)\s*(?:-->|->|<--|<-|\.\.>|--)\s*(\w+)(?:\s*:\s*(.+))?/g
|
|
2911
|
+
);
|
|
2912
|
+
}
|
|
2592
2913
|
var PlantUmlParser = class {
|
|
2593
2914
|
canParse(_content, ext) {
|
|
2594
2915
|
return ext === ".puml" || ext === ".plantuml";
|
|
2595
2916
|
}
|
|
2596
2917
|
parse(content, _filePath) {
|
|
2597
2918
|
const trimmed = content.trim();
|
|
2598
|
-
if (!trimmed)
|
|
2599
|
-
|
|
2600
|
-
entities: [],
|
|
2601
|
-
relationships: [],
|
|
2602
|
-
metadata: { format: "plantuml", diagramType: "unknown" }
|
|
2603
|
-
};
|
|
2604
|
-
}
|
|
2605
|
-
const diagramType = this.detectDiagramType(trimmed);
|
|
2919
|
+
if (!trimmed) return emptyResult4();
|
|
2920
|
+
const diagramType = detectDiagramType2(trimmed);
|
|
2606
2921
|
const entities = /* @__PURE__ */ new Map();
|
|
2607
|
-
const
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
match = classRegex.exec(body);
|
|
2612
|
-
while (match !== null) {
|
|
2613
|
-
const id = match[1] ?? "";
|
|
2614
|
-
if (id && !entities.has(id)) {
|
|
2615
|
-
entities.set(id, { id, label: id });
|
|
2616
|
-
}
|
|
2617
|
-
match = classRegex.exec(body);
|
|
2618
|
-
}
|
|
2619
|
-
const componentRegex1 = /\[([^\]]+)\]/g;
|
|
2620
|
-
match = componentRegex1.exec(body);
|
|
2621
|
-
while (match !== null) {
|
|
2622
|
-
const label = (match[1] ?? "").trim();
|
|
2623
|
-
if (label) {
|
|
2624
|
-
const id = label.replace(/\s+/g, "_");
|
|
2625
|
-
if (!entities.has(id)) {
|
|
2626
|
-
entities.set(id, { id, label });
|
|
2627
|
-
}
|
|
2628
|
-
}
|
|
2629
|
-
match = componentRegex1.exec(body);
|
|
2630
|
-
}
|
|
2631
|
-
const relRegex = /(\w+)\s*(?:-->|->|<--|<-|\.\.>|--)\s*(\w+)(?:\s*:\s*(.+))?/g;
|
|
2632
|
-
match = relRegex.exec(body);
|
|
2633
|
-
while (match !== null) {
|
|
2634
|
-
const from = match[1] ?? "";
|
|
2635
|
-
const to = match[2] ?? "";
|
|
2636
|
-
const label = match[3]?.trim();
|
|
2637
|
-
if (from && to) {
|
|
2638
|
-
relationships.push({ from, to, ...label ? { label } : {} });
|
|
2639
|
-
}
|
|
2640
|
-
match = relRegex.exec(body);
|
|
2641
|
-
}
|
|
2922
|
+
const body = stripWrappers(trimmed);
|
|
2923
|
+
extractClasses(body, entities);
|
|
2924
|
+
extractComponents(body, entities);
|
|
2925
|
+
const relationships = extractRelationships(body);
|
|
2642
2926
|
return {
|
|
2643
2927
|
entities: Array.from(entities.values()),
|
|
2644
2928
|
relationships,
|
|
2645
2929
|
metadata: { format: "plantuml", diagramType }
|
|
2646
2930
|
};
|
|
2647
2931
|
}
|
|
2648
|
-
detectDiagramType(content) {
|
|
2649
|
-
if (/class\s+\w+/.test(content)) return "class";
|
|
2650
|
-
if (/\[.+\]/.test(content) || /component\s+/.test(content)) return "component";
|
|
2651
|
-
if (/participant\s+/.test(content) || /actor\s+/.test(content)) return "sequence";
|
|
2652
|
-
return "unknown";
|
|
2653
|
-
}
|
|
2654
2932
|
};
|
|
2933
|
+
|
|
2934
|
+
// src/ingest/DiagramParser.ts
|
|
2655
2935
|
var DIAGRAM_EXTENSIONS = /* @__PURE__ */ new Set([".mmd", ".mermaid", ".d2", ".puml", ".plantuml"]);
|
|
2936
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".harness"]);
|
|
2656
2937
|
var DiagramParser = class {
|
|
2657
2938
|
constructor(store) {
|
|
2658
2939
|
this.store = store;
|
|
@@ -2664,7 +2945,7 @@ var DiagramParser = class {
|
|
|
2664
2945
|
new PlantUmlParser()
|
|
2665
2946
|
];
|
|
2666
2947
|
parse(content, filePath) {
|
|
2667
|
-
const ext =
|
|
2948
|
+
const ext = path7.extname(filePath).toLowerCase();
|
|
2668
2949
|
for (const parser of this.parsers) {
|
|
2669
2950
|
if (parser.canParse(content, ext)) {
|
|
2670
2951
|
return parser.parse(content, filePath);
|
|
@@ -2684,41 +2965,13 @@ var DiagramParser = class {
|
|
|
2684
2965
|
const diagramFiles = await this.findDiagramFiles(projectDir);
|
|
2685
2966
|
for (const filePath of diagramFiles) {
|
|
2686
2967
|
try {
|
|
2687
|
-
const content = await
|
|
2688
|
-
const relPath =
|
|
2968
|
+
const content = await fs6.readFile(filePath, "utf-8");
|
|
2969
|
+
const relPath = path7.relative(projectDir, filePath).replaceAll("\\", "/");
|
|
2689
2970
|
const result = this.parse(content, filePath);
|
|
2690
2971
|
if (result.entities.length === 0) continue;
|
|
2691
2972
|
const pathHash = hash(relPath);
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
this.store.addNode({
|
|
2695
|
-
id: nodeId,
|
|
2696
|
-
type: "business_concept",
|
|
2697
|
-
name: entity.label,
|
|
2698
|
-
path: relPath,
|
|
2699
|
-
metadata: {
|
|
2700
|
-
source: "diagram",
|
|
2701
|
-
format: result.metadata.format,
|
|
2702
|
-
diagramType: result.metadata.diagramType,
|
|
2703
|
-
confidence: 0.85,
|
|
2704
|
-
...entity.type ? { entityType: entity.type } : {}
|
|
2705
|
-
}
|
|
2706
|
-
});
|
|
2707
|
-
nodesAdded++;
|
|
2708
|
-
}
|
|
2709
|
-
for (const rel of result.relationships) {
|
|
2710
|
-
const fromId = `diagram:${pathHash}:${rel.from}`;
|
|
2711
|
-
const toId = `diagram:${pathHash}:${rel.to}`;
|
|
2712
|
-
this.store.addEdge({
|
|
2713
|
-
from: fromId,
|
|
2714
|
-
to: toId,
|
|
2715
|
-
type: "references",
|
|
2716
|
-
metadata: {
|
|
2717
|
-
...rel.label ? { label: rel.label } : {}
|
|
2718
|
-
}
|
|
2719
|
-
});
|
|
2720
|
-
edgesAdded++;
|
|
2721
|
-
}
|
|
2973
|
+
nodesAdded += this.addEntityNodes(result, relPath, pathHash);
|
|
2974
|
+
edgesAdded += this.addRelationshipEdges(result, pathHash);
|
|
2722
2975
|
} catch (err) {
|
|
2723
2976
|
errors.push(
|
|
2724
2977
|
`Failed to parse ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -2734,25 +2987,64 @@ var DiagramParser = class {
|
|
|
2734
2987
|
durationMs: Date.now() - start
|
|
2735
2988
|
};
|
|
2736
2989
|
}
|
|
2990
|
+
/** Map diagram entities to business_concept graph nodes. */
|
|
2991
|
+
addEntityNodes(result, relPath, pathHash) {
|
|
2992
|
+
let count = 0;
|
|
2993
|
+
for (const entity of result.entities) {
|
|
2994
|
+
const nodeId = `diagram:${pathHash}:${entity.id}`;
|
|
2995
|
+
this.store.addNode({
|
|
2996
|
+
id: nodeId,
|
|
2997
|
+
type: "business_concept",
|
|
2998
|
+
name: entity.label,
|
|
2999
|
+
path: relPath,
|
|
3000
|
+
metadata: {
|
|
3001
|
+
source: "diagram",
|
|
3002
|
+
format: result.metadata.format,
|
|
3003
|
+
diagramType: result.metadata.diagramType,
|
|
3004
|
+
confidence: 0.85,
|
|
3005
|
+
...entity.type ? { entityType: entity.type } : {}
|
|
3006
|
+
}
|
|
3007
|
+
});
|
|
3008
|
+
count++;
|
|
3009
|
+
}
|
|
3010
|
+
return count;
|
|
3011
|
+
}
|
|
3012
|
+
/** Map diagram relationships to references graph edges. */
|
|
3013
|
+
addRelationshipEdges(result, pathHash) {
|
|
3014
|
+
let count = 0;
|
|
3015
|
+
for (const rel of result.relationships) {
|
|
3016
|
+
const fromId = `diagram:${pathHash}:${rel.from}`;
|
|
3017
|
+
const toId = `diagram:${pathHash}:${rel.to}`;
|
|
3018
|
+
this.store.addEdge({
|
|
3019
|
+
from: fromId,
|
|
3020
|
+
to: toId,
|
|
3021
|
+
type: "references",
|
|
3022
|
+
metadata: {
|
|
3023
|
+
...rel.label ? { label: rel.label } : {}
|
|
3024
|
+
}
|
|
3025
|
+
});
|
|
3026
|
+
count++;
|
|
3027
|
+
}
|
|
3028
|
+
return count;
|
|
3029
|
+
}
|
|
2737
3030
|
async findDiagramFiles(dir) {
|
|
2738
3031
|
const files = [];
|
|
2739
|
-
const SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".harness"]);
|
|
2740
3032
|
const walk = async (currentDir) => {
|
|
2741
3033
|
let entries;
|
|
2742
3034
|
try {
|
|
2743
|
-
entries = await
|
|
3035
|
+
entries = await fs6.readdir(currentDir, { withFileTypes: true });
|
|
2744
3036
|
} catch {
|
|
2745
3037
|
return;
|
|
2746
3038
|
}
|
|
2747
3039
|
for (const entry of entries) {
|
|
2748
3040
|
if (entry.isDirectory()) {
|
|
2749
|
-
if (!
|
|
2750
|
-
await walk(
|
|
3041
|
+
if (!SKIP_DIRS.has(entry.name)) {
|
|
3042
|
+
await walk(path7.join(currentDir, entry.name));
|
|
2751
3043
|
}
|
|
2752
3044
|
} else if (entry.isFile()) {
|
|
2753
|
-
const ext =
|
|
3045
|
+
const ext = path7.extname(entry.name).toLowerCase();
|
|
2754
3046
|
if (DIAGRAM_EXTENSIONS.has(ext)) {
|
|
2755
|
-
files.push(
|
|
3047
|
+
files.push(path7.join(currentDir, entry.name));
|
|
2756
3048
|
}
|
|
2757
3049
|
}
|
|
2758
3050
|
}
|
|
@@ -2763,8 +3055,8 @@ var DiagramParser = class {
|
|
|
2763
3055
|
};
|
|
2764
3056
|
|
|
2765
3057
|
// src/ingest/KnowledgeLinker.ts
|
|
2766
|
-
var
|
|
2767
|
-
var
|
|
3058
|
+
var fs7 = __toESM(require("fs/promises"));
|
|
3059
|
+
var path8 = __toESM(require("path"));
|
|
2768
3060
|
var HEURISTIC_PATTERNS = [
|
|
2769
3061
|
{
|
|
2770
3062
|
name: "business-rule-imperative",
|
|
@@ -2911,8 +3203,10 @@ var KnowledgeLinker = class {
|
|
|
2911
3203
|
if (!duplicate) return false;
|
|
2912
3204
|
const existingSources = duplicate.metadata.sources ?? [];
|
|
2913
3205
|
if (!existingSources.includes(candidate.sourceNodeId)) {
|
|
2914
|
-
|
|
2915
|
-
|
|
3206
|
+
this.store.addNode({
|
|
3207
|
+
...duplicate,
|
|
3208
|
+
metadata: { ...duplicate.metadata, sources: [...existingSources, candidate.sourceNodeId] }
|
|
3209
|
+
});
|
|
2916
3210
|
}
|
|
2917
3211
|
return true;
|
|
2918
3212
|
}
|
|
@@ -2946,8 +3240,8 @@ var KnowledgeLinker = class {
|
|
|
2946
3240
|
*/
|
|
2947
3241
|
async writeJsonl(candidates) {
|
|
2948
3242
|
if (!this.outputDir) return;
|
|
2949
|
-
await
|
|
2950
|
-
const filePath =
|
|
3243
|
+
await fs7.mkdir(this.outputDir, { recursive: true });
|
|
3244
|
+
const filePath = path8.join(this.outputDir, "linker.jsonl");
|
|
2951
3245
|
const lines = candidates.map(
|
|
2952
3246
|
(c) => JSON.stringify({
|
|
2953
3247
|
id: c.id,
|
|
@@ -2960,16 +3254,16 @@ var KnowledgeLinker = class {
|
|
|
2960
3254
|
nodeType: c.nodeType
|
|
2961
3255
|
})
|
|
2962
3256
|
);
|
|
2963
|
-
await
|
|
3257
|
+
await fs7.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
|
|
2964
3258
|
}
|
|
2965
3259
|
/**
|
|
2966
3260
|
* Write medium-confidence candidates to staged JSONL for human review.
|
|
2967
3261
|
*/
|
|
2968
3262
|
async writeStagedJsonl(candidates) {
|
|
2969
3263
|
if (!this.outputDir || candidates.length === 0) return;
|
|
2970
|
-
const stagedDir =
|
|
2971
|
-
await
|
|
2972
|
-
const filePath =
|
|
3264
|
+
const stagedDir = path8.join(this.outputDir, "staged");
|
|
3265
|
+
await fs7.mkdir(stagedDir, { recursive: true });
|
|
3266
|
+
const filePath = path8.join(stagedDir, "linker-staged.jsonl");
|
|
2973
3267
|
const lines = candidates.map(
|
|
2974
3268
|
(c) => JSON.stringify({
|
|
2975
3269
|
id: c.id,
|
|
@@ -2982,7 +3276,7 @@ var KnowledgeLinker = class {
|
|
|
2982
3276
|
nodeType: c.nodeType
|
|
2983
3277
|
})
|
|
2984
3278
|
);
|
|
2985
|
-
await
|
|
3279
|
+
await fs7.writeFile(filePath, lines.join("\n") + "\n");
|
|
2986
3280
|
}
|
|
2987
3281
|
/**
|
|
2988
3282
|
* Apply heuristic patterns to content and return candidate extractions.
|
|
@@ -3088,13 +3382,23 @@ var StructuralDriftDetector = class {
|
|
|
3088
3382
|
};
|
|
3089
3383
|
|
|
3090
3384
|
// src/ingest/KnowledgeStagingAggregator.ts
|
|
3091
|
-
var
|
|
3092
|
-
var
|
|
3385
|
+
var fs8 = __toESM(require("fs/promises"));
|
|
3386
|
+
var path9 = __toESM(require("path"));
|
|
3387
|
+
var BUSINESS_NODE_TYPES = [
|
|
3388
|
+
"business_concept",
|
|
3389
|
+
"business_rule",
|
|
3390
|
+
"business_process",
|
|
3391
|
+
"business_term",
|
|
3392
|
+
"business_metric",
|
|
3393
|
+
"business_fact"
|
|
3394
|
+
];
|
|
3093
3395
|
var KnowledgeStagingAggregator = class {
|
|
3094
|
-
constructor(projectDir) {
|
|
3396
|
+
constructor(projectDir, inferenceOptions = {}) {
|
|
3095
3397
|
this.projectDir = projectDir;
|
|
3398
|
+
this.inferenceOptions = inferenceOptions;
|
|
3096
3399
|
}
|
|
3097
3400
|
projectDir;
|
|
3401
|
+
inferenceOptions;
|
|
3098
3402
|
async aggregate(extractorResults, linkerResults, diagramResults) {
|
|
3099
3403
|
const all = [...extractorResults, ...linkerResults, ...diagramResults];
|
|
3100
3404
|
const byHash = /* @__PURE__ */ new Map();
|
|
@@ -3108,59 +3412,157 @@ var KnowledgeStagingAggregator = class {
|
|
|
3108
3412
|
if (deduplicated.length === 0) {
|
|
3109
3413
|
return { staged: 0 };
|
|
3110
3414
|
}
|
|
3111
|
-
const stagedDir =
|
|
3112
|
-
await
|
|
3415
|
+
const stagedDir = path9.join(this.projectDir, ".harness", "knowledge", "staged");
|
|
3416
|
+
await fs8.mkdir(stagedDir, { recursive: true });
|
|
3113
3417
|
const jsonl = deduplicated.map((entry) => JSON.stringify(entry)).join("\n") + "\n";
|
|
3114
|
-
await
|
|
3418
|
+
await fs8.writeFile(path9.join(stagedDir, "pipeline-staged.jsonl"), jsonl, "utf-8");
|
|
3115
3419
|
return { staged: deduplicated.length };
|
|
3116
3420
|
}
|
|
3117
|
-
async
|
|
3118
|
-
const
|
|
3421
|
+
async extractDocName(filePath) {
|
|
3422
|
+
const raw = await fs8.readFile(filePath, "utf-8");
|
|
3423
|
+
const fmMatch = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
|
|
3424
|
+
const body = fmMatch ? fmMatch[1] : raw;
|
|
3425
|
+
const titleMatch = body.match(/^#\s+(.+)$/m);
|
|
3426
|
+
return titleMatch ? titleMatch[1].trim() : path9.basename(filePath, ".md");
|
|
3427
|
+
}
|
|
3428
|
+
async generateGapReport(knowledgeDir, store) {
|
|
3429
|
+
const domainDocNames = /* @__PURE__ */ new Map();
|
|
3430
|
+
const domainEntryCounts = /* @__PURE__ */ new Map();
|
|
3119
3431
|
let totalEntries = 0;
|
|
3120
3432
|
try {
|
|
3121
|
-
const entries = await
|
|
3433
|
+
const entries = await fs8.readdir(knowledgeDir, { withFileTypes: true });
|
|
3122
3434
|
const domainDirs = entries.filter((e) => e.isDirectory());
|
|
3123
3435
|
for (const dir of domainDirs) {
|
|
3124
|
-
const domainPath =
|
|
3125
|
-
const files = await
|
|
3436
|
+
const domainPath = path9.join(knowledgeDir, dir.name);
|
|
3437
|
+
const files = await fs8.readdir(domainPath);
|
|
3126
3438
|
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
3127
3439
|
const entryCount = mdFiles.length;
|
|
3128
3440
|
totalEntries += entryCount;
|
|
3129
|
-
|
|
3441
|
+
domainEntryCounts.set(dir.name, entryCount);
|
|
3442
|
+
if (store) {
|
|
3443
|
+
const names = [];
|
|
3444
|
+
for (const file of mdFiles) {
|
|
3445
|
+
const name = await this.extractDocName(path9.join(domainPath, file));
|
|
3446
|
+
names.push(name.toLowerCase().trim());
|
|
3447
|
+
}
|
|
3448
|
+
domainDocNames.set(dir.name, names);
|
|
3449
|
+
}
|
|
3130
3450
|
}
|
|
3131
3451
|
} catch {
|
|
3132
3452
|
}
|
|
3453
|
+
if (!store) {
|
|
3454
|
+
const domains2 = [];
|
|
3455
|
+
for (const [domain, entryCount] of domainEntryCounts) {
|
|
3456
|
+
domains2.push({ domain, entryCount, extractedCount: 0, gapCount: 0, gapEntries: [] });
|
|
3457
|
+
}
|
|
3458
|
+
return {
|
|
3459
|
+
domains: domains2,
|
|
3460
|
+
totalEntries,
|
|
3461
|
+
totalExtracted: 0,
|
|
3462
|
+
totalGaps: 0,
|
|
3463
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3464
|
+
};
|
|
3465
|
+
}
|
|
3466
|
+
const extractedByDomain = /* @__PURE__ */ new Map();
|
|
3467
|
+
for (const nodeType of BUSINESS_NODE_TYPES) {
|
|
3468
|
+
const nodes = store.findNodes({ type: nodeType });
|
|
3469
|
+
for (const node of nodes) {
|
|
3470
|
+
const domain = inferDomain(node, this.inferenceOptions);
|
|
3471
|
+
const list = extractedByDomain.get(domain) ?? [];
|
|
3472
|
+
list.push(node);
|
|
3473
|
+
extractedByDomain.set(domain, list);
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
for (const [domain, nodes] of extractedByDomain) {
|
|
3477
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3478
|
+
const deduped = nodes.filter((n) => {
|
|
3479
|
+
const key = n.name.toLowerCase().trim();
|
|
3480
|
+
if (seen.has(key)) return false;
|
|
3481
|
+
seen.add(key);
|
|
3482
|
+
return true;
|
|
3483
|
+
});
|
|
3484
|
+
extractedByDomain.set(domain, deduped);
|
|
3485
|
+
}
|
|
3486
|
+
const allDomains = /* @__PURE__ */ new Set([...domainEntryCounts.keys(), ...extractedByDomain.keys()]);
|
|
3487
|
+
const domains = [];
|
|
3488
|
+
let totalExtracted = 0;
|
|
3489
|
+
let totalGaps = 0;
|
|
3490
|
+
for (const domain of allDomains) {
|
|
3491
|
+
const entryCount = domainEntryCounts.get(domain) ?? 0;
|
|
3492
|
+
const extractedNodes = extractedByDomain.get(domain) ?? [];
|
|
3493
|
+
const extractedCount = extractedNodes.length;
|
|
3494
|
+
totalExtracted += extractedCount;
|
|
3495
|
+
const docNames = domainDocNames.get(domain) ?? [];
|
|
3496
|
+
const gapEntries = [];
|
|
3497
|
+
for (const node of extractedNodes) {
|
|
3498
|
+
const normalizedName = node.name.toLowerCase().trim();
|
|
3499
|
+
if (!docNames.includes(normalizedName)) {
|
|
3500
|
+
gapEntries.push({
|
|
3501
|
+
nodeId: node.id,
|
|
3502
|
+
name: node.name,
|
|
3503
|
+
nodeType: node.type,
|
|
3504
|
+
source: node.metadata?.source ?? "unknown",
|
|
3505
|
+
hasContent: Boolean(node.content && node.content.trim().length >= 10)
|
|
3506
|
+
});
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
totalGaps += gapEntries.length;
|
|
3510
|
+
domains.push({ domain, entryCount, extractedCount, gapCount: gapEntries.length, gapEntries });
|
|
3511
|
+
}
|
|
3133
3512
|
return {
|
|
3134
3513
|
domains,
|
|
3135
3514
|
totalEntries,
|
|
3515
|
+
totalExtracted,
|
|
3516
|
+
totalGaps,
|
|
3136
3517
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3137
3518
|
};
|
|
3138
3519
|
}
|
|
3139
3520
|
async writeGapReport(report) {
|
|
3140
|
-
const gapsDir =
|
|
3141
|
-
await
|
|
3521
|
+
const gapsDir = path9.join(this.projectDir, ".harness", "knowledge");
|
|
3522
|
+
await fs8.mkdir(gapsDir, { recursive: true });
|
|
3523
|
+
const hasDifferential = report.totalExtracted > 0;
|
|
3142
3524
|
const lines = [
|
|
3143
3525
|
"# Knowledge Gaps Report",
|
|
3144
3526
|
"",
|
|
3145
3527
|
`Generated: ${report.generatedAt}`,
|
|
3146
3528
|
"",
|
|
3147
3529
|
"## Coverage by Domain",
|
|
3148
|
-
""
|
|
3149
|
-
"| Domain | Entries |",
|
|
3150
|
-
"| ------ | ------- |"
|
|
3530
|
+
""
|
|
3151
3531
|
];
|
|
3152
|
-
|
|
3153
|
-
lines.push(
|
|
3532
|
+
if (hasDifferential) {
|
|
3533
|
+
lines.push(
|
|
3534
|
+
"| Domain | Documented | Extracted | Gaps |",
|
|
3535
|
+
"| ------ | ---------- | --------- | ---- |"
|
|
3536
|
+
);
|
|
3537
|
+
for (const domain of report.domains) {
|
|
3538
|
+
lines.push(
|
|
3539
|
+
`| ${domain.domain} | ${domain.entryCount} | ${domain.extractedCount} | ${domain.gapCount} |`
|
|
3540
|
+
);
|
|
3541
|
+
}
|
|
3542
|
+
lines.push(
|
|
3543
|
+
"",
|
|
3544
|
+
`## Summary`,
|
|
3545
|
+
"",
|
|
3546
|
+
`- **Total Documented:** ${report.totalEntries}`,
|
|
3547
|
+
`- **Total Extracted:** ${report.totalExtracted}`,
|
|
3548
|
+
`- **Total Gaps:** ${report.totalGaps}`,
|
|
3549
|
+
""
|
|
3550
|
+
);
|
|
3551
|
+
} else {
|
|
3552
|
+
lines.push("| Domain | Entries |", "| ------ | ------- |");
|
|
3553
|
+
for (const domain of report.domains) {
|
|
3554
|
+
lines.push(`| ${domain.domain} | ${domain.entryCount} |`);
|
|
3555
|
+
}
|
|
3556
|
+
lines.push("", `## Total Entries: ${report.totalEntries}`, "");
|
|
3154
3557
|
}
|
|
3155
|
-
|
|
3156
|
-
await fs7.writeFile(path8.join(gapsDir, "gaps.md"), lines.join("\n"), "utf-8");
|
|
3558
|
+
await fs8.writeFile(path9.join(gapsDir, "gaps.md"), lines.join("\n"), "utf-8");
|
|
3157
3559
|
}
|
|
3158
3560
|
};
|
|
3159
3561
|
|
|
3160
3562
|
// src/ingest/extractors/ExtractionRunner.ts
|
|
3161
|
-
var
|
|
3162
|
-
var
|
|
3163
|
-
var
|
|
3563
|
+
var fs9 = __toESM(require("fs/promises"));
|
|
3564
|
+
var path10 = __toESM(require("path"));
|
|
3565
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
3164
3566
|
"node_modules",
|
|
3165
3567
|
"dist",
|
|
3166
3568
|
"target",
|
|
@@ -3197,11 +3599,11 @@ var EXT_TO_LANGUAGE = {
|
|
|
3197
3599
|
};
|
|
3198
3600
|
var SKIP_EXTENSIONS2 = /* @__PURE__ */ new Set([".d.ts"]);
|
|
3199
3601
|
function detectLanguage(filePath) {
|
|
3200
|
-
const name =
|
|
3602
|
+
const name = path10.basename(filePath);
|
|
3201
3603
|
for (const skip of SKIP_EXTENSIONS2) {
|
|
3202
3604
|
if (name.endsWith(skip)) return void 0;
|
|
3203
3605
|
}
|
|
3204
|
-
const ext =
|
|
3606
|
+
const ext = path10.extname(filePath);
|
|
3205
3607
|
return EXT_TO_LANGUAGE[ext];
|
|
3206
3608
|
}
|
|
3207
3609
|
var EXTRACTOR_EDGE_TYPE = {
|
|
@@ -3227,7 +3629,7 @@ var ExtractionRunner = class {
|
|
|
3227
3629
|
let nodesAdded = 0;
|
|
3228
3630
|
let nodesUpdated = 0;
|
|
3229
3631
|
let edgesAdded = 0;
|
|
3230
|
-
await
|
|
3632
|
+
await fs9.mkdir(outputDir, { recursive: true });
|
|
3231
3633
|
const files = await this.findSourceFiles(projectDir);
|
|
3232
3634
|
const recordsByExtractor = /* @__PURE__ */ new Map();
|
|
3233
3635
|
for (const ext of this.extractors) {
|
|
@@ -3236,17 +3638,17 @@ var ExtractionRunner = class {
|
|
|
3236
3638
|
for (const filePath of files) {
|
|
3237
3639
|
const language = detectLanguage(filePath);
|
|
3238
3640
|
if (!language) continue;
|
|
3239
|
-
const ext =
|
|
3641
|
+
const ext = path10.extname(filePath);
|
|
3240
3642
|
let content;
|
|
3241
3643
|
try {
|
|
3242
|
-
content = await
|
|
3644
|
+
content = await fs9.readFile(filePath, "utf-8");
|
|
3243
3645
|
} catch (err) {
|
|
3244
3646
|
errors.push(
|
|
3245
3647
|
`Failed to read ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
3246
3648
|
);
|
|
3247
3649
|
continue;
|
|
3248
3650
|
}
|
|
3249
|
-
const relativePath =
|
|
3651
|
+
const relativePath = path10.relative(projectDir, filePath).replaceAll("\\", "/");
|
|
3250
3652
|
for (const extractor2 of this.extractors) {
|
|
3251
3653
|
if (!extractor2.supportedExtensions.includes(ext)) continue;
|
|
3252
3654
|
try {
|
|
@@ -3283,9 +3685,9 @@ var ExtractionRunner = class {
|
|
|
3283
3685
|
}
|
|
3284
3686
|
/** Write extraction records to JSONL file. */
|
|
3285
3687
|
async writeJsonl(records, outputDir, extractorName) {
|
|
3286
|
-
const filePath =
|
|
3688
|
+
const filePath = path10.join(outputDir, `${extractorName}.jsonl`);
|
|
3287
3689
|
const lines = records.map((r) => JSON.stringify(r));
|
|
3288
|
-
await
|
|
3690
|
+
await fs9.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
|
|
3289
3691
|
}
|
|
3290
3692
|
/** Create or update a graph node from an extraction record. */
|
|
3291
3693
|
persistRecord(record, store) {
|
|
@@ -3357,13 +3759,13 @@ var ExtractionRunner = class {
|
|
|
3357
3759
|
const results = [];
|
|
3358
3760
|
let entries;
|
|
3359
3761
|
try {
|
|
3360
|
-
entries = await
|
|
3762
|
+
entries = await fs9.readdir(dir, { withFileTypes: true });
|
|
3361
3763
|
} catch {
|
|
3362
3764
|
return results;
|
|
3363
3765
|
}
|
|
3364
3766
|
for (const entry of entries) {
|
|
3365
|
-
const fullPath =
|
|
3366
|
-
if (entry.isDirectory() && !
|
|
3767
|
+
const fullPath = path10.join(dir, entry.name);
|
|
3768
|
+
if (entry.isDirectory() && !SKIP_DIRS2.has(entry.name)) {
|
|
3367
3769
|
results.push(...await this.findSourceFiles(fullPath));
|
|
3368
3770
|
} else if (entry.isFile() && detectLanguage(fullPath) !== void 0) {
|
|
3369
3771
|
results.push(fullPath);
|
|
@@ -4489,57 +4891,9 @@ var ImageAnalysisExtractor = class {
|
|
|
4489
4891
|
const errors = [];
|
|
4490
4892
|
for (const imagePath of imagePaths) {
|
|
4491
4893
|
try {
|
|
4492
|
-
const
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
responseSchema: {},
|
|
4496
|
-
// Schema handled by provider
|
|
4497
|
-
maxTokens: 1e3
|
|
4498
|
-
});
|
|
4499
|
-
const result = response.result;
|
|
4500
|
-
const pathHash = hash(imagePath);
|
|
4501
|
-
const annotationId = `img:${pathHash}`;
|
|
4502
|
-
store.addNode({
|
|
4503
|
-
id: annotationId,
|
|
4504
|
-
type: "image_annotation",
|
|
4505
|
-
name: result.description.slice(0, 200),
|
|
4506
|
-
path: imagePath,
|
|
4507
|
-
content: result.description,
|
|
4508
|
-
hash: hash(result.description),
|
|
4509
|
-
metadata: {
|
|
4510
|
-
source: "image-analysis",
|
|
4511
|
-
detectedElements: result.detectedElements,
|
|
4512
|
-
extractedText: result.extractedText,
|
|
4513
|
-
designPatterns: result.designPatterns,
|
|
4514
|
-
accessibilityNotes: result.accessibilityNotes,
|
|
4515
|
-
model: response.model
|
|
4516
|
-
}
|
|
4517
|
-
});
|
|
4518
|
-
nodesAdded++;
|
|
4519
|
-
const fileNodes = store.findNodes({ type: "file" });
|
|
4520
|
-
const fileNode = fileNodes.find((n) => n.path === imagePath);
|
|
4521
|
-
if (fileNode) {
|
|
4522
|
-
store.addEdge({ from: annotationId, to: fileNode.id, type: "annotates" });
|
|
4523
|
-
edgesAdded++;
|
|
4524
|
-
}
|
|
4525
|
-
for (const pattern of result.designPatterns) {
|
|
4526
|
-
const conceptId = `img:concept:${hash(pattern + imagePath)}`;
|
|
4527
|
-
store.addNode({
|
|
4528
|
-
id: conceptId,
|
|
4529
|
-
type: "business_concept",
|
|
4530
|
-
name: pattern,
|
|
4531
|
-
content: `Design pattern detected in ${imagePath}: ${pattern}`,
|
|
4532
|
-
hash: hash(pattern),
|
|
4533
|
-
metadata: {
|
|
4534
|
-
source: "image-analysis",
|
|
4535
|
-
sourceImage: imagePath,
|
|
4536
|
-
domain: "design"
|
|
4537
|
-
}
|
|
4538
|
-
});
|
|
4539
|
-
nodesAdded++;
|
|
4540
|
-
store.addEdge({ from: annotationId, to: conceptId, type: "contains" });
|
|
4541
|
-
edgesAdded++;
|
|
4542
|
-
}
|
|
4894
|
+
const counts = await this.processImage(store, imagePath);
|
|
4895
|
+
nodesAdded += counts.nodes;
|
|
4896
|
+
edgesAdded += counts.edges;
|
|
4543
4897
|
} catch (err) {
|
|
4544
4898
|
errors.push(
|
|
4545
4899
|
`Image analysis failed for ${imagePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -4555,6 +4909,68 @@ var ImageAnalysisExtractor = class {
|
|
|
4555
4909
|
durationMs: Date.now() - start
|
|
4556
4910
|
};
|
|
4557
4911
|
}
|
|
4912
|
+
/** Analyze a single image and add annotation + concept nodes to the store. */
|
|
4913
|
+
async processImage(store, imagePath) {
|
|
4914
|
+
const response = await this.provider.analyze({
|
|
4915
|
+
prompt: "Analyze this image and provide a structured description of its visual contents.",
|
|
4916
|
+
systemPrompt: "You are an image analysis assistant. Describe the image contents, detect UI elements, extract visible text, identify design patterns, and note accessibility concerns.",
|
|
4917
|
+
responseSchema: {},
|
|
4918
|
+
// Schema handled by provider
|
|
4919
|
+
maxTokens: 1e3
|
|
4920
|
+
});
|
|
4921
|
+
const result = response.result;
|
|
4922
|
+
const annotationId = `img:${hash(imagePath)}`;
|
|
4923
|
+
let nodes = 0;
|
|
4924
|
+
let edges = 0;
|
|
4925
|
+
store.addNode({
|
|
4926
|
+
id: annotationId,
|
|
4927
|
+
type: "image_annotation",
|
|
4928
|
+
name: result.description.slice(0, 200),
|
|
4929
|
+
path: imagePath,
|
|
4930
|
+
content: result.description,
|
|
4931
|
+
hash: hash(result.description),
|
|
4932
|
+
metadata: {
|
|
4933
|
+
source: "image-analysis",
|
|
4934
|
+
detectedElements: result.detectedElements,
|
|
4935
|
+
extractedText: result.extractedText,
|
|
4936
|
+
designPatterns: result.designPatterns,
|
|
4937
|
+
accessibilityNotes: result.accessibilityNotes,
|
|
4938
|
+
model: response.model
|
|
4939
|
+
}
|
|
4940
|
+
});
|
|
4941
|
+
nodes++;
|
|
4942
|
+
edges += this.linkToFileNode(store, annotationId, imagePath);
|
|
4943
|
+
for (const pattern of result.designPatterns) {
|
|
4944
|
+
edges += this.addDesignPatternConcept(store, annotationId, imagePath, pattern);
|
|
4945
|
+
nodes++;
|
|
4946
|
+
}
|
|
4947
|
+
return { nodes, edges };
|
|
4948
|
+
}
|
|
4949
|
+
/** Link annotation to its source file node if it exists. Returns 1 if linked, 0 otherwise. */
|
|
4950
|
+
linkToFileNode(store, annotationId, imagePath) {
|
|
4951
|
+
const fileNode = store.findNodes({ type: "file" }).find((n) => n.path === imagePath);
|
|
4952
|
+
if (!fileNode) return 0;
|
|
4953
|
+
store.addEdge({ from: annotationId, to: fileNode.id, type: "annotates" });
|
|
4954
|
+
return 1;
|
|
4955
|
+
}
|
|
4956
|
+
/** Create a business_concept node for a design pattern and link it. Returns edges added. */
|
|
4957
|
+
addDesignPatternConcept(store, annotationId, imagePath, pattern) {
|
|
4958
|
+
const conceptId = `img:concept:${hash(pattern + imagePath)}`;
|
|
4959
|
+
store.addNode({
|
|
4960
|
+
id: conceptId,
|
|
4961
|
+
type: "business_concept",
|
|
4962
|
+
name: pattern,
|
|
4963
|
+
content: `Design pattern detected in ${imagePath}: ${pattern}`,
|
|
4964
|
+
hash: hash(pattern),
|
|
4965
|
+
metadata: {
|
|
4966
|
+
source: "image-analysis",
|
|
4967
|
+
sourceImage: imagePath,
|
|
4968
|
+
domain: "design"
|
|
4969
|
+
}
|
|
4970
|
+
});
|
|
4971
|
+
store.addEdge({ from: annotationId, to: conceptId, type: "contains" });
|
|
4972
|
+
return 1;
|
|
4973
|
+
}
|
|
4558
4974
|
};
|
|
4559
4975
|
|
|
4560
4976
|
// src/ingest/knowledgeTypes.ts
|
|
@@ -4565,6 +4981,7 @@ var KNOWLEDGE_NODE_TYPES = [
|
|
|
4565
4981
|
"business_term",
|
|
4566
4982
|
"business_concept",
|
|
4567
4983
|
"business_metric",
|
|
4984
|
+
"decision",
|
|
4568
4985
|
"design_token",
|
|
4569
4986
|
"design_constraint",
|
|
4570
4987
|
"aesthetic_intent",
|
|
@@ -4605,83 +5022,99 @@ function classifyConflict(a, b) {
|
|
|
4605
5022
|
return "temporal_conflict";
|
|
4606
5023
|
return "status_divergence";
|
|
4607
5024
|
}
|
|
4608
|
-
var
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
for (
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
for (let j = i + 1; j < group.length; j++) {
|
|
4661
|
-
addContradiction(group[i], group[j], 1);
|
|
4662
|
-
}
|
|
5025
|
+
var SEVERITY_MAP = {
|
|
5026
|
+
value_mismatch: "critical",
|
|
5027
|
+
definition_conflict: "high",
|
|
5028
|
+
status_divergence: "medium",
|
|
5029
|
+
temporal_conflict: "medium"
|
|
5030
|
+
};
|
|
5031
|
+
function buildEntry(node, source) {
|
|
5032
|
+
return {
|
|
5033
|
+
nodeId: node.id,
|
|
5034
|
+
source,
|
|
5035
|
+
name: node.name,
|
|
5036
|
+
content: node.content ?? "",
|
|
5037
|
+
lastModified: node.lastModified
|
|
5038
|
+
};
|
|
5039
|
+
}
|
|
5040
|
+
function groupByName(nodes) {
|
|
5041
|
+
const byName = /* @__PURE__ */ new Map();
|
|
5042
|
+
for (const node of nodes) {
|
|
5043
|
+
const key = node.name.toLowerCase().trim();
|
|
5044
|
+
const group = byName.get(key) ?? [];
|
|
5045
|
+
group.push(node);
|
|
5046
|
+
byName.set(key, group);
|
|
5047
|
+
}
|
|
5048
|
+
return byName;
|
|
5049
|
+
}
|
|
5050
|
+
function tryAddContradiction(acc, a, b, similarity) {
|
|
5051
|
+
const sourceA = a.metadata.source ?? "unknown";
|
|
5052
|
+
const sourceB = b.metadata.source ?? "unknown";
|
|
5053
|
+
if (sourceA === sourceB) return;
|
|
5054
|
+
if ((a.hash ?? a.id) === (b.hash ?? b.id)) return;
|
|
5055
|
+
const pairId = [a.id, b.id].sort().join(":");
|
|
5056
|
+
if (acc.seen.has(pairId)) return;
|
|
5057
|
+
acc.seen.add(pairId);
|
|
5058
|
+
const conflictType = classifyConflict(a, b);
|
|
5059
|
+
const pairKey = [sourceA, sourceB].sort().join("\u2194");
|
|
5060
|
+
acc.sourcePairCounts[pairKey] = (acc.sourcePairCounts[pairKey] ?? 0) + 1;
|
|
5061
|
+
acc.contradictions.push({
|
|
5062
|
+
id: `contradiction:${a.id}:${b.id}`,
|
|
5063
|
+
entityA: buildEntry(a, sourceA),
|
|
5064
|
+
entityB: buildEntry(b, sourceB),
|
|
5065
|
+
similarity,
|
|
5066
|
+
conflictType,
|
|
5067
|
+
severity: SEVERITY_MAP[conflictType],
|
|
5068
|
+
description: `"${a.name}" has conflicting definitions from ${sourceA} and ${sourceB}`
|
|
5069
|
+
});
|
|
5070
|
+
}
|
|
5071
|
+
function collectExactMatches(byName, acc) {
|
|
5072
|
+
for (const [, group] of byName) {
|
|
5073
|
+
if (group.length < 2) continue;
|
|
5074
|
+
for (let i = 0; i < group.length; i++) {
|
|
5075
|
+
for (let j = i + 1; j < group.length; j++) {
|
|
5076
|
+
tryAddContradiction(acc, group[i], group[j], 1);
|
|
4663
5077
|
}
|
|
4664
5078
|
}
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
5079
|
+
}
|
|
5080
|
+
}
|
|
5081
|
+
function collectFuzzyMatches(byName, acc) {
|
|
5082
|
+
const keys = [...byName.keys()];
|
|
5083
|
+
for (let i = 0; i < keys.length; i++) {
|
|
5084
|
+
for (let j = i + 1; j < keys.length; j++) {
|
|
5085
|
+
const keyA = keys[i];
|
|
5086
|
+
const keyB = keys[j];
|
|
5087
|
+
const maxLen = Math.max(keyA.length, keyB.length);
|
|
5088
|
+
const minLen = Math.min(keyA.length, keyB.length);
|
|
5089
|
+
if (minLen / maxLen < SIMILARITY_THRESHOLD) continue;
|
|
5090
|
+
const ratio = levenshteinRatio(keyA, keyB);
|
|
5091
|
+
if (ratio < SIMILARITY_THRESHOLD) continue;
|
|
5092
|
+
const groupA = byName.get(keyA);
|
|
5093
|
+
const groupB = byName.get(keyB);
|
|
5094
|
+
for (const a of groupA) {
|
|
5095
|
+
for (const b of groupB) {
|
|
5096
|
+
tryAddContradiction(acc, a, b, ratio);
|
|
4681
5097
|
}
|
|
4682
5098
|
}
|
|
4683
5099
|
}
|
|
4684
|
-
|
|
5100
|
+
}
|
|
5101
|
+
}
|
|
5102
|
+
var ContradictionDetector = class {
|
|
5103
|
+
detect(store) {
|
|
5104
|
+
const nodes = KNOWLEDGE_NODE_TYPES.flatMap((t) => store.findNodes({ type: t }));
|
|
5105
|
+
const byName = groupByName(nodes);
|
|
5106
|
+
const acc = {
|
|
5107
|
+
contradictions: [],
|
|
5108
|
+
sourcePairCounts: {},
|
|
5109
|
+
seen: /* @__PURE__ */ new Set()
|
|
5110
|
+
};
|
|
5111
|
+
collectExactMatches(byName, acc);
|
|
5112
|
+
collectFuzzyMatches(byName, acc);
|
|
5113
|
+
return {
|
|
5114
|
+
contradictions: acc.contradictions,
|
|
5115
|
+
sourcePairCounts: acc.sourcePairCounts,
|
|
5116
|
+
totalChecked: nodes.length
|
|
5117
|
+
};
|
|
4685
5118
|
}
|
|
4686
5119
|
};
|
|
4687
5120
|
|
|
@@ -4712,64 +5145,80 @@ function toGrade(score) {
|
|
|
4712
5145
|
if (score >= 20) return "D";
|
|
4713
5146
|
return "F";
|
|
4714
5147
|
}
|
|
5148
|
+
function groupByDomain(nodes, _fallback, options = {}) {
|
|
5149
|
+
const map = /* @__PURE__ */ new Map();
|
|
5150
|
+
for (const node of nodes) {
|
|
5151
|
+
const domain = inferDomain(node, options);
|
|
5152
|
+
const group = map.get(domain) ?? [];
|
|
5153
|
+
group.push(node);
|
|
5154
|
+
map.set(domain, group);
|
|
5155
|
+
}
|
|
5156
|
+
return map;
|
|
5157
|
+
}
|
|
5158
|
+
function countLinkedEntities(codeEntries, store) {
|
|
5159
|
+
const linkedIds = /* @__PURE__ */ new Set();
|
|
5160
|
+
for (const codeNode of codeEntries) {
|
|
5161
|
+
if (hasKnowledgeEdge(codeNode.id, store)) {
|
|
5162
|
+
linkedIds.add(codeNode.id);
|
|
5163
|
+
}
|
|
5164
|
+
}
|
|
5165
|
+
return linkedIds;
|
|
5166
|
+
}
|
|
5167
|
+
function hasKnowledgeEdge(nodeId, store) {
|
|
5168
|
+
for (const edgeType of KNOWLEDGE_EDGE_TYPES) {
|
|
5169
|
+
if (store.getEdges({ to: nodeId, type: edgeType }).length > 0) return true;
|
|
5170
|
+
}
|
|
5171
|
+
return false;
|
|
5172
|
+
}
|
|
5173
|
+
function computeSourceBreakdown(knEntries) {
|
|
5174
|
+
const breakdown = {};
|
|
5175
|
+
for (const kn of knEntries) {
|
|
5176
|
+
const src = kn.metadata.source ?? "unknown";
|
|
5177
|
+
breakdown[src] = (breakdown[src] ?? 0) + 1;
|
|
5178
|
+
}
|
|
5179
|
+
return breakdown;
|
|
5180
|
+
}
|
|
5181
|
+
function computeDomainScore(knowledgeEntries, codeEntities, linkedEntities, uniqueSources) {
|
|
5182
|
+
const codeCoverageComponent = codeEntities > 0 ? linkedEntities / codeEntities * 60 : 0;
|
|
5183
|
+
const knowledgeDepthComponent = Math.min(knowledgeEntries / 10, 1) * 20;
|
|
5184
|
+
const sourceDiversityComponent = Math.min(uniqueSources / 3, 1) * 20;
|
|
5185
|
+
return Math.round(codeCoverageComponent + knowledgeDepthComponent + sourceDiversityComponent);
|
|
5186
|
+
}
|
|
5187
|
+
function scoreDomain(domain, knEntries, codeEntries, store) {
|
|
5188
|
+
const linkedIds = countLinkedEntities(codeEntries, store);
|
|
5189
|
+
const sourceBreakdown = computeSourceBreakdown(knEntries);
|
|
5190
|
+
const codeEntities = codeEntries.length;
|
|
5191
|
+
const linkedEntities = linkedIds.size;
|
|
5192
|
+
const knowledgeEntries = knEntries.length;
|
|
5193
|
+
const uniqueSources = Object.keys(sourceBreakdown).length;
|
|
5194
|
+
const score = computeDomainScore(knowledgeEntries, codeEntities, linkedEntities, uniqueSources);
|
|
5195
|
+
return {
|
|
5196
|
+
domain,
|
|
5197
|
+
score,
|
|
5198
|
+
knowledgeEntries,
|
|
5199
|
+
codeEntities,
|
|
5200
|
+
linkedEntities,
|
|
5201
|
+
unlinkedEntities: codeEntities - linkedEntities,
|
|
5202
|
+
sourceBreakdown,
|
|
5203
|
+
grade: toGrade(score)
|
|
5204
|
+
};
|
|
5205
|
+
}
|
|
4715
5206
|
var CoverageScorer = class {
|
|
5207
|
+
constructor(inferenceOptions = {}) {
|
|
5208
|
+
this.inferenceOptions = inferenceOptions;
|
|
5209
|
+
}
|
|
5210
|
+
inferenceOptions;
|
|
4716
5211
|
score(store) {
|
|
4717
5212
|
const knowledgeNodes = KNOWLEDGE_TYPES.flatMap((t) => store.findNodes({ type: t }));
|
|
4718
|
-
const domainMap =
|
|
4719
|
-
for (const node of knowledgeNodes) {
|
|
4720
|
-
const domain = node.metadata.domain ?? "unclassified";
|
|
4721
|
-
const group = domainMap.get(domain) ?? [];
|
|
4722
|
-
group.push(node);
|
|
4723
|
-
domainMap.set(domain, group);
|
|
4724
|
-
}
|
|
5213
|
+
const domainMap = groupByDomain(knowledgeNodes, void 0, this.inferenceOptions);
|
|
4725
5214
|
const codeNodes = CODE_TYPES2.flatMap((t) => store.findNodes({ type: t }));
|
|
4726
|
-
const codeDomains =
|
|
4727
|
-
for (const node of codeNodes) {
|
|
4728
|
-
const domain = node.metadata.domain ?? this.domainFromPath(node.path);
|
|
4729
|
-
const group = codeDomains.get(domain) ?? [];
|
|
4730
|
-
group.push(node);
|
|
4731
|
-
codeDomains.set(domain, group);
|
|
4732
|
-
}
|
|
5215
|
+
const codeDomains = groupByDomain(codeNodes, void 0, this.inferenceOptions);
|
|
4733
5216
|
const allDomains = /* @__PURE__ */ new Set([...domainMap.keys(), ...codeDomains.keys()]);
|
|
4734
5217
|
const domains = [];
|
|
4735
5218
|
for (const domain of allDomains) {
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
const linkedIds = /* @__PURE__ */ new Set();
|
|
4739
|
-
for (const codeNode of codeEntries) {
|
|
4740
|
-
for (const edgeType of KNOWLEDGE_EDGE_TYPES) {
|
|
4741
|
-
const edges = store.getEdges({ to: codeNode.id, type: edgeType });
|
|
4742
|
-
if (edges.length > 0) {
|
|
4743
|
-
linkedIds.add(codeNode.id);
|
|
4744
|
-
break;
|
|
4745
|
-
}
|
|
4746
|
-
}
|
|
4747
|
-
}
|
|
4748
|
-
const sourceBreakdown = {};
|
|
4749
|
-
for (const kn of knEntries) {
|
|
4750
|
-
const src = kn.metadata.source ?? "unknown";
|
|
4751
|
-
sourceBreakdown[src] = (sourceBreakdown[src] ?? 0) + 1;
|
|
4752
|
-
}
|
|
4753
|
-
const codeEntities = codeEntries.length;
|
|
4754
|
-
const linkedEntities = linkedIds.size;
|
|
4755
|
-
const knowledgeEntries = knEntries.length;
|
|
4756
|
-
const uniqueSources = Object.keys(sourceBreakdown).length;
|
|
4757
|
-
const codeCoverageComponent = codeEntities > 0 ? linkedEntities / codeEntities * 60 : 0;
|
|
4758
|
-
const knowledgeDepthComponent = Math.min(knowledgeEntries / 10, 1) * 20;
|
|
4759
|
-
const sourceDiversityComponent = Math.min(uniqueSources / 3, 1) * 20;
|
|
4760
|
-
const score = Math.round(
|
|
4761
|
-
codeCoverageComponent + knowledgeDepthComponent + sourceDiversityComponent
|
|
5219
|
+
domains.push(
|
|
5220
|
+
scoreDomain(domain, domainMap.get(domain) ?? [], codeDomains.get(domain) ?? [], store)
|
|
4762
5221
|
);
|
|
4763
|
-
domains.push({
|
|
4764
|
-
domain,
|
|
4765
|
-
score,
|
|
4766
|
-
knowledgeEntries,
|
|
4767
|
-
codeEntities,
|
|
4768
|
-
linkedEntities,
|
|
4769
|
-
unlinkedEntities: codeEntities - linkedEntities,
|
|
4770
|
-
sourceBreakdown,
|
|
4771
|
-
grade: toGrade(score)
|
|
4772
|
-
});
|
|
4773
5222
|
}
|
|
4774
5223
|
const overallScore = domains.length > 0 ? Math.round(domains.reduce((sum, d) => sum + d.score, 0) / domains.length) : 0;
|
|
4775
5224
|
return {
|
|
@@ -4779,19 +5228,157 @@ var CoverageScorer = class {
|
|
|
4779
5228
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4780
5229
|
};
|
|
4781
5230
|
}
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
5231
|
+
};
|
|
5232
|
+
|
|
5233
|
+
// src/ingest/KnowledgeDocMaterializer.ts
|
|
5234
|
+
var fs10 = __toESM(require("fs/promises"));
|
|
5235
|
+
var path11 = __toESM(require("path"));
|
|
5236
|
+
var VALID_BUSINESS_TYPES = /* @__PURE__ */ new Set([
|
|
5237
|
+
"business_rule",
|
|
5238
|
+
"business_process",
|
|
5239
|
+
"business_concept",
|
|
5240
|
+
"business_term",
|
|
5241
|
+
"business_metric"
|
|
5242
|
+
]);
|
|
5243
|
+
var DEFAULT_MAX_DOCS = 50;
|
|
5244
|
+
var MAX_COLLISION_SUFFIX = 10;
|
|
5245
|
+
var KnowledgeDocMaterializer = class {
|
|
5246
|
+
constructor(store, inferenceOptions = {}) {
|
|
5247
|
+
this.store = store;
|
|
5248
|
+
this.inferenceOptions = inferenceOptions;
|
|
5249
|
+
}
|
|
5250
|
+
store;
|
|
5251
|
+
inferenceOptions;
|
|
5252
|
+
async materialize(gapEntries, options) {
|
|
5253
|
+
const maxDocs = options.maxDocs ?? DEFAULT_MAX_DOCS;
|
|
5254
|
+
const created = [];
|
|
5255
|
+
const skipped = [];
|
|
5256
|
+
const createdNames = /* @__PURE__ */ new Set();
|
|
5257
|
+
for (const entry of gapEntries) {
|
|
5258
|
+
const resolved = await this.resolveEntry(
|
|
5259
|
+
entry,
|
|
5260
|
+
created.length,
|
|
5261
|
+
maxDocs,
|
|
5262
|
+
createdNames,
|
|
5263
|
+
options
|
|
5264
|
+
);
|
|
5265
|
+
if ("reason" in resolved) {
|
|
5266
|
+
skipped.push(resolved);
|
|
5267
|
+
continue;
|
|
5268
|
+
}
|
|
5269
|
+
const { node, domain, domainDir, nameKey } = resolved;
|
|
5270
|
+
await fs10.mkdir(domainDir, { recursive: true });
|
|
5271
|
+
const basename10 = this.generateFilename(entry.name);
|
|
5272
|
+
const filename = await this.resolveCollision(domainDir, basename10);
|
|
5273
|
+
const content = this.formatDoc(node, domain);
|
|
5274
|
+
const filePath = ["docs", "knowledge", domain, filename].join("/");
|
|
5275
|
+
await fs10.writeFile(path11.join(options.projectDir, filePath), content, "utf-8");
|
|
5276
|
+
createdNames.add(nameKey);
|
|
5277
|
+
created.push({ filePath, nodeId: entry.nodeId, domain, name: entry.name });
|
|
5278
|
+
}
|
|
5279
|
+
return { created, skipped };
|
|
5280
|
+
}
|
|
5281
|
+
async resolveEntry(entry, createdCount, maxDocs, createdNames, options) {
|
|
5282
|
+
if (!entry.hasContent) {
|
|
5283
|
+
return { nodeId: entry.nodeId, name: entry.name, reason: "no_content" };
|
|
5284
|
+
}
|
|
5285
|
+
const node = this.store.getNode(entry.nodeId);
|
|
5286
|
+
if (!node || !node.content || node.content.trim().length < 10) {
|
|
5287
|
+
return { nodeId: entry.nodeId, name: entry.name, reason: "no_content" };
|
|
5288
|
+
}
|
|
5289
|
+
const domain = this.inferDomain(node);
|
|
5290
|
+
if (!domain || /[/\\]|\.\.|\0/.test(domain)) {
|
|
5291
|
+
return { nodeId: entry.nodeId, name: entry.name, reason: "no_domain" };
|
|
5292
|
+
}
|
|
5293
|
+
if (createdCount >= maxDocs) {
|
|
5294
|
+
return { nodeId: entry.nodeId, name: entry.name, reason: "cap_reached" };
|
|
5295
|
+
}
|
|
5296
|
+
const domainDir = path11.join(options.projectDir, "docs", "knowledge", domain);
|
|
5297
|
+
const nameKey = `${domain}:${entry.name.toLowerCase().trim()}`;
|
|
5298
|
+
if (!createdNames.has(nameKey) && await this.hasExistingDoc(domainDir, entry.name)) {
|
|
5299
|
+
return { nodeId: entry.nodeId, name: entry.name, reason: "already_documented" };
|
|
5300
|
+
}
|
|
5301
|
+
if (options.dryRun) {
|
|
5302
|
+
return { nodeId: entry.nodeId, name: entry.name, reason: "dry_run" };
|
|
5303
|
+
}
|
|
5304
|
+
return { node, domain, domainDir, nameKey };
|
|
5305
|
+
}
|
|
5306
|
+
inferDomain(node) {
|
|
5307
|
+
const result = inferDomain(node, this.inferenceOptions);
|
|
5308
|
+
return result === "unknown" ? null : result;
|
|
5309
|
+
}
|
|
5310
|
+
generateFilename(name) {
|
|
5311
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
|
|
5312
|
+
return `${slug}.md`;
|
|
5313
|
+
}
|
|
5314
|
+
async resolveCollision(dir, basename10) {
|
|
5315
|
+
const ext = path11.extname(basename10);
|
|
5316
|
+
const stem = path11.basename(basename10, ext);
|
|
5317
|
+
try {
|
|
5318
|
+
await fs10.access(path11.join(dir, basename10));
|
|
5319
|
+
} catch {
|
|
5320
|
+
return basename10;
|
|
5321
|
+
}
|
|
5322
|
+
for (let i = 2; i <= MAX_COLLISION_SUFFIX; i++) {
|
|
5323
|
+
const candidate = `${stem}-${i}${ext}`;
|
|
5324
|
+
try {
|
|
5325
|
+
await fs10.access(path11.join(dir, candidate));
|
|
5326
|
+
} catch {
|
|
5327
|
+
return candidate;
|
|
5328
|
+
}
|
|
5329
|
+
}
|
|
5330
|
+
throw new Error(
|
|
5331
|
+
`Cannot resolve filename collision for "${basename10}" after ${MAX_COLLISION_SUFFIX} attempts`
|
|
5332
|
+
);
|
|
5333
|
+
}
|
|
5334
|
+
formatDoc(node, domain) {
|
|
5335
|
+
const mappedType = this.mapNodeType(node);
|
|
5336
|
+
const sanitize = (s) => s.replace(/[\n\r]/g, " ").replace(/:/g, "-");
|
|
5337
|
+
const lines = ["---", `type: ${sanitize(mappedType)}`, `domain: ${sanitize(domain)}`];
|
|
5338
|
+
const tags = node.metadata?.tags;
|
|
5339
|
+
if (Array.isArray(tags) && tags.length > 0) {
|
|
5340
|
+
lines.push(`tags: [${tags.map((t) => sanitize(String(t))).join(", ")}]`);
|
|
5341
|
+
}
|
|
5342
|
+
const related = node.metadata?.related;
|
|
5343
|
+
if (Array.isArray(related) && related.length > 0) {
|
|
5344
|
+
lines.push(`related: [${related.map((r) => sanitize(String(r))).join(", ")}]`);
|
|
5345
|
+
}
|
|
5346
|
+
const title = (node.name ?? "").replace(/[\n\r]/g, " ");
|
|
5347
|
+
lines.push("---", "", `# ${title}`, "", node.content ?? "", "");
|
|
5348
|
+
return lines.join("\n");
|
|
5349
|
+
}
|
|
5350
|
+
/** Check if a doc with a matching title already exists in the domain directory. */
|
|
5351
|
+
async hasExistingDoc(domainDir, name) {
|
|
5352
|
+
const normalizedName = name.toLowerCase().trim();
|
|
5353
|
+
try {
|
|
5354
|
+
const files = await fs10.readdir(domainDir);
|
|
5355
|
+
for (const file of files) {
|
|
5356
|
+
if (!file.endsWith(".md")) continue;
|
|
5357
|
+
const raw = await fs10.readFile(path11.join(domainDir, file), "utf-8");
|
|
5358
|
+
const fmMatch = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
|
|
5359
|
+
const body = fmMatch ? fmMatch[1] : raw;
|
|
5360
|
+
const titleMatch = body.match(/^#\s+(.+)$/m);
|
|
5361
|
+
if (titleMatch && titleMatch[1].trim().toLowerCase() === normalizedName) {
|
|
5362
|
+
return true;
|
|
5363
|
+
}
|
|
5364
|
+
}
|
|
5365
|
+
} catch {
|
|
5366
|
+
}
|
|
5367
|
+
return false;
|
|
5368
|
+
}
|
|
5369
|
+
mapNodeType(node) {
|
|
5370
|
+
if (VALID_BUSINESS_TYPES.has(node.type)) {
|
|
5371
|
+
return node.type;
|
|
5372
|
+
}
|
|
5373
|
+
if (node.type === "business_fact") {
|
|
5374
|
+
return "business_rule";
|
|
5375
|
+
}
|
|
5376
|
+
return "business_concept";
|
|
4790
5377
|
}
|
|
4791
5378
|
};
|
|
4792
5379
|
|
|
4793
5380
|
// src/ingest/KnowledgePipelineRunner.ts
|
|
4794
|
-
var
|
|
5381
|
+
var BUSINESS_NODE_TYPES2 = [
|
|
4795
5382
|
"business_concept",
|
|
4796
5383
|
"business_rule",
|
|
4797
5384
|
"business_process",
|
|
@@ -4800,7 +5387,8 @@ var BUSINESS_NODE_TYPES = [
|
|
|
4800
5387
|
"business_fact"
|
|
4801
5388
|
];
|
|
4802
5389
|
var SNAPSHOT_NODE_TYPES = [
|
|
4803
|
-
...
|
|
5390
|
+
...BUSINESS_NODE_TYPES2,
|
|
5391
|
+
"decision",
|
|
4804
5392
|
"design_token",
|
|
4805
5393
|
"design_constraint",
|
|
4806
5394
|
"aesthetic_intent",
|
|
@@ -4811,52 +5399,102 @@ var KnowledgePipelineRunner = class {
|
|
|
4811
5399
|
this.store = store;
|
|
4812
5400
|
}
|
|
4813
5401
|
store;
|
|
5402
|
+
/** Resolved per-`run()` inference options. Set on entry to `run()`. */
|
|
5403
|
+
inferenceOptions = {};
|
|
4814
5404
|
async run(options) {
|
|
4815
|
-
|
|
5405
|
+
this.inferenceOptions = options.inferenceOptions ?? {};
|
|
4816
5406
|
const remediations = [];
|
|
4817
5407
|
const preSnapshot = this.buildSnapshot(options.domain);
|
|
4818
5408
|
const extraction = await this.extract(options);
|
|
4819
5409
|
const postSnapshot = this.buildSnapshot(options.domain);
|
|
4820
5410
|
let driftResult = this.reconcile(preSnapshot, postSnapshot);
|
|
4821
|
-
const
|
|
4822
|
-
const contradictions = contradictionDetector.detect(this.store);
|
|
5411
|
+
const contradictions = new ContradictionDetector().detect(this.store);
|
|
4823
5412
|
let gapReport = await this.detect(options);
|
|
4824
|
-
const
|
|
4825
|
-
|
|
5413
|
+
const coverage = new CoverageScorer(this.inferenceOptions).score(this.store);
|
|
5414
|
+
let materialization;
|
|
4826
5415
|
let iterations = 1;
|
|
4827
5416
|
if (options.fix) {
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
}
|
|
5417
|
+
const loopResult = await this.runRemediationLoop(
|
|
5418
|
+
options,
|
|
5419
|
+
driftResult,
|
|
5420
|
+
gapReport,
|
|
5421
|
+
remediations
|
|
5422
|
+
);
|
|
5423
|
+
iterations = loopResult.iterations;
|
|
5424
|
+
materialization = loopResult.materialization;
|
|
5425
|
+
}
|
|
5426
|
+
if (options.fix && iterations > 1) {
|
|
5427
|
+
const finalSnapshot = this.buildSnapshot(options.domain);
|
|
5428
|
+
driftResult = this.reconcile(preSnapshot, finalSnapshot);
|
|
5429
|
+
gapReport = await this.detect(options);
|
|
4842
5430
|
}
|
|
4843
5431
|
await this.stageNewFindings(driftResult, options);
|
|
5432
|
+
return this.buildResult(
|
|
5433
|
+
driftResult,
|
|
5434
|
+
iterations,
|
|
5435
|
+
extraction,
|
|
5436
|
+
gapReport,
|
|
5437
|
+
remediations,
|
|
5438
|
+
contradictions,
|
|
5439
|
+
coverage,
|
|
5440
|
+
materialization
|
|
5441
|
+
);
|
|
5442
|
+
}
|
|
5443
|
+
/** Run the remediation convergence loop; returns iteration count and accumulated materialization. */
|
|
5444
|
+
async runRemediationLoop(options, driftResult, gapReport, remediations) {
|
|
5445
|
+
const maxIterations = options.maxIterations ?? 5;
|
|
5446
|
+
let iterations = 1;
|
|
5447
|
+
let currentDrift = driftResult;
|
|
5448
|
+
let currentGapReport = gapReport;
|
|
5449
|
+
let previousIssueCount = currentDrift.findings.length + currentGapReport.totalGaps;
|
|
5450
|
+
let accumulatedMaterialization;
|
|
5451
|
+
while (iterations < maxIterations) {
|
|
5452
|
+
if (currentDrift.findings.length === 0 && currentGapReport.totalGaps === 0) break;
|
|
5453
|
+
const matResult = await this.remediate(currentDrift, remediations, options, currentGapReport);
|
|
5454
|
+
if (matResult) {
|
|
5455
|
+
if (!accumulatedMaterialization) {
|
|
5456
|
+
accumulatedMaterialization = matResult;
|
|
5457
|
+
} else {
|
|
5458
|
+
accumulatedMaterialization = {
|
|
5459
|
+
created: [...accumulatedMaterialization.created, ...matResult.created],
|
|
5460
|
+
skipped: [...accumulatedMaterialization.skipped, ...matResult.skipped]
|
|
5461
|
+
};
|
|
5462
|
+
}
|
|
5463
|
+
}
|
|
5464
|
+
const preSnapshot = this.buildSnapshot(options.domain);
|
|
5465
|
+
await this.extract(options);
|
|
5466
|
+
const postSnapshot = this.buildSnapshot(options.domain);
|
|
5467
|
+
currentDrift = this.reconcile(preSnapshot, postSnapshot);
|
|
5468
|
+
currentGapReport = await this.detect(options);
|
|
5469
|
+
iterations++;
|
|
5470
|
+
const currentIssueCount = currentDrift.findings.length + currentGapReport.totalGaps;
|
|
5471
|
+
if (currentIssueCount >= previousIssueCount) break;
|
|
5472
|
+
previousIssueCount = currentIssueCount;
|
|
5473
|
+
}
|
|
5474
|
+
return {
|
|
5475
|
+
iterations,
|
|
5476
|
+
...accumulatedMaterialization ? { materialization: accumulatedMaterialization } : {}
|
|
5477
|
+
};
|
|
5478
|
+
}
|
|
5479
|
+
/** Assemble the final pipeline result. */
|
|
5480
|
+
buildResult(driftResult, iterations, extraction, gaps, remediations, contradictions, coverage, materialization) {
|
|
4844
5481
|
return {
|
|
4845
5482
|
verdict: this.computeVerdict(driftResult),
|
|
4846
5483
|
driftScore: driftResult.driftScore,
|
|
4847
5484
|
iterations,
|
|
4848
5485
|
findings: driftResult.summary,
|
|
4849
5486
|
extraction,
|
|
4850
|
-
gaps
|
|
5487
|
+
gaps,
|
|
4851
5488
|
remediations,
|
|
4852
5489
|
contradictions,
|
|
4853
|
-
coverage
|
|
5490
|
+
coverage,
|
|
5491
|
+
...materialization ? { materialization } : {}
|
|
4854
5492
|
};
|
|
4855
5493
|
}
|
|
4856
5494
|
// ── Phase 1: EXTRACT ──────────────────────────────────────────────────────
|
|
4857
5495
|
async extract(options) {
|
|
4858
|
-
const extractedDir =
|
|
4859
|
-
await
|
|
5496
|
+
const extractedDir = path12.join(options.projectDir, ".harness", "knowledge", "extracted");
|
|
5497
|
+
await fs11.mkdir(extractedDir, { recursive: true });
|
|
4860
5498
|
const runner = createExtractionRunner();
|
|
4861
5499
|
const extractionResult = await runner.run(options.projectDir, this.store, extractedDir);
|
|
4862
5500
|
const diagramParser = new DiagramParser(this.store);
|
|
@@ -4870,7 +5508,7 @@ var KnowledgePipelineRunner = class {
|
|
|
4870
5508
|
const imageResult = await imageExtractor.analyze(this.store, imagePaths);
|
|
4871
5509
|
imageCount = imageResult.nodesAdded;
|
|
4872
5510
|
}
|
|
4873
|
-
const knowledgeDir =
|
|
5511
|
+
const knowledgeDir = path12.join(options.projectDir, "docs", "knowledge");
|
|
4874
5512
|
const bkIngestor = new BusinessKnowledgeIngestor(this.store);
|
|
4875
5513
|
let bkResult;
|
|
4876
5514
|
try {
|
|
@@ -4885,6 +5523,21 @@ var KnowledgePipelineRunner = class {
|
|
|
4885
5523
|
durationMs: 0
|
|
4886
5524
|
};
|
|
4887
5525
|
}
|
|
5526
|
+
const decisionsDir = path12.join(options.projectDir, "docs", "knowledge", "decisions");
|
|
5527
|
+
const decisionIngestor = new DecisionIngestor(this.store);
|
|
5528
|
+
let decisionResult;
|
|
5529
|
+
try {
|
|
5530
|
+
decisionResult = await decisionIngestor.ingest(decisionsDir);
|
|
5531
|
+
} catch {
|
|
5532
|
+
decisionResult = {
|
|
5533
|
+
nodesAdded: 0,
|
|
5534
|
+
nodesUpdated: 0,
|
|
5535
|
+
edgesAdded: 0,
|
|
5536
|
+
edgesUpdated: 0,
|
|
5537
|
+
errors: [],
|
|
5538
|
+
durationMs: 0
|
|
5539
|
+
};
|
|
5540
|
+
}
|
|
4888
5541
|
const linker = new KnowledgeLinker(this.store, extractedDir);
|
|
4889
5542
|
const linkResult = await linker.link();
|
|
4890
5543
|
return {
|
|
@@ -4892,6 +5545,7 @@ var KnowledgePipelineRunner = class {
|
|
|
4892
5545
|
diagrams: diagramResult.nodesAdded,
|
|
4893
5546
|
linkerFacts: linkResult.factsCreated,
|
|
4894
5547
|
businessKnowledge: bkResult.nodesAdded,
|
|
5548
|
+
decisions: decisionResult.nodesAdded,
|
|
4895
5549
|
images: imageCount
|
|
4896
5550
|
};
|
|
4897
5551
|
}
|
|
@@ -4918,14 +5572,14 @@ var KnowledgePipelineRunner = class {
|
|
|
4918
5572
|
}
|
|
4919
5573
|
// ── Phase 3: DETECT ───────────────────────────────────────────────────────
|
|
4920
5574
|
async detect(options) {
|
|
4921
|
-
const knowledgeDir =
|
|
4922
|
-
const aggregator = new KnowledgeStagingAggregator(options.projectDir);
|
|
4923
|
-
const gapReport = await aggregator.generateGapReport(knowledgeDir);
|
|
5575
|
+
const knowledgeDir = path12.join(options.projectDir, "docs", "knowledge");
|
|
5576
|
+
const aggregator = new KnowledgeStagingAggregator(options.projectDir, this.inferenceOptions);
|
|
5577
|
+
const gapReport = await aggregator.generateGapReport(knowledgeDir, this.store);
|
|
4924
5578
|
await aggregator.writeGapReport(gapReport);
|
|
4925
5579
|
return gapReport;
|
|
4926
5580
|
}
|
|
4927
5581
|
// ── Phase 4: REMEDIATE ────────────────────────────────────────────────────
|
|
4928
|
-
remediate(driftResult, remediations, options) {
|
|
5582
|
+
async remediate(driftResult, remediations, options, gapReport) {
|
|
4929
5583
|
for (const finding of driftResult.findings) {
|
|
4930
5584
|
switch (finding.classification) {
|
|
4931
5585
|
case "stale":
|
|
@@ -4943,6 +5597,22 @@ var KnowledgePipelineRunner = class {
|
|
|
4943
5597
|
break;
|
|
4944
5598
|
}
|
|
4945
5599
|
}
|
|
5600
|
+
if (!options.ci) {
|
|
5601
|
+
const allGapEntries = gapReport.domains.flatMap((d) => d.gapEntries);
|
|
5602
|
+
const materializable = allGapEntries.filter((e) => e.hasContent);
|
|
5603
|
+
if (materializable.length > 0) {
|
|
5604
|
+
const materializer = new KnowledgeDocMaterializer(this.store, this.inferenceOptions);
|
|
5605
|
+
const matResult = await materializer.materialize(materializable, {
|
|
5606
|
+
projectDir: options.projectDir,
|
|
5607
|
+
dryRun: false
|
|
5608
|
+
});
|
|
5609
|
+
for (const doc of matResult.created) {
|
|
5610
|
+
remediations.push(`created doc: ${doc.filePath}`);
|
|
5611
|
+
}
|
|
5612
|
+
return matResult;
|
|
5613
|
+
}
|
|
5614
|
+
}
|
|
5615
|
+
return void 0;
|
|
4946
5616
|
}
|
|
4947
5617
|
async stageNewFindings(driftResult, options) {
|
|
4948
5618
|
const newFindings = driftResult.findings.filter((f) => f.classification === "new");
|
|
@@ -4957,7 +5627,7 @@ var KnowledgePipelineRunner = class {
|
|
|
4957
5627
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4958
5628
|
}));
|
|
4959
5629
|
if (stagedEntries.length > 0) {
|
|
4960
|
-
const aggregator = new KnowledgeStagingAggregator(options.projectDir);
|
|
5630
|
+
const aggregator = new KnowledgeStagingAggregator(options.projectDir, this.inferenceOptions);
|
|
4961
5631
|
await aggregator.aggregate(stagedEntries, [], []);
|
|
4962
5632
|
}
|
|
4963
5633
|
}
|
|
@@ -4977,7 +5647,7 @@ var KnowledgePipelineRunner = class {
|
|
|
4977
5647
|
};
|
|
4978
5648
|
|
|
4979
5649
|
// src/ingest/connectors/ConnectorUtils.ts
|
|
4980
|
-
var
|
|
5650
|
+
var CODE_NODE_TYPES5 = ["file", "function", "class", "method", "interface", "variable"];
|
|
4981
5651
|
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
4982
5652
|
function withRetry(client, options) {
|
|
4983
5653
|
const maxRetries = options?.maxRetries ?? 3;
|
|
@@ -5042,7 +5712,7 @@ function sanitizeExternalText(text, maxLength = 2e3) {
|
|
|
5042
5712
|
}
|
|
5043
5713
|
function linkToCode(store, content, sourceNodeId, edgeType, options) {
|
|
5044
5714
|
let edgesCreated = 0;
|
|
5045
|
-
for (const type of
|
|
5715
|
+
for (const type of CODE_NODE_TYPES5) {
|
|
5046
5716
|
const nodes = store.findNodes({ type });
|
|
5047
5717
|
for (const node of nodes) {
|
|
5048
5718
|
if (node.name.length < 3) continue;
|
|
@@ -5062,12 +5732,12 @@ function linkToCode(store, content, sourceNodeId, edgeType, options) {
|
|
|
5062
5732
|
}
|
|
5063
5733
|
|
|
5064
5734
|
// src/ingest/connectors/SyncManager.ts
|
|
5065
|
-
var
|
|
5066
|
-
var
|
|
5735
|
+
var fs12 = __toESM(require("fs/promises"));
|
|
5736
|
+
var path13 = __toESM(require("path"));
|
|
5067
5737
|
var SyncManager = class {
|
|
5068
5738
|
constructor(store, graphDir) {
|
|
5069
5739
|
this.store = store;
|
|
5070
|
-
this.metadataPath =
|
|
5740
|
+
this.metadataPath = path13.join(graphDir, "sync-metadata.json");
|
|
5071
5741
|
}
|
|
5072
5742
|
store;
|
|
5073
5743
|
registrations = /* @__PURE__ */ new Map();
|
|
@@ -5132,15 +5802,15 @@ var SyncManager = class {
|
|
|
5132
5802
|
}
|
|
5133
5803
|
async loadMetadata() {
|
|
5134
5804
|
try {
|
|
5135
|
-
const raw = await
|
|
5805
|
+
const raw = await fs12.readFile(this.metadataPath, "utf-8");
|
|
5136
5806
|
return JSON.parse(raw);
|
|
5137
5807
|
} catch {
|
|
5138
5808
|
return { connectors: {} };
|
|
5139
5809
|
}
|
|
5140
5810
|
}
|
|
5141
5811
|
async saveMetadata(metadata) {
|
|
5142
|
-
await
|
|
5143
|
-
await
|
|
5812
|
+
await fs12.mkdir(path13.dirname(this.metadataPath), { recursive: true });
|
|
5813
|
+
await fs12.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
5144
5814
|
}
|
|
5145
5815
|
};
|
|
5146
5816
|
|
|
@@ -5245,6 +5915,14 @@ var JiraConnector = class {
|
|
|
5245
5915
|
start
|
|
5246
5916
|
);
|
|
5247
5917
|
}
|
|
5918
|
+
try {
|
|
5919
|
+
const parsed = new URL(baseUrl);
|
|
5920
|
+
if (parsed.protocol !== "https:" && parsed.hostname !== "localhost") {
|
|
5921
|
+
return buildIngestResult(0, 0, [`${baseUrlEnv} must use HTTPS`], start);
|
|
5922
|
+
}
|
|
5923
|
+
} catch {
|
|
5924
|
+
return buildIngestResult(0, 0, [`${baseUrlEnv} is not a valid URL`], start);
|
|
5925
|
+
}
|
|
5248
5926
|
const jql = buildJql(config);
|
|
5249
5927
|
const headers = { Authorization: `Basic ${apiKey}`, "Content-Type": "application/json" };
|
|
5250
5928
|
try {
|
|
@@ -5534,6 +6212,14 @@ var ConfluenceConnector = class {
|
|
|
5534
6212
|
}
|
|
5535
6213
|
const baseUrlEnv = config.baseUrlEnv ?? "CONFLUENCE_BASE_URL";
|
|
5536
6214
|
const baseUrl = process.env[baseUrlEnv] ?? "";
|
|
6215
|
+
try {
|
|
6216
|
+
const parsed = new URL(baseUrl);
|
|
6217
|
+
if (parsed.protocol !== "https:" && parsed.hostname !== "localhost") {
|
|
6218
|
+
return missingApiKeyResult(`${baseUrlEnv} must use HTTPS`, start);
|
|
6219
|
+
}
|
|
6220
|
+
} catch {
|
|
6221
|
+
return missingApiKeyResult(`${baseUrlEnv} is not a valid URL`, start);
|
|
6222
|
+
}
|
|
5537
6223
|
const spaceKey = config.spaceKey ?? "";
|
|
5538
6224
|
const counts = await this.fetchAllPagesHandled(
|
|
5539
6225
|
store,
|
|
@@ -5627,7 +6313,7 @@ var ConfluenceConnector = class {
|
|
|
5627
6313
|
};
|
|
5628
6314
|
|
|
5629
6315
|
// src/ingest/connectors/CIConnector.ts
|
|
5630
|
-
function
|
|
6316
|
+
function emptyResult5(errors, start) {
|
|
5631
6317
|
return {
|
|
5632
6318
|
nodesAdded: 0,
|
|
5633
6319
|
nodesUpdated: 0,
|
|
@@ -5695,7 +6381,7 @@ var CIConnector = class {
|
|
|
5695
6381
|
const apiKeyEnv = config.apiKeyEnv ?? "GITHUB_TOKEN";
|
|
5696
6382
|
const apiKey = process.env[apiKeyEnv];
|
|
5697
6383
|
if (!apiKey) {
|
|
5698
|
-
return
|
|
6384
|
+
return emptyResult5(
|
|
5699
6385
|
[`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
5700
6386
|
start
|
|
5701
6387
|
);
|
|
@@ -5759,6 +6445,14 @@ function hasConstraintKeyword(text) {
|
|
|
5759
6445
|
const lower = text.toLowerCase();
|
|
5760
6446
|
return CONSTRAINT_KEYWORDS.some((kw) => lower.includes(kw));
|
|
5761
6447
|
}
|
|
6448
|
+
function buildNodeMetadata(condensed, base) {
|
|
6449
|
+
const metadata = { ...base };
|
|
6450
|
+
if (condensed.method !== "passthrough") {
|
|
6451
|
+
metadata.condensed = condensed.method;
|
|
6452
|
+
metadata.originalLength = condensed.originalLength;
|
|
6453
|
+
}
|
|
6454
|
+
return metadata;
|
|
6455
|
+
}
|
|
5762
6456
|
function buildIngestResult2(nodesAdded, edgesAdded, errors, start) {
|
|
5763
6457
|
return {
|
|
5764
6458
|
nodesAdded,
|
|
@@ -5769,6 +6463,24 @@ function buildIngestResult2(nodesAdded, edgesAdded, errors, start) {
|
|
|
5769
6463
|
durationMs: Date.now() - start
|
|
5770
6464
|
};
|
|
5771
6465
|
}
|
|
6466
|
+
function validateFigmaConfig(config) {
|
|
6467
|
+
const apiKeyEnv = config.apiKeyEnv ?? "FIGMA_API_KEY";
|
|
6468
|
+
const apiKey = process.env[apiKeyEnv];
|
|
6469
|
+
if (!apiKey) return { error: `Missing API key: environment variable "${apiKeyEnv}" is not set` };
|
|
6470
|
+
const baseUrlEnv = config.baseUrlEnv ?? "FIGMA_BASE_URL";
|
|
6471
|
+
const baseUrl = process.env[baseUrlEnv] ?? "https://api.figma.com";
|
|
6472
|
+
try {
|
|
6473
|
+
const parsed = new URL(baseUrl);
|
|
6474
|
+
if (parsed.protocol !== "https:" || !parsed.hostname.endsWith("api.figma.com")) {
|
|
6475
|
+
return { error: `Invalid ${baseUrlEnv}: must be an HTTPS URL on api.figma.com` };
|
|
6476
|
+
}
|
|
6477
|
+
} catch {
|
|
6478
|
+
return { error: `Invalid ${baseUrlEnv}: not a valid URL` };
|
|
6479
|
+
}
|
|
6480
|
+
const fileIds = config.fileIds;
|
|
6481
|
+
if (!fileIds || fileIds.length === 0) return { error: "No fileIds provided in connector config" };
|
|
6482
|
+
return { apiKey, baseUrl, fileIds };
|
|
6483
|
+
}
|
|
5772
6484
|
var FigmaConnector = class {
|
|
5773
6485
|
name = "figma";
|
|
5774
6486
|
source = "figma";
|
|
@@ -5778,35 +6490,9 @@ var FigmaConnector = class {
|
|
|
5778
6490
|
}
|
|
5779
6491
|
async ingest(store, config) {
|
|
5780
6492
|
const start = Date.now();
|
|
5781
|
-
const
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
return buildIngestResult2(
|
|
5785
|
-
0,
|
|
5786
|
-
0,
|
|
5787
|
-
[`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
5788
|
-
start
|
|
5789
|
-
);
|
|
5790
|
-
}
|
|
5791
|
-
const baseUrlEnv = config.baseUrlEnv ?? "FIGMA_BASE_URL";
|
|
5792
|
-
const baseUrl = process.env[baseUrlEnv] ?? "https://api.figma.com";
|
|
5793
|
-
try {
|
|
5794
|
-
const parsed = new URL(baseUrl);
|
|
5795
|
-
if (parsed.protocol !== "https:" || !parsed.hostname.endsWith("api.figma.com")) {
|
|
5796
|
-
return buildIngestResult2(
|
|
5797
|
-
0,
|
|
5798
|
-
0,
|
|
5799
|
-
[`Invalid ${baseUrlEnv}: must be an HTTPS URL on api.figma.com`],
|
|
5800
|
-
start
|
|
5801
|
-
);
|
|
5802
|
-
}
|
|
5803
|
-
} catch {
|
|
5804
|
-
return buildIngestResult2(0, 0, [`Invalid ${baseUrlEnv}: not a valid URL`], start);
|
|
5805
|
-
}
|
|
5806
|
-
const fileIds = config.fileIds;
|
|
5807
|
-
if (!fileIds || fileIds.length === 0) {
|
|
5808
|
-
return buildIngestResult2(0, 0, ["No fileIds provided in connector config"], start);
|
|
5809
|
-
}
|
|
6493
|
+
const validated = validateFigmaConfig(config);
|
|
6494
|
+
if ("error" in validated) return buildIngestResult2(0, 0, [validated.error], start);
|
|
6495
|
+
const { apiKey, baseUrl, fileIds } = validated;
|
|
5810
6496
|
const headers = { "X-FIGMA-TOKEN": apiKey };
|
|
5811
6497
|
const maxLen = config.maxContentLength ?? 4e3;
|
|
5812
6498
|
let nodesAdded = 0;
|
|
@@ -5826,6 +6512,17 @@ var FigmaConnector = class {
|
|
|
5826
6512
|
return buildIngestResult2(nodesAdded, edgesAdded, errors, start);
|
|
5827
6513
|
}
|
|
5828
6514
|
async processFile(store, baseUrl, fileId, headers, maxLen) {
|
|
6515
|
+
let nodesAdded = 0;
|
|
6516
|
+
let edgesAdded = 0;
|
|
6517
|
+
const styleCounts = await this.ingestStyles(store, baseUrl, fileId, headers, maxLen);
|
|
6518
|
+
nodesAdded += styleCounts.nodesAdded;
|
|
6519
|
+
edgesAdded += styleCounts.edgesAdded;
|
|
6520
|
+
const componentCounts = await this.ingestComponents(store, baseUrl, fileId, headers, maxLen);
|
|
6521
|
+
nodesAdded += componentCounts.nodesAdded;
|
|
6522
|
+
edgesAdded += componentCounts.edgesAdded;
|
|
6523
|
+
return { nodesAdded, edgesAdded };
|
|
6524
|
+
}
|
|
6525
|
+
async ingestStyles(store, baseUrl, fileId, headers, maxLen) {
|
|
5829
6526
|
let nodesAdded = 0;
|
|
5830
6527
|
let edgesAdded = 0;
|
|
5831
6528
|
const stylesUrl = `${baseUrl}/v1/files/${fileId}/styles`;
|
|
@@ -5833,90 +6530,112 @@ var FigmaConnector = class {
|
|
|
5833
6530
|
if (!stylesResponse.ok) throw new Error(`Styles request failed for file ${fileId}`);
|
|
5834
6531
|
const stylesData = await stylesResponse.json();
|
|
5835
6532
|
for (const style of stylesData.meta.styles) {
|
|
5836
|
-
const
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
});
|
|
5840
|
-
const metadata = {
|
|
5841
|
-
source: "figma",
|
|
5842
|
-
key: style.key,
|
|
5843
|
-
styleType: style.style_type,
|
|
5844
|
-
fileId
|
|
5845
|
-
};
|
|
5846
|
-
if (condensed.method !== "passthrough") {
|
|
5847
|
-
metadata.condensed = condensed.method;
|
|
5848
|
-
metadata.originalLength = condensed.originalLength;
|
|
5849
|
-
}
|
|
5850
|
-
store.addNode({
|
|
5851
|
-
id: nodeId,
|
|
5852
|
-
type: "design_token",
|
|
5853
|
-
name: sanitizeExternalText(style.name, 500),
|
|
5854
|
-
content: condensed.content,
|
|
5855
|
-
metadata
|
|
5856
|
-
});
|
|
5857
|
-
nodesAdded++;
|
|
5858
|
-
const searchText = sanitizeExternalText([style.name, style.description].join(" "));
|
|
5859
|
-
edgesAdded += linkToCode(store, searchText, nodeId, "references");
|
|
6533
|
+
const counts = await this.processStyle(store, style, fileId, maxLen);
|
|
6534
|
+
nodesAdded += counts.nodesAdded;
|
|
6535
|
+
edgesAdded += counts.edgesAdded;
|
|
5860
6536
|
}
|
|
6537
|
+
return { nodesAdded, edgesAdded };
|
|
6538
|
+
}
|
|
6539
|
+
async processStyle(store, style, fileId, maxLen) {
|
|
6540
|
+
const nodeId = `figma:token:${style.key}`;
|
|
6541
|
+
const condensed = await condenseContent(sanitizeExternalText(style.description || "", 2e3), {
|
|
6542
|
+
maxLength: maxLen
|
|
6543
|
+
});
|
|
6544
|
+
const metadata = buildNodeMetadata(condensed, {
|
|
6545
|
+
source: "figma",
|
|
6546
|
+
key: style.key,
|
|
6547
|
+
styleType: style.style_type,
|
|
6548
|
+
fileId
|
|
6549
|
+
});
|
|
6550
|
+
store.addNode({
|
|
6551
|
+
id: nodeId,
|
|
6552
|
+
type: "design_token",
|
|
6553
|
+
name: sanitizeExternalText(style.name, 500),
|
|
6554
|
+
content: condensed.content,
|
|
6555
|
+
metadata
|
|
6556
|
+
});
|
|
6557
|
+
const searchText = sanitizeExternalText([style.name, style.description].join(" "));
|
|
6558
|
+
const edgesAdded = linkToCode(store, searchText, nodeId, "references");
|
|
6559
|
+
return { nodesAdded: 1, edgesAdded };
|
|
6560
|
+
}
|
|
6561
|
+
async ingestComponents(store, baseUrl, fileId, headers, maxLen) {
|
|
6562
|
+
let nodesAdded = 0;
|
|
6563
|
+
let edgesAdded = 0;
|
|
5861
6564
|
const componentsUrl = `${baseUrl}/v1/files/${fileId}/components`;
|
|
5862
6565
|
const componentsResponse = await this.httpClient(componentsUrl, { headers });
|
|
5863
6566
|
if (!componentsResponse.ok) throw new Error(`Components request failed for file ${fileId}`);
|
|
5864
6567
|
const componentsData = await componentsResponse.json();
|
|
5865
6568
|
for (const component of componentsData.meta.components) {
|
|
5866
|
-
const
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
|
|
5870
|
-
maxLength: maxLen
|
|
5871
|
-
});
|
|
5872
|
-
const metadata = {
|
|
5873
|
-
source: "figma",
|
|
5874
|
-
key: component.key,
|
|
5875
|
-
fileId
|
|
5876
|
-
};
|
|
5877
|
-
if (condensed.method !== "passthrough") {
|
|
5878
|
-
metadata.condensed = condensed.method;
|
|
5879
|
-
metadata.originalLength = condensed.originalLength;
|
|
5880
|
-
}
|
|
5881
|
-
store.addNode({
|
|
5882
|
-
id: intentId,
|
|
5883
|
-
type: "aesthetic_intent",
|
|
5884
|
-
name: sanitizeExternalText(component.name, 500),
|
|
5885
|
-
content: condensed.content,
|
|
5886
|
-
metadata
|
|
5887
|
-
});
|
|
5888
|
-
nodesAdded++;
|
|
5889
|
-
const searchText = sanitizeExternalText([component.name, description].join(" "));
|
|
5890
|
-
edgesAdded += linkToCode(store, searchText, intentId, "references");
|
|
5891
|
-
}
|
|
5892
|
-
if (description && hasConstraintKeyword(description)) {
|
|
5893
|
-
const constraintId = `figma:constraint:${component.key}`;
|
|
5894
|
-
const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
|
|
5895
|
-
maxLength: maxLen
|
|
5896
|
-
});
|
|
5897
|
-
const metadata = {
|
|
5898
|
-
source: "figma",
|
|
5899
|
-
key: component.key,
|
|
5900
|
-
fileId
|
|
5901
|
-
};
|
|
5902
|
-
if (condensed.method !== "passthrough") {
|
|
5903
|
-
metadata.condensed = condensed.method;
|
|
5904
|
-
metadata.originalLength = condensed.originalLength;
|
|
5905
|
-
}
|
|
5906
|
-
store.addNode({
|
|
5907
|
-
id: constraintId,
|
|
5908
|
-
type: "design_constraint",
|
|
5909
|
-
name: sanitizeExternalText(component.name, 500),
|
|
5910
|
-
content: condensed.content,
|
|
5911
|
-
metadata
|
|
5912
|
-
});
|
|
5913
|
-
nodesAdded++;
|
|
5914
|
-
const searchText = sanitizeExternalText([component.name, description].join(" "));
|
|
5915
|
-
edgesAdded += linkToCode(store, searchText, constraintId, "references");
|
|
5916
|
-
}
|
|
6569
|
+
const counts = await this.processComponent(store, component, fileId, maxLen);
|
|
6570
|
+
nodesAdded += counts.nodesAdded;
|
|
6571
|
+
edgesAdded += counts.edgesAdded;
|
|
5917
6572
|
}
|
|
5918
6573
|
return { nodesAdded, edgesAdded };
|
|
5919
6574
|
}
|
|
6575
|
+
async processComponent(store, component, fileId, maxLen) {
|
|
6576
|
+
let nodesAdded = 0;
|
|
6577
|
+
let edgesAdded = 0;
|
|
6578
|
+
const description = component.description || "";
|
|
6579
|
+
if (description) {
|
|
6580
|
+
const counts = await this.addComponentIntent(store, component, description, fileId, maxLen);
|
|
6581
|
+
nodesAdded += counts.nodesAdded;
|
|
6582
|
+
edgesAdded += counts.edgesAdded;
|
|
6583
|
+
}
|
|
6584
|
+
if (description && hasConstraintKeyword(description)) {
|
|
6585
|
+
const counts = await this.addComponentConstraint(
|
|
6586
|
+
store,
|
|
6587
|
+
component,
|
|
6588
|
+
description,
|
|
6589
|
+
fileId,
|
|
6590
|
+
maxLen
|
|
6591
|
+
);
|
|
6592
|
+
nodesAdded += counts.nodesAdded;
|
|
6593
|
+
edgesAdded += counts.edgesAdded;
|
|
6594
|
+
}
|
|
6595
|
+
return { nodesAdded, edgesAdded };
|
|
6596
|
+
}
|
|
6597
|
+
async addComponentIntent(store, component, description, fileId, maxLen) {
|
|
6598
|
+
const intentId = `figma:intent:${component.key}`;
|
|
6599
|
+
const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
|
|
6600
|
+
maxLength: maxLen
|
|
6601
|
+
});
|
|
6602
|
+
const metadata = buildNodeMetadata(condensed, {
|
|
6603
|
+
source: "figma",
|
|
6604
|
+
key: component.key,
|
|
6605
|
+
fileId
|
|
6606
|
+
});
|
|
6607
|
+
store.addNode({
|
|
6608
|
+
id: intentId,
|
|
6609
|
+
type: "aesthetic_intent",
|
|
6610
|
+
name: sanitizeExternalText(component.name, 500),
|
|
6611
|
+
content: condensed.content,
|
|
6612
|
+
metadata
|
|
6613
|
+
});
|
|
6614
|
+
const searchText = sanitizeExternalText([component.name, description].join(" "));
|
|
6615
|
+
const edgesAdded = linkToCode(store, searchText, intentId, "references");
|
|
6616
|
+
return { nodesAdded: 1, edgesAdded };
|
|
6617
|
+
}
|
|
6618
|
+
async addComponentConstraint(store, component, description, fileId, maxLen) {
|
|
6619
|
+
const constraintId = `figma:constraint:${component.key}`;
|
|
6620
|
+
const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
|
|
6621
|
+
maxLength: maxLen
|
|
6622
|
+
});
|
|
6623
|
+
const metadata = buildNodeMetadata(condensed, {
|
|
6624
|
+
source: "figma",
|
|
6625
|
+
key: component.key,
|
|
6626
|
+
fileId
|
|
6627
|
+
});
|
|
6628
|
+
store.addNode({
|
|
6629
|
+
id: constraintId,
|
|
6630
|
+
type: "design_constraint",
|
|
6631
|
+
name: sanitizeExternalText(component.name, 500),
|
|
6632
|
+
content: condensed.content,
|
|
6633
|
+
metadata
|
|
6634
|
+
});
|
|
6635
|
+
const searchText = sanitizeExternalText([component.name, description].join(" "));
|
|
6636
|
+
const edgesAdded = linkToCode(store, searchText, constraintId, "references");
|
|
6637
|
+
return { nodesAdded: 1, edgesAdded };
|
|
6638
|
+
}
|
|
5920
6639
|
};
|
|
5921
6640
|
|
|
5922
6641
|
// src/ingest/connectors/MiroConnector.ts
|
|
@@ -5935,6 +6654,46 @@ function buildIngestResult3(nodesAdded, edgesAdded, errors, start) {
|
|
|
5935
6654
|
durationMs: Date.now() - start
|
|
5936
6655
|
};
|
|
5937
6656
|
}
|
|
6657
|
+
function validateBaseUrl(baseUrl, baseUrlEnv) {
|
|
6658
|
+
try {
|
|
6659
|
+
const parsed = new URL(baseUrl);
|
|
6660
|
+
if (parsed.protocol !== "https:" || parsed.hostname !== "api.miro.com") {
|
|
6661
|
+
return `Invalid ${baseUrlEnv}: must be an HTTPS URL on api.miro.com`;
|
|
6662
|
+
}
|
|
6663
|
+
} catch {
|
|
6664
|
+
return `Invalid ${baseUrlEnv}: not a valid URL`;
|
|
6665
|
+
}
|
|
6666
|
+
return null;
|
|
6667
|
+
}
|
|
6668
|
+
function resolveConfig(config, start) {
|
|
6669
|
+
const apiKeyEnv = config.apiKeyEnv ?? "MIRO_API_KEY";
|
|
6670
|
+
const apiKey = process.env[apiKeyEnv];
|
|
6671
|
+
if (!apiKey) {
|
|
6672
|
+
return {
|
|
6673
|
+
ok: false,
|
|
6674
|
+
result: buildIngestResult3(
|
|
6675
|
+
0,
|
|
6676
|
+
0,
|
|
6677
|
+
[`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
6678
|
+
start
|
|
6679
|
+
)
|
|
6680
|
+
};
|
|
6681
|
+
}
|
|
6682
|
+
const baseUrlEnv = config.baseUrlEnv ?? "MIRO_BASE_URL";
|
|
6683
|
+
const baseUrl = process.env[baseUrlEnv] ?? "https://api.miro.com";
|
|
6684
|
+
const urlError = validateBaseUrl(baseUrl, baseUrlEnv);
|
|
6685
|
+
if (urlError) {
|
|
6686
|
+
return { ok: false, result: buildIngestResult3(0, 0, [urlError], start) };
|
|
6687
|
+
}
|
|
6688
|
+
const boardIds = config.boardIds;
|
|
6689
|
+
if (!boardIds || boardIds.length === 0) {
|
|
6690
|
+
return {
|
|
6691
|
+
ok: false,
|
|
6692
|
+
result: buildIngestResult3(0, 0, ["No boardIds provided in config"], start)
|
|
6693
|
+
};
|
|
6694
|
+
}
|
|
6695
|
+
return { ok: true, apiKey, baseUrl, boardIds, headers: { Authorization: `Bearer ${apiKey}` } };
|
|
6696
|
+
}
|
|
5938
6697
|
var MiroConnector = class {
|
|
5939
6698
|
name = "miro";
|
|
5940
6699
|
source = "miro";
|
|
@@ -5944,36 +6703,9 @@ var MiroConnector = class {
|
|
|
5944
6703
|
}
|
|
5945
6704
|
async ingest(store, config) {
|
|
5946
6705
|
const start = Date.now();
|
|
5947
|
-
const
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
return buildIngestResult3(
|
|
5951
|
-
0,
|
|
5952
|
-
0,
|
|
5953
|
-
[`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
5954
|
-
start
|
|
5955
|
-
);
|
|
5956
|
-
}
|
|
5957
|
-
const baseUrlEnv = config.baseUrlEnv ?? "MIRO_BASE_URL";
|
|
5958
|
-
const baseUrl = process.env[baseUrlEnv] ?? "https://api.miro.com";
|
|
5959
|
-
try {
|
|
5960
|
-
const parsed = new URL(baseUrl);
|
|
5961
|
-
if (parsed.protocol !== "https:" || parsed.hostname !== "api.miro.com") {
|
|
5962
|
-
return buildIngestResult3(
|
|
5963
|
-
0,
|
|
5964
|
-
0,
|
|
5965
|
-
[`Invalid ${baseUrlEnv}: must be an HTTPS URL on api.miro.com`],
|
|
5966
|
-
start
|
|
5967
|
-
);
|
|
5968
|
-
}
|
|
5969
|
-
} catch {
|
|
5970
|
-
return buildIngestResult3(0, 0, [`Invalid ${baseUrlEnv}: not a valid URL`], start);
|
|
5971
|
-
}
|
|
5972
|
-
const boardIds = config.boardIds;
|
|
5973
|
-
if (!boardIds || boardIds.length === 0) {
|
|
5974
|
-
return buildIngestResult3(0, 0, ["No boardIds provided in config"], start);
|
|
5975
|
-
}
|
|
5976
|
-
const headers = { Authorization: `Bearer ${apiKey}` };
|
|
6706
|
+
const resolved = resolveConfig(config, start);
|
|
6707
|
+
if (!resolved.ok) return resolved.result;
|
|
6708
|
+
const { baseUrl, boardIds, headers } = resolved;
|
|
5977
6709
|
let totalNodesAdded = 0;
|
|
5978
6710
|
let totalEdgesAdded = 0;
|
|
5979
6711
|
const errors = [];
|
|
@@ -6177,7 +6909,7 @@ var FusionLayer = class {
|
|
|
6177
6909
|
};
|
|
6178
6910
|
|
|
6179
6911
|
// src/entropy/GraphEntropyAdapter.ts
|
|
6180
|
-
var
|
|
6912
|
+
var CODE_NODE_TYPES6 = ["file", "function", "class", "method", "interface", "variable"];
|
|
6181
6913
|
var GraphEntropyAdapter = class {
|
|
6182
6914
|
constructor(store) {
|
|
6183
6915
|
this.store = store;
|
|
@@ -6257,7 +6989,7 @@ var GraphEntropyAdapter = class {
|
|
|
6257
6989
|
}
|
|
6258
6990
|
findEntryPoints() {
|
|
6259
6991
|
const entryPoints = [];
|
|
6260
|
-
for (const nodeType of
|
|
6992
|
+
for (const nodeType of CODE_NODE_TYPES6) {
|
|
6261
6993
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
6262
6994
|
for (const node of nodes) {
|
|
6263
6995
|
const isIndexFile = nodeType === "file" && node.name === "index.ts";
|
|
@@ -6293,7 +7025,7 @@ var GraphEntropyAdapter = class {
|
|
|
6293
7025
|
}
|
|
6294
7026
|
collectUnreachableNodes(visited) {
|
|
6295
7027
|
const unreachableNodes = [];
|
|
6296
|
-
for (const nodeType of
|
|
7028
|
+
for (const nodeType of CODE_NODE_TYPES6) {
|
|
6297
7029
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
6298
7030
|
for (const node of nodes) {
|
|
6299
7031
|
if (!visited.has(node.id)) {
|
|
@@ -7139,9 +7871,9 @@ var EntityExtractor = class {
|
|
|
7139
7871
|
extractPaths(trimmed, add) {
|
|
7140
7872
|
const consumed = /* @__PURE__ */ new Set();
|
|
7141
7873
|
for (const match of trimmed.matchAll(FILE_PATH_RE)) {
|
|
7142
|
-
const
|
|
7143
|
-
add(
|
|
7144
|
-
consumed.add(
|
|
7874
|
+
const path15 = match[0];
|
|
7875
|
+
add(path15);
|
|
7876
|
+
consumed.add(path15);
|
|
7145
7877
|
}
|
|
7146
7878
|
return consumed;
|
|
7147
7879
|
}
|
|
@@ -7213,8 +7945,8 @@ var EntityResolver = class {
|
|
|
7213
7945
|
if (isPathLike && node.path.includes(raw)) {
|
|
7214
7946
|
return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
|
|
7215
7947
|
}
|
|
7216
|
-
const
|
|
7217
|
-
if (
|
|
7948
|
+
const basename10 = node.path.split("/").pop() ?? "";
|
|
7949
|
+
if (basename10.includes(raw)) {
|
|
7218
7950
|
return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
|
|
7219
7951
|
}
|
|
7220
7952
|
if (raw.length >= 4 && node.path.includes(raw)) {
|
|
@@ -7293,13 +8025,13 @@ var ResponseFormatter = class {
|
|
|
7293
8025
|
const context = Array.isArray(d?.context) ? d.context : [];
|
|
7294
8026
|
const firstEntity = entities[0];
|
|
7295
8027
|
const nodeType = firstEntity?.node.type ?? "node";
|
|
7296
|
-
const
|
|
8028
|
+
const path15 = firstEntity?.node.path ?? "unknown";
|
|
7297
8029
|
let neighborCount = 0;
|
|
7298
8030
|
const firstContext = context[0];
|
|
7299
8031
|
if (firstContext && Array.isArray(firstContext.nodes)) {
|
|
7300
8032
|
neighborCount = firstContext.nodes.length;
|
|
7301
8033
|
}
|
|
7302
|
-
return `**${entityName}** is a ${nodeType} at \`${
|
|
8034
|
+
return `**${entityName}** is a ${nodeType} at \`${path15}\`. Connected to ${neighborCount} nodes.`;
|
|
7303
8035
|
}
|
|
7304
8036
|
formatAnomaly(data) {
|
|
7305
8037
|
const d = data;
|
|
@@ -7682,7 +8414,7 @@ var PHASE_NODE_TYPES = {
|
|
|
7682
8414
|
debug: ["failure", "learning", "function", "method"],
|
|
7683
8415
|
plan: ["adr", "document", "module", "layer"]
|
|
7684
8416
|
};
|
|
7685
|
-
var
|
|
8417
|
+
var CODE_NODE_TYPES7 = /* @__PURE__ */ new Set([
|
|
7686
8418
|
"file",
|
|
7687
8419
|
"function",
|
|
7688
8420
|
"class",
|
|
@@ -7901,7 +8633,7 @@ var Assembler = class {
|
|
|
7901
8633
|
*/
|
|
7902
8634
|
checkCoverage() {
|
|
7903
8635
|
const codeNodes = [];
|
|
7904
|
-
for (const type of
|
|
8636
|
+
for (const type of CODE_NODE_TYPES7) {
|
|
7905
8637
|
codeNodes.push(...this.store.findNodes({ type }));
|
|
7906
8638
|
}
|
|
7907
8639
|
const documented = [];
|
|
@@ -8081,14 +8813,14 @@ var GraphConstraintAdapter = class {
|
|
|
8081
8813
|
};
|
|
8082
8814
|
|
|
8083
8815
|
// src/ingest/DesignIngestor.ts
|
|
8084
|
-
var
|
|
8085
|
-
var
|
|
8816
|
+
var fs13 = __toESM(require("fs/promises"));
|
|
8817
|
+
var path14 = __toESM(require("path"));
|
|
8086
8818
|
function isDTCGToken(obj) {
|
|
8087
8819
|
return typeof obj === "object" && obj !== null && "$value" in obj && "$type" in obj;
|
|
8088
8820
|
}
|
|
8089
8821
|
async function readFileOrNull(filePath) {
|
|
8090
8822
|
try {
|
|
8091
|
-
return await
|
|
8823
|
+
return await fs13.readFile(filePath, "utf-8");
|
|
8092
8824
|
} catch {
|
|
8093
8825
|
return null;
|
|
8094
8826
|
}
|
|
@@ -8234,8 +8966,8 @@ var DesignIngestor = class {
|
|
|
8234
8966
|
async ingestAll(designDir) {
|
|
8235
8967
|
const start = Date.now();
|
|
8236
8968
|
const [tokensResult, intentResult] = await Promise.all([
|
|
8237
|
-
this.ingestTokens(
|
|
8238
|
-
this.ingestDesignIntent(
|
|
8969
|
+
this.ingestTokens(path14.join(designDir, "tokens.json")),
|
|
8970
|
+
this.ingestDesignIntent(path14.join(designDir, "DESIGN.md"))
|
|
8239
8971
|
]);
|
|
8240
8972
|
const merged = mergeResults(tokensResult, intentResult);
|
|
8241
8973
|
return { ...merged, durationMs: Date.now() - start };
|
|
@@ -8471,10 +9203,10 @@ var TaskIndependenceAnalyzer = class {
|
|
|
8471
9203
|
includeTypes: ["file"]
|
|
8472
9204
|
});
|
|
8473
9205
|
for (const n of queryResult.nodes) {
|
|
8474
|
-
const
|
|
8475
|
-
if (!fileSet.has(
|
|
8476
|
-
if (!result.has(
|
|
8477
|
-
result.set(
|
|
9206
|
+
const path15 = n.path ?? n.id.replace(/^file:/, "");
|
|
9207
|
+
if (!fileSet.has(path15)) {
|
|
9208
|
+
if (!result.has(path15)) {
|
|
9209
|
+
result.set(path15, file);
|
|
8478
9210
|
}
|
|
8479
9211
|
}
|
|
8480
9212
|
}
|
|
@@ -8851,7 +9583,7 @@ var ConflictPredictor = class {
|
|
|
8851
9583
|
};
|
|
8852
9584
|
|
|
8853
9585
|
// src/index.ts
|
|
8854
|
-
var VERSION = "0.
|
|
9586
|
+
var VERSION = "0.6.0";
|
|
8855
9587
|
// Annotate the CommonJS export names for ESM import in node:
|
|
8856
9588
|
0 && (module.exports = {
|
|
8857
9589
|
ALL_EXTRACTORS,
|
|
@@ -8869,6 +9601,9 @@ var VERSION = "0.4.3";
|
|
|
8869
9601
|
ContradictionDetector,
|
|
8870
9602
|
CoverageScorer,
|
|
8871
9603
|
D2Parser,
|
|
9604
|
+
DEFAULT_BLOCKLIST,
|
|
9605
|
+
DEFAULT_PATTERNS,
|
|
9606
|
+
DecisionIngestor,
|
|
8872
9607
|
DesignConstraintAdapter,
|
|
8873
9608
|
DesignIngestor,
|
|
8874
9609
|
DiagramParser,
|
|
@@ -8893,6 +9628,7 @@ var VERSION = "0.4.3";
|
|
|
8893
9628
|
ImageAnalysisExtractor,
|
|
8894
9629
|
IntentClassifier,
|
|
8895
9630
|
JiraConnector,
|
|
9631
|
+
KnowledgeDocMaterializer,
|
|
8896
9632
|
KnowledgeIngestor,
|
|
8897
9633
|
KnowledgePipelineRunner,
|
|
8898
9634
|
KnowledgeStagingAggregator,
|
|
@@ -8919,6 +9655,7 @@ var VERSION = "0.4.3";
|
|
|
8919
9655
|
createExtractionRunner,
|
|
8920
9656
|
detectLanguage,
|
|
8921
9657
|
groupNodesByImpact,
|
|
9658
|
+
inferDomain,
|
|
8922
9659
|
linkToCode,
|
|
8923
9660
|
loadGraph,
|
|
8924
9661
|
normalizeIntent,
|