@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.mjs
CHANGED
|
@@ -24,7 +24,7 @@ var NODE_TYPES = [
|
|
|
24
24
|
"build",
|
|
25
25
|
"test_result",
|
|
26
26
|
"execution_outcome",
|
|
27
|
-
// Observability
|
|
27
|
+
// Observability — reserved for future tracing/metrics integration
|
|
28
28
|
"span",
|
|
29
29
|
"metric",
|
|
30
30
|
"log",
|
|
@@ -71,7 +71,7 @@ var EDGE_TYPES = [
|
|
|
71
71
|
"triggered_by",
|
|
72
72
|
"failed_in",
|
|
73
73
|
"outcome_of",
|
|
74
|
-
// Execution relationships
|
|
74
|
+
// Execution relationships — reserved for future observability integration
|
|
75
75
|
"executed_by",
|
|
76
76
|
"measured_by",
|
|
77
77
|
// Design relationships
|
|
@@ -146,7 +146,7 @@ function streamGraphJson(filePath, nodes, edges) {
|
|
|
146
146
|
stream.write(JSON.stringify(edges[i]));
|
|
147
147
|
}
|
|
148
148
|
stream.write("]}");
|
|
149
|
-
stream.end(resolve);
|
|
149
|
+
stream.end(() => resolve());
|
|
150
150
|
});
|
|
151
151
|
}
|
|
152
152
|
async function saveGraph(dirPath, nodes, edges) {
|
|
@@ -169,15 +169,19 @@ async function loadGraph(dirPath) {
|
|
|
169
169
|
await access(metaPath);
|
|
170
170
|
await access(graphPath);
|
|
171
171
|
} catch {
|
|
172
|
-
return
|
|
172
|
+
return { status: "not_found" };
|
|
173
173
|
}
|
|
174
174
|
const metaContent = await readFile(metaPath, "utf-8");
|
|
175
175
|
const metadata = JSON.parse(metaContent);
|
|
176
176
|
if (metadata.schemaVersion !== CURRENT_SCHEMA_VERSION) {
|
|
177
|
-
return
|
|
177
|
+
return {
|
|
178
|
+
status: "schema_mismatch",
|
|
179
|
+
found: metadata.schemaVersion,
|
|
180
|
+
expected: CURRENT_SCHEMA_VERSION
|
|
181
|
+
};
|
|
178
182
|
}
|
|
179
183
|
const graphContent = await readFile(graphPath, "utf-8");
|
|
180
|
-
return JSON.parse(graphContent);
|
|
184
|
+
return { status: "loaded", graph: JSON.parse(graphContent) };
|
|
181
185
|
}
|
|
182
186
|
|
|
183
187
|
// src/store/GraphStore.ts
|
|
@@ -352,8 +356,15 @@ var GraphStore = class {
|
|
|
352
356
|
await saveGraph(dirPath, allNodes, allEdges);
|
|
353
357
|
}
|
|
354
358
|
async load(dirPath) {
|
|
355
|
-
const
|
|
356
|
-
if (
|
|
359
|
+
const result = await loadGraph(dirPath);
|
|
360
|
+
if (result.status === "not_found") return false;
|
|
361
|
+
if (result.status === "schema_mismatch") {
|
|
362
|
+
console.warn(
|
|
363
|
+
`[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.`
|
|
364
|
+
);
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
const data = result.graph;
|
|
357
368
|
this.clear();
|
|
358
369
|
for (const node of data.nodes) {
|
|
359
370
|
this.nodeMap.set(node.id, { ...node });
|
|
@@ -797,9 +808,10 @@ var CodeIngestor = class {
|
|
|
797
808
|
let edgesAdded = 0;
|
|
798
809
|
const files = await this.findSourceFiles(rootDir);
|
|
799
810
|
const nameToFiles = /* @__PURE__ */ new Map();
|
|
811
|
+
const contentCache = /* @__PURE__ */ new Map();
|
|
800
812
|
for (const filePath of files) {
|
|
801
813
|
try {
|
|
802
|
-
const result = await this.processFile(filePath, rootDir, nameToFiles);
|
|
814
|
+
const result = await this.processFile(filePath, rootDir, nameToFiles, contentCache);
|
|
803
815
|
nodesAdded += result.nodesAdded;
|
|
804
816
|
edgesAdded += result.edgesAdded;
|
|
805
817
|
} catch (err) {
|
|
@@ -809,7 +821,8 @@ var CodeIngestor = class {
|
|
|
809
821
|
for (const filePath of files) {
|
|
810
822
|
try {
|
|
811
823
|
const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
812
|
-
const content =
|
|
824
|
+
const content = contentCache.get(filePath);
|
|
825
|
+
if (content === void 0) continue;
|
|
813
826
|
const callsEdges = this.extractCallsEdgesForFile(relativePath, content, nameToFiles);
|
|
814
827
|
for (const edge of callsEdges) {
|
|
815
828
|
this.store.addEdge(edge);
|
|
@@ -820,7 +833,8 @@ var CodeIngestor = class {
|
|
|
820
833
|
}
|
|
821
834
|
for (const filePath of files) {
|
|
822
835
|
try {
|
|
823
|
-
const content =
|
|
836
|
+
const content = contentCache.get(filePath);
|
|
837
|
+
if (content === void 0) continue;
|
|
824
838
|
edgesAdded += this.extractReqAnnotationsForFile(filePath, content, rootDir);
|
|
825
839
|
} catch {
|
|
826
840
|
}
|
|
@@ -834,11 +848,12 @@ var CodeIngestor = class {
|
|
|
834
848
|
durationMs: Date.now() - start
|
|
835
849
|
};
|
|
836
850
|
}
|
|
837
|
-
async processFile(filePath, rootDir, nameToFiles) {
|
|
851
|
+
async processFile(filePath, rootDir, nameToFiles, contentCache) {
|
|
838
852
|
let nodesAdded = 0;
|
|
839
853
|
let edgesAdded = 0;
|
|
840
854
|
const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
841
855
|
const content = await fs.readFile(filePath, "utf-8");
|
|
856
|
+
contentCache.set(filePath, content);
|
|
842
857
|
const stat2 = await fs.stat(filePath);
|
|
843
858
|
const fileId = `file:${relativePath}`;
|
|
844
859
|
const fileNode = {
|
|
@@ -2016,9 +2031,119 @@ function parseFrontmatter(raw) {
|
|
|
2016
2031
|
};
|
|
2017
2032
|
}
|
|
2018
2033
|
|
|
2019
|
-
// src/ingest/
|
|
2034
|
+
// src/ingest/DecisionIngestor.ts
|
|
2020
2035
|
import * as fs4 from "fs/promises";
|
|
2021
2036
|
import * as path5 from "path";
|
|
2037
|
+
var CODE_NODE_TYPES3 = [
|
|
2038
|
+
"file",
|
|
2039
|
+
"function",
|
|
2040
|
+
"class",
|
|
2041
|
+
"method",
|
|
2042
|
+
"interface",
|
|
2043
|
+
"variable"
|
|
2044
|
+
];
|
|
2045
|
+
var DecisionIngestor = class {
|
|
2046
|
+
constructor(store) {
|
|
2047
|
+
this.store = store;
|
|
2048
|
+
}
|
|
2049
|
+
store;
|
|
2050
|
+
async ingest(decisionsDir) {
|
|
2051
|
+
const start = Date.now();
|
|
2052
|
+
const errors = [];
|
|
2053
|
+
let files;
|
|
2054
|
+
try {
|
|
2055
|
+
files = await this.findDecisionFiles(decisionsDir);
|
|
2056
|
+
} catch {
|
|
2057
|
+
return emptyResult(Date.now() - start);
|
|
2058
|
+
}
|
|
2059
|
+
let nodesAdded = 0;
|
|
2060
|
+
let edgesAdded = 0;
|
|
2061
|
+
for (const filePath of files) {
|
|
2062
|
+
try {
|
|
2063
|
+
const raw = await fs4.readFile(filePath, "utf-8");
|
|
2064
|
+
const parsed = this.parseFrontmatter(raw);
|
|
2065
|
+
if (!parsed) continue;
|
|
2066
|
+
const { frontmatter, body } = parsed;
|
|
2067
|
+
if (!frontmatter.number || !frontmatter.title) continue;
|
|
2068
|
+
const filename = path5.basename(filePath, ".md");
|
|
2069
|
+
const nodeId = `decision:${filename}`;
|
|
2070
|
+
const node = {
|
|
2071
|
+
id: nodeId,
|
|
2072
|
+
type: "decision",
|
|
2073
|
+
name: frontmatter.title,
|
|
2074
|
+
path: filePath,
|
|
2075
|
+
content: body.trim(),
|
|
2076
|
+
metadata: {
|
|
2077
|
+
number: frontmatter.number,
|
|
2078
|
+
...frontmatter.date && { date: frontmatter.date },
|
|
2079
|
+
...frontmatter.status && { status: frontmatter.status },
|
|
2080
|
+
...frontmatter.tier && { tier: frontmatter.tier },
|
|
2081
|
+
...frontmatter.source && { source: frontmatter.source },
|
|
2082
|
+
...frontmatter.supersedes && { supersedes: frontmatter.supersedes }
|
|
2083
|
+
}
|
|
2084
|
+
};
|
|
2085
|
+
this.store.addNode(node);
|
|
2086
|
+
nodesAdded++;
|
|
2087
|
+
edgesAdded += this.linkToCode(body, nodeId);
|
|
2088
|
+
} catch (err) {
|
|
2089
|
+
errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
return {
|
|
2093
|
+
nodesAdded,
|
|
2094
|
+
nodesUpdated: 0,
|
|
2095
|
+
edgesAdded,
|
|
2096
|
+
edgesUpdated: 0,
|
|
2097
|
+
errors,
|
|
2098
|
+
durationMs: Date.now() - start
|
|
2099
|
+
};
|
|
2100
|
+
}
|
|
2101
|
+
parseFrontmatter(raw) {
|
|
2102
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
2103
|
+
if (!match) return null;
|
|
2104
|
+
const yamlBlock = match[1];
|
|
2105
|
+
const body = match[2];
|
|
2106
|
+
const frontmatter = {};
|
|
2107
|
+
for (const line of yamlBlock.split("\n")) {
|
|
2108
|
+
const kvMatch = line.match(/^(\w+):\s*(.+)$/);
|
|
2109
|
+
if (!kvMatch) continue;
|
|
2110
|
+
frontmatter[kvMatch[1]] = kvMatch[2].trim();
|
|
2111
|
+
}
|
|
2112
|
+
if (!frontmatter.number || !frontmatter.title) return null;
|
|
2113
|
+
return {
|
|
2114
|
+
frontmatter,
|
|
2115
|
+
body
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
linkToCode(content, sourceNodeId) {
|
|
2119
|
+
let count = 0;
|
|
2120
|
+
for (const nodeType of CODE_NODE_TYPES3) {
|
|
2121
|
+
const codeNodes = this.store.findNodes({ type: nodeType });
|
|
2122
|
+
for (const node of codeNodes) {
|
|
2123
|
+
if (node.name.length < 3) continue;
|
|
2124
|
+
const escaped = node.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2125
|
+
const namePattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
2126
|
+
if (namePattern.test(content)) {
|
|
2127
|
+
this.store.addEdge({
|
|
2128
|
+
from: sourceNodeId,
|
|
2129
|
+
to: node.id,
|
|
2130
|
+
type: "decided"
|
|
2131
|
+
});
|
|
2132
|
+
count++;
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
return count;
|
|
2137
|
+
}
|
|
2138
|
+
async findDecisionFiles(dir) {
|
|
2139
|
+
const entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
2140
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".md") && e.name !== "README.md").map((e) => path5.join(dir, e.name));
|
|
2141
|
+
}
|
|
2142
|
+
};
|
|
2143
|
+
|
|
2144
|
+
// src/ingest/RequirementIngestor.ts
|
|
2145
|
+
import * as fs5 from "fs/promises";
|
|
2146
|
+
import * as path6 from "path";
|
|
2022
2147
|
var REQUIREMENT_SECTIONS = [
|
|
2023
2148
|
"Observable Truths",
|
|
2024
2149
|
"Success Criteria",
|
|
@@ -2035,7 +2160,7 @@ function detectEarsPattern(text) {
|
|
|
2035
2160
|
if (/^the\s+\w+\s+shall\b/.test(lower)) return "ubiquitous";
|
|
2036
2161
|
return void 0;
|
|
2037
2162
|
}
|
|
2038
|
-
var
|
|
2163
|
+
var CODE_NODE_TYPES4 = ["file", "function", "class", "method", "interface", "variable"];
|
|
2039
2164
|
var RequirementIngestor = class {
|
|
2040
2165
|
constructor(store) {
|
|
2041
2166
|
this.store = store;
|
|
@@ -2053,8 +2178,8 @@ var RequirementIngestor = class {
|
|
|
2053
2178
|
let edgesAdded = 0;
|
|
2054
2179
|
let featureDirs;
|
|
2055
2180
|
try {
|
|
2056
|
-
const entries = await
|
|
2057
|
-
featureDirs = entries.filter((e) => e.isDirectory()).map((e) =>
|
|
2181
|
+
const entries = await fs5.readdir(specsDir, { withFileTypes: true });
|
|
2182
|
+
featureDirs = entries.filter((e) => e.isDirectory()).map((e) => path6.join(specsDir, e.name));
|
|
2058
2183
|
} catch {
|
|
2059
2184
|
return emptyResult(Date.now() - start);
|
|
2060
2185
|
}
|
|
@@ -2073,11 +2198,11 @@ var RequirementIngestor = class {
|
|
|
2073
2198
|
};
|
|
2074
2199
|
}
|
|
2075
2200
|
async ingestFeatureDir(featureDir, errors) {
|
|
2076
|
-
const featureName =
|
|
2077
|
-
const specPath =
|
|
2201
|
+
const featureName = path6.basename(featureDir);
|
|
2202
|
+
const specPath = path6.join(featureDir, "proposal.md").replaceAll("\\", "/");
|
|
2078
2203
|
let content;
|
|
2079
2204
|
try {
|
|
2080
|
-
content = await
|
|
2205
|
+
content = await fs5.readFile(specPath, "utf-8");
|
|
2081
2206
|
} catch {
|
|
2082
2207
|
return { nodesAdded: 0, edgesAdded: 0 };
|
|
2083
2208
|
}
|
|
@@ -2094,7 +2219,7 @@ var RequirementIngestor = class {
|
|
|
2094
2219
|
this.store.addNode({
|
|
2095
2220
|
id: specNodeId,
|
|
2096
2221
|
type: "document",
|
|
2097
|
-
name:
|
|
2222
|
+
name: path6.basename(specPath),
|
|
2098
2223
|
path: specPath,
|
|
2099
2224
|
metadata: { featureName }
|
|
2100
2225
|
});
|
|
@@ -2209,9 +2334,9 @@ var RequirementIngestor = class {
|
|
|
2209
2334
|
for (const node of fileNodes) {
|
|
2210
2335
|
if (!node.path) continue;
|
|
2211
2336
|
const normalizedPath = node.path.replace(/\\/g, "/");
|
|
2212
|
-
const isCodeMatch = normalizedPath.includes("packages/") &&
|
|
2337
|
+
const isCodeMatch = normalizedPath.includes("packages/") && path6.basename(normalizedPath).includes(featureName);
|
|
2213
2338
|
const isTestMatch = normalizedPath.includes("/tests/") && // platform-safe
|
|
2214
|
-
|
|
2339
|
+
path6.basename(normalizedPath).includes(featureName);
|
|
2215
2340
|
if (isCodeMatch && !isTestMatch) {
|
|
2216
2341
|
this.store.addEdge({
|
|
2217
2342
|
from: reqId,
|
|
@@ -2240,7 +2365,7 @@ var RequirementIngestor = class {
|
|
|
2240
2365
|
*/
|
|
2241
2366
|
linkByKeywordOverlap(reqId, reqText) {
|
|
2242
2367
|
let count = 0;
|
|
2243
|
-
for (const nodeType of
|
|
2368
|
+
for (const nodeType of CODE_NODE_TYPES4) {
|
|
2244
2369
|
const codeNodes = this.store.findNodes({ type: nodeType });
|
|
2245
2370
|
for (const node of codeNodes) {
|
|
2246
2371
|
if (node.name.length < 3) continue;
|
|
@@ -2264,19 +2389,158 @@ var RequirementIngestor = class {
|
|
|
2264
2389
|
};
|
|
2265
2390
|
|
|
2266
2391
|
// src/ingest/KnowledgePipelineRunner.ts
|
|
2267
|
-
import * as
|
|
2268
|
-
import * as
|
|
2392
|
+
import * as fs11 from "fs/promises";
|
|
2393
|
+
import * as path12 from "path";
|
|
2269
2394
|
|
|
2270
2395
|
// src/ingest/DiagramParser.ts
|
|
2271
|
-
import * as
|
|
2272
|
-
import * as
|
|
2273
|
-
|
|
2396
|
+
import * as fs6 from "fs/promises";
|
|
2397
|
+
import * as path7 from "path";
|
|
2398
|
+
|
|
2399
|
+
// src/ingest/parsers/mermaid.ts
|
|
2400
|
+
function emptyResult2(diagramType = "unknown") {
|
|
2274
2401
|
return {
|
|
2275
2402
|
entities: [],
|
|
2276
2403
|
relationships: [],
|
|
2277
2404
|
metadata: { format: "mermaid", diagramType }
|
|
2278
2405
|
};
|
|
2279
2406
|
}
|
|
2407
|
+
function detectDiagramType(content) {
|
|
2408
|
+
for (const line of content.split("\n")) {
|
|
2409
|
+
const trimmed = line.trim();
|
|
2410
|
+
if (!trimmed) continue;
|
|
2411
|
+
if (/^(?:graph|flowchart)\b/i.test(trimmed)) return "flowchart";
|
|
2412
|
+
if (/^sequenceDiagram\b/.test(trimmed)) return "sequence";
|
|
2413
|
+
if (/^classDiagram\b/.test(trimmed)) return "class";
|
|
2414
|
+
if (/^erDiagram\b/.test(trimmed)) return "er";
|
|
2415
|
+
break;
|
|
2416
|
+
}
|
|
2417
|
+
return "unknown";
|
|
2418
|
+
}
|
|
2419
|
+
function isDecisionNode(content, nodeId) {
|
|
2420
|
+
const decisionRegex = new RegExp(`${nodeId}\\s*\\{`);
|
|
2421
|
+
return decisionRegex.test(content);
|
|
2422
|
+
}
|
|
2423
|
+
function buildFlowchartEntity(content, match) {
|
|
2424
|
+
const id = match[1] ?? "";
|
|
2425
|
+
const label = (match[2] ?? "").trim();
|
|
2426
|
+
if (!id) return null;
|
|
2427
|
+
const decision = isDecisionNode(content, id);
|
|
2428
|
+
const entity = {
|
|
2429
|
+
id,
|
|
2430
|
+
label,
|
|
2431
|
+
...decision ? { type: "decision" } : {}
|
|
2432
|
+
};
|
|
2433
|
+
return { id, entity };
|
|
2434
|
+
}
|
|
2435
|
+
function extractFlowchartNodes(content) {
|
|
2436
|
+
const entities = /* @__PURE__ */ new Map();
|
|
2437
|
+
const nodeRegex = /([A-Za-z0-9_]+)\s*[[({]([^\])}]+)[\])}]/g;
|
|
2438
|
+
let match = nodeRegex.exec(content);
|
|
2439
|
+
while (match !== null) {
|
|
2440
|
+
const parsed = buildFlowchartEntity(content, match);
|
|
2441
|
+
if (parsed && !entities.has(parsed.id)) {
|
|
2442
|
+
entities.set(parsed.id, parsed.entity);
|
|
2443
|
+
}
|
|
2444
|
+
match = nodeRegex.exec(content);
|
|
2445
|
+
}
|
|
2446
|
+
return entities;
|
|
2447
|
+
}
|
|
2448
|
+
function parseLabeledEdgeMatch(match) {
|
|
2449
|
+
const from = match[1] ?? "";
|
|
2450
|
+
const label = (match[2] ?? "").trim();
|
|
2451
|
+
const to = match[3] ?? "";
|
|
2452
|
+
if (!from || !to) return null;
|
|
2453
|
+
return { key: `${from}->${to}:${label}`, rel: { from, to, label } };
|
|
2454
|
+
}
|
|
2455
|
+
function extractLabeledEdges(stripped, edgeKeys) {
|
|
2456
|
+
const relationships = [];
|
|
2457
|
+
const labeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\|([^|]+)\|\s*([A-Za-z0-9_]+)/g;
|
|
2458
|
+
let match = labeledEdgeRegex.exec(stripped);
|
|
2459
|
+
while (match !== null) {
|
|
2460
|
+
const parsed = parseLabeledEdgeMatch(match);
|
|
2461
|
+
if (parsed && !edgeKeys.has(parsed.key)) {
|
|
2462
|
+
edgeKeys.add(parsed.key);
|
|
2463
|
+
relationships.push(parsed.rel);
|
|
2464
|
+
}
|
|
2465
|
+
match = labeledEdgeRegex.exec(stripped);
|
|
2466
|
+
}
|
|
2467
|
+
return relationships;
|
|
2468
|
+
}
|
|
2469
|
+
function hasLabeledEdgeBetween(labeledEdges, from, to) {
|
|
2470
|
+
return labeledEdges.some((r) => r.from === from && r.to === to);
|
|
2471
|
+
}
|
|
2472
|
+
function extractUnlabeledEdges(stripped, edgeKeys, labeledEdges) {
|
|
2473
|
+
const relationships = [];
|
|
2474
|
+
const unlabeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\s+([A-Za-z0-9_]+)/g;
|
|
2475
|
+
let match = unlabeledEdgeRegex.exec(stripped);
|
|
2476
|
+
while (match !== null) {
|
|
2477
|
+
const from = match[1] ?? "";
|
|
2478
|
+
const to = match[2] ?? "";
|
|
2479
|
+
const key = `${from}->${to}`;
|
|
2480
|
+
if (from && to && !hasLabeledEdgeBetween(labeledEdges, from, to) && !edgeKeys.has(key)) {
|
|
2481
|
+
edgeKeys.add(key);
|
|
2482
|
+
relationships.push({ from, to });
|
|
2483
|
+
}
|
|
2484
|
+
match = unlabeledEdgeRegex.exec(stripped);
|
|
2485
|
+
}
|
|
2486
|
+
return relationships;
|
|
2487
|
+
}
|
|
2488
|
+
function parseFlowchart(content, diagramType) {
|
|
2489
|
+
const entities = extractFlowchartNodes(content);
|
|
2490
|
+
const stripped = content.replace(/[[({][^\])}]*[\])}]/g, "");
|
|
2491
|
+
const edgeKeys = /* @__PURE__ */ new Set();
|
|
2492
|
+
const labeledEdges = extractLabeledEdges(stripped, edgeKeys);
|
|
2493
|
+
const unlabeledEdges = extractUnlabeledEdges(stripped, edgeKeys, labeledEdges);
|
|
2494
|
+
return {
|
|
2495
|
+
entities: Array.from(entities.values()),
|
|
2496
|
+
relationships: [...labeledEdges, ...unlabeledEdges],
|
|
2497
|
+
metadata: { format: "mermaid", diagramType }
|
|
2498
|
+
};
|
|
2499
|
+
}
|
|
2500
|
+
function extractParticipants(content) {
|
|
2501
|
+
const entities = [];
|
|
2502
|
+
const seenParticipants = /* @__PURE__ */ new Set();
|
|
2503
|
+
const participantRegex = /participant\s+(\w+)(?:\s+as\s+(.+))?/g;
|
|
2504
|
+
let match = participantRegex.exec(content);
|
|
2505
|
+
while (match !== null) {
|
|
2506
|
+
const id = match[1] ?? "";
|
|
2507
|
+
const label = match[2]?.trim() || id;
|
|
2508
|
+
if (id && !seenParticipants.has(id)) {
|
|
2509
|
+
seenParticipants.add(id);
|
|
2510
|
+
entities.push({ id, label });
|
|
2511
|
+
}
|
|
2512
|
+
match = participantRegex.exec(content);
|
|
2513
|
+
}
|
|
2514
|
+
return entities;
|
|
2515
|
+
}
|
|
2516
|
+
function relationshipFromMatch(match) {
|
|
2517
|
+
const from = match[1] ?? "";
|
|
2518
|
+
const to = match[2] ?? "";
|
|
2519
|
+
const label = (match[3] ?? "").trim();
|
|
2520
|
+
return from && to ? { from, to, label } : null;
|
|
2521
|
+
}
|
|
2522
|
+
function collectMessageMatches(content, regex) {
|
|
2523
|
+
const results = [];
|
|
2524
|
+
let match = regex.exec(content);
|
|
2525
|
+
while (match !== null) {
|
|
2526
|
+
const rel = relationshipFromMatch(match);
|
|
2527
|
+
if (rel) results.push(rel);
|
|
2528
|
+
match = regex.exec(content);
|
|
2529
|
+
}
|
|
2530
|
+
return results;
|
|
2531
|
+
}
|
|
2532
|
+
function extractMessages(content) {
|
|
2533
|
+
const forward = collectMessageMatches(content, /(\w+)\s*->>?\+?\s*(\w+)\s*:\s*(.+)/g);
|
|
2534
|
+
const returns = collectMessageMatches(content, /(\w+)\s*-->>?-?\s*(\w+)\s*:\s*(.+)/g);
|
|
2535
|
+
return [...forward, ...returns];
|
|
2536
|
+
}
|
|
2537
|
+
function parseSequence(content, diagramType) {
|
|
2538
|
+
return {
|
|
2539
|
+
entities: extractParticipants(content),
|
|
2540
|
+
relationships: extractMessages(content),
|
|
2541
|
+
metadata: { format: "mermaid", diagramType }
|
|
2542
|
+
};
|
|
2543
|
+
}
|
|
2280
2544
|
var MermaidParser = class {
|
|
2281
2545
|
canParse(_content, ext) {
|
|
2282
2546
|
return ext === ".mmd" || ext === ".mermaid";
|
|
@@ -2284,269 +2548,199 @@ var MermaidParser = class {
|
|
|
2284
2548
|
parse(content, _filePath) {
|
|
2285
2549
|
const trimmed = content.trim();
|
|
2286
2550
|
if (!trimmed) {
|
|
2287
|
-
return
|
|
2551
|
+
return emptyResult2();
|
|
2288
2552
|
}
|
|
2289
|
-
const diagramType =
|
|
2553
|
+
const diagramType = detectDiagramType(trimmed);
|
|
2290
2554
|
switch (diagramType) {
|
|
2291
2555
|
case "flowchart":
|
|
2292
|
-
return
|
|
2556
|
+
return parseFlowchart(trimmed, diagramType);
|
|
2293
2557
|
case "sequence":
|
|
2294
|
-
return
|
|
2558
|
+
return parseSequence(trimmed, diagramType);
|
|
2295
2559
|
default:
|
|
2296
|
-
return
|
|
2560
|
+
return emptyResult2(diagramType);
|
|
2297
2561
|
}
|
|
2298
2562
|
}
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2563
|
+
};
|
|
2564
|
+
|
|
2565
|
+
// src/ingest/parsers/d2.ts
|
|
2566
|
+
function emptyResult3() {
|
|
2567
|
+
return {
|
|
2568
|
+
entities: [],
|
|
2569
|
+
relationships: [],
|
|
2570
|
+
metadata: { format: "d2", diagramType: "architecture" }
|
|
2571
|
+
};
|
|
2572
|
+
}
|
|
2573
|
+
function parseBlockShape(stripped) {
|
|
2574
|
+
const match = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+?)\s*\{$/);
|
|
2575
|
+
if (match) {
|
|
2576
|
+
const id = match[1];
|
|
2577
|
+
const label = match[2];
|
|
2578
|
+
if (id && label) return { id, label };
|
|
2579
|
+
}
|
|
2580
|
+
return null;
|
|
2581
|
+
}
|
|
2582
|
+
function parseConnection(stripped) {
|
|
2583
|
+
const match = stripped.match(/^([a-zA-Z0-9_.-]+)\s*->\s*([a-zA-Z0-9_.-]+)(?:\s*:\s*(.+?))?$/);
|
|
2584
|
+
if (match) {
|
|
2585
|
+
const from = match[1] ?? "";
|
|
2586
|
+
const to = match[2] ?? "";
|
|
2587
|
+
const label = match[3]?.trim();
|
|
2588
|
+
if (from && to) return { from, to, ...label ? { label } : {} };
|
|
2589
|
+
}
|
|
2590
|
+
return null;
|
|
2591
|
+
}
|
|
2592
|
+
function parseSimpleShape(stripped) {
|
|
2593
|
+
const match = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+)$/);
|
|
2594
|
+
if (match) {
|
|
2595
|
+
const id = match[1];
|
|
2596
|
+
const label = (match[2] ?? "").trim();
|
|
2597
|
+
if (id && label) return { id, label };
|
|
2598
|
+
}
|
|
2599
|
+
return null;
|
|
2600
|
+
}
|
|
2601
|
+
function handleBlockOpen(stripped, state) {
|
|
2602
|
+
if (state.braceDepth === 0) {
|
|
2603
|
+
const shape = parseBlockShape(stripped);
|
|
2604
|
+
if (shape) state.entities.set(shape.id, { id: shape.id, label: shape.label });
|
|
2311
2605
|
}
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
const id = match[1] ?? "";
|
|
2320
|
-
const label = (match[2] ?? "").trim();
|
|
2321
|
-
if (id && !entities.has(id)) {
|
|
2322
|
-
const isDecision = this.isDecisionNode(content, id);
|
|
2323
|
-
entities.set(id, {
|
|
2324
|
-
id,
|
|
2325
|
-
label,
|
|
2326
|
-
...isDecision ? { type: "decision" } : {}
|
|
2327
|
-
});
|
|
2328
|
-
}
|
|
2329
|
-
match = nodeRegex.exec(content);
|
|
2330
|
-
}
|
|
2331
|
-
const stripped = content.replace(/[[({][^\])}]*[\])}]/g, "");
|
|
2332
|
-
const labeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\|([^|]+)\|\s*([A-Za-z0-9_]+)/g;
|
|
2333
|
-
const unlabeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\s+([A-Za-z0-9_]+)/g;
|
|
2334
|
-
const edgeKeys = /* @__PURE__ */ new Set();
|
|
2335
|
-
match = labeledEdgeRegex.exec(stripped);
|
|
2336
|
-
while (match !== null) {
|
|
2337
|
-
const from = match[1] ?? "";
|
|
2338
|
-
const label = (match[2] ?? "").trim();
|
|
2339
|
-
const to = match[3] ?? "";
|
|
2340
|
-
if (from && to) {
|
|
2341
|
-
const key = `${from}->${to}:${label}`;
|
|
2342
|
-
if (!edgeKeys.has(key)) {
|
|
2343
|
-
edgeKeys.add(key);
|
|
2344
|
-
relationships.push({ from, to, label });
|
|
2345
|
-
}
|
|
2346
|
-
}
|
|
2347
|
-
match = labeledEdgeRegex.exec(stripped);
|
|
2348
|
-
}
|
|
2349
|
-
match = unlabeledEdgeRegex.exec(stripped);
|
|
2350
|
-
while (match !== null) {
|
|
2351
|
-
const from = match[1] ?? "";
|
|
2352
|
-
const to = match[2] ?? "";
|
|
2353
|
-
if (from && to) {
|
|
2354
|
-
const hasLabeled = relationships.some((r) => r.from === from && r.to === to);
|
|
2355
|
-
if (!hasLabeled) {
|
|
2356
|
-
const key = `${from}->${to}`;
|
|
2357
|
-
if (!edgeKeys.has(key)) {
|
|
2358
|
-
edgeKeys.add(key);
|
|
2359
|
-
relationships.push({ from, to });
|
|
2360
|
-
}
|
|
2361
|
-
}
|
|
2362
|
-
}
|
|
2363
|
-
match = unlabeledEdgeRegex.exec(stripped);
|
|
2364
|
-
}
|
|
2365
|
-
return {
|
|
2366
|
-
entities: Array.from(entities.values()),
|
|
2367
|
-
relationships,
|
|
2368
|
-
metadata: { format: "mermaid", diagramType }
|
|
2369
|
-
};
|
|
2606
|
+
state.braceDepth++;
|
|
2607
|
+
}
|
|
2608
|
+
function processTopLevelLine(stripped, state) {
|
|
2609
|
+
const conn = parseConnection(stripped);
|
|
2610
|
+
if (conn) {
|
|
2611
|
+
state.relationships.push(conn);
|
|
2612
|
+
return;
|
|
2370
2613
|
}
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2614
|
+
const shape = parseSimpleShape(stripped);
|
|
2615
|
+
if (shape && !state.entities.has(shape.id)) {
|
|
2616
|
+
state.entities.set(shape.id, { id: shape.id, label: shape.label });
|
|
2374
2617
|
}
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
match = participantRegex.exec(content);
|
|
2382
|
-
while (match !== null) {
|
|
2383
|
-
const id = match[1] ?? "";
|
|
2384
|
-
const label = match[2]?.trim() || id;
|
|
2385
|
-
if (id && !seenParticipants.has(id)) {
|
|
2386
|
-
seenParticipants.add(id);
|
|
2387
|
-
entities.push({ id, label });
|
|
2388
|
-
}
|
|
2389
|
-
match = participantRegex.exec(content);
|
|
2390
|
-
}
|
|
2391
|
-
const forwardMsgRegex = /(\w+)\s*->>?\+?\s*(\w+)\s*:\s*(.+)/g;
|
|
2392
|
-
match = forwardMsgRegex.exec(content);
|
|
2393
|
-
while (match !== null) {
|
|
2394
|
-
const from = match[1] ?? "";
|
|
2395
|
-
const to = match[2] ?? "";
|
|
2396
|
-
const label = (match[3] ?? "").trim();
|
|
2397
|
-
if (from && to) {
|
|
2398
|
-
relationships.push({ from, to, label });
|
|
2399
|
-
}
|
|
2400
|
-
match = forwardMsgRegex.exec(content);
|
|
2401
|
-
}
|
|
2402
|
-
const returnMsgRegex = /(\w+)\s*-->>?-?\s*(\w+)\s*:\s*(.+)/g;
|
|
2403
|
-
match = returnMsgRegex.exec(content);
|
|
2404
|
-
while (match !== null) {
|
|
2405
|
-
const from = match[1] ?? "";
|
|
2406
|
-
const to = match[2] ?? "";
|
|
2407
|
-
const label = (match[3] ?? "").trim();
|
|
2408
|
-
if (from && to) {
|
|
2409
|
-
relationships.push({ from, to, label });
|
|
2410
|
-
}
|
|
2411
|
-
match = returnMsgRegex.exec(content);
|
|
2412
|
-
}
|
|
2413
|
-
return {
|
|
2414
|
-
entities,
|
|
2415
|
-
relationships,
|
|
2416
|
-
metadata: { format: "mermaid", diagramType }
|
|
2417
|
-
};
|
|
2618
|
+
}
|
|
2619
|
+
function processD2Line(stripped, state) {
|
|
2620
|
+
if (!stripped || stripped.startsWith("#")) return;
|
|
2621
|
+
if (stripped.endsWith("{")) {
|
|
2622
|
+
handleBlockOpen(stripped, state);
|
|
2623
|
+
return;
|
|
2418
2624
|
}
|
|
2419
|
-
}
|
|
2625
|
+
if (stripped === "}") {
|
|
2626
|
+
state.braceDepth = Math.max(0, state.braceDepth - 1);
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
if (state.braceDepth > 0) return;
|
|
2630
|
+
processTopLevelLine(stripped, state);
|
|
2631
|
+
}
|
|
2420
2632
|
var D2Parser = class {
|
|
2421
2633
|
canParse(_content, ext) {
|
|
2422
2634
|
return ext === ".d2";
|
|
2423
2635
|
}
|
|
2424
2636
|
parse(content, _filePath) {
|
|
2425
2637
|
const trimmed = content.trim();
|
|
2426
|
-
if (!trimmed)
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
}
|
|
2433
|
-
const entities = /* @__PURE__ */ new Map();
|
|
2434
|
-
const relationships = [];
|
|
2435
|
-
let braceDepth = 0;
|
|
2638
|
+
if (!trimmed) return emptyResult3();
|
|
2639
|
+
const state = {
|
|
2640
|
+
entities: /* @__PURE__ */ new Map(),
|
|
2641
|
+
relationships: [],
|
|
2642
|
+
braceDepth: 0
|
|
2643
|
+
};
|
|
2436
2644
|
for (const line of trimmed.split("\n")) {
|
|
2437
|
-
|
|
2438
|
-
if (!stripped || stripped.startsWith("#")) continue;
|
|
2439
|
-
if (stripped.endsWith("{")) {
|
|
2440
|
-
if (braceDepth === 0) {
|
|
2441
|
-
const shapeMatch = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+?)\s*\{$/);
|
|
2442
|
-
if (shapeMatch) {
|
|
2443
|
-
const id = shapeMatch[1];
|
|
2444
|
-
const label = shapeMatch[2];
|
|
2445
|
-
if (id && label) {
|
|
2446
|
-
entities.set(id, { id, label });
|
|
2447
|
-
}
|
|
2448
|
-
}
|
|
2449
|
-
}
|
|
2450
|
-
braceDepth++;
|
|
2451
|
-
continue;
|
|
2452
|
-
}
|
|
2453
|
-
if (stripped === "}") {
|
|
2454
|
-
braceDepth = Math.max(0, braceDepth - 1);
|
|
2455
|
-
continue;
|
|
2456
|
-
}
|
|
2457
|
-
if (braceDepth > 0) continue;
|
|
2458
|
-
const connMatch = stripped.match(
|
|
2459
|
-
/^([a-zA-Z0-9_.-]+)\s*->\s*([a-zA-Z0-9_.-]+)(?:\s*:\s*(.+?))?$/
|
|
2460
|
-
);
|
|
2461
|
-
if (connMatch) {
|
|
2462
|
-
const from = connMatch[1] ?? "";
|
|
2463
|
-
const to = connMatch[2] ?? "";
|
|
2464
|
-
const label = connMatch[3]?.trim();
|
|
2465
|
-
if (from && to) {
|
|
2466
|
-
relationships.push({ from, to, ...label ? { label } : {} });
|
|
2467
|
-
}
|
|
2468
|
-
continue;
|
|
2469
|
-
}
|
|
2470
|
-
const simpleMatch = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+)$/);
|
|
2471
|
-
if (simpleMatch) {
|
|
2472
|
-
const id = simpleMatch[1];
|
|
2473
|
-
const label = (simpleMatch[2] ?? "").trim();
|
|
2474
|
-
if (id && label && !entities.has(id)) {
|
|
2475
|
-
entities.set(id, { id, label });
|
|
2476
|
-
}
|
|
2477
|
-
}
|
|
2645
|
+
processD2Line(line.trim(), state);
|
|
2478
2646
|
}
|
|
2479
2647
|
return {
|
|
2480
|
-
entities: Array.from(entities.values()),
|
|
2481
|
-
relationships,
|
|
2648
|
+
entities: Array.from(state.entities.values()),
|
|
2649
|
+
relationships: state.relationships,
|
|
2482
2650
|
metadata: { format: "d2", diagramType: "architecture" }
|
|
2483
2651
|
};
|
|
2484
2652
|
}
|
|
2485
2653
|
};
|
|
2654
|
+
|
|
2655
|
+
// src/ingest/parsers/plantuml.ts
|
|
2656
|
+
function emptyResult4() {
|
|
2657
|
+
return {
|
|
2658
|
+
entities: [],
|
|
2659
|
+
relationships: [],
|
|
2660
|
+
metadata: { format: "plantuml", diagramType: "unknown" }
|
|
2661
|
+
};
|
|
2662
|
+
}
|
|
2663
|
+
function detectDiagramType2(content) {
|
|
2664
|
+
if (/class\s+\w+/.test(content)) return "class";
|
|
2665
|
+
if (/\[.+\]/.test(content) || /component\s+/.test(content)) return "component";
|
|
2666
|
+
if (/participant\s+/.test(content) || /actor\s+/.test(content)) return "sequence";
|
|
2667
|
+
return "unknown";
|
|
2668
|
+
}
|
|
2669
|
+
function stripWrappers(content) {
|
|
2670
|
+
return content.replace(/@startuml\b.*\n?/, "").replace(/@enduml\b.*/, "").trim();
|
|
2671
|
+
}
|
|
2672
|
+
function extractClasses(body, entities) {
|
|
2673
|
+
const classRegex = /class\s+(\w+)/g;
|
|
2674
|
+
let match = classRegex.exec(body);
|
|
2675
|
+
while (match !== null) {
|
|
2676
|
+
const id = match[1] ?? "";
|
|
2677
|
+
if (id && !entities.has(id)) {
|
|
2678
|
+
entities.set(id, { id, label: id });
|
|
2679
|
+
}
|
|
2680
|
+
match = classRegex.exec(body);
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
function extractComponents(body, entities) {
|
|
2684
|
+
const componentRegex = /\[([^\]]+)\]/g;
|
|
2685
|
+
let match = componentRegex.exec(body);
|
|
2686
|
+
while (match !== null) {
|
|
2687
|
+
const label = (match[1] ?? "").trim();
|
|
2688
|
+
if (label) {
|
|
2689
|
+
const id = label.replace(/\s+/g, "_");
|
|
2690
|
+
if (!entities.has(id)) {
|
|
2691
|
+
entities.set(id, { id, label });
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
match = componentRegex.exec(body);
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
function parseRelationshipMatch(match) {
|
|
2698
|
+
const from = match[1] ?? "";
|
|
2699
|
+
const to = match[2] ?? "";
|
|
2700
|
+
if (!from || !to) return null;
|
|
2701
|
+
const label = match[3]?.trim();
|
|
2702
|
+
return { from, to, ...label ? { label } : {} };
|
|
2703
|
+
}
|
|
2704
|
+
function collectRelationshipMatches(body, regex) {
|
|
2705
|
+
const results = [];
|
|
2706
|
+
let match = regex.exec(body);
|
|
2707
|
+
while (match !== null) {
|
|
2708
|
+
const rel = parseRelationshipMatch(match);
|
|
2709
|
+
if (rel) results.push(rel);
|
|
2710
|
+
match = regex.exec(body);
|
|
2711
|
+
}
|
|
2712
|
+
return results;
|
|
2713
|
+
}
|
|
2714
|
+
function extractRelationships(body) {
|
|
2715
|
+
return collectRelationshipMatches(
|
|
2716
|
+
body,
|
|
2717
|
+
/(\w+)\s*(?:-->|->|<--|<-|\.\.>|--)\s*(\w+)(?:\s*:\s*(.+))?/g
|
|
2718
|
+
);
|
|
2719
|
+
}
|
|
2486
2720
|
var PlantUmlParser = class {
|
|
2487
2721
|
canParse(_content, ext) {
|
|
2488
2722
|
return ext === ".puml" || ext === ".plantuml";
|
|
2489
2723
|
}
|
|
2490
2724
|
parse(content, _filePath) {
|
|
2491
2725
|
const trimmed = content.trim();
|
|
2492
|
-
if (!trimmed)
|
|
2493
|
-
|
|
2494
|
-
entities: [],
|
|
2495
|
-
relationships: [],
|
|
2496
|
-
metadata: { format: "plantuml", diagramType: "unknown" }
|
|
2497
|
-
};
|
|
2498
|
-
}
|
|
2499
|
-
const diagramType = this.detectDiagramType(trimmed);
|
|
2726
|
+
if (!trimmed) return emptyResult4();
|
|
2727
|
+
const diagramType = detectDiagramType2(trimmed);
|
|
2500
2728
|
const entities = /* @__PURE__ */ new Map();
|
|
2501
|
-
const
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
match = classRegex.exec(body);
|
|
2506
|
-
while (match !== null) {
|
|
2507
|
-
const id = match[1] ?? "";
|
|
2508
|
-
if (id && !entities.has(id)) {
|
|
2509
|
-
entities.set(id, { id, label: id });
|
|
2510
|
-
}
|
|
2511
|
-
match = classRegex.exec(body);
|
|
2512
|
-
}
|
|
2513
|
-
const componentRegex1 = /\[([^\]]+)\]/g;
|
|
2514
|
-
match = componentRegex1.exec(body);
|
|
2515
|
-
while (match !== null) {
|
|
2516
|
-
const label = (match[1] ?? "").trim();
|
|
2517
|
-
if (label) {
|
|
2518
|
-
const id = label.replace(/\s+/g, "_");
|
|
2519
|
-
if (!entities.has(id)) {
|
|
2520
|
-
entities.set(id, { id, label });
|
|
2521
|
-
}
|
|
2522
|
-
}
|
|
2523
|
-
match = componentRegex1.exec(body);
|
|
2524
|
-
}
|
|
2525
|
-
const relRegex = /(\w+)\s*(?:-->|->|<--|<-|\.\.>|--)\s*(\w+)(?:\s*:\s*(.+))?/g;
|
|
2526
|
-
match = relRegex.exec(body);
|
|
2527
|
-
while (match !== null) {
|
|
2528
|
-
const from = match[1] ?? "";
|
|
2529
|
-
const to = match[2] ?? "";
|
|
2530
|
-
const label = match[3]?.trim();
|
|
2531
|
-
if (from && to) {
|
|
2532
|
-
relationships.push({ from, to, ...label ? { label } : {} });
|
|
2533
|
-
}
|
|
2534
|
-
match = relRegex.exec(body);
|
|
2535
|
-
}
|
|
2729
|
+
const body = stripWrappers(trimmed);
|
|
2730
|
+
extractClasses(body, entities);
|
|
2731
|
+
extractComponents(body, entities);
|
|
2732
|
+
const relationships = extractRelationships(body);
|
|
2536
2733
|
return {
|
|
2537
2734
|
entities: Array.from(entities.values()),
|
|
2538
2735
|
relationships,
|
|
2539
2736
|
metadata: { format: "plantuml", diagramType }
|
|
2540
2737
|
};
|
|
2541
2738
|
}
|
|
2542
|
-
detectDiagramType(content) {
|
|
2543
|
-
if (/class\s+\w+/.test(content)) return "class";
|
|
2544
|
-
if (/\[.+\]/.test(content) || /component\s+/.test(content)) return "component";
|
|
2545
|
-
if (/participant\s+/.test(content) || /actor\s+/.test(content)) return "sequence";
|
|
2546
|
-
return "unknown";
|
|
2547
|
-
}
|
|
2548
2739
|
};
|
|
2740
|
+
|
|
2741
|
+
// src/ingest/DiagramParser.ts
|
|
2549
2742
|
var DIAGRAM_EXTENSIONS = /* @__PURE__ */ new Set([".mmd", ".mermaid", ".d2", ".puml", ".plantuml"]);
|
|
2743
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".harness"]);
|
|
2550
2744
|
var DiagramParser = class {
|
|
2551
2745
|
constructor(store) {
|
|
2552
2746
|
this.store = store;
|
|
@@ -2558,7 +2752,7 @@ var DiagramParser = class {
|
|
|
2558
2752
|
new PlantUmlParser()
|
|
2559
2753
|
];
|
|
2560
2754
|
parse(content, filePath) {
|
|
2561
|
-
const ext =
|
|
2755
|
+
const ext = path7.extname(filePath).toLowerCase();
|
|
2562
2756
|
for (const parser of this.parsers) {
|
|
2563
2757
|
if (parser.canParse(content, ext)) {
|
|
2564
2758
|
return parser.parse(content, filePath);
|
|
@@ -2578,41 +2772,13 @@ var DiagramParser = class {
|
|
|
2578
2772
|
const diagramFiles = await this.findDiagramFiles(projectDir);
|
|
2579
2773
|
for (const filePath of diagramFiles) {
|
|
2580
2774
|
try {
|
|
2581
|
-
const content = await
|
|
2582
|
-
const relPath =
|
|
2775
|
+
const content = await fs6.readFile(filePath, "utf-8");
|
|
2776
|
+
const relPath = path7.relative(projectDir, filePath).replaceAll("\\", "/");
|
|
2583
2777
|
const result = this.parse(content, filePath);
|
|
2584
2778
|
if (result.entities.length === 0) continue;
|
|
2585
2779
|
const pathHash = hash(relPath);
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
this.store.addNode({
|
|
2589
|
-
id: nodeId,
|
|
2590
|
-
type: "business_concept",
|
|
2591
|
-
name: entity.label,
|
|
2592
|
-
path: relPath,
|
|
2593
|
-
metadata: {
|
|
2594
|
-
source: "diagram",
|
|
2595
|
-
format: result.metadata.format,
|
|
2596
|
-
diagramType: result.metadata.diagramType,
|
|
2597
|
-
confidence: 0.85,
|
|
2598
|
-
...entity.type ? { entityType: entity.type } : {}
|
|
2599
|
-
}
|
|
2600
|
-
});
|
|
2601
|
-
nodesAdded++;
|
|
2602
|
-
}
|
|
2603
|
-
for (const rel of result.relationships) {
|
|
2604
|
-
const fromId = `diagram:${pathHash}:${rel.from}`;
|
|
2605
|
-
const toId = `diagram:${pathHash}:${rel.to}`;
|
|
2606
|
-
this.store.addEdge({
|
|
2607
|
-
from: fromId,
|
|
2608
|
-
to: toId,
|
|
2609
|
-
type: "references",
|
|
2610
|
-
metadata: {
|
|
2611
|
-
...rel.label ? { label: rel.label } : {}
|
|
2612
|
-
}
|
|
2613
|
-
});
|
|
2614
|
-
edgesAdded++;
|
|
2615
|
-
}
|
|
2780
|
+
nodesAdded += this.addEntityNodes(result, relPath, pathHash);
|
|
2781
|
+
edgesAdded += this.addRelationshipEdges(result, pathHash);
|
|
2616
2782
|
} catch (err) {
|
|
2617
2783
|
errors.push(
|
|
2618
2784
|
`Failed to parse ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -2628,25 +2794,64 @@ var DiagramParser = class {
|
|
|
2628
2794
|
durationMs: Date.now() - start
|
|
2629
2795
|
};
|
|
2630
2796
|
}
|
|
2797
|
+
/** Map diagram entities to business_concept graph nodes. */
|
|
2798
|
+
addEntityNodes(result, relPath, pathHash) {
|
|
2799
|
+
let count = 0;
|
|
2800
|
+
for (const entity of result.entities) {
|
|
2801
|
+
const nodeId = `diagram:${pathHash}:${entity.id}`;
|
|
2802
|
+
this.store.addNode({
|
|
2803
|
+
id: nodeId,
|
|
2804
|
+
type: "business_concept",
|
|
2805
|
+
name: entity.label,
|
|
2806
|
+
path: relPath,
|
|
2807
|
+
metadata: {
|
|
2808
|
+
source: "diagram",
|
|
2809
|
+
format: result.metadata.format,
|
|
2810
|
+
diagramType: result.metadata.diagramType,
|
|
2811
|
+
confidence: 0.85,
|
|
2812
|
+
...entity.type ? { entityType: entity.type } : {}
|
|
2813
|
+
}
|
|
2814
|
+
});
|
|
2815
|
+
count++;
|
|
2816
|
+
}
|
|
2817
|
+
return count;
|
|
2818
|
+
}
|
|
2819
|
+
/** Map diagram relationships to references graph edges. */
|
|
2820
|
+
addRelationshipEdges(result, pathHash) {
|
|
2821
|
+
let count = 0;
|
|
2822
|
+
for (const rel of result.relationships) {
|
|
2823
|
+
const fromId = `diagram:${pathHash}:${rel.from}`;
|
|
2824
|
+
const toId = `diagram:${pathHash}:${rel.to}`;
|
|
2825
|
+
this.store.addEdge({
|
|
2826
|
+
from: fromId,
|
|
2827
|
+
to: toId,
|
|
2828
|
+
type: "references",
|
|
2829
|
+
metadata: {
|
|
2830
|
+
...rel.label ? { label: rel.label } : {}
|
|
2831
|
+
}
|
|
2832
|
+
});
|
|
2833
|
+
count++;
|
|
2834
|
+
}
|
|
2835
|
+
return count;
|
|
2836
|
+
}
|
|
2631
2837
|
async findDiagramFiles(dir) {
|
|
2632
2838
|
const files = [];
|
|
2633
|
-
const SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".harness"]);
|
|
2634
2839
|
const walk = async (currentDir) => {
|
|
2635
2840
|
let entries;
|
|
2636
2841
|
try {
|
|
2637
|
-
entries = await
|
|
2842
|
+
entries = await fs6.readdir(currentDir, { withFileTypes: true });
|
|
2638
2843
|
} catch {
|
|
2639
2844
|
return;
|
|
2640
2845
|
}
|
|
2641
2846
|
for (const entry of entries) {
|
|
2642
2847
|
if (entry.isDirectory()) {
|
|
2643
|
-
if (!
|
|
2644
|
-
await walk(
|
|
2848
|
+
if (!SKIP_DIRS.has(entry.name)) {
|
|
2849
|
+
await walk(path7.join(currentDir, entry.name));
|
|
2645
2850
|
}
|
|
2646
2851
|
} else if (entry.isFile()) {
|
|
2647
|
-
const ext =
|
|
2852
|
+
const ext = path7.extname(entry.name).toLowerCase();
|
|
2648
2853
|
if (DIAGRAM_EXTENSIONS.has(ext)) {
|
|
2649
|
-
files.push(
|
|
2854
|
+
files.push(path7.join(currentDir, entry.name));
|
|
2650
2855
|
}
|
|
2651
2856
|
}
|
|
2652
2857
|
}
|
|
@@ -2657,8 +2862,8 @@ var DiagramParser = class {
|
|
|
2657
2862
|
};
|
|
2658
2863
|
|
|
2659
2864
|
// src/ingest/KnowledgeLinker.ts
|
|
2660
|
-
import * as
|
|
2661
|
-
import * as
|
|
2865
|
+
import * as fs7 from "fs/promises";
|
|
2866
|
+
import * as path8 from "path";
|
|
2662
2867
|
var HEURISTIC_PATTERNS = [
|
|
2663
2868
|
{
|
|
2664
2869
|
name: "business-rule-imperative",
|
|
@@ -2805,8 +3010,10 @@ var KnowledgeLinker = class {
|
|
|
2805
3010
|
if (!duplicate) return false;
|
|
2806
3011
|
const existingSources = duplicate.metadata.sources ?? [];
|
|
2807
3012
|
if (!existingSources.includes(candidate.sourceNodeId)) {
|
|
2808
|
-
|
|
2809
|
-
|
|
3013
|
+
this.store.addNode({
|
|
3014
|
+
...duplicate,
|
|
3015
|
+
metadata: { ...duplicate.metadata, sources: [...existingSources, candidate.sourceNodeId] }
|
|
3016
|
+
});
|
|
2810
3017
|
}
|
|
2811
3018
|
return true;
|
|
2812
3019
|
}
|
|
@@ -2840,8 +3047,8 @@ var KnowledgeLinker = class {
|
|
|
2840
3047
|
*/
|
|
2841
3048
|
async writeJsonl(candidates) {
|
|
2842
3049
|
if (!this.outputDir) return;
|
|
2843
|
-
await
|
|
2844
|
-
const filePath =
|
|
3050
|
+
await fs7.mkdir(this.outputDir, { recursive: true });
|
|
3051
|
+
const filePath = path8.join(this.outputDir, "linker.jsonl");
|
|
2845
3052
|
const lines = candidates.map(
|
|
2846
3053
|
(c) => JSON.stringify({
|
|
2847
3054
|
id: c.id,
|
|
@@ -2854,16 +3061,16 @@ var KnowledgeLinker = class {
|
|
|
2854
3061
|
nodeType: c.nodeType
|
|
2855
3062
|
})
|
|
2856
3063
|
);
|
|
2857
|
-
await
|
|
3064
|
+
await fs7.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
|
|
2858
3065
|
}
|
|
2859
3066
|
/**
|
|
2860
3067
|
* Write medium-confidence candidates to staged JSONL for human review.
|
|
2861
3068
|
*/
|
|
2862
3069
|
async writeStagedJsonl(candidates) {
|
|
2863
3070
|
if (!this.outputDir || candidates.length === 0) return;
|
|
2864
|
-
const stagedDir =
|
|
2865
|
-
await
|
|
2866
|
-
const filePath =
|
|
3071
|
+
const stagedDir = path8.join(this.outputDir, "staged");
|
|
3072
|
+
await fs7.mkdir(stagedDir, { recursive: true });
|
|
3073
|
+
const filePath = path8.join(stagedDir, "linker-staged.jsonl");
|
|
2867
3074
|
const lines = candidates.map(
|
|
2868
3075
|
(c) => JSON.stringify({
|
|
2869
3076
|
id: c.id,
|
|
@@ -2876,7 +3083,7 @@ var KnowledgeLinker = class {
|
|
|
2876
3083
|
nodeType: c.nodeType
|
|
2877
3084
|
})
|
|
2878
3085
|
);
|
|
2879
|
-
await
|
|
3086
|
+
await fs7.writeFile(filePath, lines.join("\n") + "\n");
|
|
2880
3087
|
}
|
|
2881
3088
|
/**
|
|
2882
3089
|
* Apply heuristic patterns to content and return candidate extractions.
|
|
@@ -2982,8 +3189,16 @@ var StructuralDriftDetector = class {
|
|
|
2982
3189
|
};
|
|
2983
3190
|
|
|
2984
3191
|
// src/ingest/KnowledgeStagingAggregator.ts
|
|
2985
|
-
import * as
|
|
2986
|
-
import * as
|
|
3192
|
+
import * as fs8 from "fs/promises";
|
|
3193
|
+
import * as path9 from "path";
|
|
3194
|
+
var BUSINESS_NODE_TYPES = [
|
|
3195
|
+
"business_concept",
|
|
3196
|
+
"business_rule",
|
|
3197
|
+
"business_process",
|
|
3198
|
+
"business_term",
|
|
3199
|
+
"business_metric",
|
|
3200
|
+
"business_fact"
|
|
3201
|
+
];
|
|
2987
3202
|
var KnowledgeStagingAggregator = class {
|
|
2988
3203
|
constructor(projectDir) {
|
|
2989
3204
|
this.projectDir = projectDir;
|
|
@@ -3002,59 +3217,157 @@ var KnowledgeStagingAggregator = class {
|
|
|
3002
3217
|
if (deduplicated.length === 0) {
|
|
3003
3218
|
return { staged: 0 };
|
|
3004
3219
|
}
|
|
3005
|
-
const stagedDir =
|
|
3006
|
-
await
|
|
3220
|
+
const stagedDir = path9.join(this.projectDir, ".harness", "knowledge", "staged");
|
|
3221
|
+
await fs8.mkdir(stagedDir, { recursive: true });
|
|
3007
3222
|
const jsonl = deduplicated.map((entry) => JSON.stringify(entry)).join("\n") + "\n";
|
|
3008
|
-
await
|
|
3223
|
+
await fs8.writeFile(path9.join(stagedDir, "pipeline-staged.jsonl"), jsonl, "utf-8");
|
|
3009
3224
|
return { staged: deduplicated.length };
|
|
3010
3225
|
}
|
|
3011
|
-
async
|
|
3012
|
-
const
|
|
3226
|
+
async extractDocName(filePath) {
|
|
3227
|
+
const raw = await fs8.readFile(filePath, "utf-8");
|
|
3228
|
+
const fmMatch = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
|
|
3229
|
+
const body = fmMatch ? fmMatch[1] : raw;
|
|
3230
|
+
const titleMatch = body.match(/^#\s+(.+)$/m);
|
|
3231
|
+
return titleMatch ? titleMatch[1].trim() : path9.basename(filePath, ".md");
|
|
3232
|
+
}
|
|
3233
|
+
async generateGapReport(knowledgeDir, store) {
|
|
3234
|
+
const domainDocNames = /* @__PURE__ */ new Map();
|
|
3235
|
+
const domainEntryCounts = /* @__PURE__ */ new Map();
|
|
3013
3236
|
let totalEntries = 0;
|
|
3014
3237
|
try {
|
|
3015
|
-
const entries = await
|
|
3238
|
+
const entries = await fs8.readdir(knowledgeDir, { withFileTypes: true });
|
|
3016
3239
|
const domainDirs = entries.filter((e) => e.isDirectory());
|
|
3017
3240
|
for (const dir of domainDirs) {
|
|
3018
|
-
const domainPath =
|
|
3019
|
-
const files = await
|
|
3241
|
+
const domainPath = path9.join(knowledgeDir, dir.name);
|
|
3242
|
+
const files = await fs8.readdir(domainPath);
|
|
3020
3243
|
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
3021
3244
|
const entryCount = mdFiles.length;
|
|
3022
3245
|
totalEntries += entryCount;
|
|
3023
|
-
|
|
3246
|
+
domainEntryCounts.set(dir.name, entryCount);
|
|
3247
|
+
if (store) {
|
|
3248
|
+
const names = [];
|
|
3249
|
+
for (const file of mdFiles) {
|
|
3250
|
+
const name = await this.extractDocName(path9.join(domainPath, file));
|
|
3251
|
+
names.push(name.toLowerCase().trim());
|
|
3252
|
+
}
|
|
3253
|
+
domainDocNames.set(dir.name, names);
|
|
3254
|
+
}
|
|
3024
3255
|
}
|
|
3025
3256
|
} catch {
|
|
3026
3257
|
}
|
|
3258
|
+
if (!store) {
|
|
3259
|
+
const domains2 = [];
|
|
3260
|
+
for (const [domain, entryCount] of domainEntryCounts) {
|
|
3261
|
+
domains2.push({ domain, entryCount, extractedCount: 0, gapCount: 0, gapEntries: [] });
|
|
3262
|
+
}
|
|
3263
|
+
return {
|
|
3264
|
+
domains: domains2,
|
|
3265
|
+
totalEntries,
|
|
3266
|
+
totalExtracted: 0,
|
|
3267
|
+
totalGaps: 0,
|
|
3268
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3269
|
+
};
|
|
3270
|
+
}
|
|
3271
|
+
const extractedByDomain = /* @__PURE__ */ new Map();
|
|
3272
|
+
for (const nodeType of BUSINESS_NODE_TYPES) {
|
|
3273
|
+
const nodes = store.findNodes({ type: nodeType });
|
|
3274
|
+
for (const node of nodes) {
|
|
3275
|
+
const domain = node.metadata?.domain ?? "unknown";
|
|
3276
|
+
const list = extractedByDomain.get(domain) ?? [];
|
|
3277
|
+
list.push(node);
|
|
3278
|
+
extractedByDomain.set(domain, list);
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
for (const [domain, nodes] of extractedByDomain) {
|
|
3282
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3283
|
+
const deduped = nodes.filter((n) => {
|
|
3284
|
+
const key = n.name.toLowerCase().trim();
|
|
3285
|
+
if (seen.has(key)) return false;
|
|
3286
|
+
seen.add(key);
|
|
3287
|
+
return true;
|
|
3288
|
+
});
|
|
3289
|
+
extractedByDomain.set(domain, deduped);
|
|
3290
|
+
}
|
|
3291
|
+
const allDomains = /* @__PURE__ */ new Set([...domainEntryCounts.keys(), ...extractedByDomain.keys()]);
|
|
3292
|
+
const domains = [];
|
|
3293
|
+
let totalExtracted = 0;
|
|
3294
|
+
let totalGaps = 0;
|
|
3295
|
+
for (const domain of allDomains) {
|
|
3296
|
+
const entryCount = domainEntryCounts.get(domain) ?? 0;
|
|
3297
|
+
const extractedNodes = extractedByDomain.get(domain) ?? [];
|
|
3298
|
+
const extractedCount = extractedNodes.length;
|
|
3299
|
+
totalExtracted += extractedCount;
|
|
3300
|
+
const docNames = domainDocNames.get(domain) ?? [];
|
|
3301
|
+
const gapEntries = [];
|
|
3302
|
+
for (const node of extractedNodes) {
|
|
3303
|
+
const normalizedName = node.name.toLowerCase().trim();
|
|
3304
|
+
if (!docNames.includes(normalizedName)) {
|
|
3305
|
+
gapEntries.push({
|
|
3306
|
+
nodeId: node.id,
|
|
3307
|
+
name: node.name,
|
|
3308
|
+
nodeType: node.type,
|
|
3309
|
+
source: node.metadata?.source ?? "unknown",
|
|
3310
|
+
hasContent: Boolean(node.content && node.content.trim().length >= 10)
|
|
3311
|
+
});
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
totalGaps += gapEntries.length;
|
|
3315
|
+
domains.push({ domain, entryCount, extractedCount, gapCount: gapEntries.length, gapEntries });
|
|
3316
|
+
}
|
|
3027
3317
|
return {
|
|
3028
3318
|
domains,
|
|
3029
3319
|
totalEntries,
|
|
3320
|
+
totalExtracted,
|
|
3321
|
+
totalGaps,
|
|
3030
3322
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3031
3323
|
};
|
|
3032
3324
|
}
|
|
3033
3325
|
async writeGapReport(report) {
|
|
3034
|
-
const gapsDir =
|
|
3035
|
-
await
|
|
3326
|
+
const gapsDir = path9.join(this.projectDir, ".harness", "knowledge");
|
|
3327
|
+
await fs8.mkdir(gapsDir, { recursive: true });
|
|
3328
|
+
const hasDifferential = report.totalExtracted > 0;
|
|
3036
3329
|
const lines = [
|
|
3037
3330
|
"# Knowledge Gaps Report",
|
|
3038
3331
|
"",
|
|
3039
3332
|
`Generated: ${report.generatedAt}`,
|
|
3040
3333
|
"",
|
|
3041
3334
|
"## Coverage by Domain",
|
|
3042
|
-
""
|
|
3043
|
-
"| Domain | Entries |",
|
|
3044
|
-
"| ------ | ------- |"
|
|
3335
|
+
""
|
|
3045
3336
|
];
|
|
3046
|
-
|
|
3047
|
-
lines.push(
|
|
3337
|
+
if (hasDifferential) {
|
|
3338
|
+
lines.push(
|
|
3339
|
+
"| Domain | Documented | Extracted | Gaps |",
|
|
3340
|
+
"| ------ | ---------- | --------- | ---- |"
|
|
3341
|
+
);
|
|
3342
|
+
for (const domain of report.domains) {
|
|
3343
|
+
lines.push(
|
|
3344
|
+
`| ${domain.domain} | ${domain.entryCount} | ${domain.extractedCount} | ${domain.gapCount} |`
|
|
3345
|
+
);
|
|
3346
|
+
}
|
|
3347
|
+
lines.push(
|
|
3348
|
+
"",
|
|
3349
|
+
`## Summary`,
|
|
3350
|
+
"",
|
|
3351
|
+
`- **Total Documented:** ${report.totalEntries}`,
|
|
3352
|
+
`- **Total Extracted:** ${report.totalExtracted}`,
|
|
3353
|
+
`- **Total Gaps:** ${report.totalGaps}`,
|
|
3354
|
+
""
|
|
3355
|
+
);
|
|
3356
|
+
} else {
|
|
3357
|
+
lines.push("| Domain | Entries |", "| ------ | ------- |");
|
|
3358
|
+
for (const domain of report.domains) {
|
|
3359
|
+
lines.push(`| ${domain.domain} | ${domain.entryCount} |`);
|
|
3360
|
+
}
|
|
3361
|
+
lines.push("", `## Total Entries: ${report.totalEntries}`, "");
|
|
3048
3362
|
}
|
|
3049
|
-
|
|
3050
|
-
await fs7.writeFile(path8.join(gapsDir, "gaps.md"), lines.join("\n"), "utf-8");
|
|
3363
|
+
await fs8.writeFile(path9.join(gapsDir, "gaps.md"), lines.join("\n"), "utf-8");
|
|
3051
3364
|
}
|
|
3052
3365
|
};
|
|
3053
3366
|
|
|
3054
3367
|
// src/ingest/extractors/ExtractionRunner.ts
|
|
3055
|
-
import * as
|
|
3056
|
-
import * as
|
|
3057
|
-
var
|
|
3368
|
+
import * as fs9 from "fs/promises";
|
|
3369
|
+
import * as path10 from "path";
|
|
3370
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
3058
3371
|
"node_modules",
|
|
3059
3372
|
"dist",
|
|
3060
3373
|
"target",
|
|
@@ -3091,11 +3404,11 @@ var EXT_TO_LANGUAGE = {
|
|
|
3091
3404
|
};
|
|
3092
3405
|
var SKIP_EXTENSIONS2 = /* @__PURE__ */ new Set([".d.ts"]);
|
|
3093
3406
|
function detectLanguage(filePath) {
|
|
3094
|
-
const name =
|
|
3407
|
+
const name = path10.basename(filePath);
|
|
3095
3408
|
for (const skip of SKIP_EXTENSIONS2) {
|
|
3096
3409
|
if (name.endsWith(skip)) return void 0;
|
|
3097
3410
|
}
|
|
3098
|
-
const ext =
|
|
3411
|
+
const ext = path10.extname(filePath);
|
|
3099
3412
|
return EXT_TO_LANGUAGE[ext];
|
|
3100
3413
|
}
|
|
3101
3414
|
var EXTRACTOR_EDGE_TYPE = {
|
|
@@ -3121,7 +3434,7 @@ var ExtractionRunner = class {
|
|
|
3121
3434
|
let nodesAdded = 0;
|
|
3122
3435
|
let nodesUpdated = 0;
|
|
3123
3436
|
let edgesAdded = 0;
|
|
3124
|
-
await
|
|
3437
|
+
await fs9.mkdir(outputDir, { recursive: true });
|
|
3125
3438
|
const files = await this.findSourceFiles(projectDir);
|
|
3126
3439
|
const recordsByExtractor = /* @__PURE__ */ new Map();
|
|
3127
3440
|
for (const ext of this.extractors) {
|
|
@@ -3130,17 +3443,17 @@ var ExtractionRunner = class {
|
|
|
3130
3443
|
for (const filePath of files) {
|
|
3131
3444
|
const language = detectLanguage(filePath);
|
|
3132
3445
|
if (!language) continue;
|
|
3133
|
-
const ext =
|
|
3446
|
+
const ext = path10.extname(filePath);
|
|
3134
3447
|
let content;
|
|
3135
3448
|
try {
|
|
3136
|
-
content = await
|
|
3449
|
+
content = await fs9.readFile(filePath, "utf-8");
|
|
3137
3450
|
} catch (err) {
|
|
3138
3451
|
errors.push(
|
|
3139
3452
|
`Failed to read ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
3140
3453
|
);
|
|
3141
3454
|
continue;
|
|
3142
3455
|
}
|
|
3143
|
-
const relativePath =
|
|
3456
|
+
const relativePath = path10.relative(projectDir, filePath).replaceAll("\\", "/");
|
|
3144
3457
|
for (const extractor2 of this.extractors) {
|
|
3145
3458
|
if (!extractor2.supportedExtensions.includes(ext)) continue;
|
|
3146
3459
|
try {
|
|
@@ -3177,9 +3490,9 @@ var ExtractionRunner = class {
|
|
|
3177
3490
|
}
|
|
3178
3491
|
/** Write extraction records to JSONL file. */
|
|
3179
3492
|
async writeJsonl(records, outputDir, extractorName) {
|
|
3180
|
-
const filePath =
|
|
3493
|
+
const filePath = path10.join(outputDir, `${extractorName}.jsonl`);
|
|
3181
3494
|
const lines = records.map((r) => JSON.stringify(r));
|
|
3182
|
-
await
|
|
3495
|
+
await fs9.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
|
|
3183
3496
|
}
|
|
3184
3497
|
/** Create or update a graph node from an extraction record. */
|
|
3185
3498
|
persistRecord(record, store) {
|
|
@@ -3251,13 +3564,13 @@ var ExtractionRunner = class {
|
|
|
3251
3564
|
const results = [];
|
|
3252
3565
|
let entries;
|
|
3253
3566
|
try {
|
|
3254
|
-
entries = await
|
|
3567
|
+
entries = await fs9.readdir(dir, { withFileTypes: true });
|
|
3255
3568
|
} catch {
|
|
3256
3569
|
return results;
|
|
3257
3570
|
}
|
|
3258
3571
|
for (const entry of entries) {
|
|
3259
|
-
const fullPath =
|
|
3260
|
-
if (entry.isDirectory() && !
|
|
3572
|
+
const fullPath = path10.join(dir, entry.name);
|
|
3573
|
+
if (entry.isDirectory() && !SKIP_DIRS2.has(entry.name)) {
|
|
3261
3574
|
results.push(...await this.findSourceFiles(fullPath));
|
|
3262
3575
|
} else if (entry.isFile() && detectLanguage(fullPath) !== void 0) {
|
|
3263
3576
|
results.push(fullPath);
|
|
@@ -4383,57 +4696,9 @@ var ImageAnalysisExtractor = class {
|
|
|
4383
4696
|
const errors = [];
|
|
4384
4697
|
for (const imagePath of imagePaths) {
|
|
4385
4698
|
try {
|
|
4386
|
-
const
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
responseSchema: {},
|
|
4390
|
-
// Schema handled by provider
|
|
4391
|
-
maxTokens: 1e3
|
|
4392
|
-
});
|
|
4393
|
-
const result = response.result;
|
|
4394
|
-
const pathHash = hash(imagePath);
|
|
4395
|
-
const annotationId = `img:${pathHash}`;
|
|
4396
|
-
store.addNode({
|
|
4397
|
-
id: annotationId,
|
|
4398
|
-
type: "image_annotation",
|
|
4399
|
-
name: result.description.slice(0, 200),
|
|
4400
|
-
path: imagePath,
|
|
4401
|
-
content: result.description,
|
|
4402
|
-
hash: hash(result.description),
|
|
4403
|
-
metadata: {
|
|
4404
|
-
source: "image-analysis",
|
|
4405
|
-
detectedElements: result.detectedElements,
|
|
4406
|
-
extractedText: result.extractedText,
|
|
4407
|
-
designPatterns: result.designPatterns,
|
|
4408
|
-
accessibilityNotes: result.accessibilityNotes,
|
|
4409
|
-
model: response.model
|
|
4410
|
-
}
|
|
4411
|
-
});
|
|
4412
|
-
nodesAdded++;
|
|
4413
|
-
const fileNodes = store.findNodes({ type: "file" });
|
|
4414
|
-
const fileNode = fileNodes.find((n) => n.path === imagePath);
|
|
4415
|
-
if (fileNode) {
|
|
4416
|
-
store.addEdge({ from: annotationId, to: fileNode.id, type: "annotates" });
|
|
4417
|
-
edgesAdded++;
|
|
4418
|
-
}
|
|
4419
|
-
for (const pattern of result.designPatterns) {
|
|
4420
|
-
const conceptId = `img:concept:${hash(pattern + imagePath)}`;
|
|
4421
|
-
store.addNode({
|
|
4422
|
-
id: conceptId,
|
|
4423
|
-
type: "business_concept",
|
|
4424
|
-
name: pattern,
|
|
4425
|
-
content: `Design pattern detected in ${imagePath}: ${pattern}`,
|
|
4426
|
-
hash: hash(pattern),
|
|
4427
|
-
metadata: {
|
|
4428
|
-
source: "image-analysis",
|
|
4429
|
-
sourceImage: imagePath,
|
|
4430
|
-
domain: "design"
|
|
4431
|
-
}
|
|
4432
|
-
});
|
|
4433
|
-
nodesAdded++;
|
|
4434
|
-
store.addEdge({ from: annotationId, to: conceptId, type: "contains" });
|
|
4435
|
-
edgesAdded++;
|
|
4436
|
-
}
|
|
4699
|
+
const counts = await this.processImage(store, imagePath);
|
|
4700
|
+
nodesAdded += counts.nodes;
|
|
4701
|
+
edgesAdded += counts.edges;
|
|
4437
4702
|
} catch (err) {
|
|
4438
4703
|
errors.push(
|
|
4439
4704
|
`Image analysis failed for ${imagePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -4449,6 +4714,68 @@ var ImageAnalysisExtractor = class {
|
|
|
4449
4714
|
durationMs: Date.now() - start
|
|
4450
4715
|
};
|
|
4451
4716
|
}
|
|
4717
|
+
/** Analyze a single image and add annotation + concept nodes to the store. */
|
|
4718
|
+
async processImage(store, imagePath) {
|
|
4719
|
+
const response = await this.provider.analyze({
|
|
4720
|
+
prompt: "Analyze this image and provide a structured description of its visual contents.",
|
|
4721
|
+
systemPrompt: "You are an image analysis assistant. Describe the image contents, detect UI elements, extract visible text, identify design patterns, and note accessibility concerns.",
|
|
4722
|
+
responseSchema: {},
|
|
4723
|
+
// Schema handled by provider
|
|
4724
|
+
maxTokens: 1e3
|
|
4725
|
+
});
|
|
4726
|
+
const result = response.result;
|
|
4727
|
+
const annotationId = `img:${hash(imagePath)}`;
|
|
4728
|
+
let nodes = 0;
|
|
4729
|
+
let edges = 0;
|
|
4730
|
+
store.addNode({
|
|
4731
|
+
id: annotationId,
|
|
4732
|
+
type: "image_annotation",
|
|
4733
|
+
name: result.description.slice(0, 200),
|
|
4734
|
+
path: imagePath,
|
|
4735
|
+
content: result.description,
|
|
4736
|
+
hash: hash(result.description),
|
|
4737
|
+
metadata: {
|
|
4738
|
+
source: "image-analysis",
|
|
4739
|
+
detectedElements: result.detectedElements,
|
|
4740
|
+
extractedText: result.extractedText,
|
|
4741
|
+
designPatterns: result.designPatterns,
|
|
4742
|
+
accessibilityNotes: result.accessibilityNotes,
|
|
4743
|
+
model: response.model
|
|
4744
|
+
}
|
|
4745
|
+
});
|
|
4746
|
+
nodes++;
|
|
4747
|
+
edges += this.linkToFileNode(store, annotationId, imagePath);
|
|
4748
|
+
for (const pattern of result.designPatterns) {
|
|
4749
|
+
edges += this.addDesignPatternConcept(store, annotationId, imagePath, pattern);
|
|
4750
|
+
nodes++;
|
|
4751
|
+
}
|
|
4752
|
+
return { nodes, edges };
|
|
4753
|
+
}
|
|
4754
|
+
/** Link annotation to its source file node if it exists. Returns 1 if linked, 0 otherwise. */
|
|
4755
|
+
linkToFileNode(store, annotationId, imagePath) {
|
|
4756
|
+
const fileNode = store.findNodes({ type: "file" }).find((n) => n.path === imagePath);
|
|
4757
|
+
if (!fileNode) return 0;
|
|
4758
|
+
store.addEdge({ from: annotationId, to: fileNode.id, type: "annotates" });
|
|
4759
|
+
return 1;
|
|
4760
|
+
}
|
|
4761
|
+
/** Create a business_concept node for a design pattern and link it. Returns edges added. */
|
|
4762
|
+
addDesignPatternConcept(store, annotationId, imagePath, pattern) {
|
|
4763
|
+
const conceptId = `img:concept:${hash(pattern + imagePath)}`;
|
|
4764
|
+
store.addNode({
|
|
4765
|
+
id: conceptId,
|
|
4766
|
+
type: "business_concept",
|
|
4767
|
+
name: pattern,
|
|
4768
|
+
content: `Design pattern detected in ${imagePath}: ${pattern}`,
|
|
4769
|
+
hash: hash(pattern),
|
|
4770
|
+
metadata: {
|
|
4771
|
+
source: "image-analysis",
|
|
4772
|
+
sourceImage: imagePath,
|
|
4773
|
+
domain: "design"
|
|
4774
|
+
}
|
|
4775
|
+
});
|
|
4776
|
+
store.addEdge({ from: annotationId, to: conceptId, type: "contains" });
|
|
4777
|
+
return 1;
|
|
4778
|
+
}
|
|
4452
4779
|
};
|
|
4453
4780
|
|
|
4454
4781
|
// src/ingest/knowledgeTypes.ts
|
|
@@ -4459,6 +4786,7 @@ var KNOWLEDGE_NODE_TYPES = [
|
|
|
4459
4786
|
"business_term",
|
|
4460
4787
|
"business_concept",
|
|
4461
4788
|
"business_metric",
|
|
4789
|
+
"decision",
|
|
4462
4790
|
"design_token",
|
|
4463
4791
|
"design_constraint",
|
|
4464
4792
|
"aesthetic_intent",
|
|
@@ -4499,83 +4827,99 @@ function classifyConflict(a, b) {
|
|
|
4499
4827
|
return "temporal_conflict";
|
|
4500
4828
|
return "status_divergence";
|
|
4501
4829
|
}
|
|
4502
|
-
var
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
for (
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
for (let j = i + 1; j < group.length; j++) {
|
|
4555
|
-
addContradiction(group[i], group[j], 1);
|
|
4556
|
-
}
|
|
4830
|
+
var SEVERITY_MAP = {
|
|
4831
|
+
value_mismatch: "critical",
|
|
4832
|
+
definition_conflict: "high",
|
|
4833
|
+
status_divergence: "medium",
|
|
4834
|
+
temporal_conflict: "medium"
|
|
4835
|
+
};
|
|
4836
|
+
function buildEntry(node, source) {
|
|
4837
|
+
return {
|
|
4838
|
+
nodeId: node.id,
|
|
4839
|
+
source,
|
|
4840
|
+
name: node.name,
|
|
4841
|
+
content: node.content ?? "",
|
|
4842
|
+
lastModified: node.lastModified
|
|
4843
|
+
};
|
|
4844
|
+
}
|
|
4845
|
+
function groupByName(nodes) {
|
|
4846
|
+
const byName = /* @__PURE__ */ new Map();
|
|
4847
|
+
for (const node of nodes) {
|
|
4848
|
+
const key = node.name.toLowerCase().trim();
|
|
4849
|
+
const group = byName.get(key) ?? [];
|
|
4850
|
+
group.push(node);
|
|
4851
|
+
byName.set(key, group);
|
|
4852
|
+
}
|
|
4853
|
+
return byName;
|
|
4854
|
+
}
|
|
4855
|
+
function tryAddContradiction(acc, a, b, similarity) {
|
|
4856
|
+
const sourceA = a.metadata.source ?? "unknown";
|
|
4857
|
+
const sourceB = b.metadata.source ?? "unknown";
|
|
4858
|
+
if (sourceA === sourceB) return;
|
|
4859
|
+
if ((a.hash ?? a.id) === (b.hash ?? b.id)) return;
|
|
4860
|
+
const pairId = [a.id, b.id].sort().join(":");
|
|
4861
|
+
if (acc.seen.has(pairId)) return;
|
|
4862
|
+
acc.seen.add(pairId);
|
|
4863
|
+
const conflictType = classifyConflict(a, b);
|
|
4864
|
+
const pairKey = [sourceA, sourceB].sort().join("\u2194");
|
|
4865
|
+
acc.sourcePairCounts[pairKey] = (acc.sourcePairCounts[pairKey] ?? 0) + 1;
|
|
4866
|
+
acc.contradictions.push({
|
|
4867
|
+
id: `contradiction:${a.id}:${b.id}`,
|
|
4868
|
+
entityA: buildEntry(a, sourceA),
|
|
4869
|
+
entityB: buildEntry(b, sourceB),
|
|
4870
|
+
similarity,
|
|
4871
|
+
conflictType,
|
|
4872
|
+
severity: SEVERITY_MAP[conflictType],
|
|
4873
|
+
description: `"${a.name}" has conflicting definitions from ${sourceA} and ${sourceB}`
|
|
4874
|
+
});
|
|
4875
|
+
}
|
|
4876
|
+
function collectExactMatches(byName, acc) {
|
|
4877
|
+
for (const [, group] of byName) {
|
|
4878
|
+
if (group.length < 2) continue;
|
|
4879
|
+
for (let i = 0; i < group.length; i++) {
|
|
4880
|
+
for (let j = i + 1; j < group.length; j++) {
|
|
4881
|
+
tryAddContradiction(acc, group[i], group[j], 1);
|
|
4557
4882
|
}
|
|
4558
4883
|
}
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4884
|
+
}
|
|
4885
|
+
}
|
|
4886
|
+
function collectFuzzyMatches(byName, acc) {
|
|
4887
|
+
const keys = [...byName.keys()];
|
|
4888
|
+
for (let i = 0; i < keys.length; i++) {
|
|
4889
|
+
for (let j = i + 1; j < keys.length; j++) {
|
|
4890
|
+
const keyA = keys[i];
|
|
4891
|
+
const keyB = keys[j];
|
|
4892
|
+
const maxLen = Math.max(keyA.length, keyB.length);
|
|
4893
|
+
const minLen = Math.min(keyA.length, keyB.length);
|
|
4894
|
+
if (minLen / maxLen < SIMILARITY_THRESHOLD) continue;
|
|
4895
|
+
const ratio = levenshteinRatio(keyA, keyB);
|
|
4896
|
+
if (ratio < SIMILARITY_THRESHOLD) continue;
|
|
4897
|
+
const groupA = byName.get(keyA);
|
|
4898
|
+
const groupB = byName.get(keyB);
|
|
4899
|
+
for (const a of groupA) {
|
|
4900
|
+
for (const b of groupB) {
|
|
4901
|
+
tryAddContradiction(acc, a, b, ratio);
|
|
4575
4902
|
}
|
|
4576
4903
|
}
|
|
4577
4904
|
}
|
|
4578
|
-
|
|
4905
|
+
}
|
|
4906
|
+
}
|
|
4907
|
+
var ContradictionDetector = class {
|
|
4908
|
+
detect(store) {
|
|
4909
|
+
const nodes = KNOWLEDGE_NODE_TYPES.flatMap((t) => store.findNodes({ type: t }));
|
|
4910
|
+
const byName = groupByName(nodes);
|
|
4911
|
+
const acc = {
|
|
4912
|
+
contradictions: [],
|
|
4913
|
+
sourcePairCounts: {},
|
|
4914
|
+
seen: /* @__PURE__ */ new Set()
|
|
4915
|
+
};
|
|
4916
|
+
collectExactMatches(byName, acc);
|
|
4917
|
+
collectFuzzyMatches(byName, acc);
|
|
4918
|
+
return {
|
|
4919
|
+
contradictions: acc.contradictions,
|
|
4920
|
+
sourcePairCounts: acc.sourcePairCounts,
|
|
4921
|
+
totalChecked: nodes.length
|
|
4922
|
+
};
|
|
4579
4923
|
}
|
|
4580
4924
|
};
|
|
4581
4925
|
|
|
@@ -4606,64 +4950,76 @@ function toGrade(score) {
|
|
|
4606
4950
|
if (score >= 20) return "D";
|
|
4607
4951
|
return "F";
|
|
4608
4952
|
}
|
|
4953
|
+
function groupByDomain(nodes, fallback) {
|
|
4954
|
+
const map = /* @__PURE__ */ new Map();
|
|
4955
|
+
for (const node of nodes) {
|
|
4956
|
+
const domain = node.metadata.domain ?? fallback(node);
|
|
4957
|
+
const group = map.get(domain) ?? [];
|
|
4958
|
+
group.push(node);
|
|
4959
|
+
map.set(domain, group);
|
|
4960
|
+
}
|
|
4961
|
+
return map;
|
|
4962
|
+
}
|
|
4963
|
+
function countLinkedEntities(codeEntries, store) {
|
|
4964
|
+
const linkedIds = /* @__PURE__ */ new Set();
|
|
4965
|
+
for (const codeNode of codeEntries) {
|
|
4966
|
+
if (hasKnowledgeEdge(codeNode.id, store)) {
|
|
4967
|
+
linkedIds.add(codeNode.id);
|
|
4968
|
+
}
|
|
4969
|
+
}
|
|
4970
|
+
return linkedIds;
|
|
4971
|
+
}
|
|
4972
|
+
function hasKnowledgeEdge(nodeId, store) {
|
|
4973
|
+
for (const edgeType of KNOWLEDGE_EDGE_TYPES) {
|
|
4974
|
+
if (store.getEdges({ to: nodeId, type: edgeType }).length > 0) return true;
|
|
4975
|
+
}
|
|
4976
|
+
return false;
|
|
4977
|
+
}
|
|
4978
|
+
function computeSourceBreakdown(knEntries) {
|
|
4979
|
+
const breakdown = {};
|
|
4980
|
+
for (const kn of knEntries) {
|
|
4981
|
+
const src = kn.metadata.source ?? "unknown";
|
|
4982
|
+
breakdown[src] = (breakdown[src] ?? 0) + 1;
|
|
4983
|
+
}
|
|
4984
|
+
return breakdown;
|
|
4985
|
+
}
|
|
4986
|
+
function computeDomainScore(knowledgeEntries, codeEntities, linkedEntities, uniqueSources) {
|
|
4987
|
+
const codeCoverageComponent = codeEntities > 0 ? linkedEntities / codeEntities * 60 : 0;
|
|
4988
|
+
const knowledgeDepthComponent = Math.min(knowledgeEntries / 10, 1) * 20;
|
|
4989
|
+
const sourceDiversityComponent = Math.min(uniqueSources / 3, 1) * 20;
|
|
4990
|
+
return Math.round(codeCoverageComponent + knowledgeDepthComponent + sourceDiversityComponent);
|
|
4991
|
+
}
|
|
4992
|
+
function scoreDomain(domain, knEntries, codeEntries, store) {
|
|
4993
|
+
const linkedIds = countLinkedEntities(codeEntries, store);
|
|
4994
|
+
const sourceBreakdown = computeSourceBreakdown(knEntries);
|
|
4995
|
+
const codeEntities = codeEntries.length;
|
|
4996
|
+
const linkedEntities = linkedIds.size;
|
|
4997
|
+
const knowledgeEntries = knEntries.length;
|
|
4998
|
+
const uniqueSources = Object.keys(sourceBreakdown).length;
|
|
4999
|
+
const score = computeDomainScore(knowledgeEntries, codeEntities, linkedEntities, uniqueSources);
|
|
5000
|
+
return {
|
|
5001
|
+
domain,
|
|
5002
|
+
score,
|
|
5003
|
+
knowledgeEntries,
|
|
5004
|
+
codeEntities,
|
|
5005
|
+
linkedEntities,
|
|
5006
|
+
unlinkedEntities: codeEntities - linkedEntities,
|
|
5007
|
+
sourceBreakdown,
|
|
5008
|
+
grade: toGrade(score)
|
|
5009
|
+
};
|
|
5010
|
+
}
|
|
4609
5011
|
var CoverageScorer = class {
|
|
4610
5012
|
score(store) {
|
|
4611
5013
|
const knowledgeNodes = KNOWLEDGE_TYPES.flatMap((t) => store.findNodes({ type: t }));
|
|
4612
|
-
const domainMap =
|
|
4613
|
-
for (const node of knowledgeNodes) {
|
|
4614
|
-
const domain = node.metadata.domain ?? "unclassified";
|
|
4615
|
-
const group = domainMap.get(domain) ?? [];
|
|
4616
|
-
group.push(node);
|
|
4617
|
-
domainMap.set(domain, group);
|
|
4618
|
-
}
|
|
5014
|
+
const domainMap = groupByDomain(knowledgeNodes, () => "unclassified");
|
|
4619
5015
|
const codeNodes = CODE_TYPES2.flatMap((t) => store.findNodes({ type: t }));
|
|
4620
|
-
const codeDomains =
|
|
4621
|
-
for (const node of codeNodes) {
|
|
4622
|
-
const domain = node.metadata.domain ?? this.domainFromPath(node.path);
|
|
4623
|
-
const group = codeDomains.get(domain) ?? [];
|
|
4624
|
-
group.push(node);
|
|
4625
|
-
codeDomains.set(domain, group);
|
|
4626
|
-
}
|
|
5016
|
+
const codeDomains = groupByDomain(codeNodes, (n) => this.domainFromPath(n.path));
|
|
4627
5017
|
const allDomains = /* @__PURE__ */ new Set([...domainMap.keys(), ...codeDomains.keys()]);
|
|
4628
5018
|
const domains = [];
|
|
4629
5019
|
for (const domain of allDomains) {
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
const linkedIds = /* @__PURE__ */ new Set();
|
|
4633
|
-
for (const codeNode of codeEntries) {
|
|
4634
|
-
for (const edgeType of KNOWLEDGE_EDGE_TYPES) {
|
|
4635
|
-
const edges = store.getEdges({ to: codeNode.id, type: edgeType });
|
|
4636
|
-
if (edges.length > 0) {
|
|
4637
|
-
linkedIds.add(codeNode.id);
|
|
4638
|
-
break;
|
|
4639
|
-
}
|
|
4640
|
-
}
|
|
4641
|
-
}
|
|
4642
|
-
const sourceBreakdown = {};
|
|
4643
|
-
for (const kn of knEntries) {
|
|
4644
|
-
const src = kn.metadata.source ?? "unknown";
|
|
4645
|
-
sourceBreakdown[src] = (sourceBreakdown[src] ?? 0) + 1;
|
|
4646
|
-
}
|
|
4647
|
-
const codeEntities = codeEntries.length;
|
|
4648
|
-
const linkedEntities = linkedIds.size;
|
|
4649
|
-
const knowledgeEntries = knEntries.length;
|
|
4650
|
-
const uniqueSources = Object.keys(sourceBreakdown).length;
|
|
4651
|
-
const codeCoverageComponent = codeEntities > 0 ? linkedEntities / codeEntities * 60 : 0;
|
|
4652
|
-
const knowledgeDepthComponent = Math.min(knowledgeEntries / 10, 1) * 20;
|
|
4653
|
-
const sourceDiversityComponent = Math.min(uniqueSources / 3, 1) * 20;
|
|
4654
|
-
const score = Math.round(
|
|
4655
|
-
codeCoverageComponent + knowledgeDepthComponent + sourceDiversityComponent
|
|
5020
|
+
domains.push(
|
|
5021
|
+
scoreDomain(domain, domainMap.get(domain) ?? [], codeDomains.get(domain) ?? [], store)
|
|
4656
5022
|
);
|
|
4657
|
-
domains.push({
|
|
4658
|
-
domain,
|
|
4659
|
-
score,
|
|
4660
|
-
knowledgeEntries,
|
|
4661
|
-
codeEntities,
|
|
4662
|
-
linkedEntities,
|
|
4663
|
-
unlinkedEntities: codeEntities - linkedEntities,
|
|
4664
|
-
sourceBreakdown,
|
|
4665
|
-
grade: toGrade(score)
|
|
4666
|
-
});
|
|
4667
5023
|
}
|
|
4668
5024
|
const overallScore = domains.length > 0 ? Math.round(domains.reduce((sum, d) => sum + d.score, 0) / domains.length) : 0;
|
|
4669
5025
|
return {
|
|
@@ -4684,8 +5040,166 @@ var CoverageScorer = class {
|
|
|
4684
5040
|
}
|
|
4685
5041
|
};
|
|
4686
5042
|
|
|
5043
|
+
// src/ingest/KnowledgeDocMaterializer.ts
|
|
5044
|
+
import * as fs10 from "fs/promises";
|
|
5045
|
+
import * as path11 from "path";
|
|
5046
|
+
var VALID_BUSINESS_TYPES = /* @__PURE__ */ new Set([
|
|
5047
|
+
"business_rule",
|
|
5048
|
+
"business_process",
|
|
5049
|
+
"business_concept",
|
|
5050
|
+
"business_term",
|
|
5051
|
+
"business_metric"
|
|
5052
|
+
]);
|
|
5053
|
+
var DEFAULT_MAX_DOCS = 50;
|
|
5054
|
+
var MAX_COLLISION_SUFFIX = 10;
|
|
5055
|
+
var KnowledgeDocMaterializer = class {
|
|
5056
|
+
constructor(store) {
|
|
5057
|
+
this.store = store;
|
|
5058
|
+
}
|
|
5059
|
+
store;
|
|
5060
|
+
async materialize(gapEntries, options) {
|
|
5061
|
+
const maxDocs = options.maxDocs ?? DEFAULT_MAX_DOCS;
|
|
5062
|
+
const created = [];
|
|
5063
|
+
const skipped = [];
|
|
5064
|
+
const createdNames = /* @__PURE__ */ new Set();
|
|
5065
|
+
for (const entry of gapEntries) {
|
|
5066
|
+
const resolved = await this.resolveEntry(
|
|
5067
|
+
entry,
|
|
5068
|
+
created.length,
|
|
5069
|
+
maxDocs,
|
|
5070
|
+
createdNames,
|
|
5071
|
+
options
|
|
5072
|
+
);
|
|
5073
|
+
if ("reason" in resolved) {
|
|
5074
|
+
skipped.push(resolved);
|
|
5075
|
+
continue;
|
|
5076
|
+
}
|
|
5077
|
+
const { node, domain, domainDir, nameKey } = resolved;
|
|
5078
|
+
await fs10.mkdir(domainDir, { recursive: true });
|
|
5079
|
+
const basename10 = this.generateFilename(entry.name);
|
|
5080
|
+
const filename = await this.resolveCollision(domainDir, basename10);
|
|
5081
|
+
const content = this.formatDoc(node, domain);
|
|
5082
|
+
const filePath = ["docs", "knowledge", domain, filename].join("/");
|
|
5083
|
+
await fs10.writeFile(path11.join(options.projectDir, filePath), content, "utf-8");
|
|
5084
|
+
createdNames.add(nameKey);
|
|
5085
|
+
created.push({ filePath, nodeId: entry.nodeId, domain, name: entry.name });
|
|
5086
|
+
}
|
|
5087
|
+
return { created, skipped };
|
|
5088
|
+
}
|
|
5089
|
+
async resolveEntry(entry, createdCount, maxDocs, createdNames, options) {
|
|
5090
|
+
if (!entry.hasContent) {
|
|
5091
|
+
return { nodeId: entry.nodeId, name: entry.name, reason: "no_content" };
|
|
5092
|
+
}
|
|
5093
|
+
const node = this.store.getNode(entry.nodeId);
|
|
5094
|
+
if (!node || !node.content || node.content.trim().length < 10) {
|
|
5095
|
+
return { nodeId: entry.nodeId, name: entry.name, reason: "no_content" };
|
|
5096
|
+
}
|
|
5097
|
+
const domain = this.inferDomain(node);
|
|
5098
|
+
if (!domain || /[/\\]|\.\.|\0/.test(domain)) {
|
|
5099
|
+
return { nodeId: entry.nodeId, name: entry.name, reason: "no_domain" };
|
|
5100
|
+
}
|
|
5101
|
+
if (createdCount >= maxDocs) {
|
|
5102
|
+
return { nodeId: entry.nodeId, name: entry.name, reason: "cap_reached" };
|
|
5103
|
+
}
|
|
5104
|
+
const domainDir = path11.join(options.projectDir, "docs", "knowledge", domain);
|
|
5105
|
+
const nameKey = `${domain}:${entry.name.toLowerCase().trim()}`;
|
|
5106
|
+
if (!createdNames.has(nameKey) && await this.hasExistingDoc(domainDir, entry.name)) {
|
|
5107
|
+
return { nodeId: entry.nodeId, name: entry.name, reason: "already_documented" };
|
|
5108
|
+
}
|
|
5109
|
+
if (options.dryRun) {
|
|
5110
|
+
return { nodeId: entry.nodeId, name: entry.name, reason: "dry_run" };
|
|
5111
|
+
}
|
|
5112
|
+
return { node, domain, domainDir, nameKey };
|
|
5113
|
+
}
|
|
5114
|
+
inferDomain(node) {
|
|
5115
|
+
if (node.metadata?.domain && typeof node.metadata.domain === "string") {
|
|
5116
|
+
return node.metadata.domain;
|
|
5117
|
+
}
|
|
5118
|
+
if (node.path) {
|
|
5119
|
+
const pkgMatch = node.path.match(/^packages\/([^/]+)/);
|
|
5120
|
+
if (pkgMatch) return pkgMatch[1];
|
|
5121
|
+
const srcMatch = node.path.match(/^src\/([^/]+)/);
|
|
5122
|
+
if (srcMatch) return srcMatch[1];
|
|
5123
|
+
}
|
|
5124
|
+
if (node.metadata?.source === "knowledge-linker" || node.metadata?.source === "connector") {
|
|
5125
|
+
const connector = node.metadata.connectorName;
|
|
5126
|
+
if (typeof connector === "string") return connector;
|
|
5127
|
+
return "general";
|
|
5128
|
+
}
|
|
5129
|
+
return null;
|
|
5130
|
+
}
|
|
5131
|
+
generateFilename(name) {
|
|
5132
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
|
|
5133
|
+
return `${slug}.md`;
|
|
5134
|
+
}
|
|
5135
|
+
async resolveCollision(dir, basename10) {
|
|
5136
|
+
const ext = path11.extname(basename10);
|
|
5137
|
+
const stem = path11.basename(basename10, ext);
|
|
5138
|
+
try {
|
|
5139
|
+
await fs10.access(path11.join(dir, basename10));
|
|
5140
|
+
} catch {
|
|
5141
|
+
return basename10;
|
|
5142
|
+
}
|
|
5143
|
+
for (let i = 2; i <= MAX_COLLISION_SUFFIX; i++) {
|
|
5144
|
+
const candidate = `${stem}-${i}${ext}`;
|
|
5145
|
+
try {
|
|
5146
|
+
await fs10.access(path11.join(dir, candidate));
|
|
5147
|
+
} catch {
|
|
5148
|
+
return candidate;
|
|
5149
|
+
}
|
|
5150
|
+
}
|
|
5151
|
+
throw new Error(
|
|
5152
|
+
`Cannot resolve filename collision for "${basename10}" after ${MAX_COLLISION_SUFFIX} attempts`
|
|
5153
|
+
);
|
|
5154
|
+
}
|
|
5155
|
+
formatDoc(node, domain) {
|
|
5156
|
+
const mappedType = this.mapNodeType(node);
|
|
5157
|
+
const sanitize = (s) => s.replace(/[\n\r]/g, " ").replace(/:/g, "-");
|
|
5158
|
+
const lines = ["---", `type: ${sanitize(mappedType)}`, `domain: ${sanitize(domain)}`];
|
|
5159
|
+
const tags = node.metadata?.tags;
|
|
5160
|
+
if (Array.isArray(tags) && tags.length > 0) {
|
|
5161
|
+
lines.push(`tags: [${tags.map((t) => sanitize(String(t))).join(", ")}]`);
|
|
5162
|
+
}
|
|
5163
|
+
const related = node.metadata?.related;
|
|
5164
|
+
if (Array.isArray(related) && related.length > 0) {
|
|
5165
|
+
lines.push(`related: [${related.map((r) => sanitize(String(r))).join(", ")}]`);
|
|
5166
|
+
}
|
|
5167
|
+
const title = (node.name ?? "").replace(/[\n\r]/g, " ");
|
|
5168
|
+
lines.push("---", "", `# ${title}`, "", node.content ?? "", "");
|
|
5169
|
+
return lines.join("\n");
|
|
5170
|
+
}
|
|
5171
|
+
/** Check if a doc with a matching title already exists in the domain directory. */
|
|
5172
|
+
async hasExistingDoc(domainDir, name) {
|
|
5173
|
+
const normalizedName = name.toLowerCase().trim();
|
|
5174
|
+
try {
|
|
5175
|
+
const files = await fs10.readdir(domainDir);
|
|
5176
|
+
for (const file of files) {
|
|
5177
|
+
if (!file.endsWith(".md")) continue;
|
|
5178
|
+
const raw = await fs10.readFile(path11.join(domainDir, file), "utf-8");
|
|
5179
|
+
const fmMatch = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
|
|
5180
|
+
const body = fmMatch ? fmMatch[1] : raw;
|
|
5181
|
+
const titleMatch = body.match(/^#\s+(.+)$/m);
|
|
5182
|
+
if (titleMatch && titleMatch[1].trim().toLowerCase() === normalizedName) {
|
|
5183
|
+
return true;
|
|
5184
|
+
}
|
|
5185
|
+
}
|
|
5186
|
+
} catch {
|
|
5187
|
+
}
|
|
5188
|
+
return false;
|
|
5189
|
+
}
|
|
5190
|
+
mapNodeType(node) {
|
|
5191
|
+
if (VALID_BUSINESS_TYPES.has(node.type)) {
|
|
5192
|
+
return node.type;
|
|
5193
|
+
}
|
|
5194
|
+
if (node.type === "business_fact") {
|
|
5195
|
+
return "business_rule";
|
|
5196
|
+
}
|
|
5197
|
+
return "business_concept";
|
|
5198
|
+
}
|
|
5199
|
+
};
|
|
5200
|
+
|
|
4687
5201
|
// src/ingest/KnowledgePipelineRunner.ts
|
|
4688
|
-
var
|
|
5202
|
+
var BUSINESS_NODE_TYPES2 = [
|
|
4689
5203
|
"business_concept",
|
|
4690
5204
|
"business_rule",
|
|
4691
5205
|
"business_process",
|
|
@@ -4694,7 +5208,8 @@ var BUSINESS_NODE_TYPES = [
|
|
|
4694
5208
|
"business_fact"
|
|
4695
5209
|
];
|
|
4696
5210
|
var SNAPSHOT_NODE_TYPES = [
|
|
4697
|
-
...
|
|
5211
|
+
...BUSINESS_NODE_TYPES2,
|
|
5212
|
+
"decision",
|
|
4698
5213
|
"design_token",
|
|
4699
5214
|
"design_constraint",
|
|
4700
5215
|
"aesthetic_intent",
|
|
@@ -4706,51 +5221,98 @@ var KnowledgePipelineRunner = class {
|
|
|
4706
5221
|
}
|
|
4707
5222
|
store;
|
|
4708
5223
|
async run(options) {
|
|
4709
|
-
const maxIterations = options.maxIterations ?? 5;
|
|
4710
5224
|
const remediations = [];
|
|
4711
5225
|
const preSnapshot = this.buildSnapshot(options.domain);
|
|
4712
5226
|
const extraction = await this.extract(options);
|
|
4713
5227
|
const postSnapshot = this.buildSnapshot(options.domain);
|
|
4714
5228
|
let driftResult = this.reconcile(preSnapshot, postSnapshot);
|
|
4715
|
-
const
|
|
4716
|
-
const contradictions = contradictionDetector.detect(this.store);
|
|
5229
|
+
const contradictions = new ContradictionDetector().detect(this.store);
|
|
4717
5230
|
let gapReport = await this.detect(options);
|
|
4718
|
-
const
|
|
4719
|
-
|
|
5231
|
+
const coverage = new CoverageScorer().score(this.store);
|
|
5232
|
+
let materialization;
|
|
4720
5233
|
let iterations = 1;
|
|
4721
5234
|
if (options.fix) {
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
}
|
|
5235
|
+
const loopResult = await this.runRemediationLoop(
|
|
5236
|
+
options,
|
|
5237
|
+
driftResult,
|
|
5238
|
+
gapReport,
|
|
5239
|
+
remediations
|
|
5240
|
+
);
|
|
5241
|
+
iterations = loopResult.iterations;
|
|
5242
|
+
materialization = loopResult.materialization;
|
|
5243
|
+
}
|
|
5244
|
+
if (options.fix && iterations > 1) {
|
|
5245
|
+
const finalSnapshot = this.buildSnapshot(options.domain);
|
|
5246
|
+
driftResult = this.reconcile(preSnapshot, finalSnapshot);
|
|
5247
|
+
gapReport = await this.detect(options);
|
|
4736
5248
|
}
|
|
4737
5249
|
await this.stageNewFindings(driftResult, options);
|
|
5250
|
+
return this.buildResult(
|
|
5251
|
+
driftResult,
|
|
5252
|
+
iterations,
|
|
5253
|
+
extraction,
|
|
5254
|
+
gapReport,
|
|
5255
|
+
remediations,
|
|
5256
|
+
contradictions,
|
|
5257
|
+
coverage,
|
|
5258
|
+
materialization
|
|
5259
|
+
);
|
|
5260
|
+
}
|
|
5261
|
+
/** Run the remediation convergence loop; returns iteration count and accumulated materialization. */
|
|
5262
|
+
async runRemediationLoop(options, driftResult, gapReport, remediations) {
|
|
5263
|
+
const maxIterations = options.maxIterations ?? 5;
|
|
5264
|
+
let iterations = 1;
|
|
5265
|
+
let currentDrift = driftResult;
|
|
5266
|
+
let currentGapReport = gapReport;
|
|
5267
|
+
let previousIssueCount = currentDrift.findings.length + currentGapReport.totalGaps;
|
|
5268
|
+
let accumulatedMaterialization;
|
|
5269
|
+
while (iterations < maxIterations) {
|
|
5270
|
+
if (currentDrift.findings.length === 0 && currentGapReport.totalGaps === 0) break;
|
|
5271
|
+
const matResult = await this.remediate(currentDrift, remediations, options, currentGapReport);
|
|
5272
|
+
if (matResult) {
|
|
5273
|
+
if (!accumulatedMaterialization) {
|
|
5274
|
+
accumulatedMaterialization = matResult;
|
|
5275
|
+
} else {
|
|
5276
|
+
accumulatedMaterialization = {
|
|
5277
|
+
created: [...accumulatedMaterialization.created, ...matResult.created],
|
|
5278
|
+
skipped: [...accumulatedMaterialization.skipped, ...matResult.skipped]
|
|
5279
|
+
};
|
|
5280
|
+
}
|
|
5281
|
+
}
|
|
5282
|
+
const preSnapshot = this.buildSnapshot(options.domain);
|
|
5283
|
+
await this.extract(options);
|
|
5284
|
+
const postSnapshot = this.buildSnapshot(options.domain);
|
|
5285
|
+
currentDrift = this.reconcile(preSnapshot, postSnapshot);
|
|
5286
|
+
currentGapReport = await this.detect(options);
|
|
5287
|
+
iterations++;
|
|
5288
|
+
const currentIssueCount = currentDrift.findings.length + currentGapReport.totalGaps;
|
|
5289
|
+
if (currentIssueCount >= previousIssueCount) break;
|
|
5290
|
+
previousIssueCount = currentIssueCount;
|
|
5291
|
+
}
|
|
5292
|
+
return {
|
|
5293
|
+
iterations,
|
|
5294
|
+
...accumulatedMaterialization ? { materialization: accumulatedMaterialization } : {}
|
|
5295
|
+
};
|
|
5296
|
+
}
|
|
5297
|
+
/** Assemble the final pipeline result. */
|
|
5298
|
+
buildResult(driftResult, iterations, extraction, gaps, remediations, contradictions, coverage, materialization) {
|
|
4738
5299
|
return {
|
|
4739
5300
|
verdict: this.computeVerdict(driftResult),
|
|
4740
5301
|
driftScore: driftResult.driftScore,
|
|
4741
5302
|
iterations,
|
|
4742
5303
|
findings: driftResult.summary,
|
|
4743
5304
|
extraction,
|
|
4744
|
-
gaps
|
|
5305
|
+
gaps,
|
|
4745
5306
|
remediations,
|
|
4746
5307
|
contradictions,
|
|
4747
|
-
coverage
|
|
5308
|
+
coverage,
|
|
5309
|
+
...materialization ? { materialization } : {}
|
|
4748
5310
|
};
|
|
4749
5311
|
}
|
|
4750
5312
|
// ── Phase 1: EXTRACT ──────────────────────────────────────────────────────
|
|
4751
5313
|
async extract(options) {
|
|
4752
|
-
const extractedDir =
|
|
4753
|
-
await
|
|
5314
|
+
const extractedDir = path12.join(options.projectDir, ".harness", "knowledge", "extracted");
|
|
5315
|
+
await fs11.mkdir(extractedDir, { recursive: true });
|
|
4754
5316
|
const runner = createExtractionRunner();
|
|
4755
5317
|
const extractionResult = await runner.run(options.projectDir, this.store, extractedDir);
|
|
4756
5318
|
const diagramParser = new DiagramParser(this.store);
|
|
@@ -4764,7 +5326,7 @@ var KnowledgePipelineRunner = class {
|
|
|
4764
5326
|
const imageResult = await imageExtractor.analyze(this.store, imagePaths);
|
|
4765
5327
|
imageCount = imageResult.nodesAdded;
|
|
4766
5328
|
}
|
|
4767
|
-
const knowledgeDir =
|
|
5329
|
+
const knowledgeDir = path12.join(options.projectDir, "docs", "knowledge");
|
|
4768
5330
|
const bkIngestor = new BusinessKnowledgeIngestor(this.store);
|
|
4769
5331
|
let bkResult;
|
|
4770
5332
|
try {
|
|
@@ -4779,6 +5341,21 @@ var KnowledgePipelineRunner = class {
|
|
|
4779
5341
|
durationMs: 0
|
|
4780
5342
|
};
|
|
4781
5343
|
}
|
|
5344
|
+
const decisionsDir = path12.join(options.projectDir, "docs", "knowledge", "decisions");
|
|
5345
|
+
const decisionIngestor = new DecisionIngestor(this.store);
|
|
5346
|
+
let decisionResult;
|
|
5347
|
+
try {
|
|
5348
|
+
decisionResult = await decisionIngestor.ingest(decisionsDir);
|
|
5349
|
+
} catch {
|
|
5350
|
+
decisionResult = {
|
|
5351
|
+
nodesAdded: 0,
|
|
5352
|
+
nodesUpdated: 0,
|
|
5353
|
+
edgesAdded: 0,
|
|
5354
|
+
edgesUpdated: 0,
|
|
5355
|
+
errors: [],
|
|
5356
|
+
durationMs: 0
|
|
5357
|
+
};
|
|
5358
|
+
}
|
|
4782
5359
|
const linker = new KnowledgeLinker(this.store, extractedDir);
|
|
4783
5360
|
const linkResult = await linker.link();
|
|
4784
5361
|
return {
|
|
@@ -4786,6 +5363,7 @@ var KnowledgePipelineRunner = class {
|
|
|
4786
5363
|
diagrams: diagramResult.nodesAdded,
|
|
4787
5364
|
linkerFacts: linkResult.factsCreated,
|
|
4788
5365
|
businessKnowledge: bkResult.nodesAdded,
|
|
5366
|
+
decisions: decisionResult.nodesAdded,
|
|
4789
5367
|
images: imageCount
|
|
4790
5368
|
};
|
|
4791
5369
|
}
|
|
@@ -4812,14 +5390,14 @@ var KnowledgePipelineRunner = class {
|
|
|
4812
5390
|
}
|
|
4813
5391
|
// ── Phase 3: DETECT ───────────────────────────────────────────────────────
|
|
4814
5392
|
async detect(options) {
|
|
4815
|
-
const knowledgeDir =
|
|
5393
|
+
const knowledgeDir = path12.join(options.projectDir, "docs", "knowledge");
|
|
4816
5394
|
const aggregator = new KnowledgeStagingAggregator(options.projectDir);
|
|
4817
|
-
const gapReport = await aggregator.generateGapReport(knowledgeDir);
|
|
5395
|
+
const gapReport = await aggregator.generateGapReport(knowledgeDir, this.store);
|
|
4818
5396
|
await aggregator.writeGapReport(gapReport);
|
|
4819
5397
|
return gapReport;
|
|
4820
5398
|
}
|
|
4821
5399
|
// ── Phase 4: REMEDIATE ────────────────────────────────────────────────────
|
|
4822
|
-
remediate(driftResult, remediations, options) {
|
|
5400
|
+
async remediate(driftResult, remediations, options, gapReport) {
|
|
4823
5401
|
for (const finding of driftResult.findings) {
|
|
4824
5402
|
switch (finding.classification) {
|
|
4825
5403
|
case "stale":
|
|
@@ -4837,6 +5415,22 @@ var KnowledgePipelineRunner = class {
|
|
|
4837
5415
|
break;
|
|
4838
5416
|
}
|
|
4839
5417
|
}
|
|
5418
|
+
if (!options.ci) {
|
|
5419
|
+
const allGapEntries = gapReport.domains.flatMap((d) => d.gapEntries);
|
|
5420
|
+
const materializable = allGapEntries.filter((e) => e.hasContent);
|
|
5421
|
+
if (materializable.length > 0) {
|
|
5422
|
+
const materializer = new KnowledgeDocMaterializer(this.store);
|
|
5423
|
+
const matResult = await materializer.materialize(materializable, {
|
|
5424
|
+
projectDir: options.projectDir,
|
|
5425
|
+
dryRun: false
|
|
5426
|
+
});
|
|
5427
|
+
for (const doc of matResult.created) {
|
|
5428
|
+
remediations.push(`created doc: ${doc.filePath}`);
|
|
5429
|
+
}
|
|
5430
|
+
return matResult;
|
|
5431
|
+
}
|
|
5432
|
+
}
|
|
5433
|
+
return void 0;
|
|
4840
5434
|
}
|
|
4841
5435
|
async stageNewFindings(driftResult, options) {
|
|
4842
5436
|
const newFindings = driftResult.findings.filter((f) => f.classification === "new");
|
|
@@ -4871,7 +5465,7 @@ var KnowledgePipelineRunner = class {
|
|
|
4871
5465
|
};
|
|
4872
5466
|
|
|
4873
5467
|
// src/ingest/connectors/ConnectorUtils.ts
|
|
4874
|
-
var
|
|
5468
|
+
var CODE_NODE_TYPES5 = ["file", "function", "class", "method", "interface", "variable"];
|
|
4875
5469
|
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
4876
5470
|
function withRetry(client, options) {
|
|
4877
5471
|
const maxRetries = options?.maxRetries ?? 3;
|
|
@@ -4936,7 +5530,7 @@ function sanitizeExternalText(text, maxLength = 2e3) {
|
|
|
4936
5530
|
}
|
|
4937
5531
|
function linkToCode(store, content, sourceNodeId, edgeType, options) {
|
|
4938
5532
|
let edgesCreated = 0;
|
|
4939
|
-
for (const type of
|
|
5533
|
+
for (const type of CODE_NODE_TYPES5) {
|
|
4940
5534
|
const nodes = store.findNodes({ type });
|
|
4941
5535
|
for (const node of nodes) {
|
|
4942
5536
|
if (node.name.length < 3) continue;
|
|
@@ -4956,12 +5550,12 @@ function linkToCode(store, content, sourceNodeId, edgeType, options) {
|
|
|
4956
5550
|
}
|
|
4957
5551
|
|
|
4958
5552
|
// src/ingest/connectors/SyncManager.ts
|
|
4959
|
-
import * as
|
|
4960
|
-
import * as
|
|
5553
|
+
import * as fs12 from "fs/promises";
|
|
5554
|
+
import * as path13 from "path";
|
|
4961
5555
|
var SyncManager = class {
|
|
4962
5556
|
constructor(store, graphDir) {
|
|
4963
5557
|
this.store = store;
|
|
4964
|
-
this.metadataPath =
|
|
5558
|
+
this.metadataPath = path13.join(graphDir, "sync-metadata.json");
|
|
4965
5559
|
}
|
|
4966
5560
|
store;
|
|
4967
5561
|
registrations = /* @__PURE__ */ new Map();
|
|
@@ -5026,15 +5620,15 @@ var SyncManager = class {
|
|
|
5026
5620
|
}
|
|
5027
5621
|
async loadMetadata() {
|
|
5028
5622
|
try {
|
|
5029
|
-
const raw = await
|
|
5623
|
+
const raw = await fs12.readFile(this.metadataPath, "utf-8");
|
|
5030
5624
|
return JSON.parse(raw);
|
|
5031
5625
|
} catch {
|
|
5032
5626
|
return { connectors: {} };
|
|
5033
5627
|
}
|
|
5034
5628
|
}
|
|
5035
5629
|
async saveMetadata(metadata) {
|
|
5036
|
-
await
|
|
5037
|
-
await
|
|
5630
|
+
await fs12.mkdir(path13.dirname(this.metadataPath), { recursive: true });
|
|
5631
|
+
await fs12.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
5038
5632
|
}
|
|
5039
5633
|
};
|
|
5040
5634
|
|
|
@@ -5139,6 +5733,14 @@ var JiraConnector = class {
|
|
|
5139
5733
|
start
|
|
5140
5734
|
);
|
|
5141
5735
|
}
|
|
5736
|
+
try {
|
|
5737
|
+
const parsed = new URL(baseUrl);
|
|
5738
|
+
if (parsed.protocol !== "https:" && parsed.hostname !== "localhost") {
|
|
5739
|
+
return buildIngestResult(0, 0, [`${baseUrlEnv} must use HTTPS`], start);
|
|
5740
|
+
}
|
|
5741
|
+
} catch {
|
|
5742
|
+
return buildIngestResult(0, 0, [`${baseUrlEnv} is not a valid URL`], start);
|
|
5743
|
+
}
|
|
5142
5744
|
const jql = buildJql(config);
|
|
5143
5745
|
const headers = { Authorization: `Basic ${apiKey}`, "Content-Type": "application/json" };
|
|
5144
5746
|
try {
|
|
@@ -5428,6 +6030,14 @@ var ConfluenceConnector = class {
|
|
|
5428
6030
|
}
|
|
5429
6031
|
const baseUrlEnv = config.baseUrlEnv ?? "CONFLUENCE_BASE_URL";
|
|
5430
6032
|
const baseUrl = process.env[baseUrlEnv] ?? "";
|
|
6033
|
+
try {
|
|
6034
|
+
const parsed = new URL(baseUrl);
|
|
6035
|
+
if (parsed.protocol !== "https:" && parsed.hostname !== "localhost") {
|
|
6036
|
+
return missingApiKeyResult(`${baseUrlEnv} must use HTTPS`, start);
|
|
6037
|
+
}
|
|
6038
|
+
} catch {
|
|
6039
|
+
return missingApiKeyResult(`${baseUrlEnv} is not a valid URL`, start);
|
|
6040
|
+
}
|
|
5431
6041
|
const spaceKey = config.spaceKey ?? "";
|
|
5432
6042
|
const counts = await this.fetchAllPagesHandled(
|
|
5433
6043
|
store,
|
|
@@ -5521,7 +6131,7 @@ var ConfluenceConnector = class {
|
|
|
5521
6131
|
};
|
|
5522
6132
|
|
|
5523
6133
|
// src/ingest/connectors/CIConnector.ts
|
|
5524
|
-
function
|
|
6134
|
+
function emptyResult5(errors, start) {
|
|
5525
6135
|
return {
|
|
5526
6136
|
nodesAdded: 0,
|
|
5527
6137
|
nodesUpdated: 0,
|
|
@@ -5589,7 +6199,7 @@ var CIConnector = class {
|
|
|
5589
6199
|
const apiKeyEnv = config.apiKeyEnv ?? "GITHUB_TOKEN";
|
|
5590
6200
|
const apiKey = process.env[apiKeyEnv];
|
|
5591
6201
|
if (!apiKey) {
|
|
5592
|
-
return
|
|
6202
|
+
return emptyResult5(
|
|
5593
6203
|
[`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
5594
6204
|
start
|
|
5595
6205
|
);
|
|
@@ -5653,6 +6263,14 @@ function hasConstraintKeyword(text) {
|
|
|
5653
6263
|
const lower = text.toLowerCase();
|
|
5654
6264
|
return CONSTRAINT_KEYWORDS.some((kw) => lower.includes(kw));
|
|
5655
6265
|
}
|
|
6266
|
+
function buildNodeMetadata(condensed, base) {
|
|
6267
|
+
const metadata = { ...base };
|
|
6268
|
+
if (condensed.method !== "passthrough") {
|
|
6269
|
+
metadata.condensed = condensed.method;
|
|
6270
|
+
metadata.originalLength = condensed.originalLength;
|
|
6271
|
+
}
|
|
6272
|
+
return metadata;
|
|
6273
|
+
}
|
|
5656
6274
|
function buildIngestResult2(nodesAdded, edgesAdded, errors, start) {
|
|
5657
6275
|
return {
|
|
5658
6276
|
nodesAdded,
|
|
@@ -5663,6 +6281,24 @@ function buildIngestResult2(nodesAdded, edgesAdded, errors, start) {
|
|
|
5663
6281
|
durationMs: Date.now() - start
|
|
5664
6282
|
};
|
|
5665
6283
|
}
|
|
6284
|
+
function validateFigmaConfig(config) {
|
|
6285
|
+
const apiKeyEnv = config.apiKeyEnv ?? "FIGMA_API_KEY";
|
|
6286
|
+
const apiKey = process.env[apiKeyEnv];
|
|
6287
|
+
if (!apiKey) return { error: `Missing API key: environment variable "${apiKeyEnv}" is not set` };
|
|
6288
|
+
const baseUrlEnv = config.baseUrlEnv ?? "FIGMA_BASE_URL";
|
|
6289
|
+
const baseUrl = process.env[baseUrlEnv] ?? "https://api.figma.com";
|
|
6290
|
+
try {
|
|
6291
|
+
const parsed = new URL(baseUrl);
|
|
6292
|
+
if (parsed.protocol !== "https:" || !parsed.hostname.endsWith("api.figma.com")) {
|
|
6293
|
+
return { error: `Invalid ${baseUrlEnv}: must be an HTTPS URL on api.figma.com` };
|
|
6294
|
+
}
|
|
6295
|
+
} catch {
|
|
6296
|
+
return { error: `Invalid ${baseUrlEnv}: not a valid URL` };
|
|
6297
|
+
}
|
|
6298
|
+
const fileIds = config.fileIds;
|
|
6299
|
+
if (!fileIds || fileIds.length === 0) return { error: "No fileIds provided in connector config" };
|
|
6300
|
+
return { apiKey, baseUrl, fileIds };
|
|
6301
|
+
}
|
|
5666
6302
|
var FigmaConnector = class {
|
|
5667
6303
|
name = "figma";
|
|
5668
6304
|
source = "figma";
|
|
@@ -5672,35 +6308,9 @@ var FigmaConnector = class {
|
|
|
5672
6308
|
}
|
|
5673
6309
|
async ingest(store, config) {
|
|
5674
6310
|
const start = Date.now();
|
|
5675
|
-
const
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
return buildIngestResult2(
|
|
5679
|
-
0,
|
|
5680
|
-
0,
|
|
5681
|
-
[`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
5682
|
-
start
|
|
5683
|
-
);
|
|
5684
|
-
}
|
|
5685
|
-
const baseUrlEnv = config.baseUrlEnv ?? "FIGMA_BASE_URL";
|
|
5686
|
-
const baseUrl = process.env[baseUrlEnv] ?? "https://api.figma.com";
|
|
5687
|
-
try {
|
|
5688
|
-
const parsed = new URL(baseUrl);
|
|
5689
|
-
if (parsed.protocol !== "https:" || !parsed.hostname.endsWith("api.figma.com")) {
|
|
5690
|
-
return buildIngestResult2(
|
|
5691
|
-
0,
|
|
5692
|
-
0,
|
|
5693
|
-
[`Invalid ${baseUrlEnv}: must be an HTTPS URL on api.figma.com`],
|
|
5694
|
-
start
|
|
5695
|
-
);
|
|
5696
|
-
}
|
|
5697
|
-
} catch {
|
|
5698
|
-
return buildIngestResult2(0, 0, [`Invalid ${baseUrlEnv}: not a valid URL`], start);
|
|
5699
|
-
}
|
|
5700
|
-
const fileIds = config.fileIds;
|
|
5701
|
-
if (!fileIds || fileIds.length === 0) {
|
|
5702
|
-
return buildIngestResult2(0, 0, ["No fileIds provided in connector config"], start);
|
|
5703
|
-
}
|
|
6311
|
+
const validated = validateFigmaConfig(config);
|
|
6312
|
+
if ("error" in validated) return buildIngestResult2(0, 0, [validated.error], start);
|
|
6313
|
+
const { apiKey, baseUrl, fileIds } = validated;
|
|
5704
6314
|
const headers = { "X-FIGMA-TOKEN": apiKey };
|
|
5705
6315
|
const maxLen = config.maxContentLength ?? 4e3;
|
|
5706
6316
|
let nodesAdded = 0;
|
|
@@ -5720,6 +6330,17 @@ var FigmaConnector = class {
|
|
|
5720
6330
|
return buildIngestResult2(nodesAdded, edgesAdded, errors, start);
|
|
5721
6331
|
}
|
|
5722
6332
|
async processFile(store, baseUrl, fileId, headers, maxLen) {
|
|
6333
|
+
let nodesAdded = 0;
|
|
6334
|
+
let edgesAdded = 0;
|
|
6335
|
+
const styleCounts = await this.ingestStyles(store, baseUrl, fileId, headers, maxLen);
|
|
6336
|
+
nodesAdded += styleCounts.nodesAdded;
|
|
6337
|
+
edgesAdded += styleCounts.edgesAdded;
|
|
6338
|
+
const componentCounts = await this.ingestComponents(store, baseUrl, fileId, headers, maxLen);
|
|
6339
|
+
nodesAdded += componentCounts.nodesAdded;
|
|
6340
|
+
edgesAdded += componentCounts.edgesAdded;
|
|
6341
|
+
return { nodesAdded, edgesAdded };
|
|
6342
|
+
}
|
|
6343
|
+
async ingestStyles(store, baseUrl, fileId, headers, maxLen) {
|
|
5723
6344
|
let nodesAdded = 0;
|
|
5724
6345
|
let edgesAdded = 0;
|
|
5725
6346
|
const stylesUrl = `${baseUrl}/v1/files/${fileId}/styles`;
|
|
@@ -5727,90 +6348,112 @@ var FigmaConnector = class {
|
|
|
5727
6348
|
if (!stylesResponse.ok) throw new Error(`Styles request failed for file ${fileId}`);
|
|
5728
6349
|
const stylesData = await stylesResponse.json();
|
|
5729
6350
|
for (const style of stylesData.meta.styles) {
|
|
5730
|
-
const
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
});
|
|
5734
|
-
const metadata = {
|
|
5735
|
-
source: "figma",
|
|
5736
|
-
key: style.key,
|
|
5737
|
-
styleType: style.style_type,
|
|
5738
|
-
fileId
|
|
5739
|
-
};
|
|
5740
|
-
if (condensed.method !== "passthrough") {
|
|
5741
|
-
metadata.condensed = condensed.method;
|
|
5742
|
-
metadata.originalLength = condensed.originalLength;
|
|
5743
|
-
}
|
|
5744
|
-
store.addNode({
|
|
5745
|
-
id: nodeId,
|
|
5746
|
-
type: "design_token",
|
|
5747
|
-
name: sanitizeExternalText(style.name, 500),
|
|
5748
|
-
content: condensed.content,
|
|
5749
|
-
metadata
|
|
5750
|
-
});
|
|
5751
|
-
nodesAdded++;
|
|
5752
|
-
const searchText = sanitizeExternalText([style.name, style.description].join(" "));
|
|
5753
|
-
edgesAdded += linkToCode(store, searchText, nodeId, "references");
|
|
6351
|
+
const counts = await this.processStyle(store, style, fileId, maxLen);
|
|
6352
|
+
nodesAdded += counts.nodesAdded;
|
|
6353
|
+
edgesAdded += counts.edgesAdded;
|
|
5754
6354
|
}
|
|
6355
|
+
return { nodesAdded, edgesAdded };
|
|
6356
|
+
}
|
|
6357
|
+
async processStyle(store, style, fileId, maxLen) {
|
|
6358
|
+
const nodeId = `figma:token:${style.key}`;
|
|
6359
|
+
const condensed = await condenseContent(sanitizeExternalText(style.description || "", 2e3), {
|
|
6360
|
+
maxLength: maxLen
|
|
6361
|
+
});
|
|
6362
|
+
const metadata = buildNodeMetadata(condensed, {
|
|
6363
|
+
source: "figma",
|
|
6364
|
+
key: style.key,
|
|
6365
|
+
styleType: style.style_type,
|
|
6366
|
+
fileId
|
|
6367
|
+
});
|
|
6368
|
+
store.addNode({
|
|
6369
|
+
id: nodeId,
|
|
6370
|
+
type: "design_token",
|
|
6371
|
+
name: sanitizeExternalText(style.name, 500),
|
|
6372
|
+
content: condensed.content,
|
|
6373
|
+
metadata
|
|
6374
|
+
});
|
|
6375
|
+
const searchText = sanitizeExternalText([style.name, style.description].join(" "));
|
|
6376
|
+
const edgesAdded = linkToCode(store, searchText, nodeId, "references");
|
|
6377
|
+
return { nodesAdded: 1, edgesAdded };
|
|
6378
|
+
}
|
|
6379
|
+
async ingestComponents(store, baseUrl, fileId, headers, maxLen) {
|
|
6380
|
+
let nodesAdded = 0;
|
|
6381
|
+
let edgesAdded = 0;
|
|
5755
6382
|
const componentsUrl = `${baseUrl}/v1/files/${fileId}/components`;
|
|
5756
6383
|
const componentsResponse = await this.httpClient(componentsUrl, { headers });
|
|
5757
6384
|
if (!componentsResponse.ok) throw new Error(`Components request failed for file ${fileId}`);
|
|
5758
6385
|
const componentsData = await componentsResponse.json();
|
|
5759
6386
|
for (const component of componentsData.meta.components) {
|
|
5760
|
-
const
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
|
|
5764
|
-
maxLength: maxLen
|
|
5765
|
-
});
|
|
5766
|
-
const metadata = {
|
|
5767
|
-
source: "figma",
|
|
5768
|
-
key: component.key,
|
|
5769
|
-
fileId
|
|
5770
|
-
};
|
|
5771
|
-
if (condensed.method !== "passthrough") {
|
|
5772
|
-
metadata.condensed = condensed.method;
|
|
5773
|
-
metadata.originalLength = condensed.originalLength;
|
|
5774
|
-
}
|
|
5775
|
-
store.addNode({
|
|
5776
|
-
id: intentId,
|
|
5777
|
-
type: "aesthetic_intent",
|
|
5778
|
-
name: sanitizeExternalText(component.name, 500),
|
|
5779
|
-
content: condensed.content,
|
|
5780
|
-
metadata
|
|
5781
|
-
});
|
|
5782
|
-
nodesAdded++;
|
|
5783
|
-
const searchText = sanitizeExternalText([component.name, description].join(" "));
|
|
5784
|
-
edgesAdded += linkToCode(store, searchText, intentId, "references");
|
|
5785
|
-
}
|
|
5786
|
-
if (description && hasConstraintKeyword(description)) {
|
|
5787
|
-
const constraintId = `figma:constraint:${component.key}`;
|
|
5788
|
-
const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
|
|
5789
|
-
maxLength: maxLen
|
|
5790
|
-
});
|
|
5791
|
-
const metadata = {
|
|
5792
|
-
source: "figma",
|
|
5793
|
-
key: component.key,
|
|
5794
|
-
fileId
|
|
5795
|
-
};
|
|
5796
|
-
if (condensed.method !== "passthrough") {
|
|
5797
|
-
metadata.condensed = condensed.method;
|
|
5798
|
-
metadata.originalLength = condensed.originalLength;
|
|
5799
|
-
}
|
|
5800
|
-
store.addNode({
|
|
5801
|
-
id: constraintId,
|
|
5802
|
-
type: "design_constraint",
|
|
5803
|
-
name: sanitizeExternalText(component.name, 500),
|
|
5804
|
-
content: condensed.content,
|
|
5805
|
-
metadata
|
|
5806
|
-
});
|
|
5807
|
-
nodesAdded++;
|
|
5808
|
-
const searchText = sanitizeExternalText([component.name, description].join(" "));
|
|
5809
|
-
edgesAdded += linkToCode(store, searchText, constraintId, "references");
|
|
5810
|
-
}
|
|
6387
|
+
const counts = await this.processComponent(store, component, fileId, maxLen);
|
|
6388
|
+
nodesAdded += counts.nodesAdded;
|
|
6389
|
+
edgesAdded += counts.edgesAdded;
|
|
5811
6390
|
}
|
|
5812
6391
|
return { nodesAdded, edgesAdded };
|
|
5813
6392
|
}
|
|
6393
|
+
async processComponent(store, component, fileId, maxLen) {
|
|
6394
|
+
let nodesAdded = 0;
|
|
6395
|
+
let edgesAdded = 0;
|
|
6396
|
+
const description = component.description || "";
|
|
6397
|
+
if (description) {
|
|
6398
|
+
const counts = await this.addComponentIntent(store, component, description, fileId, maxLen);
|
|
6399
|
+
nodesAdded += counts.nodesAdded;
|
|
6400
|
+
edgesAdded += counts.edgesAdded;
|
|
6401
|
+
}
|
|
6402
|
+
if (description && hasConstraintKeyword(description)) {
|
|
6403
|
+
const counts = await this.addComponentConstraint(
|
|
6404
|
+
store,
|
|
6405
|
+
component,
|
|
6406
|
+
description,
|
|
6407
|
+
fileId,
|
|
6408
|
+
maxLen
|
|
6409
|
+
);
|
|
6410
|
+
nodesAdded += counts.nodesAdded;
|
|
6411
|
+
edgesAdded += counts.edgesAdded;
|
|
6412
|
+
}
|
|
6413
|
+
return { nodesAdded, edgesAdded };
|
|
6414
|
+
}
|
|
6415
|
+
async addComponentIntent(store, component, description, fileId, maxLen) {
|
|
6416
|
+
const intentId = `figma:intent:${component.key}`;
|
|
6417
|
+
const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
|
|
6418
|
+
maxLength: maxLen
|
|
6419
|
+
});
|
|
6420
|
+
const metadata = buildNodeMetadata(condensed, {
|
|
6421
|
+
source: "figma",
|
|
6422
|
+
key: component.key,
|
|
6423
|
+
fileId
|
|
6424
|
+
});
|
|
6425
|
+
store.addNode({
|
|
6426
|
+
id: intentId,
|
|
6427
|
+
type: "aesthetic_intent",
|
|
6428
|
+
name: sanitizeExternalText(component.name, 500),
|
|
6429
|
+
content: condensed.content,
|
|
6430
|
+
metadata
|
|
6431
|
+
});
|
|
6432
|
+
const searchText = sanitizeExternalText([component.name, description].join(" "));
|
|
6433
|
+
const edgesAdded = linkToCode(store, searchText, intentId, "references");
|
|
6434
|
+
return { nodesAdded: 1, edgesAdded };
|
|
6435
|
+
}
|
|
6436
|
+
async addComponentConstraint(store, component, description, fileId, maxLen) {
|
|
6437
|
+
const constraintId = `figma:constraint:${component.key}`;
|
|
6438
|
+
const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
|
|
6439
|
+
maxLength: maxLen
|
|
6440
|
+
});
|
|
6441
|
+
const metadata = buildNodeMetadata(condensed, {
|
|
6442
|
+
source: "figma",
|
|
6443
|
+
key: component.key,
|
|
6444
|
+
fileId
|
|
6445
|
+
});
|
|
6446
|
+
store.addNode({
|
|
6447
|
+
id: constraintId,
|
|
6448
|
+
type: "design_constraint",
|
|
6449
|
+
name: sanitizeExternalText(component.name, 500),
|
|
6450
|
+
content: condensed.content,
|
|
6451
|
+
metadata
|
|
6452
|
+
});
|
|
6453
|
+
const searchText = sanitizeExternalText([component.name, description].join(" "));
|
|
6454
|
+
const edgesAdded = linkToCode(store, searchText, constraintId, "references");
|
|
6455
|
+
return { nodesAdded: 1, edgesAdded };
|
|
6456
|
+
}
|
|
5814
6457
|
};
|
|
5815
6458
|
|
|
5816
6459
|
// src/ingest/connectors/MiroConnector.ts
|
|
@@ -5829,6 +6472,46 @@ function buildIngestResult3(nodesAdded, edgesAdded, errors, start) {
|
|
|
5829
6472
|
durationMs: Date.now() - start
|
|
5830
6473
|
};
|
|
5831
6474
|
}
|
|
6475
|
+
function validateBaseUrl(baseUrl, baseUrlEnv) {
|
|
6476
|
+
try {
|
|
6477
|
+
const parsed = new URL(baseUrl);
|
|
6478
|
+
if (parsed.protocol !== "https:" || parsed.hostname !== "api.miro.com") {
|
|
6479
|
+
return `Invalid ${baseUrlEnv}: must be an HTTPS URL on api.miro.com`;
|
|
6480
|
+
}
|
|
6481
|
+
} catch {
|
|
6482
|
+
return `Invalid ${baseUrlEnv}: not a valid URL`;
|
|
6483
|
+
}
|
|
6484
|
+
return null;
|
|
6485
|
+
}
|
|
6486
|
+
function resolveConfig(config, start) {
|
|
6487
|
+
const apiKeyEnv = config.apiKeyEnv ?? "MIRO_API_KEY";
|
|
6488
|
+
const apiKey = process.env[apiKeyEnv];
|
|
6489
|
+
if (!apiKey) {
|
|
6490
|
+
return {
|
|
6491
|
+
ok: false,
|
|
6492
|
+
result: buildIngestResult3(
|
|
6493
|
+
0,
|
|
6494
|
+
0,
|
|
6495
|
+
[`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
6496
|
+
start
|
|
6497
|
+
)
|
|
6498
|
+
};
|
|
6499
|
+
}
|
|
6500
|
+
const baseUrlEnv = config.baseUrlEnv ?? "MIRO_BASE_URL";
|
|
6501
|
+
const baseUrl = process.env[baseUrlEnv] ?? "https://api.miro.com";
|
|
6502
|
+
const urlError = validateBaseUrl(baseUrl, baseUrlEnv);
|
|
6503
|
+
if (urlError) {
|
|
6504
|
+
return { ok: false, result: buildIngestResult3(0, 0, [urlError], start) };
|
|
6505
|
+
}
|
|
6506
|
+
const boardIds = config.boardIds;
|
|
6507
|
+
if (!boardIds || boardIds.length === 0) {
|
|
6508
|
+
return {
|
|
6509
|
+
ok: false,
|
|
6510
|
+
result: buildIngestResult3(0, 0, ["No boardIds provided in config"], start)
|
|
6511
|
+
};
|
|
6512
|
+
}
|
|
6513
|
+
return { ok: true, apiKey, baseUrl, boardIds, headers: { Authorization: `Bearer ${apiKey}` } };
|
|
6514
|
+
}
|
|
5832
6515
|
var MiroConnector = class {
|
|
5833
6516
|
name = "miro";
|
|
5834
6517
|
source = "miro";
|
|
@@ -5838,36 +6521,9 @@ var MiroConnector = class {
|
|
|
5838
6521
|
}
|
|
5839
6522
|
async ingest(store, config) {
|
|
5840
6523
|
const start = Date.now();
|
|
5841
|
-
const
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
return buildIngestResult3(
|
|
5845
|
-
0,
|
|
5846
|
-
0,
|
|
5847
|
-
[`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
5848
|
-
start
|
|
5849
|
-
);
|
|
5850
|
-
}
|
|
5851
|
-
const baseUrlEnv = config.baseUrlEnv ?? "MIRO_BASE_URL";
|
|
5852
|
-
const baseUrl = process.env[baseUrlEnv] ?? "https://api.miro.com";
|
|
5853
|
-
try {
|
|
5854
|
-
const parsed = new URL(baseUrl);
|
|
5855
|
-
if (parsed.protocol !== "https:" || parsed.hostname !== "api.miro.com") {
|
|
5856
|
-
return buildIngestResult3(
|
|
5857
|
-
0,
|
|
5858
|
-
0,
|
|
5859
|
-
[`Invalid ${baseUrlEnv}: must be an HTTPS URL on api.miro.com`],
|
|
5860
|
-
start
|
|
5861
|
-
);
|
|
5862
|
-
}
|
|
5863
|
-
} catch {
|
|
5864
|
-
return buildIngestResult3(0, 0, [`Invalid ${baseUrlEnv}: not a valid URL`], start);
|
|
5865
|
-
}
|
|
5866
|
-
const boardIds = config.boardIds;
|
|
5867
|
-
if (!boardIds || boardIds.length === 0) {
|
|
5868
|
-
return buildIngestResult3(0, 0, ["No boardIds provided in config"], start);
|
|
5869
|
-
}
|
|
5870
|
-
const headers = { Authorization: `Bearer ${apiKey}` };
|
|
6524
|
+
const resolved = resolveConfig(config, start);
|
|
6525
|
+
if (!resolved.ok) return resolved.result;
|
|
6526
|
+
const { baseUrl, boardIds, headers } = resolved;
|
|
5871
6527
|
let totalNodesAdded = 0;
|
|
5872
6528
|
let totalEdgesAdded = 0;
|
|
5873
6529
|
const errors = [];
|
|
@@ -6071,7 +6727,7 @@ var FusionLayer = class {
|
|
|
6071
6727
|
};
|
|
6072
6728
|
|
|
6073
6729
|
// src/entropy/GraphEntropyAdapter.ts
|
|
6074
|
-
var
|
|
6730
|
+
var CODE_NODE_TYPES6 = ["file", "function", "class", "method", "interface", "variable"];
|
|
6075
6731
|
var GraphEntropyAdapter = class {
|
|
6076
6732
|
constructor(store) {
|
|
6077
6733
|
this.store = store;
|
|
@@ -6151,7 +6807,7 @@ var GraphEntropyAdapter = class {
|
|
|
6151
6807
|
}
|
|
6152
6808
|
findEntryPoints() {
|
|
6153
6809
|
const entryPoints = [];
|
|
6154
|
-
for (const nodeType of
|
|
6810
|
+
for (const nodeType of CODE_NODE_TYPES6) {
|
|
6155
6811
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
6156
6812
|
for (const node of nodes) {
|
|
6157
6813
|
const isIndexFile = nodeType === "file" && node.name === "index.ts";
|
|
@@ -6187,7 +6843,7 @@ var GraphEntropyAdapter = class {
|
|
|
6187
6843
|
}
|
|
6188
6844
|
collectUnreachableNodes(visited) {
|
|
6189
6845
|
const unreachableNodes = [];
|
|
6190
|
-
for (const nodeType of
|
|
6846
|
+
for (const nodeType of CODE_NODE_TYPES6) {
|
|
6191
6847
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
6192
6848
|
for (const node of nodes) {
|
|
6193
6849
|
if (!visited.has(node.id)) {
|
|
@@ -7033,9 +7689,9 @@ var EntityExtractor = class {
|
|
|
7033
7689
|
extractPaths(trimmed, add) {
|
|
7034
7690
|
const consumed = /* @__PURE__ */ new Set();
|
|
7035
7691
|
for (const match of trimmed.matchAll(FILE_PATH_RE)) {
|
|
7036
|
-
const
|
|
7037
|
-
add(
|
|
7038
|
-
consumed.add(
|
|
7692
|
+
const path15 = match[0];
|
|
7693
|
+
add(path15);
|
|
7694
|
+
consumed.add(path15);
|
|
7039
7695
|
}
|
|
7040
7696
|
return consumed;
|
|
7041
7697
|
}
|
|
@@ -7107,8 +7763,8 @@ var EntityResolver = class {
|
|
|
7107
7763
|
if (isPathLike && node.path.includes(raw)) {
|
|
7108
7764
|
return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
|
|
7109
7765
|
}
|
|
7110
|
-
const
|
|
7111
|
-
if (
|
|
7766
|
+
const basename10 = node.path.split("/").pop() ?? "";
|
|
7767
|
+
if (basename10.includes(raw)) {
|
|
7112
7768
|
return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
|
|
7113
7769
|
}
|
|
7114
7770
|
if (raw.length >= 4 && node.path.includes(raw)) {
|
|
@@ -7187,13 +7843,13 @@ var ResponseFormatter = class {
|
|
|
7187
7843
|
const context = Array.isArray(d?.context) ? d.context : [];
|
|
7188
7844
|
const firstEntity = entities[0];
|
|
7189
7845
|
const nodeType = firstEntity?.node.type ?? "node";
|
|
7190
|
-
const
|
|
7846
|
+
const path15 = firstEntity?.node.path ?? "unknown";
|
|
7191
7847
|
let neighborCount = 0;
|
|
7192
7848
|
const firstContext = context[0];
|
|
7193
7849
|
if (firstContext && Array.isArray(firstContext.nodes)) {
|
|
7194
7850
|
neighborCount = firstContext.nodes.length;
|
|
7195
7851
|
}
|
|
7196
|
-
return `**${entityName}** is a ${nodeType} at \`${
|
|
7852
|
+
return `**${entityName}** is a ${nodeType} at \`${path15}\`. Connected to ${neighborCount} nodes.`;
|
|
7197
7853
|
}
|
|
7198
7854
|
formatAnomaly(data) {
|
|
7199
7855
|
const d = data;
|
|
@@ -7576,7 +8232,7 @@ var PHASE_NODE_TYPES = {
|
|
|
7576
8232
|
debug: ["failure", "learning", "function", "method"],
|
|
7577
8233
|
plan: ["adr", "document", "module", "layer"]
|
|
7578
8234
|
};
|
|
7579
|
-
var
|
|
8235
|
+
var CODE_NODE_TYPES7 = /* @__PURE__ */ new Set([
|
|
7580
8236
|
"file",
|
|
7581
8237
|
"function",
|
|
7582
8238
|
"class",
|
|
@@ -7795,7 +8451,7 @@ var Assembler = class {
|
|
|
7795
8451
|
*/
|
|
7796
8452
|
checkCoverage() {
|
|
7797
8453
|
const codeNodes = [];
|
|
7798
|
-
for (const type of
|
|
8454
|
+
for (const type of CODE_NODE_TYPES7) {
|
|
7799
8455
|
codeNodes.push(...this.store.findNodes({ type }));
|
|
7800
8456
|
}
|
|
7801
8457
|
const documented = [];
|
|
@@ -7975,14 +8631,14 @@ var GraphConstraintAdapter = class {
|
|
|
7975
8631
|
};
|
|
7976
8632
|
|
|
7977
8633
|
// src/ingest/DesignIngestor.ts
|
|
7978
|
-
import * as
|
|
7979
|
-
import * as
|
|
8634
|
+
import * as fs13 from "fs/promises";
|
|
8635
|
+
import * as path14 from "path";
|
|
7980
8636
|
function isDTCGToken(obj) {
|
|
7981
8637
|
return typeof obj === "object" && obj !== null && "$value" in obj && "$type" in obj;
|
|
7982
8638
|
}
|
|
7983
8639
|
async function readFileOrNull(filePath) {
|
|
7984
8640
|
try {
|
|
7985
|
-
return await
|
|
8641
|
+
return await fs13.readFile(filePath, "utf-8");
|
|
7986
8642
|
} catch {
|
|
7987
8643
|
return null;
|
|
7988
8644
|
}
|
|
@@ -8128,8 +8784,8 @@ var DesignIngestor = class {
|
|
|
8128
8784
|
async ingestAll(designDir) {
|
|
8129
8785
|
const start = Date.now();
|
|
8130
8786
|
const [tokensResult, intentResult] = await Promise.all([
|
|
8131
|
-
this.ingestTokens(
|
|
8132
|
-
this.ingestDesignIntent(
|
|
8787
|
+
this.ingestTokens(path14.join(designDir, "tokens.json")),
|
|
8788
|
+
this.ingestDesignIntent(path14.join(designDir, "DESIGN.md"))
|
|
8133
8789
|
]);
|
|
8134
8790
|
const merged = mergeResults(tokensResult, intentResult);
|
|
8135
8791
|
return { ...merged, durationMs: Date.now() - start };
|
|
@@ -8365,10 +9021,10 @@ var TaskIndependenceAnalyzer = class {
|
|
|
8365
9021
|
includeTypes: ["file"]
|
|
8366
9022
|
});
|
|
8367
9023
|
for (const n of queryResult.nodes) {
|
|
8368
|
-
const
|
|
8369
|
-
if (!fileSet.has(
|
|
8370
|
-
if (!result.has(
|
|
8371
|
-
result.set(
|
|
9024
|
+
const path15 = n.path ?? n.id.replace(/^file:/, "");
|
|
9025
|
+
if (!fileSet.has(path15)) {
|
|
9026
|
+
if (!result.has(path15)) {
|
|
9027
|
+
result.set(path15, file);
|
|
8372
9028
|
}
|
|
8373
9029
|
}
|
|
8374
9030
|
}
|
|
@@ -8745,7 +9401,7 @@ var ConflictPredictor = class {
|
|
|
8745
9401
|
};
|
|
8746
9402
|
|
|
8747
9403
|
// src/index.ts
|
|
8748
|
-
var VERSION = "0.
|
|
9404
|
+
var VERSION = "0.6.0";
|
|
8749
9405
|
export {
|
|
8750
9406
|
ALL_EXTRACTORS,
|
|
8751
9407
|
ApiPathExtractor,
|
|
@@ -8762,6 +9418,7 @@ export {
|
|
|
8762
9418
|
ContradictionDetector,
|
|
8763
9419
|
CoverageScorer,
|
|
8764
9420
|
D2Parser,
|
|
9421
|
+
DecisionIngestor,
|
|
8765
9422
|
DesignConstraintAdapter,
|
|
8766
9423
|
DesignIngestor,
|
|
8767
9424
|
DiagramParser,
|
|
@@ -8786,6 +9443,7 @@ export {
|
|
|
8786
9443
|
ImageAnalysisExtractor,
|
|
8787
9444
|
IntentClassifier,
|
|
8788
9445
|
JiraConnector,
|
|
9446
|
+
KnowledgeDocMaterializer,
|
|
8789
9447
|
KnowledgeIngestor,
|
|
8790
9448
|
KnowledgePipelineRunner,
|
|
8791
9449
|
KnowledgeStagingAggregator,
|