@danielblomma/cortex-mcp 2.4.0 → 2.4.2

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 (214) hide show
  1. package/CHANGELOG.md +226 -0
  2. package/README.md +24 -3
  3. package/bin/cli/arguments.mjs +60 -0
  4. package/bin/cli/context-passthrough.mjs +129 -0
  5. package/bin/cli/daemon.mjs +157 -0
  6. package/bin/cli/enterprise.mjs +516 -0
  7. package/bin/cli/help.mjs +88 -0
  8. package/bin/cli/hooks.mjs +199 -0
  9. package/bin/cli/mcp-command.mjs +87 -0
  10. package/bin/cli/paths.mjs +14 -0
  11. package/bin/cli/process.mjs +49 -0
  12. package/bin/cli/project-commands.mjs +237 -0
  13. package/bin/cli/project-runtime.mjs +34 -0
  14. package/bin/cli/query-command.mjs +18 -0
  15. package/bin/cli/router.mjs +66 -0
  16. package/bin/cli/run-command.mjs +44 -0
  17. package/bin/cli/scaffold-ownership.mjs +936 -0
  18. package/bin/cli/scaffold.mjs +687 -0
  19. package/bin/cli/stage-command.mjs +9 -0
  20. package/bin/cli/telemetry-command.mjs +18 -0
  21. package/bin/cli/trusted-runtime.mjs +18 -0
  22. package/bin/cortex.mjs +12 -1607
  23. package/bin/daemon-control.mjs +162 -0
  24. package/mcp-registry-submission.json +1 -1
  25. package/package.json +8 -3
  26. package/scaffold/mcp/dist/.cortex-build-hash +1 -0
  27. package/scaffold/mcp/dist/cli/enterprise-setup.js +134 -0
  28. package/scaffold/mcp/dist/cli/govern.js +937 -0
  29. package/scaffold/mcp/dist/cli/query.js +409 -0
  30. package/scaffold/mcp/dist/cli/run.js +295 -0
  31. package/scaffold/mcp/dist/cli/stage.js +308 -0
  32. package/scaffold/mcp/dist/cli/telemetry-test.js +141 -0
  33. package/scaffold/mcp/dist/cli/ungoverned-detector.js +133 -0
  34. package/scaffold/mcp/dist/contextEntities.js +282 -0
  35. package/scaffold/mcp/dist/core/audit/query.js +72 -0
  36. package/scaffold/mcp/dist/core/audit/writer.js +28 -0
  37. package/scaffold/mcp/dist/core/config.js +235 -0
  38. package/scaffold/mcp/dist/core/enterprise-host-identity.js +193 -0
  39. package/scaffold/mcp/dist/core/enterprise-identity.js +18 -0
  40. package/scaffold/mcp/dist/core/enterprise-rotation.js +158 -0
  41. package/scaffold/mcp/dist/core/govern-paths.js +21 -0
  42. package/scaffold/mcp/dist/core/index.js +14 -0
  43. package/scaffold/mcp/dist/core/license.js +278 -0
  44. package/scaffold/mcp/dist/core/policy/enforce.js +66 -0
  45. package/scaffold/mcp/dist/core/policy/injection.js +172 -0
  46. package/scaffold/mcp/dist/core/policy/store.js +179 -0
  47. package/scaffold/mcp/dist/core/rbac/check.js +30 -0
  48. package/scaffold/mcp/dist/core/secure-endpoint.js +25 -0
  49. package/scaffold/mcp/dist/core/telemetry/collector.js +285 -0
  50. package/scaffold/mcp/dist/core/telemetry/state-dir.js +31 -0
  51. package/scaffold/mcp/dist/core/validators/builtins.js +632 -0
  52. package/scaffold/mcp/dist/core/validators/config.js +44 -0
  53. package/scaffold/mcp/dist/core/validators/engine.js +120 -0
  54. package/scaffold/mcp/dist/core/validators/evaluators/code_comments.js +236 -0
  55. package/scaffold/mcp/dist/core/validators/evaluators/regex.js +116 -0
  56. package/scaffold/mcp/dist/core/workflow/artifact-io.js +103 -0
  57. package/scaffold/mcp/dist/core/workflow/capabilities.js +88 -0
  58. package/scaffold/mcp/dist/core/workflow/default-workflows.js +102 -0
  59. package/scaffold/mcp/dist/core/workflow/enforcement.js +164 -0
  60. package/scaffold/mcp/dist/core/workflow/envelope.js +132 -0
  61. package/scaffold/mcp/dist/core/workflow/index.js +11 -0
  62. package/scaffold/mcp/dist/core/workflow/mcp-tools.js +175 -0
  63. package/scaffold/mcp/dist/core/workflow/resolution.js +69 -0
  64. package/scaffold/mcp/dist/core/workflow/run-lifecycle.js +123 -0
  65. package/scaffold/mcp/dist/core/workflow/schemas.js +141 -0
  66. package/scaffold/mcp/dist/core/workflow/synced-capability-registry.js +60 -0
  67. package/scaffold/mcp/dist/core/workflow/synced-registry.js +60 -0
  68. package/scaffold/mcp/dist/daemon/capability-sync-checker.js +242 -0
  69. package/scaffold/mcp/dist/daemon/client.js +130 -0
  70. package/scaffold/mcp/dist/daemon/egress-proxy.js +288 -0
  71. package/scaffold/mcp/dist/daemon/global-host-events.js +222 -0
  72. package/scaffold/mcp/dist/daemon/heartbeat-pusher.js +116 -0
  73. package/scaffold/mcp/dist/daemon/heartbeat-tracker.js +165 -0
  74. package/scaffold/mcp/dist/daemon/host-events-pusher.js +249 -0
  75. package/scaffold/mcp/dist/daemon/main.js +449 -0
  76. package/scaffold/mcp/dist/daemon/paths.js +36 -0
  77. package/scaffold/mcp/dist/daemon/project-service-registry.js +78 -0
  78. package/scaffold/mcp/dist/daemon/protocol.js +8 -0
  79. package/scaffold/mcp/dist/daemon/server.js +180 -0
  80. package/scaffold/mcp/dist/daemon/skill-sync-checker.js +557 -0
  81. package/scaffold/mcp/dist/daemon/sync-checker.js +165 -0
  82. package/scaffold/mcp/dist/daemon/ungoverned-scanner.js +136 -0
  83. package/scaffold/mcp/dist/daemon/workflow-sync-checker.js +249 -0
  84. package/scaffold/mcp/dist/defaults.js +6 -0
  85. package/scaffold/mcp/dist/embed.js +627 -0
  86. package/scaffold/mcp/dist/embedScheduler.js +479 -0
  87. package/scaffold/mcp/dist/embeddings.js +167 -0
  88. package/scaffold/mcp/dist/enterprise/audit/push.js +66 -0
  89. package/scaffold/mcp/dist/enterprise/index.js +327 -0
  90. package/scaffold/mcp/dist/enterprise/model/deploy.js +27 -0
  91. package/scaffold/mcp/dist/enterprise/policy/sync.js +129 -0
  92. package/scaffold/mcp/dist/enterprise/privacy/boundary.js +184 -0
  93. package/scaffold/mcp/dist/enterprise/reviews/changed-files.js +33 -0
  94. package/scaffold/mcp/dist/enterprise/reviews/pattern-context.js +202 -0
  95. package/scaffold/mcp/dist/enterprise/reviews/policy-selection.js +46 -0
  96. package/scaffold/mcp/dist/enterprise/reviews/push.js +102 -0
  97. package/scaffold/mcp/dist/enterprise/reviews/trust-state.js +186 -0
  98. package/scaffold/mcp/dist/enterprise/telemetry/sync.js +57 -0
  99. package/scaffold/mcp/dist/enterprise/tools/enterprise.js +826 -0
  100. package/scaffold/mcp/dist/enterprise/tools/harness.js +40 -0
  101. package/scaffold/mcp/dist/enterprise/tools/walk.js +73 -0
  102. package/scaffold/mcp/dist/enterprise/violations/push.js +77 -0
  103. package/scaffold/mcp/dist/enterprise/workflow/push.js +44 -0
  104. package/scaffold/mcp/dist/enterprise/workflow/state.js +329 -0
  105. package/scaffold/mcp/dist/frontmatter.js +33 -0
  106. package/scaffold/mcp/dist/graph.js +769 -0
  107. package/scaffold/mcp/dist/graphCsv.js +55 -0
  108. package/scaffold/mcp/dist/graphMetrics.js +8 -0
  109. package/scaffold/mcp/dist/hooks/permission-request.js +89 -0
  110. package/scaffold/mcp/dist/hooks/post-tool-use.js +105 -0
  111. package/scaffold/mcp/dist/hooks/pre-compact.js +29 -0
  112. package/scaffold/mcp/dist/hooks/pre-tool-use.js +78 -0
  113. package/scaffold/mcp/dist/hooks/session-end.js +43 -0
  114. package/scaffold/mcp/dist/hooks/session-start.js +41 -0
  115. package/scaffold/mcp/dist/hooks/shared.js +194 -0
  116. package/scaffold/mcp/dist/hooks/stop.js +28 -0
  117. package/scaffold/mcp/dist/hooks/user-prompt-submit.js +33 -0
  118. package/scaffold/mcp/dist/impactPresentation.js +137 -0
  119. package/scaffold/mcp/dist/impactRanking.js +191 -0
  120. package/scaffold/mcp/dist/impactResponse.js +30 -0
  121. package/scaffold/mcp/dist/impactResults.js +105 -0
  122. package/scaffold/mcp/dist/impactSeed.js +20 -0
  123. package/scaffold/mcp/dist/impactTraversal.js +64 -0
  124. package/scaffold/mcp/dist/jsonl.js +77 -0
  125. package/scaffold/mcp/dist/loadGraph.js +759 -0
  126. package/scaffold/mcp/dist/lruCache.js +38 -0
  127. package/scaffold/mcp/dist/paths.js +97 -0
  128. package/scaffold/mcp/dist/patternEvidence.js +272 -0
  129. package/scaffold/mcp/dist/plugin.js +81 -0
  130. package/scaffold/mcp/dist/presets.js +78 -0
  131. package/scaffold/mcp/dist/relatedResponse.js +18 -0
  132. package/scaffold/mcp/dist/relatedTraversal.js +78 -0
  133. package/scaffold/mcp/dist/rules.js +23 -0
  134. package/scaffold/mcp/dist/search.js +212 -0
  135. package/scaffold/mcp/dist/searchCore.js +457 -0
  136. package/scaffold/mcp/dist/searchResults.js +230 -0
  137. package/scaffold/mcp/dist/server.js +317 -0
  138. package/scaffold/mcp/dist/types.js +1 -0
  139. package/scaffold/mcp/package-lock.json +321 -200
  140. package/scaffold/mcp/package.json +15 -6
  141. package/scaffold/mcp/src/cli/enterprise-setup.ts +82 -8
  142. package/scaffold/mcp/src/cli/govern.ts +137 -52
  143. package/scaffold/mcp/src/cli/run.ts +53 -19
  144. package/scaffold/mcp/src/cli/stage.ts +8 -3
  145. package/scaffold/mcp/src/core/config.ts +9 -2
  146. package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
  147. package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
  148. package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
  149. package/scaffold/mcp/src/core/govern-paths.ts +30 -0
  150. package/scaffold/mcp/src/core/license.ts +186 -17
  151. package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
  152. package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
  153. package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
  154. package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
  155. package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
  156. package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
  157. package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
  158. package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
  159. package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
  160. package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
  161. package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
  162. package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
  163. package/scaffold/mcp/src/daemon/main.ts +99 -25
  164. package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
  165. package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
  166. package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
  167. package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
  168. package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
  169. package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
  170. package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
  171. package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
  172. package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
  173. package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
  174. package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
  175. package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
  176. package/scaffold/mcp/src/plugin.ts +20 -0
  177. package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
  178. package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
  179. package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
  180. package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
  181. package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
  182. package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
  183. package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
  184. package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
  185. package/scaffold/mcp/tests/license.test.mjs +367 -0
  186. package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
  187. package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
  188. package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
  189. package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
  190. package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
  191. package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
  192. package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
  193. package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +40 -1
  194. package/scaffold/ownership/baseline-v2.4.1.json +22 -0
  195. package/scaffold/ownership/current.json +4 -0
  196. package/scaffold/ownership/v1.json +456 -0
  197. package/scaffold/scripts/ingest-parsers.mjs +1 -387
  198. package/scaffold/scripts/ingest-worker.mjs +4 -1
  199. package/scaffold/scripts/ingest.mjs +5 -3899
  200. package/scaffold/scripts/lib/ingest/arguments.mjs +78 -0
  201. package/scaffold/scripts/lib/ingest/chunks.mjs +212 -0
  202. package/scaffold/scripts/lib/ingest/config.mjs +120 -0
  203. package/scaffold/scripts/lib/ingest/constants.mjs +222 -0
  204. package/scaffold/scripts/lib/ingest/files.mjs +387 -0
  205. package/scaffold/scripts/lib/ingest/incremental-state.mjs +131 -0
  206. package/scaffold/scripts/lib/ingest/io.mjs +78 -0
  207. package/scaffold/scripts/lib/ingest/main.mjs +76 -0
  208. package/scaffold/scripts/lib/ingest/parser-composition.mjs +140 -0
  209. package/scaffold/scripts/lib/ingest/parser-registry.mjs +387 -0
  210. package/scaffold/scripts/lib/ingest/pipeline-stages.mjs +1625 -0
  211. package/scaffold/scripts/lib/ingest/projects.mjs +272 -0
  212. package/scaffold/scripts/lib/ingest/relations.mjs +860 -0
  213. package/scaffold/scripts/lib/ingest/runtime-paths.mjs +11 -0
  214. package/scaffold/scripts/lib/ingest/workers.mjs +264 -0
@@ -0,0 +1,759 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import ryugraph from "ryugraph";
4
+ import { readJsonl, readJsonlRecords, asString, asNumber, asBoolean } from "./jsonl.js";
5
+ import { CACHE_DIR, CONTEXT_DIR, DB_PATH } from "./paths.js";
6
+ import { CSV_COPY_OPTIONS, writeCsv, toCopyPathLiteral } from "./graphCsv.js";
7
+ const ONTOLOGY_PATH = path.join(CONTEXT_DIR, "ontology.cypher");
8
+ const GRAPH_IMPORT_DIR = path.join(CACHE_DIR, "graph-import");
9
+ const BATCH_SIZE = 50;
10
+ async function executeBatch(conn, statement, items, batchSize = BATCH_SIZE) {
11
+ for (let i = 0; i < items.length; i += batchSize) {
12
+ const batch = items.slice(i, i + batchSize);
13
+ await Promise.all(batch.map((item) => conn.execute(statement, item)));
14
+ }
15
+ }
16
+ function readEntityFile(fileName) {
17
+ return readJsonl(path.join(CACHE_DIR, fileName));
18
+ }
19
+ function readEntityRecords(fileName) {
20
+ return readJsonlRecords(path.join(CACHE_DIR, fileName));
21
+ }
22
+ function* parseFiles(raw) {
23
+ for (const item of raw) {
24
+ const id = asString(item.id);
25
+ const filePath = asString(item.path);
26
+ if (!id || !filePath) {
27
+ continue;
28
+ }
29
+ yield {
30
+ id,
31
+ path: filePath,
32
+ kind: asString(item.kind, "DOC"),
33
+ excerpt: asString(item.excerpt),
34
+ checksum: asString(item.checksum),
35
+ updated_at: asString(item.updated_at),
36
+ source_of_truth: asBoolean(item.source_of_truth, false),
37
+ trust_level: asNumber(item.trust_level, 50),
38
+ status: asString(item.status, "active")
39
+ };
40
+ }
41
+ }
42
+ function* parseRules(raw) {
43
+ for (const item of raw) {
44
+ const id = asString(item.id);
45
+ if (!id) {
46
+ continue;
47
+ }
48
+ yield {
49
+ id,
50
+ title: asString(item.title, id),
51
+ body: asString(item.body),
52
+ scope: asString(item.scope, "global"),
53
+ updated_at: asString(item.updated_at),
54
+ source_of_truth: asBoolean(item.source_of_truth, true),
55
+ trust_level: asNumber(item.trust_level, 95),
56
+ status: asString(item.status, "active"),
57
+ priority: asNumber(item.priority, 0)
58
+ };
59
+ }
60
+ }
61
+ function* parseAdrs(raw) {
62
+ for (const item of raw) {
63
+ const id = asString(item.id);
64
+ if (!id) {
65
+ continue;
66
+ }
67
+ yield {
68
+ id,
69
+ path: asString(item.path),
70
+ title: asString(item.title, id),
71
+ body: asString(item.body),
72
+ decision_date: asString(item.decision_date),
73
+ supersedes_id: asString(item.supersedes_id),
74
+ source_of_truth: asBoolean(item.source_of_truth, true),
75
+ trust_level: asNumber(item.trust_level, 95),
76
+ status: asString(item.status, "active")
77
+ };
78
+ }
79
+ }
80
+ function* parseChunks(raw) {
81
+ for (const item of raw) {
82
+ const id = asString(item.id);
83
+ const file_id = asString(item.file_id);
84
+ const name = asString(item.name);
85
+ if (!id || !file_id || !name) {
86
+ continue;
87
+ }
88
+ yield {
89
+ id,
90
+ file_id,
91
+ name,
92
+ kind: asString(item.kind, "function"),
93
+ signature: asString(item.signature),
94
+ body: asString(item.body),
95
+ description: asString(item.description),
96
+ start_line: asNumber(item.start_line, 0),
97
+ end_line: asNumber(item.end_line, 0),
98
+ language: asString(item.language, "javascript"),
99
+ exported: asBoolean(item.exported, false),
100
+ checksum: asString(item.checksum),
101
+ updated_at: asString(item.updated_at),
102
+ source_of_truth: asBoolean(item.source_of_truth, true),
103
+ trust_level: asNumber(item.trust_level, 80),
104
+ status: asString(item.status, "active")
105
+ };
106
+ }
107
+ }
108
+ function* parseModules(raw) {
109
+ for (const item of raw) {
110
+ const id = asString(item.id);
111
+ if (!id) {
112
+ continue;
113
+ }
114
+ yield {
115
+ id,
116
+ path: asString(item.path),
117
+ name: asString(item.name),
118
+ summary: asString(item.summary),
119
+ file_count: asNumber(item.file_count, 0),
120
+ exported_symbols: asString(item.exported_symbols),
121
+ updated_at: asString(item.updated_at),
122
+ source_of_truth: asBoolean(item.source_of_truth, false),
123
+ trust_level: asNumber(item.trust_level, 75),
124
+ status: asString(item.status, "active")
125
+ };
126
+ }
127
+ }
128
+ function* parseProjects(raw) {
129
+ for (const item of raw) {
130
+ const id = asString(item.id);
131
+ if (!id) {
132
+ continue;
133
+ }
134
+ yield {
135
+ id,
136
+ path: asString(item.path),
137
+ name: asString(item.name),
138
+ kind: asString(item.kind, "project"),
139
+ language: asString(item.language, "dotnet"),
140
+ target_framework: asString(item.target_framework),
141
+ summary: asString(item.summary),
142
+ file_count: asNumber(item.file_count, 0),
143
+ updated_at: asString(item.updated_at),
144
+ source_of_truth: asBoolean(item.source_of_truth, false),
145
+ trust_level: asNumber(item.trust_level, 80),
146
+ status: asString(item.status, "active")
147
+ };
148
+ }
149
+ }
150
+ function* parseRelationRecords(raw, noteField) {
151
+ for (const item of raw) {
152
+ const from = asString(item.from);
153
+ const to = asString(item.to);
154
+ if (!from || !to) {
155
+ continue;
156
+ }
157
+ yield {
158
+ from,
159
+ to,
160
+ note: asString(item[noteField])
161
+ };
162
+ }
163
+ }
164
+ function parseRelations(fileName, noteField) {
165
+ return Array.from(parseRelationRecords(readEntityRecords(fileName), noteField));
166
+ }
167
+ function* relationRecords(fileName, noteField) {
168
+ yield* parseRelationRecords(readEntityRecords(fileName), noteField);
169
+ }
170
+ function* parseCallRelationRecords(raw) {
171
+ for (const item of raw) {
172
+ const from = asString(item.from);
173
+ const to = asString(item.to);
174
+ if (!from || !to) {
175
+ continue;
176
+ }
177
+ yield {
178
+ from,
179
+ to,
180
+ call_type: asString(item.call_type, "direct")
181
+ };
182
+ }
183
+ }
184
+ function parseCallRelations(fileName) {
185
+ return Array.from(parseCallRelationRecords(readEntityRecords(fileName)));
186
+ }
187
+ function* callRelationRecords(fileName) {
188
+ yield* parseCallRelationRecords(readEntityRecords(fileName));
189
+ }
190
+ function* parseImportRelationRecords(raw) {
191
+ for (const item of raw) {
192
+ const from = asString(item.from);
193
+ const to = asString(item.to);
194
+ if (!from || !to) {
195
+ continue;
196
+ }
197
+ yield {
198
+ from,
199
+ to,
200
+ import_name: asString(item.import_name, "")
201
+ };
202
+ }
203
+ }
204
+ function parseImportRelations(fileName) {
205
+ return Array.from(parseImportRelationRecords(readEntityRecords(fileName)));
206
+ }
207
+ function* importRelationRecords(fileName) {
208
+ yield* parseImportRelationRecords(readEntityRecords(fileName));
209
+ }
210
+ function* parseSimpleRelationRecords(raw) {
211
+ for (const item of raw) {
212
+ const from = asString(item.from);
213
+ const to = asString(item.to);
214
+ if (!from || !to) {
215
+ continue;
216
+ }
217
+ yield {
218
+ from,
219
+ to,
220
+ note: ""
221
+ };
222
+ }
223
+ }
224
+ function parseSimpleRelations(fileName) {
225
+ return Array.from(parseSimpleRelationRecords(readEntityRecords(fileName)));
226
+ }
227
+ function* simpleRelationRecords(fileName) {
228
+ yield* parseSimpleRelationRecords(readEntityRecords(fileName));
229
+ }
230
+ async function rows(result) {
231
+ const resolved = Array.isArray(result) ? result[result.length - 1] : result;
232
+ return resolved.getAll();
233
+ }
234
+ function parseOntologyStatements(ontologyText) {
235
+ const withoutComments = ontologyText
236
+ .split(/\r?\n/)
237
+ .filter((line) => !line.trim().startsWith("//"))
238
+ .join("\n");
239
+ return withoutComments
240
+ .split(";")
241
+ .map((statement) => statement.trim())
242
+ .filter(Boolean)
243
+ .map((statement) => `${statement};`);
244
+ }
245
+ async function executeStatements(conn, statements) {
246
+ for (const statement of statements) {
247
+ await conn.query(statement);
248
+ }
249
+ }
250
+ async function ensureRequiredFiles() {
251
+ const required = [
252
+ path.join(CACHE_DIR, "entities.file.jsonl"),
253
+ path.join(CACHE_DIR, "entities.rule.jsonl"),
254
+ path.join(CACHE_DIR, "entities.adr.jsonl"),
255
+ path.join(CACHE_DIR, "relations.constrains.jsonl"),
256
+ path.join(CACHE_DIR, "relations.implements.jsonl"),
257
+ path.join(CACHE_DIR, "relations.supersedes.jsonl"),
258
+ ONTOLOGY_PATH
259
+ ];
260
+ for (const filePath of required) {
261
+ if (!fs.existsSync(filePath)) {
262
+ throw new Error(`Missing required file: ${filePath}`);
263
+ }
264
+ }
265
+ }
266
+ function warnIfOptionalFilesMissing() {
267
+ const optionalFiles = [
268
+ "entities.chunk.jsonl",
269
+ "relations.defines.jsonl",
270
+ "relations.calls.jsonl",
271
+ "relations.calls_sql.jsonl",
272
+ "relations.uses_config_key.jsonl",
273
+ "relations.uses_resource_key.jsonl",
274
+ "relations.uses_setting_key.jsonl",
275
+ "relations.imports.jsonl",
276
+ "entities.module.jsonl",
277
+ "entities.project.jsonl",
278
+ "relations.contains.jsonl",
279
+ "relations.contains_module.jsonl",
280
+ "relations.exports.jsonl",
281
+ "relations.includes_file.jsonl",
282
+ "relations.references_project.jsonl",
283
+ "relations.uses_resource.jsonl",
284
+ "relations.uses_setting.jsonl",
285
+ "relations.uses_config.jsonl",
286
+ "relations.transforms_config.jsonl"
287
+ ];
288
+ const missing = optionalFiles.filter((fileName) => !fs.existsSync(path.join(CACHE_DIR, fileName)));
289
+ if (missing.length === 0) {
290
+ return;
291
+ }
292
+ console.warn(`[graph-load] warning: missing optional files (${missing.join(", ")}); continuing without those nodes/relations`);
293
+ }
294
+ function parseGraphData() {
295
+ return {
296
+ fileEntities: Array.from(parseFiles(readEntityFile("entities.file.jsonl"))),
297
+ ruleEntities: Array.from(parseRules(readEntityFile("entities.rule.jsonl"))),
298
+ adrEntities: Array.from(parseAdrs(readEntityFile("entities.adr.jsonl"))),
299
+ chunkEntities: Array.from(parseChunks(readEntityFile("entities.chunk.jsonl"))),
300
+ moduleEntities: Array.from(parseModules(readEntityFile("entities.module.jsonl"))),
301
+ projectEntities: Array.from(parseProjects(readEntityFile("entities.project.jsonl"))),
302
+ constrains: parseRelations("relations.constrains.jsonl", "note"),
303
+ implementsEdges: parseRelations("relations.implements.jsonl", "note"),
304
+ supersedes: parseRelations("relations.supersedes.jsonl", "reason"),
305
+ defines: parseSimpleRelations("relations.defines.jsonl"),
306
+ calls: parseCallRelations("relations.calls.jsonl"),
307
+ callsSql: parseRelations("relations.calls_sql.jsonl", "note"),
308
+ usesConfigKey: parseRelations("relations.uses_config_key.jsonl", "note"),
309
+ usesResourceKey: parseRelations("relations.uses_resource_key.jsonl", "note"),
310
+ usesSettingKey: parseRelations("relations.uses_setting_key.jsonl", "note"),
311
+ imports: parseImportRelations("relations.imports.jsonl"),
312
+ contains: parseSimpleRelations("relations.contains.jsonl"),
313
+ containsModule: parseSimpleRelations("relations.contains_module.jsonl"),
314
+ exports: parseSimpleRelations("relations.exports.jsonl"),
315
+ includesFile: parseSimpleRelations("relations.includes_file.jsonl"),
316
+ referencesProject: parseRelations("relations.references_project.jsonl", "note"),
317
+ usesResource: parseRelations("relations.uses_resource.jsonl", "note"),
318
+ usesSetting: parseRelations("relations.uses_setting.jsonl", "note"),
319
+ usesConfig: parseRelations("relations.uses_config.jsonl", "note"),
320
+ transformsConfig: parseRelations("relations.transforms_config.jsonl", "note")
321
+ };
322
+ }
323
+ // Mirror the MATCH (...)-CREATE skip semantics of the prepared-statement
324
+ // loader: an edge whose endpoint node was never created is silently dropped.
325
+ // COPY into a rel table instead errors on an unknown primary key, so edges
326
+ // must be pre-filtered against the node-id sets to stay byte-identical.
327
+ function filterEdges(edges, fromSet, toSet) {
328
+ return (function* filteredEdges() {
329
+ for (const edge of edges) {
330
+ if (fromSet.has(edge.from) && toSet.has(edge.to)) {
331
+ yield edge;
332
+ }
333
+ }
334
+ })();
335
+ }
336
+ function* collectIds(items, collected) {
337
+ for (const item of items) {
338
+ collected.add(item.id);
339
+ yield item;
340
+ }
341
+ }
342
+ function* csvRows(items, toRow) {
343
+ for (const item of items) {
344
+ yield toRow(item);
345
+ }
346
+ }
347
+ async function copyTable(conn, table, header, rows) {
348
+ const csvPath = path.join(GRAPH_IMPORT_DIR, `${table}.csv`);
349
+ const rowCount = writeCsv(csvPath, header, rows);
350
+ if (rowCount === 0) {
351
+ fs.rmSync(csvPath, { force: true });
352
+ return;
353
+ }
354
+ // ryugraph parses COPY's path as a double-quoted literal; toCopyPathLiteral
355
+ // normalizes separators and escapes any embedded quote so a repo/cache path
356
+ // containing a double quote still bulk-loads instead of falling back.
357
+ const csvForCopy = toCopyPathLiteral(csvPath);
358
+ await conn.query(`COPY ${table} FROM "${csvForCopy}" ${CSV_COPY_OPTIONS};`);
359
+ }
360
+ // Bulk path: one COPY per table instead of a prepared statement per row. Only
361
+ // used on the reset path, where the freshly created tables are empty. It reads
362
+ // node and relation JSONL streams directly into CSVs so the hot path retains
363
+ // only endpoint id sets rather than a full parsed GraphData object.
364
+ async function bulkLoad(conn) {
365
+ fs.rmSync(GRAPH_IMPORT_DIR, { recursive: true, force: true });
366
+ fs.mkdirSync(GRAPH_IMPORT_DIR, { recursive: true });
367
+ try {
368
+ const fileIds = new Set();
369
+ const ruleIds = new Set();
370
+ const adrIds = new Set();
371
+ const chunkIds = new Set();
372
+ const moduleIds = new Set();
373
+ const projectIds = new Set();
374
+ // Nodes first — edges reference them by primary key.
375
+ await copyTable(conn, "File", ["id", "path", "kind", "excerpt", "checksum", "updated_at", "source_of_truth", "trust_level", "status"], csvRows(collectIds(parseFiles(readEntityRecords("entities.file.jsonl")), fileIds), (e) => [
376
+ e.id, e.path, e.kind, e.excerpt, e.checksum, e.updated_at, e.source_of_truth, e.trust_level, e.status
377
+ ]));
378
+ await copyTable(conn, "Rule", ["id", "title", "body", "scope", "priority", "updated_at", "source_of_truth", "trust_level", "status"], csvRows(collectIds(parseRules(readEntityRecords("entities.rule.jsonl")), ruleIds), (e) => [
379
+ e.id, e.title, e.body, e.scope, e.priority, e.updated_at, e.source_of_truth, e.trust_level, e.status
380
+ ]));
381
+ await copyTable(conn, "ADR", ["id", "path", "title", "body", "decision_date", "supersedes_id", "source_of_truth", "trust_level", "status"], csvRows(collectIds(parseAdrs(readEntityRecords("entities.adr.jsonl")), adrIds), (e) => [
382
+ e.id, e.path, e.title, e.body, e.decision_date, e.supersedes_id, e.source_of_truth, e.trust_level, e.status
383
+ ]));
384
+ await copyTable(conn, "Chunk", [
385
+ "id", "file_id", "name", "kind", "signature", "body", "description", "start_line", "end_line",
386
+ "language", "exported", "checksum", "updated_at", "source_of_truth", "trust_level", "status"
387
+ ], csvRows(collectIds(parseChunks(readEntityRecords("entities.chunk.jsonl")), chunkIds), (e) => [
388
+ e.id, e.file_id, e.name, e.kind, e.signature, e.body, e.description, e.start_line, e.end_line,
389
+ e.language, e.exported, e.checksum, e.updated_at, e.source_of_truth, e.trust_level, e.status
390
+ ]));
391
+ await copyTable(conn, "Module", ["id", "path", "name", "summary", "file_count", "exported_symbols", "updated_at", "source_of_truth", "trust_level", "status"], csvRows(collectIds(parseModules(readEntityRecords("entities.module.jsonl")), moduleIds), (e) => [
392
+ e.id, e.path, e.name, e.summary, e.file_count, e.exported_symbols, e.updated_at, e.source_of_truth, e.trust_level, e.status
393
+ ]));
394
+ await copyTable(conn, "Project", [
395
+ "id", "path", "name", "kind", "language", "target_framework", "summary", "file_count",
396
+ "updated_at", "source_of_truth", "trust_level", "status"
397
+ ], csvRows(collectIds(parseProjects(readEntityRecords("entities.project.jsonl")), projectIds), (e) => [
398
+ e.id, e.path, e.name, e.kind, e.language, e.target_framework, e.summary, e.file_count,
399
+ e.updated_at, e.source_of_truth, e.trust_level, e.status
400
+ ]));
401
+ // Edges, filtered to existing endpoints. Header names are positional only
402
+ // (COPY into a rel table reads src key, dst key, then properties in schema
403
+ // order), but emitting real names keeps the CSVs self-describing.
404
+ await copyTable(conn, "CONSTRAINS", ["from", "to", "note"], csvRows(filterEdges(relationRecords("relations.constrains.jsonl", "note"), ruleIds, fileIds), (e) => [e.from, e.to, e.note]));
405
+ await copyTable(conn, "IMPLEMENTS", ["from", "to", "note"], csvRows(filterEdges(relationRecords("relations.implements.jsonl", "note"), fileIds, ruleIds), (e) => [e.from, e.to, e.note]));
406
+ await copyTable(conn, "SUPERSEDES", ["from", "to", "reason"], csvRows(filterEdges(relationRecords("relations.supersedes.jsonl", "reason"), adrIds, adrIds), (e) => [e.from, e.to, e.note]));
407
+ await copyTable(conn, "DEFINES", ["from", "to"], csvRows(filterEdges(simpleRelationRecords("relations.defines.jsonl"), fileIds, chunkIds), (e) => [e.from, e.to]));
408
+ await copyTable(conn, "CALLS", ["from", "to", "call_type"], csvRows(filterEdges(callRelationRecords("relations.calls.jsonl"), chunkIds, chunkIds), (e) => [e.from, e.to, e.call_type]));
409
+ await copyTable(conn, "IMPORTS", ["from", "to", "import_name"], csvRows(filterEdges(importRelationRecords("relations.imports.jsonl"), chunkIds, fileIds), (e) => [e.from, e.to, e.import_name]));
410
+ await copyTable(conn, "CALLS_SQL", ["from", "to", "note"], csvRows(filterEdges(relationRecords("relations.calls_sql.jsonl", "note"), fileIds, chunkIds), (e) => [e.from, e.to, e.note]));
411
+ await copyTable(conn, "USES_CONFIG_KEY", ["from", "to", "note"], csvRows(filterEdges(relationRecords("relations.uses_config_key.jsonl", "note"), fileIds, chunkIds), (e) => [e.from, e.to, e.note]));
412
+ await copyTable(conn, "USES_RESOURCE_KEY", ["from", "to", "note"], csvRows(filterEdges(relationRecords("relations.uses_resource_key.jsonl", "note"), fileIds, chunkIds), (e) => [e.from, e.to, e.note]));
413
+ await copyTable(conn, "USES_SETTING_KEY", ["from", "to", "note"], csvRows(filterEdges(relationRecords("relations.uses_setting_key.jsonl", "note"), fileIds, chunkIds), (e) => [e.from, e.to, e.note]));
414
+ await copyTable(conn, "CONTAINS", ["from", "to"], csvRows(filterEdges(simpleRelationRecords("relations.contains.jsonl"), moduleIds, fileIds), (e) => [e.from, e.to]));
415
+ await copyTable(conn, "CONTAINS_MODULE", ["from", "to"], csvRows(filterEdges(simpleRelationRecords("relations.contains_module.jsonl"), moduleIds, moduleIds), (e) => [e.from, e.to]));
416
+ await copyTable(conn, "EXPORTS", ["from", "to"], csvRows(filterEdges(simpleRelationRecords("relations.exports.jsonl"), moduleIds, chunkIds), (e) => [e.from, e.to]));
417
+ await copyTable(conn, "INCLUDES_FILE", ["from", "to"], csvRows(filterEdges(simpleRelationRecords("relations.includes_file.jsonl"), projectIds, fileIds), (e) => [e.from, e.to]));
418
+ await copyTable(conn, "REFERENCES_PROJECT", ["from", "to", "note"], csvRows(filterEdges(relationRecords("relations.references_project.jsonl", "note"), projectIds, projectIds), (e) => [e.from, e.to, e.note]));
419
+ await copyTable(conn, "USES_RESOURCE", ["from", "to", "note"], csvRows(filterEdges(relationRecords("relations.uses_resource.jsonl", "note"), fileIds, fileIds), (e) => [e.from, e.to, e.note]));
420
+ await copyTable(conn, "USES_SETTING", ["from", "to", "note"], csvRows(filterEdges(relationRecords("relations.uses_setting.jsonl", "note"), fileIds, fileIds), (e) => [e.from, e.to, e.note]));
421
+ await copyTable(conn, "USES_CONFIG", ["from", "to", "note"], csvRows(filterEdges(relationRecords("relations.uses_config.jsonl", "note"), fileIds, fileIds), (e) => [e.from, e.to, e.note]));
422
+ await copyTable(conn, "TRANSFORMS_CONFIG", ["from", "to", "note"], csvRows(filterEdges(relationRecords("relations.transforms_config.jsonl", "note"), fileIds, fileIds), (e) => [e.from, e.to, e.note]));
423
+ }
424
+ finally {
425
+ fs.rmSync(GRAPH_IMPORT_DIR, { recursive: true, force: true });
426
+ }
427
+ }
428
+ // Original loader: clears every table, then inserts each node/edge through a
429
+ // prepared statement in batches. Used for --no-reset and as the fallback when
430
+ // bulk COPY fails. MATCH-based edge inserts skip dangling endpoints silently.
431
+ async function rowByRowLoad(conn, data) {
432
+ const { fileEntities, ruleEntities, adrEntities, chunkEntities, moduleEntities, projectEntities, constrains, implementsEdges, supersedes, defines, calls, callsSql, usesConfigKey, usesResourceKey, usesSettingKey, imports, contains, containsModule, exports, includesFile, referencesProject, usesResource, usesSetting, usesConfig, transformsConfig } = data;
433
+ // Delete all relations first, then all nodes, to avoid orphaned edges
434
+ await conn.query("MATCH (a:ADR)-[r:SUPERSEDES]->(b:ADR) DELETE r;");
435
+ await conn.query("MATCH (f:File)-[i:IMPLEMENTS]->(r:Rule) DELETE i;");
436
+ await conn.query("MATCH (r:Rule)-[c:CONSTRAINS]->(f:File) DELETE c;");
437
+ await conn.query("MATCH (f:File)-[d:DEFINES]->(c:Chunk) DELETE d;");
438
+ await conn.query("MATCH (c1:Chunk)-[ca:CALLS]->(c2:Chunk) DELETE ca;");
439
+ await conn.query("MATCH (c:Chunk)-[im:IMPORTS]->(f:File) DELETE im;");
440
+ await conn.query("MATCH (f:File)-[cs:CALLS_SQL]->(c:Chunk) DELETE cs;");
441
+ await conn.query("MATCH (f:File)-[uck:USES_CONFIG_KEY]->(c:Chunk) DELETE uck;");
442
+ await conn.query("MATCH (f:File)-[urk:USES_RESOURCE_KEY]->(c:Chunk) DELETE urk;");
443
+ await conn.query("MATCH (f:File)-[usk:USES_SETTING_KEY]->(c:Chunk) DELETE usk;");
444
+ await conn.query("MATCH (m:Module)-[co:CONTAINS]->(f:File) DELETE co;");
445
+ await conn.query("MATCH (m1:Module)-[cm:CONTAINS_MODULE]->(m2:Module) DELETE cm;");
446
+ await conn.query("MATCH (m:Module)-[ex:EXPORTS]->(c:Chunk) DELETE ex;");
447
+ await conn.query("MATCH (p:Project)-[inc:INCLUDES_FILE]->(f:File) DELETE inc;");
448
+ await conn.query("MATCH (p1:Project)-[rp:REFERENCES_PROJECT]->(p2:Project) DELETE rp;");
449
+ await conn.query("MATCH (f1:File)-[ur:USES_RESOURCE]->(f2:File) DELETE ur;");
450
+ await conn.query("MATCH (f1:File)-[us:USES_SETTING]->(f2:File) DELETE us;");
451
+ await conn.query("MATCH (f1:File)-[uc:USES_CONFIG]->(f2:File) DELETE uc;");
452
+ await conn.query("MATCH (f1:File)-[tc:TRANSFORMS_CONFIG]->(f2:File) DELETE tc;");
453
+ // Now delete all nodes
454
+ await conn.query("MATCH (n:ADR) DELETE n;");
455
+ await conn.query("MATCH (n:Rule) DELETE n;");
456
+ await conn.query("MATCH (n:Chunk) DELETE n;");
457
+ await conn.query("MATCH (n:Module) DELETE n;");
458
+ await conn.query("MATCH (n:Project) DELETE n;");
459
+ await conn.query("MATCH (n:File) DELETE n;");
460
+ const insertFile = await conn.prepare(`
461
+ CREATE (f:File {
462
+ id: $id,
463
+ path: $path,
464
+ kind: $kind,
465
+ excerpt: $excerpt,
466
+ checksum: $checksum,
467
+ updated_at: $updated_at,
468
+ source_of_truth: $source_of_truth,
469
+ trust_level: $trust_level,
470
+ status: $status
471
+ });
472
+ `);
473
+ const insertRule = await conn.prepare(`
474
+ CREATE (r:Rule {
475
+ id: $id,
476
+ title: $title,
477
+ body: $body,
478
+ scope: $scope,
479
+ priority: $priority,
480
+ updated_at: $updated_at,
481
+ source_of_truth: $source_of_truth,
482
+ trust_level: $trust_level,
483
+ status: $status
484
+ });
485
+ `);
486
+ const insertAdr = await conn.prepare(`
487
+ CREATE (a:ADR {
488
+ id: $id,
489
+ path: $path,
490
+ title: $title,
491
+ body: $body,
492
+ decision_date: $decision_date,
493
+ supersedes_id: $supersedes_id,
494
+ source_of_truth: $source_of_truth,
495
+ trust_level: $trust_level,
496
+ status: $status
497
+ });
498
+ `);
499
+ const insertChunk = await conn.prepare(`
500
+ CREATE (c:Chunk {
501
+ id: $id,
502
+ file_id: $file_id,
503
+ name: $name,
504
+ kind: $kind,
505
+ signature: $signature,
506
+ body: $body,
507
+ description: $description,
508
+ start_line: $start_line,
509
+ end_line: $end_line,
510
+ language: $language,
511
+ exported: $exported,
512
+ checksum: $checksum,
513
+ updated_at: $updated_at,
514
+ source_of_truth: $source_of_truth,
515
+ trust_level: $trust_level,
516
+ status: $status
517
+ });
518
+ `);
519
+ const insertConstrains = await conn.prepare(`
520
+ MATCH (r:Rule {id: $from}), (f:File {id: $to})
521
+ CREATE (r)-[:CONSTRAINS {note: $note}]->(f);
522
+ `);
523
+ const insertImplements = await conn.prepare(`
524
+ MATCH (f:File {id: $from}), (r:Rule {id: $to})
525
+ CREATE (f)-[:IMPLEMENTS {note: $note}]->(r);
526
+ `);
527
+ const insertSupersedes = await conn.prepare(`
528
+ MATCH (a1:ADR {id: $from}), (a2:ADR {id: $to})
529
+ CREATE (a1)-[:SUPERSEDES {reason: $note}]->(a2);
530
+ `);
531
+ const insertDefines = await conn.prepare(`
532
+ MATCH (f:File {id: $from}), (c:Chunk {id: $to})
533
+ CREATE (f)-[:DEFINES]->(c);
534
+ `);
535
+ const insertCalls = await conn.prepare(`
536
+ MATCH (c1:Chunk {id: $from}), (c2:Chunk {id: $to})
537
+ CREATE (c1)-[:CALLS {call_type: $call_type}]->(c2);
538
+ `);
539
+ const insertImports = await conn.prepare(`
540
+ MATCH (c:Chunk {id: $from}), (f:File {id: $to})
541
+ CREATE (c)-[:IMPORTS {import_name: $import_name}]->(f);
542
+ `);
543
+ const insertCallsSql = await conn.prepare(`
544
+ MATCH (f:File {id: $from}), (c:Chunk {id: $to})
545
+ CREATE (f)-[:CALLS_SQL {note: $note}]->(c);
546
+ `);
547
+ const insertUsesConfigKey = await conn.prepare(`
548
+ MATCH (f:File {id: $from}), (c:Chunk {id: $to})
549
+ CREATE (f)-[:USES_CONFIG_KEY {note: $note}]->(c);
550
+ `);
551
+ const insertUsesResourceKey = await conn.prepare(`
552
+ MATCH (f:File {id: $from}), (c:Chunk {id: $to})
553
+ CREATE (f)-[:USES_RESOURCE_KEY {note: $note}]->(c);
554
+ `);
555
+ const insertUsesSettingKey = await conn.prepare(`
556
+ MATCH (f:File {id: $from}), (c:Chunk {id: $to})
557
+ CREATE (f)-[:USES_SETTING_KEY {note: $note}]->(c);
558
+ `);
559
+ const insertModule = await conn.prepare(`
560
+ CREATE (m:Module {
561
+ id: $id,
562
+ path: $path,
563
+ name: $name,
564
+ summary: $summary,
565
+ file_count: $file_count,
566
+ exported_symbols: $exported_symbols,
567
+ updated_at: $updated_at,
568
+ source_of_truth: $source_of_truth,
569
+ trust_level: $trust_level,
570
+ status: $status
571
+ });
572
+ `);
573
+ const insertProject = await conn.prepare(`
574
+ CREATE (p:Project {
575
+ id: $id,
576
+ path: $path,
577
+ name: $name,
578
+ kind: $kind,
579
+ language: $language,
580
+ target_framework: $target_framework,
581
+ summary: $summary,
582
+ file_count: $file_count,
583
+ updated_at: $updated_at,
584
+ source_of_truth: $source_of_truth,
585
+ trust_level: $trust_level,
586
+ status: $status
587
+ });
588
+ `);
589
+ const insertContains = await conn.prepare(`
590
+ MATCH (m:Module {id: $from}), (f:File {id: $to})
591
+ CREATE (m)-[:CONTAINS]->(f);
592
+ `);
593
+ const insertContainsModule = await conn.prepare(`
594
+ MATCH (m1:Module {id: $from}), (m2:Module {id: $to})
595
+ CREATE (m1)-[:CONTAINS_MODULE]->(m2);
596
+ `);
597
+ const insertExports = await conn.prepare(`
598
+ MATCH (m:Module {id: $from}), (c:Chunk {id: $to})
599
+ CREATE (m)-[:EXPORTS]->(c);
600
+ `);
601
+ const insertIncludesFile = await conn.prepare(`
602
+ MATCH (p:Project {id: $from}), (f:File {id: $to})
603
+ CREATE (p)-[:INCLUDES_FILE]->(f);
604
+ `);
605
+ const insertReferencesProject = await conn.prepare(`
606
+ MATCH (p1:Project {id: $from}), (p2:Project {id: $to})
607
+ CREATE (p1)-[:REFERENCES_PROJECT {note: $note}]->(p2);
608
+ `);
609
+ const insertUsesResource = await conn.prepare(`
610
+ MATCH (f1:File {id: $from}), (f2:File {id: $to})
611
+ CREATE (f1)-[:USES_RESOURCE {note: $note}]->(f2);
612
+ `);
613
+ const insertUsesSetting = await conn.prepare(`
614
+ MATCH (f1:File {id: $from}), (f2:File {id: $to})
615
+ CREATE (f1)-[:USES_SETTING {note: $note}]->(f2);
616
+ `);
617
+ const insertUsesConfig = await conn.prepare(`
618
+ MATCH (f1:File {id: $from}), (f2:File {id: $to})
619
+ CREATE (f1)-[:USES_CONFIG {note: $note}]->(f2);
620
+ `);
621
+ const insertTransformsConfig = await conn.prepare(`
622
+ MATCH (f1:File {id: $from}), (f2:File {id: $to})
623
+ CREATE (f1)-[:TRANSFORMS_CONFIG {note: $note}]->(f2);
624
+ `);
625
+ // Insert all nodes first (batched for performance)
626
+ await executeBatch(conn, insertFile, fileEntities);
627
+ await executeBatch(conn, insertRule, ruleEntities.map((e) => ({
628
+ id: e.id, title: e.title, body: e.body, scope: e.scope,
629
+ priority: e.priority, updated_at: e.updated_at,
630
+ source_of_truth: e.source_of_truth, trust_level: e.trust_level, status: e.status
631
+ })));
632
+ await executeBatch(conn, insertAdr, adrEntities);
633
+ await executeBatch(conn, insertChunk, chunkEntities);
634
+ await executeBatch(conn, insertModule, moduleEntities);
635
+ await executeBatch(conn, insertProject, projectEntities);
636
+ // Insert all edges (nodes must exist first)
637
+ await executeBatch(conn, insertDefines, defines);
638
+ await executeBatch(conn, insertCalls, calls);
639
+ await executeBatch(conn, insertImports, imports);
640
+ await executeBatch(conn, insertCallsSql, callsSql);
641
+ await executeBatch(conn, insertUsesConfigKey, usesConfigKey);
642
+ await executeBatch(conn, insertUsesResourceKey, usesResourceKey);
643
+ await executeBatch(conn, insertUsesSettingKey, usesSettingKey);
644
+ await executeBatch(conn, insertConstrains, constrains);
645
+ await executeBatch(conn, insertImplements, implementsEdges);
646
+ await executeBatch(conn, insertSupersedes, supersedes);
647
+ await executeBatch(conn, insertContains, contains);
648
+ await executeBatch(conn, insertContainsModule, containsModule);
649
+ await executeBatch(conn, insertExports, exports);
650
+ await executeBatch(conn, insertIncludesFile, includesFile);
651
+ await executeBatch(conn, insertReferencesProject, referencesProject);
652
+ await executeBatch(conn, insertUsesResource, usesResource);
653
+ await executeBatch(conn, insertUsesSetting, usesSetting);
654
+ await executeBatch(conn, insertUsesConfig, usesConfig);
655
+ await executeBatch(conn, insertTransformsConfig, transformsConfig);
656
+ }
657
+ async function main() {
658
+ const args = new Set(process.argv.slice(2));
659
+ const reset = !args.has("--no-reset");
660
+ await ensureRequiredFiles();
661
+ warnIfOptionalFilesMissing();
662
+ if (reset) {
663
+ fs.rmSync(DB_PATH, { recursive: true, force: true });
664
+ fs.rmSync(`${DB_PATH}.wal`, { force: true });
665
+ fs.rmSync(`${DB_PATH}.shm`, { force: true });
666
+ }
667
+ fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
668
+ const db = new ryugraph.Database(DB_PATH);
669
+ const conn = new ryugraph.Connection(db);
670
+ const ontologyStatements = parseOntologyStatements(fs.readFileSync(ONTOLOGY_PATH, "utf8"));
671
+ await executeStatements(conn, ontologyStatements);
672
+ const bulkEnabled = reset && process.env.CORTEX_GRAPH_BULK_LOAD !== "never";
673
+ let usedBulk = false;
674
+ if (bulkEnabled) {
675
+ try {
676
+ await bulkLoad(conn);
677
+ usedBulk = true;
678
+ console.log("[graph-load] loaded via COPY bulk import");
679
+ }
680
+ catch (error) {
681
+ const message = error instanceof Error ? error.message : String(error);
682
+ console.warn(`[graph-load] bulk COPY load failed (${message}); falling back to row-by-row inserts`);
683
+ }
684
+ }
685
+ if (!usedBulk) {
686
+ const data = parseGraphData();
687
+ await rowByRowLoad(conn, data);
688
+ }
689
+ const fileCount = await rows(await conn.query("MATCH (f:File) RETURN count(*) AS count;"));
690
+ const ruleCount = await rows(await conn.query("MATCH (r:Rule) RETURN count(*) AS count;"));
691
+ const adrCount = await rows(await conn.query("MATCH (a:ADR) RETURN count(*) AS count;"));
692
+ const chunkCount = await rows(await conn.query("MATCH (c:Chunk) RETURN count(*) AS count;"));
693
+ const constrainsCount = await rows(await conn.query("MATCH (:Rule)-[c:CONSTRAINS]->(:File) RETURN count(c) AS count;"));
694
+ const implementsCount = await rows(await conn.query("MATCH (:File)-[i:IMPLEMENTS]->(:Rule) RETURN count(i) AS count;"));
695
+ const supersedesCount = await rows(await conn.query("MATCH (:ADR)-[s:SUPERSEDES]->(:ADR) RETURN count(s) AS count;"));
696
+ const definesCount = await rows(await conn.query("MATCH (:File)-[d:DEFINES]->(:Chunk) RETURN count(d) AS count;"));
697
+ const callsCount = await rows(await conn.query("MATCH (:Chunk)-[ca:CALLS]->(:Chunk) RETURN count(ca) AS count;"));
698
+ const importsCount = await rows(await conn.query("MATCH (:Chunk)-[im:IMPORTS]->(:File) RETURN count(im) AS count;"));
699
+ const callsSqlCount = await rows(await conn.query("MATCH (:File)-[cs:CALLS_SQL]->(:Chunk) RETURN count(cs) AS count;"));
700
+ const usesConfigKeyCount = await rows(await conn.query("MATCH (:File)-[uck:USES_CONFIG_KEY]->(:Chunk) RETURN count(uck) AS count;"));
701
+ const usesResourceKeyCount = await rows(await conn.query("MATCH (:File)-[urk:USES_RESOURCE_KEY]->(:Chunk) RETURN count(urk) AS count;"));
702
+ const usesSettingKeyCount = await rows(await conn.query("MATCH (:File)-[usk:USES_SETTING_KEY]->(:Chunk) RETURN count(usk) AS count;"));
703
+ const moduleCount = await rows(await conn.query("MATCH (m:Module) RETURN count(*) AS count;"));
704
+ const projectCount = await rows(await conn.query("MATCH (p:Project) RETURN count(*) AS count;"));
705
+ const containsCount = await rows(await conn.query("MATCH (:Module)-[co:CONTAINS]->(:File) RETURN count(co) AS count;"));
706
+ const containsModuleCount = await rows(await conn.query("MATCH (:Module)-[cm:CONTAINS_MODULE]->(:Module) RETURN count(cm) AS count;"));
707
+ const exportsCount = await rows(await conn.query("MATCH (:Module)-[ex:EXPORTS]->(:Chunk) RETURN count(ex) AS count;"));
708
+ const includesFileCount = await rows(await conn.query("MATCH (:Project)-[inc:INCLUDES_FILE]->(:File) RETURN count(inc) AS count;"));
709
+ const referencesProjectCount = await rows(await conn.query("MATCH (:Project)-[rp:REFERENCES_PROJECT]->(:Project) RETURN count(rp) AS count;"));
710
+ const usesResourceCount = await rows(await conn.query("MATCH (:File)-[ur:USES_RESOURCE]->(:File) RETURN count(ur) AS count;"));
711
+ const usesSettingCount = await rows(await conn.query("MATCH (:File)-[us:USES_SETTING]->(:File) RETURN count(us) AS count;"));
712
+ const usesConfigCount = await rows(await conn.query("MATCH (:File)-[uc:USES_CONFIG]->(:File) RETURN count(uc) AS count;"));
713
+ const transformsConfigCount = await rows(await conn.query("MATCH (:File)-[tc:TRANSFORMS_CONFIG]->(:File) RETURN count(tc) AS count;"));
714
+ const summary = {
715
+ generated_at: new Date().toISOString(),
716
+ db_path: DB_PATH,
717
+ counts: {
718
+ files: Number(fileCount[0]?.count ?? 0),
719
+ rules: Number(ruleCount[0]?.count ?? 0),
720
+ adrs: Number(adrCount[0]?.count ?? 0),
721
+ chunks: Number(chunkCount[0]?.count ?? 0),
722
+ constrains: Number(constrainsCount[0]?.count ?? 0),
723
+ implements: Number(implementsCount[0]?.count ?? 0),
724
+ supersedes: Number(supersedesCount[0]?.count ?? 0),
725
+ defines: Number(definesCount[0]?.count ?? 0),
726
+ calls: Number(callsCount[0]?.count ?? 0),
727
+ imports: Number(importsCount[0]?.count ?? 0),
728
+ calls_sql: Number(callsSqlCount[0]?.count ?? 0),
729
+ uses_config_key: Number(usesConfigKeyCount[0]?.count ?? 0),
730
+ uses_resource_key: Number(usesResourceKeyCount[0]?.count ?? 0),
731
+ uses_setting_key: Number(usesSettingKeyCount[0]?.count ?? 0),
732
+ modules: Number(moduleCount[0]?.count ?? 0),
733
+ projects: Number(projectCount[0]?.count ?? 0),
734
+ contains: Number(containsCount[0]?.count ?? 0),
735
+ contains_module: Number(containsModuleCount[0]?.count ?? 0),
736
+ exports: Number(exportsCount[0]?.count ?? 0),
737
+ includes_file: Number(includesFileCount[0]?.count ?? 0),
738
+ references_project: Number(referencesProjectCount[0]?.count ?? 0),
739
+ uses_resource: Number(usesResourceCount[0]?.count ?? 0),
740
+ uses_setting: Number(usesSettingCount[0]?.count ?? 0),
741
+ uses_config: Number(usesConfigCount[0]?.count ?? 0),
742
+ transforms_config: Number(transformsConfigCount[0]?.count ?? 0)
743
+ }
744
+ };
745
+ const summaryPath = path.join(CACHE_DIR, "graph-manifest.json");
746
+ fs.writeFileSync(summaryPath, `${JSON.stringify(summary, null, 2)}\n`, "utf8");
747
+ console.log(`[graph-load] db_path=${DB_PATH}`);
748
+ console.log(`[graph-load] files=${summary.counts.files} rules=${summary.counts.rules} adrs=${summary.counts.adrs} chunks=${summary.counts.chunks} modules=${summary.counts.modules} projects=${summary.counts.projects}`);
749
+ console.log(`[graph-load] rels constrains=${summary.counts.constrains} implements=${summary.counts.implements} supersedes=${summary.counts.supersedes}`);
750
+ console.log(`[graph-load] rels defines=${summary.counts.defines} calls=${summary.counts.calls} imports=${summary.counts.imports} calls_sql=${summary.counts.calls_sql} uses_config_key=${summary.counts.uses_config_key} uses_resource_key=${summary.counts.uses_resource_key} uses_setting_key=${summary.counts.uses_setting_key}`);
751
+ console.log(`[graph-load] rels contains=${summary.counts.contains} contains_module=${summary.counts.contains_module} exports=${summary.counts.exports} includes_file=${summary.counts.includes_file} references_project=${summary.counts.references_project} uses_resource=${summary.counts.uses_resource} uses_setting=${summary.counts.uses_setting} uses_config=${summary.counts.uses_config} transforms_config=${summary.counts.transforms_config}`);
752
+ console.log(`[graph-load] manifest=${summaryPath}`);
753
+ // RyuGraph Node addon can crash on explicit close in some environments.
754
+ // Let process teardown handle resource cleanup.
755
+ }
756
+ main().catch((error) => {
757
+ process.stderr.write(`${error instanceof Error ? error.message : "Unknown error"}\n`);
758
+ process.exit(1);
759
+ });