@danielblomma/cortex-mcp 0.4.2 → 0.6.4
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 +64 -16
- package/bin/cortex.mjs +32 -60
- package/package.json +17 -3
- package/scaffold/.context/ontology.cypher +47 -0
- package/scaffold/.githooks/post-commit +14 -0
- package/scaffold/.githooks/post-rewrite +23 -0
- package/scaffold/mcp/package-lock.json +19 -23
- package/scaffold/mcp/package.json +3 -1
- package/scaffold/mcp/src/contextEntities.ts +311 -0
- package/scaffold/mcp/src/defaults.ts +6 -0
- package/scaffold/mcp/src/embed.ts +163 -37
- package/scaffold/mcp/src/frontmatter.ts +39 -0
- package/scaffold/mcp/src/graph.ts +330 -109
- package/scaffold/mcp/src/graphMetrics.ts +12 -0
- package/scaffold/mcp/src/impactPresentation.ts +202 -0
- package/scaffold/mcp/src/impactRanking.ts +237 -0
- package/scaffold/mcp/src/impactResponse.ts +47 -0
- package/scaffold/mcp/src/impactResults.ts +173 -0
- package/scaffold/mcp/src/impactSeed.ts +33 -0
- package/scaffold/mcp/src/impactTraversal.ts +83 -0
- package/scaffold/mcp/src/jsonl.ts +34 -0
- package/scaffold/mcp/src/loadGraph.ts +345 -86
- package/scaffold/mcp/src/paths.ts +24 -2
- package/scaffold/mcp/src/presets.ts +137 -0
- package/scaffold/mcp/src/relatedResponse.ts +30 -0
- package/scaffold/mcp/src/relatedTraversal.ts +101 -0
- package/scaffold/mcp/src/rules.ts +27 -0
- package/scaffold/mcp/src/search.ts +191 -355
- package/scaffold/mcp/src/searchCore.ts +274 -0
- package/scaffold/mcp/src/searchResults.ts +133 -0
- package/scaffold/mcp/src/server.ts +95 -3
- package/scaffold/mcp/src/types.ts +99 -3
- package/scaffold/scripts/context.sh +12 -46
- package/scaffold/scripts/dashboard.mjs +797 -0
- package/scaffold/scripts/dashboard.sh +13 -0
- package/scaffold/scripts/ingest.mjs +2219 -59
- package/scaffold/scripts/install-git-hooks.sh +3 -1
- package/scaffold/scripts/memory-compile.mjs +232 -0
- package/scaffold/scripts/memory-compile.sh +20 -0
- package/scaffold/scripts/memory-lint.mjs +375 -0
- package/scaffold/scripts/memory-lint.sh +20 -0
- package/scaffold/scripts/parsers/config.mjs +178 -0
- package/scaffold/scripts/parsers/cpp.mjs +316 -0
- package/scaffold/scripts/parsers/dotnet/VbNetParser/Program.cs +374 -0
- package/scaffold/scripts/parsers/dotnet/VbNetParser/VbNetParser.csproj +13 -0
- package/scaffold/scripts/parsers/javascript/ast.mjs +61 -0
- package/scaffold/scripts/parsers/javascript/calls.mjs +53 -0
- package/scaffold/scripts/parsers/javascript/chunks.mjs +388 -0
- package/scaffold/scripts/parsers/javascript/imports.mjs +162 -0
- package/scaffold/scripts/parsers/javascript/patterns.mjs +82 -0
- package/scaffold/scripts/parsers/javascript/scope-analysis.mjs +3 -0
- package/scaffold/scripts/parsers/javascript/scope-builder.mjs +305 -0
- package/scaffold/scripts/parsers/javascript/scope-resolver.mjs +82 -0
- package/scaffold/scripts/parsers/javascript.mjs +27 -350
- package/scaffold/scripts/parsers/resources.mjs +166 -0
- package/scaffold/scripts/parsers/sql.mjs +137 -0
- package/scaffold/scripts/parsers/vbnet.mjs +143 -0
- package/scaffold/scripts/status.sh +15 -8
- package/scaffold/scripts/capture-note.sh +0 -55
- package/scaffold/scripts/plan-state-engine.cjs +0 -310
- package/scaffold/scripts/plan-state.sh +0 -71
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import ryugraph, { type Connection, type Database, type QueryResult } from "ryugraph";
|
|
4
|
+
import { readJsonl, asString, asNumber, asBoolean } from "./jsonl.js";
|
|
4
5
|
import { DB_PATH, DEFAULT_RANKING, PATHS } from "./paths.js";
|
|
5
6
|
import type {
|
|
6
7
|
AdrRecord,
|
|
8
|
+
ChunkRecord,
|
|
7
9
|
ContextData,
|
|
8
10
|
DocumentRecord,
|
|
9
11
|
JsonObject,
|
|
10
|
-
|
|
12
|
+
ModuleRecord,
|
|
13
|
+
ProjectRecord,
|
|
11
14
|
RankingWeights,
|
|
12
15
|
RelationRecord,
|
|
16
|
+
RelationType,
|
|
13
17
|
RuleRecord,
|
|
14
18
|
UnknownRow
|
|
15
19
|
} from "./types.js";
|
|
@@ -30,6 +34,33 @@ let ryuLastInitAttemptAt = 0;
|
|
|
30
34
|
let ryuGraphSignature: string | null = null;
|
|
31
35
|
|
|
32
36
|
const RYU_INIT_RETRY_INTERVAL_MS = 2000;
|
|
37
|
+
const REQUIRED_GRAPH_MANIFEST_COUNT_KEYS = [
|
|
38
|
+
"files",
|
|
39
|
+
"rules",
|
|
40
|
+
"adrs",
|
|
41
|
+
"chunks",
|
|
42
|
+
"constrains",
|
|
43
|
+
"implements",
|
|
44
|
+
"supersedes",
|
|
45
|
+
"defines",
|
|
46
|
+
"calls",
|
|
47
|
+
"imports",
|
|
48
|
+
"calls_sql",
|
|
49
|
+
"uses_config_key",
|
|
50
|
+
"uses_resource_key",
|
|
51
|
+
"uses_setting_key",
|
|
52
|
+
"modules",
|
|
53
|
+
"projects",
|
|
54
|
+
"contains",
|
|
55
|
+
"contains_module",
|
|
56
|
+
"exports",
|
|
57
|
+
"includes_file",
|
|
58
|
+
"references_project",
|
|
59
|
+
"uses_resource",
|
|
60
|
+
"uses_setting",
|
|
61
|
+
"uses_config",
|
|
62
|
+
"transforms_config"
|
|
63
|
+
] as const;
|
|
33
64
|
|
|
34
65
|
function readFileIfExists(filePath: string): string | null {
|
|
35
66
|
if (!fs.existsSync(filePath)) {
|
|
@@ -38,38 +69,6 @@ function readFileIfExists(filePath: string): string | null {
|
|
|
38
69
|
return fs.readFileSync(filePath, "utf8");
|
|
39
70
|
}
|
|
40
71
|
|
|
41
|
-
function readJsonl(filePath: string): JsonObject[] {
|
|
42
|
-
const raw = readFileIfExists(filePath);
|
|
43
|
-
if (!raw) {
|
|
44
|
-
return [];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return raw
|
|
48
|
-
.split(/\r?\n/)
|
|
49
|
-
.map((line) => line.trim())
|
|
50
|
-
.filter(Boolean)
|
|
51
|
-
.map((line) => {
|
|
52
|
-
try {
|
|
53
|
-
return JSON.parse(line) as JsonObject;
|
|
54
|
-
} catch {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
})
|
|
58
|
-
.filter((value): value is JsonObject => value !== null);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function asString(value: JsonValue | undefined, fallback = ""): string {
|
|
62
|
-
return typeof value === "string" ? value : fallback;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function asNumber(value: JsonValue | undefined, fallback = 0): number {
|
|
66
|
-
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function asBoolean(value: JsonValue | undefined, fallback = false): boolean {
|
|
70
|
-
return typeof value === "boolean" ? value : fallback;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
72
|
function asStringUnknown(value: unknown, fallback = ""): string {
|
|
74
73
|
if (typeof value === "string") return value;
|
|
75
74
|
if (typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") {
|
|
@@ -148,6 +147,85 @@ function parseAdrs(raw: JsonObject[]): AdrRecord[] {
|
|
|
148
147
|
.filter((item): item is AdrRecord => item !== null);
|
|
149
148
|
}
|
|
150
149
|
|
|
150
|
+
function parseChunkEntities(raw: JsonObject[]): ChunkRecord[] {
|
|
151
|
+
return raw
|
|
152
|
+
.map((item) => {
|
|
153
|
+
const id = asString(item.id);
|
|
154
|
+
if (!id) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
id,
|
|
160
|
+
file_id: asString(item.file_id),
|
|
161
|
+
name: asString(item.name),
|
|
162
|
+
kind: asString(item.kind, "chunk"),
|
|
163
|
+
signature: asString(item.signature),
|
|
164
|
+
body: asString(item.body),
|
|
165
|
+
description: asString(item.description),
|
|
166
|
+
start_line: asNumber(item.start_line),
|
|
167
|
+
end_line: asNumber(item.end_line),
|
|
168
|
+
language: asString(item.language),
|
|
169
|
+
exported: asBoolean(item.exported, false),
|
|
170
|
+
updated_at: asString(item.updated_at),
|
|
171
|
+
source_of_truth: asBoolean(item.source_of_truth, false),
|
|
172
|
+
trust_level: asNumber(item.trust_level, 60),
|
|
173
|
+
status: asString(item.status, "active")
|
|
174
|
+
};
|
|
175
|
+
})
|
|
176
|
+
.filter((item): item is ChunkRecord => item !== null);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function parseModuleEntities(raw: JsonObject[]): ModuleRecord[] {
|
|
180
|
+
return raw
|
|
181
|
+
.map((item) => {
|
|
182
|
+
const id = asString(item.id);
|
|
183
|
+
if (!id) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
id,
|
|
189
|
+
path: asString(item.path),
|
|
190
|
+
name: asString(item.name),
|
|
191
|
+
summary: asString(item.summary),
|
|
192
|
+
file_count: asNumber(item.file_count, 0),
|
|
193
|
+
exported_symbols: asString(item.exported_symbols),
|
|
194
|
+
updated_at: asString(item.updated_at),
|
|
195
|
+
source_of_truth: asBoolean(item.source_of_truth, false),
|
|
196
|
+
trust_level: asNumber(item.trust_level, 75),
|
|
197
|
+
status: asString(item.status, "active")
|
|
198
|
+
};
|
|
199
|
+
})
|
|
200
|
+
.filter((item): item is ModuleRecord => item !== null);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function parseProjectEntities(raw: JsonObject[]): ProjectRecord[] {
|
|
204
|
+
return raw
|
|
205
|
+
.map((item) => {
|
|
206
|
+
const id = asString(item.id);
|
|
207
|
+
if (!id) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
id,
|
|
213
|
+
path: asString(item.path),
|
|
214
|
+
name: asString(item.name),
|
|
215
|
+
kind: asString(item.kind, "project"),
|
|
216
|
+
language: asString(item.language, "dotnet"),
|
|
217
|
+
target_framework: asString(item.target_framework),
|
|
218
|
+
summary: asString(item.summary),
|
|
219
|
+
file_count: asNumber(item.file_count, 0),
|
|
220
|
+
updated_at: asString(item.updated_at),
|
|
221
|
+
source_of_truth: asBoolean(item.source_of_truth, false),
|
|
222
|
+
trust_level: asNumber(item.trust_level, 80),
|
|
223
|
+
status: asString(item.status, "active")
|
|
224
|
+
};
|
|
225
|
+
})
|
|
226
|
+
.filter((item): item is ProjectRecord => item !== null);
|
|
227
|
+
}
|
|
228
|
+
|
|
151
229
|
function parseRuleEntities(raw: JsonObject[]): RuleRecord[] {
|
|
152
230
|
return raw
|
|
153
231
|
.map((item) => {
|
|
@@ -243,7 +321,11 @@ function parseRulesYaml(yamlText: string | null): RuleRecord[] {
|
|
|
243
321
|
return rules;
|
|
244
322
|
}
|
|
245
323
|
|
|
246
|
-
function parseRelations(
|
|
324
|
+
function parseRelations(
|
|
325
|
+
raw: JsonObject[],
|
|
326
|
+
relation: RelationType,
|
|
327
|
+
noteFields: string[] = ["note", "reason"]
|
|
328
|
+
): RelationRecord[] {
|
|
247
329
|
return raw
|
|
248
330
|
.map((item) => {
|
|
249
331
|
const from = asString(item.from);
|
|
@@ -252,11 +334,20 @@ function parseRelations(raw: JsonObject[], relation: RelationRecord["relation"])
|
|
|
252
334
|
return null;
|
|
253
335
|
}
|
|
254
336
|
|
|
337
|
+
let note = "";
|
|
338
|
+
for (const fieldName of noteFields) {
|
|
339
|
+
const candidate = asString(item[fieldName]);
|
|
340
|
+
if (candidate) {
|
|
341
|
+
note = candidate;
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
255
346
|
return {
|
|
256
347
|
from,
|
|
257
348
|
to,
|
|
258
349
|
relation,
|
|
259
|
-
note
|
|
350
|
+
note
|
|
260
351
|
};
|
|
261
352
|
})
|
|
262
353
|
.filter((item): item is RelationRecord => item !== null);
|
|
@@ -338,6 +429,28 @@ function buildMissingDbMessage(): string {
|
|
|
338
429
|
return `RyuGraph DB not found at ${DB_PATH}. Run ${loadCommand} (or ${bootstrapCommand} on cold start).`;
|
|
339
430
|
}
|
|
340
431
|
|
|
432
|
+
function buildIncompatibleGraphMessage(missingKeys: string[]): string {
|
|
433
|
+
const loadCommand = "./scripts/context.sh graph-load";
|
|
434
|
+
const missing = missingKeys.join(", ");
|
|
435
|
+
return `RyuGraph manifest is missing schema keys (${missing}). Run ${loadCommand} to rebuild the graph DB.`;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function readGraphManifestMissingKeys(): string[] {
|
|
439
|
+
if (!fs.existsSync(PATHS.graphManifest)) {
|
|
440
|
+
return [...REQUIRED_GRAPH_MANIFEST_COUNT_KEYS];
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
const raw = JSON.parse(fs.readFileSync(PATHS.graphManifest, "utf8")) as {
|
|
445
|
+
counts?: Record<string, unknown>;
|
|
446
|
+
};
|
|
447
|
+
const counts = raw.counts ?? {};
|
|
448
|
+
return REQUIRED_GRAPH_MANIFEST_COUNT_KEYS.filter((key) => !(key in counts));
|
|
449
|
+
} catch {
|
|
450
|
+
return [...REQUIRED_GRAPH_MANIFEST_COUNT_KEYS];
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
341
454
|
async function closeRyuGraphResources(): Promise<void> {
|
|
342
455
|
const currentConnection = ryuConnection;
|
|
343
456
|
const currentDb = ryuDb;
|
|
@@ -394,6 +507,12 @@ async function getRyuGraphConnection(forceReload = false): Promise<Connection |
|
|
|
394
507
|
return null;
|
|
395
508
|
}
|
|
396
509
|
|
|
510
|
+
const missingManifestKeys = readGraphManifestMissingKeys();
|
|
511
|
+
if (missingManifestKeys.length > 0) {
|
|
512
|
+
await resetRyuGraphState(buildIncompatibleGraphMessage(missingManifestKeys));
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
|
|
397
516
|
try {
|
|
398
517
|
const nextDb = new ryugraph.Database(DB_PATH, undefined, undefined, true);
|
|
399
518
|
const nextConnection = new ryugraph.Connection(nextDb);
|
|
@@ -483,9 +602,88 @@ function parseRyuGraphAdrs(rows: UnknownRow[]): AdrRecord[] {
|
|
|
483
602
|
.filter((value): value is AdrRecord => value !== null);
|
|
484
603
|
}
|
|
485
604
|
|
|
605
|
+
function parseRyuGraphChunks(rows: UnknownRow[]): ChunkRecord[] {
|
|
606
|
+
return rows
|
|
607
|
+
.map((row) => {
|
|
608
|
+
const id = asStringUnknown(row.id);
|
|
609
|
+
if (!id) {
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return {
|
|
614
|
+
id,
|
|
615
|
+
file_id: asStringUnknown(row.file_id),
|
|
616
|
+
name: asStringUnknown(row.name),
|
|
617
|
+
kind: asStringUnknown(row.kind, "chunk"),
|
|
618
|
+
signature: asStringUnknown(row.signature),
|
|
619
|
+
body: asStringUnknown(row.body),
|
|
620
|
+
description: asStringUnknown(row.description),
|
|
621
|
+
start_line: asNumberUnknown(row.start_line),
|
|
622
|
+
end_line: asNumberUnknown(row.end_line),
|
|
623
|
+
language: asStringUnknown(row.language),
|
|
624
|
+
exported: asBooleanUnknown(row.exported, false),
|
|
625
|
+
updated_at: asStringUnknown(row.updated_at),
|
|
626
|
+
source_of_truth: asBooleanUnknown(row.source_of_truth, false),
|
|
627
|
+
trust_level: asNumberUnknown(row.trust_level, 60),
|
|
628
|
+
status: asStringUnknown(row.status, "active")
|
|
629
|
+
};
|
|
630
|
+
})
|
|
631
|
+
.filter((value): value is ChunkRecord => value !== null);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function parseRyuGraphModules(rows: UnknownRow[]): ModuleRecord[] {
|
|
635
|
+
return rows
|
|
636
|
+
.map((row) => {
|
|
637
|
+
const id = asStringUnknown(row.id);
|
|
638
|
+
if (!id) {
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return {
|
|
643
|
+
id,
|
|
644
|
+
path: asStringUnknown(row.path),
|
|
645
|
+
name: asStringUnknown(row.name),
|
|
646
|
+
summary: asStringUnknown(row.summary),
|
|
647
|
+
file_count: asNumberUnknown(row.file_count, 0),
|
|
648
|
+
exported_symbols: asStringUnknown(row.exported_symbols),
|
|
649
|
+
updated_at: asStringUnknown(row.updated_at),
|
|
650
|
+
source_of_truth: asBooleanUnknown(row.source_of_truth, false),
|
|
651
|
+
trust_level: asNumberUnknown(row.trust_level, 75),
|
|
652
|
+
status: asStringUnknown(row.status, "active")
|
|
653
|
+
};
|
|
654
|
+
})
|
|
655
|
+
.filter((value): value is ModuleRecord => value !== null);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function parseRyuGraphProjects(rows: UnknownRow[]): ProjectRecord[] {
|
|
659
|
+
return rows
|
|
660
|
+
.map((row) => {
|
|
661
|
+
const id = asStringUnknown(row.id);
|
|
662
|
+
if (!id) {
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return {
|
|
667
|
+
id,
|
|
668
|
+
path: asStringUnknown(row.path),
|
|
669
|
+
name: asStringUnknown(row.name),
|
|
670
|
+
kind: asStringUnknown(row.kind, "project"),
|
|
671
|
+
language: asStringUnknown(row.language, "dotnet"),
|
|
672
|
+
target_framework: asStringUnknown(row.target_framework),
|
|
673
|
+
summary: asStringUnknown(row.summary),
|
|
674
|
+
file_count: asNumberUnknown(row.file_count, 0),
|
|
675
|
+
updated_at: asStringUnknown(row.updated_at),
|
|
676
|
+
source_of_truth: asBooleanUnknown(row.source_of_truth, false),
|
|
677
|
+
trust_level: asNumberUnknown(row.trust_level, 80),
|
|
678
|
+
status: asStringUnknown(row.status, "active")
|
|
679
|
+
};
|
|
680
|
+
})
|
|
681
|
+
.filter((value): value is ProjectRecord => value !== null);
|
|
682
|
+
}
|
|
683
|
+
|
|
486
684
|
function parseRyuGraphRelations(
|
|
487
685
|
rows: UnknownRow[],
|
|
488
|
-
relation:
|
|
686
|
+
relation: RelationType,
|
|
489
687
|
noteField: string
|
|
490
688
|
): RelationRecord[] {
|
|
491
689
|
return rows
|
|
@@ -509,10 +707,38 @@ export async function loadContextData(): Promise<ContextData> {
|
|
|
509
707
|
const ranking = parseRankingFromConfig(readFileIfExists(PATHS.config));
|
|
510
708
|
const cachedDocuments = parseDocuments(readJsonl(PATHS.documents));
|
|
511
709
|
const cachedAdrs = parseAdrs(readJsonl(PATHS.adrEntities));
|
|
710
|
+
const cachedChunks = parseChunkEntities(readJsonl(PATHS.chunkEntities));
|
|
711
|
+
const cachedModules = parseModuleEntities(readJsonl(PATHS.moduleEntities));
|
|
712
|
+
const cachedProjects = parseProjectEntities(readJsonl(PATHS.projectEntities));
|
|
713
|
+
const cachedChunkRelations = [
|
|
714
|
+
...parseRelations(readJsonl(PATHS.definesRelations), "DEFINES"),
|
|
715
|
+
...parseRelations(readJsonl(PATHS.callsRelations), "CALLS", ["call_type"]),
|
|
716
|
+
...parseRelations(readJsonl(PATHS.importsRelations), "IMPORTS", ["import_name"]),
|
|
717
|
+
...parseRelations(readJsonl(PATHS.callsSqlRelations), "CALLS_SQL"),
|
|
718
|
+
...parseRelations(readJsonl(PATHS.usesConfigKeyRelations), "USES_CONFIG_KEY"),
|
|
719
|
+
...parseRelations(readJsonl(PATHS.usesResourceKeyRelations), "USES_RESOURCE_KEY"),
|
|
720
|
+
...parseRelations(readJsonl(PATHS.usesSettingKeyRelations), "USES_SETTING_KEY")
|
|
721
|
+
];
|
|
722
|
+
const cachedModuleRelations = [
|
|
723
|
+
...parseRelations(readJsonl(PATHS.containsRelations), "CONTAINS"),
|
|
724
|
+
...parseRelations(readJsonl(PATHS.containsModuleRelations), "CONTAINS_MODULE"),
|
|
725
|
+
...parseRelations(readJsonl(PATHS.exportsRelations), "EXPORTS")
|
|
726
|
+
];
|
|
727
|
+
const cachedProjectRelations = [
|
|
728
|
+
...parseRelations(readJsonl(PATHS.includesFileRelations), "INCLUDES_FILE"),
|
|
729
|
+
...parseRelations(readJsonl(PATHS.referencesProjectRelations), "REFERENCES_PROJECT"),
|
|
730
|
+
...parseRelations(readJsonl(PATHS.usesResourceRelations), "USES_RESOURCE"),
|
|
731
|
+
...parseRelations(readJsonl(PATHS.usesSettingRelations), "USES_SETTING"),
|
|
732
|
+
...parseRelations(readJsonl(PATHS.usesConfigRelations), "USES_CONFIG"),
|
|
733
|
+
...parseRelations(readJsonl(PATHS.transformsConfigRelations), "TRANSFORMS_CONFIG")
|
|
734
|
+
];
|
|
512
735
|
const cachedRelations = [
|
|
513
736
|
...parseRelations(readJsonl(PATHS.constrainsRelations), "CONSTRAINS"),
|
|
514
737
|
...parseRelations(readJsonl(PATHS.implementsRelations), "IMPLEMENTS"),
|
|
515
|
-
...parseRelations(readJsonl(PATHS.supersedesRelations), "SUPERSEDES")
|
|
738
|
+
...parseRelations(readJsonl(PATHS.supersedesRelations), "SUPERSEDES"),
|
|
739
|
+
...cachedChunkRelations,
|
|
740
|
+
...cachedModuleRelations,
|
|
741
|
+
...cachedProjectRelations
|
|
516
742
|
];
|
|
517
743
|
|
|
518
744
|
const yamlRules = parseRulesYaml(readFileIfExists(PATHS.rulesYaml));
|
|
@@ -525,6 +751,9 @@ export async function loadContextData(): Promise<ContextData> {
|
|
|
525
751
|
documents: cachedDocuments,
|
|
526
752
|
adrs: cachedAdrs,
|
|
527
753
|
rules: cachedRules,
|
|
754
|
+
chunks: cachedChunks,
|
|
755
|
+
modules: cachedModules,
|
|
756
|
+
projects: cachedProjects,
|
|
528
757
|
relations: cachedRelations,
|
|
529
758
|
ranking,
|
|
530
759
|
source: "cache",
|
|
@@ -533,93 +762,82 @@ export async function loadContextData(): Promise<ContextData> {
|
|
|
533
762
|
}
|
|
534
763
|
|
|
535
764
|
try {
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
MATCH (a:ADR)
|
|
573
|
-
RETURN
|
|
574
|
-
a.id AS id,
|
|
575
|
-
a.path AS path,
|
|
576
|
-
a.title AS title,
|
|
577
|
-
a.body AS body,
|
|
578
|
-
a.decision_date AS decision_date,
|
|
579
|
-
a.supersedes_id AS supersedes_id,
|
|
580
|
-
a.source_of_truth AS source_of_truth,
|
|
581
|
-
a.trust_level AS trust_level,
|
|
582
|
-
a.status AS status;
|
|
583
|
-
`
|
|
584
|
-
),
|
|
585
|
-
queryRows(
|
|
586
|
-
connection,
|
|
587
|
-
`
|
|
588
|
-
MATCH (r:Rule)-[c:CONSTRAINS]->(f:File)
|
|
589
|
-
RETURN r.id AS from, f.id AS to, c.note AS note;
|
|
590
|
-
`
|
|
591
|
-
),
|
|
592
|
-
queryRows(
|
|
593
|
-
connection,
|
|
594
|
-
`
|
|
595
|
-
MATCH (f:File)-[i:IMPLEMENTS]->(r:Rule)
|
|
596
|
-
RETURN f.id AS from, r.id AS to, i.note AS note;
|
|
597
|
-
`
|
|
598
|
-
),
|
|
599
|
-
queryRows(
|
|
600
|
-
connection,
|
|
601
|
-
`
|
|
602
|
-
MATCH (a1:ADR)-[s:SUPERSEDES]->(a2:ADR)
|
|
603
|
-
RETURN a1.id AS from, a2.id AS to, s.reason AS note;
|
|
604
|
-
`
|
|
605
|
-
)
|
|
606
|
-
]);
|
|
765
|
+
const ryuQueries = await Promise.all([
|
|
766
|
+
queryRows(connection, `MATCH (f:File) RETURN f.id AS id, f.path AS path, f.kind AS kind, f.excerpt AS excerpt, f.updated_at AS updated_at, f.source_of_truth AS source_of_truth, f.trust_level AS trust_level, f.status AS status;`),
|
|
767
|
+
queryRows(connection, `MATCH (r:Rule) RETURN r.id AS id, r.title AS title, r.body AS body, r.scope AS scope, r.priority AS priority, r.updated_at AS updated_at, r.source_of_truth AS source_of_truth, r.trust_level AS trust_level, r.status AS status;`),
|
|
768
|
+
queryRows(connection, `MATCH (a:ADR) RETURN a.id AS id, a.path AS path, a.title AS title, a.body AS body, a.decision_date AS decision_date, a.supersedes_id AS supersedes_id, a.source_of_truth AS source_of_truth, a.trust_level AS trust_level, a.status AS status;`),
|
|
769
|
+
queryRows(connection, `MATCH (c:Chunk) RETURN c.id AS id, c.file_id AS file_id, c.name AS name, c.kind AS kind, c.signature AS signature, c.body AS body, c.description AS description, c.start_line AS start_line, c.end_line AS end_line, c.language AS language, c.exported AS exported, c.updated_at AS updated_at, c.source_of_truth AS source_of_truth, c.trust_level AS trust_level, c.status AS status;`),
|
|
770
|
+
queryRows(connection, `MATCH (m:Module) RETURN m.id AS id, m.path AS path, m.name AS name, m.summary AS summary, m.file_count AS file_count, m.exported_symbols AS exported_symbols, m.updated_at AS updated_at, m.source_of_truth AS source_of_truth, m.trust_level AS trust_level, m.status AS status;`),
|
|
771
|
+
queryRows(connection, `MATCH (p:Project) RETURN p.id AS id, p.path AS path, p.name AS name, p.kind AS kind, p.language AS language, p.target_framework AS target_framework, p.summary AS summary, p.file_count AS file_count, p.updated_at AS updated_at, p.source_of_truth AS source_of_truth, p.trust_level AS trust_level, p.status AS status;`),
|
|
772
|
+
queryRows(connection, `MATCH (r:Rule)-[c:CONSTRAINS]->(f:File) RETURN r.id AS from, f.id AS to, c.note AS note;`),
|
|
773
|
+
queryRows(connection, `MATCH (f:File)-[i:IMPLEMENTS]->(r:Rule) RETURN f.id AS from, r.id AS to, i.note AS note;`),
|
|
774
|
+
queryRows(connection, `MATCH (a1:ADR)-[s:SUPERSEDES]->(a2:ADR) RETURN a1.id AS from, a2.id AS to, s.reason AS note;`),
|
|
775
|
+
queryRows(connection, `MATCH (f:File)-[:DEFINES]->(c:Chunk) RETURN f.id AS from, c.id AS to;`),
|
|
776
|
+
queryRows(connection, `MATCH (c1:Chunk)-[ca:CALLS]->(c2:Chunk) RETURN c1.id AS from, c2.id AS to, ca.call_type AS call_type;`),
|
|
777
|
+
queryRows(connection, `MATCH (c:Chunk)-[im:IMPORTS]->(f:File) RETURN c.id AS from, f.id AS to, im.import_name AS import_name;`),
|
|
778
|
+
queryRows(connection, `MATCH (f:File)-[cs:CALLS_SQL]->(c:Chunk) RETURN f.id AS from, c.id AS to, cs.note AS note;`),
|
|
779
|
+
queryRows(connection, `MATCH (f:File)-[uck:USES_CONFIG_KEY]->(c:Chunk) RETURN f.id AS from, c.id AS to, uck.note AS note;`),
|
|
780
|
+
queryRows(connection, `MATCH (f:File)-[urk:USES_RESOURCE_KEY]->(c:Chunk) RETURN f.id AS from, c.id AS to, urk.note AS note;`),
|
|
781
|
+
queryRows(connection, `MATCH (f:File)-[usk:USES_SETTING_KEY]->(c:Chunk) RETURN f.id AS from, c.id AS to, usk.note AS note;`),
|
|
782
|
+
queryRows(connection, `MATCH (m:Module)-[:CONTAINS]->(f:File) RETURN m.id AS from, f.id AS to;`),
|
|
783
|
+
queryRows(connection, `MATCH (m1:Module)-[:CONTAINS_MODULE]->(m2:Module) RETURN m1.id AS from, m2.id AS to;`),
|
|
784
|
+
queryRows(connection, `MATCH (m:Module)-[:EXPORTS]->(c:Chunk) RETURN m.id AS from, c.id AS to;`),
|
|
785
|
+
queryRows(connection, `MATCH (p:Project)-[:INCLUDES_FILE]->(f:File) RETURN p.id AS from, f.id AS to;`),
|
|
786
|
+
queryRows(connection, `MATCH (p1:Project)-[rp:REFERENCES_PROJECT]->(p2:Project) RETURN p1.id AS from, p2.id AS to, rp.note AS note;`),
|
|
787
|
+
queryRows(connection, `MATCH (f1:File)-[ur:USES_RESOURCE]->(f2:File) RETURN f1.id AS from, f2.id AS to, ur.note AS note;`),
|
|
788
|
+
queryRows(connection, `MATCH (f1:File)-[us:USES_SETTING]->(f2:File) RETURN f1.id AS from, f2.id AS to, us.note AS note;`),
|
|
789
|
+
queryRows(connection, `MATCH (f1:File)-[uc:USES_CONFIG]->(f2:File) RETURN f1.id AS from, f2.id AS to, uc.note AS note;`),
|
|
790
|
+
queryRows(connection, `MATCH (f1:File)-[tc:TRANSFORMS_CONFIG]->(f2:File) RETURN f1.id AS from, f2.id AS to, tc.note AS note;`)
|
|
791
|
+
]);
|
|
792
|
+
|
|
793
|
+
// Named destructuring to avoid positional misalignment with 14 parallel queries
|
|
794
|
+
const [
|
|
795
|
+
fileRows, ruleRows, adrRows, chunkRows, moduleRows, projectRows, // entities
|
|
796
|
+
constrainsRows, implementsRows, supersedesRows, // core relations
|
|
797
|
+
definesRows, callsRows, importsRows, callsSqlRows, usesConfigKeyRows, usesResourceKeyRows, usesSettingKeyRows, // chunk relations
|
|
798
|
+
containsRows, containsModuleRows, exportsRows, // module relations
|
|
799
|
+
includesFileRows, referencesProjectRows, usesResourceRows, usesSettingRows, usesConfigRows, transformsConfigRows // project/file relations
|
|
800
|
+
] = ryuQueries;
|
|
607
801
|
|
|
608
802
|
const contentById = new Map(cachedDocuments.map((doc) => [doc.id, doc.content]));
|
|
609
803
|
|
|
610
804
|
const ryuDocuments = parseRyuGraphDocuments(fileRows, contentById);
|
|
611
805
|
const ryuRules = parseRyuGraphRules(ruleRows);
|
|
612
806
|
const ryuAdrs = parseRyuGraphAdrs(adrRows);
|
|
807
|
+
const ryuChunks = parseRyuGraphChunks(chunkRows);
|
|
808
|
+
const ryuModules = parseRyuGraphModules(moduleRows);
|
|
809
|
+
const ryuProjects = parseRyuGraphProjects(projectRows);
|
|
613
810
|
const ryuRelations = [
|
|
614
811
|
...parseRyuGraphRelations(constrainsRows, "CONSTRAINS", "note"),
|
|
615
812
|
...parseRyuGraphRelations(implementsRows, "IMPLEMENTS", "note"),
|
|
616
|
-
...parseRyuGraphRelations(supersedesRows, "SUPERSEDES", "note")
|
|
813
|
+
...parseRyuGraphRelations(supersedesRows, "SUPERSEDES", "note"),
|
|
814
|
+
...parseRyuGraphRelations(definesRows, "DEFINES", "note"),
|
|
815
|
+
...parseRyuGraphRelations(callsRows, "CALLS", "call_type"),
|
|
816
|
+
...parseRyuGraphRelations(importsRows, "IMPORTS", "import_name"),
|
|
817
|
+
...parseRyuGraphRelations(callsSqlRows, "CALLS_SQL", "note"),
|
|
818
|
+
...parseRyuGraphRelations(usesConfigKeyRows, "USES_CONFIG_KEY", "note"),
|
|
819
|
+
...parseRyuGraphRelations(usesResourceKeyRows, "USES_RESOURCE_KEY", "note"),
|
|
820
|
+
...parseRyuGraphRelations(usesSettingKeyRows, "USES_SETTING_KEY", "note"),
|
|
821
|
+
...parseRyuGraphRelations(containsRows, "CONTAINS", "note"),
|
|
822
|
+
...parseRyuGraphRelations(containsModuleRows, "CONTAINS_MODULE", "note"),
|
|
823
|
+
...parseRyuGraphRelations(exportsRows, "EXPORTS", "note"),
|
|
824
|
+
...parseRyuGraphRelations(includesFileRows, "INCLUDES_FILE", "note"),
|
|
825
|
+
...parseRyuGraphRelations(referencesProjectRows, "REFERENCES_PROJECT", "note"),
|
|
826
|
+
...parseRyuGraphRelations(usesResourceRows, "USES_RESOURCE", "note"),
|
|
827
|
+
...parseRyuGraphRelations(usesSettingRows, "USES_SETTING", "note"),
|
|
828
|
+
...parseRyuGraphRelations(usesConfigRows, "USES_CONFIG", "note"),
|
|
829
|
+
...parseRyuGraphRelations(transformsConfigRows, "TRANSFORMS_CONFIG", "note")
|
|
617
830
|
];
|
|
618
831
|
|
|
619
832
|
return {
|
|
620
833
|
documents: ryuDocuments.length > 0 ? ryuDocuments : cachedDocuments,
|
|
621
834
|
adrs: ryuAdrs.length > 0 ? ryuAdrs : cachedAdrs,
|
|
622
835
|
rules: ryuRules.length > 0 ? ryuRules : cachedRules,
|
|
836
|
+
chunks: ryuChunks.length > 0 ? ryuChunks : cachedChunks,
|
|
837
|
+
modules: ryuModules.length > 0 ? ryuModules : cachedModules,
|
|
838
|
+
projects: ryuProjects.length > 0 ? ryuProjects : cachedProjects,
|
|
839
|
+
// Ryu now queries all relation types (core + chunk + module), so no need
|
|
840
|
+
// to merge cached chunk/module relations separately.
|
|
623
841
|
relations: ryuRelations.length > 0 ? ryuRelations : cachedRelations,
|
|
624
842
|
ranking,
|
|
625
843
|
source: "ryu"
|
|
@@ -634,6 +852,9 @@ export async function loadContextData(): Promise<ContextData> {
|
|
|
634
852
|
documents: cachedDocuments,
|
|
635
853
|
adrs: cachedAdrs,
|
|
636
854
|
rules: cachedRules,
|
|
855
|
+
chunks: cachedChunks,
|
|
856
|
+
modules: cachedModules,
|
|
857
|
+
projects: cachedProjects,
|
|
637
858
|
relations: cachedRelations,
|
|
638
859
|
ranking,
|
|
639
860
|
source: "cache",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RelationRecord } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export function relationDegree(relations: RelationRecord[]): Map<string, number> {
|
|
4
|
+
const degrees = new Map<string, number>();
|
|
5
|
+
|
|
6
|
+
for (const relation of relations) {
|
|
7
|
+
degrees.set(relation.from, (degrees.get(relation.from) ?? 0) + 1);
|
|
8
|
+
degrees.set(relation.to, (degrees.get(relation.to) ?? 0) + 1);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return degrees;
|
|
12
|
+
}
|