hecks 1.3.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 (355) 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 +5 -4
  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/prism.rb +4 -4
  23. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  24. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  25. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  26. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  27. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  28. data/lib/hecks/adapters/driven.rb +4 -4
  29. data/lib/hecks/adapters/driving/github_webhook.rb +18 -18
  30. data/lib/hecks/behaviors/dsl.rb +2 -2
  31. data/lib/hecks/behaviors/expectations.rb +23 -23
  32. data/lib/hecks/behaviors/ir.rb +1 -1
  33. data/lib/hecks/behaviors/rspec.rb +1 -1
  34. data/lib/hecks/behaviors/runner.rb +2 -2
  35. data/lib/hecks/behaviors.rb +1 -1
  36. data/lib/hecks/bluebook/aggregate.rb +13 -13
  37. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
  38. data/lib/hecks/bluebook/assembly/build.rb +1 -1
  39. data/lib/hecks/bluebook/assembly/contract.rb +39 -16
  40. data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
  41. data/lib/hecks/bluebook/assembly/marks.rb +18 -18
  42. data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
  43. data/lib/hecks/bluebook/assembly.rb +9 -9
  44. data/lib/hecks/bluebook/attribute.rb +9 -9
  45. data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
  46. data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
  47. data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
  48. data/lib/hecks/bluebook/behaviour/command.rb +23 -23
  49. data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -7
  50. data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
  51. data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
  52. data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
  53. data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
  54. data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
  55. data/lib/hecks/bluebook/behaviour/query.rb +1 -1
  56. data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
  57. data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
  58. data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
  59. data/lib/hecks/bluebook/capabilities.rb +27 -0
  60. data/lib/hecks/bluebook/chapter.rb +28 -9
  61. data/lib/hecks/bluebook/command.rb +12 -12
  62. data/lib/hecks/bluebook/domain_port.rb +9 -9
  63. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  64. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  65. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
  66. data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
  67. data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
  68. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
  69. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
  70. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  71. data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
  72. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  73. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  74. data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
  75. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  76. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
  77. data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
  78. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  79. data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
  80. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  81. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  82. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
  83. data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
  84. data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
  85. data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
  86. data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
  87. data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
  88. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  89. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  90. data/lib/hecks/bluebook/entity.rb +11 -11
  91. data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
  92. data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
  93. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
  94. data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
  95. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
  96. data/lib/hecks/bluebook/expression/resolver.rb +60 -62
  97. data/lib/hecks/bluebook/hexagon.rb +1 -1
  98. data/lib/hecks/bluebook/lifecycle.rb +1 -1
  99. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
  100. data/lib/hecks/bluebook/meta_validator/judge.rb +101 -87
  101. data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
  102. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
  103. data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
  104. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
  105. data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
  106. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
  107. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
  108. data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
  109. data/lib/hecks/bluebook/meta_validator.rb +70 -70
  110. data/lib/hecks/bluebook/model_check.rb +170 -84
  111. data/lib/hecks/bluebook/pattern_subset.rb +9 -9
  112. data/lib/hecks/bluebook/policy.rb +15 -13
  113. data/lib/hecks/bluebook/process_manager.rb +14 -14
  114. data/lib/hecks/bluebook/project_register.rb +6 -6
  115. data/lib/hecks/bluebook/query.rb +4 -4
  116. data/lib/hecks/bluebook/read_model.rb +14 -14
  117. data/lib/hecks/bluebook/reference.rb +8 -8
  118. data/lib/hecks/bluebook/smoke_test.rb +19 -19
  119. data/lib/hecks/bluebook/synthesizer.rb +12 -12
  120. data/lib/hecks/bluebook/translation.rb +4 -4
  121. data/lib/hecks/bluebook/value_object.rb +6 -6
  122. data/lib/hecks/bluebook.rb +2 -2
  123. data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
  124. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
  125. data/lib/hecks/codemod.rb +36 -35
  126. data/lib/hecks/construct.rb +6 -6
  127. data/lib/hecks/corpus.rb +317 -0
  128. data/lib/hecks/deprecation.rb +95 -0
  129. data/lib/hecks/doc/reference.rb +19 -19
  130. data/lib/hecks/embryonaut_bluebook.rb +11 -11
  131. data/lib/hecks/facade/cli_door.rb +69 -10
  132. data/lib/hecks/facade/cli_runner.rb +105 -24
  133. data/lib/hecks/facade/command_request.rb +23 -0
  134. data/lib/hecks/facade/handle.rb +79 -32
  135. data/lib/hecks/facade/json_door.rb +106 -25
  136. data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
  137. data/lib/hecks/facade/surface/chapter.rb +26 -17
  138. data/lib/hecks/facade/surface.rb +16 -3
  139. data/lib/hecks/facade.rb +15 -4
  140. data/lib/hecks/forms/app.rb +46 -30
  141. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  142. data/lib/hecks/forms/field_renderer.rb +142 -6
  143. data/lib/hecks/forms/field_shape.rb +180 -19
  144. data/lib/hecks/forms/html.rb +51 -7
  145. data/lib/hecks/forms/index_renderer.rb +14 -2
  146. data/lib/hecks/forms/params.rb +120 -23
  147. data/lib/hecks/forms/port_argument.rb +2 -2
  148. data/lib/hecks/forms/query_form_renderer.rb +2 -2
  149. data/lib/hecks/forms/record_renderer.rb +2 -2
  150. data/lib/hecks/forms/record_table.rb +1 -1
  151. data/lib/hecks/forms/value_object_shape.rb +3 -3
  152. data/lib/hecks/forms.rb +24 -4
  153. data/lib/hecks/fqn.rb +1 -1
  154. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  155. data/lib/hecks/framework.rb +48 -17
  156. data/lib/hecks/freezer.rb +11 -11
  157. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
  158. data/lib/hecks/fuzzing/combination_miner.rb +119 -0
  159. data/lib/hecks/fuzzing/concurrent_dispatch.rb +140 -45
  160. data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
  161. data/lib/hecks/fuzzing/differential.rb +158 -0
  162. data/lib/hecks/fuzzing/domain_generator.rb +694 -0
  163. data/lib/hecks/fuzzing/era_boundary.rb +37 -18
  164. data/lib/hecks/fuzzing/form_census.rb +35 -20
  165. data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
  166. data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
  167. data/lib/hecks/fuzzing/isolated_boot.rb +62 -62
  168. data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
  169. data/lib/hecks/fuzzing/persistence_parity.rb +19 -21
  170. data/lib/hecks/fuzzing/properties/corrections.rb +9 -9
  171. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +154 -49
  172. data/lib/hecks/fuzzing/properties/guards.rb +42 -42
  173. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
  174. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +22 -35
  175. data/lib/hecks/fuzzing/properties/outbox.rb +24 -24
  176. data/lib/hecks/fuzzing/properties/querying.rb +17 -17
  177. data/lib/hecks/fuzzing/properties.rb +64 -40
  178. data/lib/hecks/fuzzing/qa_settings.rb +152 -0
  179. data/lib/hecks/fuzzing/replay.rb +114 -114
  180. data/lib/hecks/fuzzing/rotation_priority.rb +10 -10
  181. data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
  182. data/lib/hecks/fuzzing/self_consistency.rb +141 -112
  183. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +35 -35
  184. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  185. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  186. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  187. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +58 -31
  188. data/lib/hecks/fuzzing/sequence_generator.rb +111 -23
  189. data/lib/hecks/fuzzing/shrinker.rb +197 -0
  190. data/lib/hecks/fuzzing/structural_skips.rb +23 -130
  191. data/lib/hecks/fuzzing/sweep_depth.rb +4 -4
  192. data/lib/hecks/fuzzing/target_capabilities.rb +49 -18
  193. data/lib/hecks/fuzzing/value_generator.rb +14 -14
  194. data/lib/hecks/fuzzing.rb +5 -0
  195. data/lib/hecks/grammar/evolve.rb +10 -10
  196. data/lib/hecks/grammar.rb +7 -7
  197. data/lib/hecks/ir.rb +13 -13
  198. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  199. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  200. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  201. data/lib/hecks/language/oidc.json +5 -0
  202. data/lib/hecks/literal.rb +9 -9
  203. data/lib/hecks/naming.rb +25 -25
  204. data/lib/hecks/ports/access_control.rb +58 -2
  205. data/lib/hecks/ports/agent/answers.rb +83 -6
  206. data/lib/hecks/ports/agent.rb +119 -35
  207. data/lib/hecks/ports/authentication.rb +44 -4
  208. data/lib/hecks/ports/authorization.rb +53 -11
  209. data/lib/hecks/ports/clock.rb +42 -23
  210. data/lib/hecks/ports/extraction.rb +16 -0
  211. data/lib/hecks/ports/identity_assignment.rb +24 -2
  212. data/lib/hecks/ports/identity_generation.rb +17 -3
  213. data/lib/hecks/ports/identity_resolution.rb +18 -1
  214. data/lib/hecks/ports/loading.rb +4 -0
  215. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  216. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  217. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  218. data/lib/hecks/ports/persistence/execution.rb +4 -0
  219. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  220. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  221. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  222. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  223. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  224. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  225. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  226. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  227. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  228. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  229. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  230. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  231. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  241. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  242. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  243. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  244. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
  245. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  246. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  247. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  248. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
  249. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
  250. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
  251. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  252. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  253. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  254. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  255. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  256. data/lib/hecks/ports/persistence.rb +36 -1
  257. data/lib/hecks/ports/projection.rb +61 -7
  258. data/lib/hecks/ports/query/in_memory.rb +3 -3
  259. data/lib/hecks/ports/query/ordering.rb +6 -6
  260. data/lib/hecks/ports/query.rb +35 -0
  261. data/lib/hecks/projections/bootstrap_table.rb +112 -0
  262. data/lib/hecks/projections/diagrams.rb +75 -75
  263. data/lib/hecks/projections/glossary/html.rb +3 -3
  264. data/lib/hecks/projections/glossary/markdown.rb +3 -3
  265. data/lib/hecks/projections/glossary/mermaid.rb +1 -1
  266. data/lib/hecks/projections/glossary/sections.rb +1 -1
  267. data/lib/hecks/projections/glossary/sentences.rb +5 -5
  268. data/lib/hecks/projections/glossary.rb +15 -15
  269. data/lib/hecks/projections/ir.rb +1 -1
  270. data/lib/hecks/projections/model/deviations.rb +18 -17
  271. data/lib/hecks/projections/model.rb +25 -21
  272. data/lib/hecks/projections/oidc.rb +7 -7
  273. data/lib/hecks/projections/parser_table.rb +5 -5
  274. data/lib/hecks/projections/reference.rb +3 -3
  275. data/lib/hecks/projections/rust_vocabulary.rb +443 -0
  276. data/lib/hecks/projections/shape.rb +2 -2
  277. data/lib/hecks/projections/statements.rb +11 -11
  278. data/lib/hecks/projections/vocabulary.rb +9 -9
  279. data/lib/hecks/projections.rb +5 -3
  280. data/lib/hecks/projector/cli_projector.rb +29 -29
  281. data/lib/hecks/projector/docs_projector.rb +13 -13
  282. data/lib/hecks/projector/exporter.rb +42 -21
  283. data/lib/hecks/projector/ir_projector.rb +1 -1
  284. data/lib/hecks/projector/narrate_projector.rb +11 -11
  285. data/lib/hecks/projector/target.rb +13 -13
  286. data/lib/hecks/projector.rb +15 -15
  287. data/lib/hecks/query_ir.rb +47 -47
  288. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  289. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  290. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  291. data/lib/hecks/query_specification/common/null_policy.rb +57 -13
  292. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  293. data/lib/hecks/query_specification/common/options.rb +25 -0
  294. data/lib/hecks/query_specification/field_path.rb +69 -15
  295. data/lib/hecks/query_specification/hop_path.rb +57 -20
  296. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  297. data/lib/hecks/rendering.rb +3 -3
  298. data/lib/hecks/router/namespace_installer.rb +3 -3
  299. data/lib/hecks/router.rb +1 -1
  300. data/lib/hecks/runtime/aggregate_lock.rb +11 -11
  301. data/lib/hecks/runtime/boot_gates.rb +3 -3
  302. data/lib/hecks/runtime/caller.rb +8 -8
  303. data/lib/hecks/runtime/capability_graph.rb +2 -2
  304. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
  305. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +42 -63
  306. data/lib/hecks/runtime/command_interpreter.rb +159 -102
  307. data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
  308. data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
  309. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  310. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  311. data/lib/hecks/runtime/command_rules/references.rb +35 -35
  312. data/lib/hecks/runtime/command_rules.rb +3 -0
  313. data/lib/hecks/runtime/dependency_planning.rb +11 -11
  314. data/lib/hecks/runtime/dispatcher.rb +283 -90
  315. data/lib/hecks/runtime/entity_element.rb +151 -67
  316. data/lib/hecks/runtime/entity_interpreter.rb +108 -82
  317. data/lib/hecks/runtime/errors.rb +19 -19
  318. data/lib/hecks/runtime/event.rb +6 -6
  319. data/lib/hecks/runtime/identity.rb +22 -22
  320. data/lib/hecks/runtime/instance.rb +39 -14
  321. data/lib/hecks/runtime/interpreting.rb +12 -12
  322. data/lib/hecks/runtime/invocation.rb +276 -0
  323. data/lib/hecks/runtime/loader.rb +14 -14
  324. data/lib/hecks/runtime/outbox.rb +23 -23
  325. data/lib/hecks/runtime/policy_interpreter.rb +54 -54
  326. data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
  327. data/lib/hecks/runtime/query_interpreter.rb +55 -64
  328. data/lib/hecks/runtime/reaction_invocation.rb +20 -20
  329. data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
  330. data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
  331. data/lib/hecks/runtime/reference_hop.rb +6 -6
  332. data/lib/hecks/runtime/refusal_wording.rb +92 -115
  333. data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
  334. data/lib/hecks/runtime/registry/verification.rb +36 -26
  335. data/lib/hecks/runtime/registry.rb +45 -28
  336. data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
  337. data/lib/hecks/runtime/routing.rb +10 -153
  338. data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
  339. data/lib/hecks/runtime/saga_interpreter.rb +64 -64
  340. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  341. data/lib/hecks/runtime/tenant_check.rb +9 -9
  342. data/lib/hecks/runtime/tenant_scope.rb +5 -5
  343. data/lib/hecks/runtime/value/admission.rb +75 -30
  344. data/lib/hecks/runtime/value/coercion.rb +318 -127
  345. data/lib/hecks/runtime/value/entity_list_coercion.rb +34 -34
  346. data/lib/hecks/runtime/value.rb +21 -21
  347. data/lib/hecks/runtime.rb +7 -7
  348. data/lib/hecks/storehouse.rb +64 -64
  349. data/lib/hecks/version.rb +3 -3
  350. data/lib/hecks/vocabulary.rb +205 -4
  351. data/lib/hecks.rb +13 -11
  352. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
  353. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
  354. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
  355. metadata +22 -2
@@ -8,10 +8,10 @@ require_relative "instance"
8
8
 
9
9
  module Hecks
10
10
  module Runtime
11
- # ONE ENTITY ELEMENT, LOCATED AND MUTATED — the walk-and-write half of
11
+ # **One entity element, located and mutated** — the walk-and-write half of
12
12
  # dispatching into a piece an aggregate holds, factored out of
13
- # `EntityInterpreter` so a SECOND caller (`CommandInterpreter`'s own
14
- # `delegate_to_entity` step) can locate and mutate the SAME element the
13
+ # `EntityInterpreter` so a second caller (`CommandInterpreter`'s own
14
+ # `delegate_to_entity` step) can locate and mutate the same element the
15
15
  # same way, against an aggregate record it already holds in memory
16
16
  # rather than one freshly loaded from a repository. `EntityInterpreter`
17
17
  # keeps its own dispatch order and Context; this is the part underneath
@@ -27,19 +27,19 @@ module Hecks
27
27
 
28
28
  # BUG#3 — an addressing value that can never match a stored element,
29
29
  # returned from `element_of`'s own `wants` coercion in place of a real
30
- # `Value` (below). A unique object, never `nil`: a stored, OPTIONAL
31
- # element field really can hold `nil`, and comparing THAT against a
30
+ # `Value` (below). A unique object, never `nil`: a stored, optional
31
+ # element field really can hold `nil`, and comparing that against a
32
32
  # bare `nil` sentinel would accidentally "match" it.
33
33
  UNMATCHABLE = Object.new.freeze
34
34
  private_constant :UNMATCHABLE
35
35
 
36
- # ONE HOP PER CHAIN ENTRY. `container` starts as `instance` (the root
36
+ # **One hop per chain entry**. `container` starts as `instance` (the root
37
37
  # aggregate record) and becomes each just-located element in turn —
38
- # Dispatch's own element is found INSIDE the Handler element
38
+ # Dispatch's own element is found inside the Handler element
39
39
  # `locate_chain` located the step before, never inside `instance`
40
- # directly. `owner` is whichever construct's OWN attribute declares
40
+ # directly. `owner` is whichever construct's own attribute declares
41
41
  # the list being searched (Handler declares `dispatches` ; the root
42
- # aggregate declares `handlers`) — `root_aggregate` stays the ROOT
42
+ # aggregate declares `handlers`) — `root_aggregate` stays the root
43
43
  # the whole way through instead, passed to `element_of` separately,
44
44
  # because coercion (`Value.for_attribute`) resolves value objects
45
45
  # against the root's own namespace only ; an entity must never
@@ -63,7 +63,7 @@ module Hecks
63
63
  container
64
64
  end
65
65
 
66
- # ONE ELEMENT, MATCHED ON EVERY PART OF ITS IDENTITY — not just the first.
66
+ # One element, matched on every part of its identity — not just the first.
67
67
  # A piece's identity may be several paths, the same shape a head's can be,
68
68
  # so a dispatch that names the element has to supply every part and every
69
69
  # part has to agree with the stored one. `routed_identity`, when given,
@@ -72,8 +72,8 @@ module Hecks
72
72
  # which element it means, so re-deriving `wants` from `args` would be
73
73
  # redundant at best and wrong if `args` no longer carries that identity
74
74
  # at all.
75
- # Locate, then copy-before-mutate, in that order — see the "ONE
76
- # LEVEL DEEPER" comment below on why the copy has to happen exactly
75
+ # Locate, then copy-before-mutate, in that order — see the "one
76
+ # level deeper" comment below on why the copy has to happen exactly
77
77
  # where it does (aliasing the adapter's own record otherwise).
78
78
  # Splitting resolution from the copy/write-back would separate two
79
79
  # halves of one aliasing-safety invariant across method boundaries.
@@ -84,27 +84,27 @@ module Hecks
84
84
  def element_of(root_aggregate, owner, entity, command_name, container, args, routed_identity = nil)
85
85
  entity_name = entity.hecks_name
86
86
  list_attr = owner.attributes.find { |a| a.list? && a.type.to_s == entity_name } ||
87
- raise(UnknownVerb, RefusalWording.render("UnknownVerb", "entity_holds_no_list",
88
- aggregate: owner.hecks_name, entity: entity_name))
87
+ raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "entity_holds_no_list",
88
+ aggregate: owner.hecks_name, entity: entity_name))
89
89
 
90
90
  wants = unless routed_identity
91
91
  entity.identity_paths.map do |path|
92
92
  head = path.to_s.split(".").first.to_sym
93
93
  raw = args[head] ||
94
- raise(NotFound, RefusalWording.render("NotFound", "entity_element_no_identity",
95
- command: command_name, entity: entity_name,
96
- identity: Identity.reading(entity)))
94
+ raise(NotFound, RefusalWording.render_site("NotFound", "entity_element_no_identity",
95
+ command: command_name, entity: entity_name,
96
+ identity: Identity.reading(entity)))
97
97
 
98
- # AN IDENTITY OFFERED FOR ADDRESSING, NOT FOR STORAGE
98
+ # An identity offered for addressing, not for storage
99
99
  # (BUG#3, found live by `bin/qa_sweep` — banking fuzz seed
100
100
  # 23, `LedgerEntry.Amend sequence: { value: 0 }` against an
101
101
  # entry-less ledger). Coercing it all the way to a typed
102
- # `Value` here ran that type's own invariant BEFORE this
102
+ # `Value` here ran that type's own invariant before this
103
103
  # method ever checks whether any element matches — a
104
104
  # `sequence: 0` against `LedgerSequence`'s own "a ledger
105
105
  # sequence is positive" invariant raised InvariantViolation,
106
106
  # not NotFound, even when (as here) nothing was ever posted
107
- # at all. Every element actually IN the list already
107
+ # at all. Every element actually in the list already
108
108
  # satisfied its own type's invariant the moment it was
109
109
  # created, so a value that fails it can never equal one —
110
110
  # degrading to `UNMATCHABLE` here, instead of propagating,
@@ -114,7 +114,7 @@ module Hecks
114
114
  # `extract_id`/`extract_wants` (a raw scalar read, never a
115
115
  # typed rebuild — rust/src/generated/*/*.rs) and
116
116
  # `Identity.from`'s own raw-comparison convention for a
117
- # ROOT aggregate's identity (this file's sibling,
117
+ # root aggregate's identity (this file's sibling,
118
118
  # `identity.rb`). `raw` rides alongside `want` so the
119
119
  # eventual NotFound below can still quote what was offered.
120
120
  want = begin
@@ -136,7 +136,7 @@ module Hecks
136
136
  end
137
137
  end
138
138
  unless position
139
- raise NotFound, RefusalWording.render(
139
+ raise NotFound, RefusalWording.render_site(
140
140
  "NotFound", "entity_element_missing",
141
141
  entity: entity_name, identity: Identity.reading(entity),
142
142
  wants: wants&.map { |_h, path, _want, raw| Identity.scalar(path, raw) }&.join(", "),
@@ -145,17 +145,17 @@ module Hecks
145
145
  )
146
146
  end
147
147
 
148
- # ONE LEVEL DEEPER THAN Instance#dup, for the same reason: a list
148
+ # One level deeper than Instance#dup, for the same reason: a list
149
149
  # attribute holds Hashes, and `apply_to_element` mutates the found
150
- # one IN PLACE — the update mechanism for an entity, not a bug. But
150
+ # one in place — the update mechanism for an entity, not a bug. But
151
151
  # in place means aliased with the adapter's own record until this
152
152
  # copies the array and the target element before handing either
153
153
  # back, and writes the fresh array into `container` so the copy is
154
- # what persists on success and NOTHING aliased survives a refusal.
154
+ # what persists on success and nothing aliased survives a refusal.
155
155
  # `container[list_attr.name] = copied` reaches `instance` itself
156
- # when this is the FIRST hop, and reaches the (already copied)
157
- # PARENT element when it is a later one — either way it is the
158
- # SAME already-fresh object `locate_chain` is about to hand back
156
+ # when this is the first hop, and reaches the (already copied)
157
+ # parent element when it is a later one — either way it is the
158
+ # same already-fresh object `locate_chain` is about to hand back
159
159
  # as `container` for the next hop, so nothing further has to
160
160
  # propagate a write back up the chain by hand.
161
161
  copied = original.dup
@@ -165,9 +165,9 @@ module Hecks
165
165
  element
166
166
  end
167
167
 
168
- # THE ELEMENT'S OWN IDENTITY, joined from its parts — the entity-level
169
- # twin of `Identity.of`, reading off the STORED ELEMENT (a Hash) rather
170
- # than a dispatch payload. An id is a SCALAR, and the PATH is how it is
168
+ # The element's own identity, joined from its parts — the entity-level
169
+ # twin of `Identity.of`, reading off the stored element (a Hash) rather
170
+ # than a dispatch payload. An id is a scalar, and the path is how it is
171
171
  # reached — never by opening a value object and taking whatever single
172
172
  # field is inside. That unwrapping is gone from the language: a piece
173
173
  # that does not name its fields is refused when the bluebook loads ("an
@@ -191,10 +191,10 @@ module Hecks
191
191
  # refuses what it cannot check applying nothing instead. `Member`/
192
192
  # `Dispatch` (S17) are the first real callers: both need to
193
193
  # append a value-object-typed element (`Pair`/`Binding`) onto a
194
- # list attribute THEY OWN, once they become entities of
194
+ # list attribute they own, once they become entities of
195
195
  # `ValueObject`/`ProcessManager` rather than separate aggregates.
196
196
  #
197
- # `:increment`/`:decrement`/`:multiply` ALSO fixed here, found
197
+ # `:increment`/`:decrement`/`:multiply` also fixed here, found
198
198
  # while proving this method against a real fixture: they wrapped
199
199
  # `amount` unconditionally whenever `attribute` existed, the same
200
200
  # asymmetric-wrapping shape `MutationApplier#rewrap_arithmetic_
@@ -246,13 +246,13 @@ module Hecks
246
246
  # `corrects` — BUG#30. `MutationApplier#apply`'s own aggregate-
247
247
  # level `:corrects` branch's own comment gives the full reasoning;
248
248
  # the same one applies here unchanged: this mutation targets no
249
- # field on THIS element at all — its own event name, and whether
250
- # the OWNING record has actually emitted it, was already checked
249
+ # field on this element at all — its own event name, and whether
250
+ # the owning record has actually emitted it, was already checked
251
251
  # once, up front, by `EntityInterpreter#step_enforce_givens`
252
252
  # (`CommandRules::Admissibility#enforce_correction_target`, called
253
- # there against the PARENT record/ROOT aggregate — see that
253
+ # there against the parent record/root aggregate — see that
254
254
  # step's own comment for exactly why). Whatever field a correction
255
- # actually changes is an ORDINARY declared `sets`/`increment`/etc.
255
+ # actually changes is an ordinary declared `sets`/`increment`/etc.
256
256
  # mutation of its own, applied by one of the branches above like
257
257
  # any other — `qa/stress_domains/corrections`' own `Entry.Amend`
258
258
  # pairs `corrects "EntryRecorded", ...` with a separate `sets
@@ -282,8 +282,8 @@ module Hecks
282
282
  end
283
283
 
284
284
  # `MutationApplier#resolve_append_source`'s own entity-scoped
285
- # twin — a caller-supplied ARG first, falling back to the
286
- # ELEMENT's own current field (never the parent instance's) when
285
+ # twin — a caller-supplied arg first, falling back to the
286
+ # element's own current field (never the parent instance's) when
287
287
  # it isn't one.
288
288
  def resolve_element_append_source(source, element, args)
289
289
  return source unless source.is_a?(Symbol)
@@ -293,25 +293,27 @@ module Hecks
293
293
  end
294
294
 
295
295
  # `MutationApplier#appended`'s own entity-scoped twin. Usually a
296
- # VALUE OBJECT element — an entity's own list, appended to by an
296
+ # value object element — an entity's own list, appended to by an
297
297
  # entity-owned command, holds a value object (`Member.pairs`'
298
298
  # own `Pair`, `Dispatch.with_spec`'s own `Binding`) the same way
299
299
  # most real corpus appends do — but entity-in-entity nesting (a
300
- # list of ANOTHER entity, owned by this one) is real now too:
300
+ # list of another entity, owned by this one) is real now too:
301
301
  # `qa/stress_domains/nested_pieces` (`Board.AddCard`, appending a
302
302
  # `Card` onto `Board`'s own `cards`) is the first corpus member to
303
303
  # do it, the comment this replaces having been written before that
304
304
  # domain existed. `element_type` naming an entity rather than a
305
305
  # value object falls through to `fields` unchanged, same as
306
- # before — `MutationApplier#entity_element`'s own identity-minting/
307
- # collision-checking fallback still isn't mirrored here (nothing
308
- # in this corpus needs auto-minting at THIS depth — Card supplies
309
- # its own identity in the append mapping — and collision-checking
310
- # a nested entity is its own separate, unfixed question) — but
311
- # BUG#12's fix (below) is: every declared attribute the append
312
- # mapping doesn't name gets its own default the same way a fresh
313
- # aggregate's own attributes already do (`Instance.defaults`),
314
- # whichever branch built `fields`.
306
+ # before — `MutationApplier#entity_element`'s own identity-minting
307
+ # fallback still isn't mirrored here (nothing in this corpus needs
308
+ # auto-minting at this depth — Card supplies its own identity in
309
+ # the append mapping)but its collision-checking fallback now is
310
+ # (BUG#145, `check_entity_collision`, below see its own call
311
+ # site's comment for why "collision-checking a nested entity is its
312
+ # own separate, unfixed question," this comment's own prior wording,
313
+ # stopped being true). BUG#12's fix (below) is likewise shared:
314
+ # every declared attribute the append mapping doesn't name gets its
315
+ # own default the same way a fresh aggregate's own attributes
316
+ # already do (`Instance.defaults`), whichever branch built `fields`.
315
317
  def appended_to_element(aggregate, entity, element, mutation, args)
316
318
  fields = mutation.source.transform_values { |source| resolve_element_append_source(source, element, args) }
317
319
  element_type = entity.attribute(mutation.target)&.type
@@ -323,22 +325,53 @@ module Hecks
323
325
  if value_object
324
326
  Value.build(value_object, fields, aggregate)
325
327
  else
326
- # `entity.entities`, NOT `aggregate.entities` — a piece
327
- # nested inside a piece is a child of the OWNING entity
328
+ # `entity.entities`, not `aggregate.entities` — a piece
329
+ # nested inside a piece is a child of the owning entity
328
330
  # (`Card` is `Board.entities`, never `Workspace.entities`;
329
331
  # `Behaviour::Entity#entities` answers direct children only,
330
332
  # by design — see its own comment), the same lexical-nesting
331
333
  # rule `EntityBuilder#entity_impl` builds the tree with in
332
334
  # the first place.
333
335
  nested_entity = entity.entities.find { |piece| piece.hecks_name == element_type.to_s }
334
- nested_entity ? fill_declared_defaults(aggregate, nested_entity, fields) : fields
336
+ if nested_entity
337
+ # BUG#145 — `MutationApplier#entity_element`'s own
338
+ # `check_entity_collision` call (one hop up, an aggregate's
339
+ # own entity list) never had a twin here: a nested entity's
340
+ # own identity is always caller-supplied at this depth
341
+ # (`Card.sequence` rides the append mapping directly — no
342
+ # nested-entity auto-mint exists anywhere in this corpus,
343
+ # see this method's own header), so this is unconditional,
344
+ # unlike `#entity_element`'s own auto-mint/collision `if`/
345
+ # `else` split — there is no auto-mint branch here to skip.
346
+ # Found live: `qa/stress_domains/nested_pieces`'s own
347
+ # differential sweep, `Board.AddCard` dispatched twice under
348
+ # the same `sequence` — Ruby silently appended a second
349
+ # `Card`, Rust's generated code (`rust/project/mutations.rb`'s
350
+ # `emit_mutation_line_body`, which never splits an
351
+ # aggregate-owned append from an entity-owned one the way
352
+ # this runtime's two separate methods do) already refused
353
+ # `AlreadyExists` for both depths.
354
+ # `entity` (the owner — `Board`), not `aggregate` (the root
355
+ # — `Workspace`), is what the refusal names as "on {…}" —
356
+ # `check_entity_collision`'s first argument is only ever
357
+ # used for that one naming purpose (`owner.hecks_name`,
358
+ # below), matching Rust's own generated wording exactly
359
+ # (`rust/project/mutations.rb`'s own `collision_guard`
360
+ # passes the entity's declaring construct's name the same
361
+ # way — "a Card already exists on Board", never "…on
362
+ # Workspace").
363
+ check_entity_collision(entity, nested_entity, element[mutation.target], fields)
364
+ fill_declared_defaults(aggregate, nested_entity, fields)
365
+ else
366
+ fields
367
+ end
335
368
  end
336
369
  Freezer.deep(Array(element[mutation.target]) + [appended])
337
370
  end
338
371
 
339
372
  # BUG#12 — an entity created via `sets :list, append: {...}` used
340
- # to leave any of its OWN declared attributes the append mapping
341
- # simply didn't name (an optional field a LATER, separate command
373
+ # to leave any of its own declared attributes the append mapping
374
+ # simply didn't name (an optional field a later, separate command
342
375
  # sets — `Board.label`, `Card.note`) absent from the stored hash
343
376
  # entirely, not even a `nil` placeholder, until that later command
344
377
  # actually ran. `rust/project/json_codec.rb#emit_to_json_flat`'s
@@ -346,12 +379,12 @@ module Hecks
346
379
  # contract for a persisted record: every declared field present,
347
380
  # `null` when unset, "because Ruby's own `JSON.generate(state)`
348
381
  # round-trip this mirrors does the same" — true for a freshly
349
- # created AGGREGATE (`Instance.defaults` already fills one key per
382
+ # created aggregate (`Instance.defaults` already fills one key per
350
383
  # declared attribute, `default_for` per attribute), never true for
351
384
  # an entity minted by an append. This closes that gap the same
352
- # way: `Instance.default_for` is the SAME per-attribute default
385
+ # way: `Instance.default_for` is the same per-attribute default
353
386
  # rule (nil with no declared `default:`, a fully-defaulted value
354
- # object when every one of ITS OWN fields has one), reused rather
387
+ # object when every one of its own fields has one), reused rather
355
388
  # than reimplemented so the two creation paths can never drift on
356
389
  # what "the default" means. Additive only — a key `fields` already
357
390
  # holds (the append mapping, an auto-minted identity, a lifecycle
@@ -366,8 +399,8 @@ module Hecks
366
399
  end
367
400
 
368
401
  # `MutationApplier#removed`'s own entity-scoped twin — matches by
369
- # VALUE EQUALITY, element-wise, the same "so a concurrent Add can
370
- # never be lost" reasoning that method's own comment gives, UNLESS
402
+ # value equality, element-wise, the same "so a concurrent Add can
403
+ # never be lost" reasoning that method's own comment gives, unless
371
404
  # the list this targets is itself entity-typed — see
372
405
  # `list_element_match?`, below, which both this and
373
406
  # `MutationApplier#removed` now share.
@@ -378,23 +411,23 @@ module Hecks
378
411
  Array(element[mutation.target]).reject { |candidate| list_element_match?(aggregate, attribute, candidate, value) }
379
412
  end
380
413
 
381
- # BUG#32 (QualityControl ledger) — THE MATCH RULE `remove:` USES
382
- # AGAINST ONE STORED LIST ELEMENT. VALUE EQUALITY for a
383
- # VALUE-OBJECT-typed list stays exactly what it always was — an
414
+ # BUG#32 (QualityControl ledger) — the match rule `remove:` uses
415
+ # against one stored list element. Value equality for a
416
+ # value-object-typed list stays exactly what it always was — an
384
417
  # element and `value` are both real `Value`s there, so `==` already
385
418
  # compares every field, the "concurrent Add can never be lost"
386
419
  # shape `removed`/`removed_from_element`'s own headers describe.
387
- # An ENTITY-typed list is different in kind, not just in type: a
420
+ # An entity-typed list is different in kind, not just in type: a
388
421
  # stored element is a plain Hash, never a `Value` (`Entity`'s own
389
422
  # header — "an entity must never answer .value_object"), so there
390
423
  # is no whole-value shape to compare against at all — only the
391
- # entity's own IDENTITY field, the same field a caller already has
424
+ # entity's own identity field, the same field a caller already has
392
425
  # to name to address that element any other way
393
426
  # (`element_of`'s own `wants`, above). `value` arrives here already
394
427
  # coerced against that identity field's declared type
395
428
  # (`Coercion#hydrate_entity_identity`, run underneath
396
429
  # `Value.for_attribute` before either caller above ever sees it),
397
- # so this only has to know WHICH field to read off the stored
430
+ # so this only has to know which field to read off the stored
398
431
  # element — `entity.identity_heads`'s own single head, when there
399
432
  # is exactly one. A composite identity (more than one head, or
400
433
  # none) has no single field a bare `remove:` target could mean —
@@ -405,7 +438,7 @@ module Hecks
405
438
  # entity identity elsewhere in this runtime.
406
439
  #
407
440
  # Shared by `MutationApplier#removed` (an aggregate's own list) and
408
- # `#removed_from_element` (a list an ENTITY owns), so the two
441
+ # `#removed_from_element` (a list an entity owns), so the two
409
442
  # `remove:` call sites can never quietly disagree on what
410
443
  # "matches" means — the same reasoning this file's own header
411
444
  # gives for centralizing `locate_chain`/`element_of` once rather
@@ -419,6 +452,57 @@ module Hecks
419
452
 
420
453
  element.is_a?(Hash) && element[head] == value
421
454
  end
455
+
456
+ # BUG#13 (PR #549) — the same check #hydrate gives every creating
457
+ # aggregate command (`repository.find(id)`,
458
+ # `command_interpreter.rb`), one level down. Originally lived in
459
+ # `MutationApplier` (mutation_applier.rb), called only from
460
+ # `#entity_element` — an aggregate's own entity list (`Workspace.
461
+ # boards`, `Ledger.entries`). Moved here (BUG#145) so `#appended_
462
+ # to_element`, above — an entity's own nested entity list one hop
463
+ # further in (`Board.cards`) — can share it too, rather than
464
+ # reimplementing it a second time the same way `#list_element_
465
+ # match?` already avoids that split for `remove:`.
466
+ #
467
+ # Reached, at the aggregate-owned call site, only on the two
468
+ # branches that do not auto-mint: a caller-supplied identity (the
469
+ # field is already in the append's own field map) or a composite
470
+ # one (`entity.identified_by` is nil for those — Runtime::
471
+ # Identified#derive_identity). Neither used to check the sibling
472
+ # list at all: a second LogVisit with the same date+sequence, or a
473
+ # second IssueKey with the same serial, appended a silent
474
+ # duplicate — worse than an ordinary duplicate row, because
475
+ # `EntityElement#element_of`'s own `find_index` always matches the
476
+ # first match, so the second becomes permanently unaddressable by
477
+ # any later command. At the entity-owned call site (`#appended_to_
478
+ # element`), there is no auto-mint branch at all — every caller
479
+ # reaches this unconditionally, since a nested entity's own
480
+ # identity is always caller-supplied in this corpus.
481
+ #
482
+ # Auto-minted (aggregate-owned) entities never reach here —
483
+ # `identity_heads` for them is still checked at mint time by
484
+ # construction (`current.size + 1` can only repeat if something
485
+ # `remove:`s from the list between mints, which no real domain does
486
+ # today), so they can't be flagged by mistake.
487
+ # `owner` — the declaring construct named in the "…already exists on
488
+ # {owner}" wording: the root aggregate at the aggregate-owned call
489
+ # site (`Workspace`, `Ledger`), the immediately-enclosing entity at
490
+ # the entity-owned one (`Board` — never the root `Workspace` two
491
+ # hops up). Used for that naming purpose only (`owner.hecks_name`) —
492
+ # never for `Value`/namespace resolution, which is why an `Entity`
493
+ # (not just an `Aggregate`) is a valid thing to pass here.
494
+ def check_entity_collision(owner, entity, current, fields)
495
+ heads = entity.identity_heads
496
+ return if heads.empty?
497
+
498
+ collision = Array(current).find { |element| heads.all? { |head| element[head] == fields[head] } }
499
+ return unless collision
500
+
501
+ raise(AlreadyExists, RefusalWording.render_site("AlreadyExists", "entity_duplicate",
502
+ entity: entity.hecks_name, aggregate: owner.hecks_name,
503
+ identity: Identity.reading(entity),
504
+ offered: heads.map { |head| Rendering.describe(fields[head]) }))
505
+ end
422
506
  end
423
507
  end
424
508
  end