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
@@ -7,7 +7,7 @@ require_relative "../../ports/query/in_memory"
7
7
  module Hecks
8
8
  module Runtime
9
9
  class CommandRules
10
- # A reference must point at something that EXISTS.
10
+ # A reference must point at something that exists.
11
11
  #
12
12
  # `reference_to Customer` is the one guarantee an aggregate reference is
13
13
  # for, and it was declared 14 times across banking and enforced nowhere :
@@ -16,13 +16,22 @@ module Hecks
16
16
  # no corpus step ever passed a dangling reference.
17
17
  module References
18
18
  # Resolved here rather than in coercion because coercion is pure — it
19
- # holds no repository. A reference INTO ANOTHER DOMAIN is left alone : a
19
+ # holds no repository. A reference into another domain is left alone : a
20
20
  # cross-domain target may legitimately not be loaded, which is the same
21
21
  # reading `across` policies already get.
22
22
  #
23
23
  # Shared by CommandInterpreter and EntityInterpreter — an entity command
24
24
  # can declare a reference-typed attribute the same way an aggregate
25
25
  # command can, even though nothing in the real corpus does yet.
26
+ #
27
+ # @param domain [String, Symbol] the domain the referenced aggregate is
28
+ # resolved in
29
+ # @param command [Class] the command or port-operation class (`Bluebook::Command`
30
+ # or `PortOperation` subclass) whose reference-typed attributes are checked
31
+ # @param args [Hash{Symbol => Object}] the offered, already-coerced argument values
32
+ # @return [void]
33
+ # @raise [Runtime::NotFound] if a reference-typed argument names an identity that
34
+ # does not exist in its target aggregate's repository
26
35
  def resolve_references(domain, command, args)
27
36
  command.attributes.each do |attribute|
28
37
  next unless attribute.reference?
@@ -41,6 +50,20 @@ module Hecks
41
50
  # Structural references are checked again against the settled state.
42
51
  # This is what makes a `has_many` declared on an aggregate honest even
43
52
  # when a command supplies its list through an ordinary typed argument.
53
+ #
54
+ # @param domain [String, Symbol] the domain the referenced aggregate is
55
+ # resolved in
56
+ # @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct
57
+ # whose reference-typed attributes are checked against `state`
58
+ # @param state [Hash{Symbol => Object}] the settled, post-mutation state to
59
+ # check every reference and relationship cardinality against
60
+ # @return [void]
61
+ # @raise [Runtime::TypeMismatch] if a required `has_one`/`belongs_to`
62
+ # relationship holds nil
63
+ # @raise [Runtime::NotFound] if a reference-typed field names an identity
64
+ # that does not exist in its target aggregate's repository
65
+ # @raise [Runtime::Unauthorized] if a referenced record belongs to a
66
+ # different tenant than `construct`'s own record
44
67
  def resolve_state_references(domain, construct, state)
45
68
  own_tenant_field = tenant_field_for(construct)
46
69
 
@@ -73,6 +96,14 @@ module Hecks
73
96
  # checking only command arguments would let a required relationship be
74
97
  # persisted as nil. `has_many` admits zero members, so its empty list is
75
98
  # already a valid cardinality and needs no presence refusal.
99
+ #
100
+ # @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct
101
+ # `attribute` is declared on, named in a refusal
102
+ # @param attribute [Bluebook::Attribute] the reference-typed attribute to check
103
+ # @param held [Object, nil] the field's settled value
104
+ # @return [void]
105
+ # @raise [Runtime::TypeMismatch] if `attribute` declares a required (non-optional,
106
+ # non-list) `has_one`/`belongs_to` relationship and `held` is nil
76
107
  def validate_relationship_cardinality(construct, attribute, held)
77
108
  return if attribute.relationship.nil? || attribute.list?
78
109
  return unless held.nil? && !attribute.optional?
@@ -83,16 +114,32 @@ module Hecks
83
114
  "#{attribute.type.target_name} identity, got nil"
84
115
  end
85
116
 
86
- # The reference RESOLVES itself — through the chapter's own IR, so the
87
- # bluebook's declared heads are the index. This used to regex the target's
88
- # name out of "Reference<Customer>" and then search
89
- # `registry.bluebook(domain).aggregates` for it — and later reached the
90
- # target through Ruby's constant tree, a class thrown away for its `.ir`
91
- # the moment it was found.
117
+ # The reference resolves itself — through the chapter's own IR, so the
118
+ # bluebook's declared heads are the index, rather than regexing the
119
+ # target's name out of "Reference<Customer>" and searching
120
+ # `registry.bluebook(domain).aggregates` for it, or reaching the
121
+ # target through Ruby's constant tree, a class thrown away for its
122
+ # `.ir` the moment it was found.
123
+ #
124
+ # @param attribute [Bluebook::Attribute] the reference-typed attribute to resolve
125
+ # @return [Bluebook::Aggregate, nil] the target aggregate the reference names,
126
+ # or nil if it cannot be resolved
127
+ # @raise [Bluebook::DSL::Malformed] if the reference does not know which
128
+ # aggregate declares it
92
129
  def referenced_aggregate(attribute)
93
130
  attribute.type.resolve
94
131
  end
95
132
 
133
+ # Refuses a reference-typed value whose target identity does not exist.
134
+ #
135
+ # @param domain [String, Symbol] the domain `target` is resolved in
136
+ # @param target [Bluebook::Aggregate] the referenced aggregate
137
+ # @param held [Object, Array<Object>] the offered value: a single reference,
138
+ # or, when `list:` is true, an Array of them
139
+ # @param list [Boolean] whether `held` is list-shaped
140
+ # @return [void]
141
+ # @raise [Runtime::NotFound] if any referenced identity does not exist in
142
+ # `target`'s repository
96
143
  def validate_reference_values(domain, target, held, list:)
97
144
  values = list ? Array(held) : [held]
98
145
  values.each do |value|
@@ -101,37 +148,53 @@ module Hecks
101
148
  next if @registry.repository(domain, target).find(key)
102
149
 
103
150
  raise NotFound,
104
- RefusalWording.render("NotFound", "reference_target_missing",
105
- target: target.name, heads: target.identity_heads.join(", "),
106
- key: key.inspect)
151
+ RefusalWording.render_site("NotFound", "reference_target_missing",
152
+ target: target.name, heads: target.identity_heads.join(", "),
153
+ key: key)
107
154
  end
108
155
  end
109
156
 
110
- # ANGLE-8's OWN WRITE-SIDE HALF of `TenantScope.apply` (runtime/
111
- # tenant_scope.rb) — the QUERY-side mechanism this mirrors. That
157
+ # Angle-8's own write-side half of `TenantScope.apply` (runtime/
158
+ # tenant_scope.rb) — the query-side mechanism this mirrors. That
112
159
  # module turns a declared `authorize policy, tenant: :field` into a
113
- # synthetic where-clause checked against the CALLER's own supplied
160
+ # synthetic where-clause checked against the caller's own supplied
114
161
  # tenant argument; there is no caller-identity/session system this
115
- # runtime has to check a WRITE's caller against (TenantScope's own
162
+ # runtime has to check a write's caller against (TenantScope's own
116
163
  # header names that as a separate, still-open gap), so this checks
117
- # the one thing that IS available without one: whether the record
164
+ # the one thing that is available without one: whether the record
118
165
  # being written and the record it references agree about which
119
166
  # tenant they belong to. `lib/hecks/fuzzing/properties/guards.rb`'s
120
167
  # `commands_respect_tenant_scope` states the identical claim,
121
168
  # read off `history[:instances]` after the fact — this is what
122
- # makes that claim hold BY CONSTRUCTION (a refused write is never
169
+ # makes that claim hold by construction (a refused write is never
123
170
  # stored) rather than merely checked for regression.
124
171
  #
125
172
  # Hooked into `resolve_state_references` rather than a new
126
173
  # DISPATCH_ORDER step deliberately: that method already walks
127
- # every `reference_to`-typed attribute against the SETTLED,
174
+ # every `reference_to`-typed attribute against the settled,
128
175
  # post-mutation state (the same moment `commands_respect_tenant_
129
176
  # scope` itself inspects), already resolves the referenced record
130
- # through the repository right above, and already runs from BOTH
177
+ # through the repository right above, and already runs from both
131
178
  # `CommandInterpreter#step_save` and `EntityInterpreter#step_save`
132
179
  # — one change, both interpreters covered, no new vocabulary step
133
180
  # to keep in sync with `Vocabulary::AggregateDispatchOrder`/
134
181
  # `EntityDispatchOrder`.
182
+ #
183
+ # @param domain [String, Symbol] the domain `target` is resolved in
184
+ # @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct
185
+ # `attribute` is declared on, named in a refusal
186
+ # @param attribute [Bluebook::Attribute] the reference-typed attribute being
187
+ # checked
188
+ # @param target [Bluebook::Aggregate] the referenced aggregate
189
+ # @param held [Object, Array<Object>] the offered value: a single reference,
190
+ # or, when `attribute.list?` is true, an Array of them
191
+ # @param state [Hash{Symbol => Object}] `construct`'s own settled state, read
192
+ # for its own tenant field's value
193
+ # @param own_tenant_field [Symbol, nil] `construct`'s own declared tenant field;
194
+ # a no-op if nil or absent from `state`
195
+ # @return [void]
196
+ # @raise [Runtime::Unauthorized] if a referenced record's own tenant field
197
+ # disagrees with `construct`'s
135
198
  def enforce_tenant_boundary(domain, construct, attribute, target, held, state, own_tenant_field)
136
199
  return unless own_tenant_field && state.key?(own_tenant_field)
137
200
 
@@ -152,50 +215,63 @@ module Hecks
152
215
  next if target_tenant == own_tenant
153
216
 
154
217
  raise Unauthorized,
155
- RefusalWording.render("Unauthorized", "cross_tenant_reference",
156
- aggregate: construct.hecks_name, field: own_tenant_field,
157
- tenant: Rendering.describe(state[own_tenant_field]),
158
- attribute: attribute.name, target: target.name,
159
- target_field: target_tenant_field,
160
- other: Rendering.describe(record.state[target_tenant_field]))
218
+ RefusalWording.render_site("Unauthorized", "cross_tenant_reference",
219
+ aggregate: construct.hecks_name, field: own_tenant_field,
220
+ tenant: Rendering.describe(state[own_tenant_field]),
221
+ attribute: attribute.name, target: target.name,
222
+ target_field: target_tenant_field,
223
+ other: Rendering.describe(record.state[target_tenant_field]))
161
224
  end
162
225
  end
163
226
 
164
- # THE FIELD AN AGGREGATE'S OWN QUERY NAMES AS TENANT-SCOPING — the
227
+ # The field an aggregate's own query names as tenant-scoping — the
165
228
  # exact same lookup `Fuzzing::Properties::Guards#tenant_field_for`
166
229
  # already established for the property that found this gap, reused
167
230
  # here rather than reinvented: an aggregate's own declared tenant
168
- # field is whichever field ONE OF ITS OWN queries names in
231
+ # field is whichever field one of its own queries names in
169
232
  # `authorize policy, tenant: :field`. `nil` for a construct that
170
233
  # declares no such query — not every aggregate is tenant-scoped,
171
234
  # and an entity never declares a query of its own at all today
172
235
  # (`Entity.queries` is always empty in the real corpus), so this
173
236
  # answers `nil` for every entity without needing to special-case
174
237
  # one.
238
+ #
239
+ # @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct
240
+ # whose declared queries are searched
241
+ # @return [Symbol, nil] the tenant-scoping field name, or nil if `construct`
242
+ # declares no query with `authorize policy, tenant: :field`
175
243
  def tenant_field_for(construct)
176
244
  authorization = construct.queries.filter_map(&:authorization).find(&:tenant)
177
245
  authorization&.tenant&.to_sym
178
246
  end
179
247
 
180
- # `value` is the referenced record's own id, EXACTLY as `Identity.of`
248
+ # Renders a reference-typed value as the plain string key its target
249
+ # record is looked up by.
250
+ #
251
+ # `value` is the referenced record's own id, exactly as `Identity.of`
181
252
  # would build it for that record — a bare scalar for a single-field
182
253
  # identity (the overwhelming common case; Banking's own plain
183
254
  # `reference_to Customer` holds one already, so `Value.
184
255
  # materialize_unwrapped` is a no-op passthrough here), or a
185
256
  # `Naming.identity`-joined string for a compound one
186
257
  # (`belongs_to Translation, as: :translation_ref` — Translation's
187
- # own `identified_by :domain, :from, :to`, THREE fields). Before
188
- # this, plain `value.to_s` on that compound case's own coerced
189
- # Value hit Ruby's default `Object#to_s` (a raw, run-to-run-random
190
- # memory address) instead of joining the record's real id — found
191
- # live via bin/fuzz on the self-hosted "translation" domain
192
- # (replay_is_deterministic), the SAME class of gap `Identity.from`
258
+ # own `identified_by :domain, :from, :to`, three fields). Without
259
+ # `materialize_unwrapped`, plain `value.to_s` on that compound case's
260
+ # own coerced Value would hit Ruby's default `Object#to_s` (a raw,
261
+ # run-to-run-random memory address) instead of joining the record's
262
+ # real id — found live via bin/fuzz on the self-hosted "translation"
263
+ # domain (replay_is_deterministic), the same class of gap `Identity.from`
193
264
  # already had for a compound `identified_by`'s own bare (undotted)
194
265
  # attribute paths. `materialize_unwrapped` recurses a multi-
195
266
  # attribute value object to a plain Hash keyed by attribute name,
196
267
  # in declaration order — `Naming.identity` on `.values` reproduces
197
268
  # the identical join `Identity.of` itself would produce for the
198
- # SAME fields.
269
+ # same fields.
270
+ #
271
+ # @param value [Object] the offered reference value: a `Runtime::Value`, a
272
+ # scalar, or anything `Value.materialize_unwrapped` can open
273
+ # @return [String] the key to look the referenced record up by; empty when
274
+ # `value` names no id
199
275
  def reference_key(value)
200
276
  unwrapped = Value.materialize_unwrapped(value)
201
277
  return Naming.identity(unwrapped.values).to_s if unwrapped.is_a?(Hash)
@@ -203,35 +279,33 @@ module Hecks
203
279
  unwrapped.to_s
204
280
  end
205
281
 
206
- # A COMMAND ARGUMENT's own related record, reachable by name from
207
- # `given`/`ensures` — `disputed_by.status`, say, `CardPayment
282
+ # Hydrates a command argument's own related record, reachable by name
283
+ # from `given`/`ensures` — `disputed_by.status`, say, `CardPayment
208
284
  # .Dispute`'s own fresh `Reference<Customer>` argument — without
209
285
  # teaching the pure expression evaluator anything about
210
- # repositories. The lookup happens HERE, once, before evaluation;
286
+ # repositories. The lookup happens here, once, before evaluation;
211
287
  # `Resolver#lookup` just digs into a plain Hash exactly as it
212
288
  # always has.
213
289
  #
214
- # `owner` NARROWED TO `command` ONLY (S12, ADR 0025 — "rules
290
+ # `owner` narrowed to `command` only (S12, ADR 0025 — "rules
215
291
  # confined to their own aggregate boundary"): dereferencing the
216
- # DECLARING aggregate/entity's own STORED `reference_to` used to
217
- # be the other half of this method's job — a live query against
218
- # another aggregate's own repository, every time a `given`/
219
- # `ensures`/`invariant` ran. That half is gone; a cross-aggregate
220
- # fact a rule needs now has to be a `projects`-maintained LOCAL
221
- # field (`AggregateBuilder#projects_impl`'s own comment), already
222
- # present in `subject`'s own state, no hydration needed. A
223
- # reference-typed COMMAND ARGUMENT stays in bounds, though — the
224
- # ADR's own boundary list names "its command arguments" as
225
- # readable, and nothing is stored yet for a fresh argument to
226
- # project from; resolving it once here, synchronous with THIS
227
- # command's own admission, is a different shape from a live query
228
- # against an ALREADY-PERSISTED reference. `enforce_givens`/
292
+ # declaring aggregate/entity's own stored `reference_to` is not part
293
+ # of this method's job — a cross-aggregate fact a rule needs has to
294
+ # be a `projects`-maintained local field (`AggregateBuilder#
295
+ # projects_impl`'s own comment), already present in `subject`'s own
296
+ # state, no hydration needed, rather than a live query against
297
+ # another aggregate's own repository on every `given`/`ensures`/
298
+ # `invariant` run. A reference-typed command argument stays in
299
+ # bounds, though — the ADR's own boundary list names "its command
300
+ # arguments" as readable, and nothing is stored yet for a fresh
301
+ # argument to project from; resolving it once here, synchronous
302
+ # with this command's own admission, is a different shape from a
303
+ # live query against an already-persisted reference. `enforce_givens`/
229
304
  # `enforce_ensures` are this method's only two remaining callers,
230
305
  # both passing `command`/`args`, never a `subject`'s own
231
- # aggregate — verified before this comment was written, not
232
- # assumed.
306
+ # aggregate — verified by grep, not assumed.
233
307
  #
234
- # RECURSES into what it finds, so a chain deeper than one hop
308
+ # Recurses into what it finds, so a chain deeper than one hop
235
309
  # still resolves in one pass. Depth-bounded rather than cycle-
236
310
  # detected — nothing in this corpus dots more than two hops on a
237
311
  # fresh argument, and a bound is simpler than tracking visited
@@ -239,6 +313,22 @@ module Hecks
239
313
  DEREFERENCE_DEPTH = 4
240
314
  private_constant :DEREFERENCE_DEPTH
241
315
 
316
+ # Hydrates `owner`'s own reference-typed attributes into the records they name.
317
+ #
318
+ # @param domain [String, Symbol] the domain a referenced aggregate is
319
+ # resolved in
320
+ # @param owner [Class, Bluebook::Aggregate, Bluebook::Entity, nil] the
321
+ # construct whose reference-typed attributes are dereferenced; nil (or a
322
+ # depth of zero) short-circuits to an empty Hash
323
+ # @param source [Hash{Symbol => Object}] the offered payload to read each
324
+ # reference-typed attribute's raw id from
325
+ # @param depth [Integer] how many more hops to recurse into a resolved
326
+ # reference's own reference-typed fields
327
+ # @return [Hash{Symbol => Object}] one entry per resolved reference-typed
328
+ # attribute, keyed by the attribute name with a trailing `_id` stripped,
329
+ # valued at the referenced record's state merged with its own dereferenced
330
+ # references; an attribute with no id, an unresolvable target, or no
331
+ # matching record contributes nothing
242
332
  def dereference(domain, owner, source, depth: DEREFERENCE_DEPTH)
243
333
  return {} if depth <= 0 || owner.nil?
244
334
 
@@ -20,6 +20,9 @@ module Hecks
20
20
 
21
21
  attr_reader :registry
22
22
 
23
+ # @param registry [Runtime::Registry, nil] the booted registry the rules read bluebooks,
24
+ # repositories and the event log from; nil serves only the rules that read no
25
+ # registry, such as `sign_of`
23
26
  def initialize(registry)
24
27
  @registry = registry
25
28
  end
@@ -17,12 +17,28 @@ module Hecks
17
17
  :unresolved_dependencies,
18
18
  keyword_init: true
19
19
  ) do
20
+ # Reports whether every owner field this command could touch is a known,
21
+ # deterministic write.
22
+ #
23
+ # @return [Boolean] `complete_state`
20
24
  def complete_state? = complete_state
25
+
26
+ # Reports whether the command needs no prior state at all (implies `complete_state?`).
27
+ #
28
+ # @return [Boolean] `state_independent`
21
29
  def state_independent? = state_independent
22
30
 
31
+ # Chooses the dispatch strategy this plan's proof and the adapter's own
32
+ # capabilities together allow.
33
+ #
23
34
  # Capability negotiation is correctness-first: an optimization is
24
35
  # selected only when both the semantic proof and adapter capability
25
36
  # are present. This is planning data only; no runtime path calls it yet.
37
+ #
38
+ # @param capabilities [Array<String, Symbol>] the repository's declared capabilities
39
+ # @return [Symbol] `DependencyPlanning::ATOMIC_PUT` when the plan is complete,
40
+ # state-independent, and the adapter declares that capability;
41
+ # `DependencyPlanning::TRANSACTIONAL_FALLBACK` otherwise
26
42
  def strategy_for(capabilities: [])
27
43
  return TRANSACTIONAL_FALLBACK unless complete_state? && state_independent?
28
44
  return TRANSACTIONAL_FALLBACK unless capabilities.map(&:to_sym).include?(ATOMIC_PUT)
@@ -38,13 +54,27 @@ module Hecks
38
54
  module ExpressionReads
39
55
  module_function
40
56
 
57
+ # Finds every dotted path a canonical expression reads, without evaluating it.
58
+ #
41
59
  # Read the same parsed canonical-expression nodes the evaluator uses.
42
60
  # A generic Struct walk keeps this additive when the expression grammar
43
61
  # gains a composed node; only Lookup nodes carry domain dependencies.
62
+ #
63
+ # @param canonical [String] the canonical expression text, such as a rule's `canonical`
64
+ # @return [Array<String>] the dotted paths the expression reads
44
65
  def paths(canonical)
45
66
  collect(Bluebook::Expression::Evaluator.parse(canonical), Set.new)
46
67
  end
47
68
 
69
+ # Walks one parsed expression node, collecting the dotted paths it reads.
70
+ #
71
+ # @param node [Object] a node from `Bluebook::Expression::Evaluator.parse` — a
72
+ # `Bluebook::Expression::Resolver::Lookup`, a `Bluebook::Expression::Resolver::
73
+ # BlockPredicate`, a `Struct` composed of further nodes, an `Array` of them, or any
74
+ # other value (a literal), which contributes no paths
75
+ # @param bound_names [Set<String>] names locally bound by an enclosing block
76
+ # predicate's own parameter, excluded from the result rather than reported as reads
77
+ # @return [Array<String>] the dotted paths read under `node`, excluding `bound_names`
48
78
  def collect(node, bound_names)
49
79
  case node
50
80
  when Bluebook::Expression::Resolver::Lookup
@@ -74,12 +104,12 @@ module Hecks
74
104
  STATEFUL_MUTATIONS = %i[append increment decrement multiply clamp remove].freeze
75
105
 
76
106
  # `root_aggregate:` — Wave 8's own audit surfaced a real bug here,
77
- # not merely a missing feature: for an ENTITY-owned command,
107
+ # not merely a missing feature: for an entity-owned command,
78
108
  # `EntityInterpreter` calls this with `aggregate:` set to the
79
- # ENTITY itself (`element_interpreter.rb`'s own `Analyzer.call
109
+ # entity itself (`element_interpreter.rb`'s own `Analyzer.call
80
110
  # (aggregate: entity, command:)`), so `owner_fields` was always
81
111
  # the entity's own attribute set. A `given`/`ensures` reading
82
- # `parent.X` legitimately means the ROOT aggregate's own field —
112
+ # `parent.X` legitimately means the root aggregate's own field —
83
113
  # a genuinely different owner — but `classify_path`'s `:parent`
84
114
  # branch checked that read against `owner_fields` (the entity's),
85
115
  # which can never contain a root-level field, so every entity
@@ -87,8 +117,20 @@ module Hecks
87
117
  # unresolved regardless of correctness. Defaults to `aggregate`
88
118
  # (a no-op) for the plain-aggregate case — `CommandInterpreter`'s
89
119
  # own call site never needed to change.
120
+ #
121
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct the command
122
+ # is dispatched against; an entity for an entity-owned command
123
+ # @param command [Bluebook::Command] the command to analyze
124
+ # @param root_aggregate [Bluebook::Aggregate] the owning aggregate a `parent.*` read
125
+ # resolves against; defaults to `aggregate` for a plain-aggregate command
126
+ # @return [Hecks::Runtime::DependencyPlanning::Plan] the derived, frozen plan
90
127
  def self.call(aggregate:, command:, root_aggregate: aggregate) = new(aggregate, command, root_aggregate).call
91
128
 
129
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct the command
130
+ # is dispatched against
131
+ # @param command [Bluebook::Command] the command to analyze
132
+ # @param root_aggregate [Bluebook::Aggregate] the owning aggregate `parent.*` reads
133
+ # resolve against; defaults to `aggregate`
92
134
  def initialize(aggregate, command, root_aggregate = aggregate)
93
135
  @aggregate = aggregate
94
136
  @command = command
@@ -96,10 +138,10 @@ module Hecks
96
138
  @owner_fields << aggregate.lifecycle.field.to_sym if aggregate.lifecycle
97
139
  @root_owner_fields = root_aggregate.attributes.to_set(&:name)
98
140
  @root_owner_fields << root_aggregate.lifecycle.field.to_sym if root_aggregate.lifecycle
99
- # `projects` FIELDS (S12, ADR 0025) ARE OWNER STATE TOO — a
141
+ # `projects` fields (S12, ADR 0025) are owner state too — a
100
142
  # `given`/`ensures` reading one (e.g. `customer_status ==
101
143
  # "active"`) is reading this record's own stored field, same
102
- # as any attribute, even though nothing here WRITES it via a
144
+ # as any attribute, even though nothing here writes it via a
103
145
  # declared mutation (`CommandInterpreter#seed_projected_fields`
104
146
  # populates it outside this analysis entirely). Left out of
105
147
  # `known_writes` deliberately: `add_preservation_reads` then
@@ -107,7 +149,7 @@ module Hecks
107
149
  # a partial mutation, which is exactly right — a projected
108
150
  # field's freshness comes from the interpreter reseeding it on
109
151
  # save, not from anything a caller-supplied write set carries.
110
- # Applies to BOTH `owner_fields` and `root_owner_fields` — an
152
+ # Applies to both `owner_fields` and `root_owner_fields` — an
111
153
  # entity's own `parent.*` read can name the root aggregate's
112
154
  # projected field just as easily as one of its real attributes
113
155
  # (`Banking::Withdrawal.Dispute`'s own `parent.account_customer_
@@ -126,6 +168,9 @@ module Hecks
126
168
  @unresolved = Set.new
127
169
  end
128
170
 
171
+ # Runs the analysis and derives the command's dependency plan.
172
+ #
173
+ # @return [Hecks::Runtime::DependencyPlanning::Plan] the derived, frozen plan
129
174
  def call
130
175
  analyze_initial_state
131
176
  analyze_mutations
@@ -237,7 +282,7 @@ module Hecks
237
282
  end
238
283
  end
239
284
 
240
- # KNOWN, HARMLESS GAP: `corrects ..., as: :name`'s bound name
285
+ # Known, harmless gap: `corrects ..., as: :name`'s bound name
241
286
  # (admissibility.rb's `enforce_correction_target`/`enforce_givens`/
242
287
  # `enforce_ensures`) isn't special-cased here the way `:old`/
243
288
  # `:parent` are — a given/ensures referencing it falls through to
@@ -247,7 +292,7 @@ module Hecks
247
292
  # `hydrate_existing` path instead of the `ATOMIC_PUT` fast path.
248
293
  # Not a correctness bug — `as:`'s runtime binding (a plain `attrs`
249
294
  # merge, exactly like `old:`'s) resolves and evaluates correctly
250
- # regardless of what this STATIC analysis concludes — just a real,
295
+ # regardless of what this static analysis concludes — just a real,
251
296
  # deliberately-left optimization gap: closing it would mean
252
297
  # threading "which names this command declares as correction
253
298
  # bindings" into the Analyzer, which doesn't have that per-command
@@ -258,9 +303,9 @@ module Hecks
258
303
  name = head.to_sym
259
304
 
260
305
  if name == :parent
261
- # `root_owner_fields` — NOT `owner_fields`. For an entity-owned
262
- # command `owner_fields` is the ENTITY's own attribute set;
263
- # `parent.X` always means the ROOT aggregate's own field, a
306
+ # `root_owner_fields` — not `owner_fields`. For an entity-owned
307
+ # command `owner_fields` is the entity's own attribute set;
308
+ # `parent.X` always means the root aggregate's own field, a
264
309
  # genuinely different owner (`root_aggregate:`'s own header,
265
310
  # above, has the full bug this fixes). Identical for a plain
266
311
  # aggregate command, where root_aggregate defaults to aggregate