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
@@ -28,14 +28,29 @@ module Hecks
28
28
 
29
29
  attr_reader :registry
30
30
 
31
+ # @param registry [Runtime::Registry] the booted registry whose declared
32
+ # process managers and saga persistence this interpreter runs against
33
+ # @param door [Runtime::Dispatcher] the dispatcher a saga leg's own dispatch
34
+ # re-enters through
31
35
  def initialize(registry, door:)
32
36
  @registry = registry
33
37
  @door = door
34
38
  end
35
39
 
40
+ # Runs `domain`'s declared process managers against `event`: begins,
41
+ # advances or ends each matching saga instance, checkpointing durably
42
+ # before its own dispatches run.
43
+ #
36
44
  # `only:` — one process manager, the outbox relay's way of running
37
45
  # exactly the consumer a row names (`Runtime::Outbox::Relay#
38
46
  # run_consumer`); nil advances every manager the domain declares.
47
+ #
48
+ # @param event [Runtime::Event] the just-emitted event to react to
49
+ # @param domain [String, Symbol] the domain whose declared process managers
50
+ # are checked
51
+ # @param only [Bluebook::ProcessManager, nil] one process manager to run
52
+ # exactly, instead of every manager `domain` declares
53
+ # @return [void]
39
54
  def advance(event, domain, only: nil)
40
55
  bluebook = @registry.bluebook(domain)
41
56
  return unless bluebook
@@ -49,26 +64,25 @@ module Hecks
49
64
 
50
65
  private
51
66
 
52
- # THE CHECKPOINT WRITE, shared by every mutation site below —
53
- # holds `saga_mutex` across BOTH the in-memory Hash mutation and
67
+ # The checkpoint write, shared by every mutation site below —
68
+ # holds `saga_mutex` across both the in-memory Hash mutation and
54
69
  # the persistence write (§7), not just the Hash mutation alone:
55
- # two threads racing the SAME (process_manager, correlation) key
70
+ # two threads racing the same (process_manager, correlation) key
56
71
  # could otherwise interleave their writes out of order, silently
57
72
  # reordering a saga's own transition history — worse for the
58
73
  # adapters with no locking of their own (Heki) than for Postgres.
59
- # `deep_copy` guards against the exact shape of bug PR #175 itself
60
- # already found once (over-freezing a live, still-mutated Hash) —
61
- # never hand a persistence adapter the SAME object `advance_saga`/
62
- # `unwind` go on to mutate in place; round-tripping through JSON
63
- # is also what guarantees the value is safe for every adapter that
64
- # itself calls `JSON.generate` on it.
74
+ # `deep_copy` guards against a real bug shape (over-freezing a live,
75
+ # still-mutated Hash) — never hand a persistence adapter the same
76
+ # object `advance_saga`/`unwind` go on to mutate in place;
77
+ # round-tripping through JSON is also what guarantees the value is
78
+ # safe for every adapter that itself calls `JSON.generate` on it.
65
79
  # `pending:` — see saga_pending_dispatch.rb. Injected into the
66
- # WRITTEN copy of memory only, never into `instance[:memory]`
80
+ # written copy of memory only, never into `instance[:memory]`
67
81
  # itself: every other reader of a live instance's memory
68
82
  # (`dispatch_args`'s "opening event memory" scope, the fuzzer's
69
83
  # own round-trip/shape checks, `saga_spec.rb`'s exact-equality
70
84
  # assertion against a fresh instance's seeded memory) sees exactly
71
- # what it always did. The marker exists ONLY in the persisted
85
+ # what it always did. The marker exists only in the persisted
72
86
  # blob, and only for as long as a dispatch cascade is genuinely
73
87
  # in flight for this instance.
74
88
  def checkpoint(process_manager, correlation, instance, domain, pending: nil)
@@ -82,8 +96,8 @@ module Hecks
82
96
  end
83
97
 
84
98
  # `deep_copy` is `JSON.parse(JSON.generate(hash), ...)`, which
85
- # only accepts an OBJECT at the top level — `completed_compensations`
86
- # is an ARRAY, so it gets its own wrap-and-unwrap rather than a
99
+ # only accepts an object at the top level — `completed_compensations`
100
+ # is an array, so it gets its own wrap-and-unwrap rather than a
87
101
  # second, parallel `deep_copy_array` reimplementing the same
88
102
  # round-trip. `|| []` — an instance from before this field existed
89
103
  # (or one that has never completed a compensable leg) rehydrates
@@ -105,15 +119,15 @@ module Hecks
105
119
  created = @registry.saga_mutex.synchronize do
106
120
  next false if @registry.saga_instances[process_manager.name].key?(correlation)
107
121
 
108
- # `.dup`, NOT THE SAME OBJECT — a fresh saga's own memory starts
109
- # as a COPY of the starting event's own payload, never the
122
+ # `.dup`, not the same object — a fresh saga's own memory starts
123
+ # as a copy of the starting event's own payload, never the
110
124
  # payload itself. A saga's own memory is meant to be written
111
125
  # into over its lifetime (remember-style, growing beyond what
112
126
  # the starting event carried) ; the payload it was seeded from
113
- # is a fact about something that ALREADY happened, logged and
127
+ # is a fact about something that already happened, logged and
114
128
  # emitted before the saga ever saw it. Sharing the one Hash
115
129
  # object between them means a write into the saga's own memory
116
- # is silently ALSO a write into an already-emitted event's own
130
+ # is silently also a write into an already-emitted event's own
117
131
  # payload — retroactively adding a field nothing announced.
118
132
  # `event.payload` is deep-frozen by `Event#emit!` by the time
119
133
  # this runs, so a naive in-place write here would raise
@@ -132,11 +146,11 @@ module Hecks
132
146
  instance: correlation, born: true, state: process_manager.states.first }
133
147
  end
134
148
 
135
- # THE MUTEX COVERS ONLY THE STATE-CHECK-AND-MUTATE-AND-CHECKPOINT
136
- # STEP, never the dispatch cascade that follows — `deliver_saga_
149
+ # The mutex covers only the state-check-and-mutate-and-checkpoint
150
+ # step, never the dispatch cascade that follows — `deliver_saga_
137
151
  # dispatch` calls `@door.reenter`, which can recursively re-enter
138
- # THIS SAME interpreter (a saga's own leg triggering another saga,
139
- # or itself again) on the SAME thread, and `Mutex` is not
152
+ # this same interpreter (a saga's own leg triggering another saga,
153
+ # or itself again) on the same thread, and `Mutex` is not
140
154
  # reentrant: holding it across that call would deadlock the
141
155
  # thread against itself the moment any real chain did that.
142
156
  def advance_saga(process_manager, event, domain)
@@ -156,7 +170,7 @@ module Hecks
156
170
  @registry.saga_log << record.merge(advanced: false, reason: "no conversation remembers #{correlation.inspect}")
157
171
  next false
158
172
  end
159
- # THE LEG IS CHOSEN BY (EVENT, CURRENT STATE) — C10.3. Read
173
+ # The leg is chosen by (event, current state) — C10.3. Read
160
174
  # under the mutex, against the state this instance holds right
161
175
  # now, so two legs on the same event from different states each
162
176
  # answer exactly when their own state is current.
@@ -189,7 +203,7 @@ module Hecks
189
203
  { on: event.name, from: from_state, to: to_state, dispatches: handler.dispatches.map(&:command_name) }
190
204
  end
191
205
 
192
- # THE SHARED TAIL of `advance_saga` and `unwind` — both are "guard,
206
+ # The shared tail of `advance_saga` and `unwind` — both are "guard,
193
207
  # mutate, checkpoint-with-pending" under the mutex (kept separate
194
208
  # per caller: `advance_saga`'s own guard also has to handle "no
195
209
  # instance at all", `unwind`'s doesn't), then this: log the real
@@ -197,11 +211,11 @@ module Hecks
197
211
  # pending marker once that cascade — however it ended — is done.
198
212
  def settle_transition(process_manager, event, handler, instance, correlation, domain, record, pre_state,
199
213
  drain_compensations: false)
200
- # `from:`/`to:` are the INSTANCE'S OWN real pre/post state — read
214
+ # `from:`/`to:` are the instance's own real pre/post state — read
201
215
  # back from `instance` itself, never re-derived from `handler.
202
216
  # from_state`/`handler.to_state` a second time. `Properties.saga_
203
217
  # advances_follow_declared_handlers` (fuzzing/properties.rb) builds
204
- # its OWN "declared edges" list from this SAME handler object (via
218
+ # its own "declared edges" list from this same handler object (via
205
219
  # `process_manager.handlers`), so a log entry that just echoed `handler.
206
220
  # from_state`/`handler.to_state` back could never disagree with
207
221
  # that list no matter what the runtime actually did — the entry
@@ -213,10 +227,10 @@ module Hecks
213
227
  # real mismatch instead of vanishing into a tautology.
214
228
  @registry.saga_log << record.merge(advanced: true, from: pre_state, to: instance[:state])
215
229
 
216
- # DERIVED COMPENSATION FIRST, NEWEST-FIRST — only for `unwind`'s
217
- # own call (`drain_compensations: true`): every leg THIS INSTANCE
230
+ # **Derived compensation first, newest-first** — only for `unwind`'s
231
+ # own call (`drain_compensations: true`): every leg this instance
218
232
  # actually completed that declared its own `compensates`, popped
219
- # and dispatched in reverse completion order, BEFORE any
233
+ # and dispatched in reverse completion order, before any
220
234
  # hand-written `on :refused` dispatches below — coexistence, not
221
235
  # replacement. Drained (not just read) as it fires: a saga's own
222
236
  # `on :refused` handler is guarded against re-entry by `unwind`'s
@@ -234,19 +248,19 @@ module Hecks
234
248
  deliver_saga_dispatch(process_manager, spec, event, instance, correlation, domain)
235
249
  end
236
250
 
237
- # THE CLEAR — guarded by the SAME identity check `end_saga`'s own
251
+ # **The clear** — guarded by the same identity check `end_saga`'s own
238
252
  # `.delete` return value implies: `deliver_saga_dispatch`'s
239
- # `@door.reenter` can synchronously trigger this SAME correlation's
253
+ # `@door.reenter` can synchronously trigger this same correlation's
240
254
  # `ends_on` event as a nested reaction (a leg's own dispatch is
241
255
  # what makes the saga's terminal event fire), which deletes this
242
256
  # row from the store before this line ever runs. Writing the
243
- # clear unconditionally would RESURRECT a legitimately-ended saga
257
+ # clear unconditionally would resurrect a legitimately-ended saga
244
258
  # — this diff's own first attempt did exactly that, caught by
245
259
  # `saga_durability_spec.rb`'s "deletes the checkpoint once a saga
246
260
  # genuinely ends" — so this only re-checkpoints when `instance`
247
- # is still THE SAME object `@saga_instances` holds for this
261
+ # is still the same object `@saga_instances` holds for this
248
262
  # correlation (`.equal?`, not `==`: a fresh saga reborn under the
249
- # same correlation between then and now is a DIFFERENT instance,
263
+ # same correlation between then and now is a different instance,
250
264
  # and writing this stale one's state onto that one's row would be
251
265
  # its own corruption). Under the mutex — dispatching is over by
252
266
  # now, so this is not the reentrancy hazard `advance_saga`'s own
@@ -264,9 +278,9 @@ module Hecks
264
278
  args = dispatch_args(process_manager, spec, event, instance, correlation)
265
279
  record = { process_manager: process_manager.name, instance: correlation, dispatch: spec.command_name }
266
280
 
267
- # THE RAW INPUTS `args` WAS RESOLVED FROM, captured alongside the
281
+ # The raw inputs `args` was resolved from, captured alongside the
268
282
  # result — never re-derived from history[:saga_instances] later
269
- # (that only ever holds the FINAL memory, after every step has
283
+ # (that only ever holds the final memory, after every step has
270
284
  # run; this dispatch's own memory, at the moment it actually
271
285
  # fired, is a different fact for a saga whose memory keeps
272
286
  # changing). `spec.with_spec.empty?` skipped: nothing declared
@@ -282,7 +296,7 @@ module Hecks
282
296
  end
283
297
 
284
298
  if @door.reaction_depth_reached?
285
- # THE CEILING IS NOT A DOMAIN DECISION EITHER — same reasoning as a
299
+ # The ceiling is not a domain decision either — same reasoning as a
286
300
  # crash, below — but unlike a crash there is nothing ambiguous
287
301
  # about it: the leg unambiguously did not run, so it unwinds
288
302
  # exactly like a refusal instead of stranding the instance for a
@@ -301,18 +315,18 @@ module Hecks
301
315
  attempt = 0
302
316
  compensation_recorded = false
303
317
  begin
304
- # RECORDED BEFORE DISPATCHING, not after `@door.reenter`
305
- # returns — `@door.reenter` can recursively RE-ENTER THIS SAME
306
- # saga interpreter (the event THIS dispatch emits triggers a
307
- # LATER handler, which can itself refuse and unwind) entirely
308
- # WITHIN this one call, before it ever returns here. Recording
309
- # "after reenter succeeds" would be too late for a NESTED
318
+ # Recorded before dispatching, not after `@door.reenter`
319
+ # returns — `@door.reenter` can recursively re-enter this same
320
+ # saga interpreter (the event this dispatch emits triggers a
321
+ # later handler, which can itself refuse and unwind) entirely
322
+ # within this one call, before it ever returns here. Recording
323
+ # "after reenter succeeds" would be too late for a nested
310
324
  # refusal to ever see this leg's own compensation — found
311
325
  # live: Settlement's own AccountDebited handler refuses
312
- # Account.Credit and unwinds from INSIDE Account.Debit's own
326
+ # Account.Credit and unwinds from inside Account.Debit's own
313
327
  # `reenter` call, so "delivered: true, then record" left the
314
328
  # ledger empty at the exact moment it was needed. Popped back
315
- # off in the rescues below if THIS leg's own attempt is the
329
+ # off in the rescues below if this leg's own attempt is the
316
330
  # one that failed — never left recorded for a refusal that
317
331
  # was never this leg's own to compensate.
318
332
  if spec.compensates && !compensation_recorded
@@ -336,7 +350,7 @@ module Hecks
336
350
  rescue *DOMAIN_REFUSALS => e
337
351
  unrecord_compensation(instance, correlation, domain, process_manager) if compensation_recorded
338
352
  # Same rule as the policy interpreter : a refusal by the target is
339
- # a recorded outcome, and the leg that raised it UNWINDS — see
353
+ # a recorded outcome, and the leg that raised it unwinds — see
340
354
  # `unwind`'s own comment for why the procedure runs its
341
355
  # compensation here rather than leaving the money (or whatever
342
356
  # else a leg moved) sitting out.
@@ -345,13 +359,13 @@ module Hecks
345
359
  rescue StandardError => e
346
360
  unrecord_compensation(instance, correlation, domain, process_manager) if compensation_recorded
347
361
  compensation_recorded = false
348
- # A DEFECT, not a refusal — see PolicyInterpreter#deliver's own
362
+ # A defect, not a refusal — see PolicyInterpreter#deliver's own
349
363
  # comment for the full reasoning: the same DOMAIN_REFUSALS split,
350
364
  # and the same "the triggering command already succeeded and
351
365
  # persisted by the time this runs" fact that makes catching it
352
366
  # here safe rather than reckless.
353
367
  #
354
- # UNLIKE a refusal, a crash is not a decision the domain made, so
368
+ # Unlike a refusal, a crash is not a decision the domain made, so
355
369
  # it does not unwind on the first failure — MAX_DEFECT_RETRIES
356
370
  # gives a transient failure (a DB timeout, a race, a cold start)
357
371
  # a chance to clear on its own, retrying the identical dispatch,
@@ -381,36 +395,36 @@ module Hecks
381
395
  end
382
396
  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
383
397
 
384
- # THE ROLLBACK HALF of `deliver_saga_dispatch`'s own speculative
398
+ # The rollback half of `deliver_saga_dispatch`'s own speculative
385
399
  # pre-record (that method's own comment for why it has to be
386
- # speculative) — THIS leg's own attempt is the one that failed,
400
+ # speculative) — this leg's own attempt is the one that failed,
387
401
  # so whatever was just pushed for it was never actually earned.
388
402
  # `.pop`, not a search-and-delete: nothing else can have pushed
389
- # AFTER this leg's own entry without this leg's own `@door.
403
+ # after this leg's own entry without this leg's own `@door.
390
404
  # reenter` call having already returned (the recursive re-entry
391
- # this whole mechanism exists for only ever runs BETWEEN this
405
+ # this whole mechanism exists for only ever runs between this
392
406
  # push and this leg's own return, and a nested refusal that
393
407
  # consumed it already popped it itself — this rollback only ever
394
- # runs for THIS leg's own, still-present entry).
408
+ # runs for this leg's own, still-present entry).
395
409
  def unrecord_compensation(instance, correlation, domain, process_manager)
396
410
  instance[:completed_compensations].pop
397
411
  checkpoint(process_manager, correlation, instance, domain)
398
412
  end
399
413
 
400
- # A refused leg UNWINDS — the procedure runs the leg declared `on :refused`,
414
+ # A refused leg unwinds — the procedure runs the leg declared `on :refused`,
401
415
  # which is where the compensation lives. So does a leg that hit the
402
416
  # reaction-depth ceiling, and so does a leg that crashed and stayed
403
417
  # crashing through MAX_DEFECT_RETRIES — see `deliver_saga_dispatch`'s own
404
418
  # comments for why each of those is safe to route here.
405
419
  #
406
- # Until this existed a refusal was RECORDED and nothing else happened. The
420
+ # Until this existed a refusal was recorded and nothing else happened. The
407
421
  # wire's thousand was taken from the source, refused by the destination, and
408
422
  # sat nowhere until a human dispatched the reversal by hand ; banking's
409
423
  # settlement left a debit standing with no credit and no compensation at all.
410
424
  # Both bluebooks had written the compensating leg. Nothing armed it.
411
425
  #
412
- # A compensation that is itself refused does NOT unwind again, and needs no
413
- # flag to stop it: the state moves to the compensating leg's to_state BEFORE
426
+ # A compensation that is itself refused does not unwind again, and needs no
427
+ # flag to stop it: the state moves to the compensating leg's to_state before
414
428
  # its dispatches run, so a second refusal finds the instance no longer in
415
429
  # from_state and records that instead. The check is the guard.
416
430
  def unwind(process_manager, event, instance, correlation, domain)
@@ -423,7 +437,7 @@ module Hecks
423
437
  # Same non-reentrancy reasoning as `advance_saga`'s own comment —
424
438
  # the mutex covers only the check-and-mutate-and-checkpoint step.
425
439
  advanced = @registry.saga_mutex.synchronize do
426
- # The compensating leg is selected by (REFUSED, current state)
440
+ # The compensating leg is selected by (`REFUSED`, current state)
427
441
  # too — C10.3, one rule for every leg.
428
442
  handler = process_manager.handler_for(REFUSED, instance[:state])
429
443
  unless handler
@@ -442,7 +456,7 @@ module Hecks
442
456
 
443
457
  # See `settle_transition`'s own comment on `pre_state`/
444
458
  # `instance[:state]` — the real observed transition, not a
445
- # second read of the SAME handler object `Properties.saga_
459
+ # second read of the same handler object `Properties.saga_
446
460
  # advances_follow_declared_handlers` checks this log against.
447
461
  # `drain_compensations: true` — only `unwind`'s own call site
448
462
  # fires derived compensation; `advance_saga`'s own call never
@@ -451,16 +465,16 @@ module Hecks
451
465
  drain_compensations: true)
452
466
  end
453
467
 
454
- # A DERIVED COMPENSATION — `entry[:args]` is already resolved
468
+ # A derived compensation — `entry[:args]` is already resolved
455
469
  # (`record_completed_compensation`'s own comment for why), so this
456
470
  # skips `dispatch_args` entirely and goes straight to delivery,
457
- # through the SAME retry-on-defect path an ordinary forward leg
471
+ # through the same retry-on-defect path an ordinary forward leg
458
472
  # uses. Never re-enters `unwind` on its own failure — a
459
473
  # compensation that itself refuses is a real, pre-existing gap
460
474
  # this feature makes visible rather than closes (see this file's
461
475
  # own class-level notes); `compensation_failed: true` tags it
462
476
  # distinctly in the log instead of recording it identically to an
463
- # ordinary failed delivery, and every OTHER completed compensation
477
+ # ordinary failed delivery, and every other completed compensation
464
478
  # still queued still gets its own attempt.
465
479
  def deliver_derived_compensation(process_manager, entry, correlation, domain)
466
480
  record = { process_manager: process_manager.name, instance: correlation, dispatch: entry[:command_name] }
@@ -506,40 +520,40 @@ module Hecks
506
520
  )
507
521
  end
508
522
 
509
- # BUG#6 — UNCONDITIONALLY THE SAGA'S OWN HOME DOMAIN, never inferred
510
- # from `command_name`'s own shape. This used to guess: a leftover
511
- # `::` after `Naming.command_ref`'s own rewrite was read as "already
512
- # domain-qualified" and left alone. That heuristic cannot actually
523
+ # BUG#6 — unconditionally the saga's own home domain, never inferred
524
+ # from `command_name`'s own shape. Guessing from a leftover
525
+ # `::` after `Naming.command_ref`'s own rewrite (read as "already
526
+ # domain-qualified" and left alone) cannot actually
513
527
  # tell a genuinely cross-domain reference (`Banking::Account::
514
- # Debit` -> one `::` survives) apart from a SAME-DOMAIN entity
528
+ # Debit` -> one `::` survives) apart from a same-domain entity
515
529
  # command reference (`Manifest::Slot::Fill` -> one `::` survives
516
530
  # too, for an unrelated reason — entity nesting, not a domain
517
531
  # qualifier) — both collapse to the identical "one `::` left" shape,
518
532
  # and the string alone carries no further signal to split them
519
533
  # (confirmed against `Naming.command_ref`'s own rewrite: it only
520
- # ever strips the LAST `::`, so the count of what remains is blind
534
+ # ever strips the last `::`, so the count of what remains is blind
521
535
  # to why it's there). Picking the cross-domain reading unconditionally
522
- # left `qa/stress_domains/waybill`'s own `Packing` saga dispatching
523
- # `Manifest::Slot::Fill` — an entity command in its OWN domain —
524
- # unprefixed, so `Naming.split_verb` read "Manifest" as a domain
536
+ # would leave `qa/stress_domains/waybill`'s own `Packing` saga dispatching
537
+ # `Manifest::Slot::Fill` — an entity command in its own domain —
538
+ # unprefixed, so `Naming.split_verb` would read "Manifest" as a domain
525
539
  # name instead of this chapter's own aggregate, and the dispatch
526
- # failed with `UnknownVerb`, silently recorded as an ordinary
540
+ # would fail with `UnknownVerb`, silently recorded as an ordinary
527
541
  # domain refusal rather than surfacing as the real bug it is.
528
542
  #
529
- # THE FIX MIRRORS `PolicyInterpreter#deliver`'s OWN MECHANISM,
543
+ # The fix mirrors `PolicyInterpreter#deliver`'s own mechanism,
530
544
  # which never had this bug: a policy's cross-domain target is a
531
- # SEPARATE, EXPLICIT field (`Policy#target_domain`, set only by the
545
+ # separate, explicit field (`Policy#target_domain`, set only by the
532
546
  # `across` keyword) — `deliver` unconditionally builds
533
547
  # `"#{policy.target_domain || domain}::#{policy.trigger_command}"`,
534
- # never asking whether `trigger_command` LOOKS already-qualified.
548
+ # never asking whether `trigger_command` looks already-qualified.
535
549
  # A saga's own `dispatch`/`compensates` has no such explicit field
536
- # and no keyword to set one — and, confirmed against the ENTIRE
550
+ # and no keyword to set one — and, confirmed against the entire
537
551
  # corpus (banking's Onboarding/Settlement/ExternalSettlement,
538
552
  # quality_control's BugCiWatch, and this domain's own Packing),
539
553
  # no saga anywhere ever dispatches genuinely cross-domain: "every
540
554
  # command a saga fires lands inside its own bluebook chapter"
541
555
  # (`Projections::Diagrams#saga_diagram`'s own comment, written
542
- # independently of this fix and still true). So the home domain IS
556
+ # independently of this fix and still true). So the home domain is
543
557
  # the only explicit context a saga dispatch ever has — this applies
544
558
  # it the same way `deliver` applies its own default (no `across`)
545
559
  # case, without inventing a keyword nothing in the corpus needs.
@@ -1,13 +1,13 @@
1
1
  module Hecks
2
2
  module Runtime
3
- # THE ONE SHARED CONSTANT between `SagaInterpreter` (the writer) and
3
+ # The one shared constant between `SagaInterpreter` (the writer) and
4
4
  # `Registry::SagaPersistence#rehydrate_sagas!` (the reader) for a
5
5
  # scoped, minimal answer to the saga-durability review's item 8 (a
6
6
  # durable outbox): a marker that survives exactly the window a crash
7
7
  # in `advance_saga`/`unwind` can otherwise hide.
8
8
  #
9
- # THE PROBLEM THIS CLOSES — `checkpoint` persists a saga's new state
10
- # BEFORE the leg that justifies it (`handler.dispatches`) runs, and
9
+ # **The problem this closes** — `checkpoint` persists a saga's new state
10
+ # before the leg that justifies it (`handler.dispatches`) runs, and
11
11
  # deliberately so: the mutex it holds is not reentrant, and a
12
12
  # dispatch can re-enter this same interpreter. If the process dies
13
13
  # in that window, the store says the transition happened and there
@@ -15,33 +15,33 @@ module Hecks
15
15
  # domain never got asked), not a defect (nothing raised), just
16
16
  # silence indistinguishable from a leg that finished cleanly.
17
17
  #
18
- # THE FIX — `checkpoint` now writes this key into the SAME already-
18
+ # **The fix** — `checkpoint` now writes this key into the same already-
19
19
  # durable `memory` blob (no new column, no adapter/schema change:
20
20
  # `memory` is already an opaque, adapter-agnostic JSON blob every
21
21
  # `save_saga` implementation round-trips verbatim) whenever it
22
- # checkpoints a state a dispatch cascade hasn't run for YET, and
22
+ # checkpoints a state a dispatch cascade hasn't run for yet, and
23
23
  # clears it (a second checkpoint, `pending: nil`) once that cascade
24
24
  # — success, refusal-compensated, defect-compensated, or ceiling-
25
25
  # compensated — has actually run. A crash between those two writes
26
26
  # leaves the marker standing; `rehydrate_sagas!` strips it back out
27
- # of the LIVE instance's own `:memory` (so no dispatch/`given`/
27
+ # of the live instance's own `:memory` (so no dispatch/`given`/
28
28
  # fuzzer/doc consumer of a saga's memory ever sees this key — it
29
29
  # exists only in the persisted blob) and surfaces it loudly instead.
30
30
  #
31
- # WHAT THIS DELIBERATELY DOES NOT DO — auto-redrive the pending leg.
31
+ # **What this deliberately does not do** — auto-redrive the pending leg.
32
32
  # Redelivering a dispatch whose outcome is genuinely unknown is only
33
33
  # safe with idempotent delivery (the downstream command recognizing
34
34
  # and no-op'ing a duplicate), which hecks's command/event pipeline
35
35
  # has no mechanism for today. Blindly re-dispatching without that is
36
- # how a stalled transfer becomes a DOUBLE-CREDITED one — a strictly
36
+ # how a stalled transfer becomes a double-credited one — a strictly
37
37
  # worse defect than the stall it would replace. So this is real,
38
- # durable, crash-surviving VISIBILITY into exactly what a stalled
38
+ # durable, crash-surviving visibility into exactly what a stalled
39
39
  # saga was doing when the process died — the missing half of "no
40
40
  # reconciliation pass exists". The full pending → claimed →
41
41
  # delivered outbox now exists too (`Runtime::Outbox`, ADR 0053) and
42
- # is COMPLEMENTARY, not a replacement: the outbox row names the
43
- # EVENT owed to this saga (and redrives it if the saga never got to
44
- # claim it); this marker names the saga's own LEG mid-flight after
42
+ # is complementary, not a replacement: the outbox row names the
43
+ # event owed to this saga (and redrives it if the saga never got to
44
+ # claim it); this marker names the saga's own leg mid-flight after
45
45
  # the event was delivered. A crash can leave either standing.
46
46
  SAGA_PENDING_DISPATCH_KEY = :__hecks_saga_pending_dispatch__
47
47
  end
@@ -2,29 +2,31 @@ require_relative "../ports/persistence/binding_policy"
2
2
 
3
3
  module Hecks
4
4
  module Runtime
5
- # The capability idiom for MULTI-TENANT hosting — mirrors EraCheck's
5
+ # The capability idiom for multi-tenant hosting — mirrors EraCheck's
6
6
  # own `lineage_capable?`, one level over. An adapter answers
7
- # `tenant_capable?` with true when its OWN instances genuinely
7
+ # `tenant_capable?` with true when its own instances genuinely
8
8
  # isolate one boot's data from another's, given each tenant is its
9
9
  # own separate `Runtime.boot` call (its own Registry, its own
10
10
  # Dispatcher, its own adapter instances) rather than one shared
11
11
  # process switching connections mid-dispatch.
12
12
  #
13
- # THAT LAST PART IS THE FINDING THIS MODULE ENCODES. The project
14
- # register (Bluebook::ProjectRegister) already resolves an address's
15
- # REALM to a DISPATCHER at registration time — Router#resolve looks
16
- # the FQN up in one flat table keyed by realm::domain::aggregate.verb,
17
- # and each entry already carries its OWN dispatcher from its OWN
18
- # boot. So "which tenant" is decided ONCE, at boot/registration time
13
+ # ## Why no ambient "current tenant"
14
+ #
15
+ # The project register (Bluebook::ProjectRegister) already resolves an
16
+ # address's realm to a dispatcher at registration time — Router#resolve
17
+ # looks the FQN up in one flat table keyed by realm::domain::aggregate.verb,
18
+ # and each entry already carries its own dispatcher from its own
19
+ # boot. So "which tenant" is decided once, at boot/registration time
19
20
  # (which of possibly many boots of the same directory a request's
20
21
  # realm resolves to), never per-dispatch inside a shared registry.
21
22
  # No ambient thread-local "current tenant" is needed, and no
22
23
  # connection cache is needed beyond what booting-once-per-tenant
23
24
  # already gives for free — each tenant's own PostgresEra instance
24
- # IS its own connection, held for the life of that boot.
25
+ # is its own connection, held for the life of that boot.
26
+ #
27
+ # ## What `tenant_capable?` really asks
25
28
  #
26
- # So `tenant_capable?` asks a narrower question than it might sound:
27
- # not "can this adapter switch tenants," but "does booting this
29
+ # Not "can this adapter switch tenants," but "does booting this
28
30
  # adapter twice, with different settings, for the same directory,
29
31
  # actually keep the two boots' data apart." Memory answers true
30
32
  # trivially — a `@records` Hash is a plain instance variable, and
@@ -41,13 +43,22 @@ module Hecks
41
43
  module TenantCheck
42
44
  module_function
43
45
 
44
- # A domain is safe to boot for MORE THAN ONE TENANT only if every
46
+ # Refuses to let `domain` boot for more than one tenant unless every
47
+ # aggregate's resolved persistence adapter is `tenant_capable?`.
48
+ #
49
+ # A domain is safe to boot for more than one tenant only if every
45
50
  # aggregate's resolved persistence adapter is tenant_capable? — one
46
51
  # ungoverned adapter sharing state across two tenant boots is a
47
52
  # real data leak, not a theoretical one, so this is checked before
48
53
  # a second tenant boot of the same directory is trusted, the same
49
54
  # severity EraCheck/refuse_ungoverned_roles! already hold their
50
55
  # own gates to.
56
+ #
57
+ # @param registry [Runtime::Registry] the booted registry to check
58
+ # @param domain [String, Symbol] the domain name to check every aggregate of
59
+ # @return [void]
60
+ # @raise [Runtime::WiringError] if any aggregate in `domain` is bound to an adapter
61
+ # that is not `tenant_capable?`
51
62
  def refuse_unless_tenant_capable!(registry, domain)
52
63
  bluebook = registry.bluebook(domain)
53
64
  return unless bluebook
@@ -67,12 +78,21 @@ module Hecks
67
78
  "or keep #{domain} single-tenant."
68
79
  end
69
80
 
70
- # The capability idiom itself — an adapter CLASS that answers
81
+ # Answers whether `adapter_name`'s Ruby implementation keeps two
82
+ # tenants' boots of the same directory from sharing data.
83
+ #
84
+ # The capability idiom itself — an adapter class that answers
71
85
  # tenant_capable? with true keeps two boots' data apart by
72
86
  # construction (Memory) or by an explicit per-boot isolation
73
87
  # setting (PostgresEra's schema:). Same defensive shape
74
88
  # EraCheck#lineage_capable? already uses: a class that doesn't
75
89
  # respond at all is false, not an error.
90
+ #
91
+ # @param registry [Runtime::Registry] the booted registry the adapter is wired into
92
+ # @param adapter_name [String] the adapter's declared name, such as `"PostgresEra"`
93
+ # @return [Boolean] true when the adapter is registered, has a Ruby implementation,
94
+ # and answers `tenant_capable?` true; false for any other case, including a
95
+ # missing adapter or one whose lookup raises
76
96
  def tenant_capable?(registry, adapter_name)
77
97
  adapter_class = registry.adapters[adapter_name] && registry.adapter_class(adapter_name)
78
98
  adapter_class.respond_to?(:tenant_capable?) && adapter_class.tenant_capable?
@@ -7,9 +7,9 @@ module Hecks
7
7
  module Runtime
8
8
  # `authorize policy, tenant: :field` declared a tenant boundary that
9
9
  # nothing enforced — the policy name and the field were stored and read
10
- # by nothing at dispatch time. This is the half that CAN be enforced
10
+ # by nothing at dispatch time. This is the half that can be enforced
11
11
  # without a caller-identity/session system: the boundary itself, made
12
- # mandatory. Whether THIS caller actually holds `policy` for THIS
12
+ # mandatory. Whether this caller actually holds `policy` for this
13
13
  # tenant needs real identity infrastructure this runtime does not have
14
14
  # — that stays a named, open gap, not something this quietly pretends
15
15
  # to answer.
@@ -22,21 +22,31 @@ module Hecks
22
22
  # free, with no per-engine code and no way for one engine to forget it.
23
23
  # `Scoped` is handed only to those engines as their `declared`/
24
24
  # `specification` argument — never returned to a caller that might call
25
- # an IR-level method (`filtered_head_name`, `to_h`, …) whose OWN
25
+ # an IR-level method (`filtered_head_name`, `to_h`, …) whose own
26
26
  # internal `wheres` call would resolve against the original object, not
27
27
  # this override, since `SimpleDelegator` only intercepts calls made
28
28
  # directly on the wrapper.
29
29
  module TenantScope
30
30
  module_function
31
31
 
32
+ # Wraps a declared query/read-model spec with its tenant boundary clause, if it has one.
33
+ #
34
+ # @param declared [Bluebook::Query, Bluebook::ReadModel] the declared specification to
35
+ # scope
36
+ # @param args [Hash] the query's arguments, checked for the declared tenant field
37
+ # @return [Bluebook::Query, Bluebook::ReadModel, Runtime::TenantScope::Scoped]
38
+ # `declared` unchanged when it declares no `authorize policy, tenant:`; otherwise a
39
+ # `Scoped` wrapper whose `#wheres` adds the tenant `eq` clause
40
+ # @raise [Runtime::Unauthorized] if `declared` declares a tenant boundary and `args`
41
+ # omits that field
32
42
  def apply(declared, args)
33
43
  tenant = declared.authorization&.tenant
34
44
  return declared unless tenant
35
45
 
36
46
  tenant = tenant.to_sym
37
47
  unless args.key?(tenant)
38
- raise Unauthorized, RefusalWording.render("Unauthorized", "tenant_required",
39
- query: declared.name, field: tenant)
48
+ raise Unauthorized, RefusalWording.render_site("Unauthorized", "tenant_required",
49
+ query: declared.name, field: tenant)
40
50
  end
41
51
 
42
52
  Scoped.new(declared, QuerySpecification::Common::WhereClause.new(field: tenant, op: "eq", value: tenant))
@@ -50,11 +60,19 @@ module Hecks
50
60
  # own internal `.wheres` read would bypass this override (see this
51
61
  # module's own header for why).
52
62
  class Scoped < SimpleDelegator
63
+ # @param declared [Bluebook::Query, Bluebook::ReadModel] the specification to wrap,
64
+ # delegated to for everything but `#wheres`
65
+ # @param clause [QuerySpecification::Common::WhereClause] the synthetic tenant `eq`
66
+ # clause to append
53
67
  def initialize(declared, clause)
54
68
  super(declared)
55
69
  @clause = clause
56
70
  end
57
71
 
72
+ # Reads the wrapped specification's where-clauses, with the tenant clause appended.
73
+ #
74
+ # @return [Array<QuerySpecification::Common::WhereClause>] `declared.wheres` with the
75
+ # tenant clause appended
58
76
  def wheres = __getobj__.wheres + [@clause]
59
77
  end
60
78
  end