hecks 2.8.0 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (404) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +4 -6
  3. data/lib/hecks/adapters/driven/claude_code.rb +4 -104
  4. data/lib/hecks/adapters/driven/d1.rb +44 -237
  5. data/lib/hecks/adapters/driven/folder.rb +17 -76
  6. data/lib/hecks/adapters/driven/google_authentication.rb +15 -49
  7. data/lib/hecks/adapters/driven/governance_authorization.rb +23 -70
  8. data/lib/hecks/adapters/driven/heki/journal.rb +8 -41
  9. data/lib/hecks/adapters/driven/heki/saga_store.rb +12 -54
  10. data/lib/hecks/adapters/driven/heki/snapshot.rb +4 -12
  11. data/lib/hecks/adapters/driven/heki.rb +13 -42
  12. data/lib/hecks/adapters/driven/identity_registry.rb +3 -14
  13. data/lib/hecks/adapters/driven/in_memory_ordering.rb +9 -26
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +2 -14
  15. data/lib/hecks/adapters/driven/lambda/client.rb +10 -51
  16. data/lib/hecks/adapters/driven/lambda.rb +10 -84
  17. data/lib/hecks/adapters/driven/local_storage.rb +5 -40
  18. data/lib/hecks/adapters/driven/memory.rb +17 -98
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +5 -21
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +6 -28
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +3 -12
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +4 -30
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +34 -108
  24. data/lib/hecks/adapters/driven/postgres.rb +145 -134
  25. data/lib/hecks/adapters/driven/postgres_era.adapter +4 -15
  26. data/lib/hecks/adapters/driven/prism.rb +7 -30
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +21 -121
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +8 -40
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +26 -82
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +40 -84
  31. data/lib/hecks/adapters/driven/sqlite.rb +128 -65
  32. data/lib/hecks/adapters/driven/tenant_provisioner.rb +5 -48
  33. data/lib/hecks/adapters/driven.rb +5 -36
  34. data/lib/hecks/adapters/driving/github_webhook.rb +8 -76
  35. data/lib/hecks/behaviors/dsl.rb +7 -25
  36. data/lib/hecks/behaviors/expectations.rb +31 -141
  37. data/lib/hecks/behaviors/ir.rb +7 -20
  38. data/lib/hecks/behaviors/rspec.rb +6 -17
  39. data/lib/hecks/behaviors/runner.rb +10 -34
  40. data/lib/hecks/behaviors.rb +4 -8
  41. data/lib/hecks/bluebook/aggregate.rb +9 -75
  42. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +9 -38
  43. data/lib/hecks/bluebook/assembly/build.rb +4 -15
  44. data/lib/hecks/bluebook/assembly/contract.rb +15 -136
  45. data/lib/hecks/bluebook/assembly/contracts.rb +54 -174
  46. data/lib/hecks/bluebook/assembly/marks.rb +31 -223
  47. data/lib/hecks/bluebook/assembly/specializer.rb +6 -52
  48. data/lib/hecks/bluebook/assembly.rb +6 -45
  49. data/lib/hecks/bluebook/attribute.rb +12 -61
  50. data/lib/hecks/bluebook/behaviour/aggregate.rb +10 -39
  51. data/lib/hecks/bluebook/behaviour/attribute.rb +3 -14
  52. data/lib/hecks/bluebook/behaviour/chapter.rb +14 -42
  53. data/lib/hecks/bluebook/behaviour/command.rb +14 -66
  54. data/lib/hecks/bluebook/behaviour/domain_port.rb +4 -20
  55. data/lib/hecks/bluebook/behaviour/entity.rb +7 -27
  56. data/lib/hecks/bluebook/behaviour/hexagon.rb +23 -41
  57. data/lib/hecks/bluebook/behaviour/lifecycle.rb +15 -40
  58. data/lib/hecks/bluebook/behaviour/policy.rb +23 -52
  59. data/lib/hecks/bluebook/behaviour/process_manager.rb +30 -57
  60. data/lib/hecks/bluebook/behaviour/read_model.rb +25 -31
  61. data/lib/hecks/bluebook/behaviour/traits.rb +27 -64
  62. data/lib/hecks/bluebook/behaviour/value_object.rb +5 -24
  63. data/lib/hecks/bluebook/capabilities.rb +17 -49
  64. data/lib/hecks/bluebook/chapter.rb +10 -48
  65. data/lib/hecks/bluebook/command.rb +23 -81
  66. data/lib/hecks/bluebook/domain_port.rb +14 -63
  67. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +50 -229
  68. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +50 -377
  69. data/lib/hecks/bluebook/dsl/attribute_collector.rb +33 -204
  70. data/lib/hecks/bluebook/dsl/binding_proxy.rb +16 -69
  71. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +86 -708
  72. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +56 -170
  73. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
  74. data/lib/hecks/bluebook/dsl/command_builder.rb +74 -546
  75. data/lib/hecks/bluebook/dsl/const_shim.rb +8 -41
  76. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +12 -87
  77. data/lib/hecks/bluebook/dsl/entity_builder.rb +44 -330
  78. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +20 -269
  79. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +26 -194
  80. data/lib/hecks/bluebook/dsl/identity_declaration.rb +13 -88
  81. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +6 -20
  82. data/lib/hecks/bluebook/dsl/policy_builder.rb +16 -88
  83. data/lib/hecks/bluebook/dsl/port_builder.rb +2 -6
  84. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +22 -96
  85. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +31 -184
  86. data/lib/hecks/bluebook/dsl/query_builder.rb +11 -42
  87. data/lib/hecks/bluebook/dsl/read_model_builder.rb +75 -201
  88. data/lib/hecks/bluebook/dsl/rule_reference.rb +22 -163
  89. data/lib/hecks/bluebook/dsl/translation_builder.rb +28 -114
  90. data/lib/hecks/bluebook/dsl/value_object_builder.rb +10 -124
  91. data/lib/hecks/bluebook/dsl/word_gate.rb +16 -162
  92. data/lib/hecks/bluebook/dsl/world_builder.rb +19 -111
  93. data/lib/hecks/bluebook/entity.rb +13 -71
  94. data/lib/hecks/bluebook/expression/ast_json.rb +24 -190
  95. data/lib/hecks/bluebook/expression/ast_reader.rb +12 -54
  96. data/lib/hecks/bluebook/expression/canonical_form.rb +11 -88
  97. data/lib/hecks/bluebook/expression/evaluator.rb +47 -295
  98. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +30 -147
  99. data/lib/hecks/bluebook/expression/resolver.rb +37 -660
  100. data/lib/hecks/bluebook/hexagon.rb +5 -28
  101. data/lib/hecks/bluebook/lifecycle.rb +5 -16
  102. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -7
  103. data/lib/hecks/bluebook/meta_validator/judge.rb +111 -361
  104. data/lib/hecks/bluebook/meta_validator/plan.rb +33 -162
  105. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -7
  106. data/lib/hecks/bluebook/meta_validator/readings.rb +56 -342
  107. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -176
  108. data/lib/hecks/bluebook/meta_validator/shapes.rb +53 -267
  109. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +43 -305
  110. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +7 -43
  111. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -18
  112. data/lib/hecks/bluebook/meta_validator.rb +79 -349
  113. data/lib/hecks/bluebook/model_check/client_profile.rb +6 -21
  114. data/lib/hecks/bluebook/model_check.rb +100 -597
  115. data/lib/hecks/bluebook/pattern_subset.rb +11 -69
  116. data/lib/hecks/bluebook/process_manager.rb +13 -83
  117. data/lib/hecks/bluebook/project_loader.rb +3 -39
  118. data/lib/hecks/bluebook/project_register.rb +12 -60
  119. data/lib/hecks/bluebook/query.rb +9 -48
  120. data/lib/hecks/bluebook/read_model.rb +57 -81
  121. data/lib/hecks/bluebook/reference.rb +7 -41
  122. data/lib/hecks/bluebook/smoke_test.rb +8 -98
  123. data/lib/hecks/bluebook/synthesizer.rb +13 -62
  124. data/lib/hecks/bluebook/translation.rb +26 -62
  125. data/lib/hecks/bluebook/value_object.rb +11 -36
  126. data/lib/hecks/bluebook.rb +5 -22
  127. data/lib/hecks/cache_dir.rb +4 -23
  128. data/lib/hecks/cli/document.rb +3 -21
  129. data/lib/hecks/cli/ir.rb +3 -5
  130. data/lib/hecks/cli/mcp.rb +2 -3
  131. data/lib/hecks/cli/mcp_door.rb +19 -114
  132. data/lib/hecks/cli/model_check.rb +18 -85
  133. data/lib/hecks/cli/project_cli.rb +7 -40
  134. data/lib/hecks/cli/project_diagrams.rb +0 -7
  135. data/lib/hecks/cli/run.rb +12 -58
  136. data/lib/hecks/cli/smoke_test.rb +2 -16
  137. data/lib/hecks/cli/stores.rb +1 -2
  138. data/lib/hecks/cli.rb +18 -24
  139. data/lib/hecks/construct.rb +8 -34
  140. data/lib/hecks/deploy/bluebook/deploy.bluebook +33 -142
  141. data/lib/hecks/deploy/bluebook/deploy.hecksagon +2 -9
  142. data/lib/hecks/embryonaut_bluebook/lock.rb +3 -35
  143. data/lib/hecks/embryonaut_bluebook/shape.rb +1 -17
  144. data/lib/hecks/embryonaut_bluebook/vendor.rb +8 -53
  145. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +4 -19
  146. data/lib/hecks/embryonaut_bluebook.rb +4 -71
  147. data/lib/hecks/facade/cli_door.rb +24 -106
  148. data/lib/hecks/facade/cli_runner.rb +28 -172
  149. data/lib/hecks/facade/command_request.rb +13 -33
  150. data/lib/hecks/facade/handle.rb +34 -121
  151. data/lib/hecks/facade/json_door.rb +31 -146
  152. data/lib/hecks/facade/surface/aggregate_door.rb +39 -104
  153. data/lib/hecks/facade/surface/chapter.rb +10 -54
  154. data/lib/hecks/facade/surface.rb +8 -23
  155. data/lib/hecks/facade.rb +10 -25
  156. data/lib/hecks/forms/app.rb +15 -70
  157. data/lib/hecks/forms/command_form_renderer.rb +10 -29
  158. data/lib/hecks/forms/field_renderer.rb +17 -156
  159. data/lib/hecks/forms/field_shape.rb +30 -222
  160. data/lib/hecks/forms/html.rb +12 -47
  161. data/lib/hecks/forms/index_renderer.rb +1 -4
  162. data/lib/hecks/forms/page.rb +3 -9
  163. data/lib/hecks/forms/params.rb +14 -60
  164. data/lib/hecks/forms/port_argument.rb +2 -19
  165. data/lib/hecks/forms/query_form_renderer.rb +4 -79
  166. data/lib/hecks/forms/record_renderer.rb +8 -75
  167. data/lib/hecks/forms/record_table.rb +7 -50
  168. data/lib/hecks/forms/reference_options.rb +3 -30
  169. data/lib/hecks/forms/value_object_shape.rb +8 -35
  170. data/lib/hecks/forms.rb +5 -33
  171. data/lib/hecks/fqn.rb +5 -12
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +16 -65
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +47 -134
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +5 -14
  175. data/lib/hecks/framework/bluebook/governance.bluebook +13 -34
  176. data/lib/hecks/framework/bluebook/identity.bluebook +11 -26
  177. data/lib/hecks/framework/bluebook/privacy.bluebook +8 -33
  178. data/lib/hecks/framework.rb +11 -80
  179. data/lib/hecks/freezer.rb +5 -39
  180. data/lib/hecks/grammar/expression.bluebook +15 -40
  181. data/lib/hecks/grammar/translation.bluebook +3 -6
  182. data/lib/hecks/grammar.rb +16 -41
  183. data/lib/hecks/ir.rb +14 -83
  184. data/lib/hecks/language/adapter.bluebook +2 -7
  185. data/lib/hecks/language/bluebook/aggregate.bluebook +56 -221
  186. data/lib/hecks/language/bluebook/attaches/paging.bluebook +6 -15
  187. data/lib/hecks/language/bluebook/bluebook.bluebook +15 -83
  188. data/lib/hecks/language/bluebook/bluebook.hecksagon +3 -15
  189. data/lib/hecks/language/bluebook/command.bluebook +27 -144
  190. data/lib/hecks/language/bluebook/entity.bluebook +24 -97
  191. data/lib/hecks/language/bluebook/policy.bluebook +16 -67
  192. data/lib/hecks/language/bluebook/process_manager.bluebook +28 -151
  193. data/lib/hecks/language/bluebook/projection.bluebook +126 -60
  194. data/lib/hecks/language/bluebook/query.bluebook +12 -43
  195. data/lib/hecks/language/bluebook/shape.bluebook +23 -73
  196. data/lib/hecks/language/bluebook/syntax.bluebook +51 -262
  197. data/lib/hecks/language/bluebook/vocabulary.bluebook +82 -197
  198. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +3 -10
  199. data/lib/hecks/language/hecksagon/domain_port.bluebook +4 -19
  200. data/lib/hecks/language/hecksagon/hecksagon.bluebook +6 -25
  201. data/lib/hecks/language/hecksagon/port_operation.bluebook +3 -14
  202. data/lib/hecks/language/oidc.json +35 -0
  203. data/lib/hecks/language/port.bluebook +4 -17
  204. data/lib/hecks/language/translation/translation.bluebook +3 -9
  205. data/lib/hecks/language/translation/translation_aggregate.bluebook +5 -24
  206. data/lib/hecks/language/world/wiring.bluebook +1 -3
  207. data/lib/hecks/language/world/world.bluebook +5 -7
  208. data/lib/hecks/literal.rb +8 -50
  209. data/lib/hecks/mcp_door_scope.rb +9 -90
  210. data/lib/hecks/mcp_stdio_guard.rb +4 -41
  211. data/lib/hecks/naming.rb +29 -137
  212. data/lib/hecks/ports/access_control.rb +7 -23
  213. data/lib/hecks/ports/agent/answers.rb +5 -23
  214. data/lib/hecks/ports/agent.rb +47 -172
  215. data/lib/hecks/ports/authentication.rb +12 -39
  216. data/lib/hecks/ports/authorization.rb +16 -43
  217. data/lib/hecks/ports/clock.rb +5 -45
  218. data/lib/hecks/ports/extraction.rb +7 -12
  219. data/lib/hecks/ports/identity_assignment.rb +8 -29
  220. data/lib/hecks/ports/identity_generation.rb +6 -29
  221. data/lib/hecks/ports/identity_resolution.rb +2 -10
  222. data/lib/hecks/ports/key_vault.rb +9 -45
  223. data/lib/hecks/ports/persistence/append_only.rb +61 -64
  224. data/lib/hecks/ports/persistence/binding_policy.rb +4 -14
  225. data/lib/hecks/ports/persistence/codec_boundary.rb +20 -80
  226. data/lib/hecks/ports/persistence/null_saga_store.rb +4 -20
  227. data/lib/hecks/ports/persistence/plugin.rb +13 -42
  228. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +38 -141
  229. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +15 -115
  230. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +16 -84
  231. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +6 -25
  232. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -22
  233. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +55 -181
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/events.rb +77 -0
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +12 -122
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +28 -128
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +73 -474
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +31 -124
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +45 -219
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +13 -108
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +18 -75
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +9 -49
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +6 -123
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +23 -55
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +14 -72
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +7 -16
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +14 -115
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +13 -45
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +127 -554
  250. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +13 -63
  251. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +2 -17
  252. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +3 -14
  253. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +13 -58
  254. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -17
  255. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +9 -26
  256. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +9 -25
  257. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -124
  258. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +34 -123
  259. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +5 -12
  260. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +3 -9
  261. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -14
  262. data/lib/hecks/ports/persistence/plugins/era.rb +4 -23
  263. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +7 -23
  264. data/lib/hecks/ports/persistence/postgres_dump.rb +4 -35
  265. data/lib/hecks/ports/persistence/remote_runtime.rb +2 -24
  266. data/lib/hecks/ports/persistence/repository_factory.rb +8 -27
  267. data/lib/hecks/ports/persistence/state_codec.rb +23 -103
  268. data/lib/hecks/ports/persistence.rb +7 -14
  269. data/lib/hecks/ports/projection.rb +42 -84
  270. data/lib/hecks/ports/query/in_memory.rb +10 -58
  271. data/lib/hecks/ports/query/ordering.rb +11 -32
  272. data/lib/hecks/ports/query.rb +14 -37
  273. data/lib/hecks/ports.rb +2 -8
  274. data/lib/hecks/projections/bootstrap_table.rb +11 -43
  275. data/lib/hecks/projections/deploy/fargate/assembly.rb +2 -15
  276. data/lib/hecks/projections/deploy/fargate/cdn.rb +1 -48
  277. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +2 -11
  278. data/lib/hecks/projections/deploy/fargate/check.rb +5 -14
  279. data/lib/hecks/projections/deploy/fargate/containers.rb +3 -78
  280. data/lib/hecks/projections/deploy/fargate/extras.rb +3 -44
  281. data/lib/hecks/projections/deploy/fargate/monitoring.rb +0 -44
  282. data/lib/hecks/projections/deploy/fargate/settings.rb +2 -54
  283. data/lib/hecks/projections/deploy/fargate/yaml.rb +13 -58
  284. data/lib/hecks/projections/deploy/fargate.rb +35 -202
  285. data/lib/hecks/projections/deploy/lambda.rb +150 -678
  286. data/lib/hecks/projections/deploy/preview/access_yaml.rb +1 -33
  287. data/lib/hecks/projections/deploy/preview/containers.rb +6 -111
  288. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +4 -56
  289. data/lib/hecks/projections/deploy/preview/script.rb +2 -14
  290. data/lib/hecks/projections/deploy/preview/settings.rb +3 -33
  291. data/lib/hecks/projections/deploy/preview/task_yaml.rb +2 -38
  292. data/lib/hecks/projections/deploy/preview/template.rb +2 -58
  293. data/lib/hecks/projections/deploy/preview.rb +2 -67
  294. data/lib/hecks/projections/deploy/scripts/settings.rb +3 -25
  295. data/lib/hecks/projections/deploy/scripts.rb +4 -57
  296. data/lib/hecks/projections/deploy/shared.rb +47 -146
  297. data/lib/hecks/projections/deploy/smoke.rb +4 -61
  298. data/lib/hecks/projections/deploy/template_diff/comparer.rb +3 -25
  299. data/lib/hecks/projections/deploy/template_diff/loader.rb +2 -11
  300. data/lib/hecks/projections/deploy/template_diff.rb +5 -57
  301. data/lib/hecks/projections/diagrams.rb +20 -582
  302. data/lib/hecks/projections/glossary/html.rb +14 -130
  303. data/lib/hecks/projections/glossary/markdown.rb +8 -69
  304. data/lib/hecks/projections/glossary/mermaid.rb +9 -66
  305. data/lib/hecks/projections/glossary/sensitivity.rb +5 -34
  306. data/lib/hecks/projections/glossary/sentences.rb +14 -159
  307. data/lib/hecks/projections/glossary.rb +30 -222
  308. data/lib/hecks/projections/ir.rb +3 -10
  309. data/lib/hecks/projections/model/deviations.rb +15 -42
  310. data/lib/hecks/projections/model.rb +12 -51
  311. data/lib/hecks/projections/oidc.rb +12 -56
  312. data/lib/hecks/projections/parser_table.rb +9 -28
  313. data/lib/hecks/projections/reference.rb +2 -15
  314. data/lib/hecks/projections/rust_vocabulary.rb +12 -250
  315. data/lib/hecks/projections/shape.rb +3 -29
  316. data/lib/hecks/projections/statements.rb +7 -86
  317. data/lib/hecks/projections/vocabulary.rb +6 -22
  318. data/lib/hecks/projections.rb +2 -16
  319. data/lib/hecks/projector/cli_projector.rb +60 -375
  320. data/lib/hecks/projector/docs_projector.rb +10 -201
  321. data/lib/hecks/projector/exporter.rb +42 -134
  322. data/lib/hecks/projector/ir_projector.rb +2 -9
  323. data/lib/hecks/projector/narrate_projector.rb +15 -189
  324. data/lib/hecks/projector/target.rb +13 -76
  325. data/lib/hecks/projector.rb +30 -121
  326. data/lib/hecks/query_specification/common/comparators.rb +5 -26
  327. data/lib/hecks/query_specification/common/comparison.rb +35 -143
  328. data/lib/hecks/query_specification/common/dsl.rb +8 -24
  329. data/lib/hecks/query_specification/common/null_policy.rb +20 -70
  330. data/lib/hecks/query_specification/common/options.rb +2 -6
  331. data/lib/hecks/query_specification/common/where_clause.rb +2 -10
  332. data/lib/hecks/query_specification/field_path.rb +37 -77
  333. data/lib/hecks/query_specification/hop_path.rb +12 -76
  334. data/lib/hecks/release/gem_pin.rb +2 -26
  335. data/lib/hecks/release/runner/ci_publisher.rb +3 -6
  336. data/lib/hecks/release/runner/commands.rb +2 -4
  337. data/lib/hecks/release/runner/gem_publisher.rb +2 -3
  338. data/lib/hecks/release/runner/npm_publisher.rb +6 -20
  339. data/lib/hecks/release/runner/preflight.rb +4 -19
  340. data/lib/hecks/release/runner/tagger.rb +2 -9
  341. data/lib/hecks/release/runner.rb +3 -32
  342. data/lib/hecks/rendering.rb +4 -26
  343. data/lib/hecks/router/namespace_installer.rb +12 -36
  344. data/lib/hecks/router.rb +4 -8
  345. data/lib/hecks/runtime/aggregate_lock.rb +8 -33
  346. data/lib/hecks/runtime/boot_gates.rb +3 -18
  347. data/lib/hecks/runtime/caller.rb +19 -59
  348. data/lib/hecks/runtime/capability_graph.rb +5 -21
  349. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +16 -78
  350. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +23 -160
  351. data/lib/hecks/runtime/command_interpreter.rb +124 -306
  352. data/lib/hecks/runtime/command_rules/admissibility.rb +95 -366
  353. data/lib/hecks/runtime/command_rules/arithmetic.rb +66 -196
  354. data/lib/hecks/runtime/command_rules/authorization.rb +13 -45
  355. data/lib/hecks/runtime/command_rules/emission.rb +8 -21
  356. data/lib/hecks/runtime/command_rules/references.rb +23 -202
  357. data/lib/hecks/runtime/dependency_planning.rb +30 -102
  358. data/lib/hecks/runtime/dispatcher.rb +82 -241
  359. data/lib/hecks/runtime/entity_element.rb +40 -462
  360. data/lib/hecks/runtime/entity_interpreter.rb +64 -219
  361. data/lib/hecks/runtime/errors.rb +32 -115
  362. data/lib/hecks/runtime/event.rb +3 -25
  363. data/lib/hecks/runtime/identity.rb +18 -154
  364. data/lib/hecks/runtime/instance.rb +18 -150
  365. data/lib/hecks/runtime/interpreting.rb +11 -70
  366. data/lib/hecks/runtime/invocation.rb +28 -164
  367. data/lib/hecks/runtime/loader.rb +48 -186
  368. data/lib/hecks/runtime/outbox.rb +38 -261
  369. data/lib/hecks/runtime/policy_interpreter.rb +38 -200
  370. data/lib/hecks/runtime/port_operation_interpreter.rb +26 -87
  371. data/lib/hecks/runtime/query_interpreter.rb +45 -187
  372. data/lib/hecks/runtime/reaction_invocation.rb +36 -138
  373. data/lib/hecks/runtime/read_model_interpreter.rb +117 -208
  374. data/lib/hecks/runtime/rebuild_sweep.rb +8 -54
  375. data/lib/hecks/runtime/reference_hop.rb +8 -81
  376. data/lib/hecks/runtime/refusal_wording.rb +8 -89
  377. data/lib/hecks/runtime/registry/saga_persistence.rb +10 -102
  378. data/lib/hecks/runtime/registry/verification.rb +52 -225
  379. data/lib/hecks/runtime/registry/world_defaults.rb +4 -53
  380. data/lib/hecks/runtime/registry.rb +99 -244
  381. data/lib/hecks/runtime/remote_dispatcher.rb +37 -152
  382. data/lib/hecks/runtime/routing.rb +4 -15
  383. data/lib/hecks/runtime/saga_interpreter/correlation.rb +14 -61
  384. data/lib/hecks/runtime/saga_interpreter.rb +79 -263
  385. data/lib/hecks/runtime/saga_pending_dispatch.rb +2 -43
  386. data/lib/hecks/runtime/tenant_check.rb +6 -68
  387. data/lib/hecks/runtime/tenant_scope.rb +3 -28
  388. data/lib/hecks/runtime/value/admission.rb +16 -67
  389. data/lib/hecks/runtime/value/coercion.rb +155 -641
  390. data/lib/hecks/runtime/value/entity_list_coercion.rb +26 -219
  391. data/lib/hecks/runtime/value.rb +12 -84
  392. data/lib/hecks/runtime.rb +17 -65
  393. data/lib/hecks/storehouse.rb +141 -568
  394. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +8 -32
  395. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +6 -23
  396. data/lib/hecks/vendoring/git_environment.rb +2 -8
  397. data/lib/hecks/vendoring/git_source.rb +4 -10
  398. data/lib/hecks/vendoring.rb +7 -46
  399. data/lib/hecks/version.rb +3 -14
  400. data/lib/hecks.rb +26 -81
  401. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -46
  402. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +11 -80
  403. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +10 -99
  404. metadata +3 -2
@@ -1,9 +1,7 @@
1
1
  module Hecks
2
2
  module Adapters
3
3
  class Sqlite
4
- # The DDL: one table per aggregate head, an append-only entry table
5
- # beside it, the shared events table, and the two ALTERs that let an
6
- # older database grow the columns newer code writes.
4
+ # DDL for one aggregate: head table, entry table, events, saga and outbox tables.
7
5
  module SchemaBuilder
8
6
  private
9
7
 
@@ -12,15 +10,7 @@ module Hecks
12
10
  @db.execute(
13
11
  "CREATE TABLE IF NOT EXISTS #{quoted_table} (id TEXT PRIMARY KEY#{', ' unless columns.empty?}#{columns.join(', ')})"
14
12
  )
15
- # Right here, not as a separate step in `Sqlite#initialize` —
16
- # `create_aggregate_table!` is the one piece of DDL `D1`
17
- # already calls verbatim through this same shared module
18
- # (`d1.rb`'s own header: "reuses Sqlite::SchemaBuilder and
19
- # Sqlite::Codec unchanged"). Folding index creation in here,
20
- # right after the table it indexes exists, is what makes D1's
21
- # own automatic indexing a real, running thing rather than an
22
- # aspiration that only Sqlite ever calls — no D1-specific
23
- # wiring needed, because D1 never has to know this exists.
13
+ # Called here so D1, which reuses this module, gets automatic indexing too.
24
14
  ensure_indexes!
25
15
  end
26
16
 
@@ -35,6 +25,10 @@ module Hecks
35
25
  occurred_at TEXT
36
26
  )
37
27
  SQL
28
+ # Backs #events_for's per-record lookup (a `corrects` command's history read).
29
+ @db.execute(
30
+ "CREATE INDEX IF NOT EXISTS hecks_events_aggregate_id_idx ON events (aggregate, aggregate_id)"
31
+ )
38
32
  end
39
33
 
40
34
  def create_entry_table!
@@ -63,14 +57,8 @@ module Hecks
63
57
  @db.execute("ALTER TABLE #{quoted_entry_table} ADD COLUMN mirrors TEXT")
64
58
  end
65
59
 
66
- # The optional saga-persistence capability's own table (§2/§4) —
67
- # shared here so `D1`, which `include`s this module verbatim for
68
- # its own `events`-table DDL (`d1.rb`), gets this for free too.
69
- # `domain` stays an explicit column even though SQLite has no
70
- # schema/namespace concept the way Postgres does — matches
71
- # `hecks_saga_instances`' own Postgres shape (§3) and covers the
72
- # (uncommon but real) case of a domain explicitly sharing one
73
- # `database` file/D1 database with another.
60
+ # Shared with D1, which includes this module. `domain` is an explicit column so
61
+ # two domains can share one database file.
74
62
  def create_saga_table!
75
63
  @db.execute(<<~SQL)
76
64
  CREATE TABLE IF NOT EXISTS hecks_saga_instances (
@@ -83,14 +71,8 @@ module Hecks
83
71
  PRIMARY KEY (domain, process_manager, correlation)
84
72
  )
85
73
  SQL
86
- # `CREATE TABLE IF NOT EXISTS` above is a no-op against a table
87
- # this same domain already created before this column existed
88
- # — the same reason Postgres's own `create_saga_table!` needs
89
- # its own `ADD COLUMN IF NOT EXISTS`. SQLite/D1's own `ALTER
90
- # TABLE ... ADD COLUMN` has no `IF NOT EXISTS` guard on every
91
- # version this adapter supports, so a duplicate-column error
92
- # is caught and treated as "already there" rather than relied
93
- # on to never happen.
74
+ # The table may predate this column, and SQLite lacks ADD COLUMN IF NOT EXISTS,
75
+ # so a duplicate-column error is treated as already applied.
94
76
  @db.execute("ALTER TABLE hecks_saga_instances ADD COLUMN completed_compensations TEXT NOT NULL DEFAULT '[]'")
95
77
  rescue StandardError => e
96
78
  raise unless e.message.include?("duplicate column name")
@@ -118,27 +100,36 @@ module Hecks
118
100
  @db.execute("CREATE INDEX IF NOT EXISTS idx_hecks_outbox_status ON hecks_outbox(aggregate, status)")
119
101
  end
120
102
 
103
+ # One row per aggregate table, naming the highest entry `sequence` this table has
104
+ # already had projected into it — shared across every aggregate in the database file,
105
+ # since it is keyed by table name rather than declared per aggregate.
106
+ def create_checkpoint_table!
107
+ @db.execute(<<~SQL)
108
+ CREATE TABLE IF NOT EXISTS hecks_checkpoints (
109
+ aggregate_table TEXT PRIMARY KEY,
110
+ last_sequence INTEGER NOT NULL DEFAULT 0
111
+ )
112
+ SQL
113
+ ensure_checkpoint_compacted_through_column!
114
+ end
115
+
116
+ # CREATE TABLE IF NOT EXISTS never adds a column to an existing table, so this healing
117
+ # step matches ensure_entry_operation_column!'s own for the entry table.
118
+ def ensure_checkpoint_compacted_through_column!
119
+ columns = @db.execute("PRAGMA table_info(hecks_checkpoints)").map { |row| row["name"] }
120
+ return if columns.include?("compacted_through")
121
+
122
+ @db.execute("ALTER TABLE hecks_checkpoints ADD COLUMN compacted_through INTEGER NOT NULL DEFAULT 0")
123
+ end
124
+
121
125
  def sql_type(attr)
122
126
  return "TEXT" if attr.list?
123
127
 
124
128
  SQL_TYPES.fetch(attr.type, "TEXT")
125
129
  end
126
130
 
127
- # ── automatic indexing (plan principle 3: derived from existing
128
- # declared `where`/`order_by` IR, no new DSL keyword — this runs
129
- # unconditionally on every boot, `CREATE INDEX IF NOT EXISTS`,
130
- # the same self-healing idiom `postgres_era.rb`'s own
131
- # `ensure_head_snapshot!`/`ensure_first_head!` already use) ────
132
-
133
- # Every field a declared query ever filters or sorts on, across
134
- # the aggregate's own queries and every entity's own queries —
135
- # `query_surfaces` in `dsl/aggregate_builder.rb` walks the
136
- # identical pair. An entity's query still runs against this
137
- # table: it compiles through `query_expression`, which is
138
- # `@aggregate`-scoped, not entity-scoped, so an entity's
139
- # declared `where`/`order_by` is indexed here too, same as any
140
- # other declared query — not skipped as "some other table's
141
- # concern."
131
+ # Indexes every field a declared query filters or sorts on, entity queries included
132
+ # (they compile against this table too). Idempotent, so it is safe on every boot.
142
133
  def ensure_indexes!
143
134
  declared_query_fields.each { |field| ensure_index_for_field!(field) }
144
135
  end
@@ -149,38 +140,10 @@ module Hecks
149
140
  .compact.map(&:to_s).uniq
150
141
  end
151
142
 
152
- # One field name resolves to exactly one of three outcomes:
153
- #
154
- # - a plain scalar attribute (or the lifecycle field itself)
155
- # — a real btree index on the column, using `plain_column`'s
156
- # own `quote_ident(name)` phrasing (reached by calling
157
- # `query_expression` itself, not a second copy of its
158
- # plain-vs-nested decision);
159
- # - a non-list value-object attribute, referenced bare or
160
- # through a member path (`field` or `field.member`) — an
161
- # expression index over `query_expression(field)`'s own
162
- # `json_extract(...)` text. Reusing the query compiler's own
163
- # expression, not re-deriving the string a second way, is
164
- # the whole point: a textual mismatch between the index and
165
- # what a real query compiles to is invisible to SQLite's
166
- # planner, and two independent copies of this logic can only
167
- # drift apart over time;
168
- # - a list-typed attribute — no index. SQLite's `contains`
169
- # compiles to `EXISTS (SELECT 1 FROM json_each(col) WHERE
170
- # ...)` (`list_contains_clause`, in `sql_query_builder.rb`)
171
- # — an element-membership scan a plain index on the raw
172
- # column (or even an expression index on it) does nothing to
173
- # speed, since neither indexes the elements individually.
174
- # SQLite has no inverted/array index short of FTS5/R-tree,
175
- # and neither fits a `list_of` scalar or value-object field
176
- # — well beyond this plan's scope. Left unindexed
177
- # deliberately, not a silent gap.
178
- #
179
- # A field naming neither a real attribute nor the lifecycle
180
- # field can't happen through the DSL today — `seal_query_field`
181
- # (`dsl/aggregate_builder.rb`) already refuses it at parse time.
182
- # This runs at adapter boot, not parse time, so it skips rather
183
- # than crashes ugly if that invariant is ever violated.
143
+ # A scalar or lifecycle field gets a btree index; a value-object path gets an expression
144
+ # index over `query_expression`'s own text, so the planner matches what queries compile to.
145
+ # List fields are left unindexed: SQLite cannot index json_each elements.
146
+ # Unknown fields are skipped rather than raised, since the DSL already refuses them.
184
147
  def ensure_index_for_field!(field)
185
148
  name, * = field.to_s.split(".")
186
149
  attribute = @aggregate.attribute(name)
@@ -195,15 +158,8 @@ module Hecks
195
158
  )
196
159
  end
197
160
 
198
- # `idx_<table>_<sanitized field>` — table-prefixed because
199
- # SQLite index names are global to the database, not scoped per
200
- # table the way a column name is; two different aggregates each
201
- # indexing a field called "name" would collide without it. The
202
- # field itself is sanitized (a dotted path's "." in particular)
203
- # to stay a valid identifier while remaining visibly tied to
204
- # what it indexes — collision-free across every field/path this
205
- # aggregate declares, since `declared_query_fields` already
206
- # de-duplicates the field strings themselves.
161
+ # Table-prefixed because SQLite index names are global to the database.
162
+ # The field is sanitized (a dotted path's ".") to stay a valid identifier.
207
163
  def index_name(field)
208
164
  sanitized = field.to_s.gsub(/[^a-zA-Z0-9_]/, "_")
209
165
  "idx_#{table}_#{sanitized}"
@@ -14,10 +14,8 @@ require_relative "../../runtime/instance"
14
14
 
15
15
  module Hecks
16
16
  module Adapters
17
- # The SQLite store: one table per aggregate head, an append-only entry
18
- # table beside it. The DDL lives in sqlite/schema_builder.rb, the
19
- # column codec in sqlite/codec.rb, and the query compilation is the
20
- # shared SqlQueryBuilder — this file supplies only SQLite's dialect.
17
+ # The SQLite store: one table per aggregate head, an append-only entry table beside it.
18
+ # Supplies SQLite's dialect to the shared SqlQueryBuilder.
21
19
  class Sqlite
22
20
  include SqlQueryBuilder
23
21
  include SchemaBuilder
@@ -44,18 +42,13 @@ module Hecks
44
42
  # @raise [LoadError] if the `sqlite3` gem is not installed
45
43
  # @raise [SQLite3::Exception] if the file cannot be opened or a table cannot be created
46
44
  def initialize(aggregate:, settings: {}, root: nil)
47
- # **Lazy, on purpose** — a domain that never wires Sqlite should never
48
- # need the gem installed. `require "hecks"` alone must not
49
- # force a database client library nobody asked for.
45
+ # Lazy so a domain that never wires Sqlite does not need the gem installed.
50
46
  require "sqlite3"
51
47
 
52
48
  @aggregate = aggregate
53
49
  @path = resolve_path(settings, root)
54
- # The optional saga-persistence capability's own scoping column
55
- # (§2/§4) — falls back to the aggregate's own name for a
56
- # directly-instantiated adapter (specs), same fallback shape
57
- # Postgres's own @domain already uses.
58
- @domain = (
50
+ # Scopes saga rows; falls back to the aggregate's name for a directly built adapter.
51
+ @domain = (
59
52
  if settings.key?(:domain)
60
53
  settings[:domain]
61
54
  elsif settings.key?("domain")
@@ -68,8 +61,7 @@ module Hecks
68
61
  FileUtils.mkdir_p(File.dirname(@path))
69
62
  @db = SQLite3::Database.new(@path)
70
63
  @db.results_as_hash = true
71
- # The append is the recovery commit point. Keep SQLite's fsync policy
72
- # explicit instead of inheriting a process-wide pragma choice.
64
+ # The append is the recovery commit point, so the fsync policy is explicit.
73
65
  @db.execute("PRAGMA synchronous = FULL")
74
66
 
75
67
  create_aggregate_table!
@@ -79,14 +71,13 @@ module Hecks
79
71
  create_event_table!
80
72
  create_saga_table!
81
73
  create_outbox_table!
74
+ create_checkpoint_table!
82
75
  end
83
76
 
84
77
  # Runs the block inside one SQLite transaction, joining an already-open one.
85
78
  #
86
- # **Re-entrant on purpose** — `atomic_put` opens its own transaction
87
- # and `Interpreting#run_dispatch_order` opens one around the whole
88
- # save+emit pair; SQLite3 refuses a BEGIN inside a BEGIN, so the
89
- # inner call joins the outer one instead. Same shape Postgres uses.
79
+ # Re-entrant because `atomic_put` and `Interpreting#run_dispatch_order` both open one,
80
+ # and SQLite3 refuses a BEGIN inside a BEGIN.
90
81
  #
91
82
  # @yield the writes to commit together; an exception raised inside rolls the
92
83
  # outermost transaction back
@@ -117,9 +108,7 @@ module Hecks
117
108
 
118
109
  # Lists every stored record, ordered by id unless an ordering attribute is given.
119
110
  #
120
- # order_by is a runtime value — see postgres.rb's own all for the
121
- # full reasoning; whitelisted the identical way before it ever
122
- # reaches order_expression.
111
+ # order_by is a runtime value, so it is checked against the aggregate before use.
123
112
  #
124
113
  # @param order_by [String, Symbol, nil] attribute (or dotted value-object path) to sort
125
114
  # by; nil orders by id alone
@@ -154,21 +143,21 @@ module Hecks
154
143
 
155
144
  # Inserts one journal row, outside any transaction of its own.
156
145
  #
146
+ # Stamps the row's assigned `sequence` onto `entry` in place, so a `project` call right
147
+ # after (same object, same transaction) can advance the checkpoint with no extra query.
148
+ #
157
149
  # @param entry [Ports::Persistence::Entry] the save or delete to journal; `state` is
158
150
  # encoded through the state codec and `mirrors` stored as JSON, or NULL when nil
159
- # @return [Ports::Persistence::Entry] the same `entry`
151
+ # @return [Ports::Persistence::Entry] the same `entry`, `sequence` now set
160
152
  # @raise [SQLite3::Exception] if the insert fails
161
153
  def append(entry)
162
154
  @db.execute(
163
155
  "INSERT INTO #{quoted_entry_table} (aggregate_id, operation, state, mirrors) VALUES (?, ?, ?, ?)",
164
- # `mirrors` (unlike `state`) is a nullable column — an absent
165
- # mirrors hash must bind a real SQL NULL, not the four-character
166
- # JSON text `"null"` (`JSON.generate(nil)`), or a future `IS NULL`
167
- # check against it would never match. Same guard `postgres_era.rb`
168
- # already uses for its own journal's `mirrors` column.
156
+ # `mirrors` is nullable: an absent hash must bind SQL NULL, not the JSON text "null".
169
157
  [entry.id, entry.operation, JSON.generate(Ports::Persistence::StateCodec.encode(@aggregate, entry.state)),
170
158
  entry.mirrors && JSON.generate(entry.mirrors)]
171
159
  )
160
+ entry.sequence = @db.last_insert_row_id
172
161
  entry
173
162
  end
174
163
 
@@ -179,6 +168,7 @@ module Hecks
179
168
  # for a delete, the `DELETE` statement's empty result rows
180
169
  # @raise [SQLite3::Exception] if the statement fails
181
170
  def project(entry)
171
+ advance_checkpoint!(entry.sequence) if entry.sequence
182
172
  return @db.execute("DELETE FROM #{quoted_table} WHERE id = ?", [entry.id]) if entry.delete?
183
173
 
184
174
  instance = Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: entry.state)
@@ -201,15 +191,65 @@ module Hecks
201
191
  # @raise [SQLite3::Exception] if the statement fails
202
192
  # @raise [JSON::ParserError] if a stored `state` or `mirrors` value is not valid JSON
203
193
  def entries
204
- @db.execute("SELECT aggregate_id, operation, state, mirrors FROM #{quoted_entry_table} ORDER BY sequence").map do |row|
205
- state = JSON.parse(row["state"])
206
- Ports::Persistence::Entry.new(
207
- operation: row["operation"] || "save",
208
- id: row["aggregate_id"],
209
- state: Ports::Persistence::StateCodec.decode(@aggregate, state),
210
- mirrors: row["mirrors"] && JSON.parse(row["mirrors"])
211
- )
212
- end
194
+ entries_matching(
195
+ "SELECT aggregate_id, operation, state, mirrors, sequence FROM #{quoted_entry_table} ORDER BY sequence"
196
+ )
197
+ end
198
+
199
+ # Reads only the journal rows past a given `sequence`, for `AppendOnly#recover!` to replay
200
+ # after a checkpoint instead of the whole journal.
201
+ #
202
+ # @param sequence [Integer] the highest `sequence` already projected; rows at or below it
203
+ # are skipped
204
+ # @return [Array<Ports::Persistence::Entry>] the journalled entries past `sequence`, in
205
+ # append order, decoded exactly as `entries` decodes them; `[]` when none are newer
206
+ # @raise [SQLite3::Exception] if the statement fails
207
+ # @raise [JSON::ParserError] if a stored `state` or `mirrors` value is not valid JSON
208
+ def entries_since(sequence)
209
+ entries_matching(
210
+ "SELECT aggregate_id, operation, state, mirrors, sequence FROM #{quoted_entry_table} " \
211
+ "WHERE sequence > ? ORDER BY sequence",
212
+ [sequence]
213
+ )
214
+ end
215
+
216
+ # Reads the highest journal `sequence` this table has already had projected into it.
217
+ #
218
+ # @return [Integer] `0` when the table has never been checkpointed (a fresh table, or one
219
+ # from before this bookkeeping existed) — `entries_since(0)` then reads the whole journal,
220
+ # matching `entries`' own full replay
221
+ # @raise [SQLite3::Exception] if the statement fails
222
+ def checkpoint
223
+ @db.get_first_value("SELECT last_sequence FROM hecks_checkpoints WHERE aggregate_table = ?", [table]).to_i
224
+ end
225
+
226
+ # Reads the highest journal `sequence` compaction has already deleted from this table.
227
+ #
228
+ # @return [Integer] `0` when nothing has ever been compacted
229
+ # @raise [SQLite3::Exception] if the statement fails
230
+ def compacted_through
231
+ @db.get_first_value("SELECT compacted_through FROM hecks_checkpoints WHERE aggregate_table = ?", [table]).to_i
232
+ end
233
+
234
+ # Deletes every journal row at or before `through` and records it, so a `:strict`
235
+ # projection catch-up can tell whether it still has the history it needs.
236
+ #
237
+ # Never moves `compacted_through` backwards, and never touches the aggregate's own
238
+ # table — only the journal, which a `:refresh` projection rebuild never needs.
239
+ #
240
+ # @param through [Integer] the highest `sequence` to delete
241
+ # @return [Integer] the number of journal rows deleted
242
+ # @raise [SQLite3::Exception] if a statement fails
243
+ def compact_entries!(through:)
244
+ @db.execute("DELETE FROM #{quoted_entry_table} WHERE sequence <= ?", [through])
245
+ removed = @db.changes
246
+ @db.execute(
247
+ "INSERT INTO hecks_checkpoints (aggregate_table, compacted_through) VALUES (?, ?) " \
248
+ "ON CONFLICT (aggregate_table) DO UPDATE SET " \
249
+ "compacted_through = MAX(compacted_through, excluded.compacted_through)",
250
+ [table, through]
251
+ )
252
+ removed
213
253
  end
214
254
 
215
255
  # Deletes every row of the aggregate's table and its journal; events, saga rows and
@@ -238,9 +278,7 @@ module Hecks
238
278
 
239
279
  # Stores an entry and reports whether it inserted, replaced or conflicted.
240
280
  #
241
- # The outcome lookup, journal append and snapshot replacement share one
242
- # SQLite transaction. The runtime performs no preliminary find; this
243
- # adapter-native operation owns both concurrency and outcome reporting.
281
+ # The outcome lookup, journal append and snapshot replacement share one transaction.
244
282
  #
245
283
  # @param entry [Ports::Persistence::Entry] the save to store
246
284
  # @param insert_only [Boolean] when true, an existing row is left untouched
@@ -305,16 +343,33 @@ module Hecks
305
343
  end
306
344
  end
307
345
 
346
+ # Reads back one record's recorded events, oldest first — pushed down as a `WHERE`
347
+ # clause (`hecks_events_aggregate_id_idx`) instead of filtering `#events`'s whole-table
348
+ # read, since a `corrects` command's history lookup only ever needs this one record.
349
+ #
350
+ # @param aggregate [String] the `"domain::AggregateName"` key events are stored under
351
+ # @param id [String, Object] the record's identity, matched as `id.to_s`
352
+ # @return [Array<Runtime::Event>] the record's stored events; `[]` when it has none
353
+ # @raise [SQLite3::Exception] if the statement fails
354
+ def events_for(aggregate:, id:)
355
+ @db.execute(
356
+ "SELECT * FROM events WHERE aggregate = ? AND aggregate_id = ? ORDER BY id",
357
+ [aggregate, id.to_s]
358
+ ).map do |row|
359
+ Runtime::Event.new(
360
+ name: row["name"],
361
+ aggregate: row["aggregate"],
362
+ id: row["aggregate_id"],
363
+ payload: JSON.parse(row["payload"], symbolize_names: true),
364
+ occurred_at: row["occurred_at"]
365
+ )
366
+ end
367
+ end
368
+
308
369
  # Inserts new outbox rows as pending, skipping any whose `delivery_id` already exists.
309
370
  #
310
- # The outbox — see `Runtime::Outbox`. Rows land in the same
311
- # database as this aggregate (the only way the enqueue shares the
312
- # save's transaction), keyed by the aggregate's storage name so an
313
- # adapter instance only ever reads back its own rows even when
314
- # several aggregates share one file. `INSERT OR IGNORE` on the
315
- # unique delivery_id makes a re-enqueue of the same (event,
316
- # consumer) a no-op; `outbox_claim`'s `WHERE status = 'pending'`
317
- # is the compare-and-set that lets exactly one relay win a row.
371
+ # Rows share this aggregate's database so the enqueue joins the save's transaction, and
372
+ # are keyed by storage name so aggregates sharing a file read back only their own.
318
373
  #
319
374
  # @param rows [Array<Runtime::Outbox::Row>] rows to enqueue; each accepted row has its
320
375
  # `id` and `status` assigned in place. `row.aggregate` is stored as given
@@ -389,19 +444,6 @@ module Hecks
389
444
  # Replaces one saga instance's checkpoint, keyed by domain, process manager and
390
445
  # correlation.
391
446
  #
392
- # ── the optional saga-persistence capability (§2) — reuses the
393
- # DDL every SQLite-backed aggregate table already lives beside
394
- # (`create_saga_table!`, `Sqlite::SchemaBuilder`, shared with D1).
395
- # SQLite's `resolve_path` defaults to one `.db` file per
396
- # aggregate unless a domain shares one `database` setting across
397
- # its aggregates — since saga persistence resolves through
398
- # whichever adapter instance backs the domain's first aggregate
399
- # (`Registry#saga_persistence`), this table ends up living inside
400
- # that one aggregate's own file by default. Correct and durable
401
- # either way; a domain that wants an obviously-named saga store
402
- # already gets one by sharing `database` across its aggregates,
403
- # the recommended, common case.
404
- #
405
447
  # @param process_manager [String, Symbol] the process manager's name
406
448
  # @param correlation [String, Object] the instance's correlation value, stored as
407
449
  # `correlation.to_s`
@@ -462,7 +504,31 @@ module Hecks
462
504
 
463
505
  private
464
506
 
465
- # ── SqlQueryBuilder's dialect hooks ─────────────────────────────
507
+ # Shared decode step for `entries`/`entries_since` — runs a query already selecting
508
+ # `aggregate_id, operation, state, mirrors, sequence` and builds one Entry per row.
509
+ def entries_matching(sql, binds = [])
510
+ @db.execute(sql, binds).map do |row|
511
+ state = JSON.parse(row["state"])
512
+ Ports::Persistence::Entry.new(
513
+ operation: row["operation"] || "save",
514
+ id: row["aggregate_id"],
515
+ state: Ports::Persistence::StateCodec.decode(@aggregate, state),
516
+ mirrors: row["mirrors"] && JSON.parse(row["mirrors"]),
517
+ sequence: row["sequence"].to_i
518
+ )
519
+ end
520
+ end
521
+
522
+ # Advances this table's checkpoint to `sequence`, never backwards — two overlapping
523
+ # `project` calls for out-of-order entries must not let an older sequence win.
524
+ def advance_checkpoint!(sequence)
525
+ @db.execute(
526
+ "INSERT INTO hecks_checkpoints (aggregate_table, last_sequence) VALUES (?, ?) " \
527
+ "ON CONFLICT (aggregate_table) DO UPDATE SET " \
528
+ "last_sequence = MAX(last_sequence, excluded.last_sequence)",
529
+ [table, sequence]
530
+ )
531
+ end
466
532
 
467
533
  def outbox_row(row)
468
534
  Runtime::Outbox::Row.new(
@@ -499,8 +565,7 @@ module Hecks
499
565
  "json_extract(#{quote_ident(name)}, '#{json_path}')"
500
566
  end
501
567
 
502
- # SQLite has no bare OFFSET — LIMIT -1 is its own documented
503
- # unbounded spelling, exactly for this case.
568
+ # SQLite has no bare OFFSET; LIMIT -1 is its unbounded spelling.
504
569
  def unbounded_limit = " LIMIT -1"
505
570
 
506
571
  def order_clause(order_by, policy)
@@ -511,8 +576,6 @@ module Hecks
511
576
  @db.execute(sql, binds).map { |row| Runtime::Instance.new(aggregate: @aggregate, id: row["id"], state: decode(row)) }
512
577
  end
513
578
 
514
- # ── the rest of the dialect ─────────────────────────────────────
515
-
516
579
  def quote_ident(name)
517
580
  %("#{name.to_s.gsub('"', '""')}")
518
581
  end
@@ -4,37 +4,8 @@ require "fileutils"
4
4
 
5
5
  module Hecks
6
6
  module Adapters
7
- # The imperative half of provisioning a tenant, pulled out of
8
- # `bin/project_tenant` and behind a real driven port
9
- # (`Deploy::Tenant.port "TenantProvisioning"`) instead — the same
10
- # hexagonal reasoning `GithubChecks` already gives for its own
11
- # port: real, impure, side-effecting work (writing the overlay
12
- # file) belongs in an adapter, not in a bare script or a command
13
- # handler. `Deploy::Tenant`'s own `asks "Provision"` operation calls
14
- # `#provision` and turns whatever it returns into `TenantProvisioned`,
15
- # or whatever it raises into `ProvisioningRefused` — see
16
- # `PortOperationInterpreter#ask`'s own "every failure is an answer"
17
- # comment for why nothing here needs its own rescue.
18
- #
19
- # ## What it leaves out
20
- #
21
- # Deliberately does not boot the target domain — that step (proving
22
- # it boots for real, running the tenant_capable? gate) stays in
23
- # `bin/project_tenant` itself, called after this operation answers,
24
- # not nested inside it. Booting a whole domain from inside an
25
- # adapter method that is itself running mid-dispatch (this port
26
- # operation) is a real, separate concern from writing one file, and
27
- # keeping the two apart avoids a nested `Hecks.boot` call ever
28
- # running inside another boot's own dispatch call stack.
29
- #
30
- # ## Return shape
31
- #
32
- # Returns a hash shaped like `Tenancy::Tenant.Register`'s own
33
- # arguments, on purpose — `tenancy.hecksagon`'s own `translates`
34
- # reaction forwards an answered event's payload verbatim, so this
35
- # adapter's own return value is the translation from Deploy's
36
- # vocabulary into Tenancy's, decided here rather than in a mapping
37
- # layer neither side could see into.
7
+ # The imperative half of tenant provisioning, behind `Deploy::Tenant.port
8
+ # "TenantProvisioning"`; booting the domain and its capability gate stay in bin/project_tenant.
38
9
  class TenantProvisioner
39
10
  # Accepts the arguments every driven adapter is built with and keeps none of them.
40
11
  #
@@ -45,24 +16,10 @@ module Hecks
45
16
 
46
17
  # Writes the tenant's environment overlay file under its domain directory.
47
18
  #
48
- # `directory:` is required, not derived from `domain` — a real,
49
- # confirmed bug found only under CI's own real-Postgres suite: a
50
- # domain's own declared name (`domain`, e.g. "Scratch") is not a
51
- # filesystem path, and File.expand_path(domain, Dir.pwd) silently
52
- # resolved to the wrong directory (relative to whatever the
53
- # running process' own cwd happened to be) whenever the CLI's own
54
- # `--domain=` value didn't happen to equal its own directory
55
- # argument by coincidence. `bin/project_tenant` passes its own
56
- # real `domain_directory` local straight through.
19
+ # `directory:` must be passed explicitly — a domain's declared name is not a
20
+ # filesystem path. Each argument arrives either bare or wrapped as `{ value: }`.
57
21
  #
58
- # Every argument arrives either bare or wrapped as `{ value: ... }`; both are read.
59
- #
60
- # @param slug [String, Hash] the tenant's slug, which names the overlay file
61
- # @param domain [String, Hash] the declared domain name the overlay is for
62
- # @param realm [String, Hash] the realm the overlay declares
63
- # @param schema [String, Hash] the schema the overlay declares
64
- # @param database [String, Hash] the database the overlay declares
65
- # @param adapter [String, Hash] the persistence adapter the overlay declares
22
+ # @param slug [String, Hash] names the overlay file
66
23
  # @param directory [String, Hash] the domain's own directory on disk
67
24
  # @return [Hash{Symbol => Object}] `:slug`, `:domain`, `:realm` and `:schema`, as given
68
25
  def provision(slug:, domain:, realm:, schema:, database:, adapter:, directory:)
@@ -1,37 +1,14 @@
1
1
  module Hecks
2
- # Reopened here purely to require in every driven adapter under
3
- # adapters/driven/ below (see the comment inside for what "driven side"
4
- # means) — the actual namespace is declared in adapters.rb.
2
+ # The driven side: every store or reader an adapter declaration can bind.
5
3
  module Adapters
6
- # The driven side — every store or reader an adapter declaration can
7
- # bind. A small adapter is one file beside this one; sqlite, postgres
8
- # and heki keep a directory for their supporting cast. Each `.adapter`
9
- # file alongside is the DSL declaration the grammar bootstrap and the
10
- # Folder adapter load as data.
11
4
  end
12
5
  end
13
6
 
14
7
  require_relative "driven/memory"
15
8
  require_relative "driven/sqlite"
16
9
  require_relative "driven/postgres"
17
- # `PostgresEra` is not `require_relative`d here — ADR 0033 moved it, and
18
- # the rest of the era/lineage/translation subsystem, behind a loadable
19
- # persistence plugin (`hecks/ports/persistence/plugins/era`) rather than
20
- # requiring every app to carry it whether or not anything ever binds
21
- # `persisted_by "PostgresEra"`. `Module#autoload` is the one-line seam
22
- # that makes that genuinely load-nothing-until-asked instead of pushing
23
- # an explicit `require` onto every one of the ~15 generic `bin/*` tools
24
- # that boot an arbitrary checked-in domain (several of which — pizzas,
25
- # compliance, chess, roster — bind PostgresEra): the first real
26
- # `Adapters.const_get("PostgresEra")` (`registry/verification.rb`'s own
27
- # adapter resolution, unchanged) transparently `require`s the plugin
28
- # entry point, which defines the constant and calls `register_plugin`
29
- # as its own side effect (`plugins/era.rb`'s last line) — the exact
30
- # same effect an explicit `require "hecks/ports/persistence/plugins/
31
- # era"` has, just deferred to the moment something is actually asked
32
- # for `PostgresEra` rather than sprinkled across every caller that
33
- # might. A domain that never binds it never triggers the autoload, so
34
- # never loads a line of era code — unchanged from ADR 0033's own goal.
10
+ # PostgresEra lives in a persistence plugin, autoloaded so a domain that never binds
11
+ # it loads no era code; the plugin entry point registers itself on load (ADR 0033).
35
12
  Hecks::Adapters.autoload(:PostgresEra, "hecks/ports/persistence/plugins/era")
36
13
  require_relative "driven/lambda"
37
14
  require_relative "driven/heki"
@@ -44,16 +21,8 @@ require_relative "driven/tenant_provisioner"
44
21
  require_relative "driven/secure_random_identity"
45
22
  require_relative "driven/in_process_key_vault"
46
23
  require_relative "driven/system_clock"
47
- # `SequentialIdentity` — the deterministic identity_generation test
48
- # double — is not required here on purpose. It lives at
49
- # spec/fixtures/sequential_identity.{adapter,rb}, loaded explicitly by
50
- # whichever spec wants it: this file's `require_relative` list is what
51
- # `Folder#load_library`'s `.adapter` glob backs, and `.adapter` DSL
52
- # declarations register into every registry any real `Hecks.boot` ever
53
- # builds. Two adapters answering `identity_generation` unconditionally
54
- # would make the port permanently ambiguous — `Ports::IdentityGeneration
55
- # .adapter` refuses to choose between more than one — for every
56
- # consumer, forever, not just specs that want the deterministic one.
24
+ # SequentialIdentity (spec/fixtures) is deliberately not required here: a second
25
+ # `identity_generation` adapter would make that port ambiguous for every boot.
57
26
  require_relative "driven/governance_authorization"
58
27
  require_relative "driven/identity_registry"
59
28
  require_relative "driven/google_authentication"