hecks 2.7.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 (463) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +11 -0
  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 +18 -99
  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 +15 -37
  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 +32 -142
  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 +17 -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 +38 -26
  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 +17 -65
  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 -189
  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 -106
  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 +54 -268
  109. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +47 -308
  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 +10 -91
  114. data/lib/hecks/bluebook/model_check.rb +102 -600
  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 +84 -0
  128. data/lib/hecks/cli/document.rb +86 -0
  129. data/lib/hecks/cli/ir.rb +45 -0
  130. data/lib/hecks/cli/mcp.rb +31 -0
  131. data/lib/hecks/cli/mcp_door.rb +366 -0
  132. data/lib/hecks/cli/model_check.rb +203 -0
  133. data/lib/hecks/cli/project_cli.rb +90 -0
  134. data/lib/hecks/cli/project_diagrams.rb +36 -0
  135. data/lib/hecks/cli/run.rb +173 -0
  136. data/lib/hecks/cli/smoke_test.rb +45 -0
  137. data/lib/hecks/cli/stores.rb +53 -0
  138. data/lib/hecks/cli.rb +160 -0
  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 +98 -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 +95 -0
  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 +62 -65
  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 +7 -26
  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 +22 -122
  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 +151 -679
  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 +9 -16
  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 +46 -138
  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 +93 -0
  336. data/lib/hecks/release/runner/commands.rb +66 -0
  337. data/lib/hecks/release/runner/console.rb +52 -0
  338. data/lib/hecks/release/runner/gem_publisher.rb +43 -0
  339. data/lib/hecks/release/runner/git.rb +48 -0
  340. data/lib/hecks/release/runner/npm_publisher.rb +75 -0
  341. data/lib/hecks/release/runner/preflight.rb +104 -0
  342. data/lib/hecks/release/runner/published.rb +55 -0
  343. data/lib/hecks/release/runner/tagger.rb +80 -0
  344. data/lib/hecks/release/runner.rb +215 -0
  345. data/lib/hecks/rendering.rb +4 -26
  346. data/lib/hecks/router/namespace_installer.rb +12 -36
  347. data/lib/hecks/router.rb +4 -8
  348. data/lib/hecks/runtime/aggregate_lock.rb +8 -33
  349. data/lib/hecks/runtime/boot_gates.rb +3 -18
  350. data/lib/hecks/runtime/caller.rb +19 -59
  351. data/lib/hecks/runtime/capability_graph.rb +5 -21
  352. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +16 -78
  353. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +23 -160
  354. data/lib/hecks/runtime/command_interpreter.rb +125 -307
  355. data/lib/hecks/runtime/command_rules/admissibility.rb +95 -366
  356. data/lib/hecks/runtime/command_rules/arithmetic.rb +66 -196
  357. data/lib/hecks/runtime/command_rules/authorization.rb +13 -45
  358. data/lib/hecks/runtime/command_rules/emission.rb +8 -21
  359. data/lib/hecks/runtime/command_rules/references.rb +23 -202
  360. data/lib/hecks/runtime/dependency_planning.rb +30 -102
  361. data/lib/hecks/runtime/dispatcher.rb +82 -241
  362. data/lib/hecks/runtime/entity_element.rb +40 -462
  363. data/lib/hecks/runtime/entity_interpreter.rb +65 -220
  364. data/lib/hecks/runtime/errors.rb +32 -115
  365. data/lib/hecks/runtime/event.rb +3 -25
  366. data/lib/hecks/runtime/identity.rb +18 -154
  367. data/lib/hecks/runtime/instance.rb +18 -150
  368. data/lib/hecks/runtime/interpreting.rb +11 -70
  369. data/lib/hecks/runtime/invocation.rb +28 -164
  370. data/lib/hecks/runtime/loader.rb +48 -186
  371. data/lib/hecks/runtime/outbox.rb +38 -261
  372. data/lib/hecks/runtime/policy_interpreter.rb +38 -200
  373. data/lib/hecks/runtime/port_operation_interpreter.rb +26 -87
  374. data/lib/hecks/runtime/query_interpreter.rb +45 -187
  375. data/lib/hecks/runtime/reaction_invocation.rb +36 -138
  376. data/lib/hecks/runtime/read_model_interpreter.rb +139 -208
  377. data/lib/hecks/runtime/rebuild_sweep.rb +8 -54
  378. data/lib/hecks/runtime/reference_hop.rb +8 -81
  379. data/lib/hecks/runtime/refusal_wording.rb +8 -89
  380. data/lib/hecks/runtime/registry/saga_persistence.rb +10 -102
  381. data/lib/hecks/runtime/registry/verification.rb +52 -225
  382. data/lib/hecks/runtime/registry/world_defaults.rb +4 -53
  383. data/lib/hecks/runtime/registry.rb +103 -244
  384. data/lib/hecks/runtime/remote_dispatcher.rb +37 -152
  385. data/lib/hecks/runtime/routing.rb +4 -15
  386. data/lib/hecks/runtime/saga_interpreter/correlation.rb +14 -61
  387. data/lib/hecks/runtime/saga_interpreter.rb +79 -263
  388. data/lib/hecks/runtime/saga_pending_dispatch.rb +2 -43
  389. data/lib/hecks/runtime/tenant_check.rb +6 -68
  390. data/lib/hecks/runtime/tenant_scope.rb +3 -28
  391. data/lib/hecks/runtime/value/admission.rb +16 -67
  392. data/lib/hecks/runtime/value/coercion.rb +155 -641
  393. data/lib/hecks/runtime/value/entity_list_coercion.rb +26 -219
  394. data/lib/hecks/runtime/value.rb +12 -84
  395. data/lib/hecks/runtime.rb +17 -65
  396. data/lib/hecks/storehouse.rb +146 -567
  397. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +8 -32
  398. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +6 -23
  399. data/lib/hecks/vendoring/git_environment.rb +2 -8
  400. data/lib/hecks/vendoring/git_source.rb +4 -10
  401. data/lib/hecks/vendoring.rb +7 -46
  402. data/lib/hecks/version.rb +3 -14
  403. data/lib/hecks/vocabulary.rb +5 -0
  404. data/lib/hecks.rb +32 -82
  405. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -46
  406. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +11 -80
  407. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +10 -99
  408. metadata +30 -59
  409. data/lib/hecks/bench/cli.rb +0 -74
  410. data/lib/hecks/bench/environment.rb +0 -100
  411. data/lib/hecks/bench/postgres_probe.rb +0 -41
  412. data/lib/hecks/bench/report.rb +0 -116
  413. data/lib/hecks/bench/ruby_runner.rb +0 -151
  414. data/lib/hecks/bench/run.rb +0 -55
  415. data/lib/hecks/bench/rust_runner.rb +0 -165
  416. data/lib/hecks/bench/stats.rb +0 -75
  417. data/lib/hecks/bench/suite.rb +0 -172
  418. data/lib/hecks/bench/workload.rb +0 -137
  419. data/lib/hecks/bench.rb +0 -28
  420. data/lib/hecks/codemod.rb +0 -443
  421. data/lib/hecks/corpus.rb +0 -479
  422. data/lib/hecks/doc/reference.rb +0 -585
  423. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
  424. data/lib/hecks/fuzzing/combination_miner.rb +0 -178
  425. data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
  426. data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
  427. data/lib/hecks/fuzzing/differential.rb +0 -192
  428. data/lib/hecks/fuzzing/domain_generator.rb +0 -871
  429. data/lib/hecks/fuzzing/era_boundary.rb +0 -154
  430. data/lib/hecks/fuzzing/form_census.rb +0 -285
  431. data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
  432. data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
  433. data/lib/hecks/fuzzing/isolated_boot.rb +0 -658
  434. data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
  435. data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
  436. data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
  437. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
  438. data/lib/hecks/fuzzing/properties/guards.rb +0 -398
  439. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
  440. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
  441. data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
  442. data/lib/hecks/fuzzing/properties/querying.rb +0 -260
  443. data/lib/hecks/fuzzing/properties.rb +0 -267
  444. data/lib/hecks/fuzzing/qa_settings.rb +0 -164
  445. data/lib/hecks/fuzzing/replay.rb +0 -930
  446. data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
  447. data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
  448. data/lib/hecks/fuzzing/self_consistency.rb +0 -825
  449. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
  450. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
  451. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
  452. data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
  453. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
  454. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
  455. data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
  456. data/lib/hecks/fuzzing/shrinker.rb +0 -309
  457. data/lib/hecks/fuzzing/structural_skips.rb +0 -53
  458. data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
  459. data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
  460. data/lib/hecks/fuzzing/value_generator.rb +0 -340
  461. data/lib/hecks/fuzzing.rb +0 -23
  462. data/lib/hecks/grammar/evolve.rb +0 -524
  463. data/lib/hecks/query_ir.rb +0 -482
@@ -2,32 +2,10 @@ require "time"
2
2
 
3
3
  module Hecks
4
4
  module Runtime
5
- # `correlation` is not on the wire — `to_h` below deliberately omits it,
6
- # the same as `bin/run`'s own event projection does. It is runtime
7
- # bookkeeping stamped by `Dispatcher#dispatch` when a saga leg's own
8
- # dispatch causes this event (see `SagaInterpreter#deliver_saga_dispatch`
9
- # and `#saga_correlation`) : a Hash of `correlation_head` -> correlation
10
- # value, so an event caused by one saga cannot be misread by an unrelated
11
- # one correlating on a different field. Absent for any event no saga
12
- # dispatch caused, which is most of them.
5
+ # An emitted domain event. `correlation` (saga head -> value) is runtime bookkeeping
6
+ # and deliberately absent from `to_h`.
13
7
  Event = Struct.new(:name, :aggregate, :id, :payload, :occurred_at, :correlation, keyword_init: true) do
14
- # Freezes the event deep, so nothing about it can change after it happens.
15
- #
16
- # An emitted event is a record of something that happened, and a
17
- # mutable audit trail is not one. The payload — the domain fact the
18
- # event carries — is frozen through on emission: freezing the Hash
19
- # alone would leave every value in it editable in place, which is
20
- # the shape all four previous freezing bugs had.
21
- #
22
- # The whole event, not just its payload. Correlation is set at
23
- # construction rather than merged in here by `Dispatcher#dispatch`
24
- # after the event already exists, because it is part of the
25
- # transaction and known from `dispatch`'s own argument before
26
- # anything is emitted — that is what keeps an event immutable once
27
- # it exists.
28
- #
29
- # The log stays appendable: new events are still recorded. It is
30
- # each event that stops changing once it exists.
8
+ # Freezes the event deep, payload and correlation included.
31
9
  #
32
10
  # @return [void]
33
11
  def emit!
@@ -3,58 +3,21 @@ require_relative "value"
3
3
 
4
4
  module Hecks
5
5
  module Runtime
6
- # **The scalar an identity path names**.
7
- #
8
- # An identity is declared as a path — `identified_by :number` — and
9
- # this is the one place that reads one. It follows the path and nothing else.
10
- #
11
- # What it replaced was `Value.identifier`, which opened a one-field value
12
- # object and took whatever was inside : that let `identified_by :number` pass
13
- # for an identity, with the runtime guessing which field had been meant. The
14
- # guess is gone. A declaration that names no field is now refused when the
15
- # bluebook loads (`an aggregate that is identified names a field`, `an entity
16
- # is known by a field`), so by the time anything is dispatched there is
17
- # always a path here to follow.
18
- #
19
- # Usage:
20
- #
21
- # Identity.scalar("number.value", account_number_value_object) # => "acct-1"
6
+ # Resolves an aggregate's or entity's identity string from a command payload.
22
7
  #
8
+ # Identity.scalar("number.value", account_number_value_object) # => "acct-1"
23
9
  module Identity
24
10
  module_function
25
11
 
26
- # Reads one key off `hash` by whichever of its Symbol/String spelling
27
- # is actually present, favoring the Symbol spelling.
28
- #
29
- # A hash read that decides which spelling of a key answers by
30
- # presence, never by `||` — a bare `||` treats a genuinely-held
31
- # `false` the same as an absent key and falls through to the other
32
- # spelling, landing on `nil` instead of the real, stored answer.
33
- #
34
- # @param hash [Hash] the hash to read, potentially keyed by either spelling
35
- # @param key [String, Symbol] the key to look up
36
- # @return [Object, nil] the value under `key.to_sym` if that key is present,
37
- # otherwise the value under `key` as given; nil if neither is a key of `hash`
12
+ # Reads `key` from `hash` by whichever Symbol/String spelling is present.
13
+ # Presence decides, not `||`, so a held `false` is not mistaken for absent.
38
14
  def hash_lookup(hash, key)
39
15
  sym = key.to_sym
40
16
  hash.key?(sym) ? hash[sym] : hash[key]
41
17
  end
42
18
 
43
- # Digs `path`'s fields out of `held`, past the head a caller already
44
- # consumed to look `held` up.
45
- #
46
- # The head names the attribute and is consumed by whoever looked the value
47
- # up; what is left is the walk down into it. A path with no fields to walk
48
- # — an aggregate that declares no identity and falls back to `id` — hands
49
- # back what it was given, because there is nothing declared to dig for.
50
- #
51
- # @param path [String, Symbol] the dotted identity path (`"number.value"`) or a
52
- # bare head (`:id`); only the segments after the head are walked
53
- # @param held [Object] the value already looked up for the path's head — a
54
- # `Runtime::Value`, a Hash, or a plain scalar
55
- # @return [Object, nil] `held` unchanged when `path` has no fields past its head;
56
- # otherwise the value found by walking those fields, or nil when a segment
57
- # is missing or the value being dug into is not a Hash
19
+ # Digs the fields of `path` out of `held`, past the head the caller consumed.
20
+ # A path with no fields past the head returns `held` unchanged.
58
21
  def scalar(path, held)
59
22
  _head, *fields = path.to_s.split(".")
60
23
  return held if fields.empty?
@@ -64,66 +27,22 @@ module Hecks
64
27
  end
65
28
  end
66
29
 
67
- # Derives `construct`'s identity string from `args`, joining every
68
- # declared identity part in declaration order.
69
- #
70
- # The identity is the join of its parts, in declaration order. Shared by
71
- # `CommandInterpreter` (an aggregate acting on itself) and
72
- # `EntityInterpreter` (a piece addressed through its aggregate) — a piece
73
- # declares an identity the same shape a head does, so it derives one the
74
- # same way. `construct` answers `identity_paths` / `identity_heads` /
75
- # `attribute` (an Aggregate or an Entity, either one) ; `value_owner`
76
- # answers for coercion (`Value.for_attribute`'s first argument), which for
77
- # an entity is its owning aggregate — an entity's value objects resolve
78
- # through the aggregate's namespace, not its own.
79
- #
80
- # A part the payload does not carry makes the whole identity unresolvable,
81
- # rather than half of one. Half an identity names nothing, and joining what
82
- # did arrive would silently name a different record on every dispatch — the
83
- # precise failure that minting an id caused, arrived at by another road.
84
- #
85
- # @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct whose
86
- # `identity_paths` are resolved
87
- # @param args [Hash{Symbol => Object}] the offered payload to resolve each
88
- # identity path against
89
- # @param value_owner [Bluebook::Aggregate, Bluebook::Entity] the construct whose
90
- # namespace a value-object identity part is coerced against; `construct` itself
91
- # unless the caller passes the owning aggregate for an entity
92
- # @return [String, nil] the joined identity string, or nil when `construct`
93
- # declares no identity path or any resolved part is nil or blank
30
+ # Joins every declared identity part of `construct` from `args`, in declaration order.
31
+ # `value_owner` is the aggregate that coerces value-object parts (an entity's owner).
32
+ # Returns nil unless every part resolves: half an identity would name a different record.
94
33
  def of(construct, args, value_owner: construct)
95
34
  paths = construct.identity_paths
96
35
  return nil if paths.empty?
97
36
 
98
37
  parts = paths.map { |path| from(construct, args, path, value_owner: value_owner) }
99
- # A blank part names nothing, the same as an absent one — an ID is a
100
- # scalar, and "" is not a fact about anything. Checking only `nil?`
101
- # would let a canonical text extracted as "" (an expression whose
102
- # source did not survive extraction) resolve to a real, empty-string
103
- # identity — a record addressable by an id no caller could have meant.
38
+ # A blank part names nothing, the same as an absent one.
104
39
  return nil if parts.any? { |part| part.nil? || (part.respond_to?(:empty?) && part.empty?) }
105
40
 
106
41
  Naming.identity(parts)
107
42
  end
108
43
 
109
- # Resolves one identity path (or a bare head such as `:id`) against `args`.
110
- #
111
- # A path digs into the value object that carries the identity, so what is
112
- # stored is the scalar inside it rather than the object serialised whole.
113
- #
114
- # @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct `key` is
115
- # checked against when it is a bare head
116
- # @param args [Hash{Symbol => Object}] the offered payload to resolve `key` against
117
- # @param key [String, Symbol, nil] the identity path to resolve, dotted
118
- # (`"number.value"`) or bare (`:id`); nil resolves to nil
119
- # @param value_owner [Bluebook::Aggregate, Bluebook::Entity] the construct whose
120
- # namespace a value-object head is coerced against; `construct` itself unless
121
- # the caller passes the owning aggregate for an entity
122
- # @return [String, Object, nil] the resolved identity text — a String once dug
123
- # through a dotted path or coerced through a declared value-object attribute;
124
- # the raw `args[key]` value, unconverted, when the bare head names no declared
125
- # identity attribute of `construct`; nil when `key` is nil, absent from `args`,
126
- # or a dotted walk finds nothing
44
+ # Resolves one identity path, dotted or a bare head such as `:id`, against `args`.
45
+ # A dotted path yields the scalar inside the value object, never the object serialised.
127
46
  def from(construct, args, key, value_owner: construct)
128
47
  return nil unless key
129
48
 
@@ -134,83 +53,28 @@ module Hecks
134
53
  unless rest.empty?
135
54
  held = args[head]
136
55
  held = held.to_h if held.respond_to?(:to_h)
137
- # **An ID is always a scalar**. The path says which field carries it, so a
138
- # caller may hand that field's value straight over — a string or a
139
- # number, never a serialised object. Only a value object that actually
140
- # arrived whole has to be opened.
56
+ # An ID is always a scalar: a caller may pass the carrying field's value directly.
141
57
  return held.to_s unless held.is_a?(Hash)
142
58
 
143
59
  return rest.reduce(held) { |h, f| h.is_a?(Hash) ? hash_lookup(h, f) : nil }&.to_s
144
60
  end
145
61
 
146
- # Coerced against the identity attribute only when the caller actually
147
- # named it. A saga addresses an aggregate by its correlation key, and
148
- # that key carries the id already resolved — coercing "w1" against a
149
- # WireReference asked the caller to pass fields for a value object they
150
- # never mentioned.
62
+ # Coerce only when the caller named the attribute; a saga's key is already resolved.
151
63
  attribute = construct.identity_heads.include?(head) ? construct.attribute(head) : nil
152
64
  raw = args[head]
153
65
  return raw unless attribute
154
66
 
155
- # **An ID is always a scalar** — same contract the dotted branch above
156
- # already keeps, just reached a different way here: a bare
157
- # (undotted) identity path names one of this construct's own
158
- # declared attributes directly, and when that attribute's type is
159
- # a value object (Translation's own compound `identified_by
160
- # :domain, :from, :to`, each typed `TranslationDomainName`/
161
- # `TranslationEraName`), `Value.for_attribute` coerces it into a
162
- # real single-field Value wrapper — never unwrapped before this,
163
- # so `Naming.identity`'s own plain `Array#join` (`Naming.identity`'s
164
- # own header: parts must already be scalars) fell through to
165
- # Ruby's default `Object#to_s`, leaking a raw, run-to-run-random
166
- # memory address (`#<Hecks::Runtime::Value:0x...>`) into
167
- # every refusal quoting this identity — found live via bin/fuzz on
168
- # the self-hosted "translation" domain (replay_is_deterministic:
169
- # the same address never repeats, so two replays of the
170
- # identical steps produced different histories the moment a
171
- # Translation went missing). `materialize_unwrapped` is the
172
- # same single-field-VO-recurses-to-its-bare-scalar helper
173
- # `read_model_interpreter.rb` already uses for exactly this
174
- # unwrap; passthrough for anything that isn't a Value at all.
67
+ # Unwrap the coerced value object so `to_s` never leaks an object address into an id.
175
68
  Value.materialize_unwrapped(Value.for_attribute(value_owner, attribute, raw)).to_s
176
69
  end
177
70
 
178
- # Renders `construct`'s identity paths for a refusal message.
179
- #
180
- # How an identity reads when the runtime has to name it in a refusal — the
181
- # paths as they were declared, so the message quotes the bluebook back.
182
- #
183
- # @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct whose
184
- # declared identity paths are rendered
185
- # @return [String] `construct`'s identity paths, comma-separated, exactly as declared
71
+ # Renders the declared identity paths of `construct` for a refusal message.
186
72
  def reading(construct)
187
73
  construct.identity_paths.join(", ")
188
74
  end
189
75
 
190
- # Resolves the best identity string available for `construct` from `args`,
191
- # without raising, for use as a lock key only.
192
- #
193
- # Best-effort, for a lock key only — `Runtime::AggregateLock`'s own
194
- # per-record striping needs some id to key on before dispatch has run
195
- # far enough to hydrate for real, so this walks the identical chain
196
- # `CommandInterpreter#hydrate_existing`/`#hydrate_prior_or_initial`
197
- # and `EntityInterpreter#parent` already use to locate the real
198
- # record — but wrapped to never raise. Choosing which Mutex to hold
199
- # must never itself become a crash. `nil` means "could not resolve
200
- # from the raw, pre-normalized payload this runs against" — the
201
- # caller locks by aggregate type alone in that case (coarser, still
202
- # correct, just less concurrent).
203
- #
204
- # @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct being
205
- # located for the lock key
206
- # @param args [Hash{Symbol => Object}] the raw, pre-normalized payload to resolve
207
- # an identity from
208
- # @param route [Runtime::Routing::Envelope, nil] the call's resolved routing
209
- # envelope, if any; its own `aggregate` identity is tried first
210
- # @param reference_key [Symbol, nil] the command's reference-key fallback,
211
- # tried last if given
212
- # @return [String, Object, nil] the best-effort identity to lock by, or nil
213
- # when nothing resolves (the caller then locks by aggregate type alone)
76
+ # Resolves a best-effort identity for `construct` to use as a lock key; never raises.
77
+ # Returns nil when nothing resolves, and the caller then locks by aggregate type alone.
214
78
  def best_effort(construct, args, route = nil, reference_key: nil)
215
79
  route&.aggregate ||
216
80
  of(construct, args) ||
@@ -4,66 +4,22 @@ require_relative "../ports/persistence/codec_boundary"
4
4
 
5
5
  module Hecks
6
6
  module Runtime
7
- # One aggregate (or entity) record's runtime state: the declared
8
- # attributes hydrated with defaults, plus the identity (`id`) and,
9
- # for a CAS-capable adapter, the optimistic-concurrency `version`
10
- # stamped on it. `[]`/`[]=`/method_missing give state-field access;
11
- # `to_h` is the wire/storage shape with `id` merged in last so a
12
- # same-named declared attribute can never clobber it.
7
+ # One aggregate or entity record's runtime state: declared attributes plus `id` and `version`.
8
+ # `to_h` is the wire/storage shape, with `id` merged last so an attribute named `id` loses.
13
9
  class Instance
14
10
  attr_reader :aggregate, :id
15
11
  attr_accessor :state
16
- # **Out-of-band adapter bookkeeping, not domain state** — the optimistic-
17
- # concurrency version a CAS-capable adapter (Postgres today) stamps
18
- # on a record it reads/writes, so a later `save` can assert "commit
19
- # only if nobody has written since". Deliberately absent from
20
- # `to_h`/`[]`/`[]=`/`method_missing` : a domain author never declares
21
- # this, a `given`/`ensures`/`invariant` can never read it, and no
22
- # adapter that doesn't understand it (Memory, Heki) ever sets it —
23
- # `nil` there just means "no CAS attempted", which is exactly what a
24
- # plain `save` already does. See docs/decisions/ (concurrency-control
25
- # ADR) for the full mechanism.
12
+ # Optimistic-concurrency version stamped by a CAS-capable adapter; not domain state.
13
+ # Left out of `to_h` and `[]` on purpose; nil means no CAS was attempted.
26
14
  attr_accessor :version
27
15
 
28
- # `args:` — the original command payload, offered only by a fresh
29
- # creation (`CommandInterpreter#hydrate_legacy_creation`/
30
- # `#hydrate_complete_state`/`#hydrate_prior_or_initial`, each already
31
- # holding it when they mint a brand-new record). See
32
- # `materialize_identity!` for why a composite identity needs it.
33
- #
34
- # `hydrate:` — on by default, and every existing caller keeps getting
35
- # exactly what it always got: `state` re-walked through
36
- # `hydrate_with_defaults` (declared defaults filled, every attribute
37
- # re-coerced through `Value.for_attribute`, an entity list's every
38
- # element rebuilt and re-validated). `false` is for exactly one
39
- # caller (`Adapters::Memory#build_instance`, judge-bootstrapping
40
- # only — see its own header) that already knows `state` needs none
41
- # of that: it is a shallow dup of an already-hydrated, already-
42
- # validated live `Instance`'s own state, not a raw value pulled off
43
- # a wire. Skipping the re-walk is what turns a `list_of` entity's Nth
44
- # save from O(N) (re-hydrating every element saved so far, for every
45
- # save) into O(1) — the quadratic cost `Adapters::Memory`'s own
46
- # header traces start to finish. `CodecBoundary.check_state!` still
47
- # runs either way ; only the re-hydration is skipped.
48
- #
49
- # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct this
50
- # record's state is declared by
51
- # @param id [String, nil] this record's identity; nil for a record whose identity
52
- # is not yet resolvable (e.g. an entity element hydrated before its own view exists)
53
- # @param state [Hash{Symbol => Object}, nil] the record's stored (or partial)
54
- # attribute values; nil for a brand-new record, hydrated entirely from declared
55
- # defaults
56
- # @param args [Hash{Symbol => Object}, nil] the original command payload, offered
57
- # only by a fresh creation; materializes a composite identity's own head
58
- # attributes
59
- # @param hydrate [Boolean] whether `state` is re-walked through
60
- # `hydrate_with_defaults`; false only for a caller that already knows `state` is
61
- # already hydrated and validated
16
+ # `args` is the creation payload, used only to fill a composite identity's heads.
17
+ # `hydrate: false` skips the default re-walk for an already-hydrated, validated state,
18
+ # keeping a `list_of` entity's Nth save O(1) rather than O(N).
62
19
  def initialize(aggregate:, id:, state: nil, args: nil, hydrate: true)
63
20
  @aggregate = aggregate
64
21
  @id = id
65
- # Inside a persistence adapter call this refuses undecoded stored
66
- # state (Ports::Persistence::CodecBoundary); everywhere else, no-op.
22
+ # Inside a persistence adapter call this refuses undecoded stored state.
67
23
  Ports::Persistence::CodecBoundary.check_state!(aggregate, state) if state
68
24
  @state = if !hydrate
69
25
  state || self.class.defaults(aggregate)
@@ -76,18 +32,7 @@ module Hecks
76
32
  materialize_identity!(args)
77
33
  end
78
34
 
79
- # Loading existing state runs the same default-fill a fresh instance
80
- # gets: an attribute the record predates — a newly-required field
81
- # with a declared default:, a list added since the record was
82
- # written — arrives filled instead of nil. Only declared defaults
83
- # fill in; an attribute with no default stays absent, exactly as
84
- # stored.
85
- #
86
- # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct whose
87
- # declared attributes and defaults `state` is hydrated against
88
- # @param state [Hash{Symbol => Object}] the raw stored (or partial) attribute values
89
- # @return [Hash{Symbol => Object}] `state` coerced through every declared attribute,
90
- # with any missing declared-default attribute filled in
35
+ # Hydrates `state` and fills declared defaults for attributes the record predates.
91
36
  def self.hydrate_with_defaults(aggregate, state)
92
37
  hydrated = Value.hydrate(aggregate, state)
93
38
  defaults(aggregate).each do |name, value|
@@ -96,39 +41,20 @@ module Hecks
96
41
  hydrated
97
42
  end
98
43
 
99
- # Builds a fresh record's starting state: every declared attribute's
100
- # default value, an empty frozen Array for a `list_of` attribute, and
101
- # the lifecycle field's declared starting value when `aggregate` has one.
102
- #
103
- # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct whose
104
- # declared attributes and lifecycle are read
105
- # @return [Hash{Symbol => Object}] the default state, keyed by attribute name
44
+ # Builds a fresh record's starting state: declared defaults, empty lists, lifecycle start.
106
45
  def self.defaults(aggregate)
107
46
  state = aggregate.attributes.to_h do |attr|
108
- # Frozen, like a list that has had something appended to it.
109
- # An untouched list is the easiest one to miss and the easiest
110
- # to mutate: nothing has replaced it yet, so a caller pushing
111
- # into it writes straight into the aggregate's own state.
47
+ # Frozen so pushing into an untouched list cannot write into the aggregate's state.
112
48
  [attr.name, attr.list? ? Freezer.deep([]) : default_for(aggregate, attr)]
113
49
  end
114
50
  state[aggregate.lifecycle.field.to_sym] = aggregate.lifecycle.default if aggregate.lifecycle
115
51
  state
116
52
  end
117
53
 
118
- # Resolves one attribute's default value: its own declared `default:`
119
- # if it has one, otherwise a value object built entirely from its own
120
- # members' defaults, when every member declares one.
121
- #
122
- # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct
123
- # `attribute` belongs to
124
- # @param attribute [Bluebook::Attribute] the attribute to resolve a default for
125
- # @return [Runtime::Value, Object, nil] the coerced default value; nil when
126
- # `attribute` declares no default and either names no value object or names
127
- # one with a member that itself declares no default
54
+ # Resolves an attribute's default, building a value object from its members' defaults.
128
55
  def self.default_for(aggregate, attribute)
129
56
  return Value.for_attribute(aggregate, attribute, attribute.default) unless attribute.default.nil?
130
- # An entity's members hydrate through the same path but an entity
131
- # declares no value objects of its own — nothing to default-build.
57
+ # An entity declares no value objects of its own.
132
58
  return nil unless aggregate.respond_to?(:value_object)
133
59
 
134
60
  value_object = aggregate.value_object(attribute.type)
@@ -138,23 +64,12 @@ module Hecks
138
64
  end
139
65
 
140
66
  # Reads one state field by name.
141
- #
142
- # @param name [String, Symbol] the declared attribute name to read
143
- # @return [Object, nil] the field's current value; nil if `name` is not a key
144
- # of `state`
145
67
  def [](name) = @state[name.to_sym]
146
68
 
147
69
  # Reports whether `state` holds a value for `name`.
148
- #
149
- # @param name [String, Symbol] the declared attribute name to check
150
- # @return [Boolean] true if `state` has a key for `name`
151
70
  def key?(name) = @state.key?(name.to_sym)
152
71
 
153
72
  # Writes one state field by name.
154
- #
155
- # @param name [String, Symbol] the declared attribute name to write
156
- # @param value [Object] the value to store
157
- # @return [Object] `value`, unchanged
158
73
  def []=(name, value)
159
74
  @state[name.to_sym] = value
160
75
  end
@@ -169,36 +84,10 @@ module Hecks
169
84
  @state.key?(name) || super
170
85
  end
171
86
 
172
- # Renders this record's wire/storage shape, state plus its identity.
173
- #
174
- # `id: @id` last, not first — see Facade::Handle#to_h's own comment
175
- # for the full story (the same fix, landed there first): an
176
- # aggregate free to declare its own attribute literally named `id`
177
- # (BurningManPrep's `Item`, `attribute :id, ItemId`) has that
178
- # attribute's own wrapped value object sitting in `@state[:id]` —
179
- # merging `@state` on top of `{ id: @id }` let it silently clobber
180
- # the correct bare identity. `@id` merged last always wins.
181
- #
182
- # @return [Hash{Symbol => Object}] `state`, with `id` merged in last
87
+ # State plus identity, `id` merged last so a declared attribute named `id` cannot clobber it.
183
88
  def to_h = @state.merge(id: @id)
184
89
 
185
- # Copies this record, deep enough that a mutation on the copy cannot
186
- # reach the original's own state.
187
- #
188
- # A copy a mutation may touch. Every adapter but Memory hands `find`
189
- # a freshly-decoded Instance already; Memory's holds the record it
190
- # eventually saves — the same state Hash, aliased. Before `ensures`
191
- # existed, nothing could refuse between apply_mutations and save, so
192
- # that aliasing was invisible: a dispatch either ran to completion or
193
- # raised before touching state at all. `ensures` is the first refusal
194
- # to sit after mutation, and it found the bug the moment it did — an
195
- # in-memory record left half-mutated by a dispatch that then refused.
196
- # `command_interpreter`/`entity_interpreter` hydrate an existing
197
- # record through this, never through the adapter's own return value
198
- # directly, so a refused ensures leaves the stored record untouched
199
- # regardless of which adapter is holding it.
200
- #
201
- # @return [Runtime::Instance] a copy of this record, with its own state Hash
90
+ # Copies this record with its own state Hash, so a refused dispatch spares the original.
202
91
  def dup
203
92
  copy = super
204
93
  copy.state = @state.dup
@@ -212,36 +101,15 @@ module Hecks
212
101
 
213
102
  private
214
103
 
215
- # M17 — a composite identity (`identity_heads.size > 1`, e.g.
216
- # `identified_by :branch_code, :box_number`) has no single
217
- # `identified_by` to fall back to `:id` for — `@aggregate.identified_by`
218
- # is nil the moment there is more than one head (`Behaviour::Identified
219
- # #derive_identity`), so the single-head branch below never runs for
220
- # it at all. Without this, a creating command that declares those heads
221
- # as ordinary attributes but doesn't also `sets` them (redundant with
222
- # the identity the command's own args already named) would persist
223
- # every head as nil — the id correctly naming the record, but the
224
- # record's own attributes forgetting what named it.
225
- #
226
- # Filled from `args`, never from splitting `@id` back apart — the
227
- # same reason the single-head branch below won't guess a multi-path
228
- # identifier from its joined string: `@id` is a display key, not a
229
- # reversible serialization, and a composite's own separator can
230
- # collide with a part's own text. `args` is only offered by a fresh
231
- # creation (`Instance.new`'s own `args:` comment); an existing record
232
- # read back from storage has no args to lean on, and doesn't need
233
- # one since a correctly-persisted record already carries its own
234
- # heads.
104
+ # A composite identity has no single head, so each head is filled from `args`.
105
+ # Never split `@id`: it is a display key and a part's text can contain the separator.
235
106
  def materialize_identity!(args = nil)
236
107
  return materialize_composite_identity!(args) if @aggregate.identity_heads.size > 1
237
108
 
238
109
  identity = @aggregate.identified_by || :id
239
110
  attribute = @aggregate.attribute(identity)
240
111
  return unless attribute && @state[identity].nil?
241
- # Several paths under one head mean the identifier is a display key,
242
- # not a reversible serialization of the structured value object. A
243
- # creating command supplies that object explicitly; persisted state
244
- # hydrates it from storage. Never guess by splitting the joined id.
112
+ # Several paths under one head make the id a display key, not reversible; never split it.
245
113
  return if @aggregate.identity_heads.one? && @aggregate.identity_paths.size > 1
246
114
 
247
115
  @state[identity] = Value.from_identifier(@aggregate, attribute, @id)
@@ -3,21 +3,9 @@ require_relative "aggregate_lock"
3
3
 
4
4
  module Hecks
5
5
  module Runtime
6
- # What every stepwise interpreter shares: the traced step, and the
7
- # coercion of a command's declared arguments. Two copies of each lived
8
- # in CommandInterpreter and EntityInterpreter, where they could only
9
- # ever drift.
6
+ # Shared by the stepwise interpreters: the traced step and coercion of declared arguments.
10
7
  module Interpreting
11
- # Ruby's module-inclusion hook. Gives `interpreter` its own `trace`
12
- # accessor, since a class including this module needs its own copy
13
- # rather than one shared across every interpreter.
14
- #
15
- # Each including interpreter gets its own `trace` — set by a spec to
16
- # observe dispatch order (Vocabulary::AggregateDispatchOrder and
17
- # Vocabulary::EntityDispatchOrder in language/bluebook/vocabulary.bluebook);
18
- # nil in production, always — one array push and a nil check per step
19
- # is the entire cost of leaving this in.
20
- #
8
+ # Gives each including interpreter its own `trace`, set by specs to observe dispatch order.
21
9
  # @param interpreter [Class] the class (`CommandInterpreter`, `EntityInterpreter`)
22
10
  # including this module
23
11
  # @return [void]
@@ -27,37 +15,15 @@ module Hecks
27
15
 
28
16
  private
29
17
 
30
- # Logged after the step's own work, so a step that wraps sub-steps logs
31
- # itself once everything inside it has already logged — trace order is
32
- # completion order, which is dispatch order.
18
+ # Logged after the step's work, so trace order is completion order.
33
19
  def step(name)
34
20
  result = yield
35
21
  self.class.trace << name if self.class.trace
36
22
  result
37
23
  end
38
24
 
39
- # Drives `DISPATCH_ORDER` (CommandInterpreter/EntityInterpreter, each
40
- # read off its own generated Vocabulary::*DispatchOrder table —
41
- # vocabulary.bluebook, via lib/hecks/vocabulary.rb) by `send`ing
42
- # each declared step name against the including interpreter's own
43
- # `step_<name>` handler, in declared order. Data drives that sequence,
44
- # rather than `call` spelling it out as a literal sequence of method
45
- # calls — tracing a real dispatch and comparing it to the declaration is
46
- # tautological once `call` mechanically follows the declaration; a
47
- # conditional step (assign_creation_attributes, advance_lifecycle) still
48
- # has to guard itself at the top of its own handler and skip tracing
49
- # when it does not fire, rather than the caller branching around it —
50
- # see CommandInterpreter#step_assign_creation_attributes.
51
- # The commit boundary. Every step up to `save` runs as before;
52
- # `save`, `emit`, and the outbox enqueue that follows them run
53
- # inside one `repository.transaction` — so the aggregate row, its
54
- # journal entry, the recorded event, and the outbox rows naming
55
- # who is owed a reaction commit together or not at all. No new
56
- # step is added to the vocabulary's dispatch order (the step list
57
- # is a pinned contract, `spec/vocabulary_conformance_spec.rb`);
58
- # the enqueue is a consequence of `emit`, not a step of its own.
59
- # A context without a repository (port operations — nothing to
60
- # save) or with no `:save` in its order runs plainly.
25
+ # Runs `order` by sending each `step_<name>` handler; `save` onward shares one transaction.
26
+ # The outbox enqueue rides on `emit`; the vocabulary's step list is a pinned contract.
61
27
  def run_dispatch_order(order, ctx)
62
28
  split = order.index(:save)
63
29
  return order.each { |name| send(:"step_#{name}", ctx) } unless split && ctx.respond_to?(:repository) && ctx.repository
@@ -69,34 +35,16 @@ module Hecks
69
35
  end
70
36
  end
71
37
 
72
- # Rows for the events this dispatch just emitted, written while
73
- # the save's transaction is still open. `nil` (no outbox on this
74
- # repository) tells the dispatcher to react directly, the
75
- # pre-outbox path; a dry run emits nothing and enqueues nothing.
38
+ # Enqueues outbox rows for the just-emitted events inside the save's transaction.
76
39
  def enqueue_outbox(ctx)
77
40
  return if ctx.dry_run || !ctx.respond_to?(:outbox_rows=)
78
41
 
79
42
  ctx.outbox_rows = @registry.outbox.enqueue(ctx.repository, Array(ctx.result), ctx.domain)
80
43
  end
81
44
 
82
- # The concurrency-control split — see docs/decisions/ (concurrency
83
- # control ADR) for the full mechanism. A repository that declares
84
- # `:optimistic_concurrency` (Postgres today) already closes the
85
- # lost-update gap itself, via `step_save`'s CAS + `#call`'s own
86
- # `StaleWrite` retry loop — an extra in-process lock here would be
87
- # pointless overhead, not incorrect, so it's skipped for clarity.
88
- # A repository that declares `:cross_process_lock` (PostgresEra —
89
- # ADR 0036) holds a real Postgres advisory lock for the whole
90
- # dispatch order instead: unlike Heki/Memory (confirmed
91
- # process-local, never a second process writing the same store),
92
- # PostgresEra's own tables can be dispatched against concurrently
93
- # by `rust/host` from a separate OS process, and an in-process
94
- # `Mutex` is invisible to that. Every other repository gets the
95
- # striped `Mutex` below, held for the whole dispatch-order run, so
96
- # a second thread's own hydrate can't start until the first
97
- # thread's save has landed. `lock_key_id` is best-effort
98
- # (`Identity.best_effort`) — `nil` still locks correctly, just
99
- # coarser (by aggregate type).
45
+ # Picks the isolation by capability: CAS needs none, a cross-process lock uses the
46
+ # repository's write lock, and the rest get a striped in-process Mutex (ADR 0036).
47
+ # A nil `lock_key_id` still locks correctly, just by aggregate type.
100
48
  def run_dispatch_order_with_isolation(order, ctx, lock_key_id:)
101
49
  capabilities = ctx.repository.capabilities
102
50
  if capabilities.include?(:optimistic_concurrency)
@@ -108,9 +56,7 @@ module Hecks
108
56
  end
109
57
  end
110
58
 
111
- # Every declared attribute present in the payload passes the reference
112
- # gate, then coercion — the same walk whether the command acts on an
113
- # aggregate or on one of its entity's elements.
59
+ # Passes each declared payload attribute through the reference gate, then coercion.
114
60
  def coerce_declared_arguments(aggregate, command, args)
115
61
  command.attributes.each_with_object(args.dup) do |attribute, normalized|
116
62
  next unless normalized.key?(attribute.name)
@@ -120,12 +66,7 @@ module Hecks
120
66
  end
121
67
  end
122
68
 
123
- # Coercion only — CommandInterpreter's own refuse_unknown_arguments/
124
- # refuse_absent_arguments are separate DISPATCH_ORDER steps now (the
125
- # declared vocabulary lists all three as flat, sequential members, not
126
- # one nesting the other two), and EntityInterpreter never had them here
127
- # at all (an entity inherits its aggregate's own gate). One shared
128
- # copy, rather than two identical ones that can only ever drift apart.
69
+ # Coercion only; the unknown/absent-argument refusals are separate dispatch steps.
129
70
  def normalize_args(aggregate, command, args)
130
71
  coerce_declared_arguments(aggregate, command, args)
131
72
  end