hecks 2.8.0 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (404) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +4 -6
  3. data/lib/hecks/adapters/driven/claude_code.rb +4 -104
  4. data/lib/hecks/adapters/driven/d1.rb +44 -237
  5. data/lib/hecks/adapters/driven/folder.rb +17 -76
  6. data/lib/hecks/adapters/driven/google_authentication.rb +15 -49
  7. data/lib/hecks/adapters/driven/governance_authorization.rb +23 -70
  8. data/lib/hecks/adapters/driven/heki/journal.rb +8 -41
  9. data/lib/hecks/adapters/driven/heki/saga_store.rb +12 -54
  10. data/lib/hecks/adapters/driven/heki/snapshot.rb +4 -12
  11. data/lib/hecks/adapters/driven/heki.rb +13 -42
  12. data/lib/hecks/adapters/driven/identity_registry.rb +3 -14
  13. data/lib/hecks/adapters/driven/in_memory_ordering.rb +9 -26
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +2 -14
  15. data/lib/hecks/adapters/driven/lambda/client.rb +10 -51
  16. data/lib/hecks/adapters/driven/lambda.rb +10 -84
  17. data/lib/hecks/adapters/driven/local_storage.rb +5 -40
  18. data/lib/hecks/adapters/driven/memory.rb +17 -98
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +5 -21
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +6 -28
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +3 -12
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +4 -30
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +34 -108
  24. data/lib/hecks/adapters/driven/postgres.rb +145 -134
  25. data/lib/hecks/adapters/driven/postgres_era.adapter +4 -15
  26. data/lib/hecks/adapters/driven/prism.rb +7 -30
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +21 -121
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +8 -40
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +26 -82
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +40 -84
  31. data/lib/hecks/adapters/driven/sqlite.rb +128 -65
  32. data/lib/hecks/adapters/driven/tenant_provisioner.rb +5 -48
  33. data/lib/hecks/adapters/driven.rb +5 -36
  34. data/lib/hecks/adapters/driving/github_webhook.rb +8 -76
  35. data/lib/hecks/behaviors/dsl.rb +7 -25
  36. data/lib/hecks/behaviors/expectations.rb +31 -141
  37. data/lib/hecks/behaviors/ir.rb +7 -20
  38. data/lib/hecks/behaviors/rspec.rb +6 -17
  39. data/lib/hecks/behaviors/runner.rb +10 -34
  40. data/lib/hecks/behaviors.rb +4 -8
  41. data/lib/hecks/bluebook/aggregate.rb +9 -75
  42. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +9 -38
  43. data/lib/hecks/bluebook/assembly/build.rb +4 -15
  44. data/lib/hecks/bluebook/assembly/contract.rb +15 -136
  45. data/lib/hecks/bluebook/assembly/contracts.rb +54 -174
  46. data/lib/hecks/bluebook/assembly/marks.rb +31 -223
  47. data/lib/hecks/bluebook/assembly/specializer.rb +6 -52
  48. data/lib/hecks/bluebook/assembly.rb +6 -45
  49. data/lib/hecks/bluebook/attribute.rb +12 -61
  50. data/lib/hecks/bluebook/behaviour/aggregate.rb +10 -39
  51. data/lib/hecks/bluebook/behaviour/attribute.rb +3 -14
  52. data/lib/hecks/bluebook/behaviour/chapter.rb +14 -42
  53. data/lib/hecks/bluebook/behaviour/command.rb +14 -66
  54. data/lib/hecks/bluebook/behaviour/domain_port.rb +4 -20
  55. data/lib/hecks/bluebook/behaviour/entity.rb +7 -27
  56. data/lib/hecks/bluebook/behaviour/hexagon.rb +23 -41
  57. data/lib/hecks/bluebook/behaviour/lifecycle.rb +15 -40
  58. data/lib/hecks/bluebook/behaviour/policy.rb +23 -52
  59. data/lib/hecks/bluebook/behaviour/process_manager.rb +30 -57
  60. data/lib/hecks/bluebook/behaviour/read_model.rb +25 -31
  61. data/lib/hecks/bluebook/behaviour/traits.rb +27 -64
  62. data/lib/hecks/bluebook/behaviour/value_object.rb +5 -24
  63. data/lib/hecks/bluebook/capabilities.rb +17 -49
  64. data/lib/hecks/bluebook/chapter.rb +10 -48
  65. data/lib/hecks/bluebook/command.rb +23 -81
  66. data/lib/hecks/bluebook/domain_port.rb +14 -63
  67. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +50 -229
  68. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +50 -377
  69. data/lib/hecks/bluebook/dsl/attribute_collector.rb +33 -204
  70. data/lib/hecks/bluebook/dsl/binding_proxy.rb +16 -69
  71. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +86 -708
  72. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +56 -170
  73. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
  74. data/lib/hecks/bluebook/dsl/command_builder.rb +74 -546
  75. data/lib/hecks/bluebook/dsl/const_shim.rb +8 -41
  76. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +12 -87
  77. data/lib/hecks/bluebook/dsl/entity_builder.rb +44 -330
  78. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +20 -269
  79. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +26 -194
  80. data/lib/hecks/bluebook/dsl/identity_declaration.rb +13 -88
  81. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +6 -20
  82. data/lib/hecks/bluebook/dsl/policy_builder.rb +16 -88
  83. data/lib/hecks/bluebook/dsl/port_builder.rb +2 -6
  84. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +22 -96
  85. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +31 -184
  86. data/lib/hecks/bluebook/dsl/query_builder.rb +11 -42
  87. data/lib/hecks/bluebook/dsl/read_model_builder.rb +75 -201
  88. data/lib/hecks/bluebook/dsl/rule_reference.rb +22 -163
  89. data/lib/hecks/bluebook/dsl/translation_builder.rb +28 -114
  90. data/lib/hecks/bluebook/dsl/value_object_builder.rb +10 -124
  91. data/lib/hecks/bluebook/dsl/word_gate.rb +16 -162
  92. data/lib/hecks/bluebook/dsl/world_builder.rb +19 -111
  93. data/lib/hecks/bluebook/entity.rb +13 -71
  94. data/lib/hecks/bluebook/expression/ast_json.rb +24 -190
  95. data/lib/hecks/bluebook/expression/ast_reader.rb +12 -54
  96. data/lib/hecks/bluebook/expression/canonical_form.rb +11 -88
  97. data/lib/hecks/bluebook/expression/evaluator.rb +47 -295
  98. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +30 -147
  99. data/lib/hecks/bluebook/expression/resolver.rb +37 -660
  100. data/lib/hecks/bluebook/hexagon.rb +5 -28
  101. data/lib/hecks/bluebook/lifecycle.rb +5 -16
  102. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -7
  103. data/lib/hecks/bluebook/meta_validator/judge.rb +111 -361
  104. data/lib/hecks/bluebook/meta_validator/plan.rb +33 -162
  105. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -7
  106. data/lib/hecks/bluebook/meta_validator/readings.rb +56 -342
  107. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -176
  108. data/lib/hecks/bluebook/meta_validator/shapes.rb +53 -267
  109. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +43 -305
  110. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +7 -43
  111. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -18
  112. data/lib/hecks/bluebook/meta_validator.rb +79 -349
  113. data/lib/hecks/bluebook/model_check/client_profile.rb +6 -21
  114. data/lib/hecks/bluebook/model_check.rb +100 -597
  115. data/lib/hecks/bluebook/pattern_subset.rb +11 -69
  116. data/lib/hecks/bluebook/process_manager.rb +13 -83
  117. data/lib/hecks/bluebook/project_loader.rb +3 -39
  118. data/lib/hecks/bluebook/project_register.rb +12 -60
  119. data/lib/hecks/bluebook/query.rb +9 -48
  120. data/lib/hecks/bluebook/read_model.rb +57 -81
  121. data/lib/hecks/bluebook/reference.rb +7 -41
  122. data/lib/hecks/bluebook/smoke_test.rb +8 -98
  123. data/lib/hecks/bluebook/synthesizer.rb +13 -62
  124. data/lib/hecks/bluebook/translation.rb +26 -62
  125. data/lib/hecks/bluebook/value_object.rb +11 -36
  126. data/lib/hecks/bluebook.rb +5 -22
  127. data/lib/hecks/cache_dir.rb +4 -23
  128. data/lib/hecks/cli/document.rb +3 -21
  129. data/lib/hecks/cli/ir.rb +3 -5
  130. data/lib/hecks/cli/mcp.rb +2 -3
  131. data/lib/hecks/cli/mcp_door.rb +19 -114
  132. data/lib/hecks/cli/model_check.rb +18 -85
  133. data/lib/hecks/cli/project_cli.rb +7 -40
  134. data/lib/hecks/cli/project_diagrams.rb +0 -7
  135. data/lib/hecks/cli/run.rb +12 -58
  136. data/lib/hecks/cli/smoke_test.rb +2 -16
  137. data/lib/hecks/cli/stores.rb +1 -2
  138. data/lib/hecks/cli.rb +18 -24
  139. data/lib/hecks/construct.rb +8 -34
  140. data/lib/hecks/deploy/bluebook/deploy.bluebook +33 -142
  141. data/lib/hecks/deploy/bluebook/deploy.hecksagon +2 -9
  142. data/lib/hecks/embryonaut_bluebook/lock.rb +3 -35
  143. data/lib/hecks/embryonaut_bluebook/shape.rb +1 -17
  144. data/lib/hecks/embryonaut_bluebook/vendor.rb +8 -53
  145. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +4 -19
  146. data/lib/hecks/embryonaut_bluebook.rb +4 -71
  147. data/lib/hecks/facade/cli_door.rb +24 -106
  148. data/lib/hecks/facade/cli_runner.rb +28 -172
  149. data/lib/hecks/facade/command_request.rb +13 -33
  150. data/lib/hecks/facade/handle.rb +34 -121
  151. data/lib/hecks/facade/json_door.rb +31 -146
  152. data/lib/hecks/facade/surface/aggregate_door.rb +39 -104
  153. data/lib/hecks/facade/surface/chapter.rb +10 -54
  154. data/lib/hecks/facade/surface.rb +8 -23
  155. data/lib/hecks/facade.rb +10 -25
  156. data/lib/hecks/forms/app.rb +15 -70
  157. data/lib/hecks/forms/command_form_renderer.rb +10 -29
  158. data/lib/hecks/forms/field_renderer.rb +17 -156
  159. data/lib/hecks/forms/field_shape.rb +30 -222
  160. data/lib/hecks/forms/html.rb +12 -47
  161. data/lib/hecks/forms/index_renderer.rb +1 -4
  162. data/lib/hecks/forms/page.rb +3 -9
  163. data/lib/hecks/forms/params.rb +14 -60
  164. data/lib/hecks/forms/port_argument.rb +2 -19
  165. data/lib/hecks/forms/query_form_renderer.rb +4 -79
  166. data/lib/hecks/forms/record_renderer.rb +8 -75
  167. data/lib/hecks/forms/record_table.rb +7 -50
  168. data/lib/hecks/forms/reference_options.rb +3 -30
  169. data/lib/hecks/forms/value_object_shape.rb +8 -35
  170. data/lib/hecks/forms.rb +5 -33
  171. data/lib/hecks/fqn.rb +5 -12
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +16 -65
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +47 -134
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +5 -14
  175. data/lib/hecks/framework/bluebook/governance.bluebook +13 -34
  176. data/lib/hecks/framework/bluebook/identity.bluebook +11 -26
  177. data/lib/hecks/framework/bluebook/privacy.bluebook +8 -33
  178. data/lib/hecks/framework.rb +11 -80
  179. data/lib/hecks/freezer.rb +5 -39
  180. data/lib/hecks/grammar/expression.bluebook +15 -40
  181. data/lib/hecks/grammar/translation.bluebook +3 -6
  182. data/lib/hecks/grammar.rb +16 -41
  183. data/lib/hecks/ir.rb +14 -83
  184. data/lib/hecks/language/adapter.bluebook +2 -7
  185. data/lib/hecks/language/bluebook/aggregate.bluebook +56 -221
  186. data/lib/hecks/language/bluebook/attaches/paging.bluebook +6 -15
  187. data/lib/hecks/language/bluebook/bluebook.bluebook +15 -83
  188. data/lib/hecks/language/bluebook/bluebook.hecksagon +3 -15
  189. data/lib/hecks/language/bluebook/command.bluebook +27 -144
  190. data/lib/hecks/language/bluebook/entity.bluebook +24 -97
  191. data/lib/hecks/language/bluebook/policy.bluebook +16 -67
  192. data/lib/hecks/language/bluebook/process_manager.bluebook +28 -151
  193. data/lib/hecks/language/bluebook/projection.bluebook +126 -60
  194. data/lib/hecks/language/bluebook/query.bluebook +12 -43
  195. data/lib/hecks/language/bluebook/shape.bluebook +23 -73
  196. data/lib/hecks/language/bluebook/syntax.bluebook +51 -262
  197. data/lib/hecks/language/bluebook/vocabulary.bluebook +82 -197
  198. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +3 -10
  199. data/lib/hecks/language/hecksagon/domain_port.bluebook +4 -19
  200. data/lib/hecks/language/hecksagon/hecksagon.bluebook +6 -25
  201. data/lib/hecks/language/hecksagon/port_operation.bluebook +3 -14
  202. data/lib/hecks/language/oidc.json +35 -0
  203. data/lib/hecks/language/port.bluebook +4 -17
  204. data/lib/hecks/language/translation/translation.bluebook +3 -9
  205. data/lib/hecks/language/translation/translation_aggregate.bluebook +5 -24
  206. data/lib/hecks/language/world/wiring.bluebook +1 -3
  207. data/lib/hecks/language/world/world.bluebook +5 -7
  208. data/lib/hecks/literal.rb +8 -50
  209. data/lib/hecks/mcp_door_scope.rb +9 -90
  210. data/lib/hecks/mcp_stdio_guard.rb +4 -41
  211. data/lib/hecks/naming.rb +29 -137
  212. data/lib/hecks/ports/access_control.rb +7 -23
  213. data/lib/hecks/ports/agent/answers.rb +5 -23
  214. data/lib/hecks/ports/agent.rb +47 -172
  215. data/lib/hecks/ports/authentication.rb +12 -39
  216. data/lib/hecks/ports/authorization.rb +16 -43
  217. data/lib/hecks/ports/clock.rb +5 -45
  218. data/lib/hecks/ports/extraction.rb +7 -12
  219. data/lib/hecks/ports/identity_assignment.rb +8 -29
  220. data/lib/hecks/ports/identity_generation.rb +6 -29
  221. data/lib/hecks/ports/identity_resolution.rb +2 -10
  222. data/lib/hecks/ports/key_vault.rb +9 -45
  223. data/lib/hecks/ports/persistence/append_only.rb +61 -64
  224. data/lib/hecks/ports/persistence/binding_policy.rb +4 -14
  225. data/lib/hecks/ports/persistence/codec_boundary.rb +20 -80
  226. data/lib/hecks/ports/persistence/null_saga_store.rb +4 -20
  227. data/lib/hecks/ports/persistence/plugin.rb +13 -42
  228. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +38 -141
  229. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +15 -115
  230. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +16 -84
  231. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +6 -25
  232. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -22
  233. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +55 -181
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/events.rb +77 -0
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +12 -122
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +28 -128
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +73 -474
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +31 -124
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +45 -219
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +13 -108
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +18 -75
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +9 -49
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +6 -123
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +23 -55
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +14 -72
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +7 -16
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +14 -115
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +13 -45
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +127 -554
  250. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +13 -63
  251. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +2 -17
  252. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +3 -14
  253. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +13 -58
  254. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -17
  255. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +9 -26
  256. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +9 -25
  257. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -124
  258. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +34 -123
  259. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +5 -12
  260. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +3 -9
  261. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -14
  262. data/lib/hecks/ports/persistence/plugins/era.rb +4 -23
  263. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +7 -23
  264. data/lib/hecks/ports/persistence/postgres_dump.rb +4 -35
  265. data/lib/hecks/ports/persistence/remote_runtime.rb +2 -24
  266. data/lib/hecks/ports/persistence/repository_factory.rb +8 -27
  267. data/lib/hecks/ports/persistence/state_codec.rb +23 -103
  268. data/lib/hecks/ports/persistence.rb +7 -14
  269. data/lib/hecks/ports/projection.rb +42 -84
  270. data/lib/hecks/ports/query/in_memory.rb +10 -58
  271. data/lib/hecks/ports/query/ordering.rb +11 -32
  272. data/lib/hecks/ports/query.rb +14 -37
  273. data/lib/hecks/ports.rb +2 -8
  274. data/lib/hecks/projections/bootstrap_table.rb +11 -43
  275. data/lib/hecks/projections/deploy/fargate/assembly.rb +2 -15
  276. data/lib/hecks/projections/deploy/fargate/cdn.rb +1 -48
  277. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +2 -11
  278. data/lib/hecks/projections/deploy/fargate/check.rb +5 -14
  279. data/lib/hecks/projections/deploy/fargate/containers.rb +3 -78
  280. data/lib/hecks/projections/deploy/fargate/extras.rb +3 -44
  281. data/lib/hecks/projections/deploy/fargate/monitoring.rb +0 -44
  282. data/lib/hecks/projections/deploy/fargate/settings.rb +2 -54
  283. data/lib/hecks/projections/deploy/fargate/yaml.rb +13 -58
  284. data/lib/hecks/projections/deploy/fargate.rb +35 -202
  285. data/lib/hecks/projections/deploy/lambda.rb +150 -678
  286. data/lib/hecks/projections/deploy/preview/access_yaml.rb +1 -33
  287. data/lib/hecks/projections/deploy/preview/containers.rb +6 -111
  288. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +4 -56
  289. data/lib/hecks/projections/deploy/preview/script.rb +2 -14
  290. data/lib/hecks/projections/deploy/preview/settings.rb +3 -33
  291. data/lib/hecks/projections/deploy/preview/task_yaml.rb +2 -38
  292. data/lib/hecks/projections/deploy/preview/template.rb +2 -58
  293. data/lib/hecks/projections/deploy/preview.rb +2 -67
  294. data/lib/hecks/projections/deploy/scripts/settings.rb +3 -25
  295. data/lib/hecks/projections/deploy/scripts.rb +4 -57
  296. data/lib/hecks/projections/deploy/shared.rb +47 -146
  297. data/lib/hecks/projections/deploy/smoke.rb +4 -61
  298. data/lib/hecks/projections/deploy/template_diff/comparer.rb +3 -25
  299. data/lib/hecks/projections/deploy/template_diff/loader.rb +2 -11
  300. data/lib/hecks/projections/deploy/template_diff.rb +5 -57
  301. data/lib/hecks/projections/diagrams.rb +20 -582
  302. data/lib/hecks/projections/glossary/html.rb +14 -130
  303. data/lib/hecks/projections/glossary/markdown.rb +8 -69
  304. data/lib/hecks/projections/glossary/mermaid.rb +9 -66
  305. data/lib/hecks/projections/glossary/sensitivity.rb +5 -34
  306. data/lib/hecks/projections/glossary/sentences.rb +14 -159
  307. data/lib/hecks/projections/glossary.rb +30 -222
  308. data/lib/hecks/projections/ir.rb +3 -10
  309. data/lib/hecks/projections/model/deviations.rb +15 -42
  310. data/lib/hecks/projections/model.rb +12 -51
  311. data/lib/hecks/projections/oidc.rb +12 -56
  312. data/lib/hecks/projections/parser_table.rb +9 -28
  313. data/lib/hecks/projections/reference.rb +2 -15
  314. data/lib/hecks/projections/rust_vocabulary.rb +12 -250
  315. data/lib/hecks/projections/shape.rb +3 -29
  316. data/lib/hecks/projections/statements.rb +7 -86
  317. data/lib/hecks/projections/vocabulary.rb +6 -22
  318. data/lib/hecks/projections.rb +2 -16
  319. data/lib/hecks/projector/cli_projector.rb +60 -375
  320. data/lib/hecks/projector/docs_projector.rb +10 -201
  321. data/lib/hecks/projector/exporter.rb +42 -134
  322. data/lib/hecks/projector/ir_projector.rb +2 -9
  323. data/lib/hecks/projector/narrate_projector.rb +15 -189
  324. data/lib/hecks/projector/target.rb +13 -76
  325. data/lib/hecks/projector.rb +30 -121
  326. data/lib/hecks/query_specification/common/comparators.rb +5 -26
  327. data/lib/hecks/query_specification/common/comparison.rb +35 -143
  328. data/lib/hecks/query_specification/common/dsl.rb +8 -24
  329. data/lib/hecks/query_specification/common/null_policy.rb +20 -70
  330. data/lib/hecks/query_specification/common/options.rb +2 -6
  331. data/lib/hecks/query_specification/common/where_clause.rb +2 -10
  332. data/lib/hecks/query_specification/field_path.rb +37 -77
  333. data/lib/hecks/query_specification/hop_path.rb +12 -76
  334. data/lib/hecks/release/gem_pin.rb +2 -26
  335. data/lib/hecks/release/runner/ci_publisher.rb +3 -6
  336. data/lib/hecks/release/runner/commands.rb +2 -4
  337. data/lib/hecks/release/runner/gem_publisher.rb +2 -3
  338. data/lib/hecks/release/runner/npm_publisher.rb +6 -20
  339. data/lib/hecks/release/runner/preflight.rb +4 -19
  340. data/lib/hecks/release/runner/tagger.rb +2 -9
  341. data/lib/hecks/release/runner.rb +3 -32
  342. data/lib/hecks/rendering.rb +4 -26
  343. data/lib/hecks/router/namespace_installer.rb +12 -36
  344. data/lib/hecks/router.rb +4 -8
  345. data/lib/hecks/runtime/aggregate_lock.rb +8 -33
  346. data/lib/hecks/runtime/boot_gates.rb +3 -18
  347. data/lib/hecks/runtime/caller.rb +19 -59
  348. data/lib/hecks/runtime/capability_graph.rb +5 -21
  349. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +16 -78
  350. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +23 -160
  351. data/lib/hecks/runtime/command_interpreter.rb +124 -306
  352. data/lib/hecks/runtime/command_rules/admissibility.rb +95 -366
  353. data/lib/hecks/runtime/command_rules/arithmetic.rb +66 -196
  354. data/lib/hecks/runtime/command_rules/authorization.rb +13 -45
  355. data/lib/hecks/runtime/command_rules/emission.rb +8 -21
  356. data/lib/hecks/runtime/command_rules/references.rb +23 -202
  357. data/lib/hecks/runtime/dependency_planning.rb +30 -102
  358. data/lib/hecks/runtime/dispatcher.rb +82 -241
  359. data/lib/hecks/runtime/entity_element.rb +40 -462
  360. data/lib/hecks/runtime/entity_interpreter.rb +64 -219
  361. data/lib/hecks/runtime/errors.rb +32 -115
  362. data/lib/hecks/runtime/event.rb +3 -25
  363. data/lib/hecks/runtime/identity.rb +18 -154
  364. data/lib/hecks/runtime/instance.rb +18 -150
  365. data/lib/hecks/runtime/interpreting.rb +11 -70
  366. data/lib/hecks/runtime/invocation.rb +28 -164
  367. data/lib/hecks/runtime/loader.rb +48 -186
  368. data/lib/hecks/runtime/outbox.rb +38 -261
  369. data/lib/hecks/runtime/policy_interpreter.rb +38 -200
  370. data/lib/hecks/runtime/port_operation_interpreter.rb +26 -87
  371. data/lib/hecks/runtime/query_interpreter.rb +45 -187
  372. data/lib/hecks/runtime/reaction_invocation.rb +36 -138
  373. data/lib/hecks/runtime/read_model_interpreter.rb +117 -208
  374. data/lib/hecks/runtime/rebuild_sweep.rb +8 -54
  375. data/lib/hecks/runtime/reference_hop.rb +8 -81
  376. data/lib/hecks/runtime/refusal_wording.rb +8 -89
  377. data/lib/hecks/runtime/registry/saga_persistence.rb +10 -102
  378. data/lib/hecks/runtime/registry/verification.rb +52 -225
  379. data/lib/hecks/runtime/registry/world_defaults.rb +4 -53
  380. data/lib/hecks/runtime/registry.rb +99 -244
  381. data/lib/hecks/runtime/remote_dispatcher.rb +37 -152
  382. data/lib/hecks/runtime/routing.rb +4 -15
  383. data/lib/hecks/runtime/saga_interpreter/correlation.rb +14 -61
  384. data/lib/hecks/runtime/saga_interpreter.rb +79 -263
  385. data/lib/hecks/runtime/saga_pending_dispatch.rb +2 -43
  386. data/lib/hecks/runtime/tenant_check.rb +6 -68
  387. data/lib/hecks/runtime/tenant_scope.rb +3 -28
  388. data/lib/hecks/runtime/value/admission.rb +16 -67
  389. data/lib/hecks/runtime/value/coercion.rb +155 -641
  390. data/lib/hecks/runtime/value/entity_list_coercion.rb +26 -219
  391. data/lib/hecks/runtime/value.rb +12 -84
  392. data/lib/hecks/runtime.rb +17 -65
  393. data/lib/hecks/storehouse.rb +141 -568
  394. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +8 -32
  395. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +6 -23
  396. data/lib/hecks/vendoring/git_environment.rb +2 -8
  397. data/lib/hecks/vendoring/git_source.rb +4 -10
  398. data/lib/hecks/vendoring.rb +7 -46
  399. data/lib/hecks/version.rb +3 -14
  400. data/lib/hecks.rb +26 -81
  401. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -46
  402. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +11 -80
  403. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +10 -99
  404. metadata +3 -2
@@ -8,22 +8,16 @@ require_relative "saga_pending_dispatch"
8
8
 
9
9
  module Hecks
10
10
  module Runtime
11
- # Runs one domain's `process_manager` (saga) declarations against a
12
- # just-emitted event: begins a new correlated instance on its
13
- # starts_on event, advances a matching handler's state and dispatches
14
- # its declared commands, and ends/deletes the instance on ends_on —
15
- # checkpointing each transition durably before its dispatches run.
11
+ # Runs one domain's declared process managers against a just-emitted
12
+ # event, checkpointing each transition durably before its dispatches run.
16
13
  class SagaInterpreter
17
14
  include Correlation
18
15
 
19
- # The trigger lives on the declaration it triggers, not on the runtime that
20
- # notices it — see ProcessManager::REFUSED.
16
+ # The trigger lives on the declaration it triggers — see ProcessManager::REFUSED.
21
17
  REFUSED = Bluebook::ProcessManager::REFUSED
22
18
 
23
- # A crash gets this many extra attempts before the procedure gives up
24
- # and treats it as something to compensate for — see
25
- # `deliver_saga_dispatch`'s own comment for why a crash isn't unwound
26
- # on the first failure the way a domain refusal is.
19
+ # A crash gets this many retries before it's treated as something to
20
+ # compensate for, instead of unwinding on the first failure.
27
21
  MAX_DEFECT_RETRIES = 3
28
22
 
29
23
  attr_reader :registry
@@ -38,12 +32,7 @@ module Hecks
38
32
  end
39
33
 
40
34
  # 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
- #
44
- # `only:` — one process manager, the outbox relay's way of running
45
- # exactly the consumer a row names (`Runtime::Outbox::Relay#
46
- # run_consumer`); nil advances every manager the domain declares.
35
+ # advances or ends each matching saga instance.
47
36
  #
48
37
  # @param event [Runtime::Event] the just-emitted event to react to
49
38
  # @param domain [String, Symbol] the domain whose declared process managers
@@ -64,27 +53,11 @@ module Hecks
64
53
 
65
54
  private
66
55
 
67
- # The checkpoint write, shared by every mutation site below —
68
- # holds `saga_mutex` across both the in-memory Hash mutation and
69
- # the persistence write (§7), not just the Hash mutation alone:
70
- # two threads racing the same (process_manager, correlation) key
71
- # could otherwise interleave their writes out of order, silently
72
- # reordering a saga's own transition history — worse for the
73
- # adapters with no locking of their own (Heki) than for Postgres.
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.
79
- # `pending:` — see saga_pending_dispatch.rb. Injected into the
80
- # written copy of memory only, never into `instance[:memory]`
81
- # itself: every other reader of a live instance's memory
82
- # (`dispatch_args`'s "opening event memory" scope, the fuzzer's
83
- # own round-trip/shape checks, `saga_spec.rb`'s exact-equality
84
- # assertion against a fresh instance's seeded memory) sees exactly
85
- # what it always did. The marker exists only in the persisted
86
- # blob, and only for as long as a dispatch cascade is genuinely
87
- # in flight for this instance.
56
+ # Holds `saga_mutex` across both the in-memory mutation and the
57
+ # persistence write, never just the mutation — otherwise two threads
58
+ # racing the same (process_manager, correlation) key could interleave
59
+ # their writes out of order. `pending:` is injected into the written
60
+ # copy only, never into `instance[:memory]` itself.
88
61
  def checkpoint(process_manager, correlation, instance, domain, pending: nil)
89
62
  memory = deep_copy(instance[:memory])
90
63
  memory[SAGA_PENDING_DISPATCH_KEY] = pending if pending
@@ -95,13 +68,9 @@ module Hecks
95
68
  )
96
69
  end
97
70
 
98
- # `deep_copy` is `JSON.parse(JSON.generate(hash), ...)`, which
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
101
- # second, parallel `deep_copy_array` reimplementing the same
102
- # round-trip. `|| []` — an instance from before this field existed
103
- # (or one that has never completed a compensable leg) rehydrates
104
- # to an empty ledger, never nil.
71
+ # Wrapped in a Hash before the round-trip since `JSON.parse` only
72
+ # accepts an object at the top level. `|| []` rehydrates a ledger
73
+ # that has never completed a compensable leg to empty, never nil.
105
74
  def deep_copy_array(array) = deep_copy(list: array || [])[:list]
106
75
 
107
76
  def deep_copy(hash) = JSON.parse(JSON.generate(hash), symbolize_names: true)
@@ -119,22 +88,9 @@ module Hecks
119
88
  created = @registry.saga_mutex.synchronize do
120
89
  next false if @registry.saga_instances[process_manager.name].key?(correlation)
121
90
 
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
124
- # payload itself. A saga's own memory is meant to be written
125
- # into over its lifetime (remember-style, growing beyond what
126
- # the starting event carried) ; the payload it was seeded from
127
- # is a fact about something that already happened, logged and
128
- # emitted before the saga ever saw it. Sharing the one Hash
129
- # object between them means a write into the saga's own memory
130
- # is silently also a write into an already-emitted event's own
131
- # payload — retroactively adding a field nothing announced.
132
- # `event.payload` is deep-frozen by `Event#emit!` by the time
133
- # this runs, so a naive in-place write here would raise
134
- # FrozenError rather than corrupt silently — but the `.dup`
135
- # still matters: it is what makes the saga's own memory a
136
- # normal, writable Hash of its own, rather than one write away
137
- # from crashing every future in-place `remember`.
91
+ # `.dup` — a fresh saga's own memory starts as a copy of the
92
+ # starting event's payload, never the frozen payload itself,
93
+ # since memory is written into over the saga's lifetime.
138
94
  instance = { state: process_manager.states.first, memory: event.payload.dup, completed_compensations: [] }
139
95
  @registry.saga_instances[process_manager.name][correlation] = instance
140
96
  checkpoint(process_manager, correlation, instance, domain)
@@ -146,13 +102,10 @@ module Hecks
146
102
  instance: correlation, born: true, state: process_manager.states.first }
147
103
  end
148
104
 
149
- # The mutex covers only the state-check-and-mutate-and-checkpoint
150
- # step, never the dispatch cascade that follows — `deliver_saga_
151
- # dispatch` calls `@door.reenter`, which can recursively re-enter
152
- # this same interpreter (a saga's own leg triggering another saga,
153
- # or itself again) on the same thread, and `Mutex` is not
154
- # reentrant: holding it across that call would deadlock the
155
- # thread against itself the moment any real chain did that.
105
+ # The mutex covers only the check-mutate-checkpoint step, never the
106
+ # dispatch cascade that follows — `deliver_saga_dispatch`'s
107
+ # `@door.reenter` can recursively re-enter this interpreter on the
108
+ # same thread, and `Mutex` is not reentrant.
156
109
  def advance_saga(process_manager, event, domain)
157
110
  return unless process_manager.handles?(event.name)
158
111
 
@@ -170,10 +123,9 @@ module Hecks
170
123
  @registry.saga_log << record.merge(advanced: false, reason: "no conversation remembers #{correlation.inspect}")
171
124
  next false
172
125
  end
173
- # The leg is chosen by (event, current state) — C10.3. Read
174
- # under the mutex, against the state this instance holds right
175
- # now, so two legs on the same event from different states each
176
- # answer exactly when their own state is current.
126
+ # Leg chosen by (event, current state) — read under the mutex so
127
+ # two legs on the same event pick based on the state that's
128
+ # actually current right now.
177
129
  handler = process_manager.handler_for(event.name, instance[:state])
178
130
  unless handler
179
131
  @registry.saga_log << record.merge(advanced: false,
@@ -192,8 +144,6 @@ module Hecks
192
144
  settle_transition(process_manager, event, handler, instance, correlation, domain, record, pre_state)
193
145
  end
194
146
 
195
- # "in X, not Y" — the same wording as before C10.3 when one leg
196
- # answers the event; every leg's own from: listed when several do.
197
147
  def leg_mismatch(process_manager, event_name, state)
198
148
  expected = process_manager.handlers_for(event_name).map { |h| h.from_state.inspect }.uniq
199
149
  "in #{state.inspect}, not #{expected.join(' or ')}"
@@ -203,41 +153,19 @@ module Hecks
203
153
  { on: event.name, from: from_state, to: to_state, dispatches: handler.dispatches.map(&:command_name) }
204
154
  end
205
155
 
206
- # The shared tail of `advance_saga` and `unwind` — both are "guard,
207
- # mutate, checkpoint-with-pending" under the mutex (kept separate
208
- # per caller: `advance_saga`'s own guard also has to handle "no
209
- # instance at all", `unwind`'s doesn't), then this: log the real
210
- # observed transition, run the leg's dispatches, and clear the
211
- # pending marker once that cascade — however it ended — is done.
156
+ # The shared tail of `advance_saga` and `unwind`, both of which guard,
157
+ # mutate and checkpoint under the mutex before calling this.
212
158
  def settle_transition(process_manager, event, handler, instance, correlation, domain, record, pre_state,
213
159
  drain_compensations: false)
214
- # `from:`/`to:` are the instance's own real pre/post state — read
215
- # back from `instance` itself, never re-derived from `handler.
216
- # from_state`/`handler.to_state` a second time. `Properties.saga_
217
- # advances_follow_declared_handlers` (fuzzing/properties.rb) builds
218
- # its own "declared edges" list from this same handler object (via
219
- # `process_manager.handlers`), so a log entry that just echoed `handler.
220
- # from_state`/`handler.to_state` back could never disagree with
221
- # that list no matter what the runtime actually did — the entry
222
- # and the thing it's checked against would be the identical fact,
223
- # read twice. Logging the instance's own observed state instead
224
- # means a future defect that moves an instance somewhere its own
225
- # declared handler didn't say (a stale handler reference, the
226
- # wrong handler picked, a second racing mutation) shows up as a
227
- # real mismatch instead of vanishing into a tautology.
160
+ # from:/to: read back from `instance` itself, never re-derived from
161
+ # `handler.from_state`/`to_state` — otherwise a log entry checked
162
+ # against that same handler object could never actually disagree
163
+ # with it, no matter what the runtime did.
228
164
  @registry.saga_log << record.merge(advanced: true, from: pre_state, to: instance[:state])
229
165
 
230
- # **Derived compensation first, newest-first** — only for `unwind`'s
231
- # own call (`drain_compensations: true`): every leg this instance
232
- # actually completed that declared its own `compensates`, popped
233
- # and dispatched in reverse completion order, before any
234
- # hand-written `on :refused` dispatches below — coexistence, not
235
- # replacement. Drained (not just read) as it fires: a saga's own
236
- # `on :refused` handler is guarded against re-entry by `unwind`'s
237
- # own `instance[:state] == handler.from_state` check, so this can
238
- # only ever run once per refusal — but draining rather than
239
- # leaving the ledger populated is what makes that true by
240
- # construction too, not only by the state guard.
166
+ # Derived compensations run newest-first, before any hand-written
167
+ # `on :refused` dispatches below. Drained (popped), not just read —
168
+ # that's what keeps a re-entrant `on :refused` from running twice.
241
169
  if drain_compensations
242
170
  compensations = instance[:completed_compensations] || []
243
171
  deliver_derived_compensation(process_manager, compensations.pop, correlation, domain) until compensations.empty?
@@ -248,23 +176,11 @@ module Hecks
248
176
  deliver_saga_dispatch(process_manager, spec, event, instance, correlation, domain)
249
177
  end
250
178
 
251
- # **The clear** — guarded by the same identity check `end_saga`'s own
252
- # `.delete` return value implies: `deliver_saga_dispatch`'s
253
- # `@door.reenter` can synchronously trigger this same correlation's
254
- # `ends_on` event as a nested reaction (a leg's own dispatch is
255
- # what makes the saga's terminal event fire), which deletes this
256
- # row from the store before this line ever runs. Writing the
257
- # clear unconditionally would resurrect a legitimately-ended saga
258
- # — this diff's own first attempt did exactly that, caught by
259
- # `saga_durability_spec.rb`'s "deletes the checkpoint once a saga
260
- # genuinely ends" — so this only re-checkpoints when `instance`
261
- # is still the same object `@saga_instances` holds for this
262
- # correlation (`.equal?`, not `==`: a fresh saga reborn under the
263
- # same correlation between then and now is a different instance,
264
- # and writing this stale one's state onto that one's row would be
265
- # its own corruption). Under the mutex — dispatching is over by
266
- # now, so this is not the reentrancy hazard `advance_saga`'s own
267
- # comment warns about.
179
+ # Guarded by `.equal?` (not `==`): `deliver_saga_dispatch`'s own
180
+ # `reenter` can trigger this correlation's `ends_on` as a nested
181
+ # reaction, deleting this row before this line runs. Clearing
182
+ # unconditionally would resurrect an already-ended saga, or stamp
183
+ # a reborn instance with a stale one's state.
268
184
  @registry.saga_mutex.synchronize do
269
185
  next unless @registry.saga_instances[process_manager.name][correlation].equal?(instance)
270
186
 
@@ -278,15 +194,9 @@ module Hecks
278
194
  args = dispatch_args(process_manager, spec, event, instance, correlation)
279
195
  record = { process_manager: process_manager.name, instance: correlation, dispatch: spec.command_name }
280
196
 
281
- # The raw inputs `args` was resolved from, captured alongside the
282
- # result — never re-derived from history[:saga_instances] later
283
- # (that only ever holds the final memory, after every step has
284
- # run; this dispatch's own memory, at the moment it actually
285
- # fired, is a different fact for a saga whose memory keeps
286
- # changing). `spec.with_spec.empty?` skipped: nothing declared
287
- # to check, a tautological pass, the same reason a command with
288
- # no givens/from is skipped by lifecycle_guard_and_given_
289
- # violations_are_refused.
197
+ # Raw inputs captured alongside the resolved result — never
198
+ # re-derived later from saga_instances, which only ever holds the
199
+ # final memory, not what it was at the moment this dispatch fired.
290
200
  unless spec.with_spec.to_a.empty?
291
201
  @registry.saga_dispatch_log << { process_manager: process_manager.name, instance: correlation,
292
202
  dispatch: spec.command_name,
@@ -296,16 +206,8 @@ module Hecks
296
206
  end
297
207
 
298
208
  if @door.reaction_depth_reached?
299
- # The ceiling is not a domain decision either — same reasoning as a
300
- # crash, below — but unlike a crash there is nothing ambiguous
301
- # about it: the leg unambiguously did not run, so it unwinds
302
- # exactly like a refusal instead of stranding the instance for a
303
- # human to notice. `unwind`'s own state guard (it moves to its
304
- # `to_state` before its dispatches run) is what keeps this from
305
- # looping if the ceiling is still in effect when the compensating
306
- # leg tries to dispatch — that leg's own attempt hits this same
307
- # branch, calls `unwind` again, and finds the instance already
308
- # past `from_state`.
209
+ # Not a domain decision, but unambiguous — the leg didn't run, so
210
+ # it unwinds like a refusal rather than stranding the instance.
309
211
  @registry.saga_log << record.merge(delivered: false,
310
212
  reason: "reaction depth #{@door.max_reaction_depth} reached")
311
213
  unwind(process_manager, event, instance, correlation, domain)
@@ -315,20 +217,12 @@ module Hecks
315
217
  attempt = 0
316
218
  compensation_recorded = false
317
219
  begin
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
324
- # refusal to ever see this leg's own compensation — found
325
- # live: Settlement's own AccountDebited handler refuses
326
- # Account.Credit and unwinds from inside Account.Debit's own
327
- # `reenter` call, so "delivered: true, then record" left the
328
- # ledger empty at the exact moment it was needed. Popped back
329
- # off in the rescues below if this leg's own attempt is the
330
- # one that failed — never left recorded for a refusal that
331
- # was never this leg's own to compensate.
220
+ # Recorded before dispatching, not after `@door.reenter` returns —
221
+ # reenter can recursively re-enter this interpreter and refuse
222
+ # before ever returning here, and recording only on success would
223
+ # be too late for that nested refusal to see this leg's own
224
+ # compensation. Popped back off in the rescues below if this
225
+ # leg's own attempt is the one that failed.
332
226
  if spec.compensates && !compensation_recorded
333
227
  resolved = dispatch_args(process_manager, spec.compensates, event, instance, correlation)
334
228
  instance[:completed_compensations] << { command_name: spec.compensates.command_name, args: resolved }
@@ -349,35 +243,18 @@ module Hecks
349
243
  @registry.saga_log << record.merge(delivered: true)
350
244
  rescue *DOMAIN_REFUSALS => e
351
245
  unrecord_compensation(instance, correlation, domain, process_manager) if compensation_recorded
352
- # Same rule as the policy interpreter : a refusal by the target is
353
- # a recorded outcome, and the leg that raised it unwinds — see
354
- # `unwind`'s own comment for why the procedure runs its
355
- # compensation here rather than leaving the money (or whatever
356
- # else a leg moved) sitting out.
246
+ # A refusal by the target is a recorded outcome, and the leg
247
+ # that raised it unwinds and runs its own compensation there.
357
248
  @registry.saga_log << record.merge(delivered: false, reason: e.message)
358
249
  unwind(process_manager, event, instance, correlation, domain)
359
250
  rescue StandardError => e
360
251
  unrecord_compensation(instance, correlation, domain, process_manager) if compensation_recorded
361
252
  compensation_recorded = false
362
- # A defect, not a refusal — see PolicyInterpreter#deliver's own
363
- # comment for the full reasoning: the same DOMAIN_REFUSALS split,
364
- # and the same "the triggering command already succeeded and
365
- # persisted by the time this runs" fact that makes catching it
366
- # here safe rather than reckless.
367
- #
368
- # Unlike a refusal, a crash is not a decision the domain made, so
369
- # it does not unwind on the first failure — MAX_DEFECT_RETRIES
370
- # gives a transient failure (a DB timeout, a race, a cold start)
371
- # a chance to clear on its own, retrying the identical dispatch,
372
- # before this is treated as something to compensate for. Every
373
- # attempt is recorded distinguishably (`defect: true`, the
374
- # error's own class); only once retries are exhausted is it
375
- # warned to STDERR and unwound — tagged `defect_compensated:
376
- # true` rather than folded into an ordinary refusal's shape, so
377
- # the log never misrepresents a crash as a decision the domain
378
- # made. Compensating a genuinely stuck leg beats leaving it for a
379
- # human to find; misrepresenting *why* it compensated is what the
380
- # tag is for.
253
+ # Unlike a refusal, a crash isn't a domain decision, so it
254
+ # doesn't unwind on the first failure — MAX_DEFECT_RETRIES lets a
255
+ # transient failure clear on retry. `defect_compensated: true`
256
+ # tags an exhausted retry distinctly, so the log never
257
+ # misrepresents a crash as a decision the domain made.
381
258
  attempt += 1
382
259
  if attempt <= MAX_DEFECT_RETRIES
383
260
  @registry.saga_log << record.merge(delivered: false, reason: e.message,
@@ -395,38 +272,23 @@ module Hecks
395
272
  end
396
273
  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
397
274
 
398
- # The rollback half of `deliver_saga_dispatch`'s own speculative
399
- # pre-record (that method's own comment for why it has to be
400
- # speculative) — this leg's own attempt is the one that failed,
401
- # so whatever was just pushed for it was never actually earned.
402
- # `.pop`, not a search-and-delete: nothing else can have pushed
403
- # after this leg's own entry without this leg's own `@door.
404
- # reenter` call having already returned (the recursive re-entry
405
- # this whole mechanism exists for only ever runs between this
406
- # push and this leg's own return, and a nested refusal that
407
- # consumed it already popped it itself — this rollback only ever
408
- # runs for this leg's own, still-present entry).
275
+ # `.pop`, not search-and-delete — nothing else can have pushed after
276
+ # this leg's own entry without this leg's own `reenter` having
277
+ # already returned first.
409
278
  def unrecord_compensation(instance, correlation, domain, process_manager)
410
279
  instance[:completed_compensations].pop
411
280
  checkpoint(process_manager, correlation, instance, domain)
412
281
  end
413
282
 
414
- # A refused leg unwinds — the procedure runs the leg declared `on :refused`,
415
- # which is where the compensation lives. So does a leg that hit the
416
- # reaction-depth ceiling, and so does a leg that crashed and stayed
417
- # crashing through MAX_DEFECT_RETRIES — see `deliver_saga_dispatch`'s own
418
- # comments for why each of those is safe to route here.
419
- #
420
- # Until this existed a refusal was recorded and nothing else happened. The
421
- # wire's thousand was taken from the source, refused by the destination, and
422
- # sat nowhere until a human dispatched the reversal by hand ; banking's
423
- # settlement left a debit standing with no credit and no compensation at all.
424
- # Both bluebooks had written the compensating leg. Nothing armed it.
283
+ # A refused leg unwinds by running its own declared `on :refused`
284
+ # handler, where compensation lives — also the destination for a
285
+ # leg that hit the reaction-depth ceiling or exhausted its defect
286
+ # retries.
425
287
  #
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
428
- # its dispatches run, so a second refusal finds the instance already out of
429
- # from_state and records that instead. The check is the guard.
288
+ # A compensation that is itself refused does not unwind again: the
289
+ # state moves to the compensating leg's to_state before its
290
+ # dispatches run, so a second refusal finds the instance already
291
+ # out of from_state and records that instead.
430
292
  def unwind(process_manager, event, instance, correlation, domain)
431
293
  return unless instance && process_manager.handles?(REFUSED)
432
294
 
@@ -435,10 +297,8 @@ module Hecks
435
297
  pre_state = nil
436
298
 
437
299
  # Same non-reentrancy reasoning as `advance_saga`'s own comment —
438
- # the mutex covers only the check-and-mutate-and-checkpoint step.
300
+ # the mutex covers only the check-mutate-checkpoint step.
439
301
  advanced = @registry.saga_mutex.synchronize do
440
- # The compensating leg is selected by (`REFUSED`, current state)
441
- # too — C10.3, one rule for every leg.
442
302
  handler = process_manager.handler_for(REFUSED, instance[:state])
443
303
  unless handler
444
304
  @registry.saga_log << record.merge(advanced: false,
@@ -454,28 +314,16 @@ module Hecks
454
314
  end
455
315
  return unless advanced
456
316
 
457
- # See `settle_transition`'s own comment on `pre_state`/
458
- # `instance[:state]` — the real observed transition, not a
459
- # second read of the same handler object `Properties.saga_
460
- # advances_follow_declared_handlers` checks this log against.
461
- # `drain_compensations: true` — only `unwind`'s own call site
462
- # fires derived compensation; `advance_saga`'s own call never
463
- # does.
317
+ # `drain_compensations: true` only here — `advance_saga`'s own call
318
+ # never fires derived compensation.
464
319
  settle_transition(process_manager, event, handler, instance, correlation, domain, record, pre_state,
465
320
  drain_compensations: true)
466
321
  end
467
322
 
468
- # A derived compensation — `entry[:args]` is already resolved
469
- # (`record_completed_compensation`'s own comment for why), so this
470
- # skips `dispatch_args` entirely and goes straight to delivery,
471
- # through the same retry-on-defect path an ordinary forward leg
472
- # uses. Never re-enters `unwind` on its own failure — a
473
- # compensation that itself refuses is a real, pre-existing gap
474
- # this feature makes visible rather than closes (see this file's
475
- # own class-level notes); `compensation_failed: true` tags it
476
- # distinctly in the log instead of recording it identically to an
477
- # ordinary failed delivery, and every other completed compensation
478
- # still queued still gets its own attempt.
323
+ # `entry[:args]` is already resolved, so this skips `dispatch_args`
324
+ # and goes straight to delivery. Never re-enters `unwind` on its own
325
+ # failure — `compensation_failed: true` tags a refused compensation
326
+ # distinctly rather than as an ordinary failed delivery.
479
327
  def deliver_derived_compensation(process_manager, entry, correlation, domain)
480
328
  record = { process_manager: process_manager.name, instance: correlation, dispatch: entry[:command_name] }
481
329
 
@@ -520,43 +368,11 @@ module Hecks
520
368
  )
521
369
  end
522
370
 
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
527
- # tell a genuinely cross-domain reference (`Banking::Account::
528
- # Debit` -> one `::` survives) apart from a same-domain entity
529
- # command reference (`Manifest::Slot::Fill` -> one `::` survives
530
- # too, for an unrelated reason — entity nesting, not a domain
531
- # qualifier) — both collapse to the identical "one `::` left" shape,
532
- # and the string alone carries no further signal to split them
533
- # (confirmed against `Naming.command_ref`'s own rewrite: it only
534
- # ever strips the last `::`, so the count of what remains is blind
535
- # to why it's there). Picking the cross-domain reading unconditionally
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
539
- # name instead of this chapter's own aggregate, and the dispatch
540
- # would fail with `UnknownVerb`, silently recorded as an ordinary
541
- # domain refusal rather than surfacing as the real bug it is.
542
- #
543
- # The fix mirrors `PolicyInterpreter#deliver`'s own mechanism,
544
- # which never had this bug: a policy's cross-domain target is a
545
- # separate, explicit field (`Policy#target_domain`, set only by the
546
- # `across` keyword) — `deliver` unconditionally builds
547
- # `"#{policy.target_domain || domain}::#{policy.trigger_command}"`,
548
- # never asking whether `trigger_command` looks already-qualified.
549
- # A saga's own `dispatch`/`compensates` has no such explicit field
550
- # and no keyword to set one — and, confirmed against the entire
551
- # corpus (banking's Onboarding/Settlement/ExternalSettlement,
552
- # quality_control's BugCiWatch, and this domain's own Packing),
553
- # no saga anywhere ever dispatches genuinely cross-domain: "every
554
- # command a saga fires lands inside its own bluebook chapter"
555
- # (`Projections::Diagrams#saga_diagram`'s own comment, written
556
- # independently of this fix and still true). So the home domain is
557
- # the only explicit context a saga dispatch ever has — this applies
558
- # it the same way `deliver` applies its own default (no `across`)
559
- # case, without inventing a keyword nothing in the corpus needs.
371
+ # Unconditionally the saga's own home domain, never inferred from
372
+ # `command_name`'s shape — unlike a policy's explicit `target_domain`
373
+ # (set by `across`), a saga's `dispatch`/`compensates` has no such
374
+ # field, and every command a saga fires lands inside its own
375
+ # bluebook chapter.
560
376
  def qualified(command_name, domain)
561
377
  "#{domain}::#{command_name}"
562
378
  end
@@ -1,48 +1,7 @@
1
1
  module Hecks
2
2
  module Runtime
3
- # The one shared constant between `SagaInterpreter` (the writer) and
4
- # `Registry::SagaPersistence#rehydrate_sagas!` (the reader) for a
5
- # scoped, minimal answer to the saga-durability review's item 8 (a
6
- # durable outbox): a marker that survives exactly the window a crash
7
- # in `advance_saga`/`unwind` can otherwise hide.
8
- #
9
- # **The problem this closes** — `checkpoint` persists a saga's new state
10
- # before the leg that justifies it (`handler.dispatches`) runs, and
11
- # deliberately so: the mutex it holds is not reentrant, and a
12
- # dispatch can re-enter this same interpreter. If the process dies
13
- # in that window, the store says the transition happened and there
14
- # is no record that its dispatch(es) never ran — not a refusal (the
15
- # domain never got asked), not a defect (nothing raised), just
16
- # silence indistinguishable from a leg that finished cleanly.
17
- #
18
- # **The fix** — `checkpoint` now writes this key into the same already-
19
- # durable `memory` blob (no new column, no adapter/schema change:
20
- # `memory` is already an opaque, adapter-agnostic JSON blob every
21
- # `save_saga` implementation round-trips verbatim) whenever it
22
- # checkpoints a state a dispatch cascade hasn't run for yet, and
23
- # clears it (a second checkpoint, `pending: nil`) once that cascade
24
- # — success, refusal-compensated, defect-compensated, or ceiling-
25
- # compensated — has actually run. A crash between those two writes
26
- # leaves the marker standing; `rehydrate_sagas!` strips it back out
27
- # of the live instance's own `:memory` (so no dispatch/`given`/
28
- # fuzzer/doc consumer of a saga's memory ever sees this key — it
29
- # exists only in the persisted blob) and surfaces it loudly instead.
30
- #
31
- # **What this deliberately does not do** — auto-redrive the pending leg.
32
- # Redelivering a dispatch whose outcome is genuinely unknown is only
33
- # safe with idempotent delivery (the downstream command recognizing
34
- # and no-op'ing a duplicate), which hecks's command/event pipeline
35
- # has no mechanism for today. Blindly re-dispatching without that is
36
- # how a stalled transfer becomes a double-credited one — a strictly
37
- # worse defect than the stall it would replace. So this is real,
38
- # durable, crash-surviving visibility into exactly what a stalled
39
- # saga was doing when the process died — the missing half of "no
40
- # reconciliation pass exists". The full pending → claimed →
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
45
- # the event was delivered. A crash can leave either standing.
3
+ # Marks a checkpointed saga leg whose dispatch may not have run (persisted in `memory`).
4
+ # Reported, never redriven: dispatch isn't idempotent, so a replay could double-apply.
46
5
  SAGA_PENDING_DISPATCH_KEY = :__hecks_saga_pending_dispatch__
47
6
  end
48
7
  end