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
@@ -1,11 +1,11 @@
1
1
  module Hecks
2
2
  module Fuzzing
3
3
  module Properties
4
- # A DRY RUN LEAVES NO TRACE — `Dispatcher#dry_run?`'s whole contract
4
+ # **A dry run leaves no trace** — `Dispatcher#dry_run?`'s whole contract
5
5
  # ("the command evaluated hypothetically, nothing saved or emitted,
6
6
  # no reaction"), held to the store rather than trusted: `Replay`
7
7
  # snapshots every instance and the event count on either side of
8
- # each `{"dry_run": …}` step, in a SEPARATE `dry_run_traces` array
8
+ # each `{"dry_run": …}` step, in a separate `dry_run_traces` array
9
9
  # (same order as `history[:dry_runs]`, never merged into it — that
10
10
  # array stays the exact `{verb:, ok:, error?:}` shape the compiled
11
11
  # Rust binary's own `dry_run` answers, so `spec/rust_conformance_
@@ -16,6 +16,12 @@ module Hecks
16
16
  # hand-built history, an older corpus) are skipped, not failed: no
17
17
  # claim, no finding.
18
18
  module DryRuns
19
+ # Checks that every dry-run step left neither the event count nor any
20
+ # instance's state changed.
21
+ #
22
+ # @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
23
+ # @return [true, String] true if every dry-run trace shows no change; otherwise
24
+ # a semicolon-joined message naming each offending dry run
19
25
  def dry_runs_leave_no_trace(history)
20
26
  offenders = Array(history[:dry_run_traces]).filter_map do |entry|
21
27
  before = entry[:before]
@@ -37,7 +43,7 @@ module Hecks
37
43
  # Dispatch-binding and mutation-recomputation properties: a saga/
38
44
  # policy dispatch is bound to the value its own with_spec names, and a
39
45
  # command's append/remove/multiply/clamp mutations land on the same
40
- # after-state an independent recomputation of the SAME rule produces.
46
+ # after-state an independent recomputation of the same rule produces.
41
47
 
42
48
  # Holds dispatch_binding_fidelity and mutations_match_recompute, plus
43
49
  # the independently-written re-derivations (#resolve_dispatch_binding,
@@ -49,17 +55,17 @@ module Hecks
49
55
  # are not claimable feature strings at all — see FEATURE_COVERAGE's
50
56
  # own comment on this entry). history[:saga_dispatches]/[:policy_dispatches]
51
57
  # (Registry#saga_dispatch_log/#policy_dispatch_log — additive,
52
- # Ruby-only, NEVER touching saga_log/reaction_log, the byte-for-
58
+ # Ruby-only, never touching saga_log/reaction_log, the byte-for-
53
59
  # byte shape spec/rust_conformance_spec.rb holds Rust to) each
54
- # carry the RAW inputs a dispatch's own args were resolved from,
60
+ # carry the raw inputs a dispatch's own args were resolved from,
55
61
  # captured live at the moment the resolution actually ran — a
56
62
  # saga's own memory keeps changing across a run, so re-deriving
57
- # from history[:saga_instances]'s FINAL memory (the only other
63
+ # from history[:saga_instances]'s final memory (the only other
58
64
  # place it would be visible) would grade the wrong moment,
59
65
  # lifecycle_guard_and_given_violations_are_refused's own false
60
66
  # positive one item earlier, in a different shape.
61
67
  #
62
- # #resolve_dispatch_binding/#resolve_trigger_binding are SEPARATE,
68
+ # #resolve_dispatch_binding/#resolve_trigger_binding are separate,
63
69
  # independently-written re-derivations of SagaInterpreter#
64
70
  # dispatch_args/PolicyInterpreter#trigger_args's own resolution —
65
71
  # never calling either method again, which would only ever agree
@@ -67,7 +73,8 @@ module Hecks
67
73
  # argument binding on a fan-out dispatch that produces a perfectly
68
74
  # normal-looking log entry (`delivered: true`) and would only ever
69
75
  # surface as a downstream assertion failure, if it surfaces at
70
- # all" — PR #325's own defect class, one level over.
76
+ # all" — a defect class one level over from what
77
+ # `fanout_dispatches_once_per_matching_row` already catches.
71
78
  #
72
79
  # Real targets: Settlement (mixed literal/correlation-head/event-
73
80
  # payload/memory-fallback bindings across three legs, plus a
@@ -75,6 +82,11 @@ module Hecks
75
82
  # the forward Credit leg carries), ExternalSettlement, Onboarding
76
83
  # (no compensation leg, by design — nothing to check there beyond
77
84
  # the forward leg's own event-payload binding).
85
+ #
86
+ # @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
87
+ # @return [true, String] true if every saga and policy dispatch's own bound args
88
+ # match an independent re-derivation of their `with_spec`; otherwise a
89
+ # semicolon-joined message naming each offending dispatch
78
90
  def dispatch_binding_fidelity(history)
79
91
  saga_offenders = history.fetch(:saga_dispatches, []).filter_map do |entry|
80
92
  expected = resolve_dispatch_binding(entry)
@@ -99,9 +111,14 @@ module Hecks
99
111
 
100
112
  # SagaInterpreter#dispatch_args's own 4-branch resolution,
101
113
  # reproduced independently: a literal, the correlation key itself,
102
- # the CURRENT triggering event's own payload, or — the fallback —
103
- # the saga's own carried memory (seeded from the STARTING event's
114
+ # the current triggering event's own payload, or — the fallback —
115
+ # the saga's own carried memory (seeded from the starting event's
104
116
  # payload, at begin_saga).
117
+ #
118
+ # @param entry [Hash] one `history[:saga_dispatches]` entry: at least
119
+ # `:with_spec`, `:correlation_head`, `:instance`, `:event_payload`, and `:memory`
120
+ # @return [Hash{Symbol => Object}] the resolved, materialized args `with_spec`
121
+ # should produce, keyed by symbol
105
122
  def resolve_dispatch_binding(entry)
106
123
  entry[:with_spec].to_h do |key, value|
107
124
  resolved = if !value.is_a?(Symbol) then value
@@ -116,7 +133,12 @@ module Hecks
116
133
  # PolicyInterpreter#trigger_args's own 2-branch resolution — a
117
134
  # policy holds no correlation and no memory, so `payload` (the
118
135
  # triggering event's own payload, already merged with a fan-out
119
- # row's id when there is one) is the WHOLE source.
136
+ # row's id when there is one) is the whole source.
137
+ #
138
+ # @param entry [Hash] one `history[:policy_dispatches]` entry: at least
139
+ # `:with_spec` and `:payload`
140
+ # @return [Hash{Symbol => Object}] the resolved, materialized args `with_spec`
141
+ # should produce, keyed by symbol
120
142
  def resolve_trigger_binding(entry)
121
143
  entry[:with_spec].to_h do |key, value|
122
144
  resolved = value.is_a?(Symbol) ? entry[:payload][value] : value
@@ -137,33 +159,86 @@ module Hecks
137
159
  #
138
160
  # `history[:mutation_traces]` (Replay's own bounded, additive
139
161
  # extension — see #build_mutation_trace's own comment) carries a
140
- # per-step before/after snapshot of the ENTITY ELEMENT an
162
+ # per-step before/after snapshot of the entity element an
141
163
  # entity-dispatched command's own mutations acted on, materialized
142
164
  # to plain data, plus the step's own raw args — the delta
143
165
  # `aggregation_matches_recompute` never had to ask for, because
144
- # count/median are pure functions of FINAL state and a mutation
166
+ # count/median are pure functions of final state and a mutation
145
167
  # is not (the same "captured live, not re-derived from final
146
168
  # state" lesson item 8's own saga_dispatch_log already learned).
147
169
  #
148
170
  # #recompute_append/#recompute_remove/#recompute_multiply/
149
- # #recompute_clamp are SEPARATE, independently-written
150
- # reproductions of MutationApplier#appended/#removed and
171
+ # #recompute_clamp/#recompute_set are separate, independently-
172
+ # written reproductions of EntityElement#appended_to_element/
173
+ # #removed_from_element/#apply_to_element's own `:set` branch and
151
174
  # CommandRules::Arithmetic#multiply/#clamp — never calling either
152
- # again, which would only ever agree with itself. `:set`/
153
- # `:increment`/`:decrement` are out of scope on purpose (the four
154
- # "vendored, not yet upstream" ops this item exists for); a
155
- # command mixing them with a recomputable op still gets the
156
- # recomputable one checked.
175
+ # again, which would only ever agree with itself. Every one of
176
+ # these five reproduces the entity-scoped applier specifically
177
+ # (`EntityElement#apply_to_element`, entity_element.rb), never
178
+ # the aggregate-level `MutationApplier#apply` (mutation_applier.rb)
179
+ # — `build_mutation_trace` (replay.rb) only ever captures an
180
+ # entity-owned command's own mutation in the first place (that
181
+ # method's own `command_name&.include?(".")` guard), so there is
182
+ # no aggregate-level trace for this property to ever compare
183
+ # against `owner_for_verb`'s own comment draws the identical
184
+ # aggregate/owner distinction for `:append`. `:increment`/
185
+ # `:decrement` remain out of scope (CommandRules::Arithmetic#
186
+ # arithmetic's own VO-vs-scalar branching and INT64_RANGE overflow
187
+ # checks are a materially larger reproduction than the other five
188
+ # ops combined — scoped out of this pass, not forgotten; see
189
+ # docs/decisions/0056); a command mixing them with a recomputable
190
+ # op still gets the recomputable one checked.
191
+ #
192
+ # `:set` closes a real, separate gap from the other four — this is
193
+ # not "the fifth op of a symmetrical set." `self_consistency.rb`'s
194
+ # own rehydration/idempotency checks (lib/hecks/fuzzing/
195
+ # self_consistency.rb) can never catch a bug in `EntityElement#
196
+ # apply_to_element`'s `:set` branch (entity_element.rb) no matter
197
+ # how much they run: both the "live" state they snapshot and the
198
+ # "rehydrated" state they fold from `Ports::Persistence::
199
+ # AppendOnly`'s own journal trace back to the same single
200
+ # `step_apply_mutations` call (entity_interpreter.rb) — the
201
+ # journal holds the full POST-mutation state, not a delta (that
202
+ # file's own header), so a wrong `:set` result is already baked
203
+ # into both sides of that comparison before either one runs. A
204
+ # Ruby/Rust differential check is subject to the identical
205
+ # structural blind spot whenever Rust's own generated `:set`
206
+ # handling was derived from — and so shares — the same
207
+ # misunderstanding Ruby's implementation has. `#recompute_set`
208
+ # is a genuinely third computation, independent of both: it
209
+ # re-derives the expected value from the mutation's own declared
210
+ # `source` (an argument or a literal — matching `apply_to_
211
+ # element`'s own `resolve_source`, which unlike the aggregate-
212
+ # level `apply` never special-cases a `StateRef` source at all)
213
+ # and the step's own captured `args`, then compares against what
214
+ # the real dispatch actually stored — closing exactly the class
215
+ # of bug the self-consistency and (whenever Rust's codegen shares
216
+ # a ruby-derived misunderstanding) differential checks cannot see.
217
+ # `spec/fuzzing/mutation_set_self_correctness_spec.rb` proves this
218
+ # concretely: a defect planted directly in `EntityElement#apply_
219
+ # to_element`'s `:set` branch leaves `SelfConsistency.check`
220
+ # completely clean while `mutations_match_recompute` names it.
157
221
  #
158
222
  # `:unrecomputable` (never compared, never a finding) covers the
159
223
  # generator's own deliberate arg-malforming (`StepBuilder#malform`)
160
224
  # landing a non-Numeric amount/non-2-element bounds where
161
- # multiply/clamp need one — the SAME shape `guard_check`'s own
225
+ # multiply/clamp need one — the same shape `guard_check`'s own
162
226
  # AbsentArgument false positive taught: a step whose raw material
163
227
  # doesn't fit the op's own contract is inconclusive, not a claimed
164
- # mismatch.
165
- RECOMPUTABLE_MUTATION_OPS = %i[append remove multiply clamp].freeze
166
-
228
+ # mismatch. `#recompute_set` returns it for the same reason,
229
+ # whenever `Value.for_attribute`'s own coercion door raises on the
230
+ # resolved raw source (malformed fuzzer input, never a real
231
+ # corpus shape).
232
+ RECOMPUTABLE_MUTATION_OPS = %i[append remove multiply clamp set].freeze
233
+
234
+ # Checks that every recomputable mutation an entity-owned command
235
+ # applied landed on the same after-state an independent
236
+ # recomputation of the same rule produces.
237
+ #
238
+ # @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
239
+ # @return [true, String] true if every recomputable mutation's after-state
240
+ # matches an independent recomputation; otherwise a semicolon-joined message
241
+ # naming each offending mutation
167
242
  def mutations_match_recompute(history)
168
243
  bluebooks = history.fetch(:bluebooks)
169
244
 
@@ -196,14 +271,20 @@ module Hecks
196
271
 
197
272
  # `command_for_verb`'s own root-aggregate half (Guards) —
198
273
  # re-derived independently rather than read off `entry[:domain]`/
199
- # `entry[:aggregate]` (present on a REAL `build_mutation_trace`
274
+ # `entry[:aggregate]` (present on a real `build_mutation_trace`
200
275
  # entry, but not on every hand-built fixture this property is
201
276
  # tested against) so this works from `entry[:verb]` alone, the
202
277
  # one field every entry always carries. BUG#5's fix needs the
203
- # ROOT aggregate specifically — `Value.for_attribute` resolves a
204
- # value-object TYPE against the root's own namespace only, the
278
+ # root aggregate specifically — `Value.for_attribute` resolves a
279
+ # value-object type against the root's own namespace only, the
205
280
  # same reason `EntityElement#locate_chain` threads `root_aggregate`
206
281
  # through every hop separately from each hop's own `owner`.
282
+ #
283
+ # @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
284
+ # keyed by domain name
285
+ # @param verb [String] the dispatched verb to resolve
286
+ # @return [Bluebook::Aggregate, nil] the root aggregate `verb`'s domain and
287
+ # aggregate name, or nil if `verb` names no domain
207
288
  def aggregate_for_verb(bluebooks, verb)
208
289
  domain_name, aggregate_name, = Naming.split_verb(verb)
209
290
  return nil unless domain_name
@@ -211,7 +292,7 @@ module Hecks
211
292
  bluebooks[domain_name]&.aggregate(aggregate_name)
212
293
  end
213
294
 
214
- # `mutation.target`'s own DECLARING construct — the root
295
+ # `mutation.target`'s own declaring construct — the root
215
296
  # aggregate for an aggregate-owned command (`AddSlot`'s own
216
297
  # `:slots`), or the entity a dot-shaped command belongs to
217
298
  # (`Board.AddCard`'s own `:cards`, declared on `Board`, not on
@@ -223,6 +304,15 @@ module Hecks
223
304
  # each hop's own `owner`. Re-derived independently from
224
305
  # `entry[:verb]` alone, the same reasoning `aggregate_for_verb`'s
225
306
  # own comment gives.
307
+ #
308
+ # @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
309
+ # keyed by domain name
310
+ # @param verb [String] the dispatched verb to resolve
311
+ # @return [Bluebook::Aggregate, Bluebook::Entity, nil] the construct that
312
+ # declares the mutation's own target: the root aggregate for an
313
+ # aggregate-owned command, or the owning entity for a dot-shaped
314
+ # (entity-owned) command; nil if `verb` names no command path or no
315
+ # resolvable aggregate
226
316
  def owner_for_verb(bluebooks, verb)
227
317
  domain_name, aggregate_name, command_path = Naming.split_verb(verb)
228
318
  return nil unless command_path
@@ -235,6 +325,21 @@ module Hecks
235
325
  aggregate.entities.find { |candidate| candidate.hecks_name == entity_name }
236
326
  end
237
327
 
328
+ # Dispatches to the recompute rule for `mutation.op`.
329
+ #
330
+ # @param mutation [Bluebook::Mutation] the mutation to recompute
331
+ # @param current [Object] the target field's before-value, as captured in
332
+ # `entry[:before]`
333
+ # @param args [Hash] the step's own raw args, as captured in `entry[:args]`
334
+ # @param before_scope [Hash] the entity element's own before-state
335
+ # (`entry[:before]`), the source an append's caller-omitted field resolves against
336
+ # @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
337
+ # namespace resolution
338
+ # @param command [Bluebook::Command] the command that declared `mutation`
339
+ # @param owner [Bluebook::Aggregate, Bluebook::Entity] the construct that
340
+ # declares `mutation.target`; defaults to `aggregate`
341
+ # @return [Object, Symbol] the recomputed after-value for `mutation.op`, or
342
+ # `:unrecomputable` if the raw material doesn't fit the op's own contract
238
343
  def recompute_mutation(mutation, current, args, before_scope, aggregate, command, owner = aggregate)
239
344
  case mutation.op
240
345
  when :append
@@ -242,52 +347,136 @@ module Hecks
242
347
  when :remove then recompute_remove(current, mutation.source, args)
243
348
  when :multiply then recompute_multiply(current, resolve_mutation_source(mutation.source, args))
244
349
  when :clamp then recompute_clamp(current, mutation.source)
350
+ when :set then recompute_set(mutation.source, args, aggregate, owner, mutation.target)
245
351
  end
246
352
  end
247
353
 
354
+ # `EntityElement#apply_to_element`'s own `:set` branch (entity_
355
+ # element.rb), reproduced independently — not `MutationApplier#
356
+ # apply`'s aggregate-level twin (mutation_applier.rb), a
357
+ # different method with a different shape: `build_mutation_trace`
358
+ # (replay.rb) only ever captures an entity-owned command's own
359
+ # mutation (`command_name&.include?(".")`, that method's own
360
+ # header), so `mutations_match_recompute` can only ever be
361
+ # checking `apply_to_element`'s branch, never `apply`'s — the
362
+ # same distinction `owner_for_verb`'s own comment already draws
363
+ # for `:append`. Confirmed by reading `apply_to_element` directly:
364
+ # its own `:set` branch resolves the source through `rules.
365
+ # resolve_source` unconditionally (no `StateRef` branch at all —
366
+ # unlike the aggregate-level `apply`, an entity-owned `sets` has
367
+ # no declared corpus site using `state(:x)` today, so this
368
+ # reproduces what ships, not a hypothetical), reads the
369
+ # attribute off `entity.attribute(mutation.target)` (the owning
370
+ # entity, `owner` here — `Board`, never `Workspace`), and coerces
371
+ # through `Value.for_attribute(aggregate, attribute, value)`
372
+ # (the root aggregate, for value-object namespace resolution
373
+ # only — `value_object_for(aggregate, attribute.type)` — the
374
+ # same aggregate/owner split `owner_for_verb`'s own comment
375
+ # explains for `recompute_append`). Coercing against
376
+ # `owner.attribute` rather than `aggregate.attribute` matters
377
+ # concretely: `NestedPieces::Workspace.Board.Label` (`sets
378
+ # :label`) has no `:label` attribute on `Workspace` at all, so
379
+ # coercing against the aggregate would silently pass the raw,
380
+ # uncoerced String through instead of wrapping it as
381
+ # `BoardLabel`, and the comparison below would then disagree with
382
+ # the real, correctly-coerced `after` state on every single run —
383
+ # a real false positive, confirmed by running this against
384
+ # `qa/stress_domains/nested_pieces`, not by inspection alone.
385
+ #
386
+ # Reusing `Value.for_attribute` for the coercion step, rather
387
+ # than re-deriving it, is the same "coercion is its own already-
388
+ # guaranteed door" reasoning `GUARANTEED_BY_CONSTRUCTION` states
389
+ # and `#coerce_recompute_append_arg` already leans on for BUG#5 —
390
+ # what this property exists to check is the source resolution and
391
+ # routing (did the right raw value, from the right source, land
392
+ # on the right target?), not whether `Value.for_attribute` itself
393
+ # coerces correctly (a separate, already-enforced concern).
394
+ # Rescued broadly: a `:set` mutation trace is only ever captured
395
+ # after a real, already-admitted dispatch (`Replay#build_
396
+ # mutation_trace`'s own header), so a raise here means this
397
+ # recomputation resolved the wrong raw material, not that the
398
+ # real dispatch was itself malformed — `:unrecomputable`, not a
399
+ # crash, the same discipline every other branch in this method
400
+ # already follows for the generator's own deliberate malforming.
401
+ #
402
+ # @param source [Symbol, Object] the mutation's own declared source: an argument
403
+ # name to resolve from `args`, or a literal value already
404
+ # @param args [Hash] the step's own raw args
405
+ # @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
406
+ # namespace resolution
407
+ # @param owner [Bluebook::Aggregate, Bluebook::Entity, nil] the construct that
408
+ # declares `target`
409
+ # @param target [Symbol, nil] the mutation's own target field name
410
+ # @return [Object, Symbol] the recomputed, materialized value, or
411
+ # `:unrecomputable` if resolving or coercing the source raises
412
+ def recompute_set(source, args, aggregate, owner, target)
413
+ raw = resolve_mutation_source(source, args)
414
+ attribute = owner&.attribute(target)
415
+ coerced = attribute ? Runtime::Value.for_attribute(aggregate, attribute, raw) : raw
416
+ Runtime::Value.materialize(coerced)
417
+ rescue StandardError
418
+ :unrecomputable
419
+ end
420
+
248
421
  # `EntityElement#appended_to_element`'s own field-mapping half,
249
- # reproduced: the field map resolved the SAME two-tier way
422
+ # reproduced: the field map resolved the same two-tier way
250
423
  # (`MutationApplier#resolve_append_source` — a caller-supplied
251
424
  # arg, or the entity's own current field), then appended. (An
252
- # entity-dispatched command's own mutations DO reach here —
425
+ # entity-dispatched command's own mutations do reach here —
253
426
  # `#build_mutation_trace`'s own comment describing them as never
254
427
  # reaching "the entity_element branch" means `MutationApplier#
255
- # appended`'s own AGGREGATE-level entity_element fallback
428
+ # appended`'s own aggregate-level entity_element fallback
256
429
  # specifically, which really is unreached from here; entity-owned
257
430
  # append dispatches go through `EntityElement#appended_to_element`
258
- # instead, and DO reach this method.)
431
+ # instead, and do reach this method.)
259
432
  #
260
433
  # BUG#5 — an entity-owned `:append` whose target field is itself
261
434
  # value-object-typed (`Board.AddCard`'s own `sets :cards, append:
262
435
  # { sequence: :sequence }`, `CardSequence`-typed). A caller-
263
- # supplied arg reaches the REAL applier (`EntityElement#
436
+ # supplied arg reaches the real applier (`EntityElement#
264
437
  # appended_to_element`) already coerced: `Interpreting#
265
438
  # coerce_declared_arguments` runs `Value.for_attribute` over
266
- # EVERY arg the acting command itself declares, BEFORE dispatch
439
+ # every arg the acting command itself declares, before dispatch
267
440
  # ever reaches a mutation applier at all — independent of, and
268
441
  # earlier than, anything `appended_to_element`'s own value_object
269
- # check does. `before_scope[source]` (the entity's OWN current
442
+ # check does. `before_scope[source]` (the entity's own current
270
443
  # field) needs no such re-coercion here: it's already the
271
- # MATERIALIZED shape `build_mutation_trace` snapshotted it in
444
+ # materialized shape `build_mutation_trace` snapshotted it in
272
445
  # (`Value.materialize`, same as `entry[:after]`), not a raw value
273
446
  # sitting behind a live `Value`.
274
447
  #
275
- # BUG#12 — `owner`/`target` (new here) let this ALSO reproduce
448
+ # BUG#12 — `owner`/`target` (new here) let this also reproduce
276
449
  # `EntityElement#fill_declared_defaults`'s own entity-nested-in-
277
450
  # entity fallback (`appended_to_element`'s `else` branch, when
278
451
  # the appended element is itself an entity — `Card`, nested
279
452
  # inside `Board` — not a value object): `owner.attribute(target)
280
453
  # &.type` names the appended element's own type; when that names
281
454
  # an entity of `aggregate` rather than a value object, every one
282
- # of ITS OWN declared attributes `fields` doesn't already hold
455
+ # of its own declared attributes `fields` doesn't already hold
283
456
  # gets `Instance.default_for`'s own default — reused, not
284
457
  # reimplemented, for the identical "never agree with itself"
285
458
  # reason BUG#5's own coercion re-derivation above already gives:
286
459
  # `Instance.default_for` is pre-existing, independently-tested
287
460
  # machinery (an ordinary aggregate's own creation already runs
288
- # through it via `Instance.defaults`), not the NEW glue
461
+ # through it via `Instance.defaults`), not the new glue
289
462
  # (`fill_declared_defaults` itself) this property exists to
290
463
  # catch a drift in.
464
+ #
465
+ # @param current [Object] the target field's before-value (an Array of elements)
466
+ # @param source_map [Hash{Symbol => Object}] the append mutation's own field map:
467
+ # each appended field's name to its source (an argument name Symbol, or a literal)
468
+ # @param before_scope [Hash] the entity element's own before-state, the source a
469
+ # caller-omitted field resolves against
470
+ # @param args [Hash] the step's own raw args
471
+ # @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
472
+ # namespace resolution
473
+ # @param command [Bluebook::Command] the command that declared the mutation, for
474
+ # coercing a caller-supplied arg
475
+ # @param owner [Bluebook::Aggregate, Bluebook::Entity] the construct whose nested
476
+ # entity `target` may declare; defaults to `aggregate`
477
+ # @param target [Symbol, nil] the append mutation's own target field name
478
+ # @return [Array] `current` (coerced to an Array) with the newly built,
479
+ # symbol-keyed element appended
291
480
  def recompute_append(current, source_map, before_scope, args, aggregate, command, owner = aggregate, target = nil)
292
481
  fields = source_map.transform_values do |source|
293
482
  resolve_mutation_append_field(source, before_scope, args, aggregate, command)
@@ -305,11 +494,22 @@ module Hecks
305
494
  # isn't an entity nested directly under `owner` (a value object,
306
495
  # or nothing declared at all — `owner.attribute` answering `nil`
307
496
  # for a target the DSL itself would already have refused at
308
- # build time). `owner.entities`, NOT `aggregate.entities` — a
309
- # piece nested inside a piece is a child of the OWNING entity
497
+ # build time). `owner.entities`, not `aggregate.entities` — a
498
+ # piece nested inside a piece is a child of the owning entity
310
499
  # (`Card` is `Board.entities`, never `Workspace.entities`), the
311
500
  # same distinction `EntityElement#appended_to_element`'s own fix
312
501
  # draws.
502
+ #
503
+ # @param aggregate [Bluebook::Aggregate] the root aggregate, forwarded to
504
+ # `Instance.default_for`
505
+ # @param owner [Bluebook::Aggregate, Bluebook::Entity, nil] the construct whose
506
+ # nested entity `target` may declare
507
+ # @param target [Symbol, nil] the append mutation's own target field name
508
+ # @param fields [Hash{Symbol => Object}] the field map already resolved by
509
+ # `recompute_append`
510
+ # @return [Hash{Symbol => Object}] `fields` unchanged if `target` is nil or does
511
+ # not name a nested entity; otherwise `fields` with every declared attribute it
512
+ # doesn't already hold filled in with its own default
313
513
  def fill_recompute_declared_defaults(aggregate, owner, target, fields)
314
514
  return fields unless target
315
515
 
@@ -325,6 +525,18 @@ module Hecks
325
525
  fields
326
526
  end
327
527
 
528
+ # Resolves one appended field's own value from its declared source.
529
+ #
530
+ # @param source [Symbol, Object] an append field's own source: an argument name,
531
+ # or a literal value
532
+ # @param before_scope [Hash] the entity element's own before-state
533
+ # @param args [Hash] the step's own raw args
534
+ # @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
535
+ # namespace resolution
536
+ # @param command [Bluebook::Command] the command that declared the mutation
537
+ # @return [Object] `before_scope[source]` if `source` names no caller-supplied
538
+ # arg; `source` unchanged if it is not a Symbol; otherwise the coerced,
539
+ # materialized arg value
328
540
  def resolve_mutation_append_field(source, before_scope, args, aggregate, command)
329
541
  return source unless source.is_a?(Symbol)
330
542
  return before_scope[source] unless args.key?(source)
@@ -335,8 +547,8 @@ module Hecks
335
547
  # `Interpreting#coerce_declared_arguments`'s own coercion,
336
548
  # reproduced independently (never calling it again, the same
337
549
  # "never agree with itself" rule this whole module's header
338
- # comment gives) — a raw arg is coerced ONLY when its own name
339
- # (`source`) is one of the ACTING COMMAND's own declared
550
+ # comment gives) — a raw arg is coerced only when its own name
551
+ # (`source`) is one of the acting command's own declared
340
552
  # attributes, exactly the condition that method checks before a
341
553
  # real dispatch ever coerces it either. `command.attribute(source)`
342
554
  # answering `nil` (a source that names no declared attribute —
@@ -350,6 +562,15 @@ module Hecks
350
562
  # throughout this whole property, so the eventual `symbolize_deep`
351
563
  # comparison is always materialized-against-materialized, never a
352
564
  # live `Value` against a Hash.
565
+ #
566
+ # @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
567
+ # namespace resolution
568
+ # @param command [Bluebook::Command] the command whose own declared attribute
569
+ # `source` may name
570
+ # @param source [Symbol] the argument name to coerce
571
+ # @param raw [Object] the raw arg value to coerce
572
+ # @return [Object] `raw`, coerced and materialized, if `source` names a declared
573
+ # attribute of `command`; `raw` unchanged otherwise
353
574
  def coerce_recompute_append_arg(aggregate, command, source, raw)
354
575
  attribute = command.attribute(source)
355
576
  return raw unless attribute
@@ -358,6 +579,13 @@ module Hecks
358
579
  end
359
580
 
360
581
  # `MutationApplier#removed`'s own value-equality match, reproduced.
582
+ #
583
+ # @param current [Object] the target field's before-value (an Array of elements)
584
+ # @param source [Symbol, Object] the remove mutation's own source: an argument
585
+ # name, or a literal value already
586
+ # @param args [Hash] the step's own raw args
587
+ # @return [Array] `current` (coerced to an Array) with every element matching the
588
+ # resolved target value removed
361
589
  def recompute_remove(current, source, args)
362
590
  target = symbolize_deep(resolve_mutation_source(source, args))
363
591
  Array(current).reject { |element| symbolize_deep(element) == target }
@@ -367,9 +595,15 @@ module Hecks
367
595
  # reproduced on plain materialized data instead of a real Value:
368
596
  # a single-numeric-field Hash (the VO-typed case — ListCount, one
369
597
  # Integer field) scales that field ; a bare Numeric scales itself.
370
- # `current ||= 0` — the SAME phantom-field fallback #multiply
598
+ # `current ||= 0` — the same phantom-field fallback #multiply
371
599
  # itself already gives (unaffected by this session's #clamp fix,
372
600
  # since #multiply never needed one).
601
+ #
602
+ # @param current [Hash, Numeric, nil] the target field's before-value
603
+ # @param amount [Object] the resolved multiplier
604
+ # @return [Hash, Numeric, Symbol] the scaled field/value, or `:unrecomputable` if
605
+ # `amount` is not Numeric or `current` is neither a Hash with a numeric field
606
+ # nor a Numeric
373
607
  def recompute_multiply(current, amount)
374
608
  return :unrecomputable unless amount.is_a?(Numeric)
375
609
 
@@ -387,12 +621,18 @@ module Hecks
387
621
  end
388
622
 
389
623
  # `CommandRules::Arithmetic#clamp`'s own two branches, reproduced
390
- # the same way #recompute_multiply is — including THIS SESSION'S
391
- # OWN `current ||= 0` fix (command_rules/arithmetic.rb), the one
624
+ # the same way #recompute_multiply is — including this session's
625
+ # own `current ||= 0` fix (command_rules/arithmetic.rb), the one
392
626
  # arithmetic op that didn't have it until now. `mutation.source`
393
627
  # is always a literal `[min, max]`, never an argument reference
394
628
  # (MutationApplier's own comment on why `resolve_source` is
395
629
  # skipped for clamp) — so nothing here reads `args` for it at all.
630
+ #
631
+ # @param current [Hash, Numeric, nil] the target field's before-value
632
+ # @param bounds [Object] the mutation's own declared `[min, max]` literal
633
+ # @return [Hash, Numeric, Symbol] the clamped field/value, or `:unrecomputable`
634
+ # if `bounds` is not a 2-element Array or `current` is neither a Hash with a
635
+ # numeric field nor a Numeric
396
636
  def recompute_clamp(current, bounds)
397
637
  return :unrecomputable unless bounds.is_a?(Array) && bounds.size == 2
398
638
 
@@ -411,21 +651,30 @@ module Hecks
411
651
  end
412
652
 
413
653
  # `CommandRules::Arithmetic#resolve_source`, reproduced: a
414
- # mutation's source is either the NAME OF AN ARGUMENT or a
415
- # LITERAL, told apart by type.
654
+ # mutation's source is either the name of an argument or a
655
+ # literal, told apart by type.
656
+ #
657
+ # @param source [Symbol, Object] an argument name to resolve from `args`, or a
658
+ # literal value already
659
+ # @param args [Hash] the step's own raw args
660
+ # @return [Object] `args[source]` if `source` is a Symbol; `source` unchanged otherwise
416
661
  def resolve_mutation_source(source, args)
417
662
  source.is_a?(Symbol) ? args[source] : source
418
663
  end
419
664
 
420
- # A generated step's own `args` arrive with STRING keys on every
665
+ # A generated step's own `args` arrive with string keys on every
421
666
  # nested Hash (the wire/JSON shape `spec/corpus/*.json` already
422
667
  # uses) while `history[:mutation_traces]`' own materialized
423
- # before/after state carries SYMBOL keys throughout (Runtime::
668
+ # before/after state carries symbol keys throughout (Runtime::
424
669
  # Value.materialize's own convention) — two hashes holding the
425
- # identical fact compare UNEQUAL by Ruby's own `Hash#==` unless
670
+ # identical fact compare unequal by Ruby's own `Hash#==` unless
426
671
  # both sides are normalized the same way first. Recursive, since
427
672
  # an appended/removed element can itself nest a value object
428
673
  # (RemoveTag's own `Tag` argument, `{"key"=>..., "value"=>...}`).
674
+ #
675
+ # @param value [Object] a Hash, Array, or scalar to normalize
676
+ # @return [Object] `value` with every Hash key symbolized, recursively through
677
+ # nested Hashes and Arrays; any other value unchanged
429
678
  def symbolize_deep(value)
430
679
  case value
431
680
  when Hash then value.to_h { |key, val| [key.to_sym, symbolize_deep(val)] }