@danielblomma/cortex-mcp 2.2.5 → 2.4.1

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 (196) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/README.md +73 -3
  3. package/bin/cortex.mjs +339 -242
  4. package/bin/daemon-control.mjs +162 -0
  5. package/package.json +7 -3
  6. package/scaffold/mcp/dist/.cortex-build-hash +1 -0
  7. package/scaffold/mcp/dist/cli/enterprise-setup.js +134 -0
  8. package/scaffold/mcp/dist/cli/govern.js +937 -0
  9. package/scaffold/mcp/dist/cli/query.js +409 -0
  10. package/scaffold/mcp/dist/cli/run.js +295 -0
  11. package/scaffold/mcp/dist/cli/stage.js +308 -0
  12. package/scaffold/mcp/dist/cli/telemetry-test.js +141 -0
  13. package/scaffold/mcp/dist/cli/ungoverned-detector.js +133 -0
  14. package/scaffold/mcp/dist/contextEntities.js +282 -0
  15. package/scaffold/mcp/dist/core/audit/query.js +72 -0
  16. package/scaffold/mcp/dist/core/audit/writer.js +28 -0
  17. package/scaffold/mcp/dist/core/config.js +235 -0
  18. package/scaffold/mcp/dist/core/enterprise-host-identity.js +193 -0
  19. package/scaffold/mcp/dist/core/enterprise-identity.js +18 -0
  20. package/scaffold/mcp/dist/core/enterprise-rotation.js +158 -0
  21. package/scaffold/mcp/dist/core/govern-paths.js +21 -0
  22. package/scaffold/mcp/dist/core/index.js +14 -0
  23. package/scaffold/mcp/dist/core/license.js +278 -0
  24. package/scaffold/mcp/dist/core/policy/enforce.js +66 -0
  25. package/scaffold/mcp/dist/core/policy/injection.js +172 -0
  26. package/scaffold/mcp/dist/core/policy/store.js +179 -0
  27. package/scaffold/mcp/dist/core/rbac/check.js +30 -0
  28. package/scaffold/mcp/dist/core/secure-endpoint.js +25 -0
  29. package/scaffold/mcp/dist/core/telemetry/collector.js +285 -0
  30. package/scaffold/mcp/dist/core/telemetry/state-dir.js +31 -0
  31. package/scaffold/mcp/dist/core/validators/builtins.js +632 -0
  32. package/scaffold/mcp/dist/core/validators/config.js +44 -0
  33. package/scaffold/mcp/dist/core/validators/engine.js +120 -0
  34. package/scaffold/mcp/dist/core/validators/evaluators/code_comments.js +236 -0
  35. package/scaffold/mcp/dist/core/validators/evaluators/regex.js +116 -0
  36. package/scaffold/mcp/dist/core/workflow/artifact-io.js +103 -0
  37. package/scaffold/mcp/dist/core/workflow/capabilities.js +88 -0
  38. package/scaffold/mcp/dist/core/workflow/default-workflows.js +102 -0
  39. package/scaffold/mcp/dist/core/workflow/enforcement.js +164 -0
  40. package/scaffold/mcp/dist/core/workflow/envelope.js +132 -0
  41. package/scaffold/mcp/dist/core/workflow/index.js +11 -0
  42. package/scaffold/mcp/dist/core/workflow/mcp-tools.js +175 -0
  43. package/scaffold/mcp/dist/core/workflow/resolution.js +69 -0
  44. package/scaffold/mcp/dist/core/workflow/run-lifecycle.js +123 -0
  45. package/scaffold/mcp/dist/core/workflow/schemas.js +141 -0
  46. package/scaffold/mcp/dist/core/workflow/synced-capability-registry.js +60 -0
  47. package/scaffold/mcp/dist/core/workflow/synced-registry.js +60 -0
  48. package/scaffold/mcp/dist/daemon/capability-sync-checker.js +242 -0
  49. package/scaffold/mcp/dist/daemon/client.js +130 -0
  50. package/scaffold/mcp/dist/daemon/egress-proxy.js +288 -0
  51. package/scaffold/mcp/dist/daemon/global-host-events.js +222 -0
  52. package/scaffold/mcp/dist/daemon/heartbeat-pusher.js +116 -0
  53. package/scaffold/mcp/dist/daemon/heartbeat-tracker.js +165 -0
  54. package/scaffold/mcp/dist/daemon/host-events-pusher.js +249 -0
  55. package/scaffold/mcp/dist/daemon/main.js +449 -0
  56. package/scaffold/mcp/dist/daemon/paths.js +36 -0
  57. package/scaffold/mcp/dist/daemon/project-service-registry.js +78 -0
  58. package/scaffold/mcp/dist/daemon/protocol.js +8 -0
  59. package/scaffold/mcp/dist/daemon/server.js +180 -0
  60. package/scaffold/mcp/dist/daemon/skill-sync-checker.js +557 -0
  61. package/scaffold/mcp/dist/daemon/sync-checker.js +165 -0
  62. package/scaffold/mcp/dist/daemon/ungoverned-scanner.js +136 -0
  63. package/scaffold/mcp/dist/daemon/workflow-sync-checker.js +249 -0
  64. package/scaffold/mcp/dist/defaults.js +6 -0
  65. package/scaffold/mcp/dist/embed.js +627 -0
  66. package/scaffold/mcp/dist/embedScheduler.js +479 -0
  67. package/scaffold/mcp/dist/embeddings.js +167 -0
  68. package/scaffold/mcp/dist/enterprise/audit/push.js +66 -0
  69. package/scaffold/mcp/dist/enterprise/index.js +327 -0
  70. package/scaffold/mcp/dist/enterprise/model/deploy.js +27 -0
  71. package/scaffold/mcp/dist/enterprise/policy/sync.js +129 -0
  72. package/scaffold/mcp/dist/enterprise/privacy/boundary.js +184 -0
  73. package/scaffold/mcp/dist/enterprise/reviews/changed-files.js +33 -0
  74. package/scaffold/mcp/dist/enterprise/reviews/pattern-context.js +202 -0
  75. package/scaffold/mcp/dist/enterprise/reviews/policy-selection.js +46 -0
  76. package/scaffold/mcp/dist/enterprise/reviews/push.js +102 -0
  77. package/scaffold/mcp/dist/enterprise/reviews/trust-state.js +186 -0
  78. package/scaffold/mcp/dist/enterprise/telemetry/sync.js +57 -0
  79. package/scaffold/mcp/dist/enterprise/tools/enterprise.js +826 -0
  80. package/scaffold/mcp/dist/enterprise/tools/harness.js +40 -0
  81. package/scaffold/mcp/dist/enterprise/tools/walk.js +73 -0
  82. package/scaffold/mcp/dist/enterprise/violations/push.js +77 -0
  83. package/scaffold/mcp/dist/enterprise/workflow/push.js +44 -0
  84. package/scaffold/mcp/dist/enterprise/workflow/state.js +329 -0
  85. package/scaffold/mcp/dist/frontmatter.js +33 -0
  86. package/scaffold/mcp/dist/graph.js +769 -0
  87. package/scaffold/mcp/dist/graphCsv.js +55 -0
  88. package/scaffold/mcp/dist/graphMetrics.js +8 -0
  89. package/scaffold/mcp/dist/hooks/permission-request.js +89 -0
  90. package/scaffold/mcp/dist/hooks/post-tool-use.js +105 -0
  91. package/scaffold/mcp/dist/hooks/pre-compact.js +29 -0
  92. package/scaffold/mcp/dist/hooks/pre-tool-use.js +78 -0
  93. package/scaffold/mcp/dist/hooks/session-end.js +43 -0
  94. package/scaffold/mcp/dist/hooks/session-start.js +41 -0
  95. package/scaffold/mcp/dist/hooks/shared.js +194 -0
  96. package/scaffold/mcp/dist/hooks/stop.js +28 -0
  97. package/scaffold/mcp/dist/hooks/user-prompt-submit.js +33 -0
  98. package/scaffold/mcp/dist/impactPresentation.js +137 -0
  99. package/scaffold/mcp/dist/impactRanking.js +191 -0
  100. package/scaffold/mcp/dist/impactResponse.js +30 -0
  101. package/scaffold/mcp/dist/impactResults.js +105 -0
  102. package/scaffold/mcp/dist/impactSeed.js +20 -0
  103. package/scaffold/mcp/dist/impactTraversal.js +64 -0
  104. package/scaffold/mcp/dist/jsonl.js +77 -0
  105. package/scaffold/mcp/dist/loadGraph.js +759 -0
  106. package/scaffold/mcp/dist/lruCache.js +38 -0
  107. package/scaffold/mcp/dist/paths.js +97 -0
  108. package/scaffold/mcp/dist/patternEvidence.js +272 -0
  109. package/scaffold/mcp/dist/plugin.js +81 -0
  110. package/scaffold/mcp/dist/presets.js +78 -0
  111. package/scaffold/mcp/dist/relatedResponse.js +18 -0
  112. package/scaffold/mcp/dist/relatedTraversal.js +78 -0
  113. package/scaffold/mcp/dist/rules.js +23 -0
  114. package/scaffold/mcp/dist/search.js +212 -0
  115. package/scaffold/mcp/dist/searchCore.js +457 -0
  116. package/scaffold/mcp/dist/searchResults.js +230 -0
  117. package/scaffold/mcp/dist/server.js +317 -0
  118. package/scaffold/mcp/dist/types.js +1 -0
  119. package/scaffold/mcp/package-lock.json +336 -212
  120. package/scaffold/mcp/package.json +15 -6
  121. package/scaffold/mcp/src/cli/enterprise-setup.ts +82 -8
  122. package/scaffold/mcp/src/cli/govern.ts +137 -52
  123. package/scaffold/mcp/src/cli/query.ts +36 -2
  124. package/scaffold/mcp/src/cli/run.ts +53 -19
  125. package/scaffold/mcp/src/cli/stage.ts +8 -3
  126. package/scaffold/mcp/src/core/config.ts +9 -2
  127. package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
  128. package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
  129. package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
  130. package/scaffold/mcp/src/core/govern-paths.ts +30 -0
  131. package/scaffold/mcp/src/core/license.ts +186 -17
  132. package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
  133. package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
  134. package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
  135. package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
  136. package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
  137. package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
  138. package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
  139. package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
  140. package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
  141. package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
  142. package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
  143. package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
  144. package/scaffold/mcp/src/daemon/main.ts +99 -25
  145. package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
  146. package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
  147. package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
  148. package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
  149. package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
  150. package/scaffold/mcp/src/embed.ts +211 -10
  151. package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
  152. package/scaffold/mcp/src/enterprise/index.ts +1 -1
  153. package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
  154. package/scaffold/mcp/src/enterprise/reviews/changed-files.ts +34 -0
  155. package/scaffold/mcp/src/enterprise/reviews/pattern-context.ts +231 -0
  156. package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
  157. package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
  158. package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
  159. package/scaffold/mcp/src/enterprise/tools/enterprise.ts +49 -36
  160. package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
  161. package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
  162. package/scaffold/mcp/src/paths.ts +3 -5
  163. package/scaffold/mcp/src/patternEvidence.ts +347 -0
  164. package/scaffold/mcp/src/plugin.ts +20 -0
  165. package/scaffold/mcp/src/search.ts +35 -9
  166. package/scaffold/mcp/src/searchCore.ts +254 -12
  167. package/scaffold/mcp/src/searchResults.ts +96 -9
  168. package/scaffold/mcp/src/types.ts +7 -0
  169. package/scaffold/mcp/tests/changed-files.test.mjs +41 -0
  170. package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
  171. package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
  172. package/scaffold/mcp/tests/embed-entities.test.mjs +109 -1
  173. package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
  174. package/scaffold/mcp/tests/enterprise-pattern-context.test.mjs +322 -0
  175. package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
  176. package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
  177. package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
  178. package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
  179. package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
  180. package/scaffold/mcp/tests/license.test.mjs +367 -0
  181. package/scaffold/mcp/tests/paths.test.mjs +11 -3
  182. package/scaffold/mcp/tests/pattern-evidence.test.mjs +321 -0
  183. package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
  184. package/scaffold/mcp/tests/query-cli.test.mjs +73 -1
  185. package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
  186. package/scaffold/mcp/tests/search-graph-score.test.mjs +167 -0
  187. package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
  188. package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
  189. package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
  190. package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
  191. package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
  192. package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +40 -1
  193. package/scaffold/scripts/dashboard.mjs +29 -18
  194. package/scaffold/scripts/doctor.sh +26 -8
  195. package/scaffold/scripts/ingest.mjs +37 -4
  196. package/scaffold/scripts/status.sh +13 -6
@@ -0,0 +1,769 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import ryugraph from "ryugraph";
4
+ import { readJsonl, asString, asNumber, asBoolean } from "./jsonl.js";
5
+ import { DB_PATH, DEFAULT_RANKING, PATHS } from "./paths.js";
6
+ let ryuDb = null;
7
+ let ryuConnection = null;
8
+ let ryuInitError = null;
9
+ let ryuLastInitAttemptAt = 0;
10
+ let ryuGraphSignature = null;
11
+ const RYU_INIT_RETRY_INTERVAL_MS = 2000;
12
+ const REQUIRED_GRAPH_MANIFEST_COUNT_KEYS = [
13
+ "files",
14
+ "rules",
15
+ "adrs",
16
+ "chunks",
17
+ "constrains",
18
+ "implements",
19
+ "supersedes",
20
+ "defines",
21
+ "calls",
22
+ "imports",
23
+ "calls_sql",
24
+ "uses_config_key",
25
+ "uses_resource_key",
26
+ "uses_setting_key",
27
+ "modules",
28
+ "projects",
29
+ "contains",
30
+ "contains_module",
31
+ "exports",
32
+ "includes_file",
33
+ "references_project",
34
+ "uses_resource",
35
+ "uses_setting",
36
+ "uses_config",
37
+ "transforms_config"
38
+ ];
39
+ function readFileIfExists(filePath) {
40
+ if (!fs.existsSync(filePath)) {
41
+ return null;
42
+ }
43
+ return fs.readFileSync(filePath, "utf8");
44
+ }
45
+ function asStringUnknown(value, fallback = "") {
46
+ if (typeof value === "string")
47
+ return value;
48
+ if (typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") {
49
+ return String(value);
50
+ }
51
+ return fallback;
52
+ }
53
+ function asNumberUnknown(value, fallback = 0) {
54
+ if (typeof value === "number" && Number.isFinite(value))
55
+ return value;
56
+ if (typeof value === "bigint")
57
+ return Number(value);
58
+ if (typeof value === "string") {
59
+ const parsed = Number(value);
60
+ if (Number.isFinite(parsed))
61
+ return parsed;
62
+ }
63
+ return fallback;
64
+ }
65
+ function asBooleanUnknown(value, fallback = false) {
66
+ if (typeof value === "boolean")
67
+ return value;
68
+ if (typeof value === "string") {
69
+ if (value.toLowerCase() === "true")
70
+ return true;
71
+ if (value.toLowerCase() === "false")
72
+ return false;
73
+ }
74
+ return fallback;
75
+ }
76
+ function parseDocuments(raw) {
77
+ return raw
78
+ .map((item) => {
79
+ const id = asString(item.id);
80
+ const filePath = asString(item.path);
81
+ if (!id || !filePath) {
82
+ return null;
83
+ }
84
+ const kindRaw = asString(item.kind, "DOC").toUpperCase();
85
+ const kind = kindRaw === "CODE" ? "CODE" : kindRaw === "ADR" ? "ADR" : "DOC";
86
+ return {
87
+ id,
88
+ path: filePath,
89
+ kind,
90
+ updated_at: asString(item.updated_at),
91
+ source_of_truth: asBoolean(item.source_of_truth),
92
+ trust_level: asNumber(item.trust_level, 50),
93
+ status: asString(item.status, "active"),
94
+ excerpt: asString(item.excerpt),
95
+ content: asString(item.content)
96
+ };
97
+ })
98
+ .filter((item) => item !== null);
99
+ }
100
+ function parseAdrs(raw) {
101
+ return raw
102
+ .map((item) => {
103
+ const id = asString(item.id);
104
+ if (!id) {
105
+ return null;
106
+ }
107
+ return {
108
+ id,
109
+ path: asString(item.path),
110
+ title: asString(item.title),
111
+ body: asString(item.body),
112
+ decision_date: asString(item.decision_date),
113
+ supersedes_id: asString(item.supersedes_id),
114
+ source_of_truth: asBoolean(item.source_of_truth, true),
115
+ trust_level: asNumber(item.trust_level, 95),
116
+ status: asString(item.status, "active")
117
+ };
118
+ })
119
+ .filter((item) => item !== null);
120
+ }
121
+ function parseChunkEntities(raw) {
122
+ return raw
123
+ .map((item) => {
124
+ const id = asString(item.id);
125
+ if (!id) {
126
+ return null;
127
+ }
128
+ return {
129
+ id,
130
+ file_id: asString(item.file_id),
131
+ name: asString(item.name),
132
+ kind: asString(item.kind, "chunk"),
133
+ signature: asString(item.signature),
134
+ body: asString(item.body),
135
+ description: asString(item.description),
136
+ start_line: asNumber(item.start_line),
137
+ end_line: asNumber(item.end_line),
138
+ language: asString(item.language),
139
+ exported: asBoolean(item.exported, false),
140
+ updated_at: asString(item.updated_at),
141
+ source_of_truth: asBoolean(item.source_of_truth, false),
142
+ trust_level: asNumber(item.trust_level, 60),
143
+ status: asString(item.status, "active")
144
+ };
145
+ })
146
+ .filter((item) => item !== null);
147
+ }
148
+ function parseModuleEntities(raw) {
149
+ return raw
150
+ .map((item) => {
151
+ const id = asString(item.id);
152
+ if (!id) {
153
+ return null;
154
+ }
155
+ return {
156
+ id,
157
+ path: asString(item.path),
158
+ name: asString(item.name),
159
+ summary: asString(item.summary),
160
+ file_count: asNumber(item.file_count, 0),
161
+ exported_symbols: asString(item.exported_symbols),
162
+ updated_at: asString(item.updated_at),
163
+ source_of_truth: asBoolean(item.source_of_truth, false),
164
+ trust_level: asNumber(item.trust_level, 75),
165
+ status: asString(item.status, "active")
166
+ };
167
+ })
168
+ .filter((item) => item !== null);
169
+ }
170
+ function parseProjectEntities(raw) {
171
+ return raw
172
+ .map((item) => {
173
+ const id = asString(item.id);
174
+ if (!id) {
175
+ return null;
176
+ }
177
+ return {
178
+ id,
179
+ path: asString(item.path),
180
+ name: asString(item.name),
181
+ kind: asString(item.kind, "project"),
182
+ language: asString(item.language, "dotnet"),
183
+ target_framework: asString(item.target_framework),
184
+ summary: asString(item.summary),
185
+ file_count: asNumber(item.file_count, 0),
186
+ updated_at: asString(item.updated_at),
187
+ source_of_truth: asBoolean(item.source_of_truth, false),
188
+ trust_level: asNumber(item.trust_level, 80),
189
+ status: asString(item.status, "active")
190
+ };
191
+ })
192
+ .filter((item) => item !== null);
193
+ }
194
+ function parseRuleEntities(raw) {
195
+ return raw
196
+ .map((item) => {
197
+ const id = asString(item.id);
198
+ if (!id) {
199
+ return null;
200
+ }
201
+ return {
202
+ id,
203
+ title: asString(item.title, id),
204
+ body: asString(item.body),
205
+ scope: asString(item.scope, "global"),
206
+ updated_at: asString(item.updated_at, new Date(0).toISOString()),
207
+ source_of_truth: asBoolean(item.source_of_truth, true),
208
+ trust_level: asNumber(item.trust_level, 95),
209
+ status: asString(item.status, "active"),
210
+ priority: asNumber(item.priority, 0)
211
+ };
212
+ })
213
+ .filter((item) => item !== null);
214
+ }
215
+ function parseRulesYaml(yamlText) {
216
+ if (!yamlText) {
217
+ return [];
218
+ }
219
+ const lines = yamlText.split(/\r?\n/);
220
+ const rules = [];
221
+ let current = null;
222
+ const pushCurrent = () => {
223
+ if (!current?.id) {
224
+ return;
225
+ }
226
+ rules.push({
227
+ id: current.id,
228
+ title: current.id,
229
+ body: current.description ?? "",
230
+ scope: current.scope ?? "global",
231
+ updated_at: new Date().toISOString(),
232
+ source_of_truth: true,
233
+ trust_level: 95,
234
+ status: current.enforce === false ? "draft" : "active",
235
+ priority: Number.isFinite(current.priority) ? current.priority : 0
236
+ });
237
+ };
238
+ for (const line of lines) {
239
+ const idMatch = line.match(/^\s*-\s*id:\s*(.+?)\s*$/);
240
+ if (idMatch) {
241
+ pushCurrent();
242
+ current = { id: idMatch[1].replace(/^['"]|['"]$/g, "") };
243
+ continue;
244
+ }
245
+ if (!current) {
246
+ continue;
247
+ }
248
+ const descriptionMatch = line.match(/^\s*description:\s*(.+?)\s*$/);
249
+ if (descriptionMatch) {
250
+ current.description = descriptionMatch[1].replace(/^['"]|['"]$/g, "");
251
+ continue;
252
+ }
253
+ const priorityMatch = line.match(/^\s*priority:\s*(\d+)\s*$/);
254
+ if (priorityMatch) {
255
+ current.priority = Number(priorityMatch[1]);
256
+ continue;
257
+ }
258
+ const enforceMatch = line.match(/^\s*enforce:\s*(true|false)\s*$/i);
259
+ if (enforceMatch) {
260
+ current.enforce = enforceMatch[1].toLowerCase() === "true";
261
+ continue;
262
+ }
263
+ const scopeMatch = line.match(/^\s*scope:\s*(.+?)\s*$/);
264
+ if (scopeMatch) {
265
+ current.scope = scopeMatch[1].replace(/^['"]|['"]$/g, "");
266
+ }
267
+ }
268
+ pushCurrent();
269
+ return rules;
270
+ }
271
+ function parseRelations(raw, relation, noteFields = ["note", "reason"]) {
272
+ return raw
273
+ .map((item) => {
274
+ const from = asString(item.from);
275
+ const to = asString(item.to);
276
+ if (!from || !to) {
277
+ return null;
278
+ }
279
+ let note = "";
280
+ for (const fieldName of noteFields) {
281
+ const candidate = asString(item[fieldName]);
282
+ if (candidate) {
283
+ note = candidate;
284
+ break;
285
+ }
286
+ }
287
+ return {
288
+ from,
289
+ to,
290
+ relation,
291
+ note
292
+ };
293
+ })
294
+ .filter((item) => item !== null);
295
+ }
296
+ function parseRankingFromConfig(configText) {
297
+ if (!configText) {
298
+ return DEFAULT_RANKING;
299
+ }
300
+ const ranking = { ...DEFAULT_RANKING };
301
+ const lines = configText.split(/\r?\n/);
302
+ let inRanking = false;
303
+ for (const line of lines) {
304
+ if (!inRanking && /^\s*ranking:\s*$/.test(line)) {
305
+ inRanking = true;
306
+ continue;
307
+ }
308
+ if (!inRanking) {
309
+ continue;
310
+ }
311
+ const entry = line.match(/^\s*(semantic|graph|trust|recency):\s*([0-9]*\.?[0-9]+)\s*$/);
312
+ if (entry) {
313
+ const key = entry[1];
314
+ ranking[key] = Number(entry[2]);
315
+ continue;
316
+ }
317
+ if (line.trim() !== "" && !/^\s/.test(line)) {
318
+ break;
319
+ }
320
+ }
321
+ return ranking;
322
+ }
323
+ async function queryRows(connection, statement) {
324
+ const result = await connection.query(statement);
325
+ const resolved = Array.isArray(result) ? result[result.length - 1] : result;
326
+ return resolved.getAll();
327
+ }
328
+ function readGraphSignature() {
329
+ if (!fs.existsSync(DB_PATH)) {
330
+ return null;
331
+ }
332
+ try {
333
+ const dbStats = fs.statSync(DB_PATH);
334
+ const dbPart = `${Math.round(dbStats.mtimeMs)}:${dbStats.size}`;
335
+ let manifestPart = "none";
336
+ if (fs.existsSync(PATHS.graphManifest)) {
337
+ const manifestStats = fs.statSync(PATHS.graphManifest);
338
+ manifestPart = `${Math.round(manifestStats.mtimeMs)}:${manifestStats.size}`;
339
+ }
340
+ return `${dbPart}:${manifestPart}`;
341
+ }
342
+ catch {
343
+ return null;
344
+ }
345
+ }
346
+ function buildMissingDbMessage() {
347
+ const dbDir = path.dirname(DB_PATH);
348
+ const loadCommand = "cortex graph-load";
349
+ const bootstrapCommand = "cortex bootstrap";
350
+ if (!fs.existsSync(dbDir)) {
351
+ return `RyuGraph directory missing at ${dbDir}. Run ${bootstrapCommand}.`;
352
+ }
353
+ return `RyuGraph DB not found at ${DB_PATH}. Run ${loadCommand} (or ${bootstrapCommand} on cold start).`;
354
+ }
355
+ function buildIncompatibleGraphMessage(missingKeys) {
356
+ const loadCommand = "cortex graph-load";
357
+ const missing = missingKeys.join(", ");
358
+ return `RyuGraph manifest is missing schema keys (${missing}). Run ${loadCommand} to rebuild the graph DB.`;
359
+ }
360
+ function readGraphManifestMissingKeys() {
361
+ if (!fs.existsSync(PATHS.graphManifest)) {
362
+ return [...REQUIRED_GRAPH_MANIFEST_COUNT_KEYS];
363
+ }
364
+ try {
365
+ const raw = JSON.parse(fs.readFileSync(PATHS.graphManifest, "utf8"));
366
+ const counts = raw.counts ?? {};
367
+ return REQUIRED_GRAPH_MANIFEST_COUNT_KEYS.filter((key) => !(key in counts));
368
+ }
369
+ catch {
370
+ return [...REQUIRED_GRAPH_MANIFEST_COUNT_KEYS];
371
+ }
372
+ }
373
+ async function closeRyuGraphResources() {
374
+ const currentConnection = ryuConnection;
375
+ const currentDb = ryuDb;
376
+ ryuConnection = null;
377
+ ryuDb = null;
378
+ ryuGraphSignature = null;
379
+ if (currentConnection) {
380
+ try {
381
+ await currentConnection.close();
382
+ }
383
+ catch {
384
+ // Ignore close errors during refresh/reset.
385
+ }
386
+ }
387
+ if (currentDb) {
388
+ try {
389
+ await currentDb.close();
390
+ }
391
+ catch {
392
+ // Ignore close errors during refresh/reset.
393
+ }
394
+ }
395
+ }
396
+ async function resetRyuGraphState(errorMessage) {
397
+ ryuInitError = errorMessage;
398
+ await closeRyuGraphResources();
399
+ }
400
+ async function getRyuGraphConnection(forceReload = false) {
401
+ const diskSignature = readGraphSignature();
402
+ if (ryuConnection) {
403
+ if (forceReload) {
404
+ await closeRyuGraphResources();
405
+ ryuLastInitAttemptAt = 0;
406
+ }
407
+ else if (diskSignature && ryuGraphSignature && diskSignature === ryuGraphSignature) {
408
+ return ryuConnection;
409
+ }
410
+ else {
411
+ await resetRyuGraphState("RyuGraph graph changed on disk; reconnecting.");
412
+ ryuLastInitAttemptAt = 0;
413
+ }
414
+ }
415
+ const now = Date.now();
416
+ if (!forceReload && now - ryuLastInitAttemptAt < RYU_INIT_RETRY_INTERVAL_MS) {
417
+ return null;
418
+ }
419
+ ryuLastInitAttemptAt = now;
420
+ if (!diskSignature) {
421
+ await resetRyuGraphState(buildMissingDbMessage());
422
+ return null;
423
+ }
424
+ const missingManifestKeys = readGraphManifestMissingKeys();
425
+ if (missingManifestKeys.length > 0) {
426
+ await resetRyuGraphState(buildIncompatibleGraphMessage(missingManifestKeys));
427
+ return null;
428
+ }
429
+ try {
430
+ const nextDb = new ryugraph.Database(DB_PATH, undefined, undefined, true);
431
+ const nextConnection = new ryugraph.Connection(nextDb);
432
+ await nextDb.init();
433
+ await nextConnection.init();
434
+ ryuDb = nextDb;
435
+ ryuConnection = nextConnection;
436
+ ryuGraphSignature = readGraphSignature() ?? diskSignature;
437
+ ryuInitError = null;
438
+ return nextConnection;
439
+ }
440
+ catch (error) {
441
+ await resetRyuGraphState(error instanceof Error ? error.message : "Failed to initialize RyuGraph");
442
+ return null;
443
+ }
444
+ }
445
+ function parseRyuGraphDocuments(rows, contentById) {
446
+ return rows
447
+ .map((row) => {
448
+ const id = asStringUnknown(row.id);
449
+ const filePath = asStringUnknown(row.path);
450
+ if (!id || !filePath) {
451
+ return null;
452
+ }
453
+ const kindRaw = asStringUnknown(row.kind, "DOC").toUpperCase();
454
+ const kind = kindRaw === "CODE" ? "CODE" : kindRaw === "ADR" ? "ADR" : "DOC";
455
+ return {
456
+ id,
457
+ path: filePath,
458
+ kind,
459
+ updated_at: asStringUnknown(row.updated_at),
460
+ source_of_truth: asBooleanUnknown(row.source_of_truth, false),
461
+ trust_level: asNumberUnknown(row.trust_level, 50),
462
+ status: asStringUnknown(row.status, "active"),
463
+ excerpt: asStringUnknown(row.excerpt),
464
+ content: contentById.get(id) ?? ""
465
+ };
466
+ })
467
+ .filter((value) => value !== null);
468
+ }
469
+ function parseRyuGraphRules(rows) {
470
+ return rows
471
+ .map((row) => {
472
+ const id = asStringUnknown(row.id);
473
+ if (!id) {
474
+ return null;
475
+ }
476
+ return {
477
+ id,
478
+ title: asStringUnknown(row.title, id),
479
+ body: asStringUnknown(row.body),
480
+ scope: asStringUnknown(row.scope, "global"),
481
+ updated_at: asStringUnknown(row.updated_at),
482
+ source_of_truth: asBooleanUnknown(row.source_of_truth, true),
483
+ trust_level: asNumberUnknown(row.trust_level, 95),
484
+ status: asStringUnknown(row.status, "active"),
485
+ priority: asNumberUnknown(row.priority, 0)
486
+ };
487
+ })
488
+ .filter((value) => value !== null);
489
+ }
490
+ function parseRyuGraphAdrs(rows) {
491
+ return rows
492
+ .map((row) => {
493
+ const id = asStringUnknown(row.id);
494
+ if (!id) {
495
+ return null;
496
+ }
497
+ return {
498
+ id,
499
+ path: asStringUnknown(row.path),
500
+ title: asStringUnknown(row.title, id),
501
+ body: asStringUnknown(row.body),
502
+ decision_date: asStringUnknown(row.decision_date),
503
+ supersedes_id: asStringUnknown(row.supersedes_id),
504
+ source_of_truth: asBooleanUnknown(row.source_of_truth, true),
505
+ trust_level: asNumberUnknown(row.trust_level, 95),
506
+ status: asStringUnknown(row.status, "active")
507
+ };
508
+ })
509
+ .filter((value) => value !== null);
510
+ }
511
+ function parseRyuGraphChunks(rows) {
512
+ return rows
513
+ .map((row) => {
514
+ const id = asStringUnknown(row.id);
515
+ if (!id) {
516
+ return null;
517
+ }
518
+ return {
519
+ id,
520
+ file_id: asStringUnknown(row.file_id),
521
+ name: asStringUnknown(row.name),
522
+ kind: asStringUnknown(row.kind, "chunk"),
523
+ signature: asStringUnknown(row.signature),
524
+ body: asStringUnknown(row.body),
525
+ description: asStringUnknown(row.description),
526
+ start_line: asNumberUnknown(row.start_line),
527
+ end_line: asNumberUnknown(row.end_line),
528
+ language: asStringUnknown(row.language),
529
+ exported: asBooleanUnknown(row.exported, false),
530
+ updated_at: asStringUnknown(row.updated_at),
531
+ source_of_truth: asBooleanUnknown(row.source_of_truth, false),
532
+ trust_level: asNumberUnknown(row.trust_level, 60),
533
+ status: asStringUnknown(row.status, "active")
534
+ };
535
+ })
536
+ .filter((value) => value !== null);
537
+ }
538
+ function parseRyuGraphModules(rows) {
539
+ return rows
540
+ .map((row) => {
541
+ const id = asStringUnknown(row.id);
542
+ if (!id) {
543
+ return null;
544
+ }
545
+ return {
546
+ id,
547
+ path: asStringUnknown(row.path),
548
+ name: asStringUnknown(row.name),
549
+ summary: asStringUnknown(row.summary),
550
+ file_count: asNumberUnknown(row.file_count, 0),
551
+ exported_symbols: asStringUnknown(row.exported_symbols),
552
+ updated_at: asStringUnknown(row.updated_at),
553
+ source_of_truth: asBooleanUnknown(row.source_of_truth, false),
554
+ trust_level: asNumberUnknown(row.trust_level, 75),
555
+ status: asStringUnknown(row.status, "active")
556
+ };
557
+ })
558
+ .filter((value) => value !== null);
559
+ }
560
+ function parseRyuGraphProjects(rows) {
561
+ return rows
562
+ .map((row) => {
563
+ const id = asStringUnknown(row.id);
564
+ if (!id) {
565
+ return null;
566
+ }
567
+ return {
568
+ id,
569
+ path: asStringUnknown(row.path),
570
+ name: asStringUnknown(row.name),
571
+ kind: asStringUnknown(row.kind, "project"),
572
+ language: asStringUnknown(row.language, "dotnet"),
573
+ target_framework: asStringUnknown(row.target_framework),
574
+ summary: asStringUnknown(row.summary),
575
+ file_count: asNumberUnknown(row.file_count, 0),
576
+ updated_at: asStringUnknown(row.updated_at),
577
+ source_of_truth: asBooleanUnknown(row.source_of_truth, false),
578
+ trust_level: asNumberUnknown(row.trust_level, 80),
579
+ status: asStringUnknown(row.status, "active")
580
+ };
581
+ })
582
+ .filter((value) => value !== null);
583
+ }
584
+ function parseRyuGraphRelations(rows, relation, noteField) {
585
+ return rows
586
+ .map((row) => {
587
+ const from = asStringUnknown(row.from);
588
+ const to = asStringUnknown(row.to);
589
+ if (!from || !to) {
590
+ return null;
591
+ }
592
+ return {
593
+ from,
594
+ to,
595
+ relation,
596
+ note: asStringUnknown(row[noteField])
597
+ };
598
+ })
599
+ .filter((value) => value !== null);
600
+ }
601
+ export async function loadContextData() {
602
+ const ranking = parseRankingFromConfig(readFileIfExists(PATHS.config));
603
+ const cachedDocuments = parseDocuments(readJsonl(PATHS.documents));
604
+ const cachedAdrs = parseAdrs(readJsonl(PATHS.adrEntities));
605
+ const cachedChunks = parseChunkEntities(readJsonl(PATHS.chunkEntities));
606
+ const cachedModules = parseModuleEntities(readJsonl(PATHS.moduleEntities));
607
+ const cachedProjects = parseProjectEntities(readJsonl(PATHS.projectEntities));
608
+ const cachedChunkRelations = [
609
+ ...parseRelations(readJsonl(PATHS.definesRelations), "DEFINES"),
610
+ ...parseRelations(readJsonl(PATHS.callsRelations), "CALLS", ["call_type"]),
611
+ ...parseRelations(readJsonl(PATHS.importsRelations), "IMPORTS", ["import_name"]),
612
+ ...parseRelations(readJsonl(PATHS.callsSqlRelations), "CALLS_SQL"),
613
+ ...parseRelations(readJsonl(PATHS.usesConfigKeyRelations), "USES_CONFIG_KEY"),
614
+ ...parseRelations(readJsonl(PATHS.usesResourceKeyRelations), "USES_RESOURCE_KEY"),
615
+ ...parseRelations(readJsonl(PATHS.usesSettingKeyRelations), "USES_SETTING_KEY")
616
+ ];
617
+ const cachedModuleRelations = [
618
+ ...parseRelations(readJsonl(PATHS.containsRelations), "CONTAINS"),
619
+ ...parseRelations(readJsonl(PATHS.containsModuleRelations), "CONTAINS_MODULE"),
620
+ ...parseRelations(readJsonl(PATHS.exportsRelations), "EXPORTS")
621
+ ];
622
+ const cachedProjectRelations = [
623
+ ...parseRelations(readJsonl(PATHS.includesFileRelations), "INCLUDES_FILE"),
624
+ ...parseRelations(readJsonl(PATHS.referencesProjectRelations), "REFERENCES_PROJECT"),
625
+ ...parseRelations(readJsonl(PATHS.usesResourceRelations), "USES_RESOURCE"),
626
+ ...parseRelations(readJsonl(PATHS.usesSettingRelations), "USES_SETTING"),
627
+ ...parseRelations(readJsonl(PATHS.usesConfigRelations), "USES_CONFIG"),
628
+ ...parseRelations(readJsonl(PATHS.transformsConfigRelations), "TRANSFORMS_CONFIG")
629
+ ];
630
+ const cachedRelations = [
631
+ ...parseRelations(readJsonl(PATHS.constrainsRelations), "CONSTRAINS"),
632
+ ...parseRelations(readJsonl(PATHS.implementsRelations), "IMPLEMENTS"),
633
+ ...parseRelations(readJsonl(PATHS.supersedesRelations), "SUPERSEDES"),
634
+ ...cachedChunkRelations,
635
+ ...cachedModuleRelations,
636
+ ...cachedProjectRelations
637
+ ];
638
+ const yamlRules = parseRulesYaml(readFileIfExists(PATHS.rulesYaml));
639
+ const entityRules = parseRuleEntities(readJsonl(PATHS.ruleEntities));
640
+ const cachedRules = yamlRules.length > 0 ? yamlRules : entityRules;
641
+ const connection = await getRyuGraphConnection();
642
+ if (!connection) {
643
+ return {
644
+ documents: cachedDocuments,
645
+ adrs: cachedAdrs,
646
+ rules: cachedRules,
647
+ chunks: cachedChunks,
648
+ modules: cachedModules,
649
+ projects: cachedProjects,
650
+ relations: cachedRelations,
651
+ ranking,
652
+ source: "cache",
653
+ warning: ryuInitError ?? "RyuGraph DB is not loaded yet."
654
+ };
655
+ }
656
+ try {
657
+ const ryuQueries = await Promise.all([
658
+ 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;`),
659
+ 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;`),
660
+ 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;`),
661
+ 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;`),
662
+ 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;`),
663
+ 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;`),
664
+ queryRows(connection, `MATCH (r:Rule)-[c:CONSTRAINS]->(f:File) RETURN r.id AS from, f.id AS to, c.note AS note;`),
665
+ queryRows(connection, `MATCH (f:File)-[i:IMPLEMENTS]->(r:Rule) RETURN f.id AS from, r.id AS to, i.note AS note;`),
666
+ queryRows(connection, `MATCH (a1:ADR)-[s:SUPERSEDES]->(a2:ADR) RETURN a1.id AS from, a2.id AS to, s.reason AS note;`),
667
+ queryRows(connection, `MATCH (f:File)-[:DEFINES]->(c:Chunk) RETURN f.id AS from, c.id AS to;`),
668
+ queryRows(connection, `MATCH (c1:Chunk)-[ca:CALLS]->(c2:Chunk) RETURN c1.id AS from, c2.id AS to, ca.call_type AS call_type;`),
669
+ queryRows(connection, `MATCH (c:Chunk)-[im:IMPORTS]->(f:File) RETURN c.id AS from, f.id AS to, im.import_name AS import_name;`),
670
+ queryRows(connection, `MATCH (f:File)-[cs:CALLS_SQL]->(c:Chunk) RETURN f.id AS from, c.id AS to, cs.note AS note;`),
671
+ queryRows(connection, `MATCH (f:File)-[uck:USES_CONFIG_KEY]->(c:Chunk) RETURN f.id AS from, c.id AS to, uck.note AS note;`),
672
+ queryRows(connection, `MATCH (f:File)-[urk:USES_RESOURCE_KEY]->(c:Chunk) RETURN f.id AS from, c.id AS to, urk.note AS note;`),
673
+ queryRows(connection, `MATCH (f:File)-[usk:USES_SETTING_KEY]->(c:Chunk) RETURN f.id AS from, c.id AS to, usk.note AS note;`),
674
+ queryRows(connection, `MATCH (m:Module)-[:CONTAINS]->(f:File) RETURN m.id AS from, f.id AS to;`),
675
+ queryRows(connection, `MATCH (m1:Module)-[:CONTAINS_MODULE]->(m2:Module) RETURN m1.id AS from, m2.id AS to;`),
676
+ queryRows(connection, `MATCH (m:Module)-[:EXPORTS]->(c:Chunk) RETURN m.id AS from, c.id AS to;`),
677
+ queryRows(connection, `MATCH (p:Project)-[:INCLUDES_FILE]->(f:File) RETURN p.id AS from, f.id AS to;`),
678
+ queryRows(connection, `MATCH (p1:Project)-[rp:REFERENCES_PROJECT]->(p2:Project) RETURN p1.id AS from, p2.id AS to, rp.note AS note;`),
679
+ queryRows(connection, `MATCH (f1:File)-[ur:USES_RESOURCE]->(f2:File) RETURN f1.id AS from, f2.id AS to, ur.note AS note;`),
680
+ queryRows(connection, `MATCH (f1:File)-[us:USES_SETTING]->(f2:File) RETURN f1.id AS from, f2.id AS to, us.note AS note;`),
681
+ queryRows(connection, `MATCH (f1:File)-[uc:USES_CONFIG]->(f2:File) RETURN f1.id AS from, f2.id AS to, uc.note AS note;`),
682
+ queryRows(connection, `MATCH (f1:File)-[tc:TRANSFORMS_CONFIG]->(f2:File) RETURN f1.id AS from, f2.id AS to, tc.note AS note;`)
683
+ ]);
684
+ // Named destructuring to avoid positional misalignment with 14 parallel queries
685
+ const [fileRows, ruleRows, adrRows, chunkRows, moduleRows, projectRows, // entities
686
+ constrainsRows, implementsRows, supersedesRows, // core relations
687
+ definesRows, callsRows, importsRows, callsSqlRows, usesConfigKeyRows, usesResourceKeyRows, usesSettingKeyRows, // chunk relations
688
+ containsRows, containsModuleRows, exportsRows, // module relations
689
+ includesFileRows, referencesProjectRows, usesResourceRows, usesSettingRows, usesConfigRows, transformsConfigRows // project/file relations
690
+ ] = ryuQueries;
691
+ const contentById = new Map(cachedDocuments.map((doc) => [doc.id, doc.content]));
692
+ const ryuDocuments = parseRyuGraphDocuments(fileRows, contentById);
693
+ const ryuRules = parseRyuGraphRules(ruleRows);
694
+ const ryuAdrs = parseRyuGraphAdrs(adrRows);
695
+ const ryuChunks = parseRyuGraphChunks(chunkRows);
696
+ const ryuModules = parseRyuGraphModules(moduleRows);
697
+ const ryuProjects = parseRyuGraphProjects(projectRows);
698
+ const ryuRelations = [
699
+ ...parseRyuGraphRelations(constrainsRows, "CONSTRAINS", "note"),
700
+ ...parseRyuGraphRelations(implementsRows, "IMPLEMENTS", "note"),
701
+ ...parseRyuGraphRelations(supersedesRows, "SUPERSEDES", "note"),
702
+ ...parseRyuGraphRelations(definesRows, "DEFINES", "note"),
703
+ ...parseRyuGraphRelations(callsRows, "CALLS", "call_type"),
704
+ ...parseRyuGraphRelations(importsRows, "IMPORTS", "import_name"),
705
+ ...parseRyuGraphRelations(callsSqlRows, "CALLS_SQL", "note"),
706
+ ...parseRyuGraphRelations(usesConfigKeyRows, "USES_CONFIG_KEY", "note"),
707
+ ...parseRyuGraphRelations(usesResourceKeyRows, "USES_RESOURCE_KEY", "note"),
708
+ ...parseRyuGraphRelations(usesSettingKeyRows, "USES_SETTING_KEY", "note"),
709
+ ...parseRyuGraphRelations(containsRows, "CONTAINS", "note"),
710
+ ...parseRyuGraphRelations(containsModuleRows, "CONTAINS_MODULE", "note"),
711
+ ...parseRyuGraphRelations(exportsRows, "EXPORTS", "note"),
712
+ ...parseRyuGraphRelations(includesFileRows, "INCLUDES_FILE", "note"),
713
+ ...parseRyuGraphRelations(referencesProjectRows, "REFERENCES_PROJECT", "note"),
714
+ ...parseRyuGraphRelations(usesResourceRows, "USES_RESOURCE", "note"),
715
+ ...parseRyuGraphRelations(usesSettingRows, "USES_SETTING", "note"),
716
+ ...parseRyuGraphRelations(usesConfigRows, "USES_CONFIG", "note"),
717
+ ...parseRyuGraphRelations(transformsConfigRows, "TRANSFORMS_CONFIG", "note")
718
+ ];
719
+ return {
720
+ documents: ryuDocuments.length > 0 ? ryuDocuments : cachedDocuments,
721
+ adrs: ryuAdrs.length > 0 ? ryuAdrs : cachedAdrs,
722
+ rules: ryuRules.length > 0 ? ryuRules : cachedRules,
723
+ chunks: ryuChunks.length > 0 ? ryuChunks : cachedChunks,
724
+ modules: ryuModules.length > 0 ? ryuModules : cachedModules,
725
+ projects: ryuProjects.length > 0 ? ryuProjects : cachedProjects,
726
+ // Ryu now queries all relation types (core + chunk + module), so no need
727
+ // to merge cached chunk/module relations separately.
728
+ relations: ryuRelations.length > 0 ? ryuRelations : cachedRelations,
729
+ ranking,
730
+ source: "ryu"
731
+ };
732
+ }
733
+ catch (error) {
734
+ const message = error instanceof Error
735
+ ? `RyuGraph query failed, using cache fallback: ${error.message}`
736
+ : "RyuGraph query failed, using cache fallback.";
737
+ await resetRyuGraphState(message);
738
+ return {
739
+ documents: cachedDocuments,
740
+ adrs: cachedAdrs,
741
+ rules: cachedRules,
742
+ chunks: cachedChunks,
743
+ modules: cachedModules,
744
+ projects: cachedProjects,
745
+ relations: cachedRelations,
746
+ ranking,
747
+ source: "cache",
748
+ warning: message
749
+ };
750
+ }
751
+ }
752
+ export async function reloadContextGraph(force = true) {
753
+ const previousSignature = ryuGraphSignature;
754
+ if (force || ryuConnection) {
755
+ await closeRyuGraphResources();
756
+ }
757
+ ryuInitError = null;
758
+ ryuLastInitAttemptAt = 0;
759
+ const nextConnection = await getRyuGraphConnection(true);
760
+ const currentSignature = readGraphSignature();
761
+ return {
762
+ forced: force,
763
+ reloaded: nextConnection !== null,
764
+ context_source: nextConnection ? "ryu" : "cache",
765
+ previous_graph_signature: previousSignature,
766
+ current_graph_signature: currentSignature,
767
+ warning: nextConnection ? undefined : ryuInitError ?? buildMissingDbMessage()
768
+ };
769
+ }