@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.
Files changed (61) hide show
  1. package/README.md +64 -16
  2. package/bin/cortex.mjs +32 -60
  3. package/package.json +17 -3
  4. package/scaffold/.context/ontology.cypher +47 -0
  5. package/scaffold/.githooks/post-commit +14 -0
  6. package/scaffold/.githooks/post-rewrite +23 -0
  7. package/scaffold/mcp/package-lock.json +19 -23
  8. package/scaffold/mcp/package.json +3 -1
  9. package/scaffold/mcp/src/contextEntities.ts +311 -0
  10. package/scaffold/mcp/src/defaults.ts +6 -0
  11. package/scaffold/mcp/src/embed.ts +163 -37
  12. package/scaffold/mcp/src/frontmatter.ts +39 -0
  13. package/scaffold/mcp/src/graph.ts +330 -109
  14. package/scaffold/mcp/src/graphMetrics.ts +12 -0
  15. package/scaffold/mcp/src/impactPresentation.ts +202 -0
  16. package/scaffold/mcp/src/impactRanking.ts +237 -0
  17. package/scaffold/mcp/src/impactResponse.ts +47 -0
  18. package/scaffold/mcp/src/impactResults.ts +173 -0
  19. package/scaffold/mcp/src/impactSeed.ts +33 -0
  20. package/scaffold/mcp/src/impactTraversal.ts +83 -0
  21. package/scaffold/mcp/src/jsonl.ts +34 -0
  22. package/scaffold/mcp/src/loadGraph.ts +345 -86
  23. package/scaffold/mcp/src/paths.ts +24 -2
  24. package/scaffold/mcp/src/presets.ts +137 -0
  25. package/scaffold/mcp/src/relatedResponse.ts +30 -0
  26. package/scaffold/mcp/src/relatedTraversal.ts +101 -0
  27. package/scaffold/mcp/src/rules.ts +27 -0
  28. package/scaffold/mcp/src/search.ts +191 -355
  29. package/scaffold/mcp/src/searchCore.ts +274 -0
  30. package/scaffold/mcp/src/searchResults.ts +133 -0
  31. package/scaffold/mcp/src/server.ts +95 -3
  32. package/scaffold/mcp/src/types.ts +99 -3
  33. package/scaffold/scripts/context.sh +12 -46
  34. package/scaffold/scripts/dashboard.mjs +797 -0
  35. package/scaffold/scripts/dashboard.sh +13 -0
  36. package/scaffold/scripts/ingest.mjs +2219 -59
  37. package/scaffold/scripts/install-git-hooks.sh +3 -1
  38. package/scaffold/scripts/memory-compile.mjs +232 -0
  39. package/scaffold/scripts/memory-compile.sh +20 -0
  40. package/scaffold/scripts/memory-lint.mjs +375 -0
  41. package/scaffold/scripts/memory-lint.sh +20 -0
  42. package/scaffold/scripts/parsers/config.mjs +178 -0
  43. package/scaffold/scripts/parsers/cpp.mjs +316 -0
  44. package/scaffold/scripts/parsers/dotnet/VbNetParser/Program.cs +374 -0
  45. package/scaffold/scripts/parsers/dotnet/VbNetParser/VbNetParser.csproj +13 -0
  46. package/scaffold/scripts/parsers/javascript/ast.mjs +61 -0
  47. package/scaffold/scripts/parsers/javascript/calls.mjs +53 -0
  48. package/scaffold/scripts/parsers/javascript/chunks.mjs +388 -0
  49. package/scaffold/scripts/parsers/javascript/imports.mjs +162 -0
  50. package/scaffold/scripts/parsers/javascript/patterns.mjs +82 -0
  51. package/scaffold/scripts/parsers/javascript/scope-analysis.mjs +3 -0
  52. package/scaffold/scripts/parsers/javascript/scope-builder.mjs +305 -0
  53. package/scaffold/scripts/parsers/javascript/scope-resolver.mjs +82 -0
  54. package/scaffold/scripts/parsers/javascript.mjs +27 -350
  55. package/scaffold/scripts/parsers/resources.mjs +166 -0
  56. package/scaffold/scripts/parsers/sql.mjs +137 -0
  57. package/scaffold/scripts/parsers/vbnet.mjs +143 -0
  58. package/scaffold/scripts/status.sh +15 -8
  59. package/scaffold/scripts/capture-note.sh +0 -55
  60. package/scaffold/scripts/plan-state-engine.cjs +0 -310
  61. 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
- JsonValue,
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(raw: JsonObject[], relation: RelationRecord["relation"]): RelationRecord[] {
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: asString(item.note) || asString(item.reason)
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: RelationRecord["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 [fileRows, ruleRows, adrRows, constrainsRows, implementsRows, supersedesRows] =
537
- await Promise.all([
538
- queryRows(
539
- connection,
540
- `
541
- MATCH (f:File)
542
- RETURN
543
- f.id AS id,
544
- f.path AS path,
545
- f.kind AS kind,
546
- f.excerpt AS excerpt,
547
- f.updated_at AS updated_at,
548
- f.source_of_truth AS source_of_truth,
549
- f.trust_level AS trust_level,
550
- f.status AS status;
551
- `
552
- ),
553
- queryRows(
554
- connection,
555
- `
556
- MATCH (r:Rule)
557
- RETURN
558
- r.id AS id,
559
- r.title AS title,
560
- r.body AS body,
561
- r.scope AS scope,
562
- r.priority AS priority,
563
- r.updated_at AS updated_at,
564
- r.source_of_truth AS source_of_truth,
565
- r.trust_level AS trust_level,
566
- r.status AS status;
567
- `
568
- ),
569
- queryRows(
570
- connection,
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
+ }