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
@@ -4,13 +4,14 @@ require_relative "value"
4
4
  require_relative "refusal_wording"
5
5
  require_relative "errors"
6
6
  require_relative "identity"
7
+ require_relative "instance"
7
8
 
8
9
  module Hecks
9
10
  module Runtime
10
- # 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
11
12
  # dispatching into a piece an aggregate holds, factored out of
12
- # `EntityInterpreter` so a SECOND caller (`CommandInterpreter`'s own
13
- # `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
14
15
  # same way, against an aggregate record it already holds in memory
15
16
  # rather than one freshly loaded from a repository. `EntityInterpreter`
16
17
  # keeps its own dispatch order and Context; this is the part underneath
@@ -24,13 +25,21 @@ module Hecks
24
25
  module EntityElement
25
26
  module_function
26
27
 
27
- # ONE HOP PER CHAIN ENTRY. `container` starts as `instance` (the root
28
+ # BUG#3 an addressing value that can never match a stored element,
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
32
+ # bare `nil` sentinel would accidentally "match" it.
33
+ UNMATCHABLE = Object.new.freeze
34
+ private_constant :UNMATCHABLE
35
+
36
+ # **One hop per chain entry**. `container` starts as `instance` (the root
28
37
  # aggregate record) and becomes each just-located element in turn —
29
- # Dispatch's own element is found INSIDE the Handler element
38
+ # Dispatch's own element is found inside the Handler element
30
39
  # `locate_chain` located the step before, never inside `instance`
31
- # directly. `owner` is whichever construct's OWN attribute declares
40
+ # directly. `owner` is whichever construct's own attribute declares
32
41
  # the list being searched (Handler declares `dispatches` ; the root
33
- # aggregate declares `handlers`) — `root_aggregate` stays the ROOT
42
+ # aggregate declares `handlers`) — `root_aggregate` stays the root
34
43
  # the whole way through instead, passed to `element_of` separately,
35
44
  # because coercion (`Value.for_attribute`) resolves value objects
36
45
  # against the root's own namespace only ; an entity must never
@@ -54,7 +63,7 @@ module Hecks
54
63
  container
55
64
  end
56
65
 
57
- # 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.
58
67
  # A piece's identity may be several paths, the same shape a head's can be,
59
68
  # so a dispatch that names the element has to supply every part and every
60
69
  # part has to agree with the stored one. `routed_identity`, when given,
@@ -63,29 +72,58 @@ module Hecks
63
72
  # which element it means, so re-deriving `wants` from `args` would be
64
73
  # redundant at best and wrong if `args` no longer carries that identity
65
74
  # at all.
66
- # Locate, then copy-before-mutate, in that order — see the "ONE
67
- # 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
68
77
  # where it does (aliasing the adapter's own record otherwise).
69
78
  # Splitting resolution from the copy/write-back would separate two
70
79
  # halves of one aliasing-safety invariant across method boundaries.
71
80
  # rubocop:disable-next Metrics/AbcSize
72
81
  # rubocop:disable-next Metrics/CyclomaticComplexity
73
82
  # rubocop:disable-next Metrics/PerceivedComplexity
83
+ # rubocop:disable-next Metrics/MethodLength
74
84
  def element_of(root_aggregate, owner, entity, command_name, container, args, routed_identity = nil)
75
85
  entity_name = entity.hecks_name
76
86
  list_attr = owner.attributes.find { |a| a.list? && a.type.to_s == entity_name } ||
77
- raise(UnknownVerb, RefusalWording.render("UnknownVerb", "entity_holds_no_list",
78
- 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))
79
89
 
80
90
  wants = unless routed_identity
81
91
  entity.identity_paths.map do |path|
82
92
  head = path.to_s.split(".").first.to_sym
83
93
  raw = args[head] ||
84
- raise(NotFound, RefusalWording.render("NotFound", "entity_element_no_identity",
85
- command: command_name, entity: entity_name,
86
- 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
+
98
+ # An identity offered for addressing, not for storage
99
+ # (BUG#3, found live by `bin/qa_sweep` — banking fuzz seed
100
+ # 23, `LedgerEntry.Amend sequence: { value: 0 }` against an
101
+ # entry-less ledger). Coercing it all the way to a typed
102
+ # `Value` here ran that type's own invariant before this
103
+ # method ever checks whether any element matches — a
104
+ # `sequence: 0` against `LedgerSequence`'s own "a ledger
105
+ # sequence is positive" invariant raised InvariantViolation,
106
+ # not NotFound, even when (as here) nothing was ever posted
107
+ # at all. Every element actually in the list already
108
+ # satisfied its own type's invariant the moment it was
109
+ # created, so a value that fails it can never equal one —
110
+ # degrading to `UNMATCHABLE` here, instead of propagating,
111
+ # is exactly as safe as the ordinary "no match found" case
112
+ # below, and lines this addressing path up with the two
113
+ # conventions it already disagreed with: Rust's own
114
+ # `extract_id`/`extract_wants` (a raw scalar read, never a
115
+ # typed rebuild — rust/src/generated/*/*.rs) and
116
+ # `Identity.from`'s own raw-comparison convention for a
117
+ # root aggregate's identity (this file's sibling,
118
+ # `identity.rb`). `raw` rides alongside `want` so the
119
+ # eventual NotFound below can still quote what was offered.
120
+ want = begin
121
+ Value.for_attribute(root_aggregate, entity.attribute(head), raw)
122
+ rescue InvariantViolation
123
+ UNMATCHABLE
124
+ end
87
125
 
88
- [head, path, Value.for_attribute(root_aggregate, entity.attribute(head), raw)]
126
+ [head, path, want, raw]
89
127
  end
90
128
  end
91
129
 
@@ -93,29 +131,31 @@ module Hecks
93
131
  position = if routed_identity
94
132
  original.find_index { |element| element_identity(entity, element).to_s == routed_identity.to_s }
95
133
  else
96
- original.find_index { |el| wants.all? { |head, _path, want| el[head] == want } }
134
+ original.find_index do |el|
135
+ wants.all? { |head, _path, want, _raw| want != UNMATCHABLE && el[head] == want }
136
+ end
97
137
  end
98
138
  unless position
99
- raise NotFound, RefusalWording.render(
139
+ raise NotFound, RefusalWording.render_site(
100
140
  "NotFound", "entity_element_missing",
101
141
  entity: entity_name, identity: Identity.reading(entity),
102
- wants: wants&.map { |_h, path, want| Identity.scalar(path, want) }&.join(", "),
142
+ wants: wants&.map { |_h, path, _want, raw| Identity.scalar(path, raw) }&.join(", "),
103
143
  aggregate: owner.hecks_name,
104
144
  parent_id: container.respond_to?(:id) ? container.id.inspect : Rendering.describe(container)
105
145
  )
106
146
  end
107
147
 
108
- # 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
109
149
  # attribute holds Hashes, and `apply_to_element` mutates the found
110
- # 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
111
151
  # in place means aliased with the adapter's own record until this
112
152
  # copies the array and the target element before handing either
113
153
  # back, and writes the fresh array into `container` so the copy is
114
- # what persists on success and NOTHING aliased survives a refusal.
154
+ # what persists on success and nothing aliased survives a refusal.
115
155
  # `container[list_attr.name] = copied` reaches `instance` itself
116
- # when this is the FIRST hop, and reaches the (already copied)
117
- # PARENT element when it is a later one — either way it is the
118
- # 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
119
159
  # as `container` for the next hop, so nothing further has to
120
160
  # propagate a write back up the chain by hand.
121
161
  copied = original.dup
@@ -125,9 +165,9 @@ module Hecks
125
165
  element
126
166
  end
127
167
 
128
- # THE ELEMENT'S OWN IDENTITY, joined from its parts — the entity-level
129
- # twin of `Identity.of`, reading off the STORED ELEMENT (a Hash) rather
130
- # 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
131
171
  # reached — never by opening a value object and taking whatever single
132
172
  # field is inside. That unwrapping is gone from the language: a piece
133
173
  # that does not name its fields is refused when the bluebook loads ("an
@@ -151,10 +191,10 @@ module Hecks
151
191
  # refuses what it cannot check applying nothing instead. `Member`/
152
192
  # `Dispatch` (S17) are the first real callers: both need to
153
193
  # append a value-object-typed element (`Pair`/`Binding`) onto a
154
- # list attribute THEY OWN, once they become entities of
194
+ # list attribute they own, once they become entities of
155
195
  # `ValueObject`/`ProcessManager` rather than separate aggregates.
156
196
  #
157
- # `:increment`/`:decrement`/`:multiply` ALSO fixed here, found
197
+ # `:increment`/`:decrement`/`:multiply` also fixed here, found
158
198
  # while proving this method against a real fixture: they wrapped
159
199
  # `amount` unconditionally whenever `attribute` existed, the same
160
200
  # asymmetric-wrapping shape `MutationApplier#rewrap_arithmetic_
@@ -174,7 +214,7 @@ module Hecks
174
214
  # shape here without doing the same there would break that
175
215
  # intentional mirroring, which is what lets the two be diffed
176
216
  # against each other when one gets a fix the other needs too.
177
- # rubocop:disable-next Metrics/AbcSize
217
+ # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity
178
218
  # `pre` — the element as it was before this command (C4.2): every
179
219
  # read below goes through it, every write lands on `element`.
180
220
  def apply_to_element(rules, aggregate, entity, element, mutation, args, pre = element)
@@ -203,6 +243,22 @@ module Hecks
203
243
  element[mutation.target] = rewrap_arithmetic_result(aggregate, attribute, current, result)
204
244
  when :clamp
205
245
  element[mutation.target] = rules.clamp(pre[mutation.target], mutation.source, mutation.target)
246
+ # `corrects` — BUG#30. `MutationApplier#apply`'s own aggregate-
247
+ # level `:corrects` branch's own comment gives the full reasoning;
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
251
+ # once, up front, by `EntityInterpreter#step_enforce_givens`
252
+ # (`CommandRules::Admissibility#enforce_correction_target`, called
253
+ # there against the parent record/root aggregate — see that
254
+ # step's own comment for exactly why). Whatever field a correction
255
+ # actually changes is an ordinary declared `sets`/`increment`/etc.
256
+ # mutation of its own, applied by one of the branches above like
257
+ # any other — `qa/stress_domains/corrections`' own `Entry.Amend`
258
+ # pairs `corrects "EntryRecorded", ...` with a separate `sets
259
+ # :amount`, exactly this shape.
260
+ when :corrects
261
+ nil
206
262
  else
207
263
  # The aggregate-level twin's own backstop
208
264
  # (MutationApplier#apply), for the same reason: applying
@@ -226,8 +282,8 @@ module Hecks
226
282
  end
227
283
 
228
284
  # `MutationApplier#resolve_append_source`'s own entity-scoped
229
- # twin — a caller-supplied ARG first, falling back to the
230
- # 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
231
287
  # it isn't one.
232
288
  def resolve_element_append_source(source, element, args)
233
289
  return source unless source.is_a?(Symbol)
@@ -236,17 +292,28 @@ module Hecks
236
292
  element[source]
237
293
  end
238
294
 
239
- # `MutationApplier#appended`'s own entity-scoped twin. VALUE-
240
- # OBJECT elements only — an entity's own list, appended to by an
295
+ # `MutationApplier#appended`'s own entity-scoped twin. Usually a
296
+ # value object element — an entity's own list, appended to by an
241
297
  # entity-owned command, holds a value object (`Member.pairs`'
242
298
  # own `Pair`, `Dispatch.with_spec`'s own `Binding`) the same way
243
- # every real corpus append does; entity-in-entity nesting (a
244
- # list of ANOTHER entity, owned by this one) is out of scope —
245
- # `MutationApplier#entity_element`'s own fallback is deliberately
246
- # not mirrored here, since nothing in this language's own
247
- # `EntityBuilder` can declare a nested entity to need it (see
248
- # S17's own scoping note on why Dispatch flattens under
249
- # ProcessManager instead of nesting under Handler).
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:
301
+ # `qa/stress_domains/nested_pieces` (`Board.AddCard`, appending a
302
+ # `Card` onto `Board`'s own `cards`) is the first corpus member to
303
+ # do it, the comment this replaces having been written before that
304
+ # domain existed. `element_type` naming an entity rather than a
305
+ # value object falls through to `fields` unchanged, same as
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`.
250
317
  def appended_to_element(aggregate, entity, element, mutation, args)
251
318
  fields = mutation.source.transform_values { |source| resolve_element_append_source(source, element, args) }
252
319
  element_type = entity.attribute(mutation.target)&.type
@@ -254,18 +321,187 @@ module Hecks
254
321
  value_object&.attributes&.each do |attribute|
255
322
  fields[attribute.name] = Value.scalar(fields[attribute.name]) if fields[attribute.name].is_a?(Value)
256
323
  end
257
- appended = value_object ? Value.build(value_object, fields, aggregate) : fields
324
+ appended =
325
+ if value_object
326
+ Value.build(value_object, fields, aggregate)
327
+ else
328
+ # `entity.entities`, not `aggregate.entities` — a piece
329
+ # nested inside a piece is a child of the owning entity
330
+ # (`Card` is `Board.entities`, never `Workspace.entities`;
331
+ # `Behaviour::Entity#entities` answers direct children only,
332
+ # by design — see its own comment), the same lexical-nesting
333
+ # rule `EntityBuilder#entity_impl` builds the tree with in
334
+ # the first place.
335
+ nested_entity = entity.entities.find { |piece| piece.hecks_name == element_type.to_s }
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
368
+ end
258
369
  Freezer.deep(Array(element[mutation.target]) + [appended])
259
370
  end
260
371
 
372
+ # BUG#12 — an entity created via `sets :list, append: {...}` used
373
+ # to leave any of its own declared attributes the append mapping
374
+ # simply didn't name (an optional field a later, separate command
375
+ # sets — `Board.label`, `Card.note`) absent from the stored hash
376
+ # entirely, not even a `nil` placeholder, until that later command
377
+ # actually ran. `rust/project/json_codec.rb#emit_to_json_flat`'s
378
+ # own header comment documents the opposite as the intended
379
+ # contract for a persisted record: every declared field present,
380
+ # `null` when unset, "because Ruby's own `JSON.generate(state)`
381
+ # round-trip this mirrors does the same" — true for a freshly
382
+ # created aggregate (`Instance.defaults` already fills one key per
383
+ # declared attribute, `default_for` per attribute), never true for
384
+ # an entity minted by an append. This closes that gap the same
385
+ # way: `Instance.default_for` is the same per-attribute default
386
+ # rule (nil with no declared `default:`, a fully-defaulted value
387
+ # object when every one of its own fields has one), reused rather
388
+ # than reimplemented so the two creation paths can never drift on
389
+ # what "the default" means. Additive only — a key `fields` already
390
+ # holds (the append mapping, an auto-minted identity, a lifecycle
391
+ # default) is never overwritten.
392
+ def fill_declared_defaults(aggregate, entity, fields)
393
+ entity.attributes.each do |attribute|
394
+ next if fields.key?(attribute.name)
395
+
396
+ fields[attribute.name] = attribute.list? ? Freezer.deep([]) : Instance.default_for(aggregate, attribute)
397
+ end
398
+ fields
399
+ end
400
+
261
401
  # `MutationApplier#removed`'s own entity-scoped twin — matches by
262
- # VALUE EQUALITY, element-wise, the same "so a concurrent Add can
263
- # never be lost" reasoning that method's own comment gives.
402
+ # value equality, element-wise, the same "so a concurrent Add can
403
+ # never be lost" reasoning that method's own comment gives, unless
404
+ # the list this targets is itself entity-typed — see
405
+ # `list_element_match?`, below, which both this and
406
+ # `MutationApplier#removed` now share.
264
407
  def removed_from_element(rules, aggregate, entity, element, mutation, args)
265
408
  value = rules.resolve_source(mutation.source, args)
266
409
  attribute = entity.attribute(mutation.target)
267
410
  value = Value.for_attribute(aggregate, attribute, value) if attribute
268
- Array(element[mutation.target]).reject { |candidate| candidate == value }
411
+ Array(element[mutation.target]).reject { |candidate| list_element_match?(aggregate, attribute, candidate, value) }
412
+ end
413
+
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
417
+ # element and `value` are both real `Value`s there, so `==` already
418
+ # compares every field, the "concurrent Add can never be lost"
419
+ # shape `removed`/`removed_from_element`'s own headers describe.
420
+ # An entity-typed list is different in kind, not just in type: a
421
+ # stored element is a plain Hash, never a `Value` (`Entity`'s own
422
+ # header — "an entity must never answer .value_object"), so there
423
+ # is no whole-value shape to compare against at all — only the
424
+ # entity's own identity field, the same field a caller already has
425
+ # to name to address that element any other way
426
+ # (`element_of`'s own `wants`, above). `value` arrives here already
427
+ # coerced against that identity field's declared type
428
+ # (`Coercion#hydrate_entity_identity`, run underneath
429
+ # `Value.for_attribute` before either caller above ever sees it),
430
+ # so this only has to know which field to read off the stored
431
+ # element — `entity.identity_heads`'s own single head, when there
432
+ # is exactly one. A composite identity (more than one head, or
433
+ # none) has no single field a bare `remove:` target could mean —
434
+ # this returns `false` (never a match, the same documented no-op
435
+ # `hydrate_entity_identity`'s own header already commits to) rather
436
+ # than guessing which head, matching the narrow, honest boundary
437
+ # `MutationApplier#check_entity_collision`'s own header draws for
438
+ # entity identity elsewhere in this runtime.
439
+ #
440
+ # Shared by `MutationApplier#removed` (an aggregate's own list) and
441
+ # `#removed_from_element` (a list an entity owns), so the two
442
+ # `remove:` call sites can never quietly disagree on what
443
+ # "matches" means — the same reasoning this file's own header
444
+ # gives for centralizing `locate_chain`/`element_of` once rather
445
+ # than twice.
446
+ def list_element_match?(aggregate, attribute, element, value)
447
+ entity = attribute&.list? ? Value.find_entity(aggregate, attribute.type.to_s) : nil
448
+ return element == value unless entity
449
+
450
+ head = entity.identity_heads.one? ? entity.identity_heads.first : nil
451
+ return false unless head
452
+
453
+ element.is_a?(Hash) && element[head] == value
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]) }))
269
505
  end
270
506
  end
271
507
  end