hecks 1.3.0 → 1.5.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 (373) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +72 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +83 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
  7. data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
  9. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  10. data/lib/hecks/adapters/driven/heki.rb +106 -9
  11. data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
  12. data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
  13. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  15. data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
  16. data/lib/hecks/adapters/driven/lambda.rb +82 -35
  17. data/lib/hecks/adapters/driven/local_storage.rb +83 -10
  18. data/lib/hecks/adapters/driven/memory.rb +205 -9
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  24. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  25. data/lib/hecks/adapters/driven/prism.rb +50 -11
  26. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  31. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  32. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  33. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  34. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  35. data/lib/hecks/adapters/driven.rb +6 -4
  36. data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
  37. data/lib/hecks/behaviors/dsl.rb +60 -2
  38. data/lib/hecks/behaviors/expectations.rb +190 -29
  39. data/lib/hecks/behaviors/ir.rb +12 -1
  40. data/lib/hecks/behaviors/rspec.rb +9 -1
  41. data/lib/hecks/behaviors/runner.rb +21 -2
  42. data/lib/hecks/behaviors.rb +9 -1
  43. data/lib/hecks/bluebook/aggregate.rb +43 -13
  44. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
  45. data/lib/hecks/bluebook/assembly/build.rb +26 -5
  46. data/lib/hecks/bluebook/assembly/contract.rb +98 -23
  47. data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
  48. data/lib/hecks/bluebook/assembly/marks.rb +159 -30
  49. data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
  50. data/lib/hecks/bluebook/assembly.rb +32 -14
  51. data/lib/hecks/bluebook/attribute.rb +26 -12
  52. data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
  53. data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
  54. data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
  55. data/lib/hecks/bluebook/behaviour/command.rb +55 -25
  56. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
  57. data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
  58. data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
  59. data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
  60. data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
  61. data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
  62. data/lib/hecks/bluebook/behaviour/query.rb +6 -1
  63. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
  64. data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
  65. data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
  66. data/lib/hecks/bluebook/capabilities.rb +27 -0
  67. data/lib/hecks/bluebook/chapter.rb +51 -9
  68. data/lib/hecks/bluebook/command.rb +62 -17
  69. data/lib/hecks/bluebook/domain_port.rb +34 -9
  70. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  71. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  72. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
  73. data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
  74. data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
  75. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
  76. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
  77. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  78. data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
  79. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  80. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  81. data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
  82. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  83. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
  84. data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
  85. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  86. data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
  87. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  88. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  89. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
  90. data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
  91. data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
  92. data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
  93. data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
  94. data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
  95. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  96. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  97. data/lib/hecks/bluebook/entity.rb +40 -11
  98. data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
  99. data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
  100. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
  101. data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
  102. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
  103. data/lib/hecks/bluebook/expression/resolver.rb +369 -128
  104. data/lib/hecks/bluebook/hexagon.rb +35 -1
  105. data/lib/hecks/bluebook/lifecycle.rb +12 -1
  106. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
  107. data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
  108. data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
  109. data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
  110. data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
  111. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
  112. data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
  113. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
  114. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
  115. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
  116. data/lib/hecks/bluebook/meta_validator.rb +235 -139
  117. data/lib/hecks/bluebook/model_check.rb +434 -104
  118. data/lib/hecks/bluebook/pattern_subset.rb +32 -10
  119. data/lib/hecks/bluebook/policy.rb +15 -13
  120. data/lib/hecks/bluebook/process_manager.rb +27 -14
  121. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  122. data/lib/hecks/bluebook/project_loader.rb +40 -0
  123. data/lib/hecks/bluebook/project_register.rb +50 -6
  124. data/lib/hecks/bluebook/query.rb +31 -4
  125. data/lib/hecks/bluebook/read_model.rb +35 -15
  126. data/lib/hecks/bluebook/reference.rb +26 -13
  127. data/lib/hecks/bluebook/smoke_test.rb +46 -23
  128. data/lib/hecks/bluebook/synthesizer.rb +46 -12
  129. data/lib/hecks/bluebook/translation.rb +34 -5
  130. data/lib/hecks/bluebook/value_object.rb +29 -11
  131. data/lib/hecks/bluebook.rb +5 -6
  132. data/lib/hecks/codemod.rb +138 -50
  133. data/lib/hecks/construct.rb +21 -7
  134. data/lib/hecks/corpus.rb +438 -0
  135. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  136. data/lib/hecks/doc/reference.rb +200 -31
  137. data/lib/hecks/embryonaut_bluebook.rb +38 -15
  138. data/lib/hecks/facade/cli_door.rb +69 -10
  139. data/lib/hecks/facade/cli_runner.rb +105 -24
  140. data/lib/hecks/facade/command_request.rb +23 -0
  141. data/lib/hecks/facade/handle.rb +155 -35
  142. data/lib/hecks/facade/json_door.rb +106 -25
  143. data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
  144. data/lib/hecks/facade/surface/chapter.rb +26 -17
  145. data/lib/hecks/facade/surface.rb +16 -3
  146. data/lib/hecks/facade.rb +15 -4
  147. data/lib/hecks/forms/app.rb +46 -30
  148. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  149. data/lib/hecks/forms/field_renderer.rb +142 -6
  150. data/lib/hecks/forms/field_shape.rb +183 -19
  151. data/lib/hecks/forms/html.rb +51 -7
  152. data/lib/hecks/forms/index_renderer.rb +14 -2
  153. data/lib/hecks/forms/page.rb +14 -0
  154. data/lib/hecks/forms/params.rb +120 -23
  155. data/lib/hecks/forms/port_argument.rb +14 -2
  156. data/lib/hecks/forms/query_form_renderer.rb +65 -2
  157. data/lib/hecks/forms/record_renderer.rb +60 -2
  158. data/lib/hecks/forms/record_table.rb +28 -1
  159. data/lib/hecks/forms/reference_options.rb +24 -0
  160. data/lib/hecks/forms/value_object_shape.rb +13 -3
  161. data/lib/hecks/forms.rb +24 -4
  162. data/lib/hecks/fqn.rb +59 -1
  163. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  164. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  165. data/lib/hecks/framework/oidc.json +15 -0
  166. data/lib/hecks/framework.rb +79 -25
  167. data/lib/hecks/freezer.rb +27 -11
  168. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
  169. data/lib/hecks/fuzzing/combination_miner.rb +178 -0
  170. data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
  171. data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
  172. data/lib/hecks/fuzzing/differential.rb +192 -0
  173. data/lib/hecks/fuzzing/domain_generator.rb +871 -0
  174. data/lib/hecks/fuzzing/era_boundary.rb +68 -19
  175. data/lib/hecks/fuzzing/form_census.rb +121 -20
  176. data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
  177. data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
  178. data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
  179. data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
  180. data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
  181. data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
  182. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
  183. data/lib/hecks/fuzzing/properties/guards.rb +86 -42
  184. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
  185. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
  186. data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
  187. data/lib/hecks/fuzzing/properties/querying.rb +82 -28
  188. data/lib/hecks/fuzzing/properties.rb +84 -51
  189. data/lib/hecks/fuzzing/qa_settings.rb +164 -0
  190. data/lib/hecks/fuzzing/replay.rb +241 -133
  191. data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
  192. data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
  193. data/lib/hecks/fuzzing/self_consistency.rb +315 -137
  194. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
  195. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  196. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  197. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  198. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
  199. data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
  200. data/lib/hecks/fuzzing/shrinker.rb +309 -0
  201. data/lib/hecks/fuzzing/structural_skips.rb +37 -130
  202. data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
  203. data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
  204. data/lib/hecks/fuzzing/value_generator.rb +110 -22
  205. data/lib/hecks/fuzzing.rb +5 -0
  206. data/lib/hecks/grammar/evolve.rb +188 -12
  207. data/lib/hecks/grammar.rb +53 -7
  208. data/lib/hecks/ir.rb +51 -20
  209. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  210. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  211. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  212. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  213. data/lib/hecks/language/oidc.json +5 -0
  214. data/lib/hecks/literal.rb +41 -9
  215. data/lib/hecks/naming.rb +112 -31
  216. data/lib/hecks/ports/access_control.rb +53 -2
  217. data/lib/hecks/ports/agent/answers.rb +83 -6
  218. data/lib/hecks/ports/agent.rb +119 -35
  219. data/lib/hecks/ports/authentication.rb +44 -4
  220. data/lib/hecks/ports/authorization.rb +50 -11
  221. data/lib/hecks/ports/clock.rb +42 -23
  222. data/lib/hecks/ports/extraction.rb +16 -0
  223. data/lib/hecks/ports/identity_assignment.rb +23 -2
  224. data/lib/hecks/ports/identity_generation.rb +17 -3
  225. data/lib/hecks/ports/identity_resolution.rb +17 -1
  226. data/lib/hecks/ports/key_vault.port +6 -0
  227. data/lib/hecks/ports/key_vault.rb +58 -0
  228. data/lib/hecks/ports/loading.rb +4 -0
  229. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  230. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  231. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  232. data/lib/hecks/ports/persistence/execution.rb +4 -0
  233. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  234. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  235. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  236. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  237. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  238. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  239. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  250. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  251. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  252. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
  253. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  254. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  255. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  256. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  257. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  258. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
  259. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  260. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  261. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  262. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
  263. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
  264. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  265. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
  266. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  267. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  268. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  269. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  270. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  271. data/lib/hecks/ports/persistence.rb +36 -1
  272. data/lib/hecks/ports/projection.rb +61 -7
  273. data/lib/hecks/ports/query/in_memory.rb +41 -8
  274. data/lib/hecks/ports/query/ordering.rb +21 -6
  275. data/lib/hecks/ports/query.rb +35 -0
  276. data/lib/hecks/ports.rb +1 -0
  277. data/lib/hecks/projections/bootstrap_table.rb +147 -0
  278. data/lib/hecks/projections/diagrams.rb +315 -79
  279. data/lib/hecks/projections/glossary/html.rb +91 -3
  280. data/lib/hecks/projections/glossary/markdown.rb +50 -3
  281. data/lib/hecks/projections/glossary/mermaid.rb +49 -1
  282. data/lib/hecks/projections/glossary/sections.rb +1 -1
  283. data/lib/hecks/projections/glossary/sentences.rb +110 -5
  284. data/lib/hecks/projections/glossary.rb +173 -31
  285. data/lib/hecks/projections/ir.rb +1 -1
  286. data/lib/hecks/projections/model/deviations.rb +62 -17
  287. data/lib/hecks/projections/model.rb +75 -21
  288. data/lib/hecks/projections/oidc.rb +45 -8
  289. data/lib/hecks/projections/parser_table.rb +53 -12
  290. data/lib/hecks/projections/reference.rb +15 -3
  291. data/lib/hecks/projections/rust_vocabulary.rb +646 -0
  292. data/lib/hecks/projections/shape.rb +10 -2
  293. data/lib/hecks/projections/statements.rb +69 -22
  294. data/lib/hecks/projections/vocabulary.rb +26 -9
  295. data/lib/hecks/projections.rb +5 -3
  296. data/lib/hecks/projector/cli_projector.rb +245 -37
  297. data/lib/hecks/projector/docs_projector.rb +154 -28
  298. data/lib/hecks/projector/exporter.rb +104 -29
  299. data/lib/hecks/projector/ir_projector.rb +7 -1
  300. data/lib/hecks/projector/narrate_projector.rb +144 -23
  301. data/lib/hecks/projector/target.rb +42 -18
  302. data/lib/hecks/projector.rb +86 -18
  303. data/lib/hecks/query_ir.rb +94 -47
  304. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  305. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  306. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  307. data/lib/hecks/query_specification/common/null_policy.rb +61 -15
  308. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  309. data/lib/hecks/query_specification/common/options.rb +25 -0
  310. data/lib/hecks/query_specification/field_path.rb +69 -15
  311. data/lib/hecks/query_specification/hop_path.rb +57 -20
  312. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  313. data/lib/hecks/rendering.rb +9 -3
  314. data/lib/hecks/router/namespace_installer.rb +16 -3
  315. data/lib/hecks/router.rb +56 -1
  316. data/lib/hecks/runtime/aggregate_lock.rb +20 -11
  317. data/lib/hecks/runtime/boot_gates.rb +21 -3
  318. data/lib/hecks/runtime/caller.rb +40 -8
  319. data/lib/hecks/runtime/capability_graph.rb +13 -2
  320. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
  321. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
  322. data/lib/hecks/runtime/command_interpreter.rb +195 -113
  323. data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
  324. data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
  325. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  326. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  327. data/lib/hecks/runtime/command_rules/references.rb +145 -55
  328. data/lib/hecks/runtime/command_rules.rb +3 -0
  329. data/lib/hecks/runtime/dependency_planning.rb +56 -11
  330. data/lib/hecks/runtime/dispatcher.rb +263 -92
  331. data/lib/hecks/runtime/entity_element.rb +304 -67
  332. data/lib/hecks/runtime/entity_interpreter.rb +149 -88
  333. data/lib/hecks/runtime/errors.rb +37 -23
  334. data/lib/hecks/runtime/event.rb +15 -10
  335. data/lib/hecks/runtime/identity.rb +91 -23
  336. data/lib/hecks/runtime/instance.rb +103 -18
  337. data/lib/hecks/runtime/interpreting.rb +24 -16
  338. data/lib/hecks/runtime/invocation.rb +358 -0
  339. data/lib/hecks/runtime/loader.rb +105 -19
  340. data/lib/hecks/runtime/outbox.rb +164 -26
  341. data/lib/hecks/runtime/policy_interpreter.rb +73 -60
  342. data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
  343. data/lib/hecks/runtime/query_interpreter.rb +93 -74
  344. data/lib/hecks/runtime/reaction_invocation.rb +73 -28
  345. data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
  346. data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
  347. data/lib/hecks/runtime/reference_hop.rb +48 -6
  348. data/lib/hecks/runtime/refusal_wording.rb +142 -115
  349. data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
  350. data/lib/hecks/runtime/registry/verification.rb +153 -28
  351. data/lib/hecks/runtime/registry.rb +202 -32
  352. data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
  353. data/lib/hecks/runtime/routing.rb +36 -154
  354. data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
  355. data/lib/hecks/runtime/saga_interpreter.rb +90 -76
  356. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  357. data/lib/hecks/runtime/tenant_check.rb +33 -13
  358. data/lib/hecks/runtime/tenant_scope.rb +23 -5
  359. data/lib/hecks/runtime/value/admission.rb +75 -30
  360. data/lib/hecks/runtime/value/coercion.rb +555 -142
  361. data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
  362. data/lib/hecks/runtime/value.rb +71 -21
  363. data/lib/hecks/runtime.rb +39 -7
  364. data/lib/hecks/storehouse.rb +368 -72
  365. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  366. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  367. data/lib/hecks/version.rb +3 -3
  368. data/lib/hecks/vocabulary.rb +205 -4
  369. data/lib/hecks.rb +91 -11
  370. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
  371. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
  372. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
  373. metadata +28 -2
@@ -3,19 +3,19 @@ require_relative "../runtime/registry"
3
3
  module Hecks
4
4
  module Ports
5
5
  # A creating command with no natural key needs a value from
6
- # SOMEWHERE to be its identity — this is that somewhere. Resolved
6
+ # somewhere to be its identity — this is that somewhere. Resolved
7
7
  # the same way `Ports::Extraction` resolves its own adapter (one
8
8
  # adapter registry-wide implements this port, not a per-aggregate
9
9
  # binding the way persistence needs — different aggregates have no
10
10
  # real reason to want different id-generation strategies within
11
11
  # one running app).
12
12
  #
13
- # REPLAY NEEDS NO SUPPRESSION HERE, ON PURPOSE. A UUID minted for a
13
+ # Replay needs no suppression here, on purpose. A UUID minted for a
14
14
  # creating command's identity gets baked into that step's own args
15
15
  # — an ordinary string value — the moment it's generated, by
16
16
  # whoever issues the first, live dispatch. A recorded corpus
17
17
  # script or a captured fuzz-replay step already holds that
18
- # concrete value; replaying it calls `dispatch` with the SAME
18
+ # concrete value; replaying it calls `dispatch` with the same
19
19
  # args, and this module is never consulted again, for the same
20
20
  # reason `SecureRandom.uuid` never runs twice for one
21
21
  # already-recorded step today. `Event#occurred_at`
@@ -28,8 +28,22 @@ module Hecks
28
28
 
29
29
  module_function
30
30
 
31
+ # Mints a fresh identity value from the bound adapter.
32
+ #
33
+ # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
34
+ # @return [String] a newly minted identity: a random UUID from `SecureRandomIdentity`,
35
+ # or a decimal counter (`"1"`, `"2"`, …) from the deterministic `SequentialIdentity`
36
+ # spec double — unique per adapter, not guaranteed UUID-shaped
37
+ # @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
38
+ # (see `adapter`)
31
39
  def uuid(registry) = adapter(registry).uuid
32
40
 
41
+ # Finds the single adapter bound to this port, refusing an ambiguous wiring.
42
+ #
43
+ # @param registry [Runtime::Registry] the booted registry to search
44
+ # @return [Module] the adapter module or class implementing this port
45
+ # @raise [Runtime::WiringError] if no adapter, or more than one, implements this port,
46
+ # or the one that does has no Ruby implementation under `Hecks::Adapters`
33
47
  def adapter(registry)
34
48
  implementations = registry.adapters.values.select { |a| a.port == NAME }
35
49
 
@@ -2,7 +2,7 @@ require_relative "../runtime/registry"
2
2
 
3
3
  module Hecks
4
4
  module Ports
5
- # AN AUTHENTICATED (issuer, subject) PAIR, RESOLVED TO A STABLE
5
+ # An authenticated (issuer, subject) pair, resolved to the id of a stable
6
6
  # `Identity` — the other half of the same symmetry `Authorization`
7
7
  # already has for Governance: one adapter registry-wide answers this
8
8
  # port, resolved the same zero/one/many way, so an application never
@@ -17,10 +17,26 @@ module Hecks
17
17
 
18
18
  module_function
19
19
 
20
+ # Looks up the id of the identity an authenticated (issuer, subject) pair is linked to.
21
+ #
22
+ # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
23
+ # @param issuer [String] the OIDC issuer that authenticated the caller
24
+ # @param subject [String] the OIDC subject the issuer vouches for
25
+ # @return [String, nil] the linked identity's id, usable as an `actor_id` for
26
+ # `Ports::Authorization.holds_role?` — not an `Identity` record; nil if nothing has
27
+ # linked this pair
28
+ # @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
29
+ # (see `adapter`)
20
30
  def resolve(registry, issuer:, subject:)
21
31
  adapter(registry).resolve(registry, issuer: issuer, subject: subject)
22
32
  end
23
33
 
34
+ # Finds the single adapter bound to this port, refusing an ambiguous wiring.
35
+ #
36
+ # @param registry [Runtime::Registry] the booted registry to search
37
+ # @return [Module] the adapter module or class implementing this port
38
+ # @raise [Runtime::WiringError] if no adapter, or more than one, implements this port,
39
+ # or the one that does has no Ruby implementation under `Hecks::Adapters`
24
40
  def adapter(registry)
25
41
  implementations = registry.adapters.values.select { |a| a.port == NAME }
26
42
 
@@ -0,0 +1,6 @@
1
+ Hecks.port "key_vault" do
2
+ verb "vaulted_by"
3
+ signal :reply
4
+ answers :issue
5
+ answers :destroy
6
+ end
@@ -0,0 +1,58 @@
1
+ require_relative "../runtime/registry"
2
+
3
+ module Hecks
4
+ module Ports
5
+ # A subject's own encryption key lives here, one boundary removed from
6
+ # any event or bluebook attribute — cryptoshredding depends on the key
7
+ # material never being reachable except through this port's opaque
8
+ # reference (see `Privacy::SubjectKey`, `lib/hecks/framework/bluebook/
9
+ # privacy.bluebook`). Resolved the same way `Ports::IdentityGeneration`
10
+ # resolves its own adapter: one adapter registry-wide answers this
11
+ # port, not a per-aggregate binding.
12
+ module KeyVault
13
+ NAME = "key_vault".freeze
14
+
15
+ module_function
16
+
17
+ # Mints a fresh per-subject encryption key and hands back an opaque reference to it.
18
+ #
19
+ # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
20
+ # @param subject_id [String] the data subject the key is being issued for
21
+ # @return [String] an opaque key reference; the key material itself never leaves the
22
+ # bound adapter, and is never suitable to store on an event or a bluebook attribute
23
+ def issue(registry, subject_id:) = adapter(registry).issue(subject_id: subject_id)
24
+
25
+ # Irrevocably destroys a key that {#issue} already returned a reference for.
26
+ #
27
+ # Ciphertext produced under `key_reference` becomes permanently unrecoverable the
28
+ # moment this returns — the mechanism a right-to-erasure request satisfies without
29
+ # rewriting or deleting any event.
30
+ #
31
+ # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
32
+ # @param key_reference [String] the opaque reference {#issue} returned
33
+ # @return [Boolean] true when a live key was destroyed; false when this reference was
34
+ # already destroyed, or was never issued
35
+ def destroy(registry, key_reference:) = adapter(registry).destroy(key_reference: key_reference)
36
+
37
+ # Finds the single adapter bound to this port, refusing an ambiguous wiring.
38
+ #
39
+ # @param registry [Runtime::Registry] the booted registry to search
40
+ # @return [Module] the adapter module or class implementing this port
41
+ # @raise [Runtime::WiringError] if no adapter, or more than one, implements this port
42
+ def adapter(registry)
43
+ implementations = registry.adapters.values.select { |a| a.port == NAME }
44
+
45
+ case implementations.size
46
+ when 1 then registry.adapter_class(implementations.first.name)
47
+ when 0
48
+ raise Runtime::WiringError,
49
+ "no adapter implements the #{NAME} port — nothing can issue or destroy a key"
50
+ else
51
+ raise Runtime::WiringError,
52
+ "#{implementations.size} adapters implement the #{NAME} port " \
53
+ "(#{implementations.map(&:name).sort.join(', ')}) — the runtime will not choose for you"
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -10,6 +10,10 @@ module Hecks
10
10
 
11
11
  module_function
12
12
 
13
+ # Builds the loader a boot starts from, before any registry exists to resolve one.
14
+ #
15
+ # @return [Adapters::Folder] a new `Folder` adapter with no settings and no root,
16
+ # constructed directly rather than resolved through a registry
13
17
  def bootstrap = Adapters::Folder.new
14
18
  end
15
19
  end
@@ -7,7 +7,14 @@ module Hecks
7
7
  # `mirrors` is durable replication intent. It is part of the same
8
8
  # append as the authoritative state, never a second outbox store.
9
9
  Entry = Struct.new(:operation, :id, :state, :mirrors, keyword_init: true) do
10
+ # Tells a projecting adapter that this entry writes a record.
11
+ #
12
+ # @return [Boolean] true when `operation` is the String `"save"`
10
13
  def save? = operation == "save"
14
+
15
+ # Tells a projecting adapter that this entry removes a record.
16
+ #
17
+ # @return [Boolean] true when `operation` is the String `"delete"`
11
18
  def delete? = operation == "delete"
12
19
  end
13
20
 
@@ -17,8 +24,14 @@ module Hecks
17
24
  class AppendOnly
18
25
  attr_reader :adapter
19
26
 
27
+ # Names the aggregate this repository stores, as the wrapped adapter holds it.
28
+ #
29
+ # @return [Bluebook::Aggregate] the aggregate the adapter was built for
20
30
  def aggregate = @adapter.aggregate
21
31
 
32
+ # @param adapter [Object] a driven persistence adapter; it must respond to `append`,
33
+ # `project` and `entries`
34
+ # @raise [Runtime::WiringError] if the adapter lacks any of those three methods
22
35
  def initialize(adapter)
23
36
  @adapter = adapter
24
37
  required = %i[append project entries]
@@ -29,58 +42,130 @@ module Hecks
29
42
  end
30
43
  end
31
44
 
45
+ # Reads one record's current projected state from the adapter.
46
+ #
47
+ # @param id [String, Object] the record's identity; adapters compare it as `id.to_s`
48
+ # @return [Runtime::Instance, nil] the stored record, or nil when no record has that id
32
49
  def find(id) = @adapter.find(id)
50
+
51
+ # Lists every record the adapter currently projects, forwarding the keywords untouched.
52
+ #
53
+ # The shipped adapters accept `order_by:` (an attribute name, default nil for id or
54
+ # insertion order) and `direction:` (`:asc` or `:desc`).
55
+ #
56
+ # @return [Array<Runtime::Instance>] the stored records; `[]` when there are none
57
+ # @raise [Runtime::WiringError] from a SQL adapter when `order_by:` names no attribute
58
+ # of the aggregate
33
59
  def all(**) = @adapter.all(**)
60
+
61
+ # Counts the records the adapter currently projects.
62
+ #
63
+ # @return [Integer] number of stored records, not of journal entries
34
64
  def count = @adapter.count
65
+
66
+ # Reads the adapter's whole journal, oldest entry first.
67
+ #
68
+ # @return [Array<Persistence::Entry>] every appended entry with decoded state; `[]` for
69
+ # an empty journal or a `RemoteRuntime` adapter
70
+ # @raise [Runtime::WiringError] if a guarded adapter answers an entry whose state is
71
+ # undecoded (`CodecBoundary.check_entries!`)
35
72
  def entries = @adapter.entries
36
73
 
74
+ # Lists the optional persistence behaviours the adapter advertises.
75
+ #
76
+ # @return [Array<Symbol>] frozen capability names such as `:atomic_put`,
77
+ # `:optimistic_concurrency` or `:cross_process_lock`; `[]` when the adapter declares
78
+ # no `persistence_capabilities`
37
79
  def capabilities
38
80
  return [] unless @adapter.respond_to?(:persistence_capabilities)
39
81
 
40
82
  Array(@adapter.persistence_capabilities).map(&:to_sym).freeze
41
83
  end
42
84
 
85
+ # Clears the adapter's stored records and journal so a kept runtime starts clean.
86
+ #
87
+ # @return [Object] whatever the adapter's `reset!` returns; every shipped adapter
88
+ # returns itself
89
+ # @raise [Runtime::WiringError] if the adapter has no `reset!`, or (PostgresEra) if row
90
+ # level security silently matched none of the journal rows
43
91
  def reset!
44
92
  raise Runtime::WiringError, "append-only adapter cannot reset" unless @adapter.respond_to?(:reset!)
45
93
 
46
94
  @adapter.reset!
47
95
  end
48
96
 
49
- # NOT an endless `def events = ... if ...` — that modifier binds to
50
- # the WHOLE `def`, not just its body, so it evaluates against
97
+ # Reads the events the adapter has durably recorded.
98
+ #
99
+ # Not an endless `def events = ... if ...` — that modifier binds to
100
+ # the whole `def`, not just its body, so it evaluates against
51
101
  # `@adapter` while `@adapter` is still nil (class-body time,
52
102
  # before `initialize` ever runs) and silently skips defining the
53
103
  # method at all. Found live: nothing in this codebase called
54
104
  # `AppendOnly#events` before Memory got a `reset!` test that did.
105
+ #
106
+ # @return [Array<Runtime::Event>, nil] recorded events, oldest first; nil when the
107
+ # adapter keeps no event log
55
108
  def events
56
109
  @adapter.events if @adapter.respond_to?(:events)
57
110
  end
58
111
 
112
+ # Replays the whole journal through `project` to rebuild the projected records.
113
+ #
59
114
  # An append is durable before a projection is attempted. Replaying the
60
115
  # log restores a snapshot/table after a crash in that small window.
116
+ #
117
+ # @return [Persistence::AppendOnly] self, so a factory can build and recover in one
118
+ # expression
61
119
  def recover!
62
120
  entries.each { |entry| project(entry) }
63
121
  self
64
122
  end
65
123
 
124
+ # Writes one entry to the adapter's durable journal, before any projection of it.
125
+ #
126
+ # @param entry [Persistence::Entry] the save or delete to journal
127
+ # @return [Persistence::Entry] the entry as the adapter returns it; every shipped
128
+ # adapter returns the entry it was given
129
+ # @raise [Runtime::WiringError] from a `RemoteRuntime` adapter, which has no local
130
+ # journal, and from PostgresEra when its era is superseded
66
131
  def append(entry) = @adapter.append(entry)
132
+
133
+ # Applies one journaled entry to the adapter's current-state store.
134
+ #
135
+ # @param entry [Persistence::Entry] the save or delete to materialize
136
+ # @return [Object, nil] adapter-defined: Memory, Sqlite, Postgres and PostgresEra answer
137
+ # the saved `Runtime::Instance`; Heki and `SqliteProjection` answer the entry;
138
+ # a delete answers a driver result, the removed record, or nil
139
+ # @raise [Runtime::WiringError] from a `RemoteRuntime` adapter, which projects nothing
140
+ # locally
67
141
  def project(entry) = @adapter.project(entry)
68
142
 
143
+ # Journals an instance's state, projects it, and reports how the write landed.
144
+ #
69
145
  # Returns an `Outcome`, not a bare `Instance` — every call site
70
146
  # (`CommandInterpreter`/`EntityInterpreter`'s own `step_save`,
71
147
  # `RebuildSweep#refresh`) reads it that way.
72
148
  #
73
149
  # `expected_version:` requests optimistic-concurrency CAS — commit
74
- # only if the stored record's version still matches what THIS
150
+ # only if the stored record's version still matches what this
75
151
  # instance was read at. It is `nil` both when a caller explicitly
76
152
  # doesn't want CAS (`RebuildSweep#refresh`'s own projection-field
77
153
  # touch-up, which has no `given` to protect) and when the instance
78
154
  # is brand new (never read from storage, so `instance.version` is
79
155
  # nil) — both cases fall through to the plain, unconditional
80
156
  # `project(entry)` below, byte-for-byte today's behavior. Only an
81
- # adapter that both receives a non-nil `expected_version` AND
157
+ # adapter that both receives a non-nil `expected_version` and
82
158
  # declares `:optimistic_concurrency` gets CAS treatment; every
83
159
  # other adapter/call site is unaffected.
160
+ #
161
+ # @param instance [Runtime::Instance] the record to persist; its `state` is shallow
162
+ # copied into the entry
163
+ # @param expected_version [Integer, nil] the version the instance was read at, or nil
164
+ # for an unconditional write
165
+ # @return [Persistence::Outcome] status `:saved` or `:stale`; on `:saved` its `instance`
166
+ # is what the adapter's `project` answered, else the instance passed in; on `:stale`
167
+ # it is the instance passed in. The entry is journaled even when the result is `:stale`
168
+ # @raise [Runtime::WiringError] when the adapter's `append` or `project` refuses
84
169
  def save(instance, expected_version: nil)
85
170
  entry = Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
86
171
  append(entry)
@@ -94,6 +179,17 @@ module Hecks
94
179
  Outcome.new(status: :saved, instance: saved || instance)
95
180
  end
96
181
 
182
+ # Writes an instance through the adapter's own existence-check-and-write critical
183
+ # section, so a create cannot race another writer.
184
+ #
185
+ # @param instance [Runtime::Instance] the record to persist
186
+ # @param insert_only [Boolean] true to refuse replacing a record that already exists
187
+ # @return [Persistence::Outcome] status `:inserted`, `:replaced`, or `:conflicted` when
188
+ # `insert_only` met an existing record and nothing was written; `instance` is always
189
+ # the instance passed in
190
+ # @raise [Runtime::WiringError] if the adapter does not both advertise `:atomic_put`
191
+ # and implement it
192
+ # @raise [ArgumentError] if the adapter answers a status outside `Outcome::STATUSES`
97
193
  def atomic_put(instance, insert_only: false)
98
194
  unless capabilities.include?(:atomic_put) && @adapter.respond_to?(:atomic_put)
99
195
  raise Runtime::WiringError,
@@ -105,6 +201,12 @@ module Hecks
105
201
  Outcome.new(status: status, instance: instance)
106
202
  end
107
203
 
204
+ # Journals and projects the removal of one record, if it exists.
205
+ #
206
+ # @param id [String, Object] the record's identity; journaled as `id.to_s`
207
+ # @return [Boolean] true when a record was found and deleted, false when there was
208
+ # none and nothing was journaled
209
+ # @raise [Runtime::WiringError] when the adapter's `append` or `project` refuses
108
210
  def delete(id)
109
211
  return false unless find(id)
110
212
 
@@ -114,7 +216,9 @@ module Hecks
114
216
  true
115
217
  end
116
218
 
117
- # NOT an endless `def record_event = ... if ...` — same gotcha as
219
+ # Records one emitted event durably, when the adapter keeps an event log.
220
+ #
221
+ # Not an endless `def record_event = ... if ...` — same gotcha as
118
222
  # `events` above, and it bit for real here: this guard evaluated
119
223
  # against `@adapter` at class-body time (nil, always false), so
120
224
  # `record_event` was never defined at all. `emission.rb`'s own
@@ -126,11 +230,17 @@ module Hecks
126
230
  # found nothing to tail. `sqlite_spec.rb`/`postgres_spec.rb`/
127
231
  # `postgres_era_spec.rb` all call `adapter.record_event` directly,
128
232
  # bypassing this wrapper — which is exactly why no spec noticed.
233
+ #
234
+ # @param event [Runtime::Event] the event to record
235
+ # @return [Object, nil] adapter-defined write result (an Array for Memory, a driver
236
+ # result for the SQL adapters); nil when the adapter has no `record_event`
129
237
  def record_event(event)
130
238
  @adapter.record_event(event) if @adapter.respond_to?(:record_event)
131
239
  end
132
240
 
133
- # ONE COMMIT BOUNDARY FOR SAVE + EMIT + OUTBOX — `Interpreting#
241
+ # Runs the block inside the adapter's transaction, or plainly when it has none.
242
+ #
243
+ # One COMMIT boundary for save + emit + outbox — `Interpreting#
134
244
  # run_dispatch_order` runs the `save` and `emit` steps inside
135
245
  # this block, so an adapter that owns a real transaction
136
246
  # (Sqlite, Postgres) commits the aggregate row, its journal
@@ -141,39 +251,93 @@ module Hecks
141
251
  # re-entrant (both SQL adapters check for an open transaction
142
252
  # first) — a reaction dispatched from inside a drain never nests
143
253
  # here anyway, because draining happens after this block returns.
254
+ #
255
+ # @yield the writes to commit together; an exception raised inside rolls back an
256
+ # adapter-owned transaction and propagates
257
+ # @return [Object] adapter-defined; the block's own value for every adapter without a
258
+ # `transaction` and for Memory
144
259
  def transaction(&)
145
260
  return @adapter.transaction(&) if @adapter.respond_to?(:transaction)
146
261
 
147
262
  yield
148
263
  end
149
264
 
150
- # ONLY an adapter advertising `:cross_process_lock` (PostgresEra —
265
+ # Runs the block while holding the adapter's cross-process write lock.
266
+ #
267
+ # Only an adapter advertising `:cross_process_lock` (PostgresEra —
151
268
  # see ADR 0036) implements this; `run_dispatch_order_with_isolation`
152
269
  # (runtime/interpreting.rb) checks `capabilities` before ever
153
270
  # calling it, so the plain `yield` fallback here only guards
154
271
  # against a stray direct call, not the real dispatch path.
272
+ #
273
+ # @yield the dispatch to run with writers serialised
274
+ # @return [Object] adapter-defined; the block's own value when the adapter has no
275
+ # `with_write_lock`
155
276
  def with_write_lock(&)
156
277
  return @adapter.with_write_lock(&) if @adapter.respond_to?(:with_write_lock)
157
278
 
158
279
  yield
159
280
  end
160
281
 
161
- # THE OUTBOX CONTRACT — four optional adapter methods, probed
282
+ # **The outbox contract** — four optional adapter methods, probed
162
283
  # together the way `save_saga`/`delete_saga`/`each_saga` are
163
284
  # (`Registry::SagaPersistence`): an adapter either has an outbox
164
285
  # or it doesn't, never half of one. See `Runtime::Outbox`.
165
286
  OUTBOX_METHODS = %i[outbox_enqueue outbox_claim outbox_settle outbox_rows].freeze
166
287
 
288
+ # Reports whether the adapter implements the whole outbox contract; the answer is
289
+ # memoized per repository.
290
+ #
291
+ # @return [Boolean] true only when the adapter responds to all four `OUTBOX_METHODS`
167
292
  def outbox?
168
293
  @outbox = OUTBOX_METHODS.all? { |method| @adapter.respond_to?(method) } if @outbox.nil?
169
294
  @outbox
170
295
  end
171
296
 
297
+ # Stores pending outbox rows, skipping any whose `delivery_id` is already held.
298
+ #
299
+ # Call only after `outbox?` answers true; the other three outbox methods share that
300
+ # precondition.
301
+ #
302
+ # @param rows [Array<Runtime::Outbox::Row>] the rows to enqueue, one per event and consumer
303
+ # @return [Array<Runtime::Outbox::Row>] the rows actually stored, with `id` assigned;
304
+ # duplicates are dropped
172
305
  def outbox_enqueue(rows) = @adapter.outbox_enqueue(rows)
306
+
307
+ # Moves one pending outbox row to `claimed` and counts the attempt.
308
+ #
309
+ # @param id [Integer] the row id assigned by `outbox_enqueue`
310
+ # @return [Boolean] true when this call claimed the row; false when it is missing or
311
+ # no longer pending
173
312
  def outbox_claim(id) = @adapter.outbox_claim(id)
313
+
314
+ # Records the final status of a claimed outbox row.
315
+ #
316
+ # @param id [Integer] the row id assigned by `outbox_enqueue`
317
+ # @param status [String, Symbol] the settled status; `Runtime::Outbox` passes
318
+ # `"delivered"` or `"failed"`
319
+ # @param error [String, nil] failure text, `"ErrorClass: message"`; nil on success
320
+ # @return [Boolean] true when a row with that id was updated
174
321
  def outbox_settle(id, status:, error: nil) = @adapter.outbox_settle(id, status: status, error: error)
322
+
323
+ # Lists this aggregate's outbox rows, oldest first.
324
+ #
325
+ # @param status [String, Symbol, nil] keep only rows with this status; nil for all rows
326
+ # @return [Array<Runtime::Outbox::Row>] copies of the stored rows; `[]` when none match
175
327
  def outbox_rows(status: nil) = @adapter.outbox_rows(status: status)
176
328
 
329
+ # Answers a read model from the adapter's own projected tables, when it can.
330
+ #
331
+ # @param domain [String, Symbol] name of the domain declaring the read model
332
+ # @param model [Bluebook::ReadModel] the read model to answer
333
+ # @param args [Hash{Symbol => Object}] the read model's arguments, including its
334
+ # reference argument
335
+ # @param bluebook [Bluebook::Chapter, nil] the domain's bluebook, which the SQLite
336
+ # projection needs to find the included aggregates
337
+ # @return [Array<Hash>, nil] a one-element Array holding the report Hash keyed by head
338
+ # name; nil when the adapter has no `query_read_model`
339
+ # @raise [ArgumentError] if the adapter answers natively and `bluebook` is nil
340
+ # @raise [Runtime::NotFound] if the referenced root record is not in the projection
177
341
  def query_read_model(domain, model, args, bluebook = nil)
178
342
  return unless @adapter.respond_to?(:query_read_model)
179
343
 
@@ -10,6 +10,18 @@ module Hecks
10
10
  module BindingPolicy
11
11
  module_function
12
12
 
13
+ # Picks the one bind that names an aggregate's authoritative store.
14
+ #
15
+ # A domain with no hecksagon gets the default in-memory bind. A domain that has one
16
+ # must bind the aggregate exactly once without a role.
17
+ #
18
+ # @param registry [Runtime::Registry] the registry holding the domain's hecksagon
19
+ # @param domain [String, Symbol] name of the domain the aggregate belongs to
20
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose binding is wanted
21
+ # @return [Bluebook::Bind] the authoritative `persisted_by` bind, or the default
22
+ # `Memory` bind when the domain declares no hecksagon
23
+ # @raise [Runtime::WiringError] if the hecksagon has no `persisted_by` bind for the
24
+ # aggregate, more or fewer than one bind without a role, or any bind with a role
13
25
  def resolve(registry, domain, aggregate)
14
26
  hexagon = registry.hecksagon(domain)
15
27
  return default_binding(aggregate) unless hexagon
@@ -24,10 +36,19 @@ module Hecks
24
36
  authoritative.first
25
37
  end
26
38
 
39
+ # Builds the bind an aggregate gets when its domain declares no hecksagon.
40
+ #
41
+ # @param aggregate [Bluebook::Aggregate] the aggregate to bind
42
+ # @return [Bluebook::Bind] a roleless `persisted_by` bind to `DEFAULT_ADAPTER`
27
43
  def default_binding(aggregate)
28
44
  Bluebook::Bind.new(aggregate: aggregate.hecks_name, verb: VERB, adapter: DEFAULT_ADAPTER)
29
45
  end
30
46
 
47
+ # Builds, without raising, the error for an aggregate a hecksagon leaves unbound.
48
+ #
49
+ # @param domain [String, Symbol] name of the domain, used in the message
50
+ # @param aggregate [Bluebook::Aggregate] the aggregate with no bind
51
+ # @return [Runtime::WiringError] an error whose message says how to bind the aggregate
31
52
  def missing_binding(domain, aggregate)
32
53
  Runtime::WiringError.new(
33
54
  "#{domain}::#{aggregate.hecks_name} has no #{VERB} bind. #{domain} declares a " \
@@ -37,6 +58,13 @@ module Hecks
37
58
  )
38
59
  end
39
60
 
61
+ # Builds, without raising, the error for an aggregate whose count of roleless binds
62
+ # is not one.
63
+ #
64
+ # @param domain [String, Symbol] name of the domain, used in the message
65
+ # @param aggregate [Bluebook::Aggregate] the aggregate with the wrong bind count
66
+ # @param authoritative [Array<Bluebook::Bind>] the roleless binds found; may be empty
67
+ # @return [Runtime::WiringError] an error whose message reports the count
40
68
  def ambiguous_binding(domain, aggregate, authoritative)
41
69
  Runtime::WiringError.new(
42
70
  "#{domain}::#{aggregate.hecks_name} has #{authoritative.size} authoritative #{VERB} bindings. " \
@@ -44,6 +72,12 @@ module Hecks
44
72
  )
45
73
  end
46
74
 
75
+ # Builds, without raising, the error for binds that carry a role this port ignores.
76
+ #
77
+ # @param domain [String, Symbol] name of the domain, used in the message
78
+ # @param aggregate [Bluebook::Aggregate] the aggregate carrying the binds
79
+ # @param bindings [Array<Bluebook::Bind>] the binds that declare a role
80
+ # @return [Runtime::WiringError] an error whose message lists each role
47
81
  def unsupported_roles(domain, aggregate, bindings)
48
82
  Runtime::WiringError.new(
49
83
  "#{domain}::#{aggregate.hecks_name} uses persistence role#{'s' unless bindings.size == 1} " \