hecks 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (373) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +72 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +83 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
  7. data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
  9. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  10. data/lib/hecks/adapters/driven/heki.rb +106 -9
  11. data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
  12. data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
  13. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  15. data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
  16. data/lib/hecks/adapters/driven/lambda.rb +82 -35
  17. data/lib/hecks/adapters/driven/local_storage.rb +83 -10
  18. data/lib/hecks/adapters/driven/memory.rb +205 -9
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  24. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  25. data/lib/hecks/adapters/driven/prism.rb +50 -11
  26. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  31. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  32. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  33. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  34. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  35. data/lib/hecks/adapters/driven.rb +6 -4
  36. data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
  37. data/lib/hecks/behaviors/dsl.rb +60 -2
  38. data/lib/hecks/behaviors/expectations.rb +190 -29
  39. data/lib/hecks/behaviors/ir.rb +12 -1
  40. data/lib/hecks/behaviors/rspec.rb +9 -1
  41. data/lib/hecks/behaviors/runner.rb +21 -2
  42. data/lib/hecks/behaviors.rb +9 -1
  43. data/lib/hecks/bluebook/aggregate.rb +43 -13
  44. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
  45. data/lib/hecks/bluebook/assembly/build.rb +26 -5
  46. data/lib/hecks/bluebook/assembly/contract.rb +98 -23
  47. data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
  48. data/lib/hecks/bluebook/assembly/marks.rb +159 -30
  49. data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
  50. data/lib/hecks/bluebook/assembly.rb +32 -14
  51. data/lib/hecks/bluebook/attribute.rb +26 -12
  52. data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
  53. data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
  54. data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
  55. data/lib/hecks/bluebook/behaviour/command.rb +55 -25
  56. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
  57. data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
  58. data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
  59. data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
  60. data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
  61. data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
  62. data/lib/hecks/bluebook/behaviour/query.rb +6 -1
  63. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
  64. data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
  65. data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
  66. data/lib/hecks/bluebook/capabilities.rb +27 -0
  67. data/lib/hecks/bluebook/chapter.rb +51 -9
  68. data/lib/hecks/bluebook/command.rb +62 -17
  69. data/lib/hecks/bluebook/domain_port.rb +34 -9
  70. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  71. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  72. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
  73. data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
  74. data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
  75. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
  76. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
  77. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  78. data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
  79. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  80. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  81. data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
  82. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  83. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
  84. data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
  85. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  86. data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
  87. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  88. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  89. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
  90. data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
  91. data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
  92. data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
  93. data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
  94. data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
  95. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  96. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  97. data/lib/hecks/bluebook/entity.rb +40 -11
  98. data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
  99. data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
  100. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
  101. data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
  102. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
  103. data/lib/hecks/bluebook/expression/resolver.rb +369 -128
  104. data/lib/hecks/bluebook/hexagon.rb +35 -1
  105. data/lib/hecks/bluebook/lifecycle.rb +12 -1
  106. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
  107. data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
  108. data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
  109. data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
  110. data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
  111. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
  112. data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
  113. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
  114. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
  115. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
  116. data/lib/hecks/bluebook/meta_validator.rb +235 -139
  117. data/lib/hecks/bluebook/model_check.rb +434 -104
  118. data/lib/hecks/bluebook/pattern_subset.rb +32 -10
  119. data/lib/hecks/bluebook/policy.rb +15 -13
  120. data/lib/hecks/bluebook/process_manager.rb +27 -14
  121. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  122. data/lib/hecks/bluebook/project_loader.rb +40 -0
  123. data/lib/hecks/bluebook/project_register.rb +50 -6
  124. data/lib/hecks/bluebook/query.rb +31 -4
  125. data/lib/hecks/bluebook/read_model.rb +35 -15
  126. data/lib/hecks/bluebook/reference.rb +26 -13
  127. data/lib/hecks/bluebook/smoke_test.rb +46 -23
  128. data/lib/hecks/bluebook/synthesizer.rb +46 -12
  129. data/lib/hecks/bluebook/translation.rb +34 -5
  130. data/lib/hecks/bluebook/value_object.rb +29 -11
  131. data/lib/hecks/bluebook.rb +5 -6
  132. data/lib/hecks/codemod.rb +138 -50
  133. data/lib/hecks/construct.rb +21 -7
  134. data/lib/hecks/corpus.rb +438 -0
  135. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  136. data/lib/hecks/doc/reference.rb +200 -31
  137. data/lib/hecks/embryonaut_bluebook.rb +38 -15
  138. data/lib/hecks/facade/cli_door.rb +69 -10
  139. data/lib/hecks/facade/cli_runner.rb +105 -24
  140. data/lib/hecks/facade/command_request.rb +23 -0
  141. data/lib/hecks/facade/handle.rb +155 -35
  142. data/lib/hecks/facade/json_door.rb +106 -25
  143. data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
  144. data/lib/hecks/facade/surface/chapter.rb +26 -17
  145. data/lib/hecks/facade/surface.rb +16 -3
  146. data/lib/hecks/facade.rb +15 -4
  147. data/lib/hecks/forms/app.rb +46 -30
  148. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  149. data/lib/hecks/forms/field_renderer.rb +142 -6
  150. data/lib/hecks/forms/field_shape.rb +183 -19
  151. data/lib/hecks/forms/html.rb +51 -7
  152. data/lib/hecks/forms/index_renderer.rb +14 -2
  153. data/lib/hecks/forms/page.rb +14 -0
  154. data/lib/hecks/forms/params.rb +120 -23
  155. data/lib/hecks/forms/port_argument.rb +14 -2
  156. data/lib/hecks/forms/query_form_renderer.rb +65 -2
  157. data/lib/hecks/forms/record_renderer.rb +60 -2
  158. data/lib/hecks/forms/record_table.rb +28 -1
  159. data/lib/hecks/forms/reference_options.rb +24 -0
  160. data/lib/hecks/forms/value_object_shape.rb +13 -3
  161. data/lib/hecks/forms.rb +24 -4
  162. data/lib/hecks/fqn.rb +59 -1
  163. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  164. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  165. data/lib/hecks/framework/oidc.json +15 -0
  166. data/lib/hecks/framework.rb +79 -25
  167. data/lib/hecks/freezer.rb +27 -11
  168. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
  169. data/lib/hecks/fuzzing/combination_miner.rb +178 -0
  170. data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
  171. data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
  172. data/lib/hecks/fuzzing/differential.rb +192 -0
  173. data/lib/hecks/fuzzing/domain_generator.rb +871 -0
  174. data/lib/hecks/fuzzing/era_boundary.rb +68 -19
  175. data/lib/hecks/fuzzing/form_census.rb +121 -20
  176. data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
  177. data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
  178. data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
  179. data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
  180. data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
  181. data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
  182. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
  183. data/lib/hecks/fuzzing/properties/guards.rb +86 -42
  184. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
  185. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
  186. data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
  187. data/lib/hecks/fuzzing/properties/querying.rb +82 -28
  188. data/lib/hecks/fuzzing/properties.rb +84 -51
  189. data/lib/hecks/fuzzing/qa_settings.rb +164 -0
  190. data/lib/hecks/fuzzing/replay.rb +241 -133
  191. data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
  192. data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
  193. data/lib/hecks/fuzzing/self_consistency.rb +315 -137
  194. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
  195. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  196. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  197. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  198. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
  199. data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
  200. data/lib/hecks/fuzzing/shrinker.rb +309 -0
  201. data/lib/hecks/fuzzing/structural_skips.rb +37 -130
  202. data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
  203. data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
  204. data/lib/hecks/fuzzing/value_generator.rb +110 -22
  205. data/lib/hecks/fuzzing.rb +5 -0
  206. data/lib/hecks/grammar/evolve.rb +188 -12
  207. data/lib/hecks/grammar.rb +53 -7
  208. data/lib/hecks/ir.rb +51 -20
  209. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  210. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  211. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  212. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  213. data/lib/hecks/language/oidc.json +5 -0
  214. data/lib/hecks/literal.rb +41 -9
  215. data/lib/hecks/naming.rb +112 -31
  216. data/lib/hecks/ports/access_control.rb +53 -2
  217. data/lib/hecks/ports/agent/answers.rb +83 -6
  218. data/lib/hecks/ports/agent.rb +119 -35
  219. data/lib/hecks/ports/authentication.rb +44 -4
  220. data/lib/hecks/ports/authorization.rb +50 -11
  221. data/lib/hecks/ports/clock.rb +42 -23
  222. data/lib/hecks/ports/extraction.rb +16 -0
  223. data/lib/hecks/ports/identity_assignment.rb +23 -2
  224. data/lib/hecks/ports/identity_generation.rb +17 -3
  225. data/lib/hecks/ports/identity_resolution.rb +17 -1
  226. data/lib/hecks/ports/key_vault.port +6 -0
  227. data/lib/hecks/ports/key_vault.rb +58 -0
  228. data/lib/hecks/ports/loading.rb +4 -0
  229. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  230. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  231. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  232. data/lib/hecks/ports/persistence/execution.rb +4 -0
  233. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  234. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  235. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  236. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  237. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  238. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  239. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  250. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  251. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  252. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
  253. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  254. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  255. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  256. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  257. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  258. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
  259. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  260. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  261. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  262. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
  263. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
  264. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  265. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
  266. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  267. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  268. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  269. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  270. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  271. data/lib/hecks/ports/persistence.rb +36 -1
  272. data/lib/hecks/ports/projection.rb +61 -7
  273. data/lib/hecks/ports/query/in_memory.rb +41 -8
  274. data/lib/hecks/ports/query/ordering.rb +21 -6
  275. data/lib/hecks/ports/query.rb +35 -0
  276. data/lib/hecks/ports.rb +1 -0
  277. data/lib/hecks/projections/bootstrap_table.rb +147 -0
  278. data/lib/hecks/projections/diagrams.rb +315 -79
  279. data/lib/hecks/projections/glossary/html.rb +91 -3
  280. data/lib/hecks/projections/glossary/markdown.rb +50 -3
  281. data/lib/hecks/projections/glossary/mermaid.rb +49 -1
  282. data/lib/hecks/projections/glossary/sections.rb +1 -1
  283. data/lib/hecks/projections/glossary/sentences.rb +110 -5
  284. data/lib/hecks/projections/glossary.rb +173 -31
  285. data/lib/hecks/projections/ir.rb +1 -1
  286. data/lib/hecks/projections/model/deviations.rb +62 -17
  287. data/lib/hecks/projections/model.rb +75 -21
  288. data/lib/hecks/projections/oidc.rb +45 -8
  289. data/lib/hecks/projections/parser_table.rb +53 -12
  290. data/lib/hecks/projections/reference.rb +15 -3
  291. data/lib/hecks/projections/rust_vocabulary.rb +646 -0
  292. data/lib/hecks/projections/shape.rb +10 -2
  293. data/lib/hecks/projections/statements.rb +69 -22
  294. data/lib/hecks/projections/vocabulary.rb +26 -9
  295. data/lib/hecks/projections.rb +5 -3
  296. data/lib/hecks/projector/cli_projector.rb +245 -37
  297. data/lib/hecks/projector/docs_projector.rb +154 -28
  298. data/lib/hecks/projector/exporter.rb +104 -29
  299. data/lib/hecks/projector/ir_projector.rb +7 -1
  300. data/lib/hecks/projector/narrate_projector.rb +144 -23
  301. data/lib/hecks/projector/target.rb +42 -18
  302. data/lib/hecks/projector.rb +86 -18
  303. data/lib/hecks/query_ir.rb +94 -47
  304. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  305. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  306. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  307. data/lib/hecks/query_specification/common/null_policy.rb +61 -15
  308. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  309. data/lib/hecks/query_specification/common/options.rb +25 -0
  310. data/lib/hecks/query_specification/field_path.rb +69 -15
  311. data/lib/hecks/query_specification/hop_path.rb +57 -20
  312. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  313. data/lib/hecks/rendering.rb +9 -3
  314. data/lib/hecks/router/namespace_installer.rb +16 -3
  315. data/lib/hecks/router.rb +56 -1
  316. data/lib/hecks/runtime/aggregate_lock.rb +20 -11
  317. data/lib/hecks/runtime/boot_gates.rb +21 -3
  318. data/lib/hecks/runtime/caller.rb +40 -8
  319. data/lib/hecks/runtime/capability_graph.rb +13 -2
  320. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
  321. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
  322. data/lib/hecks/runtime/command_interpreter.rb +195 -113
  323. data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
  324. data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
  325. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  326. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  327. data/lib/hecks/runtime/command_rules/references.rb +145 -55
  328. data/lib/hecks/runtime/command_rules.rb +3 -0
  329. data/lib/hecks/runtime/dependency_planning.rb +56 -11
  330. data/lib/hecks/runtime/dispatcher.rb +263 -92
  331. data/lib/hecks/runtime/entity_element.rb +304 -67
  332. data/lib/hecks/runtime/entity_interpreter.rb +149 -88
  333. data/lib/hecks/runtime/errors.rb +37 -23
  334. data/lib/hecks/runtime/event.rb +15 -10
  335. data/lib/hecks/runtime/identity.rb +91 -23
  336. data/lib/hecks/runtime/instance.rb +103 -18
  337. data/lib/hecks/runtime/interpreting.rb +24 -16
  338. data/lib/hecks/runtime/invocation.rb +358 -0
  339. data/lib/hecks/runtime/loader.rb +105 -19
  340. data/lib/hecks/runtime/outbox.rb +164 -26
  341. data/lib/hecks/runtime/policy_interpreter.rb +73 -60
  342. data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
  343. data/lib/hecks/runtime/query_interpreter.rb +93 -74
  344. data/lib/hecks/runtime/reaction_invocation.rb +73 -28
  345. data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
  346. data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
  347. data/lib/hecks/runtime/reference_hop.rb +48 -6
  348. data/lib/hecks/runtime/refusal_wording.rb +142 -115
  349. data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
  350. data/lib/hecks/runtime/registry/verification.rb +153 -28
  351. data/lib/hecks/runtime/registry.rb +202 -32
  352. data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
  353. data/lib/hecks/runtime/routing.rb +36 -154
  354. data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
  355. data/lib/hecks/runtime/saga_interpreter.rb +90 -76
  356. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  357. data/lib/hecks/runtime/tenant_check.rb +33 -13
  358. data/lib/hecks/runtime/tenant_scope.rb +23 -5
  359. data/lib/hecks/runtime/value/admission.rb +75 -30
  360. data/lib/hecks/runtime/value/coercion.rb +555 -142
  361. data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
  362. data/lib/hecks/runtime/value.rb +71 -21
  363. data/lib/hecks/runtime.rb +39 -7
  364. data/lib/hecks/storehouse.rb +368 -72
  365. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  366. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  367. data/lib/hecks/version.rb +3 -3
  368. data/lib/hecks/vocabulary.rb +205 -4
  369. data/lib/hecks.rb +91 -11
  370. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
  371. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
  372. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
  373. metadata +28 -2
@@ -3,9 +3,9 @@ require_relative "value"
3
3
 
4
4
  module Hecks
5
5
  module Runtime
6
- # THE SCALAR AN IDENTITY PATH NAMES.
6
+ # **The scalar an identity path names**.
7
7
  #
8
- # An identity is DECLARED as a path — `identified_by :number` — and
8
+ # An identity is declared as a path — `identified_by :number` — and
9
9
  # this is the one place that reads one. It follows the path and nothing else.
10
10
  #
11
11
  # What it replaced was `Value.identifier`, which opened a one-field value
@@ -23,19 +23,38 @@ module Hecks
23
23
  module Identity
24
24
  module_function
25
25
 
26
+ # Reads one key off `hash` by whichever of its Symbol/String spelling
27
+ # is actually present, favoring the Symbol spelling.
28
+ #
26
29
  # A hash read that decides which spelling of a key answers by
27
- # PRESENCE, never by `||` — a bare `||` treats a genuinely-held
30
+ # presence, never by `||` — a bare `||` treats a genuinely-held
28
31
  # `false` the same as an absent key and falls through to the other
29
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`
30
38
  def hash_lookup(hash, key)
31
39
  sym = key.to_sym
32
40
  hash.key?(sym) ? hash[sym] : hash[key]
33
41
  end
34
42
 
35
- # The head names the ATTRIBUTE and is consumed by whoever looked the value
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
36
47
  # up; what is left is the walk down into it. A path with no fields to walk
37
48
  # — an aggregate that declares no identity and falls back to `id` — hands
38
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
39
58
  def scalar(path, held)
40
59
  _head, *fields = path.to_s.split(".")
41
60
  return held if fields.empty?
@@ -45,37 +64,66 @@ module Hecks
45
64
  end
46
65
  end
47
66
 
48
- # THE IDENTITY IS THE JOIN OF ITS PARTS, in declaration order. Shared by
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
49
71
  # `CommandInterpreter` (an aggregate acting on itself) and
50
72
  # `EntityInterpreter` (a piece addressed through its aggregate) — a piece
51
73
  # declares an identity the same shape a head does, so it derives one the
52
74
  # same way. `construct` answers `identity_paths` / `identity_heads` /
53
75
  # `attribute` (an Aggregate or an Entity, either one) ; `value_owner`
54
76
  # answers for coercion (`Value.for_attribute`'s first argument), which for
55
- # an entity is its OWNING aggregate — an entity's value objects resolve
77
+ # an entity is its owning aggregate — an entity's value objects resolve
56
78
  # through the aggregate's namespace, not its own.
57
79
  #
58
- # A part the payload does not carry makes the WHOLE identity unresolvable,
80
+ # A part the payload does not carry makes the whole identity unresolvable,
59
81
  # rather than half of one. Half an identity names nothing, and joining what
60
82
  # did arrive would silently name a different record on every dispatch — the
61
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
62
94
  def of(construct, args, value_owner: construct)
63
95
  paths = construct.identity_paths
64
96
  return nil if paths.empty?
65
97
 
66
98
  parts = paths.map { |path| from(construct, args, path, value_owner: value_owner) }
67
- # A BLANK PART NAMES NOTHING, the same as an ABSENT one — AN ID IS A
68
- # SCALAR, and "" is not a fact about anything. This used to check only
69
- # `nil?`, so a canonical text extracted as "" (an expression whose
70
- # source did not survive extraction) resolved to a REAL, empty-string
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
71
103
  # identity — a record addressable by an id no caller could have meant.
72
104
  return nil if parts.any? { |part| part.nil? || (part.respond_to?(:empty?) && part.empty?) }
73
105
 
74
106
  Naming.identity(parts)
75
107
  end
76
108
 
109
+ # Resolves one identity path (or a bare head such as `:id`) against `args`.
110
+ #
77
111
  # A path digs into the value object that carries the identity, so what is
78
- # stored is the SCALAR inside it rather than the object serialised whole.
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
79
127
  def from(construct, args, key, value_owner: construct)
80
128
  return nil unless key
81
129
 
@@ -86,7 +134,7 @@ module Hecks
86
134
  unless rest.empty?
87
135
  held = args[head]
88
136
  held = held.to_h if held.respond_to?(:to_h)
89
- # AN ID IS ALWAYS A SCALAR. The path says WHICH FIELD carries it, so a
137
+ # **An ID is always a scalar**. The path says which field carries it, so a
90
138
  # caller may hand that field's value straight over — a string or a
91
139
  # number, never a serialised object. Only a value object that actually
92
140
  # arrived whole has to be opened.
@@ -95,18 +143,18 @@ module Hecks
95
143
  return rest.reduce(held) { |h, f| h.is_a?(Hash) ? hash_lookup(h, f) : nil }&.to_s
96
144
  end
97
145
 
98
- # Coerced against the identity ATTRIBUTE only when the caller actually
146
+ # Coerced against the identity attribute only when the caller actually
99
147
  # named it. A saga addresses an aggregate by its correlation key, and
100
- # that key carries the id ALREADY RESOLVED — coercing "w1" against a
148
+ # that key carries the id already resolved — coercing "w1" against a
101
149
  # WireReference asked the caller to pass fields for a value object they
102
150
  # never mentioned.
103
151
  attribute = construct.identity_heads.include?(head) ? construct.attribute(head) : nil
104
152
  raw = args[head]
105
153
  return raw unless attribute
106
154
 
107
- # AN ID IS ALWAYS A SCALAR — same contract the dotted branch above
108
- # already keeps, just reached a different way here: a BARE
109
- # (undotted) identity path names one of THIS construct's own
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
110
158
  # declared attributes directly, and when that attribute's type is
111
159
  # a value object (Translation's own compound `identified_by
112
160
  # :domain, :from, :to`, each typed `TranslationDomainName`/
@@ -118,23 +166,32 @@ module Hecks
118
166
  # memory address (`#<Hecks::Runtime::Value:0x...>`) into
119
167
  # every refusal quoting this identity — found live via bin/fuzz on
120
168
  # the self-hosted "translation" domain (replay_is_deterministic:
121
- # the SAME address never repeats, so two replays of the
169
+ # the same address never repeats, so two replays of the
122
170
  # identical steps produced different histories the moment a
123
171
  # Translation went missing). `materialize_unwrapped` is the
124
- # SAME single-field-VO-recurses-to-its-bare-scalar helper
172
+ # same single-field-VO-recurses-to-its-bare-scalar helper
125
173
  # `read_model_interpreter.rb` already uses for exactly this
126
174
  # unwrap; passthrough for anything that isn't a Value at all.
127
175
  Value.materialize_unwrapped(Value.for_attribute(value_owner, attribute, raw)).to_s
128
176
  end
129
177
 
130
- # How an identity READS when the runtime has to name it in a refusal — the
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
131
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
132
186
  def reading(construct)
133
187
  construct.identity_paths.join(", ")
134
188
  end
135
189
 
136
- # BEST-EFFORT, FOR A LOCK KEY ONLY — `Runtime::AggregateLock`'s own
137
- # per-record striping needs SOME id to key on before dispatch has run
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
138
195
  # far enough to hydrate for real, so this walks the identical chain
139
196
  # `CommandInterpreter#hydrate_existing`/`#hydrate_prior_or_initial`
140
197
  # and `EntityInterpreter#parent` already use to locate the real
@@ -143,6 +200,17 @@ module Hecks
143
200
  # from the raw, pre-normalized payload this runs against" — the
144
201
  # caller locks by aggregate type alone in that case (coarser, still
145
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)
146
214
  def best_effort(construct, args, route = nil, reference_key: nil)
147
215
  route&.aggregate ||
148
216
  of(construct, args) ||
@@ -1,5 +1,6 @@
1
1
  require_relative "value"
2
2
  require_relative "identity"
3
+ require_relative "../ports/persistence/codec_boundary"
3
4
 
4
5
  module Hecks
5
6
  module Runtime
@@ -12,7 +13,7 @@ module Hecks
12
13
  class Instance
13
14
  attr_reader :aggregate, :id
14
15
  attr_accessor :state
15
- # OUT-OF-BAND ADAPTER BOOKKEEPING, NOT DOMAIN STATE — the optimistic-
16
+ # **Out-of-band adapter bookkeeping, not domain state** — the optimistic-
16
17
  # concurrency version a CAS-capable adapter (Postgres today) stamps
17
18
  # on a record it reads/writes, so a later `save` can assert "commit
18
19
  # only if nobody has written since". Deliberately absent from
@@ -24,16 +25,54 @@ module Hecks
24
25
  # ADR) for the full mechanism.
25
26
  attr_accessor :version
26
27
 
27
- # `args:` — THE ORIGINAL COMMAND PAYLOAD, offered only by a fresh
28
+ # `args:` — the original command payload, offered only by a fresh
28
29
  # creation (`CommandInterpreter#hydrate_legacy_creation`/
29
30
  # `#hydrate_complete_state`/`#hydrate_prior_or_initial`, each already
30
31
  # holding it when they mint a brand-new record). See
31
32
  # `materialize_identity!` for why a composite identity needs it.
32
- def initialize(aggregate:, id:, state: nil, args: nil)
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
62
+ def initialize(aggregate:, id:, state: nil, args: nil, hydrate: true)
33
63
  @aggregate = aggregate
34
64
  @id = id
35
- @state = state ? self.class.hydrate_with_defaults(aggregate, state) : self.class.defaults(aggregate)
36
- @version = nil
65
+ # Inside a persistence adapter call this refuses undecoded stored
66
+ # state (Ports::Persistence::CodecBoundary); everywhere else, no-op.
67
+ Ports::Persistence::CodecBoundary.check_state!(aggregate, state) if state
68
+ @state = if !hydrate
69
+ state || self.class.defaults(aggregate)
70
+ elsif state
71
+ self.class.hydrate_with_defaults(aggregate, state)
72
+ else
73
+ self.class.defaults(aggregate)
74
+ end
75
+ @version = nil
37
76
  materialize_identity!(args)
38
77
  end
39
78
 
@@ -43,6 +82,12 @@ module Hecks
43
82
  # written — arrives filled instead of nil. Only declared defaults
44
83
  # fill in; an attribute with no default stays absent, exactly as
45
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
46
91
  def self.hydrate_with_defaults(aggregate, state)
47
92
  hydrated = Value.hydrate(aggregate, state)
48
93
  defaults(aggregate).each do |name, value|
@@ -51,9 +96,16 @@ module Hecks
51
96
  hydrated
52
97
  end
53
98
 
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
54
106
  def self.defaults(aggregate)
55
107
  state = aggregate.attributes.to_h do |attr|
56
- # FROZEN, like a list that has had something appended to it.
108
+ # Frozen, like a list that has had something appended to it.
57
109
  # An untouched list is the easiest one to miss and the easiest
58
110
  # to mutate: nothing has replaced it yet, so a caller pushing
59
111
  # into it writes straight into the aggregate's own state.
@@ -63,6 +115,16 @@ module Hecks
63
115
  state
64
116
  end
65
117
 
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
66
128
  def self.default_for(aggregate, attribute)
67
129
  return Value.for_attribute(aggregate, attribute, attribute.default) unless attribute.default.nil?
68
130
  # An entity's members hydrate through the same path but an entity
@@ -75,10 +137,24 @@ module Hecks
75
137
  Value.build(value_object, {}, aggregate)
76
138
  end
77
139
 
140
+ # 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`
78
145
  def [](name) = @state[name.to_sym]
79
146
 
147
+ # 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`
80
151
  def key?(name) = @state.key?(name.to_sym)
81
152
 
153
+ # 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
82
158
  def []=(name, value)
83
159
  @state[name.to_sym] = value
84
160
  end
@@ -93,27 +169,36 @@ module Hecks
93
169
  @state.key?(name) || super
94
170
  end
95
171
 
96
- # `id: @id` LAST, not first — see Facade::Handle#to_h's own comment
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
97
175
  # for the full story (the same fix, landed there first): an
98
176
  # aggregate free to declare its own attribute literally named `id`
99
177
  # (BurningManPrep's `Item`, `attribute :id, ItemId`) has that
100
178
  # attribute's own wrapped value object sitting in `@state[:id]` —
101
179
  # merging `@state` on top of `{ id: @id }` let it silently clobber
102
180
  # the correct bare identity. `@id` merged last always wins.
181
+ #
182
+ # @return [Hash{Symbol => Object}] `state`, with `id` merged in last
103
183
  def to_h = @state.merge(id: @id)
104
184
 
105
- # A COPY A MUTATION MAY TOUCH. Every adapter but Memory hands `find`
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`
106
189
  # a freshly-decoded Instance already; Memory's holds the record it
107
- # eventually saves — the SAME state Hash, aliased. Before `ensures`
190
+ # eventually saves — the same state Hash, aliased. Before `ensures`
108
191
  # existed, nothing could refuse between apply_mutations and save, so
109
192
  # that aliasing was invisible: a dispatch either ran to completion or
110
193
  # raised before touching state at all. `ensures` is the first refusal
111
- # to sit AFTER mutation, and it found the bug the moment it did — an
194
+ # to sit after mutation, and it found the bug the moment it did — an
112
195
  # in-memory record left half-mutated by a dispatch that then refused.
113
- # `command_interpreter`/`entity_interpreter` hydrate an EXISTING
196
+ # `command_interpreter`/`entity_interpreter` hydrate an existing
114
197
  # record through this, never through the adapter's own return value
115
198
  # directly, so a refused ensures leaves the stored record untouched
116
199
  # regardless of which adapter is holding it.
200
+ #
201
+ # @return [Runtime::Instance] a copy of this record, with its own state Hash
117
202
  def dup
118
203
  copy = super
119
204
  copy.state = @state.dup
@@ -127,22 +212,22 @@ module Hecks
127
212
 
128
213
  private
129
214
 
130
- # M17 — a COMPOSITE identity (`identity_heads.size > 1`, e.g.
215
+ # M17 — a composite identity (`identity_heads.size > 1`, e.g.
131
216
  # `identified_by :branch_code, :box_number`) has no single
132
217
  # `identified_by` to fall back to `:id` for — `@aggregate.identified_by`
133
218
  # is nil the moment there is more than one head (`Behaviour::Identified
134
219
  # #derive_identity`), so the single-head branch below never runs for
135
- # it at all. A creating command that declares those heads as ordinary
136
- # attributes but doesn't ALSO `sets` them (redundant with the identity
137
- # the command's own args already named) used to persist every head as
138
- # nil — the id correctly named the record, but the record's own
139
- # attributes forgot what named it.
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.
140
225
  #
141
226
  # Filled from `args`, never from splitting `@id` back apart — the
142
227
  # same reason the single-head branch below won't guess a multi-path
143
228
  # identifier from its joined string: `@id` is a display key, not a
144
229
  # reversible serialization, and a composite's own separator can
145
- # collide with a part's own text. `args` is only offered by a FRESH
230
+ # collide with a part's own text. `args` is only offered by a fresh
146
231
  # creation (`Instance.new`'s own `args:` comment); an existing record
147
232
  # read back from storage has no args to lean on, and doesn't need
148
233
  # one since a correctly-persisted record already carries its own
@@ -8,18 +8,26 @@ module Hecks
8
8
  # in CommandInterpreter and EntityInterpreter, where they could only
9
9
  # ever drift.
10
10
  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
+ #
11
15
  # Each including interpreter gets its own `trace` — set by a spec to
12
16
  # observe dispatch order (Vocabulary::AggregateDispatchOrder and
13
17
  # Vocabulary::EntityDispatchOrder in language/bluebook/vocabulary.bluebook);
14
18
  # nil in production, always — one array push and a nil check per step
15
19
  # is the entire cost of leaving this in.
20
+ #
21
+ # @param interpreter [Class] the class (`CommandInterpreter`, `EntityInterpreter`)
22
+ # including this module
23
+ # @return [void]
16
24
  def self.included(interpreter)
17
25
  interpreter.singleton_class.attr_accessor :trace
18
26
  end
19
27
 
20
28
  private
21
29
 
22
- # Logged AFTER the step's own work, so a step that wraps sub-steps logs
30
+ # Logged after the step's own work, so a step that wraps sub-steps logs
23
31
  # itself once everything inside it has already logged — trace order is
24
32
  # completion order, which is dispatch order.
25
33
  def step(name)
@@ -28,21 +36,21 @@ module Hecks
28
36
  result
29
37
  end
30
38
 
31
- # DRIVES `DISPATCH_ORDER` (CommandInterpreter/EntityInterpreter, each
32
- # mirroring its own Vocabulary::*DispatchOrder — vocabulary.bluebook,
33
- # held equal to it by spec/vocabulary_conformance_spec.rb) by `send`ing
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
34
42
  # each declared step name against the including interpreter's own
35
- # `step_<name>` handler, in declared order. What used to be `call`'s own
36
- # literal sequence of method calls is now DATA driving that sequence —
37
- # tracing a real dispatch and comparing it to the declaration is
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
38
46
  # tautological once `call` mechanically follows the declaration; a
39
47
  # conditional step (assign_creation_attributes, advance_lifecycle) still
40
- # has to guard ITSELF at the top of its own handler and skip tracing
48
+ # has to guard itself at the top of its own handler and skip tracing
41
49
  # when it does not fire, rather than the caller branching around it —
42
50
  # see CommandInterpreter#step_assign_creation_attributes.
43
- # THE COMMIT BOUNDARY. Every step up to `save` runs as before;
51
+ # The commit boundary. Every step up to `save` runs as before;
44
52
  # `save`, `emit`, and the outbox enqueue that follows them run
45
- # inside ONE `repository.transaction` — so the aggregate row, its
53
+ # inside one `repository.transaction` — so the aggregate row, its
46
54
  # journal entry, the recorded event, and the outbox rows naming
47
55
  # who is owed a reaction commit together or not at all. No new
48
56
  # step is added to the vocabulary's dispatch order (the step list
@@ -71,20 +79,20 @@ module Hecks
71
79
  ctx.outbox_rows = @registry.outbox.enqueue(ctx.repository, Array(ctx.result), ctx.domain)
72
80
  end
73
81
 
74
- # THE CONCURRENCY-CONTROL SPLIT — see docs/decisions/ (concurrency
82
+ # The concurrency-control split — see docs/decisions/ (concurrency
75
83
  # control ADR) for the full mechanism. A repository that declares
76
84
  # `:optimistic_concurrency` (Postgres today) already closes the
77
85
  # lost-update gap itself, via `step_save`'s CAS + `#call`'s own
78
86
  # `StaleWrite` retry loop — an extra in-process lock here would be
79
87
  # pointless overhead, not incorrect, so it's skipped for clarity.
80
88
  # A repository that declares `:cross_process_lock` (PostgresEra —
81
- # ADR 0036) holds a REAL Postgres advisory lock for the whole
89
+ # ADR 0036) holds a real Postgres advisory lock for the whole
82
90
  # dispatch order instead: unlike Heki/Memory (confirmed
83
91
  # process-local, never a second process writing the same store),
84
92
  # PostgresEra's own tables can be dispatched against concurrently
85
93
  # by `rust/host` from a separate OS process, and an in-process
86
- # `Mutex` is invisible to that. Every OTHER repository gets the
87
- # striped `Mutex` below, held for the WHOLE dispatch-order run, so
94
+ # `Mutex` is invisible to that. Every other repository gets the
95
+ # striped `Mutex` below, held for the whole dispatch-order run, so
88
96
  # a second thread's own hydrate can't start until the first
89
97
  # thread's save has landed. `lock_key_id` is best-effort
90
98
  # (`Identity.best_effort`) — `nil` still locks correctly, just
@@ -116,8 +124,8 @@ module Hecks
116
124
  # refuse_absent_arguments are separate DISPATCH_ORDER steps now (the
117
125
  # declared vocabulary lists all three as flat, sequential members, not
118
126
  # one nesting the other two), and EntityInterpreter never had them here
119
- # at all (an entity inherits its aggregate's own gate). One copy,
120
- # shared, rather than the two identical ones that used to drift.
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.
121
129
  def normalize_args(aggregate, command, args)
122
130
  coerce_declared_arguments(aggregate, command, args)
123
131
  end