hecks 1.3.0 → 1.4.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 (355) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +7 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +10 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
  7. data/lib/hecks/adapters/driven/heki/journal.rb +5 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
  9. data/lib/hecks/adapters/driven/heki.rb +13 -7
  10. data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
  12. data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
  13. data/lib/hecks/adapters/driven/lambda.rb +39 -33
  14. data/lib/hecks/adapters/driven/local_storage.rb +17 -10
  15. data/lib/hecks/adapters/driven/memory.rb +205 -9
  16. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
  17. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  18. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  19. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  20. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  21. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  22. data/lib/hecks/adapters/driven/prism.rb +4 -4
  23. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  24. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  25. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  26. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  27. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  28. data/lib/hecks/adapters/driven.rb +4 -4
  29. data/lib/hecks/adapters/driving/github_webhook.rb +18 -18
  30. data/lib/hecks/behaviors/dsl.rb +2 -2
  31. data/lib/hecks/behaviors/expectations.rb +23 -23
  32. data/lib/hecks/behaviors/ir.rb +1 -1
  33. data/lib/hecks/behaviors/rspec.rb +1 -1
  34. data/lib/hecks/behaviors/runner.rb +2 -2
  35. data/lib/hecks/behaviors.rb +1 -1
  36. data/lib/hecks/bluebook/aggregate.rb +13 -13
  37. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
  38. data/lib/hecks/bluebook/assembly/build.rb +1 -1
  39. data/lib/hecks/bluebook/assembly/contract.rb +39 -16
  40. data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
  41. data/lib/hecks/bluebook/assembly/marks.rb +18 -18
  42. data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
  43. data/lib/hecks/bluebook/assembly.rb +9 -9
  44. data/lib/hecks/bluebook/attribute.rb +9 -9
  45. data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
  46. data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
  47. data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
  48. data/lib/hecks/bluebook/behaviour/command.rb +23 -23
  49. data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -7
  50. data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
  51. data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
  52. data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
  53. data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
  54. data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
  55. data/lib/hecks/bluebook/behaviour/query.rb +1 -1
  56. data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
  57. data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
  58. data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
  59. data/lib/hecks/bluebook/capabilities.rb +27 -0
  60. data/lib/hecks/bluebook/chapter.rb +28 -9
  61. data/lib/hecks/bluebook/command.rb +12 -12
  62. data/lib/hecks/bluebook/domain_port.rb +9 -9
  63. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  64. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  65. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
  66. data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
  67. data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
  68. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
  69. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
  70. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  71. data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
  72. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  73. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  74. data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
  75. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  76. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
  77. data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
  78. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  79. data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
  80. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  81. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  82. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
  83. data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
  84. data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
  85. data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
  86. data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
  87. data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
  88. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  89. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  90. data/lib/hecks/bluebook/entity.rb +11 -11
  91. data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
  92. data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
  93. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
  94. data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
  95. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
  96. data/lib/hecks/bluebook/expression/resolver.rb +60 -62
  97. data/lib/hecks/bluebook/hexagon.rb +1 -1
  98. data/lib/hecks/bluebook/lifecycle.rb +1 -1
  99. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
  100. data/lib/hecks/bluebook/meta_validator/judge.rb +101 -87
  101. data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
  102. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
  103. data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
  104. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
  105. data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
  106. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
  107. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
  108. data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
  109. data/lib/hecks/bluebook/meta_validator.rb +70 -70
  110. data/lib/hecks/bluebook/model_check.rb +170 -84
  111. data/lib/hecks/bluebook/pattern_subset.rb +9 -9
  112. data/lib/hecks/bluebook/policy.rb +15 -13
  113. data/lib/hecks/bluebook/process_manager.rb +14 -14
  114. data/lib/hecks/bluebook/project_register.rb +6 -6
  115. data/lib/hecks/bluebook/query.rb +4 -4
  116. data/lib/hecks/bluebook/read_model.rb +14 -14
  117. data/lib/hecks/bluebook/reference.rb +8 -8
  118. data/lib/hecks/bluebook/smoke_test.rb +19 -19
  119. data/lib/hecks/bluebook/synthesizer.rb +12 -12
  120. data/lib/hecks/bluebook/translation.rb +4 -4
  121. data/lib/hecks/bluebook/value_object.rb +6 -6
  122. data/lib/hecks/bluebook.rb +2 -2
  123. data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
  124. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
  125. data/lib/hecks/codemod.rb +36 -35
  126. data/lib/hecks/construct.rb +6 -6
  127. data/lib/hecks/corpus.rb +317 -0
  128. data/lib/hecks/deprecation.rb +95 -0
  129. data/lib/hecks/doc/reference.rb +19 -19
  130. data/lib/hecks/embryonaut_bluebook.rb +11 -11
  131. data/lib/hecks/facade/cli_door.rb +69 -10
  132. data/lib/hecks/facade/cli_runner.rb +105 -24
  133. data/lib/hecks/facade/command_request.rb +23 -0
  134. data/lib/hecks/facade/handle.rb +79 -32
  135. data/lib/hecks/facade/json_door.rb +106 -25
  136. data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
  137. data/lib/hecks/facade/surface/chapter.rb +26 -17
  138. data/lib/hecks/facade/surface.rb +16 -3
  139. data/lib/hecks/facade.rb +15 -4
  140. data/lib/hecks/forms/app.rb +46 -30
  141. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  142. data/lib/hecks/forms/field_renderer.rb +142 -6
  143. data/lib/hecks/forms/field_shape.rb +180 -19
  144. data/lib/hecks/forms/html.rb +51 -7
  145. data/lib/hecks/forms/index_renderer.rb +14 -2
  146. data/lib/hecks/forms/params.rb +120 -23
  147. data/lib/hecks/forms/port_argument.rb +2 -2
  148. data/lib/hecks/forms/query_form_renderer.rb +2 -2
  149. data/lib/hecks/forms/record_renderer.rb +2 -2
  150. data/lib/hecks/forms/record_table.rb +1 -1
  151. data/lib/hecks/forms/value_object_shape.rb +3 -3
  152. data/lib/hecks/forms.rb +24 -4
  153. data/lib/hecks/fqn.rb +1 -1
  154. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  155. data/lib/hecks/framework.rb +48 -17
  156. data/lib/hecks/freezer.rb +11 -11
  157. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
  158. data/lib/hecks/fuzzing/combination_miner.rb +119 -0
  159. data/lib/hecks/fuzzing/concurrent_dispatch.rb +140 -45
  160. data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
  161. data/lib/hecks/fuzzing/differential.rb +158 -0
  162. data/lib/hecks/fuzzing/domain_generator.rb +694 -0
  163. data/lib/hecks/fuzzing/era_boundary.rb +37 -18
  164. data/lib/hecks/fuzzing/form_census.rb +35 -20
  165. data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
  166. data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
  167. data/lib/hecks/fuzzing/isolated_boot.rb +62 -62
  168. data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
  169. data/lib/hecks/fuzzing/persistence_parity.rb +19 -21
  170. data/lib/hecks/fuzzing/properties/corrections.rb +9 -9
  171. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +154 -49
  172. data/lib/hecks/fuzzing/properties/guards.rb +42 -42
  173. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
  174. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +22 -35
  175. data/lib/hecks/fuzzing/properties/outbox.rb +24 -24
  176. data/lib/hecks/fuzzing/properties/querying.rb +17 -17
  177. data/lib/hecks/fuzzing/properties.rb +64 -40
  178. data/lib/hecks/fuzzing/qa_settings.rb +152 -0
  179. data/lib/hecks/fuzzing/replay.rb +114 -114
  180. data/lib/hecks/fuzzing/rotation_priority.rb +10 -10
  181. data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
  182. data/lib/hecks/fuzzing/self_consistency.rb +141 -112
  183. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +35 -35
  184. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  185. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  186. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  187. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +58 -31
  188. data/lib/hecks/fuzzing/sequence_generator.rb +111 -23
  189. data/lib/hecks/fuzzing/shrinker.rb +197 -0
  190. data/lib/hecks/fuzzing/structural_skips.rb +23 -130
  191. data/lib/hecks/fuzzing/sweep_depth.rb +4 -4
  192. data/lib/hecks/fuzzing/target_capabilities.rb +49 -18
  193. data/lib/hecks/fuzzing/value_generator.rb +14 -14
  194. data/lib/hecks/fuzzing.rb +5 -0
  195. data/lib/hecks/grammar/evolve.rb +10 -10
  196. data/lib/hecks/grammar.rb +7 -7
  197. data/lib/hecks/ir.rb +13 -13
  198. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  199. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  200. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  201. data/lib/hecks/language/oidc.json +5 -0
  202. data/lib/hecks/literal.rb +9 -9
  203. data/lib/hecks/naming.rb +25 -25
  204. data/lib/hecks/ports/access_control.rb +58 -2
  205. data/lib/hecks/ports/agent/answers.rb +83 -6
  206. data/lib/hecks/ports/agent.rb +119 -35
  207. data/lib/hecks/ports/authentication.rb +44 -4
  208. data/lib/hecks/ports/authorization.rb +53 -11
  209. data/lib/hecks/ports/clock.rb +42 -23
  210. data/lib/hecks/ports/extraction.rb +16 -0
  211. data/lib/hecks/ports/identity_assignment.rb +24 -2
  212. data/lib/hecks/ports/identity_generation.rb +17 -3
  213. data/lib/hecks/ports/identity_resolution.rb +18 -1
  214. data/lib/hecks/ports/loading.rb +4 -0
  215. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  216. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  217. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  218. data/lib/hecks/ports/persistence/execution.rb +4 -0
  219. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  220. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  221. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  222. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  223. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  224. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  225. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  226. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  227. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  228. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  229. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  230. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  231. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  241. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  242. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  243. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  244. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
  245. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  246. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  247. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  248. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
  249. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
  250. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
  251. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  252. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  253. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  254. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  255. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  256. data/lib/hecks/ports/persistence.rb +36 -1
  257. data/lib/hecks/ports/projection.rb +61 -7
  258. data/lib/hecks/ports/query/in_memory.rb +3 -3
  259. data/lib/hecks/ports/query/ordering.rb +6 -6
  260. data/lib/hecks/ports/query.rb +35 -0
  261. data/lib/hecks/projections/bootstrap_table.rb +112 -0
  262. data/lib/hecks/projections/diagrams.rb +75 -75
  263. data/lib/hecks/projections/glossary/html.rb +3 -3
  264. data/lib/hecks/projections/glossary/markdown.rb +3 -3
  265. data/lib/hecks/projections/glossary/mermaid.rb +1 -1
  266. data/lib/hecks/projections/glossary/sections.rb +1 -1
  267. data/lib/hecks/projections/glossary/sentences.rb +5 -5
  268. data/lib/hecks/projections/glossary.rb +15 -15
  269. data/lib/hecks/projections/ir.rb +1 -1
  270. data/lib/hecks/projections/model/deviations.rb +18 -17
  271. data/lib/hecks/projections/model.rb +25 -21
  272. data/lib/hecks/projections/oidc.rb +7 -7
  273. data/lib/hecks/projections/parser_table.rb +5 -5
  274. data/lib/hecks/projections/reference.rb +3 -3
  275. data/lib/hecks/projections/rust_vocabulary.rb +443 -0
  276. data/lib/hecks/projections/shape.rb +2 -2
  277. data/lib/hecks/projections/statements.rb +11 -11
  278. data/lib/hecks/projections/vocabulary.rb +9 -9
  279. data/lib/hecks/projections.rb +5 -3
  280. data/lib/hecks/projector/cli_projector.rb +29 -29
  281. data/lib/hecks/projector/docs_projector.rb +13 -13
  282. data/lib/hecks/projector/exporter.rb +42 -21
  283. data/lib/hecks/projector/ir_projector.rb +1 -1
  284. data/lib/hecks/projector/narrate_projector.rb +11 -11
  285. data/lib/hecks/projector/target.rb +13 -13
  286. data/lib/hecks/projector.rb +15 -15
  287. data/lib/hecks/query_ir.rb +47 -47
  288. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  289. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  290. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  291. data/lib/hecks/query_specification/common/null_policy.rb +57 -13
  292. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  293. data/lib/hecks/query_specification/common/options.rb +25 -0
  294. data/lib/hecks/query_specification/field_path.rb +69 -15
  295. data/lib/hecks/query_specification/hop_path.rb +57 -20
  296. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  297. data/lib/hecks/rendering.rb +3 -3
  298. data/lib/hecks/router/namespace_installer.rb +3 -3
  299. data/lib/hecks/router.rb +1 -1
  300. data/lib/hecks/runtime/aggregate_lock.rb +11 -11
  301. data/lib/hecks/runtime/boot_gates.rb +3 -3
  302. data/lib/hecks/runtime/caller.rb +8 -8
  303. data/lib/hecks/runtime/capability_graph.rb +2 -2
  304. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
  305. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +42 -63
  306. data/lib/hecks/runtime/command_interpreter.rb +159 -102
  307. data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
  308. data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
  309. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  310. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  311. data/lib/hecks/runtime/command_rules/references.rb +35 -35
  312. data/lib/hecks/runtime/command_rules.rb +3 -0
  313. data/lib/hecks/runtime/dependency_planning.rb +11 -11
  314. data/lib/hecks/runtime/dispatcher.rb +283 -90
  315. data/lib/hecks/runtime/entity_element.rb +151 -67
  316. data/lib/hecks/runtime/entity_interpreter.rb +108 -82
  317. data/lib/hecks/runtime/errors.rb +19 -19
  318. data/lib/hecks/runtime/event.rb +6 -6
  319. data/lib/hecks/runtime/identity.rb +22 -22
  320. data/lib/hecks/runtime/instance.rb +39 -14
  321. data/lib/hecks/runtime/interpreting.rb +12 -12
  322. data/lib/hecks/runtime/invocation.rb +276 -0
  323. data/lib/hecks/runtime/loader.rb +14 -14
  324. data/lib/hecks/runtime/outbox.rb +23 -23
  325. data/lib/hecks/runtime/policy_interpreter.rb +54 -54
  326. data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
  327. data/lib/hecks/runtime/query_interpreter.rb +55 -64
  328. data/lib/hecks/runtime/reaction_invocation.rb +20 -20
  329. data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
  330. data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
  331. data/lib/hecks/runtime/reference_hop.rb +6 -6
  332. data/lib/hecks/runtime/refusal_wording.rb +92 -115
  333. data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
  334. data/lib/hecks/runtime/registry/verification.rb +36 -26
  335. data/lib/hecks/runtime/registry.rb +45 -28
  336. data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
  337. data/lib/hecks/runtime/routing.rb +10 -153
  338. data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
  339. data/lib/hecks/runtime/saga_interpreter.rb +64 -64
  340. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  341. data/lib/hecks/runtime/tenant_check.rb +9 -9
  342. data/lib/hecks/runtime/tenant_scope.rb +5 -5
  343. data/lib/hecks/runtime/value/admission.rb +75 -30
  344. data/lib/hecks/runtime/value/coercion.rb +318 -127
  345. data/lib/hecks/runtime/value/entity_list_coercion.rb +34 -34
  346. data/lib/hecks/runtime/value.rb +21 -21
  347. data/lib/hecks/runtime.rb +7 -7
  348. data/lib/hecks/storehouse.rb +64 -64
  349. data/lib/hecks/version.rb +3 -3
  350. data/lib/hecks/vocabulary.rb +205 -4
  351. data/lib/hecks.rb +13 -11
  352. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
  353. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
  354. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
  355. metadata +22 -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,27 @@ 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, used to resolve the adapter
23
+ # and handed on to it
24
+ # @param issuer [String] the OIDC issuer that authenticated the caller
25
+ # @param subject [String] the OIDC subject the issuer vouches for
26
+ # @return [String, nil] the linked identity's id, usable as an `actor_id` for
27
+ # `Ports::Authorization.holds_role?` — not an `Identity` record; nil if nothing has
28
+ # linked this pair
29
+ # @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
30
+ # (see `adapter`)
20
31
  def resolve(registry, issuer:, subject:)
21
32
  adapter(registry).resolve(registry, issuer: issuer, subject: subject)
22
33
  end
23
34
 
35
+ # Finds the single adapter bound to this port, refusing an ambiguous wiring.
36
+ #
37
+ # @param registry [Runtime::Registry] the booted registry to search
38
+ # @return [Module] the adapter module or class implementing this port
39
+ # @raise [Runtime::WiringError] if no adapter, or more than one, implements this port,
40
+ # or the one that does has no Ruby implementation under `Hecks::Adapters`
24
41
  def adapter(registry)
25
42
  implementations = registry.adapters.values.select { |a| a.port == NAME }
26
43
 
@@ -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} " \
@@ -0,0 +1,178 @@
1
+ require_relative "state_codec"
2
+ require_relative "../../runtime/errors"
3
+
4
+ module Hecks
5
+ module Ports
6
+ module Persistence
7
+ # No adapter can build an `Instance` from undecoded state (Phase 2,
8
+ # Track A, PR A3). Routing every adapter through `StateCodec` is a
9
+ # convention until something refuses the bypass; this is that
10
+ # something, and it is installed by `RepositoryFactory.build` — the
11
+ # one place every runtime repository is made — so no adapter can opt
12
+ # out of it.
13
+ #
14
+ # ## The mechanism, two halves
15
+ #
16
+ # 1. `guard!(adapter)` extends the adapter object (not its class,
17
+ # not a proxy — `is_a?`, `class`, and `===` stay the adapter's
18
+ # own) with a module that wraps every public method the adapter's
19
+ # class defines. Each call runs inside the boundary: a
20
+ # thread-local flag, re-entrant, restored on the way out. A block
21
+ # the caller passes (`transaction`, `with_write_lock`,
22
+ # `each_saga`) runs outside it — that block is the dispatch
23
+ # itself (hydrate, entity views, mutation), not adapter code.
24
+ # Reaching the adapter through `repository.adapter` (the query
25
+ # port, saga persistence, `bin/heki_compact`) is guarded all the
26
+ # same: the wrapper is on the object.
27
+ #
28
+ # 2. `Runtime::Instance#initialize` asks `check_state!` whenever it
29
+ # is handed `state:`. Outside the boundary that is a no-op; inside
30
+ # it, state `StateCodec.decoded?` rejects refuses by name. An
31
+ # `entries` answer is checked the same way on its way out, since a
32
+ # journal `Entry` is not an `Instance`.
33
+ #
34
+ # ## Checked, not silently decoded
35
+ #
36
+ # An adapter that forgets the codec is a bug in that adapter, and
37
+ # decoding for it here would hide the forgetting. Hydration does not
38
+ # respell keys either (A4) — `Value.hydrate` refuses a non-Symbol
39
+ # top-level key everywhere — so this boundary's deep check and
40
+ # hydration's shallow one agree.
41
+ module CodecBoundary
42
+ KEY = :hecks_persistence_codec_boundary
43
+
44
+ module_function
45
+
46
+ # Extends an adapter object so every public method its class defines runs inside the
47
+ # boundary; guarding an adapter twice wraps it once.
48
+ #
49
+ # @param adapter [Object] a driven persistence adapter instance
50
+ # @return [Object] the same adapter object, now including `Guarded`
51
+ def guard!(adapter)
52
+ adapter.extend(wrapper_for(adapter.class)) unless adapter.singleton_class.include?(Guarded)
53
+ adapter
54
+ end
55
+
56
+ # Reports whether the current thread is executing inside a guarded adapter call.
57
+ #
58
+ # @return [Boolean] true between entering a guarded adapter method and yielding to the
59
+ # caller's block or returning
60
+ def active? = Thread.current[KEY] == true
61
+
62
+ # Runs the block with the boundary switched on for this thread, restoring the earlier
63
+ # setting afterwards.
64
+ #
65
+ # @yield adapter code whose `Instance` construction is to be checked
66
+ # @return [Object] the block's value
67
+ def within(&) = with_flag(true, &)
68
+
69
+ # Runs the block with the boundary switched off for this thread, restoring the earlier
70
+ # setting afterwards.
71
+ #
72
+ # @yield caller code, such as a dispatch running inside the adapter's transaction
73
+ # @return [Object] the block's value
74
+ def outside(&) = with_flag(false, &)
75
+
76
+ # Refuses state an adapter is about to build an `Instance` from unless it is decoded;
77
+ # outside the boundary it checks nothing.
78
+ #
79
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct the state
80
+ # belongs to
81
+ # @param state [Hash, Runtime::Value] the state handed to `Runtime::Instance.new`
82
+ # @return [nil] when the boundary is inactive or the state is decoded
83
+ # @raise [Runtime::WiringError] if the boundary is active and
84
+ # `StateCodec.decoded?` rejects the state
85
+ def check_state!(aggregate, state)
86
+ return unless active?
87
+ return if StateCodec.decoded?(aggregate, state)
88
+
89
+ raise Runtime::WiringError,
90
+ "a persistence adapter built a #{aggregate.name} record from undecoded state " \
91
+ "(#{state.keys.inspect}) — decode stored state through " \
92
+ "Hecks::Ports::Persistence::StateCodec.decode before handing it to Runtime::Instance"
93
+ end
94
+
95
+ # Refuses a guarded adapter's `entries` answer if any entry's state is undecoded.
96
+ #
97
+ # @param adapter [Object] the guarded adapter, asked for its `aggregate` and class name
98
+ # @param entries [Array<Persistence::Entry>, Object] what the adapter's `entries`
99
+ # returned; anything that is not an Array passes through unchecked
100
+ # @return [Array<Persistence::Entry>, Object] `entries` itself
101
+ # @raise [Runtime::WiringError] if an `Entry` in the Array carries state
102
+ # `StateCodec.decoded?` rejects
103
+ def check_entries!(adapter, entries)
104
+ return entries unless entries.is_a?(Array)
105
+
106
+ entries.each do |entry|
107
+ next unless entry.is_a?(Entry) && !StateCodec.decoded?(adapter.aggregate, entry.state)
108
+
109
+ raise Runtime::WiringError,
110
+ "#{adapter.class} answered a #{adapter.aggregate.name} journal entry #{entry.id.inspect} with " \
111
+ "undecoded state — decode it through Hecks::Ports::Persistence::StateCodec.decode"
112
+ end
113
+ entries
114
+ end
115
+
116
+ # Sets the thread-local boundary flag for the length of the block, which is what makes
117
+ # the boundary re-entrant.
118
+ #
119
+ # @param value [Boolean] true to switch the boundary on, false to switch it off
120
+ # @yield the code to run under that setting
121
+ # @return [Object] the block's value; the earlier flag is restored even when the
122
+ # block raises
123
+ def with_flag(value)
124
+ previous = Thread.current[KEY]
125
+ Thread.current[KEY] = value
126
+ yield
127
+ ensure
128
+ Thread.current[KEY] = previous
129
+ end
130
+
131
+ # Marks a guarded adapter so a second `guard!` (a projection and an
132
+ # authoritative repository built over one object) never wraps twice.
133
+ module Guarded; end
134
+
135
+ # Filled lazily under `WRAPPERS_LOCK`, one entry per adapter class —
136
+ # a cache, deliberately mutable.
137
+ WRAPPERS = {} # rubocop:disable Style/MutableConstant
138
+ WRAPPERS_LOCK = Mutex.new
139
+
140
+ # Builds, or fetches from the cache, the module that guards one adapter class.
141
+ #
142
+ # One wrapper module per adapter class, built once: every public
143
+ # instance method the class (and its ancestors below Object)
144
+ # defines, each forwarding to `super` inside the boundary.
145
+ #
146
+ # @param klass [Class] the adapter's class
147
+ # @return [Module] an anonymous module including `Guarded`, meant to be `extend`ed onto
148
+ # instances of `klass`
149
+ def wrapper_for(klass)
150
+ WRAPPERS_LOCK.synchronize do
151
+ WRAPPERS[klass] ||= Module.new do
152
+ include Guarded
153
+
154
+ (klass.public_instance_methods - Object.public_instance_methods).each do |name|
155
+ define_method(name) do |*args, **kwargs, &block|
156
+ result = CodecBoundary.within { super(*args, **kwargs, &CodecBoundary.outside_block(block)) }
157
+ name == :entries ? CodecBoundary.check_entries!(self, result) : result
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+
164
+ # Wraps the caller's own block so it runs outside the boundary
165
+ # whenever the adapter yields to it.
166
+ #
167
+ # @param block [Proc, nil] the block the caller passed to the adapter method
168
+ # @return [Proc, nil] a proc forwarding its arguments to `block` under `outside`; nil
169
+ # when no block was given
170
+ def outside_block(block)
171
+ return nil unless block
172
+
173
+ proc { |*yielded, **yielded_kwargs, &inner| outside { block.call(*yielded, **yielded_kwargs, &inner) } }
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
@@ -10,6 +10,10 @@ module Hecks
10
10
  # `Runtime::StaleWrite` (runtime/errors.rb) and `AppendOnly#save`.
11
11
  STATUSES = %i[inserted replaced updated conflicted missing saved stale].freeze
12
12
 
13
+ # @param status [Symbol, String] one of `STATUSES`; stored as a Symbol
14
+ # @param instance [Runtime::Instance, Object, nil] the record the write concerned, as
15
+ # the repository reports it; nil when there is none
16
+ # @raise [ArgumentError] if `status` is not one of `STATUSES`
13
17
  def initialize(status:, instance: nil)
14
18
  normalized = status.to_sym
15
19
  raise ArgumentError, "unknown persistence outcome #{status.inspect}" unless STATUSES.include?(normalized)