hecks 1.2.0 → 1.4.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 (358) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +7 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +10 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
  7. data/lib/hecks/adapters/driven/heki/journal.rb +60 -2
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
  9. data/lib/hecks/adapters/driven/heki.rb +13 -7
  10. data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
  12. data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
  13. data/lib/hecks/adapters/driven/lambda.rb +39 -33
  14. data/lib/hecks/adapters/driven/local_storage.rb +17 -10
  15. data/lib/hecks/adapters/driven/memory.rb +205 -9
  16. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
  17. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  18. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  19. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  20. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  21. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  22. data/lib/hecks/adapters/driven/postgres_era.adapter +5 -0
  23. data/lib/hecks/adapters/driven/prism.rb +4 -4
  24. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  25. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  26. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  27. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  28. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  29. data/lib/hecks/adapters/driven.rb +4 -4
  30. data/lib/hecks/adapters/driving/github_webhook.rb +145 -0
  31. data/lib/hecks/behaviors/dsl.rb +2 -2
  32. data/lib/hecks/behaviors/expectations.rb +51 -23
  33. data/lib/hecks/behaviors/ir.rb +1 -1
  34. data/lib/hecks/behaviors/rspec.rb +1 -1
  35. data/lib/hecks/behaviors/runner.rb +2 -2
  36. data/lib/hecks/behaviors.rb +1 -1
  37. data/lib/hecks/bluebook/aggregate.rb +13 -13
  38. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
  39. data/lib/hecks/bluebook/assembly/build.rb +1 -1
  40. data/lib/hecks/bluebook/assembly/contract.rb +39 -16
  41. data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
  42. data/lib/hecks/bluebook/assembly/marks.rb +18 -18
  43. data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
  44. data/lib/hecks/bluebook/assembly.rb +9 -9
  45. data/lib/hecks/bluebook/attribute.rb +9 -9
  46. data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
  47. data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
  48. data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
  49. data/lib/hecks/bluebook/behaviour/command.rb +23 -23
  50. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -3
  51. data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
  52. data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
  53. data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
  54. data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
  55. data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
  56. data/lib/hecks/bluebook/behaviour/query.rb +1 -1
  57. data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
  58. data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
  59. data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
  60. data/lib/hecks/bluebook/capabilities.rb +27 -0
  61. data/lib/hecks/bluebook/chapter.rb +28 -9
  62. data/lib/hecks/bluebook/command.rb +12 -12
  63. data/lib/hecks/bluebook/domain_port.rb +9 -9
  64. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  65. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  66. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
  67. data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
  68. data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
  69. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
  70. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
  71. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  72. data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
  73. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  74. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  75. data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
  76. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  77. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
  78. data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
  79. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  80. data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
  81. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  82. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  83. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
  84. data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
  85. data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
  86. data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
  87. data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
  88. data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
  89. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  90. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  91. data/lib/hecks/bluebook/entity.rb +11 -11
  92. data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
  93. data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
  94. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
  95. data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
  96. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
  97. data/lib/hecks/bluebook/expression/resolver.rb +60 -62
  98. data/lib/hecks/bluebook/hexagon.rb +1 -1
  99. data/lib/hecks/bluebook/lifecycle.rb +1 -1
  100. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
  101. data/lib/hecks/bluebook/meta_validator/judge.rb +122 -86
  102. data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
  103. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
  104. data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
  105. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
  106. data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
  107. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
  108. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
  109. data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
  110. data/lib/hecks/bluebook/meta_validator.rb +70 -70
  111. data/lib/hecks/bluebook/model_check.rb +301 -84
  112. data/lib/hecks/bluebook/pattern_subset.rb +9 -9
  113. data/lib/hecks/bluebook/policy.rb +15 -13
  114. data/lib/hecks/bluebook/process_manager.rb +14 -14
  115. data/lib/hecks/bluebook/project_register.rb +6 -6
  116. data/lib/hecks/bluebook/query.rb +4 -4
  117. data/lib/hecks/bluebook/read_model.rb +14 -14
  118. data/lib/hecks/bluebook/reference.rb +8 -8
  119. data/lib/hecks/bluebook/smoke_test.rb +19 -19
  120. data/lib/hecks/bluebook/synthesizer.rb +12 -12
  121. data/lib/hecks/bluebook/translation.rb +4 -4
  122. data/lib/hecks/bluebook/value_object.rb +6 -6
  123. data/lib/hecks/bluebook.rb +2 -2
  124. data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
  125. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
  126. data/lib/hecks/codemod.rb +36 -35
  127. data/lib/hecks/construct.rb +6 -6
  128. data/lib/hecks/corpus.rb +317 -0
  129. data/lib/hecks/deprecation.rb +95 -0
  130. data/lib/hecks/doc/reference.rb +19 -19
  131. data/lib/hecks/embryonaut_bluebook.rb +11 -11
  132. data/lib/hecks/facade/cli_door.rb +69 -10
  133. data/lib/hecks/facade/cli_runner.rb +105 -24
  134. data/lib/hecks/facade/command_request.rb +23 -0
  135. data/lib/hecks/facade/handle.rb +79 -32
  136. data/lib/hecks/facade/json_door.rb +106 -25
  137. data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
  138. data/lib/hecks/facade/surface/chapter.rb +26 -17
  139. data/lib/hecks/facade/surface.rb +16 -3
  140. data/lib/hecks/facade.rb +15 -4
  141. data/lib/hecks/forms/app.rb +46 -30
  142. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  143. data/lib/hecks/forms/field_renderer.rb +142 -6
  144. data/lib/hecks/forms/field_shape.rb +183 -20
  145. data/lib/hecks/forms/html.rb +51 -7
  146. data/lib/hecks/forms/index_renderer.rb +14 -2
  147. data/lib/hecks/forms/params.rb +120 -23
  148. data/lib/hecks/forms/port_argument.rb +2 -2
  149. data/lib/hecks/forms/query_form_renderer.rb +2 -2
  150. data/lib/hecks/forms/record_renderer.rb +2 -2
  151. data/lib/hecks/forms/record_table.rb +1 -1
  152. data/lib/hecks/forms/value_object_shape.rb +3 -3
  153. data/lib/hecks/forms.rb +24 -4
  154. data/lib/hecks/fqn.rb +1 -1
  155. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  156. data/lib/hecks/framework.rb +48 -17
  157. data/lib/hecks/freezer.rb +11 -11
  158. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
  159. data/lib/hecks/fuzzing/combination_miner.rb +119 -0
  160. data/lib/hecks/fuzzing/concurrent_dispatch.rb +361 -0
  161. data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
  162. data/lib/hecks/fuzzing/differential.rb +158 -0
  163. data/lib/hecks/fuzzing/domain_generator.rb +694 -0
  164. data/lib/hecks/fuzzing/era_boundary.rb +124 -0
  165. data/lib/hecks/fuzzing/form_census.rb +199 -0
  166. data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
  167. data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
  168. data/lib/hecks/fuzzing/isolated_boot.rb +226 -38
  169. data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
  170. data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
  171. data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
  172. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
  173. data/lib/hecks/fuzzing/properties/guards.rb +129 -26
  174. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
  175. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
  176. data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
  177. data/lib/hecks/fuzzing/properties/querying.rb +17 -17
  178. data/lib/hecks/fuzzing/properties.rb +84 -39
  179. data/lib/hecks/fuzzing/qa_settings.rb +152 -0
  180. data/lib/hecks/fuzzing/replay.rb +224 -114
  181. data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
  182. data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
  183. data/lib/hecks/fuzzing/self_consistency.rb +676 -0
  184. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
  189. data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
  190. data/lib/hecks/fuzzing/shrinker.rb +197 -0
  191. data/lib/hecks/fuzzing/structural_skips.rb +39 -0
  192. data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
  193. data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
  194. data/lib/hecks/fuzzing/value_generator.rb +66 -14
  195. data/lib/hecks/fuzzing.rb +11 -0
  196. data/lib/hecks/grammar/evolve.rb +10 -10
  197. data/lib/hecks/grammar.rb +7 -7
  198. data/lib/hecks/ir.rb +13 -13
  199. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  200. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  201. data/lib/hecks/language/bluebook/vocabulary.bluebook +365 -15
  202. data/lib/hecks/language/oidc.json +5 -0
  203. data/lib/hecks/literal.rb +9 -9
  204. data/lib/hecks/naming.rb +89 -21
  205. data/lib/hecks/ports/access_control.rb +58 -2
  206. data/lib/hecks/ports/agent/answers.rb +83 -6
  207. data/lib/hecks/ports/agent.rb +119 -35
  208. data/lib/hecks/ports/authentication.rb +44 -4
  209. data/lib/hecks/ports/authorization.rb +53 -11
  210. data/lib/hecks/ports/clock.rb +42 -23
  211. data/lib/hecks/ports/extraction.rb +16 -0
  212. data/lib/hecks/ports/identity_assignment.rb +24 -2
  213. data/lib/hecks/ports/identity_generation.rb +17 -3
  214. data/lib/hecks/ports/identity_resolution.rb +18 -1
  215. data/lib/hecks/ports/loading.rb +4 -0
  216. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  217. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  218. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  219. data/lib/hecks/ports/persistence/execution.rb +4 -0
  220. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  221. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  222. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  223. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  224. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  225. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  226. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  227. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  228. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  229. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  230. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  231. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +160 -58
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +44 -6
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +338 -83
  242. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  243. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  244. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  245. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
  246. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  247. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  248. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  249. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
  250. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
  251. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
  252. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  253. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  254. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  255. data/lib/hecks/ports/persistence/repository_factory.rb +29 -6
  256. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  257. data/lib/hecks/ports/persistence.rb +36 -1
  258. data/lib/hecks/ports/projection.rb +61 -7
  259. data/lib/hecks/ports/query/in_memory.rb +3 -3
  260. data/lib/hecks/ports/query/ordering.rb +6 -6
  261. data/lib/hecks/ports/query.rb +35 -0
  262. data/lib/hecks/projections/bootstrap_table.rb +112 -0
  263. data/lib/hecks/projections/diagrams.rb +75 -75
  264. data/lib/hecks/projections/glossary/html.rb +250 -0
  265. data/lib/hecks/projections/glossary/markdown.rb +105 -0
  266. data/lib/hecks/projections/glossary/mermaid.rb +110 -0
  267. data/lib/hecks/projections/glossary/page.css +271 -0
  268. data/lib/hecks/projections/glossary/page.js +72 -0
  269. data/lib/hecks/projections/glossary/sections.rb +17 -0
  270. data/lib/hecks/projections/glossary/sentences.rb +205 -0
  271. data/lib/hecks/projections/glossary.rb +214 -286
  272. data/lib/hecks/projections/ir.rb +1 -1
  273. data/lib/hecks/projections/model/deviations.rb +18 -17
  274. data/lib/hecks/projections/model.rb +25 -21
  275. data/lib/hecks/projections/oidc.rb +7 -7
  276. data/lib/hecks/projections/parser_table.rb +5 -5
  277. data/lib/hecks/projections/reference.rb +3 -3
  278. data/lib/hecks/projections/rust_vocabulary.rb +443 -0
  279. data/lib/hecks/projections/shape.rb +2 -2
  280. data/lib/hecks/projections/statements.rb +11 -11
  281. data/lib/hecks/projections/vocabulary.rb +9 -9
  282. data/lib/hecks/projections.rb +5 -3
  283. data/lib/hecks/projector/cli_projector.rb +29 -29
  284. data/lib/hecks/projector/docs_projector.rb +13 -13
  285. data/lib/hecks/projector/exporter.rb +42 -21
  286. data/lib/hecks/projector/ir_projector.rb +1 -1
  287. data/lib/hecks/projector/narrate_projector.rb +15 -22
  288. data/lib/hecks/projector/target.rb +13 -13
  289. data/lib/hecks/projector.rb +15 -15
  290. data/lib/hecks/query_ir.rb +47 -47
  291. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  292. data/lib/hecks/query_specification/common/comparison.rb +132 -24
  293. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  294. data/lib/hecks/query_specification/common/null_policy.rb +57 -13
  295. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  296. data/lib/hecks/query_specification/common/options.rb +25 -0
  297. data/lib/hecks/query_specification/field_path.rb +69 -15
  298. data/lib/hecks/query_specification/hop_path.rb +57 -20
  299. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  300. data/lib/hecks/rendering.rb +3 -3
  301. data/lib/hecks/router/namespace_installer.rb +3 -3
  302. data/lib/hecks/router.rb +1 -1
  303. data/lib/hecks/runtime/aggregate_lock.rb +11 -11
  304. data/lib/hecks/runtime/boot_gates.rb +3 -3
  305. data/lib/hecks/runtime/caller.rb +8 -8
  306. data/lib/hecks/runtime/capability_graph.rb +2 -2
  307. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
  308. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +64 -67
  309. data/lib/hecks/runtime/command_interpreter.rb +159 -102
  310. data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
  311. data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
  312. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  313. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  314. data/lib/hecks/runtime/command_rules/references.rb +94 -19
  315. data/lib/hecks/runtime/command_rules.rb +3 -0
  316. data/lib/hecks/runtime/dependency_planning.rb +11 -11
  317. data/lib/hecks/runtime/dispatcher.rb +283 -90
  318. data/lib/hecks/runtime/entity_element.rb +282 -46
  319. data/lib/hecks/runtime/entity_interpreter.rb +160 -69
  320. data/lib/hecks/runtime/errors.rb +19 -19
  321. data/lib/hecks/runtime/event.rb +6 -6
  322. data/lib/hecks/runtime/identity.rb +22 -22
  323. data/lib/hecks/runtime/instance.rb +39 -14
  324. data/lib/hecks/runtime/interpreting.rb +12 -12
  325. data/lib/hecks/runtime/invocation.rb +276 -0
  326. data/lib/hecks/runtime/loader.rb +14 -14
  327. data/lib/hecks/runtime/outbox.rb +23 -23
  328. data/lib/hecks/runtime/policy_interpreter.rb +54 -54
  329. data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
  330. data/lib/hecks/runtime/query_interpreter.rb +111 -56
  331. data/lib/hecks/runtime/reaction_invocation.rb +76 -9
  332. data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
  333. data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
  334. data/lib/hecks/runtime/reference_hop.rb +6 -6
  335. data/lib/hecks/runtime/refusal_wording.rb +92 -112
  336. data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
  337. data/lib/hecks/runtime/registry/verification.rb +36 -26
  338. data/lib/hecks/runtime/registry.rb +56 -27
  339. data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
  340. data/lib/hecks/runtime/routing.rb +10 -88
  341. data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
  342. data/lib/hecks/runtime/saga_interpreter.rb +93 -56
  343. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  344. data/lib/hecks/runtime/tenant_check.rb +9 -9
  345. data/lib/hecks/runtime/tenant_scope.rb +5 -5
  346. data/lib/hecks/runtime/value/admission.rb +75 -30
  347. data/lib/hecks/runtime/value/coercion.rb +379 -226
  348. data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
  349. data/lib/hecks/runtime/value.rb +28 -23
  350. data/lib/hecks/runtime.rb +7 -7
  351. data/lib/hecks/storehouse.rb +64 -64
  352. data/lib/hecks/version.rb +3 -3
  353. data/lib/hecks/vocabulary.rb +207 -5
  354. data/lib/hecks.rb +13 -11
  355. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
  356. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
  357. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
  358. metadata +43 -2
@@ -0,0 +1,100 @@
1
+ module Hecks
2
+ module Fuzzing
3
+ module Properties
4
+ # Angle-9 — `corrects` (retroactive correction) had exactly one
5
+ # declaration anywhere in the corpus (`examples/banking/bluebook/
6
+ # deposit_accounts.bluebook:353`, aggregate-level) and no property
7
+ # anywhere in this file ever checked it, and no `FEATURE_COVERAGE`
8
+ # claim either — confirmed absent before this file existed. `qa/
9
+ # stress_domains/corrections` gives it its first real coverage; see
10
+ # that domain's own NOTES.md for what it found (an entity-level
11
+ # `corrects` crashes Ruby at dispatch outright, and Rust's own
12
+ # generated code has no admissibility check for it at all — neither
13
+ # engine can be compared on the untested combination this property
14
+ # was written to watch, which is itself the headline finding).
15
+ module Corrections
16
+ # Every event named by a `corrects` mutation, on any command a
17
+ # booted history's own bluebooks declare — aggregate-level, or
18
+ # nested inside an entity at any depth (`each_command_including_
19
+ # entities`, below, is the same recursive walk `SequenceGenerator::
20
+ # Catalog#each_entity_chain` already uses, for the identical reason:
21
+ # `Aggregate#entities`/`Entity#entities` nest, ADR 0026, S17). For
22
+ # every such command, every event this history actually recorded
23
+ # under one of the command's own `emits` names must have an event
24
+ # named by the command's own `corrects` target — same aggregate-
25
+ # qualified name, same id — appearing strictly earlier in the same
26
+ # history.
27
+ #
28
+ # `history[:events]` is already in occurrence order (`Replay.call`'s
29
+ # own `runtime.events`, appended as each step dispatches) — "earlier"
30
+ # is therefore "earlier in this array," no timestamp comparison
31
+ # needed, and no per-step attribution back to which command produced
32
+ # which event is needed either: an event's own declared name already
33
+ # identifies the one command in its aggregate that can produce it
34
+ # (`AggregateBuilder::Sealing#seal_correction_targets`'s own
35
+ # `emitted_by` hash reads the identical fact, one level shallower).
36
+ #
37
+ # Why this cannot be `GUARANTEED_BY_CONSTRUCTION` the way the
38
+ # aggregate-level case almost is: `CommandRules::Admissibility#
39
+ # enforce_correction_target` (the dispatch-time check) and
40
+ # `AggregateBuilder::Sealing#seal_correction_targets` (the build-time
41
+ # check) both exist only for an aggregate-level `corrects` —
42
+ # `EntityInterpreter#step_enforce_givens` never calls the former at
43
+ # all, and the latter walks only `@commands` (the aggregate's own
44
+ # top-level list), never `@entities`. An entity-level `corrects`
45
+ # mutation is invisible to both doors today — this property is the
46
+ # only thing anywhere, on either engine, that would ever catch one
47
+ # going wrong.
48
+ def corrections_reference_an_emitted_event(history)
49
+ violations = []
50
+
51
+ (history[:bluebooks] || {}).each do |domain, bluebook|
52
+ bluebook.aggregates.each do |aggregate|
53
+ aggregate_key = "#{domain}::#{aggregate.hecks_name}"
54
+
55
+ each_command_including_entities(aggregate) do |command|
56
+ corrects_mutations = command.mutations.select { |mutation| mutation.op == :corrects }
57
+ next if corrects_mutations.empty?
58
+
59
+ corrects_mutations.each do |mutation|
60
+ corrected_event = mutation.target.to_s
61
+
62
+ command.emits.each do |produced_event_name|
63
+ violations.concat(unmatched_corrections(history[:events], aggregate_key,
64
+ produced_event_name.to_s, corrected_event,
65
+ command.hecks_name))
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ violations.empty? || violations.uniq.join("; ")
73
+ end
74
+
75
+ def unmatched_corrections(events, aggregate_key, produced_event_name, corrected_event, command_name)
76
+ own_events = events.each_with_index.select do |event, _index|
77
+ event[:name] == produced_event_name && event[:aggregate] == aggregate_key
78
+ end
79
+
80
+ own_events.filter_map do |event, index|
81
+ preceding = events.first(index)
82
+ next if preceding.any? do |earlier|
83
+ earlier[:name] == corrected_event && earlier[:aggregate] == aggregate_key &&
84
+ earlier[:id].to_s == event[:id].to_s
85
+ end
86
+
87
+ "#{command_name} (#{aggregate_key}##{event[:id]}) emitted #{produced_event_name}, claiming to " \
88
+ "correct #{corrected_event}, but no #{corrected_event} for the same aggregate/id appears " \
89
+ "earlier in this history"
90
+ end
91
+ end
92
+
93
+ def each_command_including_entities(owner, &block)
94
+ owner.commands.each(&block)
95
+ owner.entities.each { |entity| each_command_including_entities(entity, &block) }
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -1,10 +1,43 @@
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
5
+ # ("the command evaluated hypothetically, nothing saved or emitted,
6
+ # no reaction"), held to the store rather than trusted: `Replay`
7
+ # snapshots every instance and the event count on either side of
8
+ # each `{"dry_run": …}` step, in a separate `dry_run_traces` array
9
+ # (same order as `history[:dry_runs]`, never merged into it — that
10
+ # array stays the exact `{verb:, ok:, error?:}` shape the compiled
11
+ # Rust binary's own `dry_run` answers, so `spec/rust_conformance_
12
+ # spec.rb`'s direct comparison against it never sees a key Rust
13
+ # doesn't have), and this is the comparison. Refused or accepted
14
+ # makes no difference — a hypothetical that was refused had even
15
+ # less business writing anything. Entries without the snapshots (a
16
+ # hand-built history, an older corpus) are skipped, not failed: no
17
+ # claim, no finding.
18
+ module DryRuns
19
+ def dry_runs_leave_no_trace(history)
20
+ offenders = Array(history[:dry_run_traces]).filter_map do |entry|
21
+ before = entry[:before]
22
+ after = entry[:after]
23
+ next unless before && after
24
+
25
+ traces = []
26
+ traces << "events #{before[:events]} -> #{after[:events]}" unless before[:events] == after[:events]
27
+ traces << "instances changed" unless before[:instances] == after[:instances]
28
+ next if traces.empty?
29
+
30
+ "dry run of #{entry[:verb]} (ok: #{entry[:ok]}) left a trace: #{traces.join(', ')}"
31
+ end
32
+
33
+ offenders.empty? || offenders.join("; ")
34
+ end
35
+ end
36
+
4
37
  # Dispatch-binding and mutation-recomputation properties: a saga/
5
38
  # policy dispatch is bound to the value its own with_spec names, and a
6
39
  # command's append/remove/multiply/clamp mutations land on the same
7
- # after-state an independent recomputation of the SAME rule produces.
40
+ # after-state an independent recomputation of the same rule produces.
8
41
 
9
42
  # Holds dispatch_binding_fidelity and mutations_match_recompute, plus
10
43
  # the independently-written re-derivations (#resolve_dispatch_binding,
@@ -16,17 +49,17 @@ module Hecks
16
49
  # are not claimable feature strings at all — see FEATURE_COVERAGE's
17
50
  # own comment on this entry). history[:saga_dispatches]/[:policy_dispatches]
18
51
  # (Registry#saga_dispatch_log/#policy_dispatch_log — additive,
19
- # Ruby-only, NEVER touching saga_log/reaction_log, the byte-for-
52
+ # Ruby-only, never touching saga_log/reaction_log, the byte-for-
20
53
  # byte shape spec/rust_conformance_spec.rb holds Rust to) each
21
- # carry the RAW inputs a dispatch's own args were resolved from,
54
+ # carry the raw inputs a dispatch's own args were resolved from,
22
55
  # captured live at the moment the resolution actually ran — a
23
56
  # saga's own memory keeps changing across a run, so re-deriving
24
- # from history[:saga_instances]'s FINAL memory (the only other
57
+ # from history[:saga_instances]'s final memory (the only other
25
58
  # place it would be visible) would grade the wrong moment,
26
59
  # lifecycle_guard_and_given_violations_are_refused's own false
27
60
  # positive one item earlier, in a different shape.
28
61
  #
29
- # #resolve_dispatch_binding/#resolve_trigger_binding are SEPARATE,
62
+ # #resolve_dispatch_binding/#resolve_trigger_binding are separate,
30
63
  # independently-written re-derivations of SagaInterpreter#
31
64
  # dispatch_args/PolicyInterpreter#trigger_args's own resolution —
32
65
  # never calling either method again, which would only ever agree
@@ -66,8 +99,8 @@ module Hecks
66
99
 
67
100
  # SagaInterpreter#dispatch_args's own 4-branch resolution,
68
101
  # reproduced independently: a literal, the correlation key itself,
69
- # the CURRENT triggering event's own payload, or — the fallback —
70
- # the saga's own carried memory (seeded from the STARTING event's
102
+ # the current triggering event's own payload, or — the fallback —
103
+ # the saga's own carried memory (seeded from the starting event's
71
104
  # payload, at begin_saga).
72
105
  def resolve_dispatch_binding(entry)
73
106
  entry[:with_spec].to_h do |key, value|
@@ -83,7 +116,7 @@ module Hecks
83
116
  # PolicyInterpreter#trigger_args's own 2-branch resolution — a
84
117
  # policy holds no correlation and no memory, so `payload` (the
85
118
  # triggering event's own payload, already merged with a fan-out
86
- # row's id when there is one) is the WHOLE source.
119
+ # row's id when there is one) is the whole source.
87
120
  def resolve_trigger_binding(entry)
88
121
  entry[:with_spec].to_h do |key, value|
89
122
  resolved = value.is_a?(Symbol) ? entry[:payload][value] : value
@@ -104,32 +137,77 @@ module Hecks
104
137
  #
105
138
  # `history[:mutation_traces]` (Replay's own bounded, additive
106
139
  # extension — see #build_mutation_trace's own comment) carries a
107
- # per-step before/after snapshot of the ENTITY ELEMENT an
140
+ # per-step before/after snapshot of the entity element an
108
141
  # entity-dispatched command's own mutations acted on, materialized
109
142
  # to plain data, plus the step's own raw args — the delta
110
143
  # `aggregation_matches_recompute` never had to ask for, because
111
- # count/median are pure functions of FINAL state and a mutation
144
+ # count/median are pure functions of final state and a mutation
112
145
  # is not (the same "captured live, not re-derived from final
113
146
  # state" lesson item 8's own saga_dispatch_log already learned).
114
147
  #
115
148
  # #recompute_append/#recompute_remove/#recompute_multiply/
116
- # #recompute_clamp are SEPARATE, independently-written
117
- # reproductions of MutationApplier#appended/#removed and
149
+ # #recompute_clamp/#recompute_set are separate, independently-
150
+ # written reproductions of EntityElement#appended_to_element/
151
+ # #removed_from_element/#apply_to_element's own `:set` branch and
118
152
  # CommandRules::Arithmetic#multiply/#clamp — never calling either
119
- # again, which would only ever agree with itself. `:set`/
120
- # `:increment`/`:decrement` are out of scope on purpose (the four
121
- # "vendored, not yet upstream" ops this item exists for); a
122
- # command mixing them with a recomputable op still gets the
123
- # recomputable one checked.
153
+ # again, which would only ever agree with itself. Every one of
154
+ # these five reproduces the entity-scoped applier specifically
155
+ # (`EntityElement#apply_to_element`, entity_element.rb), never
156
+ # the aggregate-level `MutationApplier#apply` (mutation_applier.rb)
157
+ # `build_mutation_trace` (replay.rb) only ever captures an
158
+ # entity-owned command's own mutation in the first place (that
159
+ # method's own `command_name&.include?(".")` guard), so there is
160
+ # no aggregate-level trace for this property to ever compare
161
+ # against `owner_for_verb`'s own comment draws the identical
162
+ # aggregate/owner distinction for `:append`. `:increment`/
163
+ # `:decrement` remain out of scope (CommandRules::Arithmetic#
164
+ # arithmetic's own VO-vs-scalar branching and INT64_RANGE overflow
165
+ # checks are a materially larger reproduction than the other five
166
+ # ops combined — scoped out of this pass, not forgotten; see
167
+ # docs/decisions/0056); a command mixing them with a recomputable
168
+ # op still gets the recomputable one checked.
169
+ #
170
+ # `:set` closes a real, separate gap from the other four — this is
171
+ # not "the fifth op of a symmetrical set." `self_consistency.rb`'s
172
+ # own rehydration/idempotency checks (lib/hecks/fuzzing/
173
+ # self_consistency.rb) can never catch a bug in `EntityElement#
174
+ # apply_to_element`'s `:set` branch (entity_element.rb) no matter
175
+ # how much they run: both the "live" state they snapshot and the
176
+ # "rehydrated" state they fold from `Ports::Persistence::
177
+ # AppendOnly`'s own journal trace back to the same single
178
+ # `step_apply_mutations` call (entity_interpreter.rb) — the
179
+ # journal holds the full POST-mutation state, not a delta (that
180
+ # file's own header), so a wrong `:set` result is already baked
181
+ # into both sides of that comparison before either one runs. A
182
+ # Ruby/Rust differential check is subject to the identical
183
+ # structural blind spot whenever Rust's own generated `:set`
184
+ # handling was derived from — and so shares — the same
185
+ # misunderstanding Ruby's implementation has. `#recompute_set`
186
+ # is a genuinely third computation, independent of both: it
187
+ # re-derives the expected value from the mutation's own declared
188
+ # `source` (an argument or a literal — matching `apply_to_
189
+ # element`'s own `resolve_source`, which unlike the aggregate-
190
+ # level `apply` never special-cases a `StateRef` source at all)
191
+ # and the step's own captured `args`, then compares against what
192
+ # the real dispatch actually stored — closing exactly the class
193
+ # of bug the self-consistency and (whenever Rust's codegen shares
194
+ # a ruby-derived misunderstanding) differential checks cannot see.
195
+ # `spec/fuzzing/mutation_set_self_correctness_spec.rb` proves this
196
+ # concretely: a defect planted directly in `EntityElement#apply_
197
+ # to_element`'s `:set` branch leaves `SelfConsistency.check`
198
+ # completely clean while `mutations_match_recompute` names it.
124
199
  #
125
200
  # `:unrecomputable` (never compared, never a finding) covers the
126
201
  # generator's own deliberate arg-malforming (`StepBuilder#malform`)
127
202
  # landing a non-Numeric amount/non-2-element bounds where
128
- # multiply/clamp need one — the SAME shape `guard_check`'s own
203
+ # multiply/clamp need one — the same shape `guard_check`'s own
129
204
  # AbsentArgument false positive taught: a step whose raw material
130
205
  # doesn't fit the op's own contract is inconclusive, not a claimed
131
- # mismatch.
132
- RECOMPUTABLE_MUTATION_OPS = %i[append remove multiply clamp].freeze
206
+ # mismatch. `#recompute_set` returns it for the same reason,
207
+ # whenever `Value.for_attribute`'s own coercion door raises on the
208
+ # resolved raw source (malformed fuzzer input, never a real
209
+ # corpus shape).
210
+ RECOMPUTABLE_MUTATION_OPS = %i[append remove multiply clamp set].freeze
133
211
 
134
212
  def mutations_match_recompute(history)
135
213
  bluebooks = history.fetch(:bluebooks)
@@ -140,8 +218,14 @@ module Hecks
140
218
  command = command_for_verb(bluebooks, entry[:verb])
141
219
  next [] unless command
142
220
 
221
+ aggregate = aggregate_for_verb(bluebooks, entry[:verb])
222
+ next [] unless aggregate
223
+
224
+ owner = owner_for_verb(bluebooks, entry[:verb]) || aggregate
225
+
143
226
  command.mutations.select { |m| RECOMPUTABLE_MUTATION_OPS.include?(m.op) }.filter_map do |mutation|
144
- expected = recompute_mutation(mutation, entry[:before][mutation.target], entry[:args], entry[:before])
227
+ expected = recompute_mutation(mutation, entry[:before][mutation.target], entry[:args], entry[:before],
228
+ aggregate, command, owner)
145
229
  next if expected == :unrecomputable
146
230
 
147
231
  actual = entry[:after][mutation.target]
@@ -155,31 +239,227 @@ module Hecks
155
239
  offenders.empty? || offenders.join("; ")
156
240
  end
157
241
 
158
- def recompute_mutation(mutation, current, args, before_scope)
242
+ # `command_for_verb`'s own root-aggregate half (Guards)
243
+ # re-derived independently rather than read off `entry[:domain]`/
244
+ # `entry[:aggregate]` (present on a real `build_mutation_trace`
245
+ # entry, but not on every hand-built fixture this property is
246
+ # tested against) so this works from `entry[:verb]` alone, the
247
+ # one field every entry always carries. BUG#5's fix needs the
248
+ # root aggregate specifically — `Value.for_attribute` resolves a
249
+ # value-object type against the root's own namespace only, the
250
+ # same reason `EntityElement#locate_chain` threads `root_aggregate`
251
+ # through every hop separately from each hop's own `owner`.
252
+ def aggregate_for_verb(bluebooks, verb)
253
+ domain_name, aggregate_name, = Naming.split_verb(verb)
254
+ return nil unless domain_name
255
+
256
+ bluebooks[domain_name]&.aggregate(aggregate_name)
257
+ end
258
+
259
+ # `mutation.target`'s own declaring construct — the root
260
+ # aggregate for an aggregate-owned command (`AddSlot`'s own
261
+ # `:slots`), or the entity a dot-shaped command belongs to
262
+ # (`Board.AddCard`'s own `:cards`, declared on `Board`, not on
263
+ # `Workspace`) — needed only by BUG#12's own recompute-side fix
264
+ # (`recompute_append`, below): `entity.attribute(mutation.target)`
265
+ # has to be asked of whichever construct actually declares it,
266
+ # never the root aggregate unconditionally, the same distinction
267
+ # `EntityElement#locate_chain` draws between `root_aggregate` and
268
+ # each hop's own `owner`. Re-derived independently from
269
+ # `entry[:verb]` alone, the same reasoning `aggregate_for_verb`'s
270
+ # own comment gives.
271
+ def owner_for_verb(bluebooks, verb)
272
+ domain_name, aggregate_name, command_path = Naming.split_verb(verb)
273
+ return nil unless command_path
274
+
275
+ aggregate = bluebooks[domain_name]&.aggregate(aggregate_name)
276
+ return nil unless aggregate
277
+ return aggregate unless command_path.include?(".")
278
+
279
+ entity_name, = command_path.split(".", 2)
280
+ aggregate.entities.find { |candidate| candidate.hecks_name == entity_name }
281
+ end
282
+
283
+ def recompute_mutation(mutation, current, args, before_scope, aggregate, command, owner = aggregate)
159
284
  case mutation.op
160
- when :append then recompute_append(current, mutation.source, before_scope, args)
285
+ when :append
286
+ recompute_append(current, mutation.source, before_scope, args, aggregate, command, owner, mutation.target)
161
287
  when :remove then recompute_remove(current, mutation.source, args)
162
288
  when :multiply then recompute_multiply(current, resolve_mutation_source(mutation.source, args))
163
289
  when :clamp then recompute_clamp(current, mutation.source)
290
+ when :set then recompute_set(mutation.source, args, aggregate, owner, mutation.target)
164
291
  end
165
292
  end
166
293
 
167
- # `MutationApplier#appended`'s own value-object branch (never the
168
- # entity_element branchsee #build_mutation_trace's own comment
169
- # on why an entity-dispatched command's own mutations never reach
170
- # it), reproduced: the field map resolved the SAME two-tier way
294
+ # `EntityElement#apply_to_element`'s own `:set` branch (entity_
295
+ # element.rb), reproduced independently not `MutationApplier#
296
+ # apply`'s aggregate-level twin (mutation_applier.rb), a
297
+ # different method with a different shape: `build_mutation_trace`
298
+ # (replay.rb) only ever captures an entity-owned command's own
299
+ # mutation (`command_name&.include?(".")`, that method's own
300
+ # header), so `mutations_match_recompute` can only ever be
301
+ # checking `apply_to_element`'s branch, never `apply`'s — the
302
+ # same distinction `owner_for_verb`'s own comment already draws
303
+ # for `:append`. Confirmed by reading `apply_to_element` directly:
304
+ # its own `:set` branch resolves the source through `rules.
305
+ # resolve_source` unconditionally (no `StateRef` branch at all —
306
+ # unlike the aggregate-level `apply`, an entity-owned `sets` has
307
+ # no declared corpus site using `state(:x)` today, so this
308
+ # reproduces what ships, not a hypothetical), reads the
309
+ # attribute off `entity.attribute(mutation.target)` (the owning
310
+ # entity, `owner` here — `Board`, never `Workspace`), and coerces
311
+ # through `Value.for_attribute(aggregate, attribute, value)`
312
+ # (the root aggregate, for value-object namespace resolution
313
+ # only — `value_object_for(aggregate, attribute.type)` — the
314
+ # same aggregate/owner split `owner_for_verb`'s own comment
315
+ # explains for `recompute_append`). An earlier version of this
316
+ # method mirrored `MutationApplier#apply`'s `:set` branch instead
317
+ # (StateRef-aware, coerced against `aggregate.attribute` rather
318
+ # than `owner.attribute`) and false-positived on every real
319
+ # entity-owned `sets` in the corpus — `NestedPieces::Workspace.
320
+ # Board.Label` (`sets :label`) has no `:label` attribute on
321
+ # `Workspace` at all, so `Value.for(aggregate, :label, raw)`
322
+ # silently passed the raw, uncoerced String through instead of
323
+ # wrapping it as `BoardLabel`, and the comparison below then
324
+ # disagreed with the real, correctly-coerced `after` state on
325
+ # every single run — caught immediately by running this against
326
+ # `qa/stress_domains/nested_pieces` before this comment existed,
327
+ # not by inspection alone.
328
+ #
329
+ # Reusing `Value.for_attribute` for the coercion step, rather
330
+ # than re-deriving it, is the same "coercion is its own already-
331
+ # guaranteed door" reasoning `GUARANTEED_BY_CONSTRUCTION` states
332
+ # and `#coerce_recompute_append_arg` already leans on for BUG#5 —
333
+ # what this property exists to check is the source resolution and
334
+ # routing (did the right raw value, from the right source, land
335
+ # on the right target?), not whether `Value.for_attribute` itself
336
+ # coerces correctly (a separate, already-enforced concern).
337
+ # Rescued broadly: a `:set` mutation trace is only ever captured
338
+ # after a real, already-admitted dispatch (`Replay#build_
339
+ # mutation_trace`'s own header), so a raise here means this
340
+ # recomputation resolved the wrong raw material, not that the
341
+ # real dispatch was itself malformed — `:unrecomputable`, not a
342
+ # crash, the same discipline every other branch in this method
343
+ # already follows for the generator's own deliberate malforming.
344
+ def recompute_set(source, args, aggregate, owner, target)
345
+ raw = resolve_mutation_source(source, args)
346
+ attribute = owner&.attribute(target)
347
+ coerced = attribute ? Runtime::Value.for_attribute(aggregate, attribute, raw) : raw
348
+ Runtime::Value.materialize(coerced)
349
+ rescue StandardError
350
+ :unrecomputable
351
+ end
352
+
353
+ # `EntityElement#appended_to_element`'s own field-mapping half,
354
+ # reproduced: the field map resolved the same two-tier way
171
355
  # (`MutationApplier#resolve_append_source` — a caller-supplied
172
- # arg, or the entity's own current field), then appended.
173
- def recompute_append(current, source_map, before_scope, args)
174
- fields = source_map.transform_values { |source| resolve_mutation_append_field(source, before_scope, args) }
356
+ # arg, or the entity's own current field), then appended. (An
357
+ # entity-dispatched command's own mutations do reach here —
358
+ # `#build_mutation_trace`'s own comment describing them as never
359
+ # reaching "the entity_element branch" means `MutationApplier#
360
+ # appended`'s own aggregate-level entity_element fallback
361
+ # specifically, which really is unreached from here; entity-owned
362
+ # append dispatches go through `EntityElement#appended_to_element`
363
+ # instead, and do reach this method.)
364
+ #
365
+ # BUG#5 — an entity-owned `:append` whose target field is itself
366
+ # value-object-typed (`Board.AddCard`'s own `sets :cards, append:
367
+ # { sequence: :sequence }`, `CardSequence`-typed). A caller-
368
+ # supplied arg reaches the real applier (`EntityElement#
369
+ # appended_to_element`) already coerced: `Interpreting#
370
+ # coerce_declared_arguments` runs `Value.for_attribute` over
371
+ # every arg the acting command itself declares, before dispatch
372
+ # ever reaches a mutation applier at all — independent of, and
373
+ # earlier than, anything `appended_to_element`'s own value_object
374
+ # check does. `before_scope[source]` (the entity's own current
375
+ # field) needs no such re-coercion here: it's already the
376
+ # materialized shape `build_mutation_trace` snapshotted it in
377
+ # (`Value.materialize`, same as `entry[:after]`), not a raw value
378
+ # sitting behind a live `Value`.
379
+ #
380
+ # BUG#12 — `owner`/`target` (new here) let this also reproduce
381
+ # `EntityElement#fill_declared_defaults`'s own entity-nested-in-
382
+ # entity fallback (`appended_to_element`'s `else` branch, when
383
+ # the appended element is itself an entity — `Card`, nested
384
+ # inside `Board` — not a value object): `owner.attribute(target)
385
+ # &.type` names the appended element's own type; when that names
386
+ # an entity of `aggregate` rather than a value object, every one
387
+ # of its own declared attributes `fields` doesn't already hold
388
+ # gets `Instance.default_for`'s own default — reused, not
389
+ # reimplemented, for the identical "never agree with itself"
390
+ # reason BUG#5's own coercion re-derivation above already gives:
391
+ # `Instance.default_for` is pre-existing, independently-tested
392
+ # machinery (an ordinary aggregate's own creation already runs
393
+ # through it via `Instance.defaults`), not the new glue
394
+ # (`fill_declared_defaults` itself) this property exists to
395
+ # catch a drift in.
396
+ def recompute_append(current, source_map, before_scope, args, aggregate, command, owner = aggregate, target = nil)
397
+ fields = source_map.transform_values do |source|
398
+ resolve_mutation_append_field(source, before_scope, args, aggregate, command)
399
+ end
400
+ fill_recompute_declared_defaults(aggregate, owner, target, fields)
175
401
  Array(current) + [symbolize_deep(fields)]
176
402
  end
177
403
 
178
- def resolve_mutation_append_field(source, before_scope, args)
404
+ # BUG#12's own recompute-side half — see `#recompute_append`'s
405
+ # own comment above for why this exists and why it reuses
406
+ # `Instance.default_for` rather than calling `EntityElement#
407
+ # fill_declared_defaults` again. A no-op whenever `target` names
408
+ # no attribute at all (every RECOMPUTABLE_MUTATION_OPS caller but
409
+ # `:append` passes no `target`) or `target`'s own declared type
410
+ # isn't an entity nested directly under `owner` (a value object,
411
+ # or nothing declared at all — `owner.attribute` answering `nil`
412
+ # for a target the DSL itself would already have refused at
413
+ # build time). `owner.entities`, not `aggregate.entities` — a
414
+ # piece nested inside a piece is a child of the owning entity
415
+ # (`Card` is `Board.entities`, never `Workspace.entities`), the
416
+ # same distinction `EntityElement#appended_to_element`'s own fix
417
+ # draws.
418
+ def fill_recompute_declared_defaults(aggregate, owner, target, fields)
419
+ return fields unless target
420
+
421
+ element_type = owner&.attribute(target)&.type
422
+ entity = element_type && owner.entities.find { |piece| piece.hecks_name == element_type.to_s }
423
+ return fields unless entity
424
+
425
+ entity.attributes.each do |attribute|
426
+ next if fields.key?(attribute.name)
427
+
428
+ fields[attribute.name] = attribute.list? ? [] : Runtime::Instance.default_for(aggregate, attribute)
429
+ end
430
+ fields
431
+ end
432
+
433
+ def resolve_mutation_append_field(source, before_scope, args, aggregate, command)
179
434
  return source unless source.is_a?(Symbol)
180
- return args[source] if args.key?(source)
435
+ return before_scope[source] unless args.key?(source)
436
+
437
+ coerce_recompute_append_arg(aggregate, command, source, args[source])
438
+ end
439
+
440
+ # `Interpreting#coerce_declared_arguments`'s own coercion,
441
+ # reproduced independently (never calling it again, the same
442
+ # "never agree with itself" rule this whole module's header
443
+ # comment gives) — a raw arg is coerced only when its own name
444
+ # (`source`) is one of the acting command's own declared
445
+ # attributes, exactly the condition that method checks before a
446
+ # real dispatch ever coerces it either. `command.attribute(source)`
447
+ # answering `nil` (a source that names no declared attribute —
448
+ # never possible for `coerce_declared_arguments` to have touched
449
+ # it in the real dispatch either) leaves `raw` exactly as it
450
+ # arrived, the same as every bare-scalar append this already
451
+ # handled correctly before BUG#5's fix.
452
+ #
453
+ # `Value.materialize`d immediately after coercing — matching the
454
+ # plain-data shape `entry[:before]`/`entry[:after]` already carry
455
+ # throughout this whole property, so the eventual `symbolize_deep`
456
+ # comparison is always materialized-against-materialized, never a
457
+ # live `Value` against a Hash.
458
+ def coerce_recompute_append_arg(aggregate, command, source, raw)
459
+ attribute = command.attribute(source)
460
+ return raw unless attribute
181
461
 
182
- before_scope[source]
462
+ Runtime::Value.materialize(Runtime::Value.for_attribute(aggregate, attribute, raw, argument: true))
183
463
  end
184
464
 
185
465
  # `MutationApplier#removed`'s own value-equality match, reproduced.
@@ -192,7 +472,7 @@ module Hecks
192
472
  # reproduced on plain materialized data instead of a real Value:
193
473
  # a single-numeric-field Hash (the VO-typed case — ListCount, one
194
474
  # Integer field) scales that field ; a bare Numeric scales itself.
195
- # `current ||= 0` — the SAME phantom-field fallback #multiply
475
+ # `current ||= 0` — the same phantom-field fallback #multiply
196
476
  # itself already gives (unaffected by this session's #clamp fix,
197
477
  # since #multiply never needed one).
198
478
  def recompute_multiply(current, amount)
@@ -212,8 +492,8 @@ module Hecks
212
492
  end
213
493
 
214
494
  # `CommandRules::Arithmetic#clamp`'s own two branches, reproduced
215
- # the same way #recompute_multiply is — including THIS SESSION'S
216
- # OWN `current ||= 0` fix (command_rules/arithmetic.rb), the one
495
+ # the same way #recompute_multiply is — including this session's
496
+ # own `current ||= 0` fix (command_rules/arithmetic.rb), the one
217
497
  # arithmetic op that didn't have it until now. `mutation.source`
218
498
  # is always a literal `[min, max]`, never an argument reference
219
499
  # (MutationApplier's own comment on why `resolve_source` is
@@ -236,18 +516,18 @@ module Hecks
236
516
  end
237
517
 
238
518
  # `CommandRules::Arithmetic#resolve_source`, reproduced: a
239
- # mutation's source is either the NAME OF AN ARGUMENT or a
240
- # LITERAL, told apart by type.
519
+ # mutation's source is either the name of an argument or a
520
+ # literal, told apart by type.
241
521
  def resolve_mutation_source(source, args)
242
522
  source.is_a?(Symbol) ? args[source] : source
243
523
  end
244
524
 
245
- # A generated step's own `args` arrive with STRING keys on every
525
+ # A generated step's own `args` arrive with string keys on every
246
526
  # nested Hash (the wire/JSON shape `spec/corpus/*.json` already
247
527
  # uses) while `history[:mutation_traces]`' own materialized
248
- # before/after state carries SYMBOL keys throughout (Runtime::
528
+ # before/after state carries symbol keys throughout (Runtime::
249
529
  # Value.materialize's own convention) — two hashes holding the
250
- # identical fact compare UNEQUAL by Ruby's own `Hash#==` unless
530
+ # identical fact compare unequal by Ruby's own `Hash#==` unless
251
531
  # both sides are normalized the same way first. Recursive, since
252
532
  # an appended/removed element can itself nest a value object
253
533
  # (RemoveTag's own `Tag` argument, `{"key"=>..., "value"=>...}`).