@harness-engineering/graph 0.4.2 → 0.5.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/README.md +50 -36
- package/dist/index.d.mts +544 -15
- package/dist/index.d.ts +544 -15
- package/dist/index.js +3911 -216
- package/dist/index.mjs +3890 -219
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -30,7 +30,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
ALL_EXTRACTORS: () => ALL_EXTRACTORS,
|
|
34
|
+
ApiPathExtractor: () => ApiPathExtractor,
|
|
33
35
|
Assembler: () => Assembler,
|
|
36
|
+
BusinessKnowledgeIngestor: () => BusinessKnowledgeIngestor,
|
|
34
37
|
CIConnector: () => CIConnector,
|
|
35
38
|
CURRENT_SCHEMA_VERSION: () => CURRENT_SCHEMA_VERSION,
|
|
36
39
|
CascadeSimulator: () => CascadeSimulator,
|
|
@@ -39,11 +42,18 @@ __export(index_exports, {
|
|
|
39
42
|
ConflictPredictor: () => ConflictPredictor,
|
|
40
43
|
ConfluenceConnector: () => ConfluenceConnector,
|
|
41
44
|
ContextQL: () => ContextQL,
|
|
45
|
+
ContradictionDetector: () => ContradictionDetector,
|
|
46
|
+
CoverageScorer: () => CoverageScorer,
|
|
47
|
+
D2Parser: () => D2Parser,
|
|
42
48
|
DesignConstraintAdapter: () => DesignConstraintAdapter,
|
|
43
49
|
DesignIngestor: () => DesignIngestor,
|
|
50
|
+
DiagramParser: () => DiagramParser,
|
|
44
51
|
EDGE_TYPES: () => EDGE_TYPES,
|
|
45
52
|
EntityExtractor: () => EntityExtractor,
|
|
46
53
|
EntityResolver: () => EntityResolver,
|
|
54
|
+
EnumConstantExtractor: () => EnumConstantExtractor,
|
|
55
|
+
ExtractionRunner: () => ExtractionRunner,
|
|
56
|
+
FigmaConnector: () => FigmaConnector,
|
|
47
57
|
FusionLayer: () => FusionLayer,
|
|
48
58
|
GitIngestor: () => GitIngestor,
|
|
49
59
|
GraphAnomalyAdapter: () => GraphAnomalyAdapter,
|
|
@@ -56,24 +66,38 @@ __export(index_exports, {
|
|
|
56
66
|
GraphNodeSchema: () => GraphNodeSchema,
|
|
57
67
|
GraphStore: () => GraphStore,
|
|
58
68
|
INTENTS: () => INTENTS,
|
|
69
|
+
ImageAnalysisExtractor: () => ImageAnalysisExtractor,
|
|
59
70
|
IntentClassifier: () => IntentClassifier,
|
|
60
71
|
JiraConnector: () => JiraConnector,
|
|
61
72
|
KnowledgeIngestor: () => KnowledgeIngestor,
|
|
73
|
+
KnowledgePipelineRunner: () => KnowledgePipelineRunner,
|
|
74
|
+
KnowledgeStagingAggregator: () => KnowledgeStagingAggregator,
|
|
75
|
+
MermaidParser: () => MermaidParser,
|
|
76
|
+
MiroConnector: () => MiroConnector,
|
|
77
|
+
NODE_STABILITY: () => NODE_STABILITY,
|
|
62
78
|
NODE_TYPES: () => NODE_TYPES,
|
|
63
79
|
OBSERVABILITY_TYPES: () => OBSERVABILITY_TYPES,
|
|
80
|
+
PackedSummaryCache: () => PackedSummaryCache,
|
|
81
|
+
PlantUmlParser: () => PlantUmlParser,
|
|
64
82
|
RequirementIngestor: () => RequirementIngestor,
|
|
65
83
|
ResponseFormatter: () => ResponseFormatter,
|
|
66
84
|
SlackConnector: () => SlackConnector,
|
|
85
|
+
StructuralDriftDetector: () => StructuralDriftDetector,
|
|
67
86
|
SyncManager: () => SyncManager,
|
|
68
87
|
TaskIndependenceAnalyzer: () => TaskIndependenceAnalyzer,
|
|
88
|
+
TestDescriptionExtractor: () => TestDescriptionExtractor,
|
|
69
89
|
TopologicalLinker: () => TopologicalLinker,
|
|
70
90
|
VERSION: () => VERSION,
|
|
91
|
+
ValidationRuleExtractor: () => ValidationRuleExtractor,
|
|
71
92
|
VectorStore: () => VectorStore,
|
|
72
93
|
askGraph: () => askGraph,
|
|
73
94
|
classifyNodeCategory: () => classifyNodeCategory,
|
|
95
|
+
createExtractionRunner: () => createExtractionRunner,
|
|
96
|
+
detectLanguage: () => detectLanguage,
|
|
74
97
|
groupNodesByImpact: () => groupNodesByImpact,
|
|
75
98
|
linkToCode: () => linkToCode,
|
|
76
99
|
loadGraph: () => loadGraph,
|
|
100
|
+
normalizeIntent: () => normalizeIntent,
|
|
77
101
|
project: () => project,
|
|
78
102
|
queryTraceability: () => queryTraceability,
|
|
79
103
|
saveGraph: () => saveGraph
|
|
@@ -105,6 +129,7 @@ var NODE_TYPES = [
|
|
|
105
129
|
"commit",
|
|
106
130
|
"build",
|
|
107
131
|
"test_result",
|
|
132
|
+
"execution_outcome",
|
|
108
133
|
// Observability (future)
|
|
109
134
|
"span",
|
|
110
135
|
"metric",
|
|
@@ -118,8 +143,18 @@ var NODE_TYPES = [
|
|
|
118
143
|
"design_token",
|
|
119
144
|
"aesthetic_intent",
|
|
120
145
|
"design_constraint",
|
|
146
|
+
"image_annotation",
|
|
121
147
|
// Traceability
|
|
122
|
-
"requirement"
|
|
148
|
+
"requirement",
|
|
149
|
+
// Business Knowledge
|
|
150
|
+
"business_rule",
|
|
151
|
+
"business_process",
|
|
152
|
+
"business_concept",
|
|
153
|
+
"business_term",
|
|
154
|
+
"business_metric",
|
|
155
|
+
"business_fact",
|
|
156
|
+
// Cache
|
|
157
|
+
"packed_summary"
|
|
123
158
|
];
|
|
124
159
|
var EDGE_TYPES = [
|
|
125
160
|
// Code relationships
|
|
@@ -141,6 +176,7 @@ var EDGE_TYPES = [
|
|
|
141
176
|
"co_changes_with",
|
|
142
177
|
"triggered_by",
|
|
143
178
|
"failed_in",
|
|
179
|
+
"outcome_of",
|
|
144
180
|
// Execution relationships (future)
|
|
145
181
|
"executed_by",
|
|
146
182
|
"measured_by",
|
|
@@ -152,10 +188,25 @@ var EDGE_TYPES = [
|
|
|
152
188
|
// Traceability relationships
|
|
153
189
|
"requires",
|
|
154
190
|
"verified_by",
|
|
155
|
-
"tested_by"
|
|
191
|
+
"tested_by",
|
|
192
|
+
// Business Knowledge relationships
|
|
193
|
+
"governs",
|
|
194
|
+
"measures",
|
|
195
|
+
"annotates",
|
|
196
|
+
// Cache relationships
|
|
197
|
+
"caches"
|
|
156
198
|
];
|
|
157
199
|
var OBSERVABILITY_TYPES = /* @__PURE__ */ new Set(["span", "metric", "log"]);
|
|
158
200
|
var CURRENT_SCHEMA_VERSION = 1;
|
|
201
|
+
var NODE_STABILITY = {
|
|
202
|
+
File: "session",
|
|
203
|
+
Function: "session",
|
|
204
|
+
Class: "session",
|
|
205
|
+
Constraint: "session",
|
|
206
|
+
PackedSummary: "session",
|
|
207
|
+
SkillDefinition: "static",
|
|
208
|
+
ToolDefinition: "static"
|
|
209
|
+
};
|
|
159
210
|
var GraphNodeSchema = import_zod.z.object({
|
|
160
211
|
id: import_zod.z.string(),
|
|
161
212
|
type: import_zod.z.enum(NODE_TYPES),
|
|
@@ -184,10 +235,28 @@ var GraphEdgeSchema = import_zod.z.object({
|
|
|
184
235
|
|
|
185
236
|
// src/store/Serializer.ts
|
|
186
237
|
var import_promises = require("fs/promises");
|
|
238
|
+
var import_node_fs = require("fs");
|
|
187
239
|
var import_node_path = require("path");
|
|
240
|
+
function streamGraphJson(filePath, nodes, edges) {
|
|
241
|
+
return new Promise((resolve, reject) => {
|
|
242
|
+
const stream = (0, import_node_fs.createWriteStream)(filePath, { encoding: "utf-8" });
|
|
243
|
+
stream.on("error", reject);
|
|
244
|
+
stream.write('{"nodes":[');
|
|
245
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
246
|
+
if (i > 0) stream.write(",");
|
|
247
|
+
stream.write(JSON.stringify(nodes[i]));
|
|
248
|
+
}
|
|
249
|
+
stream.write('],"edges":[');
|
|
250
|
+
for (let i = 0; i < edges.length; i++) {
|
|
251
|
+
if (i > 0) stream.write(",");
|
|
252
|
+
stream.write(JSON.stringify(edges[i]));
|
|
253
|
+
}
|
|
254
|
+
stream.write("]}");
|
|
255
|
+
stream.end(resolve);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
188
258
|
async function saveGraph(dirPath, nodes, edges) {
|
|
189
259
|
await (0, import_promises.mkdir)(dirPath, { recursive: true });
|
|
190
|
-
const graphData = { nodes, edges };
|
|
191
260
|
const metadata = {
|
|
192
261
|
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
193
262
|
lastScanTimestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -195,7 +264,7 @@ async function saveGraph(dirPath, nodes, edges) {
|
|
|
195
264
|
edgeCount: edges.length
|
|
196
265
|
};
|
|
197
266
|
await Promise.all([
|
|
198
|
-
(
|
|
267
|
+
streamGraphJson((0, import_node_path.join)(dirPath, "graph.json"), nodes, edges),
|
|
199
268
|
(0, import_promises.writeFile)((0, import_node_path.join)(dirPath, "metadata.json"), JSON.stringify(metadata, null, 2), "utf-8")
|
|
200
269
|
]);
|
|
201
270
|
}
|
|
@@ -384,8 +453,8 @@ var GraphStore = class {
|
|
|
384
453
|
}
|
|
385
454
|
// --- Persistence ---
|
|
386
455
|
async save(dirPath) {
|
|
387
|
-
const allNodes = Array.from(this.nodeMap.values())
|
|
388
|
-
const allEdges = Array.from(this.edgeMap.values())
|
|
456
|
+
const allNodes = Array.from(this.nodeMap.values());
|
|
457
|
+
const allEdges = Array.from(this.edgeMap.values());
|
|
389
458
|
await saveGraph(dirPath, allNodes, allEdges);
|
|
390
459
|
}
|
|
391
460
|
async load(dirPath) {
|
|
@@ -491,6 +560,94 @@ var VectorStore = class _VectorStore {
|
|
|
491
560
|
}
|
|
492
561
|
};
|
|
493
562
|
|
|
563
|
+
// src/store/PackedSummaryCache.ts
|
|
564
|
+
var DEFAULT_TTL_MS = 60 * 60 * 1e3;
|
|
565
|
+
function normalizeIntent(intent) {
|
|
566
|
+
return intent.trim().toLowerCase().replace(/\s+/g, " ");
|
|
567
|
+
}
|
|
568
|
+
function cacheNodeId(normalizedIntent) {
|
|
569
|
+
return `packed_summary:${normalizedIntent}`;
|
|
570
|
+
}
|
|
571
|
+
var PackedSummaryCache = class {
|
|
572
|
+
constructor(store, ttlMs = DEFAULT_TTL_MS) {
|
|
573
|
+
this.store = store;
|
|
574
|
+
this.ttlMs = ttlMs;
|
|
575
|
+
}
|
|
576
|
+
store;
|
|
577
|
+
ttlMs;
|
|
578
|
+
/** Returns cached envelope with `cached: true` if valid, or null if miss/stale. */
|
|
579
|
+
get(intent) {
|
|
580
|
+
const normalized = normalizeIntent(intent);
|
|
581
|
+
const nodeId = cacheNodeId(normalized);
|
|
582
|
+
const node = this.store.getNode(nodeId);
|
|
583
|
+
if (!node) return null;
|
|
584
|
+
const createdMs = this.parseCreatedMs(node.metadata["createdAt"]);
|
|
585
|
+
if (createdMs === null) return null;
|
|
586
|
+
if (Date.now() - createdMs > this.ttlMs) return null;
|
|
587
|
+
if (!this.areSourcesFresh(nodeId, node, createdMs)) return null;
|
|
588
|
+
return this.parseEnvelope(node.metadata["envelope"]);
|
|
589
|
+
}
|
|
590
|
+
/** Parse and validate createdAt. Returns epoch ms or null if missing/malformed (GC-002). */
|
|
591
|
+
parseCreatedMs(createdAt) {
|
|
592
|
+
if (!createdAt) return null;
|
|
593
|
+
const ms = new Date(createdAt).getTime();
|
|
594
|
+
return Number.isNaN(ms) ? null : ms;
|
|
595
|
+
}
|
|
596
|
+
/** GC-001: Checks source nodes exist and are unmodified since cache creation. */
|
|
597
|
+
areSourcesFresh(nodeId, node, createdMs) {
|
|
598
|
+
const sourceNodeIds = node.metadata["sourceNodeIds"];
|
|
599
|
+
const edges = this.store.getEdges({ from: nodeId, type: "caches" });
|
|
600
|
+
if (sourceNodeIds && edges.length < sourceNodeIds.length) return false;
|
|
601
|
+
for (const edge of edges) {
|
|
602
|
+
const sourceNode = this.store.getNode(edge.to);
|
|
603
|
+
if (!sourceNode) return false;
|
|
604
|
+
if (sourceNode.lastModified) {
|
|
605
|
+
const sourceModMs = new Date(sourceNode.lastModified).getTime();
|
|
606
|
+
if (sourceModMs > createdMs) return false;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return true;
|
|
610
|
+
}
|
|
611
|
+
/** Parse envelope JSON and set cached: true. Returns null on invalid JSON. */
|
|
612
|
+
parseEnvelope(raw) {
|
|
613
|
+
try {
|
|
614
|
+
const envelope = JSON.parse(raw);
|
|
615
|
+
return { ...envelope, meta: { ...envelope.meta, cached: true } };
|
|
616
|
+
} catch {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
/** Write a PackedSummary node with caches edges to source nodes. */
|
|
621
|
+
set(intent, envelope, sourceNodeIds) {
|
|
622
|
+
const normalized = normalizeIntent(intent);
|
|
623
|
+
const nodeId = cacheNodeId(normalized);
|
|
624
|
+
this.store.removeNode(nodeId);
|
|
625
|
+
this.store.addNode({
|
|
626
|
+
id: nodeId,
|
|
627
|
+
type: "packed_summary",
|
|
628
|
+
name: normalized,
|
|
629
|
+
metadata: {
|
|
630
|
+
envelope: JSON.stringify(envelope),
|
|
631
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
632
|
+
sourceNodeIds
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
for (const sourceId of sourceNodeIds) {
|
|
636
|
+
this.store.addEdge({
|
|
637
|
+
from: nodeId,
|
|
638
|
+
to: sourceId,
|
|
639
|
+
type: "caches"
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
/** Explicitly invalidate a cached packed summary. */
|
|
644
|
+
invalidate(intent) {
|
|
645
|
+
const normalized = normalizeIntent(intent);
|
|
646
|
+
const nodeId = cacheNodeId(normalized);
|
|
647
|
+
this.store.removeNode(nodeId);
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
|
|
494
651
|
// src/query/ContextQL.ts
|
|
495
652
|
function edgeKey2(e) {
|
|
496
653
|
return `${e.from}|${e.to}|${e.type}`;
|
|
@@ -666,6 +823,49 @@ function groupNodesByImpact(nodes, excludeId) {
|
|
|
666
823
|
var fs = __toESM(require("fs/promises"));
|
|
667
824
|
var path = __toESM(require("path"));
|
|
668
825
|
var SKIP_METHOD_NAMES = /* @__PURE__ */ new Set(["constructor", "if", "for", "while", "switch"]);
|
|
826
|
+
var FUNCTION_PATTERNS = {
|
|
827
|
+
python: /^def\s+(\w+)\s*\(/,
|
|
828
|
+
go: /^func\s+(\w+)\s*[[(]/,
|
|
829
|
+
rust: /^(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/,
|
|
830
|
+
typescript: /(?:export\s+)?(?:async\s+)?function\s+(\w+)/,
|
|
831
|
+
javascript: /(?:export\s+)?(?:async\s+)?function\s+(\w+)/
|
|
832
|
+
};
|
|
833
|
+
var CLASS_PATTERNS = {
|
|
834
|
+
python: /^class\s+(\w+)/,
|
|
835
|
+
go: /^type\s+(\w+)\s+struct\b/,
|
|
836
|
+
rust: /^(?:pub\s+)?struct\s+(\w+)/,
|
|
837
|
+
java: /(?:public|private|protected)?\s*(?:abstract\s+|final\s+)?class\s+(\w+)/,
|
|
838
|
+
typescript: /(?:export\s+)?class\s+(\w+)/,
|
|
839
|
+
javascript: /(?:export\s+)?class\s+(\w+)/
|
|
840
|
+
};
|
|
841
|
+
var INTERFACE_PATTERNS = {
|
|
842
|
+
go: /^type\s+(\w+)\s+interface\b/,
|
|
843
|
+
rust: /^(?:pub\s+)?trait\s+(\w+)/,
|
|
844
|
+
java: /(?:public\s+)?interface\s+(\w+)/,
|
|
845
|
+
typescript: /(?:export\s+)?interface\s+(\w+)/,
|
|
846
|
+
javascript: /(?:export\s+)?interface\s+(\w+)/
|
|
847
|
+
};
|
|
848
|
+
var METHOD_PATTERNS = {
|
|
849
|
+
python: /^\s+def\s+(\w+)\s*\(/,
|
|
850
|
+
rust: /^\s+(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/,
|
|
851
|
+
java: /^\s+(?:(?:public|private|protected|static|final|abstract|synchronized)\s+)*(?:\w+(?:<[^>]*>)?)\s+(\w+)\s*\(/,
|
|
852
|
+
typescript: /^\s+(?:(?:public|private|protected|readonly|static|abstract)\s+)*(?:async\s+)?(\w+)\s*\(/,
|
|
853
|
+
javascript: /^\s+(?:(?:public|private|protected|readonly|static|abstract)\s+)*(?:async\s+)?(\w+)\s*\(/
|
|
854
|
+
};
|
|
855
|
+
var GO_METHOD_PATTERN = /^func\s+\(\w+\s+\*?(\w+)\)\s+(\w+)\s*\(/;
|
|
856
|
+
var VARIABLE_PATTERNS = {
|
|
857
|
+
python: /^([A-Z]\w*)\s*=/,
|
|
858
|
+
go: /^(?:var|const)\s+(\w+)/,
|
|
859
|
+
rust: /^(?:pub\s+)?(?:const|static)\s+(\w+)/,
|
|
860
|
+
typescript: /(?:export\s+)?(?:const|let|var)\s+(\w+)/,
|
|
861
|
+
javascript: /(?:export\s+)?(?:const|let|var)\s+(\w+)/
|
|
862
|
+
};
|
|
863
|
+
function isExported(lang, line, name) {
|
|
864
|
+
if (lang === "go") return /^[A-Z]/.test(name);
|
|
865
|
+
if (lang === "rust") return /^\s*pub\b/.test(line);
|
|
866
|
+
if (lang === "java") return /\bpublic\b/.test(line);
|
|
867
|
+
return line.includes("export");
|
|
868
|
+
}
|
|
669
869
|
function countBraces(line) {
|
|
670
870
|
let net = 0;
|
|
671
871
|
for (const ch of line) {
|
|
@@ -674,6 +874,23 @@ function countBraces(line) {
|
|
|
674
874
|
}
|
|
675
875
|
return net;
|
|
676
876
|
}
|
|
877
|
+
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
878
|
+
".ts",
|
|
879
|
+
".tsx",
|
|
880
|
+
".js",
|
|
881
|
+
".jsx",
|
|
882
|
+
".py",
|
|
883
|
+
".go",
|
|
884
|
+
".rs",
|
|
885
|
+
".java",
|
|
886
|
+
".groovy"
|
|
887
|
+
]);
|
|
888
|
+
var SKIP_EXTENSIONS = /* @__PURE__ */ new Set([".d.ts"]);
|
|
889
|
+
function isSupportedSourceFile(name) {
|
|
890
|
+
if (SKIP_EXTENSIONS.has(name.slice(name.lastIndexOf(".")))) return false;
|
|
891
|
+
const ext = name.slice(name.lastIndexOf("."));
|
|
892
|
+
return SUPPORTED_EXTENSIONS.has(ext);
|
|
893
|
+
}
|
|
677
894
|
var CodeIngestor = class {
|
|
678
895
|
constructor(store) {
|
|
679
896
|
this.store = store;
|
|
@@ -686,22 +903,34 @@ var CodeIngestor = class {
|
|
|
686
903
|
let edgesAdded = 0;
|
|
687
904
|
const files = await this.findSourceFiles(rootDir);
|
|
688
905
|
const nameToFiles = /* @__PURE__ */ new Map();
|
|
689
|
-
const fileContents = /* @__PURE__ */ new Map();
|
|
690
906
|
for (const filePath of files) {
|
|
691
907
|
try {
|
|
692
|
-
const result = await this.processFile(filePath, rootDir, nameToFiles
|
|
908
|
+
const result = await this.processFile(filePath, rootDir, nameToFiles);
|
|
693
909
|
nodesAdded += result.nodesAdded;
|
|
694
910
|
edgesAdded += result.edgesAdded;
|
|
695
911
|
} catch (err) {
|
|
696
912
|
errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
697
913
|
}
|
|
698
914
|
}
|
|
699
|
-
const
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
915
|
+
for (const filePath of files) {
|
|
916
|
+
try {
|
|
917
|
+
const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
918
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
919
|
+
const callsEdges = this.extractCallsEdgesForFile(relativePath, content, nameToFiles);
|
|
920
|
+
for (const edge of callsEdges) {
|
|
921
|
+
this.store.addEdge(edge);
|
|
922
|
+
edgesAdded++;
|
|
923
|
+
}
|
|
924
|
+
} catch {
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
for (const filePath of files) {
|
|
928
|
+
try {
|
|
929
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
930
|
+
edgesAdded += this.extractReqAnnotationsForFile(filePath, content, rootDir);
|
|
931
|
+
} catch {
|
|
932
|
+
}
|
|
703
933
|
}
|
|
704
|
-
edgesAdded += this.extractReqAnnotations(fileContents, rootDir);
|
|
705
934
|
return {
|
|
706
935
|
nodesAdded,
|
|
707
936
|
nodesUpdated: 0,
|
|
@@ -711,14 +940,13 @@ var CodeIngestor = class {
|
|
|
711
940
|
durationMs: Date.now() - start
|
|
712
941
|
};
|
|
713
942
|
}
|
|
714
|
-
async processFile(filePath, rootDir, nameToFiles
|
|
943
|
+
async processFile(filePath, rootDir, nameToFiles) {
|
|
715
944
|
let nodesAdded = 0;
|
|
716
945
|
let edgesAdded = 0;
|
|
717
946
|
const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
718
947
|
const content = await fs.readFile(filePath, "utf-8");
|
|
719
948
|
const stat2 = await fs.stat(filePath);
|
|
720
949
|
const fileId = `file:${relativePath}`;
|
|
721
|
-
fileContents.set(relativePath, content);
|
|
722
950
|
const fileNode = {
|
|
723
951
|
id: fileId,
|
|
724
952
|
type: "file",
|
|
@@ -758,9 +986,9 @@ var CodeIngestor = class {
|
|
|
758
986
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
759
987
|
for (const entry of entries) {
|
|
760
988
|
const fullPath = path.join(dir, entry.name);
|
|
761
|
-
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "dist") {
|
|
989
|
+
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "dist" && entry.name !== "target" && entry.name !== "build" && entry.name !== ".git" && entry.name !== "__pycache__" && entry.name !== ".venv" && entry.name !== ".turbo" && entry.name !== ".harness" && entry.name !== ".next" && entry.name !== ".vscode" && entry.name !== ".idea" && entry.name !== "vendor" && entry.name !== "out" && entry.name !== ".gradle" && entry.name !== ".gradle-home" && entry.name !== "bin" && entry.name !== "obj" && entry.name !== "venv" && entry.name !== "_build" && entry.name !== "deps" && entry.name !== "coverage" && entry.name !== ".nyc_output") {
|
|
762
990
|
results.push(...await this.findSourceFiles(fullPath));
|
|
763
|
-
} else if (entry.isFile() &&
|
|
991
|
+
} else if (entry.isFile() && isSupportedSourceFile(entry.name)) {
|
|
764
992
|
results.push(fullPath);
|
|
765
993
|
}
|
|
766
994
|
}
|
|
@@ -769,25 +997,40 @@ var CodeIngestor = class {
|
|
|
769
997
|
extractSymbols(content, fileId, relativePath) {
|
|
770
998
|
const results = [];
|
|
771
999
|
const lines = content.split("\n");
|
|
1000
|
+
const lang = this.detectLanguage(relativePath);
|
|
772
1001
|
const ctx = { className: null, classId: null, insideClass: false, braceDepth: 0 };
|
|
773
1002
|
for (let i = 0; i < lines.length; i++) {
|
|
774
1003
|
const line = lines[i];
|
|
775
|
-
|
|
776
|
-
if (this.tryExtractClass(line, lines, i, fileId, relativePath, ctx, results)) continue;
|
|
777
|
-
if (this.tryExtractInterface(line, lines, i, fileId, relativePath, ctx, results)) continue;
|
|
778
|
-
if (this.updateClassContext(line, ctx)) continue;
|
|
779
|
-
if (this.tryExtractMethod(line, lines, i, fileId, relativePath, ctx, results)) continue;
|
|
780
|
-
if (ctx.insideClass) continue;
|
|
781
|
-
this.tryExtractVariable(line, i, fileId, relativePath, results);
|
|
1004
|
+
this.processSymbolLine(line, lines, i, fileId, relativePath, ctx, results, lang);
|
|
782
1005
|
}
|
|
783
1006
|
return results;
|
|
784
1007
|
}
|
|
785
|
-
|
|
786
|
-
|
|
1008
|
+
processSymbolLine(line, lines, i, fileId, relativePath, ctx, results, lang) {
|
|
1009
|
+
if (lang === "python") {
|
|
1010
|
+
this.updatePythonClassContext(lines, i, ctx);
|
|
1011
|
+
}
|
|
1012
|
+
if (this.tryExtractFunction(line, lines, i, fileId, relativePath, ctx, results, lang)) return;
|
|
1013
|
+
if (this.tryExtractClass(line, lines, i, fileId, relativePath, ctx, results, lang)) return;
|
|
1014
|
+
if (this.tryExtractInterface(line, lines, i, fileId, relativePath, ctx, results, lang)) return;
|
|
1015
|
+
if (this.tryEnterImplBlock(line, lang, relativePath, ctx)) return;
|
|
1016
|
+
if (lang !== "python" && this.updateClassContext(line, ctx)) return;
|
|
1017
|
+
if (this.tryExtractMethod(line, lines, i, fileId, relativePath, ctx, results, lang)) return;
|
|
1018
|
+
if (ctx.insideClass) return;
|
|
1019
|
+
this.tryExtractVariable(line, i, fileId, relativePath, results, lang);
|
|
1020
|
+
}
|
|
1021
|
+
matchFunction(line, lang) {
|
|
1022
|
+
if (lang === "java") return null;
|
|
1023
|
+
const pattern = FUNCTION_PATTERNS[lang];
|
|
1024
|
+
return pattern ? line.match(pattern) : null;
|
|
1025
|
+
}
|
|
1026
|
+
tryExtractFunction(line, lines, i, fileId, relativePath, ctx, results, lang) {
|
|
1027
|
+
if (ctx.insideClass) return false;
|
|
1028
|
+
const fnMatch = this.matchFunction(line, lang);
|
|
787
1029
|
if (!fnMatch) return false;
|
|
788
1030
|
const name = fnMatch[1];
|
|
789
1031
|
const id = `function:${relativePath}:${name}`;
|
|
790
|
-
const endLine = this.findClosingBrace(lines, i);
|
|
1032
|
+
const endLine = lang === "python" ? this.findEndOfIndentBlock(lines, i) : this.findClosingBrace(lines, i);
|
|
1033
|
+
const exported = isExported(lang, line, name);
|
|
791
1034
|
results.push({
|
|
792
1035
|
node: {
|
|
793
1036
|
id,
|
|
@@ -796,7 +1039,7 @@ var CodeIngestor = class {
|
|
|
796
1039
|
path: relativePath,
|
|
797
1040
|
location: { fileId, startLine: i + 1, endLine },
|
|
798
1041
|
metadata: {
|
|
799
|
-
exported
|
|
1042
|
+
exported,
|
|
800
1043
|
cyclomaticComplexity: this.computeCyclomaticComplexity(lines.slice(i, endLine)),
|
|
801
1044
|
nestingDepth: this.computeMaxNesting(lines.slice(i, endLine)),
|
|
802
1045
|
lineCount: endLine - i,
|
|
@@ -811,12 +1054,17 @@ var CodeIngestor = class {
|
|
|
811
1054
|
}
|
|
812
1055
|
return true;
|
|
813
1056
|
}
|
|
814
|
-
|
|
815
|
-
const
|
|
1057
|
+
matchClass(line, lang) {
|
|
1058
|
+
const pattern = CLASS_PATTERNS[lang];
|
|
1059
|
+
return pattern ? line.match(pattern) : null;
|
|
1060
|
+
}
|
|
1061
|
+
tryExtractClass(line, lines, i, fileId, relativePath, ctx, results, lang) {
|
|
1062
|
+
const classMatch = this.matchClass(line, lang);
|
|
816
1063
|
if (!classMatch) return false;
|
|
817
1064
|
const name = classMatch[1];
|
|
818
1065
|
const id = `class:${relativePath}:${name}`;
|
|
819
|
-
const endLine = this.findClosingBrace(lines, i);
|
|
1066
|
+
const endLine = lang === "python" ? this.findEndOfIndentBlock(lines, i) : this.findClosingBrace(lines, i);
|
|
1067
|
+
const exported = isExported(lang, line, name);
|
|
820
1068
|
results.push({
|
|
821
1069
|
node: {
|
|
822
1070
|
id,
|
|
@@ -824,22 +1072,27 @@ var CodeIngestor = class {
|
|
|
824
1072
|
name,
|
|
825
1073
|
path: relativePath,
|
|
826
1074
|
location: { fileId, startLine: i + 1, endLine },
|
|
827
|
-
metadata: { exported
|
|
1075
|
+
metadata: { exported }
|
|
828
1076
|
},
|
|
829
1077
|
edge: { from: fileId, to: id, type: "contains" }
|
|
830
1078
|
});
|
|
831
1079
|
ctx.className = name;
|
|
832
1080
|
ctx.classId = id;
|
|
833
1081
|
ctx.insideClass = true;
|
|
834
|
-
ctx.braceDepth = countBraces(line);
|
|
1082
|
+
ctx.braceDepth = lang === "python" ? 1 : countBraces(line);
|
|
835
1083
|
return true;
|
|
836
1084
|
}
|
|
837
|
-
|
|
838
|
-
const
|
|
1085
|
+
matchInterface(line, lang) {
|
|
1086
|
+
const pattern = INTERFACE_PATTERNS[lang];
|
|
1087
|
+
return pattern ? line.match(pattern) : null;
|
|
1088
|
+
}
|
|
1089
|
+
tryExtractInterface(line, lines, i, fileId, relativePath, ctx, results, lang) {
|
|
1090
|
+
const ifaceMatch = this.matchInterface(line, lang);
|
|
839
1091
|
if (!ifaceMatch) return false;
|
|
840
1092
|
const name = ifaceMatch[1];
|
|
841
1093
|
const id = `interface:${relativePath}:${name}`;
|
|
842
1094
|
const endLine = this.findClosingBrace(lines, i);
|
|
1095
|
+
const exported = isExported(lang, line, name);
|
|
843
1096
|
results.push({
|
|
844
1097
|
node: {
|
|
845
1098
|
id,
|
|
@@ -847,7 +1100,7 @@ var CodeIngestor = class {
|
|
|
847
1100
|
name,
|
|
848
1101
|
path: relativePath,
|
|
849
1102
|
location: { fileId, startLine: i + 1, endLine },
|
|
850
|
-
metadata: { exported
|
|
1103
|
+
metadata: { exported }
|
|
851
1104
|
},
|
|
852
1105
|
edge: { from: fileId, to: id, type: "contains" }
|
|
853
1106
|
});
|
|
@@ -856,6 +1109,23 @@ var CodeIngestor = class {
|
|
|
856
1109
|
ctx.insideClass = false;
|
|
857
1110
|
return true;
|
|
858
1111
|
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Enter an impl block (Rust) or Java class body — sets class context without creating a node.
|
|
1114
|
+
*/
|
|
1115
|
+
tryEnterImplBlock(line, lang, relativePath, ctx) {
|
|
1116
|
+
if (lang === "rust") {
|
|
1117
|
+
const implMatch = line.match(/^impl(?:<[^>]*>)?\s+(\w+)/);
|
|
1118
|
+
if (implMatch) {
|
|
1119
|
+
const name = implMatch[1];
|
|
1120
|
+
ctx.className = name;
|
|
1121
|
+
ctx.classId = `class:${relativePath}:${name}`;
|
|
1122
|
+
ctx.insideClass = true;
|
|
1123
|
+
ctx.braceDepth = countBraces(line);
|
|
1124
|
+
return true;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
return false;
|
|
1128
|
+
}
|
|
859
1129
|
/** Update brace tracking; returns true when line is consumed (class ended or tracked). */
|
|
860
1130
|
updateClassContext(line, ctx) {
|
|
861
1131
|
if (!ctx.insideClass) return false;
|
|
@@ -868,16 +1138,32 @@ var CodeIngestor = class {
|
|
|
868
1138
|
}
|
|
869
1139
|
return false;
|
|
870
1140
|
}
|
|
871
|
-
|
|
872
|
-
if (
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
);
|
|
1141
|
+
matchMethod(line, lang, insideClass) {
|
|
1142
|
+
if (lang === "go") return line.match(GO_METHOD_PATTERN);
|
|
1143
|
+
if (!insideClass) return null;
|
|
1144
|
+
const pattern = METHOD_PATTERNS[lang];
|
|
1145
|
+
return pattern ? line.match(pattern) : null;
|
|
1146
|
+
}
|
|
1147
|
+
tryExtractMethod(line, lines, i, fileId, relativePath, ctx, results, lang) {
|
|
1148
|
+
const methodMatch = this.matchMethod(line, lang, ctx.insideClass);
|
|
876
1149
|
if (!methodMatch) return false;
|
|
877
|
-
|
|
1150
|
+
let methodName;
|
|
1151
|
+
let className;
|
|
1152
|
+
let classId;
|
|
1153
|
+
if (lang === "go") {
|
|
1154
|
+
const receiverType = methodMatch[1];
|
|
1155
|
+
methodName = methodMatch[2];
|
|
1156
|
+
className = receiverType;
|
|
1157
|
+
classId = `class:${relativePath}:${receiverType}`;
|
|
1158
|
+
} else {
|
|
1159
|
+
if (!ctx.insideClass || !ctx.className || !ctx.classId) return false;
|
|
1160
|
+
methodName = methodMatch[1];
|
|
1161
|
+
className = ctx.className;
|
|
1162
|
+
classId = ctx.classId;
|
|
1163
|
+
}
|
|
878
1164
|
if (SKIP_METHOD_NAMES.has(methodName)) return false;
|
|
879
|
-
const id = `method:${relativePath}:${
|
|
880
|
-
const endLine = this.findClosingBrace(lines, i);
|
|
1165
|
+
const id = `method:${relativePath}:${className}.${methodName}`;
|
|
1166
|
+
const endLine = lang === "python" ? this.findEndOfIndentBlock(lines, i) : this.findClosingBrace(lines, i);
|
|
881
1167
|
results.push({
|
|
882
1168
|
node: {
|
|
883
1169
|
id,
|
|
@@ -886,23 +1172,28 @@ var CodeIngestor = class {
|
|
|
886
1172
|
path: relativePath,
|
|
887
1173
|
location: { fileId, startLine: i + 1, endLine },
|
|
888
1174
|
metadata: {
|
|
889
|
-
className
|
|
890
|
-
exported:
|
|
1175
|
+
className,
|
|
1176
|
+
exported: isExported(lang, line, methodName),
|
|
891
1177
|
cyclomaticComplexity: this.computeCyclomaticComplexity(lines.slice(i, endLine)),
|
|
892
1178
|
nestingDepth: this.computeMaxNesting(lines.slice(i, endLine)),
|
|
893
1179
|
lineCount: endLine - i,
|
|
894
1180
|
parameterCount: this.countParameters(line)
|
|
895
1181
|
}
|
|
896
1182
|
},
|
|
897
|
-
edge: { from:
|
|
1183
|
+
edge: { from: classId, to: id, type: "contains" }
|
|
898
1184
|
});
|
|
899
1185
|
return true;
|
|
900
1186
|
}
|
|
901
|
-
|
|
902
|
-
const
|
|
1187
|
+
matchVariable(line, lang) {
|
|
1188
|
+
const pattern = VARIABLE_PATTERNS[lang];
|
|
1189
|
+
return pattern ? line.match(pattern) : null;
|
|
1190
|
+
}
|
|
1191
|
+
tryExtractVariable(line, i, fileId, relativePath, results, lang) {
|
|
1192
|
+
const varMatch = this.matchVariable(line, lang);
|
|
903
1193
|
if (!varMatch) return;
|
|
904
1194
|
const name = varMatch[1];
|
|
905
1195
|
const id = `variable:${relativePath}:${name}`;
|
|
1196
|
+
const exported = isExported(lang, line, name);
|
|
906
1197
|
results.push({
|
|
907
1198
|
node: {
|
|
908
1199
|
id,
|
|
@@ -910,7 +1201,7 @@ var CodeIngestor = class {
|
|
|
910
1201
|
name,
|
|
911
1202
|
path: relativePath,
|
|
912
1203
|
location: { fileId, startLine: i + 1, endLine: i + 1 },
|
|
913
|
-
metadata: { exported
|
|
1204
|
+
metadata: { exported }
|
|
914
1205
|
},
|
|
915
1206
|
edge: { from: fileId, to: id, type: "contains" }
|
|
916
1207
|
});
|
|
@@ -939,48 +1230,79 @@ var CodeIngestor = class {
|
|
|
939
1230
|
return startIndex + 1;
|
|
940
1231
|
}
|
|
941
1232
|
/**
|
|
942
|
-
*
|
|
1233
|
+
* Find the end of an indentation-based block (Python).
|
|
1234
|
+
* The block ends when a subsequent non-empty line has indentation <= the starting line.
|
|
1235
|
+
*/
|
|
1236
|
+
findEndOfIndentBlock(lines, startIndex) {
|
|
1237
|
+
const startLine = lines[startIndex] ?? "";
|
|
1238
|
+
const baseIndent = startLine.search(/\S/);
|
|
1239
|
+
let lastNonEmpty = startIndex;
|
|
1240
|
+
for (let i = startIndex + 1; i < lines.length; i++) {
|
|
1241
|
+
const line = lines[i];
|
|
1242
|
+
if (line.trim() === "") continue;
|
|
1243
|
+
const indent = line.search(/\S/);
|
|
1244
|
+
if (indent <= baseIndent) {
|
|
1245
|
+
return lastNonEmpty + 1;
|
|
1246
|
+
}
|
|
1247
|
+
lastNonEmpty = i;
|
|
1248
|
+
}
|
|
1249
|
+
return lastNonEmpty + 1;
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Update class context for Python using indentation tracking.
|
|
1253
|
+
*/
|
|
1254
|
+
updatePythonClassContext(lines, i, ctx) {
|
|
1255
|
+
if (!ctx.insideClass) return false;
|
|
1256
|
+
const line = lines[i];
|
|
1257
|
+
if (line.trim() === "") return false;
|
|
1258
|
+
const indent = line.search(/\S/);
|
|
1259
|
+
if (indent <= 0) {
|
|
1260
|
+
ctx.className = null;
|
|
1261
|
+
ctx.classId = null;
|
|
1262
|
+
ctx.insideClass = false;
|
|
1263
|
+
return false;
|
|
1264
|
+
}
|
|
1265
|
+
return false;
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Scan a file for identifiers matching known callable names,
|
|
943
1269
|
* then create file-to-file "calls" edges. Uses regex heuristic (not AST).
|
|
944
1270
|
*/
|
|
945
|
-
|
|
1271
|
+
extractCallsEdgesForFile(relativePath, content, nameToFiles) {
|
|
946
1272
|
const edges = [];
|
|
947
1273
|
const seen = /* @__PURE__ */ new Set();
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
});
|
|
968
|
-
}
|
|
1274
|
+
const callerFileId = `file:${relativePath}`;
|
|
1275
|
+
const callPattern = /\b([a-zA-Z_$][\w$]*)\s*\(/g;
|
|
1276
|
+
let match;
|
|
1277
|
+
while ((match = callPattern.exec(content)) !== null) {
|
|
1278
|
+
const name = match[1];
|
|
1279
|
+
const targetFiles = nameToFiles.get(name);
|
|
1280
|
+
if (!targetFiles) continue;
|
|
1281
|
+
for (const targetFile of targetFiles) {
|
|
1282
|
+
if (targetFile === relativePath) continue;
|
|
1283
|
+
const targetFileId = `file:${targetFile}`;
|
|
1284
|
+
const key = `${callerFileId}|${targetFileId}`;
|
|
1285
|
+
if (seen.has(key)) continue;
|
|
1286
|
+
seen.add(key);
|
|
1287
|
+
edges.push({
|
|
1288
|
+
from: callerFileId,
|
|
1289
|
+
to: targetFileId,
|
|
1290
|
+
type: "calls",
|
|
1291
|
+
metadata: { confidence: "regex" }
|
|
1292
|
+
});
|
|
969
1293
|
}
|
|
970
1294
|
}
|
|
971
1295
|
return edges;
|
|
972
1296
|
}
|
|
973
1297
|
async extractImports(content, fileId, relativePath, rootDir) {
|
|
1298
|
+
const lang = this.detectLanguage(relativePath);
|
|
974
1299
|
const edges = [];
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
const importPath = match[1];
|
|
979
|
-
if (!importPath.startsWith(".")) continue;
|
|
1300
|
+
const importPaths = this.extractImportPaths(content, lang);
|
|
1301
|
+
for (const { importPath, isTypeOnly } of importPaths) {
|
|
1302
|
+
if ((lang === "typescript" || lang === "javascript") && !importPath.startsWith(".")) continue;
|
|
980
1303
|
const resolvedPath = await this.resolveImportPath(relativePath, importPath, rootDir);
|
|
981
1304
|
if (resolvedPath) {
|
|
982
1305
|
const targetId = `file:${resolvedPath}`;
|
|
983
|
-
const isTypeOnly = match[0].includes("import type");
|
|
984
1306
|
edges.push({
|
|
985
1307
|
from: fileId,
|
|
986
1308
|
to: targetId,
|
|
@@ -991,10 +1313,53 @@ var CodeIngestor = class {
|
|
|
991
1313
|
}
|
|
992
1314
|
return edges;
|
|
993
1315
|
}
|
|
1316
|
+
extractImportPaths(content, lang) {
|
|
1317
|
+
const results = [];
|
|
1318
|
+
if (lang === "typescript" || lang === "javascript") {
|
|
1319
|
+
const importRegex = /import\s+(?:type\s+)?(?:\{[^}]*\}|[\w*]+)\s+from\s+['"]([^'"]+)['"]/g;
|
|
1320
|
+
let match;
|
|
1321
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
1322
|
+
results.push({
|
|
1323
|
+
importPath: match[1],
|
|
1324
|
+
isTypeOnly: match[0].includes("import type")
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
} else if (lang === "python") {
|
|
1328
|
+
const fromImport = /(?:from\s+([\w.]+)\s+import|import\s+([\w.]+))/g;
|
|
1329
|
+
let match;
|
|
1330
|
+
while ((match = fromImport.exec(content)) !== null) {
|
|
1331
|
+
const importPath = match[1] ?? match[2];
|
|
1332
|
+
if (importPath.startsWith(".")) {
|
|
1333
|
+
results.push({ importPath, isTypeOnly: false });
|
|
1334
|
+
} else {
|
|
1335
|
+
results.push({ importPath: importPath.replace(/\./g, "/"), isTypeOnly: false });
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
} else if (lang === "go") {
|
|
1339
|
+
const goImport = /"([^"]+)"/g;
|
|
1340
|
+
let match;
|
|
1341
|
+
while ((match = goImport.exec(content)) !== null) {
|
|
1342
|
+
results.push({ importPath: match[1], isTypeOnly: false });
|
|
1343
|
+
}
|
|
1344
|
+
} else if (lang === "rust") {
|
|
1345
|
+
const useDecl = /use\s+((?:crate|super|self)(?:::\w+)+)/g;
|
|
1346
|
+
let match;
|
|
1347
|
+
while ((match = useDecl.exec(content)) !== null) {
|
|
1348
|
+
results.push({ importPath: match[1], isTypeOnly: false });
|
|
1349
|
+
}
|
|
1350
|
+
} else if (lang === "java") {
|
|
1351
|
+
const javaImport = /import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;/g;
|
|
1352
|
+
let match;
|
|
1353
|
+
while ((match = javaImport.exec(content)) !== null) {
|
|
1354
|
+
results.push({ importPath: match[1], isTypeOnly: false });
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
return results;
|
|
1358
|
+
}
|
|
994
1359
|
async resolveImportPath(fromFile, importPath, rootDir) {
|
|
995
1360
|
const fromDir = path.dirname(fromFile);
|
|
996
1361
|
const resolved = path.normalize(path.join(fromDir, importPath)).replace(/\\/g, "/");
|
|
997
|
-
const extensions = [".ts", ".tsx", ".js", ".jsx"];
|
|
1362
|
+
const extensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
|
|
998
1363
|
for (const ext of extensions) {
|
|
999
1364
|
const candidate = resolved.replace(/\.js$/, "") + ext;
|
|
1000
1365
|
const fullPath = path.join(rootDir, candidate);
|
|
@@ -1004,7 +1369,8 @@ var CodeIngestor = class {
|
|
|
1004
1369
|
} catch {
|
|
1005
1370
|
}
|
|
1006
1371
|
}
|
|
1007
|
-
|
|
1372
|
+
const indexExtensions = [".ts", ".tsx", ".js", ".jsx"];
|
|
1373
|
+
for (const ext of indexExtensions) {
|
|
1008
1374
|
const candidate = path.join(resolved, `index${ext}`).replace(/\\/g, "/");
|
|
1009
1375
|
const fullPath = path.join(rootDir, candidate);
|
|
1010
1376
|
try {
|
|
@@ -1013,6 +1379,12 @@ var CodeIngestor = class {
|
|
|
1013
1379
|
} catch {
|
|
1014
1380
|
}
|
|
1015
1381
|
}
|
|
1382
|
+
const pyInit = path.join(resolved, "__init__.py").replace(/\\/g, "/");
|
|
1383
|
+
try {
|
|
1384
|
+
await fs.access(path.join(rootDir, pyInit));
|
|
1385
|
+
return pyInit;
|
|
1386
|
+
} catch {
|
|
1387
|
+
}
|
|
1016
1388
|
return null;
|
|
1017
1389
|
}
|
|
1018
1390
|
computeCyclomaticComplexity(lines) {
|
|
@@ -1058,6 +1430,11 @@ var CodeIngestor = class {
|
|
|
1058
1430
|
detectLanguage(filePath) {
|
|
1059
1431
|
if (/\.tsx?$/.test(filePath)) return "typescript";
|
|
1060
1432
|
if (/\.jsx?$/.test(filePath)) return "javascript";
|
|
1433
|
+
if (/\.py$/.test(filePath)) return "python";
|
|
1434
|
+
if (/\.go$/.test(filePath)) return "go";
|
|
1435
|
+
if (/\.rs$/.test(filePath)) return "rust";
|
|
1436
|
+
if (/\.java$/.test(filePath)) return "java";
|
|
1437
|
+
if (/\.groovy$/.test(filePath)) return "java";
|
|
1061
1438
|
return "unknown";
|
|
1062
1439
|
}
|
|
1063
1440
|
/**
|
|
@@ -1065,40 +1442,38 @@ var CodeIngestor = class {
|
|
|
1065
1442
|
* linking requirement nodes to the annotated files.
|
|
1066
1443
|
* Format: // @req <feature-name>#<index>
|
|
1067
1444
|
*/
|
|
1068
|
-
|
|
1069
|
-
const REQ_TAG =
|
|
1445
|
+
extractReqAnnotationsForFile(filePath, content, rootDir) {
|
|
1446
|
+
const REQ_TAG = /(?:\/\/|#|\/\*)\s*@req\s+([\w-]+)#(\d+)/g;
|
|
1070
1447
|
const reqNodes = this.store.findNodes({ type: "requirement" });
|
|
1071
1448
|
let edgesAdded = 0;
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1449
|
+
let match;
|
|
1450
|
+
REQ_TAG.lastIndex = 0;
|
|
1451
|
+
while ((match = REQ_TAG.exec(content)) !== null) {
|
|
1452
|
+
const featureName = match[1];
|
|
1453
|
+
const reqIndex = parseInt(match[2], 10);
|
|
1454
|
+
const reqNode = reqNodes.find(
|
|
1455
|
+
(n) => n.metadata.featureName === featureName && n.metadata.index === reqIndex
|
|
1456
|
+
);
|
|
1457
|
+
if (!reqNode) {
|
|
1458
|
+
console.warn(
|
|
1459
|
+
`@req annotation references non-existent requirement: ${featureName}#${reqIndex} in ${filePath}`
|
|
1080
1460
|
);
|
|
1081
|
-
|
|
1082
|
-
console.warn(
|
|
1083
|
-
`@req annotation references non-existent requirement: ${featureName}#${reqIndex} in ${filePath}`
|
|
1084
|
-
);
|
|
1085
|
-
continue;
|
|
1086
|
-
}
|
|
1087
|
-
const relPath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
1088
|
-
const fileNodeId = `file:${relPath}`;
|
|
1089
|
-
this.store.addEdge({
|
|
1090
|
-
from: reqNode.id,
|
|
1091
|
-
to: fileNodeId,
|
|
1092
|
-
type: "verified_by",
|
|
1093
|
-
confidence: 1,
|
|
1094
|
-
metadata: {
|
|
1095
|
-
method: "annotation",
|
|
1096
|
-
tag: `@req ${featureName}#${reqIndex}`,
|
|
1097
|
-
confidence: 1
|
|
1098
|
-
}
|
|
1099
|
-
});
|
|
1100
|
-
edgesAdded++;
|
|
1461
|
+
continue;
|
|
1101
1462
|
}
|
|
1463
|
+
const relPath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
1464
|
+
const fileNodeId = `file:${relPath}`;
|
|
1465
|
+
this.store.addEdge({
|
|
1466
|
+
from: reqNode.id,
|
|
1467
|
+
to: fileNodeId,
|
|
1468
|
+
type: "verified_by",
|
|
1469
|
+
confidence: 1,
|
|
1470
|
+
metadata: {
|
|
1471
|
+
method: "annotation",
|
|
1472
|
+
tag: `@req ${featureName}#${reqIndex}`,
|
|
1473
|
+
confidence: 1
|
|
1474
|
+
}
|
|
1475
|
+
});
|
|
1476
|
+
edgesAdded++;
|
|
1102
1477
|
}
|
|
1103
1478
|
return edgesAdded;
|
|
1104
1479
|
}
|
|
@@ -1502,7 +1877,7 @@ var KnowledgeIngestor = class {
|
|
|
1502
1877
|
const entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
1503
1878
|
for (const entry of entries) {
|
|
1504
1879
|
const fullPath = path3.join(dir, entry.name);
|
|
1505
|
-
if (entry.isDirectory()) {
|
|
1880
|
+
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "dist" && entry.name !== "target" && entry.name !== "build" && entry.name !== ".git" && entry.name !== ".gradle" && entry.name !== ".harness" && entry.name !== "vendor" && entry.name !== "bin" && entry.name !== "obj" && entry.name !== "venv" && entry.name !== "_build" && entry.name !== "deps" && entry.name !== "coverage") {
|
|
1506
1881
|
results.push(...await this.findMarkdownFiles(fullPath));
|
|
1507
1882
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1508
1883
|
results.push(fullPath);
|
|
@@ -1580,9 +1955,176 @@ function parseFailureSection(section) {
|
|
|
1580
1955
|
};
|
|
1581
1956
|
}
|
|
1582
1957
|
|
|
1583
|
-
// src/ingest/
|
|
1958
|
+
// src/ingest/BusinessKnowledgeIngestor.ts
|
|
1584
1959
|
var fs3 = __toESM(require("fs/promises"));
|
|
1585
1960
|
var path4 = __toESM(require("path"));
|
|
1961
|
+
var BUSINESS_KNOWLEDGE_TYPES = /* @__PURE__ */ new Set([
|
|
1962
|
+
"business_rule",
|
|
1963
|
+
"business_process",
|
|
1964
|
+
"business_concept",
|
|
1965
|
+
"business_term",
|
|
1966
|
+
"business_metric"
|
|
1967
|
+
]);
|
|
1968
|
+
var GOVERNS_SOURCE_TYPES = /* @__PURE__ */ new Set(["business_rule", "business_process"]);
|
|
1969
|
+
var CODE_NODE_TYPES2 = [
|
|
1970
|
+
"file",
|
|
1971
|
+
"function",
|
|
1972
|
+
"class",
|
|
1973
|
+
"method",
|
|
1974
|
+
"interface",
|
|
1975
|
+
"variable"
|
|
1976
|
+
];
|
|
1977
|
+
var MEASURABLE_TYPES = /* @__PURE__ */ new Set(["business_process", "business_concept"]);
|
|
1978
|
+
var BusinessKnowledgeIngestor = class {
|
|
1979
|
+
constructor(store) {
|
|
1980
|
+
this.store = store;
|
|
1981
|
+
}
|
|
1982
|
+
store;
|
|
1983
|
+
async ingest(knowledgeDir) {
|
|
1984
|
+
const start = Date.now();
|
|
1985
|
+
const errors = [];
|
|
1986
|
+
let files;
|
|
1987
|
+
try {
|
|
1988
|
+
files = await this.findMarkdownFiles(knowledgeDir);
|
|
1989
|
+
} catch {
|
|
1990
|
+
return emptyResult(Date.now() - start);
|
|
1991
|
+
}
|
|
1992
|
+
const nodeEntries = await this.createNodes(files, knowledgeDir, errors);
|
|
1993
|
+
const edgesAdded = this.createEdges(nodeEntries);
|
|
1994
|
+
return {
|
|
1995
|
+
nodesAdded: nodeEntries.length,
|
|
1996
|
+
nodesUpdated: 0,
|
|
1997
|
+
edgesAdded,
|
|
1998
|
+
edgesUpdated: 0,
|
|
1999
|
+
errors,
|
|
2000
|
+
durationMs: Date.now() - start
|
|
2001
|
+
};
|
|
2002
|
+
}
|
|
2003
|
+
async createNodes(files, knowledgeDir, errors) {
|
|
2004
|
+
const entries = [];
|
|
2005
|
+
for (const filePath of files) {
|
|
2006
|
+
try {
|
|
2007
|
+
const entry = await this.parseAndAddNode(filePath, knowledgeDir);
|
|
2008
|
+
if (entry) entries.push(entry);
|
|
2009
|
+
} catch (err) {
|
|
2010
|
+
errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
return entries;
|
|
2014
|
+
}
|
|
2015
|
+
async parseAndAddNode(filePath, knowledgeDir) {
|
|
2016
|
+
const raw = await fs3.readFile(filePath, "utf-8");
|
|
2017
|
+
const parsed = parseFrontmatter(raw);
|
|
2018
|
+
if (!parsed) return null;
|
|
2019
|
+
const { frontmatter, body } = parsed;
|
|
2020
|
+
if (!BUSINESS_KNOWLEDGE_TYPES.has(frontmatter.type)) return null;
|
|
2021
|
+
const relPath = path4.relative(knowledgeDir, filePath).replaceAll("\\", "/");
|
|
2022
|
+
const domain = frontmatter.domain ?? relPath.split("/")[0] ?? "unknown";
|
|
2023
|
+
const filename = path4.basename(filePath, ".md");
|
|
2024
|
+
const nodeId = `bk:${domain}:${filename}`;
|
|
2025
|
+
const titleMatch = body.match(/^#\s+(.+)$/m);
|
|
2026
|
+
const name = titleMatch ? titleMatch[1].trim() : filename;
|
|
2027
|
+
const node = {
|
|
2028
|
+
id: nodeId,
|
|
2029
|
+
type: frontmatter.type,
|
|
2030
|
+
name,
|
|
2031
|
+
path: relPath,
|
|
2032
|
+
content: body.trim(),
|
|
2033
|
+
metadata: {
|
|
2034
|
+
domain,
|
|
2035
|
+
...frontmatter.tags && { tags: frontmatter.tags },
|
|
2036
|
+
...frontmatter.related && { related: frontmatter.related }
|
|
2037
|
+
}
|
|
2038
|
+
};
|
|
2039
|
+
this.store.addNode(node);
|
|
2040
|
+
return { nodeId, node, content: body };
|
|
2041
|
+
}
|
|
2042
|
+
createEdges(nodeEntries) {
|
|
2043
|
+
let edgesAdded = 0;
|
|
2044
|
+
for (const { nodeId, node, content } of nodeEntries) {
|
|
2045
|
+
if (GOVERNS_SOURCE_TYPES.has(node.type)) {
|
|
2046
|
+
edgesAdded += this.linkToNodes(content, nodeId, "governs", CODE_NODE_TYPES2);
|
|
2047
|
+
} else {
|
|
2048
|
+
edgesAdded += this.linkToNodes(content, nodeId, "documents", CODE_NODE_TYPES2);
|
|
2049
|
+
}
|
|
2050
|
+
if (node.type === "business_metric") {
|
|
2051
|
+
edgesAdded += this.linkToBusinessNodes(content, nodeId, "measures", MEASURABLE_TYPES);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
return edgesAdded;
|
|
2055
|
+
}
|
|
2056
|
+
linkToNodes(content, sourceNodeId, edgeType, targetTypes) {
|
|
2057
|
+
let count = 0;
|
|
2058
|
+
for (const nodeType of targetTypes) {
|
|
2059
|
+
const nodes = this.store.findNodes({ type: nodeType });
|
|
2060
|
+
for (const node of nodes) {
|
|
2061
|
+
if (node.name.length < 3) continue;
|
|
2062
|
+
const escaped = node.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2063
|
+
const namePattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
2064
|
+
if (namePattern.test(content)) {
|
|
2065
|
+
this.store.addEdge({ from: sourceNodeId, to: node.id, type: edgeType });
|
|
2066
|
+
count++;
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
return count;
|
|
2071
|
+
}
|
|
2072
|
+
linkToBusinessNodes(content, sourceNodeId, edgeType, targetTypes) {
|
|
2073
|
+
let count = 0;
|
|
2074
|
+
for (const node of this.store.findNodes({})) {
|
|
2075
|
+
if (!targetTypes.has(node.type)) continue;
|
|
2076
|
+
if (node.name.length < 3) continue;
|
|
2077
|
+
const escaped = node.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2078
|
+
const namePattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
2079
|
+
if (namePattern.test(content)) {
|
|
2080
|
+
this.store.addEdge({ from: sourceNodeId, to: node.id, type: edgeType });
|
|
2081
|
+
count++;
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
return count;
|
|
2085
|
+
}
|
|
2086
|
+
async findMarkdownFiles(dir) {
|
|
2087
|
+
const results = [];
|
|
2088
|
+
const entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
2089
|
+
for (const entry of entries) {
|
|
2090
|
+
const fullPath = path4.join(dir, entry.name);
|
|
2091
|
+
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "dist" && entry.name !== "target" && entry.name !== "build" && entry.name !== ".git" && entry.name !== ".gradle" && entry.name !== ".harness" && entry.name !== "vendor" && entry.name !== "bin" && entry.name !== "obj" && entry.name !== "venv" && entry.name !== "_build" && entry.name !== "deps" && entry.name !== "coverage") {
|
|
2092
|
+
results.push(...await this.findMarkdownFiles(fullPath));
|
|
2093
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
2094
|
+
results.push(fullPath);
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
return results;
|
|
2098
|
+
}
|
|
2099
|
+
};
|
|
2100
|
+
function parseFrontmatter(raw) {
|
|
2101
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
2102
|
+
if (!match) return null;
|
|
2103
|
+
const yamlBlock = match[1];
|
|
2104
|
+
const body = match[2];
|
|
2105
|
+
const frontmatter = {};
|
|
2106
|
+
for (const line of yamlBlock.split("\n")) {
|
|
2107
|
+
const kvMatch = line.match(/^(\w+):\s*(.+)$/);
|
|
2108
|
+
if (!kvMatch) continue;
|
|
2109
|
+
const key = kvMatch[1];
|
|
2110
|
+
const value = kvMatch[2].trim();
|
|
2111
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
2112
|
+
frontmatter[key] = value.slice(1, -1).split(",").map((s) => s.trim());
|
|
2113
|
+
} else {
|
|
2114
|
+
frontmatter[key] = value;
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
if (!frontmatter.type || typeof frontmatter.type !== "string") return null;
|
|
2118
|
+
if (!frontmatter.domain || typeof frontmatter.domain !== "string") return null;
|
|
2119
|
+
return {
|
|
2120
|
+
frontmatter,
|
|
2121
|
+
body
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
// src/ingest/RequirementIngestor.ts
|
|
2126
|
+
var fs4 = __toESM(require("fs/promises"));
|
|
2127
|
+
var path5 = __toESM(require("path"));
|
|
1586
2128
|
var REQUIREMENT_SECTIONS = [
|
|
1587
2129
|
"Observable Truths",
|
|
1588
2130
|
"Success Criteria",
|
|
@@ -1599,7 +2141,7 @@ function detectEarsPattern(text) {
|
|
|
1599
2141
|
if (/^the\s+\w+\s+shall\b/.test(lower)) return "ubiquitous";
|
|
1600
2142
|
return void 0;
|
|
1601
2143
|
}
|
|
1602
|
-
var
|
|
2144
|
+
var CODE_NODE_TYPES3 = ["file", "function", "class", "method", "interface", "variable"];
|
|
1603
2145
|
var RequirementIngestor = class {
|
|
1604
2146
|
constructor(store) {
|
|
1605
2147
|
this.store = store;
|
|
@@ -1617,8 +2159,8 @@ var RequirementIngestor = class {
|
|
|
1617
2159
|
let edgesAdded = 0;
|
|
1618
2160
|
let featureDirs;
|
|
1619
2161
|
try {
|
|
1620
|
-
const entries = await
|
|
1621
|
-
featureDirs = entries.filter((e) => e.isDirectory()).map((e) =>
|
|
2162
|
+
const entries = await fs4.readdir(specsDir, { withFileTypes: true });
|
|
2163
|
+
featureDirs = entries.filter((e) => e.isDirectory()).map((e) => path5.join(specsDir, e.name));
|
|
1622
2164
|
} catch {
|
|
1623
2165
|
return emptyResult(Date.now() - start);
|
|
1624
2166
|
}
|
|
@@ -1637,11 +2179,11 @@ var RequirementIngestor = class {
|
|
|
1637
2179
|
};
|
|
1638
2180
|
}
|
|
1639
2181
|
async ingestFeatureDir(featureDir, errors) {
|
|
1640
|
-
const featureName =
|
|
1641
|
-
const specPath =
|
|
2182
|
+
const featureName = path5.basename(featureDir);
|
|
2183
|
+
const specPath = path5.join(featureDir, "proposal.md").replaceAll("\\", "/");
|
|
1642
2184
|
let content;
|
|
1643
2185
|
try {
|
|
1644
|
-
content = await
|
|
2186
|
+
content = await fs4.readFile(specPath, "utf-8");
|
|
1645
2187
|
} catch {
|
|
1646
2188
|
return { nodesAdded: 0, edgesAdded: 0 };
|
|
1647
2189
|
}
|
|
@@ -1658,7 +2200,7 @@ var RequirementIngestor = class {
|
|
|
1658
2200
|
this.store.addNode({
|
|
1659
2201
|
id: specNodeId,
|
|
1660
2202
|
type: "document",
|
|
1661
|
-
name:
|
|
2203
|
+
name: path5.basename(specPath),
|
|
1662
2204
|
path: specPath,
|
|
1663
2205
|
metadata: { featureName }
|
|
1664
2206
|
});
|
|
@@ -1773,9 +2315,9 @@ var RequirementIngestor = class {
|
|
|
1773
2315
|
for (const node of fileNodes) {
|
|
1774
2316
|
if (!node.path) continue;
|
|
1775
2317
|
const normalizedPath = node.path.replace(/\\/g, "/");
|
|
1776
|
-
const isCodeMatch = normalizedPath.includes("packages/") &&
|
|
2318
|
+
const isCodeMatch = normalizedPath.includes("packages/") && path5.basename(normalizedPath).includes(featureName);
|
|
1777
2319
|
const isTestMatch = normalizedPath.includes("/tests/") && // platform-safe
|
|
1778
|
-
|
|
2320
|
+
path5.basename(normalizedPath).includes(featureName);
|
|
1779
2321
|
if (isCodeMatch && !isTestMatch) {
|
|
1780
2322
|
this.store.addEdge({
|
|
1781
2323
|
from: reqId,
|
|
@@ -1796,39 +2338,2676 @@ var RequirementIngestor = class {
|
|
|
1796
2338
|
count++;
|
|
1797
2339
|
}
|
|
1798
2340
|
}
|
|
1799
|
-
return count;
|
|
2341
|
+
return count;
|
|
2342
|
+
}
|
|
2343
|
+
/**
|
|
2344
|
+
* Convention-based linking: match requirement text to code nodes
|
|
2345
|
+
* by keyword overlap (function/class names appearing in requirement text).
|
|
2346
|
+
*/
|
|
2347
|
+
linkByKeywordOverlap(reqId, reqText) {
|
|
2348
|
+
let count = 0;
|
|
2349
|
+
for (const nodeType of CODE_NODE_TYPES3) {
|
|
2350
|
+
const codeNodes = this.store.findNodes({ type: nodeType });
|
|
2351
|
+
for (const node of codeNodes) {
|
|
2352
|
+
if (node.name.length < 3) continue;
|
|
2353
|
+
const escaped = node.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2354
|
+
const namePattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
2355
|
+
if (namePattern.test(reqText)) {
|
|
2356
|
+
const edgeType = node.path?.replace(/\\/g, "/").includes("/tests/") ? "verified_by" : "requires";
|
|
2357
|
+
this.store.addEdge({
|
|
2358
|
+
from: reqId,
|
|
2359
|
+
to: node.id,
|
|
2360
|
+
type: edgeType,
|
|
2361
|
+
confidence: 0.6,
|
|
2362
|
+
metadata: { method: "convention", matchReason: "keyword-overlap" }
|
|
2363
|
+
});
|
|
2364
|
+
count++;
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
return count;
|
|
2369
|
+
}
|
|
2370
|
+
};
|
|
2371
|
+
|
|
2372
|
+
// src/ingest/KnowledgePipelineRunner.ts
|
|
2373
|
+
var fs9 = __toESM(require("fs/promises"));
|
|
2374
|
+
var path10 = __toESM(require("path"));
|
|
2375
|
+
|
|
2376
|
+
// src/ingest/DiagramParser.ts
|
|
2377
|
+
var fs5 = __toESM(require("fs/promises"));
|
|
2378
|
+
var path6 = __toESM(require("path"));
|
|
2379
|
+
function emptyMermaidResult(diagramType = "unknown") {
|
|
2380
|
+
return {
|
|
2381
|
+
entities: [],
|
|
2382
|
+
relationships: [],
|
|
2383
|
+
metadata: { format: "mermaid", diagramType }
|
|
2384
|
+
};
|
|
2385
|
+
}
|
|
2386
|
+
var MermaidParser = class {
|
|
2387
|
+
canParse(_content, ext) {
|
|
2388
|
+
return ext === ".mmd" || ext === ".mermaid";
|
|
2389
|
+
}
|
|
2390
|
+
parse(content, _filePath) {
|
|
2391
|
+
const trimmed = content.trim();
|
|
2392
|
+
if (!trimmed) {
|
|
2393
|
+
return emptyMermaidResult();
|
|
2394
|
+
}
|
|
2395
|
+
const diagramType = this.detectDiagramType(trimmed);
|
|
2396
|
+
switch (diagramType) {
|
|
2397
|
+
case "flowchart":
|
|
2398
|
+
return this.parseFlowchart(trimmed, diagramType);
|
|
2399
|
+
case "sequence":
|
|
2400
|
+
return this.parseSequence(trimmed, diagramType);
|
|
2401
|
+
default:
|
|
2402
|
+
return emptyMermaidResult(diagramType);
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
detectDiagramType(content) {
|
|
2406
|
+
const lines = content.split("\n");
|
|
2407
|
+
for (const line of lines) {
|
|
2408
|
+
const trimmedLine = line.trim();
|
|
2409
|
+
if (!trimmedLine) continue;
|
|
2410
|
+
if (/^(?:graph|flowchart)\b/i.test(trimmedLine)) return "flowchart";
|
|
2411
|
+
if (/^sequenceDiagram\b/.test(trimmedLine)) return "sequence";
|
|
2412
|
+
if (/^classDiagram\b/.test(trimmedLine)) return "class";
|
|
2413
|
+
if (/^erDiagram\b/.test(trimmedLine)) return "er";
|
|
2414
|
+
break;
|
|
2415
|
+
}
|
|
2416
|
+
return "unknown";
|
|
2417
|
+
}
|
|
2418
|
+
parseFlowchart(content, diagramType) {
|
|
2419
|
+
const entities = /* @__PURE__ */ new Map();
|
|
2420
|
+
const relationships = [];
|
|
2421
|
+
const nodeRegex = /([A-Za-z0-9_]+)\s*[[({]([^\])}]+)[\])}]/g;
|
|
2422
|
+
let match;
|
|
2423
|
+
match = nodeRegex.exec(content);
|
|
2424
|
+
while (match !== null) {
|
|
2425
|
+
const id = match[1] ?? "";
|
|
2426
|
+
const label = (match[2] ?? "").trim();
|
|
2427
|
+
if (id && !entities.has(id)) {
|
|
2428
|
+
const isDecision = this.isDecisionNode(content, id);
|
|
2429
|
+
entities.set(id, {
|
|
2430
|
+
id,
|
|
2431
|
+
label,
|
|
2432
|
+
...isDecision ? { type: "decision" } : {}
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
match = nodeRegex.exec(content);
|
|
2436
|
+
}
|
|
2437
|
+
const stripped = content.replace(/[[({][^\])}]*[\])}]/g, "");
|
|
2438
|
+
const labeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\|([^|]+)\|\s*([A-Za-z0-9_]+)/g;
|
|
2439
|
+
const unlabeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\s+([A-Za-z0-9_]+)/g;
|
|
2440
|
+
const edgeKeys = /* @__PURE__ */ new Set();
|
|
2441
|
+
match = labeledEdgeRegex.exec(stripped);
|
|
2442
|
+
while (match !== null) {
|
|
2443
|
+
const from = match[1] ?? "";
|
|
2444
|
+
const label = (match[2] ?? "").trim();
|
|
2445
|
+
const to = match[3] ?? "";
|
|
2446
|
+
if (from && to) {
|
|
2447
|
+
const key = `${from}->${to}:${label}`;
|
|
2448
|
+
if (!edgeKeys.has(key)) {
|
|
2449
|
+
edgeKeys.add(key);
|
|
2450
|
+
relationships.push({ from, to, label });
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
match = labeledEdgeRegex.exec(stripped);
|
|
2454
|
+
}
|
|
2455
|
+
match = unlabeledEdgeRegex.exec(stripped);
|
|
2456
|
+
while (match !== null) {
|
|
2457
|
+
const from = match[1] ?? "";
|
|
2458
|
+
const to = match[2] ?? "";
|
|
2459
|
+
if (from && to) {
|
|
2460
|
+
const hasLabeled = relationships.some((r) => r.from === from && r.to === to);
|
|
2461
|
+
if (!hasLabeled) {
|
|
2462
|
+
const key = `${from}->${to}`;
|
|
2463
|
+
if (!edgeKeys.has(key)) {
|
|
2464
|
+
edgeKeys.add(key);
|
|
2465
|
+
relationships.push({ from, to });
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
match = unlabeledEdgeRegex.exec(stripped);
|
|
2470
|
+
}
|
|
2471
|
+
return {
|
|
2472
|
+
entities: Array.from(entities.values()),
|
|
2473
|
+
relationships,
|
|
2474
|
+
metadata: { format: "mermaid", diagramType }
|
|
2475
|
+
};
|
|
2476
|
+
}
|
|
2477
|
+
isDecisionNode(content, nodeId) {
|
|
2478
|
+
const decisionRegex = new RegExp(`${nodeId}\\s*\\{`);
|
|
2479
|
+
return decisionRegex.test(content);
|
|
2480
|
+
}
|
|
2481
|
+
parseSequence(content, diagramType) {
|
|
2482
|
+
const entities = [];
|
|
2483
|
+
const relationships = [];
|
|
2484
|
+
const seenParticipants = /* @__PURE__ */ new Set();
|
|
2485
|
+
const participantRegex = /participant\s+(\w+)(?:\s+as\s+(.+))?/g;
|
|
2486
|
+
let match;
|
|
2487
|
+
match = participantRegex.exec(content);
|
|
2488
|
+
while (match !== null) {
|
|
2489
|
+
const id = match[1] ?? "";
|
|
2490
|
+
const label = match[2]?.trim() || id;
|
|
2491
|
+
if (id && !seenParticipants.has(id)) {
|
|
2492
|
+
seenParticipants.add(id);
|
|
2493
|
+
entities.push({ id, label });
|
|
2494
|
+
}
|
|
2495
|
+
match = participantRegex.exec(content);
|
|
2496
|
+
}
|
|
2497
|
+
const forwardMsgRegex = /(\w+)\s*->>?\+?\s*(\w+)\s*:\s*(.+)/g;
|
|
2498
|
+
match = forwardMsgRegex.exec(content);
|
|
2499
|
+
while (match !== null) {
|
|
2500
|
+
const from = match[1] ?? "";
|
|
2501
|
+
const to = match[2] ?? "";
|
|
2502
|
+
const label = (match[3] ?? "").trim();
|
|
2503
|
+
if (from && to) {
|
|
2504
|
+
relationships.push({ from, to, label });
|
|
2505
|
+
}
|
|
2506
|
+
match = forwardMsgRegex.exec(content);
|
|
2507
|
+
}
|
|
2508
|
+
const returnMsgRegex = /(\w+)\s*-->>?-?\s*(\w+)\s*:\s*(.+)/g;
|
|
2509
|
+
match = returnMsgRegex.exec(content);
|
|
2510
|
+
while (match !== null) {
|
|
2511
|
+
const from = match[1] ?? "";
|
|
2512
|
+
const to = match[2] ?? "";
|
|
2513
|
+
const label = (match[3] ?? "").trim();
|
|
2514
|
+
if (from && to) {
|
|
2515
|
+
relationships.push({ from, to, label });
|
|
2516
|
+
}
|
|
2517
|
+
match = returnMsgRegex.exec(content);
|
|
2518
|
+
}
|
|
2519
|
+
return {
|
|
2520
|
+
entities,
|
|
2521
|
+
relationships,
|
|
2522
|
+
metadata: { format: "mermaid", diagramType }
|
|
2523
|
+
};
|
|
2524
|
+
}
|
|
2525
|
+
};
|
|
2526
|
+
var D2Parser = class {
|
|
2527
|
+
canParse(_content, ext) {
|
|
2528
|
+
return ext === ".d2";
|
|
2529
|
+
}
|
|
2530
|
+
parse(content, _filePath) {
|
|
2531
|
+
const trimmed = content.trim();
|
|
2532
|
+
if (!trimmed) {
|
|
2533
|
+
return {
|
|
2534
|
+
entities: [],
|
|
2535
|
+
relationships: [],
|
|
2536
|
+
metadata: { format: "d2", diagramType: "architecture" }
|
|
2537
|
+
};
|
|
2538
|
+
}
|
|
2539
|
+
const entities = /* @__PURE__ */ new Map();
|
|
2540
|
+
const relationships = [];
|
|
2541
|
+
let braceDepth = 0;
|
|
2542
|
+
for (const line of trimmed.split("\n")) {
|
|
2543
|
+
const stripped = line.trim();
|
|
2544
|
+
if (!stripped || stripped.startsWith("#")) continue;
|
|
2545
|
+
if (stripped.endsWith("{")) {
|
|
2546
|
+
if (braceDepth === 0) {
|
|
2547
|
+
const shapeMatch = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+?)\s*\{$/);
|
|
2548
|
+
if (shapeMatch) {
|
|
2549
|
+
const id = shapeMatch[1];
|
|
2550
|
+
const label = shapeMatch[2];
|
|
2551
|
+
if (id && label) {
|
|
2552
|
+
entities.set(id, { id, label });
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
braceDepth++;
|
|
2557
|
+
continue;
|
|
2558
|
+
}
|
|
2559
|
+
if (stripped === "}") {
|
|
2560
|
+
braceDepth = Math.max(0, braceDepth - 1);
|
|
2561
|
+
continue;
|
|
2562
|
+
}
|
|
2563
|
+
if (braceDepth > 0) continue;
|
|
2564
|
+
const connMatch = stripped.match(
|
|
2565
|
+
/^([a-zA-Z0-9_.-]+)\s*->\s*([a-zA-Z0-9_.-]+)(?:\s*:\s*(.+?))?$/
|
|
2566
|
+
);
|
|
2567
|
+
if (connMatch) {
|
|
2568
|
+
const from = connMatch[1] ?? "";
|
|
2569
|
+
const to = connMatch[2] ?? "";
|
|
2570
|
+
const label = connMatch[3]?.trim();
|
|
2571
|
+
if (from && to) {
|
|
2572
|
+
relationships.push({ from, to, ...label ? { label } : {} });
|
|
2573
|
+
}
|
|
2574
|
+
continue;
|
|
2575
|
+
}
|
|
2576
|
+
const simpleMatch = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+)$/);
|
|
2577
|
+
if (simpleMatch) {
|
|
2578
|
+
const id = simpleMatch[1];
|
|
2579
|
+
const label = (simpleMatch[2] ?? "").trim();
|
|
2580
|
+
if (id && label && !entities.has(id)) {
|
|
2581
|
+
entities.set(id, { id, label });
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
return {
|
|
2586
|
+
entities: Array.from(entities.values()),
|
|
2587
|
+
relationships,
|
|
2588
|
+
metadata: { format: "d2", diagramType: "architecture" }
|
|
2589
|
+
};
|
|
2590
|
+
}
|
|
2591
|
+
};
|
|
2592
|
+
var PlantUmlParser = class {
|
|
2593
|
+
canParse(_content, ext) {
|
|
2594
|
+
return ext === ".puml" || ext === ".plantuml";
|
|
2595
|
+
}
|
|
2596
|
+
parse(content, _filePath) {
|
|
2597
|
+
const trimmed = content.trim();
|
|
2598
|
+
if (!trimmed) {
|
|
2599
|
+
return {
|
|
2600
|
+
entities: [],
|
|
2601
|
+
relationships: [],
|
|
2602
|
+
metadata: { format: "plantuml", diagramType: "unknown" }
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2605
|
+
const diagramType = this.detectDiagramType(trimmed);
|
|
2606
|
+
const entities = /* @__PURE__ */ new Map();
|
|
2607
|
+
const relationships = [];
|
|
2608
|
+
const body = trimmed.replace(/@startuml\b.*\n?/, "").replace(/@enduml\b.*/, "").trim();
|
|
2609
|
+
const classRegex = /class\s+(\w+)/g;
|
|
2610
|
+
let match;
|
|
2611
|
+
match = classRegex.exec(body);
|
|
2612
|
+
while (match !== null) {
|
|
2613
|
+
const id = match[1] ?? "";
|
|
2614
|
+
if (id && !entities.has(id)) {
|
|
2615
|
+
entities.set(id, { id, label: id });
|
|
2616
|
+
}
|
|
2617
|
+
match = classRegex.exec(body);
|
|
2618
|
+
}
|
|
2619
|
+
const componentRegex1 = /\[([^\]]+)\]/g;
|
|
2620
|
+
match = componentRegex1.exec(body);
|
|
2621
|
+
while (match !== null) {
|
|
2622
|
+
const label = (match[1] ?? "").trim();
|
|
2623
|
+
if (label) {
|
|
2624
|
+
const id = label.replace(/\s+/g, "_");
|
|
2625
|
+
if (!entities.has(id)) {
|
|
2626
|
+
entities.set(id, { id, label });
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
match = componentRegex1.exec(body);
|
|
2630
|
+
}
|
|
2631
|
+
const relRegex = /(\w+)\s*(?:-->|->|<--|<-|\.\.>|--)\s*(\w+)(?:\s*:\s*(.+))?/g;
|
|
2632
|
+
match = relRegex.exec(body);
|
|
2633
|
+
while (match !== null) {
|
|
2634
|
+
const from = match[1] ?? "";
|
|
2635
|
+
const to = match[2] ?? "";
|
|
2636
|
+
const label = match[3]?.trim();
|
|
2637
|
+
if (from && to) {
|
|
2638
|
+
relationships.push({ from, to, ...label ? { label } : {} });
|
|
2639
|
+
}
|
|
2640
|
+
match = relRegex.exec(body);
|
|
2641
|
+
}
|
|
2642
|
+
return {
|
|
2643
|
+
entities: Array.from(entities.values()),
|
|
2644
|
+
relationships,
|
|
2645
|
+
metadata: { format: "plantuml", diagramType }
|
|
2646
|
+
};
|
|
2647
|
+
}
|
|
2648
|
+
detectDiagramType(content) {
|
|
2649
|
+
if (/class\s+\w+/.test(content)) return "class";
|
|
2650
|
+
if (/\[.+\]/.test(content) || /component\s+/.test(content)) return "component";
|
|
2651
|
+
if (/participant\s+/.test(content) || /actor\s+/.test(content)) return "sequence";
|
|
2652
|
+
return "unknown";
|
|
2653
|
+
}
|
|
2654
|
+
};
|
|
2655
|
+
var DIAGRAM_EXTENSIONS = /* @__PURE__ */ new Set([".mmd", ".mermaid", ".d2", ".puml", ".plantuml"]);
|
|
2656
|
+
var DiagramParser = class {
|
|
2657
|
+
constructor(store) {
|
|
2658
|
+
this.store = store;
|
|
2659
|
+
}
|
|
2660
|
+
store;
|
|
2661
|
+
parsers = [
|
|
2662
|
+
new MermaidParser(),
|
|
2663
|
+
new D2Parser(),
|
|
2664
|
+
new PlantUmlParser()
|
|
2665
|
+
];
|
|
2666
|
+
parse(content, filePath) {
|
|
2667
|
+
const ext = path6.extname(filePath).toLowerCase();
|
|
2668
|
+
for (const parser of this.parsers) {
|
|
2669
|
+
if (parser.canParse(content, ext)) {
|
|
2670
|
+
return parser.parse(content, filePath);
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
return {
|
|
2674
|
+
entities: [],
|
|
2675
|
+
relationships: [],
|
|
2676
|
+
metadata: { format: "mermaid", diagramType: "unknown" }
|
|
2677
|
+
};
|
|
2678
|
+
}
|
|
2679
|
+
async ingest(projectDir) {
|
|
2680
|
+
const start = Date.now();
|
|
2681
|
+
let nodesAdded = 0;
|
|
2682
|
+
let edgesAdded = 0;
|
|
2683
|
+
const errors = [];
|
|
2684
|
+
const diagramFiles = await this.findDiagramFiles(projectDir);
|
|
2685
|
+
for (const filePath of diagramFiles) {
|
|
2686
|
+
try {
|
|
2687
|
+
const content = await fs5.readFile(filePath, "utf-8");
|
|
2688
|
+
const relPath = path6.relative(projectDir, filePath).replaceAll("\\", "/");
|
|
2689
|
+
const result = this.parse(content, filePath);
|
|
2690
|
+
if (result.entities.length === 0) continue;
|
|
2691
|
+
const pathHash = hash(relPath);
|
|
2692
|
+
for (const entity of result.entities) {
|
|
2693
|
+
const nodeId = `diagram:${pathHash}:${entity.id}`;
|
|
2694
|
+
this.store.addNode({
|
|
2695
|
+
id: nodeId,
|
|
2696
|
+
type: "business_concept",
|
|
2697
|
+
name: entity.label,
|
|
2698
|
+
path: relPath,
|
|
2699
|
+
metadata: {
|
|
2700
|
+
source: "diagram",
|
|
2701
|
+
format: result.metadata.format,
|
|
2702
|
+
diagramType: result.metadata.diagramType,
|
|
2703
|
+
confidence: 0.85,
|
|
2704
|
+
...entity.type ? { entityType: entity.type } : {}
|
|
2705
|
+
}
|
|
2706
|
+
});
|
|
2707
|
+
nodesAdded++;
|
|
2708
|
+
}
|
|
2709
|
+
for (const rel of result.relationships) {
|
|
2710
|
+
const fromId = `diagram:${pathHash}:${rel.from}`;
|
|
2711
|
+
const toId = `diagram:${pathHash}:${rel.to}`;
|
|
2712
|
+
this.store.addEdge({
|
|
2713
|
+
from: fromId,
|
|
2714
|
+
to: toId,
|
|
2715
|
+
type: "references",
|
|
2716
|
+
metadata: {
|
|
2717
|
+
...rel.label ? { label: rel.label } : {}
|
|
2718
|
+
}
|
|
2719
|
+
});
|
|
2720
|
+
edgesAdded++;
|
|
2721
|
+
}
|
|
2722
|
+
} catch (err) {
|
|
2723
|
+
errors.push(
|
|
2724
|
+
`Failed to parse ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
2725
|
+
);
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
return {
|
|
2729
|
+
nodesAdded,
|
|
2730
|
+
nodesUpdated: 0,
|
|
2731
|
+
edgesAdded,
|
|
2732
|
+
edgesUpdated: 0,
|
|
2733
|
+
errors,
|
|
2734
|
+
durationMs: Date.now() - start
|
|
2735
|
+
};
|
|
2736
|
+
}
|
|
2737
|
+
async findDiagramFiles(dir) {
|
|
2738
|
+
const files = [];
|
|
2739
|
+
const SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".harness"]);
|
|
2740
|
+
const walk = async (currentDir) => {
|
|
2741
|
+
let entries;
|
|
2742
|
+
try {
|
|
2743
|
+
entries = await fs5.readdir(currentDir, { withFileTypes: true });
|
|
2744
|
+
} catch {
|
|
2745
|
+
return;
|
|
2746
|
+
}
|
|
2747
|
+
for (const entry of entries) {
|
|
2748
|
+
if (entry.isDirectory()) {
|
|
2749
|
+
if (!SKIP_DIRS2.has(entry.name)) {
|
|
2750
|
+
await walk(path6.join(currentDir, entry.name));
|
|
2751
|
+
}
|
|
2752
|
+
} else if (entry.isFile()) {
|
|
2753
|
+
const ext = path6.extname(entry.name).toLowerCase();
|
|
2754
|
+
if (DIAGRAM_EXTENSIONS.has(ext)) {
|
|
2755
|
+
files.push(path6.join(currentDir, entry.name));
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
};
|
|
2760
|
+
await walk(dir);
|
|
2761
|
+
return files;
|
|
2762
|
+
}
|
|
2763
|
+
};
|
|
2764
|
+
|
|
2765
|
+
// src/ingest/KnowledgeLinker.ts
|
|
2766
|
+
var fs6 = __toESM(require("fs/promises"));
|
|
2767
|
+
var path7 = __toESM(require("path"));
|
|
2768
|
+
var HEURISTIC_PATTERNS = [
|
|
2769
|
+
{
|
|
2770
|
+
name: "business-rule-imperative",
|
|
2771
|
+
pattern: /\b(?:must|shall|required)\b.*\b(?:system|service|api|user|data|app|application|client|server|process|handler|module)\b/i,
|
|
2772
|
+
signal: "Business rule",
|
|
2773
|
+
confidence: 0.7,
|
|
2774
|
+
nodeType: "business_fact"
|
|
2775
|
+
},
|
|
2776
|
+
{
|
|
2777
|
+
name: "sla-slo-pattern",
|
|
2778
|
+
pattern: /(?:\b\d+(?:\.\d+)?%\s*(?:availability|uptime|success\s*rate)|\bunder\s+\d+\s*(?:ms|seconds?|minutes?|hours?)\b|\b(?:SLA|SLO)\b|\b99(?:\.\d+)?%\b)/i,
|
|
2779
|
+
signal: "Business constraint (SLA/SLO)",
|
|
2780
|
+
confidence: 0.8,
|
|
2781
|
+
nodeType: "business_fact"
|
|
2782
|
+
},
|
|
2783
|
+
{
|
|
2784
|
+
name: "monetary-amount",
|
|
2785
|
+
pattern: /\$[\d,]+(?:\.\d{2})?\s*(?:\b(?:cost|revenue|budget|price|fee|license|subscription|annual|monthly)\b)?/i,
|
|
2786
|
+
signal: "Business fact (monetary)",
|
|
2787
|
+
confidence: 0.6,
|
|
2788
|
+
nodeType: "business_fact"
|
|
2789
|
+
},
|
|
2790
|
+
{
|
|
2791
|
+
name: "acceptance-criteria",
|
|
2792
|
+
pattern: /\b(?:Given\b.*\bWhen\b.*\bThen\b|\[[ x]\])/i,
|
|
2793
|
+
signal: "Business rule (acceptance criteria)",
|
|
2794
|
+
confidence: 0.8,
|
|
2795
|
+
nodeType: "business_fact"
|
|
2796
|
+
},
|
|
2797
|
+
{
|
|
2798
|
+
name: "regulatory-reference",
|
|
2799
|
+
pattern: /\b(?:GDPR|SOC\s*2|PCI(?:\s*-?\s*DSS)?|HIPAA|CCPA|FERPA|SOX)\b/i,
|
|
2800
|
+
signal: "Business rule (regulatory)",
|
|
2801
|
+
confidence: 0.9,
|
|
2802
|
+
nodeType: "business_fact"
|
|
2803
|
+
}
|
|
2804
|
+
];
|
|
2805
|
+
var SCANNABLE_TYPES = ["issue", "conversation", "document"];
|
|
2806
|
+
function applyReactionBoost(candidate, nodeType, metadata) {
|
|
2807
|
+
if (nodeType !== "conversation" || !metadata.reactions) return;
|
|
2808
|
+
const reactions = metadata.reactions;
|
|
2809
|
+
const totalReactions = Object.values(reactions).reduce((sum, count) => sum + count, 0);
|
|
2810
|
+
if (totalReactions > 0) {
|
|
2811
|
+
candidate.confidence = Math.min(1, Math.round((candidate.confidence + 0.1) * 100) / 100);
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
var KnowledgeLinker = class {
|
|
2815
|
+
constructor(store, outputDir) {
|
|
2816
|
+
this.store = store;
|
|
2817
|
+
this.outputDir = outputDir;
|
|
2818
|
+
}
|
|
2819
|
+
store;
|
|
2820
|
+
outputDir;
|
|
2821
|
+
async link() {
|
|
2822
|
+
const errors = [];
|
|
2823
|
+
const candidates = this.scanAllNodes(errors);
|
|
2824
|
+
await this.writeJsonl(candidates);
|
|
2825
|
+
const conceptsClustered = this.clusterBySource(candidates);
|
|
2826
|
+
const promoted = this.promoteAndDeduplicate(candidates);
|
|
2827
|
+
await this.writeStagedJsonl(promoted.staged);
|
|
2828
|
+
return {
|
|
2829
|
+
factsCreated: promoted.factsCreated,
|
|
2830
|
+
conceptsClustered,
|
|
2831
|
+
duplicatesMerged: promoted.duplicatesMerged,
|
|
2832
|
+
stagedForReview: promoted.staged.length,
|
|
2833
|
+
errors
|
|
2834
|
+
};
|
|
2835
|
+
}
|
|
2836
|
+
/** Stage 1: Scan all scannable node types for heuristic pattern matches. */
|
|
2837
|
+
scanAllNodes(errors) {
|
|
2838
|
+
const candidates = [];
|
|
2839
|
+
for (const type of SCANNABLE_TYPES) {
|
|
2840
|
+
const nodes = this.store.findNodes({ type });
|
|
2841
|
+
for (const node of nodes) {
|
|
2842
|
+
if (!node.content) continue;
|
|
2843
|
+
try {
|
|
2844
|
+
const matches = this.scanPatterns(node.content, node.id, type);
|
|
2845
|
+
for (const match of matches) {
|
|
2846
|
+
applyReactionBoost(match, type, node.metadata);
|
|
2847
|
+
candidates.push(match);
|
|
2848
|
+
}
|
|
2849
|
+
} catch (err) {
|
|
2850
|
+
errors.push(
|
|
2851
|
+
`Scan failed for ${node.id}: ${err instanceof Error ? err.message : String(err)}`
|
|
2852
|
+
);
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
return candidates;
|
|
2857
|
+
}
|
|
2858
|
+
/** Stage 2: Group candidates by source node and create business_concept for 3+. */
|
|
2859
|
+
clusterBySource(candidates) {
|
|
2860
|
+
const sourceGroups = /* @__PURE__ */ new Map();
|
|
2861
|
+
for (const candidate of candidates) {
|
|
2862
|
+
const group = sourceGroups.get(candidate.sourceNodeId) ?? [];
|
|
2863
|
+
group.push(candidate);
|
|
2864
|
+
sourceGroups.set(candidate.sourceNodeId, group);
|
|
2865
|
+
}
|
|
2866
|
+
let clustered = 0;
|
|
2867
|
+
for (const [sourceNodeId, group] of sourceGroups) {
|
|
2868
|
+
if (group.length >= 3) {
|
|
2869
|
+
this.store.addNode({
|
|
2870
|
+
id: `concept:linker:${hash(sourceNodeId)}`,
|
|
2871
|
+
type: "business_concept",
|
|
2872
|
+
name: `Business concept cluster from ${sourceNodeId}`,
|
|
2873
|
+
metadata: {
|
|
2874
|
+
source: "knowledge-linker",
|
|
2875
|
+
sources: [sourceNodeId],
|
|
2876
|
+
factCount: group.length
|
|
2877
|
+
}
|
|
2878
|
+
});
|
|
2879
|
+
clustered++;
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
return clustered;
|
|
2883
|
+
}
|
|
2884
|
+
/** Stage 3: Promote high-confidence candidates, stage medium, discard low. */
|
|
2885
|
+
promoteAndDeduplicate(candidates) {
|
|
2886
|
+
let factsCreated = 0;
|
|
2887
|
+
let duplicatesMerged = 0;
|
|
2888
|
+
const staged = [];
|
|
2889
|
+
for (const candidate of candidates) {
|
|
2890
|
+
if (candidate.confidence >= 0.8) {
|
|
2891
|
+
const merged = this.tryMergeDuplicate(candidate);
|
|
2892
|
+
if (merged) {
|
|
2893
|
+
duplicatesMerged++;
|
|
2894
|
+
continue;
|
|
2895
|
+
}
|
|
2896
|
+
this.createFactNode(candidate);
|
|
2897
|
+
factsCreated++;
|
|
2898
|
+
} else if (candidate.confidence >= 0.5) {
|
|
2899
|
+
staged.push(candidate);
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
return { factsCreated, duplicatesMerged, staged };
|
|
2903
|
+
}
|
|
2904
|
+
/** Check for existing duplicate fact and merge sources if found. Returns true if merged. */
|
|
2905
|
+
tryMergeDuplicate(candidate) {
|
|
2906
|
+
if (this.store.getNode(candidate.id)) return true;
|
|
2907
|
+
const existingFacts = this.store.findNodes({ type: "business_fact" });
|
|
2908
|
+
const duplicate = existingFacts.find(
|
|
2909
|
+
(f) => f.content === candidate.content && f.id !== candidate.id
|
|
2910
|
+
);
|
|
2911
|
+
if (!duplicate) return false;
|
|
2912
|
+
const existingSources = duplicate.metadata.sources ?? [];
|
|
2913
|
+
if (!existingSources.includes(candidate.sourceNodeId)) {
|
|
2914
|
+
existingSources.push(candidate.sourceNodeId);
|
|
2915
|
+
duplicate.metadata.sources = existingSources;
|
|
2916
|
+
}
|
|
2917
|
+
return true;
|
|
2918
|
+
}
|
|
2919
|
+
/** Create a business_fact node and governs edge for a promoted candidate. */
|
|
2920
|
+
createFactNode(candidate) {
|
|
2921
|
+
this.store.addNode({
|
|
2922
|
+
id: candidate.id,
|
|
2923
|
+
type: candidate.nodeType,
|
|
2924
|
+
name: candidate.name,
|
|
2925
|
+
content: candidate.content,
|
|
2926
|
+
metadata: {
|
|
2927
|
+
source: "knowledge-linker",
|
|
2928
|
+
pattern: candidate.pattern,
|
|
2929
|
+
signal: candidate.signal,
|
|
2930
|
+
confidence: candidate.confidence,
|
|
2931
|
+
sourceNodeId: candidate.sourceNodeId,
|
|
2932
|
+
sourceNodeType: candidate.sourceNodeType,
|
|
2933
|
+
sources: [candidate.sourceNodeId]
|
|
2934
|
+
}
|
|
2935
|
+
});
|
|
2936
|
+
this.store.addEdge({
|
|
2937
|
+
from: candidate.id,
|
|
2938
|
+
to: candidate.sourceNodeId,
|
|
2939
|
+
type: "governs",
|
|
2940
|
+
confidence: candidate.confidence,
|
|
2941
|
+
metadata: { source: "knowledge-linker" }
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
/**
|
|
2945
|
+
* Write candidates to JSONL file for audit trail.
|
|
2946
|
+
*/
|
|
2947
|
+
async writeJsonl(candidates) {
|
|
2948
|
+
if (!this.outputDir) return;
|
|
2949
|
+
await fs6.mkdir(this.outputDir, { recursive: true });
|
|
2950
|
+
const filePath = path7.join(this.outputDir, "linker.jsonl");
|
|
2951
|
+
const lines = candidates.map(
|
|
2952
|
+
(c) => JSON.stringify({
|
|
2953
|
+
id: c.id,
|
|
2954
|
+
sourceNodeId: c.sourceNodeId,
|
|
2955
|
+
sourceNodeType: c.sourceNodeType,
|
|
2956
|
+
name: c.name,
|
|
2957
|
+
confidence: c.confidence,
|
|
2958
|
+
pattern: c.pattern,
|
|
2959
|
+
signal: c.signal,
|
|
2960
|
+
nodeType: c.nodeType
|
|
2961
|
+
})
|
|
2962
|
+
);
|
|
2963
|
+
await fs6.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
|
|
2964
|
+
}
|
|
2965
|
+
/**
|
|
2966
|
+
* Write medium-confidence candidates to staged JSONL for human review.
|
|
2967
|
+
*/
|
|
2968
|
+
async writeStagedJsonl(candidates) {
|
|
2969
|
+
if (!this.outputDir || candidates.length === 0) return;
|
|
2970
|
+
const stagedDir = path7.join(this.outputDir, "staged");
|
|
2971
|
+
await fs6.mkdir(stagedDir, { recursive: true });
|
|
2972
|
+
const filePath = path7.join(stagedDir, "linker-staged.jsonl");
|
|
2973
|
+
const lines = candidates.map(
|
|
2974
|
+
(c) => JSON.stringify({
|
|
2975
|
+
id: c.id,
|
|
2976
|
+
sourceNodeId: c.sourceNodeId,
|
|
2977
|
+
sourceNodeType: c.sourceNodeType,
|
|
2978
|
+
name: c.name,
|
|
2979
|
+
confidence: c.confidence,
|
|
2980
|
+
pattern: c.pattern,
|
|
2981
|
+
signal: c.signal,
|
|
2982
|
+
nodeType: c.nodeType
|
|
2983
|
+
})
|
|
2984
|
+
);
|
|
2985
|
+
await fs6.writeFile(filePath, lines.join("\n") + "\n");
|
|
2986
|
+
}
|
|
2987
|
+
/**
|
|
2988
|
+
* Apply heuristic patterns to content and return candidate extractions.
|
|
2989
|
+
*/
|
|
2990
|
+
scanPatterns(content, nodeId, nodeType) {
|
|
2991
|
+
const candidates = [];
|
|
2992
|
+
for (const heuristic of HEURISTIC_PATTERNS) {
|
|
2993
|
+
if (heuristic.pattern.test(content)) {
|
|
2994
|
+
const candidateId = `extracted:linker:${hash(nodeId + ":" + heuristic.name)}`;
|
|
2995
|
+
candidates.push({
|
|
2996
|
+
id: candidateId,
|
|
2997
|
+
sourceNodeId: nodeId,
|
|
2998
|
+
sourceNodeType: nodeType,
|
|
2999
|
+
name: `${heuristic.signal} from ${nodeId}`,
|
|
3000
|
+
content,
|
|
3001
|
+
confidence: heuristic.confidence,
|
|
3002
|
+
pattern: heuristic.name,
|
|
3003
|
+
signal: heuristic.signal,
|
|
3004
|
+
nodeType: heuristic.nodeType
|
|
3005
|
+
});
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
return candidates;
|
|
3009
|
+
}
|
|
3010
|
+
};
|
|
3011
|
+
|
|
3012
|
+
// src/ingest/StructuralDriftDetector.ts
|
|
3013
|
+
var StructuralDriftDetector = class {
|
|
3014
|
+
detect(current, fresh) {
|
|
3015
|
+
const findings = [];
|
|
3016
|
+
const currentById = new Map(current.entries.map((e) => [e.id, e]));
|
|
3017
|
+
const freshById = new Map(fresh.entries.map((e) => [e.id, e]));
|
|
3018
|
+
for (const [id, entry] of freshById) {
|
|
3019
|
+
if (!currentById.has(id)) {
|
|
3020
|
+
findings.push({
|
|
3021
|
+
entryId: id,
|
|
3022
|
+
classification: "new",
|
|
3023
|
+
fresh: entry,
|
|
3024
|
+
severity: "low"
|
|
3025
|
+
});
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
for (const [id, entry] of currentById) {
|
|
3029
|
+
if (!freshById.has(id)) {
|
|
3030
|
+
findings.push({
|
|
3031
|
+
entryId: id,
|
|
3032
|
+
classification: "stale",
|
|
3033
|
+
current: entry,
|
|
3034
|
+
severity: "high"
|
|
3035
|
+
});
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
for (const [id, freshEntry] of freshById) {
|
|
3039
|
+
const currentEntry = currentById.get(id);
|
|
3040
|
+
if (currentEntry && currentEntry.contentHash !== freshEntry.contentHash) {
|
|
3041
|
+
findings.push({
|
|
3042
|
+
entryId: id,
|
|
3043
|
+
classification: "drifted",
|
|
3044
|
+
current: currentEntry,
|
|
3045
|
+
fresh: freshEntry,
|
|
3046
|
+
severity: "medium"
|
|
3047
|
+
});
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
const byName = /* @__PURE__ */ new Map();
|
|
3051
|
+
for (const entry of fresh.entries) {
|
|
3052
|
+
const group = byName.get(entry.name) ?? [];
|
|
3053
|
+
group.push(entry);
|
|
3054
|
+
byName.set(entry.name, group);
|
|
3055
|
+
}
|
|
3056
|
+
for (const [, group] of byName) {
|
|
3057
|
+
if (group.length > 1) {
|
|
3058
|
+
const sources = new Set(group.map((e) => e.source));
|
|
3059
|
+
const hashes = new Set(group.map((e) => e.contentHash));
|
|
3060
|
+
if (sources.size > 1 && hashes.size > 1) {
|
|
3061
|
+
const alreadyContradicting = new Set(
|
|
3062
|
+
findings.filter((f) => f.classification === "contradicting").map((f) => f.entryId)
|
|
3063
|
+
);
|
|
3064
|
+
for (const entry of group) {
|
|
3065
|
+
if (!alreadyContradicting.has(entry.id)) {
|
|
3066
|
+
findings.push({
|
|
3067
|
+
entryId: entry.id,
|
|
3068
|
+
classification: "contradicting",
|
|
3069
|
+
fresh: entry,
|
|
3070
|
+
severity: "critical"
|
|
3071
|
+
});
|
|
3072
|
+
break;
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
const totalEntries = (/* @__PURE__ */ new Set([...currentById.keys(), ...freshById.keys()])).size;
|
|
3079
|
+
const driftScore = totalEntries > 0 ? findings.length / totalEntries : 0;
|
|
3080
|
+
const summary = {
|
|
3081
|
+
new: findings.filter((f) => f.classification === "new").length,
|
|
3082
|
+
drifted: findings.filter((f) => f.classification === "drifted").length,
|
|
3083
|
+
stale: findings.filter((f) => f.classification === "stale").length,
|
|
3084
|
+
contradicting: findings.filter((f) => f.classification === "contradicting").length
|
|
3085
|
+
};
|
|
3086
|
+
return { findings, driftScore, summary };
|
|
3087
|
+
}
|
|
3088
|
+
};
|
|
3089
|
+
|
|
3090
|
+
// src/ingest/KnowledgeStagingAggregator.ts
|
|
3091
|
+
var fs7 = __toESM(require("fs/promises"));
|
|
3092
|
+
var path8 = __toESM(require("path"));
|
|
3093
|
+
var KnowledgeStagingAggregator = class {
|
|
3094
|
+
constructor(projectDir) {
|
|
3095
|
+
this.projectDir = projectDir;
|
|
3096
|
+
}
|
|
3097
|
+
projectDir;
|
|
3098
|
+
async aggregate(extractorResults, linkerResults, diagramResults) {
|
|
3099
|
+
const all = [...extractorResults, ...linkerResults, ...diagramResults];
|
|
3100
|
+
const byHash = /* @__PURE__ */ new Map();
|
|
3101
|
+
for (const entry of all) {
|
|
3102
|
+
const existing = byHash.get(entry.contentHash);
|
|
3103
|
+
if (!existing || entry.confidence > existing.confidence) {
|
|
3104
|
+
byHash.set(entry.contentHash, entry);
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
const deduplicated = Array.from(byHash.values());
|
|
3108
|
+
if (deduplicated.length === 0) {
|
|
3109
|
+
return { staged: 0 };
|
|
3110
|
+
}
|
|
3111
|
+
const stagedDir = path8.join(this.projectDir, ".harness", "knowledge", "staged");
|
|
3112
|
+
await fs7.mkdir(stagedDir, { recursive: true });
|
|
3113
|
+
const jsonl = deduplicated.map((entry) => JSON.stringify(entry)).join("\n") + "\n";
|
|
3114
|
+
await fs7.writeFile(path8.join(stagedDir, "pipeline-staged.jsonl"), jsonl, "utf-8");
|
|
3115
|
+
return { staged: deduplicated.length };
|
|
3116
|
+
}
|
|
3117
|
+
async generateGapReport(knowledgeDir) {
|
|
3118
|
+
const domains = [];
|
|
3119
|
+
let totalEntries = 0;
|
|
3120
|
+
try {
|
|
3121
|
+
const entries = await fs7.readdir(knowledgeDir, { withFileTypes: true });
|
|
3122
|
+
const domainDirs = entries.filter((e) => e.isDirectory());
|
|
3123
|
+
for (const dir of domainDirs) {
|
|
3124
|
+
const domainPath = path8.join(knowledgeDir, dir.name);
|
|
3125
|
+
const files = await fs7.readdir(domainPath);
|
|
3126
|
+
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
3127
|
+
const entryCount = mdFiles.length;
|
|
3128
|
+
totalEntries += entryCount;
|
|
3129
|
+
domains.push({ domain: dir.name, entryCount });
|
|
3130
|
+
}
|
|
3131
|
+
} catch {
|
|
3132
|
+
}
|
|
3133
|
+
return {
|
|
3134
|
+
domains,
|
|
3135
|
+
totalEntries,
|
|
3136
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3137
|
+
};
|
|
3138
|
+
}
|
|
3139
|
+
async writeGapReport(report) {
|
|
3140
|
+
const gapsDir = path8.join(this.projectDir, ".harness", "knowledge");
|
|
3141
|
+
await fs7.mkdir(gapsDir, { recursive: true });
|
|
3142
|
+
const lines = [
|
|
3143
|
+
"# Knowledge Gaps Report",
|
|
3144
|
+
"",
|
|
3145
|
+
`Generated: ${report.generatedAt}`,
|
|
3146
|
+
"",
|
|
3147
|
+
"## Coverage by Domain",
|
|
3148
|
+
"",
|
|
3149
|
+
"| Domain | Entries |",
|
|
3150
|
+
"| ------ | ------- |"
|
|
3151
|
+
];
|
|
3152
|
+
for (const domain of report.domains) {
|
|
3153
|
+
lines.push(`| ${domain.domain} | ${domain.entryCount} |`);
|
|
3154
|
+
}
|
|
3155
|
+
lines.push("", `## Total Entries: ${report.totalEntries}`, "");
|
|
3156
|
+
await fs7.writeFile(path8.join(gapsDir, "gaps.md"), lines.join("\n"), "utf-8");
|
|
3157
|
+
}
|
|
3158
|
+
};
|
|
3159
|
+
|
|
3160
|
+
// src/ingest/extractors/ExtractionRunner.ts
|
|
3161
|
+
var fs8 = __toESM(require("fs/promises"));
|
|
3162
|
+
var path9 = __toESM(require("path"));
|
|
3163
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
3164
|
+
"node_modules",
|
|
3165
|
+
"dist",
|
|
3166
|
+
"target",
|
|
3167
|
+
"build",
|
|
3168
|
+
".git",
|
|
3169
|
+
"__pycache__",
|
|
3170
|
+
".venv",
|
|
3171
|
+
".turbo",
|
|
3172
|
+
".harness",
|
|
3173
|
+
".next",
|
|
3174
|
+
".vscode",
|
|
3175
|
+
".idea",
|
|
3176
|
+
"vendor",
|
|
3177
|
+
"out",
|
|
3178
|
+
".gradle",
|
|
3179
|
+
".gradle-home",
|
|
3180
|
+
"bin",
|
|
3181
|
+
"obj",
|
|
3182
|
+
"venv",
|
|
3183
|
+
"_build",
|
|
3184
|
+
"deps",
|
|
3185
|
+
"coverage",
|
|
3186
|
+
".nyc_output"
|
|
3187
|
+
]);
|
|
3188
|
+
var EXT_TO_LANGUAGE = {
|
|
3189
|
+
".ts": "typescript",
|
|
3190
|
+
".tsx": "typescript",
|
|
3191
|
+
".js": "javascript",
|
|
3192
|
+
".jsx": "javascript",
|
|
3193
|
+
".py": "python",
|
|
3194
|
+
".go": "go",
|
|
3195
|
+
".rs": "rust",
|
|
3196
|
+
".java": "java"
|
|
3197
|
+
};
|
|
3198
|
+
var SKIP_EXTENSIONS2 = /* @__PURE__ */ new Set([".d.ts"]);
|
|
3199
|
+
function detectLanguage(filePath) {
|
|
3200
|
+
const name = path9.basename(filePath);
|
|
3201
|
+
for (const skip of SKIP_EXTENSIONS2) {
|
|
3202
|
+
if (name.endsWith(skip)) return void 0;
|
|
3203
|
+
}
|
|
3204
|
+
const ext = path9.extname(filePath);
|
|
3205
|
+
return EXT_TO_LANGUAGE[ext];
|
|
3206
|
+
}
|
|
3207
|
+
var EXTRACTOR_EDGE_TYPE = {
|
|
3208
|
+
"test-descriptions": "governs",
|
|
3209
|
+
"enum-constants": "documents",
|
|
3210
|
+
"validation-rules": "governs",
|
|
3211
|
+
"api-paths": "documents"
|
|
3212
|
+
};
|
|
3213
|
+
var ExtractionRunner = class {
|
|
3214
|
+
constructor(extractors) {
|
|
3215
|
+
this.extractors = extractors;
|
|
3216
|
+
}
|
|
3217
|
+
extractors;
|
|
3218
|
+
/**
|
|
3219
|
+
* Run all extractors against a project directory.
|
|
3220
|
+
* @param projectDir - Project root directory
|
|
3221
|
+
* @param store - GraphStore for node/edge persistence
|
|
3222
|
+
* @param outputDir - Directory for JSONL output (e.g. .harness/knowledge/extracted/)
|
|
3223
|
+
*/
|
|
3224
|
+
async run(projectDir, store, outputDir) {
|
|
3225
|
+
const start = Date.now();
|
|
3226
|
+
const errors = [];
|
|
3227
|
+
let nodesAdded = 0;
|
|
3228
|
+
let nodesUpdated = 0;
|
|
3229
|
+
let edgesAdded = 0;
|
|
3230
|
+
await fs8.mkdir(outputDir, { recursive: true });
|
|
3231
|
+
const files = await this.findSourceFiles(projectDir);
|
|
3232
|
+
const recordsByExtractor = /* @__PURE__ */ new Map();
|
|
3233
|
+
for (const ext of this.extractors) {
|
|
3234
|
+
recordsByExtractor.set(ext.name, []);
|
|
3235
|
+
}
|
|
3236
|
+
for (const filePath of files) {
|
|
3237
|
+
const language = detectLanguage(filePath);
|
|
3238
|
+
if (!language) continue;
|
|
3239
|
+
const ext = path9.extname(filePath);
|
|
3240
|
+
let content;
|
|
3241
|
+
try {
|
|
3242
|
+
content = await fs8.readFile(filePath, "utf-8");
|
|
3243
|
+
} catch (err) {
|
|
3244
|
+
errors.push(
|
|
3245
|
+
`Failed to read ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
3246
|
+
);
|
|
3247
|
+
continue;
|
|
3248
|
+
}
|
|
3249
|
+
const relativePath = path9.relative(projectDir, filePath).replaceAll("\\", "/");
|
|
3250
|
+
for (const extractor2 of this.extractors) {
|
|
3251
|
+
if (!extractor2.supportedExtensions.includes(ext)) continue;
|
|
3252
|
+
try {
|
|
3253
|
+
const records = extractor2.extract(content, relativePath, language);
|
|
3254
|
+
recordsByExtractor.get(extractor2.name).push(...records);
|
|
3255
|
+
} catch (err) {
|
|
3256
|
+
errors.push(
|
|
3257
|
+
`Extractor ${extractor2.name} failed on ${relativePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
3258
|
+
);
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
const allCurrentIds = /* @__PURE__ */ new Set();
|
|
3263
|
+
for (const [extractorName, records] of recordsByExtractor) {
|
|
3264
|
+
await this.writeJsonl(records, outputDir, extractorName);
|
|
3265
|
+
for (const record of records) {
|
|
3266
|
+
allCurrentIds.add(record.id);
|
|
3267
|
+
const result = this.persistRecord(record, store);
|
|
3268
|
+
nodesAdded += result.nodesAdded;
|
|
3269
|
+
nodesUpdated += result.nodesUpdated;
|
|
3270
|
+
edgesAdded += result.edgesAdded;
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
const staleCount = this.markStale(store, allCurrentIds);
|
|
3274
|
+
nodesUpdated += staleCount;
|
|
3275
|
+
return {
|
|
3276
|
+
nodesAdded,
|
|
3277
|
+
nodesUpdated,
|
|
3278
|
+
edgesAdded,
|
|
3279
|
+
edgesUpdated: 0,
|
|
3280
|
+
errors,
|
|
3281
|
+
durationMs: Date.now() - start
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
/** Write extraction records to JSONL file. */
|
|
3285
|
+
async writeJsonl(records, outputDir, extractorName) {
|
|
3286
|
+
const filePath = path9.join(outputDir, `${extractorName}.jsonl`);
|
|
3287
|
+
const lines = records.map((r) => JSON.stringify(r));
|
|
3288
|
+
await fs8.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
|
|
3289
|
+
}
|
|
3290
|
+
/** Create or update a graph node from an extraction record. */
|
|
3291
|
+
persistRecord(record, store) {
|
|
3292
|
+
const existing = store.getNode(record.id);
|
|
3293
|
+
const node = {
|
|
3294
|
+
id: record.id,
|
|
3295
|
+
type: record.nodeType,
|
|
3296
|
+
name: record.name,
|
|
3297
|
+
path: record.filePath,
|
|
3298
|
+
location: {
|
|
3299
|
+
fileId: `file:${hash(record.filePath)}`,
|
|
3300
|
+
startLine: record.line,
|
|
3301
|
+
endLine: record.line
|
|
3302
|
+
},
|
|
3303
|
+
content: record.content,
|
|
3304
|
+
metadata: {
|
|
3305
|
+
...record.metadata,
|
|
3306
|
+
source: "code-extractor",
|
|
3307
|
+
extractor: record.extractor,
|
|
3308
|
+
confidence: record.confidence,
|
|
3309
|
+
language: record.language,
|
|
3310
|
+
stale: false
|
|
3311
|
+
}
|
|
3312
|
+
};
|
|
3313
|
+
store.addNode(node);
|
|
3314
|
+
const fileNodeId = `file:${hash(record.filePath)}`;
|
|
3315
|
+
const edgeType = EXTRACTOR_EDGE_TYPE[record.extractor] ?? "documents";
|
|
3316
|
+
const edge = {
|
|
3317
|
+
from: record.id,
|
|
3318
|
+
to: fileNodeId,
|
|
3319
|
+
type: edgeType,
|
|
3320
|
+
confidence: record.confidence,
|
|
3321
|
+
metadata: { source: "code-extractor" }
|
|
3322
|
+
};
|
|
3323
|
+
store.addEdge(edge);
|
|
3324
|
+
return {
|
|
3325
|
+
nodesAdded: existing ? 0 : 1,
|
|
3326
|
+
nodesUpdated: existing ? 1 : 0,
|
|
3327
|
+
edgesAdded: existing ? 0 : 1
|
|
3328
|
+
};
|
|
3329
|
+
}
|
|
3330
|
+
/**
|
|
3331
|
+
* Mark nodes from previous extractions that are no longer present as stale.
|
|
3332
|
+
* Returns the number of nodes marked stale.
|
|
3333
|
+
*/
|
|
3334
|
+
markStale(store, currentIds) {
|
|
3335
|
+
let count = 0;
|
|
3336
|
+
const businessTypes = ["business_rule", "business_process", "business_term"];
|
|
3337
|
+
for (const type of businessTypes) {
|
|
3338
|
+
const nodes = store.findNodes({ type });
|
|
3339
|
+
for (const node of nodes) {
|
|
3340
|
+
if (node.metadata.source === "code-extractor" && !node.metadata.stale && !currentIds.has(node.id)) {
|
|
3341
|
+
store.addNode({
|
|
3342
|
+
...node,
|
|
3343
|
+
metadata: {
|
|
3344
|
+
...node.metadata,
|
|
3345
|
+
stale: true,
|
|
3346
|
+
staleAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3347
|
+
}
|
|
3348
|
+
});
|
|
3349
|
+
count++;
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
return count;
|
|
3354
|
+
}
|
|
3355
|
+
/** Recursively find source files, skipping common non-source directories. */
|
|
3356
|
+
async findSourceFiles(dir) {
|
|
3357
|
+
const results = [];
|
|
3358
|
+
let entries;
|
|
3359
|
+
try {
|
|
3360
|
+
entries = await fs8.readdir(dir, { withFileTypes: true });
|
|
3361
|
+
} catch {
|
|
3362
|
+
return results;
|
|
3363
|
+
}
|
|
3364
|
+
for (const entry of entries) {
|
|
3365
|
+
const fullPath = path9.join(dir, entry.name);
|
|
3366
|
+
if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
|
|
3367
|
+
results.push(...await this.findSourceFiles(fullPath));
|
|
3368
|
+
} else if (entry.isFile() && detectLanguage(fullPath) !== void 0) {
|
|
3369
|
+
results.push(fullPath);
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
return results;
|
|
3373
|
+
}
|
|
3374
|
+
};
|
|
3375
|
+
|
|
3376
|
+
// src/ingest/extractors/TestDescriptionExtractor.ts
|
|
3377
|
+
var PATTERNS = {
|
|
3378
|
+
typescript: [/(?:describe|it|test)\s*\(\s*(['"`])((?:(?!\1).)*)\1/g],
|
|
3379
|
+
javascript: [/(?:describe|it|test)\s*\(\s*(['"`])((?:(?!\1).)*)\1/g],
|
|
3380
|
+
python: [/def\s+(test_\w+)\s*\(/g, /"""((?:(?!""").)*?)"""/gs, /class\s+(Test\w+)/g],
|
|
3381
|
+
go: [/func\s+(Test\w+)\s*\(/g, /t\.Run\(\s*"([^"]+)"/g],
|
|
3382
|
+
rust: [/#\[test\]\s*\n\s*(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/g, /\/\/\/\s*(.+)/g],
|
|
3383
|
+
java: [
|
|
3384
|
+
/@DisplayName\s*\(\s*"([^"]+)"\s*\)/g,
|
|
3385
|
+
/@Test\s*\n\s*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:void|[\w<>]+)\s+(\w+)\s*\(/g
|
|
3386
|
+
]
|
|
3387
|
+
};
|
|
3388
|
+
var TestDescriptionExtractor = class {
|
|
3389
|
+
name = "test-descriptions";
|
|
3390
|
+
supportedExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
|
|
3391
|
+
extract(content, filePath, language) {
|
|
3392
|
+
const records = [];
|
|
3393
|
+
const patterns = PATTERNS[language];
|
|
3394
|
+
if (!patterns) return records;
|
|
3395
|
+
const lines = content.split("\n");
|
|
3396
|
+
if (language === "typescript" || language === "javascript") {
|
|
3397
|
+
return this.extractJsTs(content, filePath, language, lines);
|
|
3398
|
+
}
|
|
3399
|
+
if (language === "python") {
|
|
3400
|
+
return this.extractPython(content, filePath, language, lines);
|
|
3401
|
+
}
|
|
3402
|
+
if (language === "go") {
|
|
3403
|
+
return this.extractGo(content, filePath, language, lines);
|
|
3404
|
+
}
|
|
3405
|
+
if (language === "rust") {
|
|
3406
|
+
return this.extractRust(content, filePath, language, lines);
|
|
3407
|
+
}
|
|
3408
|
+
if (language === "java") {
|
|
3409
|
+
return this.extractJava(content, filePath, language, lines);
|
|
3410
|
+
}
|
|
3411
|
+
return records;
|
|
3412
|
+
}
|
|
3413
|
+
extractJsTs(_content, filePath, language, lines) {
|
|
3414
|
+
const records = [];
|
|
3415
|
+
const describeStack = [];
|
|
3416
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3417
|
+
const line = lines[i];
|
|
3418
|
+
const describeMatch = line.match(/describe\s*\(\s*(['"`])((?:(?!\1).)*)\1/);
|
|
3419
|
+
if (describeMatch) {
|
|
3420
|
+
describeStack.push(describeMatch[2]);
|
|
3421
|
+
}
|
|
3422
|
+
const itMatch = line.match(/(?:it|test)\s*\(\s*(['"`])((?:(?!\1).)*)\1/);
|
|
3423
|
+
if (itMatch) {
|
|
3424
|
+
const testName = itMatch[2];
|
|
3425
|
+
const fullPath = [...describeStack, testName].join(" > ");
|
|
3426
|
+
const patternKey = fullPath;
|
|
3427
|
+
records.push({
|
|
3428
|
+
id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
|
|
3429
|
+
extractor: "test-descriptions",
|
|
3430
|
+
language,
|
|
3431
|
+
filePath,
|
|
3432
|
+
line: i + 1,
|
|
3433
|
+
nodeType: "business_rule",
|
|
3434
|
+
name: testName,
|
|
3435
|
+
content: fullPath,
|
|
3436
|
+
confidence: 0.7,
|
|
3437
|
+
metadata: {
|
|
3438
|
+
suite: describeStack.length > 0 ? describeStack[describeStack.length - 1] : void 0,
|
|
3439
|
+
framework: "vitest"
|
|
3440
|
+
}
|
|
3441
|
+
});
|
|
3442
|
+
}
|
|
3443
|
+
if (line.match(/^\s*\}\s*\)\s*;?\s*$/) && describeStack.length > 0) {
|
|
3444
|
+
describeStack.pop();
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
return records;
|
|
3448
|
+
}
|
|
3449
|
+
extractPython(_content, filePath, language, lines) {
|
|
3450
|
+
const records = [];
|
|
3451
|
+
let currentClass;
|
|
3452
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3453
|
+
const line = lines[i];
|
|
3454
|
+
const classMatch = line.match(/^class\s+(Test\w+)/);
|
|
3455
|
+
if (classMatch) {
|
|
3456
|
+
currentClass = classMatch[1];
|
|
3457
|
+
}
|
|
3458
|
+
const funcMatch = line.match(/^\s*def\s+(test_\w+)\s*\(/);
|
|
3459
|
+
if (funcMatch) {
|
|
3460
|
+
const testName = funcMatch[1];
|
|
3461
|
+
const humanName = testName.replace(/^test_/, "").replace(/_/g, " ");
|
|
3462
|
+
let docstring;
|
|
3463
|
+
if (i + 1 < lines.length) {
|
|
3464
|
+
const nextLine = lines[i + 1].trim();
|
|
3465
|
+
const docMatch = nextLine.match(/^"""(.+?)"""/);
|
|
3466
|
+
if (docMatch) {
|
|
3467
|
+
docstring = docMatch[1];
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
const patternKey = currentClass ? `${currentClass}.${testName}` : testName;
|
|
3471
|
+
records.push({
|
|
3472
|
+
id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
|
|
3473
|
+
extractor: "test-descriptions",
|
|
3474
|
+
language,
|
|
3475
|
+
filePath,
|
|
3476
|
+
line: i + 1,
|
|
3477
|
+
nodeType: "business_rule",
|
|
3478
|
+
name: docstring ?? humanName,
|
|
3479
|
+
content: patternKey,
|
|
3480
|
+
confidence: docstring ? 0.7 : 0.5,
|
|
3481
|
+
metadata: {
|
|
3482
|
+
suite: currentClass,
|
|
3483
|
+
framework: "pytest",
|
|
3484
|
+
functionName: testName
|
|
3485
|
+
}
|
|
3486
|
+
});
|
|
3487
|
+
}
|
|
3488
|
+
if (currentClass && /^\S/.test(line) && !line.startsWith("class ") && !line.startsWith("#") && line.trim() !== "") {
|
|
3489
|
+
currentClass = void 0;
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
return records;
|
|
3493
|
+
}
|
|
3494
|
+
extractGo(_content, filePath, language, lines) {
|
|
3495
|
+
const records = [];
|
|
3496
|
+
let currentTest;
|
|
3497
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3498
|
+
const line = lines[i];
|
|
3499
|
+
const funcMatch = line.match(/^func\s+(Test\w+)\s*\(/);
|
|
3500
|
+
if (funcMatch) {
|
|
3501
|
+
currentTest = funcMatch[1];
|
|
3502
|
+
const humanName = currentTest.replace(/^Test/, "").replace(/([A-Z])/g, " $1").trim();
|
|
3503
|
+
records.push({
|
|
3504
|
+
id: `extracted:test-descriptions:${hash(filePath + ":" + currentTest)}`,
|
|
3505
|
+
extractor: "test-descriptions",
|
|
3506
|
+
language,
|
|
3507
|
+
filePath,
|
|
3508
|
+
line: i + 1,
|
|
3509
|
+
nodeType: "business_rule",
|
|
3510
|
+
name: humanName,
|
|
3511
|
+
content: currentTest,
|
|
3512
|
+
confidence: 0.5,
|
|
3513
|
+
metadata: { framework: "testing" }
|
|
3514
|
+
});
|
|
3515
|
+
}
|
|
3516
|
+
const runMatch = line.match(/t\.Run\(\s*"([^"]+)"/);
|
|
3517
|
+
if (runMatch && currentTest) {
|
|
3518
|
+
const subtestName = runMatch[1];
|
|
3519
|
+
const patternKey = `${currentTest} > ${subtestName}`;
|
|
3520
|
+
records.push({
|
|
3521
|
+
id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
|
|
3522
|
+
extractor: "test-descriptions",
|
|
3523
|
+
language,
|
|
3524
|
+
filePath,
|
|
3525
|
+
line: i + 1,
|
|
3526
|
+
nodeType: "business_rule",
|
|
3527
|
+
name: subtestName,
|
|
3528
|
+
content: patternKey,
|
|
3529
|
+
confidence: 0.7,
|
|
3530
|
+
metadata: {
|
|
3531
|
+
suite: currentTest,
|
|
3532
|
+
framework: "testing"
|
|
3533
|
+
}
|
|
3534
|
+
});
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
return records;
|
|
3538
|
+
}
|
|
3539
|
+
extractRust(_content, filePath, language, lines) {
|
|
3540
|
+
const records = [];
|
|
3541
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3542
|
+
const line = lines[i];
|
|
3543
|
+
if (line.trim() === "#[test]") {
|
|
3544
|
+
for (let j = i + 1; j < lines.length && j <= i + 3; j++) {
|
|
3545
|
+
const fnLine = lines[j];
|
|
3546
|
+
const fnMatch = fnLine.match(/(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/);
|
|
3547
|
+
if (fnMatch) {
|
|
3548
|
+
const testName = fnMatch[1];
|
|
3549
|
+
const humanName = testName.replace(/^test_/, "").replace(/_/g, " ");
|
|
3550
|
+
let docComment;
|
|
3551
|
+
if (i > 0) {
|
|
3552
|
+
const prevLine = lines[i - 1].trim();
|
|
3553
|
+
const docMatch = prevLine.match(/^\/\/\/\s*(.+)/);
|
|
3554
|
+
if (docMatch) {
|
|
3555
|
+
docComment = docMatch[1];
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
records.push({
|
|
3559
|
+
id: `extracted:test-descriptions:${hash(filePath + ":" + testName)}`,
|
|
3560
|
+
extractor: "test-descriptions",
|
|
3561
|
+
language,
|
|
3562
|
+
filePath,
|
|
3563
|
+
line: j + 1,
|
|
3564
|
+
nodeType: "business_rule",
|
|
3565
|
+
name: docComment ?? humanName,
|
|
3566
|
+
content: testName,
|
|
3567
|
+
confidence: docComment ? 0.7 : 0.5,
|
|
3568
|
+
metadata: { framework: "rust-test" }
|
|
3569
|
+
});
|
|
3570
|
+
break;
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
return records;
|
|
3576
|
+
}
|
|
3577
|
+
extractJava(_content, filePath, language, lines) {
|
|
3578
|
+
const records = [];
|
|
3579
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3580
|
+
const line = lines[i];
|
|
3581
|
+
const testMatch = line.match(/@Test\s*$/);
|
|
3582
|
+
if (testMatch) {
|
|
3583
|
+
let displayName;
|
|
3584
|
+
for (let k = Math.max(0, i - 3); k < i; k++) {
|
|
3585
|
+
const prevLine = lines[k];
|
|
3586
|
+
const dm = prevLine.match(/@DisplayName\s*\(\s*"([^"]+)"\s*\)/);
|
|
3587
|
+
if (dm) displayName = dm[1];
|
|
3588
|
+
}
|
|
3589
|
+
for (let j = i + 1; j < lines.length && j <= i + 5; j++) {
|
|
3590
|
+
const scanLine = lines[j];
|
|
3591
|
+
const adjacentDisplay = scanLine.match(/@DisplayName\s*\(\s*"([^"]+)"\s*\)/);
|
|
3592
|
+
if (adjacentDisplay) {
|
|
3593
|
+
displayName = adjacentDisplay[1];
|
|
3594
|
+
continue;
|
|
3595
|
+
}
|
|
3596
|
+
const methodMatch = scanLine.match(
|
|
3597
|
+
/^\s*(?:(?:public|private|protected)\s+)?(?:static\s+)?(?:void|[\w<>[\]]+)\s+(\w+)\s*\(/
|
|
3598
|
+
);
|
|
3599
|
+
if (methodMatch) {
|
|
3600
|
+
const methodName = methodMatch[1];
|
|
3601
|
+
const name = displayName ?? methodName;
|
|
3602
|
+
const patternKey = displayName ?? methodName;
|
|
3603
|
+
records.push({
|
|
3604
|
+
id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
|
|
3605
|
+
extractor: "test-descriptions",
|
|
3606
|
+
language,
|
|
3607
|
+
filePath,
|
|
3608
|
+
line: j + 1,
|
|
3609
|
+
nodeType: "business_rule",
|
|
3610
|
+
name,
|
|
3611
|
+
content: patternKey,
|
|
3612
|
+
confidence: displayName ? 0.7 : 0.5,
|
|
3613
|
+
metadata: { framework: "junit5" }
|
|
3614
|
+
});
|
|
3615
|
+
break;
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
return records;
|
|
3621
|
+
}
|
|
3622
|
+
};
|
|
3623
|
+
|
|
3624
|
+
// src/ingest/extractors/EnumConstantExtractor.ts
|
|
3625
|
+
var EnumConstantExtractor = class {
|
|
3626
|
+
name = "enum-constants";
|
|
3627
|
+
supportedExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
|
|
3628
|
+
extract(content, filePath, language) {
|
|
3629
|
+
switch (language) {
|
|
3630
|
+
case "typescript":
|
|
3631
|
+
return this.extractTypeScript(content, filePath, language);
|
|
3632
|
+
case "javascript":
|
|
3633
|
+
return this.extractJavaScript(content, filePath, language);
|
|
3634
|
+
case "python":
|
|
3635
|
+
return this.extractPython(content, filePath, language);
|
|
3636
|
+
case "go":
|
|
3637
|
+
return this.extractGo(content, filePath, language);
|
|
3638
|
+
case "rust":
|
|
3639
|
+
return this.extractRust(content, filePath, language);
|
|
3640
|
+
case "java":
|
|
3641
|
+
return this.extractJava(content, filePath, language);
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
extractTypeScript(content, filePath, language) {
|
|
3645
|
+
const records = [];
|
|
3646
|
+
const lines = content.split("\n");
|
|
3647
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3648
|
+
const line = lines[i];
|
|
3649
|
+
const enumMatch = line.match(/(?:export\s+)?enum\s+(\w+)/);
|
|
3650
|
+
if (enumMatch) {
|
|
3651
|
+
const enumName = enumMatch[1];
|
|
3652
|
+
const members = this.collectEnumMembers(lines, i + 1);
|
|
3653
|
+
records.push({
|
|
3654
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + enumName)}`,
|
|
3655
|
+
extractor: "enum-constants",
|
|
3656
|
+
language,
|
|
3657
|
+
filePath,
|
|
3658
|
+
line: i + 1,
|
|
3659
|
+
nodeType: "business_term",
|
|
3660
|
+
name: enumName,
|
|
3661
|
+
content: `enum ${enumName} { ${members.join(", ")} }`,
|
|
3662
|
+
confidence: 0.8,
|
|
3663
|
+
metadata: { kind: "enum", members }
|
|
3664
|
+
});
|
|
3665
|
+
}
|
|
3666
|
+
const constMatch = line.match(/(?:export\s+)?const\s+(\w+)\s*=\s*\{/);
|
|
3667
|
+
if (constMatch && content.includes("as const")) {
|
|
3668
|
+
const blockEnd = this.findClosingBrace(lines, i);
|
|
3669
|
+
const block = lines.slice(i, blockEnd + 1).join("\n");
|
|
3670
|
+
if (block.includes("as const")) {
|
|
3671
|
+
const constName = constMatch[1];
|
|
3672
|
+
const members = this.collectObjectKeys(lines, i + 1);
|
|
3673
|
+
records.push({
|
|
3674
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + constName)}`,
|
|
3675
|
+
extractor: "enum-constants",
|
|
3676
|
+
language,
|
|
3677
|
+
filePath,
|
|
3678
|
+
line: i + 1,
|
|
3679
|
+
nodeType: "business_term",
|
|
3680
|
+
name: constName,
|
|
3681
|
+
content: `const ${constName} = { ${members.join(", ")} } as const`,
|
|
3682
|
+
confidence: 0.8,
|
|
3683
|
+
metadata: { kind: "as-const", members }
|
|
3684
|
+
});
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
const unionMatch = line.match(
|
|
3688
|
+
/(?:export\s+)?type\s+(\w+)\s*=\s*(['"`][\w]+['"`](?:\s*\|\s*['"`][\w]+['"`])+)/
|
|
3689
|
+
);
|
|
3690
|
+
if (unionMatch) {
|
|
3691
|
+
const typeName = unionMatch[1];
|
|
3692
|
+
const values = unionMatch[2].split("|").map((v) => v.trim().replace(/['"`]/g, ""));
|
|
3693
|
+
records.push({
|
|
3694
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + typeName)}`,
|
|
3695
|
+
extractor: "enum-constants",
|
|
3696
|
+
language,
|
|
3697
|
+
filePath,
|
|
3698
|
+
line: i + 1,
|
|
3699
|
+
nodeType: "business_term",
|
|
3700
|
+
name: typeName,
|
|
3701
|
+
content: `type ${typeName} = ${values.map((v) => `'${v}'`).join(" | ")}`,
|
|
3702
|
+
confidence: 0.7,
|
|
3703
|
+
metadata: { kind: "union-type", members: values }
|
|
3704
|
+
});
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
return records;
|
|
3708
|
+
}
|
|
3709
|
+
extractJavaScript(content, filePath, language) {
|
|
3710
|
+
const records = [];
|
|
3711
|
+
const lines = content.split("\n");
|
|
3712
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3713
|
+
const line = lines[i];
|
|
3714
|
+
const freezeMatch = line.match(/(?:export\s+)?const\s+(\w+)\s*=\s*Object\.freeze\s*\(\s*\{/);
|
|
3715
|
+
if (freezeMatch) {
|
|
3716
|
+
const name = freezeMatch[1];
|
|
3717
|
+
const members = this.collectObjectKeys(lines, i + 1);
|
|
3718
|
+
records.push({
|
|
3719
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + name)}`,
|
|
3720
|
+
extractor: "enum-constants",
|
|
3721
|
+
language,
|
|
3722
|
+
filePath,
|
|
3723
|
+
line: i + 1,
|
|
3724
|
+
nodeType: "business_term",
|
|
3725
|
+
name,
|
|
3726
|
+
content: `const ${name} = Object.freeze({ ${members.join(", ")} })`,
|
|
3727
|
+
confidence: 0.8,
|
|
3728
|
+
metadata: { kind: "frozen-object", members }
|
|
3729
|
+
});
|
|
3730
|
+
}
|
|
3731
|
+
const constMatch = line.match(/(?:export\s+)?const\s+([A-Z][A-Z_\d]*)\s*=\s*\{/);
|
|
3732
|
+
if (constMatch && !freezeMatch) {
|
|
3733
|
+
const name = constMatch[1];
|
|
3734
|
+
const members = this.collectObjectKeys(lines, i + 1);
|
|
3735
|
+
if (members.length > 0 && members.every((m) => /^[A-Z][A-Z_\d]*$/.test(m))) {
|
|
3736
|
+
records.push({
|
|
3737
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + name)}`,
|
|
3738
|
+
extractor: "enum-constants",
|
|
3739
|
+
language,
|
|
3740
|
+
filePath,
|
|
3741
|
+
line: i + 1,
|
|
3742
|
+
nodeType: "business_term",
|
|
3743
|
+
name,
|
|
3744
|
+
content: `const ${name} = { ${members.join(", ")} }`,
|
|
3745
|
+
confidence: 0.6,
|
|
3746
|
+
metadata: { kind: "const-object", members }
|
|
3747
|
+
});
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
return records;
|
|
3752
|
+
}
|
|
3753
|
+
extractPython(content, filePath, language) {
|
|
3754
|
+
const records = [];
|
|
3755
|
+
const lines = content.split("\n");
|
|
3756
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3757
|
+
const line = lines[i];
|
|
3758
|
+
const enumMatch = line.match(
|
|
3759
|
+
/^class\s+(\w+)\s*\(\s*(?:str\s*,\s*)?(?:Enum|StrEnum|IntEnum|Flag|IntFlag)\s*\)/
|
|
3760
|
+
);
|
|
3761
|
+
if (enumMatch) {
|
|
3762
|
+
const enumName = enumMatch[1];
|
|
3763
|
+
const members = this.collectPythonEnumMembers(lines, i + 1);
|
|
3764
|
+
records.push({
|
|
3765
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + enumName)}`,
|
|
3766
|
+
extractor: "enum-constants",
|
|
3767
|
+
language,
|
|
3768
|
+
filePath,
|
|
3769
|
+
line: i + 1,
|
|
3770
|
+
nodeType: "business_term",
|
|
3771
|
+
name: enumName,
|
|
3772
|
+
content: `class ${enumName}(Enum) { ${members.join(", ")} }`,
|
|
3773
|
+
confidence: 0.8,
|
|
3774
|
+
metadata: { kind: "enum", members }
|
|
3775
|
+
});
|
|
3776
|
+
}
|
|
3777
|
+
const literalMatch = line.match(/(\w+)\s*=\s*Literal\s*\[([^\]]+)\]/);
|
|
3778
|
+
if (literalMatch) {
|
|
3779
|
+
const typeName = literalMatch[1];
|
|
3780
|
+
const values = literalMatch[2].split(",").map((v) => v.trim().replace(/["']/g, ""));
|
|
3781
|
+
records.push({
|
|
3782
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + typeName)}`,
|
|
3783
|
+
extractor: "enum-constants",
|
|
3784
|
+
language,
|
|
3785
|
+
filePath,
|
|
3786
|
+
line: i + 1,
|
|
3787
|
+
nodeType: "business_term",
|
|
3788
|
+
name: typeName,
|
|
3789
|
+
content: `${typeName} = Literal[${values.map((v) => `"${v}"`).join(", ")}]`,
|
|
3790
|
+
confidence: 0.7,
|
|
3791
|
+
metadata: { kind: "literal-type", members: values }
|
|
3792
|
+
});
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
return records;
|
|
3796
|
+
}
|
|
3797
|
+
extractGo(content, filePath, language) {
|
|
3798
|
+
const records = [];
|
|
3799
|
+
const lines = content.split("\n");
|
|
3800
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3801
|
+
const line = lines[i];
|
|
3802
|
+
const typeMatch = line.match(/^type\s+(\w+)\s+(?:int|string|uint|int32|int64|uint32|uint64)/);
|
|
3803
|
+
if (typeMatch) {
|
|
3804
|
+
}
|
|
3805
|
+
const constBlockMatch = line.match(/^const\s*\(/);
|
|
3806
|
+
if (constBlockMatch) {
|
|
3807
|
+
const consts = [];
|
|
3808
|
+
let typeName;
|
|
3809
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
3810
|
+
const constLine = lines[j].trim();
|
|
3811
|
+
if (constLine === ")") break;
|
|
3812
|
+
if (constLine === "" || constLine.startsWith("//")) continue;
|
|
3813
|
+
const iotaMatch = constLine.match(/^(\w+)\s+(\w+)\s*=\s*iota/);
|
|
3814
|
+
if (iotaMatch) {
|
|
3815
|
+
typeName = iotaMatch[2];
|
|
3816
|
+
consts.push(iotaMatch[1]);
|
|
3817
|
+
continue;
|
|
3818
|
+
}
|
|
3819
|
+
const typedMatch = constLine.match(/^(\w+)\s+(\w+)\s*=\s*"[^"]*"/);
|
|
3820
|
+
if (typedMatch) {
|
|
3821
|
+
typeName = typeName ?? typedMatch[2];
|
|
3822
|
+
consts.push(typedMatch[1]);
|
|
3823
|
+
continue;
|
|
3824
|
+
}
|
|
3825
|
+
const bareMatch = constLine.match(/^(\w+)\s*$/);
|
|
3826
|
+
if (bareMatch) {
|
|
3827
|
+
consts.push(bareMatch[1]);
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3830
|
+
if (consts.length > 0) {
|
|
3831
|
+
const name = typeName ?? consts[0];
|
|
3832
|
+
records.push({
|
|
3833
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + name)}`,
|
|
3834
|
+
extractor: "enum-constants",
|
|
3835
|
+
language,
|
|
3836
|
+
filePath,
|
|
3837
|
+
line: i + 1,
|
|
3838
|
+
nodeType: "business_term",
|
|
3839
|
+
name,
|
|
3840
|
+
content: `const ( ${consts.join(", ")} )`,
|
|
3841
|
+
confidence: typeName ? 0.8 : 0.6,
|
|
3842
|
+
metadata: { kind: typeName ? "typed-const" : "const-block", members: consts }
|
|
3843
|
+
});
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
return records;
|
|
3848
|
+
}
|
|
3849
|
+
extractRust(content, filePath, language) {
|
|
3850
|
+
const records = [];
|
|
3851
|
+
const lines = content.split("\n");
|
|
3852
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3853
|
+
const line = lines[i];
|
|
3854
|
+
const enumMatch = line.match(/^(?:pub\s+)?enum\s+(\w+)/);
|
|
3855
|
+
if (enumMatch) {
|
|
3856
|
+
const enumName = enumMatch[1];
|
|
3857
|
+
const members = this.collectRustEnumVariants(lines, i + 1);
|
|
3858
|
+
records.push({
|
|
3859
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + enumName)}`,
|
|
3860
|
+
extractor: "enum-constants",
|
|
3861
|
+
language,
|
|
3862
|
+
filePath,
|
|
3863
|
+
line: i + 1,
|
|
3864
|
+
nodeType: "business_term",
|
|
3865
|
+
name: enumName,
|
|
3866
|
+
content: `enum ${enumName} { ${members.join(", ")} }`,
|
|
3867
|
+
confidence: 0.8,
|
|
3868
|
+
metadata: { kind: "enum", members }
|
|
3869
|
+
});
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
return records;
|
|
3873
|
+
}
|
|
3874
|
+
extractJava(content, filePath, language) {
|
|
3875
|
+
const records = [];
|
|
3876
|
+
const lines = content.split("\n");
|
|
3877
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3878
|
+
const line = lines[i];
|
|
3879
|
+
const enumMatch = line.match(/(?:public\s+|private\s+|protected\s+)?enum\s+(\w+)/);
|
|
3880
|
+
if (enumMatch) {
|
|
3881
|
+
const enumName = enumMatch[1];
|
|
3882
|
+
const members = this.collectJavaEnumConstants(lines, i + 1);
|
|
3883
|
+
records.push({
|
|
3884
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + enumName)}`,
|
|
3885
|
+
extractor: "enum-constants",
|
|
3886
|
+
language,
|
|
3887
|
+
filePath,
|
|
3888
|
+
line: i + 1,
|
|
3889
|
+
nodeType: "business_term",
|
|
3890
|
+
name: enumName,
|
|
3891
|
+
content: `enum ${enumName} { ${members.join(", ")} }`,
|
|
3892
|
+
confidence: 0.8,
|
|
3893
|
+
metadata: { kind: "enum", members }
|
|
3894
|
+
});
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
3897
|
+
return records;
|
|
3898
|
+
}
|
|
3899
|
+
// --- Helper methods ---
|
|
3900
|
+
collectEnumMembers(lines, startLine) {
|
|
3901
|
+
const members = [];
|
|
3902
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3903
|
+
const line = lines[i].trim();
|
|
3904
|
+
if (line === "}") break;
|
|
3905
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
3906
|
+
const match = line.match(/^(\w+)/);
|
|
3907
|
+
if (match) members.push(match[1]);
|
|
3908
|
+
}
|
|
3909
|
+
return members;
|
|
3910
|
+
}
|
|
3911
|
+
collectObjectKeys(lines, startLine) {
|
|
3912
|
+
const keys = [];
|
|
3913
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3914
|
+
const line = lines[i].trim();
|
|
3915
|
+
if (line.startsWith("}")) break;
|
|
3916
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
3917
|
+
const match = line.match(/^(\w+)\s*:/);
|
|
3918
|
+
if (match) keys.push(match[1]);
|
|
3919
|
+
}
|
|
3920
|
+
return keys;
|
|
3921
|
+
}
|
|
3922
|
+
findClosingBrace(lines, startLine) {
|
|
3923
|
+
let depth = 0;
|
|
3924
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3925
|
+
for (const ch of lines[i]) {
|
|
3926
|
+
if (ch === "{") depth++;
|
|
3927
|
+
if (ch === "}") {
|
|
3928
|
+
depth--;
|
|
3929
|
+
if (depth === 0) return i;
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
return lines.length - 1;
|
|
3934
|
+
}
|
|
3935
|
+
collectPythonEnumMembers(lines, startLine) {
|
|
3936
|
+
const members = [];
|
|
3937
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3938
|
+
const line = lines[i];
|
|
3939
|
+
if (/^\S/.test(line) && line.trim() !== "") break;
|
|
3940
|
+
const match = line.match(/^\s+(\w+)\s*=/);
|
|
3941
|
+
if (match) members.push(match[1]);
|
|
3942
|
+
}
|
|
3943
|
+
return members;
|
|
3944
|
+
}
|
|
3945
|
+
collectRustEnumVariants(lines, startLine) {
|
|
3946
|
+
const variants = [];
|
|
3947
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3948
|
+
const line = lines[i].trim();
|
|
3949
|
+
if (line === "}") break;
|
|
3950
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
3951
|
+
const match = line.match(/^(\w+)/);
|
|
3952
|
+
if (match) variants.push(match[1]);
|
|
3953
|
+
}
|
|
3954
|
+
return variants;
|
|
3955
|
+
}
|
|
3956
|
+
collectJavaEnumConstants(lines, startLine) {
|
|
3957
|
+
const constants = [];
|
|
3958
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3959
|
+
const line = lines[i].trim();
|
|
3960
|
+
if (line === "}") break;
|
|
3961
|
+
if (line === "" || line.startsWith("//") || line.startsWith("/*")) continue;
|
|
3962
|
+
if (line.match(/^\s*(?:private|public|protected|static)/)) break;
|
|
3963
|
+
const match = line.match(/^(\w+)[\s(,;]/);
|
|
3964
|
+
if (match) constants.push(match[1]);
|
|
3965
|
+
const singleMatch = line.match(/^(\w+)$/);
|
|
3966
|
+
if (singleMatch) constants.push(singleMatch[1]);
|
|
3967
|
+
}
|
|
3968
|
+
return constants;
|
|
3969
|
+
}
|
|
3970
|
+
};
|
|
3971
|
+
|
|
3972
|
+
// src/ingest/extractors/ValidationRuleExtractor.ts
|
|
3973
|
+
var ValidationRuleExtractor = class {
|
|
3974
|
+
name = "validation-rules";
|
|
3975
|
+
supportedExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
|
|
3976
|
+
extract(content, filePath, language) {
|
|
3977
|
+
switch (language) {
|
|
3978
|
+
case "typescript":
|
|
3979
|
+
case "javascript":
|
|
3980
|
+
return this.extractZod(content, filePath, language);
|
|
3981
|
+
case "python":
|
|
3982
|
+
return this.extractPydantic(content, filePath, language);
|
|
3983
|
+
case "go":
|
|
3984
|
+
return this.extractGoValidate(content, filePath, language);
|
|
3985
|
+
case "rust":
|
|
3986
|
+
return this.extractRustValidate(content, filePath, language);
|
|
3987
|
+
case "java":
|
|
3988
|
+
return this.extractBeanValidation(content, filePath, language);
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
extractZod(content, filePath, language) {
|
|
3992
|
+
const records = [];
|
|
3993
|
+
const lines = content.split("\n");
|
|
3994
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3995
|
+
const line = lines[i];
|
|
3996
|
+
const schemaMatch = line.match(/(?:export\s+)?const\s+(\w+)\s*=\s*z\.object\s*\(\s*\{/);
|
|
3997
|
+
if (schemaMatch) {
|
|
3998
|
+
const schemaName = schemaMatch[1];
|
|
3999
|
+
const constraints = this.collectZodConstraints(lines, i + 1);
|
|
4000
|
+
records.push({
|
|
4001
|
+
id: `extracted:validation-rules:${hash(filePath + ":" + schemaName)}`,
|
|
4002
|
+
extractor: "validation-rules",
|
|
4003
|
+
language,
|
|
4004
|
+
filePath,
|
|
4005
|
+
line: i + 1,
|
|
4006
|
+
nodeType: "business_rule",
|
|
4007
|
+
name: schemaName,
|
|
4008
|
+
content: `${schemaName}: ${constraints.join("; ")}`,
|
|
4009
|
+
confidence: 0.8,
|
|
4010
|
+
metadata: { kind: "zod-schema", constraints, framework: "zod" }
|
|
4011
|
+
});
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
return records;
|
|
4015
|
+
}
|
|
4016
|
+
collectZodConstraints(lines, startLine) {
|
|
4017
|
+
const constraints = [];
|
|
4018
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
4019
|
+
const line = lines[i].trim();
|
|
4020
|
+
if (line.startsWith("}") || line.startsWith(")")) break;
|
|
4021
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
4022
|
+
const fieldMatch = line.match(/(\w+)\s*:\s*z\.(.+?)(?:,?\s*$)/);
|
|
4023
|
+
if (fieldMatch) {
|
|
4024
|
+
const fieldName = fieldMatch[1];
|
|
4025
|
+
const chain = fieldMatch[2];
|
|
4026
|
+
constraints.push(`${fieldName}: ${chain}`);
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
return constraints;
|
|
4030
|
+
}
|
|
4031
|
+
extractPydantic(content, filePath, language) {
|
|
4032
|
+
const records = [];
|
|
4033
|
+
const lines = content.split("\n");
|
|
4034
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4035
|
+
const line = lines[i];
|
|
4036
|
+
const modelMatch = line.match(/^class\s+(\w+)\s*\(\s*BaseModel\s*\)/);
|
|
4037
|
+
if (modelMatch) {
|
|
4038
|
+
const modelName = modelMatch[1];
|
|
4039
|
+
const constraints = this.collectPydanticConstraints(lines, i + 1);
|
|
4040
|
+
records.push({
|
|
4041
|
+
id: `extracted:validation-rules:${hash(filePath + ":" + modelName)}`,
|
|
4042
|
+
extractor: "validation-rules",
|
|
4043
|
+
language,
|
|
4044
|
+
filePath,
|
|
4045
|
+
line: i + 1,
|
|
4046
|
+
nodeType: "business_rule",
|
|
4047
|
+
name: modelName,
|
|
4048
|
+
content: `${modelName}: ${constraints.join("; ")}`,
|
|
4049
|
+
confidence: 0.8,
|
|
4050
|
+
metadata: { kind: "pydantic-model", constraints, framework: "pydantic" }
|
|
4051
|
+
});
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
return records;
|
|
4055
|
+
}
|
|
4056
|
+
collectPydanticConstraints(lines, startLine) {
|
|
4057
|
+
const constraints = [];
|
|
4058
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
4059
|
+
const line = lines[i];
|
|
4060
|
+
if (/^\S/.test(line) && line.trim() !== "") break;
|
|
4061
|
+
if (line.trim() === "" || line.trim().startsWith("#")) continue;
|
|
4062
|
+
const fieldMatch = line.match(/^\s+(\w+)\s*:\s*(.+?)(?:\s*$)/);
|
|
4063
|
+
if (fieldMatch) {
|
|
4064
|
+
constraints.push(`${fieldMatch[1]}: ${fieldMatch[2]}`);
|
|
4065
|
+
}
|
|
4066
|
+
}
|
|
4067
|
+
return constraints;
|
|
4068
|
+
}
|
|
4069
|
+
extractGoValidate(content, filePath, language) {
|
|
4070
|
+
const records = [];
|
|
4071
|
+
const lines = content.split("\n");
|
|
4072
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4073
|
+
const line = lines[i];
|
|
4074
|
+
const structMatch = line.match(/^type\s+(\w+)\s+struct\s*\{/);
|
|
4075
|
+
if (structMatch) {
|
|
4076
|
+
const structName = structMatch[1];
|
|
4077
|
+
const constraints = this.collectGoValidateTags(lines, i + 1);
|
|
4078
|
+
if (constraints.length > 0) {
|
|
4079
|
+
records.push({
|
|
4080
|
+
id: `extracted:validation-rules:${hash(filePath + ":" + structName)}`,
|
|
4081
|
+
extractor: "validation-rules",
|
|
4082
|
+
language,
|
|
4083
|
+
filePath,
|
|
4084
|
+
line: i + 1,
|
|
4085
|
+
nodeType: "business_rule",
|
|
4086
|
+
name: structName,
|
|
4087
|
+
content: `${structName}: ${constraints.join("; ")}`,
|
|
4088
|
+
confidence: 0.8,
|
|
4089
|
+
metadata: { kind: "struct-tags", constraints, framework: "go-playground/validator" }
|
|
4090
|
+
});
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
return records;
|
|
4095
|
+
}
|
|
4096
|
+
collectGoValidateTags(lines, startLine) {
|
|
4097
|
+
const constraints = [];
|
|
4098
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
4099
|
+
const line = lines[i].trim();
|
|
4100
|
+
if (line === "}") break;
|
|
4101
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
4102
|
+
const tagMatch = line.match(/(\w+)\s+\S+\s+`[^`]*validate:"([^"]+)"[^`]*`/);
|
|
4103
|
+
if (tagMatch) {
|
|
4104
|
+
constraints.push(`${tagMatch[1]}: ${tagMatch[2]}`);
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
return constraints;
|
|
4108
|
+
}
|
|
4109
|
+
extractRustValidate(content, filePath, language) {
|
|
4110
|
+
const records = [];
|
|
4111
|
+
const lines = content.split("\n");
|
|
4112
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4113
|
+
const line = lines[i];
|
|
4114
|
+
const deriveMatch = line.match(/#\[derive\([^)]*Validate[^)]*\)\]/);
|
|
4115
|
+
if (deriveMatch) {
|
|
4116
|
+
for (let j = i + 1; j < lines.length && j <= i + 3; j++) {
|
|
4117
|
+
const structLine = lines[j];
|
|
4118
|
+
const structMatch = structLine.match(/^(?:pub\s+)?struct\s+(\w+)/);
|
|
4119
|
+
if (structMatch) {
|
|
4120
|
+
const structName = structMatch[1];
|
|
4121
|
+
const constraints = this.collectRustValidateAttrs(lines, j + 1);
|
|
4122
|
+
if (constraints.length > 0) {
|
|
4123
|
+
records.push({
|
|
4124
|
+
id: `extracted:validation-rules:${hash(filePath + ":" + structName)}`,
|
|
4125
|
+
extractor: "validation-rules",
|
|
4126
|
+
language,
|
|
4127
|
+
filePath,
|
|
4128
|
+
line: j + 1,
|
|
4129
|
+
nodeType: "business_rule",
|
|
4130
|
+
name: structName,
|
|
4131
|
+
content: `${structName}: ${constraints.join("; ")}`,
|
|
4132
|
+
confidence: 0.8,
|
|
4133
|
+
metadata: { kind: "validate-derive", constraints, framework: "validator" }
|
|
4134
|
+
});
|
|
4135
|
+
}
|
|
4136
|
+
break;
|
|
4137
|
+
}
|
|
4138
|
+
}
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
return records;
|
|
4142
|
+
}
|
|
4143
|
+
collectRustValidateAttrs(lines, startLine) {
|
|
4144
|
+
const constraints = [];
|
|
4145
|
+
let pendingValidate;
|
|
4146
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
4147
|
+
const line = lines[i].trim();
|
|
4148
|
+
if (line === "}") break;
|
|
4149
|
+
const attrMatch = line.match(/#\[validate\((.+?)\)\]/);
|
|
4150
|
+
if (attrMatch) {
|
|
4151
|
+
pendingValidate = attrMatch[1];
|
|
4152
|
+
continue;
|
|
4153
|
+
}
|
|
4154
|
+
if (pendingValidate) {
|
|
4155
|
+
const fieldMatch = line.match(/(?:pub\s+)?(\w+)\s*:/);
|
|
4156
|
+
if (fieldMatch) {
|
|
4157
|
+
constraints.push(`${fieldMatch[1]}: ${pendingValidate}`);
|
|
4158
|
+
pendingValidate = void 0;
|
|
4159
|
+
}
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
return constraints;
|
|
4163
|
+
}
|
|
4164
|
+
extractBeanValidation(content, filePath, language) {
|
|
4165
|
+
const records = [];
|
|
4166
|
+
const lines = content.split("\n");
|
|
4167
|
+
const classRanges = this.findJavaClasses(lines);
|
|
4168
|
+
for (const { name: className, startLine, endLine } of classRanges) {
|
|
4169
|
+
const constraints = this.collectBeanConstraints(lines, startLine, endLine);
|
|
4170
|
+
if (constraints.length > 0) {
|
|
4171
|
+
records.push({
|
|
4172
|
+
id: `extracted:validation-rules:${hash(filePath + ":" + className)}`,
|
|
4173
|
+
extractor: "validation-rules",
|
|
4174
|
+
language,
|
|
4175
|
+
filePath,
|
|
4176
|
+
line: startLine + 1,
|
|
4177
|
+
nodeType: "business_rule",
|
|
4178
|
+
name: className,
|
|
4179
|
+
content: `${className}: ${constraints.join("; ")}`,
|
|
4180
|
+
confidence: 0.8,
|
|
4181
|
+
metadata: { kind: "bean-validation", constraints, framework: "javax.validation" }
|
|
4182
|
+
});
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
return records;
|
|
4186
|
+
}
|
|
4187
|
+
findJavaClasses(lines) {
|
|
4188
|
+
const classes = [];
|
|
4189
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4190
|
+
const line = lines[i];
|
|
4191
|
+
const match = line.match(/(?:public\s+|private\s+|protected\s+)?class\s+(\w+)/);
|
|
4192
|
+
if (match) {
|
|
4193
|
+
const endLine = this.findClosingBrace(lines, i);
|
|
4194
|
+
classes.push({ name: match[1], startLine: i, endLine });
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
return classes;
|
|
4198
|
+
}
|
|
4199
|
+
findClosingBrace(lines, startLine) {
|
|
4200
|
+
let depth = 0;
|
|
4201
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
4202
|
+
for (const ch of lines[i]) {
|
|
4203
|
+
if (ch === "{") depth++;
|
|
4204
|
+
if (ch === "}") {
|
|
4205
|
+
depth--;
|
|
4206
|
+
if (depth === 0) return i;
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
}
|
|
4210
|
+
return lines.length - 1;
|
|
4211
|
+
}
|
|
4212
|
+
collectBeanConstraints(lines, startLine, endLine) {
|
|
4213
|
+
const constraints = [];
|
|
4214
|
+
const validationAnnotations = [
|
|
4215
|
+
"@NotNull",
|
|
4216
|
+
"@NotBlank",
|
|
4217
|
+
"@NotEmpty",
|
|
4218
|
+
"@Size",
|
|
4219
|
+
"@Min",
|
|
4220
|
+
"@Max",
|
|
4221
|
+
"@DecimalMin",
|
|
4222
|
+
"@DecimalMax",
|
|
4223
|
+
"@Email",
|
|
4224
|
+
"@Pattern",
|
|
4225
|
+
"@Positive",
|
|
4226
|
+
"@Negative",
|
|
4227
|
+
"@Past",
|
|
4228
|
+
"@Future"
|
|
4229
|
+
];
|
|
4230
|
+
let pendingAnnotations = [];
|
|
4231
|
+
for (let i = startLine + 1; i < endLine; i++) {
|
|
4232
|
+
const line = lines[i].trim();
|
|
4233
|
+
for (const anno of validationAnnotations) {
|
|
4234
|
+
if (line.startsWith(anno)) {
|
|
4235
|
+
pendingAnnotations.push(line.replace(/;?\s*$/, ""));
|
|
4236
|
+
}
|
|
4237
|
+
}
|
|
4238
|
+
if (pendingAnnotations.length > 0) {
|
|
4239
|
+
const fieldMatch = line.match(
|
|
4240
|
+
/(?:private|public|protected)\s+(?:[\w<>[\]]+)\s+(\w+)\s*[;=]/
|
|
4241
|
+
);
|
|
4242
|
+
if (fieldMatch) {
|
|
4243
|
+
constraints.push(`${fieldMatch[1]}: ${pendingAnnotations.join(", ")}`);
|
|
4244
|
+
pendingAnnotations = [];
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
return constraints;
|
|
4249
|
+
}
|
|
4250
|
+
};
|
|
4251
|
+
|
|
4252
|
+
// src/ingest/extractors/ApiPathExtractor.ts
|
|
4253
|
+
var ApiPathExtractor = class {
|
|
4254
|
+
name = "api-paths";
|
|
4255
|
+
supportedExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
|
|
4256
|
+
extract(content, filePath, language) {
|
|
4257
|
+
switch (language) {
|
|
4258
|
+
case "typescript":
|
|
4259
|
+
case "javascript":
|
|
4260
|
+
return this.extractJsTs(content, filePath, language);
|
|
4261
|
+
case "python":
|
|
4262
|
+
return this.extractPython(content, filePath, language);
|
|
4263
|
+
case "go":
|
|
4264
|
+
return this.extractGo(content, filePath, language);
|
|
4265
|
+
case "rust":
|
|
4266
|
+
return this.extractRust(content, filePath, language);
|
|
4267
|
+
case "java":
|
|
4268
|
+
return this.extractJava(content, filePath, language);
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4271
|
+
extractJsTs(content, filePath, language) {
|
|
4272
|
+
const records = [];
|
|
4273
|
+
const lines = content.split("\n");
|
|
4274
|
+
const routePattern = /(?:app|router|server|fastify)\.(get|post|put|patch|delete|head|options)\s*\(\s*(['"`])([^'"`]+)\2/i;
|
|
4275
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4276
|
+
const line = lines[i];
|
|
4277
|
+
const match = line.match(routePattern);
|
|
4278
|
+
if (match) {
|
|
4279
|
+
const method = match[1].toUpperCase();
|
|
4280
|
+
const routePath = match[3];
|
|
4281
|
+
const patternKey = `${method} ${routePath}`;
|
|
4282
|
+
records.push({
|
|
4283
|
+
id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
|
|
4284
|
+
extractor: "api-paths",
|
|
4285
|
+
language,
|
|
4286
|
+
filePath,
|
|
4287
|
+
line: i + 1,
|
|
4288
|
+
nodeType: "business_process",
|
|
4289
|
+
name: patternKey,
|
|
4290
|
+
content: `${method} ${routePath}`,
|
|
4291
|
+
confidence: 0.9,
|
|
4292
|
+
metadata: { method, path: routePath, framework: "express" }
|
|
4293
|
+
});
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
return records;
|
|
4297
|
+
}
|
|
4298
|
+
extractPython(content, filePath, language) {
|
|
4299
|
+
const records = [];
|
|
4300
|
+
const lines = content.split("\n");
|
|
4301
|
+
const decoratorPattern = /@(?:app|router)\.(get|post|put|patch|delete|head|options)\s*\(\s*["']([^"']+)["']/i;
|
|
4302
|
+
const flaskPattern = /@(?:app|blueprint)\.route\s*\(\s*["']([^"']+)["'](?:\s*,\s*methods\s*=\s*\[([^\]]+)\])?/i;
|
|
4303
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4304
|
+
const line = lines[i];
|
|
4305
|
+
const fastApiMatch = line.match(decoratorPattern);
|
|
4306
|
+
if (fastApiMatch) {
|
|
4307
|
+
const method = fastApiMatch[1].toUpperCase();
|
|
4308
|
+
const routePath = fastApiMatch[2];
|
|
4309
|
+
const patternKey = `${method} ${routePath}`;
|
|
4310
|
+
records.push({
|
|
4311
|
+
id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
|
|
4312
|
+
extractor: "api-paths",
|
|
4313
|
+
language,
|
|
4314
|
+
filePath,
|
|
4315
|
+
line: i + 1,
|
|
4316
|
+
nodeType: "business_process",
|
|
4317
|
+
name: patternKey,
|
|
4318
|
+
content: `${method} ${routePath}`,
|
|
4319
|
+
confidence: 0.9,
|
|
4320
|
+
metadata: { method, path: routePath, framework: "fastapi" }
|
|
4321
|
+
});
|
|
4322
|
+
continue;
|
|
4323
|
+
}
|
|
4324
|
+
const flaskMatch = line.match(flaskPattern);
|
|
4325
|
+
if (flaskMatch) {
|
|
4326
|
+
const routePath = flaskMatch[1];
|
|
4327
|
+
const methods = flaskMatch[2] ? flaskMatch[2].split(",").map((m) => m.trim().replace(/["']/g, "")) : ["GET"];
|
|
4328
|
+
for (const method of methods) {
|
|
4329
|
+
const patternKey = `${method} ${routePath}`;
|
|
4330
|
+
records.push({
|
|
4331
|
+
id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
|
|
4332
|
+
extractor: "api-paths",
|
|
4333
|
+
language,
|
|
4334
|
+
filePath,
|
|
4335
|
+
line: i + 1,
|
|
4336
|
+
nodeType: "business_process",
|
|
4337
|
+
name: patternKey,
|
|
4338
|
+
content: `${method} ${routePath}`,
|
|
4339
|
+
confidence: 0.9,
|
|
4340
|
+
metadata: { method, path: routePath, framework: "flask" }
|
|
4341
|
+
});
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
4345
|
+
return records;
|
|
4346
|
+
}
|
|
4347
|
+
extractGo(content, filePath, language) {
|
|
4348
|
+
const records = [];
|
|
4349
|
+
const lines = content.split("\n");
|
|
4350
|
+
const ginPattern = /(?:\w+)\.(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\(\s*"([^"]+)"/i;
|
|
4351
|
+
const httpPattern = /http\.HandleFunc\s*\(\s*"([^"]+)"/;
|
|
4352
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4353
|
+
const line = lines[i];
|
|
4354
|
+
const ginMatch = line.match(ginPattern);
|
|
4355
|
+
if (ginMatch) {
|
|
4356
|
+
const method = ginMatch[1].toUpperCase();
|
|
4357
|
+
const routePath = ginMatch[2];
|
|
4358
|
+
const patternKey = `${method} ${routePath}`;
|
|
4359
|
+
records.push({
|
|
4360
|
+
id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
|
|
4361
|
+
extractor: "api-paths",
|
|
4362
|
+
language,
|
|
4363
|
+
filePath,
|
|
4364
|
+
line: i + 1,
|
|
4365
|
+
nodeType: "business_process",
|
|
4366
|
+
name: patternKey,
|
|
4367
|
+
content: `${method} ${routePath}`,
|
|
4368
|
+
confidence: 0.9,
|
|
4369
|
+
metadata: { method, path: routePath, framework: "gin" }
|
|
4370
|
+
});
|
|
4371
|
+
continue;
|
|
4372
|
+
}
|
|
4373
|
+
const httpMatch = line.match(httpPattern);
|
|
4374
|
+
if (httpMatch) {
|
|
4375
|
+
const routePath = httpMatch[1];
|
|
4376
|
+
const patternKey = `ANY ${routePath}`;
|
|
4377
|
+
records.push({
|
|
4378
|
+
id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
|
|
4379
|
+
extractor: "api-paths",
|
|
4380
|
+
language,
|
|
4381
|
+
filePath,
|
|
4382
|
+
line: i + 1,
|
|
4383
|
+
nodeType: "business_process",
|
|
4384
|
+
name: patternKey,
|
|
4385
|
+
content: `ANY ${routePath}`,
|
|
4386
|
+
confidence: 0.6,
|
|
4387
|
+
metadata: { method: "ANY", path: routePath, framework: "net/http" }
|
|
4388
|
+
});
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
return records;
|
|
4392
|
+
}
|
|
4393
|
+
extractRust(content, filePath, language) {
|
|
4394
|
+
const records = [];
|
|
4395
|
+
const lines = content.split("\n");
|
|
4396
|
+
const actixPattern = /#\[(get|post|put|patch|delete|head|options)\s*\(\s*"([^"]+)"\s*\)\s*\]/i;
|
|
4397
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4398
|
+
const line = lines[i];
|
|
4399
|
+
const match = line.match(actixPattern);
|
|
4400
|
+
if (match) {
|
|
4401
|
+
const method = match[1].toUpperCase();
|
|
4402
|
+
const routePath = match[2];
|
|
4403
|
+
const patternKey = `${method} ${routePath}`;
|
|
4404
|
+
records.push({
|
|
4405
|
+
id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
|
|
4406
|
+
extractor: "api-paths",
|
|
4407
|
+
language,
|
|
4408
|
+
filePath,
|
|
4409
|
+
line: i + 1,
|
|
4410
|
+
nodeType: "business_process",
|
|
4411
|
+
name: patternKey,
|
|
4412
|
+
content: `${method} ${routePath}`,
|
|
4413
|
+
confidence: 0.9,
|
|
4414
|
+
metadata: { method, path: routePath, framework: "actix" }
|
|
4415
|
+
});
|
|
4416
|
+
}
|
|
4417
|
+
}
|
|
4418
|
+
return records;
|
|
4419
|
+
}
|
|
4420
|
+
extractJava(content, filePath, language) {
|
|
4421
|
+
const records = [];
|
|
4422
|
+
const lines = content.split("\n");
|
|
4423
|
+
const springPattern = /@(GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping|RequestMapping)\s*\(\s*(?:value\s*=\s*)?["']([^"']+)["']/;
|
|
4424
|
+
let basePath = "";
|
|
4425
|
+
for (const line of lines) {
|
|
4426
|
+
const baseMatch = line.match(/@RequestMapping\s*\(\s*(?:value\s*=\s*)?["']([^"']+)["']\s*\)/);
|
|
4427
|
+
if (baseMatch && line.match(/class\s/) === null) {
|
|
4428
|
+
basePath = baseMatch[1];
|
|
4429
|
+
}
|
|
4430
|
+
}
|
|
4431
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4432
|
+
const line = lines[i];
|
|
4433
|
+
const match = line.match(springPattern);
|
|
4434
|
+
if (match) {
|
|
4435
|
+
const annotation = match[1];
|
|
4436
|
+
const routePath = basePath + match[2];
|
|
4437
|
+
const methodMap = {
|
|
4438
|
+
GetMapping: "GET",
|
|
4439
|
+
PostMapping: "POST",
|
|
4440
|
+
PutMapping: "PUT",
|
|
4441
|
+
PatchMapping: "PATCH",
|
|
4442
|
+
DeleteMapping: "DELETE",
|
|
4443
|
+
RequestMapping: "ANY"
|
|
4444
|
+
};
|
|
4445
|
+
const method = methodMap[annotation] ?? "ANY";
|
|
4446
|
+
const patternKey = `${method} ${routePath}`;
|
|
4447
|
+
records.push({
|
|
4448
|
+
id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
|
|
4449
|
+
extractor: "api-paths",
|
|
4450
|
+
language,
|
|
4451
|
+
filePath,
|
|
4452
|
+
line: i + 1,
|
|
4453
|
+
nodeType: "business_process",
|
|
4454
|
+
name: patternKey,
|
|
4455
|
+
content: `${method} ${routePath}`,
|
|
4456
|
+
confidence: 0.9,
|
|
4457
|
+
metadata: { method, path: routePath, framework: "spring" }
|
|
4458
|
+
});
|
|
4459
|
+
}
|
|
4460
|
+
}
|
|
4461
|
+
return records;
|
|
4462
|
+
}
|
|
4463
|
+
};
|
|
4464
|
+
|
|
4465
|
+
// src/ingest/extractors/index.ts
|
|
4466
|
+
var ALL_EXTRACTORS = [
|
|
4467
|
+
new TestDescriptionExtractor(),
|
|
4468
|
+
new EnumConstantExtractor(),
|
|
4469
|
+
new ValidationRuleExtractor(),
|
|
4470
|
+
new ApiPathExtractor()
|
|
4471
|
+
];
|
|
4472
|
+
function createExtractionRunner() {
|
|
4473
|
+
return new ExtractionRunner(ALL_EXTRACTORS);
|
|
4474
|
+
}
|
|
4475
|
+
|
|
4476
|
+
// src/ingest/ImageAnalysisExtractor.ts
|
|
4477
|
+
var ImageAnalysisExtractor = class {
|
|
4478
|
+
provider;
|
|
4479
|
+
/** Maximum file size in bytes. Reserved for future file-size filtering. */
|
|
4480
|
+
maxFileSizeBytes;
|
|
4481
|
+
constructor(options) {
|
|
4482
|
+
this.provider = options.analysisProvider;
|
|
4483
|
+
this.maxFileSizeBytes = (options.maxFileSizeMB ?? 10) * 1024 * 1024;
|
|
4484
|
+
}
|
|
4485
|
+
async analyze(store, imagePaths) {
|
|
4486
|
+
const start = Date.now();
|
|
4487
|
+
let nodesAdded = 0;
|
|
4488
|
+
let edgesAdded = 0;
|
|
4489
|
+
const errors = [];
|
|
4490
|
+
for (const imagePath of imagePaths) {
|
|
4491
|
+
try {
|
|
4492
|
+
const response = await this.provider.analyze({
|
|
4493
|
+
prompt: "Analyze this image and provide a structured description of its visual contents.",
|
|
4494
|
+
systemPrompt: "You are an image analysis assistant. Describe the image contents, detect UI elements, extract visible text, identify design patterns, and note accessibility concerns.",
|
|
4495
|
+
responseSchema: {},
|
|
4496
|
+
// Schema handled by provider
|
|
4497
|
+
maxTokens: 1e3
|
|
4498
|
+
});
|
|
4499
|
+
const result = response.result;
|
|
4500
|
+
const pathHash = hash(imagePath);
|
|
4501
|
+
const annotationId = `img:${pathHash}`;
|
|
4502
|
+
store.addNode({
|
|
4503
|
+
id: annotationId,
|
|
4504
|
+
type: "image_annotation",
|
|
4505
|
+
name: result.description.slice(0, 200),
|
|
4506
|
+
path: imagePath,
|
|
4507
|
+
content: result.description,
|
|
4508
|
+
hash: hash(result.description),
|
|
4509
|
+
metadata: {
|
|
4510
|
+
source: "image-analysis",
|
|
4511
|
+
detectedElements: result.detectedElements,
|
|
4512
|
+
extractedText: result.extractedText,
|
|
4513
|
+
designPatterns: result.designPatterns,
|
|
4514
|
+
accessibilityNotes: result.accessibilityNotes,
|
|
4515
|
+
model: response.model
|
|
4516
|
+
}
|
|
4517
|
+
});
|
|
4518
|
+
nodesAdded++;
|
|
4519
|
+
const fileNodes = store.findNodes({ type: "file" });
|
|
4520
|
+
const fileNode = fileNodes.find((n) => n.path === imagePath);
|
|
4521
|
+
if (fileNode) {
|
|
4522
|
+
store.addEdge({ from: annotationId, to: fileNode.id, type: "annotates" });
|
|
4523
|
+
edgesAdded++;
|
|
4524
|
+
}
|
|
4525
|
+
for (const pattern of result.designPatterns) {
|
|
4526
|
+
const conceptId = `img:concept:${hash(pattern + imagePath)}`;
|
|
4527
|
+
store.addNode({
|
|
4528
|
+
id: conceptId,
|
|
4529
|
+
type: "business_concept",
|
|
4530
|
+
name: pattern,
|
|
4531
|
+
content: `Design pattern detected in ${imagePath}: ${pattern}`,
|
|
4532
|
+
hash: hash(pattern),
|
|
4533
|
+
metadata: {
|
|
4534
|
+
source: "image-analysis",
|
|
4535
|
+
sourceImage: imagePath,
|
|
4536
|
+
domain: "design"
|
|
4537
|
+
}
|
|
4538
|
+
});
|
|
4539
|
+
nodesAdded++;
|
|
4540
|
+
store.addEdge({ from: annotationId, to: conceptId, type: "contains" });
|
|
4541
|
+
edgesAdded++;
|
|
4542
|
+
}
|
|
4543
|
+
} catch (err) {
|
|
4544
|
+
errors.push(
|
|
4545
|
+
`Image analysis failed for ${imagePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
4546
|
+
);
|
|
4547
|
+
}
|
|
4548
|
+
}
|
|
4549
|
+
return {
|
|
4550
|
+
nodesAdded,
|
|
4551
|
+
nodesUpdated: 0,
|
|
4552
|
+
edgesAdded,
|
|
4553
|
+
edgesUpdated: 0,
|
|
4554
|
+
errors,
|
|
4555
|
+
durationMs: Date.now() - start
|
|
4556
|
+
};
|
|
4557
|
+
}
|
|
4558
|
+
};
|
|
4559
|
+
|
|
4560
|
+
// src/ingest/knowledgeTypes.ts
|
|
4561
|
+
var KNOWLEDGE_NODE_TYPES = [
|
|
4562
|
+
"business_fact",
|
|
4563
|
+
"business_rule",
|
|
4564
|
+
"business_process",
|
|
4565
|
+
"business_term",
|
|
4566
|
+
"business_concept",
|
|
4567
|
+
"business_metric",
|
|
4568
|
+
"design_token",
|
|
4569
|
+
"design_constraint",
|
|
4570
|
+
"aesthetic_intent",
|
|
4571
|
+
"image_annotation"
|
|
4572
|
+
];
|
|
4573
|
+
|
|
4574
|
+
// src/ingest/ContradictionDetector.ts
|
|
4575
|
+
var SIMILARITY_THRESHOLD = 0.8;
|
|
4576
|
+
function levenshteinDistance(a, b) {
|
|
4577
|
+
const m = a.length;
|
|
4578
|
+
const n = b.length;
|
|
4579
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
4580
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
4581
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
4582
|
+
for (let i = 1; i <= m; i++) {
|
|
4583
|
+
for (let j = 1; j <= n; j++) {
|
|
4584
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
4585
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
|
|
4586
|
+
}
|
|
4587
|
+
}
|
|
4588
|
+
return dp[m][n];
|
|
4589
|
+
}
|
|
4590
|
+
function levenshteinRatio(a, b) {
|
|
4591
|
+
const maxLen = Math.max(a.length, b.length);
|
|
4592
|
+
if (maxLen === 0) return 1;
|
|
4593
|
+
return 1 - levenshteinDistance(a, b) / maxLen;
|
|
4594
|
+
}
|
|
4595
|
+
function classifyConflict(a, b) {
|
|
4596
|
+
const factTypes = /* @__PURE__ */ new Set([
|
|
4597
|
+
"business_fact",
|
|
4598
|
+
"business_metric",
|
|
4599
|
+
"design_token"
|
|
4600
|
+
]);
|
|
4601
|
+
const termTypes = /* @__PURE__ */ new Set(["business_term", "business_concept"]);
|
|
4602
|
+
if (factTypes.has(a.type) || factTypes.has(b.type)) return "value_mismatch";
|
|
4603
|
+
if (termTypes.has(a.type) || termTypes.has(b.type)) return "definition_conflict";
|
|
4604
|
+
if (a.lastModified && b.lastModified && a.lastModified !== b.lastModified)
|
|
4605
|
+
return "temporal_conflict";
|
|
4606
|
+
return "status_divergence";
|
|
4607
|
+
}
|
|
4608
|
+
var ContradictionDetector = class {
|
|
4609
|
+
detect(store) {
|
|
4610
|
+
const nodes = KNOWLEDGE_NODE_TYPES.flatMap((t) => store.findNodes({ type: t }));
|
|
4611
|
+
const byName = /* @__PURE__ */ new Map();
|
|
4612
|
+
for (const node of nodes) {
|
|
4613
|
+
const key = node.name.toLowerCase().trim();
|
|
4614
|
+
const group = byName.get(key) ?? [];
|
|
4615
|
+
group.push(node);
|
|
4616
|
+
byName.set(key, group);
|
|
4617
|
+
}
|
|
4618
|
+
const contradictions = [];
|
|
4619
|
+
const sourcePairCounts = {};
|
|
4620
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4621
|
+
const addContradiction = (a, b, similarity) => {
|
|
4622
|
+
const sourceA = a.metadata.source ?? "unknown";
|
|
4623
|
+
const sourceB = b.metadata.source ?? "unknown";
|
|
4624
|
+
if (sourceA === sourceB) return;
|
|
4625
|
+
const hashA = a.hash ?? a.id;
|
|
4626
|
+
const hashB = b.hash ?? b.id;
|
|
4627
|
+
if (hashA === hashB) return;
|
|
4628
|
+
const pairId = [a.id, b.id].sort().join(":");
|
|
4629
|
+
if (seen.has(pairId)) return;
|
|
4630
|
+
seen.add(pairId);
|
|
4631
|
+
const conflictType = classifyConflict(a, b);
|
|
4632
|
+
const severity = conflictType === "value_mismatch" ? "critical" : conflictType === "definition_conflict" ? "high" : "medium";
|
|
4633
|
+
const pairKey = [sourceA, sourceB].sort().join("\u2194");
|
|
4634
|
+
sourcePairCounts[pairKey] = (sourcePairCounts[pairKey] ?? 0) + 1;
|
|
4635
|
+
contradictions.push({
|
|
4636
|
+
id: `contradiction:${a.id}:${b.id}`,
|
|
4637
|
+
entityA: {
|
|
4638
|
+
nodeId: a.id,
|
|
4639
|
+
source: sourceA,
|
|
4640
|
+
name: a.name,
|
|
4641
|
+
content: a.content ?? "",
|
|
4642
|
+
lastModified: a.lastModified
|
|
4643
|
+
},
|
|
4644
|
+
entityB: {
|
|
4645
|
+
nodeId: b.id,
|
|
4646
|
+
source: sourceB,
|
|
4647
|
+
name: b.name,
|
|
4648
|
+
content: b.content ?? "",
|
|
4649
|
+
lastModified: b.lastModified
|
|
4650
|
+
},
|
|
4651
|
+
similarity,
|
|
4652
|
+
conflictType,
|
|
4653
|
+
severity,
|
|
4654
|
+
description: `"${a.name}" has conflicting definitions from ${sourceA} and ${sourceB}`
|
|
4655
|
+
});
|
|
4656
|
+
};
|
|
4657
|
+
for (const [, group] of byName) {
|
|
4658
|
+
if (group.length < 2) continue;
|
|
4659
|
+
for (let i = 0; i < group.length; i++) {
|
|
4660
|
+
for (let j = i + 1; j < group.length; j++) {
|
|
4661
|
+
addContradiction(group[i], group[j], 1);
|
|
4662
|
+
}
|
|
4663
|
+
}
|
|
4664
|
+
}
|
|
4665
|
+
const keys = [...byName.keys()];
|
|
4666
|
+
for (let i = 0; i < keys.length; i++) {
|
|
4667
|
+
for (let j = i + 1; j < keys.length; j++) {
|
|
4668
|
+
const keyA = keys[i];
|
|
4669
|
+
const keyB = keys[j];
|
|
4670
|
+
const maxLen = Math.max(keyA.length, keyB.length);
|
|
4671
|
+
const minLen = Math.min(keyA.length, keyB.length);
|
|
4672
|
+
if (minLen / maxLen < SIMILARITY_THRESHOLD) continue;
|
|
4673
|
+
const ratio = levenshteinRatio(keyA, keyB);
|
|
4674
|
+
if (ratio < SIMILARITY_THRESHOLD) continue;
|
|
4675
|
+
const groupA = byName.get(keyA);
|
|
4676
|
+
const groupB = byName.get(keyB);
|
|
4677
|
+
for (const a of groupA) {
|
|
4678
|
+
for (const b of groupB) {
|
|
4679
|
+
addContradiction(a, b, ratio);
|
|
4680
|
+
}
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
4683
|
+
}
|
|
4684
|
+
return { contradictions, sourcePairCounts, totalChecked: nodes.length };
|
|
4685
|
+
}
|
|
4686
|
+
};
|
|
4687
|
+
|
|
4688
|
+
// src/ingest/CoverageScorer.ts
|
|
4689
|
+
var KNOWLEDGE_TYPES = KNOWLEDGE_NODE_TYPES;
|
|
4690
|
+
var CODE_TYPES2 = [
|
|
4691
|
+
"file",
|
|
4692
|
+
"function",
|
|
4693
|
+
"class",
|
|
4694
|
+
"method",
|
|
4695
|
+
"interface",
|
|
4696
|
+
"variable"
|
|
4697
|
+
];
|
|
4698
|
+
var KNOWLEDGE_EDGE_TYPES = [
|
|
4699
|
+
"governs",
|
|
4700
|
+
"documents",
|
|
4701
|
+
"measures",
|
|
4702
|
+
"applies_to",
|
|
4703
|
+
"references",
|
|
4704
|
+
"uses_token",
|
|
4705
|
+
"declares_intent",
|
|
4706
|
+
"annotates"
|
|
4707
|
+
];
|
|
4708
|
+
function toGrade(score) {
|
|
4709
|
+
if (score >= 80) return "A";
|
|
4710
|
+
if (score >= 60) return "B";
|
|
4711
|
+
if (score >= 40) return "C";
|
|
4712
|
+
if (score >= 20) return "D";
|
|
4713
|
+
return "F";
|
|
4714
|
+
}
|
|
4715
|
+
var CoverageScorer = class {
|
|
4716
|
+
score(store) {
|
|
4717
|
+
const knowledgeNodes = KNOWLEDGE_TYPES.flatMap((t) => store.findNodes({ type: t }));
|
|
4718
|
+
const domainMap = /* @__PURE__ */ new Map();
|
|
4719
|
+
for (const node of knowledgeNodes) {
|
|
4720
|
+
const domain = node.metadata.domain ?? "unclassified";
|
|
4721
|
+
const group = domainMap.get(domain) ?? [];
|
|
4722
|
+
group.push(node);
|
|
4723
|
+
domainMap.set(domain, group);
|
|
4724
|
+
}
|
|
4725
|
+
const codeNodes = CODE_TYPES2.flatMap((t) => store.findNodes({ type: t }));
|
|
4726
|
+
const codeDomains = /* @__PURE__ */ new Map();
|
|
4727
|
+
for (const node of codeNodes) {
|
|
4728
|
+
const domain = node.metadata.domain ?? this.domainFromPath(node.path);
|
|
4729
|
+
const group = codeDomains.get(domain) ?? [];
|
|
4730
|
+
group.push(node);
|
|
4731
|
+
codeDomains.set(domain, group);
|
|
4732
|
+
}
|
|
4733
|
+
const allDomains = /* @__PURE__ */ new Set([...domainMap.keys(), ...codeDomains.keys()]);
|
|
4734
|
+
const domains = [];
|
|
4735
|
+
for (const domain of allDomains) {
|
|
4736
|
+
const knEntries = domainMap.get(domain) ?? [];
|
|
4737
|
+
const codeEntries = codeDomains.get(domain) ?? [];
|
|
4738
|
+
const linkedIds = /* @__PURE__ */ new Set();
|
|
4739
|
+
for (const codeNode of codeEntries) {
|
|
4740
|
+
for (const edgeType of KNOWLEDGE_EDGE_TYPES) {
|
|
4741
|
+
const edges = store.getEdges({ to: codeNode.id, type: edgeType });
|
|
4742
|
+
if (edges.length > 0) {
|
|
4743
|
+
linkedIds.add(codeNode.id);
|
|
4744
|
+
break;
|
|
4745
|
+
}
|
|
4746
|
+
}
|
|
4747
|
+
}
|
|
4748
|
+
const sourceBreakdown = {};
|
|
4749
|
+
for (const kn of knEntries) {
|
|
4750
|
+
const src = kn.metadata.source ?? "unknown";
|
|
4751
|
+
sourceBreakdown[src] = (sourceBreakdown[src] ?? 0) + 1;
|
|
4752
|
+
}
|
|
4753
|
+
const codeEntities = codeEntries.length;
|
|
4754
|
+
const linkedEntities = linkedIds.size;
|
|
4755
|
+
const knowledgeEntries = knEntries.length;
|
|
4756
|
+
const uniqueSources = Object.keys(sourceBreakdown).length;
|
|
4757
|
+
const codeCoverageComponent = codeEntities > 0 ? linkedEntities / codeEntities * 60 : 0;
|
|
4758
|
+
const knowledgeDepthComponent = Math.min(knowledgeEntries / 10, 1) * 20;
|
|
4759
|
+
const sourceDiversityComponent = Math.min(uniqueSources / 3, 1) * 20;
|
|
4760
|
+
const score = Math.round(
|
|
4761
|
+
codeCoverageComponent + knowledgeDepthComponent + sourceDiversityComponent
|
|
4762
|
+
);
|
|
4763
|
+
domains.push({
|
|
4764
|
+
domain,
|
|
4765
|
+
score,
|
|
4766
|
+
knowledgeEntries,
|
|
4767
|
+
codeEntities,
|
|
4768
|
+
linkedEntities,
|
|
4769
|
+
unlinkedEntities: codeEntities - linkedEntities,
|
|
4770
|
+
sourceBreakdown,
|
|
4771
|
+
grade: toGrade(score)
|
|
4772
|
+
});
|
|
4773
|
+
}
|
|
4774
|
+
const overallScore = domains.length > 0 ? Math.round(domains.reduce((sum, d) => sum + d.score, 0) / domains.length) : 0;
|
|
4775
|
+
return {
|
|
4776
|
+
domains,
|
|
4777
|
+
overallScore,
|
|
4778
|
+
overallGrade: toGrade(overallScore),
|
|
4779
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4780
|
+
};
|
|
4781
|
+
}
|
|
4782
|
+
domainFromPath(filePath) {
|
|
4783
|
+
if (!filePath) return "unclassified";
|
|
4784
|
+
const parts = filePath.split("/");
|
|
4785
|
+
const pkgIdx = parts.indexOf("packages");
|
|
4786
|
+
if (pkgIdx >= 0 && parts[pkgIdx + 1]) return parts[pkgIdx + 1];
|
|
4787
|
+
const srcIdx = parts.indexOf("src");
|
|
4788
|
+
if (srcIdx >= 0 && parts[srcIdx + 1]) return parts[srcIdx + 1];
|
|
4789
|
+
return parts[0] ?? "unclassified";
|
|
4790
|
+
}
|
|
4791
|
+
};
|
|
4792
|
+
|
|
4793
|
+
// src/ingest/KnowledgePipelineRunner.ts
|
|
4794
|
+
var BUSINESS_NODE_TYPES = [
|
|
4795
|
+
"business_concept",
|
|
4796
|
+
"business_rule",
|
|
4797
|
+
"business_process",
|
|
4798
|
+
"business_term",
|
|
4799
|
+
"business_metric",
|
|
4800
|
+
"business_fact"
|
|
4801
|
+
];
|
|
4802
|
+
var SNAPSHOT_NODE_TYPES = [
|
|
4803
|
+
...BUSINESS_NODE_TYPES,
|
|
4804
|
+
"design_token",
|
|
4805
|
+
"design_constraint",
|
|
4806
|
+
"aesthetic_intent",
|
|
4807
|
+
"image_annotation"
|
|
4808
|
+
];
|
|
4809
|
+
var KnowledgePipelineRunner = class {
|
|
4810
|
+
constructor(store) {
|
|
4811
|
+
this.store = store;
|
|
4812
|
+
}
|
|
4813
|
+
store;
|
|
4814
|
+
async run(options) {
|
|
4815
|
+
const maxIterations = options.maxIterations ?? 5;
|
|
4816
|
+
const remediations = [];
|
|
4817
|
+
const preSnapshot = this.buildSnapshot(options.domain);
|
|
4818
|
+
const extraction = await this.extract(options);
|
|
4819
|
+
const postSnapshot = this.buildSnapshot(options.domain);
|
|
4820
|
+
let driftResult = this.reconcile(preSnapshot, postSnapshot);
|
|
4821
|
+
const contradictionDetector = new ContradictionDetector();
|
|
4822
|
+
const contradictions = contradictionDetector.detect(this.store);
|
|
4823
|
+
let gapReport = await this.detect(options);
|
|
4824
|
+
const coverageScorer = new CoverageScorer();
|
|
4825
|
+
const coverage = coverageScorer.score(this.store);
|
|
4826
|
+
let iterations = 1;
|
|
4827
|
+
if (options.fix) {
|
|
4828
|
+
let previousFindingCount = driftResult.findings.length;
|
|
4829
|
+
while (iterations < maxIterations) {
|
|
4830
|
+
if (driftResult.findings.length === 0) break;
|
|
4831
|
+
this.remediate(driftResult, remediations, options);
|
|
4832
|
+
const rePreSnapshot = this.buildSnapshot(options.domain);
|
|
4833
|
+
await this.extract(options);
|
|
4834
|
+
const rePostSnapshot = this.buildSnapshot(options.domain);
|
|
4835
|
+
driftResult = this.reconcile(rePreSnapshot, rePostSnapshot);
|
|
4836
|
+
gapReport = await this.detect(options);
|
|
4837
|
+
iterations++;
|
|
4838
|
+
const newFindingCount = driftResult.findings.length;
|
|
4839
|
+
if (newFindingCount >= previousFindingCount) break;
|
|
4840
|
+
previousFindingCount = newFindingCount;
|
|
4841
|
+
}
|
|
4842
|
+
}
|
|
4843
|
+
await this.stageNewFindings(driftResult, options);
|
|
4844
|
+
return {
|
|
4845
|
+
verdict: this.computeVerdict(driftResult),
|
|
4846
|
+
driftScore: driftResult.driftScore,
|
|
4847
|
+
iterations,
|
|
4848
|
+
findings: driftResult.summary,
|
|
4849
|
+
extraction,
|
|
4850
|
+
gaps: gapReport,
|
|
4851
|
+
remediations,
|
|
4852
|
+
contradictions,
|
|
4853
|
+
coverage
|
|
4854
|
+
};
|
|
4855
|
+
}
|
|
4856
|
+
// ── Phase 1: EXTRACT ──────────────────────────────────────────────────────
|
|
4857
|
+
async extract(options) {
|
|
4858
|
+
const extractedDir = path10.join(options.projectDir, ".harness", "knowledge", "extracted");
|
|
4859
|
+
await fs9.mkdir(extractedDir, { recursive: true });
|
|
4860
|
+
const runner = createExtractionRunner();
|
|
4861
|
+
const extractionResult = await runner.run(options.projectDir, this.store, extractedDir);
|
|
4862
|
+
const diagramParser = new DiagramParser(this.store);
|
|
4863
|
+
const diagramResult = await diagramParser.ingest(options.projectDir);
|
|
4864
|
+
let imageCount = 0;
|
|
4865
|
+
const imagePaths = options.imagePaths ?? [];
|
|
4866
|
+
if (options.analyzeImages && options.analysisProvider && imagePaths.length > 0) {
|
|
4867
|
+
const imageExtractor = new ImageAnalysisExtractor({
|
|
4868
|
+
analysisProvider: options.analysisProvider
|
|
4869
|
+
});
|
|
4870
|
+
const imageResult = await imageExtractor.analyze(this.store, imagePaths);
|
|
4871
|
+
imageCount = imageResult.nodesAdded;
|
|
4872
|
+
}
|
|
4873
|
+
const knowledgeDir = path10.join(options.projectDir, "docs", "knowledge");
|
|
4874
|
+
const bkIngestor = new BusinessKnowledgeIngestor(this.store);
|
|
4875
|
+
let bkResult;
|
|
4876
|
+
try {
|
|
4877
|
+
bkResult = await bkIngestor.ingest(knowledgeDir);
|
|
4878
|
+
} catch {
|
|
4879
|
+
bkResult = {
|
|
4880
|
+
nodesAdded: 0,
|
|
4881
|
+
nodesUpdated: 0,
|
|
4882
|
+
edgesAdded: 0,
|
|
4883
|
+
edgesUpdated: 0,
|
|
4884
|
+
errors: [],
|
|
4885
|
+
durationMs: 0
|
|
4886
|
+
};
|
|
4887
|
+
}
|
|
4888
|
+
const linker = new KnowledgeLinker(this.store, extractedDir);
|
|
4889
|
+
const linkResult = await linker.link();
|
|
4890
|
+
return {
|
|
4891
|
+
codeSignals: extractionResult.nodesAdded,
|
|
4892
|
+
diagrams: diagramResult.nodesAdded,
|
|
4893
|
+
linkerFacts: linkResult.factsCreated,
|
|
4894
|
+
businessKnowledge: bkResult.nodesAdded,
|
|
4895
|
+
images: imageCount
|
|
4896
|
+
};
|
|
1800
4897
|
}
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
4898
|
+
// ── Phase 2: RECONCILE ────────────────────────────────────────────────────
|
|
4899
|
+
buildSnapshot(domain) {
|
|
4900
|
+
let nodes = SNAPSHOT_NODE_TYPES.flatMap((type) => this.store.findNodes({ type }));
|
|
4901
|
+
if (domain) {
|
|
4902
|
+
nodes = nodes.filter((n) => n.metadata?.domain === domain);
|
|
4903
|
+
}
|
|
4904
|
+
return {
|
|
4905
|
+
entries: nodes.map((n) => ({
|
|
4906
|
+
id: n.id,
|
|
4907
|
+
type: n.type,
|
|
4908
|
+
contentHash: n.hash ?? n.id,
|
|
4909
|
+
source: n.metadata?.source ?? "unknown",
|
|
4910
|
+
name: n.name
|
|
4911
|
+
})),
|
|
4912
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4913
|
+
};
|
|
4914
|
+
}
|
|
4915
|
+
reconcile(current, fresh) {
|
|
4916
|
+
const detector = new StructuralDriftDetector();
|
|
4917
|
+
return detector.detect(current, fresh);
|
|
4918
|
+
}
|
|
4919
|
+
// ── Phase 3: DETECT ───────────────────────────────────────────────────────
|
|
4920
|
+
async detect(options) {
|
|
4921
|
+
const knowledgeDir = path10.join(options.projectDir, "docs", "knowledge");
|
|
4922
|
+
const aggregator = new KnowledgeStagingAggregator(options.projectDir);
|
|
4923
|
+
const gapReport = await aggregator.generateGapReport(knowledgeDir);
|
|
4924
|
+
await aggregator.writeGapReport(gapReport);
|
|
4925
|
+
return gapReport;
|
|
4926
|
+
}
|
|
4927
|
+
// ── Phase 4: REMEDIATE ────────────────────────────────────────────────────
|
|
4928
|
+
remediate(driftResult, remediations, options) {
|
|
4929
|
+
for (const finding of driftResult.findings) {
|
|
4930
|
+
switch (finding.classification) {
|
|
4931
|
+
case "stale":
|
|
4932
|
+
this.store.removeNode(finding.entryId);
|
|
4933
|
+
remediations.push(`removed stale: ${finding.entryId}`);
|
|
4934
|
+
break;
|
|
4935
|
+
case "new":
|
|
4936
|
+
break;
|
|
4937
|
+
case "drifted":
|
|
4938
|
+
if (!options.ci) {
|
|
4939
|
+
remediations.push(`flagged drifted: ${finding.entryId}`);
|
|
4940
|
+
}
|
|
4941
|
+
break;
|
|
4942
|
+
case "contradicting":
|
|
4943
|
+
break;
|
|
1824
4944
|
}
|
|
1825
4945
|
}
|
|
1826
|
-
|
|
4946
|
+
}
|
|
4947
|
+
async stageNewFindings(driftResult, options) {
|
|
4948
|
+
const newFindings = driftResult.findings.filter((f) => f.classification === "new");
|
|
4949
|
+
if (newFindings.length === 0) return;
|
|
4950
|
+
const stagedEntries = newFindings.filter((f) => f.fresh != null).map((f) => ({
|
|
4951
|
+
id: f.fresh.id,
|
|
4952
|
+
source: this.classifySource(f.fresh.source),
|
|
4953
|
+
nodeType: f.fresh.type,
|
|
4954
|
+
name: f.fresh.name,
|
|
4955
|
+
confidence: 0.7,
|
|
4956
|
+
contentHash: f.fresh.contentHash,
|
|
4957
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4958
|
+
}));
|
|
4959
|
+
if (stagedEntries.length > 0) {
|
|
4960
|
+
const aggregator = new KnowledgeStagingAggregator(options.projectDir);
|
|
4961
|
+
await aggregator.aggregate(stagedEntries, [], []);
|
|
4962
|
+
}
|
|
4963
|
+
}
|
|
4964
|
+
classifySource(source) {
|
|
4965
|
+
if (source === "linker" || source === "knowledge-linker") return "linker";
|
|
4966
|
+
if (source === "diagram") return "diagram";
|
|
4967
|
+
return "extractor";
|
|
4968
|
+
}
|
|
4969
|
+
// ── Verdict ───────────────────────────────────────────────────────────────
|
|
4970
|
+
computeVerdict(driftResult) {
|
|
4971
|
+
const { summary } = driftResult;
|
|
4972
|
+
const unresolved = summary.drifted + summary.stale + summary.contradicting;
|
|
4973
|
+
if (unresolved === 0 && summary.new === 0) return "pass";
|
|
4974
|
+
if (unresolved === 0) return "warn";
|
|
4975
|
+
return "fail";
|
|
1827
4976
|
}
|
|
1828
4977
|
};
|
|
1829
4978
|
|
|
1830
4979
|
// src/ingest/connectors/ConnectorUtils.ts
|
|
1831
|
-
var
|
|
4980
|
+
var CODE_NODE_TYPES4 = ["file", "function", "class", "method", "interface", "variable"];
|
|
4981
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
4982
|
+
function withRetry(client, options) {
|
|
4983
|
+
const maxRetries = options?.maxRetries ?? 3;
|
|
4984
|
+
const baseDelayMs = options?.baseDelayMs ?? 1e3;
|
|
4985
|
+
const maxDelayMs = options?.maxDelayMs ?? 3e4;
|
|
4986
|
+
return async (url, requestOptions) => {
|
|
4987
|
+
let lastError;
|
|
4988
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
4989
|
+
try {
|
|
4990
|
+
const response = await client(url, requestOptions);
|
|
4991
|
+
if (response.ok || !RETRYABLE_STATUSES.has(response.status ?? 0)) {
|
|
4992
|
+
return response;
|
|
4993
|
+
}
|
|
4994
|
+
if (attempt === maxRetries) {
|
|
4995
|
+
return response;
|
|
4996
|
+
}
|
|
4997
|
+
} catch (err) {
|
|
4998
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
4999
|
+
if (attempt === maxRetries) {
|
|
5000
|
+
throw lastError;
|
|
5001
|
+
}
|
|
5002
|
+
}
|
|
5003
|
+
const exponentialDelay = baseDelayMs * 2 ** attempt;
|
|
5004
|
+
const cappedDelay = Math.min(exponentialDelay, maxDelayMs);
|
|
5005
|
+
const jitteredDelay = cappedDelay * (0.5 + Math.random() * 0.5);
|
|
5006
|
+
await new Promise((resolve) => setTimeout(resolve, jitteredDelay));
|
|
5007
|
+
}
|
|
5008
|
+
throw lastError ?? new Error("Request failed after retries");
|
|
5009
|
+
};
|
|
5010
|
+
}
|
|
1832
5011
|
var SANITIZE_RULES = [
|
|
1833
5012
|
// Strip XML/HTML-like instruction tags that could be interpreted as system prompts
|
|
1834
5013
|
{
|
|
@@ -1863,7 +5042,7 @@ function sanitizeExternalText(text, maxLength = 2e3) {
|
|
|
1863
5042
|
}
|
|
1864
5043
|
function linkToCode(store, content, sourceNodeId, edgeType, options) {
|
|
1865
5044
|
let edgesCreated = 0;
|
|
1866
|
-
for (const type of
|
|
5045
|
+
for (const type of CODE_NODE_TYPES4) {
|
|
1867
5046
|
const nodes = store.findNodes({ type });
|
|
1868
5047
|
for (const node of nodes) {
|
|
1869
5048
|
if (node.name.length < 3) continue;
|
|
@@ -1883,12 +5062,12 @@ function linkToCode(store, content, sourceNodeId, edgeType, options) {
|
|
|
1883
5062
|
}
|
|
1884
5063
|
|
|
1885
5064
|
// src/ingest/connectors/SyncManager.ts
|
|
1886
|
-
var
|
|
1887
|
-
var
|
|
5065
|
+
var fs10 = __toESM(require("fs/promises"));
|
|
5066
|
+
var path11 = __toESM(require("path"));
|
|
1888
5067
|
var SyncManager = class {
|
|
1889
5068
|
constructor(store, graphDir) {
|
|
1890
5069
|
this.store = store;
|
|
1891
|
-
this.metadataPath =
|
|
5070
|
+
this.metadataPath = path11.join(graphDir, "sync-metadata.json");
|
|
1892
5071
|
}
|
|
1893
5072
|
store;
|
|
1894
5073
|
registrations = /* @__PURE__ */ new Map();
|
|
@@ -1936,6 +5115,16 @@ var SyncManager = class {
|
|
|
1936
5115
|
combined.errors.push(...result.errors);
|
|
1937
5116
|
combined.durationMs += result.durationMs;
|
|
1938
5117
|
}
|
|
5118
|
+
try {
|
|
5119
|
+
const linker = new KnowledgeLinker(this.store);
|
|
5120
|
+
const linkResult = await linker.link();
|
|
5121
|
+
combined.nodesAdded += linkResult.factsCreated + linkResult.conceptsClustered;
|
|
5122
|
+
combined.errors.push(...linkResult.errors);
|
|
5123
|
+
} catch (err) {
|
|
5124
|
+
combined.errors.push(
|
|
5125
|
+
`KnowledgeLinker error: ${err instanceof Error ? err.message : String(err)}`
|
|
5126
|
+
);
|
|
5127
|
+
}
|
|
1939
5128
|
return combined;
|
|
1940
5129
|
}
|
|
1941
5130
|
async getMetadata() {
|
|
@@ -1943,18 +5132,64 @@ var SyncManager = class {
|
|
|
1943
5132
|
}
|
|
1944
5133
|
async loadMetadata() {
|
|
1945
5134
|
try {
|
|
1946
|
-
const raw = await
|
|
5135
|
+
const raw = await fs10.readFile(this.metadataPath, "utf-8");
|
|
1947
5136
|
return JSON.parse(raw);
|
|
1948
5137
|
} catch {
|
|
1949
5138
|
return { connectors: {} };
|
|
1950
5139
|
}
|
|
1951
5140
|
}
|
|
1952
5141
|
async saveMetadata(metadata) {
|
|
1953
|
-
await
|
|
1954
|
-
await
|
|
5142
|
+
await fs10.mkdir(path11.dirname(this.metadataPath), { recursive: true });
|
|
5143
|
+
await fs10.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
1955
5144
|
}
|
|
1956
5145
|
};
|
|
1957
5146
|
|
|
5147
|
+
// src/ingest/connectors/ContentCondenser.ts
|
|
5148
|
+
var SUMMARIZE_PROMPT = `Summarize the following content to fit within the specified length.
|
|
5149
|
+
Preserve all business rules, SLAs, requirements, decisions, and regulatory references.
|
|
5150
|
+
Remove redundant details and boilerplate while keeping all actionable information.
|
|
5151
|
+
|
|
5152
|
+
Content to summarize:
|
|
5153
|
+
`;
|
|
5154
|
+
async function condenseContent(raw, options, summarizeFn) {
|
|
5155
|
+
const originalLength = raw.length;
|
|
5156
|
+
const { maxLength } = options;
|
|
5157
|
+
const summarizationThreshold = options.summarizationThreshold ?? maxLength * 2;
|
|
5158
|
+
if (raw.length <= maxLength) {
|
|
5159
|
+
return { content: raw, method: "passthrough", originalLength };
|
|
5160
|
+
}
|
|
5161
|
+
if (raw.length < summarizationThreshold) {
|
|
5162
|
+
return {
|
|
5163
|
+
content: sanitizeExternalText(raw, maxLength),
|
|
5164
|
+
method: "truncated",
|
|
5165
|
+
originalLength
|
|
5166
|
+
};
|
|
5167
|
+
}
|
|
5168
|
+
if (options.modelEndpoint && summarizeFn) {
|
|
5169
|
+
try {
|
|
5170
|
+
const summarized = await summarizeFn(SUMMARIZE_PROMPT + raw, {
|
|
5171
|
+
endpoint: options.modelEndpoint,
|
|
5172
|
+
model: options.modelName ?? "default",
|
|
5173
|
+
maxTokens: Math.ceil(maxLength / 4)
|
|
5174
|
+
// rough token estimate
|
|
5175
|
+
});
|
|
5176
|
+
const finalContent = summarized.length > maxLength ? sanitizeExternalText(summarized, maxLength) : summarized;
|
|
5177
|
+
return { content: finalContent, method: "summarized", originalLength };
|
|
5178
|
+
} catch {
|
|
5179
|
+
return {
|
|
5180
|
+
content: sanitizeExternalText(raw, maxLength),
|
|
5181
|
+
method: "truncated",
|
|
5182
|
+
originalLength
|
|
5183
|
+
};
|
|
5184
|
+
}
|
|
5185
|
+
}
|
|
5186
|
+
return {
|
|
5187
|
+
content: sanitizeExternalText(raw, maxLength),
|
|
5188
|
+
method: "truncated",
|
|
5189
|
+
originalLength
|
|
5190
|
+
};
|
|
5191
|
+
}
|
|
5192
|
+
|
|
1958
5193
|
// src/ingest/connectors/JiraConnector.ts
|
|
1959
5194
|
function buildIngestResult(nodesAdded, edgesAdded, errors, start) {
|
|
1960
5195
|
return {
|
|
@@ -1986,7 +5221,7 @@ var JiraConnector = class {
|
|
|
1986
5221
|
source = "jira";
|
|
1987
5222
|
httpClient;
|
|
1988
5223
|
constructor(httpClient) {
|
|
1989
|
-
this.httpClient = httpClient ?? ((url, options) => fetch(url, options));
|
|
5224
|
+
this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
|
|
1990
5225
|
}
|
|
1991
5226
|
async ingest(store, config) {
|
|
1992
5227
|
const start = Date.now();
|
|
@@ -2013,7 +5248,7 @@ var JiraConnector = class {
|
|
|
2013
5248
|
const jql = buildJql(config);
|
|
2014
5249
|
const headers = { Authorization: `Basic ${apiKey}`, "Content-Type": "application/json" };
|
|
2015
5250
|
try {
|
|
2016
|
-
const counts = await this.fetchAllIssues(store, baseUrl, jql, headers);
|
|
5251
|
+
const counts = await this.fetchAllIssues(store, baseUrl, jql, headers, config);
|
|
2017
5252
|
return buildIngestResult(counts.nodesAdded, counts.edgesAdded, [], start);
|
|
2018
5253
|
} catch (err) {
|
|
2019
5254
|
return buildIngestResult(
|
|
@@ -2024,7 +5259,7 @@ var JiraConnector = class {
|
|
|
2024
5259
|
);
|
|
2025
5260
|
}
|
|
2026
5261
|
}
|
|
2027
|
-
async fetchAllIssues(store, baseUrl, jql, headers) {
|
|
5262
|
+
async fetchAllIssues(store, baseUrl, jql, headers, config) {
|
|
2028
5263
|
let nodesAdded = 0;
|
|
2029
5264
|
let edgesAdded = 0;
|
|
2030
5265
|
let startAt = 0;
|
|
@@ -2037,7 +5272,7 @@ var JiraConnector = class {
|
|
|
2037
5272
|
const data = await response.json();
|
|
2038
5273
|
total = data.total;
|
|
2039
5274
|
for (const issue of data.issues) {
|
|
2040
|
-
const counts = this.processIssue(store, issue);
|
|
5275
|
+
const counts = await this.processIssue(store, issue, baseUrl, headers, config);
|
|
2041
5276
|
nodesAdded += counts.nodesAdded;
|
|
2042
5277
|
edgesAdded += counts.edgesAdded;
|
|
2043
5278
|
}
|
|
@@ -2045,19 +5280,47 @@ var JiraConnector = class {
|
|
|
2045
5280
|
}
|
|
2046
5281
|
return { nodesAdded, edgesAdded };
|
|
2047
5282
|
}
|
|
2048
|
-
processIssue(store, issue) {
|
|
5283
|
+
async processIssue(store, issue, baseUrl, headers, config) {
|
|
2049
5284
|
const nodeId = `issue:jira:${issue.key}`;
|
|
5285
|
+
const comments = await this.fetchComments(baseUrl, issue.key, headers);
|
|
5286
|
+
const parts = [issue.fields.summary];
|
|
5287
|
+
if (issue.fields.description) {
|
|
5288
|
+
parts.push(issue.fields.description);
|
|
5289
|
+
}
|
|
5290
|
+
if (comments.length > 0) {
|
|
5291
|
+
for (const comment of comments) {
|
|
5292
|
+
parts.push(`${comment.author.displayName} (${comment.created}): ${comment.body}`);
|
|
5293
|
+
}
|
|
5294
|
+
}
|
|
5295
|
+
const rawContent = parts.join("\n");
|
|
5296
|
+
const acceptanceCriteria = parseAcceptanceCriteria(issue.fields.description ?? "");
|
|
5297
|
+
const customFields = extractCustomFields(issue.fields);
|
|
5298
|
+
const maxLen = config.maxContentLength ?? 4e3;
|
|
5299
|
+
const condensed = await condenseContent(rawContent, { maxLength: maxLen });
|
|
5300
|
+
const metadata = {
|
|
5301
|
+
key: issue.key,
|
|
5302
|
+
status: issue.fields.status?.name,
|
|
5303
|
+
priority: issue.fields.priority?.name,
|
|
5304
|
+
assignee: issue.fields.assignee?.displayName,
|
|
5305
|
+
labels: issue.fields.labels ?? [],
|
|
5306
|
+
commentCount: comments.length
|
|
5307
|
+
};
|
|
5308
|
+
if (acceptanceCriteria.length > 0) {
|
|
5309
|
+
metadata.acceptanceCriteria = acceptanceCriteria;
|
|
5310
|
+
}
|
|
5311
|
+
if (Object.keys(customFields).length > 0) {
|
|
5312
|
+
metadata.customFields = customFields;
|
|
5313
|
+
}
|
|
5314
|
+
if (condensed.method !== "passthrough") {
|
|
5315
|
+
metadata.condensed = condensed.method;
|
|
5316
|
+
metadata.originalLength = condensed.originalLength;
|
|
5317
|
+
}
|
|
2050
5318
|
store.addNode({
|
|
2051
5319
|
id: nodeId,
|
|
2052
5320
|
type: "issue",
|
|
2053
5321
|
name: sanitizeExternalText(issue.fields.summary, 500),
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
status: issue.fields.status?.name,
|
|
2057
|
-
priority: issue.fields.priority?.name,
|
|
2058
|
-
assignee: issue.fields.assignee?.displayName,
|
|
2059
|
-
labels: issue.fields.labels ?? []
|
|
2060
|
-
}
|
|
5322
|
+
content: condensed.content,
|
|
5323
|
+
metadata
|
|
2061
5324
|
});
|
|
2062
5325
|
const searchText = sanitizeExternalText(
|
|
2063
5326
|
[issue.fields.summary, issue.fields.description ?? ""].join(" ")
|
|
@@ -2065,7 +5328,40 @@ var JiraConnector = class {
|
|
|
2065
5328
|
const edgesAdded = linkToCode(store, searchText, nodeId, "applies_to");
|
|
2066
5329
|
return { nodesAdded: 1, edgesAdded };
|
|
2067
5330
|
}
|
|
5331
|
+
async fetchComments(baseUrl, issueKey, headers) {
|
|
5332
|
+
try {
|
|
5333
|
+
const url = `${baseUrl}/rest/api/2/issue/${issueKey}/comment`;
|
|
5334
|
+
const response = await this.httpClient(url, { headers });
|
|
5335
|
+
if (!response.ok) return [];
|
|
5336
|
+
const data = await response.json();
|
|
5337
|
+
return data.comments ?? [];
|
|
5338
|
+
} catch {
|
|
5339
|
+
return [];
|
|
5340
|
+
}
|
|
5341
|
+
}
|
|
2068
5342
|
};
|
|
5343
|
+
function parseAcceptanceCriteria(description) {
|
|
5344
|
+
const criteria = [];
|
|
5345
|
+
const checkboxRegex = /[-*]\s*\[[ x]\]\s*(.+)/gi;
|
|
5346
|
+
let match;
|
|
5347
|
+
while ((match = checkboxRegex.exec(description)) !== null) {
|
|
5348
|
+
criteria.push(match[1].trim());
|
|
5349
|
+
}
|
|
5350
|
+
const gwtRegex = /Given\b.+?When\b.+?Then\b.+/gi;
|
|
5351
|
+
while ((match = gwtRegex.exec(description)) !== null) {
|
|
5352
|
+
criteria.push(match[0].trim());
|
|
5353
|
+
}
|
|
5354
|
+
return criteria;
|
|
5355
|
+
}
|
|
5356
|
+
function extractCustomFields(fields) {
|
|
5357
|
+
const result = {};
|
|
5358
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
5359
|
+
if (key.startsWith("customfield_") && value != null && typeof value === "string") {
|
|
5360
|
+
result[key] = value;
|
|
5361
|
+
}
|
|
5362
|
+
}
|
|
5363
|
+
return result;
|
|
5364
|
+
}
|
|
2069
5365
|
|
|
2070
5366
|
// src/ingest/connectors/SlackConnector.ts
|
|
2071
5367
|
var SlackConnector = class {
|
|
@@ -2073,7 +5369,7 @@ var SlackConnector = class {
|
|
|
2073
5369
|
source = "slack";
|
|
2074
5370
|
httpClient;
|
|
2075
5371
|
constructor(httpClient) {
|
|
2076
|
-
this.httpClient = httpClient ?? ((url, options) => fetch(url, options));
|
|
5372
|
+
this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
|
|
2077
5373
|
}
|
|
2078
5374
|
async ingest(store, config) {
|
|
2079
5375
|
const start = Date.now();
|
|
@@ -2096,7 +5392,7 @@ var SlackConnector = class {
|
|
|
2096
5392
|
const oldest = config.lookbackDays ? String(Math.floor((Date.now() - Number(config.lookbackDays) * 864e5) / 1e3)) : void 0;
|
|
2097
5393
|
for (const channel of channels) {
|
|
2098
5394
|
try {
|
|
2099
|
-
const result = await this.processChannel(store, channel, apiKey, oldest);
|
|
5395
|
+
const result = await this.processChannel(store, channel, apiKey, oldest, config);
|
|
2100
5396
|
nodesAdded += result.nodesAdded;
|
|
2101
5397
|
edgesAdded += result.edgesAdded;
|
|
2102
5398
|
errors.push(...result.errors);
|
|
@@ -2115,7 +5411,7 @@ var SlackConnector = class {
|
|
|
2115
5411
|
durationMs: Date.now() - start
|
|
2116
5412
|
};
|
|
2117
5413
|
}
|
|
2118
|
-
async processChannel(store, channel, apiKey, oldest) {
|
|
5414
|
+
async processChannel(store, channel, apiKey, oldest, config) {
|
|
2119
5415
|
const errors = [];
|
|
2120
5416
|
let nodesAdded = 0;
|
|
2121
5417
|
let edgesAdded = 0;
|
|
@@ -2140,27 +5436,74 @@ var SlackConnector = class {
|
|
|
2140
5436
|
if (!data.ok) {
|
|
2141
5437
|
return { nodesAdded: 0, edgesAdded: 0, errors: [`Slack API error for channel ${channel}`] };
|
|
2142
5438
|
}
|
|
5439
|
+
const maxLen = config.maxContentLength ?? 2e3;
|
|
2143
5440
|
for (const message of data.messages) {
|
|
2144
5441
|
const nodeId = `conversation:slack:${channel}:${message.ts}`;
|
|
2145
|
-
|
|
2146
|
-
|
|
5442
|
+
let assembledText = message.text;
|
|
5443
|
+
let threadReplyCount;
|
|
5444
|
+
if (message.reply_count && message.reply_count > 0 && message.thread_ts) {
|
|
5445
|
+
const replies = await this.fetchThreadReplies(channel, message.thread_ts, apiKey);
|
|
5446
|
+
if (replies.length > 0) {
|
|
5447
|
+
const threadReplies = replies.slice(1);
|
|
5448
|
+
threadReplyCount = threadReplies.length;
|
|
5449
|
+
if (threadReplies.length > 0) {
|
|
5450
|
+
const replyLines = threadReplies.map((r) => `${r.user} (${r.ts}): ${r.text}`);
|
|
5451
|
+
assembledText = `${message.text}
|
|
5452
|
+
${replyLines.join("\n")}`;
|
|
5453
|
+
}
|
|
5454
|
+
}
|
|
5455
|
+
}
|
|
5456
|
+
const condensed = await condenseContent(assembledText, { maxLength: maxLen });
|
|
5457
|
+
const snippet = condensed.content.length > 100 ? condensed.content.slice(0, 100) : condensed.content;
|
|
5458
|
+
const reactions = message.reactions ? message.reactions.reduce(
|
|
5459
|
+
(acc, r) => ({ ...acc, [r.name]: r.count }),
|
|
5460
|
+
{}
|
|
5461
|
+
) : void 0;
|
|
5462
|
+
const metadata = {
|
|
5463
|
+
author: message.user,
|
|
5464
|
+
channel,
|
|
5465
|
+
timestamp: message.ts
|
|
5466
|
+
};
|
|
5467
|
+
if (threadReplyCount !== void 0) {
|
|
5468
|
+
metadata.threadReplyCount = threadReplyCount;
|
|
5469
|
+
}
|
|
5470
|
+
if (reactions) {
|
|
5471
|
+
metadata.reactions = reactions;
|
|
5472
|
+
}
|
|
5473
|
+
if (condensed.method !== "passthrough") {
|
|
5474
|
+
metadata.condensed = condensed.method;
|
|
5475
|
+
metadata.originalLength = condensed.originalLength;
|
|
5476
|
+
}
|
|
2147
5477
|
store.addNode({
|
|
2148
5478
|
id: nodeId,
|
|
2149
5479
|
type: "conversation",
|
|
2150
5480
|
name: snippet,
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
channel,
|
|
2154
|
-
timestamp: message.ts
|
|
2155
|
-
}
|
|
5481
|
+
content: condensed.content,
|
|
5482
|
+
metadata
|
|
2156
5483
|
});
|
|
2157
5484
|
nodesAdded++;
|
|
2158
|
-
edgesAdded += linkToCode(store,
|
|
5485
|
+
edgesAdded += linkToCode(store, condensed.content, nodeId, "references", {
|
|
2159
5486
|
checkPaths: true
|
|
2160
5487
|
});
|
|
2161
5488
|
}
|
|
2162
5489
|
return { nodesAdded, edgesAdded, errors };
|
|
2163
5490
|
}
|
|
5491
|
+
async fetchThreadReplies(channel, threadTs, apiKey) {
|
|
5492
|
+
try {
|
|
5493
|
+
const url = `https://slack.com/api/conversations.replies?channel=${encodeURIComponent(channel)}&ts=${threadTs}`;
|
|
5494
|
+
const response = await this.httpClient(url, {
|
|
5495
|
+
headers: {
|
|
5496
|
+
Authorization: `Bearer ${apiKey}`,
|
|
5497
|
+
"Content-Type": "application/json"
|
|
5498
|
+
}
|
|
5499
|
+
});
|
|
5500
|
+
if (!response.ok) return [];
|
|
5501
|
+
const data = await response.json();
|
|
5502
|
+
return data.ok ? data.messages : [];
|
|
5503
|
+
} catch {
|
|
5504
|
+
return [];
|
|
5505
|
+
}
|
|
5506
|
+
}
|
|
2164
5507
|
};
|
|
2165
5508
|
|
|
2166
5509
|
// src/ingest/connectors/ConfluenceConnector.ts
|
|
@@ -2179,7 +5522,7 @@ var ConfluenceConnector = class {
|
|
|
2179
5522
|
source = "confluence";
|
|
2180
5523
|
httpClient;
|
|
2181
5524
|
constructor(httpClient) {
|
|
2182
|
-
this.httpClient = httpClient ?? ((url, options) => fetch(url, options));
|
|
5525
|
+
this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
|
|
2183
5526
|
}
|
|
2184
5527
|
async ingest(store, config) {
|
|
2185
5528
|
const start = Date.now();
|
|
@@ -2192,7 +5535,14 @@ var ConfluenceConnector = class {
|
|
|
2192
5535
|
const baseUrlEnv = config.baseUrlEnv ?? "CONFLUENCE_BASE_URL";
|
|
2193
5536
|
const baseUrl = process.env[baseUrlEnv] ?? "";
|
|
2194
5537
|
const spaceKey = config.spaceKey ?? "";
|
|
2195
|
-
const counts = await this.fetchAllPagesHandled(
|
|
5538
|
+
const counts = await this.fetchAllPagesHandled(
|
|
5539
|
+
store,
|
|
5540
|
+
baseUrl,
|
|
5541
|
+
apiKey,
|
|
5542
|
+
spaceKey,
|
|
5543
|
+
errors,
|
|
5544
|
+
config
|
|
5545
|
+
);
|
|
2196
5546
|
return {
|
|
2197
5547
|
nodesAdded: counts.nodesAdded,
|
|
2198
5548
|
nodesUpdated: 0,
|
|
@@ -2202,9 +5552,9 @@ var ConfluenceConnector = class {
|
|
|
2202
5552
|
durationMs: Date.now() - start
|
|
2203
5553
|
};
|
|
2204
5554
|
}
|
|
2205
|
-
async fetchAllPagesHandled(store, baseUrl, apiKey, spaceKey, errors) {
|
|
5555
|
+
async fetchAllPagesHandled(store, baseUrl, apiKey, spaceKey, errors, config) {
|
|
2206
5556
|
try {
|
|
2207
|
-
const result = await this.fetchAllPages(store, baseUrl, apiKey, spaceKey);
|
|
5557
|
+
const result = await this.fetchAllPages(store, baseUrl, apiKey, spaceKey, config);
|
|
2208
5558
|
errors.push(...result.errors);
|
|
2209
5559
|
return { nodesAdded: result.nodesAdded, edgesAdded: result.edgesAdded };
|
|
2210
5560
|
} catch (err) {
|
|
@@ -2212,7 +5562,7 @@ var ConfluenceConnector = class {
|
|
|
2212
5562
|
return { nodesAdded: 0, edgesAdded: 0 };
|
|
2213
5563
|
}
|
|
2214
5564
|
}
|
|
2215
|
-
async fetchAllPages(store, baseUrl, apiKey, spaceKey) {
|
|
5565
|
+
async fetchAllPages(store, baseUrl, apiKey, spaceKey, config) {
|
|
2216
5566
|
const errors = [];
|
|
2217
5567
|
let nodesAdded = 0;
|
|
2218
5568
|
let edgesAdded = 0;
|
|
@@ -2227,7 +5577,7 @@ var ConfluenceConnector = class {
|
|
|
2227
5577
|
}
|
|
2228
5578
|
const data = await response.json();
|
|
2229
5579
|
for (const page of data.results) {
|
|
2230
|
-
const counts = this.processPage(store, page, spaceKey);
|
|
5580
|
+
const counts = await this.processPage(store, page, spaceKey, config);
|
|
2231
5581
|
nodesAdded += counts.nodesAdded;
|
|
2232
5582
|
edgesAdded += counts.edgesAdded;
|
|
2233
5583
|
}
|
|
@@ -2235,22 +5585,43 @@ var ConfluenceConnector = class {
|
|
|
2235
5585
|
}
|
|
2236
5586
|
return { nodesAdded, edgesAdded, errors };
|
|
2237
5587
|
}
|
|
2238
|
-
processPage(store, page, spaceKey) {
|
|
5588
|
+
async processPage(store, page, spaceKey, config) {
|
|
2239
5589
|
const nodeId = `confluence:${page.id}`;
|
|
5590
|
+
let edgesAdded = 0;
|
|
5591
|
+
const labels = page.metadata?.labels?.results?.map((l) => l.name) ?? [];
|
|
5592
|
+
const parentPageId = page.ancestors && page.ancestors.length > 0 ? page.ancestors[page.ancestors.length - 1].id : void 0;
|
|
5593
|
+
const rawContent = `${page.title} ${page.body?.storage?.value ?? ""}`;
|
|
5594
|
+
const maxLen = config.maxContentLength ?? 8e3;
|
|
5595
|
+
const condensed = await condenseContent(rawContent, { maxLength: maxLen });
|
|
5596
|
+
const metadata = {
|
|
5597
|
+
source: "confluence",
|
|
5598
|
+
spaceKey,
|
|
5599
|
+
pageId: page.id,
|
|
5600
|
+
status: page.status,
|
|
5601
|
+
url: page._links?.webui ?? "",
|
|
5602
|
+
labels
|
|
5603
|
+
};
|
|
5604
|
+
if (parentPageId) {
|
|
5605
|
+
metadata.parentPageId = parentPageId;
|
|
5606
|
+
}
|
|
5607
|
+
if (condensed.method !== "passthrough") {
|
|
5608
|
+
metadata.condensed = condensed.method;
|
|
5609
|
+
metadata.originalLength = condensed.originalLength;
|
|
5610
|
+
}
|
|
2240
5611
|
store.addNode({
|
|
2241
5612
|
id: nodeId,
|
|
2242
5613
|
type: "document",
|
|
2243
5614
|
name: sanitizeExternalText(page.title, 500),
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
spaceKey,
|
|
2247
|
-
pageId: page.id,
|
|
2248
|
-
status: page.status,
|
|
2249
|
-
url: page._links?.webui ?? ""
|
|
2250
|
-
}
|
|
5615
|
+
content: condensed.content,
|
|
5616
|
+
metadata
|
|
2251
5617
|
});
|
|
5618
|
+
if (parentPageId) {
|
|
5619
|
+
const parentNodeId = `confluence:${parentPageId}`;
|
|
5620
|
+
store.addEdge({ from: parentNodeId, to: nodeId, type: "contains" });
|
|
5621
|
+
edgesAdded++;
|
|
5622
|
+
}
|
|
2252
5623
|
const text = sanitizeExternalText(`${page.title} ${page.body?.storage?.value ?? ""}`);
|
|
2253
|
-
|
|
5624
|
+
edgesAdded += linkToCode(store, text, nodeId, "documents");
|
|
2254
5625
|
return { nodesAdded: 1, edgesAdded };
|
|
2255
5626
|
}
|
|
2256
5627
|
};
|
|
@@ -2316,7 +5687,7 @@ var CIConnector = class {
|
|
|
2316
5687
|
source = "github-actions";
|
|
2317
5688
|
httpClient;
|
|
2318
5689
|
constructor(httpClient) {
|
|
2319
|
-
this.httpClient = httpClient ?? ((url, options) => fetch(url, options));
|
|
5690
|
+
this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
|
|
2320
5691
|
}
|
|
2321
5692
|
async ingest(store, config) {
|
|
2322
5693
|
const start = Date.now();
|
|
@@ -2368,6 +5739,306 @@ var CIConnector = class {
|
|
|
2368
5739
|
}
|
|
2369
5740
|
};
|
|
2370
5741
|
|
|
5742
|
+
// src/ingest/connectors/FigmaConnector.ts
|
|
5743
|
+
var CONSTRAINT_KEYWORDS = [
|
|
5744
|
+
"constraint",
|
|
5745
|
+
"must",
|
|
5746
|
+
"required",
|
|
5747
|
+
"minimum",
|
|
5748
|
+
"maximum",
|
|
5749
|
+
"spacing",
|
|
5750
|
+
"padding",
|
|
5751
|
+
"margin",
|
|
5752
|
+
"breakpoint",
|
|
5753
|
+
"responsive",
|
|
5754
|
+
"accessible",
|
|
5755
|
+
"a11y",
|
|
5756
|
+
"wcag"
|
|
5757
|
+
];
|
|
5758
|
+
function hasConstraintKeyword(text) {
|
|
5759
|
+
const lower = text.toLowerCase();
|
|
5760
|
+
return CONSTRAINT_KEYWORDS.some((kw) => lower.includes(kw));
|
|
5761
|
+
}
|
|
5762
|
+
function buildIngestResult2(nodesAdded, edgesAdded, errors, start) {
|
|
5763
|
+
return {
|
|
5764
|
+
nodesAdded,
|
|
5765
|
+
nodesUpdated: 0,
|
|
5766
|
+
edgesAdded,
|
|
5767
|
+
edgesUpdated: 0,
|
|
5768
|
+
errors,
|
|
5769
|
+
durationMs: Date.now() - start
|
|
5770
|
+
};
|
|
5771
|
+
}
|
|
5772
|
+
var FigmaConnector = class {
|
|
5773
|
+
name = "figma";
|
|
5774
|
+
source = "figma";
|
|
5775
|
+
httpClient;
|
|
5776
|
+
constructor(httpClient) {
|
|
5777
|
+
this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
|
|
5778
|
+
}
|
|
5779
|
+
async ingest(store, config) {
|
|
5780
|
+
const start = Date.now();
|
|
5781
|
+
const apiKeyEnv = config.apiKeyEnv ?? "FIGMA_API_KEY";
|
|
5782
|
+
const apiKey = process.env[apiKeyEnv];
|
|
5783
|
+
if (!apiKey) {
|
|
5784
|
+
return buildIngestResult2(
|
|
5785
|
+
0,
|
|
5786
|
+
0,
|
|
5787
|
+
[`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
5788
|
+
start
|
|
5789
|
+
);
|
|
5790
|
+
}
|
|
5791
|
+
const baseUrlEnv = config.baseUrlEnv ?? "FIGMA_BASE_URL";
|
|
5792
|
+
const baseUrl = process.env[baseUrlEnv] ?? "https://api.figma.com";
|
|
5793
|
+
try {
|
|
5794
|
+
const parsed = new URL(baseUrl);
|
|
5795
|
+
if (parsed.protocol !== "https:" || !parsed.hostname.endsWith("api.figma.com")) {
|
|
5796
|
+
return buildIngestResult2(
|
|
5797
|
+
0,
|
|
5798
|
+
0,
|
|
5799
|
+
[`Invalid ${baseUrlEnv}: must be an HTTPS URL on api.figma.com`],
|
|
5800
|
+
start
|
|
5801
|
+
);
|
|
5802
|
+
}
|
|
5803
|
+
} catch {
|
|
5804
|
+
return buildIngestResult2(0, 0, [`Invalid ${baseUrlEnv}: not a valid URL`], start);
|
|
5805
|
+
}
|
|
5806
|
+
const fileIds = config.fileIds;
|
|
5807
|
+
if (!fileIds || fileIds.length === 0) {
|
|
5808
|
+
return buildIngestResult2(0, 0, ["No fileIds provided in connector config"], start);
|
|
5809
|
+
}
|
|
5810
|
+
const headers = { "X-FIGMA-TOKEN": apiKey };
|
|
5811
|
+
const maxLen = config.maxContentLength ?? 4e3;
|
|
5812
|
+
let nodesAdded = 0;
|
|
5813
|
+
let edgesAdded = 0;
|
|
5814
|
+
const errors = [];
|
|
5815
|
+
for (const fileId of fileIds) {
|
|
5816
|
+
try {
|
|
5817
|
+
const counts = await this.processFile(store, baseUrl, fileId, headers, maxLen);
|
|
5818
|
+
nodesAdded += counts.nodesAdded;
|
|
5819
|
+
edgesAdded += counts.edgesAdded;
|
|
5820
|
+
} catch (err) {
|
|
5821
|
+
errors.push(
|
|
5822
|
+
`Figma API error for file ${fileId}: ${err instanceof Error ? err.message : String(err)}`
|
|
5823
|
+
);
|
|
5824
|
+
}
|
|
5825
|
+
}
|
|
5826
|
+
return buildIngestResult2(nodesAdded, edgesAdded, errors, start);
|
|
5827
|
+
}
|
|
5828
|
+
async processFile(store, baseUrl, fileId, headers, maxLen) {
|
|
5829
|
+
let nodesAdded = 0;
|
|
5830
|
+
let edgesAdded = 0;
|
|
5831
|
+
const stylesUrl = `${baseUrl}/v1/files/${fileId}/styles`;
|
|
5832
|
+
const stylesResponse = await this.httpClient(stylesUrl, { headers });
|
|
5833
|
+
if (!stylesResponse.ok) throw new Error(`Styles request failed for file ${fileId}`);
|
|
5834
|
+
const stylesData = await stylesResponse.json();
|
|
5835
|
+
for (const style of stylesData.meta.styles) {
|
|
5836
|
+
const nodeId = `figma:token:${style.key}`;
|
|
5837
|
+
const condensed = await condenseContent(sanitizeExternalText(style.description || "", 2e3), {
|
|
5838
|
+
maxLength: maxLen
|
|
5839
|
+
});
|
|
5840
|
+
const metadata = {
|
|
5841
|
+
source: "figma",
|
|
5842
|
+
key: style.key,
|
|
5843
|
+
styleType: style.style_type,
|
|
5844
|
+
fileId
|
|
5845
|
+
};
|
|
5846
|
+
if (condensed.method !== "passthrough") {
|
|
5847
|
+
metadata.condensed = condensed.method;
|
|
5848
|
+
metadata.originalLength = condensed.originalLength;
|
|
5849
|
+
}
|
|
5850
|
+
store.addNode({
|
|
5851
|
+
id: nodeId,
|
|
5852
|
+
type: "design_token",
|
|
5853
|
+
name: sanitizeExternalText(style.name, 500),
|
|
5854
|
+
content: condensed.content,
|
|
5855
|
+
metadata
|
|
5856
|
+
});
|
|
5857
|
+
nodesAdded++;
|
|
5858
|
+
const searchText = sanitizeExternalText([style.name, style.description].join(" "));
|
|
5859
|
+
edgesAdded += linkToCode(store, searchText, nodeId, "references");
|
|
5860
|
+
}
|
|
5861
|
+
const componentsUrl = `${baseUrl}/v1/files/${fileId}/components`;
|
|
5862
|
+
const componentsResponse = await this.httpClient(componentsUrl, { headers });
|
|
5863
|
+
if (!componentsResponse.ok) throw new Error(`Components request failed for file ${fileId}`);
|
|
5864
|
+
const componentsData = await componentsResponse.json();
|
|
5865
|
+
for (const component of componentsData.meta.components) {
|
|
5866
|
+
const description = component.description || "";
|
|
5867
|
+
if (description) {
|
|
5868
|
+
const intentId = `figma:intent:${component.key}`;
|
|
5869
|
+
const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
|
|
5870
|
+
maxLength: maxLen
|
|
5871
|
+
});
|
|
5872
|
+
const metadata = {
|
|
5873
|
+
source: "figma",
|
|
5874
|
+
key: component.key,
|
|
5875
|
+
fileId
|
|
5876
|
+
};
|
|
5877
|
+
if (condensed.method !== "passthrough") {
|
|
5878
|
+
metadata.condensed = condensed.method;
|
|
5879
|
+
metadata.originalLength = condensed.originalLength;
|
|
5880
|
+
}
|
|
5881
|
+
store.addNode({
|
|
5882
|
+
id: intentId,
|
|
5883
|
+
type: "aesthetic_intent",
|
|
5884
|
+
name: sanitizeExternalText(component.name, 500),
|
|
5885
|
+
content: condensed.content,
|
|
5886
|
+
metadata
|
|
5887
|
+
});
|
|
5888
|
+
nodesAdded++;
|
|
5889
|
+
const searchText = sanitizeExternalText([component.name, description].join(" "));
|
|
5890
|
+
edgesAdded += linkToCode(store, searchText, intentId, "references");
|
|
5891
|
+
}
|
|
5892
|
+
if (description && hasConstraintKeyword(description)) {
|
|
5893
|
+
const constraintId = `figma:constraint:${component.key}`;
|
|
5894
|
+
const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
|
|
5895
|
+
maxLength: maxLen
|
|
5896
|
+
});
|
|
5897
|
+
const metadata = {
|
|
5898
|
+
source: "figma",
|
|
5899
|
+
key: component.key,
|
|
5900
|
+
fileId
|
|
5901
|
+
};
|
|
5902
|
+
if (condensed.method !== "passthrough") {
|
|
5903
|
+
metadata.condensed = condensed.method;
|
|
5904
|
+
metadata.originalLength = condensed.originalLength;
|
|
5905
|
+
}
|
|
5906
|
+
store.addNode({
|
|
5907
|
+
id: constraintId,
|
|
5908
|
+
type: "design_constraint",
|
|
5909
|
+
name: sanitizeExternalText(component.name, 500),
|
|
5910
|
+
content: condensed.content,
|
|
5911
|
+
metadata
|
|
5912
|
+
});
|
|
5913
|
+
nodesAdded++;
|
|
5914
|
+
const searchText = sanitizeExternalText([component.name, description].join(" "));
|
|
5915
|
+
edgesAdded += linkToCode(store, searchText, constraintId, "references");
|
|
5916
|
+
}
|
|
5917
|
+
}
|
|
5918
|
+
return { nodesAdded, edgesAdded };
|
|
5919
|
+
}
|
|
5920
|
+
};
|
|
5921
|
+
|
|
5922
|
+
// src/ingest/connectors/MiroConnector.ts
|
|
5923
|
+
var MIN_CONTENT_LENGTH = 10;
|
|
5924
|
+
var CONCEPT_ITEM_TYPES = /* @__PURE__ */ new Set(["sticky_note", "text"]);
|
|
5925
|
+
function stripHtml(html) {
|
|
5926
|
+
return html.replace(/<[^>]*>/g, "");
|
|
5927
|
+
}
|
|
5928
|
+
function buildIngestResult3(nodesAdded, edgesAdded, errors, start) {
|
|
5929
|
+
return {
|
|
5930
|
+
nodesAdded,
|
|
5931
|
+
nodesUpdated: 0,
|
|
5932
|
+
edgesAdded,
|
|
5933
|
+
edgesUpdated: 0,
|
|
5934
|
+
errors,
|
|
5935
|
+
durationMs: Date.now() - start
|
|
5936
|
+
};
|
|
5937
|
+
}
|
|
5938
|
+
var MiroConnector = class {
|
|
5939
|
+
name = "miro";
|
|
5940
|
+
source = "miro";
|
|
5941
|
+
httpClient;
|
|
5942
|
+
constructor(httpClient) {
|
|
5943
|
+
this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
|
|
5944
|
+
}
|
|
5945
|
+
async ingest(store, config) {
|
|
5946
|
+
const start = Date.now();
|
|
5947
|
+
const apiKeyEnv = config.apiKeyEnv ?? "MIRO_API_KEY";
|
|
5948
|
+
const apiKey = process.env[apiKeyEnv];
|
|
5949
|
+
if (!apiKey) {
|
|
5950
|
+
return buildIngestResult3(
|
|
5951
|
+
0,
|
|
5952
|
+
0,
|
|
5953
|
+
[`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
5954
|
+
start
|
|
5955
|
+
);
|
|
5956
|
+
}
|
|
5957
|
+
const baseUrlEnv = config.baseUrlEnv ?? "MIRO_BASE_URL";
|
|
5958
|
+
const baseUrl = process.env[baseUrlEnv] ?? "https://api.miro.com";
|
|
5959
|
+
try {
|
|
5960
|
+
const parsed = new URL(baseUrl);
|
|
5961
|
+
if (parsed.protocol !== "https:" || parsed.hostname !== "api.miro.com") {
|
|
5962
|
+
return buildIngestResult3(
|
|
5963
|
+
0,
|
|
5964
|
+
0,
|
|
5965
|
+
[`Invalid ${baseUrlEnv}: must be an HTTPS URL on api.miro.com`],
|
|
5966
|
+
start
|
|
5967
|
+
);
|
|
5968
|
+
}
|
|
5969
|
+
} catch {
|
|
5970
|
+
return buildIngestResult3(0, 0, [`Invalid ${baseUrlEnv}: not a valid URL`], start);
|
|
5971
|
+
}
|
|
5972
|
+
const boardIds = config.boardIds;
|
|
5973
|
+
if (!boardIds || boardIds.length === 0) {
|
|
5974
|
+
return buildIngestResult3(0, 0, ["No boardIds provided in config"], start);
|
|
5975
|
+
}
|
|
5976
|
+
const headers = { Authorization: `Bearer ${apiKey}` };
|
|
5977
|
+
let totalNodesAdded = 0;
|
|
5978
|
+
let totalEdgesAdded = 0;
|
|
5979
|
+
const errors = [];
|
|
5980
|
+
for (const boardId of boardIds) {
|
|
5981
|
+
try {
|
|
5982
|
+
const counts = await this.processBoard(store, baseUrl, boardId, headers);
|
|
5983
|
+
totalNodesAdded += counts.nodesAdded;
|
|
5984
|
+
totalEdgesAdded += counts.edgesAdded;
|
|
5985
|
+
} catch (err) {
|
|
5986
|
+
errors.push(
|
|
5987
|
+
`Miro API error for board ${boardId}: ${err instanceof Error ? err.message : String(err)}`
|
|
5988
|
+
);
|
|
5989
|
+
}
|
|
5990
|
+
}
|
|
5991
|
+
return buildIngestResult3(totalNodesAdded, totalEdgesAdded, errors, start);
|
|
5992
|
+
}
|
|
5993
|
+
async processBoard(store, baseUrl, boardId, headers) {
|
|
5994
|
+
let nodesAdded = 0;
|
|
5995
|
+
let edgesAdded = 0;
|
|
5996
|
+
const boardResponse = await this.httpClient(`${baseUrl}/v2/boards/${boardId}`, { headers });
|
|
5997
|
+
if (!boardResponse.ok) throw new Error(`Failed to fetch board ${boardId}`);
|
|
5998
|
+
const board = await boardResponse.json();
|
|
5999
|
+
const boardNodeId = `miro:board:${boardId}`;
|
|
6000
|
+
store.addNode({
|
|
6001
|
+
id: boardNodeId,
|
|
6002
|
+
type: "document",
|
|
6003
|
+
name: sanitizeExternalText(board.name, 500),
|
|
6004
|
+
content: sanitizeExternalText(board.description ?? "", 2e3),
|
|
6005
|
+
metadata: {
|
|
6006
|
+
source: "miro",
|
|
6007
|
+
boardId: board.id
|
|
6008
|
+
}
|
|
6009
|
+
});
|
|
6010
|
+
nodesAdded++;
|
|
6011
|
+
const itemsResponse = await this.httpClient(`${baseUrl}/v2/boards/${boardId}/items`, {
|
|
6012
|
+
headers
|
|
6013
|
+
});
|
|
6014
|
+
if (!itemsResponse.ok) throw new Error(`Failed to fetch items for board ${boardId}`);
|
|
6015
|
+
const itemsData = await itemsResponse.json();
|
|
6016
|
+
for (const item of itemsData.data) {
|
|
6017
|
+
const rawContent = item.data?.content ?? "";
|
|
6018
|
+
const plainContent = stripHtml(rawContent);
|
|
6019
|
+
if (plainContent.length < MIN_CONTENT_LENGTH) continue;
|
|
6020
|
+
if (!CONCEPT_ITEM_TYPES.has(item.type)) continue;
|
|
6021
|
+
const itemNodeId = `miro:item:${item.id}`;
|
|
6022
|
+
store.addNode({
|
|
6023
|
+
id: itemNodeId,
|
|
6024
|
+
type: "business_concept",
|
|
6025
|
+
name: sanitizeExternalText(plainContent, 500),
|
|
6026
|
+
content: sanitizeExternalText(plainContent, 2e3),
|
|
6027
|
+
metadata: {
|
|
6028
|
+
source: "miro",
|
|
6029
|
+
boardId: board.id,
|
|
6030
|
+
itemType: item.type
|
|
6031
|
+
}
|
|
6032
|
+
});
|
|
6033
|
+
nodesAdded++;
|
|
6034
|
+
store.addEdge({ from: boardNodeId, to: itemNodeId, type: "contains" });
|
|
6035
|
+
edgesAdded++;
|
|
6036
|
+
edgesAdded += linkToCode(store, plainContent, itemNodeId, "documents");
|
|
6037
|
+
}
|
|
6038
|
+
return { nodesAdded, edgesAdded };
|
|
6039
|
+
}
|
|
6040
|
+
};
|
|
6041
|
+
|
|
2371
6042
|
// src/search/FusionLayer.ts
|
|
2372
6043
|
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
2373
6044
|
"the",
|
|
@@ -2506,7 +6177,7 @@ var FusionLayer = class {
|
|
|
2506
6177
|
};
|
|
2507
6178
|
|
|
2508
6179
|
// src/entropy/GraphEntropyAdapter.ts
|
|
2509
|
-
var
|
|
6180
|
+
var CODE_NODE_TYPES5 = ["file", "function", "class", "method", "interface", "variable"];
|
|
2510
6181
|
var GraphEntropyAdapter = class {
|
|
2511
6182
|
constructor(store) {
|
|
2512
6183
|
this.store = store;
|
|
@@ -2586,7 +6257,7 @@ var GraphEntropyAdapter = class {
|
|
|
2586
6257
|
}
|
|
2587
6258
|
findEntryPoints() {
|
|
2588
6259
|
const entryPoints = [];
|
|
2589
|
-
for (const nodeType of
|
|
6260
|
+
for (const nodeType of CODE_NODE_TYPES5) {
|
|
2590
6261
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
2591
6262
|
for (const node of nodes) {
|
|
2592
6263
|
const isIndexFile = nodeType === "file" && node.name === "index.ts";
|
|
@@ -2622,7 +6293,7 @@ var GraphEntropyAdapter = class {
|
|
|
2622
6293
|
}
|
|
2623
6294
|
collectUnreachableNodes(visited) {
|
|
2624
6295
|
const unreachableNodes = [];
|
|
2625
|
-
for (const nodeType of
|
|
6296
|
+
for (const nodeType of CODE_NODE_TYPES5) {
|
|
2626
6297
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
2627
6298
|
for (const node of nodes) {
|
|
2628
6299
|
if (!visited.has(node.id)) {
|
|
@@ -3468,9 +7139,9 @@ var EntityExtractor = class {
|
|
|
3468
7139
|
extractPaths(trimmed, add) {
|
|
3469
7140
|
const consumed = /* @__PURE__ */ new Set();
|
|
3470
7141
|
for (const match of trimmed.matchAll(FILE_PATH_RE)) {
|
|
3471
|
-
const
|
|
3472
|
-
add(
|
|
3473
|
-
consumed.add(
|
|
7142
|
+
const path13 = match[0];
|
|
7143
|
+
add(path13);
|
|
7144
|
+
consumed.add(path13);
|
|
3474
7145
|
}
|
|
3475
7146
|
return consumed;
|
|
3476
7147
|
}
|
|
@@ -3542,8 +7213,8 @@ var EntityResolver = class {
|
|
|
3542
7213
|
if (isPathLike && node.path.includes(raw)) {
|
|
3543
7214
|
return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
|
|
3544
7215
|
}
|
|
3545
|
-
const
|
|
3546
|
-
if (
|
|
7216
|
+
const basename7 = node.path.split("/").pop() ?? "";
|
|
7217
|
+
if (basename7.includes(raw)) {
|
|
3547
7218
|
return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
|
|
3548
7219
|
}
|
|
3549
7220
|
if (raw.length >= 4 && node.path.includes(raw)) {
|
|
@@ -3622,13 +7293,13 @@ var ResponseFormatter = class {
|
|
|
3622
7293
|
const context = Array.isArray(d?.context) ? d.context : [];
|
|
3623
7294
|
const firstEntity = entities[0];
|
|
3624
7295
|
const nodeType = firstEntity?.node.type ?? "node";
|
|
3625
|
-
const
|
|
7296
|
+
const path13 = firstEntity?.node.path ?? "unknown";
|
|
3626
7297
|
let neighborCount = 0;
|
|
3627
7298
|
const firstContext = context[0];
|
|
3628
7299
|
if (firstContext && Array.isArray(firstContext.nodes)) {
|
|
3629
7300
|
neighborCount = firstContext.nodes.length;
|
|
3630
7301
|
}
|
|
3631
|
-
return `**${entityName}** is a ${nodeType} at \`${
|
|
7302
|
+
return `**${entityName}** is a ${nodeType} at \`${path13}\`. Connected to ${neighborCount} nodes.`;
|
|
3632
7303
|
}
|
|
3633
7304
|
formatAnomaly(data) {
|
|
3634
7305
|
const d = data;
|
|
@@ -4011,7 +7682,7 @@ var PHASE_NODE_TYPES = {
|
|
|
4011
7682
|
debug: ["failure", "learning", "function", "method"],
|
|
4012
7683
|
plan: ["adr", "document", "module", "layer"]
|
|
4013
7684
|
};
|
|
4014
|
-
var
|
|
7685
|
+
var CODE_NODE_TYPES6 = /* @__PURE__ */ new Set([
|
|
4015
7686
|
"file",
|
|
4016
7687
|
"function",
|
|
4017
7688
|
"class",
|
|
@@ -4230,7 +7901,7 @@ var Assembler = class {
|
|
|
4230
7901
|
*/
|
|
4231
7902
|
checkCoverage() {
|
|
4232
7903
|
const codeNodes = [];
|
|
4233
|
-
for (const type of
|
|
7904
|
+
for (const type of CODE_NODE_TYPES6) {
|
|
4234
7905
|
codeNodes.push(...this.store.findNodes({ type }));
|
|
4235
7906
|
}
|
|
4236
7907
|
const documented = [];
|
|
@@ -4410,14 +8081,14 @@ var GraphConstraintAdapter = class {
|
|
|
4410
8081
|
};
|
|
4411
8082
|
|
|
4412
8083
|
// src/ingest/DesignIngestor.ts
|
|
4413
|
-
var
|
|
4414
|
-
var
|
|
8084
|
+
var fs11 = __toESM(require("fs/promises"));
|
|
8085
|
+
var path12 = __toESM(require("path"));
|
|
4415
8086
|
function isDTCGToken(obj) {
|
|
4416
8087
|
return typeof obj === "object" && obj !== null && "$value" in obj && "$type" in obj;
|
|
4417
8088
|
}
|
|
4418
8089
|
async function readFileOrNull(filePath) {
|
|
4419
8090
|
try {
|
|
4420
|
-
return await
|
|
8091
|
+
return await fs11.readFile(filePath, "utf-8");
|
|
4421
8092
|
} catch {
|
|
4422
8093
|
return null;
|
|
4423
8094
|
}
|
|
@@ -4563,8 +8234,8 @@ var DesignIngestor = class {
|
|
|
4563
8234
|
async ingestAll(designDir) {
|
|
4564
8235
|
const start = Date.now();
|
|
4565
8236
|
const [tokensResult, intentResult] = await Promise.all([
|
|
4566
|
-
this.ingestTokens(
|
|
4567
|
-
this.ingestDesignIntent(
|
|
8237
|
+
this.ingestTokens(path12.join(designDir, "tokens.json")),
|
|
8238
|
+
this.ingestDesignIntent(path12.join(designDir, "DESIGN.md"))
|
|
4568
8239
|
]);
|
|
4569
8240
|
const merged = mergeResults(tokensResult, intentResult);
|
|
4570
8241
|
return { ...merged, durationMs: Date.now() - start };
|
|
@@ -4800,10 +8471,10 @@ var TaskIndependenceAnalyzer = class {
|
|
|
4800
8471
|
includeTypes: ["file"]
|
|
4801
8472
|
});
|
|
4802
8473
|
for (const n of queryResult.nodes) {
|
|
4803
|
-
const
|
|
4804
|
-
if (!fileSet.has(
|
|
4805
|
-
if (!result.has(
|
|
4806
|
-
result.set(
|
|
8474
|
+
const path13 = n.path ?? n.id.replace(/^file:/, "");
|
|
8475
|
+
if (!fileSet.has(path13)) {
|
|
8476
|
+
if (!result.has(path13)) {
|
|
8477
|
+
result.set(path13, file);
|
|
4807
8478
|
}
|
|
4808
8479
|
}
|
|
4809
8480
|
}
|
|
@@ -5180,10 +8851,13 @@ var ConflictPredictor = class {
|
|
|
5180
8851
|
};
|
|
5181
8852
|
|
|
5182
8853
|
// src/index.ts
|
|
5183
|
-
var VERSION = "0.4.
|
|
8854
|
+
var VERSION = "0.4.3";
|
|
5184
8855
|
// Annotate the CommonJS export names for ESM import in node:
|
|
5185
8856
|
0 && (module.exports = {
|
|
8857
|
+
ALL_EXTRACTORS,
|
|
8858
|
+
ApiPathExtractor,
|
|
5186
8859
|
Assembler,
|
|
8860
|
+
BusinessKnowledgeIngestor,
|
|
5187
8861
|
CIConnector,
|
|
5188
8862
|
CURRENT_SCHEMA_VERSION,
|
|
5189
8863
|
CascadeSimulator,
|
|
@@ -5192,11 +8866,18 @@ var VERSION = "0.4.1";
|
|
|
5192
8866
|
ConflictPredictor,
|
|
5193
8867
|
ConfluenceConnector,
|
|
5194
8868
|
ContextQL,
|
|
8869
|
+
ContradictionDetector,
|
|
8870
|
+
CoverageScorer,
|
|
8871
|
+
D2Parser,
|
|
5195
8872
|
DesignConstraintAdapter,
|
|
5196
8873
|
DesignIngestor,
|
|
8874
|
+
DiagramParser,
|
|
5197
8875
|
EDGE_TYPES,
|
|
5198
8876
|
EntityExtractor,
|
|
5199
8877
|
EntityResolver,
|
|
8878
|
+
EnumConstantExtractor,
|
|
8879
|
+
ExtractionRunner,
|
|
8880
|
+
FigmaConnector,
|
|
5200
8881
|
FusionLayer,
|
|
5201
8882
|
GitIngestor,
|
|
5202
8883
|
GraphAnomalyAdapter,
|
|
@@ -5209,24 +8890,38 @@ var VERSION = "0.4.1";
|
|
|
5209
8890
|
GraphNodeSchema,
|
|
5210
8891
|
GraphStore,
|
|
5211
8892
|
INTENTS,
|
|
8893
|
+
ImageAnalysisExtractor,
|
|
5212
8894
|
IntentClassifier,
|
|
5213
8895
|
JiraConnector,
|
|
5214
8896
|
KnowledgeIngestor,
|
|
8897
|
+
KnowledgePipelineRunner,
|
|
8898
|
+
KnowledgeStagingAggregator,
|
|
8899
|
+
MermaidParser,
|
|
8900
|
+
MiroConnector,
|
|
8901
|
+
NODE_STABILITY,
|
|
5215
8902
|
NODE_TYPES,
|
|
5216
8903
|
OBSERVABILITY_TYPES,
|
|
8904
|
+
PackedSummaryCache,
|
|
8905
|
+
PlantUmlParser,
|
|
5217
8906
|
RequirementIngestor,
|
|
5218
8907
|
ResponseFormatter,
|
|
5219
8908
|
SlackConnector,
|
|
8909
|
+
StructuralDriftDetector,
|
|
5220
8910
|
SyncManager,
|
|
5221
8911
|
TaskIndependenceAnalyzer,
|
|
8912
|
+
TestDescriptionExtractor,
|
|
5222
8913
|
TopologicalLinker,
|
|
5223
8914
|
VERSION,
|
|
8915
|
+
ValidationRuleExtractor,
|
|
5224
8916
|
VectorStore,
|
|
5225
8917
|
askGraph,
|
|
5226
8918
|
classifyNodeCategory,
|
|
8919
|
+
createExtractionRunner,
|
|
8920
|
+
detectLanguage,
|
|
5227
8921
|
groupNodesByImpact,
|
|
5228
8922
|
linkToCode,
|
|
5229
8923
|
loadGraph,
|
|
8924
|
+
normalizeIntent,
|
|
5230
8925
|
project,
|
|
5231
8926
|
queryTraceability,
|
|
5232
8927
|
saveGraph
|