hecks 2.8.0 → 2.9.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 (404) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +4 -6
  3. data/lib/hecks/adapters/driven/claude_code.rb +4 -104
  4. data/lib/hecks/adapters/driven/d1.rb +44 -237
  5. data/lib/hecks/adapters/driven/folder.rb +17 -76
  6. data/lib/hecks/adapters/driven/google_authentication.rb +15 -49
  7. data/lib/hecks/adapters/driven/governance_authorization.rb +23 -70
  8. data/lib/hecks/adapters/driven/heki/journal.rb +8 -41
  9. data/lib/hecks/adapters/driven/heki/saga_store.rb +12 -54
  10. data/lib/hecks/adapters/driven/heki/snapshot.rb +4 -12
  11. data/lib/hecks/adapters/driven/heki.rb +13 -42
  12. data/lib/hecks/adapters/driven/identity_registry.rb +3 -14
  13. data/lib/hecks/adapters/driven/in_memory_ordering.rb +9 -26
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +2 -14
  15. data/lib/hecks/adapters/driven/lambda/client.rb +10 -51
  16. data/lib/hecks/adapters/driven/lambda.rb +10 -84
  17. data/lib/hecks/adapters/driven/local_storage.rb +5 -40
  18. data/lib/hecks/adapters/driven/memory.rb +17 -98
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +5 -21
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +6 -28
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +3 -12
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +4 -30
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +34 -108
  24. data/lib/hecks/adapters/driven/postgres.rb +145 -134
  25. data/lib/hecks/adapters/driven/postgres_era.adapter +4 -15
  26. data/lib/hecks/adapters/driven/prism.rb +7 -30
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +21 -121
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +8 -40
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +26 -82
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +40 -84
  31. data/lib/hecks/adapters/driven/sqlite.rb +128 -65
  32. data/lib/hecks/adapters/driven/tenant_provisioner.rb +5 -48
  33. data/lib/hecks/adapters/driven.rb +5 -36
  34. data/lib/hecks/adapters/driving/github_webhook.rb +8 -76
  35. data/lib/hecks/behaviors/dsl.rb +7 -25
  36. data/lib/hecks/behaviors/expectations.rb +31 -141
  37. data/lib/hecks/behaviors/ir.rb +7 -20
  38. data/lib/hecks/behaviors/rspec.rb +6 -17
  39. data/lib/hecks/behaviors/runner.rb +10 -34
  40. data/lib/hecks/behaviors.rb +4 -8
  41. data/lib/hecks/bluebook/aggregate.rb +9 -75
  42. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +9 -38
  43. data/lib/hecks/bluebook/assembly/build.rb +4 -15
  44. data/lib/hecks/bluebook/assembly/contract.rb +15 -136
  45. data/lib/hecks/bluebook/assembly/contracts.rb +54 -174
  46. data/lib/hecks/bluebook/assembly/marks.rb +31 -223
  47. data/lib/hecks/bluebook/assembly/specializer.rb +6 -52
  48. data/lib/hecks/bluebook/assembly.rb +6 -45
  49. data/lib/hecks/bluebook/attribute.rb +12 -61
  50. data/lib/hecks/bluebook/behaviour/aggregate.rb +10 -39
  51. data/lib/hecks/bluebook/behaviour/attribute.rb +3 -14
  52. data/lib/hecks/bluebook/behaviour/chapter.rb +14 -42
  53. data/lib/hecks/bluebook/behaviour/command.rb +14 -66
  54. data/lib/hecks/bluebook/behaviour/domain_port.rb +4 -20
  55. data/lib/hecks/bluebook/behaviour/entity.rb +7 -27
  56. data/lib/hecks/bluebook/behaviour/hexagon.rb +23 -41
  57. data/lib/hecks/bluebook/behaviour/lifecycle.rb +15 -40
  58. data/lib/hecks/bluebook/behaviour/policy.rb +23 -52
  59. data/lib/hecks/bluebook/behaviour/process_manager.rb +30 -57
  60. data/lib/hecks/bluebook/behaviour/read_model.rb +25 -31
  61. data/lib/hecks/bluebook/behaviour/traits.rb +27 -64
  62. data/lib/hecks/bluebook/behaviour/value_object.rb +5 -24
  63. data/lib/hecks/bluebook/capabilities.rb +17 -49
  64. data/lib/hecks/bluebook/chapter.rb +10 -48
  65. data/lib/hecks/bluebook/command.rb +23 -81
  66. data/lib/hecks/bluebook/domain_port.rb +14 -63
  67. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +50 -229
  68. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +50 -377
  69. data/lib/hecks/bluebook/dsl/attribute_collector.rb +33 -204
  70. data/lib/hecks/bluebook/dsl/binding_proxy.rb +16 -69
  71. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +86 -708
  72. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +56 -170
  73. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
  74. data/lib/hecks/bluebook/dsl/command_builder.rb +74 -546
  75. data/lib/hecks/bluebook/dsl/const_shim.rb +8 -41
  76. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +12 -87
  77. data/lib/hecks/bluebook/dsl/entity_builder.rb +44 -330
  78. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +20 -269
  79. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +26 -194
  80. data/lib/hecks/bluebook/dsl/identity_declaration.rb +13 -88
  81. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +6 -20
  82. data/lib/hecks/bluebook/dsl/policy_builder.rb +16 -88
  83. data/lib/hecks/bluebook/dsl/port_builder.rb +2 -6
  84. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +22 -96
  85. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +31 -184
  86. data/lib/hecks/bluebook/dsl/query_builder.rb +11 -42
  87. data/lib/hecks/bluebook/dsl/read_model_builder.rb +75 -201
  88. data/lib/hecks/bluebook/dsl/rule_reference.rb +22 -163
  89. data/lib/hecks/bluebook/dsl/translation_builder.rb +28 -114
  90. data/lib/hecks/bluebook/dsl/value_object_builder.rb +10 -124
  91. data/lib/hecks/bluebook/dsl/word_gate.rb +16 -162
  92. data/lib/hecks/bluebook/dsl/world_builder.rb +19 -111
  93. data/lib/hecks/bluebook/entity.rb +13 -71
  94. data/lib/hecks/bluebook/expression/ast_json.rb +24 -190
  95. data/lib/hecks/bluebook/expression/ast_reader.rb +12 -54
  96. data/lib/hecks/bluebook/expression/canonical_form.rb +11 -88
  97. data/lib/hecks/bluebook/expression/evaluator.rb +47 -295
  98. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +30 -147
  99. data/lib/hecks/bluebook/expression/resolver.rb +37 -660
  100. data/lib/hecks/bluebook/hexagon.rb +5 -28
  101. data/lib/hecks/bluebook/lifecycle.rb +5 -16
  102. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -7
  103. data/lib/hecks/bluebook/meta_validator/judge.rb +111 -361
  104. data/lib/hecks/bluebook/meta_validator/plan.rb +33 -162
  105. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -7
  106. data/lib/hecks/bluebook/meta_validator/readings.rb +56 -342
  107. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -176
  108. data/lib/hecks/bluebook/meta_validator/shapes.rb +53 -267
  109. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +43 -305
  110. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +7 -43
  111. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -18
  112. data/lib/hecks/bluebook/meta_validator.rb +79 -349
  113. data/lib/hecks/bluebook/model_check/client_profile.rb +6 -21
  114. data/lib/hecks/bluebook/model_check.rb +100 -597
  115. data/lib/hecks/bluebook/pattern_subset.rb +11 -69
  116. data/lib/hecks/bluebook/process_manager.rb +13 -83
  117. data/lib/hecks/bluebook/project_loader.rb +3 -39
  118. data/lib/hecks/bluebook/project_register.rb +12 -60
  119. data/lib/hecks/bluebook/query.rb +9 -48
  120. data/lib/hecks/bluebook/read_model.rb +57 -81
  121. data/lib/hecks/bluebook/reference.rb +7 -41
  122. data/lib/hecks/bluebook/smoke_test.rb +8 -98
  123. data/lib/hecks/bluebook/synthesizer.rb +13 -62
  124. data/lib/hecks/bluebook/translation.rb +26 -62
  125. data/lib/hecks/bluebook/value_object.rb +11 -36
  126. data/lib/hecks/bluebook.rb +5 -22
  127. data/lib/hecks/cache_dir.rb +4 -23
  128. data/lib/hecks/cli/document.rb +3 -21
  129. data/lib/hecks/cli/ir.rb +3 -5
  130. data/lib/hecks/cli/mcp.rb +2 -3
  131. data/lib/hecks/cli/mcp_door.rb +19 -114
  132. data/lib/hecks/cli/model_check.rb +18 -85
  133. data/lib/hecks/cli/project_cli.rb +7 -40
  134. data/lib/hecks/cli/project_diagrams.rb +0 -7
  135. data/lib/hecks/cli/run.rb +12 -58
  136. data/lib/hecks/cli/smoke_test.rb +2 -16
  137. data/lib/hecks/cli/stores.rb +1 -2
  138. data/lib/hecks/cli.rb +18 -24
  139. data/lib/hecks/construct.rb +8 -34
  140. data/lib/hecks/deploy/bluebook/deploy.bluebook +33 -142
  141. data/lib/hecks/deploy/bluebook/deploy.hecksagon +2 -9
  142. data/lib/hecks/embryonaut_bluebook/lock.rb +3 -35
  143. data/lib/hecks/embryonaut_bluebook/shape.rb +1 -17
  144. data/lib/hecks/embryonaut_bluebook/vendor.rb +8 -53
  145. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +4 -19
  146. data/lib/hecks/embryonaut_bluebook.rb +4 -71
  147. data/lib/hecks/facade/cli_door.rb +24 -106
  148. data/lib/hecks/facade/cli_runner.rb +28 -172
  149. data/lib/hecks/facade/command_request.rb +13 -33
  150. data/lib/hecks/facade/handle.rb +34 -121
  151. data/lib/hecks/facade/json_door.rb +31 -146
  152. data/lib/hecks/facade/surface/aggregate_door.rb +39 -104
  153. data/lib/hecks/facade/surface/chapter.rb +10 -54
  154. data/lib/hecks/facade/surface.rb +8 -23
  155. data/lib/hecks/facade.rb +10 -25
  156. data/lib/hecks/forms/app.rb +15 -70
  157. data/lib/hecks/forms/command_form_renderer.rb +10 -29
  158. data/lib/hecks/forms/field_renderer.rb +17 -156
  159. data/lib/hecks/forms/field_shape.rb +30 -222
  160. data/lib/hecks/forms/html.rb +12 -47
  161. data/lib/hecks/forms/index_renderer.rb +1 -4
  162. data/lib/hecks/forms/page.rb +3 -9
  163. data/lib/hecks/forms/params.rb +14 -60
  164. data/lib/hecks/forms/port_argument.rb +2 -19
  165. data/lib/hecks/forms/query_form_renderer.rb +4 -79
  166. data/lib/hecks/forms/record_renderer.rb +8 -75
  167. data/lib/hecks/forms/record_table.rb +7 -50
  168. data/lib/hecks/forms/reference_options.rb +3 -30
  169. data/lib/hecks/forms/value_object_shape.rb +8 -35
  170. data/lib/hecks/forms.rb +5 -33
  171. data/lib/hecks/fqn.rb +5 -12
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +16 -65
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +47 -134
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +5 -14
  175. data/lib/hecks/framework/bluebook/governance.bluebook +13 -34
  176. data/lib/hecks/framework/bluebook/identity.bluebook +11 -26
  177. data/lib/hecks/framework/bluebook/privacy.bluebook +8 -33
  178. data/lib/hecks/framework.rb +11 -80
  179. data/lib/hecks/freezer.rb +5 -39
  180. data/lib/hecks/grammar/expression.bluebook +15 -40
  181. data/lib/hecks/grammar/translation.bluebook +3 -6
  182. data/lib/hecks/grammar.rb +16 -41
  183. data/lib/hecks/ir.rb +14 -83
  184. data/lib/hecks/language/adapter.bluebook +2 -7
  185. data/lib/hecks/language/bluebook/aggregate.bluebook +56 -221
  186. data/lib/hecks/language/bluebook/attaches/paging.bluebook +6 -15
  187. data/lib/hecks/language/bluebook/bluebook.bluebook +15 -83
  188. data/lib/hecks/language/bluebook/bluebook.hecksagon +3 -15
  189. data/lib/hecks/language/bluebook/command.bluebook +27 -144
  190. data/lib/hecks/language/bluebook/entity.bluebook +24 -97
  191. data/lib/hecks/language/bluebook/policy.bluebook +16 -67
  192. data/lib/hecks/language/bluebook/process_manager.bluebook +28 -151
  193. data/lib/hecks/language/bluebook/projection.bluebook +126 -60
  194. data/lib/hecks/language/bluebook/query.bluebook +12 -43
  195. data/lib/hecks/language/bluebook/shape.bluebook +23 -73
  196. data/lib/hecks/language/bluebook/syntax.bluebook +51 -262
  197. data/lib/hecks/language/bluebook/vocabulary.bluebook +82 -197
  198. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +3 -10
  199. data/lib/hecks/language/hecksagon/domain_port.bluebook +4 -19
  200. data/lib/hecks/language/hecksagon/hecksagon.bluebook +6 -25
  201. data/lib/hecks/language/hecksagon/port_operation.bluebook +3 -14
  202. data/lib/hecks/language/oidc.json +35 -0
  203. data/lib/hecks/language/port.bluebook +4 -17
  204. data/lib/hecks/language/translation/translation.bluebook +3 -9
  205. data/lib/hecks/language/translation/translation_aggregate.bluebook +5 -24
  206. data/lib/hecks/language/world/wiring.bluebook +1 -3
  207. data/lib/hecks/language/world/world.bluebook +5 -7
  208. data/lib/hecks/literal.rb +8 -50
  209. data/lib/hecks/mcp_door_scope.rb +9 -90
  210. data/lib/hecks/mcp_stdio_guard.rb +4 -41
  211. data/lib/hecks/naming.rb +29 -137
  212. data/lib/hecks/ports/access_control.rb +7 -23
  213. data/lib/hecks/ports/agent/answers.rb +5 -23
  214. data/lib/hecks/ports/agent.rb +47 -172
  215. data/lib/hecks/ports/authentication.rb +12 -39
  216. data/lib/hecks/ports/authorization.rb +16 -43
  217. data/lib/hecks/ports/clock.rb +5 -45
  218. data/lib/hecks/ports/extraction.rb +7 -12
  219. data/lib/hecks/ports/identity_assignment.rb +8 -29
  220. data/lib/hecks/ports/identity_generation.rb +6 -29
  221. data/lib/hecks/ports/identity_resolution.rb +2 -10
  222. data/lib/hecks/ports/key_vault.rb +9 -45
  223. data/lib/hecks/ports/persistence/append_only.rb +61 -64
  224. data/lib/hecks/ports/persistence/binding_policy.rb +4 -14
  225. data/lib/hecks/ports/persistence/codec_boundary.rb +20 -80
  226. data/lib/hecks/ports/persistence/null_saga_store.rb +4 -20
  227. data/lib/hecks/ports/persistence/plugin.rb +13 -42
  228. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +38 -141
  229. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +15 -115
  230. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +16 -84
  231. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +6 -25
  232. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -22
  233. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +55 -181
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/events.rb +77 -0
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +12 -122
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +28 -128
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +73 -474
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +31 -124
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +45 -219
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +13 -108
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +18 -75
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +9 -49
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +6 -123
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +23 -55
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +14 -72
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +7 -16
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +14 -115
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +13 -45
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +127 -554
  250. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +13 -63
  251. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +2 -17
  252. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +3 -14
  253. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +13 -58
  254. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -17
  255. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +9 -26
  256. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +9 -25
  257. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -124
  258. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +34 -123
  259. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +5 -12
  260. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +3 -9
  261. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -14
  262. data/lib/hecks/ports/persistence/plugins/era.rb +4 -23
  263. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +7 -23
  264. data/lib/hecks/ports/persistence/postgres_dump.rb +4 -35
  265. data/lib/hecks/ports/persistence/remote_runtime.rb +2 -24
  266. data/lib/hecks/ports/persistence/repository_factory.rb +8 -27
  267. data/lib/hecks/ports/persistence/state_codec.rb +23 -103
  268. data/lib/hecks/ports/persistence.rb +7 -14
  269. data/lib/hecks/ports/projection.rb +42 -84
  270. data/lib/hecks/ports/query/in_memory.rb +10 -58
  271. data/lib/hecks/ports/query/ordering.rb +11 -32
  272. data/lib/hecks/ports/query.rb +14 -37
  273. data/lib/hecks/ports.rb +2 -8
  274. data/lib/hecks/projections/bootstrap_table.rb +11 -43
  275. data/lib/hecks/projections/deploy/fargate/assembly.rb +2 -15
  276. data/lib/hecks/projections/deploy/fargate/cdn.rb +1 -48
  277. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +2 -11
  278. data/lib/hecks/projections/deploy/fargate/check.rb +5 -14
  279. data/lib/hecks/projections/deploy/fargate/containers.rb +3 -78
  280. data/lib/hecks/projections/deploy/fargate/extras.rb +3 -44
  281. data/lib/hecks/projections/deploy/fargate/monitoring.rb +0 -44
  282. data/lib/hecks/projections/deploy/fargate/settings.rb +2 -54
  283. data/lib/hecks/projections/deploy/fargate/yaml.rb +13 -58
  284. data/lib/hecks/projections/deploy/fargate.rb +35 -202
  285. data/lib/hecks/projections/deploy/lambda.rb +150 -678
  286. data/lib/hecks/projections/deploy/preview/access_yaml.rb +1 -33
  287. data/lib/hecks/projections/deploy/preview/containers.rb +6 -111
  288. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +4 -56
  289. data/lib/hecks/projections/deploy/preview/script.rb +2 -14
  290. data/lib/hecks/projections/deploy/preview/settings.rb +3 -33
  291. data/lib/hecks/projections/deploy/preview/task_yaml.rb +2 -38
  292. data/lib/hecks/projections/deploy/preview/template.rb +2 -58
  293. data/lib/hecks/projections/deploy/preview.rb +2 -67
  294. data/lib/hecks/projections/deploy/scripts/settings.rb +3 -25
  295. data/lib/hecks/projections/deploy/scripts.rb +4 -57
  296. data/lib/hecks/projections/deploy/shared.rb +47 -146
  297. data/lib/hecks/projections/deploy/smoke.rb +4 -61
  298. data/lib/hecks/projections/deploy/template_diff/comparer.rb +3 -25
  299. data/lib/hecks/projections/deploy/template_diff/loader.rb +2 -11
  300. data/lib/hecks/projections/deploy/template_diff.rb +5 -57
  301. data/lib/hecks/projections/diagrams.rb +20 -582
  302. data/lib/hecks/projections/glossary/html.rb +14 -130
  303. data/lib/hecks/projections/glossary/markdown.rb +8 -69
  304. data/lib/hecks/projections/glossary/mermaid.rb +9 -66
  305. data/lib/hecks/projections/glossary/sensitivity.rb +5 -34
  306. data/lib/hecks/projections/glossary/sentences.rb +14 -159
  307. data/lib/hecks/projections/glossary.rb +30 -222
  308. data/lib/hecks/projections/ir.rb +3 -10
  309. data/lib/hecks/projections/model/deviations.rb +15 -42
  310. data/lib/hecks/projections/model.rb +12 -51
  311. data/lib/hecks/projections/oidc.rb +12 -56
  312. data/lib/hecks/projections/parser_table.rb +9 -28
  313. data/lib/hecks/projections/reference.rb +2 -15
  314. data/lib/hecks/projections/rust_vocabulary.rb +12 -250
  315. data/lib/hecks/projections/shape.rb +3 -29
  316. data/lib/hecks/projections/statements.rb +7 -86
  317. data/lib/hecks/projections/vocabulary.rb +6 -22
  318. data/lib/hecks/projections.rb +2 -16
  319. data/lib/hecks/projector/cli_projector.rb +60 -375
  320. data/lib/hecks/projector/docs_projector.rb +10 -201
  321. data/lib/hecks/projector/exporter.rb +42 -134
  322. data/lib/hecks/projector/ir_projector.rb +2 -9
  323. data/lib/hecks/projector/narrate_projector.rb +15 -189
  324. data/lib/hecks/projector/target.rb +13 -76
  325. data/lib/hecks/projector.rb +30 -121
  326. data/lib/hecks/query_specification/common/comparators.rb +5 -26
  327. data/lib/hecks/query_specification/common/comparison.rb +35 -143
  328. data/lib/hecks/query_specification/common/dsl.rb +8 -24
  329. data/lib/hecks/query_specification/common/null_policy.rb +20 -70
  330. data/lib/hecks/query_specification/common/options.rb +2 -6
  331. data/lib/hecks/query_specification/common/where_clause.rb +2 -10
  332. data/lib/hecks/query_specification/field_path.rb +37 -77
  333. data/lib/hecks/query_specification/hop_path.rb +12 -76
  334. data/lib/hecks/release/gem_pin.rb +2 -26
  335. data/lib/hecks/release/runner/ci_publisher.rb +3 -6
  336. data/lib/hecks/release/runner/commands.rb +2 -4
  337. data/lib/hecks/release/runner/gem_publisher.rb +2 -3
  338. data/lib/hecks/release/runner/npm_publisher.rb +6 -20
  339. data/lib/hecks/release/runner/preflight.rb +4 -19
  340. data/lib/hecks/release/runner/tagger.rb +2 -9
  341. data/lib/hecks/release/runner.rb +3 -32
  342. data/lib/hecks/rendering.rb +4 -26
  343. data/lib/hecks/router/namespace_installer.rb +12 -36
  344. data/lib/hecks/router.rb +4 -8
  345. data/lib/hecks/runtime/aggregate_lock.rb +8 -33
  346. data/lib/hecks/runtime/boot_gates.rb +3 -18
  347. data/lib/hecks/runtime/caller.rb +19 -59
  348. data/lib/hecks/runtime/capability_graph.rb +5 -21
  349. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +16 -78
  350. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +23 -160
  351. data/lib/hecks/runtime/command_interpreter.rb +124 -306
  352. data/lib/hecks/runtime/command_rules/admissibility.rb +95 -366
  353. data/lib/hecks/runtime/command_rules/arithmetic.rb +66 -196
  354. data/lib/hecks/runtime/command_rules/authorization.rb +13 -45
  355. data/lib/hecks/runtime/command_rules/emission.rb +8 -21
  356. data/lib/hecks/runtime/command_rules/references.rb +23 -202
  357. data/lib/hecks/runtime/dependency_planning.rb +30 -102
  358. data/lib/hecks/runtime/dispatcher.rb +82 -241
  359. data/lib/hecks/runtime/entity_element.rb +40 -462
  360. data/lib/hecks/runtime/entity_interpreter.rb +64 -219
  361. data/lib/hecks/runtime/errors.rb +32 -115
  362. data/lib/hecks/runtime/event.rb +3 -25
  363. data/lib/hecks/runtime/identity.rb +18 -154
  364. data/lib/hecks/runtime/instance.rb +18 -150
  365. data/lib/hecks/runtime/interpreting.rb +11 -70
  366. data/lib/hecks/runtime/invocation.rb +28 -164
  367. data/lib/hecks/runtime/loader.rb +48 -186
  368. data/lib/hecks/runtime/outbox.rb +38 -261
  369. data/lib/hecks/runtime/policy_interpreter.rb +38 -200
  370. data/lib/hecks/runtime/port_operation_interpreter.rb +26 -87
  371. data/lib/hecks/runtime/query_interpreter.rb +45 -187
  372. data/lib/hecks/runtime/reaction_invocation.rb +36 -138
  373. data/lib/hecks/runtime/read_model_interpreter.rb +117 -208
  374. data/lib/hecks/runtime/rebuild_sweep.rb +8 -54
  375. data/lib/hecks/runtime/reference_hop.rb +8 -81
  376. data/lib/hecks/runtime/refusal_wording.rb +8 -89
  377. data/lib/hecks/runtime/registry/saga_persistence.rb +10 -102
  378. data/lib/hecks/runtime/registry/verification.rb +52 -225
  379. data/lib/hecks/runtime/registry/world_defaults.rb +4 -53
  380. data/lib/hecks/runtime/registry.rb +99 -244
  381. data/lib/hecks/runtime/remote_dispatcher.rb +37 -152
  382. data/lib/hecks/runtime/routing.rb +4 -15
  383. data/lib/hecks/runtime/saga_interpreter/correlation.rb +14 -61
  384. data/lib/hecks/runtime/saga_interpreter.rb +79 -263
  385. data/lib/hecks/runtime/saga_pending_dispatch.rb +2 -43
  386. data/lib/hecks/runtime/tenant_check.rb +6 -68
  387. data/lib/hecks/runtime/tenant_scope.rb +3 -28
  388. data/lib/hecks/runtime/value/admission.rb +16 -67
  389. data/lib/hecks/runtime/value/coercion.rb +155 -641
  390. data/lib/hecks/runtime/value/entity_list_coercion.rb +26 -219
  391. data/lib/hecks/runtime/value.rb +12 -84
  392. data/lib/hecks/runtime.rb +17 -65
  393. data/lib/hecks/storehouse.rb +141 -568
  394. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +8 -32
  395. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +6 -23
  396. data/lib/hecks/vendoring/git_environment.rb +2 -8
  397. data/lib/hecks/vendoring/git_source.rb +4 -10
  398. data/lib/hecks/vendoring.rb +7 -46
  399. data/lib/hecks/version.rb +3 -14
  400. data/lib/hecks.rb +26 -81
  401. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -46
  402. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +11 -80
  403. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +10 -99
  404. metadata +3 -2
@@ -8,69 +8,18 @@ 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
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
15
- # same way, against an aggregate record it already holds in memory
16
- # rather than one freshly loaded from a repository. `EntityInterpreter`
17
- # keeps its own dispatch order and Context; this is the part underneath
18
- # that both now share, so it exists in exactly one place rather than two
19
- # that could only ever drift (the same reasoning `Runtime::Identity`'s
20
- # own header gives for the join/dig/reading trio it centralizes).
21
- #
22
- # `rules` (a `CommandRules` instance) is passed explicitly throughout
23
- # rather than closed over, since this module has no instance of its own
24
- # to hold one.
11
+ # Locates and mutates one entity element within an aggregate record — the shared
12
+ # implementation behind EntityInterpreter and CommandInterpreter's own delegation.
25
13
  module EntityElement
26
14
  module_function
27
15
 
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.
16
+ # A sentinel that never equals a stored element field, since a real field can hold nil.
33
17
  UNMATCHABLE = Object.new.freeze
34
18
  private_constant :UNMATCHABLE
35
19
 
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
40
- # aggregate record) and becomes each just-located element in turn —
41
- # Dispatch's own element is found inside the Handler element
42
- # `locate_chain` located the step before, never inside `instance`
43
- # directly. `owner` is whichever construct's own attribute declares
44
- # the list being searched (Handler declares `dispatches` ; the root
45
- # aggregate declares `handlers`) — `root_aggregate` stays the root
46
- # the whole way through instead, passed to `element_of` separately,
47
- # because coercion (`Value.for_attribute`) resolves value objects
48
- # against the root's own namespace only ; an entity must never
49
- # answer `.value_object` (Entity's own header comment) so handing
50
- # it an intermediate owner instead would break every VO-typed
51
- # identity field a nested entity declares.
52
- #
53
- # `route`, if given, is the routing envelope's own entity chain
54
- # (`ctx.route.entities`, one entry per hop) — the routing/payload
55
- # split's own consequence: an entity's own identity may arrive via
56
- # `to:` rather than duplicated into `args`, so each hop is offered
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
20
+ # Walks `chain` one hop at a time and returns the located element (or `instance`
21
+ # itself when `chain` is empty). `route`, when given, offers each hop's identity
22
+ # before falling back to `args`.
74
23
  def locate_chain(root_aggregate, chain, instance, args, command_name, route = nil)
75
24
  container = instance
76
25
  owner = root_aggregate
@@ -82,42 +31,14 @@ module Hecks
82
31
  container
83
32
  end
84
33
 
85
- # One element, matched on every part of its identity — not just the first.
86
- # A piece's identity may be several paths, the same shape a head's can be,
87
- # so a dispatch that names the element has to supply every part and every
88
- # part has to agree with the stored one. `routed_identity`, when given,
89
- # matches by the element's own minted identity string directly instead
90
- # (`element_identity`, below) — the routing envelope already resolved
91
- # which element it means, so re-deriving `wants` from `args` would be
92
- # redundant at best and wrong if `args` does not carry that identity
93
- # at all.
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
96
- # where it does (aliasing the adapter's own record otherwise).
97
- # Splitting resolution from the copy/write-back would separate two
98
- # halves of one aliasing-safety invariant across method boundaries.
34
+ # Locates one element, matching every part of its declared identity, and copies
35
+ # the owning list and the element before any write so nothing aliases the caller's
36
+ # record. `routed_identity`, when given, matches directly by the element's own
37
+ # identity string instead of re-deriving `wants` from `args`.
99
38
  # rubocop:disable-next Metrics/AbcSize
100
39
  # rubocop:disable-next Metrics/CyclomaticComplexity
101
40
  # rubocop:disable-next Metrics/PerceivedComplexity
102
41
  # 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
121
42
  def element_of(root_aggregate, owner, entity, command_name, container, args, routed_identity = nil)
122
43
  entity_name = entity.hecks_name
123
44
  list_attr = owner.attributes.find { |a| a.list? && a.type.to_s == entity_name } ||
@@ -132,28 +53,9 @@ module Hecks
132
53
  command: command_name, entity: entity_name,
133
54
  identity: Identity.reading(entity)))
134
55
 
135
- # An identity offered for addressing, not for storage
136
- # (BUG#3, found live by `bin/qa_sweep` — banking fuzz seed
137
- # 23, `LedgerEntry.Amend sequence: { value: 0 }` against an
138
- # entry-less ledger). Coercing it all the way to a typed
139
- # `Value` here ran that type's own invariant before this
140
- # method ever checks whether any element matches — a
141
- # `sequence: 0` against `LedgerSequence`'s own "a ledger
142
- # sequence is positive" invariant raised InvariantViolation,
143
- # not NotFound, even when (as here) nothing was ever posted
144
- # at all. Every element actually in the list already
145
- # satisfied its own type's invariant the moment it was
146
- # created, so a value that fails it can never equal one —
147
- # degrading to `UNMATCHABLE` here, instead of propagating,
148
- # is exactly as safe as the ordinary "no match found" case
149
- # below, and lines this addressing path up with the two
150
- # conventions it already disagreed with: Rust's own
151
- # `extract_id`/`extract_wants` (a raw scalar read, never a
152
- # typed rebuild — rust/src/generated/*/*.rs) and
153
- # `Identity.from`'s own raw-comparison convention for a
154
- # root aggregate's identity (this file's sibling,
155
- # `identity.rb`). `raw` rides alongside `want` so the
156
- # eventual NotFound below can still quote what was offered.
56
+ # A value that fails its own type's invariant can never match a stored
57
+ # element (every stored one already satisfies it) — degrade to
58
+ # `UNMATCHABLE` here rather than letting InvariantViolation propagate.
157
59
  want = begin
158
60
  Value.for_attribute(root_aggregate, entity.attribute(head), raw)
159
61
  rescue InvariantViolation
@@ -182,19 +84,9 @@ module Hecks
182
84
  )
183
85
  end
184
86
 
185
- # One level deeper than Instance#dup, for the same reason: a list
186
- # attribute holds Hashes, and `apply_to_element` mutates the found
187
- # one in place — the update mechanism for an entity, not a bug. But
188
- # in place means aliased with the adapter's own record until this
189
- # copies the array and the target element before handing either
190
- # back, and writes the fresh array into `container` so the copy is
191
- # what persists on success and nothing aliased survives a refusal.
192
- # `container[list_attr.name] = copied` reaches `instance` itself
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
196
- # as `container` for the next hop, so nothing further has to
197
- # propagate a write back up the chain by hand.
87
+ # Copies the list and the found element before handing either back — the list
88
+ # attribute holds Hashes, and the caller mutates the returned element in place,
89
+ # so this keeps that mutation off the adapter's own record until it commits.
198
90
  copied = original.dup
199
91
  element = copied[position].dup
200
92
  copied[position] = element
@@ -202,20 +94,8 @@ module Hecks
202
94
  element
203
95
  end
204
96
 
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
208
- # reached — never by opening a value object and taking whatever single
209
- # field is inside. That unwrapping is gone from the language: a piece
210
- # that does not name its fields is refused when the bluebook loads ("an
211
- # entity says what it is known by", "an identity part names something"),
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
97
+ # An element's identity, joined from its declared identity paths (read off the
98
+ # stored Hash, not a dispatch payload).
219
99
  def element_identity(entity, element)
220
100
  parts = entity.identity_paths.map do |path|
221
101
  head = path.to_s.split(".").first.to_sym
@@ -225,65 +105,10 @@ module Hecks
225
105
  Naming.identity(parts)
226
106
  end
227
107
 
228
- # S17, ADR 0026 — `:append`/`:remove`/`:multiply`/`:clamp`, an
229
- # entity-scoped mirror of `CommandInterpreter::MutationApplier
230
- # #apply`'s own four (that module's own header names each one's
231
- # origin). Missing until now — an entity-owned command declaring
232
- # `sets :some_list, append: {...}` matched no `when` here and
233
- # silently no-opped, the one place this language otherwise
234
- # refuses what it cannot check applying nothing instead. `Member`/
235
- # `Dispatch` (S17) are the first real callers: both need to
236
- # append a value-object-typed element (`Pair`/`Binding`) onto a
237
- # list attribute they own, once they become entities of
238
- # `ValueObject`/`ProcessManager` rather than separate aggregates.
239
- #
240
- # `:increment`/`:decrement`/`:multiply` also fixed here, found
241
- # while proving this method against a real fixture: they wrapped
242
- # `amount` unconditionally whenever `attribute` existed, the same
243
- # asymmetric-wrapping shape `MutationApplier#rewrap_arithmetic_
244
- # result`'s own comment documents fixing at the aggregate level
245
- # (migration plan task 9) — a phantom-created VO-typed field's
246
- # `current` reads back a raw, unwrapped default, `amount` was
247
- # wrapped anyway, and the two sides of one arithmetic call
248
- # disagreed on Value-ness. Confirmed live, not theoretical: this
249
- # method's own fixture (TaggedList.Bump, a VO-typed `count` with
250
- # `default: 0`) raised exactly this TypeMismatch on its first
251
- # real run.
252
- # A case dispatching over a closed, declared set of mutation ops —
253
- # deliberately kept byte-for-byte parallel to its aggregate-level
254
- # twin, MutationApplier#apply (see this method's own comment
255
- # above and each branch's "own entity-scoped twin" cross-reference):
256
- # extracting the near-duplicate increment/decrement/multiply
257
- # shape here without doing the same there would break that
258
- # intentional mirroring, which is what lets the two be diffed
259
- # against each other when one gets a fix the other needs too.
108
+ # Applies one declared mutation to an entity element, in place. `pre` is the
109
+ # element as it stood before this command: every read goes through it, every
110
+ # write lands on `element`.
260
111
  # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity
261
- # Applies one declared mutation to an entity element, in place.
262
- #
263
- # `pre` — the element as it was before this command (C4.2): every
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
287
112
  def apply_to_element(rules, aggregate, entity, element, mutation, args, pre = element)
288
113
  case mutation.op
289
114
  when :set
@@ -310,73 +135,26 @@ module Hecks
310
135
  element[mutation.target] = rewrap_arithmetic_result(aggregate, attribute, current, result)
311
136
  when :clamp
312
137
  element[mutation.target] = rules.clamp(pre[mutation.target], mutation.source, mutation.target)
313
- # `corrects` — BUG#30. `MutationApplier#apply`'s own aggregate-
314
- # level `:corrects` branch's own comment gives the full reasoning;
315
- # the same one applies here unchanged: this mutation targets no
316
- # field on this element at all — its own event name, and whether
317
- # the owning record has actually emitted it, was already checked
318
- # once, up front, by `EntityInterpreter#step_enforce_givens`
319
- # (`CommandRules::Admissibility#enforce_correction_target`, called
320
- # there against the parent record/root aggregate — see that
321
- # step's own comment for exactly why). Whatever field a correction
322
- # actually changes is an ordinary declared `sets`/`increment`/etc.
323
- # mutation of its own, applied by one of the branches above like
324
- # any other — `qa/stress_domains/corrections`' own `Entry.Amend`
325
- # pairs `corrects "EntryRecorded", ...` with a separate `sets
326
- # :amount`, exactly this shape.
138
+ # `corrects`: a no-op here; the target event was already checked by
139
+ # EntityInterpreter#step_enforce_givens.
327
140
  when :corrects
328
141
  nil
329
142
  else
330
- # The aggregate-level twin's own backstop
331
- # (MutationApplier#apply), for the same reason: applying
332
- # nothing and refusing nothing would be the one silent
333
- # no-op in a language that otherwise refuses what it cannot
334
- # check.
143
+ # Backstop: an unhandled op must refuse, not silently apply nothing.
335
144
  raise WiringError, "no entity mutation applier handles :#{mutation.op} — add one before declaring it"
336
145
  end
337
146
  end
338
147
 
339
- # Rewraps a plain-Numeric arithmetic result into `attribute`'s own declared
340
- # value-object type, when the arithmetic itself ran unwrapped.
341
- #
342
- # `MutationApplier#rewrap_arithmetic_result`'s own entity-scoped
343
- # twin, byte-for-byte the same fix — see that method's own
344
- # comment for the full "phantom-field asymmetric wrapping" story.
345
- # A no-op whenever `current` was already a Value (the arithmetic
346
- # call already returned one) or the mutation targets no declared
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
148
+ # Rewraps a plain-Numeric arithmetic result into the attribute's own Value type.
149
+ # A no-op when `current` was already a Value, or the mutation targets no attribute.
358
150
  def rewrap_arithmetic_result(aggregate, attribute, current, result)
359
151
  return result if current.is_a?(Value) || attribute.nil? || result.is_a?(Value)
360
152
 
361
153
  Value.for_attribute(aggregate, attribute, result)
362
154
  end
363
155
 
364
- # Resolves an `:append` mutation's own field source against the offered
365
- # arguments, falling back to the element's own current field.
366
- #
367
- # `MutationApplier#resolve_append_source`'s own entity-scoped
368
- # twin — a caller-supplied arg first, falling back to the
369
- # element's own current field (never the parent instance's) when
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
156
+ # A caller-supplied arg wins; otherwise the field falls back to the element's own
157
+ # value. `args.key?` so an explicit nil still counts as supplied.
380
158
  def resolve_element_append_source(source, element, args)
381
159
  return source unless source.is_a?(Symbol)
382
160
  return args[source] if args.key?(source)
@@ -384,42 +162,6 @@ module Hecks
384
162
  element[source]
385
163
  end
386
164
 
387
- # `MutationApplier#appended`'s own entity-scoped twin. Usually a
388
- # value object element — an entity's own list, appended to by an
389
- # entity-owned command, holds a value object (`Member.pairs`'
390
- # own `Pair`, `Dispatch.with_spec`'s own `Binding`) the same way
391
- # most real corpus appends do — but entity-in-entity nesting (a
392
- # list of another entity, owned by this one) is real now too:
393
- # `qa/stress_domains/nested_pieces` (`Board.AddCard`, appending a
394
- # `Card` onto `Board`'s own `cards`) is the first corpus member to
395
- # do it, the comment this replaces having been written before that
396
- # domain existed. `element_type` naming an entity rather than a
397
- # value object falls through to `fields` unchanged, same as
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
423
165
  def appended_to_element(aggregate, entity, element, mutation, args)
424
166
  fields = mutation.source.transform_values { |source| resolve_element_append_source(source, element, args) }
425
167
  element_type = entity.attribute(mutation.target)&.type
@@ -431,41 +173,10 @@ module Hecks
431
173
  if value_object
432
174
  Value.build(value_object, fields, aggregate)
433
175
  else
434
- # `entity.entities`, not `aggregate.entities` — a piece
435
- # nested inside a piece is a child of the owning entity
436
- # (`Card` is `Board.entities`, never `Workspace.entities`;
437
- # `Behaviour::Entity#entities` answers direct children only,
438
- # by design — see its own comment), the same lexical-nesting
439
- # rule `EntityBuilder#entity_impl` builds the tree with in
440
- # the first place.
176
+ # `entity.entities`, not `aggregate.entities` — a nested piece is a child of
177
+ # the owning entity, not of the root aggregate.
441
178
  nested_entity = entity.entities.find { |piece| piece.hecks_name == element_type.to_s }
442
179
  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
180
  check_entity_collision(entity, nested_entity, element[mutation.target], fields)
470
181
  fill_declared_defaults(aggregate, nested_entity, fields)
471
182
  else
@@ -475,35 +186,9 @@ module Hecks
475
186
  Freezer.deep(Array(element[mutation.target]) + [appended])
476
187
  end
477
188
 
478
- # BUG#12 — an entity created via `sets :list, append: {...}` used
479
- # to leave any of its own declared attributes the append mapping
480
- # simply didn't name (an optional field a later, separate command
481
- # sets — `Board.label`, `Card.note`) absent from the stored hash
482
- # entirely, not even a `nil` placeholder, until that later command
483
- # actually ran. `rust/project/json_codec.rb#emit_to_json_flat`'s
484
- # own header comment documents the opposite as the intended
485
- # contract for a persisted record: every declared field present,
486
- # `null` when unset, "because Ruby's own `JSON.generate(state)`
487
- # round-trip this mirrors does the same" — true for a freshly
488
- # created aggregate (`Instance.defaults` already fills one key per
489
- # declared attribute, `default_for` per attribute), never true for
490
- # an entity minted by an append. This closes that gap the same
491
- # way: `Instance.default_for` is the same per-attribute default
492
- # rule (nil with no declared `default:`, a fully-defaulted value
493
- # object when every one of its own fields has one), reused rather
494
- # than reimplemented so the two creation paths can never drift on
495
- # what "the default" means. Additive only — a key `fields` already
496
- # holds (the append mapping, an auto-minted identity, a lifecycle
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
189
+ # Fills every declared attribute `fields` does not already hold with its own
190
+ # default (Instance.default_for), matching a freshly created aggregate's own
191
+ # per-attribute defaults. Additive only: an existing key in `fields` is kept.
507
192
  def fill_declared_defaults(aggregate, entity, fields)
508
193
  entity.attributes.each do |attribute|
509
194
  next if fields.key?(attribute.name)
@@ -513,24 +198,7 @@ module Hecks
513
198
  fields
514
199
  end
515
200
 
516
- # `MutationApplier#removed`'s own entity-scoped twin — matches by
517
- # value equality, element-wise, the same "so a concurrent Add can
518
- # never be lost" reasoning that method's own comment gives, unless
519
- # the list this targets is itself entity-typed — see
520
- # `list_element_match?`, below, which both this and
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
201
+ # Removes the element matching by value, or by identity for entity-typed lists.
534
202
  def removed_from_element(rules, aggregate, entity, element, mutation, args)
535
203
  value = rules.resolve_source(mutation.source, args)
536
204
  attribute = entity.attribute(mutation.target)
@@ -538,49 +206,9 @@ module Hecks
538
206
  Array(element[mutation.target]).reject { |candidate| list_element_match?(aggregate, attribute, candidate, value) }
539
207
  end
540
208
 
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
544
- # element and `value` are both real `Value`s there, so `==` already
545
- # compares every field, the "concurrent Add can never be lost"
546
- # shape `removed`/`removed_from_element`'s own headers describe.
547
- # An entity-typed list is different in kind, not just in type: a
548
- # stored element is a plain Hash, never a `Value` (`Entity`'s own
549
- # header — "an entity must never answer .value_object"), so there
550
- # is no whole-value shape to compare against at all — only the
551
- # entity's own identity field, the same field a caller already has
552
- # to name to address that element any other way
553
- # (`element_of`'s own `wants`, above). `value` arrives here already
554
- # coerced against that identity field's declared type
555
- # (`Coercion#hydrate_entity_identity`, run underneath
556
- # `Value.for_attribute` before either caller above ever sees it),
557
- # so this only has to know which field to read off the stored
558
- # element — `entity.identity_heads`'s own single head, when there
559
- # is exactly one. A composite identity (more than one head, or
560
- # none) has no single field a bare `remove:` target could mean —
561
- # this returns `false` (never a match, the same documented no-op
562
- # `hydrate_entity_identity`'s own header already commits to) rather
563
- # than guessing which head, matching the narrow, honest boundary
564
- # `MutationApplier#check_entity_collision`'s own header draws for
565
- # entity identity elsewhere in this runtime.
566
- #
567
- # Shared by `MutationApplier#removed` (an aggregate's own list) and
568
- # `#removed_from_element` (a list an entity owns), so the two
569
- # `remove:` call sites can never quietly disagree on what
570
- # "matches" means — the same reasoning this file's own header
571
- # gives for centralizing `locate_chain`/`element_of` once rather
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
209
+ # The match rule `remove:` uses against one stored list element. Whole-value
210
+ # equality for a non-entity-typed list; for an entity-typed one, matches by its
211
+ # single identity head (a composite or absent identity never matches).
584
212
  def list_element_match?(aggregate, attribute, element, value)
585
213
  entity = attribute&.list? ? Value.find_entity(aggregate, attribute.type.to_s) : nil
586
214
  return element == value unless entity
@@ -591,59 +219,9 @@ module Hecks
591
219
  element.is_a?(Hash) && element[head] == value
592
220
  end
593
221
 
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
222
+ # Refuses a caller-supplied or composite identity that already names an element
223
+ # in `current`. Shared by the aggregate-owned and entity-owned append paths so
224
+ # neither reimplements the check. `owner` is named in the refusal only.
647
225
  def check_entity_collision(owner, entity, current, fields)
648
226
  heads = entity.identity_heads
649
227
  return if heads.empty?