hecks 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (373) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +72 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +83 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
  7. data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
  9. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  10. data/lib/hecks/adapters/driven/heki.rb +106 -9
  11. data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
  12. data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
  13. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  15. data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
  16. data/lib/hecks/adapters/driven/lambda.rb +82 -35
  17. data/lib/hecks/adapters/driven/local_storage.rb +83 -10
  18. data/lib/hecks/adapters/driven/memory.rb +205 -9
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  24. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  25. data/lib/hecks/adapters/driven/prism.rb +50 -11
  26. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  31. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  32. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  33. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  34. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  35. data/lib/hecks/adapters/driven.rb +6 -4
  36. data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
  37. data/lib/hecks/behaviors/dsl.rb +60 -2
  38. data/lib/hecks/behaviors/expectations.rb +190 -29
  39. data/lib/hecks/behaviors/ir.rb +12 -1
  40. data/lib/hecks/behaviors/rspec.rb +9 -1
  41. data/lib/hecks/behaviors/runner.rb +21 -2
  42. data/lib/hecks/behaviors.rb +9 -1
  43. data/lib/hecks/bluebook/aggregate.rb +43 -13
  44. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
  45. data/lib/hecks/bluebook/assembly/build.rb +26 -5
  46. data/lib/hecks/bluebook/assembly/contract.rb +98 -23
  47. data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
  48. data/lib/hecks/bluebook/assembly/marks.rb +159 -30
  49. data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
  50. data/lib/hecks/bluebook/assembly.rb +32 -14
  51. data/lib/hecks/bluebook/attribute.rb +26 -12
  52. data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
  53. data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
  54. data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
  55. data/lib/hecks/bluebook/behaviour/command.rb +55 -25
  56. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
  57. data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
  58. data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
  59. data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
  60. data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
  61. data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
  62. data/lib/hecks/bluebook/behaviour/query.rb +6 -1
  63. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
  64. data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
  65. data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
  66. data/lib/hecks/bluebook/capabilities.rb +27 -0
  67. data/lib/hecks/bluebook/chapter.rb +51 -9
  68. data/lib/hecks/bluebook/command.rb +62 -17
  69. data/lib/hecks/bluebook/domain_port.rb +34 -9
  70. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  71. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  72. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
  73. data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
  74. data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
  75. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
  76. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
  77. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  78. data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
  79. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  80. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  81. data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
  82. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  83. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
  84. data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
  85. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  86. data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
  87. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  88. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  89. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
  90. data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
  91. data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
  92. data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
  93. data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
  94. data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
  95. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  96. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  97. data/lib/hecks/bluebook/entity.rb +40 -11
  98. data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
  99. data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
  100. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
  101. data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
  102. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
  103. data/lib/hecks/bluebook/expression/resolver.rb +369 -128
  104. data/lib/hecks/bluebook/hexagon.rb +35 -1
  105. data/lib/hecks/bluebook/lifecycle.rb +12 -1
  106. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
  107. data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
  108. data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
  109. data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
  110. data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
  111. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
  112. data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
  113. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
  114. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
  115. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
  116. data/lib/hecks/bluebook/meta_validator.rb +235 -139
  117. data/lib/hecks/bluebook/model_check.rb +434 -104
  118. data/lib/hecks/bluebook/pattern_subset.rb +32 -10
  119. data/lib/hecks/bluebook/policy.rb +15 -13
  120. data/lib/hecks/bluebook/process_manager.rb +27 -14
  121. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  122. data/lib/hecks/bluebook/project_loader.rb +40 -0
  123. data/lib/hecks/bluebook/project_register.rb +50 -6
  124. data/lib/hecks/bluebook/query.rb +31 -4
  125. data/lib/hecks/bluebook/read_model.rb +35 -15
  126. data/lib/hecks/bluebook/reference.rb +26 -13
  127. data/lib/hecks/bluebook/smoke_test.rb +46 -23
  128. data/lib/hecks/bluebook/synthesizer.rb +46 -12
  129. data/lib/hecks/bluebook/translation.rb +34 -5
  130. data/lib/hecks/bluebook/value_object.rb +29 -11
  131. data/lib/hecks/bluebook.rb +5 -6
  132. data/lib/hecks/codemod.rb +138 -50
  133. data/lib/hecks/construct.rb +21 -7
  134. data/lib/hecks/corpus.rb +438 -0
  135. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  136. data/lib/hecks/doc/reference.rb +200 -31
  137. data/lib/hecks/embryonaut_bluebook.rb +38 -15
  138. data/lib/hecks/facade/cli_door.rb +69 -10
  139. data/lib/hecks/facade/cli_runner.rb +105 -24
  140. data/lib/hecks/facade/command_request.rb +23 -0
  141. data/lib/hecks/facade/handle.rb +155 -35
  142. data/lib/hecks/facade/json_door.rb +106 -25
  143. data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
  144. data/lib/hecks/facade/surface/chapter.rb +26 -17
  145. data/lib/hecks/facade/surface.rb +16 -3
  146. data/lib/hecks/facade.rb +15 -4
  147. data/lib/hecks/forms/app.rb +46 -30
  148. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  149. data/lib/hecks/forms/field_renderer.rb +142 -6
  150. data/lib/hecks/forms/field_shape.rb +183 -19
  151. data/lib/hecks/forms/html.rb +51 -7
  152. data/lib/hecks/forms/index_renderer.rb +14 -2
  153. data/lib/hecks/forms/page.rb +14 -0
  154. data/lib/hecks/forms/params.rb +120 -23
  155. data/lib/hecks/forms/port_argument.rb +14 -2
  156. data/lib/hecks/forms/query_form_renderer.rb +65 -2
  157. data/lib/hecks/forms/record_renderer.rb +60 -2
  158. data/lib/hecks/forms/record_table.rb +28 -1
  159. data/lib/hecks/forms/reference_options.rb +24 -0
  160. data/lib/hecks/forms/value_object_shape.rb +13 -3
  161. data/lib/hecks/forms.rb +24 -4
  162. data/lib/hecks/fqn.rb +59 -1
  163. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  164. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  165. data/lib/hecks/framework/oidc.json +15 -0
  166. data/lib/hecks/framework.rb +79 -25
  167. data/lib/hecks/freezer.rb +27 -11
  168. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
  169. data/lib/hecks/fuzzing/combination_miner.rb +178 -0
  170. data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
  171. data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
  172. data/lib/hecks/fuzzing/differential.rb +192 -0
  173. data/lib/hecks/fuzzing/domain_generator.rb +871 -0
  174. data/lib/hecks/fuzzing/era_boundary.rb +68 -19
  175. data/lib/hecks/fuzzing/form_census.rb +121 -20
  176. data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
  177. data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
  178. data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
  179. data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
  180. data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
  181. data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
  182. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
  183. data/lib/hecks/fuzzing/properties/guards.rb +86 -42
  184. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
  185. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
  186. data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
  187. data/lib/hecks/fuzzing/properties/querying.rb +82 -28
  188. data/lib/hecks/fuzzing/properties.rb +84 -51
  189. data/lib/hecks/fuzzing/qa_settings.rb +164 -0
  190. data/lib/hecks/fuzzing/replay.rb +241 -133
  191. data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
  192. data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
  193. data/lib/hecks/fuzzing/self_consistency.rb +315 -137
  194. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
  195. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  196. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  197. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  198. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
  199. data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
  200. data/lib/hecks/fuzzing/shrinker.rb +309 -0
  201. data/lib/hecks/fuzzing/structural_skips.rb +37 -130
  202. data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
  203. data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
  204. data/lib/hecks/fuzzing/value_generator.rb +110 -22
  205. data/lib/hecks/fuzzing.rb +5 -0
  206. data/lib/hecks/grammar/evolve.rb +188 -12
  207. data/lib/hecks/grammar.rb +53 -7
  208. data/lib/hecks/ir.rb +51 -20
  209. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  210. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  211. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  212. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  213. data/lib/hecks/language/oidc.json +5 -0
  214. data/lib/hecks/literal.rb +41 -9
  215. data/lib/hecks/naming.rb +112 -31
  216. data/lib/hecks/ports/access_control.rb +53 -2
  217. data/lib/hecks/ports/agent/answers.rb +83 -6
  218. data/lib/hecks/ports/agent.rb +119 -35
  219. data/lib/hecks/ports/authentication.rb +44 -4
  220. data/lib/hecks/ports/authorization.rb +50 -11
  221. data/lib/hecks/ports/clock.rb +42 -23
  222. data/lib/hecks/ports/extraction.rb +16 -0
  223. data/lib/hecks/ports/identity_assignment.rb +23 -2
  224. data/lib/hecks/ports/identity_generation.rb +17 -3
  225. data/lib/hecks/ports/identity_resolution.rb +17 -1
  226. data/lib/hecks/ports/key_vault.port +6 -0
  227. data/lib/hecks/ports/key_vault.rb +58 -0
  228. data/lib/hecks/ports/loading.rb +4 -0
  229. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  230. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  231. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  232. data/lib/hecks/ports/persistence/execution.rb +4 -0
  233. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  234. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  235. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  236. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  237. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  238. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  239. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  250. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  251. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  252. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
  253. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  254. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  255. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  256. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  257. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  258. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
  259. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  260. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  261. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  262. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
  263. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
  264. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  265. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
  266. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  267. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  268. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  269. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  270. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  271. data/lib/hecks/ports/persistence.rb +36 -1
  272. data/lib/hecks/ports/projection.rb +61 -7
  273. data/lib/hecks/ports/query/in_memory.rb +41 -8
  274. data/lib/hecks/ports/query/ordering.rb +21 -6
  275. data/lib/hecks/ports/query.rb +35 -0
  276. data/lib/hecks/ports.rb +1 -0
  277. data/lib/hecks/projections/bootstrap_table.rb +147 -0
  278. data/lib/hecks/projections/diagrams.rb +315 -79
  279. data/lib/hecks/projections/glossary/html.rb +91 -3
  280. data/lib/hecks/projections/glossary/markdown.rb +50 -3
  281. data/lib/hecks/projections/glossary/mermaid.rb +49 -1
  282. data/lib/hecks/projections/glossary/sections.rb +1 -1
  283. data/lib/hecks/projections/glossary/sentences.rb +110 -5
  284. data/lib/hecks/projections/glossary.rb +173 -31
  285. data/lib/hecks/projections/ir.rb +1 -1
  286. data/lib/hecks/projections/model/deviations.rb +62 -17
  287. data/lib/hecks/projections/model.rb +75 -21
  288. data/lib/hecks/projections/oidc.rb +45 -8
  289. data/lib/hecks/projections/parser_table.rb +53 -12
  290. data/lib/hecks/projections/reference.rb +15 -3
  291. data/lib/hecks/projections/rust_vocabulary.rb +646 -0
  292. data/lib/hecks/projections/shape.rb +10 -2
  293. data/lib/hecks/projections/statements.rb +69 -22
  294. data/lib/hecks/projections/vocabulary.rb +26 -9
  295. data/lib/hecks/projections.rb +5 -3
  296. data/lib/hecks/projector/cli_projector.rb +245 -37
  297. data/lib/hecks/projector/docs_projector.rb +154 -28
  298. data/lib/hecks/projector/exporter.rb +104 -29
  299. data/lib/hecks/projector/ir_projector.rb +7 -1
  300. data/lib/hecks/projector/narrate_projector.rb +144 -23
  301. data/lib/hecks/projector/target.rb +42 -18
  302. data/lib/hecks/projector.rb +86 -18
  303. data/lib/hecks/query_ir.rb +94 -47
  304. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  305. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  306. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  307. data/lib/hecks/query_specification/common/null_policy.rb +61 -15
  308. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  309. data/lib/hecks/query_specification/common/options.rb +25 -0
  310. data/lib/hecks/query_specification/field_path.rb +69 -15
  311. data/lib/hecks/query_specification/hop_path.rb +57 -20
  312. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  313. data/lib/hecks/rendering.rb +9 -3
  314. data/lib/hecks/router/namespace_installer.rb +16 -3
  315. data/lib/hecks/router.rb +56 -1
  316. data/lib/hecks/runtime/aggregate_lock.rb +20 -11
  317. data/lib/hecks/runtime/boot_gates.rb +21 -3
  318. data/lib/hecks/runtime/caller.rb +40 -8
  319. data/lib/hecks/runtime/capability_graph.rb +13 -2
  320. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
  321. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
  322. data/lib/hecks/runtime/command_interpreter.rb +195 -113
  323. data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
  324. data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
  325. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  326. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  327. data/lib/hecks/runtime/command_rules/references.rb +145 -55
  328. data/lib/hecks/runtime/command_rules.rb +3 -0
  329. data/lib/hecks/runtime/dependency_planning.rb +56 -11
  330. data/lib/hecks/runtime/dispatcher.rb +263 -92
  331. data/lib/hecks/runtime/entity_element.rb +304 -67
  332. data/lib/hecks/runtime/entity_interpreter.rb +149 -88
  333. data/lib/hecks/runtime/errors.rb +37 -23
  334. data/lib/hecks/runtime/event.rb +15 -10
  335. data/lib/hecks/runtime/identity.rb +91 -23
  336. data/lib/hecks/runtime/instance.rb +103 -18
  337. data/lib/hecks/runtime/interpreting.rb +24 -16
  338. data/lib/hecks/runtime/invocation.rb +358 -0
  339. data/lib/hecks/runtime/loader.rb +105 -19
  340. data/lib/hecks/runtime/outbox.rb +164 -26
  341. data/lib/hecks/runtime/policy_interpreter.rb +73 -60
  342. data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
  343. data/lib/hecks/runtime/query_interpreter.rb +93 -74
  344. data/lib/hecks/runtime/reaction_invocation.rb +73 -28
  345. data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
  346. data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
  347. data/lib/hecks/runtime/reference_hop.rb +48 -6
  348. data/lib/hecks/runtime/refusal_wording.rb +142 -115
  349. data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
  350. data/lib/hecks/runtime/registry/verification.rb +153 -28
  351. data/lib/hecks/runtime/registry.rb +202 -32
  352. data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
  353. data/lib/hecks/runtime/routing.rb +36 -154
  354. data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
  355. data/lib/hecks/runtime/saga_interpreter.rb +90 -76
  356. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  357. data/lib/hecks/runtime/tenant_check.rb +33 -13
  358. data/lib/hecks/runtime/tenant_scope.rb +23 -5
  359. data/lib/hecks/runtime/value/admission.rb +75 -30
  360. data/lib/hecks/runtime/value/coercion.rb +555 -142
  361. data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
  362. data/lib/hecks/runtime/value.rb +71 -21
  363. data/lib/hecks/runtime.rb +39 -7
  364. data/lib/hecks/storehouse.rb +368 -72
  365. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  366. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  367. data/lib/hecks/version.rb +3 -3
  368. data/lib/hecks/vocabulary.rb +205 -4
  369. data/lib/hecks.rb +91 -11
  370. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
  371. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
  372. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
  373. metadata +28 -2
@@ -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,22 @@ 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
+ # Walks `chain`, one hop per entry, and returns the located element (or
37
+ # `instance` itself, when `chain` is empty).
38
+ #
39
+ # **One hop per chain entry**. `container` starts as `instance` (the root
37
40
  # aggregate record) and becomes each just-located element in turn —
38
- # Dispatch's own element is found INSIDE the Handler element
41
+ # Dispatch's own element is found inside the Handler element
39
42
  # `locate_chain` located the step before, never inside `instance`
40
- # directly. `owner` is whichever construct's OWN attribute declares
43
+ # directly. `owner` is whichever construct's own attribute declares
41
44
  # the list being searched (Handler declares `dispatches` ; the root
42
- # aggregate declares `handlers`) — `root_aggregate` stays the ROOT
45
+ # aggregate declares `handlers`) — `root_aggregate` stays the root
43
46
  # the whole way through instead, passed to `element_of` separately,
44
47
  # because coercion (`Value.for_attribute`) resolves value objects
45
48
  # against the root's own namespace only ; an entity must never
@@ -52,6 +55,22 @@ module Hecks
52
55
  # split's own consequence: an entity's own identity may arrive via
53
56
  # `to:` rather than duplicated into `args`, so each hop is offered
54
57
  # its routed identity ahead of falling back to `args`.
58
+ #
59
+ # @param root_aggregate [Bluebook::Aggregate] the root aggregate record's
60
+ # own construct, held constant across every hop for coercion
61
+ # @param chain [Array<Bluebook::Entity>] the entity chain to walk, root-first
62
+ # @param instance [Runtime::Instance] the root aggregate record to walk from
63
+ # @param args [Hash{String, Symbol => Object}] the offered command arguments,
64
+ # read for each hop's own identity when `route` does not supply it
65
+ # @param command_name [String] the command name, quoted in a refusal
66
+ # @param route [Runtime::Routing::Envelope, nil] the call's resolved routing
67
+ # envelope, if any; its own `entities` supply each hop's identity first
68
+ # @return [Runtime::Instance, Hash{Symbol => Object}] the located element;
69
+ # `instance` itself, unchanged, when `chain` is empty
70
+ # @raise [Runtime::UnknownVerb] if a hop's owner declares no list attribute
71
+ # for that entity
72
+ # @raise [Runtime::NotFound] if a hop's identity is absent from `args`, or no
73
+ # element matches it
55
74
  def locate_chain(root_aggregate, chain, instance, args, command_name, route = nil)
56
75
  container = instance
57
76
  owner = root_aggregate
@@ -63,7 +82,7 @@ module Hecks
63
82
  container
64
83
  end
65
84
 
66
- # ONE ELEMENT, MATCHED ON EVERY PART OF ITS IDENTITY — not just the first.
85
+ # One element, matched on every part of its identity — not just the first.
67
86
  # A piece's identity may be several paths, the same shape a head's can be,
68
87
  # so a dispatch that names the element has to supply every part and every
69
88
  # part has to agree with the stored one. `routed_identity`, when given,
@@ -72,8 +91,8 @@ module Hecks
72
91
  # which element it means, so re-deriving `wants` from `args` would be
73
92
  # redundant at best and wrong if `args` no longer carries that identity
74
93
  # 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
94
+ # Locate, then copy-before-mutate, in that order — see the "one
95
+ # level deeper" comment below on why the copy has to happen exactly
77
96
  # where it does (aliasing the adapter's own record otherwise).
78
97
  # Splitting resolution from the copy/write-back would separate two
79
98
  # halves of one aliasing-safety invariant across method boundaries.
@@ -81,30 +100,48 @@ module Hecks
81
100
  # rubocop:disable-next Metrics/CyclomaticComplexity
82
101
  # rubocop:disable-next Metrics/PerceivedComplexity
83
102
  # rubocop:disable-next Metrics/MethodLength
103
+ #
104
+ # @param root_aggregate [Bluebook::Aggregate] the root aggregate record's own
105
+ # construct, used for value-object coercion
106
+ # @param owner [Bluebook::Aggregate, Bluebook::Entity] the construct whose own
107
+ # attribute declares the list `entity` is searched in
108
+ # @param entity [Bluebook::Entity] the entity type being located
109
+ # @param command_name [String] the command name, quoted in a refusal
110
+ # @param container [Runtime::Instance, Hash{Symbol => Object}] the record or
111
+ # element holding the list to search
112
+ # @param args [Hash{String, Symbol => Object}] the offered command arguments,
113
+ # read for the element's own identity when `routed_identity` is nil
114
+ # @param routed_identity [String, nil] the routing envelope's own identity
115
+ # string for this hop, matched directly instead of deriving one from `args`
116
+ # @return [Hash{Symbol => Object}] a fresh copy of the located element; the
117
+ # owning list inside `container` is replaced with a fresh copy too
118
+ # @raise [Runtime::UnknownVerb] if `owner` declares no list attribute for `entity`
119
+ # @raise [Runtime::NotFound] if an identity part is absent from `args`
120
+ # (`routed_identity` nil only), or no element matches
84
121
  def element_of(root_aggregate, owner, entity, command_name, container, args, routed_identity = nil)
85
122
  entity_name = entity.hecks_name
86
123
  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))
124
+ raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "entity_holds_no_list",
125
+ aggregate: owner.hecks_name, entity: entity_name))
89
126
 
90
127
  wants = unless routed_identity
91
128
  entity.identity_paths.map do |path|
92
129
  head = path.to_s.split(".").first.to_sym
93
130
  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)))
131
+ raise(NotFound, RefusalWording.render_site("NotFound", "entity_element_no_identity",
132
+ command: command_name, entity: entity_name,
133
+ identity: Identity.reading(entity)))
97
134
 
98
- # AN IDENTITY OFFERED FOR ADDRESSING, NOT FOR STORAGE
135
+ # An identity offered for addressing, not for storage
99
136
  # (BUG#3, found live by `bin/qa_sweep` — banking fuzz seed
100
137
  # 23, `LedgerEntry.Amend sequence: { value: 0 }` against an
101
138
  # entry-less ledger). Coercing it all the way to a typed
102
- # `Value` here ran that type's own invariant BEFORE this
139
+ # `Value` here ran that type's own invariant before this
103
140
  # method ever checks whether any element matches — a
104
141
  # `sequence: 0` against `LedgerSequence`'s own "a ledger
105
142
  # sequence is positive" invariant raised InvariantViolation,
106
143
  # not NotFound, even when (as here) nothing was ever posted
107
- # at all. Every element actually IN the list already
144
+ # at all. Every element actually in the list already
108
145
  # satisfied its own type's invariant the moment it was
109
146
  # created, so a value that fails it can never equal one —
110
147
  # degrading to `UNMATCHABLE` here, instead of propagating,
@@ -114,7 +151,7 @@ module Hecks
114
151
  # `extract_id`/`extract_wants` (a raw scalar read, never a
115
152
  # typed rebuild — rust/src/generated/*/*.rs) and
116
153
  # `Identity.from`'s own raw-comparison convention for a
117
- # ROOT aggregate's identity (this file's sibling,
154
+ # root aggregate's identity (this file's sibling,
118
155
  # `identity.rb`). `raw` rides alongside `want` so the
119
156
  # eventual NotFound below can still quote what was offered.
120
157
  want = begin
@@ -136,7 +173,7 @@ module Hecks
136
173
  end
137
174
  end
138
175
  unless position
139
- raise NotFound, RefusalWording.render(
176
+ raise NotFound, RefusalWording.render_site(
140
177
  "NotFound", "entity_element_missing",
141
178
  entity: entity_name, identity: Identity.reading(entity),
142
179
  wants: wants&.map { |_h, path, _want, raw| Identity.scalar(path, raw) }&.join(", "),
@@ -145,17 +182,17 @@ module Hecks
145
182
  )
146
183
  end
147
184
 
148
- # ONE LEVEL DEEPER THAN Instance#dup, for the same reason: a list
185
+ # One level deeper than Instance#dup, for the same reason: a list
149
186
  # attribute holds Hashes, and `apply_to_element` mutates the found
150
- # one IN PLACE — the update mechanism for an entity, not a bug. But
187
+ # one in place — the update mechanism for an entity, not a bug. But
151
188
  # in place means aliased with the adapter's own record until this
152
189
  # copies the array and the target element before handing either
153
190
  # back, and writes the fresh array into `container` so the copy is
154
- # what persists on success and NOTHING aliased survives a refusal.
191
+ # what persists on success and nothing aliased survives a refusal.
155
192
  # `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
193
+ # when this is the first hop, and reaches the (already copied)
194
+ # parent element when it is a later one — either way it is the
195
+ # same already-fresh object `locate_chain` is about to hand back
159
196
  # as `container` for the next hop, so nothing further has to
160
197
  # propagate a write back up the chain by hand.
161
198
  copied = original.dup
@@ -165,14 +202,20 @@ module Hecks
165
202
  element
166
203
  end
167
204
 
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
205
+ # The element's own identity, joined from its parts — the entity-level
206
+ # twin of `Identity.of`, reading off the stored element (a Hash) rather
207
+ # than a dispatch payload. An id is a scalar, and the path is how it is
171
208
  # reached — never by opening a value object and taking whatever single
172
209
  # field is inside. That unwrapping is gone from the language: a piece
173
210
  # that does not name its fields is refused when the bluebook loads ("an
174
211
  # entity says what it is known by", "an identity part names something"),
175
212
  # so by the time a dispatch arrives here there is always a path to dig.
213
+ #
214
+ # @param entity [Bluebook::Entity] the entity type whose identity paths
215
+ # are read
216
+ # @param element [Hash{Symbol => Object}] the stored element to read an
217
+ # identity off
218
+ # @return [String] the element's identity, joined from its declared parts
176
219
  def element_identity(entity, element)
177
220
  parts = entity.identity_paths.map do |path|
178
221
  head = path.to_s.split(".").first.to_sym
@@ -191,10 +234,10 @@ module Hecks
191
234
  # refuses what it cannot check applying nothing instead. `Member`/
192
235
  # `Dispatch` (S17) are the first real callers: both need to
193
236
  # append a value-object-typed element (`Pair`/`Binding`) onto a
194
- # list attribute THEY OWN, once they become entities of
237
+ # list attribute they own, once they become entities of
195
238
  # `ValueObject`/`ProcessManager` rather than separate aggregates.
196
239
  #
197
- # `:increment`/`:decrement`/`:multiply` ALSO fixed here, found
240
+ # `:increment`/`:decrement`/`:multiply` also fixed here, found
198
241
  # while proving this method against a real fixture: they wrapped
199
242
  # `amount` unconditionally whenever `attribute` existed, the same
200
243
  # asymmetric-wrapping shape `MutationApplier#rewrap_arithmetic_
@@ -215,8 +258,32 @@ module Hecks
215
258
  # intentional mirroring, which is what lets the two be diffed
216
259
  # against each other when one gets a fix the other needs too.
217
260
  # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity
261
+ # Applies one declared mutation to an entity element, in place.
262
+ #
218
263
  # `pre` — the element as it was before this command (C4.2): every
219
264
  # read below goes through it, every write lands on `element`.
265
+ #
266
+ # @param rules [Runtime::CommandRules] the shared rules engine `resolve_source`/
267
+ # `arithmetic`/`multiply`/`clamp`/`sign_of` are read through
268
+ # @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
269
+ # coercion
270
+ # @param entity [Bluebook::Entity] the entity type `element` is an instance of
271
+ # @param element [Hash{Symbol => Object}] the element to mutate; written in place
272
+ # @param mutation [Bluebook::Mutation] the declared mutation to apply
273
+ # @param args [Hash{String, Symbol => Object}] the offered command arguments,
274
+ # a mutation's source may read from
275
+ # @param pre [Hash{Symbol => Object}] the element as it stood before this
276
+ # command; every read goes through this, every write lands on `element`
277
+ # @return [void]
278
+ # @raise [Runtime::TypeMismatch] if a mutation's own value cannot be coerced,
279
+ # or an arithmetic op's operands are not numeric or compatible value objects
280
+ # @raise [Runtime::InvariantViolation] if a coerced or arithmetic-derived value
281
+ # object breaks one of its own invariants
282
+ # @raise [Runtime::AlreadyExists] if an `:append` mints a nested entity whose
283
+ # identity collides with an existing element
284
+ # @raise [Bluebook::Expression::EvaluationError] if an arithmetic op's product
285
+ # does not fit a signed 64-bit Integer, or is a non-finite Float
286
+ # @raise [Runtime::WiringError] if `mutation.op` names no handled mutation kind
220
287
  def apply_to_element(rules, aggregate, entity, element, mutation, args, pre = element)
221
288
  case mutation.op
222
289
  when :set
@@ -246,13 +313,13 @@ module Hecks
246
313
  # `corrects` — BUG#30. `MutationApplier#apply`'s own aggregate-
247
314
  # level `:corrects` branch's own comment gives the full reasoning;
248
315
  # 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
316
+ # field on this element at all — its own event name, and whether
317
+ # the owning record has actually emitted it, was already checked
251
318
  # once, up front, by `EntityInterpreter#step_enforce_givens`
252
319
  # (`CommandRules::Admissibility#enforce_correction_target`, called
253
- # there against the PARENT record/ROOT aggregate — see that
320
+ # there against the parent record/root aggregate — see that
254
321
  # step's own comment for exactly why). Whatever field a correction
255
- # actually changes is an ORDINARY declared `sets`/`increment`/etc.
322
+ # actually changes is an ordinary declared `sets`/`increment`/etc.
256
323
  # mutation of its own, applied by one of the branches above like
257
324
  # any other — `qa/stress_domains/corrections`' own `Entry.Amend`
258
325
  # pairs `corrects "EntryRecorded", ...` with a separate `sets
@@ -269,22 +336,47 @@ module Hecks
269
336
  end
270
337
  end
271
338
 
339
+ # Rewraps a plain-Numeric arithmetic result into `attribute`'s own declared
340
+ # value-object type, when the arithmetic itself ran unwrapped.
341
+ #
272
342
  # `MutationApplier#rewrap_arithmetic_result`'s own entity-scoped
273
343
  # twin, byte-for-byte the same fix — see that method's own
274
344
  # comment for the full "phantom-field asymmetric wrapping" story.
275
345
  # A no-op whenever `current` was already a Value (the arithmetic
276
346
  # call already returned one) or the mutation targets no declared
277
347
  # attribute at all.
348
+ #
349
+ # @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
350
+ # coercion
351
+ # @param attribute [Bluebook::Attribute, nil] the mutated field's own declared
352
+ # attribute; a no-op when nil (targets no declared attribute)
353
+ # @param current [Object] the field's pre-mutation value, as read off `pre`
354
+ # @param result [Object] the arithmetic op's own result
355
+ # @return [Runtime::Value, Object] `result` unchanged when already a `Value`,
356
+ # `current` is a `Value`, or `attribute` is nil; otherwise `result` coerced
357
+ # into `attribute`'s own declared type
278
358
  def rewrap_arithmetic_result(aggregate, attribute, current, result)
279
359
  return result if current.is_a?(Value) || attribute.nil? || result.is_a?(Value)
280
360
 
281
361
  Value.for_attribute(aggregate, attribute, result)
282
362
  end
283
363
 
364
+ # Resolves an `:append` mutation's own field source against the offered
365
+ # arguments, falling back to the element's own current field.
366
+ #
284
367
  # `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
368
+ # twin — a caller-supplied arg first, falling back to the
369
+ # element's own current field (never the parent instance's) when
287
370
  # it isn't one.
371
+ #
372
+ # @param source [Symbol, Object] the mutation's own field source: a Symbol
373
+ # names a command argument or, failing that, an element field; anything
374
+ # else is returned as is
375
+ # @param element [Hash{Symbol => Object}] the element to fall back to reading
376
+ # from
377
+ # @param args [Hash{String, Symbol => Object}] the offered command arguments
378
+ # @return [Object, nil] the named argument's value; failing that, the named
379
+ # element field's value; `source` itself when it is not a Symbol
288
380
  def resolve_element_append_source(source, element, args)
289
381
  return source unless source.is_a?(Symbol)
290
382
  return args[source] if args.key?(source)
@@ -293,25 +385,41 @@ module Hecks
293
385
  end
294
386
 
295
387
  # `MutationApplier#appended`'s own entity-scoped twin. Usually a
296
- # VALUE OBJECT element — an entity's own list, appended to by an
388
+ # value object element — an entity's own list, appended to by an
297
389
  # entity-owned command, holds a value object (`Member.pairs`'
298
390
  # own `Pair`, `Dispatch.with_spec`'s own `Binding`) the same way
299
391
  # most real corpus appends do — but entity-in-entity nesting (a
300
- # list of ANOTHER entity, owned by this one) is real now too:
392
+ # list of another entity, owned by this one) is real now too:
301
393
  # `qa/stress_domains/nested_pieces` (`Board.AddCard`, appending a
302
394
  # `Card` onto `Board`'s own `cards`) is the first corpus member to
303
395
  # do it, the comment this replaces having been written before that
304
396
  # domain existed. `element_type` naming an entity rather than a
305
397
  # 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`.
398
+ # before — `MutationApplier#entity_element`'s own identity-minting
399
+ # fallback still isn't mirrored here (nothing in this corpus needs
400
+ # auto-minting at this depth — Card supplies its own identity in
401
+ # the append mapping) — but its collision-checking fallback now is
402
+ # (BUG#145, `check_entity_collision`, below — see its own call
403
+ # site's comment for why "collision-checking a nested entity is its
404
+ # own separate, unfixed question," this comment's own prior wording,
405
+ # stopped being true). BUG#12's fix (below) is likewise shared:
406
+ # every declared attribute the append mapping doesn't name gets its
407
+ # own default the same way a fresh aggregate's own attributes
408
+ # already do (`Instance.defaults`), whichever branch built `fields`.
409
+ #
410
+ # @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
411
+ # coercion
412
+ # @param entity [Bluebook::Entity] the entity type `element`'s mutated list
413
+ # attribute belongs to
414
+ # @param element [Hash{Symbol => Object}] the element being appended to, read
415
+ # as the source for a field the mutation's own map does not supply directly
416
+ # @param mutation [Bluebook::Mutation] the declared `:append` mutation
417
+ # @param args [Hash{String, Symbol => Object}] the offered command arguments
418
+ # @return [Array] `element[mutation.target]`'s existing elements, frozen deep,
419
+ # with the newly built element (a `Runtime::Value`, or a Hash for an entity
420
+ # or untyped element) appended last
421
+ # @raise [Runtime::AlreadyExists] if the appended element is a nested entity
422
+ # whose identity collides with an existing one
315
423
  def appended_to_element(aggregate, entity, element, mutation, args)
316
424
  fields = mutation.source.transform_values { |source| resolve_element_append_source(source, element, args) }
317
425
  element_type = entity.attribute(mutation.target)&.type
@@ -323,22 +431,53 @@ module Hecks
323
431
  if value_object
324
432
  Value.build(value_object, fields, aggregate)
325
433
  else
326
- # `entity.entities`, NOT `aggregate.entities` — a piece
327
- # nested inside a piece is a child of the OWNING entity
434
+ # `entity.entities`, not `aggregate.entities` — a piece
435
+ # nested inside a piece is a child of the owning entity
328
436
  # (`Card` is `Board.entities`, never `Workspace.entities`;
329
437
  # `Behaviour::Entity#entities` answers direct children only,
330
438
  # by design — see its own comment), the same lexical-nesting
331
439
  # rule `EntityBuilder#entity_impl` builds the tree with in
332
440
  # the first place.
333
441
  nested_entity = entity.entities.find { |piece| piece.hecks_name == element_type.to_s }
334
- nested_entity ? fill_declared_defaults(aggregate, nested_entity, fields) : fields
442
+ if nested_entity
443
+ # BUG#145 — `MutationApplier#entity_element`'s own
444
+ # `check_entity_collision` call (one hop up, an aggregate's
445
+ # own entity list) never had a twin here: a nested entity's
446
+ # own identity is always caller-supplied at this depth
447
+ # (`Card.sequence` rides the append mapping directly — no
448
+ # nested-entity auto-mint exists anywhere in this corpus,
449
+ # see this method's own header), so this is unconditional,
450
+ # unlike `#entity_element`'s own auto-mint/collision `if`/
451
+ # `else` split — there is no auto-mint branch here to skip.
452
+ # Found live: `qa/stress_domains/nested_pieces`'s own
453
+ # differential sweep, `Board.AddCard` dispatched twice under
454
+ # the same `sequence` — Ruby silently appended a second
455
+ # `Card`, Rust's generated code (`rust/project/mutations.rb`'s
456
+ # `emit_mutation_line_body`, which never splits an
457
+ # aggregate-owned append from an entity-owned one the way
458
+ # this runtime's two separate methods do) already refused
459
+ # `AlreadyExists` for both depths.
460
+ # `entity` (the owner — `Board`), not `aggregate` (the root
461
+ # — `Workspace`), is what the refusal names as "on {…}" —
462
+ # `check_entity_collision`'s first argument is only ever
463
+ # used for that one naming purpose (`owner.hecks_name`,
464
+ # below), matching Rust's own generated wording exactly
465
+ # (`rust/project/mutations.rb`'s own `collision_guard`
466
+ # passes the entity's declaring construct's name the same
467
+ # way — "a Card already exists on Board", never "…on
468
+ # Workspace").
469
+ check_entity_collision(entity, nested_entity, element[mutation.target], fields)
470
+ fill_declared_defaults(aggregate, nested_entity, fields)
471
+ else
472
+ fields
473
+ end
335
474
  end
336
475
  Freezer.deep(Array(element[mutation.target]) + [appended])
337
476
  end
338
477
 
339
478
  # 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
479
+ # to leave any of its own declared attributes the append mapping
480
+ # simply didn't name (an optional field a later, separate command
342
481
  # sets — `Board.label`, `Card.note`) absent from the stored hash
343
482
  # entirely, not even a `nil` placeholder, until that later command
344
483
  # actually ran. `rust/project/json_codec.rb#emit_to_json_flat`'s
@@ -346,16 +485,25 @@ module Hecks
346
485
  # contract for a persisted record: every declared field present,
347
486
  # `null` when unset, "because Ruby's own `JSON.generate(state)`
348
487
  # round-trip this mirrors does the same" — true for a freshly
349
- # created AGGREGATE (`Instance.defaults` already fills one key per
488
+ # created aggregate (`Instance.defaults` already fills one key per
350
489
  # declared attribute, `default_for` per attribute), never true for
351
490
  # an entity minted by an append. This closes that gap the same
352
- # way: `Instance.default_for` is the SAME per-attribute default
491
+ # way: `Instance.default_for` is the same per-attribute default
353
492
  # rule (nil with no declared `default:`, a fully-defaulted value
354
- # object when every one of ITS OWN fields has one), reused rather
493
+ # object when every one of its own fields has one), reused rather
355
494
  # than reimplemented so the two creation paths can never drift on
356
495
  # what "the default" means. Additive only — a key `fields` already
357
496
  # holds (the append mapping, an auto-minted identity, a lifecycle
358
497
  # default) is never overwritten.
498
+ #
499
+ # @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
500
+ # coercion
501
+ # @param entity [Bluebook::Entity] the entity type whose declared attributes
502
+ # are defaulted
503
+ # @param fields [Hash{Symbol => Object}] the element fields already resolved;
504
+ # written in place
505
+ # @return [Hash{Symbol => Object}] `fields`, with every declared attribute it
506
+ # did not already hold filled with its own default
359
507
  def fill_declared_defaults(aggregate, entity, fields)
360
508
  entity.attributes.each do |attribute|
361
509
  next if fields.key?(attribute.name)
@@ -366,11 +514,23 @@ module Hecks
366
514
  end
367
515
 
368
516
  # `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
517
+ # value equality, element-wise, the same "so a concurrent Add can
518
+ # never be lost" reasoning that method's own comment gives, unless
371
519
  # the list this targets is itself entity-typed — see
372
520
  # `list_element_match?`, below, which both this and
373
521
  # `MutationApplier#removed` now share.
522
+ #
523
+ # @param rules [Runtime::CommandRules] the shared rules engine `resolve_source`
524
+ # is read through
525
+ # @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
526
+ # coercion
527
+ # @param entity [Bluebook::Entity] the entity type `element`'s mutated list
528
+ # attribute belongs to
529
+ # @param element [Hash{Symbol => Object}] the element being removed from
530
+ # @param mutation [Bluebook::Mutation] the declared `:remove` mutation
531
+ # @param args [Hash{String, Symbol => Object}] the offered command arguments
532
+ # @return [Array] `element[mutation.target]`'s existing elements, with any
533
+ # matching the resolved remove target left out
374
534
  def removed_from_element(rules, aggregate, entity, element, mutation, args)
375
535
  value = rules.resolve_source(mutation.source, args)
376
536
  attribute = entity.attribute(mutation.target)
@@ -378,23 +538,23 @@ module Hecks
378
538
  Array(element[mutation.target]).reject { |candidate| list_element_match?(aggregate, attribute, candidate, value) }
379
539
  end
380
540
 
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
541
+ # BUG#32 (QualityControl ledger) — the match rule `remove:` uses
542
+ # against one stored list element. Value equality for a
543
+ # value-object-typed list stays exactly what it always was — an
384
544
  # element and `value` are both real `Value`s there, so `==` already
385
545
  # compares every field, the "concurrent Add can never be lost"
386
546
  # shape `removed`/`removed_from_element`'s own headers describe.
387
- # An ENTITY-typed list is different in kind, not just in type: a
547
+ # An entity-typed list is different in kind, not just in type: a
388
548
  # stored element is a plain Hash, never a `Value` (`Entity`'s own
389
549
  # header — "an entity must never answer .value_object"), so there
390
550
  # 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
551
+ # entity's own identity field, the same field a caller already has
392
552
  # to name to address that element any other way
393
553
  # (`element_of`'s own `wants`, above). `value` arrives here already
394
554
  # coerced against that identity field's declared type
395
555
  # (`Coercion#hydrate_entity_identity`, run underneath
396
556
  # `Value.for_attribute` before either caller above ever sees it),
397
- # so this only has to know WHICH field to read off the stored
557
+ # so this only has to know which field to read off the stored
398
558
  # element — `entity.identity_heads`'s own single head, when there
399
559
  # is exactly one. A composite identity (more than one head, or
400
560
  # none) has no single field a bare `remove:` target could mean —
@@ -405,11 +565,22 @@ module Hecks
405
565
  # entity identity elsewhere in this runtime.
406
566
  #
407
567
  # Shared by `MutationApplier#removed` (an aggregate's own list) and
408
- # `#removed_from_element` (a list an ENTITY owns), so the two
568
+ # `#removed_from_element` (a list an entity owns), so the two
409
569
  # `remove:` call sites can never quietly disagree on what
410
570
  # "matches" means — the same reasoning this file's own header
411
571
  # gives for centralizing `locate_chain`/`element_of` once rather
412
572
  # than twice.
573
+ #
574
+ # @param aggregate [Bluebook::Aggregate] the root aggregate, to resolve
575
+ # whether `attribute`'s own type is an entity
576
+ # @param attribute [Bluebook::Attribute, nil] the list attribute `element`
577
+ # belongs to
578
+ # @param element [Object] the stored list element to check: a Hash for an
579
+ # entity-typed list, a `Runtime::Value` otherwise
580
+ # @param value [Object] the `remove:` target to match `element` against
581
+ # @return [Boolean] whole-value equality for a non-entity-typed list;
582
+ # identity-field equality for an entity-typed one with a single identity
583
+ # head; false for a composite or absent identity
413
584
  def list_element_match?(aggregate, attribute, element, value)
414
585
  entity = attribute&.list? ? Value.find_entity(aggregate, attribute.type.to_s) : nil
415
586
  return element == value unless entity
@@ -419,6 +590,72 @@ module Hecks
419
590
 
420
591
  element.is_a?(Hash) && element[head] == value
421
592
  end
593
+
594
+ # Refuses a caller-supplied or composite identity that already names an
595
+ # element on `current`.
596
+ #
597
+ # BUG#13 — the same check #hydrate gives every creating
598
+ # aggregate command (`repository.find(id)`,
599
+ # `command_interpreter.rb`), one level down. Shared with
600
+ # `MutationApplier` (mutation_applier.rb), called from
601
+ # `#entity_element` — an aggregate's own entity list (`Workspace.
602
+ # boards`, `Ledger.entries`) — and, moved here (BUG#145), from `#appended_
603
+ # to_element`, above — an entity's own nested entity list one hop
604
+ # further in (`Board.cards`) — so neither call site reimplements
605
+ # it a second time, the same way `#list_element_
606
+ # match?` already avoids that split for `remove:`.
607
+ #
608
+ # Reached, at the aggregate-owned call site, only on the two
609
+ # branches that do not auto-mint: a caller-supplied identity (the
610
+ # field is already in the append's own field map) or a composite
611
+ # one (`entity.identified_by` is nil for those — Runtime::
612
+ # Identified#derive_identity). Without this, neither checks the sibling
613
+ # list at all: a second LogVisit with the same date+sequence, or a
614
+ # second IssueKey with the same serial, would append a silent
615
+ # duplicate — worse than an ordinary duplicate row, because
616
+ # `EntityElement#element_of`'s own `find_index` always matches the
617
+ # first match, so the second becomes permanently unaddressable by
618
+ # any later command. At the entity-owned call site (`#appended_to_
619
+ # element`), there is no auto-mint branch at all — every caller
620
+ # reaches this unconditionally, since a nested entity's own
621
+ # identity is always caller-supplied in this corpus.
622
+ #
623
+ # Auto-minted (aggregate-owned) entities never reach here —
624
+ # `identity_heads` for them is still checked at mint time by
625
+ # construction (`current.size + 1` can only repeat if something
626
+ # `remove:`s from the list between mints, which no real domain does
627
+ # today), so they can't be flagged by mistake.
628
+ # `owner` — the declaring construct named in the "…already exists on
629
+ # {owner}" wording: the root aggregate at the aggregate-owned call
630
+ # site (`Workspace`, `Ledger`), the immediately-enclosing entity at
631
+ # the entity-owned one (`Board` — never the root `Workspace` two
632
+ # hops up). Used for that naming purpose only (`owner.hecks_name`) —
633
+ # never for `Value`/namespace resolution, which is why an `Entity`
634
+ # (not just an `Aggregate`) is a valid thing to pass here.
635
+ #
636
+ # @param owner [Bluebook::Aggregate, Bluebook::Entity] the construct named
637
+ # in a refusal as what the duplicate "already exists on"
638
+ # @param entity [Bluebook::Entity] the entity type being checked for a
639
+ # colliding identity
640
+ # @param current [Array<Hash>, Object] the entity's own existing elements;
641
+ # coerced through `Array()`, so a single element or nil is also accepted
642
+ # @param fields [Hash{Symbol => Object}] the new element's own fields, whose
643
+ # identity heads are checked against every element in `current`
644
+ # @return [void]
645
+ # @raise [Runtime::AlreadyExists] if `current` already holds an element
646
+ # whose identity heads match `fields`'s own
647
+ def check_entity_collision(owner, entity, current, fields)
648
+ heads = entity.identity_heads
649
+ return if heads.empty?
650
+
651
+ collision = Array(current).find { |element| heads.all? { |head| element[head] == fields[head] } }
652
+ return unless collision
653
+
654
+ raise(AlreadyExists, RefusalWording.render_site("AlreadyExists", "entity_duplicate",
655
+ entity: entity.hecks_name, aggregate: owner.hecks_name,
656
+ identity: Identity.reading(entity),
657
+ offered: heads.map { |head| Rendering.describe(fields[head]) }))
658
+ end
422
659
  end
423
660
  end
424
661
  end