@harness-engineering/graph 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +180 -16
- package/dist/index.d.ts +180 -16
- package/dist/index.js +1452 -715
- package/dist/index.mjs +1447 -715
- package/package.json +1 -1
package/dist/index.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;
|
|
@@ -2263,20 +2388,241 @@ var RequirementIngestor = class {
|
|
|
2263
2388
|
}
|
|
2264
2389
|
};
|
|
2265
2390
|
|
|
2391
|
+
// src/ingest/domain-inference.ts
|
|
2392
|
+
var DEFAULT_PATTERNS = [
|
|
2393
|
+
"packages/<dir>",
|
|
2394
|
+
"apps/<dir>",
|
|
2395
|
+
"services/<dir>",
|
|
2396
|
+
"src/<dir>",
|
|
2397
|
+
"lib/<dir>"
|
|
2398
|
+
];
|
|
2399
|
+
var DEFAULT_BLOCKLIST = /* @__PURE__ */ new Set([
|
|
2400
|
+
"node_modules",
|
|
2401
|
+
".harness",
|
|
2402
|
+
"dist",
|
|
2403
|
+
"build",
|
|
2404
|
+
".git",
|
|
2405
|
+
"coverage",
|
|
2406
|
+
".next",
|
|
2407
|
+
".turbo",
|
|
2408
|
+
".cache",
|
|
2409
|
+
"out",
|
|
2410
|
+
"tmp"
|
|
2411
|
+
]);
|
|
2412
|
+
var CODE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
2413
|
+
function matchPattern(filePath, pattern) {
|
|
2414
|
+
const patternParts = pattern.split("/").filter((s) => s.length > 0);
|
|
2415
|
+
if (patternParts.length !== 2 || patternParts[1] !== "<dir>") {
|
|
2416
|
+
return null;
|
|
2417
|
+
}
|
|
2418
|
+
const prefix = patternParts[0];
|
|
2419
|
+
const pathParts = filePath.split("/").filter((s) => s.length > 0);
|
|
2420
|
+
if (pathParts.length < 2) return null;
|
|
2421
|
+
if (pathParts[0] !== prefix) return null;
|
|
2422
|
+
let dir = pathParts[1];
|
|
2423
|
+
if (dir.length === 0) return null;
|
|
2424
|
+
if (CODE_EXTENSIONS.some((ext) => dir.endsWith(ext))) {
|
|
2425
|
+
const dotIdx = dir.lastIndexOf(".");
|
|
2426
|
+
if (dotIdx > 0) dir = dir.slice(0, dotIdx);
|
|
2427
|
+
}
|
|
2428
|
+
if (dir.length === 0) return null;
|
|
2429
|
+
return dir;
|
|
2430
|
+
}
|
|
2431
|
+
function inferDomain(node, options = {}) {
|
|
2432
|
+
if (node.metadata?.domain && typeof node.metadata.domain === "string" && node.metadata.domain.length > 0) {
|
|
2433
|
+
return node.metadata.domain;
|
|
2434
|
+
}
|
|
2435
|
+
const filePath = typeof node.path === "string" ? node.path : "";
|
|
2436
|
+
const blocklist = new Set(DEFAULT_BLOCKLIST);
|
|
2437
|
+
if (options.extraBlocklist) {
|
|
2438
|
+
for (const seg of options.extraBlocklist) {
|
|
2439
|
+
if (seg && seg.length > 0) blocklist.add(seg);
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
if (filePath.length > 0) {
|
|
2443
|
+
const extraPatterns = options.extraPatterns ?? [];
|
|
2444
|
+
for (const pattern of extraPatterns) {
|
|
2445
|
+
const dir = matchPattern(filePath, pattern);
|
|
2446
|
+
if (dir !== null) {
|
|
2447
|
+
if (blocklist.has(dir)) return "unknown";
|
|
2448
|
+
return dir;
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
for (const pattern of DEFAULT_PATTERNS) {
|
|
2452
|
+
const dir = matchPattern(filePath, pattern);
|
|
2453
|
+
if (dir !== null) {
|
|
2454
|
+
if (blocklist.has(dir)) return "unknown";
|
|
2455
|
+
return dir;
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
const segments = filePath.split("/").filter((s) => s.length > 0);
|
|
2459
|
+
if (segments.length > 0) {
|
|
2460
|
+
const first = segments[0];
|
|
2461
|
+
if (!blocklist.has(first)) return first;
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
const source = node.metadata?.source;
|
|
2465
|
+
if (source === "knowledge-linker" || source === "connector") {
|
|
2466
|
+
const connector = node.metadata?.connectorName;
|
|
2467
|
+
if (typeof connector === "string" && connector.length > 0) return connector;
|
|
2468
|
+
return "general";
|
|
2469
|
+
}
|
|
2470
|
+
return "unknown";
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2266
2473
|
// src/ingest/KnowledgePipelineRunner.ts
|
|
2267
|
-
import * as
|
|
2268
|
-
import * as
|
|
2474
|
+
import * as fs11 from "fs/promises";
|
|
2475
|
+
import * as path12 from "path";
|
|
2269
2476
|
|
|
2270
2477
|
// src/ingest/DiagramParser.ts
|
|
2271
|
-
import * as
|
|
2272
|
-
import * as
|
|
2273
|
-
|
|
2478
|
+
import * as fs6 from "fs/promises";
|
|
2479
|
+
import * as path7 from "path";
|
|
2480
|
+
|
|
2481
|
+
// src/ingest/parsers/mermaid.ts
|
|
2482
|
+
function emptyResult2(diagramType = "unknown") {
|
|
2274
2483
|
return {
|
|
2275
2484
|
entities: [],
|
|
2276
2485
|
relationships: [],
|
|
2277
2486
|
metadata: { format: "mermaid", diagramType }
|
|
2278
2487
|
};
|
|
2279
2488
|
}
|
|
2489
|
+
function detectDiagramType(content) {
|
|
2490
|
+
for (const line of content.split("\n")) {
|
|
2491
|
+
const trimmed = line.trim();
|
|
2492
|
+
if (!trimmed) continue;
|
|
2493
|
+
if (/^(?:graph|flowchart)\b/i.test(trimmed)) return "flowchart";
|
|
2494
|
+
if (/^sequenceDiagram\b/.test(trimmed)) return "sequence";
|
|
2495
|
+
if (/^classDiagram\b/.test(trimmed)) return "class";
|
|
2496
|
+
if (/^erDiagram\b/.test(trimmed)) return "er";
|
|
2497
|
+
break;
|
|
2498
|
+
}
|
|
2499
|
+
return "unknown";
|
|
2500
|
+
}
|
|
2501
|
+
function isDecisionNode(content, nodeId) {
|
|
2502
|
+
const decisionRegex = new RegExp(`${nodeId}\\s*\\{`);
|
|
2503
|
+
return decisionRegex.test(content);
|
|
2504
|
+
}
|
|
2505
|
+
function buildFlowchartEntity(content, match) {
|
|
2506
|
+
const id = match[1] ?? "";
|
|
2507
|
+
const label = (match[2] ?? "").trim();
|
|
2508
|
+
if (!id) return null;
|
|
2509
|
+
const decision = isDecisionNode(content, id);
|
|
2510
|
+
const entity = {
|
|
2511
|
+
id,
|
|
2512
|
+
label,
|
|
2513
|
+
...decision ? { type: "decision" } : {}
|
|
2514
|
+
};
|
|
2515
|
+
return { id, entity };
|
|
2516
|
+
}
|
|
2517
|
+
function extractFlowchartNodes(content) {
|
|
2518
|
+
const entities = /* @__PURE__ */ new Map();
|
|
2519
|
+
const nodeRegex = /([A-Za-z0-9_]+)\s*[[({]([^\])}]+)[\])}]/g;
|
|
2520
|
+
let match = nodeRegex.exec(content);
|
|
2521
|
+
while (match !== null) {
|
|
2522
|
+
const parsed = buildFlowchartEntity(content, match);
|
|
2523
|
+
if (parsed && !entities.has(parsed.id)) {
|
|
2524
|
+
entities.set(parsed.id, parsed.entity);
|
|
2525
|
+
}
|
|
2526
|
+
match = nodeRegex.exec(content);
|
|
2527
|
+
}
|
|
2528
|
+
return entities;
|
|
2529
|
+
}
|
|
2530
|
+
function parseLabeledEdgeMatch(match) {
|
|
2531
|
+
const from = match[1] ?? "";
|
|
2532
|
+
const label = (match[2] ?? "").trim();
|
|
2533
|
+
const to = match[3] ?? "";
|
|
2534
|
+
if (!from || !to) return null;
|
|
2535
|
+
return { key: `${from}->${to}:${label}`, rel: { from, to, label } };
|
|
2536
|
+
}
|
|
2537
|
+
function extractLabeledEdges(stripped, edgeKeys) {
|
|
2538
|
+
const relationships = [];
|
|
2539
|
+
const labeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\|([^|]+)\|\s*([A-Za-z0-9_]+)/g;
|
|
2540
|
+
let match = labeledEdgeRegex.exec(stripped);
|
|
2541
|
+
while (match !== null) {
|
|
2542
|
+
const parsed = parseLabeledEdgeMatch(match);
|
|
2543
|
+
if (parsed && !edgeKeys.has(parsed.key)) {
|
|
2544
|
+
edgeKeys.add(parsed.key);
|
|
2545
|
+
relationships.push(parsed.rel);
|
|
2546
|
+
}
|
|
2547
|
+
match = labeledEdgeRegex.exec(stripped);
|
|
2548
|
+
}
|
|
2549
|
+
return relationships;
|
|
2550
|
+
}
|
|
2551
|
+
function hasLabeledEdgeBetween(labeledEdges, from, to) {
|
|
2552
|
+
return labeledEdges.some((r) => r.from === from && r.to === to);
|
|
2553
|
+
}
|
|
2554
|
+
function extractUnlabeledEdges(stripped, edgeKeys, labeledEdges) {
|
|
2555
|
+
const relationships = [];
|
|
2556
|
+
const unlabeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\s+([A-Za-z0-9_]+)/g;
|
|
2557
|
+
let match = unlabeledEdgeRegex.exec(stripped);
|
|
2558
|
+
while (match !== null) {
|
|
2559
|
+
const from = match[1] ?? "";
|
|
2560
|
+
const to = match[2] ?? "";
|
|
2561
|
+
const key = `${from}->${to}`;
|
|
2562
|
+
if (from && to && !hasLabeledEdgeBetween(labeledEdges, from, to) && !edgeKeys.has(key)) {
|
|
2563
|
+
edgeKeys.add(key);
|
|
2564
|
+
relationships.push({ from, to });
|
|
2565
|
+
}
|
|
2566
|
+
match = unlabeledEdgeRegex.exec(stripped);
|
|
2567
|
+
}
|
|
2568
|
+
return relationships;
|
|
2569
|
+
}
|
|
2570
|
+
function parseFlowchart(content, diagramType) {
|
|
2571
|
+
const entities = extractFlowchartNodes(content);
|
|
2572
|
+
const stripped = content.replace(/[[({][^\])}]*[\])}]/g, "");
|
|
2573
|
+
const edgeKeys = /* @__PURE__ */ new Set();
|
|
2574
|
+
const labeledEdges = extractLabeledEdges(stripped, edgeKeys);
|
|
2575
|
+
const unlabeledEdges = extractUnlabeledEdges(stripped, edgeKeys, labeledEdges);
|
|
2576
|
+
return {
|
|
2577
|
+
entities: Array.from(entities.values()),
|
|
2578
|
+
relationships: [...labeledEdges, ...unlabeledEdges],
|
|
2579
|
+
metadata: { format: "mermaid", diagramType }
|
|
2580
|
+
};
|
|
2581
|
+
}
|
|
2582
|
+
function extractParticipants(content) {
|
|
2583
|
+
const entities = [];
|
|
2584
|
+
const seenParticipants = /* @__PURE__ */ new Set();
|
|
2585
|
+
const participantRegex = /participant\s+(\w+)(?:\s+as\s+(.+))?/g;
|
|
2586
|
+
let match = participantRegex.exec(content);
|
|
2587
|
+
while (match !== null) {
|
|
2588
|
+
const id = match[1] ?? "";
|
|
2589
|
+
const label = match[2]?.trim() || id;
|
|
2590
|
+
if (id && !seenParticipants.has(id)) {
|
|
2591
|
+
seenParticipants.add(id);
|
|
2592
|
+
entities.push({ id, label });
|
|
2593
|
+
}
|
|
2594
|
+
match = participantRegex.exec(content);
|
|
2595
|
+
}
|
|
2596
|
+
return entities;
|
|
2597
|
+
}
|
|
2598
|
+
function relationshipFromMatch(match) {
|
|
2599
|
+
const from = match[1] ?? "";
|
|
2600
|
+
const to = match[2] ?? "";
|
|
2601
|
+
const label = (match[3] ?? "").trim();
|
|
2602
|
+
return from && to ? { from, to, label } : null;
|
|
2603
|
+
}
|
|
2604
|
+
function collectMessageMatches(content, regex) {
|
|
2605
|
+
const results = [];
|
|
2606
|
+
let match = regex.exec(content);
|
|
2607
|
+
while (match !== null) {
|
|
2608
|
+
const rel = relationshipFromMatch(match);
|
|
2609
|
+
if (rel) results.push(rel);
|
|
2610
|
+
match = regex.exec(content);
|
|
2611
|
+
}
|
|
2612
|
+
return results;
|
|
2613
|
+
}
|
|
2614
|
+
function extractMessages(content) {
|
|
2615
|
+
const forward = collectMessageMatches(content, /(\w+)\s*->>?\+?\s*(\w+)\s*:\s*(.+)/g);
|
|
2616
|
+
const returns = collectMessageMatches(content, /(\w+)\s*-->>?-?\s*(\w+)\s*:\s*(.+)/g);
|
|
2617
|
+
return [...forward, ...returns];
|
|
2618
|
+
}
|
|
2619
|
+
function parseSequence(content, diagramType) {
|
|
2620
|
+
return {
|
|
2621
|
+
entities: extractParticipants(content),
|
|
2622
|
+
relationships: extractMessages(content),
|
|
2623
|
+
metadata: { format: "mermaid", diagramType }
|
|
2624
|
+
};
|
|
2625
|
+
}
|
|
2280
2626
|
var MermaidParser = class {
|
|
2281
2627
|
canParse(_content, ext) {
|
|
2282
2628
|
return ext === ".mmd" || ext === ".mermaid";
|
|
@@ -2284,269 +2630,199 @@ var MermaidParser = class {
|
|
|
2284
2630
|
parse(content, _filePath) {
|
|
2285
2631
|
const trimmed = content.trim();
|
|
2286
2632
|
if (!trimmed) {
|
|
2287
|
-
return
|
|
2633
|
+
return emptyResult2();
|
|
2288
2634
|
}
|
|
2289
|
-
const diagramType =
|
|
2635
|
+
const diagramType = detectDiagramType(trimmed);
|
|
2290
2636
|
switch (diagramType) {
|
|
2291
2637
|
case "flowchart":
|
|
2292
|
-
return
|
|
2638
|
+
return parseFlowchart(trimmed, diagramType);
|
|
2293
2639
|
case "sequence":
|
|
2294
|
-
return
|
|
2640
|
+
return parseSequence(trimmed, diagramType);
|
|
2295
2641
|
default:
|
|
2296
|
-
return
|
|
2642
|
+
return emptyResult2(diagramType);
|
|
2297
2643
|
}
|
|
2298
2644
|
}
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2645
|
+
};
|
|
2646
|
+
|
|
2647
|
+
// src/ingest/parsers/d2.ts
|
|
2648
|
+
function emptyResult3() {
|
|
2649
|
+
return {
|
|
2650
|
+
entities: [],
|
|
2651
|
+
relationships: [],
|
|
2652
|
+
metadata: { format: "d2", diagramType: "architecture" }
|
|
2653
|
+
};
|
|
2654
|
+
}
|
|
2655
|
+
function parseBlockShape(stripped) {
|
|
2656
|
+
const match = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+?)\s*\{$/);
|
|
2657
|
+
if (match) {
|
|
2658
|
+
const id = match[1];
|
|
2659
|
+
const label = match[2];
|
|
2660
|
+
if (id && label) return { id, label };
|
|
2661
|
+
}
|
|
2662
|
+
return null;
|
|
2663
|
+
}
|
|
2664
|
+
function parseConnection(stripped) {
|
|
2665
|
+
const match = stripped.match(/^([a-zA-Z0-9_.-]+)\s*->\s*([a-zA-Z0-9_.-]+)(?:\s*:\s*(.+?))?$/);
|
|
2666
|
+
if (match) {
|
|
2667
|
+
const from = match[1] ?? "";
|
|
2668
|
+
const to = match[2] ?? "";
|
|
2669
|
+
const label = match[3]?.trim();
|
|
2670
|
+
if (from && to) return { from, to, ...label ? { label } : {} };
|
|
2671
|
+
}
|
|
2672
|
+
return null;
|
|
2673
|
+
}
|
|
2674
|
+
function parseSimpleShape(stripped) {
|
|
2675
|
+
const match = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+)$/);
|
|
2676
|
+
if (match) {
|
|
2677
|
+
const id = match[1];
|
|
2678
|
+
const label = (match[2] ?? "").trim();
|
|
2679
|
+
if (id && label) return { id, label };
|
|
2680
|
+
}
|
|
2681
|
+
return null;
|
|
2682
|
+
}
|
|
2683
|
+
function handleBlockOpen(stripped, state) {
|
|
2684
|
+
if (state.braceDepth === 0) {
|
|
2685
|
+
const shape = parseBlockShape(stripped);
|
|
2686
|
+
if (shape) state.entities.set(shape.id, { id: shape.id, label: shape.label });
|
|
2311
2687
|
}
|
|
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
|
-
};
|
|
2688
|
+
state.braceDepth++;
|
|
2689
|
+
}
|
|
2690
|
+
function processTopLevelLine(stripped, state) {
|
|
2691
|
+
const conn = parseConnection(stripped);
|
|
2692
|
+
if (conn) {
|
|
2693
|
+
state.relationships.push(conn);
|
|
2694
|
+
return;
|
|
2370
2695
|
}
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2696
|
+
const shape = parseSimpleShape(stripped);
|
|
2697
|
+
if (shape && !state.entities.has(shape.id)) {
|
|
2698
|
+
state.entities.set(shape.id, { id: shape.id, label: shape.label });
|
|
2374
2699
|
}
|
|
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
|
-
};
|
|
2700
|
+
}
|
|
2701
|
+
function processD2Line(stripped, state) {
|
|
2702
|
+
if (!stripped || stripped.startsWith("#")) return;
|
|
2703
|
+
if (stripped.endsWith("{")) {
|
|
2704
|
+
handleBlockOpen(stripped, state);
|
|
2705
|
+
return;
|
|
2418
2706
|
}
|
|
2419
|
-
}
|
|
2707
|
+
if (stripped === "}") {
|
|
2708
|
+
state.braceDepth = Math.max(0, state.braceDepth - 1);
|
|
2709
|
+
return;
|
|
2710
|
+
}
|
|
2711
|
+
if (state.braceDepth > 0) return;
|
|
2712
|
+
processTopLevelLine(stripped, state);
|
|
2713
|
+
}
|
|
2420
2714
|
var D2Parser = class {
|
|
2421
2715
|
canParse(_content, ext) {
|
|
2422
2716
|
return ext === ".d2";
|
|
2423
2717
|
}
|
|
2424
2718
|
parse(content, _filePath) {
|
|
2425
2719
|
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;
|
|
2720
|
+
if (!trimmed) return emptyResult3();
|
|
2721
|
+
const state = {
|
|
2722
|
+
entities: /* @__PURE__ */ new Map(),
|
|
2723
|
+
relationships: [],
|
|
2724
|
+
braceDepth: 0
|
|
2725
|
+
};
|
|
2436
2726
|
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
|
-
}
|
|
2727
|
+
processD2Line(line.trim(), state);
|
|
2478
2728
|
}
|
|
2479
2729
|
return {
|
|
2480
|
-
entities: Array.from(entities.values()),
|
|
2481
|
-
relationships,
|
|
2730
|
+
entities: Array.from(state.entities.values()),
|
|
2731
|
+
relationships: state.relationships,
|
|
2482
2732
|
metadata: { format: "d2", diagramType: "architecture" }
|
|
2483
2733
|
};
|
|
2484
2734
|
}
|
|
2485
2735
|
};
|
|
2736
|
+
|
|
2737
|
+
// src/ingest/parsers/plantuml.ts
|
|
2738
|
+
function emptyResult4() {
|
|
2739
|
+
return {
|
|
2740
|
+
entities: [],
|
|
2741
|
+
relationships: [],
|
|
2742
|
+
metadata: { format: "plantuml", diagramType: "unknown" }
|
|
2743
|
+
};
|
|
2744
|
+
}
|
|
2745
|
+
function detectDiagramType2(content) {
|
|
2746
|
+
if (/class\s+\w+/.test(content)) return "class";
|
|
2747
|
+
if (/\[.+\]/.test(content) || /component\s+/.test(content)) return "component";
|
|
2748
|
+
if (/participant\s+/.test(content) || /actor\s+/.test(content)) return "sequence";
|
|
2749
|
+
return "unknown";
|
|
2750
|
+
}
|
|
2751
|
+
function stripWrappers(content) {
|
|
2752
|
+
return content.replace(/@startuml\b.*\n?/, "").replace(/@enduml\b.*/, "").trim();
|
|
2753
|
+
}
|
|
2754
|
+
function extractClasses(body, entities) {
|
|
2755
|
+
const classRegex = /class\s+(\w+)/g;
|
|
2756
|
+
let match = classRegex.exec(body);
|
|
2757
|
+
while (match !== null) {
|
|
2758
|
+
const id = match[1] ?? "";
|
|
2759
|
+
if (id && !entities.has(id)) {
|
|
2760
|
+
entities.set(id, { id, label: id });
|
|
2761
|
+
}
|
|
2762
|
+
match = classRegex.exec(body);
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
function extractComponents(body, entities) {
|
|
2766
|
+
const componentRegex = /\[([^\]]+)\]/g;
|
|
2767
|
+
let match = componentRegex.exec(body);
|
|
2768
|
+
while (match !== null) {
|
|
2769
|
+
const label = (match[1] ?? "").trim();
|
|
2770
|
+
if (label) {
|
|
2771
|
+
const id = label.replace(/\s+/g, "_");
|
|
2772
|
+
if (!entities.has(id)) {
|
|
2773
|
+
entities.set(id, { id, label });
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
match = componentRegex.exec(body);
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
function parseRelationshipMatch(match) {
|
|
2780
|
+
const from = match[1] ?? "";
|
|
2781
|
+
const to = match[2] ?? "";
|
|
2782
|
+
if (!from || !to) return null;
|
|
2783
|
+
const label = match[3]?.trim();
|
|
2784
|
+
return { from, to, ...label ? { label } : {} };
|
|
2785
|
+
}
|
|
2786
|
+
function collectRelationshipMatches(body, regex) {
|
|
2787
|
+
const results = [];
|
|
2788
|
+
let match = regex.exec(body);
|
|
2789
|
+
while (match !== null) {
|
|
2790
|
+
const rel = parseRelationshipMatch(match);
|
|
2791
|
+
if (rel) results.push(rel);
|
|
2792
|
+
match = regex.exec(body);
|
|
2793
|
+
}
|
|
2794
|
+
return results;
|
|
2795
|
+
}
|
|
2796
|
+
function extractRelationships(body) {
|
|
2797
|
+
return collectRelationshipMatches(
|
|
2798
|
+
body,
|
|
2799
|
+
/(\w+)\s*(?:-->|->|<--|<-|\.\.>|--)\s*(\w+)(?:\s*:\s*(.+))?/g
|
|
2800
|
+
);
|
|
2801
|
+
}
|
|
2486
2802
|
var PlantUmlParser = class {
|
|
2487
2803
|
canParse(_content, ext) {
|
|
2488
2804
|
return ext === ".puml" || ext === ".plantuml";
|
|
2489
2805
|
}
|
|
2490
2806
|
parse(content, _filePath) {
|
|
2491
2807
|
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);
|
|
2808
|
+
if (!trimmed) return emptyResult4();
|
|
2809
|
+
const diagramType = detectDiagramType2(trimmed);
|
|
2500
2810
|
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
|
-
}
|
|
2811
|
+
const body = stripWrappers(trimmed);
|
|
2812
|
+
extractClasses(body, entities);
|
|
2813
|
+
extractComponents(body, entities);
|
|
2814
|
+
const relationships = extractRelationships(body);
|
|
2536
2815
|
return {
|
|
2537
2816
|
entities: Array.from(entities.values()),
|
|
2538
2817
|
relationships,
|
|
2539
2818
|
metadata: { format: "plantuml", diagramType }
|
|
2540
2819
|
};
|
|
2541
2820
|
}
|
|
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
2821
|
};
|
|
2822
|
+
|
|
2823
|
+
// src/ingest/DiagramParser.ts
|
|
2549
2824
|
var DIAGRAM_EXTENSIONS = /* @__PURE__ */ new Set([".mmd", ".mermaid", ".d2", ".puml", ".plantuml"]);
|
|
2825
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".harness"]);
|
|
2550
2826
|
var DiagramParser = class {
|
|
2551
2827
|
constructor(store) {
|
|
2552
2828
|
this.store = store;
|
|
@@ -2558,7 +2834,7 @@ var DiagramParser = class {
|
|
|
2558
2834
|
new PlantUmlParser()
|
|
2559
2835
|
];
|
|
2560
2836
|
parse(content, filePath) {
|
|
2561
|
-
const ext =
|
|
2837
|
+
const ext = path7.extname(filePath).toLowerCase();
|
|
2562
2838
|
for (const parser of this.parsers) {
|
|
2563
2839
|
if (parser.canParse(content, ext)) {
|
|
2564
2840
|
return parser.parse(content, filePath);
|
|
@@ -2578,41 +2854,13 @@ var DiagramParser = class {
|
|
|
2578
2854
|
const diagramFiles = await this.findDiagramFiles(projectDir);
|
|
2579
2855
|
for (const filePath of diagramFiles) {
|
|
2580
2856
|
try {
|
|
2581
|
-
const content = await
|
|
2582
|
-
const relPath =
|
|
2857
|
+
const content = await fs6.readFile(filePath, "utf-8");
|
|
2858
|
+
const relPath = path7.relative(projectDir, filePath).replaceAll("\\", "/");
|
|
2583
2859
|
const result = this.parse(content, filePath);
|
|
2584
2860
|
if (result.entities.length === 0) continue;
|
|
2585
2861
|
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
|
-
}
|
|
2862
|
+
nodesAdded += this.addEntityNodes(result, relPath, pathHash);
|
|
2863
|
+
edgesAdded += this.addRelationshipEdges(result, pathHash);
|
|
2616
2864
|
} catch (err) {
|
|
2617
2865
|
errors.push(
|
|
2618
2866
|
`Failed to parse ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -2628,25 +2876,64 @@ var DiagramParser = class {
|
|
|
2628
2876
|
durationMs: Date.now() - start
|
|
2629
2877
|
};
|
|
2630
2878
|
}
|
|
2879
|
+
/** Map diagram entities to business_concept graph nodes. */
|
|
2880
|
+
addEntityNodes(result, relPath, pathHash) {
|
|
2881
|
+
let count = 0;
|
|
2882
|
+
for (const entity of result.entities) {
|
|
2883
|
+
const nodeId = `diagram:${pathHash}:${entity.id}`;
|
|
2884
|
+
this.store.addNode({
|
|
2885
|
+
id: nodeId,
|
|
2886
|
+
type: "business_concept",
|
|
2887
|
+
name: entity.label,
|
|
2888
|
+
path: relPath,
|
|
2889
|
+
metadata: {
|
|
2890
|
+
source: "diagram",
|
|
2891
|
+
format: result.metadata.format,
|
|
2892
|
+
diagramType: result.metadata.diagramType,
|
|
2893
|
+
confidence: 0.85,
|
|
2894
|
+
...entity.type ? { entityType: entity.type } : {}
|
|
2895
|
+
}
|
|
2896
|
+
});
|
|
2897
|
+
count++;
|
|
2898
|
+
}
|
|
2899
|
+
return count;
|
|
2900
|
+
}
|
|
2901
|
+
/** Map diagram relationships to references graph edges. */
|
|
2902
|
+
addRelationshipEdges(result, pathHash) {
|
|
2903
|
+
let count = 0;
|
|
2904
|
+
for (const rel of result.relationships) {
|
|
2905
|
+
const fromId = `diagram:${pathHash}:${rel.from}`;
|
|
2906
|
+
const toId = `diagram:${pathHash}:${rel.to}`;
|
|
2907
|
+
this.store.addEdge({
|
|
2908
|
+
from: fromId,
|
|
2909
|
+
to: toId,
|
|
2910
|
+
type: "references",
|
|
2911
|
+
metadata: {
|
|
2912
|
+
...rel.label ? { label: rel.label } : {}
|
|
2913
|
+
}
|
|
2914
|
+
});
|
|
2915
|
+
count++;
|
|
2916
|
+
}
|
|
2917
|
+
return count;
|
|
2918
|
+
}
|
|
2631
2919
|
async findDiagramFiles(dir) {
|
|
2632
2920
|
const files = [];
|
|
2633
|
-
const SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".harness"]);
|
|
2634
2921
|
const walk = async (currentDir) => {
|
|
2635
2922
|
let entries;
|
|
2636
2923
|
try {
|
|
2637
|
-
entries = await
|
|
2924
|
+
entries = await fs6.readdir(currentDir, { withFileTypes: true });
|
|
2638
2925
|
} catch {
|
|
2639
2926
|
return;
|
|
2640
2927
|
}
|
|
2641
2928
|
for (const entry of entries) {
|
|
2642
2929
|
if (entry.isDirectory()) {
|
|
2643
|
-
if (!
|
|
2644
|
-
await walk(
|
|
2930
|
+
if (!SKIP_DIRS.has(entry.name)) {
|
|
2931
|
+
await walk(path7.join(currentDir, entry.name));
|
|
2645
2932
|
}
|
|
2646
2933
|
} else if (entry.isFile()) {
|
|
2647
|
-
const ext =
|
|
2934
|
+
const ext = path7.extname(entry.name).toLowerCase();
|
|
2648
2935
|
if (DIAGRAM_EXTENSIONS.has(ext)) {
|
|
2649
|
-
files.push(
|
|
2936
|
+
files.push(path7.join(currentDir, entry.name));
|
|
2650
2937
|
}
|
|
2651
2938
|
}
|
|
2652
2939
|
}
|
|
@@ -2657,8 +2944,8 @@ var DiagramParser = class {
|
|
|
2657
2944
|
};
|
|
2658
2945
|
|
|
2659
2946
|
// src/ingest/KnowledgeLinker.ts
|
|
2660
|
-
import * as
|
|
2661
|
-
import * as
|
|
2947
|
+
import * as fs7 from "fs/promises";
|
|
2948
|
+
import * as path8 from "path";
|
|
2662
2949
|
var HEURISTIC_PATTERNS = [
|
|
2663
2950
|
{
|
|
2664
2951
|
name: "business-rule-imperative",
|
|
@@ -2805,8 +3092,10 @@ var KnowledgeLinker = class {
|
|
|
2805
3092
|
if (!duplicate) return false;
|
|
2806
3093
|
const existingSources = duplicate.metadata.sources ?? [];
|
|
2807
3094
|
if (!existingSources.includes(candidate.sourceNodeId)) {
|
|
2808
|
-
|
|
2809
|
-
|
|
3095
|
+
this.store.addNode({
|
|
3096
|
+
...duplicate,
|
|
3097
|
+
metadata: { ...duplicate.metadata, sources: [...existingSources, candidate.sourceNodeId] }
|
|
3098
|
+
});
|
|
2810
3099
|
}
|
|
2811
3100
|
return true;
|
|
2812
3101
|
}
|
|
@@ -2840,8 +3129,8 @@ var KnowledgeLinker = class {
|
|
|
2840
3129
|
*/
|
|
2841
3130
|
async writeJsonl(candidates) {
|
|
2842
3131
|
if (!this.outputDir) return;
|
|
2843
|
-
await
|
|
2844
|
-
const filePath =
|
|
3132
|
+
await fs7.mkdir(this.outputDir, { recursive: true });
|
|
3133
|
+
const filePath = path8.join(this.outputDir, "linker.jsonl");
|
|
2845
3134
|
const lines = candidates.map(
|
|
2846
3135
|
(c) => JSON.stringify({
|
|
2847
3136
|
id: c.id,
|
|
@@ -2854,16 +3143,16 @@ var KnowledgeLinker = class {
|
|
|
2854
3143
|
nodeType: c.nodeType
|
|
2855
3144
|
})
|
|
2856
3145
|
);
|
|
2857
|
-
await
|
|
3146
|
+
await fs7.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
|
|
2858
3147
|
}
|
|
2859
3148
|
/**
|
|
2860
3149
|
* Write medium-confidence candidates to staged JSONL for human review.
|
|
2861
3150
|
*/
|
|
2862
3151
|
async writeStagedJsonl(candidates) {
|
|
2863
3152
|
if (!this.outputDir || candidates.length === 0) return;
|
|
2864
|
-
const stagedDir =
|
|
2865
|
-
await
|
|
2866
|
-
const filePath =
|
|
3153
|
+
const stagedDir = path8.join(this.outputDir, "staged");
|
|
3154
|
+
await fs7.mkdir(stagedDir, { recursive: true });
|
|
3155
|
+
const filePath = path8.join(stagedDir, "linker-staged.jsonl");
|
|
2867
3156
|
const lines = candidates.map(
|
|
2868
3157
|
(c) => JSON.stringify({
|
|
2869
3158
|
id: c.id,
|
|
@@ -2876,7 +3165,7 @@ var KnowledgeLinker = class {
|
|
|
2876
3165
|
nodeType: c.nodeType
|
|
2877
3166
|
})
|
|
2878
3167
|
);
|
|
2879
|
-
await
|
|
3168
|
+
await fs7.writeFile(filePath, lines.join("\n") + "\n");
|
|
2880
3169
|
}
|
|
2881
3170
|
/**
|
|
2882
3171
|
* Apply heuristic patterns to content and return candidate extractions.
|
|
@@ -2982,13 +3271,23 @@ var StructuralDriftDetector = class {
|
|
|
2982
3271
|
};
|
|
2983
3272
|
|
|
2984
3273
|
// src/ingest/KnowledgeStagingAggregator.ts
|
|
2985
|
-
import * as
|
|
2986
|
-
import * as
|
|
3274
|
+
import * as fs8 from "fs/promises";
|
|
3275
|
+
import * as path9 from "path";
|
|
3276
|
+
var BUSINESS_NODE_TYPES = [
|
|
3277
|
+
"business_concept",
|
|
3278
|
+
"business_rule",
|
|
3279
|
+
"business_process",
|
|
3280
|
+
"business_term",
|
|
3281
|
+
"business_metric",
|
|
3282
|
+
"business_fact"
|
|
3283
|
+
];
|
|
2987
3284
|
var KnowledgeStagingAggregator = class {
|
|
2988
|
-
constructor(projectDir) {
|
|
3285
|
+
constructor(projectDir, inferenceOptions = {}) {
|
|
2989
3286
|
this.projectDir = projectDir;
|
|
3287
|
+
this.inferenceOptions = inferenceOptions;
|
|
2990
3288
|
}
|
|
2991
3289
|
projectDir;
|
|
3290
|
+
inferenceOptions;
|
|
2992
3291
|
async aggregate(extractorResults, linkerResults, diagramResults) {
|
|
2993
3292
|
const all = [...extractorResults, ...linkerResults, ...diagramResults];
|
|
2994
3293
|
const byHash = /* @__PURE__ */ new Map();
|
|
@@ -3002,59 +3301,157 @@ var KnowledgeStagingAggregator = class {
|
|
|
3002
3301
|
if (deduplicated.length === 0) {
|
|
3003
3302
|
return { staged: 0 };
|
|
3004
3303
|
}
|
|
3005
|
-
const stagedDir =
|
|
3006
|
-
await
|
|
3304
|
+
const stagedDir = path9.join(this.projectDir, ".harness", "knowledge", "staged");
|
|
3305
|
+
await fs8.mkdir(stagedDir, { recursive: true });
|
|
3007
3306
|
const jsonl = deduplicated.map((entry) => JSON.stringify(entry)).join("\n") + "\n";
|
|
3008
|
-
await
|
|
3307
|
+
await fs8.writeFile(path9.join(stagedDir, "pipeline-staged.jsonl"), jsonl, "utf-8");
|
|
3009
3308
|
return { staged: deduplicated.length };
|
|
3010
3309
|
}
|
|
3011
|
-
async
|
|
3012
|
-
const
|
|
3310
|
+
async extractDocName(filePath) {
|
|
3311
|
+
const raw = await fs8.readFile(filePath, "utf-8");
|
|
3312
|
+
const fmMatch = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
|
|
3313
|
+
const body = fmMatch ? fmMatch[1] : raw;
|
|
3314
|
+
const titleMatch = body.match(/^#\s+(.+)$/m);
|
|
3315
|
+
return titleMatch ? titleMatch[1].trim() : path9.basename(filePath, ".md");
|
|
3316
|
+
}
|
|
3317
|
+
async generateGapReport(knowledgeDir, store) {
|
|
3318
|
+
const domainDocNames = /* @__PURE__ */ new Map();
|
|
3319
|
+
const domainEntryCounts = /* @__PURE__ */ new Map();
|
|
3013
3320
|
let totalEntries = 0;
|
|
3014
3321
|
try {
|
|
3015
|
-
const entries = await
|
|
3322
|
+
const entries = await fs8.readdir(knowledgeDir, { withFileTypes: true });
|
|
3016
3323
|
const domainDirs = entries.filter((e) => e.isDirectory());
|
|
3017
3324
|
for (const dir of domainDirs) {
|
|
3018
|
-
const domainPath =
|
|
3019
|
-
const files = await
|
|
3325
|
+
const domainPath = path9.join(knowledgeDir, dir.name);
|
|
3326
|
+
const files = await fs8.readdir(domainPath);
|
|
3020
3327
|
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
3021
3328
|
const entryCount = mdFiles.length;
|
|
3022
3329
|
totalEntries += entryCount;
|
|
3023
|
-
|
|
3330
|
+
domainEntryCounts.set(dir.name, entryCount);
|
|
3331
|
+
if (store) {
|
|
3332
|
+
const names = [];
|
|
3333
|
+
for (const file of mdFiles) {
|
|
3334
|
+
const name = await this.extractDocName(path9.join(domainPath, file));
|
|
3335
|
+
names.push(name.toLowerCase().trim());
|
|
3336
|
+
}
|
|
3337
|
+
domainDocNames.set(dir.name, names);
|
|
3338
|
+
}
|
|
3024
3339
|
}
|
|
3025
3340
|
} catch {
|
|
3026
3341
|
}
|
|
3342
|
+
if (!store) {
|
|
3343
|
+
const domains2 = [];
|
|
3344
|
+
for (const [domain, entryCount] of domainEntryCounts) {
|
|
3345
|
+
domains2.push({ domain, entryCount, extractedCount: 0, gapCount: 0, gapEntries: [] });
|
|
3346
|
+
}
|
|
3347
|
+
return {
|
|
3348
|
+
domains: domains2,
|
|
3349
|
+
totalEntries,
|
|
3350
|
+
totalExtracted: 0,
|
|
3351
|
+
totalGaps: 0,
|
|
3352
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3353
|
+
};
|
|
3354
|
+
}
|
|
3355
|
+
const extractedByDomain = /* @__PURE__ */ new Map();
|
|
3356
|
+
for (const nodeType of BUSINESS_NODE_TYPES) {
|
|
3357
|
+
const nodes = store.findNodes({ type: nodeType });
|
|
3358
|
+
for (const node of nodes) {
|
|
3359
|
+
const domain = inferDomain(node, this.inferenceOptions);
|
|
3360
|
+
const list = extractedByDomain.get(domain) ?? [];
|
|
3361
|
+
list.push(node);
|
|
3362
|
+
extractedByDomain.set(domain, list);
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
for (const [domain, nodes] of extractedByDomain) {
|
|
3366
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3367
|
+
const deduped = nodes.filter((n) => {
|
|
3368
|
+
const key = n.name.toLowerCase().trim();
|
|
3369
|
+
if (seen.has(key)) return false;
|
|
3370
|
+
seen.add(key);
|
|
3371
|
+
return true;
|
|
3372
|
+
});
|
|
3373
|
+
extractedByDomain.set(domain, deduped);
|
|
3374
|
+
}
|
|
3375
|
+
const allDomains = /* @__PURE__ */ new Set([...domainEntryCounts.keys(), ...extractedByDomain.keys()]);
|
|
3376
|
+
const domains = [];
|
|
3377
|
+
let totalExtracted = 0;
|
|
3378
|
+
let totalGaps = 0;
|
|
3379
|
+
for (const domain of allDomains) {
|
|
3380
|
+
const entryCount = domainEntryCounts.get(domain) ?? 0;
|
|
3381
|
+
const extractedNodes = extractedByDomain.get(domain) ?? [];
|
|
3382
|
+
const extractedCount = extractedNodes.length;
|
|
3383
|
+
totalExtracted += extractedCount;
|
|
3384
|
+
const docNames = domainDocNames.get(domain) ?? [];
|
|
3385
|
+
const gapEntries = [];
|
|
3386
|
+
for (const node of extractedNodes) {
|
|
3387
|
+
const normalizedName = node.name.toLowerCase().trim();
|
|
3388
|
+
if (!docNames.includes(normalizedName)) {
|
|
3389
|
+
gapEntries.push({
|
|
3390
|
+
nodeId: node.id,
|
|
3391
|
+
name: node.name,
|
|
3392
|
+
nodeType: node.type,
|
|
3393
|
+
source: node.metadata?.source ?? "unknown",
|
|
3394
|
+
hasContent: Boolean(node.content && node.content.trim().length >= 10)
|
|
3395
|
+
});
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
totalGaps += gapEntries.length;
|
|
3399
|
+
domains.push({ domain, entryCount, extractedCount, gapCount: gapEntries.length, gapEntries });
|
|
3400
|
+
}
|
|
3027
3401
|
return {
|
|
3028
3402
|
domains,
|
|
3029
3403
|
totalEntries,
|
|
3404
|
+
totalExtracted,
|
|
3405
|
+
totalGaps,
|
|
3030
3406
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3031
3407
|
};
|
|
3032
3408
|
}
|
|
3033
3409
|
async writeGapReport(report) {
|
|
3034
|
-
const gapsDir =
|
|
3035
|
-
await
|
|
3410
|
+
const gapsDir = path9.join(this.projectDir, ".harness", "knowledge");
|
|
3411
|
+
await fs8.mkdir(gapsDir, { recursive: true });
|
|
3412
|
+
const hasDifferential = report.totalExtracted > 0;
|
|
3036
3413
|
const lines = [
|
|
3037
3414
|
"# Knowledge Gaps Report",
|
|
3038
3415
|
"",
|
|
3039
3416
|
`Generated: ${report.generatedAt}`,
|
|
3040
3417
|
"",
|
|
3041
3418
|
"## Coverage by Domain",
|
|
3042
|
-
""
|
|
3043
|
-
"| Domain | Entries |",
|
|
3044
|
-
"| ------ | ------- |"
|
|
3419
|
+
""
|
|
3045
3420
|
];
|
|
3046
|
-
|
|
3047
|
-
lines.push(
|
|
3421
|
+
if (hasDifferential) {
|
|
3422
|
+
lines.push(
|
|
3423
|
+
"| Domain | Documented | Extracted | Gaps |",
|
|
3424
|
+
"| ------ | ---------- | --------- | ---- |"
|
|
3425
|
+
);
|
|
3426
|
+
for (const domain of report.domains) {
|
|
3427
|
+
lines.push(
|
|
3428
|
+
`| ${domain.domain} | ${domain.entryCount} | ${domain.extractedCount} | ${domain.gapCount} |`
|
|
3429
|
+
);
|
|
3430
|
+
}
|
|
3431
|
+
lines.push(
|
|
3432
|
+
"",
|
|
3433
|
+
`## Summary`,
|
|
3434
|
+
"",
|
|
3435
|
+
`- **Total Documented:** ${report.totalEntries}`,
|
|
3436
|
+
`- **Total Extracted:** ${report.totalExtracted}`,
|
|
3437
|
+
`- **Total Gaps:** ${report.totalGaps}`,
|
|
3438
|
+
""
|
|
3439
|
+
);
|
|
3440
|
+
} else {
|
|
3441
|
+
lines.push("| Domain | Entries |", "| ------ | ------- |");
|
|
3442
|
+
for (const domain of report.domains) {
|
|
3443
|
+
lines.push(`| ${domain.domain} | ${domain.entryCount} |`);
|
|
3444
|
+
}
|
|
3445
|
+
lines.push("", `## Total Entries: ${report.totalEntries}`, "");
|
|
3048
3446
|
}
|
|
3049
|
-
|
|
3050
|
-
await fs7.writeFile(path8.join(gapsDir, "gaps.md"), lines.join("\n"), "utf-8");
|
|
3447
|
+
await fs8.writeFile(path9.join(gapsDir, "gaps.md"), lines.join("\n"), "utf-8");
|
|
3051
3448
|
}
|
|
3052
3449
|
};
|
|
3053
3450
|
|
|
3054
3451
|
// src/ingest/extractors/ExtractionRunner.ts
|
|
3055
|
-
import * as
|
|
3056
|
-
import * as
|
|
3057
|
-
var
|
|
3452
|
+
import * as fs9 from "fs/promises";
|
|
3453
|
+
import * as path10 from "path";
|
|
3454
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
3058
3455
|
"node_modules",
|
|
3059
3456
|
"dist",
|
|
3060
3457
|
"target",
|
|
@@ -3091,11 +3488,11 @@ var EXT_TO_LANGUAGE = {
|
|
|
3091
3488
|
};
|
|
3092
3489
|
var SKIP_EXTENSIONS2 = /* @__PURE__ */ new Set([".d.ts"]);
|
|
3093
3490
|
function detectLanguage(filePath) {
|
|
3094
|
-
const name =
|
|
3491
|
+
const name = path10.basename(filePath);
|
|
3095
3492
|
for (const skip of SKIP_EXTENSIONS2) {
|
|
3096
3493
|
if (name.endsWith(skip)) return void 0;
|
|
3097
3494
|
}
|
|
3098
|
-
const ext =
|
|
3495
|
+
const ext = path10.extname(filePath);
|
|
3099
3496
|
return EXT_TO_LANGUAGE[ext];
|
|
3100
3497
|
}
|
|
3101
3498
|
var EXTRACTOR_EDGE_TYPE = {
|
|
@@ -3121,7 +3518,7 @@ var ExtractionRunner = class {
|
|
|
3121
3518
|
let nodesAdded = 0;
|
|
3122
3519
|
let nodesUpdated = 0;
|
|
3123
3520
|
let edgesAdded = 0;
|
|
3124
|
-
await
|
|
3521
|
+
await fs9.mkdir(outputDir, { recursive: true });
|
|
3125
3522
|
const files = await this.findSourceFiles(projectDir);
|
|
3126
3523
|
const recordsByExtractor = /* @__PURE__ */ new Map();
|
|
3127
3524
|
for (const ext of this.extractors) {
|
|
@@ -3130,17 +3527,17 @@ var ExtractionRunner = class {
|
|
|
3130
3527
|
for (const filePath of files) {
|
|
3131
3528
|
const language = detectLanguage(filePath);
|
|
3132
3529
|
if (!language) continue;
|
|
3133
|
-
const ext =
|
|
3530
|
+
const ext = path10.extname(filePath);
|
|
3134
3531
|
let content;
|
|
3135
3532
|
try {
|
|
3136
|
-
content = await
|
|
3533
|
+
content = await fs9.readFile(filePath, "utf-8");
|
|
3137
3534
|
} catch (err) {
|
|
3138
3535
|
errors.push(
|
|
3139
3536
|
`Failed to read ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
3140
3537
|
);
|
|
3141
3538
|
continue;
|
|
3142
3539
|
}
|
|
3143
|
-
const relativePath =
|
|
3540
|
+
const relativePath = path10.relative(projectDir, filePath).replaceAll("\\", "/");
|
|
3144
3541
|
for (const extractor2 of this.extractors) {
|
|
3145
3542
|
if (!extractor2.supportedExtensions.includes(ext)) continue;
|
|
3146
3543
|
try {
|
|
@@ -3177,9 +3574,9 @@ var ExtractionRunner = class {
|
|
|
3177
3574
|
}
|
|
3178
3575
|
/** Write extraction records to JSONL file. */
|
|
3179
3576
|
async writeJsonl(records, outputDir, extractorName) {
|
|
3180
|
-
const filePath =
|
|
3577
|
+
const filePath = path10.join(outputDir, `${extractorName}.jsonl`);
|
|
3181
3578
|
const lines = records.map((r) => JSON.stringify(r));
|
|
3182
|
-
await
|
|
3579
|
+
await fs9.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
|
|
3183
3580
|
}
|
|
3184
3581
|
/** Create or update a graph node from an extraction record. */
|
|
3185
3582
|
persistRecord(record, store) {
|
|
@@ -3251,13 +3648,13 @@ var ExtractionRunner = class {
|
|
|
3251
3648
|
const results = [];
|
|
3252
3649
|
let entries;
|
|
3253
3650
|
try {
|
|
3254
|
-
entries = await
|
|
3651
|
+
entries = await fs9.readdir(dir, { withFileTypes: true });
|
|
3255
3652
|
} catch {
|
|
3256
3653
|
return results;
|
|
3257
3654
|
}
|
|
3258
3655
|
for (const entry of entries) {
|
|
3259
|
-
const fullPath =
|
|
3260
|
-
if (entry.isDirectory() && !
|
|
3656
|
+
const fullPath = path10.join(dir, entry.name);
|
|
3657
|
+
if (entry.isDirectory() && !SKIP_DIRS2.has(entry.name)) {
|
|
3261
3658
|
results.push(...await this.findSourceFiles(fullPath));
|
|
3262
3659
|
} else if (entry.isFile() && detectLanguage(fullPath) !== void 0) {
|
|
3263
3660
|
results.push(fullPath);
|
|
@@ -4383,57 +4780,9 @@ var ImageAnalysisExtractor = class {
|
|
|
4383
4780
|
const errors = [];
|
|
4384
4781
|
for (const imagePath of imagePaths) {
|
|
4385
4782
|
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
|
-
}
|
|
4783
|
+
const counts = await this.processImage(store, imagePath);
|
|
4784
|
+
nodesAdded += counts.nodes;
|
|
4785
|
+
edgesAdded += counts.edges;
|
|
4437
4786
|
} catch (err) {
|
|
4438
4787
|
errors.push(
|
|
4439
4788
|
`Image analysis failed for ${imagePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -4449,6 +4798,68 @@ var ImageAnalysisExtractor = class {
|
|
|
4449
4798
|
durationMs: Date.now() - start
|
|
4450
4799
|
};
|
|
4451
4800
|
}
|
|
4801
|
+
/** Analyze a single image and add annotation + concept nodes to the store. */
|
|
4802
|
+
async processImage(store, imagePath) {
|
|
4803
|
+
const response = await this.provider.analyze({
|
|
4804
|
+
prompt: "Analyze this image and provide a structured description of its visual contents.",
|
|
4805
|
+
systemPrompt: "You are an image analysis assistant. Describe the image contents, detect UI elements, extract visible text, identify design patterns, and note accessibility concerns.",
|
|
4806
|
+
responseSchema: {},
|
|
4807
|
+
// Schema handled by provider
|
|
4808
|
+
maxTokens: 1e3
|
|
4809
|
+
});
|
|
4810
|
+
const result = response.result;
|
|
4811
|
+
const annotationId = `img:${hash(imagePath)}`;
|
|
4812
|
+
let nodes = 0;
|
|
4813
|
+
let edges = 0;
|
|
4814
|
+
store.addNode({
|
|
4815
|
+
id: annotationId,
|
|
4816
|
+
type: "image_annotation",
|
|
4817
|
+
name: result.description.slice(0, 200),
|
|
4818
|
+
path: imagePath,
|
|
4819
|
+
content: result.description,
|
|
4820
|
+
hash: hash(result.description),
|
|
4821
|
+
metadata: {
|
|
4822
|
+
source: "image-analysis",
|
|
4823
|
+
detectedElements: result.detectedElements,
|
|
4824
|
+
extractedText: result.extractedText,
|
|
4825
|
+
designPatterns: result.designPatterns,
|
|
4826
|
+
accessibilityNotes: result.accessibilityNotes,
|
|
4827
|
+
model: response.model
|
|
4828
|
+
}
|
|
4829
|
+
});
|
|
4830
|
+
nodes++;
|
|
4831
|
+
edges += this.linkToFileNode(store, annotationId, imagePath);
|
|
4832
|
+
for (const pattern of result.designPatterns) {
|
|
4833
|
+
edges += this.addDesignPatternConcept(store, annotationId, imagePath, pattern);
|
|
4834
|
+
nodes++;
|
|
4835
|
+
}
|
|
4836
|
+
return { nodes, edges };
|
|
4837
|
+
}
|
|
4838
|
+
/** Link annotation to its source file node if it exists. Returns 1 if linked, 0 otherwise. */
|
|
4839
|
+
linkToFileNode(store, annotationId, imagePath) {
|
|
4840
|
+
const fileNode = store.findNodes({ type: "file" }).find((n) => n.path === imagePath);
|
|
4841
|
+
if (!fileNode) return 0;
|
|
4842
|
+
store.addEdge({ from: annotationId, to: fileNode.id, type: "annotates" });
|
|
4843
|
+
return 1;
|
|
4844
|
+
}
|
|
4845
|
+
/** Create a business_concept node for a design pattern and link it. Returns edges added. */
|
|
4846
|
+
addDesignPatternConcept(store, annotationId, imagePath, pattern) {
|
|
4847
|
+
const conceptId = `img:concept:${hash(pattern + imagePath)}`;
|
|
4848
|
+
store.addNode({
|
|
4849
|
+
id: conceptId,
|
|
4850
|
+
type: "business_concept",
|
|
4851
|
+
name: pattern,
|
|
4852
|
+
content: `Design pattern detected in ${imagePath}: ${pattern}`,
|
|
4853
|
+
hash: hash(pattern),
|
|
4854
|
+
metadata: {
|
|
4855
|
+
source: "image-analysis",
|
|
4856
|
+
sourceImage: imagePath,
|
|
4857
|
+
domain: "design"
|
|
4858
|
+
}
|
|
4859
|
+
});
|
|
4860
|
+
store.addEdge({ from: annotationId, to: conceptId, type: "contains" });
|
|
4861
|
+
return 1;
|
|
4862
|
+
}
|
|
4452
4863
|
};
|
|
4453
4864
|
|
|
4454
4865
|
// src/ingest/knowledgeTypes.ts
|
|
@@ -4459,6 +4870,7 @@ var KNOWLEDGE_NODE_TYPES = [
|
|
|
4459
4870
|
"business_term",
|
|
4460
4871
|
"business_concept",
|
|
4461
4872
|
"business_metric",
|
|
4873
|
+
"decision",
|
|
4462
4874
|
"design_token",
|
|
4463
4875
|
"design_constraint",
|
|
4464
4876
|
"aesthetic_intent",
|
|
@@ -4499,83 +4911,99 @@ function classifyConflict(a, b) {
|
|
|
4499
4911
|
return "temporal_conflict";
|
|
4500
4912
|
return "status_divergence";
|
|
4501
4913
|
}
|
|
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
|
-
}
|
|
4914
|
+
var SEVERITY_MAP = {
|
|
4915
|
+
value_mismatch: "critical",
|
|
4916
|
+
definition_conflict: "high",
|
|
4917
|
+
status_divergence: "medium",
|
|
4918
|
+
temporal_conflict: "medium"
|
|
4919
|
+
};
|
|
4920
|
+
function buildEntry(node, source) {
|
|
4921
|
+
return {
|
|
4922
|
+
nodeId: node.id,
|
|
4923
|
+
source,
|
|
4924
|
+
name: node.name,
|
|
4925
|
+
content: node.content ?? "",
|
|
4926
|
+
lastModified: node.lastModified
|
|
4927
|
+
};
|
|
4928
|
+
}
|
|
4929
|
+
function groupByName(nodes) {
|
|
4930
|
+
const byName = /* @__PURE__ */ new Map();
|
|
4931
|
+
for (const node of nodes) {
|
|
4932
|
+
const key = node.name.toLowerCase().trim();
|
|
4933
|
+
const group = byName.get(key) ?? [];
|
|
4934
|
+
group.push(node);
|
|
4935
|
+
byName.set(key, group);
|
|
4936
|
+
}
|
|
4937
|
+
return byName;
|
|
4938
|
+
}
|
|
4939
|
+
function tryAddContradiction(acc, a, b, similarity) {
|
|
4940
|
+
const sourceA = a.metadata.source ?? "unknown";
|
|
4941
|
+
const sourceB = b.metadata.source ?? "unknown";
|
|
4942
|
+
if (sourceA === sourceB) return;
|
|
4943
|
+
if ((a.hash ?? a.id) === (b.hash ?? b.id)) return;
|
|
4944
|
+
const pairId = [a.id, b.id].sort().join(":");
|
|
4945
|
+
if (acc.seen.has(pairId)) return;
|
|
4946
|
+
acc.seen.add(pairId);
|
|
4947
|
+
const conflictType = classifyConflict(a, b);
|
|
4948
|
+
const pairKey = [sourceA, sourceB].sort().join("\u2194");
|
|
4949
|
+
acc.sourcePairCounts[pairKey] = (acc.sourcePairCounts[pairKey] ?? 0) + 1;
|
|
4950
|
+
acc.contradictions.push({
|
|
4951
|
+
id: `contradiction:${a.id}:${b.id}`,
|
|
4952
|
+
entityA: buildEntry(a, sourceA),
|
|
4953
|
+
entityB: buildEntry(b, sourceB),
|
|
4954
|
+
similarity,
|
|
4955
|
+
conflictType,
|
|
4956
|
+
severity: SEVERITY_MAP[conflictType],
|
|
4957
|
+
description: `"${a.name}" has conflicting definitions from ${sourceA} and ${sourceB}`
|
|
4958
|
+
});
|
|
4959
|
+
}
|
|
4960
|
+
function collectExactMatches(byName, acc) {
|
|
4961
|
+
for (const [, group] of byName) {
|
|
4962
|
+
if (group.length < 2) continue;
|
|
4963
|
+
for (let i = 0; i < group.length; i++) {
|
|
4964
|
+
for (let j = i + 1; j < group.length; j++) {
|
|
4965
|
+
tryAddContradiction(acc, group[i], group[j], 1);
|
|
4557
4966
|
}
|
|
4558
4967
|
}
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4968
|
+
}
|
|
4969
|
+
}
|
|
4970
|
+
function collectFuzzyMatches(byName, acc) {
|
|
4971
|
+
const keys = [...byName.keys()];
|
|
4972
|
+
for (let i = 0; i < keys.length; i++) {
|
|
4973
|
+
for (let j = i + 1; j < keys.length; j++) {
|
|
4974
|
+
const keyA = keys[i];
|
|
4975
|
+
const keyB = keys[j];
|
|
4976
|
+
const maxLen = Math.max(keyA.length, keyB.length);
|
|
4977
|
+
const minLen = Math.min(keyA.length, keyB.length);
|
|
4978
|
+
if (minLen / maxLen < SIMILARITY_THRESHOLD) continue;
|
|
4979
|
+
const ratio = levenshteinRatio(keyA, keyB);
|
|
4980
|
+
if (ratio < SIMILARITY_THRESHOLD) continue;
|
|
4981
|
+
const groupA = byName.get(keyA);
|
|
4982
|
+
const groupB = byName.get(keyB);
|
|
4983
|
+
for (const a of groupA) {
|
|
4984
|
+
for (const b of groupB) {
|
|
4985
|
+
tryAddContradiction(acc, a, b, ratio);
|
|
4575
4986
|
}
|
|
4576
4987
|
}
|
|
4577
4988
|
}
|
|
4578
|
-
|
|
4989
|
+
}
|
|
4990
|
+
}
|
|
4991
|
+
var ContradictionDetector = class {
|
|
4992
|
+
detect(store) {
|
|
4993
|
+
const nodes = KNOWLEDGE_NODE_TYPES.flatMap((t) => store.findNodes({ type: t }));
|
|
4994
|
+
const byName = groupByName(nodes);
|
|
4995
|
+
const acc = {
|
|
4996
|
+
contradictions: [],
|
|
4997
|
+
sourcePairCounts: {},
|
|
4998
|
+
seen: /* @__PURE__ */ new Set()
|
|
4999
|
+
};
|
|
5000
|
+
collectExactMatches(byName, acc);
|
|
5001
|
+
collectFuzzyMatches(byName, acc);
|
|
5002
|
+
return {
|
|
5003
|
+
contradictions: acc.contradictions,
|
|
5004
|
+
sourcePairCounts: acc.sourcePairCounts,
|
|
5005
|
+
totalChecked: nodes.length
|
|
5006
|
+
};
|
|
4579
5007
|
}
|
|
4580
5008
|
};
|
|
4581
5009
|
|
|
@@ -4606,64 +5034,80 @@ function toGrade(score) {
|
|
|
4606
5034
|
if (score >= 20) return "D";
|
|
4607
5035
|
return "F";
|
|
4608
5036
|
}
|
|
5037
|
+
function groupByDomain(nodes, _fallback, options = {}) {
|
|
5038
|
+
const map = /* @__PURE__ */ new Map();
|
|
5039
|
+
for (const node of nodes) {
|
|
5040
|
+
const domain = inferDomain(node, options);
|
|
5041
|
+
const group = map.get(domain) ?? [];
|
|
5042
|
+
group.push(node);
|
|
5043
|
+
map.set(domain, group);
|
|
5044
|
+
}
|
|
5045
|
+
return map;
|
|
5046
|
+
}
|
|
5047
|
+
function countLinkedEntities(codeEntries, store) {
|
|
5048
|
+
const linkedIds = /* @__PURE__ */ new Set();
|
|
5049
|
+
for (const codeNode of codeEntries) {
|
|
5050
|
+
if (hasKnowledgeEdge(codeNode.id, store)) {
|
|
5051
|
+
linkedIds.add(codeNode.id);
|
|
5052
|
+
}
|
|
5053
|
+
}
|
|
5054
|
+
return linkedIds;
|
|
5055
|
+
}
|
|
5056
|
+
function hasKnowledgeEdge(nodeId, store) {
|
|
5057
|
+
for (const edgeType of KNOWLEDGE_EDGE_TYPES) {
|
|
5058
|
+
if (store.getEdges({ to: nodeId, type: edgeType }).length > 0) return true;
|
|
5059
|
+
}
|
|
5060
|
+
return false;
|
|
5061
|
+
}
|
|
5062
|
+
function computeSourceBreakdown(knEntries) {
|
|
5063
|
+
const breakdown = {};
|
|
5064
|
+
for (const kn of knEntries) {
|
|
5065
|
+
const src = kn.metadata.source ?? "unknown";
|
|
5066
|
+
breakdown[src] = (breakdown[src] ?? 0) + 1;
|
|
5067
|
+
}
|
|
5068
|
+
return breakdown;
|
|
5069
|
+
}
|
|
5070
|
+
function computeDomainScore(knowledgeEntries, codeEntities, linkedEntities, uniqueSources) {
|
|
5071
|
+
const codeCoverageComponent = codeEntities > 0 ? linkedEntities / codeEntities * 60 : 0;
|
|
5072
|
+
const knowledgeDepthComponent = Math.min(knowledgeEntries / 10, 1) * 20;
|
|
5073
|
+
const sourceDiversityComponent = Math.min(uniqueSources / 3, 1) * 20;
|
|
5074
|
+
return Math.round(codeCoverageComponent + knowledgeDepthComponent + sourceDiversityComponent);
|
|
5075
|
+
}
|
|
5076
|
+
function scoreDomain(domain, knEntries, codeEntries, store) {
|
|
5077
|
+
const linkedIds = countLinkedEntities(codeEntries, store);
|
|
5078
|
+
const sourceBreakdown = computeSourceBreakdown(knEntries);
|
|
5079
|
+
const codeEntities = codeEntries.length;
|
|
5080
|
+
const linkedEntities = linkedIds.size;
|
|
5081
|
+
const knowledgeEntries = knEntries.length;
|
|
5082
|
+
const uniqueSources = Object.keys(sourceBreakdown).length;
|
|
5083
|
+
const score = computeDomainScore(knowledgeEntries, codeEntities, linkedEntities, uniqueSources);
|
|
5084
|
+
return {
|
|
5085
|
+
domain,
|
|
5086
|
+
score,
|
|
5087
|
+
knowledgeEntries,
|
|
5088
|
+
codeEntities,
|
|
5089
|
+
linkedEntities,
|
|
5090
|
+
unlinkedEntities: codeEntities - linkedEntities,
|
|
5091
|
+
sourceBreakdown,
|
|
5092
|
+
grade: toGrade(score)
|
|
5093
|
+
};
|
|
5094
|
+
}
|
|
4609
5095
|
var CoverageScorer = class {
|
|
5096
|
+
constructor(inferenceOptions = {}) {
|
|
5097
|
+
this.inferenceOptions = inferenceOptions;
|
|
5098
|
+
}
|
|
5099
|
+
inferenceOptions;
|
|
4610
5100
|
score(store) {
|
|
4611
5101
|
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
|
-
}
|
|
5102
|
+
const domainMap = groupByDomain(knowledgeNodes, void 0, this.inferenceOptions);
|
|
4619
5103
|
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
|
-
}
|
|
5104
|
+
const codeDomains = groupByDomain(codeNodes, void 0, this.inferenceOptions);
|
|
4627
5105
|
const allDomains = /* @__PURE__ */ new Set([...domainMap.keys(), ...codeDomains.keys()]);
|
|
4628
5106
|
const domains = [];
|
|
4629
5107
|
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
|
|
5108
|
+
domains.push(
|
|
5109
|
+
scoreDomain(domain, domainMap.get(domain) ?? [], codeDomains.get(domain) ?? [], store)
|
|
4656
5110
|
);
|
|
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
5111
|
}
|
|
4668
5112
|
const overallScore = domains.length > 0 ? Math.round(domains.reduce((sum, d) => sum + d.score, 0) / domains.length) : 0;
|
|
4669
5113
|
return {
|
|
@@ -4673,19 +5117,157 @@ var CoverageScorer = class {
|
|
|
4673
5117
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4674
5118
|
};
|
|
4675
5119
|
}
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
5120
|
+
};
|
|
5121
|
+
|
|
5122
|
+
// src/ingest/KnowledgeDocMaterializer.ts
|
|
5123
|
+
import * as fs10 from "fs/promises";
|
|
5124
|
+
import * as path11 from "path";
|
|
5125
|
+
var VALID_BUSINESS_TYPES = /* @__PURE__ */ new Set([
|
|
5126
|
+
"business_rule",
|
|
5127
|
+
"business_process",
|
|
5128
|
+
"business_concept",
|
|
5129
|
+
"business_term",
|
|
5130
|
+
"business_metric"
|
|
5131
|
+
]);
|
|
5132
|
+
var DEFAULT_MAX_DOCS = 50;
|
|
5133
|
+
var MAX_COLLISION_SUFFIX = 10;
|
|
5134
|
+
var KnowledgeDocMaterializer = class {
|
|
5135
|
+
constructor(store, inferenceOptions = {}) {
|
|
5136
|
+
this.store = store;
|
|
5137
|
+
this.inferenceOptions = inferenceOptions;
|
|
5138
|
+
}
|
|
5139
|
+
store;
|
|
5140
|
+
inferenceOptions;
|
|
5141
|
+
async materialize(gapEntries, options) {
|
|
5142
|
+
const maxDocs = options.maxDocs ?? DEFAULT_MAX_DOCS;
|
|
5143
|
+
const created = [];
|
|
5144
|
+
const skipped = [];
|
|
5145
|
+
const createdNames = /* @__PURE__ */ new Set();
|
|
5146
|
+
for (const entry of gapEntries) {
|
|
5147
|
+
const resolved = await this.resolveEntry(
|
|
5148
|
+
entry,
|
|
5149
|
+
created.length,
|
|
5150
|
+
maxDocs,
|
|
5151
|
+
createdNames,
|
|
5152
|
+
options
|
|
5153
|
+
);
|
|
5154
|
+
if ("reason" in resolved) {
|
|
5155
|
+
skipped.push(resolved);
|
|
5156
|
+
continue;
|
|
5157
|
+
}
|
|
5158
|
+
const { node, domain, domainDir, nameKey } = resolved;
|
|
5159
|
+
await fs10.mkdir(domainDir, { recursive: true });
|
|
5160
|
+
const basename10 = this.generateFilename(entry.name);
|
|
5161
|
+
const filename = await this.resolveCollision(domainDir, basename10);
|
|
5162
|
+
const content = this.formatDoc(node, domain);
|
|
5163
|
+
const filePath = ["docs", "knowledge", domain, filename].join("/");
|
|
5164
|
+
await fs10.writeFile(path11.join(options.projectDir, filePath), content, "utf-8");
|
|
5165
|
+
createdNames.add(nameKey);
|
|
5166
|
+
created.push({ filePath, nodeId: entry.nodeId, domain, name: entry.name });
|
|
5167
|
+
}
|
|
5168
|
+
return { created, skipped };
|
|
5169
|
+
}
|
|
5170
|
+
async resolveEntry(entry, createdCount, maxDocs, createdNames, options) {
|
|
5171
|
+
if (!entry.hasContent) {
|
|
5172
|
+
return { nodeId: entry.nodeId, name: entry.name, reason: "no_content" };
|
|
5173
|
+
}
|
|
5174
|
+
const node = this.store.getNode(entry.nodeId);
|
|
5175
|
+
if (!node || !node.content || node.content.trim().length < 10) {
|
|
5176
|
+
return { nodeId: entry.nodeId, name: entry.name, reason: "no_content" };
|
|
5177
|
+
}
|
|
5178
|
+
const domain = this.inferDomain(node);
|
|
5179
|
+
if (!domain || /[/\\]|\.\.|\0/.test(domain)) {
|
|
5180
|
+
return { nodeId: entry.nodeId, name: entry.name, reason: "no_domain" };
|
|
5181
|
+
}
|
|
5182
|
+
if (createdCount >= maxDocs) {
|
|
5183
|
+
return { nodeId: entry.nodeId, name: entry.name, reason: "cap_reached" };
|
|
5184
|
+
}
|
|
5185
|
+
const domainDir = path11.join(options.projectDir, "docs", "knowledge", domain);
|
|
5186
|
+
const nameKey = `${domain}:${entry.name.toLowerCase().trim()}`;
|
|
5187
|
+
if (!createdNames.has(nameKey) && await this.hasExistingDoc(domainDir, entry.name)) {
|
|
5188
|
+
return { nodeId: entry.nodeId, name: entry.name, reason: "already_documented" };
|
|
5189
|
+
}
|
|
5190
|
+
if (options.dryRun) {
|
|
5191
|
+
return { nodeId: entry.nodeId, name: entry.name, reason: "dry_run" };
|
|
5192
|
+
}
|
|
5193
|
+
return { node, domain, domainDir, nameKey };
|
|
5194
|
+
}
|
|
5195
|
+
inferDomain(node) {
|
|
5196
|
+
const result = inferDomain(node, this.inferenceOptions);
|
|
5197
|
+
return result === "unknown" ? null : result;
|
|
5198
|
+
}
|
|
5199
|
+
generateFilename(name) {
|
|
5200
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
|
|
5201
|
+
return `${slug}.md`;
|
|
5202
|
+
}
|
|
5203
|
+
async resolveCollision(dir, basename10) {
|
|
5204
|
+
const ext = path11.extname(basename10);
|
|
5205
|
+
const stem = path11.basename(basename10, ext);
|
|
5206
|
+
try {
|
|
5207
|
+
await fs10.access(path11.join(dir, basename10));
|
|
5208
|
+
} catch {
|
|
5209
|
+
return basename10;
|
|
5210
|
+
}
|
|
5211
|
+
for (let i = 2; i <= MAX_COLLISION_SUFFIX; i++) {
|
|
5212
|
+
const candidate = `${stem}-${i}${ext}`;
|
|
5213
|
+
try {
|
|
5214
|
+
await fs10.access(path11.join(dir, candidate));
|
|
5215
|
+
} catch {
|
|
5216
|
+
return candidate;
|
|
5217
|
+
}
|
|
5218
|
+
}
|
|
5219
|
+
throw new Error(
|
|
5220
|
+
`Cannot resolve filename collision for "${basename10}" after ${MAX_COLLISION_SUFFIX} attempts`
|
|
5221
|
+
);
|
|
5222
|
+
}
|
|
5223
|
+
formatDoc(node, domain) {
|
|
5224
|
+
const mappedType = this.mapNodeType(node);
|
|
5225
|
+
const sanitize = (s) => s.replace(/[\n\r]/g, " ").replace(/:/g, "-");
|
|
5226
|
+
const lines = ["---", `type: ${sanitize(mappedType)}`, `domain: ${sanitize(domain)}`];
|
|
5227
|
+
const tags = node.metadata?.tags;
|
|
5228
|
+
if (Array.isArray(tags) && tags.length > 0) {
|
|
5229
|
+
lines.push(`tags: [${tags.map((t) => sanitize(String(t))).join(", ")}]`);
|
|
5230
|
+
}
|
|
5231
|
+
const related = node.metadata?.related;
|
|
5232
|
+
if (Array.isArray(related) && related.length > 0) {
|
|
5233
|
+
lines.push(`related: [${related.map((r) => sanitize(String(r))).join(", ")}]`);
|
|
5234
|
+
}
|
|
5235
|
+
const title = (node.name ?? "").replace(/[\n\r]/g, " ");
|
|
5236
|
+
lines.push("---", "", `# ${title}`, "", node.content ?? "", "");
|
|
5237
|
+
return lines.join("\n");
|
|
5238
|
+
}
|
|
5239
|
+
/** Check if a doc with a matching title already exists in the domain directory. */
|
|
5240
|
+
async hasExistingDoc(domainDir, name) {
|
|
5241
|
+
const normalizedName = name.toLowerCase().trim();
|
|
5242
|
+
try {
|
|
5243
|
+
const files = await fs10.readdir(domainDir);
|
|
5244
|
+
for (const file of files) {
|
|
5245
|
+
if (!file.endsWith(".md")) continue;
|
|
5246
|
+
const raw = await fs10.readFile(path11.join(domainDir, file), "utf-8");
|
|
5247
|
+
const fmMatch = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
|
|
5248
|
+
const body = fmMatch ? fmMatch[1] : raw;
|
|
5249
|
+
const titleMatch = body.match(/^#\s+(.+)$/m);
|
|
5250
|
+
if (titleMatch && titleMatch[1].trim().toLowerCase() === normalizedName) {
|
|
5251
|
+
return true;
|
|
5252
|
+
}
|
|
5253
|
+
}
|
|
5254
|
+
} catch {
|
|
5255
|
+
}
|
|
5256
|
+
return false;
|
|
5257
|
+
}
|
|
5258
|
+
mapNodeType(node) {
|
|
5259
|
+
if (VALID_BUSINESS_TYPES.has(node.type)) {
|
|
5260
|
+
return node.type;
|
|
5261
|
+
}
|
|
5262
|
+
if (node.type === "business_fact") {
|
|
5263
|
+
return "business_rule";
|
|
5264
|
+
}
|
|
5265
|
+
return "business_concept";
|
|
4684
5266
|
}
|
|
4685
5267
|
};
|
|
4686
5268
|
|
|
4687
5269
|
// src/ingest/KnowledgePipelineRunner.ts
|
|
4688
|
-
var
|
|
5270
|
+
var BUSINESS_NODE_TYPES2 = [
|
|
4689
5271
|
"business_concept",
|
|
4690
5272
|
"business_rule",
|
|
4691
5273
|
"business_process",
|
|
@@ -4694,7 +5276,8 @@ var BUSINESS_NODE_TYPES = [
|
|
|
4694
5276
|
"business_fact"
|
|
4695
5277
|
];
|
|
4696
5278
|
var SNAPSHOT_NODE_TYPES = [
|
|
4697
|
-
...
|
|
5279
|
+
...BUSINESS_NODE_TYPES2,
|
|
5280
|
+
"decision",
|
|
4698
5281
|
"design_token",
|
|
4699
5282
|
"design_constraint",
|
|
4700
5283
|
"aesthetic_intent",
|
|
@@ -4705,52 +5288,102 @@ var KnowledgePipelineRunner = class {
|
|
|
4705
5288
|
this.store = store;
|
|
4706
5289
|
}
|
|
4707
5290
|
store;
|
|
5291
|
+
/** Resolved per-`run()` inference options. Set on entry to `run()`. */
|
|
5292
|
+
inferenceOptions = {};
|
|
4708
5293
|
async run(options) {
|
|
4709
|
-
|
|
5294
|
+
this.inferenceOptions = options.inferenceOptions ?? {};
|
|
4710
5295
|
const remediations = [];
|
|
4711
5296
|
const preSnapshot = this.buildSnapshot(options.domain);
|
|
4712
5297
|
const extraction = await this.extract(options);
|
|
4713
5298
|
const postSnapshot = this.buildSnapshot(options.domain);
|
|
4714
5299
|
let driftResult = this.reconcile(preSnapshot, postSnapshot);
|
|
4715
|
-
const
|
|
4716
|
-
const contradictions = contradictionDetector.detect(this.store);
|
|
5300
|
+
const contradictions = new ContradictionDetector().detect(this.store);
|
|
4717
5301
|
let gapReport = await this.detect(options);
|
|
4718
|
-
const
|
|
4719
|
-
|
|
5302
|
+
const coverage = new CoverageScorer(this.inferenceOptions).score(this.store);
|
|
5303
|
+
let materialization;
|
|
4720
5304
|
let iterations = 1;
|
|
4721
5305
|
if (options.fix) {
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
}
|
|
5306
|
+
const loopResult = await this.runRemediationLoop(
|
|
5307
|
+
options,
|
|
5308
|
+
driftResult,
|
|
5309
|
+
gapReport,
|
|
5310
|
+
remediations
|
|
5311
|
+
);
|
|
5312
|
+
iterations = loopResult.iterations;
|
|
5313
|
+
materialization = loopResult.materialization;
|
|
5314
|
+
}
|
|
5315
|
+
if (options.fix && iterations > 1) {
|
|
5316
|
+
const finalSnapshot = this.buildSnapshot(options.domain);
|
|
5317
|
+
driftResult = this.reconcile(preSnapshot, finalSnapshot);
|
|
5318
|
+
gapReport = await this.detect(options);
|
|
4736
5319
|
}
|
|
4737
5320
|
await this.stageNewFindings(driftResult, options);
|
|
5321
|
+
return this.buildResult(
|
|
5322
|
+
driftResult,
|
|
5323
|
+
iterations,
|
|
5324
|
+
extraction,
|
|
5325
|
+
gapReport,
|
|
5326
|
+
remediations,
|
|
5327
|
+
contradictions,
|
|
5328
|
+
coverage,
|
|
5329
|
+
materialization
|
|
5330
|
+
);
|
|
5331
|
+
}
|
|
5332
|
+
/** Run the remediation convergence loop; returns iteration count and accumulated materialization. */
|
|
5333
|
+
async runRemediationLoop(options, driftResult, gapReport, remediations) {
|
|
5334
|
+
const maxIterations = options.maxIterations ?? 5;
|
|
5335
|
+
let iterations = 1;
|
|
5336
|
+
let currentDrift = driftResult;
|
|
5337
|
+
let currentGapReport = gapReport;
|
|
5338
|
+
let previousIssueCount = currentDrift.findings.length + currentGapReport.totalGaps;
|
|
5339
|
+
let accumulatedMaterialization;
|
|
5340
|
+
while (iterations < maxIterations) {
|
|
5341
|
+
if (currentDrift.findings.length === 0 && currentGapReport.totalGaps === 0) break;
|
|
5342
|
+
const matResult = await this.remediate(currentDrift, remediations, options, currentGapReport);
|
|
5343
|
+
if (matResult) {
|
|
5344
|
+
if (!accumulatedMaterialization) {
|
|
5345
|
+
accumulatedMaterialization = matResult;
|
|
5346
|
+
} else {
|
|
5347
|
+
accumulatedMaterialization = {
|
|
5348
|
+
created: [...accumulatedMaterialization.created, ...matResult.created],
|
|
5349
|
+
skipped: [...accumulatedMaterialization.skipped, ...matResult.skipped]
|
|
5350
|
+
};
|
|
5351
|
+
}
|
|
5352
|
+
}
|
|
5353
|
+
const preSnapshot = this.buildSnapshot(options.domain);
|
|
5354
|
+
await this.extract(options);
|
|
5355
|
+
const postSnapshot = this.buildSnapshot(options.domain);
|
|
5356
|
+
currentDrift = this.reconcile(preSnapshot, postSnapshot);
|
|
5357
|
+
currentGapReport = await this.detect(options);
|
|
5358
|
+
iterations++;
|
|
5359
|
+
const currentIssueCount = currentDrift.findings.length + currentGapReport.totalGaps;
|
|
5360
|
+
if (currentIssueCount >= previousIssueCount) break;
|
|
5361
|
+
previousIssueCount = currentIssueCount;
|
|
5362
|
+
}
|
|
5363
|
+
return {
|
|
5364
|
+
iterations,
|
|
5365
|
+
...accumulatedMaterialization ? { materialization: accumulatedMaterialization } : {}
|
|
5366
|
+
};
|
|
5367
|
+
}
|
|
5368
|
+
/** Assemble the final pipeline result. */
|
|
5369
|
+
buildResult(driftResult, iterations, extraction, gaps, remediations, contradictions, coverage, materialization) {
|
|
4738
5370
|
return {
|
|
4739
5371
|
verdict: this.computeVerdict(driftResult),
|
|
4740
5372
|
driftScore: driftResult.driftScore,
|
|
4741
5373
|
iterations,
|
|
4742
5374
|
findings: driftResult.summary,
|
|
4743
5375
|
extraction,
|
|
4744
|
-
gaps
|
|
5376
|
+
gaps,
|
|
4745
5377
|
remediations,
|
|
4746
5378
|
contradictions,
|
|
4747
|
-
coverage
|
|
5379
|
+
coverage,
|
|
5380
|
+
...materialization ? { materialization } : {}
|
|
4748
5381
|
};
|
|
4749
5382
|
}
|
|
4750
5383
|
// ── Phase 1: EXTRACT ──────────────────────────────────────────────────────
|
|
4751
5384
|
async extract(options) {
|
|
4752
|
-
const extractedDir =
|
|
4753
|
-
await
|
|
5385
|
+
const extractedDir = path12.join(options.projectDir, ".harness", "knowledge", "extracted");
|
|
5386
|
+
await fs11.mkdir(extractedDir, { recursive: true });
|
|
4754
5387
|
const runner = createExtractionRunner();
|
|
4755
5388
|
const extractionResult = await runner.run(options.projectDir, this.store, extractedDir);
|
|
4756
5389
|
const diagramParser = new DiagramParser(this.store);
|
|
@@ -4764,7 +5397,7 @@ var KnowledgePipelineRunner = class {
|
|
|
4764
5397
|
const imageResult = await imageExtractor.analyze(this.store, imagePaths);
|
|
4765
5398
|
imageCount = imageResult.nodesAdded;
|
|
4766
5399
|
}
|
|
4767
|
-
const knowledgeDir =
|
|
5400
|
+
const knowledgeDir = path12.join(options.projectDir, "docs", "knowledge");
|
|
4768
5401
|
const bkIngestor = new BusinessKnowledgeIngestor(this.store);
|
|
4769
5402
|
let bkResult;
|
|
4770
5403
|
try {
|
|
@@ -4779,6 +5412,21 @@ var KnowledgePipelineRunner = class {
|
|
|
4779
5412
|
durationMs: 0
|
|
4780
5413
|
};
|
|
4781
5414
|
}
|
|
5415
|
+
const decisionsDir = path12.join(options.projectDir, "docs", "knowledge", "decisions");
|
|
5416
|
+
const decisionIngestor = new DecisionIngestor(this.store);
|
|
5417
|
+
let decisionResult;
|
|
5418
|
+
try {
|
|
5419
|
+
decisionResult = await decisionIngestor.ingest(decisionsDir);
|
|
5420
|
+
} catch {
|
|
5421
|
+
decisionResult = {
|
|
5422
|
+
nodesAdded: 0,
|
|
5423
|
+
nodesUpdated: 0,
|
|
5424
|
+
edgesAdded: 0,
|
|
5425
|
+
edgesUpdated: 0,
|
|
5426
|
+
errors: [],
|
|
5427
|
+
durationMs: 0
|
|
5428
|
+
};
|
|
5429
|
+
}
|
|
4782
5430
|
const linker = new KnowledgeLinker(this.store, extractedDir);
|
|
4783
5431
|
const linkResult = await linker.link();
|
|
4784
5432
|
return {
|
|
@@ -4786,6 +5434,7 @@ var KnowledgePipelineRunner = class {
|
|
|
4786
5434
|
diagrams: diagramResult.nodesAdded,
|
|
4787
5435
|
linkerFacts: linkResult.factsCreated,
|
|
4788
5436
|
businessKnowledge: bkResult.nodesAdded,
|
|
5437
|
+
decisions: decisionResult.nodesAdded,
|
|
4789
5438
|
images: imageCount
|
|
4790
5439
|
};
|
|
4791
5440
|
}
|
|
@@ -4812,14 +5461,14 @@ var KnowledgePipelineRunner = class {
|
|
|
4812
5461
|
}
|
|
4813
5462
|
// ── Phase 3: DETECT ───────────────────────────────────────────────────────
|
|
4814
5463
|
async detect(options) {
|
|
4815
|
-
const knowledgeDir =
|
|
4816
|
-
const aggregator = new KnowledgeStagingAggregator(options.projectDir);
|
|
4817
|
-
const gapReport = await aggregator.generateGapReport(knowledgeDir);
|
|
5464
|
+
const knowledgeDir = path12.join(options.projectDir, "docs", "knowledge");
|
|
5465
|
+
const aggregator = new KnowledgeStagingAggregator(options.projectDir, this.inferenceOptions);
|
|
5466
|
+
const gapReport = await aggregator.generateGapReport(knowledgeDir, this.store);
|
|
4818
5467
|
await aggregator.writeGapReport(gapReport);
|
|
4819
5468
|
return gapReport;
|
|
4820
5469
|
}
|
|
4821
5470
|
// ── Phase 4: REMEDIATE ────────────────────────────────────────────────────
|
|
4822
|
-
remediate(driftResult, remediations, options) {
|
|
5471
|
+
async remediate(driftResult, remediations, options, gapReport) {
|
|
4823
5472
|
for (const finding of driftResult.findings) {
|
|
4824
5473
|
switch (finding.classification) {
|
|
4825
5474
|
case "stale":
|
|
@@ -4837,6 +5486,22 @@ var KnowledgePipelineRunner = class {
|
|
|
4837
5486
|
break;
|
|
4838
5487
|
}
|
|
4839
5488
|
}
|
|
5489
|
+
if (!options.ci) {
|
|
5490
|
+
const allGapEntries = gapReport.domains.flatMap((d) => d.gapEntries);
|
|
5491
|
+
const materializable = allGapEntries.filter((e) => e.hasContent);
|
|
5492
|
+
if (materializable.length > 0) {
|
|
5493
|
+
const materializer = new KnowledgeDocMaterializer(this.store, this.inferenceOptions);
|
|
5494
|
+
const matResult = await materializer.materialize(materializable, {
|
|
5495
|
+
projectDir: options.projectDir,
|
|
5496
|
+
dryRun: false
|
|
5497
|
+
});
|
|
5498
|
+
for (const doc of matResult.created) {
|
|
5499
|
+
remediations.push(`created doc: ${doc.filePath}`);
|
|
5500
|
+
}
|
|
5501
|
+
return matResult;
|
|
5502
|
+
}
|
|
5503
|
+
}
|
|
5504
|
+
return void 0;
|
|
4840
5505
|
}
|
|
4841
5506
|
async stageNewFindings(driftResult, options) {
|
|
4842
5507
|
const newFindings = driftResult.findings.filter((f) => f.classification === "new");
|
|
@@ -4851,7 +5516,7 @@ var KnowledgePipelineRunner = class {
|
|
|
4851
5516
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4852
5517
|
}));
|
|
4853
5518
|
if (stagedEntries.length > 0) {
|
|
4854
|
-
const aggregator = new KnowledgeStagingAggregator(options.projectDir);
|
|
5519
|
+
const aggregator = new KnowledgeStagingAggregator(options.projectDir, this.inferenceOptions);
|
|
4855
5520
|
await aggregator.aggregate(stagedEntries, [], []);
|
|
4856
5521
|
}
|
|
4857
5522
|
}
|
|
@@ -4871,7 +5536,7 @@ var KnowledgePipelineRunner = class {
|
|
|
4871
5536
|
};
|
|
4872
5537
|
|
|
4873
5538
|
// src/ingest/connectors/ConnectorUtils.ts
|
|
4874
|
-
var
|
|
5539
|
+
var CODE_NODE_TYPES5 = ["file", "function", "class", "method", "interface", "variable"];
|
|
4875
5540
|
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
4876
5541
|
function withRetry(client, options) {
|
|
4877
5542
|
const maxRetries = options?.maxRetries ?? 3;
|
|
@@ -4936,7 +5601,7 @@ function sanitizeExternalText(text, maxLength = 2e3) {
|
|
|
4936
5601
|
}
|
|
4937
5602
|
function linkToCode(store, content, sourceNodeId, edgeType, options) {
|
|
4938
5603
|
let edgesCreated = 0;
|
|
4939
|
-
for (const type of
|
|
5604
|
+
for (const type of CODE_NODE_TYPES5) {
|
|
4940
5605
|
const nodes = store.findNodes({ type });
|
|
4941
5606
|
for (const node of nodes) {
|
|
4942
5607
|
if (node.name.length < 3) continue;
|
|
@@ -4956,12 +5621,12 @@ function linkToCode(store, content, sourceNodeId, edgeType, options) {
|
|
|
4956
5621
|
}
|
|
4957
5622
|
|
|
4958
5623
|
// src/ingest/connectors/SyncManager.ts
|
|
4959
|
-
import * as
|
|
4960
|
-
import * as
|
|
5624
|
+
import * as fs12 from "fs/promises";
|
|
5625
|
+
import * as path13 from "path";
|
|
4961
5626
|
var SyncManager = class {
|
|
4962
5627
|
constructor(store, graphDir) {
|
|
4963
5628
|
this.store = store;
|
|
4964
|
-
this.metadataPath =
|
|
5629
|
+
this.metadataPath = path13.join(graphDir, "sync-metadata.json");
|
|
4965
5630
|
}
|
|
4966
5631
|
store;
|
|
4967
5632
|
registrations = /* @__PURE__ */ new Map();
|
|
@@ -5026,15 +5691,15 @@ var SyncManager = class {
|
|
|
5026
5691
|
}
|
|
5027
5692
|
async loadMetadata() {
|
|
5028
5693
|
try {
|
|
5029
|
-
const raw = await
|
|
5694
|
+
const raw = await fs12.readFile(this.metadataPath, "utf-8");
|
|
5030
5695
|
return JSON.parse(raw);
|
|
5031
5696
|
} catch {
|
|
5032
5697
|
return { connectors: {} };
|
|
5033
5698
|
}
|
|
5034
5699
|
}
|
|
5035
5700
|
async saveMetadata(metadata) {
|
|
5036
|
-
await
|
|
5037
|
-
await
|
|
5701
|
+
await fs12.mkdir(path13.dirname(this.metadataPath), { recursive: true });
|
|
5702
|
+
await fs12.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
5038
5703
|
}
|
|
5039
5704
|
};
|
|
5040
5705
|
|
|
@@ -5139,6 +5804,14 @@ var JiraConnector = class {
|
|
|
5139
5804
|
start
|
|
5140
5805
|
);
|
|
5141
5806
|
}
|
|
5807
|
+
try {
|
|
5808
|
+
const parsed = new URL(baseUrl);
|
|
5809
|
+
if (parsed.protocol !== "https:" && parsed.hostname !== "localhost") {
|
|
5810
|
+
return buildIngestResult(0, 0, [`${baseUrlEnv} must use HTTPS`], start);
|
|
5811
|
+
}
|
|
5812
|
+
} catch {
|
|
5813
|
+
return buildIngestResult(0, 0, [`${baseUrlEnv} is not a valid URL`], start);
|
|
5814
|
+
}
|
|
5142
5815
|
const jql = buildJql(config);
|
|
5143
5816
|
const headers = { Authorization: `Basic ${apiKey}`, "Content-Type": "application/json" };
|
|
5144
5817
|
try {
|
|
@@ -5428,6 +6101,14 @@ var ConfluenceConnector = class {
|
|
|
5428
6101
|
}
|
|
5429
6102
|
const baseUrlEnv = config.baseUrlEnv ?? "CONFLUENCE_BASE_URL";
|
|
5430
6103
|
const baseUrl = process.env[baseUrlEnv] ?? "";
|
|
6104
|
+
try {
|
|
6105
|
+
const parsed = new URL(baseUrl);
|
|
6106
|
+
if (parsed.protocol !== "https:" && parsed.hostname !== "localhost") {
|
|
6107
|
+
return missingApiKeyResult(`${baseUrlEnv} must use HTTPS`, start);
|
|
6108
|
+
}
|
|
6109
|
+
} catch {
|
|
6110
|
+
return missingApiKeyResult(`${baseUrlEnv} is not a valid URL`, start);
|
|
6111
|
+
}
|
|
5431
6112
|
const spaceKey = config.spaceKey ?? "";
|
|
5432
6113
|
const counts = await this.fetchAllPagesHandled(
|
|
5433
6114
|
store,
|
|
@@ -5521,7 +6202,7 @@ var ConfluenceConnector = class {
|
|
|
5521
6202
|
};
|
|
5522
6203
|
|
|
5523
6204
|
// src/ingest/connectors/CIConnector.ts
|
|
5524
|
-
function
|
|
6205
|
+
function emptyResult5(errors, start) {
|
|
5525
6206
|
return {
|
|
5526
6207
|
nodesAdded: 0,
|
|
5527
6208
|
nodesUpdated: 0,
|
|
@@ -5589,7 +6270,7 @@ var CIConnector = class {
|
|
|
5589
6270
|
const apiKeyEnv = config.apiKeyEnv ?? "GITHUB_TOKEN";
|
|
5590
6271
|
const apiKey = process.env[apiKeyEnv];
|
|
5591
6272
|
if (!apiKey) {
|
|
5592
|
-
return
|
|
6273
|
+
return emptyResult5(
|
|
5593
6274
|
[`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
5594
6275
|
start
|
|
5595
6276
|
);
|
|
@@ -5653,6 +6334,14 @@ function hasConstraintKeyword(text) {
|
|
|
5653
6334
|
const lower = text.toLowerCase();
|
|
5654
6335
|
return CONSTRAINT_KEYWORDS.some((kw) => lower.includes(kw));
|
|
5655
6336
|
}
|
|
6337
|
+
function buildNodeMetadata(condensed, base) {
|
|
6338
|
+
const metadata = { ...base };
|
|
6339
|
+
if (condensed.method !== "passthrough") {
|
|
6340
|
+
metadata.condensed = condensed.method;
|
|
6341
|
+
metadata.originalLength = condensed.originalLength;
|
|
6342
|
+
}
|
|
6343
|
+
return metadata;
|
|
6344
|
+
}
|
|
5656
6345
|
function buildIngestResult2(nodesAdded, edgesAdded, errors, start) {
|
|
5657
6346
|
return {
|
|
5658
6347
|
nodesAdded,
|
|
@@ -5663,6 +6352,24 @@ function buildIngestResult2(nodesAdded, edgesAdded, errors, start) {
|
|
|
5663
6352
|
durationMs: Date.now() - start
|
|
5664
6353
|
};
|
|
5665
6354
|
}
|
|
6355
|
+
function validateFigmaConfig(config) {
|
|
6356
|
+
const apiKeyEnv = config.apiKeyEnv ?? "FIGMA_API_KEY";
|
|
6357
|
+
const apiKey = process.env[apiKeyEnv];
|
|
6358
|
+
if (!apiKey) return { error: `Missing API key: environment variable "${apiKeyEnv}" is not set` };
|
|
6359
|
+
const baseUrlEnv = config.baseUrlEnv ?? "FIGMA_BASE_URL";
|
|
6360
|
+
const baseUrl = process.env[baseUrlEnv] ?? "https://api.figma.com";
|
|
6361
|
+
try {
|
|
6362
|
+
const parsed = new URL(baseUrl);
|
|
6363
|
+
if (parsed.protocol !== "https:" || !parsed.hostname.endsWith("api.figma.com")) {
|
|
6364
|
+
return { error: `Invalid ${baseUrlEnv}: must be an HTTPS URL on api.figma.com` };
|
|
6365
|
+
}
|
|
6366
|
+
} catch {
|
|
6367
|
+
return { error: `Invalid ${baseUrlEnv}: not a valid URL` };
|
|
6368
|
+
}
|
|
6369
|
+
const fileIds = config.fileIds;
|
|
6370
|
+
if (!fileIds || fileIds.length === 0) return { error: "No fileIds provided in connector config" };
|
|
6371
|
+
return { apiKey, baseUrl, fileIds };
|
|
6372
|
+
}
|
|
5666
6373
|
var FigmaConnector = class {
|
|
5667
6374
|
name = "figma";
|
|
5668
6375
|
source = "figma";
|
|
@@ -5672,35 +6379,9 @@ var FigmaConnector = class {
|
|
|
5672
6379
|
}
|
|
5673
6380
|
async ingest(store, config) {
|
|
5674
6381
|
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
|
-
}
|
|
6382
|
+
const validated = validateFigmaConfig(config);
|
|
6383
|
+
if ("error" in validated) return buildIngestResult2(0, 0, [validated.error], start);
|
|
6384
|
+
const { apiKey, baseUrl, fileIds } = validated;
|
|
5704
6385
|
const headers = { "X-FIGMA-TOKEN": apiKey };
|
|
5705
6386
|
const maxLen = config.maxContentLength ?? 4e3;
|
|
5706
6387
|
let nodesAdded = 0;
|
|
@@ -5720,6 +6401,17 @@ var FigmaConnector = class {
|
|
|
5720
6401
|
return buildIngestResult2(nodesAdded, edgesAdded, errors, start);
|
|
5721
6402
|
}
|
|
5722
6403
|
async processFile(store, baseUrl, fileId, headers, maxLen) {
|
|
6404
|
+
let nodesAdded = 0;
|
|
6405
|
+
let edgesAdded = 0;
|
|
6406
|
+
const styleCounts = await this.ingestStyles(store, baseUrl, fileId, headers, maxLen);
|
|
6407
|
+
nodesAdded += styleCounts.nodesAdded;
|
|
6408
|
+
edgesAdded += styleCounts.edgesAdded;
|
|
6409
|
+
const componentCounts = await this.ingestComponents(store, baseUrl, fileId, headers, maxLen);
|
|
6410
|
+
nodesAdded += componentCounts.nodesAdded;
|
|
6411
|
+
edgesAdded += componentCounts.edgesAdded;
|
|
6412
|
+
return { nodesAdded, edgesAdded };
|
|
6413
|
+
}
|
|
6414
|
+
async ingestStyles(store, baseUrl, fileId, headers, maxLen) {
|
|
5723
6415
|
let nodesAdded = 0;
|
|
5724
6416
|
let edgesAdded = 0;
|
|
5725
6417
|
const stylesUrl = `${baseUrl}/v1/files/${fileId}/styles`;
|
|
@@ -5727,90 +6419,112 @@ var FigmaConnector = class {
|
|
|
5727
6419
|
if (!stylesResponse.ok) throw new Error(`Styles request failed for file ${fileId}`);
|
|
5728
6420
|
const stylesData = await stylesResponse.json();
|
|
5729
6421
|
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");
|
|
6422
|
+
const counts = await this.processStyle(store, style, fileId, maxLen);
|
|
6423
|
+
nodesAdded += counts.nodesAdded;
|
|
6424
|
+
edgesAdded += counts.edgesAdded;
|
|
5754
6425
|
}
|
|
6426
|
+
return { nodesAdded, edgesAdded };
|
|
6427
|
+
}
|
|
6428
|
+
async processStyle(store, style, fileId, maxLen) {
|
|
6429
|
+
const nodeId = `figma:token:${style.key}`;
|
|
6430
|
+
const condensed = await condenseContent(sanitizeExternalText(style.description || "", 2e3), {
|
|
6431
|
+
maxLength: maxLen
|
|
6432
|
+
});
|
|
6433
|
+
const metadata = buildNodeMetadata(condensed, {
|
|
6434
|
+
source: "figma",
|
|
6435
|
+
key: style.key,
|
|
6436
|
+
styleType: style.style_type,
|
|
6437
|
+
fileId
|
|
6438
|
+
});
|
|
6439
|
+
store.addNode({
|
|
6440
|
+
id: nodeId,
|
|
6441
|
+
type: "design_token",
|
|
6442
|
+
name: sanitizeExternalText(style.name, 500),
|
|
6443
|
+
content: condensed.content,
|
|
6444
|
+
metadata
|
|
6445
|
+
});
|
|
6446
|
+
const searchText = sanitizeExternalText([style.name, style.description].join(" "));
|
|
6447
|
+
const edgesAdded = linkToCode(store, searchText, nodeId, "references");
|
|
6448
|
+
return { nodesAdded: 1, edgesAdded };
|
|
6449
|
+
}
|
|
6450
|
+
async ingestComponents(store, baseUrl, fileId, headers, maxLen) {
|
|
6451
|
+
let nodesAdded = 0;
|
|
6452
|
+
let edgesAdded = 0;
|
|
5755
6453
|
const componentsUrl = `${baseUrl}/v1/files/${fileId}/components`;
|
|
5756
6454
|
const componentsResponse = await this.httpClient(componentsUrl, { headers });
|
|
5757
6455
|
if (!componentsResponse.ok) throw new Error(`Components request failed for file ${fileId}`);
|
|
5758
6456
|
const componentsData = await componentsResponse.json();
|
|
5759
6457
|
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
|
-
}
|
|
6458
|
+
const counts = await this.processComponent(store, component, fileId, maxLen);
|
|
6459
|
+
nodesAdded += counts.nodesAdded;
|
|
6460
|
+
edgesAdded += counts.edgesAdded;
|
|
5811
6461
|
}
|
|
5812
6462
|
return { nodesAdded, edgesAdded };
|
|
5813
6463
|
}
|
|
6464
|
+
async processComponent(store, component, fileId, maxLen) {
|
|
6465
|
+
let nodesAdded = 0;
|
|
6466
|
+
let edgesAdded = 0;
|
|
6467
|
+
const description = component.description || "";
|
|
6468
|
+
if (description) {
|
|
6469
|
+
const counts = await this.addComponentIntent(store, component, description, fileId, maxLen);
|
|
6470
|
+
nodesAdded += counts.nodesAdded;
|
|
6471
|
+
edgesAdded += counts.edgesAdded;
|
|
6472
|
+
}
|
|
6473
|
+
if (description && hasConstraintKeyword(description)) {
|
|
6474
|
+
const counts = await this.addComponentConstraint(
|
|
6475
|
+
store,
|
|
6476
|
+
component,
|
|
6477
|
+
description,
|
|
6478
|
+
fileId,
|
|
6479
|
+
maxLen
|
|
6480
|
+
);
|
|
6481
|
+
nodesAdded += counts.nodesAdded;
|
|
6482
|
+
edgesAdded += counts.edgesAdded;
|
|
6483
|
+
}
|
|
6484
|
+
return { nodesAdded, edgesAdded };
|
|
6485
|
+
}
|
|
6486
|
+
async addComponentIntent(store, component, description, fileId, maxLen) {
|
|
6487
|
+
const intentId = `figma:intent:${component.key}`;
|
|
6488
|
+
const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
|
|
6489
|
+
maxLength: maxLen
|
|
6490
|
+
});
|
|
6491
|
+
const metadata = buildNodeMetadata(condensed, {
|
|
6492
|
+
source: "figma",
|
|
6493
|
+
key: component.key,
|
|
6494
|
+
fileId
|
|
6495
|
+
});
|
|
6496
|
+
store.addNode({
|
|
6497
|
+
id: intentId,
|
|
6498
|
+
type: "aesthetic_intent",
|
|
6499
|
+
name: sanitizeExternalText(component.name, 500),
|
|
6500
|
+
content: condensed.content,
|
|
6501
|
+
metadata
|
|
6502
|
+
});
|
|
6503
|
+
const searchText = sanitizeExternalText([component.name, description].join(" "));
|
|
6504
|
+
const edgesAdded = linkToCode(store, searchText, intentId, "references");
|
|
6505
|
+
return { nodesAdded: 1, edgesAdded };
|
|
6506
|
+
}
|
|
6507
|
+
async addComponentConstraint(store, component, description, fileId, maxLen) {
|
|
6508
|
+
const constraintId = `figma:constraint:${component.key}`;
|
|
6509
|
+
const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
|
|
6510
|
+
maxLength: maxLen
|
|
6511
|
+
});
|
|
6512
|
+
const metadata = buildNodeMetadata(condensed, {
|
|
6513
|
+
source: "figma",
|
|
6514
|
+
key: component.key,
|
|
6515
|
+
fileId
|
|
6516
|
+
});
|
|
6517
|
+
store.addNode({
|
|
6518
|
+
id: constraintId,
|
|
6519
|
+
type: "design_constraint",
|
|
6520
|
+
name: sanitizeExternalText(component.name, 500),
|
|
6521
|
+
content: condensed.content,
|
|
6522
|
+
metadata
|
|
6523
|
+
});
|
|
6524
|
+
const searchText = sanitizeExternalText([component.name, description].join(" "));
|
|
6525
|
+
const edgesAdded = linkToCode(store, searchText, constraintId, "references");
|
|
6526
|
+
return { nodesAdded: 1, edgesAdded };
|
|
6527
|
+
}
|
|
5814
6528
|
};
|
|
5815
6529
|
|
|
5816
6530
|
// src/ingest/connectors/MiroConnector.ts
|
|
@@ -5829,6 +6543,46 @@ function buildIngestResult3(nodesAdded, edgesAdded, errors, start) {
|
|
|
5829
6543
|
durationMs: Date.now() - start
|
|
5830
6544
|
};
|
|
5831
6545
|
}
|
|
6546
|
+
function validateBaseUrl(baseUrl, baseUrlEnv) {
|
|
6547
|
+
try {
|
|
6548
|
+
const parsed = new URL(baseUrl);
|
|
6549
|
+
if (parsed.protocol !== "https:" || parsed.hostname !== "api.miro.com") {
|
|
6550
|
+
return `Invalid ${baseUrlEnv}: must be an HTTPS URL on api.miro.com`;
|
|
6551
|
+
}
|
|
6552
|
+
} catch {
|
|
6553
|
+
return `Invalid ${baseUrlEnv}: not a valid URL`;
|
|
6554
|
+
}
|
|
6555
|
+
return null;
|
|
6556
|
+
}
|
|
6557
|
+
function resolveConfig(config, start) {
|
|
6558
|
+
const apiKeyEnv = config.apiKeyEnv ?? "MIRO_API_KEY";
|
|
6559
|
+
const apiKey = process.env[apiKeyEnv];
|
|
6560
|
+
if (!apiKey) {
|
|
6561
|
+
return {
|
|
6562
|
+
ok: false,
|
|
6563
|
+
result: buildIngestResult3(
|
|
6564
|
+
0,
|
|
6565
|
+
0,
|
|
6566
|
+
[`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
6567
|
+
start
|
|
6568
|
+
)
|
|
6569
|
+
};
|
|
6570
|
+
}
|
|
6571
|
+
const baseUrlEnv = config.baseUrlEnv ?? "MIRO_BASE_URL";
|
|
6572
|
+
const baseUrl = process.env[baseUrlEnv] ?? "https://api.miro.com";
|
|
6573
|
+
const urlError = validateBaseUrl(baseUrl, baseUrlEnv);
|
|
6574
|
+
if (urlError) {
|
|
6575
|
+
return { ok: false, result: buildIngestResult3(0, 0, [urlError], start) };
|
|
6576
|
+
}
|
|
6577
|
+
const boardIds = config.boardIds;
|
|
6578
|
+
if (!boardIds || boardIds.length === 0) {
|
|
6579
|
+
return {
|
|
6580
|
+
ok: false,
|
|
6581
|
+
result: buildIngestResult3(0, 0, ["No boardIds provided in config"], start)
|
|
6582
|
+
};
|
|
6583
|
+
}
|
|
6584
|
+
return { ok: true, apiKey, baseUrl, boardIds, headers: { Authorization: `Bearer ${apiKey}` } };
|
|
6585
|
+
}
|
|
5832
6586
|
var MiroConnector = class {
|
|
5833
6587
|
name = "miro";
|
|
5834
6588
|
source = "miro";
|
|
@@ -5838,36 +6592,9 @@ var MiroConnector = class {
|
|
|
5838
6592
|
}
|
|
5839
6593
|
async ingest(store, config) {
|
|
5840
6594
|
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}` };
|
|
6595
|
+
const resolved = resolveConfig(config, start);
|
|
6596
|
+
if (!resolved.ok) return resolved.result;
|
|
6597
|
+
const { baseUrl, boardIds, headers } = resolved;
|
|
5871
6598
|
let totalNodesAdded = 0;
|
|
5872
6599
|
let totalEdgesAdded = 0;
|
|
5873
6600
|
const errors = [];
|
|
@@ -6071,7 +6798,7 @@ var FusionLayer = class {
|
|
|
6071
6798
|
};
|
|
6072
6799
|
|
|
6073
6800
|
// src/entropy/GraphEntropyAdapter.ts
|
|
6074
|
-
var
|
|
6801
|
+
var CODE_NODE_TYPES6 = ["file", "function", "class", "method", "interface", "variable"];
|
|
6075
6802
|
var GraphEntropyAdapter = class {
|
|
6076
6803
|
constructor(store) {
|
|
6077
6804
|
this.store = store;
|
|
@@ -6151,7 +6878,7 @@ var GraphEntropyAdapter = class {
|
|
|
6151
6878
|
}
|
|
6152
6879
|
findEntryPoints() {
|
|
6153
6880
|
const entryPoints = [];
|
|
6154
|
-
for (const nodeType of
|
|
6881
|
+
for (const nodeType of CODE_NODE_TYPES6) {
|
|
6155
6882
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
6156
6883
|
for (const node of nodes) {
|
|
6157
6884
|
const isIndexFile = nodeType === "file" && node.name === "index.ts";
|
|
@@ -6187,7 +6914,7 @@ var GraphEntropyAdapter = class {
|
|
|
6187
6914
|
}
|
|
6188
6915
|
collectUnreachableNodes(visited) {
|
|
6189
6916
|
const unreachableNodes = [];
|
|
6190
|
-
for (const nodeType of
|
|
6917
|
+
for (const nodeType of CODE_NODE_TYPES6) {
|
|
6191
6918
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
6192
6919
|
for (const node of nodes) {
|
|
6193
6920
|
if (!visited.has(node.id)) {
|
|
@@ -7033,9 +7760,9 @@ var EntityExtractor = class {
|
|
|
7033
7760
|
extractPaths(trimmed, add) {
|
|
7034
7761
|
const consumed = /* @__PURE__ */ new Set();
|
|
7035
7762
|
for (const match of trimmed.matchAll(FILE_PATH_RE)) {
|
|
7036
|
-
const
|
|
7037
|
-
add(
|
|
7038
|
-
consumed.add(
|
|
7763
|
+
const path15 = match[0];
|
|
7764
|
+
add(path15);
|
|
7765
|
+
consumed.add(path15);
|
|
7039
7766
|
}
|
|
7040
7767
|
return consumed;
|
|
7041
7768
|
}
|
|
@@ -7107,8 +7834,8 @@ var EntityResolver = class {
|
|
|
7107
7834
|
if (isPathLike && node.path.includes(raw)) {
|
|
7108
7835
|
return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
|
|
7109
7836
|
}
|
|
7110
|
-
const
|
|
7111
|
-
if (
|
|
7837
|
+
const basename10 = node.path.split("/").pop() ?? "";
|
|
7838
|
+
if (basename10.includes(raw)) {
|
|
7112
7839
|
return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
|
|
7113
7840
|
}
|
|
7114
7841
|
if (raw.length >= 4 && node.path.includes(raw)) {
|
|
@@ -7187,13 +7914,13 @@ var ResponseFormatter = class {
|
|
|
7187
7914
|
const context = Array.isArray(d?.context) ? d.context : [];
|
|
7188
7915
|
const firstEntity = entities[0];
|
|
7189
7916
|
const nodeType = firstEntity?.node.type ?? "node";
|
|
7190
|
-
const
|
|
7917
|
+
const path15 = firstEntity?.node.path ?? "unknown";
|
|
7191
7918
|
let neighborCount = 0;
|
|
7192
7919
|
const firstContext = context[0];
|
|
7193
7920
|
if (firstContext && Array.isArray(firstContext.nodes)) {
|
|
7194
7921
|
neighborCount = firstContext.nodes.length;
|
|
7195
7922
|
}
|
|
7196
|
-
return `**${entityName}** is a ${nodeType} at \`${
|
|
7923
|
+
return `**${entityName}** is a ${nodeType} at \`${path15}\`. Connected to ${neighborCount} nodes.`;
|
|
7197
7924
|
}
|
|
7198
7925
|
formatAnomaly(data) {
|
|
7199
7926
|
const d = data;
|
|
@@ -7576,7 +8303,7 @@ var PHASE_NODE_TYPES = {
|
|
|
7576
8303
|
debug: ["failure", "learning", "function", "method"],
|
|
7577
8304
|
plan: ["adr", "document", "module", "layer"]
|
|
7578
8305
|
};
|
|
7579
|
-
var
|
|
8306
|
+
var CODE_NODE_TYPES7 = /* @__PURE__ */ new Set([
|
|
7580
8307
|
"file",
|
|
7581
8308
|
"function",
|
|
7582
8309
|
"class",
|
|
@@ -7795,7 +8522,7 @@ var Assembler = class {
|
|
|
7795
8522
|
*/
|
|
7796
8523
|
checkCoverage() {
|
|
7797
8524
|
const codeNodes = [];
|
|
7798
|
-
for (const type of
|
|
8525
|
+
for (const type of CODE_NODE_TYPES7) {
|
|
7799
8526
|
codeNodes.push(...this.store.findNodes({ type }));
|
|
7800
8527
|
}
|
|
7801
8528
|
const documented = [];
|
|
@@ -7975,14 +8702,14 @@ var GraphConstraintAdapter = class {
|
|
|
7975
8702
|
};
|
|
7976
8703
|
|
|
7977
8704
|
// src/ingest/DesignIngestor.ts
|
|
7978
|
-
import * as
|
|
7979
|
-
import * as
|
|
8705
|
+
import * as fs13 from "fs/promises";
|
|
8706
|
+
import * as path14 from "path";
|
|
7980
8707
|
function isDTCGToken(obj) {
|
|
7981
8708
|
return typeof obj === "object" && obj !== null && "$value" in obj && "$type" in obj;
|
|
7982
8709
|
}
|
|
7983
8710
|
async function readFileOrNull(filePath) {
|
|
7984
8711
|
try {
|
|
7985
|
-
return await
|
|
8712
|
+
return await fs13.readFile(filePath, "utf-8");
|
|
7986
8713
|
} catch {
|
|
7987
8714
|
return null;
|
|
7988
8715
|
}
|
|
@@ -8128,8 +8855,8 @@ var DesignIngestor = class {
|
|
|
8128
8855
|
async ingestAll(designDir) {
|
|
8129
8856
|
const start = Date.now();
|
|
8130
8857
|
const [tokensResult, intentResult] = await Promise.all([
|
|
8131
|
-
this.ingestTokens(
|
|
8132
|
-
this.ingestDesignIntent(
|
|
8858
|
+
this.ingestTokens(path14.join(designDir, "tokens.json")),
|
|
8859
|
+
this.ingestDesignIntent(path14.join(designDir, "DESIGN.md"))
|
|
8133
8860
|
]);
|
|
8134
8861
|
const merged = mergeResults(tokensResult, intentResult);
|
|
8135
8862
|
return { ...merged, durationMs: Date.now() - start };
|
|
@@ -8365,10 +9092,10 @@ var TaskIndependenceAnalyzer = class {
|
|
|
8365
9092
|
includeTypes: ["file"]
|
|
8366
9093
|
});
|
|
8367
9094
|
for (const n of queryResult.nodes) {
|
|
8368
|
-
const
|
|
8369
|
-
if (!fileSet.has(
|
|
8370
|
-
if (!result.has(
|
|
8371
|
-
result.set(
|
|
9095
|
+
const path15 = n.path ?? n.id.replace(/^file:/, "");
|
|
9096
|
+
if (!fileSet.has(path15)) {
|
|
9097
|
+
if (!result.has(path15)) {
|
|
9098
|
+
result.set(path15, file);
|
|
8372
9099
|
}
|
|
8373
9100
|
}
|
|
8374
9101
|
}
|
|
@@ -8745,7 +9472,7 @@ var ConflictPredictor = class {
|
|
|
8745
9472
|
};
|
|
8746
9473
|
|
|
8747
9474
|
// src/index.ts
|
|
8748
|
-
var VERSION = "0.
|
|
9475
|
+
var VERSION = "0.6.0";
|
|
8749
9476
|
export {
|
|
8750
9477
|
ALL_EXTRACTORS,
|
|
8751
9478
|
ApiPathExtractor,
|
|
@@ -8762,6 +9489,9 @@ export {
|
|
|
8762
9489
|
ContradictionDetector,
|
|
8763
9490
|
CoverageScorer,
|
|
8764
9491
|
D2Parser,
|
|
9492
|
+
DEFAULT_BLOCKLIST,
|
|
9493
|
+
DEFAULT_PATTERNS,
|
|
9494
|
+
DecisionIngestor,
|
|
8765
9495
|
DesignConstraintAdapter,
|
|
8766
9496
|
DesignIngestor,
|
|
8767
9497
|
DiagramParser,
|
|
@@ -8786,6 +9516,7 @@ export {
|
|
|
8786
9516
|
ImageAnalysisExtractor,
|
|
8787
9517
|
IntentClassifier,
|
|
8788
9518
|
JiraConnector,
|
|
9519
|
+
KnowledgeDocMaterializer,
|
|
8789
9520
|
KnowledgeIngestor,
|
|
8790
9521
|
KnowledgePipelineRunner,
|
|
8791
9522
|
KnowledgeStagingAggregator,
|
|
@@ -8812,6 +9543,7 @@ export {
|
|
|
8812
9543
|
createExtractionRunner,
|
|
8813
9544
|
detectLanguage,
|
|
8814
9545
|
groupNodesByImpact,
|
|
9546
|
+
inferDomain,
|
|
8815
9547
|
linkToCode,
|
|
8816
9548
|
loadGraph,
|
|
8817
9549
|
normalizeIntent,
|