@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.mjs
CHANGED
|
@@ -23,6 +23,7 @@ var NODE_TYPES = [
|
|
|
23
23
|
"commit",
|
|
24
24
|
"build",
|
|
25
25
|
"test_result",
|
|
26
|
+
"execution_outcome",
|
|
26
27
|
// Observability (future)
|
|
27
28
|
"span",
|
|
28
29
|
"metric",
|
|
@@ -36,8 +37,18 @@ var NODE_TYPES = [
|
|
|
36
37
|
"design_token",
|
|
37
38
|
"aesthetic_intent",
|
|
38
39
|
"design_constraint",
|
|
40
|
+
"image_annotation",
|
|
39
41
|
// Traceability
|
|
40
|
-
"requirement"
|
|
42
|
+
"requirement",
|
|
43
|
+
// Business Knowledge
|
|
44
|
+
"business_rule",
|
|
45
|
+
"business_process",
|
|
46
|
+
"business_concept",
|
|
47
|
+
"business_term",
|
|
48
|
+
"business_metric",
|
|
49
|
+
"business_fact",
|
|
50
|
+
// Cache
|
|
51
|
+
"packed_summary"
|
|
41
52
|
];
|
|
42
53
|
var EDGE_TYPES = [
|
|
43
54
|
// Code relationships
|
|
@@ -59,6 +70,7 @@ var EDGE_TYPES = [
|
|
|
59
70
|
"co_changes_with",
|
|
60
71
|
"triggered_by",
|
|
61
72
|
"failed_in",
|
|
73
|
+
"outcome_of",
|
|
62
74
|
// Execution relationships (future)
|
|
63
75
|
"executed_by",
|
|
64
76
|
"measured_by",
|
|
@@ -70,10 +82,25 @@ var EDGE_TYPES = [
|
|
|
70
82
|
// Traceability relationships
|
|
71
83
|
"requires",
|
|
72
84
|
"verified_by",
|
|
73
|
-
"tested_by"
|
|
85
|
+
"tested_by",
|
|
86
|
+
// Business Knowledge relationships
|
|
87
|
+
"governs",
|
|
88
|
+
"measures",
|
|
89
|
+
"annotates",
|
|
90
|
+
// Cache relationships
|
|
91
|
+
"caches"
|
|
74
92
|
];
|
|
75
93
|
var OBSERVABILITY_TYPES = /* @__PURE__ */ new Set(["span", "metric", "log"]);
|
|
76
94
|
var CURRENT_SCHEMA_VERSION = 1;
|
|
95
|
+
var NODE_STABILITY = {
|
|
96
|
+
File: "session",
|
|
97
|
+
Function: "session",
|
|
98
|
+
Class: "session",
|
|
99
|
+
Constraint: "session",
|
|
100
|
+
PackedSummary: "session",
|
|
101
|
+
SkillDefinition: "static",
|
|
102
|
+
ToolDefinition: "static"
|
|
103
|
+
};
|
|
77
104
|
var GraphNodeSchema = z.object({
|
|
78
105
|
id: z.string(),
|
|
79
106
|
type: z.enum(NODE_TYPES),
|
|
@@ -102,10 +129,28 @@ var GraphEdgeSchema = z.object({
|
|
|
102
129
|
|
|
103
130
|
// src/store/Serializer.ts
|
|
104
131
|
import { readFile, writeFile, mkdir, access } from "fs/promises";
|
|
132
|
+
import { createWriteStream } from "fs";
|
|
105
133
|
import { join } from "path";
|
|
134
|
+
function streamGraphJson(filePath, nodes, edges) {
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
const stream = createWriteStream(filePath, { encoding: "utf-8" });
|
|
137
|
+
stream.on("error", reject);
|
|
138
|
+
stream.write('{"nodes":[');
|
|
139
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
140
|
+
if (i > 0) stream.write(",");
|
|
141
|
+
stream.write(JSON.stringify(nodes[i]));
|
|
142
|
+
}
|
|
143
|
+
stream.write('],"edges":[');
|
|
144
|
+
for (let i = 0; i < edges.length; i++) {
|
|
145
|
+
if (i > 0) stream.write(",");
|
|
146
|
+
stream.write(JSON.stringify(edges[i]));
|
|
147
|
+
}
|
|
148
|
+
stream.write("]}");
|
|
149
|
+
stream.end(resolve);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
106
152
|
async function saveGraph(dirPath, nodes, edges) {
|
|
107
153
|
await mkdir(dirPath, { recursive: true });
|
|
108
|
-
const graphData = { nodes, edges };
|
|
109
154
|
const metadata = {
|
|
110
155
|
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
111
156
|
lastScanTimestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -113,7 +158,7 @@ async function saveGraph(dirPath, nodes, edges) {
|
|
|
113
158
|
edgeCount: edges.length
|
|
114
159
|
};
|
|
115
160
|
await Promise.all([
|
|
116
|
-
|
|
161
|
+
streamGraphJson(join(dirPath, "graph.json"), nodes, edges),
|
|
117
162
|
writeFile(join(dirPath, "metadata.json"), JSON.stringify(metadata, null, 2), "utf-8")
|
|
118
163
|
]);
|
|
119
164
|
}
|
|
@@ -302,8 +347,8 @@ var GraphStore = class {
|
|
|
302
347
|
}
|
|
303
348
|
// --- Persistence ---
|
|
304
349
|
async save(dirPath) {
|
|
305
|
-
const allNodes = Array.from(this.nodeMap.values())
|
|
306
|
-
const allEdges = Array.from(this.edgeMap.values())
|
|
350
|
+
const allNodes = Array.from(this.nodeMap.values());
|
|
351
|
+
const allEdges = Array.from(this.edgeMap.values());
|
|
307
352
|
await saveGraph(dirPath, allNodes, allEdges);
|
|
308
353
|
}
|
|
309
354
|
async load(dirPath) {
|
|
@@ -409,6 +454,94 @@ var VectorStore = class _VectorStore {
|
|
|
409
454
|
}
|
|
410
455
|
};
|
|
411
456
|
|
|
457
|
+
// src/store/PackedSummaryCache.ts
|
|
458
|
+
var DEFAULT_TTL_MS = 60 * 60 * 1e3;
|
|
459
|
+
function normalizeIntent(intent) {
|
|
460
|
+
return intent.trim().toLowerCase().replace(/\s+/g, " ");
|
|
461
|
+
}
|
|
462
|
+
function cacheNodeId(normalizedIntent) {
|
|
463
|
+
return `packed_summary:${normalizedIntent}`;
|
|
464
|
+
}
|
|
465
|
+
var PackedSummaryCache = class {
|
|
466
|
+
constructor(store, ttlMs = DEFAULT_TTL_MS) {
|
|
467
|
+
this.store = store;
|
|
468
|
+
this.ttlMs = ttlMs;
|
|
469
|
+
}
|
|
470
|
+
store;
|
|
471
|
+
ttlMs;
|
|
472
|
+
/** Returns cached envelope with `cached: true` if valid, or null if miss/stale. */
|
|
473
|
+
get(intent) {
|
|
474
|
+
const normalized = normalizeIntent(intent);
|
|
475
|
+
const nodeId = cacheNodeId(normalized);
|
|
476
|
+
const node = this.store.getNode(nodeId);
|
|
477
|
+
if (!node) return null;
|
|
478
|
+
const createdMs = this.parseCreatedMs(node.metadata["createdAt"]);
|
|
479
|
+
if (createdMs === null) return null;
|
|
480
|
+
if (Date.now() - createdMs > this.ttlMs) return null;
|
|
481
|
+
if (!this.areSourcesFresh(nodeId, node, createdMs)) return null;
|
|
482
|
+
return this.parseEnvelope(node.metadata["envelope"]);
|
|
483
|
+
}
|
|
484
|
+
/** Parse and validate createdAt. Returns epoch ms or null if missing/malformed (GC-002). */
|
|
485
|
+
parseCreatedMs(createdAt) {
|
|
486
|
+
if (!createdAt) return null;
|
|
487
|
+
const ms = new Date(createdAt).getTime();
|
|
488
|
+
return Number.isNaN(ms) ? null : ms;
|
|
489
|
+
}
|
|
490
|
+
/** GC-001: Checks source nodes exist and are unmodified since cache creation. */
|
|
491
|
+
areSourcesFresh(nodeId, node, createdMs) {
|
|
492
|
+
const sourceNodeIds = node.metadata["sourceNodeIds"];
|
|
493
|
+
const edges = this.store.getEdges({ from: nodeId, type: "caches" });
|
|
494
|
+
if (sourceNodeIds && edges.length < sourceNodeIds.length) return false;
|
|
495
|
+
for (const edge of edges) {
|
|
496
|
+
const sourceNode = this.store.getNode(edge.to);
|
|
497
|
+
if (!sourceNode) return false;
|
|
498
|
+
if (sourceNode.lastModified) {
|
|
499
|
+
const sourceModMs = new Date(sourceNode.lastModified).getTime();
|
|
500
|
+
if (sourceModMs > createdMs) return false;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
/** Parse envelope JSON and set cached: true. Returns null on invalid JSON. */
|
|
506
|
+
parseEnvelope(raw) {
|
|
507
|
+
try {
|
|
508
|
+
const envelope = JSON.parse(raw);
|
|
509
|
+
return { ...envelope, meta: { ...envelope.meta, cached: true } };
|
|
510
|
+
} catch {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
/** Write a PackedSummary node with caches edges to source nodes. */
|
|
515
|
+
set(intent, envelope, sourceNodeIds) {
|
|
516
|
+
const normalized = normalizeIntent(intent);
|
|
517
|
+
const nodeId = cacheNodeId(normalized);
|
|
518
|
+
this.store.removeNode(nodeId);
|
|
519
|
+
this.store.addNode({
|
|
520
|
+
id: nodeId,
|
|
521
|
+
type: "packed_summary",
|
|
522
|
+
name: normalized,
|
|
523
|
+
metadata: {
|
|
524
|
+
envelope: JSON.stringify(envelope),
|
|
525
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
526
|
+
sourceNodeIds
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
for (const sourceId of sourceNodeIds) {
|
|
530
|
+
this.store.addEdge({
|
|
531
|
+
from: nodeId,
|
|
532
|
+
to: sourceId,
|
|
533
|
+
type: "caches"
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
/** Explicitly invalidate a cached packed summary. */
|
|
538
|
+
invalidate(intent) {
|
|
539
|
+
const normalized = normalizeIntent(intent);
|
|
540
|
+
const nodeId = cacheNodeId(normalized);
|
|
541
|
+
this.store.removeNode(nodeId);
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
|
|
412
545
|
// src/query/ContextQL.ts
|
|
413
546
|
function edgeKey2(e) {
|
|
414
547
|
return `${e.from}|${e.to}|${e.type}`;
|
|
@@ -584,6 +717,49 @@ function groupNodesByImpact(nodes, excludeId) {
|
|
|
584
717
|
import * as fs from "fs/promises";
|
|
585
718
|
import * as path from "path";
|
|
586
719
|
var SKIP_METHOD_NAMES = /* @__PURE__ */ new Set(["constructor", "if", "for", "while", "switch"]);
|
|
720
|
+
var FUNCTION_PATTERNS = {
|
|
721
|
+
python: /^def\s+(\w+)\s*\(/,
|
|
722
|
+
go: /^func\s+(\w+)\s*[[(]/,
|
|
723
|
+
rust: /^(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/,
|
|
724
|
+
typescript: /(?:export\s+)?(?:async\s+)?function\s+(\w+)/,
|
|
725
|
+
javascript: /(?:export\s+)?(?:async\s+)?function\s+(\w+)/
|
|
726
|
+
};
|
|
727
|
+
var CLASS_PATTERNS = {
|
|
728
|
+
python: /^class\s+(\w+)/,
|
|
729
|
+
go: /^type\s+(\w+)\s+struct\b/,
|
|
730
|
+
rust: /^(?:pub\s+)?struct\s+(\w+)/,
|
|
731
|
+
java: /(?:public|private|protected)?\s*(?:abstract\s+|final\s+)?class\s+(\w+)/,
|
|
732
|
+
typescript: /(?:export\s+)?class\s+(\w+)/,
|
|
733
|
+
javascript: /(?:export\s+)?class\s+(\w+)/
|
|
734
|
+
};
|
|
735
|
+
var INTERFACE_PATTERNS = {
|
|
736
|
+
go: /^type\s+(\w+)\s+interface\b/,
|
|
737
|
+
rust: /^(?:pub\s+)?trait\s+(\w+)/,
|
|
738
|
+
java: /(?:public\s+)?interface\s+(\w+)/,
|
|
739
|
+
typescript: /(?:export\s+)?interface\s+(\w+)/,
|
|
740
|
+
javascript: /(?:export\s+)?interface\s+(\w+)/
|
|
741
|
+
};
|
|
742
|
+
var METHOD_PATTERNS = {
|
|
743
|
+
python: /^\s+def\s+(\w+)\s*\(/,
|
|
744
|
+
rust: /^\s+(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/,
|
|
745
|
+
java: /^\s+(?:(?:public|private|protected|static|final|abstract|synchronized)\s+)*(?:\w+(?:<[^>]*>)?)\s+(\w+)\s*\(/,
|
|
746
|
+
typescript: /^\s+(?:(?:public|private|protected|readonly|static|abstract)\s+)*(?:async\s+)?(\w+)\s*\(/,
|
|
747
|
+
javascript: /^\s+(?:(?:public|private|protected|readonly|static|abstract)\s+)*(?:async\s+)?(\w+)\s*\(/
|
|
748
|
+
};
|
|
749
|
+
var GO_METHOD_PATTERN = /^func\s+\(\w+\s+\*?(\w+)\)\s+(\w+)\s*\(/;
|
|
750
|
+
var VARIABLE_PATTERNS = {
|
|
751
|
+
python: /^([A-Z]\w*)\s*=/,
|
|
752
|
+
go: /^(?:var|const)\s+(\w+)/,
|
|
753
|
+
rust: /^(?:pub\s+)?(?:const|static)\s+(\w+)/,
|
|
754
|
+
typescript: /(?:export\s+)?(?:const|let|var)\s+(\w+)/,
|
|
755
|
+
javascript: /(?:export\s+)?(?:const|let|var)\s+(\w+)/
|
|
756
|
+
};
|
|
757
|
+
function isExported(lang, line, name) {
|
|
758
|
+
if (lang === "go") return /^[A-Z]/.test(name);
|
|
759
|
+
if (lang === "rust") return /^\s*pub\b/.test(line);
|
|
760
|
+
if (lang === "java") return /\bpublic\b/.test(line);
|
|
761
|
+
return line.includes("export");
|
|
762
|
+
}
|
|
587
763
|
function countBraces(line) {
|
|
588
764
|
let net = 0;
|
|
589
765
|
for (const ch of line) {
|
|
@@ -592,6 +768,23 @@ function countBraces(line) {
|
|
|
592
768
|
}
|
|
593
769
|
return net;
|
|
594
770
|
}
|
|
771
|
+
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
772
|
+
".ts",
|
|
773
|
+
".tsx",
|
|
774
|
+
".js",
|
|
775
|
+
".jsx",
|
|
776
|
+
".py",
|
|
777
|
+
".go",
|
|
778
|
+
".rs",
|
|
779
|
+
".java",
|
|
780
|
+
".groovy"
|
|
781
|
+
]);
|
|
782
|
+
var SKIP_EXTENSIONS = /* @__PURE__ */ new Set([".d.ts"]);
|
|
783
|
+
function isSupportedSourceFile(name) {
|
|
784
|
+
if (SKIP_EXTENSIONS.has(name.slice(name.lastIndexOf(".")))) return false;
|
|
785
|
+
const ext = name.slice(name.lastIndexOf("."));
|
|
786
|
+
return SUPPORTED_EXTENSIONS.has(ext);
|
|
787
|
+
}
|
|
595
788
|
var CodeIngestor = class {
|
|
596
789
|
constructor(store) {
|
|
597
790
|
this.store = store;
|
|
@@ -604,22 +797,34 @@ var CodeIngestor = class {
|
|
|
604
797
|
let edgesAdded = 0;
|
|
605
798
|
const files = await this.findSourceFiles(rootDir);
|
|
606
799
|
const nameToFiles = /* @__PURE__ */ new Map();
|
|
607
|
-
const fileContents = /* @__PURE__ */ new Map();
|
|
608
800
|
for (const filePath of files) {
|
|
609
801
|
try {
|
|
610
|
-
const result = await this.processFile(filePath, rootDir, nameToFiles
|
|
802
|
+
const result = await this.processFile(filePath, rootDir, nameToFiles);
|
|
611
803
|
nodesAdded += result.nodesAdded;
|
|
612
804
|
edgesAdded += result.edgesAdded;
|
|
613
805
|
} catch (err) {
|
|
614
806
|
errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
615
807
|
}
|
|
616
808
|
}
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
809
|
+
for (const filePath of files) {
|
|
810
|
+
try {
|
|
811
|
+
const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
812
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
813
|
+
const callsEdges = this.extractCallsEdgesForFile(relativePath, content, nameToFiles);
|
|
814
|
+
for (const edge of callsEdges) {
|
|
815
|
+
this.store.addEdge(edge);
|
|
816
|
+
edgesAdded++;
|
|
817
|
+
}
|
|
818
|
+
} catch {
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
for (const filePath of files) {
|
|
822
|
+
try {
|
|
823
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
824
|
+
edgesAdded += this.extractReqAnnotationsForFile(filePath, content, rootDir);
|
|
825
|
+
} catch {
|
|
826
|
+
}
|
|
621
827
|
}
|
|
622
|
-
edgesAdded += this.extractReqAnnotations(fileContents, rootDir);
|
|
623
828
|
return {
|
|
624
829
|
nodesAdded,
|
|
625
830
|
nodesUpdated: 0,
|
|
@@ -629,14 +834,13 @@ var CodeIngestor = class {
|
|
|
629
834
|
durationMs: Date.now() - start
|
|
630
835
|
};
|
|
631
836
|
}
|
|
632
|
-
async processFile(filePath, rootDir, nameToFiles
|
|
837
|
+
async processFile(filePath, rootDir, nameToFiles) {
|
|
633
838
|
let nodesAdded = 0;
|
|
634
839
|
let edgesAdded = 0;
|
|
635
840
|
const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
636
841
|
const content = await fs.readFile(filePath, "utf-8");
|
|
637
842
|
const stat2 = await fs.stat(filePath);
|
|
638
843
|
const fileId = `file:${relativePath}`;
|
|
639
|
-
fileContents.set(relativePath, content);
|
|
640
844
|
const fileNode = {
|
|
641
845
|
id: fileId,
|
|
642
846
|
type: "file",
|
|
@@ -676,9 +880,9 @@ var CodeIngestor = class {
|
|
|
676
880
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
677
881
|
for (const entry of entries) {
|
|
678
882
|
const fullPath = path.join(dir, entry.name);
|
|
679
|
-
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "dist") {
|
|
883
|
+
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") {
|
|
680
884
|
results.push(...await this.findSourceFiles(fullPath));
|
|
681
|
-
} else if (entry.isFile() &&
|
|
885
|
+
} else if (entry.isFile() && isSupportedSourceFile(entry.name)) {
|
|
682
886
|
results.push(fullPath);
|
|
683
887
|
}
|
|
684
888
|
}
|
|
@@ -687,25 +891,40 @@ var CodeIngestor = class {
|
|
|
687
891
|
extractSymbols(content, fileId, relativePath) {
|
|
688
892
|
const results = [];
|
|
689
893
|
const lines = content.split("\n");
|
|
894
|
+
const lang = this.detectLanguage(relativePath);
|
|
690
895
|
const ctx = { className: null, classId: null, insideClass: false, braceDepth: 0 };
|
|
691
896
|
for (let i = 0; i < lines.length; i++) {
|
|
692
897
|
const line = lines[i];
|
|
693
|
-
|
|
694
|
-
if (this.tryExtractClass(line, lines, i, fileId, relativePath, ctx, results)) continue;
|
|
695
|
-
if (this.tryExtractInterface(line, lines, i, fileId, relativePath, ctx, results)) continue;
|
|
696
|
-
if (this.updateClassContext(line, ctx)) continue;
|
|
697
|
-
if (this.tryExtractMethod(line, lines, i, fileId, relativePath, ctx, results)) continue;
|
|
698
|
-
if (ctx.insideClass) continue;
|
|
699
|
-
this.tryExtractVariable(line, i, fileId, relativePath, results);
|
|
898
|
+
this.processSymbolLine(line, lines, i, fileId, relativePath, ctx, results, lang);
|
|
700
899
|
}
|
|
701
900
|
return results;
|
|
702
901
|
}
|
|
703
|
-
|
|
704
|
-
|
|
902
|
+
processSymbolLine(line, lines, i, fileId, relativePath, ctx, results, lang) {
|
|
903
|
+
if (lang === "python") {
|
|
904
|
+
this.updatePythonClassContext(lines, i, ctx);
|
|
905
|
+
}
|
|
906
|
+
if (this.tryExtractFunction(line, lines, i, fileId, relativePath, ctx, results, lang)) return;
|
|
907
|
+
if (this.tryExtractClass(line, lines, i, fileId, relativePath, ctx, results, lang)) return;
|
|
908
|
+
if (this.tryExtractInterface(line, lines, i, fileId, relativePath, ctx, results, lang)) return;
|
|
909
|
+
if (this.tryEnterImplBlock(line, lang, relativePath, ctx)) return;
|
|
910
|
+
if (lang !== "python" && this.updateClassContext(line, ctx)) return;
|
|
911
|
+
if (this.tryExtractMethod(line, lines, i, fileId, relativePath, ctx, results, lang)) return;
|
|
912
|
+
if (ctx.insideClass) return;
|
|
913
|
+
this.tryExtractVariable(line, i, fileId, relativePath, results, lang);
|
|
914
|
+
}
|
|
915
|
+
matchFunction(line, lang) {
|
|
916
|
+
if (lang === "java") return null;
|
|
917
|
+
const pattern = FUNCTION_PATTERNS[lang];
|
|
918
|
+
return pattern ? line.match(pattern) : null;
|
|
919
|
+
}
|
|
920
|
+
tryExtractFunction(line, lines, i, fileId, relativePath, ctx, results, lang) {
|
|
921
|
+
if (ctx.insideClass) return false;
|
|
922
|
+
const fnMatch = this.matchFunction(line, lang);
|
|
705
923
|
if (!fnMatch) return false;
|
|
706
924
|
const name = fnMatch[1];
|
|
707
925
|
const id = `function:${relativePath}:${name}`;
|
|
708
|
-
const endLine = this.findClosingBrace(lines, i);
|
|
926
|
+
const endLine = lang === "python" ? this.findEndOfIndentBlock(lines, i) : this.findClosingBrace(lines, i);
|
|
927
|
+
const exported = isExported(lang, line, name);
|
|
709
928
|
results.push({
|
|
710
929
|
node: {
|
|
711
930
|
id,
|
|
@@ -714,7 +933,7 @@ var CodeIngestor = class {
|
|
|
714
933
|
path: relativePath,
|
|
715
934
|
location: { fileId, startLine: i + 1, endLine },
|
|
716
935
|
metadata: {
|
|
717
|
-
exported
|
|
936
|
+
exported,
|
|
718
937
|
cyclomaticComplexity: this.computeCyclomaticComplexity(lines.slice(i, endLine)),
|
|
719
938
|
nestingDepth: this.computeMaxNesting(lines.slice(i, endLine)),
|
|
720
939
|
lineCount: endLine - i,
|
|
@@ -729,12 +948,17 @@ var CodeIngestor = class {
|
|
|
729
948
|
}
|
|
730
949
|
return true;
|
|
731
950
|
}
|
|
732
|
-
|
|
733
|
-
const
|
|
951
|
+
matchClass(line, lang) {
|
|
952
|
+
const pattern = CLASS_PATTERNS[lang];
|
|
953
|
+
return pattern ? line.match(pattern) : null;
|
|
954
|
+
}
|
|
955
|
+
tryExtractClass(line, lines, i, fileId, relativePath, ctx, results, lang) {
|
|
956
|
+
const classMatch = this.matchClass(line, lang);
|
|
734
957
|
if (!classMatch) return false;
|
|
735
958
|
const name = classMatch[1];
|
|
736
959
|
const id = `class:${relativePath}:${name}`;
|
|
737
|
-
const endLine = this.findClosingBrace(lines, i);
|
|
960
|
+
const endLine = lang === "python" ? this.findEndOfIndentBlock(lines, i) : this.findClosingBrace(lines, i);
|
|
961
|
+
const exported = isExported(lang, line, name);
|
|
738
962
|
results.push({
|
|
739
963
|
node: {
|
|
740
964
|
id,
|
|
@@ -742,22 +966,27 @@ var CodeIngestor = class {
|
|
|
742
966
|
name,
|
|
743
967
|
path: relativePath,
|
|
744
968
|
location: { fileId, startLine: i + 1, endLine },
|
|
745
|
-
metadata: { exported
|
|
969
|
+
metadata: { exported }
|
|
746
970
|
},
|
|
747
971
|
edge: { from: fileId, to: id, type: "contains" }
|
|
748
972
|
});
|
|
749
973
|
ctx.className = name;
|
|
750
974
|
ctx.classId = id;
|
|
751
975
|
ctx.insideClass = true;
|
|
752
|
-
ctx.braceDepth = countBraces(line);
|
|
976
|
+
ctx.braceDepth = lang === "python" ? 1 : countBraces(line);
|
|
753
977
|
return true;
|
|
754
978
|
}
|
|
755
|
-
|
|
756
|
-
const
|
|
979
|
+
matchInterface(line, lang) {
|
|
980
|
+
const pattern = INTERFACE_PATTERNS[lang];
|
|
981
|
+
return pattern ? line.match(pattern) : null;
|
|
982
|
+
}
|
|
983
|
+
tryExtractInterface(line, lines, i, fileId, relativePath, ctx, results, lang) {
|
|
984
|
+
const ifaceMatch = this.matchInterface(line, lang);
|
|
757
985
|
if (!ifaceMatch) return false;
|
|
758
986
|
const name = ifaceMatch[1];
|
|
759
987
|
const id = `interface:${relativePath}:${name}`;
|
|
760
988
|
const endLine = this.findClosingBrace(lines, i);
|
|
989
|
+
const exported = isExported(lang, line, name);
|
|
761
990
|
results.push({
|
|
762
991
|
node: {
|
|
763
992
|
id,
|
|
@@ -765,7 +994,7 @@ var CodeIngestor = class {
|
|
|
765
994
|
name,
|
|
766
995
|
path: relativePath,
|
|
767
996
|
location: { fileId, startLine: i + 1, endLine },
|
|
768
|
-
metadata: { exported
|
|
997
|
+
metadata: { exported }
|
|
769
998
|
},
|
|
770
999
|
edge: { from: fileId, to: id, type: "contains" }
|
|
771
1000
|
});
|
|
@@ -774,6 +1003,23 @@ var CodeIngestor = class {
|
|
|
774
1003
|
ctx.insideClass = false;
|
|
775
1004
|
return true;
|
|
776
1005
|
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Enter an impl block (Rust) or Java class body — sets class context without creating a node.
|
|
1008
|
+
*/
|
|
1009
|
+
tryEnterImplBlock(line, lang, relativePath, ctx) {
|
|
1010
|
+
if (lang === "rust") {
|
|
1011
|
+
const implMatch = line.match(/^impl(?:<[^>]*>)?\s+(\w+)/);
|
|
1012
|
+
if (implMatch) {
|
|
1013
|
+
const name = implMatch[1];
|
|
1014
|
+
ctx.className = name;
|
|
1015
|
+
ctx.classId = `class:${relativePath}:${name}`;
|
|
1016
|
+
ctx.insideClass = true;
|
|
1017
|
+
ctx.braceDepth = countBraces(line);
|
|
1018
|
+
return true;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
return false;
|
|
1022
|
+
}
|
|
777
1023
|
/** Update brace tracking; returns true when line is consumed (class ended or tracked). */
|
|
778
1024
|
updateClassContext(line, ctx) {
|
|
779
1025
|
if (!ctx.insideClass) return false;
|
|
@@ -786,16 +1032,32 @@ var CodeIngestor = class {
|
|
|
786
1032
|
}
|
|
787
1033
|
return false;
|
|
788
1034
|
}
|
|
789
|
-
|
|
790
|
-
if (
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
);
|
|
1035
|
+
matchMethod(line, lang, insideClass) {
|
|
1036
|
+
if (lang === "go") return line.match(GO_METHOD_PATTERN);
|
|
1037
|
+
if (!insideClass) return null;
|
|
1038
|
+
const pattern = METHOD_PATTERNS[lang];
|
|
1039
|
+
return pattern ? line.match(pattern) : null;
|
|
1040
|
+
}
|
|
1041
|
+
tryExtractMethod(line, lines, i, fileId, relativePath, ctx, results, lang) {
|
|
1042
|
+
const methodMatch = this.matchMethod(line, lang, ctx.insideClass);
|
|
794
1043
|
if (!methodMatch) return false;
|
|
795
|
-
|
|
1044
|
+
let methodName;
|
|
1045
|
+
let className;
|
|
1046
|
+
let classId;
|
|
1047
|
+
if (lang === "go") {
|
|
1048
|
+
const receiverType = methodMatch[1];
|
|
1049
|
+
methodName = methodMatch[2];
|
|
1050
|
+
className = receiverType;
|
|
1051
|
+
classId = `class:${relativePath}:${receiverType}`;
|
|
1052
|
+
} else {
|
|
1053
|
+
if (!ctx.insideClass || !ctx.className || !ctx.classId) return false;
|
|
1054
|
+
methodName = methodMatch[1];
|
|
1055
|
+
className = ctx.className;
|
|
1056
|
+
classId = ctx.classId;
|
|
1057
|
+
}
|
|
796
1058
|
if (SKIP_METHOD_NAMES.has(methodName)) return false;
|
|
797
|
-
const id = `method:${relativePath}:${
|
|
798
|
-
const endLine = this.findClosingBrace(lines, i);
|
|
1059
|
+
const id = `method:${relativePath}:${className}.${methodName}`;
|
|
1060
|
+
const endLine = lang === "python" ? this.findEndOfIndentBlock(lines, i) : this.findClosingBrace(lines, i);
|
|
799
1061
|
results.push({
|
|
800
1062
|
node: {
|
|
801
1063
|
id,
|
|
@@ -804,23 +1066,28 @@ var CodeIngestor = class {
|
|
|
804
1066
|
path: relativePath,
|
|
805
1067
|
location: { fileId, startLine: i + 1, endLine },
|
|
806
1068
|
metadata: {
|
|
807
|
-
className
|
|
808
|
-
exported:
|
|
1069
|
+
className,
|
|
1070
|
+
exported: isExported(lang, line, methodName),
|
|
809
1071
|
cyclomaticComplexity: this.computeCyclomaticComplexity(lines.slice(i, endLine)),
|
|
810
1072
|
nestingDepth: this.computeMaxNesting(lines.slice(i, endLine)),
|
|
811
1073
|
lineCount: endLine - i,
|
|
812
1074
|
parameterCount: this.countParameters(line)
|
|
813
1075
|
}
|
|
814
1076
|
},
|
|
815
|
-
edge: { from:
|
|
1077
|
+
edge: { from: classId, to: id, type: "contains" }
|
|
816
1078
|
});
|
|
817
1079
|
return true;
|
|
818
1080
|
}
|
|
819
|
-
|
|
820
|
-
const
|
|
1081
|
+
matchVariable(line, lang) {
|
|
1082
|
+
const pattern = VARIABLE_PATTERNS[lang];
|
|
1083
|
+
return pattern ? line.match(pattern) : null;
|
|
1084
|
+
}
|
|
1085
|
+
tryExtractVariable(line, i, fileId, relativePath, results, lang) {
|
|
1086
|
+
const varMatch = this.matchVariable(line, lang);
|
|
821
1087
|
if (!varMatch) return;
|
|
822
1088
|
const name = varMatch[1];
|
|
823
1089
|
const id = `variable:${relativePath}:${name}`;
|
|
1090
|
+
const exported = isExported(lang, line, name);
|
|
824
1091
|
results.push({
|
|
825
1092
|
node: {
|
|
826
1093
|
id,
|
|
@@ -828,7 +1095,7 @@ var CodeIngestor = class {
|
|
|
828
1095
|
name,
|
|
829
1096
|
path: relativePath,
|
|
830
1097
|
location: { fileId, startLine: i + 1, endLine: i + 1 },
|
|
831
|
-
metadata: { exported
|
|
1098
|
+
metadata: { exported }
|
|
832
1099
|
},
|
|
833
1100
|
edge: { from: fileId, to: id, type: "contains" }
|
|
834
1101
|
});
|
|
@@ -857,48 +1124,79 @@ var CodeIngestor = class {
|
|
|
857
1124
|
return startIndex + 1;
|
|
858
1125
|
}
|
|
859
1126
|
/**
|
|
860
|
-
*
|
|
1127
|
+
* Find the end of an indentation-based block (Python).
|
|
1128
|
+
* The block ends when a subsequent non-empty line has indentation <= the starting line.
|
|
1129
|
+
*/
|
|
1130
|
+
findEndOfIndentBlock(lines, startIndex) {
|
|
1131
|
+
const startLine = lines[startIndex] ?? "";
|
|
1132
|
+
const baseIndent = startLine.search(/\S/);
|
|
1133
|
+
let lastNonEmpty = startIndex;
|
|
1134
|
+
for (let i = startIndex + 1; i < lines.length; i++) {
|
|
1135
|
+
const line = lines[i];
|
|
1136
|
+
if (line.trim() === "") continue;
|
|
1137
|
+
const indent = line.search(/\S/);
|
|
1138
|
+
if (indent <= baseIndent) {
|
|
1139
|
+
return lastNonEmpty + 1;
|
|
1140
|
+
}
|
|
1141
|
+
lastNonEmpty = i;
|
|
1142
|
+
}
|
|
1143
|
+
return lastNonEmpty + 1;
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* Update class context for Python using indentation tracking.
|
|
1147
|
+
*/
|
|
1148
|
+
updatePythonClassContext(lines, i, ctx) {
|
|
1149
|
+
if (!ctx.insideClass) return false;
|
|
1150
|
+
const line = lines[i];
|
|
1151
|
+
if (line.trim() === "") return false;
|
|
1152
|
+
const indent = line.search(/\S/);
|
|
1153
|
+
if (indent <= 0) {
|
|
1154
|
+
ctx.className = null;
|
|
1155
|
+
ctx.classId = null;
|
|
1156
|
+
ctx.insideClass = false;
|
|
1157
|
+
return false;
|
|
1158
|
+
}
|
|
1159
|
+
return false;
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Scan a file for identifiers matching known callable names,
|
|
861
1163
|
* then create file-to-file "calls" edges. Uses regex heuristic (not AST).
|
|
862
1164
|
*/
|
|
863
|
-
|
|
1165
|
+
extractCallsEdgesForFile(relativePath, content, nameToFiles) {
|
|
864
1166
|
const edges = [];
|
|
865
1167
|
const seen = /* @__PURE__ */ new Set();
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
});
|
|
886
|
-
}
|
|
1168
|
+
const callerFileId = `file:${relativePath}`;
|
|
1169
|
+
const callPattern = /\b([a-zA-Z_$][\w$]*)\s*\(/g;
|
|
1170
|
+
let match;
|
|
1171
|
+
while ((match = callPattern.exec(content)) !== null) {
|
|
1172
|
+
const name = match[1];
|
|
1173
|
+
const targetFiles = nameToFiles.get(name);
|
|
1174
|
+
if (!targetFiles) continue;
|
|
1175
|
+
for (const targetFile of targetFiles) {
|
|
1176
|
+
if (targetFile === relativePath) continue;
|
|
1177
|
+
const targetFileId = `file:${targetFile}`;
|
|
1178
|
+
const key = `${callerFileId}|${targetFileId}`;
|
|
1179
|
+
if (seen.has(key)) continue;
|
|
1180
|
+
seen.add(key);
|
|
1181
|
+
edges.push({
|
|
1182
|
+
from: callerFileId,
|
|
1183
|
+
to: targetFileId,
|
|
1184
|
+
type: "calls",
|
|
1185
|
+
metadata: { confidence: "regex" }
|
|
1186
|
+
});
|
|
887
1187
|
}
|
|
888
1188
|
}
|
|
889
1189
|
return edges;
|
|
890
1190
|
}
|
|
891
1191
|
async extractImports(content, fileId, relativePath, rootDir) {
|
|
1192
|
+
const lang = this.detectLanguage(relativePath);
|
|
892
1193
|
const edges = [];
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
const importPath = match[1];
|
|
897
|
-
if (!importPath.startsWith(".")) continue;
|
|
1194
|
+
const importPaths = this.extractImportPaths(content, lang);
|
|
1195
|
+
for (const { importPath, isTypeOnly } of importPaths) {
|
|
1196
|
+
if ((lang === "typescript" || lang === "javascript") && !importPath.startsWith(".")) continue;
|
|
898
1197
|
const resolvedPath = await this.resolveImportPath(relativePath, importPath, rootDir);
|
|
899
1198
|
if (resolvedPath) {
|
|
900
1199
|
const targetId = `file:${resolvedPath}`;
|
|
901
|
-
const isTypeOnly = match[0].includes("import type");
|
|
902
1200
|
edges.push({
|
|
903
1201
|
from: fileId,
|
|
904
1202
|
to: targetId,
|
|
@@ -909,10 +1207,53 @@ var CodeIngestor = class {
|
|
|
909
1207
|
}
|
|
910
1208
|
return edges;
|
|
911
1209
|
}
|
|
1210
|
+
extractImportPaths(content, lang) {
|
|
1211
|
+
const results = [];
|
|
1212
|
+
if (lang === "typescript" || lang === "javascript") {
|
|
1213
|
+
const importRegex = /import\s+(?:type\s+)?(?:\{[^}]*\}|[\w*]+)\s+from\s+['"]([^'"]+)['"]/g;
|
|
1214
|
+
let match;
|
|
1215
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
1216
|
+
results.push({
|
|
1217
|
+
importPath: match[1],
|
|
1218
|
+
isTypeOnly: match[0].includes("import type")
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
} else if (lang === "python") {
|
|
1222
|
+
const fromImport = /(?:from\s+([\w.]+)\s+import|import\s+([\w.]+))/g;
|
|
1223
|
+
let match;
|
|
1224
|
+
while ((match = fromImport.exec(content)) !== null) {
|
|
1225
|
+
const importPath = match[1] ?? match[2];
|
|
1226
|
+
if (importPath.startsWith(".")) {
|
|
1227
|
+
results.push({ importPath, isTypeOnly: false });
|
|
1228
|
+
} else {
|
|
1229
|
+
results.push({ importPath: importPath.replace(/\./g, "/"), isTypeOnly: false });
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
} else if (lang === "go") {
|
|
1233
|
+
const goImport = /"([^"]+)"/g;
|
|
1234
|
+
let match;
|
|
1235
|
+
while ((match = goImport.exec(content)) !== null) {
|
|
1236
|
+
results.push({ importPath: match[1], isTypeOnly: false });
|
|
1237
|
+
}
|
|
1238
|
+
} else if (lang === "rust") {
|
|
1239
|
+
const useDecl = /use\s+((?:crate|super|self)(?:::\w+)+)/g;
|
|
1240
|
+
let match;
|
|
1241
|
+
while ((match = useDecl.exec(content)) !== null) {
|
|
1242
|
+
results.push({ importPath: match[1], isTypeOnly: false });
|
|
1243
|
+
}
|
|
1244
|
+
} else if (lang === "java") {
|
|
1245
|
+
const javaImport = /import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;/g;
|
|
1246
|
+
let match;
|
|
1247
|
+
while ((match = javaImport.exec(content)) !== null) {
|
|
1248
|
+
results.push({ importPath: match[1], isTypeOnly: false });
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
return results;
|
|
1252
|
+
}
|
|
912
1253
|
async resolveImportPath(fromFile, importPath, rootDir) {
|
|
913
1254
|
const fromDir = path.dirname(fromFile);
|
|
914
1255
|
const resolved = path.normalize(path.join(fromDir, importPath)).replace(/\\/g, "/");
|
|
915
|
-
const extensions = [".ts", ".tsx", ".js", ".jsx"];
|
|
1256
|
+
const extensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
|
|
916
1257
|
for (const ext of extensions) {
|
|
917
1258
|
const candidate = resolved.replace(/\.js$/, "") + ext;
|
|
918
1259
|
const fullPath = path.join(rootDir, candidate);
|
|
@@ -922,7 +1263,8 @@ var CodeIngestor = class {
|
|
|
922
1263
|
} catch {
|
|
923
1264
|
}
|
|
924
1265
|
}
|
|
925
|
-
|
|
1266
|
+
const indexExtensions = [".ts", ".tsx", ".js", ".jsx"];
|
|
1267
|
+
for (const ext of indexExtensions) {
|
|
926
1268
|
const candidate = path.join(resolved, `index${ext}`).replace(/\\/g, "/");
|
|
927
1269
|
const fullPath = path.join(rootDir, candidate);
|
|
928
1270
|
try {
|
|
@@ -931,6 +1273,12 @@ var CodeIngestor = class {
|
|
|
931
1273
|
} catch {
|
|
932
1274
|
}
|
|
933
1275
|
}
|
|
1276
|
+
const pyInit = path.join(resolved, "__init__.py").replace(/\\/g, "/");
|
|
1277
|
+
try {
|
|
1278
|
+
await fs.access(path.join(rootDir, pyInit));
|
|
1279
|
+
return pyInit;
|
|
1280
|
+
} catch {
|
|
1281
|
+
}
|
|
934
1282
|
return null;
|
|
935
1283
|
}
|
|
936
1284
|
computeCyclomaticComplexity(lines) {
|
|
@@ -976,6 +1324,11 @@ var CodeIngestor = class {
|
|
|
976
1324
|
detectLanguage(filePath) {
|
|
977
1325
|
if (/\.tsx?$/.test(filePath)) return "typescript";
|
|
978
1326
|
if (/\.jsx?$/.test(filePath)) return "javascript";
|
|
1327
|
+
if (/\.py$/.test(filePath)) return "python";
|
|
1328
|
+
if (/\.go$/.test(filePath)) return "go";
|
|
1329
|
+
if (/\.rs$/.test(filePath)) return "rust";
|
|
1330
|
+
if (/\.java$/.test(filePath)) return "java";
|
|
1331
|
+
if (/\.groovy$/.test(filePath)) return "java";
|
|
979
1332
|
return "unknown";
|
|
980
1333
|
}
|
|
981
1334
|
/**
|
|
@@ -983,40 +1336,38 @@ var CodeIngestor = class {
|
|
|
983
1336
|
* linking requirement nodes to the annotated files.
|
|
984
1337
|
* Format: // @req <feature-name>#<index>
|
|
985
1338
|
*/
|
|
986
|
-
|
|
987
|
-
const REQ_TAG =
|
|
1339
|
+
extractReqAnnotationsForFile(filePath, content, rootDir) {
|
|
1340
|
+
const REQ_TAG = /(?:\/\/|#|\/\*)\s*@req\s+([\w-]+)#(\d+)/g;
|
|
988
1341
|
const reqNodes = this.store.findNodes({ type: "requirement" });
|
|
989
1342
|
let edgesAdded = 0;
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1343
|
+
let match;
|
|
1344
|
+
REQ_TAG.lastIndex = 0;
|
|
1345
|
+
while ((match = REQ_TAG.exec(content)) !== null) {
|
|
1346
|
+
const featureName = match[1];
|
|
1347
|
+
const reqIndex = parseInt(match[2], 10);
|
|
1348
|
+
const reqNode = reqNodes.find(
|
|
1349
|
+
(n) => n.metadata.featureName === featureName && n.metadata.index === reqIndex
|
|
1350
|
+
);
|
|
1351
|
+
if (!reqNode) {
|
|
1352
|
+
console.warn(
|
|
1353
|
+
`@req annotation references non-existent requirement: ${featureName}#${reqIndex} in ${filePath}`
|
|
998
1354
|
);
|
|
999
|
-
|
|
1000
|
-
console.warn(
|
|
1001
|
-
`@req annotation references non-existent requirement: ${featureName}#${reqIndex} in ${filePath}`
|
|
1002
|
-
);
|
|
1003
|
-
continue;
|
|
1004
|
-
}
|
|
1005
|
-
const relPath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
1006
|
-
const fileNodeId = `file:${relPath}`;
|
|
1007
|
-
this.store.addEdge({
|
|
1008
|
-
from: reqNode.id,
|
|
1009
|
-
to: fileNodeId,
|
|
1010
|
-
type: "verified_by",
|
|
1011
|
-
confidence: 1,
|
|
1012
|
-
metadata: {
|
|
1013
|
-
method: "annotation",
|
|
1014
|
-
tag: `@req ${featureName}#${reqIndex}`,
|
|
1015
|
-
confidence: 1
|
|
1016
|
-
}
|
|
1017
|
-
});
|
|
1018
|
-
edgesAdded++;
|
|
1355
|
+
continue;
|
|
1019
1356
|
}
|
|
1357
|
+
const relPath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
1358
|
+
const fileNodeId = `file:${relPath}`;
|
|
1359
|
+
this.store.addEdge({
|
|
1360
|
+
from: reqNode.id,
|
|
1361
|
+
to: fileNodeId,
|
|
1362
|
+
type: "verified_by",
|
|
1363
|
+
confidence: 1,
|
|
1364
|
+
metadata: {
|
|
1365
|
+
method: "annotation",
|
|
1366
|
+
tag: `@req ${featureName}#${reqIndex}`,
|
|
1367
|
+
confidence: 1
|
|
1368
|
+
}
|
|
1369
|
+
});
|
|
1370
|
+
edgesAdded++;
|
|
1020
1371
|
}
|
|
1021
1372
|
return edgesAdded;
|
|
1022
1373
|
}
|
|
@@ -1420,7 +1771,7 @@ var KnowledgeIngestor = class {
|
|
|
1420
1771
|
const entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
1421
1772
|
for (const entry of entries) {
|
|
1422
1773
|
const fullPath = path3.join(dir, entry.name);
|
|
1423
|
-
if (entry.isDirectory()) {
|
|
1774
|
+
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") {
|
|
1424
1775
|
results.push(...await this.findMarkdownFiles(fullPath));
|
|
1425
1776
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1426
1777
|
results.push(fullPath);
|
|
@@ -1498,9 +1849,176 @@ function parseFailureSection(section) {
|
|
|
1498
1849
|
};
|
|
1499
1850
|
}
|
|
1500
1851
|
|
|
1501
|
-
// src/ingest/
|
|
1852
|
+
// src/ingest/BusinessKnowledgeIngestor.ts
|
|
1502
1853
|
import * as fs3 from "fs/promises";
|
|
1503
1854
|
import * as path4 from "path";
|
|
1855
|
+
var BUSINESS_KNOWLEDGE_TYPES = /* @__PURE__ */ new Set([
|
|
1856
|
+
"business_rule",
|
|
1857
|
+
"business_process",
|
|
1858
|
+
"business_concept",
|
|
1859
|
+
"business_term",
|
|
1860
|
+
"business_metric"
|
|
1861
|
+
]);
|
|
1862
|
+
var GOVERNS_SOURCE_TYPES = /* @__PURE__ */ new Set(["business_rule", "business_process"]);
|
|
1863
|
+
var CODE_NODE_TYPES2 = [
|
|
1864
|
+
"file",
|
|
1865
|
+
"function",
|
|
1866
|
+
"class",
|
|
1867
|
+
"method",
|
|
1868
|
+
"interface",
|
|
1869
|
+
"variable"
|
|
1870
|
+
];
|
|
1871
|
+
var MEASURABLE_TYPES = /* @__PURE__ */ new Set(["business_process", "business_concept"]);
|
|
1872
|
+
var BusinessKnowledgeIngestor = class {
|
|
1873
|
+
constructor(store) {
|
|
1874
|
+
this.store = store;
|
|
1875
|
+
}
|
|
1876
|
+
store;
|
|
1877
|
+
async ingest(knowledgeDir) {
|
|
1878
|
+
const start = Date.now();
|
|
1879
|
+
const errors = [];
|
|
1880
|
+
let files;
|
|
1881
|
+
try {
|
|
1882
|
+
files = await this.findMarkdownFiles(knowledgeDir);
|
|
1883
|
+
} catch {
|
|
1884
|
+
return emptyResult(Date.now() - start);
|
|
1885
|
+
}
|
|
1886
|
+
const nodeEntries = await this.createNodes(files, knowledgeDir, errors);
|
|
1887
|
+
const edgesAdded = this.createEdges(nodeEntries);
|
|
1888
|
+
return {
|
|
1889
|
+
nodesAdded: nodeEntries.length,
|
|
1890
|
+
nodesUpdated: 0,
|
|
1891
|
+
edgesAdded,
|
|
1892
|
+
edgesUpdated: 0,
|
|
1893
|
+
errors,
|
|
1894
|
+
durationMs: Date.now() - start
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
async createNodes(files, knowledgeDir, errors) {
|
|
1898
|
+
const entries = [];
|
|
1899
|
+
for (const filePath of files) {
|
|
1900
|
+
try {
|
|
1901
|
+
const entry = await this.parseAndAddNode(filePath, knowledgeDir);
|
|
1902
|
+
if (entry) entries.push(entry);
|
|
1903
|
+
} catch (err) {
|
|
1904
|
+
errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
return entries;
|
|
1908
|
+
}
|
|
1909
|
+
async parseAndAddNode(filePath, knowledgeDir) {
|
|
1910
|
+
const raw = await fs3.readFile(filePath, "utf-8");
|
|
1911
|
+
const parsed = parseFrontmatter(raw);
|
|
1912
|
+
if (!parsed) return null;
|
|
1913
|
+
const { frontmatter, body } = parsed;
|
|
1914
|
+
if (!BUSINESS_KNOWLEDGE_TYPES.has(frontmatter.type)) return null;
|
|
1915
|
+
const relPath = path4.relative(knowledgeDir, filePath).replaceAll("\\", "/");
|
|
1916
|
+
const domain = frontmatter.domain ?? relPath.split("/")[0] ?? "unknown";
|
|
1917
|
+
const filename = path4.basename(filePath, ".md");
|
|
1918
|
+
const nodeId = `bk:${domain}:${filename}`;
|
|
1919
|
+
const titleMatch = body.match(/^#\s+(.+)$/m);
|
|
1920
|
+
const name = titleMatch ? titleMatch[1].trim() : filename;
|
|
1921
|
+
const node = {
|
|
1922
|
+
id: nodeId,
|
|
1923
|
+
type: frontmatter.type,
|
|
1924
|
+
name,
|
|
1925
|
+
path: relPath,
|
|
1926
|
+
content: body.trim(),
|
|
1927
|
+
metadata: {
|
|
1928
|
+
domain,
|
|
1929
|
+
...frontmatter.tags && { tags: frontmatter.tags },
|
|
1930
|
+
...frontmatter.related && { related: frontmatter.related }
|
|
1931
|
+
}
|
|
1932
|
+
};
|
|
1933
|
+
this.store.addNode(node);
|
|
1934
|
+
return { nodeId, node, content: body };
|
|
1935
|
+
}
|
|
1936
|
+
createEdges(nodeEntries) {
|
|
1937
|
+
let edgesAdded = 0;
|
|
1938
|
+
for (const { nodeId, node, content } of nodeEntries) {
|
|
1939
|
+
if (GOVERNS_SOURCE_TYPES.has(node.type)) {
|
|
1940
|
+
edgesAdded += this.linkToNodes(content, nodeId, "governs", CODE_NODE_TYPES2);
|
|
1941
|
+
} else {
|
|
1942
|
+
edgesAdded += this.linkToNodes(content, nodeId, "documents", CODE_NODE_TYPES2);
|
|
1943
|
+
}
|
|
1944
|
+
if (node.type === "business_metric") {
|
|
1945
|
+
edgesAdded += this.linkToBusinessNodes(content, nodeId, "measures", MEASURABLE_TYPES);
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
return edgesAdded;
|
|
1949
|
+
}
|
|
1950
|
+
linkToNodes(content, sourceNodeId, edgeType, targetTypes) {
|
|
1951
|
+
let count = 0;
|
|
1952
|
+
for (const nodeType of targetTypes) {
|
|
1953
|
+
const nodes = this.store.findNodes({ type: nodeType });
|
|
1954
|
+
for (const node of nodes) {
|
|
1955
|
+
if (node.name.length < 3) continue;
|
|
1956
|
+
const escaped = node.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1957
|
+
const namePattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
1958
|
+
if (namePattern.test(content)) {
|
|
1959
|
+
this.store.addEdge({ from: sourceNodeId, to: node.id, type: edgeType });
|
|
1960
|
+
count++;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
return count;
|
|
1965
|
+
}
|
|
1966
|
+
linkToBusinessNodes(content, sourceNodeId, edgeType, targetTypes) {
|
|
1967
|
+
let count = 0;
|
|
1968
|
+
for (const node of this.store.findNodes({})) {
|
|
1969
|
+
if (!targetTypes.has(node.type)) continue;
|
|
1970
|
+
if (node.name.length < 3) continue;
|
|
1971
|
+
const escaped = node.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1972
|
+
const namePattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
1973
|
+
if (namePattern.test(content)) {
|
|
1974
|
+
this.store.addEdge({ from: sourceNodeId, to: node.id, type: edgeType });
|
|
1975
|
+
count++;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
return count;
|
|
1979
|
+
}
|
|
1980
|
+
async findMarkdownFiles(dir) {
|
|
1981
|
+
const results = [];
|
|
1982
|
+
const entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
1983
|
+
for (const entry of entries) {
|
|
1984
|
+
const fullPath = path4.join(dir, entry.name);
|
|
1985
|
+
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") {
|
|
1986
|
+
results.push(...await this.findMarkdownFiles(fullPath));
|
|
1987
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1988
|
+
results.push(fullPath);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
return results;
|
|
1992
|
+
}
|
|
1993
|
+
};
|
|
1994
|
+
function parseFrontmatter(raw) {
|
|
1995
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
1996
|
+
if (!match) return null;
|
|
1997
|
+
const yamlBlock = match[1];
|
|
1998
|
+
const body = match[2];
|
|
1999
|
+
const frontmatter = {};
|
|
2000
|
+
for (const line of yamlBlock.split("\n")) {
|
|
2001
|
+
const kvMatch = line.match(/^(\w+):\s*(.+)$/);
|
|
2002
|
+
if (!kvMatch) continue;
|
|
2003
|
+
const key = kvMatch[1];
|
|
2004
|
+
const value = kvMatch[2].trim();
|
|
2005
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
2006
|
+
frontmatter[key] = value.slice(1, -1).split(",").map((s) => s.trim());
|
|
2007
|
+
} else {
|
|
2008
|
+
frontmatter[key] = value;
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
if (!frontmatter.type || typeof frontmatter.type !== "string") return null;
|
|
2012
|
+
if (!frontmatter.domain || typeof frontmatter.domain !== "string") return null;
|
|
2013
|
+
return {
|
|
2014
|
+
frontmatter,
|
|
2015
|
+
body
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// src/ingest/RequirementIngestor.ts
|
|
2020
|
+
import * as fs4 from "fs/promises";
|
|
2021
|
+
import * as path5 from "path";
|
|
1504
2022
|
var REQUIREMENT_SECTIONS = [
|
|
1505
2023
|
"Observable Truths",
|
|
1506
2024
|
"Success Criteria",
|
|
@@ -1517,7 +2035,7 @@ function detectEarsPattern(text) {
|
|
|
1517
2035
|
if (/^the\s+\w+\s+shall\b/.test(lower)) return "ubiquitous";
|
|
1518
2036
|
return void 0;
|
|
1519
2037
|
}
|
|
1520
|
-
var
|
|
2038
|
+
var CODE_NODE_TYPES3 = ["file", "function", "class", "method", "interface", "variable"];
|
|
1521
2039
|
var RequirementIngestor = class {
|
|
1522
2040
|
constructor(store) {
|
|
1523
2041
|
this.store = store;
|
|
@@ -1535,8 +2053,8 @@ var RequirementIngestor = class {
|
|
|
1535
2053
|
let edgesAdded = 0;
|
|
1536
2054
|
let featureDirs;
|
|
1537
2055
|
try {
|
|
1538
|
-
const entries = await
|
|
1539
|
-
featureDirs = entries.filter((e) => e.isDirectory()).map((e) =>
|
|
2056
|
+
const entries = await fs4.readdir(specsDir, { withFileTypes: true });
|
|
2057
|
+
featureDirs = entries.filter((e) => e.isDirectory()).map((e) => path5.join(specsDir, e.name));
|
|
1540
2058
|
} catch {
|
|
1541
2059
|
return emptyResult(Date.now() - start);
|
|
1542
2060
|
}
|
|
@@ -1555,11 +2073,11 @@ var RequirementIngestor = class {
|
|
|
1555
2073
|
};
|
|
1556
2074
|
}
|
|
1557
2075
|
async ingestFeatureDir(featureDir, errors) {
|
|
1558
|
-
const featureName =
|
|
1559
|
-
const specPath =
|
|
2076
|
+
const featureName = path5.basename(featureDir);
|
|
2077
|
+
const specPath = path5.join(featureDir, "proposal.md").replaceAll("\\", "/");
|
|
1560
2078
|
let content;
|
|
1561
2079
|
try {
|
|
1562
|
-
content = await
|
|
2080
|
+
content = await fs4.readFile(specPath, "utf-8");
|
|
1563
2081
|
} catch {
|
|
1564
2082
|
return { nodesAdded: 0, edgesAdded: 0 };
|
|
1565
2083
|
}
|
|
@@ -1576,7 +2094,7 @@ var RequirementIngestor = class {
|
|
|
1576
2094
|
this.store.addNode({
|
|
1577
2095
|
id: specNodeId,
|
|
1578
2096
|
type: "document",
|
|
1579
|
-
name:
|
|
2097
|
+
name: path5.basename(specPath),
|
|
1580
2098
|
path: specPath,
|
|
1581
2099
|
metadata: { featureName }
|
|
1582
2100
|
});
|
|
@@ -1691,9 +2209,9 @@ var RequirementIngestor = class {
|
|
|
1691
2209
|
for (const node of fileNodes) {
|
|
1692
2210
|
if (!node.path) continue;
|
|
1693
2211
|
const normalizedPath = node.path.replace(/\\/g, "/");
|
|
1694
|
-
const isCodeMatch = normalizedPath.includes("packages/") &&
|
|
2212
|
+
const isCodeMatch = normalizedPath.includes("packages/") && path5.basename(normalizedPath).includes(featureName);
|
|
1695
2213
|
const isTestMatch = normalizedPath.includes("/tests/") && // platform-safe
|
|
1696
|
-
|
|
2214
|
+
path5.basename(normalizedPath).includes(featureName);
|
|
1697
2215
|
if (isCodeMatch && !isTestMatch) {
|
|
1698
2216
|
this.store.addEdge({
|
|
1699
2217
|
from: reqId,
|
|
@@ -1714,39 +2232,2676 @@ var RequirementIngestor = class {
|
|
|
1714
2232
|
count++;
|
|
1715
2233
|
}
|
|
1716
2234
|
}
|
|
1717
|
-
return count;
|
|
2235
|
+
return count;
|
|
2236
|
+
}
|
|
2237
|
+
/**
|
|
2238
|
+
* Convention-based linking: match requirement text to code nodes
|
|
2239
|
+
* by keyword overlap (function/class names appearing in requirement text).
|
|
2240
|
+
*/
|
|
2241
|
+
linkByKeywordOverlap(reqId, reqText) {
|
|
2242
|
+
let count = 0;
|
|
2243
|
+
for (const nodeType of CODE_NODE_TYPES3) {
|
|
2244
|
+
const codeNodes = this.store.findNodes({ type: nodeType });
|
|
2245
|
+
for (const node of codeNodes) {
|
|
2246
|
+
if (node.name.length < 3) continue;
|
|
2247
|
+
const escaped = node.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2248
|
+
const namePattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
2249
|
+
if (namePattern.test(reqText)) {
|
|
2250
|
+
const edgeType = node.path?.replace(/\\/g, "/").includes("/tests/") ? "verified_by" : "requires";
|
|
2251
|
+
this.store.addEdge({
|
|
2252
|
+
from: reqId,
|
|
2253
|
+
to: node.id,
|
|
2254
|
+
type: edgeType,
|
|
2255
|
+
confidence: 0.6,
|
|
2256
|
+
metadata: { method: "convention", matchReason: "keyword-overlap" }
|
|
2257
|
+
});
|
|
2258
|
+
count++;
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
return count;
|
|
2263
|
+
}
|
|
2264
|
+
};
|
|
2265
|
+
|
|
2266
|
+
// src/ingest/KnowledgePipelineRunner.ts
|
|
2267
|
+
import * as fs9 from "fs/promises";
|
|
2268
|
+
import * as path10 from "path";
|
|
2269
|
+
|
|
2270
|
+
// src/ingest/DiagramParser.ts
|
|
2271
|
+
import * as fs5 from "fs/promises";
|
|
2272
|
+
import * as path6 from "path";
|
|
2273
|
+
function emptyMermaidResult(diagramType = "unknown") {
|
|
2274
|
+
return {
|
|
2275
|
+
entities: [],
|
|
2276
|
+
relationships: [],
|
|
2277
|
+
metadata: { format: "mermaid", diagramType }
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
var MermaidParser = class {
|
|
2281
|
+
canParse(_content, ext) {
|
|
2282
|
+
return ext === ".mmd" || ext === ".mermaid";
|
|
2283
|
+
}
|
|
2284
|
+
parse(content, _filePath) {
|
|
2285
|
+
const trimmed = content.trim();
|
|
2286
|
+
if (!trimmed) {
|
|
2287
|
+
return emptyMermaidResult();
|
|
2288
|
+
}
|
|
2289
|
+
const diagramType = this.detectDiagramType(trimmed);
|
|
2290
|
+
switch (diagramType) {
|
|
2291
|
+
case "flowchart":
|
|
2292
|
+
return this.parseFlowchart(trimmed, diagramType);
|
|
2293
|
+
case "sequence":
|
|
2294
|
+
return this.parseSequence(trimmed, diagramType);
|
|
2295
|
+
default:
|
|
2296
|
+
return emptyMermaidResult(diagramType);
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
detectDiagramType(content) {
|
|
2300
|
+
const lines = content.split("\n");
|
|
2301
|
+
for (const line of lines) {
|
|
2302
|
+
const trimmedLine = line.trim();
|
|
2303
|
+
if (!trimmedLine) continue;
|
|
2304
|
+
if (/^(?:graph|flowchart)\b/i.test(trimmedLine)) return "flowchart";
|
|
2305
|
+
if (/^sequenceDiagram\b/.test(trimmedLine)) return "sequence";
|
|
2306
|
+
if (/^classDiagram\b/.test(trimmedLine)) return "class";
|
|
2307
|
+
if (/^erDiagram\b/.test(trimmedLine)) return "er";
|
|
2308
|
+
break;
|
|
2309
|
+
}
|
|
2310
|
+
return "unknown";
|
|
2311
|
+
}
|
|
2312
|
+
parseFlowchart(content, diagramType) {
|
|
2313
|
+
const entities = /* @__PURE__ */ new Map();
|
|
2314
|
+
const relationships = [];
|
|
2315
|
+
const nodeRegex = /([A-Za-z0-9_]+)\s*[[({]([^\])}]+)[\])}]/g;
|
|
2316
|
+
let match;
|
|
2317
|
+
match = nodeRegex.exec(content);
|
|
2318
|
+
while (match !== null) {
|
|
2319
|
+
const id = match[1] ?? "";
|
|
2320
|
+
const label = (match[2] ?? "").trim();
|
|
2321
|
+
if (id && !entities.has(id)) {
|
|
2322
|
+
const isDecision = this.isDecisionNode(content, id);
|
|
2323
|
+
entities.set(id, {
|
|
2324
|
+
id,
|
|
2325
|
+
label,
|
|
2326
|
+
...isDecision ? { type: "decision" } : {}
|
|
2327
|
+
});
|
|
2328
|
+
}
|
|
2329
|
+
match = nodeRegex.exec(content);
|
|
2330
|
+
}
|
|
2331
|
+
const stripped = content.replace(/[[({][^\])}]*[\])}]/g, "");
|
|
2332
|
+
const labeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\|([^|]+)\|\s*([A-Za-z0-9_]+)/g;
|
|
2333
|
+
const unlabeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\s+([A-Za-z0-9_]+)/g;
|
|
2334
|
+
const edgeKeys = /* @__PURE__ */ new Set();
|
|
2335
|
+
match = labeledEdgeRegex.exec(stripped);
|
|
2336
|
+
while (match !== null) {
|
|
2337
|
+
const from = match[1] ?? "";
|
|
2338
|
+
const label = (match[2] ?? "").trim();
|
|
2339
|
+
const to = match[3] ?? "";
|
|
2340
|
+
if (from && to) {
|
|
2341
|
+
const key = `${from}->${to}:${label}`;
|
|
2342
|
+
if (!edgeKeys.has(key)) {
|
|
2343
|
+
edgeKeys.add(key);
|
|
2344
|
+
relationships.push({ from, to, label });
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
match = labeledEdgeRegex.exec(stripped);
|
|
2348
|
+
}
|
|
2349
|
+
match = unlabeledEdgeRegex.exec(stripped);
|
|
2350
|
+
while (match !== null) {
|
|
2351
|
+
const from = match[1] ?? "";
|
|
2352
|
+
const to = match[2] ?? "";
|
|
2353
|
+
if (from && to) {
|
|
2354
|
+
const hasLabeled = relationships.some((r) => r.from === from && r.to === to);
|
|
2355
|
+
if (!hasLabeled) {
|
|
2356
|
+
const key = `${from}->${to}`;
|
|
2357
|
+
if (!edgeKeys.has(key)) {
|
|
2358
|
+
edgeKeys.add(key);
|
|
2359
|
+
relationships.push({ from, to });
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
match = unlabeledEdgeRegex.exec(stripped);
|
|
2364
|
+
}
|
|
2365
|
+
return {
|
|
2366
|
+
entities: Array.from(entities.values()),
|
|
2367
|
+
relationships,
|
|
2368
|
+
metadata: { format: "mermaid", diagramType }
|
|
2369
|
+
};
|
|
2370
|
+
}
|
|
2371
|
+
isDecisionNode(content, nodeId) {
|
|
2372
|
+
const decisionRegex = new RegExp(`${nodeId}\\s*\\{`);
|
|
2373
|
+
return decisionRegex.test(content);
|
|
2374
|
+
}
|
|
2375
|
+
parseSequence(content, diagramType) {
|
|
2376
|
+
const entities = [];
|
|
2377
|
+
const relationships = [];
|
|
2378
|
+
const seenParticipants = /* @__PURE__ */ new Set();
|
|
2379
|
+
const participantRegex = /participant\s+(\w+)(?:\s+as\s+(.+))?/g;
|
|
2380
|
+
let match;
|
|
2381
|
+
match = participantRegex.exec(content);
|
|
2382
|
+
while (match !== null) {
|
|
2383
|
+
const id = match[1] ?? "";
|
|
2384
|
+
const label = match[2]?.trim() || id;
|
|
2385
|
+
if (id && !seenParticipants.has(id)) {
|
|
2386
|
+
seenParticipants.add(id);
|
|
2387
|
+
entities.push({ id, label });
|
|
2388
|
+
}
|
|
2389
|
+
match = participantRegex.exec(content);
|
|
2390
|
+
}
|
|
2391
|
+
const forwardMsgRegex = /(\w+)\s*->>?\+?\s*(\w+)\s*:\s*(.+)/g;
|
|
2392
|
+
match = forwardMsgRegex.exec(content);
|
|
2393
|
+
while (match !== null) {
|
|
2394
|
+
const from = match[1] ?? "";
|
|
2395
|
+
const to = match[2] ?? "";
|
|
2396
|
+
const label = (match[3] ?? "").trim();
|
|
2397
|
+
if (from && to) {
|
|
2398
|
+
relationships.push({ from, to, label });
|
|
2399
|
+
}
|
|
2400
|
+
match = forwardMsgRegex.exec(content);
|
|
2401
|
+
}
|
|
2402
|
+
const returnMsgRegex = /(\w+)\s*-->>?-?\s*(\w+)\s*:\s*(.+)/g;
|
|
2403
|
+
match = returnMsgRegex.exec(content);
|
|
2404
|
+
while (match !== null) {
|
|
2405
|
+
const from = match[1] ?? "";
|
|
2406
|
+
const to = match[2] ?? "";
|
|
2407
|
+
const label = (match[3] ?? "").trim();
|
|
2408
|
+
if (from && to) {
|
|
2409
|
+
relationships.push({ from, to, label });
|
|
2410
|
+
}
|
|
2411
|
+
match = returnMsgRegex.exec(content);
|
|
2412
|
+
}
|
|
2413
|
+
return {
|
|
2414
|
+
entities,
|
|
2415
|
+
relationships,
|
|
2416
|
+
metadata: { format: "mermaid", diagramType }
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
};
|
|
2420
|
+
var D2Parser = class {
|
|
2421
|
+
canParse(_content, ext) {
|
|
2422
|
+
return ext === ".d2";
|
|
2423
|
+
}
|
|
2424
|
+
parse(content, _filePath) {
|
|
2425
|
+
const trimmed = content.trim();
|
|
2426
|
+
if (!trimmed) {
|
|
2427
|
+
return {
|
|
2428
|
+
entities: [],
|
|
2429
|
+
relationships: [],
|
|
2430
|
+
metadata: { format: "d2", diagramType: "architecture" }
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2433
|
+
const entities = /* @__PURE__ */ new Map();
|
|
2434
|
+
const relationships = [];
|
|
2435
|
+
let braceDepth = 0;
|
|
2436
|
+
for (const line of trimmed.split("\n")) {
|
|
2437
|
+
const stripped = line.trim();
|
|
2438
|
+
if (!stripped || stripped.startsWith("#")) continue;
|
|
2439
|
+
if (stripped.endsWith("{")) {
|
|
2440
|
+
if (braceDepth === 0) {
|
|
2441
|
+
const shapeMatch = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+?)\s*\{$/);
|
|
2442
|
+
if (shapeMatch) {
|
|
2443
|
+
const id = shapeMatch[1];
|
|
2444
|
+
const label = shapeMatch[2];
|
|
2445
|
+
if (id && label) {
|
|
2446
|
+
entities.set(id, { id, label });
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
braceDepth++;
|
|
2451
|
+
continue;
|
|
2452
|
+
}
|
|
2453
|
+
if (stripped === "}") {
|
|
2454
|
+
braceDepth = Math.max(0, braceDepth - 1);
|
|
2455
|
+
continue;
|
|
2456
|
+
}
|
|
2457
|
+
if (braceDepth > 0) continue;
|
|
2458
|
+
const connMatch = stripped.match(
|
|
2459
|
+
/^([a-zA-Z0-9_.-]+)\s*->\s*([a-zA-Z0-9_.-]+)(?:\s*:\s*(.+?))?$/
|
|
2460
|
+
);
|
|
2461
|
+
if (connMatch) {
|
|
2462
|
+
const from = connMatch[1] ?? "";
|
|
2463
|
+
const to = connMatch[2] ?? "";
|
|
2464
|
+
const label = connMatch[3]?.trim();
|
|
2465
|
+
if (from && to) {
|
|
2466
|
+
relationships.push({ from, to, ...label ? { label } : {} });
|
|
2467
|
+
}
|
|
2468
|
+
continue;
|
|
2469
|
+
}
|
|
2470
|
+
const simpleMatch = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+)$/);
|
|
2471
|
+
if (simpleMatch) {
|
|
2472
|
+
const id = simpleMatch[1];
|
|
2473
|
+
const label = (simpleMatch[2] ?? "").trim();
|
|
2474
|
+
if (id && label && !entities.has(id)) {
|
|
2475
|
+
entities.set(id, { id, label });
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
return {
|
|
2480
|
+
entities: Array.from(entities.values()),
|
|
2481
|
+
relationships,
|
|
2482
|
+
metadata: { format: "d2", diagramType: "architecture" }
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
};
|
|
2486
|
+
var PlantUmlParser = class {
|
|
2487
|
+
canParse(_content, ext) {
|
|
2488
|
+
return ext === ".puml" || ext === ".plantuml";
|
|
2489
|
+
}
|
|
2490
|
+
parse(content, _filePath) {
|
|
2491
|
+
const trimmed = content.trim();
|
|
2492
|
+
if (!trimmed) {
|
|
2493
|
+
return {
|
|
2494
|
+
entities: [],
|
|
2495
|
+
relationships: [],
|
|
2496
|
+
metadata: { format: "plantuml", diagramType: "unknown" }
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
const diagramType = this.detectDiagramType(trimmed);
|
|
2500
|
+
const entities = /* @__PURE__ */ new Map();
|
|
2501
|
+
const relationships = [];
|
|
2502
|
+
const body = trimmed.replace(/@startuml\b.*\n?/, "").replace(/@enduml\b.*/, "").trim();
|
|
2503
|
+
const classRegex = /class\s+(\w+)/g;
|
|
2504
|
+
let match;
|
|
2505
|
+
match = classRegex.exec(body);
|
|
2506
|
+
while (match !== null) {
|
|
2507
|
+
const id = match[1] ?? "";
|
|
2508
|
+
if (id && !entities.has(id)) {
|
|
2509
|
+
entities.set(id, { id, label: id });
|
|
2510
|
+
}
|
|
2511
|
+
match = classRegex.exec(body);
|
|
2512
|
+
}
|
|
2513
|
+
const componentRegex1 = /\[([^\]]+)\]/g;
|
|
2514
|
+
match = componentRegex1.exec(body);
|
|
2515
|
+
while (match !== null) {
|
|
2516
|
+
const label = (match[1] ?? "").trim();
|
|
2517
|
+
if (label) {
|
|
2518
|
+
const id = label.replace(/\s+/g, "_");
|
|
2519
|
+
if (!entities.has(id)) {
|
|
2520
|
+
entities.set(id, { id, label });
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
match = componentRegex1.exec(body);
|
|
2524
|
+
}
|
|
2525
|
+
const relRegex = /(\w+)\s*(?:-->|->|<--|<-|\.\.>|--)\s*(\w+)(?:\s*:\s*(.+))?/g;
|
|
2526
|
+
match = relRegex.exec(body);
|
|
2527
|
+
while (match !== null) {
|
|
2528
|
+
const from = match[1] ?? "";
|
|
2529
|
+
const to = match[2] ?? "";
|
|
2530
|
+
const label = match[3]?.trim();
|
|
2531
|
+
if (from && to) {
|
|
2532
|
+
relationships.push({ from, to, ...label ? { label } : {} });
|
|
2533
|
+
}
|
|
2534
|
+
match = relRegex.exec(body);
|
|
2535
|
+
}
|
|
2536
|
+
return {
|
|
2537
|
+
entities: Array.from(entities.values()),
|
|
2538
|
+
relationships,
|
|
2539
|
+
metadata: { format: "plantuml", diagramType }
|
|
2540
|
+
};
|
|
2541
|
+
}
|
|
2542
|
+
detectDiagramType(content) {
|
|
2543
|
+
if (/class\s+\w+/.test(content)) return "class";
|
|
2544
|
+
if (/\[.+\]/.test(content) || /component\s+/.test(content)) return "component";
|
|
2545
|
+
if (/participant\s+/.test(content) || /actor\s+/.test(content)) return "sequence";
|
|
2546
|
+
return "unknown";
|
|
2547
|
+
}
|
|
2548
|
+
};
|
|
2549
|
+
var DIAGRAM_EXTENSIONS = /* @__PURE__ */ new Set([".mmd", ".mermaid", ".d2", ".puml", ".plantuml"]);
|
|
2550
|
+
var DiagramParser = class {
|
|
2551
|
+
constructor(store) {
|
|
2552
|
+
this.store = store;
|
|
2553
|
+
}
|
|
2554
|
+
store;
|
|
2555
|
+
parsers = [
|
|
2556
|
+
new MermaidParser(),
|
|
2557
|
+
new D2Parser(),
|
|
2558
|
+
new PlantUmlParser()
|
|
2559
|
+
];
|
|
2560
|
+
parse(content, filePath) {
|
|
2561
|
+
const ext = path6.extname(filePath).toLowerCase();
|
|
2562
|
+
for (const parser of this.parsers) {
|
|
2563
|
+
if (parser.canParse(content, ext)) {
|
|
2564
|
+
return parser.parse(content, filePath);
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
return {
|
|
2568
|
+
entities: [],
|
|
2569
|
+
relationships: [],
|
|
2570
|
+
metadata: { format: "mermaid", diagramType: "unknown" }
|
|
2571
|
+
};
|
|
2572
|
+
}
|
|
2573
|
+
async ingest(projectDir) {
|
|
2574
|
+
const start = Date.now();
|
|
2575
|
+
let nodesAdded = 0;
|
|
2576
|
+
let edgesAdded = 0;
|
|
2577
|
+
const errors = [];
|
|
2578
|
+
const diagramFiles = await this.findDiagramFiles(projectDir);
|
|
2579
|
+
for (const filePath of diagramFiles) {
|
|
2580
|
+
try {
|
|
2581
|
+
const content = await fs5.readFile(filePath, "utf-8");
|
|
2582
|
+
const relPath = path6.relative(projectDir, filePath).replaceAll("\\", "/");
|
|
2583
|
+
const result = this.parse(content, filePath);
|
|
2584
|
+
if (result.entities.length === 0) continue;
|
|
2585
|
+
const pathHash = hash(relPath);
|
|
2586
|
+
for (const entity of result.entities) {
|
|
2587
|
+
const nodeId = `diagram:${pathHash}:${entity.id}`;
|
|
2588
|
+
this.store.addNode({
|
|
2589
|
+
id: nodeId,
|
|
2590
|
+
type: "business_concept",
|
|
2591
|
+
name: entity.label,
|
|
2592
|
+
path: relPath,
|
|
2593
|
+
metadata: {
|
|
2594
|
+
source: "diagram",
|
|
2595
|
+
format: result.metadata.format,
|
|
2596
|
+
diagramType: result.metadata.diagramType,
|
|
2597
|
+
confidence: 0.85,
|
|
2598
|
+
...entity.type ? { entityType: entity.type } : {}
|
|
2599
|
+
}
|
|
2600
|
+
});
|
|
2601
|
+
nodesAdded++;
|
|
2602
|
+
}
|
|
2603
|
+
for (const rel of result.relationships) {
|
|
2604
|
+
const fromId = `diagram:${pathHash}:${rel.from}`;
|
|
2605
|
+
const toId = `diagram:${pathHash}:${rel.to}`;
|
|
2606
|
+
this.store.addEdge({
|
|
2607
|
+
from: fromId,
|
|
2608
|
+
to: toId,
|
|
2609
|
+
type: "references",
|
|
2610
|
+
metadata: {
|
|
2611
|
+
...rel.label ? { label: rel.label } : {}
|
|
2612
|
+
}
|
|
2613
|
+
});
|
|
2614
|
+
edgesAdded++;
|
|
2615
|
+
}
|
|
2616
|
+
} catch (err) {
|
|
2617
|
+
errors.push(
|
|
2618
|
+
`Failed to parse ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
2619
|
+
);
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
return {
|
|
2623
|
+
nodesAdded,
|
|
2624
|
+
nodesUpdated: 0,
|
|
2625
|
+
edgesAdded,
|
|
2626
|
+
edgesUpdated: 0,
|
|
2627
|
+
errors,
|
|
2628
|
+
durationMs: Date.now() - start
|
|
2629
|
+
};
|
|
2630
|
+
}
|
|
2631
|
+
async findDiagramFiles(dir) {
|
|
2632
|
+
const files = [];
|
|
2633
|
+
const SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".harness"]);
|
|
2634
|
+
const walk = async (currentDir) => {
|
|
2635
|
+
let entries;
|
|
2636
|
+
try {
|
|
2637
|
+
entries = await fs5.readdir(currentDir, { withFileTypes: true });
|
|
2638
|
+
} catch {
|
|
2639
|
+
return;
|
|
2640
|
+
}
|
|
2641
|
+
for (const entry of entries) {
|
|
2642
|
+
if (entry.isDirectory()) {
|
|
2643
|
+
if (!SKIP_DIRS2.has(entry.name)) {
|
|
2644
|
+
await walk(path6.join(currentDir, entry.name));
|
|
2645
|
+
}
|
|
2646
|
+
} else if (entry.isFile()) {
|
|
2647
|
+
const ext = path6.extname(entry.name).toLowerCase();
|
|
2648
|
+
if (DIAGRAM_EXTENSIONS.has(ext)) {
|
|
2649
|
+
files.push(path6.join(currentDir, entry.name));
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
};
|
|
2654
|
+
await walk(dir);
|
|
2655
|
+
return files;
|
|
2656
|
+
}
|
|
2657
|
+
};
|
|
2658
|
+
|
|
2659
|
+
// src/ingest/KnowledgeLinker.ts
|
|
2660
|
+
import * as fs6 from "fs/promises";
|
|
2661
|
+
import * as path7 from "path";
|
|
2662
|
+
var HEURISTIC_PATTERNS = [
|
|
2663
|
+
{
|
|
2664
|
+
name: "business-rule-imperative",
|
|
2665
|
+
pattern: /\b(?:must|shall|required)\b.*\b(?:system|service|api|user|data|app|application|client|server|process|handler|module)\b/i,
|
|
2666
|
+
signal: "Business rule",
|
|
2667
|
+
confidence: 0.7,
|
|
2668
|
+
nodeType: "business_fact"
|
|
2669
|
+
},
|
|
2670
|
+
{
|
|
2671
|
+
name: "sla-slo-pattern",
|
|
2672
|
+
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,
|
|
2673
|
+
signal: "Business constraint (SLA/SLO)",
|
|
2674
|
+
confidence: 0.8,
|
|
2675
|
+
nodeType: "business_fact"
|
|
2676
|
+
},
|
|
2677
|
+
{
|
|
2678
|
+
name: "monetary-amount",
|
|
2679
|
+
pattern: /\$[\d,]+(?:\.\d{2})?\s*(?:\b(?:cost|revenue|budget|price|fee|license|subscription|annual|monthly)\b)?/i,
|
|
2680
|
+
signal: "Business fact (monetary)",
|
|
2681
|
+
confidence: 0.6,
|
|
2682
|
+
nodeType: "business_fact"
|
|
2683
|
+
},
|
|
2684
|
+
{
|
|
2685
|
+
name: "acceptance-criteria",
|
|
2686
|
+
pattern: /\b(?:Given\b.*\bWhen\b.*\bThen\b|\[[ x]\])/i,
|
|
2687
|
+
signal: "Business rule (acceptance criteria)",
|
|
2688
|
+
confidence: 0.8,
|
|
2689
|
+
nodeType: "business_fact"
|
|
2690
|
+
},
|
|
2691
|
+
{
|
|
2692
|
+
name: "regulatory-reference",
|
|
2693
|
+
pattern: /\b(?:GDPR|SOC\s*2|PCI(?:\s*-?\s*DSS)?|HIPAA|CCPA|FERPA|SOX)\b/i,
|
|
2694
|
+
signal: "Business rule (regulatory)",
|
|
2695
|
+
confidence: 0.9,
|
|
2696
|
+
nodeType: "business_fact"
|
|
2697
|
+
}
|
|
2698
|
+
];
|
|
2699
|
+
var SCANNABLE_TYPES = ["issue", "conversation", "document"];
|
|
2700
|
+
function applyReactionBoost(candidate, nodeType, metadata) {
|
|
2701
|
+
if (nodeType !== "conversation" || !metadata.reactions) return;
|
|
2702
|
+
const reactions = metadata.reactions;
|
|
2703
|
+
const totalReactions = Object.values(reactions).reduce((sum, count) => sum + count, 0);
|
|
2704
|
+
if (totalReactions > 0) {
|
|
2705
|
+
candidate.confidence = Math.min(1, Math.round((candidate.confidence + 0.1) * 100) / 100);
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
var KnowledgeLinker = class {
|
|
2709
|
+
constructor(store, outputDir) {
|
|
2710
|
+
this.store = store;
|
|
2711
|
+
this.outputDir = outputDir;
|
|
2712
|
+
}
|
|
2713
|
+
store;
|
|
2714
|
+
outputDir;
|
|
2715
|
+
async link() {
|
|
2716
|
+
const errors = [];
|
|
2717
|
+
const candidates = this.scanAllNodes(errors);
|
|
2718
|
+
await this.writeJsonl(candidates);
|
|
2719
|
+
const conceptsClustered = this.clusterBySource(candidates);
|
|
2720
|
+
const promoted = this.promoteAndDeduplicate(candidates);
|
|
2721
|
+
await this.writeStagedJsonl(promoted.staged);
|
|
2722
|
+
return {
|
|
2723
|
+
factsCreated: promoted.factsCreated,
|
|
2724
|
+
conceptsClustered,
|
|
2725
|
+
duplicatesMerged: promoted.duplicatesMerged,
|
|
2726
|
+
stagedForReview: promoted.staged.length,
|
|
2727
|
+
errors
|
|
2728
|
+
};
|
|
2729
|
+
}
|
|
2730
|
+
/** Stage 1: Scan all scannable node types for heuristic pattern matches. */
|
|
2731
|
+
scanAllNodes(errors) {
|
|
2732
|
+
const candidates = [];
|
|
2733
|
+
for (const type of SCANNABLE_TYPES) {
|
|
2734
|
+
const nodes = this.store.findNodes({ type });
|
|
2735
|
+
for (const node of nodes) {
|
|
2736
|
+
if (!node.content) continue;
|
|
2737
|
+
try {
|
|
2738
|
+
const matches = this.scanPatterns(node.content, node.id, type);
|
|
2739
|
+
for (const match of matches) {
|
|
2740
|
+
applyReactionBoost(match, type, node.metadata);
|
|
2741
|
+
candidates.push(match);
|
|
2742
|
+
}
|
|
2743
|
+
} catch (err) {
|
|
2744
|
+
errors.push(
|
|
2745
|
+
`Scan failed for ${node.id}: ${err instanceof Error ? err.message : String(err)}`
|
|
2746
|
+
);
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
return candidates;
|
|
2751
|
+
}
|
|
2752
|
+
/** Stage 2: Group candidates by source node and create business_concept for 3+. */
|
|
2753
|
+
clusterBySource(candidates) {
|
|
2754
|
+
const sourceGroups = /* @__PURE__ */ new Map();
|
|
2755
|
+
for (const candidate of candidates) {
|
|
2756
|
+
const group = sourceGroups.get(candidate.sourceNodeId) ?? [];
|
|
2757
|
+
group.push(candidate);
|
|
2758
|
+
sourceGroups.set(candidate.sourceNodeId, group);
|
|
2759
|
+
}
|
|
2760
|
+
let clustered = 0;
|
|
2761
|
+
for (const [sourceNodeId, group] of sourceGroups) {
|
|
2762
|
+
if (group.length >= 3) {
|
|
2763
|
+
this.store.addNode({
|
|
2764
|
+
id: `concept:linker:${hash(sourceNodeId)}`,
|
|
2765
|
+
type: "business_concept",
|
|
2766
|
+
name: `Business concept cluster from ${sourceNodeId}`,
|
|
2767
|
+
metadata: {
|
|
2768
|
+
source: "knowledge-linker",
|
|
2769
|
+
sources: [sourceNodeId],
|
|
2770
|
+
factCount: group.length
|
|
2771
|
+
}
|
|
2772
|
+
});
|
|
2773
|
+
clustered++;
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
return clustered;
|
|
2777
|
+
}
|
|
2778
|
+
/** Stage 3: Promote high-confidence candidates, stage medium, discard low. */
|
|
2779
|
+
promoteAndDeduplicate(candidates) {
|
|
2780
|
+
let factsCreated = 0;
|
|
2781
|
+
let duplicatesMerged = 0;
|
|
2782
|
+
const staged = [];
|
|
2783
|
+
for (const candidate of candidates) {
|
|
2784
|
+
if (candidate.confidence >= 0.8) {
|
|
2785
|
+
const merged = this.tryMergeDuplicate(candidate);
|
|
2786
|
+
if (merged) {
|
|
2787
|
+
duplicatesMerged++;
|
|
2788
|
+
continue;
|
|
2789
|
+
}
|
|
2790
|
+
this.createFactNode(candidate);
|
|
2791
|
+
factsCreated++;
|
|
2792
|
+
} else if (candidate.confidence >= 0.5) {
|
|
2793
|
+
staged.push(candidate);
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
return { factsCreated, duplicatesMerged, staged };
|
|
2797
|
+
}
|
|
2798
|
+
/** Check for existing duplicate fact and merge sources if found. Returns true if merged. */
|
|
2799
|
+
tryMergeDuplicate(candidate) {
|
|
2800
|
+
if (this.store.getNode(candidate.id)) return true;
|
|
2801
|
+
const existingFacts = this.store.findNodes({ type: "business_fact" });
|
|
2802
|
+
const duplicate = existingFacts.find(
|
|
2803
|
+
(f) => f.content === candidate.content && f.id !== candidate.id
|
|
2804
|
+
);
|
|
2805
|
+
if (!duplicate) return false;
|
|
2806
|
+
const existingSources = duplicate.metadata.sources ?? [];
|
|
2807
|
+
if (!existingSources.includes(candidate.sourceNodeId)) {
|
|
2808
|
+
existingSources.push(candidate.sourceNodeId);
|
|
2809
|
+
duplicate.metadata.sources = existingSources;
|
|
2810
|
+
}
|
|
2811
|
+
return true;
|
|
2812
|
+
}
|
|
2813
|
+
/** Create a business_fact node and governs edge for a promoted candidate. */
|
|
2814
|
+
createFactNode(candidate) {
|
|
2815
|
+
this.store.addNode({
|
|
2816
|
+
id: candidate.id,
|
|
2817
|
+
type: candidate.nodeType,
|
|
2818
|
+
name: candidate.name,
|
|
2819
|
+
content: candidate.content,
|
|
2820
|
+
metadata: {
|
|
2821
|
+
source: "knowledge-linker",
|
|
2822
|
+
pattern: candidate.pattern,
|
|
2823
|
+
signal: candidate.signal,
|
|
2824
|
+
confidence: candidate.confidence,
|
|
2825
|
+
sourceNodeId: candidate.sourceNodeId,
|
|
2826
|
+
sourceNodeType: candidate.sourceNodeType,
|
|
2827
|
+
sources: [candidate.sourceNodeId]
|
|
2828
|
+
}
|
|
2829
|
+
});
|
|
2830
|
+
this.store.addEdge({
|
|
2831
|
+
from: candidate.id,
|
|
2832
|
+
to: candidate.sourceNodeId,
|
|
2833
|
+
type: "governs",
|
|
2834
|
+
confidence: candidate.confidence,
|
|
2835
|
+
metadata: { source: "knowledge-linker" }
|
|
2836
|
+
});
|
|
2837
|
+
}
|
|
2838
|
+
/**
|
|
2839
|
+
* Write candidates to JSONL file for audit trail.
|
|
2840
|
+
*/
|
|
2841
|
+
async writeJsonl(candidates) {
|
|
2842
|
+
if (!this.outputDir) return;
|
|
2843
|
+
await fs6.mkdir(this.outputDir, { recursive: true });
|
|
2844
|
+
const filePath = path7.join(this.outputDir, "linker.jsonl");
|
|
2845
|
+
const lines = candidates.map(
|
|
2846
|
+
(c) => JSON.stringify({
|
|
2847
|
+
id: c.id,
|
|
2848
|
+
sourceNodeId: c.sourceNodeId,
|
|
2849
|
+
sourceNodeType: c.sourceNodeType,
|
|
2850
|
+
name: c.name,
|
|
2851
|
+
confidence: c.confidence,
|
|
2852
|
+
pattern: c.pattern,
|
|
2853
|
+
signal: c.signal,
|
|
2854
|
+
nodeType: c.nodeType
|
|
2855
|
+
})
|
|
2856
|
+
);
|
|
2857
|
+
await fs6.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
|
|
2858
|
+
}
|
|
2859
|
+
/**
|
|
2860
|
+
* Write medium-confidence candidates to staged JSONL for human review.
|
|
2861
|
+
*/
|
|
2862
|
+
async writeStagedJsonl(candidates) {
|
|
2863
|
+
if (!this.outputDir || candidates.length === 0) return;
|
|
2864
|
+
const stagedDir = path7.join(this.outputDir, "staged");
|
|
2865
|
+
await fs6.mkdir(stagedDir, { recursive: true });
|
|
2866
|
+
const filePath = path7.join(stagedDir, "linker-staged.jsonl");
|
|
2867
|
+
const lines = candidates.map(
|
|
2868
|
+
(c) => JSON.stringify({
|
|
2869
|
+
id: c.id,
|
|
2870
|
+
sourceNodeId: c.sourceNodeId,
|
|
2871
|
+
sourceNodeType: c.sourceNodeType,
|
|
2872
|
+
name: c.name,
|
|
2873
|
+
confidence: c.confidence,
|
|
2874
|
+
pattern: c.pattern,
|
|
2875
|
+
signal: c.signal,
|
|
2876
|
+
nodeType: c.nodeType
|
|
2877
|
+
})
|
|
2878
|
+
);
|
|
2879
|
+
await fs6.writeFile(filePath, lines.join("\n") + "\n");
|
|
2880
|
+
}
|
|
2881
|
+
/**
|
|
2882
|
+
* Apply heuristic patterns to content and return candidate extractions.
|
|
2883
|
+
*/
|
|
2884
|
+
scanPatterns(content, nodeId, nodeType) {
|
|
2885
|
+
const candidates = [];
|
|
2886
|
+
for (const heuristic of HEURISTIC_PATTERNS) {
|
|
2887
|
+
if (heuristic.pattern.test(content)) {
|
|
2888
|
+
const candidateId = `extracted:linker:${hash(nodeId + ":" + heuristic.name)}`;
|
|
2889
|
+
candidates.push({
|
|
2890
|
+
id: candidateId,
|
|
2891
|
+
sourceNodeId: nodeId,
|
|
2892
|
+
sourceNodeType: nodeType,
|
|
2893
|
+
name: `${heuristic.signal} from ${nodeId}`,
|
|
2894
|
+
content,
|
|
2895
|
+
confidence: heuristic.confidence,
|
|
2896
|
+
pattern: heuristic.name,
|
|
2897
|
+
signal: heuristic.signal,
|
|
2898
|
+
nodeType: heuristic.nodeType
|
|
2899
|
+
});
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
return candidates;
|
|
2903
|
+
}
|
|
2904
|
+
};
|
|
2905
|
+
|
|
2906
|
+
// src/ingest/StructuralDriftDetector.ts
|
|
2907
|
+
var StructuralDriftDetector = class {
|
|
2908
|
+
detect(current, fresh) {
|
|
2909
|
+
const findings = [];
|
|
2910
|
+
const currentById = new Map(current.entries.map((e) => [e.id, e]));
|
|
2911
|
+
const freshById = new Map(fresh.entries.map((e) => [e.id, e]));
|
|
2912
|
+
for (const [id, entry] of freshById) {
|
|
2913
|
+
if (!currentById.has(id)) {
|
|
2914
|
+
findings.push({
|
|
2915
|
+
entryId: id,
|
|
2916
|
+
classification: "new",
|
|
2917
|
+
fresh: entry,
|
|
2918
|
+
severity: "low"
|
|
2919
|
+
});
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
for (const [id, entry] of currentById) {
|
|
2923
|
+
if (!freshById.has(id)) {
|
|
2924
|
+
findings.push({
|
|
2925
|
+
entryId: id,
|
|
2926
|
+
classification: "stale",
|
|
2927
|
+
current: entry,
|
|
2928
|
+
severity: "high"
|
|
2929
|
+
});
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
for (const [id, freshEntry] of freshById) {
|
|
2933
|
+
const currentEntry = currentById.get(id);
|
|
2934
|
+
if (currentEntry && currentEntry.contentHash !== freshEntry.contentHash) {
|
|
2935
|
+
findings.push({
|
|
2936
|
+
entryId: id,
|
|
2937
|
+
classification: "drifted",
|
|
2938
|
+
current: currentEntry,
|
|
2939
|
+
fresh: freshEntry,
|
|
2940
|
+
severity: "medium"
|
|
2941
|
+
});
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2945
|
+
for (const entry of fresh.entries) {
|
|
2946
|
+
const group = byName.get(entry.name) ?? [];
|
|
2947
|
+
group.push(entry);
|
|
2948
|
+
byName.set(entry.name, group);
|
|
2949
|
+
}
|
|
2950
|
+
for (const [, group] of byName) {
|
|
2951
|
+
if (group.length > 1) {
|
|
2952
|
+
const sources = new Set(group.map((e) => e.source));
|
|
2953
|
+
const hashes = new Set(group.map((e) => e.contentHash));
|
|
2954
|
+
if (sources.size > 1 && hashes.size > 1) {
|
|
2955
|
+
const alreadyContradicting = new Set(
|
|
2956
|
+
findings.filter((f) => f.classification === "contradicting").map((f) => f.entryId)
|
|
2957
|
+
);
|
|
2958
|
+
for (const entry of group) {
|
|
2959
|
+
if (!alreadyContradicting.has(entry.id)) {
|
|
2960
|
+
findings.push({
|
|
2961
|
+
entryId: entry.id,
|
|
2962
|
+
classification: "contradicting",
|
|
2963
|
+
fresh: entry,
|
|
2964
|
+
severity: "critical"
|
|
2965
|
+
});
|
|
2966
|
+
break;
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
const totalEntries = (/* @__PURE__ */ new Set([...currentById.keys(), ...freshById.keys()])).size;
|
|
2973
|
+
const driftScore = totalEntries > 0 ? findings.length / totalEntries : 0;
|
|
2974
|
+
const summary = {
|
|
2975
|
+
new: findings.filter((f) => f.classification === "new").length,
|
|
2976
|
+
drifted: findings.filter((f) => f.classification === "drifted").length,
|
|
2977
|
+
stale: findings.filter((f) => f.classification === "stale").length,
|
|
2978
|
+
contradicting: findings.filter((f) => f.classification === "contradicting").length
|
|
2979
|
+
};
|
|
2980
|
+
return { findings, driftScore, summary };
|
|
2981
|
+
}
|
|
2982
|
+
};
|
|
2983
|
+
|
|
2984
|
+
// src/ingest/KnowledgeStagingAggregator.ts
|
|
2985
|
+
import * as fs7 from "fs/promises";
|
|
2986
|
+
import * as path8 from "path";
|
|
2987
|
+
var KnowledgeStagingAggregator = class {
|
|
2988
|
+
constructor(projectDir) {
|
|
2989
|
+
this.projectDir = projectDir;
|
|
2990
|
+
}
|
|
2991
|
+
projectDir;
|
|
2992
|
+
async aggregate(extractorResults, linkerResults, diagramResults) {
|
|
2993
|
+
const all = [...extractorResults, ...linkerResults, ...diagramResults];
|
|
2994
|
+
const byHash = /* @__PURE__ */ new Map();
|
|
2995
|
+
for (const entry of all) {
|
|
2996
|
+
const existing = byHash.get(entry.contentHash);
|
|
2997
|
+
if (!existing || entry.confidence > existing.confidence) {
|
|
2998
|
+
byHash.set(entry.contentHash, entry);
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
const deduplicated = Array.from(byHash.values());
|
|
3002
|
+
if (deduplicated.length === 0) {
|
|
3003
|
+
return { staged: 0 };
|
|
3004
|
+
}
|
|
3005
|
+
const stagedDir = path8.join(this.projectDir, ".harness", "knowledge", "staged");
|
|
3006
|
+
await fs7.mkdir(stagedDir, { recursive: true });
|
|
3007
|
+
const jsonl = deduplicated.map((entry) => JSON.stringify(entry)).join("\n") + "\n";
|
|
3008
|
+
await fs7.writeFile(path8.join(stagedDir, "pipeline-staged.jsonl"), jsonl, "utf-8");
|
|
3009
|
+
return { staged: deduplicated.length };
|
|
3010
|
+
}
|
|
3011
|
+
async generateGapReport(knowledgeDir) {
|
|
3012
|
+
const domains = [];
|
|
3013
|
+
let totalEntries = 0;
|
|
3014
|
+
try {
|
|
3015
|
+
const entries = await fs7.readdir(knowledgeDir, { withFileTypes: true });
|
|
3016
|
+
const domainDirs = entries.filter((e) => e.isDirectory());
|
|
3017
|
+
for (const dir of domainDirs) {
|
|
3018
|
+
const domainPath = path8.join(knowledgeDir, dir.name);
|
|
3019
|
+
const files = await fs7.readdir(domainPath);
|
|
3020
|
+
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
3021
|
+
const entryCount = mdFiles.length;
|
|
3022
|
+
totalEntries += entryCount;
|
|
3023
|
+
domains.push({ domain: dir.name, entryCount });
|
|
3024
|
+
}
|
|
3025
|
+
} catch {
|
|
3026
|
+
}
|
|
3027
|
+
return {
|
|
3028
|
+
domains,
|
|
3029
|
+
totalEntries,
|
|
3030
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3031
|
+
};
|
|
3032
|
+
}
|
|
3033
|
+
async writeGapReport(report) {
|
|
3034
|
+
const gapsDir = path8.join(this.projectDir, ".harness", "knowledge");
|
|
3035
|
+
await fs7.mkdir(gapsDir, { recursive: true });
|
|
3036
|
+
const lines = [
|
|
3037
|
+
"# Knowledge Gaps Report",
|
|
3038
|
+
"",
|
|
3039
|
+
`Generated: ${report.generatedAt}`,
|
|
3040
|
+
"",
|
|
3041
|
+
"## Coverage by Domain",
|
|
3042
|
+
"",
|
|
3043
|
+
"| Domain | Entries |",
|
|
3044
|
+
"| ------ | ------- |"
|
|
3045
|
+
];
|
|
3046
|
+
for (const domain of report.domains) {
|
|
3047
|
+
lines.push(`| ${domain.domain} | ${domain.entryCount} |`);
|
|
3048
|
+
}
|
|
3049
|
+
lines.push("", `## Total Entries: ${report.totalEntries}`, "");
|
|
3050
|
+
await fs7.writeFile(path8.join(gapsDir, "gaps.md"), lines.join("\n"), "utf-8");
|
|
3051
|
+
}
|
|
3052
|
+
};
|
|
3053
|
+
|
|
3054
|
+
// src/ingest/extractors/ExtractionRunner.ts
|
|
3055
|
+
import * as fs8 from "fs/promises";
|
|
3056
|
+
import * as path9 from "path";
|
|
3057
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
3058
|
+
"node_modules",
|
|
3059
|
+
"dist",
|
|
3060
|
+
"target",
|
|
3061
|
+
"build",
|
|
3062
|
+
".git",
|
|
3063
|
+
"__pycache__",
|
|
3064
|
+
".venv",
|
|
3065
|
+
".turbo",
|
|
3066
|
+
".harness",
|
|
3067
|
+
".next",
|
|
3068
|
+
".vscode",
|
|
3069
|
+
".idea",
|
|
3070
|
+
"vendor",
|
|
3071
|
+
"out",
|
|
3072
|
+
".gradle",
|
|
3073
|
+
".gradle-home",
|
|
3074
|
+
"bin",
|
|
3075
|
+
"obj",
|
|
3076
|
+
"venv",
|
|
3077
|
+
"_build",
|
|
3078
|
+
"deps",
|
|
3079
|
+
"coverage",
|
|
3080
|
+
".nyc_output"
|
|
3081
|
+
]);
|
|
3082
|
+
var EXT_TO_LANGUAGE = {
|
|
3083
|
+
".ts": "typescript",
|
|
3084
|
+
".tsx": "typescript",
|
|
3085
|
+
".js": "javascript",
|
|
3086
|
+
".jsx": "javascript",
|
|
3087
|
+
".py": "python",
|
|
3088
|
+
".go": "go",
|
|
3089
|
+
".rs": "rust",
|
|
3090
|
+
".java": "java"
|
|
3091
|
+
};
|
|
3092
|
+
var SKIP_EXTENSIONS2 = /* @__PURE__ */ new Set([".d.ts"]);
|
|
3093
|
+
function detectLanguage(filePath) {
|
|
3094
|
+
const name = path9.basename(filePath);
|
|
3095
|
+
for (const skip of SKIP_EXTENSIONS2) {
|
|
3096
|
+
if (name.endsWith(skip)) return void 0;
|
|
3097
|
+
}
|
|
3098
|
+
const ext = path9.extname(filePath);
|
|
3099
|
+
return EXT_TO_LANGUAGE[ext];
|
|
3100
|
+
}
|
|
3101
|
+
var EXTRACTOR_EDGE_TYPE = {
|
|
3102
|
+
"test-descriptions": "governs",
|
|
3103
|
+
"enum-constants": "documents",
|
|
3104
|
+
"validation-rules": "governs",
|
|
3105
|
+
"api-paths": "documents"
|
|
3106
|
+
};
|
|
3107
|
+
var ExtractionRunner = class {
|
|
3108
|
+
constructor(extractors) {
|
|
3109
|
+
this.extractors = extractors;
|
|
3110
|
+
}
|
|
3111
|
+
extractors;
|
|
3112
|
+
/**
|
|
3113
|
+
* Run all extractors against a project directory.
|
|
3114
|
+
* @param projectDir - Project root directory
|
|
3115
|
+
* @param store - GraphStore for node/edge persistence
|
|
3116
|
+
* @param outputDir - Directory for JSONL output (e.g. .harness/knowledge/extracted/)
|
|
3117
|
+
*/
|
|
3118
|
+
async run(projectDir, store, outputDir) {
|
|
3119
|
+
const start = Date.now();
|
|
3120
|
+
const errors = [];
|
|
3121
|
+
let nodesAdded = 0;
|
|
3122
|
+
let nodesUpdated = 0;
|
|
3123
|
+
let edgesAdded = 0;
|
|
3124
|
+
await fs8.mkdir(outputDir, { recursive: true });
|
|
3125
|
+
const files = await this.findSourceFiles(projectDir);
|
|
3126
|
+
const recordsByExtractor = /* @__PURE__ */ new Map();
|
|
3127
|
+
for (const ext of this.extractors) {
|
|
3128
|
+
recordsByExtractor.set(ext.name, []);
|
|
3129
|
+
}
|
|
3130
|
+
for (const filePath of files) {
|
|
3131
|
+
const language = detectLanguage(filePath);
|
|
3132
|
+
if (!language) continue;
|
|
3133
|
+
const ext = path9.extname(filePath);
|
|
3134
|
+
let content;
|
|
3135
|
+
try {
|
|
3136
|
+
content = await fs8.readFile(filePath, "utf-8");
|
|
3137
|
+
} catch (err) {
|
|
3138
|
+
errors.push(
|
|
3139
|
+
`Failed to read ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
3140
|
+
);
|
|
3141
|
+
continue;
|
|
3142
|
+
}
|
|
3143
|
+
const relativePath = path9.relative(projectDir, filePath).replaceAll("\\", "/");
|
|
3144
|
+
for (const extractor2 of this.extractors) {
|
|
3145
|
+
if (!extractor2.supportedExtensions.includes(ext)) continue;
|
|
3146
|
+
try {
|
|
3147
|
+
const records = extractor2.extract(content, relativePath, language);
|
|
3148
|
+
recordsByExtractor.get(extractor2.name).push(...records);
|
|
3149
|
+
} catch (err) {
|
|
3150
|
+
errors.push(
|
|
3151
|
+
`Extractor ${extractor2.name} failed on ${relativePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
3152
|
+
);
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
const allCurrentIds = /* @__PURE__ */ new Set();
|
|
3157
|
+
for (const [extractorName, records] of recordsByExtractor) {
|
|
3158
|
+
await this.writeJsonl(records, outputDir, extractorName);
|
|
3159
|
+
for (const record of records) {
|
|
3160
|
+
allCurrentIds.add(record.id);
|
|
3161
|
+
const result = this.persistRecord(record, store);
|
|
3162
|
+
nodesAdded += result.nodesAdded;
|
|
3163
|
+
nodesUpdated += result.nodesUpdated;
|
|
3164
|
+
edgesAdded += result.edgesAdded;
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
const staleCount = this.markStale(store, allCurrentIds);
|
|
3168
|
+
nodesUpdated += staleCount;
|
|
3169
|
+
return {
|
|
3170
|
+
nodesAdded,
|
|
3171
|
+
nodesUpdated,
|
|
3172
|
+
edgesAdded,
|
|
3173
|
+
edgesUpdated: 0,
|
|
3174
|
+
errors,
|
|
3175
|
+
durationMs: Date.now() - start
|
|
3176
|
+
};
|
|
3177
|
+
}
|
|
3178
|
+
/** Write extraction records to JSONL file. */
|
|
3179
|
+
async writeJsonl(records, outputDir, extractorName) {
|
|
3180
|
+
const filePath = path9.join(outputDir, `${extractorName}.jsonl`);
|
|
3181
|
+
const lines = records.map((r) => JSON.stringify(r));
|
|
3182
|
+
await fs8.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
|
|
3183
|
+
}
|
|
3184
|
+
/** Create or update a graph node from an extraction record. */
|
|
3185
|
+
persistRecord(record, store) {
|
|
3186
|
+
const existing = store.getNode(record.id);
|
|
3187
|
+
const node = {
|
|
3188
|
+
id: record.id,
|
|
3189
|
+
type: record.nodeType,
|
|
3190
|
+
name: record.name,
|
|
3191
|
+
path: record.filePath,
|
|
3192
|
+
location: {
|
|
3193
|
+
fileId: `file:${hash(record.filePath)}`,
|
|
3194
|
+
startLine: record.line,
|
|
3195
|
+
endLine: record.line
|
|
3196
|
+
},
|
|
3197
|
+
content: record.content,
|
|
3198
|
+
metadata: {
|
|
3199
|
+
...record.metadata,
|
|
3200
|
+
source: "code-extractor",
|
|
3201
|
+
extractor: record.extractor,
|
|
3202
|
+
confidence: record.confidence,
|
|
3203
|
+
language: record.language,
|
|
3204
|
+
stale: false
|
|
3205
|
+
}
|
|
3206
|
+
};
|
|
3207
|
+
store.addNode(node);
|
|
3208
|
+
const fileNodeId = `file:${hash(record.filePath)}`;
|
|
3209
|
+
const edgeType = EXTRACTOR_EDGE_TYPE[record.extractor] ?? "documents";
|
|
3210
|
+
const edge = {
|
|
3211
|
+
from: record.id,
|
|
3212
|
+
to: fileNodeId,
|
|
3213
|
+
type: edgeType,
|
|
3214
|
+
confidence: record.confidence,
|
|
3215
|
+
metadata: { source: "code-extractor" }
|
|
3216
|
+
};
|
|
3217
|
+
store.addEdge(edge);
|
|
3218
|
+
return {
|
|
3219
|
+
nodesAdded: existing ? 0 : 1,
|
|
3220
|
+
nodesUpdated: existing ? 1 : 0,
|
|
3221
|
+
edgesAdded: existing ? 0 : 1
|
|
3222
|
+
};
|
|
3223
|
+
}
|
|
3224
|
+
/**
|
|
3225
|
+
* Mark nodes from previous extractions that are no longer present as stale.
|
|
3226
|
+
* Returns the number of nodes marked stale.
|
|
3227
|
+
*/
|
|
3228
|
+
markStale(store, currentIds) {
|
|
3229
|
+
let count = 0;
|
|
3230
|
+
const businessTypes = ["business_rule", "business_process", "business_term"];
|
|
3231
|
+
for (const type of businessTypes) {
|
|
3232
|
+
const nodes = store.findNodes({ type });
|
|
3233
|
+
for (const node of nodes) {
|
|
3234
|
+
if (node.metadata.source === "code-extractor" && !node.metadata.stale && !currentIds.has(node.id)) {
|
|
3235
|
+
store.addNode({
|
|
3236
|
+
...node,
|
|
3237
|
+
metadata: {
|
|
3238
|
+
...node.metadata,
|
|
3239
|
+
stale: true,
|
|
3240
|
+
staleAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3241
|
+
}
|
|
3242
|
+
});
|
|
3243
|
+
count++;
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
return count;
|
|
3248
|
+
}
|
|
3249
|
+
/** Recursively find source files, skipping common non-source directories. */
|
|
3250
|
+
async findSourceFiles(dir) {
|
|
3251
|
+
const results = [];
|
|
3252
|
+
let entries;
|
|
3253
|
+
try {
|
|
3254
|
+
entries = await fs8.readdir(dir, { withFileTypes: true });
|
|
3255
|
+
} catch {
|
|
3256
|
+
return results;
|
|
3257
|
+
}
|
|
3258
|
+
for (const entry of entries) {
|
|
3259
|
+
const fullPath = path9.join(dir, entry.name);
|
|
3260
|
+
if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
|
|
3261
|
+
results.push(...await this.findSourceFiles(fullPath));
|
|
3262
|
+
} else if (entry.isFile() && detectLanguage(fullPath) !== void 0) {
|
|
3263
|
+
results.push(fullPath);
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
return results;
|
|
3267
|
+
}
|
|
3268
|
+
};
|
|
3269
|
+
|
|
3270
|
+
// src/ingest/extractors/TestDescriptionExtractor.ts
|
|
3271
|
+
var PATTERNS = {
|
|
3272
|
+
typescript: [/(?:describe|it|test)\s*\(\s*(['"`])((?:(?!\1).)*)\1/g],
|
|
3273
|
+
javascript: [/(?:describe|it|test)\s*\(\s*(['"`])((?:(?!\1).)*)\1/g],
|
|
3274
|
+
python: [/def\s+(test_\w+)\s*\(/g, /"""((?:(?!""").)*?)"""/gs, /class\s+(Test\w+)/g],
|
|
3275
|
+
go: [/func\s+(Test\w+)\s*\(/g, /t\.Run\(\s*"([^"]+)"/g],
|
|
3276
|
+
rust: [/#\[test\]\s*\n\s*(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/g, /\/\/\/\s*(.+)/g],
|
|
3277
|
+
java: [
|
|
3278
|
+
/@DisplayName\s*\(\s*"([^"]+)"\s*\)/g,
|
|
3279
|
+
/@Test\s*\n\s*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:void|[\w<>]+)\s+(\w+)\s*\(/g
|
|
3280
|
+
]
|
|
3281
|
+
};
|
|
3282
|
+
var TestDescriptionExtractor = class {
|
|
3283
|
+
name = "test-descriptions";
|
|
3284
|
+
supportedExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
|
|
3285
|
+
extract(content, filePath, language) {
|
|
3286
|
+
const records = [];
|
|
3287
|
+
const patterns = PATTERNS[language];
|
|
3288
|
+
if (!patterns) return records;
|
|
3289
|
+
const lines = content.split("\n");
|
|
3290
|
+
if (language === "typescript" || language === "javascript") {
|
|
3291
|
+
return this.extractJsTs(content, filePath, language, lines);
|
|
3292
|
+
}
|
|
3293
|
+
if (language === "python") {
|
|
3294
|
+
return this.extractPython(content, filePath, language, lines);
|
|
3295
|
+
}
|
|
3296
|
+
if (language === "go") {
|
|
3297
|
+
return this.extractGo(content, filePath, language, lines);
|
|
3298
|
+
}
|
|
3299
|
+
if (language === "rust") {
|
|
3300
|
+
return this.extractRust(content, filePath, language, lines);
|
|
3301
|
+
}
|
|
3302
|
+
if (language === "java") {
|
|
3303
|
+
return this.extractJava(content, filePath, language, lines);
|
|
3304
|
+
}
|
|
3305
|
+
return records;
|
|
3306
|
+
}
|
|
3307
|
+
extractJsTs(_content, filePath, language, lines) {
|
|
3308
|
+
const records = [];
|
|
3309
|
+
const describeStack = [];
|
|
3310
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3311
|
+
const line = lines[i];
|
|
3312
|
+
const describeMatch = line.match(/describe\s*\(\s*(['"`])((?:(?!\1).)*)\1/);
|
|
3313
|
+
if (describeMatch) {
|
|
3314
|
+
describeStack.push(describeMatch[2]);
|
|
3315
|
+
}
|
|
3316
|
+
const itMatch = line.match(/(?:it|test)\s*\(\s*(['"`])((?:(?!\1).)*)\1/);
|
|
3317
|
+
if (itMatch) {
|
|
3318
|
+
const testName = itMatch[2];
|
|
3319
|
+
const fullPath = [...describeStack, testName].join(" > ");
|
|
3320
|
+
const patternKey = fullPath;
|
|
3321
|
+
records.push({
|
|
3322
|
+
id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
|
|
3323
|
+
extractor: "test-descriptions",
|
|
3324
|
+
language,
|
|
3325
|
+
filePath,
|
|
3326
|
+
line: i + 1,
|
|
3327
|
+
nodeType: "business_rule",
|
|
3328
|
+
name: testName,
|
|
3329
|
+
content: fullPath,
|
|
3330
|
+
confidence: 0.7,
|
|
3331
|
+
metadata: {
|
|
3332
|
+
suite: describeStack.length > 0 ? describeStack[describeStack.length - 1] : void 0,
|
|
3333
|
+
framework: "vitest"
|
|
3334
|
+
}
|
|
3335
|
+
});
|
|
3336
|
+
}
|
|
3337
|
+
if (line.match(/^\s*\}\s*\)\s*;?\s*$/) && describeStack.length > 0) {
|
|
3338
|
+
describeStack.pop();
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
return records;
|
|
3342
|
+
}
|
|
3343
|
+
extractPython(_content, filePath, language, lines) {
|
|
3344
|
+
const records = [];
|
|
3345
|
+
let currentClass;
|
|
3346
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3347
|
+
const line = lines[i];
|
|
3348
|
+
const classMatch = line.match(/^class\s+(Test\w+)/);
|
|
3349
|
+
if (classMatch) {
|
|
3350
|
+
currentClass = classMatch[1];
|
|
3351
|
+
}
|
|
3352
|
+
const funcMatch = line.match(/^\s*def\s+(test_\w+)\s*\(/);
|
|
3353
|
+
if (funcMatch) {
|
|
3354
|
+
const testName = funcMatch[1];
|
|
3355
|
+
const humanName = testName.replace(/^test_/, "").replace(/_/g, " ");
|
|
3356
|
+
let docstring;
|
|
3357
|
+
if (i + 1 < lines.length) {
|
|
3358
|
+
const nextLine = lines[i + 1].trim();
|
|
3359
|
+
const docMatch = nextLine.match(/^"""(.+?)"""/);
|
|
3360
|
+
if (docMatch) {
|
|
3361
|
+
docstring = docMatch[1];
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
const patternKey = currentClass ? `${currentClass}.${testName}` : testName;
|
|
3365
|
+
records.push({
|
|
3366
|
+
id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
|
|
3367
|
+
extractor: "test-descriptions",
|
|
3368
|
+
language,
|
|
3369
|
+
filePath,
|
|
3370
|
+
line: i + 1,
|
|
3371
|
+
nodeType: "business_rule",
|
|
3372
|
+
name: docstring ?? humanName,
|
|
3373
|
+
content: patternKey,
|
|
3374
|
+
confidence: docstring ? 0.7 : 0.5,
|
|
3375
|
+
metadata: {
|
|
3376
|
+
suite: currentClass,
|
|
3377
|
+
framework: "pytest",
|
|
3378
|
+
functionName: testName
|
|
3379
|
+
}
|
|
3380
|
+
});
|
|
3381
|
+
}
|
|
3382
|
+
if (currentClass && /^\S/.test(line) && !line.startsWith("class ") && !line.startsWith("#") && line.trim() !== "") {
|
|
3383
|
+
currentClass = void 0;
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
return records;
|
|
3387
|
+
}
|
|
3388
|
+
extractGo(_content, filePath, language, lines) {
|
|
3389
|
+
const records = [];
|
|
3390
|
+
let currentTest;
|
|
3391
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3392
|
+
const line = lines[i];
|
|
3393
|
+
const funcMatch = line.match(/^func\s+(Test\w+)\s*\(/);
|
|
3394
|
+
if (funcMatch) {
|
|
3395
|
+
currentTest = funcMatch[1];
|
|
3396
|
+
const humanName = currentTest.replace(/^Test/, "").replace(/([A-Z])/g, " $1").trim();
|
|
3397
|
+
records.push({
|
|
3398
|
+
id: `extracted:test-descriptions:${hash(filePath + ":" + currentTest)}`,
|
|
3399
|
+
extractor: "test-descriptions",
|
|
3400
|
+
language,
|
|
3401
|
+
filePath,
|
|
3402
|
+
line: i + 1,
|
|
3403
|
+
nodeType: "business_rule",
|
|
3404
|
+
name: humanName,
|
|
3405
|
+
content: currentTest,
|
|
3406
|
+
confidence: 0.5,
|
|
3407
|
+
metadata: { framework: "testing" }
|
|
3408
|
+
});
|
|
3409
|
+
}
|
|
3410
|
+
const runMatch = line.match(/t\.Run\(\s*"([^"]+)"/);
|
|
3411
|
+
if (runMatch && currentTest) {
|
|
3412
|
+
const subtestName = runMatch[1];
|
|
3413
|
+
const patternKey = `${currentTest} > ${subtestName}`;
|
|
3414
|
+
records.push({
|
|
3415
|
+
id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
|
|
3416
|
+
extractor: "test-descriptions",
|
|
3417
|
+
language,
|
|
3418
|
+
filePath,
|
|
3419
|
+
line: i + 1,
|
|
3420
|
+
nodeType: "business_rule",
|
|
3421
|
+
name: subtestName,
|
|
3422
|
+
content: patternKey,
|
|
3423
|
+
confidence: 0.7,
|
|
3424
|
+
metadata: {
|
|
3425
|
+
suite: currentTest,
|
|
3426
|
+
framework: "testing"
|
|
3427
|
+
}
|
|
3428
|
+
});
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
return records;
|
|
3432
|
+
}
|
|
3433
|
+
extractRust(_content, filePath, language, lines) {
|
|
3434
|
+
const records = [];
|
|
3435
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3436
|
+
const line = lines[i];
|
|
3437
|
+
if (line.trim() === "#[test]") {
|
|
3438
|
+
for (let j = i + 1; j < lines.length && j <= i + 3; j++) {
|
|
3439
|
+
const fnLine = lines[j];
|
|
3440
|
+
const fnMatch = fnLine.match(/(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/);
|
|
3441
|
+
if (fnMatch) {
|
|
3442
|
+
const testName = fnMatch[1];
|
|
3443
|
+
const humanName = testName.replace(/^test_/, "").replace(/_/g, " ");
|
|
3444
|
+
let docComment;
|
|
3445
|
+
if (i > 0) {
|
|
3446
|
+
const prevLine = lines[i - 1].trim();
|
|
3447
|
+
const docMatch = prevLine.match(/^\/\/\/\s*(.+)/);
|
|
3448
|
+
if (docMatch) {
|
|
3449
|
+
docComment = docMatch[1];
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
records.push({
|
|
3453
|
+
id: `extracted:test-descriptions:${hash(filePath + ":" + testName)}`,
|
|
3454
|
+
extractor: "test-descriptions",
|
|
3455
|
+
language,
|
|
3456
|
+
filePath,
|
|
3457
|
+
line: j + 1,
|
|
3458
|
+
nodeType: "business_rule",
|
|
3459
|
+
name: docComment ?? humanName,
|
|
3460
|
+
content: testName,
|
|
3461
|
+
confidence: docComment ? 0.7 : 0.5,
|
|
3462
|
+
metadata: { framework: "rust-test" }
|
|
3463
|
+
});
|
|
3464
|
+
break;
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3469
|
+
return records;
|
|
3470
|
+
}
|
|
3471
|
+
extractJava(_content, filePath, language, lines) {
|
|
3472
|
+
const records = [];
|
|
3473
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3474
|
+
const line = lines[i];
|
|
3475
|
+
const testMatch = line.match(/@Test\s*$/);
|
|
3476
|
+
if (testMatch) {
|
|
3477
|
+
let displayName;
|
|
3478
|
+
for (let k = Math.max(0, i - 3); k < i; k++) {
|
|
3479
|
+
const prevLine = lines[k];
|
|
3480
|
+
const dm = prevLine.match(/@DisplayName\s*\(\s*"([^"]+)"\s*\)/);
|
|
3481
|
+
if (dm) displayName = dm[1];
|
|
3482
|
+
}
|
|
3483
|
+
for (let j = i + 1; j < lines.length && j <= i + 5; j++) {
|
|
3484
|
+
const scanLine = lines[j];
|
|
3485
|
+
const adjacentDisplay = scanLine.match(/@DisplayName\s*\(\s*"([^"]+)"\s*\)/);
|
|
3486
|
+
if (adjacentDisplay) {
|
|
3487
|
+
displayName = adjacentDisplay[1];
|
|
3488
|
+
continue;
|
|
3489
|
+
}
|
|
3490
|
+
const methodMatch = scanLine.match(
|
|
3491
|
+
/^\s*(?:(?:public|private|protected)\s+)?(?:static\s+)?(?:void|[\w<>[\]]+)\s+(\w+)\s*\(/
|
|
3492
|
+
);
|
|
3493
|
+
if (methodMatch) {
|
|
3494
|
+
const methodName = methodMatch[1];
|
|
3495
|
+
const name = displayName ?? methodName;
|
|
3496
|
+
const patternKey = displayName ?? methodName;
|
|
3497
|
+
records.push({
|
|
3498
|
+
id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
|
|
3499
|
+
extractor: "test-descriptions",
|
|
3500
|
+
language,
|
|
3501
|
+
filePath,
|
|
3502
|
+
line: j + 1,
|
|
3503
|
+
nodeType: "business_rule",
|
|
3504
|
+
name,
|
|
3505
|
+
content: patternKey,
|
|
3506
|
+
confidence: displayName ? 0.7 : 0.5,
|
|
3507
|
+
metadata: { framework: "junit5" }
|
|
3508
|
+
});
|
|
3509
|
+
break;
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
return records;
|
|
3515
|
+
}
|
|
3516
|
+
};
|
|
3517
|
+
|
|
3518
|
+
// src/ingest/extractors/EnumConstantExtractor.ts
|
|
3519
|
+
var EnumConstantExtractor = class {
|
|
3520
|
+
name = "enum-constants";
|
|
3521
|
+
supportedExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
|
|
3522
|
+
extract(content, filePath, language) {
|
|
3523
|
+
switch (language) {
|
|
3524
|
+
case "typescript":
|
|
3525
|
+
return this.extractTypeScript(content, filePath, language);
|
|
3526
|
+
case "javascript":
|
|
3527
|
+
return this.extractJavaScript(content, filePath, language);
|
|
3528
|
+
case "python":
|
|
3529
|
+
return this.extractPython(content, filePath, language);
|
|
3530
|
+
case "go":
|
|
3531
|
+
return this.extractGo(content, filePath, language);
|
|
3532
|
+
case "rust":
|
|
3533
|
+
return this.extractRust(content, filePath, language);
|
|
3534
|
+
case "java":
|
|
3535
|
+
return this.extractJava(content, filePath, language);
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
extractTypeScript(content, filePath, language) {
|
|
3539
|
+
const records = [];
|
|
3540
|
+
const lines = content.split("\n");
|
|
3541
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3542
|
+
const line = lines[i];
|
|
3543
|
+
const enumMatch = line.match(/(?:export\s+)?enum\s+(\w+)/);
|
|
3544
|
+
if (enumMatch) {
|
|
3545
|
+
const enumName = enumMatch[1];
|
|
3546
|
+
const members = this.collectEnumMembers(lines, i + 1);
|
|
3547
|
+
records.push({
|
|
3548
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + enumName)}`,
|
|
3549
|
+
extractor: "enum-constants",
|
|
3550
|
+
language,
|
|
3551
|
+
filePath,
|
|
3552
|
+
line: i + 1,
|
|
3553
|
+
nodeType: "business_term",
|
|
3554
|
+
name: enumName,
|
|
3555
|
+
content: `enum ${enumName} { ${members.join(", ")} }`,
|
|
3556
|
+
confidence: 0.8,
|
|
3557
|
+
metadata: { kind: "enum", members }
|
|
3558
|
+
});
|
|
3559
|
+
}
|
|
3560
|
+
const constMatch = line.match(/(?:export\s+)?const\s+(\w+)\s*=\s*\{/);
|
|
3561
|
+
if (constMatch && content.includes("as const")) {
|
|
3562
|
+
const blockEnd = this.findClosingBrace(lines, i);
|
|
3563
|
+
const block = lines.slice(i, blockEnd + 1).join("\n");
|
|
3564
|
+
if (block.includes("as const")) {
|
|
3565
|
+
const constName = constMatch[1];
|
|
3566
|
+
const members = this.collectObjectKeys(lines, i + 1);
|
|
3567
|
+
records.push({
|
|
3568
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + constName)}`,
|
|
3569
|
+
extractor: "enum-constants",
|
|
3570
|
+
language,
|
|
3571
|
+
filePath,
|
|
3572
|
+
line: i + 1,
|
|
3573
|
+
nodeType: "business_term",
|
|
3574
|
+
name: constName,
|
|
3575
|
+
content: `const ${constName} = { ${members.join(", ")} } as const`,
|
|
3576
|
+
confidence: 0.8,
|
|
3577
|
+
metadata: { kind: "as-const", members }
|
|
3578
|
+
});
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
const unionMatch = line.match(
|
|
3582
|
+
/(?:export\s+)?type\s+(\w+)\s*=\s*(['"`][\w]+['"`](?:\s*\|\s*['"`][\w]+['"`])+)/
|
|
3583
|
+
);
|
|
3584
|
+
if (unionMatch) {
|
|
3585
|
+
const typeName = unionMatch[1];
|
|
3586
|
+
const values = unionMatch[2].split("|").map((v) => v.trim().replace(/['"`]/g, ""));
|
|
3587
|
+
records.push({
|
|
3588
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + typeName)}`,
|
|
3589
|
+
extractor: "enum-constants",
|
|
3590
|
+
language,
|
|
3591
|
+
filePath,
|
|
3592
|
+
line: i + 1,
|
|
3593
|
+
nodeType: "business_term",
|
|
3594
|
+
name: typeName,
|
|
3595
|
+
content: `type ${typeName} = ${values.map((v) => `'${v}'`).join(" | ")}`,
|
|
3596
|
+
confidence: 0.7,
|
|
3597
|
+
metadata: { kind: "union-type", members: values }
|
|
3598
|
+
});
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
return records;
|
|
3602
|
+
}
|
|
3603
|
+
extractJavaScript(content, filePath, language) {
|
|
3604
|
+
const records = [];
|
|
3605
|
+
const lines = content.split("\n");
|
|
3606
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3607
|
+
const line = lines[i];
|
|
3608
|
+
const freezeMatch = line.match(/(?:export\s+)?const\s+(\w+)\s*=\s*Object\.freeze\s*\(\s*\{/);
|
|
3609
|
+
if (freezeMatch) {
|
|
3610
|
+
const name = freezeMatch[1];
|
|
3611
|
+
const members = this.collectObjectKeys(lines, i + 1);
|
|
3612
|
+
records.push({
|
|
3613
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + name)}`,
|
|
3614
|
+
extractor: "enum-constants",
|
|
3615
|
+
language,
|
|
3616
|
+
filePath,
|
|
3617
|
+
line: i + 1,
|
|
3618
|
+
nodeType: "business_term",
|
|
3619
|
+
name,
|
|
3620
|
+
content: `const ${name} = Object.freeze({ ${members.join(", ")} })`,
|
|
3621
|
+
confidence: 0.8,
|
|
3622
|
+
metadata: { kind: "frozen-object", members }
|
|
3623
|
+
});
|
|
3624
|
+
}
|
|
3625
|
+
const constMatch = line.match(/(?:export\s+)?const\s+([A-Z][A-Z_\d]*)\s*=\s*\{/);
|
|
3626
|
+
if (constMatch && !freezeMatch) {
|
|
3627
|
+
const name = constMatch[1];
|
|
3628
|
+
const members = this.collectObjectKeys(lines, i + 1);
|
|
3629
|
+
if (members.length > 0 && members.every((m) => /^[A-Z][A-Z_\d]*$/.test(m))) {
|
|
3630
|
+
records.push({
|
|
3631
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + name)}`,
|
|
3632
|
+
extractor: "enum-constants",
|
|
3633
|
+
language,
|
|
3634
|
+
filePath,
|
|
3635
|
+
line: i + 1,
|
|
3636
|
+
nodeType: "business_term",
|
|
3637
|
+
name,
|
|
3638
|
+
content: `const ${name} = { ${members.join(", ")} }`,
|
|
3639
|
+
confidence: 0.6,
|
|
3640
|
+
metadata: { kind: "const-object", members }
|
|
3641
|
+
});
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3645
|
+
return records;
|
|
3646
|
+
}
|
|
3647
|
+
extractPython(content, filePath, language) {
|
|
3648
|
+
const records = [];
|
|
3649
|
+
const lines = content.split("\n");
|
|
3650
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3651
|
+
const line = lines[i];
|
|
3652
|
+
const enumMatch = line.match(
|
|
3653
|
+
/^class\s+(\w+)\s*\(\s*(?:str\s*,\s*)?(?:Enum|StrEnum|IntEnum|Flag|IntFlag)\s*\)/
|
|
3654
|
+
);
|
|
3655
|
+
if (enumMatch) {
|
|
3656
|
+
const enumName = enumMatch[1];
|
|
3657
|
+
const members = this.collectPythonEnumMembers(lines, i + 1);
|
|
3658
|
+
records.push({
|
|
3659
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + enumName)}`,
|
|
3660
|
+
extractor: "enum-constants",
|
|
3661
|
+
language,
|
|
3662
|
+
filePath,
|
|
3663
|
+
line: i + 1,
|
|
3664
|
+
nodeType: "business_term",
|
|
3665
|
+
name: enumName,
|
|
3666
|
+
content: `class ${enumName}(Enum) { ${members.join(", ")} }`,
|
|
3667
|
+
confidence: 0.8,
|
|
3668
|
+
metadata: { kind: "enum", members }
|
|
3669
|
+
});
|
|
3670
|
+
}
|
|
3671
|
+
const literalMatch = line.match(/(\w+)\s*=\s*Literal\s*\[([^\]]+)\]/);
|
|
3672
|
+
if (literalMatch) {
|
|
3673
|
+
const typeName = literalMatch[1];
|
|
3674
|
+
const values = literalMatch[2].split(",").map((v) => v.trim().replace(/["']/g, ""));
|
|
3675
|
+
records.push({
|
|
3676
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + typeName)}`,
|
|
3677
|
+
extractor: "enum-constants",
|
|
3678
|
+
language,
|
|
3679
|
+
filePath,
|
|
3680
|
+
line: i + 1,
|
|
3681
|
+
nodeType: "business_term",
|
|
3682
|
+
name: typeName,
|
|
3683
|
+
content: `${typeName} = Literal[${values.map((v) => `"${v}"`).join(", ")}]`,
|
|
3684
|
+
confidence: 0.7,
|
|
3685
|
+
metadata: { kind: "literal-type", members: values }
|
|
3686
|
+
});
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
return records;
|
|
3690
|
+
}
|
|
3691
|
+
extractGo(content, filePath, language) {
|
|
3692
|
+
const records = [];
|
|
3693
|
+
const lines = content.split("\n");
|
|
3694
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3695
|
+
const line = lines[i];
|
|
3696
|
+
const typeMatch = line.match(/^type\s+(\w+)\s+(?:int|string|uint|int32|int64|uint32|uint64)/);
|
|
3697
|
+
if (typeMatch) {
|
|
3698
|
+
}
|
|
3699
|
+
const constBlockMatch = line.match(/^const\s*\(/);
|
|
3700
|
+
if (constBlockMatch) {
|
|
3701
|
+
const consts = [];
|
|
3702
|
+
let typeName;
|
|
3703
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
3704
|
+
const constLine = lines[j].trim();
|
|
3705
|
+
if (constLine === ")") break;
|
|
3706
|
+
if (constLine === "" || constLine.startsWith("//")) continue;
|
|
3707
|
+
const iotaMatch = constLine.match(/^(\w+)\s+(\w+)\s*=\s*iota/);
|
|
3708
|
+
if (iotaMatch) {
|
|
3709
|
+
typeName = iotaMatch[2];
|
|
3710
|
+
consts.push(iotaMatch[1]);
|
|
3711
|
+
continue;
|
|
3712
|
+
}
|
|
3713
|
+
const typedMatch = constLine.match(/^(\w+)\s+(\w+)\s*=\s*"[^"]*"/);
|
|
3714
|
+
if (typedMatch) {
|
|
3715
|
+
typeName = typeName ?? typedMatch[2];
|
|
3716
|
+
consts.push(typedMatch[1]);
|
|
3717
|
+
continue;
|
|
3718
|
+
}
|
|
3719
|
+
const bareMatch = constLine.match(/^(\w+)\s*$/);
|
|
3720
|
+
if (bareMatch) {
|
|
3721
|
+
consts.push(bareMatch[1]);
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
if (consts.length > 0) {
|
|
3725
|
+
const name = typeName ?? consts[0];
|
|
3726
|
+
records.push({
|
|
3727
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + name)}`,
|
|
3728
|
+
extractor: "enum-constants",
|
|
3729
|
+
language,
|
|
3730
|
+
filePath,
|
|
3731
|
+
line: i + 1,
|
|
3732
|
+
nodeType: "business_term",
|
|
3733
|
+
name,
|
|
3734
|
+
content: `const ( ${consts.join(", ")} )`,
|
|
3735
|
+
confidence: typeName ? 0.8 : 0.6,
|
|
3736
|
+
metadata: { kind: typeName ? "typed-const" : "const-block", members: consts }
|
|
3737
|
+
});
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
return records;
|
|
3742
|
+
}
|
|
3743
|
+
extractRust(content, filePath, language) {
|
|
3744
|
+
const records = [];
|
|
3745
|
+
const lines = content.split("\n");
|
|
3746
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3747
|
+
const line = lines[i];
|
|
3748
|
+
const enumMatch = line.match(/^(?:pub\s+)?enum\s+(\w+)/);
|
|
3749
|
+
if (enumMatch) {
|
|
3750
|
+
const enumName = enumMatch[1];
|
|
3751
|
+
const members = this.collectRustEnumVariants(lines, i + 1);
|
|
3752
|
+
records.push({
|
|
3753
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + enumName)}`,
|
|
3754
|
+
extractor: "enum-constants",
|
|
3755
|
+
language,
|
|
3756
|
+
filePath,
|
|
3757
|
+
line: i + 1,
|
|
3758
|
+
nodeType: "business_term",
|
|
3759
|
+
name: enumName,
|
|
3760
|
+
content: `enum ${enumName} { ${members.join(", ")} }`,
|
|
3761
|
+
confidence: 0.8,
|
|
3762
|
+
metadata: { kind: "enum", members }
|
|
3763
|
+
});
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
return records;
|
|
3767
|
+
}
|
|
3768
|
+
extractJava(content, filePath, language) {
|
|
3769
|
+
const records = [];
|
|
3770
|
+
const lines = content.split("\n");
|
|
3771
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3772
|
+
const line = lines[i];
|
|
3773
|
+
const enumMatch = line.match(/(?:public\s+|private\s+|protected\s+)?enum\s+(\w+)/);
|
|
3774
|
+
if (enumMatch) {
|
|
3775
|
+
const enumName = enumMatch[1];
|
|
3776
|
+
const members = this.collectJavaEnumConstants(lines, i + 1);
|
|
3777
|
+
records.push({
|
|
3778
|
+
id: `extracted:enum-constants:${hash(filePath + ":" + enumName)}`,
|
|
3779
|
+
extractor: "enum-constants",
|
|
3780
|
+
language,
|
|
3781
|
+
filePath,
|
|
3782
|
+
line: i + 1,
|
|
3783
|
+
nodeType: "business_term",
|
|
3784
|
+
name: enumName,
|
|
3785
|
+
content: `enum ${enumName} { ${members.join(", ")} }`,
|
|
3786
|
+
confidence: 0.8,
|
|
3787
|
+
metadata: { kind: "enum", members }
|
|
3788
|
+
});
|
|
3789
|
+
}
|
|
3790
|
+
}
|
|
3791
|
+
return records;
|
|
3792
|
+
}
|
|
3793
|
+
// --- Helper methods ---
|
|
3794
|
+
collectEnumMembers(lines, startLine) {
|
|
3795
|
+
const members = [];
|
|
3796
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3797
|
+
const line = lines[i].trim();
|
|
3798
|
+
if (line === "}") break;
|
|
3799
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
3800
|
+
const match = line.match(/^(\w+)/);
|
|
3801
|
+
if (match) members.push(match[1]);
|
|
3802
|
+
}
|
|
3803
|
+
return members;
|
|
3804
|
+
}
|
|
3805
|
+
collectObjectKeys(lines, startLine) {
|
|
3806
|
+
const keys = [];
|
|
3807
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3808
|
+
const line = lines[i].trim();
|
|
3809
|
+
if (line.startsWith("}")) break;
|
|
3810
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
3811
|
+
const match = line.match(/^(\w+)\s*:/);
|
|
3812
|
+
if (match) keys.push(match[1]);
|
|
3813
|
+
}
|
|
3814
|
+
return keys;
|
|
3815
|
+
}
|
|
3816
|
+
findClosingBrace(lines, startLine) {
|
|
3817
|
+
let depth = 0;
|
|
3818
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3819
|
+
for (const ch of lines[i]) {
|
|
3820
|
+
if (ch === "{") depth++;
|
|
3821
|
+
if (ch === "}") {
|
|
3822
|
+
depth--;
|
|
3823
|
+
if (depth === 0) return i;
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
return lines.length - 1;
|
|
3828
|
+
}
|
|
3829
|
+
collectPythonEnumMembers(lines, startLine) {
|
|
3830
|
+
const members = [];
|
|
3831
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3832
|
+
const line = lines[i];
|
|
3833
|
+
if (/^\S/.test(line) && line.trim() !== "") break;
|
|
3834
|
+
const match = line.match(/^\s+(\w+)\s*=/);
|
|
3835
|
+
if (match) members.push(match[1]);
|
|
3836
|
+
}
|
|
3837
|
+
return members;
|
|
3838
|
+
}
|
|
3839
|
+
collectRustEnumVariants(lines, startLine) {
|
|
3840
|
+
const variants = [];
|
|
3841
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3842
|
+
const line = lines[i].trim();
|
|
3843
|
+
if (line === "}") break;
|
|
3844
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
3845
|
+
const match = line.match(/^(\w+)/);
|
|
3846
|
+
if (match) variants.push(match[1]);
|
|
3847
|
+
}
|
|
3848
|
+
return variants;
|
|
3849
|
+
}
|
|
3850
|
+
collectJavaEnumConstants(lines, startLine) {
|
|
3851
|
+
const constants = [];
|
|
3852
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3853
|
+
const line = lines[i].trim();
|
|
3854
|
+
if (line === "}") break;
|
|
3855
|
+
if (line === "" || line.startsWith("//") || line.startsWith("/*")) continue;
|
|
3856
|
+
if (line.match(/^\s*(?:private|public|protected|static)/)) break;
|
|
3857
|
+
const match = line.match(/^(\w+)[\s(,;]/);
|
|
3858
|
+
if (match) constants.push(match[1]);
|
|
3859
|
+
const singleMatch = line.match(/^(\w+)$/);
|
|
3860
|
+
if (singleMatch) constants.push(singleMatch[1]);
|
|
3861
|
+
}
|
|
3862
|
+
return constants;
|
|
3863
|
+
}
|
|
3864
|
+
};
|
|
3865
|
+
|
|
3866
|
+
// src/ingest/extractors/ValidationRuleExtractor.ts
|
|
3867
|
+
var ValidationRuleExtractor = class {
|
|
3868
|
+
name = "validation-rules";
|
|
3869
|
+
supportedExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
|
|
3870
|
+
extract(content, filePath, language) {
|
|
3871
|
+
switch (language) {
|
|
3872
|
+
case "typescript":
|
|
3873
|
+
case "javascript":
|
|
3874
|
+
return this.extractZod(content, filePath, language);
|
|
3875
|
+
case "python":
|
|
3876
|
+
return this.extractPydantic(content, filePath, language);
|
|
3877
|
+
case "go":
|
|
3878
|
+
return this.extractGoValidate(content, filePath, language);
|
|
3879
|
+
case "rust":
|
|
3880
|
+
return this.extractRustValidate(content, filePath, language);
|
|
3881
|
+
case "java":
|
|
3882
|
+
return this.extractBeanValidation(content, filePath, language);
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
extractZod(content, filePath, language) {
|
|
3886
|
+
const records = [];
|
|
3887
|
+
const lines = content.split("\n");
|
|
3888
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3889
|
+
const line = lines[i];
|
|
3890
|
+
const schemaMatch = line.match(/(?:export\s+)?const\s+(\w+)\s*=\s*z\.object\s*\(\s*\{/);
|
|
3891
|
+
if (schemaMatch) {
|
|
3892
|
+
const schemaName = schemaMatch[1];
|
|
3893
|
+
const constraints = this.collectZodConstraints(lines, i + 1);
|
|
3894
|
+
records.push({
|
|
3895
|
+
id: `extracted:validation-rules:${hash(filePath + ":" + schemaName)}`,
|
|
3896
|
+
extractor: "validation-rules",
|
|
3897
|
+
language,
|
|
3898
|
+
filePath,
|
|
3899
|
+
line: i + 1,
|
|
3900
|
+
nodeType: "business_rule",
|
|
3901
|
+
name: schemaName,
|
|
3902
|
+
content: `${schemaName}: ${constraints.join("; ")}`,
|
|
3903
|
+
confidence: 0.8,
|
|
3904
|
+
metadata: { kind: "zod-schema", constraints, framework: "zod" }
|
|
3905
|
+
});
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
return records;
|
|
3909
|
+
}
|
|
3910
|
+
collectZodConstraints(lines, startLine) {
|
|
3911
|
+
const constraints = [];
|
|
3912
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3913
|
+
const line = lines[i].trim();
|
|
3914
|
+
if (line.startsWith("}") || line.startsWith(")")) break;
|
|
3915
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
3916
|
+
const fieldMatch = line.match(/(\w+)\s*:\s*z\.(.+?)(?:,?\s*$)/);
|
|
3917
|
+
if (fieldMatch) {
|
|
3918
|
+
const fieldName = fieldMatch[1];
|
|
3919
|
+
const chain = fieldMatch[2];
|
|
3920
|
+
constraints.push(`${fieldName}: ${chain}`);
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
return constraints;
|
|
3924
|
+
}
|
|
3925
|
+
extractPydantic(content, filePath, language) {
|
|
3926
|
+
const records = [];
|
|
3927
|
+
const lines = content.split("\n");
|
|
3928
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3929
|
+
const line = lines[i];
|
|
3930
|
+
const modelMatch = line.match(/^class\s+(\w+)\s*\(\s*BaseModel\s*\)/);
|
|
3931
|
+
if (modelMatch) {
|
|
3932
|
+
const modelName = modelMatch[1];
|
|
3933
|
+
const constraints = this.collectPydanticConstraints(lines, i + 1);
|
|
3934
|
+
records.push({
|
|
3935
|
+
id: `extracted:validation-rules:${hash(filePath + ":" + modelName)}`,
|
|
3936
|
+
extractor: "validation-rules",
|
|
3937
|
+
language,
|
|
3938
|
+
filePath,
|
|
3939
|
+
line: i + 1,
|
|
3940
|
+
nodeType: "business_rule",
|
|
3941
|
+
name: modelName,
|
|
3942
|
+
content: `${modelName}: ${constraints.join("; ")}`,
|
|
3943
|
+
confidence: 0.8,
|
|
3944
|
+
metadata: { kind: "pydantic-model", constraints, framework: "pydantic" }
|
|
3945
|
+
});
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
return records;
|
|
3949
|
+
}
|
|
3950
|
+
collectPydanticConstraints(lines, startLine) {
|
|
3951
|
+
const constraints = [];
|
|
3952
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3953
|
+
const line = lines[i];
|
|
3954
|
+
if (/^\S/.test(line) && line.trim() !== "") break;
|
|
3955
|
+
if (line.trim() === "" || line.trim().startsWith("#")) continue;
|
|
3956
|
+
const fieldMatch = line.match(/^\s+(\w+)\s*:\s*(.+?)(?:\s*$)/);
|
|
3957
|
+
if (fieldMatch) {
|
|
3958
|
+
constraints.push(`${fieldMatch[1]}: ${fieldMatch[2]}`);
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
return constraints;
|
|
3962
|
+
}
|
|
3963
|
+
extractGoValidate(content, filePath, language) {
|
|
3964
|
+
const records = [];
|
|
3965
|
+
const lines = content.split("\n");
|
|
3966
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3967
|
+
const line = lines[i];
|
|
3968
|
+
const structMatch = line.match(/^type\s+(\w+)\s+struct\s*\{/);
|
|
3969
|
+
if (structMatch) {
|
|
3970
|
+
const structName = structMatch[1];
|
|
3971
|
+
const constraints = this.collectGoValidateTags(lines, i + 1);
|
|
3972
|
+
if (constraints.length > 0) {
|
|
3973
|
+
records.push({
|
|
3974
|
+
id: `extracted:validation-rules:${hash(filePath + ":" + structName)}`,
|
|
3975
|
+
extractor: "validation-rules",
|
|
3976
|
+
language,
|
|
3977
|
+
filePath,
|
|
3978
|
+
line: i + 1,
|
|
3979
|
+
nodeType: "business_rule",
|
|
3980
|
+
name: structName,
|
|
3981
|
+
content: `${structName}: ${constraints.join("; ")}`,
|
|
3982
|
+
confidence: 0.8,
|
|
3983
|
+
metadata: { kind: "struct-tags", constraints, framework: "go-playground/validator" }
|
|
3984
|
+
});
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3987
|
+
}
|
|
3988
|
+
return records;
|
|
3989
|
+
}
|
|
3990
|
+
collectGoValidateTags(lines, startLine) {
|
|
3991
|
+
const constraints = [];
|
|
3992
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
3993
|
+
const line = lines[i].trim();
|
|
3994
|
+
if (line === "}") break;
|
|
3995
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
3996
|
+
const tagMatch = line.match(/(\w+)\s+\S+\s+`[^`]*validate:"([^"]+)"[^`]*`/);
|
|
3997
|
+
if (tagMatch) {
|
|
3998
|
+
constraints.push(`${tagMatch[1]}: ${tagMatch[2]}`);
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
return constraints;
|
|
4002
|
+
}
|
|
4003
|
+
extractRustValidate(content, filePath, language) {
|
|
4004
|
+
const records = [];
|
|
4005
|
+
const lines = content.split("\n");
|
|
4006
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4007
|
+
const line = lines[i];
|
|
4008
|
+
const deriveMatch = line.match(/#\[derive\([^)]*Validate[^)]*\)\]/);
|
|
4009
|
+
if (deriveMatch) {
|
|
4010
|
+
for (let j = i + 1; j < lines.length && j <= i + 3; j++) {
|
|
4011
|
+
const structLine = lines[j];
|
|
4012
|
+
const structMatch = structLine.match(/^(?:pub\s+)?struct\s+(\w+)/);
|
|
4013
|
+
if (structMatch) {
|
|
4014
|
+
const structName = structMatch[1];
|
|
4015
|
+
const constraints = this.collectRustValidateAttrs(lines, j + 1);
|
|
4016
|
+
if (constraints.length > 0) {
|
|
4017
|
+
records.push({
|
|
4018
|
+
id: `extracted:validation-rules:${hash(filePath + ":" + structName)}`,
|
|
4019
|
+
extractor: "validation-rules",
|
|
4020
|
+
language,
|
|
4021
|
+
filePath,
|
|
4022
|
+
line: j + 1,
|
|
4023
|
+
nodeType: "business_rule",
|
|
4024
|
+
name: structName,
|
|
4025
|
+
content: `${structName}: ${constraints.join("; ")}`,
|
|
4026
|
+
confidence: 0.8,
|
|
4027
|
+
metadata: { kind: "validate-derive", constraints, framework: "validator" }
|
|
4028
|
+
});
|
|
4029
|
+
}
|
|
4030
|
+
break;
|
|
4031
|
+
}
|
|
4032
|
+
}
|
|
4033
|
+
}
|
|
4034
|
+
}
|
|
4035
|
+
return records;
|
|
4036
|
+
}
|
|
4037
|
+
collectRustValidateAttrs(lines, startLine) {
|
|
4038
|
+
const constraints = [];
|
|
4039
|
+
let pendingValidate;
|
|
4040
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
4041
|
+
const line = lines[i].trim();
|
|
4042
|
+
if (line === "}") break;
|
|
4043
|
+
const attrMatch = line.match(/#\[validate\((.+?)\)\]/);
|
|
4044
|
+
if (attrMatch) {
|
|
4045
|
+
pendingValidate = attrMatch[1];
|
|
4046
|
+
continue;
|
|
4047
|
+
}
|
|
4048
|
+
if (pendingValidate) {
|
|
4049
|
+
const fieldMatch = line.match(/(?:pub\s+)?(\w+)\s*:/);
|
|
4050
|
+
if (fieldMatch) {
|
|
4051
|
+
constraints.push(`${fieldMatch[1]}: ${pendingValidate}`);
|
|
4052
|
+
pendingValidate = void 0;
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
return constraints;
|
|
4057
|
+
}
|
|
4058
|
+
extractBeanValidation(content, filePath, language) {
|
|
4059
|
+
const records = [];
|
|
4060
|
+
const lines = content.split("\n");
|
|
4061
|
+
const classRanges = this.findJavaClasses(lines);
|
|
4062
|
+
for (const { name: className, startLine, endLine } of classRanges) {
|
|
4063
|
+
const constraints = this.collectBeanConstraints(lines, startLine, endLine);
|
|
4064
|
+
if (constraints.length > 0) {
|
|
4065
|
+
records.push({
|
|
4066
|
+
id: `extracted:validation-rules:${hash(filePath + ":" + className)}`,
|
|
4067
|
+
extractor: "validation-rules",
|
|
4068
|
+
language,
|
|
4069
|
+
filePath,
|
|
4070
|
+
line: startLine + 1,
|
|
4071
|
+
nodeType: "business_rule",
|
|
4072
|
+
name: className,
|
|
4073
|
+
content: `${className}: ${constraints.join("; ")}`,
|
|
4074
|
+
confidence: 0.8,
|
|
4075
|
+
metadata: { kind: "bean-validation", constraints, framework: "javax.validation" }
|
|
4076
|
+
});
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
return records;
|
|
4080
|
+
}
|
|
4081
|
+
findJavaClasses(lines) {
|
|
4082
|
+
const classes = [];
|
|
4083
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4084
|
+
const line = lines[i];
|
|
4085
|
+
const match = line.match(/(?:public\s+|private\s+|protected\s+)?class\s+(\w+)/);
|
|
4086
|
+
if (match) {
|
|
4087
|
+
const endLine = this.findClosingBrace(lines, i);
|
|
4088
|
+
classes.push({ name: match[1], startLine: i, endLine });
|
|
4089
|
+
}
|
|
4090
|
+
}
|
|
4091
|
+
return classes;
|
|
4092
|
+
}
|
|
4093
|
+
findClosingBrace(lines, startLine) {
|
|
4094
|
+
let depth = 0;
|
|
4095
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
4096
|
+
for (const ch of lines[i]) {
|
|
4097
|
+
if (ch === "{") depth++;
|
|
4098
|
+
if (ch === "}") {
|
|
4099
|
+
depth--;
|
|
4100
|
+
if (depth === 0) return i;
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
return lines.length - 1;
|
|
4105
|
+
}
|
|
4106
|
+
collectBeanConstraints(lines, startLine, endLine) {
|
|
4107
|
+
const constraints = [];
|
|
4108
|
+
const validationAnnotations = [
|
|
4109
|
+
"@NotNull",
|
|
4110
|
+
"@NotBlank",
|
|
4111
|
+
"@NotEmpty",
|
|
4112
|
+
"@Size",
|
|
4113
|
+
"@Min",
|
|
4114
|
+
"@Max",
|
|
4115
|
+
"@DecimalMin",
|
|
4116
|
+
"@DecimalMax",
|
|
4117
|
+
"@Email",
|
|
4118
|
+
"@Pattern",
|
|
4119
|
+
"@Positive",
|
|
4120
|
+
"@Negative",
|
|
4121
|
+
"@Past",
|
|
4122
|
+
"@Future"
|
|
4123
|
+
];
|
|
4124
|
+
let pendingAnnotations = [];
|
|
4125
|
+
for (let i = startLine + 1; i < endLine; i++) {
|
|
4126
|
+
const line = lines[i].trim();
|
|
4127
|
+
for (const anno of validationAnnotations) {
|
|
4128
|
+
if (line.startsWith(anno)) {
|
|
4129
|
+
pendingAnnotations.push(line.replace(/;?\s*$/, ""));
|
|
4130
|
+
}
|
|
4131
|
+
}
|
|
4132
|
+
if (pendingAnnotations.length > 0) {
|
|
4133
|
+
const fieldMatch = line.match(
|
|
4134
|
+
/(?:private|public|protected)\s+(?:[\w<>[\]]+)\s+(\w+)\s*[;=]/
|
|
4135
|
+
);
|
|
4136
|
+
if (fieldMatch) {
|
|
4137
|
+
constraints.push(`${fieldMatch[1]}: ${pendingAnnotations.join(", ")}`);
|
|
4138
|
+
pendingAnnotations = [];
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4142
|
+
return constraints;
|
|
4143
|
+
}
|
|
4144
|
+
};
|
|
4145
|
+
|
|
4146
|
+
// src/ingest/extractors/ApiPathExtractor.ts
|
|
4147
|
+
var ApiPathExtractor = class {
|
|
4148
|
+
name = "api-paths";
|
|
4149
|
+
supportedExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
|
|
4150
|
+
extract(content, filePath, language) {
|
|
4151
|
+
switch (language) {
|
|
4152
|
+
case "typescript":
|
|
4153
|
+
case "javascript":
|
|
4154
|
+
return this.extractJsTs(content, filePath, language);
|
|
4155
|
+
case "python":
|
|
4156
|
+
return this.extractPython(content, filePath, language);
|
|
4157
|
+
case "go":
|
|
4158
|
+
return this.extractGo(content, filePath, language);
|
|
4159
|
+
case "rust":
|
|
4160
|
+
return this.extractRust(content, filePath, language);
|
|
4161
|
+
case "java":
|
|
4162
|
+
return this.extractJava(content, filePath, language);
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
4165
|
+
extractJsTs(content, filePath, language) {
|
|
4166
|
+
const records = [];
|
|
4167
|
+
const lines = content.split("\n");
|
|
4168
|
+
const routePattern = /(?:app|router|server|fastify)\.(get|post|put|patch|delete|head|options)\s*\(\s*(['"`])([^'"`]+)\2/i;
|
|
4169
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4170
|
+
const line = lines[i];
|
|
4171
|
+
const match = line.match(routePattern);
|
|
4172
|
+
if (match) {
|
|
4173
|
+
const method = match[1].toUpperCase();
|
|
4174
|
+
const routePath = match[3];
|
|
4175
|
+
const patternKey = `${method} ${routePath}`;
|
|
4176
|
+
records.push({
|
|
4177
|
+
id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
|
|
4178
|
+
extractor: "api-paths",
|
|
4179
|
+
language,
|
|
4180
|
+
filePath,
|
|
4181
|
+
line: i + 1,
|
|
4182
|
+
nodeType: "business_process",
|
|
4183
|
+
name: patternKey,
|
|
4184
|
+
content: `${method} ${routePath}`,
|
|
4185
|
+
confidence: 0.9,
|
|
4186
|
+
metadata: { method, path: routePath, framework: "express" }
|
|
4187
|
+
});
|
|
4188
|
+
}
|
|
4189
|
+
}
|
|
4190
|
+
return records;
|
|
4191
|
+
}
|
|
4192
|
+
extractPython(content, filePath, language) {
|
|
4193
|
+
const records = [];
|
|
4194
|
+
const lines = content.split("\n");
|
|
4195
|
+
const decoratorPattern = /@(?:app|router)\.(get|post|put|patch|delete|head|options)\s*\(\s*["']([^"']+)["']/i;
|
|
4196
|
+
const flaskPattern = /@(?:app|blueprint)\.route\s*\(\s*["']([^"']+)["'](?:\s*,\s*methods\s*=\s*\[([^\]]+)\])?/i;
|
|
4197
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4198
|
+
const line = lines[i];
|
|
4199
|
+
const fastApiMatch = line.match(decoratorPattern);
|
|
4200
|
+
if (fastApiMatch) {
|
|
4201
|
+
const method = fastApiMatch[1].toUpperCase();
|
|
4202
|
+
const routePath = fastApiMatch[2];
|
|
4203
|
+
const patternKey = `${method} ${routePath}`;
|
|
4204
|
+
records.push({
|
|
4205
|
+
id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
|
|
4206
|
+
extractor: "api-paths",
|
|
4207
|
+
language,
|
|
4208
|
+
filePath,
|
|
4209
|
+
line: i + 1,
|
|
4210
|
+
nodeType: "business_process",
|
|
4211
|
+
name: patternKey,
|
|
4212
|
+
content: `${method} ${routePath}`,
|
|
4213
|
+
confidence: 0.9,
|
|
4214
|
+
metadata: { method, path: routePath, framework: "fastapi" }
|
|
4215
|
+
});
|
|
4216
|
+
continue;
|
|
4217
|
+
}
|
|
4218
|
+
const flaskMatch = line.match(flaskPattern);
|
|
4219
|
+
if (flaskMatch) {
|
|
4220
|
+
const routePath = flaskMatch[1];
|
|
4221
|
+
const methods = flaskMatch[2] ? flaskMatch[2].split(",").map((m) => m.trim().replace(/["']/g, "")) : ["GET"];
|
|
4222
|
+
for (const method of methods) {
|
|
4223
|
+
const patternKey = `${method} ${routePath}`;
|
|
4224
|
+
records.push({
|
|
4225
|
+
id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
|
|
4226
|
+
extractor: "api-paths",
|
|
4227
|
+
language,
|
|
4228
|
+
filePath,
|
|
4229
|
+
line: i + 1,
|
|
4230
|
+
nodeType: "business_process",
|
|
4231
|
+
name: patternKey,
|
|
4232
|
+
content: `${method} ${routePath}`,
|
|
4233
|
+
confidence: 0.9,
|
|
4234
|
+
metadata: { method, path: routePath, framework: "flask" }
|
|
4235
|
+
});
|
|
4236
|
+
}
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
return records;
|
|
4240
|
+
}
|
|
4241
|
+
extractGo(content, filePath, language) {
|
|
4242
|
+
const records = [];
|
|
4243
|
+
const lines = content.split("\n");
|
|
4244
|
+
const ginPattern = /(?:\w+)\.(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\(\s*"([^"]+)"/i;
|
|
4245
|
+
const httpPattern = /http\.HandleFunc\s*\(\s*"([^"]+)"/;
|
|
4246
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4247
|
+
const line = lines[i];
|
|
4248
|
+
const ginMatch = line.match(ginPattern);
|
|
4249
|
+
if (ginMatch) {
|
|
4250
|
+
const method = ginMatch[1].toUpperCase();
|
|
4251
|
+
const routePath = ginMatch[2];
|
|
4252
|
+
const patternKey = `${method} ${routePath}`;
|
|
4253
|
+
records.push({
|
|
4254
|
+
id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
|
|
4255
|
+
extractor: "api-paths",
|
|
4256
|
+
language,
|
|
4257
|
+
filePath,
|
|
4258
|
+
line: i + 1,
|
|
4259
|
+
nodeType: "business_process",
|
|
4260
|
+
name: patternKey,
|
|
4261
|
+
content: `${method} ${routePath}`,
|
|
4262
|
+
confidence: 0.9,
|
|
4263
|
+
metadata: { method, path: routePath, framework: "gin" }
|
|
4264
|
+
});
|
|
4265
|
+
continue;
|
|
4266
|
+
}
|
|
4267
|
+
const httpMatch = line.match(httpPattern);
|
|
4268
|
+
if (httpMatch) {
|
|
4269
|
+
const routePath = httpMatch[1];
|
|
4270
|
+
const patternKey = `ANY ${routePath}`;
|
|
4271
|
+
records.push({
|
|
4272
|
+
id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
|
|
4273
|
+
extractor: "api-paths",
|
|
4274
|
+
language,
|
|
4275
|
+
filePath,
|
|
4276
|
+
line: i + 1,
|
|
4277
|
+
nodeType: "business_process",
|
|
4278
|
+
name: patternKey,
|
|
4279
|
+
content: `ANY ${routePath}`,
|
|
4280
|
+
confidence: 0.6,
|
|
4281
|
+
metadata: { method: "ANY", path: routePath, framework: "net/http" }
|
|
4282
|
+
});
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
return records;
|
|
4286
|
+
}
|
|
4287
|
+
extractRust(content, filePath, language) {
|
|
4288
|
+
const records = [];
|
|
4289
|
+
const lines = content.split("\n");
|
|
4290
|
+
const actixPattern = /#\[(get|post|put|patch|delete|head|options)\s*\(\s*"([^"]+)"\s*\)\s*\]/i;
|
|
4291
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4292
|
+
const line = lines[i];
|
|
4293
|
+
const match = line.match(actixPattern);
|
|
4294
|
+
if (match) {
|
|
4295
|
+
const method = match[1].toUpperCase();
|
|
4296
|
+
const routePath = match[2];
|
|
4297
|
+
const patternKey = `${method} ${routePath}`;
|
|
4298
|
+
records.push({
|
|
4299
|
+
id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
|
|
4300
|
+
extractor: "api-paths",
|
|
4301
|
+
language,
|
|
4302
|
+
filePath,
|
|
4303
|
+
line: i + 1,
|
|
4304
|
+
nodeType: "business_process",
|
|
4305
|
+
name: patternKey,
|
|
4306
|
+
content: `${method} ${routePath}`,
|
|
4307
|
+
confidence: 0.9,
|
|
4308
|
+
metadata: { method, path: routePath, framework: "actix" }
|
|
4309
|
+
});
|
|
4310
|
+
}
|
|
4311
|
+
}
|
|
4312
|
+
return records;
|
|
4313
|
+
}
|
|
4314
|
+
extractJava(content, filePath, language) {
|
|
4315
|
+
const records = [];
|
|
4316
|
+
const lines = content.split("\n");
|
|
4317
|
+
const springPattern = /@(GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping|RequestMapping)\s*\(\s*(?:value\s*=\s*)?["']([^"']+)["']/;
|
|
4318
|
+
let basePath = "";
|
|
4319
|
+
for (const line of lines) {
|
|
4320
|
+
const baseMatch = line.match(/@RequestMapping\s*\(\s*(?:value\s*=\s*)?["']([^"']+)["']\s*\)/);
|
|
4321
|
+
if (baseMatch && line.match(/class\s/) === null) {
|
|
4322
|
+
basePath = baseMatch[1];
|
|
4323
|
+
}
|
|
4324
|
+
}
|
|
4325
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4326
|
+
const line = lines[i];
|
|
4327
|
+
const match = line.match(springPattern);
|
|
4328
|
+
if (match) {
|
|
4329
|
+
const annotation = match[1];
|
|
4330
|
+
const routePath = basePath + match[2];
|
|
4331
|
+
const methodMap = {
|
|
4332
|
+
GetMapping: "GET",
|
|
4333
|
+
PostMapping: "POST",
|
|
4334
|
+
PutMapping: "PUT",
|
|
4335
|
+
PatchMapping: "PATCH",
|
|
4336
|
+
DeleteMapping: "DELETE",
|
|
4337
|
+
RequestMapping: "ANY"
|
|
4338
|
+
};
|
|
4339
|
+
const method = methodMap[annotation] ?? "ANY";
|
|
4340
|
+
const patternKey = `${method} ${routePath}`;
|
|
4341
|
+
records.push({
|
|
4342
|
+
id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
|
|
4343
|
+
extractor: "api-paths",
|
|
4344
|
+
language,
|
|
4345
|
+
filePath,
|
|
4346
|
+
line: i + 1,
|
|
4347
|
+
nodeType: "business_process",
|
|
4348
|
+
name: patternKey,
|
|
4349
|
+
content: `${method} ${routePath}`,
|
|
4350
|
+
confidence: 0.9,
|
|
4351
|
+
metadata: { method, path: routePath, framework: "spring" }
|
|
4352
|
+
});
|
|
4353
|
+
}
|
|
4354
|
+
}
|
|
4355
|
+
return records;
|
|
4356
|
+
}
|
|
4357
|
+
};
|
|
4358
|
+
|
|
4359
|
+
// src/ingest/extractors/index.ts
|
|
4360
|
+
var ALL_EXTRACTORS = [
|
|
4361
|
+
new TestDescriptionExtractor(),
|
|
4362
|
+
new EnumConstantExtractor(),
|
|
4363
|
+
new ValidationRuleExtractor(),
|
|
4364
|
+
new ApiPathExtractor()
|
|
4365
|
+
];
|
|
4366
|
+
function createExtractionRunner() {
|
|
4367
|
+
return new ExtractionRunner(ALL_EXTRACTORS);
|
|
4368
|
+
}
|
|
4369
|
+
|
|
4370
|
+
// src/ingest/ImageAnalysisExtractor.ts
|
|
4371
|
+
var ImageAnalysisExtractor = class {
|
|
4372
|
+
provider;
|
|
4373
|
+
/** Maximum file size in bytes. Reserved for future file-size filtering. */
|
|
4374
|
+
maxFileSizeBytes;
|
|
4375
|
+
constructor(options) {
|
|
4376
|
+
this.provider = options.analysisProvider;
|
|
4377
|
+
this.maxFileSizeBytes = (options.maxFileSizeMB ?? 10) * 1024 * 1024;
|
|
4378
|
+
}
|
|
4379
|
+
async analyze(store, imagePaths) {
|
|
4380
|
+
const start = Date.now();
|
|
4381
|
+
let nodesAdded = 0;
|
|
4382
|
+
let edgesAdded = 0;
|
|
4383
|
+
const errors = [];
|
|
4384
|
+
for (const imagePath of imagePaths) {
|
|
4385
|
+
try {
|
|
4386
|
+
const response = await this.provider.analyze({
|
|
4387
|
+
prompt: "Analyze this image and provide a structured description of its visual contents.",
|
|
4388
|
+
systemPrompt: "You are an image analysis assistant. Describe the image contents, detect UI elements, extract visible text, identify design patterns, and note accessibility concerns.",
|
|
4389
|
+
responseSchema: {},
|
|
4390
|
+
// Schema handled by provider
|
|
4391
|
+
maxTokens: 1e3
|
|
4392
|
+
});
|
|
4393
|
+
const result = response.result;
|
|
4394
|
+
const pathHash = hash(imagePath);
|
|
4395
|
+
const annotationId = `img:${pathHash}`;
|
|
4396
|
+
store.addNode({
|
|
4397
|
+
id: annotationId,
|
|
4398
|
+
type: "image_annotation",
|
|
4399
|
+
name: result.description.slice(0, 200),
|
|
4400
|
+
path: imagePath,
|
|
4401
|
+
content: result.description,
|
|
4402
|
+
hash: hash(result.description),
|
|
4403
|
+
metadata: {
|
|
4404
|
+
source: "image-analysis",
|
|
4405
|
+
detectedElements: result.detectedElements,
|
|
4406
|
+
extractedText: result.extractedText,
|
|
4407
|
+
designPatterns: result.designPatterns,
|
|
4408
|
+
accessibilityNotes: result.accessibilityNotes,
|
|
4409
|
+
model: response.model
|
|
4410
|
+
}
|
|
4411
|
+
});
|
|
4412
|
+
nodesAdded++;
|
|
4413
|
+
const fileNodes = store.findNodes({ type: "file" });
|
|
4414
|
+
const fileNode = fileNodes.find((n) => n.path === imagePath);
|
|
4415
|
+
if (fileNode) {
|
|
4416
|
+
store.addEdge({ from: annotationId, to: fileNode.id, type: "annotates" });
|
|
4417
|
+
edgesAdded++;
|
|
4418
|
+
}
|
|
4419
|
+
for (const pattern of result.designPatterns) {
|
|
4420
|
+
const conceptId = `img:concept:${hash(pattern + imagePath)}`;
|
|
4421
|
+
store.addNode({
|
|
4422
|
+
id: conceptId,
|
|
4423
|
+
type: "business_concept",
|
|
4424
|
+
name: pattern,
|
|
4425
|
+
content: `Design pattern detected in ${imagePath}: ${pattern}`,
|
|
4426
|
+
hash: hash(pattern),
|
|
4427
|
+
metadata: {
|
|
4428
|
+
source: "image-analysis",
|
|
4429
|
+
sourceImage: imagePath,
|
|
4430
|
+
domain: "design"
|
|
4431
|
+
}
|
|
4432
|
+
});
|
|
4433
|
+
nodesAdded++;
|
|
4434
|
+
store.addEdge({ from: annotationId, to: conceptId, type: "contains" });
|
|
4435
|
+
edgesAdded++;
|
|
4436
|
+
}
|
|
4437
|
+
} catch (err) {
|
|
4438
|
+
errors.push(
|
|
4439
|
+
`Image analysis failed for ${imagePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
4440
|
+
);
|
|
4441
|
+
}
|
|
4442
|
+
}
|
|
4443
|
+
return {
|
|
4444
|
+
nodesAdded,
|
|
4445
|
+
nodesUpdated: 0,
|
|
4446
|
+
edgesAdded,
|
|
4447
|
+
edgesUpdated: 0,
|
|
4448
|
+
errors,
|
|
4449
|
+
durationMs: Date.now() - start
|
|
4450
|
+
};
|
|
4451
|
+
}
|
|
4452
|
+
};
|
|
4453
|
+
|
|
4454
|
+
// src/ingest/knowledgeTypes.ts
|
|
4455
|
+
var KNOWLEDGE_NODE_TYPES = [
|
|
4456
|
+
"business_fact",
|
|
4457
|
+
"business_rule",
|
|
4458
|
+
"business_process",
|
|
4459
|
+
"business_term",
|
|
4460
|
+
"business_concept",
|
|
4461
|
+
"business_metric",
|
|
4462
|
+
"design_token",
|
|
4463
|
+
"design_constraint",
|
|
4464
|
+
"aesthetic_intent",
|
|
4465
|
+
"image_annotation"
|
|
4466
|
+
];
|
|
4467
|
+
|
|
4468
|
+
// src/ingest/ContradictionDetector.ts
|
|
4469
|
+
var SIMILARITY_THRESHOLD = 0.8;
|
|
4470
|
+
function levenshteinDistance(a, b) {
|
|
4471
|
+
const m = a.length;
|
|
4472
|
+
const n = b.length;
|
|
4473
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
4474
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
4475
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
4476
|
+
for (let i = 1; i <= m; i++) {
|
|
4477
|
+
for (let j = 1; j <= n; j++) {
|
|
4478
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
4479
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
|
|
4480
|
+
}
|
|
4481
|
+
}
|
|
4482
|
+
return dp[m][n];
|
|
4483
|
+
}
|
|
4484
|
+
function levenshteinRatio(a, b) {
|
|
4485
|
+
const maxLen = Math.max(a.length, b.length);
|
|
4486
|
+
if (maxLen === 0) return 1;
|
|
4487
|
+
return 1 - levenshteinDistance(a, b) / maxLen;
|
|
4488
|
+
}
|
|
4489
|
+
function classifyConflict(a, b) {
|
|
4490
|
+
const factTypes = /* @__PURE__ */ new Set([
|
|
4491
|
+
"business_fact",
|
|
4492
|
+
"business_metric",
|
|
4493
|
+
"design_token"
|
|
4494
|
+
]);
|
|
4495
|
+
const termTypes = /* @__PURE__ */ new Set(["business_term", "business_concept"]);
|
|
4496
|
+
if (factTypes.has(a.type) || factTypes.has(b.type)) return "value_mismatch";
|
|
4497
|
+
if (termTypes.has(a.type) || termTypes.has(b.type)) return "definition_conflict";
|
|
4498
|
+
if (a.lastModified && b.lastModified && a.lastModified !== b.lastModified)
|
|
4499
|
+
return "temporal_conflict";
|
|
4500
|
+
return "status_divergence";
|
|
4501
|
+
}
|
|
4502
|
+
var ContradictionDetector = class {
|
|
4503
|
+
detect(store) {
|
|
4504
|
+
const nodes = KNOWLEDGE_NODE_TYPES.flatMap((t) => store.findNodes({ type: t }));
|
|
4505
|
+
const byName = /* @__PURE__ */ new Map();
|
|
4506
|
+
for (const node of nodes) {
|
|
4507
|
+
const key = node.name.toLowerCase().trim();
|
|
4508
|
+
const group = byName.get(key) ?? [];
|
|
4509
|
+
group.push(node);
|
|
4510
|
+
byName.set(key, group);
|
|
4511
|
+
}
|
|
4512
|
+
const contradictions = [];
|
|
4513
|
+
const sourcePairCounts = {};
|
|
4514
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4515
|
+
const addContradiction = (a, b, similarity) => {
|
|
4516
|
+
const sourceA = a.metadata.source ?? "unknown";
|
|
4517
|
+
const sourceB = b.metadata.source ?? "unknown";
|
|
4518
|
+
if (sourceA === sourceB) return;
|
|
4519
|
+
const hashA = a.hash ?? a.id;
|
|
4520
|
+
const hashB = b.hash ?? b.id;
|
|
4521
|
+
if (hashA === hashB) return;
|
|
4522
|
+
const pairId = [a.id, b.id].sort().join(":");
|
|
4523
|
+
if (seen.has(pairId)) return;
|
|
4524
|
+
seen.add(pairId);
|
|
4525
|
+
const conflictType = classifyConflict(a, b);
|
|
4526
|
+
const severity = conflictType === "value_mismatch" ? "critical" : conflictType === "definition_conflict" ? "high" : "medium";
|
|
4527
|
+
const pairKey = [sourceA, sourceB].sort().join("\u2194");
|
|
4528
|
+
sourcePairCounts[pairKey] = (sourcePairCounts[pairKey] ?? 0) + 1;
|
|
4529
|
+
contradictions.push({
|
|
4530
|
+
id: `contradiction:${a.id}:${b.id}`,
|
|
4531
|
+
entityA: {
|
|
4532
|
+
nodeId: a.id,
|
|
4533
|
+
source: sourceA,
|
|
4534
|
+
name: a.name,
|
|
4535
|
+
content: a.content ?? "",
|
|
4536
|
+
lastModified: a.lastModified
|
|
4537
|
+
},
|
|
4538
|
+
entityB: {
|
|
4539
|
+
nodeId: b.id,
|
|
4540
|
+
source: sourceB,
|
|
4541
|
+
name: b.name,
|
|
4542
|
+
content: b.content ?? "",
|
|
4543
|
+
lastModified: b.lastModified
|
|
4544
|
+
},
|
|
4545
|
+
similarity,
|
|
4546
|
+
conflictType,
|
|
4547
|
+
severity,
|
|
4548
|
+
description: `"${a.name}" has conflicting definitions from ${sourceA} and ${sourceB}`
|
|
4549
|
+
});
|
|
4550
|
+
};
|
|
4551
|
+
for (const [, group] of byName) {
|
|
4552
|
+
if (group.length < 2) continue;
|
|
4553
|
+
for (let i = 0; i < group.length; i++) {
|
|
4554
|
+
for (let j = i + 1; j < group.length; j++) {
|
|
4555
|
+
addContradiction(group[i], group[j], 1);
|
|
4556
|
+
}
|
|
4557
|
+
}
|
|
4558
|
+
}
|
|
4559
|
+
const keys = [...byName.keys()];
|
|
4560
|
+
for (let i = 0; i < keys.length; i++) {
|
|
4561
|
+
for (let j = i + 1; j < keys.length; j++) {
|
|
4562
|
+
const keyA = keys[i];
|
|
4563
|
+
const keyB = keys[j];
|
|
4564
|
+
const maxLen = Math.max(keyA.length, keyB.length);
|
|
4565
|
+
const minLen = Math.min(keyA.length, keyB.length);
|
|
4566
|
+
if (minLen / maxLen < SIMILARITY_THRESHOLD) continue;
|
|
4567
|
+
const ratio = levenshteinRatio(keyA, keyB);
|
|
4568
|
+
if (ratio < SIMILARITY_THRESHOLD) continue;
|
|
4569
|
+
const groupA = byName.get(keyA);
|
|
4570
|
+
const groupB = byName.get(keyB);
|
|
4571
|
+
for (const a of groupA) {
|
|
4572
|
+
for (const b of groupB) {
|
|
4573
|
+
addContradiction(a, b, ratio);
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
return { contradictions, sourcePairCounts, totalChecked: nodes.length };
|
|
4579
|
+
}
|
|
4580
|
+
};
|
|
4581
|
+
|
|
4582
|
+
// src/ingest/CoverageScorer.ts
|
|
4583
|
+
var KNOWLEDGE_TYPES = KNOWLEDGE_NODE_TYPES;
|
|
4584
|
+
var CODE_TYPES2 = [
|
|
4585
|
+
"file",
|
|
4586
|
+
"function",
|
|
4587
|
+
"class",
|
|
4588
|
+
"method",
|
|
4589
|
+
"interface",
|
|
4590
|
+
"variable"
|
|
4591
|
+
];
|
|
4592
|
+
var KNOWLEDGE_EDGE_TYPES = [
|
|
4593
|
+
"governs",
|
|
4594
|
+
"documents",
|
|
4595
|
+
"measures",
|
|
4596
|
+
"applies_to",
|
|
4597
|
+
"references",
|
|
4598
|
+
"uses_token",
|
|
4599
|
+
"declares_intent",
|
|
4600
|
+
"annotates"
|
|
4601
|
+
];
|
|
4602
|
+
function toGrade(score) {
|
|
4603
|
+
if (score >= 80) return "A";
|
|
4604
|
+
if (score >= 60) return "B";
|
|
4605
|
+
if (score >= 40) return "C";
|
|
4606
|
+
if (score >= 20) return "D";
|
|
4607
|
+
return "F";
|
|
4608
|
+
}
|
|
4609
|
+
var CoverageScorer = class {
|
|
4610
|
+
score(store) {
|
|
4611
|
+
const knowledgeNodes = KNOWLEDGE_TYPES.flatMap((t) => store.findNodes({ type: t }));
|
|
4612
|
+
const domainMap = /* @__PURE__ */ new Map();
|
|
4613
|
+
for (const node of knowledgeNodes) {
|
|
4614
|
+
const domain = node.metadata.domain ?? "unclassified";
|
|
4615
|
+
const group = domainMap.get(domain) ?? [];
|
|
4616
|
+
group.push(node);
|
|
4617
|
+
domainMap.set(domain, group);
|
|
4618
|
+
}
|
|
4619
|
+
const codeNodes = CODE_TYPES2.flatMap((t) => store.findNodes({ type: t }));
|
|
4620
|
+
const codeDomains = /* @__PURE__ */ new Map();
|
|
4621
|
+
for (const node of codeNodes) {
|
|
4622
|
+
const domain = node.metadata.domain ?? this.domainFromPath(node.path);
|
|
4623
|
+
const group = codeDomains.get(domain) ?? [];
|
|
4624
|
+
group.push(node);
|
|
4625
|
+
codeDomains.set(domain, group);
|
|
4626
|
+
}
|
|
4627
|
+
const allDomains = /* @__PURE__ */ new Set([...domainMap.keys(), ...codeDomains.keys()]);
|
|
4628
|
+
const domains = [];
|
|
4629
|
+
for (const domain of allDomains) {
|
|
4630
|
+
const knEntries = domainMap.get(domain) ?? [];
|
|
4631
|
+
const codeEntries = codeDomains.get(domain) ?? [];
|
|
4632
|
+
const linkedIds = /* @__PURE__ */ new Set();
|
|
4633
|
+
for (const codeNode of codeEntries) {
|
|
4634
|
+
for (const edgeType of KNOWLEDGE_EDGE_TYPES) {
|
|
4635
|
+
const edges = store.getEdges({ to: codeNode.id, type: edgeType });
|
|
4636
|
+
if (edges.length > 0) {
|
|
4637
|
+
linkedIds.add(codeNode.id);
|
|
4638
|
+
break;
|
|
4639
|
+
}
|
|
4640
|
+
}
|
|
4641
|
+
}
|
|
4642
|
+
const sourceBreakdown = {};
|
|
4643
|
+
for (const kn of knEntries) {
|
|
4644
|
+
const src = kn.metadata.source ?? "unknown";
|
|
4645
|
+
sourceBreakdown[src] = (sourceBreakdown[src] ?? 0) + 1;
|
|
4646
|
+
}
|
|
4647
|
+
const codeEntities = codeEntries.length;
|
|
4648
|
+
const linkedEntities = linkedIds.size;
|
|
4649
|
+
const knowledgeEntries = knEntries.length;
|
|
4650
|
+
const uniqueSources = Object.keys(sourceBreakdown).length;
|
|
4651
|
+
const codeCoverageComponent = codeEntities > 0 ? linkedEntities / codeEntities * 60 : 0;
|
|
4652
|
+
const knowledgeDepthComponent = Math.min(knowledgeEntries / 10, 1) * 20;
|
|
4653
|
+
const sourceDiversityComponent = Math.min(uniqueSources / 3, 1) * 20;
|
|
4654
|
+
const score = Math.round(
|
|
4655
|
+
codeCoverageComponent + knowledgeDepthComponent + sourceDiversityComponent
|
|
4656
|
+
);
|
|
4657
|
+
domains.push({
|
|
4658
|
+
domain,
|
|
4659
|
+
score,
|
|
4660
|
+
knowledgeEntries,
|
|
4661
|
+
codeEntities,
|
|
4662
|
+
linkedEntities,
|
|
4663
|
+
unlinkedEntities: codeEntities - linkedEntities,
|
|
4664
|
+
sourceBreakdown,
|
|
4665
|
+
grade: toGrade(score)
|
|
4666
|
+
});
|
|
4667
|
+
}
|
|
4668
|
+
const overallScore = domains.length > 0 ? Math.round(domains.reduce((sum, d) => sum + d.score, 0) / domains.length) : 0;
|
|
4669
|
+
return {
|
|
4670
|
+
domains,
|
|
4671
|
+
overallScore,
|
|
4672
|
+
overallGrade: toGrade(overallScore),
|
|
4673
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4674
|
+
};
|
|
4675
|
+
}
|
|
4676
|
+
domainFromPath(filePath) {
|
|
4677
|
+
if (!filePath) return "unclassified";
|
|
4678
|
+
const parts = filePath.split("/");
|
|
4679
|
+
const pkgIdx = parts.indexOf("packages");
|
|
4680
|
+
if (pkgIdx >= 0 && parts[pkgIdx + 1]) return parts[pkgIdx + 1];
|
|
4681
|
+
const srcIdx = parts.indexOf("src");
|
|
4682
|
+
if (srcIdx >= 0 && parts[srcIdx + 1]) return parts[srcIdx + 1];
|
|
4683
|
+
return parts[0] ?? "unclassified";
|
|
4684
|
+
}
|
|
4685
|
+
};
|
|
4686
|
+
|
|
4687
|
+
// src/ingest/KnowledgePipelineRunner.ts
|
|
4688
|
+
var BUSINESS_NODE_TYPES = [
|
|
4689
|
+
"business_concept",
|
|
4690
|
+
"business_rule",
|
|
4691
|
+
"business_process",
|
|
4692
|
+
"business_term",
|
|
4693
|
+
"business_metric",
|
|
4694
|
+
"business_fact"
|
|
4695
|
+
];
|
|
4696
|
+
var SNAPSHOT_NODE_TYPES = [
|
|
4697
|
+
...BUSINESS_NODE_TYPES,
|
|
4698
|
+
"design_token",
|
|
4699
|
+
"design_constraint",
|
|
4700
|
+
"aesthetic_intent",
|
|
4701
|
+
"image_annotation"
|
|
4702
|
+
];
|
|
4703
|
+
var KnowledgePipelineRunner = class {
|
|
4704
|
+
constructor(store) {
|
|
4705
|
+
this.store = store;
|
|
4706
|
+
}
|
|
4707
|
+
store;
|
|
4708
|
+
async run(options) {
|
|
4709
|
+
const maxIterations = options.maxIterations ?? 5;
|
|
4710
|
+
const remediations = [];
|
|
4711
|
+
const preSnapshot = this.buildSnapshot(options.domain);
|
|
4712
|
+
const extraction = await this.extract(options);
|
|
4713
|
+
const postSnapshot = this.buildSnapshot(options.domain);
|
|
4714
|
+
let driftResult = this.reconcile(preSnapshot, postSnapshot);
|
|
4715
|
+
const contradictionDetector = new ContradictionDetector();
|
|
4716
|
+
const contradictions = contradictionDetector.detect(this.store);
|
|
4717
|
+
let gapReport = await this.detect(options);
|
|
4718
|
+
const coverageScorer = new CoverageScorer();
|
|
4719
|
+
const coverage = coverageScorer.score(this.store);
|
|
4720
|
+
let iterations = 1;
|
|
4721
|
+
if (options.fix) {
|
|
4722
|
+
let previousFindingCount = driftResult.findings.length;
|
|
4723
|
+
while (iterations < maxIterations) {
|
|
4724
|
+
if (driftResult.findings.length === 0) break;
|
|
4725
|
+
this.remediate(driftResult, remediations, options);
|
|
4726
|
+
const rePreSnapshot = this.buildSnapshot(options.domain);
|
|
4727
|
+
await this.extract(options);
|
|
4728
|
+
const rePostSnapshot = this.buildSnapshot(options.domain);
|
|
4729
|
+
driftResult = this.reconcile(rePreSnapshot, rePostSnapshot);
|
|
4730
|
+
gapReport = await this.detect(options);
|
|
4731
|
+
iterations++;
|
|
4732
|
+
const newFindingCount = driftResult.findings.length;
|
|
4733
|
+
if (newFindingCount >= previousFindingCount) break;
|
|
4734
|
+
previousFindingCount = newFindingCount;
|
|
4735
|
+
}
|
|
4736
|
+
}
|
|
4737
|
+
await this.stageNewFindings(driftResult, options);
|
|
4738
|
+
return {
|
|
4739
|
+
verdict: this.computeVerdict(driftResult),
|
|
4740
|
+
driftScore: driftResult.driftScore,
|
|
4741
|
+
iterations,
|
|
4742
|
+
findings: driftResult.summary,
|
|
4743
|
+
extraction,
|
|
4744
|
+
gaps: gapReport,
|
|
4745
|
+
remediations,
|
|
4746
|
+
contradictions,
|
|
4747
|
+
coverage
|
|
4748
|
+
};
|
|
4749
|
+
}
|
|
4750
|
+
// ── Phase 1: EXTRACT ──────────────────────────────────────────────────────
|
|
4751
|
+
async extract(options) {
|
|
4752
|
+
const extractedDir = path10.join(options.projectDir, ".harness", "knowledge", "extracted");
|
|
4753
|
+
await fs9.mkdir(extractedDir, { recursive: true });
|
|
4754
|
+
const runner = createExtractionRunner();
|
|
4755
|
+
const extractionResult = await runner.run(options.projectDir, this.store, extractedDir);
|
|
4756
|
+
const diagramParser = new DiagramParser(this.store);
|
|
4757
|
+
const diagramResult = await diagramParser.ingest(options.projectDir);
|
|
4758
|
+
let imageCount = 0;
|
|
4759
|
+
const imagePaths = options.imagePaths ?? [];
|
|
4760
|
+
if (options.analyzeImages && options.analysisProvider && imagePaths.length > 0) {
|
|
4761
|
+
const imageExtractor = new ImageAnalysisExtractor({
|
|
4762
|
+
analysisProvider: options.analysisProvider
|
|
4763
|
+
});
|
|
4764
|
+
const imageResult = await imageExtractor.analyze(this.store, imagePaths);
|
|
4765
|
+
imageCount = imageResult.nodesAdded;
|
|
4766
|
+
}
|
|
4767
|
+
const knowledgeDir = path10.join(options.projectDir, "docs", "knowledge");
|
|
4768
|
+
const bkIngestor = new BusinessKnowledgeIngestor(this.store);
|
|
4769
|
+
let bkResult;
|
|
4770
|
+
try {
|
|
4771
|
+
bkResult = await bkIngestor.ingest(knowledgeDir);
|
|
4772
|
+
} catch {
|
|
4773
|
+
bkResult = {
|
|
4774
|
+
nodesAdded: 0,
|
|
4775
|
+
nodesUpdated: 0,
|
|
4776
|
+
edgesAdded: 0,
|
|
4777
|
+
edgesUpdated: 0,
|
|
4778
|
+
errors: [],
|
|
4779
|
+
durationMs: 0
|
|
4780
|
+
};
|
|
4781
|
+
}
|
|
4782
|
+
const linker = new KnowledgeLinker(this.store, extractedDir);
|
|
4783
|
+
const linkResult = await linker.link();
|
|
4784
|
+
return {
|
|
4785
|
+
codeSignals: extractionResult.nodesAdded,
|
|
4786
|
+
diagrams: diagramResult.nodesAdded,
|
|
4787
|
+
linkerFacts: linkResult.factsCreated,
|
|
4788
|
+
businessKnowledge: bkResult.nodesAdded,
|
|
4789
|
+
images: imageCount
|
|
4790
|
+
};
|
|
1718
4791
|
}
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
4792
|
+
// ── Phase 2: RECONCILE ────────────────────────────────────────────────────
|
|
4793
|
+
buildSnapshot(domain) {
|
|
4794
|
+
let nodes = SNAPSHOT_NODE_TYPES.flatMap((type) => this.store.findNodes({ type }));
|
|
4795
|
+
if (domain) {
|
|
4796
|
+
nodes = nodes.filter((n) => n.metadata?.domain === domain);
|
|
4797
|
+
}
|
|
4798
|
+
return {
|
|
4799
|
+
entries: nodes.map((n) => ({
|
|
4800
|
+
id: n.id,
|
|
4801
|
+
type: n.type,
|
|
4802
|
+
contentHash: n.hash ?? n.id,
|
|
4803
|
+
source: n.metadata?.source ?? "unknown",
|
|
4804
|
+
name: n.name
|
|
4805
|
+
})),
|
|
4806
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4807
|
+
};
|
|
4808
|
+
}
|
|
4809
|
+
reconcile(current, fresh) {
|
|
4810
|
+
const detector = new StructuralDriftDetector();
|
|
4811
|
+
return detector.detect(current, fresh);
|
|
4812
|
+
}
|
|
4813
|
+
// ── Phase 3: DETECT ───────────────────────────────────────────────────────
|
|
4814
|
+
async detect(options) {
|
|
4815
|
+
const knowledgeDir = path10.join(options.projectDir, "docs", "knowledge");
|
|
4816
|
+
const aggregator = new KnowledgeStagingAggregator(options.projectDir);
|
|
4817
|
+
const gapReport = await aggregator.generateGapReport(knowledgeDir);
|
|
4818
|
+
await aggregator.writeGapReport(gapReport);
|
|
4819
|
+
return gapReport;
|
|
4820
|
+
}
|
|
4821
|
+
// ── Phase 4: REMEDIATE ────────────────────────────────────────────────────
|
|
4822
|
+
remediate(driftResult, remediations, options) {
|
|
4823
|
+
for (const finding of driftResult.findings) {
|
|
4824
|
+
switch (finding.classification) {
|
|
4825
|
+
case "stale":
|
|
4826
|
+
this.store.removeNode(finding.entryId);
|
|
4827
|
+
remediations.push(`removed stale: ${finding.entryId}`);
|
|
4828
|
+
break;
|
|
4829
|
+
case "new":
|
|
4830
|
+
break;
|
|
4831
|
+
case "drifted":
|
|
4832
|
+
if (!options.ci) {
|
|
4833
|
+
remediations.push(`flagged drifted: ${finding.entryId}`);
|
|
4834
|
+
}
|
|
4835
|
+
break;
|
|
4836
|
+
case "contradicting":
|
|
4837
|
+
break;
|
|
1742
4838
|
}
|
|
1743
4839
|
}
|
|
1744
|
-
|
|
4840
|
+
}
|
|
4841
|
+
async stageNewFindings(driftResult, options) {
|
|
4842
|
+
const newFindings = driftResult.findings.filter((f) => f.classification === "new");
|
|
4843
|
+
if (newFindings.length === 0) return;
|
|
4844
|
+
const stagedEntries = newFindings.filter((f) => f.fresh != null).map((f) => ({
|
|
4845
|
+
id: f.fresh.id,
|
|
4846
|
+
source: this.classifySource(f.fresh.source),
|
|
4847
|
+
nodeType: f.fresh.type,
|
|
4848
|
+
name: f.fresh.name,
|
|
4849
|
+
confidence: 0.7,
|
|
4850
|
+
contentHash: f.fresh.contentHash,
|
|
4851
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4852
|
+
}));
|
|
4853
|
+
if (stagedEntries.length > 0) {
|
|
4854
|
+
const aggregator = new KnowledgeStagingAggregator(options.projectDir);
|
|
4855
|
+
await aggregator.aggregate(stagedEntries, [], []);
|
|
4856
|
+
}
|
|
4857
|
+
}
|
|
4858
|
+
classifySource(source) {
|
|
4859
|
+
if (source === "linker" || source === "knowledge-linker") return "linker";
|
|
4860
|
+
if (source === "diagram") return "diagram";
|
|
4861
|
+
return "extractor";
|
|
4862
|
+
}
|
|
4863
|
+
// ── Verdict ───────────────────────────────────────────────────────────────
|
|
4864
|
+
computeVerdict(driftResult) {
|
|
4865
|
+
const { summary } = driftResult;
|
|
4866
|
+
const unresolved = summary.drifted + summary.stale + summary.contradicting;
|
|
4867
|
+
if (unresolved === 0 && summary.new === 0) return "pass";
|
|
4868
|
+
if (unresolved === 0) return "warn";
|
|
4869
|
+
return "fail";
|
|
1745
4870
|
}
|
|
1746
4871
|
};
|
|
1747
4872
|
|
|
1748
4873
|
// src/ingest/connectors/ConnectorUtils.ts
|
|
1749
|
-
var
|
|
4874
|
+
var CODE_NODE_TYPES4 = ["file", "function", "class", "method", "interface", "variable"];
|
|
4875
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
4876
|
+
function withRetry(client, options) {
|
|
4877
|
+
const maxRetries = options?.maxRetries ?? 3;
|
|
4878
|
+
const baseDelayMs = options?.baseDelayMs ?? 1e3;
|
|
4879
|
+
const maxDelayMs = options?.maxDelayMs ?? 3e4;
|
|
4880
|
+
return async (url, requestOptions) => {
|
|
4881
|
+
let lastError;
|
|
4882
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
4883
|
+
try {
|
|
4884
|
+
const response = await client(url, requestOptions);
|
|
4885
|
+
if (response.ok || !RETRYABLE_STATUSES.has(response.status ?? 0)) {
|
|
4886
|
+
return response;
|
|
4887
|
+
}
|
|
4888
|
+
if (attempt === maxRetries) {
|
|
4889
|
+
return response;
|
|
4890
|
+
}
|
|
4891
|
+
} catch (err) {
|
|
4892
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
4893
|
+
if (attempt === maxRetries) {
|
|
4894
|
+
throw lastError;
|
|
4895
|
+
}
|
|
4896
|
+
}
|
|
4897
|
+
const exponentialDelay = baseDelayMs * 2 ** attempt;
|
|
4898
|
+
const cappedDelay = Math.min(exponentialDelay, maxDelayMs);
|
|
4899
|
+
const jitteredDelay = cappedDelay * (0.5 + Math.random() * 0.5);
|
|
4900
|
+
await new Promise((resolve) => setTimeout(resolve, jitteredDelay));
|
|
4901
|
+
}
|
|
4902
|
+
throw lastError ?? new Error("Request failed after retries");
|
|
4903
|
+
};
|
|
4904
|
+
}
|
|
1750
4905
|
var SANITIZE_RULES = [
|
|
1751
4906
|
// Strip XML/HTML-like instruction tags that could be interpreted as system prompts
|
|
1752
4907
|
{
|
|
@@ -1781,7 +4936,7 @@ function sanitizeExternalText(text, maxLength = 2e3) {
|
|
|
1781
4936
|
}
|
|
1782
4937
|
function linkToCode(store, content, sourceNodeId, edgeType, options) {
|
|
1783
4938
|
let edgesCreated = 0;
|
|
1784
|
-
for (const type of
|
|
4939
|
+
for (const type of CODE_NODE_TYPES4) {
|
|
1785
4940
|
const nodes = store.findNodes({ type });
|
|
1786
4941
|
for (const node of nodes) {
|
|
1787
4942
|
if (node.name.length < 3) continue;
|
|
@@ -1801,12 +4956,12 @@ function linkToCode(store, content, sourceNodeId, edgeType, options) {
|
|
|
1801
4956
|
}
|
|
1802
4957
|
|
|
1803
4958
|
// src/ingest/connectors/SyncManager.ts
|
|
1804
|
-
import * as
|
|
1805
|
-
import * as
|
|
4959
|
+
import * as fs10 from "fs/promises";
|
|
4960
|
+
import * as path11 from "path";
|
|
1806
4961
|
var SyncManager = class {
|
|
1807
4962
|
constructor(store, graphDir) {
|
|
1808
4963
|
this.store = store;
|
|
1809
|
-
this.metadataPath =
|
|
4964
|
+
this.metadataPath = path11.join(graphDir, "sync-metadata.json");
|
|
1810
4965
|
}
|
|
1811
4966
|
store;
|
|
1812
4967
|
registrations = /* @__PURE__ */ new Map();
|
|
@@ -1854,6 +5009,16 @@ var SyncManager = class {
|
|
|
1854
5009
|
combined.errors.push(...result.errors);
|
|
1855
5010
|
combined.durationMs += result.durationMs;
|
|
1856
5011
|
}
|
|
5012
|
+
try {
|
|
5013
|
+
const linker = new KnowledgeLinker(this.store);
|
|
5014
|
+
const linkResult = await linker.link();
|
|
5015
|
+
combined.nodesAdded += linkResult.factsCreated + linkResult.conceptsClustered;
|
|
5016
|
+
combined.errors.push(...linkResult.errors);
|
|
5017
|
+
} catch (err) {
|
|
5018
|
+
combined.errors.push(
|
|
5019
|
+
`KnowledgeLinker error: ${err instanceof Error ? err.message : String(err)}`
|
|
5020
|
+
);
|
|
5021
|
+
}
|
|
1857
5022
|
return combined;
|
|
1858
5023
|
}
|
|
1859
5024
|
async getMetadata() {
|
|
@@ -1861,18 +5026,64 @@ var SyncManager = class {
|
|
|
1861
5026
|
}
|
|
1862
5027
|
async loadMetadata() {
|
|
1863
5028
|
try {
|
|
1864
|
-
const raw = await
|
|
5029
|
+
const raw = await fs10.readFile(this.metadataPath, "utf-8");
|
|
1865
5030
|
return JSON.parse(raw);
|
|
1866
5031
|
} catch {
|
|
1867
5032
|
return { connectors: {} };
|
|
1868
5033
|
}
|
|
1869
5034
|
}
|
|
1870
5035
|
async saveMetadata(metadata) {
|
|
1871
|
-
await
|
|
1872
|
-
await
|
|
5036
|
+
await fs10.mkdir(path11.dirname(this.metadataPath), { recursive: true });
|
|
5037
|
+
await fs10.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
1873
5038
|
}
|
|
1874
5039
|
};
|
|
1875
5040
|
|
|
5041
|
+
// src/ingest/connectors/ContentCondenser.ts
|
|
5042
|
+
var SUMMARIZE_PROMPT = `Summarize the following content to fit within the specified length.
|
|
5043
|
+
Preserve all business rules, SLAs, requirements, decisions, and regulatory references.
|
|
5044
|
+
Remove redundant details and boilerplate while keeping all actionable information.
|
|
5045
|
+
|
|
5046
|
+
Content to summarize:
|
|
5047
|
+
`;
|
|
5048
|
+
async function condenseContent(raw, options, summarizeFn) {
|
|
5049
|
+
const originalLength = raw.length;
|
|
5050
|
+
const { maxLength } = options;
|
|
5051
|
+
const summarizationThreshold = options.summarizationThreshold ?? maxLength * 2;
|
|
5052
|
+
if (raw.length <= maxLength) {
|
|
5053
|
+
return { content: raw, method: "passthrough", originalLength };
|
|
5054
|
+
}
|
|
5055
|
+
if (raw.length < summarizationThreshold) {
|
|
5056
|
+
return {
|
|
5057
|
+
content: sanitizeExternalText(raw, maxLength),
|
|
5058
|
+
method: "truncated",
|
|
5059
|
+
originalLength
|
|
5060
|
+
};
|
|
5061
|
+
}
|
|
5062
|
+
if (options.modelEndpoint && summarizeFn) {
|
|
5063
|
+
try {
|
|
5064
|
+
const summarized = await summarizeFn(SUMMARIZE_PROMPT + raw, {
|
|
5065
|
+
endpoint: options.modelEndpoint,
|
|
5066
|
+
model: options.modelName ?? "default",
|
|
5067
|
+
maxTokens: Math.ceil(maxLength / 4)
|
|
5068
|
+
// rough token estimate
|
|
5069
|
+
});
|
|
5070
|
+
const finalContent = summarized.length > maxLength ? sanitizeExternalText(summarized, maxLength) : summarized;
|
|
5071
|
+
return { content: finalContent, method: "summarized", originalLength };
|
|
5072
|
+
} catch {
|
|
5073
|
+
return {
|
|
5074
|
+
content: sanitizeExternalText(raw, maxLength),
|
|
5075
|
+
method: "truncated",
|
|
5076
|
+
originalLength
|
|
5077
|
+
};
|
|
5078
|
+
}
|
|
5079
|
+
}
|
|
5080
|
+
return {
|
|
5081
|
+
content: sanitizeExternalText(raw, maxLength),
|
|
5082
|
+
method: "truncated",
|
|
5083
|
+
originalLength
|
|
5084
|
+
};
|
|
5085
|
+
}
|
|
5086
|
+
|
|
1876
5087
|
// src/ingest/connectors/JiraConnector.ts
|
|
1877
5088
|
function buildIngestResult(nodesAdded, edgesAdded, errors, start) {
|
|
1878
5089
|
return {
|
|
@@ -1904,7 +5115,7 @@ var JiraConnector = class {
|
|
|
1904
5115
|
source = "jira";
|
|
1905
5116
|
httpClient;
|
|
1906
5117
|
constructor(httpClient) {
|
|
1907
|
-
this.httpClient = httpClient ?? ((url, options) => fetch(url, options));
|
|
5118
|
+
this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
|
|
1908
5119
|
}
|
|
1909
5120
|
async ingest(store, config) {
|
|
1910
5121
|
const start = Date.now();
|
|
@@ -1931,7 +5142,7 @@ var JiraConnector = class {
|
|
|
1931
5142
|
const jql = buildJql(config);
|
|
1932
5143
|
const headers = { Authorization: `Basic ${apiKey}`, "Content-Type": "application/json" };
|
|
1933
5144
|
try {
|
|
1934
|
-
const counts = await this.fetchAllIssues(store, baseUrl, jql, headers);
|
|
5145
|
+
const counts = await this.fetchAllIssues(store, baseUrl, jql, headers, config);
|
|
1935
5146
|
return buildIngestResult(counts.nodesAdded, counts.edgesAdded, [], start);
|
|
1936
5147
|
} catch (err) {
|
|
1937
5148
|
return buildIngestResult(
|
|
@@ -1942,7 +5153,7 @@ var JiraConnector = class {
|
|
|
1942
5153
|
);
|
|
1943
5154
|
}
|
|
1944
5155
|
}
|
|
1945
|
-
async fetchAllIssues(store, baseUrl, jql, headers) {
|
|
5156
|
+
async fetchAllIssues(store, baseUrl, jql, headers, config) {
|
|
1946
5157
|
let nodesAdded = 0;
|
|
1947
5158
|
let edgesAdded = 0;
|
|
1948
5159
|
let startAt = 0;
|
|
@@ -1955,7 +5166,7 @@ var JiraConnector = class {
|
|
|
1955
5166
|
const data = await response.json();
|
|
1956
5167
|
total = data.total;
|
|
1957
5168
|
for (const issue of data.issues) {
|
|
1958
|
-
const counts = this.processIssue(store, issue);
|
|
5169
|
+
const counts = await this.processIssue(store, issue, baseUrl, headers, config);
|
|
1959
5170
|
nodesAdded += counts.nodesAdded;
|
|
1960
5171
|
edgesAdded += counts.edgesAdded;
|
|
1961
5172
|
}
|
|
@@ -1963,19 +5174,47 @@ var JiraConnector = class {
|
|
|
1963
5174
|
}
|
|
1964
5175
|
return { nodesAdded, edgesAdded };
|
|
1965
5176
|
}
|
|
1966
|
-
processIssue(store, issue) {
|
|
5177
|
+
async processIssue(store, issue, baseUrl, headers, config) {
|
|
1967
5178
|
const nodeId = `issue:jira:${issue.key}`;
|
|
5179
|
+
const comments = await this.fetchComments(baseUrl, issue.key, headers);
|
|
5180
|
+
const parts = [issue.fields.summary];
|
|
5181
|
+
if (issue.fields.description) {
|
|
5182
|
+
parts.push(issue.fields.description);
|
|
5183
|
+
}
|
|
5184
|
+
if (comments.length > 0) {
|
|
5185
|
+
for (const comment of comments) {
|
|
5186
|
+
parts.push(`${comment.author.displayName} (${comment.created}): ${comment.body}`);
|
|
5187
|
+
}
|
|
5188
|
+
}
|
|
5189
|
+
const rawContent = parts.join("\n");
|
|
5190
|
+
const acceptanceCriteria = parseAcceptanceCriteria(issue.fields.description ?? "");
|
|
5191
|
+
const customFields = extractCustomFields(issue.fields);
|
|
5192
|
+
const maxLen = config.maxContentLength ?? 4e3;
|
|
5193
|
+
const condensed = await condenseContent(rawContent, { maxLength: maxLen });
|
|
5194
|
+
const metadata = {
|
|
5195
|
+
key: issue.key,
|
|
5196
|
+
status: issue.fields.status?.name,
|
|
5197
|
+
priority: issue.fields.priority?.name,
|
|
5198
|
+
assignee: issue.fields.assignee?.displayName,
|
|
5199
|
+
labels: issue.fields.labels ?? [],
|
|
5200
|
+
commentCount: comments.length
|
|
5201
|
+
};
|
|
5202
|
+
if (acceptanceCriteria.length > 0) {
|
|
5203
|
+
metadata.acceptanceCriteria = acceptanceCriteria;
|
|
5204
|
+
}
|
|
5205
|
+
if (Object.keys(customFields).length > 0) {
|
|
5206
|
+
metadata.customFields = customFields;
|
|
5207
|
+
}
|
|
5208
|
+
if (condensed.method !== "passthrough") {
|
|
5209
|
+
metadata.condensed = condensed.method;
|
|
5210
|
+
metadata.originalLength = condensed.originalLength;
|
|
5211
|
+
}
|
|
1968
5212
|
store.addNode({
|
|
1969
5213
|
id: nodeId,
|
|
1970
5214
|
type: "issue",
|
|
1971
5215
|
name: sanitizeExternalText(issue.fields.summary, 500),
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
status: issue.fields.status?.name,
|
|
1975
|
-
priority: issue.fields.priority?.name,
|
|
1976
|
-
assignee: issue.fields.assignee?.displayName,
|
|
1977
|
-
labels: issue.fields.labels ?? []
|
|
1978
|
-
}
|
|
5216
|
+
content: condensed.content,
|
|
5217
|
+
metadata
|
|
1979
5218
|
});
|
|
1980
5219
|
const searchText = sanitizeExternalText(
|
|
1981
5220
|
[issue.fields.summary, issue.fields.description ?? ""].join(" ")
|
|
@@ -1983,7 +5222,40 @@ var JiraConnector = class {
|
|
|
1983
5222
|
const edgesAdded = linkToCode(store, searchText, nodeId, "applies_to");
|
|
1984
5223
|
return { nodesAdded: 1, edgesAdded };
|
|
1985
5224
|
}
|
|
5225
|
+
async fetchComments(baseUrl, issueKey, headers) {
|
|
5226
|
+
try {
|
|
5227
|
+
const url = `${baseUrl}/rest/api/2/issue/${issueKey}/comment`;
|
|
5228
|
+
const response = await this.httpClient(url, { headers });
|
|
5229
|
+
if (!response.ok) return [];
|
|
5230
|
+
const data = await response.json();
|
|
5231
|
+
return data.comments ?? [];
|
|
5232
|
+
} catch {
|
|
5233
|
+
return [];
|
|
5234
|
+
}
|
|
5235
|
+
}
|
|
1986
5236
|
};
|
|
5237
|
+
function parseAcceptanceCriteria(description) {
|
|
5238
|
+
const criteria = [];
|
|
5239
|
+
const checkboxRegex = /[-*]\s*\[[ x]\]\s*(.+)/gi;
|
|
5240
|
+
let match;
|
|
5241
|
+
while ((match = checkboxRegex.exec(description)) !== null) {
|
|
5242
|
+
criteria.push(match[1].trim());
|
|
5243
|
+
}
|
|
5244
|
+
const gwtRegex = /Given\b.+?When\b.+?Then\b.+/gi;
|
|
5245
|
+
while ((match = gwtRegex.exec(description)) !== null) {
|
|
5246
|
+
criteria.push(match[0].trim());
|
|
5247
|
+
}
|
|
5248
|
+
return criteria;
|
|
5249
|
+
}
|
|
5250
|
+
function extractCustomFields(fields) {
|
|
5251
|
+
const result = {};
|
|
5252
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
5253
|
+
if (key.startsWith("customfield_") && value != null && typeof value === "string") {
|
|
5254
|
+
result[key] = value;
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5257
|
+
return result;
|
|
5258
|
+
}
|
|
1987
5259
|
|
|
1988
5260
|
// src/ingest/connectors/SlackConnector.ts
|
|
1989
5261
|
var SlackConnector = class {
|
|
@@ -1991,7 +5263,7 @@ var SlackConnector = class {
|
|
|
1991
5263
|
source = "slack";
|
|
1992
5264
|
httpClient;
|
|
1993
5265
|
constructor(httpClient) {
|
|
1994
|
-
this.httpClient = httpClient ?? ((url, options) => fetch(url, options));
|
|
5266
|
+
this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
|
|
1995
5267
|
}
|
|
1996
5268
|
async ingest(store, config) {
|
|
1997
5269
|
const start = Date.now();
|
|
@@ -2014,7 +5286,7 @@ var SlackConnector = class {
|
|
|
2014
5286
|
const oldest = config.lookbackDays ? String(Math.floor((Date.now() - Number(config.lookbackDays) * 864e5) / 1e3)) : void 0;
|
|
2015
5287
|
for (const channel of channels) {
|
|
2016
5288
|
try {
|
|
2017
|
-
const result = await this.processChannel(store, channel, apiKey, oldest);
|
|
5289
|
+
const result = await this.processChannel(store, channel, apiKey, oldest, config);
|
|
2018
5290
|
nodesAdded += result.nodesAdded;
|
|
2019
5291
|
edgesAdded += result.edgesAdded;
|
|
2020
5292
|
errors.push(...result.errors);
|
|
@@ -2033,7 +5305,7 @@ var SlackConnector = class {
|
|
|
2033
5305
|
durationMs: Date.now() - start
|
|
2034
5306
|
};
|
|
2035
5307
|
}
|
|
2036
|
-
async processChannel(store, channel, apiKey, oldest) {
|
|
5308
|
+
async processChannel(store, channel, apiKey, oldest, config) {
|
|
2037
5309
|
const errors = [];
|
|
2038
5310
|
let nodesAdded = 0;
|
|
2039
5311
|
let edgesAdded = 0;
|
|
@@ -2058,27 +5330,74 @@ var SlackConnector = class {
|
|
|
2058
5330
|
if (!data.ok) {
|
|
2059
5331
|
return { nodesAdded: 0, edgesAdded: 0, errors: [`Slack API error for channel ${channel}`] };
|
|
2060
5332
|
}
|
|
5333
|
+
const maxLen = config.maxContentLength ?? 2e3;
|
|
2061
5334
|
for (const message of data.messages) {
|
|
2062
5335
|
const nodeId = `conversation:slack:${channel}:${message.ts}`;
|
|
2063
|
-
|
|
2064
|
-
|
|
5336
|
+
let assembledText = message.text;
|
|
5337
|
+
let threadReplyCount;
|
|
5338
|
+
if (message.reply_count && message.reply_count > 0 && message.thread_ts) {
|
|
5339
|
+
const replies = await this.fetchThreadReplies(channel, message.thread_ts, apiKey);
|
|
5340
|
+
if (replies.length > 0) {
|
|
5341
|
+
const threadReplies = replies.slice(1);
|
|
5342
|
+
threadReplyCount = threadReplies.length;
|
|
5343
|
+
if (threadReplies.length > 0) {
|
|
5344
|
+
const replyLines = threadReplies.map((r) => `${r.user} (${r.ts}): ${r.text}`);
|
|
5345
|
+
assembledText = `${message.text}
|
|
5346
|
+
${replyLines.join("\n")}`;
|
|
5347
|
+
}
|
|
5348
|
+
}
|
|
5349
|
+
}
|
|
5350
|
+
const condensed = await condenseContent(assembledText, { maxLength: maxLen });
|
|
5351
|
+
const snippet = condensed.content.length > 100 ? condensed.content.slice(0, 100) : condensed.content;
|
|
5352
|
+
const reactions = message.reactions ? message.reactions.reduce(
|
|
5353
|
+
(acc, r) => ({ ...acc, [r.name]: r.count }),
|
|
5354
|
+
{}
|
|
5355
|
+
) : void 0;
|
|
5356
|
+
const metadata = {
|
|
5357
|
+
author: message.user,
|
|
5358
|
+
channel,
|
|
5359
|
+
timestamp: message.ts
|
|
5360
|
+
};
|
|
5361
|
+
if (threadReplyCount !== void 0) {
|
|
5362
|
+
metadata.threadReplyCount = threadReplyCount;
|
|
5363
|
+
}
|
|
5364
|
+
if (reactions) {
|
|
5365
|
+
metadata.reactions = reactions;
|
|
5366
|
+
}
|
|
5367
|
+
if (condensed.method !== "passthrough") {
|
|
5368
|
+
metadata.condensed = condensed.method;
|
|
5369
|
+
metadata.originalLength = condensed.originalLength;
|
|
5370
|
+
}
|
|
2065
5371
|
store.addNode({
|
|
2066
5372
|
id: nodeId,
|
|
2067
5373
|
type: "conversation",
|
|
2068
5374
|
name: snippet,
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
channel,
|
|
2072
|
-
timestamp: message.ts
|
|
2073
|
-
}
|
|
5375
|
+
content: condensed.content,
|
|
5376
|
+
metadata
|
|
2074
5377
|
});
|
|
2075
5378
|
nodesAdded++;
|
|
2076
|
-
edgesAdded += linkToCode(store,
|
|
5379
|
+
edgesAdded += linkToCode(store, condensed.content, nodeId, "references", {
|
|
2077
5380
|
checkPaths: true
|
|
2078
5381
|
});
|
|
2079
5382
|
}
|
|
2080
5383
|
return { nodesAdded, edgesAdded, errors };
|
|
2081
5384
|
}
|
|
5385
|
+
async fetchThreadReplies(channel, threadTs, apiKey) {
|
|
5386
|
+
try {
|
|
5387
|
+
const url = `https://slack.com/api/conversations.replies?channel=${encodeURIComponent(channel)}&ts=${threadTs}`;
|
|
5388
|
+
const response = await this.httpClient(url, {
|
|
5389
|
+
headers: {
|
|
5390
|
+
Authorization: `Bearer ${apiKey}`,
|
|
5391
|
+
"Content-Type": "application/json"
|
|
5392
|
+
}
|
|
5393
|
+
});
|
|
5394
|
+
if (!response.ok) return [];
|
|
5395
|
+
const data = await response.json();
|
|
5396
|
+
return data.ok ? data.messages : [];
|
|
5397
|
+
} catch {
|
|
5398
|
+
return [];
|
|
5399
|
+
}
|
|
5400
|
+
}
|
|
2082
5401
|
};
|
|
2083
5402
|
|
|
2084
5403
|
// src/ingest/connectors/ConfluenceConnector.ts
|
|
@@ -2097,7 +5416,7 @@ var ConfluenceConnector = class {
|
|
|
2097
5416
|
source = "confluence";
|
|
2098
5417
|
httpClient;
|
|
2099
5418
|
constructor(httpClient) {
|
|
2100
|
-
this.httpClient = httpClient ?? ((url, options) => fetch(url, options));
|
|
5419
|
+
this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
|
|
2101
5420
|
}
|
|
2102
5421
|
async ingest(store, config) {
|
|
2103
5422
|
const start = Date.now();
|
|
@@ -2110,7 +5429,14 @@ var ConfluenceConnector = class {
|
|
|
2110
5429
|
const baseUrlEnv = config.baseUrlEnv ?? "CONFLUENCE_BASE_URL";
|
|
2111
5430
|
const baseUrl = process.env[baseUrlEnv] ?? "";
|
|
2112
5431
|
const spaceKey = config.spaceKey ?? "";
|
|
2113
|
-
const counts = await this.fetchAllPagesHandled(
|
|
5432
|
+
const counts = await this.fetchAllPagesHandled(
|
|
5433
|
+
store,
|
|
5434
|
+
baseUrl,
|
|
5435
|
+
apiKey,
|
|
5436
|
+
spaceKey,
|
|
5437
|
+
errors,
|
|
5438
|
+
config
|
|
5439
|
+
);
|
|
2114
5440
|
return {
|
|
2115
5441
|
nodesAdded: counts.nodesAdded,
|
|
2116
5442
|
nodesUpdated: 0,
|
|
@@ -2120,9 +5446,9 @@ var ConfluenceConnector = class {
|
|
|
2120
5446
|
durationMs: Date.now() - start
|
|
2121
5447
|
};
|
|
2122
5448
|
}
|
|
2123
|
-
async fetchAllPagesHandled(store, baseUrl, apiKey, spaceKey, errors) {
|
|
5449
|
+
async fetchAllPagesHandled(store, baseUrl, apiKey, spaceKey, errors, config) {
|
|
2124
5450
|
try {
|
|
2125
|
-
const result = await this.fetchAllPages(store, baseUrl, apiKey, spaceKey);
|
|
5451
|
+
const result = await this.fetchAllPages(store, baseUrl, apiKey, spaceKey, config);
|
|
2126
5452
|
errors.push(...result.errors);
|
|
2127
5453
|
return { nodesAdded: result.nodesAdded, edgesAdded: result.edgesAdded };
|
|
2128
5454
|
} catch (err) {
|
|
@@ -2130,7 +5456,7 @@ var ConfluenceConnector = class {
|
|
|
2130
5456
|
return { nodesAdded: 0, edgesAdded: 0 };
|
|
2131
5457
|
}
|
|
2132
5458
|
}
|
|
2133
|
-
async fetchAllPages(store, baseUrl, apiKey, spaceKey) {
|
|
5459
|
+
async fetchAllPages(store, baseUrl, apiKey, spaceKey, config) {
|
|
2134
5460
|
const errors = [];
|
|
2135
5461
|
let nodesAdded = 0;
|
|
2136
5462
|
let edgesAdded = 0;
|
|
@@ -2145,7 +5471,7 @@ var ConfluenceConnector = class {
|
|
|
2145
5471
|
}
|
|
2146
5472
|
const data = await response.json();
|
|
2147
5473
|
for (const page of data.results) {
|
|
2148
|
-
const counts = this.processPage(store, page, spaceKey);
|
|
5474
|
+
const counts = await this.processPage(store, page, spaceKey, config);
|
|
2149
5475
|
nodesAdded += counts.nodesAdded;
|
|
2150
5476
|
edgesAdded += counts.edgesAdded;
|
|
2151
5477
|
}
|
|
@@ -2153,22 +5479,43 @@ var ConfluenceConnector = class {
|
|
|
2153
5479
|
}
|
|
2154
5480
|
return { nodesAdded, edgesAdded, errors };
|
|
2155
5481
|
}
|
|
2156
|
-
processPage(store, page, spaceKey) {
|
|
5482
|
+
async processPage(store, page, spaceKey, config) {
|
|
2157
5483
|
const nodeId = `confluence:${page.id}`;
|
|
5484
|
+
let edgesAdded = 0;
|
|
5485
|
+
const labels = page.metadata?.labels?.results?.map((l) => l.name) ?? [];
|
|
5486
|
+
const parentPageId = page.ancestors && page.ancestors.length > 0 ? page.ancestors[page.ancestors.length - 1].id : void 0;
|
|
5487
|
+
const rawContent = `${page.title} ${page.body?.storage?.value ?? ""}`;
|
|
5488
|
+
const maxLen = config.maxContentLength ?? 8e3;
|
|
5489
|
+
const condensed = await condenseContent(rawContent, { maxLength: maxLen });
|
|
5490
|
+
const metadata = {
|
|
5491
|
+
source: "confluence",
|
|
5492
|
+
spaceKey,
|
|
5493
|
+
pageId: page.id,
|
|
5494
|
+
status: page.status,
|
|
5495
|
+
url: page._links?.webui ?? "",
|
|
5496
|
+
labels
|
|
5497
|
+
};
|
|
5498
|
+
if (parentPageId) {
|
|
5499
|
+
metadata.parentPageId = parentPageId;
|
|
5500
|
+
}
|
|
5501
|
+
if (condensed.method !== "passthrough") {
|
|
5502
|
+
metadata.condensed = condensed.method;
|
|
5503
|
+
metadata.originalLength = condensed.originalLength;
|
|
5504
|
+
}
|
|
2158
5505
|
store.addNode({
|
|
2159
5506
|
id: nodeId,
|
|
2160
5507
|
type: "document",
|
|
2161
5508
|
name: sanitizeExternalText(page.title, 500),
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
spaceKey,
|
|
2165
|
-
pageId: page.id,
|
|
2166
|
-
status: page.status,
|
|
2167
|
-
url: page._links?.webui ?? ""
|
|
2168
|
-
}
|
|
5509
|
+
content: condensed.content,
|
|
5510
|
+
metadata
|
|
2169
5511
|
});
|
|
5512
|
+
if (parentPageId) {
|
|
5513
|
+
const parentNodeId = `confluence:${parentPageId}`;
|
|
5514
|
+
store.addEdge({ from: parentNodeId, to: nodeId, type: "contains" });
|
|
5515
|
+
edgesAdded++;
|
|
5516
|
+
}
|
|
2170
5517
|
const text = sanitizeExternalText(`${page.title} ${page.body?.storage?.value ?? ""}`);
|
|
2171
|
-
|
|
5518
|
+
edgesAdded += linkToCode(store, text, nodeId, "documents");
|
|
2172
5519
|
return { nodesAdded: 1, edgesAdded };
|
|
2173
5520
|
}
|
|
2174
5521
|
};
|
|
@@ -2234,7 +5581,7 @@ var CIConnector = class {
|
|
|
2234
5581
|
source = "github-actions";
|
|
2235
5582
|
httpClient;
|
|
2236
5583
|
constructor(httpClient) {
|
|
2237
|
-
this.httpClient = httpClient ?? ((url, options) => fetch(url, options));
|
|
5584
|
+
this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
|
|
2238
5585
|
}
|
|
2239
5586
|
async ingest(store, config) {
|
|
2240
5587
|
const start = Date.now();
|
|
@@ -2286,6 +5633,306 @@ var CIConnector = class {
|
|
|
2286
5633
|
}
|
|
2287
5634
|
};
|
|
2288
5635
|
|
|
5636
|
+
// src/ingest/connectors/FigmaConnector.ts
|
|
5637
|
+
var CONSTRAINT_KEYWORDS = [
|
|
5638
|
+
"constraint",
|
|
5639
|
+
"must",
|
|
5640
|
+
"required",
|
|
5641
|
+
"minimum",
|
|
5642
|
+
"maximum",
|
|
5643
|
+
"spacing",
|
|
5644
|
+
"padding",
|
|
5645
|
+
"margin",
|
|
5646
|
+
"breakpoint",
|
|
5647
|
+
"responsive",
|
|
5648
|
+
"accessible",
|
|
5649
|
+
"a11y",
|
|
5650
|
+
"wcag"
|
|
5651
|
+
];
|
|
5652
|
+
function hasConstraintKeyword(text) {
|
|
5653
|
+
const lower = text.toLowerCase();
|
|
5654
|
+
return CONSTRAINT_KEYWORDS.some((kw) => lower.includes(kw));
|
|
5655
|
+
}
|
|
5656
|
+
function buildIngestResult2(nodesAdded, edgesAdded, errors, start) {
|
|
5657
|
+
return {
|
|
5658
|
+
nodesAdded,
|
|
5659
|
+
nodesUpdated: 0,
|
|
5660
|
+
edgesAdded,
|
|
5661
|
+
edgesUpdated: 0,
|
|
5662
|
+
errors,
|
|
5663
|
+
durationMs: Date.now() - start
|
|
5664
|
+
};
|
|
5665
|
+
}
|
|
5666
|
+
var FigmaConnector = class {
|
|
5667
|
+
name = "figma";
|
|
5668
|
+
source = "figma";
|
|
5669
|
+
httpClient;
|
|
5670
|
+
constructor(httpClient) {
|
|
5671
|
+
this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
|
|
5672
|
+
}
|
|
5673
|
+
async ingest(store, config) {
|
|
5674
|
+
const start = Date.now();
|
|
5675
|
+
const apiKeyEnv = config.apiKeyEnv ?? "FIGMA_API_KEY";
|
|
5676
|
+
const apiKey = process.env[apiKeyEnv];
|
|
5677
|
+
if (!apiKey) {
|
|
5678
|
+
return buildIngestResult2(
|
|
5679
|
+
0,
|
|
5680
|
+
0,
|
|
5681
|
+
[`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
5682
|
+
start
|
|
5683
|
+
);
|
|
5684
|
+
}
|
|
5685
|
+
const baseUrlEnv = config.baseUrlEnv ?? "FIGMA_BASE_URL";
|
|
5686
|
+
const baseUrl = process.env[baseUrlEnv] ?? "https://api.figma.com";
|
|
5687
|
+
try {
|
|
5688
|
+
const parsed = new URL(baseUrl);
|
|
5689
|
+
if (parsed.protocol !== "https:" || !parsed.hostname.endsWith("api.figma.com")) {
|
|
5690
|
+
return buildIngestResult2(
|
|
5691
|
+
0,
|
|
5692
|
+
0,
|
|
5693
|
+
[`Invalid ${baseUrlEnv}: must be an HTTPS URL on api.figma.com`],
|
|
5694
|
+
start
|
|
5695
|
+
);
|
|
5696
|
+
}
|
|
5697
|
+
} catch {
|
|
5698
|
+
return buildIngestResult2(0, 0, [`Invalid ${baseUrlEnv}: not a valid URL`], start);
|
|
5699
|
+
}
|
|
5700
|
+
const fileIds = config.fileIds;
|
|
5701
|
+
if (!fileIds || fileIds.length === 0) {
|
|
5702
|
+
return buildIngestResult2(0, 0, ["No fileIds provided in connector config"], start);
|
|
5703
|
+
}
|
|
5704
|
+
const headers = { "X-FIGMA-TOKEN": apiKey };
|
|
5705
|
+
const maxLen = config.maxContentLength ?? 4e3;
|
|
5706
|
+
let nodesAdded = 0;
|
|
5707
|
+
let edgesAdded = 0;
|
|
5708
|
+
const errors = [];
|
|
5709
|
+
for (const fileId of fileIds) {
|
|
5710
|
+
try {
|
|
5711
|
+
const counts = await this.processFile(store, baseUrl, fileId, headers, maxLen);
|
|
5712
|
+
nodesAdded += counts.nodesAdded;
|
|
5713
|
+
edgesAdded += counts.edgesAdded;
|
|
5714
|
+
} catch (err) {
|
|
5715
|
+
errors.push(
|
|
5716
|
+
`Figma API error for file ${fileId}: ${err instanceof Error ? err.message : String(err)}`
|
|
5717
|
+
);
|
|
5718
|
+
}
|
|
5719
|
+
}
|
|
5720
|
+
return buildIngestResult2(nodesAdded, edgesAdded, errors, start);
|
|
5721
|
+
}
|
|
5722
|
+
async processFile(store, baseUrl, fileId, headers, maxLen) {
|
|
5723
|
+
let nodesAdded = 0;
|
|
5724
|
+
let edgesAdded = 0;
|
|
5725
|
+
const stylesUrl = `${baseUrl}/v1/files/${fileId}/styles`;
|
|
5726
|
+
const stylesResponse = await this.httpClient(stylesUrl, { headers });
|
|
5727
|
+
if (!stylesResponse.ok) throw new Error(`Styles request failed for file ${fileId}`);
|
|
5728
|
+
const stylesData = await stylesResponse.json();
|
|
5729
|
+
for (const style of stylesData.meta.styles) {
|
|
5730
|
+
const nodeId = `figma:token:${style.key}`;
|
|
5731
|
+
const condensed = await condenseContent(sanitizeExternalText(style.description || "", 2e3), {
|
|
5732
|
+
maxLength: maxLen
|
|
5733
|
+
});
|
|
5734
|
+
const metadata = {
|
|
5735
|
+
source: "figma",
|
|
5736
|
+
key: style.key,
|
|
5737
|
+
styleType: style.style_type,
|
|
5738
|
+
fileId
|
|
5739
|
+
};
|
|
5740
|
+
if (condensed.method !== "passthrough") {
|
|
5741
|
+
metadata.condensed = condensed.method;
|
|
5742
|
+
metadata.originalLength = condensed.originalLength;
|
|
5743
|
+
}
|
|
5744
|
+
store.addNode({
|
|
5745
|
+
id: nodeId,
|
|
5746
|
+
type: "design_token",
|
|
5747
|
+
name: sanitizeExternalText(style.name, 500),
|
|
5748
|
+
content: condensed.content,
|
|
5749
|
+
metadata
|
|
5750
|
+
});
|
|
5751
|
+
nodesAdded++;
|
|
5752
|
+
const searchText = sanitizeExternalText([style.name, style.description].join(" "));
|
|
5753
|
+
edgesAdded += linkToCode(store, searchText, nodeId, "references");
|
|
5754
|
+
}
|
|
5755
|
+
const componentsUrl = `${baseUrl}/v1/files/${fileId}/components`;
|
|
5756
|
+
const componentsResponse = await this.httpClient(componentsUrl, { headers });
|
|
5757
|
+
if (!componentsResponse.ok) throw new Error(`Components request failed for file ${fileId}`);
|
|
5758
|
+
const componentsData = await componentsResponse.json();
|
|
5759
|
+
for (const component of componentsData.meta.components) {
|
|
5760
|
+
const description = component.description || "";
|
|
5761
|
+
if (description) {
|
|
5762
|
+
const intentId = `figma:intent:${component.key}`;
|
|
5763
|
+
const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
|
|
5764
|
+
maxLength: maxLen
|
|
5765
|
+
});
|
|
5766
|
+
const metadata = {
|
|
5767
|
+
source: "figma",
|
|
5768
|
+
key: component.key,
|
|
5769
|
+
fileId
|
|
5770
|
+
};
|
|
5771
|
+
if (condensed.method !== "passthrough") {
|
|
5772
|
+
metadata.condensed = condensed.method;
|
|
5773
|
+
metadata.originalLength = condensed.originalLength;
|
|
5774
|
+
}
|
|
5775
|
+
store.addNode({
|
|
5776
|
+
id: intentId,
|
|
5777
|
+
type: "aesthetic_intent",
|
|
5778
|
+
name: sanitizeExternalText(component.name, 500),
|
|
5779
|
+
content: condensed.content,
|
|
5780
|
+
metadata
|
|
5781
|
+
});
|
|
5782
|
+
nodesAdded++;
|
|
5783
|
+
const searchText = sanitizeExternalText([component.name, description].join(" "));
|
|
5784
|
+
edgesAdded += linkToCode(store, searchText, intentId, "references");
|
|
5785
|
+
}
|
|
5786
|
+
if (description && hasConstraintKeyword(description)) {
|
|
5787
|
+
const constraintId = `figma:constraint:${component.key}`;
|
|
5788
|
+
const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
|
|
5789
|
+
maxLength: maxLen
|
|
5790
|
+
});
|
|
5791
|
+
const metadata = {
|
|
5792
|
+
source: "figma",
|
|
5793
|
+
key: component.key,
|
|
5794
|
+
fileId
|
|
5795
|
+
};
|
|
5796
|
+
if (condensed.method !== "passthrough") {
|
|
5797
|
+
metadata.condensed = condensed.method;
|
|
5798
|
+
metadata.originalLength = condensed.originalLength;
|
|
5799
|
+
}
|
|
5800
|
+
store.addNode({
|
|
5801
|
+
id: constraintId,
|
|
5802
|
+
type: "design_constraint",
|
|
5803
|
+
name: sanitizeExternalText(component.name, 500),
|
|
5804
|
+
content: condensed.content,
|
|
5805
|
+
metadata
|
|
5806
|
+
});
|
|
5807
|
+
nodesAdded++;
|
|
5808
|
+
const searchText = sanitizeExternalText([component.name, description].join(" "));
|
|
5809
|
+
edgesAdded += linkToCode(store, searchText, constraintId, "references");
|
|
5810
|
+
}
|
|
5811
|
+
}
|
|
5812
|
+
return { nodesAdded, edgesAdded };
|
|
5813
|
+
}
|
|
5814
|
+
};
|
|
5815
|
+
|
|
5816
|
+
// src/ingest/connectors/MiroConnector.ts
|
|
5817
|
+
var MIN_CONTENT_LENGTH = 10;
|
|
5818
|
+
var CONCEPT_ITEM_TYPES = /* @__PURE__ */ new Set(["sticky_note", "text"]);
|
|
5819
|
+
function stripHtml(html) {
|
|
5820
|
+
return html.replace(/<[^>]*>/g, "");
|
|
5821
|
+
}
|
|
5822
|
+
function buildIngestResult3(nodesAdded, edgesAdded, errors, start) {
|
|
5823
|
+
return {
|
|
5824
|
+
nodesAdded,
|
|
5825
|
+
nodesUpdated: 0,
|
|
5826
|
+
edgesAdded,
|
|
5827
|
+
edgesUpdated: 0,
|
|
5828
|
+
errors,
|
|
5829
|
+
durationMs: Date.now() - start
|
|
5830
|
+
};
|
|
5831
|
+
}
|
|
5832
|
+
var MiroConnector = class {
|
|
5833
|
+
name = "miro";
|
|
5834
|
+
source = "miro";
|
|
5835
|
+
httpClient;
|
|
5836
|
+
constructor(httpClient) {
|
|
5837
|
+
this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
|
|
5838
|
+
}
|
|
5839
|
+
async ingest(store, config) {
|
|
5840
|
+
const start = Date.now();
|
|
5841
|
+
const apiKeyEnv = config.apiKeyEnv ?? "MIRO_API_KEY";
|
|
5842
|
+
const apiKey = process.env[apiKeyEnv];
|
|
5843
|
+
if (!apiKey) {
|
|
5844
|
+
return buildIngestResult3(
|
|
5845
|
+
0,
|
|
5846
|
+
0,
|
|
5847
|
+
[`Missing API key: environment variable "${apiKeyEnv}" is not set`],
|
|
5848
|
+
start
|
|
5849
|
+
);
|
|
5850
|
+
}
|
|
5851
|
+
const baseUrlEnv = config.baseUrlEnv ?? "MIRO_BASE_URL";
|
|
5852
|
+
const baseUrl = process.env[baseUrlEnv] ?? "https://api.miro.com";
|
|
5853
|
+
try {
|
|
5854
|
+
const parsed = new URL(baseUrl);
|
|
5855
|
+
if (parsed.protocol !== "https:" || parsed.hostname !== "api.miro.com") {
|
|
5856
|
+
return buildIngestResult3(
|
|
5857
|
+
0,
|
|
5858
|
+
0,
|
|
5859
|
+
[`Invalid ${baseUrlEnv}: must be an HTTPS URL on api.miro.com`],
|
|
5860
|
+
start
|
|
5861
|
+
);
|
|
5862
|
+
}
|
|
5863
|
+
} catch {
|
|
5864
|
+
return buildIngestResult3(0, 0, [`Invalid ${baseUrlEnv}: not a valid URL`], start);
|
|
5865
|
+
}
|
|
5866
|
+
const boardIds = config.boardIds;
|
|
5867
|
+
if (!boardIds || boardIds.length === 0) {
|
|
5868
|
+
return buildIngestResult3(0, 0, ["No boardIds provided in config"], start);
|
|
5869
|
+
}
|
|
5870
|
+
const headers = { Authorization: `Bearer ${apiKey}` };
|
|
5871
|
+
let totalNodesAdded = 0;
|
|
5872
|
+
let totalEdgesAdded = 0;
|
|
5873
|
+
const errors = [];
|
|
5874
|
+
for (const boardId of boardIds) {
|
|
5875
|
+
try {
|
|
5876
|
+
const counts = await this.processBoard(store, baseUrl, boardId, headers);
|
|
5877
|
+
totalNodesAdded += counts.nodesAdded;
|
|
5878
|
+
totalEdgesAdded += counts.edgesAdded;
|
|
5879
|
+
} catch (err) {
|
|
5880
|
+
errors.push(
|
|
5881
|
+
`Miro API error for board ${boardId}: ${err instanceof Error ? err.message : String(err)}`
|
|
5882
|
+
);
|
|
5883
|
+
}
|
|
5884
|
+
}
|
|
5885
|
+
return buildIngestResult3(totalNodesAdded, totalEdgesAdded, errors, start);
|
|
5886
|
+
}
|
|
5887
|
+
async processBoard(store, baseUrl, boardId, headers) {
|
|
5888
|
+
let nodesAdded = 0;
|
|
5889
|
+
let edgesAdded = 0;
|
|
5890
|
+
const boardResponse = await this.httpClient(`${baseUrl}/v2/boards/${boardId}`, { headers });
|
|
5891
|
+
if (!boardResponse.ok) throw new Error(`Failed to fetch board ${boardId}`);
|
|
5892
|
+
const board = await boardResponse.json();
|
|
5893
|
+
const boardNodeId = `miro:board:${boardId}`;
|
|
5894
|
+
store.addNode({
|
|
5895
|
+
id: boardNodeId,
|
|
5896
|
+
type: "document",
|
|
5897
|
+
name: sanitizeExternalText(board.name, 500),
|
|
5898
|
+
content: sanitizeExternalText(board.description ?? "", 2e3),
|
|
5899
|
+
metadata: {
|
|
5900
|
+
source: "miro",
|
|
5901
|
+
boardId: board.id
|
|
5902
|
+
}
|
|
5903
|
+
});
|
|
5904
|
+
nodesAdded++;
|
|
5905
|
+
const itemsResponse = await this.httpClient(`${baseUrl}/v2/boards/${boardId}/items`, {
|
|
5906
|
+
headers
|
|
5907
|
+
});
|
|
5908
|
+
if (!itemsResponse.ok) throw new Error(`Failed to fetch items for board ${boardId}`);
|
|
5909
|
+
const itemsData = await itemsResponse.json();
|
|
5910
|
+
for (const item of itemsData.data) {
|
|
5911
|
+
const rawContent = item.data?.content ?? "";
|
|
5912
|
+
const plainContent = stripHtml(rawContent);
|
|
5913
|
+
if (plainContent.length < MIN_CONTENT_LENGTH) continue;
|
|
5914
|
+
if (!CONCEPT_ITEM_TYPES.has(item.type)) continue;
|
|
5915
|
+
const itemNodeId = `miro:item:${item.id}`;
|
|
5916
|
+
store.addNode({
|
|
5917
|
+
id: itemNodeId,
|
|
5918
|
+
type: "business_concept",
|
|
5919
|
+
name: sanitizeExternalText(plainContent, 500),
|
|
5920
|
+
content: sanitizeExternalText(plainContent, 2e3),
|
|
5921
|
+
metadata: {
|
|
5922
|
+
source: "miro",
|
|
5923
|
+
boardId: board.id,
|
|
5924
|
+
itemType: item.type
|
|
5925
|
+
}
|
|
5926
|
+
});
|
|
5927
|
+
nodesAdded++;
|
|
5928
|
+
store.addEdge({ from: boardNodeId, to: itemNodeId, type: "contains" });
|
|
5929
|
+
edgesAdded++;
|
|
5930
|
+
edgesAdded += linkToCode(store, plainContent, itemNodeId, "documents");
|
|
5931
|
+
}
|
|
5932
|
+
return { nodesAdded, edgesAdded };
|
|
5933
|
+
}
|
|
5934
|
+
};
|
|
5935
|
+
|
|
2289
5936
|
// src/search/FusionLayer.ts
|
|
2290
5937
|
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
2291
5938
|
"the",
|
|
@@ -2424,7 +6071,7 @@ var FusionLayer = class {
|
|
|
2424
6071
|
};
|
|
2425
6072
|
|
|
2426
6073
|
// src/entropy/GraphEntropyAdapter.ts
|
|
2427
|
-
var
|
|
6074
|
+
var CODE_NODE_TYPES5 = ["file", "function", "class", "method", "interface", "variable"];
|
|
2428
6075
|
var GraphEntropyAdapter = class {
|
|
2429
6076
|
constructor(store) {
|
|
2430
6077
|
this.store = store;
|
|
@@ -2504,7 +6151,7 @@ var GraphEntropyAdapter = class {
|
|
|
2504
6151
|
}
|
|
2505
6152
|
findEntryPoints() {
|
|
2506
6153
|
const entryPoints = [];
|
|
2507
|
-
for (const nodeType of
|
|
6154
|
+
for (const nodeType of CODE_NODE_TYPES5) {
|
|
2508
6155
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
2509
6156
|
for (const node of nodes) {
|
|
2510
6157
|
const isIndexFile = nodeType === "file" && node.name === "index.ts";
|
|
@@ -2540,7 +6187,7 @@ var GraphEntropyAdapter = class {
|
|
|
2540
6187
|
}
|
|
2541
6188
|
collectUnreachableNodes(visited) {
|
|
2542
6189
|
const unreachableNodes = [];
|
|
2543
|
-
for (const nodeType of
|
|
6190
|
+
for (const nodeType of CODE_NODE_TYPES5) {
|
|
2544
6191
|
const nodes = this.store.findNodes({ type: nodeType });
|
|
2545
6192
|
for (const node of nodes) {
|
|
2546
6193
|
if (!visited.has(node.id)) {
|
|
@@ -3386,9 +7033,9 @@ var EntityExtractor = class {
|
|
|
3386
7033
|
extractPaths(trimmed, add) {
|
|
3387
7034
|
const consumed = /* @__PURE__ */ new Set();
|
|
3388
7035
|
for (const match of trimmed.matchAll(FILE_PATH_RE)) {
|
|
3389
|
-
const
|
|
3390
|
-
add(
|
|
3391
|
-
consumed.add(
|
|
7036
|
+
const path13 = match[0];
|
|
7037
|
+
add(path13);
|
|
7038
|
+
consumed.add(path13);
|
|
3392
7039
|
}
|
|
3393
7040
|
return consumed;
|
|
3394
7041
|
}
|
|
@@ -3460,8 +7107,8 @@ var EntityResolver = class {
|
|
|
3460
7107
|
if (isPathLike && node.path.includes(raw)) {
|
|
3461
7108
|
return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
|
|
3462
7109
|
}
|
|
3463
|
-
const
|
|
3464
|
-
if (
|
|
7110
|
+
const basename7 = node.path.split("/").pop() ?? "";
|
|
7111
|
+
if (basename7.includes(raw)) {
|
|
3465
7112
|
return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
|
|
3466
7113
|
}
|
|
3467
7114
|
if (raw.length >= 4 && node.path.includes(raw)) {
|
|
@@ -3540,13 +7187,13 @@ var ResponseFormatter = class {
|
|
|
3540
7187
|
const context = Array.isArray(d?.context) ? d.context : [];
|
|
3541
7188
|
const firstEntity = entities[0];
|
|
3542
7189
|
const nodeType = firstEntity?.node.type ?? "node";
|
|
3543
|
-
const
|
|
7190
|
+
const path13 = firstEntity?.node.path ?? "unknown";
|
|
3544
7191
|
let neighborCount = 0;
|
|
3545
7192
|
const firstContext = context[0];
|
|
3546
7193
|
if (firstContext && Array.isArray(firstContext.nodes)) {
|
|
3547
7194
|
neighborCount = firstContext.nodes.length;
|
|
3548
7195
|
}
|
|
3549
|
-
return `**${entityName}** is a ${nodeType} at \`${
|
|
7196
|
+
return `**${entityName}** is a ${nodeType} at \`${path13}\`. Connected to ${neighborCount} nodes.`;
|
|
3550
7197
|
}
|
|
3551
7198
|
formatAnomaly(data) {
|
|
3552
7199
|
const d = data;
|
|
@@ -3929,7 +7576,7 @@ var PHASE_NODE_TYPES = {
|
|
|
3929
7576
|
debug: ["failure", "learning", "function", "method"],
|
|
3930
7577
|
plan: ["adr", "document", "module", "layer"]
|
|
3931
7578
|
};
|
|
3932
|
-
var
|
|
7579
|
+
var CODE_NODE_TYPES6 = /* @__PURE__ */ new Set([
|
|
3933
7580
|
"file",
|
|
3934
7581
|
"function",
|
|
3935
7582
|
"class",
|
|
@@ -4148,7 +7795,7 @@ var Assembler = class {
|
|
|
4148
7795
|
*/
|
|
4149
7796
|
checkCoverage() {
|
|
4150
7797
|
const codeNodes = [];
|
|
4151
|
-
for (const type of
|
|
7798
|
+
for (const type of CODE_NODE_TYPES6) {
|
|
4152
7799
|
codeNodes.push(...this.store.findNodes({ type }));
|
|
4153
7800
|
}
|
|
4154
7801
|
const documented = [];
|
|
@@ -4267,7 +7914,7 @@ function queryTraceability(store, options) {
|
|
|
4267
7914
|
|
|
4268
7915
|
// src/constraints/GraphConstraintAdapter.ts
|
|
4269
7916
|
import { minimatch } from "minimatch";
|
|
4270
|
-
import { relative as
|
|
7917
|
+
import { relative as relative5 } from "path";
|
|
4271
7918
|
var GraphConstraintAdapter = class {
|
|
4272
7919
|
constructor(store) {
|
|
4273
7920
|
this.store = store;
|
|
@@ -4296,8 +7943,8 @@ var GraphConstraintAdapter = class {
|
|
|
4296
7943
|
const { edges } = this.computeDependencyGraph();
|
|
4297
7944
|
const violations = [];
|
|
4298
7945
|
for (const edge of edges) {
|
|
4299
|
-
const fromRelative =
|
|
4300
|
-
const toRelative =
|
|
7946
|
+
const fromRelative = relative5(rootDir, edge.from).replaceAll("\\", "/");
|
|
7947
|
+
const toRelative = relative5(rootDir, edge.to).replaceAll("\\", "/");
|
|
4301
7948
|
const fromLayer = this.resolveLayer(fromRelative, layers);
|
|
4302
7949
|
const toLayer = this.resolveLayer(toRelative, layers);
|
|
4303
7950
|
if (!fromLayer || !toLayer) continue;
|
|
@@ -4328,14 +7975,14 @@ var GraphConstraintAdapter = class {
|
|
|
4328
7975
|
};
|
|
4329
7976
|
|
|
4330
7977
|
// src/ingest/DesignIngestor.ts
|
|
4331
|
-
import * as
|
|
4332
|
-
import * as
|
|
7978
|
+
import * as fs11 from "fs/promises";
|
|
7979
|
+
import * as path12 from "path";
|
|
4333
7980
|
function isDTCGToken(obj) {
|
|
4334
7981
|
return typeof obj === "object" && obj !== null && "$value" in obj && "$type" in obj;
|
|
4335
7982
|
}
|
|
4336
7983
|
async function readFileOrNull(filePath) {
|
|
4337
7984
|
try {
|
|
4338
|
-
return await
|
|
7985
|
+
return await fs11.readFile(filePath, "utf-8");
|
|
4339
7986
|
} catch {
|
|
4340
7987
|
return null;
|
|
4341
7988
|
}
|
|
@@ -4481,8 +8128,8 @@ var DesignIngestor = class {
|
|
|
4481
8128
|
async ingestAll(designDir) {
|
|
4482
8129
|
const start = Date.now();
|
|
4483
8130
|
const [tokensResult, intentResult] = await Promise.all([
|
|
4484
|
-
this.ingestTokens(
|
|
4485
|
-
this.ingestDesignIntent(
|
|
8131
|
+
this.ingestTokens(path12.join(designDir, "tokens.json")),
|
|
8132
|
+
this.ingestDesignIntent(path12.join(designDir, "DESIGN.md"))
|
|
4486
8133
|
]);
|
|
4487
8134
|
const merged = mergeResults(tokensResult, intentResult);
|
|
4488
8135
|
return { ...merged, durationMs: Date.now() - start };
|
|
@@ -4718,10 +8365,10 @@ var TaskIndependenceAnalyzer = class {
|
|
|
4718
8365
|
includeTypes: ["file"]
|
|
4719
8366
|
});
|
|
4720
8367
|
for (const n of queryResult.nodes) {
|
|
4721
|
-
const
|
|
4722
|
-
if (!fileSet.has(
|
|
4723
|
-
if (!result.has(
|
|
4724
|
-
result.set(
|
|
8368
|
+
const path13 = n.path ?? n.id.replace(/^file:/, "");
|
|
8369
|
+
if (!fileSet.has(path13)) {
|
|
8370
|
+
if (!result.has(path13)) {
|
|
8371
|
+
result.set(path13, file);
|
|
4725
8372
|
}
|
|
4726
8373
|
}
|
|
4727
8374
|
}
|
|
@@ -5098,9 +8745,12 @@ var ConflictPredictor = class {
|
|
|
5098
8745
|
};
|
|
5099
8746
|
|
|
5100
8747
|
// src/index.ts
|
|
5101
|
-
var VERSION = "0.4.
|
|
8748
|
+
var VERSION = "0.4.3";
|
|
5102
8749
|
export {
|
|
8750
|
+
ALL_EXTRACTORS,
|
|
8751
|
+
ApiPathExtractor,
|
|
5103
8752
|
Assembler,
|
|
8753
|
+
BusinessKnowledgeIngestor,
|
|
5104
8754
|
CIConnector,
|
|
5105
8755
|
CURRENT_SCHEMA_VERSION,
|
|
5106
8756
|
CascadeSimulator,
|
|
@@ -5109,11 +8759,18 @@ export {
|
|
|
5109
8759
|
ConflictPredictor,
|
|
5110
8760
|
ConfluenceConnector,
|
|
5111
8761
|
ContextQL,
|
|
8762
|
+
ContradictionDetector,
|
|
8763
|
+
CoverageScorer,
|
|
8764
|
+
D2Parser,
|
|
5112
8765
|
DesignConstraintAdapter,
|
|
5113
8766
|
DesignIngestor,
|
|
8767
|
+
DiagramParser,
|
|
5114
8768
|
EDGE_TYPES,
|
|
5115
8769
|
EntityExtractor,
|
|
5116
8770
|
EntityResolver,
|
|
8771
|
+
EnumConstantExtractor,
|
|
8772
|
+
ExtractionRunner,
|
|
8773
|
+
FigmaConnector,
|
|
5117
8774
|
FusionLayer,
|
|
5118
8775
|
GitIngestor,
|
|
5119
8776
|
GraphAnomalyAdapter,
|
|
@@ -5126,24 +8783,38 @@ export {
|
|
|
5126
8783
|
GraphNodeSchema,
|
|
5127
8784
|
GraphStore,
|
|
5128
8785
|
INTENTS,
|
|
8786
|
+
ImageAnalysisExtractor,
|
|
5129
8787
|
IntentClassifier,
|
|
5130
8788
|
JiraConnector,
|
|
5131
8789
|
KnowledgeIngestor,
|
|
8790
|
+
KnowledgePipelineRunner,
|
|
8791
|
+
KnowledgeStagingAggregator,
|
|
8792
|
+
MermaidParser,
|
|
8793
|
+
MiroConnector,
|
|
8794
|
+
NODE_STABILITY,
|
|
5132
8795
|
NODE_TYPES,
|
|
5133
8796
|
OBSERVABILITY_TYPES,
|
|
8797
|
+
PackedSummaryCache,
|
|
8798
|
+
PlantUmlParser,
|
|
5134
8799
|
RequirementIngestor,
|
|
5135
8800
|
ResponseFormatter,
|
|
5136
8801
|
SlackConnector,
|
|
8802
|
+
StructuralDriftDetector,
|
|
5137
8803
|
SyncManager,
|
|
5138
8804
|
TaskIndependenceAnalyzer,
|
|
8805
|
+
TestDescriptionExtractor,
|
|
5139
8806
|
TopologicalLinker,
|
|
5140
8807
|
VERSION,
|
|
8808
|
+
ValidationRuleExtractor,
|
|
5141
8809
|
VectorStore,
|
|
5142
8810
|
askGraph,
|
|
5143
8811
|
classifyNodeCategory,
|
|
8812
|
+
createExtractionRunner,
|
|
8813
|
+
detectLanguage,
|
|
5144
8814
|
groupNodesByImpact,
|
|
5145
8815
|
linkToCode,
|
|
5146
8816
|
loadGraph,
|
|
8817
|
+
normalizeIntent,
|
|
5147
8818
|
project,
|
|
5148
8819
|
queryTraceability,
|
|
5149
8820
|
saveGraph
|