@harness-engineering/graph 0.5.0 → 0.6.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 +139 -14
- package/dist/index.d.ts +139 -14
- package/dist/index.js +1364 -704
- package/dist/index.mjs +1362 -704
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -45,6 +45,7 @@ __export(index_exports, {
|
|
|
45
45
|
ContradictionDetector: () => ContradictionDetector,
|
|
46
46
|
CoverageScorer: () => CoverageScorer,
|
|
47
47
|
D2Parser: () => D2Parser,
|
|
48
|
+
DecisionIngestor: () => DecisionIngestor,
|
|
48
49
|
DesignConstraintAdapter: () => DesignConstraintAdapter,
|
|
49
50
|
DesignIngestor: () => DesignIngestor,
|
|
50
51
|
DiagramParser: () => DiagramParser,
|
|
@@ -69,6 +70,7 @@ __export(index_exports, {
|
|
|
69
70
|
ImageAnalysisExtractor: () => ImageAnalysisExtractor,
|
|
70
71
|
IntentClassifier: () => IntentClassifier,
|
|
71
72
|
JiraConnector: () => JiraConnector,
|
|
73
|
+
KnowledgeDocMaterializer: () => KnowledgeDocMaterializer,
|
|
72
74
|
KnowledgeIngestor: () => KnowledgeIngestor,
|
|
73
75
|
KnowledgePipelineRunner: () => KnowledgePipelineRunner,
|
|
74
76
|
KnowledgeStagingAggregator: () => KnowledgeStagingAggregator,
|
|
@@ -130,7 +132,7 @@ var NODE_TYPES = [
|
|
|
130
132
|
"build",
|
|
131
133
|
"test_result",
|
|
132
134
|
"execution_outcome",
|
|
133
|
-
// Observability
|
|
135
|
+
// Observability — reserved for future tracing/metrics integration
|
|
134
136
|
"span",
|
|
135
137
|
"metric",
|
|
136
138
|
"log",
|
|
@@ -177,7 +179,7 @@ var EDGE_TYPES = [
|
|
|
177
179
|
"triggered_by",
|
|
178
180
|
"failed_in",
|
|
179
181
|
"outcome_of",
|
|
180
|
-
// Execution relationships
|
|
182
|
+
// Execution relationships — reserved for future observability integration
|
|
181
183
|
"executed_by",
|
|
182
184
|
"measured_by",
|
|
183
185
|
// Design relationships
|
|
@@ -252,7 +254,7 @@ function streamGraphJson(filePath, nodes, edges) {
|
|
|
252
254
|
stream.write(JSON.stringify(edges[i]));
|
|
253
255
|
}
|
|
254
256
|
stream.write("]}");
|
|
255
|
-
stream.end(resolve);
|
|
257
|
+
stream.end(() => resolve());
|
|
256
258
|
});
|
|
257
259
|
}
|
|
258
260
|
async function saveGraph(dirPath, nodes, edges) {
|
|
@@ -275,15 +277,19 @@ async function loadGraph(dirPath) {
|
|
|
275
277
|
await (0, import_promises.access)(metaPath);
|
|
276
278
|
await (0, import_promises.access)(graphPath);
|
|
277
279
|
} catch {
|
|
278
|
-
return
|
|
280
|
+
return { status: "not_found" };
|
|
279
281
|
}
|
|
280
282
|
const metaContent = await (0, import_promises.readFile)(metaPath, "utf-8");
|
|
281
283
|
const metadata = JSON.parse(metaContent);
|
|
282
284
|
if (metadata.schemaVersion !== CURRENT_SCHEMA_VERSION) {
|
|
283
|
-
return
|
|
285
|
+
return {
|
|
286
|
+
status: "schema_mismatch",
|
|
287
|
+
found: metadata.schemaVersion,
|
|
288
|
+
expected: CURRENT_SCHEMA_VERSION
|
|
289
|
+
};
|
|
284
290
|
}
|
|
285
291
|
const graphContent = await (0, import_promises.readFile)(graphPath, "utf-8");
|
|
286
|
-
return JSON.parse(graphContent);
|
|
292
|
+
return { status: "loaded", graph: JSON.parse(graphContent) };
|
|
287
293
|
}
|
|
288
294
|
|
|
289
295
|
// src/store/GraphStore.ts
|
|
@@ -458,8 +464,15 @@ var GraphStore = class {
|
|
|
458
464
|
await saveGraph(dirPath, allNodes, allEdges);
|
|
459
465
|
}
|
|
460
466
|
async load(dirPath) {
|
|
461
|
-
const
|
|
462
|
-
if (
|
|
467
|
+
const result = await loadGraph(dirPath);
|
|
468
|
+
if (result.status === "not_found") return false;
|
|
469
|
+
if (result.status === "schema_mismatch") {
|
|
470
|
+
console.warn(
|
|
471
|
+
`[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.`
|
|
472
|
+
);
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
const data = result.graph;
|
|
463
476
|
this.clear();
|
|
464
477
|
for (const node of data.nodes) {
|
|
465
478
|
this.nodeMap.set(node.id, { ...node });
|
|
@@ -903,9 +916,10 @@ var CodeIngestor = class {
|
|
|
903
916
|
let edgesAdded = 0;
|
|
904
917
|
const files = await this.findSourceFiles(rootDir);
|
|
905
918
|
const nameToFiles = /* @__PURE__ */ new Map();
|
|
919
|
+
const contentCache = /* @__PURE__ */ new Map();
|
|
906
920
|
for (const filePath of files) {
|
|
907
921
|
try {
|
|
908
|
-
const result = await this.processFile(filePath, rootDir, nameToFiles);
|
|
922
|
+
const result = await this.processFile(filePath, rootDir, nameToFiles, contentCache);
|
|
909
923
|
nodesAdded += result.nodesAdded;
|
|
910
924
|
edgesAdded += result.edgesAdded;
|
|
911
925
|
} catch (err) {
|
|
@@ -915,7 +929,8 @@ var CodeIngestor = class {
|
|
|
915
929
|
for (const filePath of files) {
|
|
916
930
|
try {
|
|
917
931
|
const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
918
|
-
const content =
|
|
932
|
+
const content = contentCache.get(filePath);
|
|
933
|
+
if (content === void 0) continue;
|
|
919
934
|
const callsEdges = this.extractCallsEdgesForFile(relativePath, content, nameToFiles);
|
|
920
935
|
for (const edge of callsEdges) {
|
|
921
936
|
this.store.addEdge(edge);
|
|
@@ -926,7 +941,8 @@ var CodeIngestor = class {
|
|
|
926
941
|
}
|
|
927
942
|
for (const filePath of files) {
|
|
928
943
|
try {
|
|
929
|
-
const content =
|
|
944
|
+
const content = contentCache.get(filePath);
|
|
945
|
+
if (content === void 0) continue;
|
|
930
946
|
edgesAdded += this.extractReqAnnotationsForFile(filePath, content, rootDir);
|
|
931
947
|
} catch {
|
|
932
948
|
}
|
|
@@ -940,11 +956,12 @@ var CodeIngestor = class {
|
|
|
940
956
|
durationMs: Date.now() - start
|
|
941
957
|
};
|
|
942
958
|
}
|
|
943
|
-
async processFile(filePath, rootDir, nameToFiles) {
|
|
959
|
+
async processFile(filePath, rootDir, nameToFiles, contentCache) {
|
|
944
960
|
let nodesAdded = 0;
|
|
945
961
|
let edgesAdded = 0;
|
|
946
962
|
const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
947
963
|
const content = await fs.readFile(filePath, "utf-8");
|
|
964
|
+
contentCache.set(filePath, content);
|
|
948
965
|
const stat2 = await fs.stat(filePath);
|
|
949
966
|
const fileId = `file:${relativePath}`;
|
|
950
967
|
const fileNode = {
|
|
@@ -2122,9 +2139,119 @@ function parseFrontmatter(raw) {
|
|
|
2122
2139
|
};
|
|
2123
2140
|
}
|
|
2124
2141
|
|
|
2125
|
-
// src/ingest/
|
|
2142
|
+
// src/ingest/DecisionIngestor.ts
|
|
2126
2143
|
var fs4 = __toESM(require("fs/promises"));
|
|
2127
2144
|
var path5 = __toESM(require("path"));
|
|
2145
|
+
var CODE_NODE_TYPES3 = [
|
|
2146
|
+
"file",
|
|
2147
|
+
"function",
|
|
2148
|
+
"class",
|
|
2149
|
+
"method",
|
|
2150
|
+
"interface",
|
|
2151
|
+
"variable"
|
|
2152
|
+
];
|
|
2153
|
+
var DecisionIngestor = class {
|
|
2154
|
+
constructor(store) {
|
|
2155
|
+
this.store = store;
|
|
2156
|
+
}
|
|
2157
|
+
store;
|
|
2158
|
+
async ingest(decisionsDir) {
|
|
2159
|
+
const start = Date.now();
|
|
2160
|
+
const errors = [];
|
|
2161
|
+
let files;
|
|
2162
|
+
try {
|
|
2163
|
+
files = await this.findDecisionFiles(decisionsDir);
|
|
2164
|
+
} catch {
|
|
2165
|
+
return emptyResult(Date.now() - start);
|
|
2166
|
+
}
|
|
2167
|
+
let nodesAdded = 0;
|
|
2168
|
+
let edgesAdded = 0;
|
|
2169
|
+
for (const filePath of files) {
|
|
2170
|
+
try {
|
|
2171
|
+
const raw = await fs4.readFile(filePath, "utf-8");
|
|
2172
|
+
const parsed = this.parseFrontmatter(raw);
|
|
2173
|
+
if (!parsed) continue;
|
|
2174
|
+
const { frontmatter, body } = parsed;
|
|
2175
|
+
if (!frontmatter.number || !frontmatter.title) continue;
|
|
2176
|
+
const filename = path5.basename(filePath, ".md");
|
|
2177
|
+
const nodeId = `decision:${filename}`;
|
|
2178
|
+
const node = {
|
|
2179
|
+
id: nodeId,
|
|
2180
|
+
type: "decision",
|
|
2181
|
+
name: frontmatter.title,
|
|
2182
|
+
path: filePath,
|
|
2183
|
+
content: body.trim(),
|
|
2184
|
+
metadata: {
|
|
2185
|
+
number: frontmatter.number,
|
|
2186
|
+
...frontmatter.date && { date: frontmatter.date },
|
|
2187
|
+
...frontmatter.status && { status: frontmatter.status },
|
|
2188
|
+
...frontmatter.tier && { tier: frontmatter.tier },
|
|
2189
|
+
...frontmatter.source && { source: frontmatter.source },
|
|
2190
|
+
...frontmatter.supersedes && { supersedes: frontmatter.supersedes }
|
|
2191
|
+
}
|
|
2192
|
+
};
|
|
2193
|
+
this.store.addNode(node);
|
|
2194
|
+
nodesAdded++;
|
|
2195
|
+
edgesAdded += this.linkToCode(body, nodeId);
|
|
2196
|
+
} catch (err) {
|
|
2197
|
+
errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
return {
|
|
2201
|
+
nodesAdded,
|
|
2202
|
+
nodesUpdated: 0,
|
|
2203
|
+
edgesAdded,
|
|
2204
|
+
edgesUpdated: 0,
|
|
2205
|
+
errors,
|
|
2206
|
+
durationMs: Date.now() - start
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
parseFrontmatter(raw) {
|
|
2210
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
2211
|
+
if (!match) return null;
|
|
2212
|
+
const yamlBlock = match[1];
|
|
2213
|
+
const body = match[2];
|
|
2214
|
+
const frontmatter = {};
|
|
2215
|
+
for (const line of yamlBlock.split("\n")) {
|
|
2216
|
+
const kvMatch = line.match(/^(\w+):\s*(.+)$/);
|
|
2217
|
+
if (!kvMatch) continue;
|
|
2218
|
+
frontmatter[kvMatch[1]] = kvMatch[2].trim();
|
|
2219
|
+
}
|
|
2220
|
+
if (!frontmatter.number || !frontmatter.title) return null;
|
|
2221
|
+
return {
|
|
2222
|
+
frontmatter,
|
|
2223
|
+
body
|
|
2224
|
+
};
|
|
2225
|
+
}
|
|
2226
|
+
linkToCode(content, sourceNodeId) {
|
|
2227
|
+
let count = 0;
|
|
2228
|
+
for (const nodeType of CODE_NODE_TYPES3) {
|
|
2229
|
+
const codeNodes = this.store.findNodes({ type: nodeType });
|
|
2230
|
+
for (const node of codeNodes) {
|
|
2231
|
+
if (node.name.length < 3) continue;
|
|
2232
|
+
const escaped = node.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2233
|
+
const namePattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
2234
|
+
if (namePattern.test(content)) {
|
|
2235
|
+
this.store.addEdge({
|
|
2236
|
+
from: sourceNodeId,
|
|
2237
|
+
to: node.id,
|
|
2238
|
+
type: "decided"
|
|
2239
|
+
});
|
|
2240
|
+
count++;
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
return count;
|
|
2245
|
+
}
|
|
2246
|
+
async findDecisionFiles(dir) {
|
|
2247
|
+
const entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
2248
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".md") && e.name !== "README.md").map((e) => path5.join(dir, e.name));
|
|
2249
|
+
}
|
|
2250
|
+
};
|
|
2251
|
+
|
|
2252
|
+
// src/ingest/RequirementIngestor.ts
|
|
2253
|
+
var fs5 = __toESM(require("fs/promises"));
|
|
2254
|
+
var path6 = __toESM(require("path"));
|
|
2128
2255
|
var REQUIREMENT_SECTIONS = [
|
|
2129
2256
|
"Observable Truths",
|
|
2130
2257
|
"Success Criteria",
|
|
@@ -2141,7 +2268,7 @@ function detectEarsPattern(text) {
|
|
|
2141
2268
|
if (/^the\s+\w+\s+shall\b/.test(lower)) return "ubiquitous";
|
|
2142
2269
|
return void 0;
|
|
2143
2270
|
}
|
|
2144
|
-
var
|
|
2271
|
+
var CODE_NODE_TYPES4 = ["file", "function", "class", "method", "interface", "variable"];
|
|
2145
2272
|
var RequirementIngestor = class {
|
|
2146
2273
|
constructor(store) {
|
|
2147
2274
|
this.store = store;
|
|
@@ -2159,8 +2286,8 @@ var RequirementIngestor = class {
|
|
|
2159
2286
|
let edgesAdded = 0;
|
|
2160
2287
|
let featureDirs;
|
|
2161
2288
|
try {
|
|
2162
|
-
const entries = await
|
|
2163
|
-
featureDirs = entries.filter((e) => e.isDirectory()).map((e) =>
|
|
2289
|
+
const entries = await fs5.readdir(specsDir, { withFileTypes: true });
|
|
2290
|
+
featureDirs = entries.filter((e) => e.isDirectory()).map((e) => path6.join(specsDir, e.name));
|
|
2164
2291
|
} catch {
|
|
2165
2292
|
return emptyResult(Date.now() - start);
|
|
2166
2293
|
}
|
|
@@ -2179,11 +2306,11 @@ var RequirementIngestor = class {
|
|
|
2179
2306
|
};
|
|
2180
2307
|
}
|
|
2181
2308
|
async ingestFeatureDir(featureDir, errors) {
|
|
2182
|
-
const featureName =
|
|
2183
|
-
const specPath =
|
|
2309
|
+
const featureName = path6.basename(featureDir);
|
|
2310
|
+
const specPath = path6.join(featureDir, "proposal.md").replaceAll("\\", "/");
|
|
2184
2311
|
let content;
|
|
2185
2312
|
try {
|
|
2186
|
-
content = await
|
|
2313
|
+
content = await fs5.readFile(specPath, "utf-8");
|
|
2187
2314
|
} catch {
|
|
2188
2315
|
return { nodesAdded: 0, edgesAdded: 0 };
|
|
2189
2316
|
}
|
|
@@ -2200,7 +2327,7 @@ var RequirementIngestor = class {
|
|
|
2200
2327
|
this.store.addNode({
|
|
2201
2328
|
id: specNodeId,
|
|
2202
2329
|
type: "document",
|
|
2203
|
-
name:
|
|
2330
|
+
name: path6.basename(specPath),
|
|
2204
2331
|
path: specPath,
|
|
2205
2332
|
metadata: { featureName }
|
|
2206
2333
|
});
|
|
@@ -2315,9 +2442,9 @@ var RequirementIngestor = class {
|
|
|
2315
2442
|
for (const node of fileNodes) {
|
|
2316
2443
|
if (!node.path) continue;
|
|
2317
2444
|
const normalizedPath = node.path.replace(/\\/g, "/");
|
|
2318
|
-
const isCodeMatch = normalizedPath.includes("packages/") &&
|
|
2445
|
+
const isCodeMatch = normalizedPath.includes("packages/") && path6.basename(normalizedPath).includes(featureName);
|
|
2319
2446
|
const isTestMatch = normalizedPath.includes("/tests/") && // platform-safe
|
|
2320
|
-
|
|
2447
|
+
path6.basename(normalizedPath).includes(featureName);
|
|
2321
2448
|
if (isCodeMatch && !isTestMatch) {
|
|
2322
2449
|
this.store.addEdge({
|
|
2323
2450
|
from: reqId,
|
|
@@ -2346,7 +2473,7 @@ var RequirementIngestor = class {
|
|
|
2346
2473
|
*/
|
|
2347
2474
|
linkByKeywordOverlap(reqId, reqText) {
|
|
2348
2475
|
let count = 0;
|
|
2349
|
-
for (const nodeType of
|
|
2476
|
+
for (const nodeType of CODE_NODE_TYPES4) {
|
|
2350
2477
|
const codeNodes = this.store.findNodes({ type: nodeType });
|
|
2351
2478
|
for (const node of codeNodes) {
|
|
2352
2479
|
if (node.name.length < 3) continue;
|
|
@@ -2370,19 +2497,158 @@ var RequirementIngestor = class {
|
|
|
2370
2497
|
};
|
|
2371
2498
|
|
|
2372
2499
|
// src/ingest/KnowledgePipelineRunner.ts
|
|
2373
|
-
var
|
|
2374
|
-
var
|
|
2500
|
+
var fs11 = __toESM(require("fs/promises"));
|
|
2501
|
+
var path12 = __toESM(require("path"));
|
|
2375
2502
|
|
|
2376
2503
|
// src/ingest/DiagramParser.ts
|
|
2377
|
-
var
|
|
2378
|
-
var
|
|
2379
|
-
|
|
2504
|
+
var fs6 = __toESM(require("fs/promises"));
|
|
2505
|
+
var path7 = __toESM(require("path"));
|
|
2506
|
+
|
|
2507
|
+
// src/ingest/parsers/mermaid.ts
|
|
2508
|
+
function emptyResult2(diagramType = "unknown") {
|
|
2380
2509
|
return {
|
|
2381
2510
|
entities: [],
|
|
2382
2511
|
relationships: [],
|
|
2383
2512
|
metadata: { format: "mermaid", diagramType }
|
|
2384
2513
|
};
|
|
2385
2514
|
}
|
|
2515
|
+
function detectDiagramType(content) {
|
|
2516
|
+
for (const line of content.split("\n")) {
|
|
2517
|
+
const trimmed = line.trim();
|
|
2518
|
+
if (!trimmed) continue;
|
|
2519
|
+
if (/^(?:graph|flowchart)\b/i.test(trimmed)) return "flowchart";
|
|
2520
|
+
if (/^sequenceDiagram\b/.test(trimmed)) return "sequence";
|
|
2521
|
+
if (/^classDiagram\b/.test(trimmed)) return "class";
|
|
2522
|
+
if (/^erDiagram\b/.test(trimmed)) return "er";
|
|
2523
|
+
break;
|
|
2524
|
+
}
|
|
2525
|
+
return "unknown";
|
|
2526
|
+
}
|
|
2527
|
+
function isDecisionNode(content, nodeId) {
|
|
2528
|
+
const decisionRegex = new RegExp(`${nodeId}\\s*\\{`);
|
|
2529
|
+
return decisionRegex.test(content);
|
|
2530
|
+
}
|
|
2531
|
+
function buildFlowchartEntity(content, match) {
|
|
2532
|
+
const id = match[1] ?? "";
|
|
2533
|
+
const label = (match[2] ?? "").trim();
|
|
2534
|
+
if (!id) return null;
|
|
2535
|
+
const decision = isDecisionNode(content, id);
|
|
2536
|
+
const entity = {
|
|
2537
|
+
id,
|
|
2538
|
+
label,
|
|
2539
|
+
...decision ? { type: "decision" } : {}
|
|
2540
|
+
};
|
|
2541
|
+
return { id, entity };
|
|
2542
|
+
}
|
|
2543
|
+
function extractFlowchartNodes(content) {
|
|
2544
|
+
const entities = /* @__PURE__ */ new Map();
|
|
2545
|
+
const nodeRegex = /([A-Za-z0-9_]+)\s*[[({]([^\])}]+)[\])}]/g;
|
|
2546
|
+
let match = nodeRegex.exec(content);
|
|
2547
|
+
while (match !== null) {
|
|
2548
|
+
const parsed = buildFlowchartEntity(content, match);
|
|
2549
|
+
if (parsed && !entities.has(parsed.id)) {
|
|
2550
|
+
entities.set(parsed.id, parsed.entity);
|
|
2551
|
+
}
|
|
2552
|
+
match = nodeRegex.exec(content);
|
|
2553
|
+
}
|
|
2554
|
+
return entities;
|
|
2555
|
+
}
|
|
2556
|
+
function parseLabeledEdgeMatch(match) {
|
|
2557
|
+
const from = match[1] ?? "";
|
|
2558
|
+
const label = (match[2] ?? "").trim();
|
|
2559
|
+
const to = match[3] ?? "";
|
|
2560
|
+
if (!from || !to) return null;
|
|
2561
|
+
return { key: `${from}->${to}:${label}`, rel: { from, to, label } };
|
|
2562
|
+
}
|
|
2563
|
+
function extractLabeledEdges(stripped, edgeKeys) {
|
|
2564
|
+
const relationships = [];
|
|
2565
|
+
const labeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\|([^|]+)\|\s*([A-Za-z0-9_]+)/g;
|
|
2566
|
+
let match = labeledEdgeRegex.exec(stripped);
|
|
2567
|
+
while (match !== null) {
|
|
2568
|
+
const parsed = parseLabeledEdgeMatch(match);
|
|
2569
|
+
if (parsed && !edgeKeys.has(parsed.key)) {
|
|
2570
|
+
edgeKeys.add(parsed.key);
|
|
2571
|
+
relationships.push(parsed.rel);
|
|
2572
|
+
}
|
|
2573
|
+
match = labeledEdgeRegex.exec(stripped);
|
|
2574
|
+
}
|
|
2575
|
+
return relationships;
|
|
2576
|
+
}
|
|
2577
|
+
function hasLabeledEdgeBetween(labeledEdges, from, to) {
|
|
2578
|
+
return labeledEdges.some((r) => r.from === from && r.to === to);
|
|
2579
|
+
}
|
|
2580
|
+
function extractUnlabeledEdges(stripped, edgeKeys, labeledEdges) {
|
|
2581
|
+
const relationships = [];
|
|
2582
|
+
const unlabeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\s+([A-Za-z0-9_]+)/g;
|
|
2583
|
+
let match = unlabeledEdgeRegex.exec(stripped);
|
|
2584
|
+
while (match !== null) {
|
|
2585
|
+
const from = match[1] ?? "";
|
|
2586
|
+
const to = match[2] ?? "";
|
|
2587
|
+
const key = `${from}->${to}`;
|
|
2588
|
+
if (from && to && !hasLabeledEdgeBetween(labeledEdges, from, to) && !edgeKeys.has(key)) {
|
|
2589
|
+
edgeKeys.add(key);
|
|
2590
|
+
relationships.push({ from, to });
|
|
2591
|
+
}
|
|
2592
|
+
match = unlabeledEdgeRegex.exec(stripped);
|
|
2593
|
+
}
|
|
2594
|
+
return relationships;
|
|
2595
|
+
}
|
|
2596
|
+
function parseFlowchart(content, diagramType) {
|
|
2597
|
+
const entities = extractFlowchartNodes(content);
|
|
2598
|
+
const stripped = content.replace(/[[({][^\])}]*[\])}]/g, "");
|
|
2599
|
+
const edgeKeys = /* @__PURE__ */ new Set();
|
|
2600
|
+
const labeledEdges = extractLabeledEdges(stripped, edgeKeys);
|
|
2601
|
+
const unlabeledEdges = extractUnlabeledEdges(stripped, edgeKeys, labeledEdges);
|
|
2602
|
+
return {
|
|
2603
|
+
entities: Array.from(entities.values()),
|
|
2604
|
+
relationships: [...labeledEdges, ...unlabeledEdges],
|
|
2605
|
+
metadata: { format: "mermaid", diagramType }
|
|
2606
|
+
};
|
|
2607
|
+
}
|
|
2608
|
+
function extractParticipants(content) {
|
|
2609
|
+
const entities = [];
|
|
2610
|
+
const seenParticipants = /* @__PURE__ */ new Set();
|
|
2611
|
+
const participantRegex = /participant\s+(\w+)(?:\s+as\s+(.+))?/g;
|
|
2612
|
+
let match = participantRegex.exec(content);
|
|
2613
|
+
while (match !== null) {
|
|
2614
|
+
const id = match[1] ?? "";
|
|
2615
|
+
const label = match[2]?.trim() || id;
|
|
2616
|
+
if (id && !seenParticipants.has(id)) {
|
|
2617
|
+
seenParticipants.add(id);
|
|
2618
|
+
entities.push({ id, label });
|
|
2619
|
+
}
|
|
2620
|
+
match = participantRegex.exec(content);
|
|
2621
|
+
}
|
|
2622
|
+
return entities;
|
|
2623
|
+
}
|
|
2624
|
+
function relationshipFromMatch(match) {
|
|
2625
|
+
const from = match[1] ?? "";
|
|
2626
|
+
const to = match[2] ?? "";
|
|
2627
|
+
const label = (match[3] ?? "").trim();
|
|
2628
|
+
return from && to ? { from, to, label } : null;
|
|
2629
|
+
}
|
|
2630
|
+
function collectMessageMatches(content, regex) {
|
|
2631
|
+
const results = [];
|
|
2632
|
+
let match = regex.exec(content);
|
|
2633
|
+
while (match !== null) {
|
|
2634
|
+
const rel = relationshipFromMatch(match);
|
|
2635
|
+
if (rel) results.push(rel);
|
|
2636
|
+
match = regex.exec(content);
|
|
2637
|
+
}
|
|
2638
|
+
return results;
|
|
2639
|
+
}
|
|
2640
|
+
function extractMessages(content) {
|
|
2641
|
+
const forward = collectMessageMatches(content, /(\w+)\s*->>?\+?\s*(\w+)\s*:\s*(.+)/g);
|
|
2642
|
+
const returns = collectMessageMatches(content, /(\w+)\s*-->>?-?\s*(\w+)\s*:\s*(.+)/g);
|
|
2643
|
+
return [...forward, ...returns];
|
|
2644
|
+
}
|
|
2645
|
+
function parseSequence(content, diagramType) {
|
|
2646
|
+
return {
|
|
2647
|
+
entities: extractParticipants(content),
|
|
2648
|
+
relationships: extractMessages(content),
|
|
2649
|
+
metadata: { format: "mermaid", diagramType }
|
|
2650
|
+
};
|
|
2651
|
+
}
|
|
2386
2652
|
var MermaidParser = class {
|
|
2387
2653
|
canParse(_content, ext) {
|
|
2388
2654
|
return ext === ".mmd" || ext === ".mermaid";
|
|
@@ -2390,269 +2656,199 @@ var MermaidParser = class {
|
|
|
2390
2656
|
parse(content, _filePath) {
|
|
2391
2657
|
const trimmed = content.trim();
|
|
2392
2658
|
if (!trimmed) {
|
|
2393
|
-
return
|
|
2659
|
+
return emptyResult2();
|
|
2394
2660
|
}
|
|
2395
|
-
const diagramType =
|
|
2661
|
+
const diagramType = detectDiagramType(trimmed);
|
|
2396
2662
|
switch (diagramType) {
|
|
2397
2663
|
case "flowchart":
|
|
2398
|
-
return
|
|
2664
|
+
return parseFlowchart(trimmed, diagramType);
|
|
2399
2665
|
case "sequence":
|
|
2400
|
-
return
|
|
2666
|
+
return parseSequence(trimmed, diagramType);
|
|
2401
2667
|
default:
|
|
2402
|
-
return
|
|
2668
|
+
return emptyResult2(diagramType);
|
|
2403
2669
|
}
|
|
2404
2670
|
}
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2671
|
+
};
|
|
2672
|
+
|
|
2673
|
+
// src/ingest/parsers/d2.ts
|
|
2674
|
+
function emptyResult3() {
|
|
2675
|
+
return {
|
|
2676
|
+
entities: [],
|
|
2677
|
+
relationships: [],
|
|
2678
|
+
metadata: { format: "d2", diagramType: "architecture" }
|
|
2679
|
+
};
|
|
2680
|
+
}
|
|
2681
|
+
function parseBlockShape(stripped) {
|
|
2682
|
+
const match = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+?)\s*\{$/);
|
|
2683
|
+
if (match) {
|
|
2684
|
+
const id = match[1];
|
|
2685
|
+
const label = match[2];
|
|
2686
|
+
if (id && label) return { id, label };
|
|
2687
|
+
}
|
|
2688
|
+
return null;
|
|
2689
|
+
}
|
|
2690
|
+
function parseConnection(stripped) {
|
|
2691
|
+
const match = stripped.match(/^([a-zA-Z0-9_.-]+)\s*->\s*([a-zA-Z0-9_.-]+)(?:\s*:\s*(.+?))?$/);
|
|
2692
|
+
if (match) {
|
|
2693
|
+
const from = match[1] ?? "";
|
|
2694
|
+
const to = match[2] ?? "";
|
|
2695
|
+
const label = match[3]?.trim();
|
|
2696
|
+
if (from && to) return { from, to, ...label ? { label } : {} };
|
|
2697
|
+
}
|
|
2698
|
+
return null;
|
|
2699
|
+
}
|
|
2700
|
+
function parseSimpleShape(stripped) {
|
|
2701
|
+
const match = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+)$/);
|
|
2702
|
+
if (match) {
|
|
2703
|
+
const id = match[1];
|
|
2704
|
+
const label = (match[2] ?? "").trim();
|
|
2705
|
+
if (id && label) return { id, label };
|
|
2706
|
+
}
|
|
2707
|
+
return null;
|
|
2708
|
+
}
|
|
2709
|
+
function handleBlockOpen(stripped, state) {
|
|
2710
|
+
if (state.braceDepth === 0) {
|
|
2711
|
+
const shape = parseBlockShape(stripped);
|
|
2712
|
+
if (shape) state.entities.set(shape.id, { id: shape.id, label: shape.label });
|
|
2417
2713
|
}
|
|
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
|
-
};
|
|
2714
|
+
state.braceDepth++;
|
|
2715
|
+
}
|
|
2716
|
+
function processTopLevelLine(stripped, state) {
|
|
2717
|
+
const conn = parseConnection(stripped);
|
|
2718
|
+
if (conn) {
|
|
2719
|
+
state.relationships.push(conn);
|
|
2720
|
+
return;
|
|
2476
2721
|
}
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2722
|
+
const shape = parseSimpleShape(stripped);
|
|
2723
|
+
if (shape && !state.entities.has(shape.id)) {
|
|
2724
|
+
state.entities.set(shape.id, { id: shape.id, label: shape.label });
|
|
2480
2725
|
}
|
|
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
|
-
};
|
|
2726
|
+
}
|
|
2727
|
+
function processD2Line(stripped, state) {
|
|
2728
|
+
if (!stripped || stripped.startsWith("#")) return;
|
|
2729
|
+
if (stripped.endsWith("{")) {
|
|
2730
|
+
handleBlockOpen(stripped, state);
|
|
2731
|
+
return;
|
|
2524
2732
|
}
|
|
2525
|
-
}
|
|
2733
|
+
if (stripped === "}") {
|
|
2734
|
+
state.braceDepth = Math.max(0, state.braceDepth - 1);
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
if (state.braceDepth > 0) return;
|
|
2738
|
+
processTopLevelLine(stripped, state);
|
|
2739
|
+
}
|
|
2526
2740
|
var D2Parser = class {
|
|
2527
2741
|
canParse(_content, ext) {
|
|
2528
2742
|
return ext === ".d2";
|
|
2529
2743
|
}
|
|
2530
2744
|
parse(content, _filePath) {
|
|
2531
2745
|
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;
|
|
2746
|
+
if (!trimmed) return emptyResult3();
|
|
2747
|
+
const state = {
|
|
2748
|
+
entities: /* @__PURE__ */ new Map(),
|
|
2749
|
+
relationships: [],
|
|
2750
|
+
braceDepth: 0
|
|
2751
|
+
};
|
|
2542
2752
|
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
|
-
}
|
|
2753
|
+
processD2Line(line.trim(), state);
|
|
2584
2754
|
}
|
|
2585
2755
|
return {
|
|
2586
|
-
entities: Array.from(entities.values()),
|
|
2587
|
-
relationships,
|
|
2756
|
+
entities: Array.from(state.entities.values()),
|
|
2757
|
+
relationships: state.relationships,
|
|
2588
2758
|
metadata: { format: "d2", diagramType: "architecture" }
|
|
2589
2759
|
};
|
|
2590
2760
|
}
|
|
2591
2761
|
};
|
|
2762
|
+
|
|
2763
|
+
// src/ingest/parsers/plantuml.ts
|
|
2764
|
+
function emptyResult4() {
|
|
2765
|
+
return {
|
|
2766
|
+
entities: [],
|
|
2767
|
+
relationships: [],
|
|
2768
|
+
metadata: { format: "plantuml", diagramType: "unknown" }
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2771
|
+
function detectDiagramType2(content) {
|
|
2772
|
+
if (/class\s+\w+/.test(content)) return "class";
|
|
2773
|
+
if (/\[.+\]/.test(content) || /component\s+/.test(content)) return "component";
|
|
2774
|
+
if (/participant\s+/.test(content) || /actor\s+/.test(content)) return "sequence";
|
|
2775
|
+
return "unknown";
|
|
2776
|
+
}
|
|
2777
|
+
function stripWrappers(content) {
|
|
2778
|
+
return content.replace(/@startuml\b.*\n?/, "").replace(/@enduml\b.*/, "").trim();
|
|
2779
|
+
}
|
|
2780
|
+
function extractClasses(body, entities) {
|
|
2781
|
+
const classRegex = /class\s+(\w+)/g;
|
|
2782
|
+
let match = classRegex.exec(body);
|
|
2783
|
+
while (match !== null) {
|
|
2784
|
+
const id = match[1] ?? "";
|
|
2785
|
+
if (id && !entities.has(id)) {
|
|
2786
|
+
entities.set(id, { id, label: id });
|
|
2787
|
+
}
|
|
2788
|
+
match = classRegex.exec(body);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
function extractComponents(body, entities) {
|
|
2792
|
+
const componentRegex = /\[([^\]]+)\]/g;
|
|
2793
|
+
let match = componentRegex.exec(body);
|
|
2794
|
+
while (match !== null) {
|
|
2795
|
+
const label = (match[1] ?? "").trim();
|
|
2796
|
+
if (label) {
|
|
2797
|
+
const id = label.replace(/\s+/g, "_");
|
|
2798
|
+
if (!entities.has(id)) {
|
|
2799
|
+
entities.set(id, { id, label });
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
match = componentRegex.exec(body);
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
function parseRelationshipMatch(match) {
|
|
2806
|
+
const from = match[1] ?? "";
|
|
2807
|
+
const to = match[2] ?? "";
|
|
2808
|
+
if (!from || !to) return null;
|
|
2809
|
+
const label = match[3]?.trim();
|
|
2810
|
+
return { from, to, ...label ? { label } : {} };
|
|
2811
|
+
}
|
|
2812
|
+
function collectRelationshipMatches(body, regex) {
|
|
2813
|
+
const results = [];
|
|
2814
|
+
let match = regex.exec(body);
|
|
2815
|
+
while (match !== null) {
|
|
2816
|
+
const rel = parseRelationshipMatch(match);
|
|
2817
|
+
if (rel) results.push(rel);
|
|
2818
|
+
match = regex.exec(body);
|
|
2819
|
+
}
|
|
2820
|
+
return results;
|
|
2821
|
+
}
|
|
2822
|
+
function extractRelationships(body) {
|
|
2823
|
+
return collectRelationshipMatches(
|
|
2824
|
+
body,
|
|
2825
|
+
/(\w+)\s*(?:-->|->|<--|<-|\.\.>|--)\s*(\w+)(?:\s*:\s*(.+))?/g
|
|
2826
|
+
);
|
|
2827
|
+
}
|
|
2592
2828
|
var PlantUmlParser = class {
|
|
2593
2829
|
canParse(_content, ext) {
|
|
2594
2830
|
return ext === ".puml" || ext === ".plantuml";
|
|
2595
2831
|
}
|
|
2596
2832
|
parse(content, _filePath) {
|
|
2597
2833
|
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);
|
|
2834
|
+
if (!trimmed) return emptyResult4();
|
|
2835
|
+
const diagramType = detectDiagramType2(trimmed);
|
|
2606
2836
|
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
|
-
}
|
|
2837
|
+
const body = stripWrappers(trimmed);
|
|
2838
|
+
extractClasses(body, entities);
|
|
2839
|
+
extractComponents(body, entities);
|
|
2840
|
+
const relationships = extractRelationships(body);
|
|
2642
2841
|
return {
|
|
2643
2842
|
entities: Array.from(entities.values()),
|
|
2644
2843
|
relationships,
|
|
2645
2844
|
metadata: { format: "plantuml", diagramType }
|
|
2646
2845
|
};
|
|
2647
2846
|
}
|
|
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
2847
|
};
|
|
2848
|
+
|
|
2849
|
+
// src/ingest/DiagramParser.ts
|
|
2655
2850
|
var DIAGRAM_EXTENSIONS = /* @__PURE__ */ new Set([".mmd", ".mermaid", ".d2", ".puml", ".plantuml"]);
|
|
2851
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".harness"]);
|
|
2656
2852
|
var DiagramParser = class {
|
|
2657
2853
|
constructor(store) {
|
|
2658
2854
|
this.store = store;
|
|
@@ -2664,7 +2860,7 @@ var DiagramParser = class {
|
|
|
2664
2860
|
new PlantUmlParser()
|
|
2665
2861
|
];
|
|
2666
2862
|
parse(content, filePath) {
|
|
2667
|
-
const ext =
|
|
2863
|
+
const ext = path7.extname(filePath).toLowerCase();
|
|
2668
2864
|
for (const parser of this.parsers) {
|
|
2669
2865
|
if (parser.canParse(content, ext)) {
|
|
2670
2866
|
return parser.parse(content, filePath);
|
|
@@ -2684,41 +2880,13 @@ var DiagramParser = class {
|
|
|
2684
2880
|
const diagramFiles = await this.findDiagramFiles(projectDir);
|
|
2685
2881
|
for (const filePath of diagramFiles) {
|
|
2686
2882
|
try {
|
|
2687
|
-
const content = await
|
|
2688
|
-
const relPath =
|
|
2883
|
+
const content = await fs6.readFile(filePath, "utf-8");
|
|
2884
|
+
const relPath = path7.relative(projectDir, filePath).replaceAll("\\", "/");
|
|
2689
2885
|
const result = this.parse(content, filePath);
|
|
2690
2886
|
if (result.entities.length === 0) continue;
|
|
2691
2887
|
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
|
-
}
|
|
2888
|
+
nodesAdded += this.addEntityNodes(result, relPath, pathHash);
|
|
2889
|
+
edgesAdded += this.addRelationshipEdges(result, pathHash);
|
|
2722
2890
|
} catch (err) {
|
|
2723
2891
|
errors.push(
|
|
2724
2892
|
`Failed to parse ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -2734,25 +2902,64 @@ var DiagramParser = class {
|
|
|
2734
2902
|
durationMs: Date.now() - start
|
|
2735
2903
|
};
|
|
2736
2904
|
}
|
|
2905
|
+
/** Map diagram entities to business_concept graph nodes. */
|
|
2906
|
+
addEntityNodes(result, relPath, pathHash) {
|
|
2907
|
+
let count = 0;
|
|
2908
|
+
for (const entity of result.entities) {
|
|
2909
|
+
const nodeId = `diagram:${pathHash}:${entity.id}`;
|
|
2910
|
+
this.store.addNode({
|
|
2911
|
+
id: nodeId,
|
|
2912
|
+
type: "business_concept",
|
|
2913
|
+
name: entity.label,
|
|
2914
|
+
path: relPath,
|
|
2915
|
+
metadata: {
|
|
2916
|
+
source: "diagram",
|
|
2917
|
+
format: result.metadata.format,
|
|
2918
|
+
diagramType: result.metadata.diagramType,
|
|
2919
|
+
confidence: 0.85,
|
|
2920
|
+
...entity.type ? { entityType: entity.type } : {}
|
|
2921
|
+
}
|
|
2922
|
+
});
|
|
2923
|
+
count++;
|
|
2924
|
+
}
|
|
2925
|
+
return count;
|
|
2926
|
+
}
|
|
2927
|
+
/** Map diagram relationships to references graph edges. */
|
|
2928
|
+
addRelationshipEdges(result, pathHash) {
|
|
2929
|
+
let count = 0;
|
|
2930
|
+
for (const rel of result.relationships) {
|
|
2931
|
+
const fromId = `diagram:${pathHash}:${rel.from}`;
|
|
2932
|
+
const toId = `diagram:${pathHash}:${rel.to}`;
|
|
2933
|
+
this.store.addEdge({
|
|
2934
|
+
from: fromId,
|
|
2935
|
+
to: toId,
|
|
2936
|
+
type: "references",
|
|
2937
|
+
metadata: {
|
|
2938
|
+
...rel.label ? { label: rel.label } : {}
|
|
2939
|
+
}
|
|
2940
|
+
});
|
|
2941
|
+
count++;
|
|
2942
|
+
}
|
|
2943
|
+
return count;
|
|
2944
|
+
}
|
|
2737
2945
|
async findDiagramFiles(dir) {
|
|
2738
2946
|
const files = [];
|
|
2739
|
-
const SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".harness"]);
|
|
2740
2947
|
const walk = async (currentDir) => {
|
|
2741
2948
|
let entries;
|
|
2742
2949
|
try {
|
|
2743
|
-
entries = await
|
|
2950
|
+
entries = await fs6.readdir(currentDir, { withFileTypes: true });
|
|
2744
2951
|
} catch {
|
|
2745
2952
|
return;
|
|
2746
2953
|
}
|
|
2747
2954
|
for (const entry of entries) {
|
|
2748
2955
|
if (entry.isDirectory()) {
|
|
2749
|
-
if (!
|
|
2750
|
-
await walk(
|
|
2956
|
+
if (!SKIP_DIRS.has(entry.name)) {
|
|
2957
|
+
await walk(path7.join(currentDir, entry.name));
|
|
2751
2958
|
}
|
|
2752
2959
|
} else if (entry.isFile()) {
|
|
2753
|
-
const ext =
|
|
2960
|
+
const ext = path7.extname(entry.name).toLowerCase();
|
|
2754
2961
|
if (DIAGRAM_EXTENSIONS.has(ext)) {
|
|
2755
|
-
files.push(
|
|
2962
|
+
files.push(path7.join(currentDir, entry.name));
|
|
2756
2963
|
}
|
|
2757
2964
|
}
|
|
2758
2965
|
}
|
|
@@ -2763,8 +2970,8 @@ var DiagramParser = class {
|
|
|
2763
2970
|
};
|
|
2764
2971
|
|
|
2765
2972
|
// src/ingest/KnowledgeLinker.ts
|
|
2766
|
-
var
|
|
2767
|
-
var
|
|
2973
|
+
var fs7 = __toESM(require("fs/promises"));
|
|
2974
|
+
var path8 = __toESM(require("path"));
|
|
2768
2975
|
var HEURISTIC_PATTERNS = [
|
|
2769
2976
|
{
|
|
2770
2977
|
name: "business-rule-imperative",
|
|
@@ -2911,8 +3118,10 @@ var KnowledgeLinker = class {
|
|
|
2911
3118
|
if (!duplicate) return false;
|
|
2912
3119
|
const existingSources = duplicate.metadata.sources ?? [];
|
|
2913
3120
|
if (!existingSources.includes(candidate.sourceNodeId)) {
|
|
2914
|
-
|
|
2915
|
-
|
|
3121
|
+
this.store.addNode({
|
|
3122
|
+
...duplicate,
|
|
3123
|
+
metadata: { ...duplicate.metadata, sources: [...existingSources, candidate.sourceNodeId] }
|
|
3124
|
+
});
|
|
2916
3125
|
}
|
|
2917
3126
|
return true;
|
|
2918
3127
|
}
|
|
@@ -2946,8 +3155,8 @@ var KnowledgeLinker = class {
|
|
|
2946
3155
|
*/
|
|
2947
3156
|
async writeJsonl(candidates) {
|
|
2948
3157
|
if (!this.outputDir) return;
|
|
2949
|
-
await
|
|
2950
|
-
const filePath =
|
|
3158
|
+
await fs7.mkdir(this.outputDir, { recursive: true });
|
|
3159
|
+
const filePath = path8.join(this.outputDir, "linker.jsonl");
|
|
2951
3160
|
const lines = candidates.map(
|
|
2952
3161
|
(c) => JSON.stringify({
|
|
2953
3162
|
id: c.id,
|
|
@@ -2960,16 +3169,16 @@ var KnowledgeLinker = class {
|
|
|
2960
3169
|
nodeType: c.nodeType
|
|
2961
3170
|
})
|
|
2962
3171
|
);
|
|
2963
|
-
await
|
|
3172
|
+
await fs7.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
|
|
2964
3173
|
}
|
|
2965
3174
|
/**
|
|
2966
3175
|
* Write medium-confidence candidates to staged JSONL for human review.
|
|
2967
3176
|
*/
|
|
2968
3177
|
async writeStagedJsonl(candidates) {
|
|
2969
3178
|
if (!this.outputDir || candidates.length === 0) return;
|
|
2970
|
-
const stagedDir =
|
|
2971
|
-
await
|
|
2972
|
-
const filePath =
|
|
3179
|
+
const stagedDir = path8.join(this.outputDir, "staged");
|
|
3180
|
+
await fs7.mkdir(stagedDir, { recursive: true });
|
|
3181
|
+
const filePath = path8.join(stagedDir, "linker-staged.jsonl");
|
|
2973
3182
|
const lines = candidates.map(
|
|
2974
3183
|
(c) => JSON.stringify({
|
|
2975
3184
|
id: c.id,
|
|
@@ -2982,7 +3191,7 @@ var KnowledgeLinker = class {
|
|
|
2982
3191
|
nodeType: c.nodeType
|
|
2983
3192
|
})
|
|
2984
3193
|
);
|
|
2985
|
-
await
|
|
3194
|
+
await fs7.writeFile(filePath, lines.join("\n") + "\n");
|
|
2986
3195
|
}
|
|
2987
3196
|
/**
|
|
2988
3197
|
* Apply heuristic patterns to content and return candidate extractions.
|
|
@@ -3088,8 +3297,16 @@ var StructuralDriftDetector = class {
|
|
|
3088
3297
|
};
|
|
3089
3298
|
|
|
3090
3299
|
// src/ingest/KnowledgeStagingAggregator.ts
|
|
3091
|
-
var
|
|
3092
|
-
var
|
|
3300
|
+
var fs8 = __toESM(require("fs/promises"));
|
|
3301
|
+
var path9 = __toESM(require("path"));
|
|
3302
|
+
var BUSINESS_NODE_TYPES = [
|
|
3303
|
+
"business_concept",
|
|
3304
|
+
"business_rule",
|
|
3305
|
+
"business_process",
|
|
3306
|
+
"business_term",
|
|
3307
|
+
"business_metric",
|
|
3308
|
+
"business_fact"
|
|
3309
|
+
];
|
|
3093
3310
|
var KnowledgeStagingAggregator = class {
|
|
3094
3311
|
constructor(projectDir) {
|
|
3095
3312
|
this.projectDir = projectDir;
|
|
@@ -3108,59 +3325,157 @@ var KnowledgeStagingAggregator = class {
|
|
|
3108
3325
|
if (deduplicated.length === 0) {
|
|
3109
3326
|
return { staged: 0 };
|
|
3110
3327
|
}
|
|
3111
|
-
const stagedDir =
|
|
3112
|
-
await
|
|
3328
|
+
const stagedDir = path9.join(this.projectDir, ".harness", "knowledge", "staged");
|
|
3329
|
+
await fs8.mkdir(stagedDir, { recursive: true });
|
|
3113
3330
|
const jsonl = deduplicated.map((entry) => JSON.stringify(entry)).join("\n") + "\n";
|
|
3114
|
-
await
|
|
3331
|
+
await fs8.writeFile(path9.join(stagedDir, "pipeline-staged.jsonl"), jsonl, "utf-8");
|
|
3115
3332
|
return { staged: deduplicated.length };
|
|
3116
3333
|
}
|
|
3117
|
-
async
|
|
3118
|
-
const
|
|
3334
|
+
async extractDocName(filePath) {
|
|
3335
|
+
const raw = await fs8.readFile(filePath, "utf-8");
|
|
3336
|
+
const fmMatch = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
|
|
3337
|
+
const body = fmMatch ? fmMatch[1] : raw;
|
|
3338
|
+
const titleMatch = body.match(/^#\s+(.+)$/m);
|
|
3339
|
+
return titleMatch ? titleMatch[1].trim() : path9.basename(filePath, ".md");
|
|
3340
|
+
}
|
|
3341
|
+
async generateGapReport(knowledgeDir, store) {
|
|
3342
|
+
const domainDocNames = /* @__PURE__ */ new Map();
|
|
3343
|
+
const domainEntryCounts = /* @__PURE__ */ new Map();
|
|
3119
3344
|
let totalEntries = 0;
|
|
3120
3345
|
try {
|
|
3121
|
-
const entries = await
|
|
3346
|
+
const entries = await fs8.readdir(knowledgeDir, { withFileTypes: true });
|
|
3122
3347
|
const domainDirs = entries.filter((e) => e.isDirectory());
|
|
3123
3348
|
for (const dir of domainDirs) {
|
|
3124
|
-
const domainPath =
|
|
3125
|
-
const files = await
|
|
3349
|
+
const domainPath = path9.join(knowledgeDir, dir.name);
|
|
3350
|
+
const files = await fs8.readdir(domainPath);
|
|
3126
3351
|
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
3127
3352
|
const entryCount = mdFiles.length;
|
|
3128
3353
|
totalEntries += entryCount;
|
|
3129
|
-
|
|
3354
|
+
domainEntryCounts.set(dir.name, entryCount);
|
|
3355
|
+
if (store) {
|
|
3356
|
+
const names = [];
|
|
3357
|
+
for (const file of mdFiles) {
|
|
3358
|
+
const name = await this.extractDocName(path9.join(domainPath, file));
|
|
3359
|
+
names.push(name.toLowerCase().trim());
|
|
3360
|
+
}
|
|
3361
|
+
domainDocNames.set(dir.name, names);
|
|
3362
|
+
}
|
|
3130
3363
|
}
|
|
3131
3364
|
} catch {
|
|
3132
3365
|
}
|
|
3366
|
+
if (!store) {
|
|
3367
|
+
const domains2 = [];
|
|
3368
|
+
for (const [domain, entryCount] of domainEntryCounts) {
|
|
3369
|
+
domains2.push({ domain, entryCount, extractedCount: 0, gapCount: 0, gapEntries: [] });
|
|
3370
|
+
}
|
|
3371
|
+
return {
|
|
3372
|
+
domains: domains2,
|
|
3373
|
+
totalEntries,
|
|
3374
|
+
totalExtracted: 0,
|
|
3375
|
+
totalGaps: 0,
|
|
3376
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3377
|
+
};
|
|
3378
|
+
}
|
|
3379
|
+
const extractedByDomain = /* @__PURE__ */ new Map();
|
|
3380
|
+
for (const nodeType of BUSINESS_NODE_TYPES) {
|
|
3381
|
+
const nodes = store.findNodes({ type: nodeType });
|
|
3382
|
+
for (const node of nodes) {
|
|
3383
|
+
const domain = node.metadata?.domain ?? "unknown";
|
|
3384
|
+
const list = extractedByDomain.get(domain) ?? [];
|
|
3385
|
+
list.push(node);
|
|
3386
|
+
extractedByDomain.set(domain, list);
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
for (const [domain, nodes] of extractedByDomain) {
|
|
3390
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3391
|
+
const deduped = nodes.filter((n) => {
|
|
3392
|
+
const key = n.name.toLowerCase().trim();
|
|
3393
|
+
if (seen.has(key)) return false;
|
|
3394
|
+
seen.add(key);
|
|
3395
|
+
return true;
|
|
3396
|
+
});
|
|
3397
|
+
extractedByDomain.set(domain, deduped);
|
|
3398
|
+
}
|
|
3399
|
+
const allDomains = /* @__PURE__ */ new Set([...domainEntryCounts.keys(), ...extractedByDomain.keys()]);
|
|
3400
|
+
const domains = [];
|
|
3401
|
+
let totalExtracted = 0;
|
|
3402
|
+
let totalGaps = 0;
|
|
3403
|
+
for (const domain of allDomains) {
|
|
3404
|
+
const entryCount = domainEntryCounts.get(domain) ?? 0;
|
|
3405
|
+
const extractedNodes = extractedByDomain.get(domain) ?? [];
|
|
3406
|
+
const extractedCount = extractedNodes.length;
|
|
3407
|
+
totalExtracted += extractedCount;
|
|
3408
|
+
const docNames = domainDocNames.get(domain) ?? [];
|
|
3409
|
+
const gapEntries = [];
|
|
3410
|
+
for (const node of extractedNodes) {
|
|
3411
|
+
const normalizedName = node.name.toLowerCase().trim();
|
|
3412
|
+
if (!docNames.includes(normalizedName)) {
|
|
3413
|
+
gapEntries.push({
|
|
3414
|
+
nodeId: node.id,
|
|
3415
|
+
name: node.name,
|
|
3416
|
+
nodeType: node.type,
|
|
3417
|
+
source: node.metadata?.source ?? "unknown",
|
|
3418
|
+
hasContent: Boolean(node.content && node.content.trim().length >= 10)
|
|
3419
|
+
});
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
totalGaps += gapEntries.length;
|
|
3423
|
+
domains.push({ domain, entryCount, extractedCount, gapCount: gapEntries.length, gapEntries });
|
|
3424
|
+
}
|
|
3133
3425
|
return {
|
|
3134
3426
|
domains,
|
|
3135
3427
|
totalEntries,
|
|
3428
|
+
totalExtracted,
|
|
3429
|
+
totalGaps,
|
|
3136
3430
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3137
3431
|
};
|
|
3138
3432
|
}
|
|
3139
3433
|
async writeGapReport(report) {
|
|
3140
|
-
const gapsDir =
|
|
3141
|
-
await
|
|
3434
|
+
const gapsDir = path9.join(this.projectDir, ".harness", "knowledge");
|
|
3435
|
+
await fs8.mkdir(gapsDir, { recursive: true });
|
|
3436
|
+
const hasDifferential = report.totalExtracted > 0;
|
|
3142
3437
|
const lines = [
|
|
3143
3438
|
"# Knowledge Gaps Report",
|
|
3144
3439
|
"",
|
|
3145
3440
|
`Generated: ${report.generatedAt}`,
|
|
3146
3441
|
"",
|
|
3147
3442
|
"## Coverage by Domain",
|
|
3148
|
-
""
|
|
3149
|
-
"| Domain | Entries |",
|
|
3150
|
-
"| ------ | ------- |"
|
|
3443
|
+
""
|
|
3151
3444
|
];
|
|
3152
|
-
|
|
3153
|
-
lines.push(
|
|
3445
|
+
if (hasDifferential) {
|
|
3446
|
+
lines.push(
|
|
3447
|
+
"| Domain | Documented | Extracted | Gaps |",
|
|
3448
|
+
"| ------ | ---------- | --------- | ---- |"
|
|
3449
|
+
);
|
|
3450
|
+
for (const domain of report.domains) {
|
|
3451
|
+
lines.push(
|
|
3452
|
+
`| ${domain.domain} | ${domain.entryCount} | ${domain.extractedCount} | ${domain.gapCount} |`
|
|
3453
|
+
);
|
|
3454
|
+
}
|
|
3455
|
+
lines.push(
|
|
3456
|
+
"",
|
|
3457
|
+
`## Summary`,
|
|
3458
|
+
"",
|
|
3459
|
+
`- **Total Documented:** ${report.totalEntries}`,
|
|
3460
|
+
`- **Total Extracted:** ${report.totalExtracted}`,
|
|
3461
|
+
`- **Total Gaps:** ${report.totalGaps}`,
|
|
3462
|
+
""
|
|
3463
|
+
);
|
|
3464
|
+
} else {
|
|
3465
|
+
lines.push("| Domain | Entries |", "| ------ | ------- |");
|
|
3466
|
+
for (const domain of report.domains) {
|
|
3467
|
+
lines.push(`| ${domain.domain} | ${domain.entryCount} |`);
|
|
3468
|
+
}
|
|
3469
|
+
lines.push("", `## Total Entries: ${report.totalEntries}`, "");
|
|
3154
3470
|
}
|
|
3155
|
-
|
|
3156
|
-
await fs7.writeFile(path8.join(gapsDir, "gaps.md"), lines.join("\n"), "utf-8");
|
|
3471
|
+
await fs8.writeFile(path9.join(gapsDir, "gaps.md"), lines.join("\n"), "utf-8");
|
|
3157
3472
|
}
|
|
3158
3473
|
};
|
|
3159
3474
|
|
|
3160
3475
|
// src/ingest/extractors/ExtractionRunner.ts
|
|
3161
|
-
var
|
|
3162
|
-
var
|
|
3163
|
-
var
|
|
3476
|
+
var fs9 = __toESM(require("fs/promises"));
|
|
3477
|
+
var path10 = __toESM(require("path"));
|
|
3478
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
3164
3479
|
"node_modules",
|
|
3165
3480
|
"dist",
|
|
3166
3481
|
"target",
|
|
@@ -3197,11 +3512,11 @@ var EXT_TO_LANGUAGE = {
|
|
|
3197
3512
|
};
|
|
3198
3513
|
var SKIP_EXTENSIONS2 = /* @__PURE__ */ new Set([".d.ts"]);
|
|
3199
3514
|
function detectLanguage(filePath) {
|
|
3200
|
-
const name =
|
|
3515
|
+
const name = path10.basename(filePath);
|
|
3201
3516
|
for (const skip of SKIP_EXTENSIONS2) {
|
|
3202
3517
|
if (name.endsWith(skip)) return void 0;
|
|
3203
3518
|
}
|
|
3204
|
-
const ext =
|
|
3519
|
+
const ext = path10.extname(filePath);
|
|
3205
3520
|
return EXT_TO_LANGUAGE[ext];
|
|
3206
3521
|
}
|
|
3207
3522
|
var EXTRACTOR_EDGE_TYPE = {
|
|
@@ -3227,7 +3542,7 @@ var ExtractionRunner = class {
|
|
|
3227
3542
|
let nodesAdded = 0;
|
|
3228
3543
|
let nodesUpdated = 0;
|
|
3229
3544
|
let edgesAdded = 0;
|
|
3230
|
-
await
|
|
3545
|
+
await fs9.mkdir(outputDir, { recursive: true });
|
|
3231
3546
|
const files = await this.findSourceFiles(projectDir);
|
|
3232
3547
|
const recordsByExtractor = /* @__PURE__ */ new Map();
|
|
3233
3548
|
for (const ext of this.extractors) {
|
|
@@ -3236,17 +3551,17 @@ var ExtractionRunner = class {
|
|
|
3236
3551
|
for (const filePath of files) {
|
|
3237
3552
|
const language = detectLanguage(filePath);
|
|
3238
3553
|
if (!language) continue;
|
|
3239
|
-
const ext =
|
|
3554
|
+
const ext = path10.extname(filePath);
|
|
3240
3555
|
let content;
|
|
3241
3556
|
try {
|
|
3242
|
-
content = await
|
|
3557
|
+
content = await fs9.readFile(filePath, "utf-8");
|
|
3243
3558
|
} catch (err) {
|
|
3244
3559
|
errors.push(
|
|
3245
3560
|
`Failed to read ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
3246
3561
|
);
|
|
3247
3562
|
continue;
|
|
3248
3563
|
}
|
|
3249
|
-
const relativePath =
|
|
3564
|
+
const relativePath = path10.relative(projectDir, filePath).replaceAll("\\", "/");
|
|
3250
3565
|
for (const extractor2 of this.extractors) {
|
|
3251
3566
|
if (!extractor2.supportedExtensions.includes(ext)) continue;
|
|
3252
3567
|
try {
|
|
@@ -3283,9 +3598,9 @@ var ExtractionRunner = class {
|
|
|
3283
3598
|
}
|
|
3284
3599
|
/** Write extraction records to JSONL file. */
|
|
3285
3600
|
async writeJsonl(records, outputDir, extractorName) {
|
|
3286
|
-
const filePath =
|
|
3601
|
+
const filePath = path10.join(outputDir, `${extractorName}.jsonl`);
|
|
3287
3602
|
const lines = records.map((r) => JSON.stringify(r));
|
|
3288
|
-
await
|
|
3603
|
+
await fs9.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
|
|
3289
3604
|
}
|
|
3290
3605
|
/** Create or update a graph node from an extraction record. */
|
|
3291
3606
|
persistRecord(record, store) {
|
|
@@ -3357,13 +3672,13 @@ var ExtractionRunner = class {
|
|
|
3357
3672
|
const results = [];
|
|
3358
3673
|
let entries;
|
|
3359
3674
|
try {
|
|
3360
|
-
entries = await
|
|
3675
|
+
entries = await fs9.readdir(dir, { withFileTypes: true });
|
|
3361
3676
|
} catch {
|
|
3362
3677
|
return results;
|
|
3363
3678
|
}
|
|
3364
3679
|
for (const entry of entries) {
|
|
3365
|
-
const fullPath =
|
|
3366
|
-
if (entry.isDirectory() && !
|
|
3680
|
+
const fullPath = path10.join(dir, entry.name);
|
|
3681
|
+
if (entry.isDirectory() && !SKIP_DIRS2.has(entry.name)) {
|
|
3367
3682
|
results.push(...await this.findSourceFiles(fullPath));
|
|
3368
3683
|
} else if (entry.isFile() && detectLanguage(fullPath) !== void 0) {
|
|
3369
3684
|
results.push(fullPath);
|
|
@@ -4489,57 +4804,9 @@ var ImageAnalysisExtractor = class {
|
|
|
4489
4804
|
const errors = [];
|
|
4490
4805
|
for (const imagePath of imagePaths) {
|
|
4491
4806
|
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
|
-
}
|
|
4807
|
+
const counts = await this.processImage(store, imagePath);
|
|
4808
|
+
nodesAdded += counts.nodes;
|
|
4809
|
+
edgesAdded += counts.edges;
|
|
4543
4810
|
} catch (err) {
|
|
4544
4811
|
errors.push(
|
|
4545
4812
|
`Image analysis failed for ${imagePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -4555,6 +4822,68 @@ var ImageAnalysisExtractor = class {
|
|
|
4555
4822
|
durationMs: Date.now() - start
|
|
4556
4823
|
};
|
|
4557
4824
|
}
|
|
4825
|
+
/** Analyze a single image and add annotation + concept nodes to the store. */
|
|
4826
|
+
async processImage(store, imagePath) {
|
|
4827
|
+
const response = await this.provider.analyze({
|
|
4828
|
+
prompt: "Analyze this image and provide a structured description of its visual contents.",
|
|
4829
|
+
systemPrompt: "You are an image analysis assistant. Describe the image contents, detect UI elements, extract visible text, identify design patterns, and note accessibility concerns.",
|
|
4830
|
+
responseSchema: {},
|
|
4831
|
+
// Schema handled by provider
|
|
4832
|
+
maxTokens: 1e3
|
|
4833
|
+
});
|
|
4834
|
+
const result = response.result;
|
|
4835
|
+
const annotationId = `img:${hash(imagePath)}`;
|
|
4836
|
+
let nodes = 0;
|
|
4837
|
+
let edges = 0;
|
|
4838
|
+
store.addNode({
|
|
4839
|
+
id: annotationId,
|
|
4840
|
+
type: "image_annotation",
|
|
4841
|
+
name: result.description.slice(0, 200),
|
|
4842
|
+
path: imagePath,
|
|
4843
|
+
content: result.description,
|
|
4844
|
+
hash: hash(result.description),
|
|
4845
|
+
metadata: {
|
|
4846
|
+
source: "image-analysis",
|
|
4847
|
+
detectedElements: result.detectedElements,
|
|
4848
|
+
extractedText: result.extractedText,
|
|
4849
|
+
designPatterns: result.designPatterns,
|
|
4850
|
+
accessibilityNotes: result.accessibilityNotes,
|
|
4851
|
+
model: response.model
|
|
4852
|
+
}
|
|
4853
|
+
});
|
|
4854
|
+
nodes++;
|
|
4855
|
+
edges += this.linkToFileNode(store, annotationId, imagePath);
|
|
4856
|
+
for (const pattern of result.designPatterns) {
|
|
4857
|
+
edges += this.addDesignPatternConcept(store, annotationId, imagePath, pattern);
|
|
4858
|
+
nodes++;
|
|
4859
|
+
}
|
|
4860
|
+
return { nodes, edges };
|
|
4861
|
+
}
|
|
4862
|
+
/** Link annotation to its source file node if it exists. Returns 1 if linked, 0 otherwise. */
|
|
4863
|
+
linkToFileNode(store, annotationId, imagePath) {
|
|
4864
|
+
const fileNode = store.findNodes({ type: "file" }).find((n) => n.path === imagePath);
|
|
4865
|
+
if (!fileNode) return 0;
|
|
4866
|
+
store.addEdge({ from: annotationId, to: fileNode.id, type: "annotates" });
|
|
4867
|
+
return 1;
|
|
4868
|
+
}
|
|
4869
|
+
/** Create a business_concept node for a design pattern and link it. Returns edges added. */
|
|
4870
|
+
addDesignPatternConcept(store, annotationId, imagePath, pattern) {
|
|
4871
|
+
const conceptId = `img:concept:${hash(pattern + imagePath)}`;
|
|
4872
|
+
store.addNode({
|
|
4873
|
+
id: conceptId,
|
|
4874
|
+
type: "business_concept",
|
|
4875
|
+
name: pattern,
|
|
4876
|
+
content: `Design pattern detected in ${imagePath}: ${pattern}`,
|
|
4877
|
+
hash: hash(pattern),
|
|
4878
|
+
metadata: {
|
|
4879
|
+
source: "image-analysis",
|
|
4880
|
+
sourceImage: imagePath,
|
|
4881
|
+
domain: "design"
|
|
4882
|
+
}
|
|
4883
|
+
});
|
|
4884
|
+
store.addEdge({ from: annotationId, to: conceptId, type: "contains" });
|
|
4885
|
+
return 1;
|
|
4886
|
+
}
|
|
4558
4887
|
};
|
|
4559
4888
|
|
|
4560
4889
|
// src/ingest/knowledgeTypes.ts
|
|
@@ -4565,6 +4894,7 @@ var KNOWLEDGE_NODE_TYPES = [
|
|
|
4565
4894
|
"business_term",
|
|
4566
4895
|
"business_concept",
|
|
4567
4896
|
"business_metric",
|
|
4897
|
+
"decision",
|
|
4568
4898
|
"design_token",
|
|
4569
4899
|
"design_constraint",
|
|
4570
4900
|
"aesthetic_intent",
|
|
@@ -4605,83 +4935,99 @@ function classifyConflict(a, b) {
|
|
|
4605
4935
|
return "temporal_conflict";
|
|
4606
4936
|
return "status_divergence";
|
|
4607
4937
|
}
|
|
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
|
-
}
|
|
4938
|
+
var SEVERITY_MAP = {
|
|
4939
|
+
value_mismatch: "critical",
|
|
4940
|
+
definition_conflict: "high",
|
|
4941
|
+
status_divergence: "medium",
|
|
4942
|
+
temporal_conflict: "medium"
|
|
4943
|
+
};
|
|
4944
|
+
function buildEntry(node, source) {
|
|
4945
|
+
return {
|
|
4946
|
+
nodeId: node.id,
|
|
4947
|
+
source,
|
|
4948
|
+
name: node.name,
|
|
4949
|
+
content: node.content ?? "",
|
|
4950
|
+
lastModified: node.lastModified
|
|
4951
|
+
};
|
|
4952
|
+
}
|
|
4953
|
+
function groupByName(nodes) {
|
|
4954
|
+
const byName = /* @__PURE__ */ new Map();
|
|
4955
|
+
for (const node of nodes) {
|
|
4956
|
+
const key = node.name.toLowerCase().trim();
|
|
4957
|
+
const group = byName.get(key) ?? [];
|
|
4958
|
+
group.push(node);
|
|
4959
|
+
byName.set(key, group);
|
|
4960
|
+
}
|
|
4961
|
+
return byName;
|
|
4962
|
+
}
|
|
4963
|
+
function tryAddContradiction(acc, a, b, similarity) {
|
|
4964
|
+
const sourceA = a.metadata.source ?? "unknown";
|
|
4965
|
+
const sourceB = b.metadata.source ?? "unknown";
|
|
4966
|
+
if (sourceA === sourceB) return;
|
|
4967
|
+
if ((a.hash ?? a.id) === (b.hash ?? b.id)) return;
|
|
4968
|
+
const pairId = [a.id, b.id].sort().join(":");
|
|
4969
|
+
if (acc.seen.has(pairId)) return;
|
|
4970
|
+
acc.seen.add(pairId);
|
|
4971
|
+
const conflictType = classifyConflict(a, b);
|
|
4972
|
+
const pairKey = [sourceA, sourceB].sort().join("\u2194");
|
|
4973
|
+
acc.sourcePairCounts[pairKey] = (acc.sourcePairCounts[pairKey] ?? 0) + 1;
|
|
4974
|
+
acc.contradictions.push({
|
|
4975
|
+
id: `contradiction:${a.id}:${b.id}`,
|
|
4976
|
+
entityA: buildEntry(a, sourceA),
|
|
4977
|
+
entityB: buildEntry(b, sourceB),
|
|
4978
|
+
similarity,
|
|
4979
|
+
conflictType,
|
|
4980
|
+
severity: SEVERITY_MAP[conflictType],
|
|
4981
|
+
description: `"${a.name}" has conflicting definitions from ${sourceA} and ${sourceB}`
|
|
4982
|
+
});
|
|
4983
|
+
}
|
|
4984
|
+
function collectExactMatches(byName, acc) {
|
|
4985
|
+
for (const [, group] of byName) {
|
|
4986
|
+
if (group.length < 2) continue;
|
|
4987
|
+
for (let i = 0; i < group.length; i++) {
|
|
4988
|
+
for (let j = i + 1; j < group.length; j++) {
|
|
4989
|
+
tryAddContradiction(acc, group[i], group[j], 1);
|
|
4663
4990
|
}
|
|
4664
4991
|
}
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4992
|
+
}
|
|
4993
|
+
}
|
|
4994
|
+
function collectFuzzyMatches(byName, acc) {
|
|
4995
|
+
const keys = [...byName.keys()];
|
|
4996
|
+
for (let i = 0; i < keys.length; i++) {
|
|
4997
|
+
for (let j = i + 1; j < keys.length; j++) {
|
|
4998
|
+
const keyA = keys[i];
|
|
4999
|
+
const keyB = keys[j];
|
|
5000
|
+
const maxLen = Math.max(keyA.length, keyB.length);
|
|
5001
|
+
const minLen = Math.min(keyA.length, keyB.length);
|
|
5002
|
+
if (minLen / maxLen < SIMILARITY_THRESHOLD) continue;
|
|
5003
|
+
const ratio = levenshteinRatio(keyA, keyB);
|
|
5004
|
+
if (ratio < SIMILARITY_THRESHOLD) continue;
|
|
5005
|
+
const groupA = byName.get(keyA);
|
|
5006
|
+
const groupB = byName.get(keyB);
|
|
5007
|
+
for (const a of groupA) {
|
|
5008
|
+
for (const b of groupB) {
|
|
5009
|
+
tryAddContradiction(acc, a, b, ratio);
|
|
4681
5010
|
}
|
|
4682
5011
|
}
|
|
4683
5012
|
}
|
|
4684
|
-
|
|
5013
|
+
}
|
|
5014
|
+
}
|
|
5015
|
+
var ContradictionDetector = class {
|
|
5016
|
+
detect(store) {
|
|
5017
|
+
const nodes = KNOWLEDGE_NODE_TYPES.flatMap((t) => store.findNodes({ type: t }));
|
|
5018
|
+
const byName = groupByName(nodes);
|
|
5019
|
+
const acc = {
|
|
5020
|
+
contradictions: [],
|
|
5021
|
+
sourcePairCounts: {},
|
|
5022
|
+
seen: /* @__PURE__ */ new Set()
|
|
5023
|
+
};
|
|
5024
|
+
collectExactMatches(byName, acc);
|
|
5025
|
+
collectFuzzyMatches(byName, acc);
|
|
5026
|
+
return {
|
|
5027
|
+
contradictions: acc.contradictions,
|
|
5028
|
+
sourcePairCounts: acc.sourcePairCounts,
|
|
5029
|
+
totalChecked: nodes.length
|
|
5030
|
+
};
|
|
4685
5031
|
}
|
|
4686
5032
|
};
|
|
4687
5033
|
|
|
@@ -4712,64 +5058,76 @@ function toGrade(score) {
|
|
|
4712
5058
|
if (score >= 20) return "D";
|
|
4713
5059
|
return "F";
|
|
4714
5060
|
}
|
|
5061
|
+
function groupByDomain(nodes, fallback) {
|
|
5062
|
+
const map = /* @__PURE__ */ new Map();
|
|
5063
|
+
for (const node of nodes) {
|
|
5064
|
+
const domain = node.metadata.domain ?? fallback(node);
|
|
5065
|
+
const group = map.get(domain) ?? [];
|
|
5066
|
+
group.push(node);
|
|
5067
|
+
map.set(domain, group);
|
|
5068
|
+
}
|
|
5069
|
+
return map;
|
|
5070
|
+
}
|
|
5071
|
+
function countLinkedEntities(codeEntries, store) {
|
|
5072
|
+
const linkedIds = /* @__PURE__ */ new Set();
|
|
5073
|
+
for (const codeNode of codeEntries) {
|
|
5074
|
+
if (hasKnowledgeEdge(codeNode.id, store)) {
|
|
5075
|
+
linkedIds.add(codeNode.id);
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
5078
|
+
return linkedIds;
|
|
5079
|
+
}
|
|
5080
|
+
function hasKnowledgeEdge(nodeId, store) {
|
|
5081
|
+
for (const edgeType of KNOWLEDGE_EDGE_TYPES) {
|
|
5082
|
+
if (store.getEdges({ to: nodeId, type: edgeType }).length > 0) return true;
|
|
5083
|
+
}
|
|
5084
|
+
return false;
|
|
5085
|
+
}
|
|
5086
|
+
function computeSourceBreakdown(knEntries) {
|
|
5087
|
+
const breakdown = {};
|
|
5088
|
+
for (const kn of knEntries) {
|
|
5089
|
+
const src = kn.metadata.source ?? "unknown";
|
|
5090
|
+
breakdown[src] = (breakdown[src] ?? 0) + 1;
|
|
5091
|
+
}
|
|
5092
|
+
return breakdown;
|
|
5093
|
+
}
|
|
5094
|
+
function computeDomainScore(knowledgeEntries, codeEntities, linkedEntities, uniqueSources) {
|
|
5095
|
+
const codeCoverageComponent = codeEntities > 0 ? linkedEntities / codeEntities * 60 : 0;
|
|
5096
|
+
const knowledgeDepthComponent = Math.min(knowledgeEntries / 10, 1) * 20;
|
|
5097
|
+
const sourceDiversityComponent = Math.min(uniqueSources / 3, 1) * 20;
|
|
5098
|
+
return Math.round(codeCoverageComponent + knowledgeDepthComponent + sourceDiversityComponent);
|
|
5099
|
+
}
|
|
5100
|
+
function scoreDomain(domain, knEntries, codeEntries, store) {
|
|
5101
|
+
const linkedIds = countLinkedEntities(codeEntries, store);
|
|
5102
|
+
const sourceBreakdown = computeSourceBreakdown(knEntries);
|
|
5103
|
+
const codeEntities = codeEntries.length;
|
|
5104
|
+
const linkedEntities = linkedIds.size;
|
|
5105
|
+
const knowledgeEntries = knEntries.length;
|
|
5106
|
+
const uniqueSources = Object.keys(sourceBreakdown).length;
|
|
5107
|
+
const score = computeDomainScore(knowledgeEntries, codeEntities, linkedEntities, uniqueSources);
|
|
5108
|
+
return {
|
|
5109
|
+
domain,
|
|
5110
|
+
score,
|
|
5111
|
+
knowledgeEntries,
|
|
5112
|
+
codeEntities,
|
|
5113
|
+
linkedEntities,
|
|
5114
|
+
unlinkedEntities: codeEntities - linkedEntities,
|
|
5115
|
+
sourceBreakdown,
|
|
5116
|
+
grade: toGrade(score)
|
|
5117
|
+
};
|
|
5118
|
+
}
|
|
4715
5119
|
var CoverageScorer = class {
|
|
4716
5120
|
score(store) {
|
|
4717
5121
|
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
|
-
}
|
|
5122
|
+
const domainMap = groupByDomain(knowledgeNodes, () => "unclassified");
|
|
4725
5123
|
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
|
-
}
|
|
5124
|
+
const codeDomains = groupByDomain(codeNodes, (n) => this.domainFromPath(n.path));
|
|
4733
5125
|
const allDomains = /* @__PURE__ */ new Set([...domainMap.keys(), ...codeDomains.keys()]);
|
|
4734
5126
|
const domains = [];
|
|
4735
5127
|
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
|
|
5128
|
+
domains.push(
|
|
5129
|
+
scoreDomain(domain, domainMap.get(domain) ?? [], codeDomains.get(domain) ?? [], store)
|
|
4762
5130
|
);
|
|
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
5131
|
}
|
|
4774
5132
|
const overallScore = domains.length > 0 ? Math.round(domains.reduce((sum, d) => sum + d.score, 0) / domains.length) : 0;
|
|
4775
5133
|
return {
|
|
@@ -4790,8 +5148,166 @@ var CoverageScorer = class {
|
|
|
4790
5148
|
}
|
|
4791
5149
|
};
|
|
4792
5150
|
|
|
5151
|
+
// src/ingest/KnowledgeDocMaterializer.ts
|
|
5152
|
+
var fs10 = __toESM(require("fs/promises"));
|
|
5153
|
+
var path11 = __toESM(require("path"));
|
|
5154
|
+
var VALID_BUSINESS_TYPES = /* @__PURE__ */ new Set([
|
|
5155
|
+
"business_rule",
|
|
5156
|
+
"business_process",
|
|
5157
|
+
"business_concept",
|
|
5158
|
+
"business_term",
|
|
5159
|
+
"business_metric"
|
|
5160
|
+
]);
|
|
5161
|
+
var DEFAULT_MAX_DOCS = 50;
|
|
5162
|
+
var MAX_COLLISION_SUFFIX = 10;
|
|
5163
|
+
var KnowledgeDocMaterializer = class {
|
|
5164
|
+
constructor(store) {
|
|
5165
|
+
this.store = store;
|
|
5166
|
+
}
|
|
5167
|
+
store;
|
|
5168
|
+
async materialize(gapEntries, options) {
|
|
5169
|
+
const maxDocs = options.maxDocs ?? DEFAULT_MAX_DOCS;
|
|
5170
|
+
const created = [];
|
|
5171
|
+
const skipped = [];
|
|
5172
|
+
const createdNames = /* @__PURE__ */ new Set();
|
|
5173
|
+
for (const entry of gapEntries) {
|
|
5174
|
+
const resolved = await this.resolveEntry(
|
|
5175
|
+
entry,
|
|
5176
|
+
created.length,
|
|
5177
|
+
maxDocs,
|
|
5178
|
+
createdNames,
|
|
5179
|
+
options
|
|
5180
|
+
);
|
|
5181
|
+
if ("reason" in resolved) {
|
|
5182
|
+
skipped.push(resolved);
|
|
5183
|
+
continue;
|
|
5184
|
+
}
|
|
5185
|
+
const { node, domain, domainDir, nameKey } = resolved;
|
|
5186
|
+
await fs10.mkdir(domainDir, { recursive: true });
|
|
5187
|
+
const basename10 = this.generateFilename(entry.name);
|
|
5188
|
+
const filename = await this.resolveCollision(domainDir, basename10);
|
|
5189
|
+
const content = this.formatDoc(node, domain);
|
|
5190
|
+
const filePath = ["docs", "knowledge", domain, filename].join("/");
|
|
5191
|
+
await fs10.writeFile(path11.join(options.projectDir, filePath), content, "utf-8");
|
|
5192
|
+
createdNames.add(nameKey);
|
|
5193
|
+
created.push({ filePath, nodeId: entry.nodeId, domain, name: entry.name });
|
|
5194
|
+
}
|
|
5195
|
+
return { created, skipped };
|
|
5196
|
+
}
|
|
5197
|
+
async resolveEntry(entry, createdCount, maxDocs, createdNames, options) {
|
|
5198
|
+
if (!entry.hasContent) {
|
|
5199
|
+
return { nodeId: entry.nodeId, name: entry.name, reason: "no_content" };
|
|
5200
|
+
}
|
|
5201
|
+
const node = this.store.getNode(entry.nodeId);
|
|
5202
|
+
if (!node || !node.content || node.content.trim().length < 10) {
|
|
5203
|
+
return { nodeId: entry.nodeId, name: entry.name, reason: "no_content" };
|
|
5204
|
+
}
|
|
5205
|
+
const domain = this.inferDomain(node);
|
|
5206
|
+
if (!domain || /[/\\]|\.\.|\0/.test(domain)) {
|
|
5207
|
+
return { nodeId: entry.nodeId, name: entry.name, reason: "no_domain" };
|
|
5208
|
+
}
|
|
5209
|
+
if (createdCount >= maxDocs) {
|
|
5210
|
+
return { nodeId: entry.nodeId, name: entry.name, reason: "cap_reached" };
|
|
5211
|
+
}
|
|
5212
|
+
const domainDir = path11.join(options.projectDir, "docs", "knowledge", domain);
|
|
5213
|
+
const nameKey = `${domain}:${entry.name.toLowerCase().trim()}`;
|
|
5214
|
+
if (!createdNames.has(nameKey) && await this.hasExistingDoc(domainDir, entry.name)) {
|
|
5215
|
+
return { nodeId: entry.nodeId, name: entry.name, reason: "already_documented" };
|
|
5216
|
+
}
|
|
5217
|
+
if (options.dryRun) {
|
|
5218
|
+
return { nodeId: entry.nodeId, name: entry.name, reason: "dry_run" };
|
|
5219
|
+
}
|
|
5220
|
+
return { node, domain, domainDir, nameKey };
|
|
5221
|
+
}
|
|
5222
|
+
inferDomain(node) {
|
|
5223
|
+
if (node.metadata?.domain && typeof node.metadata.domain === "string") {
|
|
5224
|
+
return node.metadata.domain;
|
|
5225
|
+
}
|
|
5226
|
+
if (node.path) {
|
|
5227
|
+
const pkgMatch = node.path.match(/^packages\/([^/]+)/);
|
|
5228
|
+
if (pkgMatch) return pkgMatch[1];
|
|
5229
|
+
const srcMatch = node.path.match(/^src\/([^/]+)/);
|
|
5230
|
+
if (srcMatch) return srcMatch[1];
|
|
5231
|
+
}
|
|
5232
|
+
if (node.metadata?.source === "knowledge-linker" || node.metadata?.source === "connector") {
|
|
5233
|
+
const connector = node.metadata.connectorName;
|
|
5234
|
+
if (typeof connector === "string") return connector;
|
|
5235
|
+
return "general";
|
|
5236
|
+
}
|
|
5237
|
+
return null;
|
|
5238
|
+
}
|
|
5239
|
+
generateFilename(name) {
|
|
5240
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
|
|
5241
|
+
return `${slug}.md`;
|
|
5242
|
+
}
|
|
5243
|
+
async resolveCollision(dir, basename10) {
|
|
5244
|
+
const ext = path11.extname(basename10);
|
|
5245
|
+
const stem = path11.basename(basename10, ext);
|
|
5246
|
+
try {
|
|
5247
|
+
await fs10.access(path11.join(dir, basename10));
|
|
5248
|
+
} catch {
|
|
5249
|
+
return basename10;
|
|
5250
|
+
}
|
|
5251
|
+
for (let i = 2; i <= MAX_COLLISION_SUFFIX; i++) {
|
|
5252
|
+
const candidate = `${stem}-${i}${ext}`;
|
|
5253
|
+
try {
|
|
5254
|
+
await fs10.access(path11.join(dir, candidate));
|
|
5255
|
+
} catch {
|
|
5256
|
+
return candidate;
|
|
5257
|
+
}
|
|
5258
|
+
}
|
|
5259
|
+
throw new Error(
|
|
5260
|
+
`Cannot resolve filename collision for "${basename10}" after ${MAX_COLLISION_SUFFIX} attempts`
|
|
5261
|
+
);
|
|
5262
|
+
}
|
|
5263
|
+
formatDoc(node, domain) {
|
|
5264
|
+
const mappedType = this.mapNodeType(node);
|
|
5265
|
+
const sanitize = (s) => s.replace(/[\n\r]/g, " ").replace(/:/g, "-");
|
|
5266
|
+
const lines = ["---", `type: ${sanitize(mappedType)}`, `domain: ${sanitize(domain)}`];
|
|
5267
|
+
const tags = node.metadata?.tags;
|
|
5268
|
+
if (Array.isArray(tags) && tags.length > 0) {
|
|
5269
|
+
lines.push(`tags: [${tags.map((t) => sanitize(String(t))).join(", ")}]`);
|
|
5270
|
+
}
|
|
5271
|
+
const related = node.metadata?.related;
|
|
5272
|
+
if (Array.isArray(related) && related.length > 0) {
|
|
5273
|
+
lines.push(`related: [${related.map((r) => sanitize(String(r))).join(", ")}]`);
|
|
5274
|
+
}
|
|
5275
|
+
const title = (node.name ?? "").replace(/[\n\r]/g, " ");
|
|
5276
|
+
lines.push("---", "", `# ${title}`, "", node.content ?? "", "");
|
|
5277
|
+
return lines.join("\n");
|
|
5278
|
+
}
|
|
5279
|
+
/** Check if a doc with a matching title already exists in the domain directory. */
|
|
5280
|
+
async hasExistingDoc(domainDir, name) {
|
|
5281
|
+
const normalizedName = name.toLowerCase().trim();
|
|
5282
|
+
try {
|
|
5283
|
+
const files = await fs10.readdir(domainDir);
|
|
5284
|
+
for (const file of files) {
|
|
5285
|
+
if (!file.endsWith(".md")) continue;
|
|
5286
|
+
const raw = await fs10.readFile(path11.join(domainDir, file), "utf-8");
|
|
5287
|
+
const fmMatch = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
|
|
5288
|
+
const body = fmMatch ? fmMatch[1] : raw;
|
|
5289
|
+
const titleMatch = body.match(/^#\s+(.+)$/m);
|
|
5290
|
+
if (titleMatch && titleMatch[1].trim().toLowerCase() === normalizedName) {
|
|
5291
|
+
return true;
|
|
5292
|
+
}
|
|
5293
|
+
}
|
|
5294
|
+
} catch {
|
|
5295
|
+
}
|
|
5296
|
+
return false;
|
|
5297
|
+
}
|
|
5298
|
+
mapNodeType(node) {
|
|
5299
|
+
if (VALID_BUSINESS_TYPES.has(node.type)) {
|
|
5300
|
+
return node.type;
|
|
5301
|
+
}
|
|
5302
|
+
if (node.type === "business_fact") {
|
|
5303
|
+
return "business_rule";
|
|
5304
|
+
}
|
|
5305
|
+
return "business_concept";
|
|
5306
|
+
}
|
|
5307
|
+
};
|
|
5308
|
+
|
|
4793
5309
|
// src/ingest/KnowledgePipelineRunner.ts
|
|
4794
|
-
var
|
|
5310
|
+
var BUSINESS_NODE_TYPES2 = [
|
|
4795
5311
|
"business_concept",
|
|
4796
5312
|
"business_rule",
|
|
4797
5313
|
"business_process",
|
|
@@ -4800,7 +5316,8 @@ var BUSINESS_NODE_TYPES = [
|
|
|
4800
5316
|
"business_fact"
|
|
4801
5317
|
];
|
|
4802
5318
|
var SNAPSHOT_NODE_TYPES = [
|
|
4803
|
-
...
|
|
5319
|
+
...BUSINESS_NODE_TYPES2,
|
|
5320
|
+
"decision",
|
|
4804
5321
|
"design_token",
|
|
4805
5322
|
"design_constraint",
|
|
4806
5323
|
"aesthetic_intent",
|
|
@@ -4812,51 +5329,98 @@ var KnowledgePipelineRunner = class {
|
|
|
4812
5329
|
}
|
|
4813
5330
|
store;
|
|
4814
5331
|
async run(options) {
|
|
4815
|
-
const maxIterations = options.maxIterations ?? 5;
|
|
4816
5332
|
const remediations = [];
|
|
4817
5333
|
const preSnapshot = this.buildSnapshot(options.domain);
|
|
4818
5334
|
const extraction = await this.extract(options);
|
|
4819
5335
|
const postSnapshot = this.buildSnapshot(options.domain);
|
|
4820
5336
|
let driftResult = this.reconcile(preSnapshot, postSnapshot);
|
|
4821
|
-
const
|
|
4822
|
-
const contradictions = contradictionDetector.detect(this.store);
|
|
5337
|
+
const contradictions = new ContradictionDetector().detect(this.store);
|
|
4823
5338
|
let gapReport = await this.detect(options);
|
|
4824
|
-
const
|
|
4825
|
-
|
|
5339
|
+
const coverage = new CoverageScorer().score(this.store);
|
|
5340
|
+
let materialization;
|
|
4826
5341
|
let iterations = 1;
|
|
4827
5342
|
if (options.fix) {
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
}
|
|
5343
|
+
const loopResult = await this.runRemediationLoop(
|
|
5344
|
+
options,
|
|
5345
|
+
driftResult,
|
|
5346
|
+
gapReport,
|
|
5347
|
+
remediations
|
|
5348
|
+
);
|
|
5349
|
+
iterations = loopResult.iterations;
|
|
5350
|
+
materialization = loopResult.materialization;
|
|
5351
|
+
}
|
|
5352
|
+
if (options.fix && iterations > 1) {
|
|
5353
|
+
const finalSnapshot = this.buildSnapshot(options.domain);
|
|
5354
|
+
driftResult = this.reconcile(preSnapshot, finalSnapshot);
|
|
5355
|
+
gapReport = await this.detect(options);
|
|
4842
5356
|
}
|
|
4843
5357
|
await this.stageNewFindings(driftResult, options);
|
|
5358
|
+
return this.buildResult(
|
|
5359
|
+
driftResult,
|
|
5360
|
+
iterations,
|
|
5361
|
+
extraction,
|
|
5362
|
+
gapReport,
|
|
5363
|
+
remediations,
|
|
5364
|
+
contradictions,
|
|
5365
|
+
coverage,
|
|
5366
|
+
materialization
|
|
5367
|
+
);
|
|
5368
|
+
}
|
|
5369
|
+
/** Run the remediation convergence loop; returns iteration count and accumulated materialization. */
|
|
5370
|
+
async runRemediationLoop(options, driftResult, gapReport, remediations) {
|
|
5371
|
+
const maxIterations = options.maxIterations ?? 5;
|
|
5372
|
+
let iterations = 1;
|
|
5373
|
+
let currentDrift = driftResult;
|
|
5374
|
+
let currentGapReport = gapReport;
|
|
5375
|
+
let previousIssueCount = currentDrift.findings.length + currentGapReport.totalGaps;
|
|
5376
|
+
let accumulatedMaterialization;
|
|
5377
|
+
while (iterations < maxIterations) {
|
|
5378
|
+
if (currentDrift.findings.length === 0 && currentGapReport.totalGaps === 0) break;
|
|
5379
|
+
const matResult = await this.remediate(currentDrift, remediations, options, currentGapReport);
|
|
5380
|
+
if (matResult) {
|
|
5381
|
+
if (!accumulatedMaterialization) {
|
|
5382
|
+
accumulatedMaterialization = matResult;
|
|
5383
|
+
} else {
|
|
5384
|
+
accumulatedMaterialization = {
|
|
5385
|
+
created: [...accumulatedMaterialization.created, ...matResult.created],
|
|
5386
|
+
skipped: [...accumulatedMaterialization.skipped, ...matResult.skipped]
|
|
5387
|
+
};
|
|
5388
|
+
}
|
|
5389
|
+
}
|
|
5390
|
+
const preSnapshot = this.buildSnapshot(options.domain);
|
|
5391
|
+
await this.extract(options);
|
|
5392
|
+
const postSnapshot = this.buildSnapshot(options.domain);
|
|
5393
|
+
currentDrift = this.reconcile(preSnapshot, postSnapshot);
|
|
5394
|
+
currentGapReport = await this.detect(options);
|
|
5395
|
+
iterations++;
|
|
5396
|
+
const currentIssueCount = currentDrift.findings.length + currentGapReport.totalGaps;
|
|
5397
|
+
if (currentIssueCount >= previousIssueCount) break;
|
|
5398
|
+
previousIssueCount = currentIssueCount;
|
|
5399
|
+
}
|
|
5400
|
+
return {
|
|
5401
|
+
iterations,
|
|
5402
|
+
...accumulatedMaterialization ? { materialization: accumulatedMaterialization } : {}
|
|
5403
|
+
};
|
|
5404
|
+
}
|
|
5405
|
+
/** Assemble the final pipeline result. */
|
|
5406
|
+
buildResult(driftResult, iterations, extraction, gaps, remediations, contradictions, coverage, materialization) {
|
|
4844
5407
|
return {
|
|
4845
5408
|
verdict: this.computeVerdict(driftResult),
|
|
4846
5409
|
driftScore: driftResult.driftScore,
|
|
4847
5410
|
iterations,
|
|
4848
5411
|
findings: driftResult.summary,
|
|
4849
5412
|
extraction,
|
|
4850
|
-
gaps
|
|
5413
|
+
gaps,
|
|
4851
5414
|
remediations,
|
|
4852
5415
|
contradictions,
|
|
4853
|
-
coverage
|
|
5416
|
+
coverage,
|
|
5417
|
+
...materialization ? { materialization } : {}
|
|
4854
5418
|
};
|
|
4855
5419
|
}
|
|
4856
5420
|
// ── Phase 1: EXTRACT ──────────────────────────────────────────────────────
|
|
4857
5421
|
async extract(options) {
|
|
4858
|
-
const extractedDir =
|
|
4859
|
-
await
|
|
5422
|
+
const extractedDir = path12.join(options.projectDir, ".harness", "knowledge", "extracted");
|
|
5423
|
+
await fs11.mkdir(extractedDir, { recursive: true });
|
|
4860
5424
|
const runner = createExtractionRunner();
|
|
4861
5425
|
const extractionResult = await runner.run(options.projectDir, this.store, extractedDir);
|
|
4862
5426
|
const diagramParser = new DiagramParser(this.store);
|
|
@@ -4870,7 +5434,7 @@ var KnowledgePipelineRunner = class {
|
|
|
4870
5434
|
const imageResult = await imageExtractor.analyze(this.store, imagePaths);
|
|
4871
5435
|
imageCount = imageResult.nodesAdded;
|
|
4872
5436
|
}
|
|
4873
|
-
const knowledgeDir =
|
|
5437
|
+
const knowledgeDir = path12.join(options.projectDir, "docs", "knowledge");
|
|
4874
5438
|
const bkIngestor = new BusinessKnowledgeIngestor(this.store);
|
|
4875
5439
|
let bkResult;
|
|
4876
5440
|
try {
|
|
@@ -4885,6 +5449,21 @@ var KnowledgePipelineRunner = class {
|
|
|
4885
5449
|
durationMs: 0
|
|
4886
5450
|
};
|
|
4887
5451
|
}
|
|
5452
|
+
const decisionsDir = path12.join(options.projectDir, "docs", "knowledge", "decisions");
|
|
5453
|
+
const decisionIngestor = new DecisionIngestor(this.store);
|
|
5454
|
+
let decisionResult;
|
|
5455
|
+
try {
|
|
5456
|
+
decisionResult = await decisionIngestor.ingest(decisionsDir);
|
|
5457
|
+
} catch {
|
|
5458
|
+
decisionResult = {
|
|
5459
|
+
nodesAdded: 0,
|
|
5460
|
+
nodesUpdated: 0,
|
|
5461
|
+
edgesAdded: 0,
|
|
5462
|
+
edgesUpdated: 0,
|
|
5463
|
+
errors: [],
|
|
5464
|
+
durationMs: 0
|
|
5465
|
+
};
|
|
5466
|
+
}
|
|
4888
5467
|
const linker = new KnowledgeLinker(this.store, extractedDir);
|
|
4889
5468
|
const linkResult = await linker.link();
|
|
4890
5469
|
return {
|
|
@@ -4892,6 +5471,7 @@ var KnowledgePipelineRunner = class {
|
|
|
4892
5471
|
diagrams: diagramResult.nodesAdded,
|
|
4893
5472
|
linkerFacts: linkResult.factsCreated,
|
|
4894
5473
|
businessKnowledge: bkResult.nodesAdded,
|
|
5474
|
+
decisions: decisionResult.nodesAdded,
|
|
4895
5475
|
images: imageCount
|
|
4896
5476
|
};
|
|
4897
5477
|
}
|
|
@@ -4918,14 +5498,14 @@ var KnowledgePipelineRunner = class {
|
|
|
4918
5498
|
}
|
|
4919
5499
|
// ── Phase 3: DETECT ───────────────────────────────────────────────────────
|
|
4920
5500
|
async detect(options) {
|
|
4921
|
-
const knowledgeDir =
|
|
5501
|
+
const knowledgeDir = path12.join(options.projectDir, "docs", "knowledge");
|
|
4922
5502
|
const aggregator = new KnowledgeStagingAggregator(options.projectDir);
|
|
4923
|
-
const gapReport = await aggregator.generateGapReport(knowledgeDir);
|
|
5503
|
+
const gapReport = await aggregator.generateGapReport(knowledgeDir, this.store);
|
|
4924
5504
|
await aggregator.writeGapReport(gapReport);
|
|
4925
5505
|
return gapReport;
|
|
4926
5506
|
}
|
|
4927
5507
|
// ── Phase 4: REMEDIATE ────────────────────────────────────────────────────
|
|
4928
|
-
remediate(driftResult, remediations, options) {
|
|
5508
|
+
async remediate(driftResult, remediations, options, gapReport) {
|
|
4929
5509
|
for (const finding of driftResult.findings) {
|
|
4930
5510
|
switch (finding.classification) {
|
|
4931
5511
|
case "stale":
|
|
@@ -4943,6 +5523,22 @@ var KnowledgePipelineRunner = class {
|
|
|
4943
5523
|
break;
|
|
4944
5524
|
}
|
|
4945
5525
|
}
|
|
5526
|
+
if (!options.ci) {
|
|
5527
|
+
const allGapEntries = gapReport.domains.flatMap((d) => d.gapEntries);
|
|
5528
|
+
const materializable = allGapEntries.filter((e) => e.hasContent);
|
|
5529
|
+
if (materializable.length > 0) {
|
|
5530
|
+
const materializer = new KnowledgeDocMaterializer(this.store);
|
|
5531
|
+
const matResult = await materializer.materialize(materializable, {
|
|
5532
|
+
projectDir: options.projectDir,
|
|
5533
|
+
dryRun: false
|
|
5534
|
+
});
|
|
5535
|
+
for (const doc of matResult.created) {
|
|
5536
|
+
remediations.push(`created doc: ${doc.filePath}`);
|
|
5537
|
+
}
|
|
5538
|
+
return matResult;
|
|
5539
|
+
}
|
|
5540
|
+
}
|
|
5541
|
+
return void 0;
|
|
4946
5542
|
}
|
|
4947
5543
|
async stageNewFindings(driftResult, options) {
|
|
4948
5544
|
const newFindings = driftResult.findings.filter((f) => f.classification === "new");
|
|
@@ -4977,7 +5573,7 @@ var KnowledgePipelineRunner = class {
|
|
|
4977
5573
|
};
|
|
4978
5574
|
|
|
4979
5575
|
// src/ingest/connectors/ConnectorUtils.ts
|
|
4980
|
-
var
|
|
5576
|
+
var CODE_NODE_TYPES5 = ["file", "function", "class", "method", "interface", "variable"];
|
|
4981
5577
|
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
4982
5578
|
function withRetry(client, options) {
|
|
4983
5579
|
const maxRetries = options?.maxRetries ?? 3;
|
|
@@ -5042,7 +5638,7 @@ function sanitizeExternalText(text, maxLength = 2e3) {
|
|
|
5042
5638
|
}
|
|
5043
5639
|
function linkToCode(store, content, sourceNodeId, edgeType, options) {
|
|
5044
5640
|
let edgesCreated = 0;
|
|
5045
|
-
for (const type of
|
|
5641
|
+
for (const type of CODE_NODE_TYPES5) {
|
|
5046
5642
|
const nodes = store.findNodes({ type });
|
|
5047
5643
|
for (const node of nodes) {
|
|
5048
5644
|
if (node.name.length < 3) continue;
|
|
@@ -5062,12 +5658,12 @@ function linkToCode(store, content, sourceNodeId, edgeType, options) {
|
|
|
5062
5658
|
}
|
|
5063
5659
|
|
|
5064
5660
|
// src/ingest/connectors/SyncManager.ts
|
|
5065
|
-
var
|
|
5066
|
-
var
|
|
5661
|
+
var fs12 = __toESM(require("fs/promises"));
|
|
5662
|
+
var path13 = __toESM(require("path"));
|
|
5067
5663
|
var SyncManager = class {
|
|
5068
5664
|
constructor(store, graphDir) {
|
|
5069
5665
|
this.store = store;
|
|
5070
|
-
this.metadataPath =
|
|
5666
|
+
this.metadataPath = path13.join(graphDir, "sync-metadata.json");
|
|
5071
5667
|
}
|
|
5072
5668
|
store;
|
|
5073
5669
|
registrations = /* @__PURE__ */ new Map();
|
|
@@ -5132,15 +5728,15 @@ var SyncManager = class {
|
|
|
5132
5728
|
}
|
|
5133
5729
|
async loadMetadata() {
|
|
5134
5730
|
try {
|
|
5135
|
-
const raw = await
|
|
5731
|
+
const raw = await fs12.readFile(this.metadataPath, "utf-8");
|
|
5136
5732
|
return JSON.parse(raw);
|
|
5137
5733
|
} catch {
|
|
5138
5734
|
return { connectors: {} };
|
|
5139
5735
|
}
|
|
5140
5736
|
}
|
|
5141
5737
|
async saveMetadata(metadata) {
|
|
5142
|
-
await
|
|
5143
|
-
await
|
|
5738
|
+
await fs12.mkdir(path13.dirname(this.metadataPath), { recursive: true });
|
|
5739
|
+
await fs12.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
5144
5740
|
}
|
|
5145
5741
|
};
|
|
5146
5742
|
|
|
@@ -5245,6 +5841,14 @@ var JiraConnector = class {
|
|
|
5245
5841
|
start
|
|
5246
5842
|
);
|
|
5247
5843
|
}
|
|
5844
|
+
try {
|
|
5845
|
+
const parsed = new URL(baseUrl);
|
|
5846
|
+
if (parsed.protocol !== "https:" && parsed.hostname !== "localhost") {
|
|
5847
|
+
return buildIngestResult(0, 0, [`${baseUrlEnv} must use HTTPS`], start);
|
|
5848
|
+
}
|
|
5849
|
+
} catch {
|
|
5850
|
+
return buildIngestResult(0, 0, [`${baseUrlEnv} is not a valid URL`], start);
|
|
5851
|
+
}
|
|
5248
5852
|
const jql = buildJql(config);
|
|
5249
5853
|
const headers = { Authorization: `Basic ${apiKey}`, "Content-Type": "application/json" };
|
|
5250
5854
|
try {
|
|
@@ -5534,6 +6138,14 @@ var ConfluenceConnector = class {
|
|
|
5534
6138
|
}
|
|
5535
6139
|
const baseUrlEnv = config.baseUrlEnv ?? "CONFLUENCE_BASE_URL";
|
|
5536
6140
|
const baseUrl = process.env[baseUrlEnv] ?? "";
|
|
6141
|
+
try {
|
|
6142
|
+
const parsed = new URL(baseUrl);
|
|
6143
|
+
if (parsed.protocol !== "https:" && parsed.hostname !== "localhost") {
|
|
6144
|
+
return missingApiKeyResult(`${baseUrlEnv} must use HTTPS`, start);
|
|
6145
|
+
}
|
|
6146
|
+
} catch {
|
|
6147
|
+
return missingApiKeyResult(`${baseUrlEnv} is not a valid URL`, start);
|
|
6148
|
+
}
|
|
5537
6149
|
const spaceKey = config.spaceKey ?? "";
|
|
5538
6150
|
const counts = await this.fetchAllPagesHandled(
|
|
5539
6151
|
store,
|
|
@@ -5627,7 +6239,7 @@ var ConfluenceConnector = class {
|
|
|
5627
6239
|
};
|
|
5628
6240
|
|
|
5629
6241
|
// src/ingest/connectors/CIConnector.ts
|
|
5630
|
-
function
|
|
6242
|
+
function emptyResult5(errors, start) {
|
|
5631
6243
|
return {
|
|
5632
6244
|
nodesAdded: 0,
|
|
5633
6245
|
nodesUpdated: 0,
|
|
@@ -5695,7 +6307,7 @@ var CIConnector = class {
|
|
|
5695
6307
|
const apiKeyEnv = config.apiKeyEnv ?? "GITHUB_TOKEN";
|
|
5696
6308
|
const apiKey = process.env[apiKeyEnv];
|
|
5697
6309
|
if (!apiKey) {
|
|
5698
|
-
return
|
|
6310
|
+
return emptyResult5(
|
|
5699
6311
|
[`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
5700
6312
|
start
|
|
5701
6313
|
);
|
|
@@ -5759,6 +6371,14 @@ function hasConstraintKeyword(text) {
|
|
|
5759
6371
|
const lower = text.toLowerCase();
|
|
5760
6372
|
return CONSTRAINT_KEYWORDS.some((kw) => lower.includes(kw));
|
|
5761
6373
|
}
|
|
6374
|
+
function buildNodeMetadata(condensed, base) {
|
|
6375
|
+
const metadata = { ...base };
|
|
6376
|
+
if (condensed.method !== "passthrough") {
|
|
6377
|
+
metadata.condensed = condensed.method;
|
|
6378
|
+
metadata.originalLength = condensed.originalLength;
|
|
6379
|
+
}
|
|
6380
|
+
return metadata;
|
|
6381
|
+
}
|
|
5762
6382
|
function buildIngestResult2(nodesAdded, edgesAdded, errors, start) {
|
|
5763
6383
|
return {
|
|
5764
6384
|
nodesAdded,
|
|
@@ -5769,6 +6389,24 @@ function buildIngestResult2(nodesAdded, edgesAdded, errors, start) {
|
|
|
5769
6389
|
durationMs: Date.now() - start
|
|
5770
6390
|
};
|
|
5771
6391
|
}
|
|
6392
|
+
function validateFigmaConfig(config) {
|
|
6393
|
+
const apiKeyEnv = config.apiKeyEnv ?? "FIGMA_API_KEY";
|
|
6394
|
+
const apiKey = process.env[apiKeyEnv];
|
|
6395
|
+
if (!apiKey) return { error: `Missing API key: environment variable "${apiKeyEnv}" is not set` };
|
|
6396
|
+
const baseUrlEnv = config.baseUrlEnv ?? "FIGMA_BASE_URL";
|
|
6397
|
+
const baseUrl = process.env[baseUrlEnv] ?? "https://api.figma.com";
|
|
6398
|
+
try {
|
|
6399
|
+
const parsed = new URL(baseUrl);
|
|
6400
|
+
if (parsed.protocol !== "https:" || !parsed.hostname.endsWith("api.figma.com")) {
|
|
6401
|
+
return { error: `Invalid ${baseUrlEnv}: must be an HTTPS URL on api.figma.com` };
|
|
6402
|
+
}
|
|
6403
|
+
} catch {
|
|
6404
|
+
return { error: `Invalid ${baseUrlEnv}: not a valid URL` };
|
|
6405
|
+
}
|
|
6406
|
+
const fileIds = config.fileIds;
|
|
6407
|
+
if (!fileIds || fileIds.length === 0) return { error: "No fileIds provided in connector config" };
|
|
6408
|
+
return { apiKey, baseUrl, fileIds };
|
|
6409
|
+
}
|
|
5772
6410
|
var FigmaConnector = class {
|
|
5773
6411
|
name = "figma";
|
|
5774
6412
|
source = "figma";
|
|
@@ -5778,35 +6416,9 @@ var FigmaConnector = class {
|
|
|
5778
6416
|
}
|
|
5779
6417
|
async ingest(store, config) {
|
|
5780
6418
|
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
|
-
}
|
|
6419
|
+
const validated = validateFigmaConfig(config);
|
|
6420
|
+
if ("error" in validated) return buildIngestResult2(0, 0, [validated.error], start);
|
|
6421
|
+
const { apiKey, baseUrl, fileIds } = validated;
|
|
5810
6422
|
const headers = { "X-FIGMA-TOKEN": apiKey };
|
|
5811
6423
|
const maxLen = config.maxContentLength ?? 4e3;
|
|
5812
6424
|
let nodesAdded = 0;
|
|
@@ -5826,6 +6438,17 @@ var FigmaConnector = class {
|
|
|
5826
6438
|
return buildIngestResult2(nodesAdded, edgesAdded, errors, start);
|
|
5827
6439
|
}
|
|
5828
6440
|
async processFile(store, baseUrl, fileId, headers, maxLen) {
|
|
6441
|
+
let nodesAdded = 0;
|
|
6442
|
+
let edgesAdded = 0;
|
|
6443
|
+
const styleCounts = await this.ingestStyles(store, baseUrl, fileId, headers, maxLen);
|
|
6444
|
+
nodesAdded += styleCounts.nodesAdded;
|
|
6445
|
+
edgesAdded += styleCounts.edgesAdded;
|
|
6446
|
+
const componentCounts = await this.ingestComponents(store, baseUrl, fileId, headers, maxLen);
|
|
6447
|
+
nodesAdded += componentCounts.nodesAdded;
|
|
6448
|
+
edgesAdded += componentCounts.edgesAdded;
|
|
6449
|
+
return { nodesAdded, edgesAdded };
|
|
6450
|
+
}
|
|
6451
|
+
async ingestStyles(store, baseUrl, fileId, headers, maxLen) {
|
|
5829
6452
|
let nodesAdded = 0;
|
|
5830
6453
|
let edgesAdded = 0;
|
|
5831
6454
|
const stylesUrl = `${baseUrl}/v1/files/${fileId}/styles`;
|
|
@@ -5833,90 +6456,112 @@ var FigmaConnector = class {
|
|
|
5833
6456
|
if (!stylesResponse.ok) throw new Error(`Styles request failed for file ${fileId}`);
|
|
5834
6457
|
const stylesData = await stylesResponse.json();
|
|
5835
6458
|
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");
|
|
6459
|
+
const counts = await this.processStyle(store, style, fileId, maxLen);
|
|
6460
|
+
nodesAdded += counts.nodesAdded;
|
|
6461
|
+
edgesAdded += counts.edgesAdded;
|
|
5860
6462
|
}
|
|
6463
|
+
return { nodesAdded, edgesAdded };
|
|
6464
|
+
}
|
|
6465
|
+
async processStyle(store, style, fileId, maxLen) {
|
|
6466
|
+
const nodeId = `figma:token:${style.key}`;
|
|
6467
|
+
const condensed = await condenseContent(sanitizeExternalText(style.description || "", 2e3), {
|
|
6468
|
+
maxLength: maxLen
|
|
6469
|
+
});
|
|
6470
|
+
const metadata = buildNodeMetadata(condensed, {
|
|
6471
|
+
source: "figma",
|
|
6472
|
+
key: style.key,
|
|
6473
|
+
styleType: style.style_type,
|
|
6474
|
+
fileId
|
|
6475
|
+
});
|
|
6476
|
+
store.addNode({
|
|
6477
|
+
id: nodeId,
|
|
6478
|
+
type: "design_token",
|
|
6479
|
+
name: sanitizeExternalText(style.name, 500),
|
|
6480
|
+
content: condensed.content,
|
|
6481
|
+
metadata
|
|
6482
|
+
});
|
|
6483
|
+
const searchText = sanitizeExternalText([style.name, style.description].join(" "));
|
|
6484
|
+
const edgesAdded = linkToCode(store, searchText, nodeId, "references");
|
|
6485
|
+
return { nodesAdded: 1, edgesAdded };
|
|
6486
|
+
}
|
|
6487
|
+
async ingestComponents(store, baseUrl, fileId, headers, maxLen) {
|
|
6488
|
+
let nodesAdded = 0;
|
|
6489
|
+
let edgesAdded = 0;
|
|
5861
6490
|
const componentsUrl = `${baseUrl}/v1/files/${fileId}/components`;
|
|
5862
6491
|
const componentsResponse = await this.httpClient(componentsUrl, { headers });
|
|
5863
6492
|
if (!componentsResponse.ok) throw new Error(`Components request failed for file ${fileId}`);
|
|
5864
6493
|
const componentsData = await componentsResponse.json();
|
|
5865
6494
|
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
|
-
}
|
|
6495
|
+
const counts = await this.processComponent(store, component, fileId, maxLen);
|
|
6496
|
+
nodesAdded += counts.nodesAdded;
|
|
6497
|
+
edgesAdded += counts.edgesAdded;
|
|
5917
6498
|
}
|
|
5918
6499
|
return { nodesAdded, edgesAdded };
|
|
5919
6500
|
}
|
|
6501
|
+
async processComponent(store, component, fileId, maxLen) {
|
|
6502
|
+
let nodesAdded = 0;
|
|
6503
|
+
let edgesAdded = 0;
|
|
6504
|
+
const description = component.description || "";
|
|
6505
|
+
if (description) {
|
|
6506
|
+
const counts = await this.addComponentIntent(store, component, description, fileId, maxLen);
|
|
6507
|
+
nodesAdded += counts.nodesAdded;
|
|
6508
|
+
edgesAdded += counts.edgesAdded;
|
|
6509
|
+
}
|
|
6510
|
+
if (description && hasConstraintKeyword(description)) {
|
|
6511
|
+
const counts = await this.addComponentConstraint(
|
|
6512
|
+
store,
|
|
6513
|
+
component,
|
|
6514
|
+
description,
|
|
6515
|
+
fileId,
|
|
6516
|
+
maxLen
|
|
6517
|
+
);
|
|
6518
|
+
nodesAdded += counts.nodesAdded;
|
|
6519
|
+
edgesAdded += counts.edgesAdded;
|
|
6520
|
+
}
|
|
6521
|
+
return { nodesAdded, edgesAdded };
|
|
6522
|
+
}
|
|
6523
|
+
async addComponentIntent(store, component, description, fileId, maxLen) {
|
|
6524
|
+
const intentId = `figma:intent:${component.key}`;
|
|
6525
|
+
const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
|
|
6526
|
+
maxLength: maxLen
|
|
6527
|
+
});
|
|
6528
|
+
const metadata = buildNodeMetadata(condensed, {
|
|
6529
|
+
source: "figma",
|
|
6530
|
+
key: component.key,
|
|
6531
|
+
fileId
|
|
6532
|
+
});
|
|
6533
|
+
store.addNode({
|
|
6534
|
+
id: intentId,
|
|
6535
|
+
type: "aesthetic_intent",
|
|
6536
|
+
name: sanitizeExternalText(component.name, 500),
|
|
6537
|
+
content: condensed.content,
|
|
6538
|
+
metadata
|
|
6539
|
+
});
|
|
6540
|
+
const searchText = sanitizeExternalText([component.name, description].join(" "));
|
|
6541
|
+
const edgesAdded = linkToCode(store, searchText, intentId, "references");
|
|
6542
|
+
return { nodesAdded: 1, edgesAdded };
|
|
6543
|
+
}
|
|
6544
|
+
async addComponentConstraint(store, component, description, fileId, maxLen) {
|
|
6545
|
+
const constraintId = `figma:constraint:${component.key}`;
|
|
6546
|
+
const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
|
|
6547
|
+
maxLength: maxLen
|
|
6548
|
+
});
|
|
6549
|
+
const metadata = buildNodeMetadata(condensed, {
|
|
6550
|
+
source: "figma",
|
|
6551
|
+
key: component.key,
|
|
6552
|
+
fileId
|
|
6553
|
+
});
|
|
6554
|
+
store.addNode({
|
|
6555
|
+
id: constraintId,
|
|
6556
|
+
type: "design_constraint",
|
|
6557
|
+
name: sanitizeExternalText(component.name, 500),
|
|
6558
|
+
content: condensed.content,
|
|
6559
|
+
metadata
|
|
6560
|
+
});
|
|
6561
|
+
const searchText = sanitizeExternalText([component.name, description].join(" "));
|
|
6562
|
+
const edgesAdded = linkToCode(store, searchText, constraintId, "references");
|
|
6563
|
+
return { nodesAdded: 1, edgesAdded };
|
|
6564
|
+
}
|
|
5920
6565
|
};
|
|
5921
6566
|
|
|
5922
6567
|
// src/ingest/connectors/MiroConnector.ts
|
|
@@ -5935,6 +6580,46 @@ function buildIngestResult3(nodesAdded, edgesAdded, errors, start) {
|
|
|
5935
6580
|
durationMs: Date.now() - start
|
|
5936
6581
|
};
|
|
5937
6582
|
}
|
|
6583
|
+
function validateBaseUrl(baseUrl, baseUrlEnv) {
|
|
6584
|
+
try {
|
|
6585
|
+
const parsed = new URL(baseUrl);
|
|
6586
|
+
if (parsed.protocol !== "https:" || parsed.hostname !== "api.miro.com") {
|
|
6587
|
+
return `Invalid ${baseUrlEnv}: must be an HTTPS URL on api.miro.com`;
|
|
6588
|
+
}
|
|
6589
|
+
} catch {
|
|
6590
|
+
return `Invalid ${baseUrlEnv}: not a valid URL`;
|
|
6591
|
+
}
|
|
6592
|
+
return null;
|
|
6593
|
+
}
|
|
6594
|
+
function resolveConfig(config, start) {
|
|
6595
|
+
const apiKeyEnv = config.apiKeyEnv ?? "MIRO_API_KEY";
|
|
6596
|
+
const apiKey = process.env[apiKeyEnv];
|
|
6597
|
+
if (!apiKey) {
|
|
6598
|
+
return {
|
|
6599
|
+
ok: false,
|
|
6600
|
+
result: buildIngestResult3(
|
|
6601
|
+
0,
|
|
6602
|
+
0,
|
|
6603
|
+
[`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
6604
|
+
start
|
|
6605
|
+
)
|
|
6606
|
+
};
|
|
6607
|
+
}
|
|
6608
|
+
const baseUrlEnv = config.baseUrlEnv ?? "MIRO_BASE_URL";
|
|
6609
|
+
const baseUrl = process.env[baseUrlEnv] ?? "https://api.miro.com";
|
|
6610
|
+
const urlError = validateBaseUrl(baseUrl, baseUrlEnv);
|
|
6611
|
+
if (urlError) {
|
|
6612
|
+
return { ok: false, result: buildIngestResult3(0, 0, [urlError], start) };
|
|
6613
|
+
}
|
|
6614
|
+
const boardIds = config.boardIds;
|
|
6615
|
+
if (!boardIds || boardIds.length === 0) {
|
|
6616
|
+
return {
|
|
6617
|
+
ok: false,
|
|
6618
|
+
result: buildIngestResult3(0, 0, ["No boardIds provided in config"], start)
|
|
6619
|
+
};
|
|
6620
|
+
}
|
|
6621
|
+
return { ok: true, apiKey, baseUrl, boardIds, headers: { Authorization: `Bearer ${apiKey}` } };
|
|
6622
|
+
}
|
|
5938
6623
|
var MiroConnector = class {
|
|
5939
6624
|
name = "miro";
|
|
5940
6625
|
source = "miro";
|
|
@@ -5944,36 +6629,9 @@ var MiroConnector = class {
|
|
|
5944
6629
|
}
|
|
5945
6630
|
async ingest(store, config) {
|
|
5946
6631
|
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}` };
|
|
6632
|
+
const resolved = resolveConfig(config, start);
|
|
6633
|
+
if (!resolved.ok) return resolved.result;
|
|
6634
|
+
const { baseUrl, boardIds, headers } = resolved;
|
|
5977
6635
|
let totalNodesAdded = 0;
|
|
5978
6636
|
let totalEdgesAdded = 0;
|
|
5979
6637
|
const errors = [];
|
|
@@ -6177,7 +6835,7 @@ var FusionLayer = class {
|
|
|
6177
6835
|
};
|
|
6178
6836
|
|
|
6179
6837
|
// src/entropy/GraphEntropyAdapter.ts
|
|
6180
|
-
var
|
|
6838
|
+
var CODE_NODE_TYPES6 = ["file", "function", "class", "method", "interface", "variable"];
|
|
6181
6839
|
var GraphEntropyAdapter = class {
|
|
6182
6840
|
constructor(store) {
|
|
6183
6841
|
this.store = store;
|
|
@@ -6257,7 +6915,7 @@ var GraphEntropyAdapter = class {
|
|
|
6257
6915
|
}
|
|
6258
6916
|
findEntryPoints() {
|
|
6259
6917
|
const entryPoints = [];
|
|
6260
|
-
for (const nodeType of
|
|
6918
|
+
for (const nodeType of CODE_NODE_TYPES6) {
|
|
6261
6919
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
6262
6920
|
for (const node of nodes) {
|
|
6263
6921
|
const isIndexFile = nodeType === "file" && node.name === "index.ts";
|
|
@@ -6293,7 +6951,7 @@ var GraphEntropyAdapter = class {
|
|
|
6293
6951
|
}
|
|
6294
6952
|
collectUnreachableNodes(visited) {
|
|
6295
6953
|
const unreachableNodes = [];
|
|
6296
|
-
for (const nodeType of
|
|
6954
|
+
for (const nodeType of CODE_NODE_TYPES6) {
|
|
6297
6955
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
6298
6956
|
for (const node of nodes) {
|
|
6299
6957
|
if (!visited.has(node.id)) {
|
|
@@ -7139,9 +7797,9 @@ var EntityExtractor = class {
|
|
|
7139
7797
|
extractPaths(trimmed, add) {
|
|
7140
7798
|
const consumed = /* @__PURE__ */ new Set();
|
|
7141
7799
|
for (const match of trimmed.matchAll(FILE_PATH_RE)) {
|
|
7142
|
-
const
|
|
7143
|
-
add(
|
|
7144
|
-
consumed.add(
|
|
7800
|
+
const path15 = match[0];
|
|
7801
|
+
add(path15);
|
|
7802
|
+
consumed.add(path15);
|
|
7145
7803
|
}
|
|
7146
7804
|
return consumed;
|
|
7147
7805
|
}
|
|
@@ -7213,8 +7871,8 @@ var EntityResolver = class {
|
|
|
7213
7871
|
if (isPathLike && node.path.includes(raw)) {
|
|
7214
7872
|
return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
|
|
7215
7873
|
}
|
|
7216
|
-
const
|
|
7217
|
-
if (
|
|
7874
|
+
const basename10 = node.path.split("/").pop() ?? "";
|
|
7875
|
+
if (basename10.includes(raw)) {
|
|
7218
7876
|
return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
|
|
7219
7877
|
}
|
|
7220
7878
|
if (raw.length >= 4 && node.path.includes(raw)) {
|
|
@@ -7293,13 +7951,13 @@ var ResponseFormatter = class {
|
|
|
7293
7951
|
const context = Array.isArray(d?.context) ? d.context : [];
|
|
7294
7952
|
const firstEntity = entities[0];
|
|
7295
7953
|
const nodeType = firstEntity?.node.type ?? "node";
|
|
7296
|
-
const
|
|
7954
|
+
const path15 = firstEntity?.node.path ?? "unknown";
|
|
7297
7955
|
let neighborCount = 0;
|
|
7298
7956
|
const firstContext = context[0];
|
|
7299
7957
|
if (firstContext && Array.isArray(firstContext.nodes)) {
|
|
7300
7958
|
neighborCount = firstContext.nodes.length;
|
|
7301
7959
|
}
|
|
7302
|
-
return `**${entityName}** is a ${nodeType} at \`${
|
|
7960
|
+
return `**${entityName}** is a ${nodeType} at \`${path15}\`. Connected to ${neighborCount} nodes.`;
|
|
7303
7961
|
}
|
|
7304
7962
|
formatAnomaly(data) {
|
|
7305
7963
|
const d = data;
|
|
@@ -7682,7 +8340,7 @@ var PHASE_NODE_TYPES = {
|
|
|
7682
8340
|
debug: ["failure", "learning", "function", "method"],
|
|
7683
8341
|
plan: ["adr", "document", "module", "layer"]
|
|
7684
8342
|
};
|
|
7685
|
-
var
|
|
8343
|
+
var CODE_NODE_TYPES7 = /* @__PURE__ */ new Set([
|
|
7686
8344
|
"file",
|
|
7687
8345
|
"function",
|
|
7688
8346
|
"class",
|
|
@@ -7901,7 +8559,7 @@ var Assembler = class {
|
|
|
7901
8559
|
*/
|
|
7902
8560
|
checkCoverage() {
|
|
7903
8561
|
const codeNodes = [];
|
|
7904
|
-
for (const type of
|
|
8562
|
+
for (const type of CODE_NODE_TYPES7) {
|
|
7905
8563
|
codeNodes.push(...this.store.findNodes({ type }));
|
|
7906
8564
|
}
|
|
7907
8565
|
const documented = [];
|
|
@@ -8081,14 +8739,14 @@ var GraphConstraintAdapter = class {
|
|
|
8081
8739
|
};
|
|
8082
8740
|
|
|
8083
8741
|
// src/ingest/DesignIngestor.ts
|
|
8084
|
-
var
|
|
8085
|
-
var
|
|
8742
|
+
var fs13 = __toESM(require("fs/promises"));
|
|
8743
|
+
var path14 = __toESM(require("path"));
|
|
8086
8744
|
function isDTCGToken(obj) {
|
|
8087
8745
|
return typeof obj === "object" && obj !== null && "$value" in obj && "$type" in obj;
|
|
8088
8746
|
}
|
|
8089
8747
|
async function readFileOrNull(filePath) {
|
|
8090
8748
|
try {
|
|
8091
|
-
return await
|
|
8749
|
+
return await fs13.readFile(filePath, "utf-8");
|
|
8092
8750
|
} catch {
|
|
8093
8751
|
return null;
|
|
8094
8752
|
}
|
|
@@ -8234,8 +8892,8 @@ var DesignIngestor = class {
|
|
|
8234
8892
|
async ingestAll(designDir) {
|
|
8235
8893
|
const start = Date.now();
|
|
8236
8894
|
const [tokensResult, intentResult] = await Promise.all([
|
|
8237
|
-
this.ingestTokens(
|
|
8238
|
-
this.ingestDesignIntent(
|
|
8895
|
+
this.ingestTokens(path14.join(designDir, "tokens.json")),
|
|
8896
|
+
this.ingestDesignIntent(path14.join(designDir, "DESIGN.md"))
|
|
8239
8897
|
]);
|
|
8240
8898
|
const merged = mergeResults(tokensResult, intentResult);
|
|
8241
8899
|
return { ...merged, durationMs: Date.now() - start };
|
|
@@ -8471,10 +9129,10 @@ var TaskIndependenceAnalyzer = class {
|
|
|
8471
9129
|
includeTypes: ["file"]
|
|
8472
9130
|
});
|
|
8473
9131
|
for (const n of queryResult.nodes) {
|
|
8474
|
-
const
|
|
8475
|
-
if (!fileSet.has(
|
|
8476
|
-
if (!result.has(
|
|
8477
|
-
result.set(
|
|
9132
|
+
const path15 = n.path ?? n.id.replace(/^file:/, "");
|
|
9133
|
+
if (!fileSet.has(path15)) {
|
|
9134
|
+
if (!result.has(path15)) {
|
|
9135
|
+
result.set(path15, file);
|
|
8478
9136
|
}
|
|
8479
9137
|
}
|
|
8480
9138
|
}
|
|
@@ -8851,7 +9509,7 @@ var ConflictPredictor = class {
|
|
|
8851
9509
|
};
|
|
8852
9510
|
|
|
8853
9511
|
// src/index.ts
|
|
8854
|
-
var VERSION = "0.
|
|
9512
|
+
var VERSION = "0.6.0";
|
|
8855
9513
|
// Annotate the CommonJS export names for ESM import in node:
|
|
8856
9514
|
0 && (module.exports = {
|
|
8857
9515
|
ALL_EXTRACTORS,
|
|
@@ -8869,6 +9527,7 @@ var VERSION = "0.4.3";
|
|
|
8869
9527
|
ContradictionDetector,
|
|
8870
9528
|
CoverageScorer,
|
|
8871
9529
|
D2Parser,
|
|
9530
|
+
DecisionIngestor,
|
|
8872
9531
|
DesignConstraintAdapter,
|
|
8873
9532
|
DesignIngestor,
|
|
8874
9533
|
DiagramParser,
|
|
@@ -8893,6 +9552,7 @@ var VERSION = "0.4.3";
|
|
|
8893
9552
|
ImageAnalysisExtractor,
|
|
8894
9553
|
IntentClassifier,
|
|
8895
9554
|
JiraConnector,
|
|
9555
|
+
KnowledgeDocMaterializer,
|
|
8896
9556
|
KnowledgeIngestor,
|
|
8897
9557
|
KnowledgePipelineRunner,
|
|
8898
9558
|
KnowledgeStagingAggregator,
|