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
@@ -1,9 +1,9 @@
1
1
  module Hecks
2
2
  module Runtime
3
- # WHO IS DISPATCHING, bound for the duration of a block — the ambient
3
+ # Who is dispatching, bound for the duration of a block — the ambient
4
4
  # counterpart to `Runtime.with_registry`, but `Thread.current`-backed
5
- # rather than a plain module ivar: registry-binding happens at BOOT
6
- # time (single-threaded by construction), this happens at DISPATCH
5
+ # rather than a plain module ivar: registry-binding happens at boot
6
+ # time (single-threaded by construction), this happens at dispatch
7
7
  # time, which a future per-request caller (docs/rails-integration.md's
8
8
  # still-unbuilt `WebDoor`) needs to bind safely under concurrency.
9
9
  #
@@ -12,15 +12,15 @@ module Hecks
12
12
  # worker thread mid-dispatch today, so this is a fact worth naming
13
13
  # rather than a gap worth solving.
14
14
  module Caller
15
- # `actor_id` is OPTIONAL, on top of the role that has always been
15
+ # `actor_id` is optional, on top of the role that has always been
16
16
  # here — a caller that states only a role (every caller before this)
17
17
  # is checked the way it always was, string equality against the
18
- # command's own `role`. A caller that also names WHO it is lets
18
+ # command's own `role`. A caller that also names who it is lets
19
19
  # `CommandRules::Authorization` check a real Governance
20
20
  # `RoleAssignment` instead, once the command's domain has Governance
21
21
  # attached — see that module's own header for the full split.
22
22
  #
23
- # `as_of` and `scope` are BOTH optional too, and both self-asserted
23
+ # `as_of` and `scope` are both optional too, and both self-asserted
24
24
  # by the caller rather than derived from the command — deliberately:
25
25
  # `as_of` is filled at the door from `Ports::Clock.now`, never
26
26
  # inside the interpreter (see `Ports::Clock`'s own header), so
@@ -31,14 +31,40 @@ module Hecks
31
31
  # keeps it an application-boundary fact instead, the same shape
32
32
  # `role`/`actor_id` already are. The tradeoff: a caller states what
33
33
  # scope it is acting in, and `holds_role?` verifies a live grant
34
- # exists for THAT scope — it does not independently confirm the
34
+ # exists for that scope — it does not independently confirm the
35
35
  # scope matches whatever the command's own target data belongs to.
36
36
  Current = Struct.new(:role, :actor_id, :as_of, :scope, keyword_init: true)
37
37
 
38
38
  module_function
39
39
 
40
+ # The ambient caller bound by the innermost enclosing `as` block on this thread.
41
+ #
42
+ # @return [Runtime::Caller::Current, nil] the bound caller, or nil when no `as`
43
+ # block is on the stack for this thread
40
44
  def current = Thread.current[:hecks_caller]
41
45
 
46
+ # Binds the ambient caller for the duration of the block, restoring whatever
47
+ # caller (if any) was bound before, even if the block raises.
48
+ #
49
+ # `actor_id`, `as_of` and `scope` are each optional, self-asserted facts about
50
+ # the caller rather than derived from the command — see `Current`'s own header
51
+ # above for why each stays here instead of becoming a command-level DSL
52
+ # construct.
53
+ #
54
+ # @param role [String] the role the caller states it holds, checked by string
55
+ # equality against the command's declared `role`
56
+ # @param actor_id [String, nil] who the caller is; when given, lets
57
+ # `CommandRules::Authorization` check a real Governance `RoleAssignment`
58
+ # instead of the bare role-string comparison
59
+ # @param as_of [Integer, nil] Unix epoch seconds the caller asserts as "now",
60
+ # filled at the door from `Ports::Clock.now`; nil leaves a `RoleAssignment`'s
61
+ # `starts_at` unchecked
62
+ # @param scope [String, nil] the scope the caller states it is acting in,
63
+ # checked against the matching `RoleAssignment`'s own `scope`; nil skips
64
+ # that check
65
+ # @yield the code that should see `role`/`actor_id`/`as_of`/`scope` as the
66
+ # ambient caller
67
+ # @return [Object] the block's result
42
68
  def as(role:, actor_id: nil, as_of: nil, scope: nil)
43
69
  previous = Thread.current[:hecks_caller]
44
70
  Thread.current[:hecks_caller] = Current.new(
@@ -49,11 +75,17 @@ module Hecks
49
75
  Thread.current[:hecks_caller] = previous
50
76
  end
51
77
 
52
- # A REACTION IS THE SYSTEM ACTING, not the caller who happened to be
78
+ # Clears the ambient caller for the duration of the block, restoring
79
+ # whatever caller (if any) was bound before, even if the block raises.
80
+ #
81
+ # A reaction is the system acting, not the caller who happened to be
53
82
  # on the stack when the triggering command ran — `Dispatcher#reenter`
54
83
  # clears the ambient caller around a reaction's own dispatch so a
55
84
  # triggering caller's role can neither satisfy nor block a reaction
56
85
  # command it has nothing to do with.
86
+ #
87
+ # @yield the code that should see no ambient caller bound
88
+ # @return [Object] the block's result
57
89
  def without
58
90
  previous = Thread.current[:hecks_caller]
59
91
  Thread.current[:hecks_caller] = nil
@@ -1,12 +1,12 @@
1
1
  module Hecks
2
2
  module Runtime
3
- # WHICH PORTS A BOOT CAN ACTUALLY FULFILL, read off the registry it already
3
+ # Which ports a boot can actually fulfill, read off the registry it already
4
4
  # holds. `registry.ports` names every port a domain declared a dependency
5
5
  # on ; `registry.adapters` names every adapter wired to implement one — the
6
6
  # same `adapter.port == port.name` match `Ports::Extraction` and
7
7
  # `Ports::IdentityGeneration` already make for themselves, one at a time,
8
8
  # each time they resolve. This is that same question asked once, for
9
- # every port at once, so a gap in the wiring is something a caller can ASK
9
+ # every port at once, so a gap in the wiring is something a caller can ask
10
10
  # about rather than something a live dispatch discovers by refusing.
11
11
  #
12
12
  # `cycles` answers `[]`, always, and honestly: nothing in this port model
@@ -14,6 +14,8 @@ module Hecks
14
14
  # adapter names the port it implements, and neither can point at a third
15
15
  # port. There is no edge for a cycle to be made of.
16
16
  class CapabilityGraph
17
+ # @param registry [Runtime::Registry] the booted registry to read ports and adapters from
18
+ # @return [void]
17
19
  def initialize(registry)
18
20
  @registry = registry
19
21
  end
@@ -21,6 +23,9 @@ module Hecks
21
23
  # { port name => [adapter name, ...] }, for every port the registry
22
24
  # declares — including the ports nothing implements, so a caller can
23
25
  # tell "declared, unfulfilled" from "never declared at all".
26
+ #
27
+ # @return [Hash{String => Array<String>}] each declared port's name mapped to the names
28
+ # of every adapter bound to it (empty when none are)
24
29
  def fulfillments
25
30
  @fulfillments ||= @registry.ports.each_key.to_h { |name| [name, adapters_for(name)] }
26
31
  end
@@ -28,10 +33,16 @@ module Hecks
28
33
  # The ports with zero adapters bound — the gap `Runtime::WiringError`
29
34
  # would otherwise only surface at the moment something tries to dispatch
30
35
  # through one.
36
+ #
37
+ # @return [Array<String>] the names of every declared port with no adapter bound to it
31
38
  def unfulfilled
32
39
  fulfillments.select { |_, adapters| adapters.empty? }.keys
33
40
  end
34
41
 
42
+ # Reports the port-dependency cycles this registry has — always none; see the module
43
+ # header for why no port model here can express one.
44
+ #
45
+ # @return [Array] always empty
35
46
  def cycles = []
36
47
 
37
48
  private
@@ -10,19 +10,19 @@ module Hecks
10
10
  module ArgumentGate
11
11
  private
12
12
 
13
- # Anything else used to ride along in the payload untouched —
14
- # normalize_args walks the DECLARED attributes, so a name the command
15
- # never had was simply never looked at. A misspelled argument did
16
- # nothing, in silence.
13
+ # Without this check, anything else would ride along in the payload
14
+ # untouched — normalize_args walks only the declared attributes, so
15
+ # a name the command never had is simply never looked at, and a
16
+ # misspelled argument would do nothing, in silence.
17
17
  #
18
- # The keys that are legitimately not attributes are the ones that ADDRESS
18
+ # The keys that are legitimately not attributes are the ones that address
19
19
  # the aggregate rather than describe it : `id`, whatever the aggregate is
20
20
  # identified by, and the reference key of the root a command reaches
21
21
  # through. Refusing those would refuse every dispatch there is.
22
22
  #
23
23
  # `extra_identity_heads:` — EntityInterpreter's own callers only. An
24
24
  # entity dispatch addresses not just the root aggregate but every
25
- # entity ALONG THE CHAIN it walks to reach the piece the command
25
+ # entity along the chain it walks to reach the piece the command
26
26
  # actually belongs to (`Handler.Dispatch.Bind` is two hops), and each
27
27
  # hop's own `identity_heads` is addressing the same way the root's
28
28
  # are — `element_of` (entity_element.rb) reads them straight out of
@@ -33,66 +33,68 @@ module Hecks
33
33
  addressing = [:id, *aggregate.identity_heads, *extra_identity_heads, reference_key(command)] +
34
34
  correlation_keys(domain)
35
35
  known = (command.attributes.map(&:name) + addressing).compact.map(&:to_sym)
36
- # SORTED. Payload order is whatever the caller happened to write, and
36
+ # Sorted. Payload order is whatever the caller happened to write, and
37
37
  # refusal wording is contract — pinned byte-for-byte by the corpus, so
38
38
  # it cannot depend on hash iteration order.
39
39
  unknown = (args.keys.map(&:to_sym) - known).sort
40
40
  return if unknown.empty?
41
41
 
42
42
  raise UnknownArgument,
43
- RefusalWording.render("UnknownArgument", "unknown_args",
44
- command: command.hecks_name, unknown: unknown.join(", "),
45
- declared: declared_reading(command))
43
+ RefusalWording.render_site("UnknownArgument", "unknown_args",
44
+ command: command.hecks_name, unknown: unknown,
45
+ declared: declared_names(command))
46
46
  end
47
47
 
48
- # And it takes ALL of them. The other half of the same sentence, missing
48
+ # And it takes all of them. The other half of the same sentence, missing
49
49
  # until fuzz went looking : a name the command never declared was refused,
50
- # while a name it DID declare could simply be left out.
50
+ # while a name it did declare could simply be left out.
51
51
  #
52
- # `Customer.Register` without its `name` used to be refused — but by
53
- # ACCIDENT, and with a lie for a message. `then_set :name, to: :name` found
54
- # nothing to resolve, passed the literal symbol on, and coercion reported
55
- # `name is a PersonName — pass its fields as an object`, which describes a
56
- # mistake the caller did not make. The real mistake — an argument simply
57
- # missing — was never the one named, and nothing refused it on purpose.
52
+ # Refusing `Customer.Register` for a missing `name` needs to be
53
+ # deliberate, not an accident with a lie for a message. Without this
54
+ # check, `then_set :name, to: :name` finds nothing to resolve, passes
55
+ # the literal symbol on, and coercion reports `name is a PersonName —
56
+ # pass its fields as an object`, which describes a mistake the caller
57
+ # did not make. The real mistake — an argument simply missing — is
58
+ # never the one named unless this refuses it on purpose.
58
59
  #
59
60
  # No command attribute anywhere in the corpus carries a default — checked,
60
61
  # all eight chapters, zero — so there is no optional argument for this to
61
62
  # step on. Every declared attribute is a fact the command needs.
62
63
  #
63
- # `aggregate:` — PRESENT ONLY FOR A PORT OPERATION. An aggregate
64
+ # `aggregate:` — present only for a port operation. An aggregate
64
65
  # command's own self-address never reaches this list at all
65
66
  # (`CommandBuilder#reference_to`'s bare self-reference mints no
66
67
  # attribute to be absent) — but `PortOperationBuilder#reference_to`
67
- # ALWAYS mints one (this file's own header on `PortOperation`: "no
68
- # creates?/acts_on distinction to protect"), because an operation
69
- # historically had no OTHER way to say which record it addressed.
70
- # `to:` is that other way now (`Dispatcher#port_invocation`
71
- # promotes the identity attribute's own value out of the payload
72
- # and into routing) — which left the attribute still DECLARED,
73
- # still non-optional, and now never present in `args` at all: every
74
- # operation with an identity attribute refused its own well-formed
75
- # calls, dispatched exactly the way `to:` intends. Exempted here on
76
- # the same terms an aggregate command's self-address always was —
77
- # an address, not a fact the operation still needs handed back.
68
+ # always mints one (this file's own header on `PortOperation`: "no
69
+ # creates?/acts_on distinction to protect"), because an operation had
70
+ # no other way to say which record it addressed before `to:` existed.
71
+ # `to:` is that way now (`Dispatcher#port_invocation` promotes the
72
+ # identity attribute's own value out of the payload and into
73
+ # routing) — which leaves the attribute still declared, still
74
+ # non-optional, and never present in `args` at all: without this
75
+ # exemption, every operation with an identity attribute would refuse
76
+ # its own well-formed calls, dispatched exactly the way `to:`
77
+ # intends. Exempted here on the same terms an aggregate command's
78
+ # self-address always was — an address, not a fact the operation
79
+ # still needs handed back.
78
80
  def refuse_absent_arguments(command, args, aggregate: nil)
79
81
  given = args.keys.map(&:to_sym)
80
82
  exempt = aggregate && command.respond_to?(:identity_attribute) &&
81
83
  command.identity_attribute(aggregate.hecks_name)&.name
82
84
  required = command.attributes.reject(&:optional?).map { |attribute| attribute.name.to_sym } - [exempt]
83
- # SORTED, for the same reason the unknown list is : refusal wording is
85
+ # Sorted, for the same reason the unknown list is : refusal wording is
84
86
  # contract, and a pinned wording cannot depend on the order a set
85
87
  # difference happens to be computed in.
86
88
  absent = (required - given).sort
87
89
  return if absent.empty?
88
90
 
89
91
  raise AbsentArgument,
90
- RefusalWording.render("AbsentArgument", "absent_args",
91
- command: command.hecks_name, absent: absent.join(", "),
92
- declared: declared_reading(command))
92
+ RefusalWording.render_site("AbsentArgument", "absent_args",
93
+ command: command.hecks_name, absent: absent,
94
+ declared: declared_names(command))
93
95
  end
94
96
 
95
- # A COMMAND THAT DECLARES NOTHING STILL HAS TO SAY SO. `Account
97
+ # A command that declares nothing still has to say so. `Account
96
98
  # .Freeze` is `reference_to Account` and no attributes at all, so
97
99
  # `{declared}` rendered empty and the sentence trailed off mid-
98
100
  # clause : "Freeze does not declare standing — it takes ". Read
@@ -104,17 +106,14 @@ module Hecks
104
106
  # one attribute renders exactly as it did before, because refusal
105
107
  # wording is contract and pinned byte-for-byte by the corpus (see
106
108
  # `refuse_unknown_arguments`' own note on sorting for why).
107
- def declared_reading(command)
108
- declared = command.attributes.map(&:name)
109
- declared.empty? ? "none" : declared.join(", ")
110
- end
109
+ def declared_names(command) = command.attributes.map(&:name)
111
110
 
112
- # What a process manager correlates by is ROUTING, not description. A saga
111
+ # What a process manager correlates by is routing, not description. A saga
113
112
  # threads its correlation key through every leg it dispatches so the event
114
113
  # each leg emits carries it and the next step can be correlated — so the key
115
114
  # arrives on commands that never declare it, and legitimately.
116
115
  #
117
- # This is the weakest part of the gate. Correlation is the SAGA's business,
116
+ # This is the weakest part of the gate. Correlation is the saga's business,
118
117
  # and the better shape is for the saga to stamp its own key onto the event
119
118
  # it caused rather than smuggle it through the command's payload. Until it
120
119
  # does, refusing the key here would break every saga in the corpus.
@@ -19,19 +19,19 @@ module Hecks
19
19
  end
20
20
  end
21
21
 
22
- # A CASE STATEMENT OVER A CLOSED, DECLARED SET — every mutation op
22
+ # A case statement over a closed, declared set — every mutation op
23
23
  # the grammar can emit gets its own branch, including the `else`
24
24
  # backstop for the day a new op reaches this method undeclared (see
25
25
  # its own comment). Splitting each branch into its own method would
26
26
  # not reduce what a reader has to hold at once (each op's own
27
- # comment already explains why IT is shaped the way it is) and
27
+ # comment already explains why it is shaped the way it is) and
28
28
  # would obscure that the set is closed and exhaustive.
29
29
  # rubocop:disable Lint/DuplicateBranch -- :delegate and :corrects
30
30
  # both no-op here, for two unrelated documented reasons (see each
31
31
  # branch's own comment below); merging would blur that distinction.
32
32
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
33
33
  #
34
- # `pre` — THE PRE-DISPATCH STATE (C4.2): every source below reads
34
+ # `pre` — the pre-dispatch state (C4.2): every source below reads
35
35
  # it, every target is written to `instance`. A command's effects
36
36
  # are one update set; declaration order carries no meaning, and
37
37
  # build refuses a field written twice (`CommandBuilder#
@@ -54,7 +54,7 @@ module Hecks
54
54
  current = pre[mutation.target]
55
55
  # Vendored fix, not (yet) upstream hecks (migration plan
56
56
  # task 9): see #rewrap_arithmetic_result's own comment below
57
- # -- `amount` is wrapped ONLY when `current` already is, not
57
+ # -- `amount` is wrapped only when `current` already is, not
58
58
  # merely because the target attribute exists.
59
59
  amount = Value.for_attribute(aggregate, attribute, amount) if attribute && current.is_a?(Value)
60
60
  result = @rules.arithmetic(current, amount, mutation.target, @rules.sign_of(mutation.op))
@@ -86,22 +86,22 @@ module Hecks
86
86
  when :clamp
87
87
  instance[mutation.target] = @rules.clamp(pre[mutation.target], mutation.source, mutation.target)
88
88
  # `delegates_to` — CommandBuilder#delegates_to's own comment gives
89
- # the full reasoning for storing it as a mutation at all. A REAL
90
- # no-op here, not a gap: it targets no field on THIS instance —
91
- # `CommandInterpreter#step_delegate_to_entity`, a LATER step in
89
+ # the full reasoning for storing it as a mutation at all. A real
90
+ # no-op here, not a gap: it targets no field on this instance —
91
+ # `CommandInterpreter#step_delegate_to_entity`, a later step in
92
92
  # the same dispatch, is what actually applies it, against the
93
93
  # target entity element `EntityElement.apply_to_element` reaches,
94
94
  # never through this method.
95
95
  when :delegate
96
96
  nil
97
97
  # `corrects` — CommandBuilder#corrects_impl's own comment gives
98
- # the full reasoning for storing it as a mutation at all. A REAL
99
- # no-op here too: it targets no field on THIS instance either —
100
- # its own event name, and whether THIS record has actually
98
+ # the full reasoning for storing it as a mutation at all. A real
99
+ # no-op here too: it targets no field on this instance either —
100
+ # its own event name, and whether this record has actually
101
101
  # emitted it, is checked once, up front, by
102
102
  # CommandRules::Admissibility#enforce_correction_target, not
103
103
  # here. Whatever field this correction actually changes is an
104
- # ORDINARY declared (or, for `reverses: true`, derived — see
104
+ # ordinary declared (or, for `reverses: true`, derived — see
105
105
  # AggregateBuilder#seal_correction_targets) mutation of its own,
106
106
  # applied by one of the branches above like any other.
107
107
  when :corrects
@@ -120,9 +120,9 @@ module Hecks
120
120
  # rubocop:enable Lint/DuplicateBranch
121
121
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
122
122
 
123
- # A CALLER-SUPPLIED ARG, FIRST -- an append's own field can also
124
- # name something the SUBJECT ALREADY KNOWS about itself, falling
125
- # back to the AGGREGATE'S OWN CURRENT FIELD when it isn't one — a
123
+ # A caller-supplied arg, first -- an append's own field can also
124
+ # name something the subject already knows about itself, falling
125
+ # back to the aggregate's own current field when it isn't one — a
126
126
  # caller appending at the end without computing or supplying a
127
127
  # position (`position: :next_position`, a field the command never
128
128
  # declares as an argument at all). `args.key?`, not a truthiness
@@ -149,9 +149,9 @@ module Hecks
149
149
  value_object = aggregate.value_object(element_type)
150
150
  value_object&.attributes&.each do |attribute|
151
151
  held = fields[attribute.name]
152
- # A SINGLE-FIELD VALUE unwraps to its scalar so it bridges
152
+ # A single-field value unwraps to its scalar so it bridges
153
153
  # into the element's own (differently named) wrapper; a
154
- # MULTI-FIELD one (a `state(:en_passant_square)` Square
154
+ # multi-field one (a `state(:en_passant_square)` Square
155
155
  # copied off the record) has no scalar to stand in for it
156
156
  # and is handed across whole — `Value.for_attribute` keeps a
157
157
  # value of the element field's own type as it is.
@@ -165,24 +165,24 @@ module Hecks
165
165
  fields)
166
166
  end
167
167
 
168
- # FROZEN, like every other value the domain hands back. An
169
- # appended list used to come back mutable, so a caller could
170
- # push straight into an aggregate's own state after the
168
+ # Frozen, like every other value the domain hands back — without
169
+ # this, an appended list would come back mutable, letting a
170
+ # caller push straight into an aggregate's own state after the
171
171
  # dispatch had finished.
172
172
  Freezer.deep(Array(instance[mutation.target]) + [element])
173
173
  end
174
174
 
175
175
  # Vendored addition, not (yet) upstream hecks (migration plan
176
176
  # task 4): the removal counterpart to #appended -- matches by
177
- # VALUE EQUALITY, element-wise, no read-modify-write (plan.
177
+ # value equality, element-wise, no read-modify-write (plan.
178
178
  # bluebook's own words: "so a concurrent Add can never be lost")
179
- # -- UNLESS the target list is itself entity-typed (BUG#32,
179
+ # -- unless the target list is itself entity-typed (BUG#32,
180
180
  # QualityControl ledger), in which case `EntityElement.
181
- # list_element_match?` matches by the entity's own IDENTITY
181
+ # list_element_match?` matches by the entity's own identity
182
182
  # field instead -- see that method's own comment for the full
183
183
  # "why identity, not whole-value equality" reasoning. `mutation.
184
184
  # source` is a single field reference (`:dependency`), unlike
185
- # append's field-map -- resolved and Value-coerced the SAME way
185
+ # append's field-map -- resolved and Value-coerced the same way
186
186
  # increment/decrement already coerce their own amount, so the
187
187
  # comparison is against a like-shaped Value, not a raw scalar
188
188
  # against a wrapped one.
@@ -197,26 +197,26 @@ module Hecks
197
197
 
198
198
  # Vendored fix, not (yet) upstream hecks (migration plan
199
199
  # task 9): #apply's `:increment`/`:decrement`/`:multiply`
200
- # branches used to wrap `amount` into a `Value` unconditionally
200
+ # branches wrapping `amount` into a `Value` unconditionally
201
201
  # whenever the target attribute existed, never checking whether
202
- # `current` (the field's OWN existing value, read straight off
203
- # `instance[mutation.target]`) was ALSO wrapped -- the two sides
204
- # of the same arithmetic call could disagree on Value-ness. On a
205
- # PHANTOM-CREATED field this is the common case, not an edge
202
+ # `current` (the field's own existing value, read straight off
203
+ # `instance[mutation.target]`) was also wrapped, would let the two
204
+ # sides of the same arithmetic call disagree on Value-ness. On a
205
+ # phantom-created field this is the common case, not an edge
206
206
  # one: `Instance.defaults`/`#default_for` leaves a VO-typed
207
207
  # attribute with no declared `default:` genuinely absent (nil),
208
208
  # and `CommandRules::Arithmetic#arithmetic`/`#multiply`'s own
209
- # `current ||= 0` then turns that nil into a RAW, unwrapped
209
+ # `current ||= 0` then turns that nil into a raw, unwrapped
210
210
  # Integer `0` -- so `current.is_a?(Value) && amount.is_a?(Value)`
211
211
  # read false even though `amount` (correctly wrapped by the old
212
212
  # unconditional line) genuinely held a valid, correctly-typed
213
213
  # number, and the primitive path's `unless amount.is_a?(Numeric)`
214
- # guard refused it as a TYPE MISMATCH the caller never made --
214
+ # guard refused it as a type mismatch the caller never made --
215
215
  # an artifact of this method's own asymmetric wrapping, not bad
216
216
  # input.
217
217
  #
218
- # Fixed at the call site (above) by wrapping `amount` ONLY when
219
- # `current` is ALREADY a `Value` -- so an established VO-typed
218
+ # Fixed at the call site (above) by wrapping `amount` only when
219
+ # `current` is already a `Value` -- so an established VO-typed
220
220
  # field (a multi-field Money balance, say, already hydrated from
221
221
  # a prior save) keeps going through
222
222
  # `Arithmetic#arithmetic_value_object` exactly as before (the
@@ -225,7 +225,7 @@ module Hecks
225
225
  # `current` and raw `amount` both take the plain-Numeric path
226
226
  # together. This method closes the other half: the plain-Numeric
227
227
  # path returns a bare Ruby number, and if the attribute is
228
- # itself VO-typed (the norm), that raw result needs the SAME
228
+ # itself VO-typed (the norm), that raw result needs the same
229
229
  # wrap `:set` already gives its own resolved value (`Value.for`)
230
230
  # before it's stored, so a field's stored shape doesn't depend
231
231
  # on which dispatch happened to mutate it first. A no-op
@@ -251,10 +251,10 @@ module Hecks
251
251
  attribute = entity.attribute(entity.identified_by)
252
252
  fields[entity.identified_by] = Value.from_identifier(aggregate, attribute, next_identity(current, entity))
253
253
  else
254
- check_entity_collision(aggregate, entity, current, fields)
254
+ EntityElement.check_entity_collision(aggregate, entity, current, fields)
255
255
  end
256
256
  fields[entity.lifecycle.field] ||= entity.lifecycle.default if entity.lifecycle
257
- # BUG#12 — every one of THIS entity's own declared attributes
257
+ # BUG#12 — every one of this entity's own declared attributes
258
258
  # the append mapping (and the identity/lifecycle filling just
259
259
  # above) didn't already touch gets its own default, the same
260
260
  # way a fresh aggregate's own attributes already do
@@ -267,7 +267,7 @@ module Hecks
267
267
  EntityElement.fill_declared_defaults(aggregate, entity, fields)
268
268
  end
269
269
 
270
- # THE MINTED IDENTITY IS ONE PAST THE HIGHEST HELD (C4.5) — not
270
+ # The minted identity is one past the highest held (C4.5) — not
271
271
  # `size + 1`, which repeats an identity the moment the list has
272
272
  # ever shrunk. One strategy for the language, so the IR declares
273
273
  # none; the Rust generators mint by the same rule.
@@ -276,37 +276,15 @@ module Hecks
276
276
  held.max.to_i + 1
277
277
  end
278
278
 
279
- # THE SAME CHECK #hydrate GIVES EVERY CREATING AGGREGATE COMMAND
280
- # (`repository.find(id)`, above this file in command_interpreter.rb),
281
- # one level down. Reached only on the two branches #entity_element
282
- # does NOT auto-mint: a CALLER-SUPPLIED identity (the field is
283
- # already in the append's own field map, so the `if` above skips
284
- # it) or a COMPOSITE one (`entity.identified_by` is nil for those —
285
- # Runtime::Identified#derive_identity — so the `if` above is false
286
- # unconditionally). Neither used to check the sibling list at all:
287
- # a second LogVisit with the same date+sequence, or a second
288
- # IssueKey with the same serial, appended a silent duplicate — worse
289
- # than an ordinary duplicate row, because EntityInterpreter#element_of's
290
- # `find_index` always matches the FIRST match, so the second becomes
291
- # permanently unaddressable by any later command.
292
- #
293
- # Auto-minted entities never reach here — `identity_heads` for them
294
- # is still checked at mint time by construction (`current.size + 1`
295
- # can only repeat if something `remove:`s from the list between
296
- # mints, which no real domain does today), so they can't be flagged
297
- # by mistake.
298
- def check_entity_collision(aggregate, entity, current, fields)
299
- heads = entity.identity_heads
300
- return if heads.empty?
301
-
302
- collision = Array(current).find { |element| heads.all? { |head| element[head] == fields[head] } }
303
- return unless collision
304
-
305
- raise(AlreadyExists, RefusalWording.render("AlreadyExists", "entity_duplicate",
306
- entity: entity.hecks_name, aggregate: aggregate.hecks_name,
307
- identity: Identity.reading(entity),
308
- offered: heads.map { |head| Rendering.describe(fields[head]) }.join(", ")))
309
- end
279
+ # The entity-collision check lives in `EntityElement.check_entity_collision`
280
+ # (entity_element.rb) — BUG#145 — shared by both `#entity_element` above
281
+ # (an aggregate's own entity list, e.g. `Workspace.boards`) and
282
+ # `EntityElement#appended_to_element`'s own nested-entity branch (an
283
+ # entity's own entity list one hop further in, e.g. `Board.cards`),
284
+ # which needs the exact same guard against a caller-supplied nested
285
+ # identity silently duplicating. See that method's own call site and
286
+ # comment; the check's own doc comment (heads/composite/auto-mint
287
+ # reasoning) lives with the code, in entity_element.rb.
310
288
  end
311
289
  end
312
290
  end