@danielblomma/cortex-mcp 0.4.5 → 1.0.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.
Files changed (64) hide show
  1. package/README.md +125 -42
  2. package/bin/cortex.mjs +36 -63
  3. package/bin/wsl.mjs +30 -0
  4. package/package.json +15 -3
  5. package/scaffold/.context/ontology.cypher +47 -0
  6. package/scaffold/.githooks/post-commit +14 -0
  7. package/scaffold/.githooks/post-rewrite +23 -0
  8. package/scaffold/mcp/package-lock.json +16 -16
  9. package/scaffold/mcp/package.json +4 -1
  10. package/scaffold/mcp/src/contextEntities.ts +311 -0
  11. package/scaffold/mcp/src/defaults.ts +6 -0
  12. package/scaffold/mcp/src/embed.ts +163 -37
  13. package/scaffold/mcp/src/frontmatter.ts +39 -0
  14. package/scaffold/mcp/src/graph.ts +253 -130
  15. package/scaffold/mcp/src/graphMetrics.ts +12 -0
  16. package/scaffold/mcp/src/impactPresentation.ts +202 -0
  17. package/scaffold/mcp/src/impactRanking.ts +237 -0
  18. package/scaffold/mcp/src/impactResponse.ts +47 -0
  19. package/scaffold/mcp/src/impactResults.ts +173 -0
  20. package/scaffold/mcp/src/impactSeed.ts +33 -0
  21. package/scaffold/mcp/src/impactTraversal.ts +83 -0
  22. package/scaffold/mcp/src/jsonl.ts +34 -0
  23. package/scaffold/mcp/src/loadGraph.ts +345 -86
  24. package/scaffold/mcp/src/paths.ts +33 -2
  25. package/scaffold/mcp/src/presets.ts +137 -0
  26. package/scaffold/mcp/src/relatedResponse.ts +30 -0
  27. package/scaffold/mcp/src/relatedTraversal.ts +101 -0
  28. package/scaffold/mcp/src/rules.ts +27 -0
  29. package/scaffold/mcp/src/search.ts +186 -455
  30. package/scaffold/mcp/src/searchCore.ts +274 -0
  31. package/scaffold/mcp/src/searchResults.ts +133 -0
  32. package/scaffold/mcp/src/server.ts +95 -3
  33. package/scaffold/mcp/src/types.ts +82 -3
  34. package/scaffold/scripts/context.sh +12 -46
  35. package/scaffold/scripts/dashboard.mjs +797 -0
  36. package/scaffold/scripts/dashboard.sh +13 -0
  37. package/scaffold/scripts/ingest.mjs +2227 -59
  38. package/scaffold/scripts/install-git-hooks.sh +3 -1
  39. package/scaffold/scripts/memory-compile.mjs +241 -0
  40. package/scaffold/scripts/memory-compile.sh +20 -0
  41. package/scaffold/scripts/memory-lint.mjs +384 -0
  42. package/scaffold/scripts/memory-lint.sh +20 -0
  43. package/scaffold/scripts/parsers/config.mjs +178 -0
  44. package/scaffold/scripts/parsers/cpp.mjs +316 -0
  45. package/scaffold/scripts/parsers/dotnet/VbNetParser/Program.cs +374 -0
  46. package/scaffold/scripts/parsers/dotnet/VbNetParser/VbNetParser.csproj +13 -0
  47. package/scaffold/scripts/parsers/javascript/ast.mjs +61 -0
  48. package/scaffold/scripts/parsers/javascript/calls.mjs +53 -0
  49. package/scaffold/scripts/parsers/javascript/chunks.mjs +388 -0
  50. package/scaffold/scripts/parsers/javascript/imports.mjs +162 -0
  51. package/scaffold/scripts/parsers/javascript/patterns.mjs +82 -0
  52. package/scaffold/scripts/parsers/javascript/scope-analysis.mjs +3 -0
  53. package/scaffold/scripts/parsers/javascript/scope-builder.mjs +305 -0
  54. package/scaffold/scripts/parsers/javascript/scope-resolver.mjs +82 -0
  55. package/scaffold/scripts/parsers/javascript.mjs +27 -350
  56. package/scaffold/scripts/parsers/resources.mjs +166 -0
  57. package/scaffold/scripts/parsers/rust.mjs +515 -0
  58. package/scaffold/scripts/parsers/sql.mjs +137 -0
  59. package/scaffold/scripts/parsers/vbnet.mjs +143 -0
  60. package/scaffold/scripts/status.sh +0 -7
  61. package/scaffold/scripts/watch.sh +9 -1
  62. package/scaffold/scripts/capture-note.sh +0 -55
  63. package/scaffold/scripts/plan-state-engine.cjs +0 -310
  64. package/scaffold/scripts/plan-state.sh +0 -71
@@ -1,6 +1,7 @@
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,9 +9,11 @@ import type {
8
9
  ContextData,
9
10
  DocumentRecord,
10
11
  JsonObject,
11
- JsonValue,
12
+ ModuleRecord,
13
+ ProjectRecord,
12
14
  RankingWeights,
13
15
  RelationRecord,
16
+ RelationType,
14
17
  RuleRecord,
15
18
  UnknownRow
16
19
  } from "./types.js";
@@ -31,6 +34,33 @@ let ryuLastInitAttemptAt = 0;
31
34
  let ryuGraphSignature: string | null = null;
32
35
 
33
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;
34
64
 
35
65
  function readFileIfExists(filePath: string): string | null {
36
66
  if (!fs.existsSync(filePath)) {
@@ -39,38 +69,6 @@ function readFileIfExists(filePath: string): string | null {
39
69
  return fs.readFileSync(filePath, "utf8");
40
70
  }
41
71
 
42
- function readJsonl(filePath: string): JsonObject[] {
43
- const raw = readFileIfExists(filePath);
44
- if (!raw) {
45
- return [];
46
- }
47
-
48
- return raw
49
- .split(/\r?\n/)
50
- .map((line) => line.trim())
51
- .filter(Boolean)
52
- .map((line) => {
53
- try {
54
- return JSON.parse(line) as JsonObject;
55
- } catch {
56
- return null;
57
- }
58
- })
59
- .filter((value): value is JsonObject => value !== null);
60
- }
61
-
62
- function asString(value: JsonValue | undefined, fallback = ""): string {
63
- return typeof value === "string" ? value : fallback;
64
- }
65
-
66
- function asNumber(value: JsonValue | undefined, fallback = 0): number {
67
- return typeof value === "number" && Number.isFinite(value) ? value : fallback;
68
- }
69
-
70
- function asBoolean(value: JsonValue | undefined, fallback = false): boolean {
71
- return typeof value === "boolean" ? value : fallback;
72
- }
73
-
74
72
  function asStringUnknown(value: unknown, fallback = ""): string {
75
73
  if (typeof value === "string") return value;
76
74
  if (typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") {
@@ -164,9 +162,11 @@ function parseChunkEntities(raw: JsonObject[]): ChunkRecord[] {
164
162
  kind: asString(item.kind, "chunk"),
165
163
  signature: asString(item.signature),
166
164
  body: asString(item.body),
165
+ description: asString(item.description),
167
166
  start_line: asNumber(item.start_line),
168
167
  end_line: asNumber(item.end_line),
169
168
  language: asString(item.language),
169
+ exported: asBoolean(item.exported, false),
170
170
  updated_at: asString(item.updated_at),
171
171
  source_of_truth: asBoolean(item.source_of_truth, false),
172
172
  trust_level: asNumber(item.trust_level, 60),
@@ -176,6 +176,56 @@ function parseChunkEntities(raw: JsonObject[]): ChunkRecord[] {
176
176
  .filter((item): item is ChunkRecord => item !== null);
177
177
  }
178
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
+
179
229
  function parseRuleEntities(raw: JsonObject[]): RuleRecord[] {
180
230
  return raw
181
231
  .map((item) => {
@@ -273,7 +323,7 @@ function parseRulesYaml(yamlText: string | null): RuleRecord[] {
273
323
 
274
324
  function parseRelations(
275
325
  raw: JsonObject[],
276
- relation: RelationRecord["relation"],
326
+ relation: RelationType,
277
327
  noteFields: string[] = ["note", "reason"]
278
328
  ): RelationRecord[] {
279
329
  return raw
@@ -379,6 +429,28 @@ function buildMissingDbMessage(): string {
379
429
  return `RyuGraph DB not found at ${DB_PATH}. Run ${loadCommand} (or ${bootstrapCommand} on cold start).`;
380
430
  }
381
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
+
382
454
  async function closeRyuGraphResources(): Promise<void> {
383
455
  const currentConnection = ryuConnection;
384
456
  const currentDb = ryuDb;
@@ -435,6 +507,12 @@ async function getRyuGraphConnection(forceReload = false): Promise<Connection |
435
507
  return null;
436
508
  }
437
509
 
510
+ const missingManifestKeys = readGraphManifestMissingKeys();
511
+ if (missingManifestKeys.length > 0) {
512
+ await resetRyuGraphState(buildIncompatibleGraphMessage(missingManifestKeys));
513
+ return null;
514
+ }
515
+
438
516
  try {
439
517
  const nextDb = new ryugraph.Database(DB_PATH, undefined, undefined, true);
440
518
  const nextConnection = new ryugraph.Connection(nextDb);
@@ -539,9 +617,11 @@ function parseRyuGraphChunks(rows: UnknownRow[]): ChunkRecord[] {
539
617
  kind: asStringUnknown(row.kind, "chunk"),
540
618
  signature: asStringUnknown(row.signature),
541
619
  body: asStringUnknown(row.body),
620
+ description: asStringUnknown(row.description),
542
621
  start_line: asNumberUnknown(row.start_line),
543
622
  end_line: asNumberUnknown(row.end_line),
544
623
  language: asStringUnknown(row.language),
624
+ exported: asBooleanUnknown(row.exported, false),
545
625
  updated_at: asStringUnknown(row.updated_at),
546
626
  source_of_truth: asBooleanUnknown(row.source_of_truth, false),
547
627
  trust_level: asNumberUnknown(row.trust_level, 60),
@@ -551,9 +631,59 @@ function parseRyuGraphChunks(rows: UnknownRow[]): ChunkRecord[] {
551
631
  .filter((value): value is ChunkRecord => value !== null);
552
632
  }
553
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
+
554
684
  function parseRyuGraphRelations(
555
685
  rows: UnknownRow[],
556
- relation: RelationRecord["relation"],
686
+ relation: RelationType,
557
687
  noteField: string
558
688
  ): RelationRecord[] {
559
689
  return rows
@@ -578,15 +708,37 @@ export async function loadContextData(): Promise<ContextData> {
578
708
  const cachedDocuments = parseDocuments(readJsonl(PATHS.documents));
579
709
  const cachedAdrs = parseAdrs(readJsonl(PATHS.adrEntities));
580
710
  const cachedChunks = parseChunkEntities(readJsonl(PATHS.chunkEntities));
711
+ const cachedModules = parseModuleEntities(readJsonl(PATHS.moduleEntities));
712
+ const cachedProjects = parseProjectEntities(readJsonl(PATHS.projectEntities));
581
713
  const cachedChunkRelations = [
714
+ ...parseRelations(readJsonl(PATHS.definesRelations), "DEFINES"),
582
715
  ...parseRelations(readJsonl(PATHS.callsRelations), "CALLS", ["call_type"]),
583
- ...parseRelations(readJsonl(PATHS.importsRelations), "IMPORTS", ["import_name"])
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")
584
734
  ];
585
735
  const cachedRelations = [
586
736
  ...parseRelations(readJsonl(PATHS.constrainsRelations), "CONSTRAINS"),
587
737
  ...parseRelations(readJsonl(PATHS.implementsRelations), "IMPLEMENTS"),
588
738
  ...parseRelations(readJsonl(PATHS.supersedesRelations), "SUPERSEDES"),
589
- ...cachedChunkRelations
739
+ ...cachedChunkRelations,
740
+ ...cachedModuleRelations,
741
+ ...cachedProjectRelations
590
742
  ];
591
743
 
592
744
  const yamlRules = parseRulesYaml(readFileIfExists(PATHS.rulesYaml));
@@ -600,6 +752,8 @@ export async function loadContextData(): Promise<ContextData> {
600
752
  adrs: cachedAdrs,
601
753
  rules: cachedRules,
602
754
  chunks: cachedChunks,
755
+ modules: cachedModules,
756
+ projects: cachedProjects,
603
757
  relations: cachedRelations,
604
758
  ranking,
605
759
  source: "cache",
@@ -608,97 +762,42 @@ export async function loadContextData(): Promise<ContextData> {
608
762
  }
609
763
 
610
764
  try {
611
- const [fileRows, ruleRows, adrRows, chunkRows, constrainsRows, implementsRows, supersedesRows] =
612
- await Promise.all([
613
- queryRows(
614
- connection,
615
- `
616
- MATCH (f:File)
617
- RETURN
618
- f.id AS id,
619
- f.path AS path,
620
- f.kind AS kind,
621
- f.excerpt AS excerpt,
622
- f.updated_at AS updated_at,
623
- f.source_of_truth AS source_of_truth,
624
- f.trust_level AS trust_level,
625
- f.status AS status;
626
- `
627
- ),
628
- queryRows(
629
- connection,
630
- `
631
- MATCH (r:Rule)
632
- RETURN
633
- r.id AS id,
634
- r.title AS title,
635
- r.body AS body,
636
- r.scope AS scope,
637
- r.priority AS priority,
638
- r.updated_at AS updated_at,
639
- r.source_of_truth AS source_of_truth,
640
- r.trust_level AS trust_level,
641
- r.status AS status;
642
- `
643
- ),
644
- queryRows(
645
- connection,
646
- `
647
- MATCH (a:ADR)
648
- RETURN
649
- a.id AS id,
650
- a.path AS path,
651
- a.title AS title,
652
- a.body AS body,
653
- a.decision_date AS decision_date,
654
- a.supersedes_id AS supersedes_id,
655
- a.source_of_truth AS source_of_truth,
656
- a.trust_level AS trust_level,
657
- a.status AS status;
658
- `
659
- ),
660
- queryRows(
661
- connection,
662
- `
663
- MATCH (c:Chunk)
664
- RETURN
665
- c.id AS id,
666
- c.file_id AS file_id,
667
- c.name AS name,
668
- c.kind AS kind,
669
- c.signature AS signature,
670
- c.body AS body,
671
- c.start_line AS start_line,
672
- c.end_line AS end_line,
673
- c.language AS language,
674
- c.updated_at AS updated_at,
675
- c.source_of_truth AS source_of_truth,
676
- c.trust_level AS trust_level,
677
- c.status AS status;
678
- `
679
- ),
680
- queryRows(
681
- connection,
682
- `
683
- MATCH (r:Rule)-[c:CONSTRAINS]->(f:File)
684
- RETURN r.id AS from, f.id AS to, c.note AS note;
685
- `
686
- ),
687
- queryRows(
688
- connection,
689
- `
690
- MATCH (f:File)-[i:IMPLEMENTS]->(r:Rule)
691
- RETURN f.id AS from, r.id AS to, i.note AS note;
692
- `
693
- ),
694
- queryRows(
695
- connection,
696
- `
697
- MATCH (a1:ADR)-[s:SUPERSEDES]->(a2:ADR)
698
- RETURN a1.id AS from, a2.id AS to, s.reason AS note;
699
- `
700
- )
701
- ]);
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;
702
801
 
703
802
  const contentById = new Map(cachedDocuments.map((doc) => [doc.id, doc.content]));
704
803
 
@@ -706,10 +805,28 @@ export async function loadContextData(): Promise<ContextData> {
706
805
  const ryuRules = parseRyuGraphRules(ruleRows);
707
806
  const ryuAdrs = parseRyuGraphAdrs(adrRows);
708
807
  const ryuChunks = parseRyuGraphChunks(chunkRows);
808
+ const ryuModules = parseRyuGraphModules(moduleRows);
809
+ const ryuProjects = parseRyuGraphProjects(projectRows);
709
810
  const ryuRelations = [
710
811
  ...parseRyuGraphRelations(constrainsRows, "CONSTRAINS", "note"),
711
812
  ...parseRyuGraphRelations(implementsRows, "IMPLEMENTS", "note"),
712
- ...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")
713
830
  ];
714
831
 
715
832
  return {
@@ -717,7 +834,11 @@ export async function loadContextData(): Promise<ContextData> {
717
834
  adrs: ryuAdrs.length > 0 ? ryuAdrs : cachedAdrs,
718
835
  rules: ryuRules.length > 0 ? ryuRules : cachedRules,
719
836
  chunks: ryuChunks.length > 0 ? ryuChunks : cachedChunks,
720
- relations: ryuRelations.length > 0 ? [...ryuRelations, ...cachedChunkRelations] : cachedRelations,
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.
841
+ relations: ryuRelations.length > 0 ? ryuRelations : cachedRelations,
721
842
  ranking,
722
843
  source: "ryu"
723
844
  };
@@ -732,6 +853,8 @@ export async function loadContextData(): Promise<ContextData> {
732
853
  adrs: cachedAdrs,
733
854
  rules: cachedRules,
734
855
  chunks: cachedChunks,
856
+ modules: cachedModules,
857
+ projects: cachedProjects,
735
858
  relations: cachedRelations,
736
859
  ranking,
737
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
+ }