hecks 0.2.0 → 0.3.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 (408) hide show
  1. checksums.yaml +5 -5
  2. data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
  3. data/lib/hecks/adapters/driven/claude_code.rb +127 -0
  4. data/lib/hecks/adapters/driven/d1.adapter +12 -0
  5. data/lib/hecks/adapters/driven/d1.rb +403 -0
  6. data/lib/hecks/adapters/driven/folder.adapter +3 -0
  7. data/lib/hecks/adapters/driven/folder.rb +199 -0
  8. data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
  9. data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
  10. data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
  11. data/lib/hecks/adapters/driven/governance_authorization.rb +62 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +51 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +38 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +148 -0
  17. data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
  18. data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
  19. data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
  20. data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
  21. data/lib/hecks/adapters/driven/lambda.adapter +4 -0
  22. data/lib/hecks/adapters/driven/lambda.rb +131 -0
  23. data/lib/hecks/adapters/driven/memory.adapter +3 -0
  24. data/lib/hecks/adapters/driven/memory.rb +103 -0
  25. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
  26. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
  27. data/lib/hecks/adapters/driven/postgres/codec.rb +80 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +168 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +391 -0
  31. data/lib/hecks/adapters/driven/postgres_era/lineage/era_store.rb +171 -0
  32. data/lib/hecks/adapters/driven/postgres_era/lineage/field_cache.rb +190 -0
  33. data/lib/hecks/adapters/driven/postgres_era/lineage/head_compiler.rb +429 -0
  34. data/lib/hecks/adapters/driven/postgres_era/lineage/mint_transaction.rb +166 -0
  35. data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +286 -0
  36. data/lib/hecks/adapters/driven/postgres_era/lineage/resumable_backfill.rb +168 -0
  37. data/lib/hecks/adapters/driven/postgres_era/lineage/tail_merge.rb +163 -0
  38. data/lib/hecks/adapters/driven/postgres_era/lineage/transform_installer.rb +114 -0
  39. data/lib/hecks/adapters/driven/postgres_era/lineage.rb +137 -0
  40. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/coverage_check.rb +89 -0
  41. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +82 -0
  42. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  43. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/minter.rb +132 -0
  44. data/lib/hecks/adapters/driven/postgres_era/lineage_manager.rb +74 -0
  45. data/lib/hecks/adapters/driven/postgres_era.adapter +6 -0
  46. data/lib/hecks/adapters/driven/postgres_era.rb +671 -0
  47. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  48. data/lib/hecks/adapters/driven/prism.rb +80 -0
  49. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  50. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  51. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  52. data/lib/hecks/adapters/driven/sqlite/codec.rb +63 -0
  53. data/lib/hecks/adapters/driven/sqlite/projection.rb +105 -0
  54. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +180 -0
  55. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  56. data/lib/hecks/adapters/driven/sqlite.rb +285 -0
  57. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  58. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  59. data/lib/hecks/adapters/driven.rb +36 -0
  60. data/lib/hecks/adapters.rb +6 -0
  61. data/lib/hecks/behaviors/dsl.rb +96 -0
  62. data/lib/hecks/behaviors/expectations.rb +280 -0
  63. data/lib/hecks/behaviors/ir.rb +31 -0
  64. data/lib/hecks/behaviors/rspec.rb +42 -0
  65. data/lib/hecks/behaviors/runner.rb +96 -0
  66. data/lib/hecks/behaviors.rb +25 -0
  67. data/lib/hecks/bluebook/aggregate.rb +108 -0
  68. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  69. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  70. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  71. data/lib/hecks/bluebook/assembly/contracts.rb +409 -0
  72. data/lib/hecks/bluebook/assembly/marks.rb +227 -0
  73. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  74. data/lib/hecks/bluebook/assembly.rb +78 -0
  75. data/lib/hecks/bluebook/attribute.rb +96 -0
  76. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  77. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  78. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  79. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  80. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  81. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  82. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  83. data/lib/hecks/bluebook/behaviour/lifecycle.rb +51 -0
  84. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  85. data/lib/hecks/bluebook/behaviour/process_manager.rb +38 -0
  86. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  87. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  88. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  89. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  90. data/lib/hecks/bluebook/chapter.rb +68 -0
  91. data/lib/hecks/bluebook/command.rb +124 -0
  92. data/lib/hecks/bluebook/domain_port.rb +102 -0
  93. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  94. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +906 -0
  95. data/lib/hecks/bluebook/dsl/attribute_collector.rb +327 -0
  96. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  97. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1018 -0
  98. data/lib/hecks/bluebook/dsl/command_builder.rb +626 -0
  99. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  100. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  101. data/lib/hecks/bluebook/dsl/entity_builder.rb +305 -0
  102. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  103. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  104. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  105. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  106. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  107. data/lib/hecks/bluebook/dsl/policy_builder.rb +120 -0
  108. data/lib/hecks/bluebook/dsl/port_builder.rb +30 -0
  109. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  110. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +207 -0
  111. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  112. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  113. data/lib/hecks/bluebook/dsl/rule_reference.rb +172 -0
  114. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  115. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  116. data/lib/hecks/bluebook/dsl/word_gate.rb +221 -0
  117. data/lib/hecks/bluebook/dsl/world_builder.rb +77 -0
  118. data/lib/hecks/bluebook/dsl.rb +45 -0
  119. data/lib/hecks/bluebook/entity.rb +103 -0
  120. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -0
  121. data/lib/hecks/bluebook/expression/evaluator.rb +261 -0
  122. data/lib/hecks/bluebook/expression/projection.json +170 -0
  123. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  124. data/lib/hecks/bluebook/expression/resolver.rb +630 -0
  125. data/lib/hecks/bluebook/expression.rb +13 -0
  126. data/lib/hecks/bluebook/hexagon.rb +60 -0
  127. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  128. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  129. data/lib/hecks/bluebook/meta_validator/judge.rb +559 -0
  130. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  131. data/lib/hecks/bluebook/meta_validator/port_judge.rb +47 -0
  132. data/lib/hecks/bluebook/meta_validator/readings.rb +350 -0
  133. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +312 -0
  134. data/lib/hecks/bluebook/meta_validator/shapes.rb +246 -0
  135. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  136. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  137. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  138. data/lib/hecks/bluebook/meta_validator.rb +452 -0
  139. data/lib/hecks/bluebook/model_check.rb +318 -0
  140. data/lib/hecks/bluebook/pattern_subset.rb +120 -0
  141. data/lib/hecks/bluebook/policy.rb +41 -0
  142. data/lib/hecks/bluebook/process_manager.rb +86 -0
  143. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  144. data/lib/hecks/bluebook/project_loader.rb +40 -0
  145. data/lib/hecks/bluebook/project_register.rb +82 -0
  146. data/lib/hecks/bluebook/query.rb +61 -0
  147. data/lib/hecks/bluebook/read_model.rb +109 -0
  148. data/lib/hecks/bluebook/reference.rb +74 -0
  149. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  150. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  151. data/lib/hecks/bluebook/translation.rb +92 -0
  152. data/lib/hecks/bluebook/value_object.rb +58 -0
  153. data/lib/hecks/bluebook.rb +74 -0
  154. data/lib/hecks/codemod.rb +342 -0
  155. data/lib/hecks/construct.rb +71 -0
  156. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  157. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  158. data/lib/hecks/deploy/oidc.json +18 -0
  159. data/lib/hecks/doc/reference.rb +389 -0
  160. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  161. data/lib/hecks/facade/cli_door.rb +116 -0
  162. data/lib/hecks/facade/cli_runner.rb +190 -0
  163. data/lib/hecks/facade/command_request.rb +105 -0
  164. data/lib/hecks/facade/handle.rb +173 -0
  165. data/lib/hecks/facade/json_door.rb +154 -0
  166. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  167. data/lib/hecks/facade/surface/chapter.rb +107 -0
  168. data/lib/hecks/facade/surface.rb +48 -0
  169. data/lib/hecks/facade.rb +44 -0
  170. data/lib/hecks/forms/app.rb +300 -0
  171. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  172. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  173. data/lib/hecks/forms/field_renderer.rb +170 -0
  174. data/lib/hecks/forms/field_shape.rb +232 -0
  175. data/lib/hecks/forms/html.rb +53 -0
  176. data/lib/hecks/forms/index_renderer.rb +35 -0
  177. data/lib/hecks/forms/page.rb +157 -0
  178. data/lib/hecks/forms/params.rb +132 -0
  179. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  180. data/lib/hecks/forms/record_renderer.rb +115 -0
  181. data/lib/hecks/forms/record_table.rb +63 -0
  182. data/lib/hecks/forms/reference_options.rb +30 -0
  183. data/lib/hecks/forms/value_object_shape.rb +46 -0
  184. data/lib/hecks/forms.rb +54 -0
  185. data/lib/hecks/fqn.rb +94 -0
  186. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  187. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  188. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  189. data/lib/hecks/framework/bluebook/governance.bluebook +130 -0
  190. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  191. data/lib/hecks/framework/oidc.json +39 -0
  192. data/lib/hecks/framework.rb +90 -0
  193. data/lib/hecks/freezer.rb +67 -0
  194. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  195. data/lib/hecks/fuzzing/isolated_boot.rb +92 -0
  196. data/lib/hecks/fuzzing/properties.rb +1146 -0
  197. data/lib/hecks/fuzzing/replay.rb +635 -0
  198. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  199. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +54 -0
  200. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  201. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +185 -0
  202. data/lib/hecks/fuzzing/sequence_generator.rb +130 -0
  203. data/lib/hecks/fuzzing/value_generator.rb +177 -0
  204. data/lib/hecks/fuzzing.rb +12 -0
  205. data/lib/hecks/grammar/evolve.rb +291 -0
  206. data/lib/hecks/grammar/expression.bluebook +417 -0
  207. data/lib/hecks/grammar/expression_operators.json +1152 -0
  208. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  209. data/lib/hecks/grammar/oidc.json +69 -0
  210. data/lib/hecks/grammar/translation.bluebook +203 -0
  211. data/lib/hecks/grammar.rb +178 -0
  212. data/lib/hecks/ir.rb +126 -0
  213. data/lib/hecks/language/adapter.bluebook +116 -0
  214. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  215. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  216. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  217. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  218. data/lib/hecks/language/bluebook/command.bluebook +437 -0
  219. data/lib/hecks/language/bluebook/entity.bluebook +316 -0
  220. data/lib/hecks/language/bluebook/policy.bluebook +172 -0
  221. data/lib/hecks/language/bluebook/process_manager.bluebook +264 -0
  222. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  223. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  224. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  225. data/lib/hecks/language/bluebook/syntax.bluebook +447 -0
  226. data/lib/hecks/language/bluebook/vocabulary.bluebook +364 -0
  227. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  228. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  229. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  230. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  231. data/lib/hecks/language/oidc.json +313 -0
  232. data/lib/hecks/language/port.bluebook +92 -0
  233. data/lib/hecks/language/translation/translation.bluebook +110 -0
  234. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  235. data/lib/hecks/language/world/wiring.bluebook +62 -0
  236. data/lib/hecks/language/world/world.bluebook +84 -0
  237. data/lib/hecks/literal.rb +125 -0
  238. data/lib/hecks/naming.rb +121 -0
  239. data/lib/hecks/ports/access_control.port +4 -0
  240. data/lib/hecks/ports/access_control.rb +62 -0
  241. data/lib/hecks/ports/agent/answers.rb +104 -0
  242. data/lib/hecks/ports/agent.port +4 -0
  243. data/lib/hecks/ports/agent.rb +167 -0
  244. data/lib/hecks/ports/authentication.port +4 -0
  245. data/lib/hecks/ports/authentication.rb +50 -0
  246. data/lib/hecks/ports/authorization.port +4 -0
  247. data/lib/hecks/ports/authorization.rb +59 -0
  248. data/lib/hecks/ports/clock.port +4 -0
  249. data/lib/hecks/ports/clock.rb +62 -0
  250. data/lib/hecks/ports/extraction.port +4 -0
  251. data/lib/hecks/ports/extraction.rb +37 -0
  252. data/lib/hecks/ports/identity_assignment.port +4 -0
  253. data/lib/hecks/ports/identity_assignment.rb +45 -0
  254. data/lib/hecks/ports/identity_generation.port +4 -0
  255. data/lib/hecks/ports/identity_generation.rb +49 -0
  256. data/lib/hecks/ports/identity_resolution.port +4 -0
  257. data/lib/hecks/ports/identity_resolution.rb +40 -0
  258. data/lib/hecks/ports/loading.port +4 -0
  259. data/lib/hecks/ports/loading.rb +13 -0
  260. data/lib/hecks/ports/persistence/append_only.rb +102 -0
  261. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  262. data/lib/hecks/ports/persistence/execution.rb +18 -0
  263. data/lib/hecks/ports/persistence/lineage.rb +277 -0
  264. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  265. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  266. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  267. data/lib/hecks/ports/persistence.port +4 -0
  268. data/lib/hecks/ports/persistence.rb +39 -0
  269. data/lib/hecks/ports/projection.port +4 -0
  270. data/lib/hecks/ports/projection.rb +76 -0
  271. data/lib/hecks/ports/query/in_memory.rb +60 -0
  272. data/lib/hecks/ports/query/ordering.rb +41 -0
  273. data/lib/hecks/ports/query.rb +36 -0
  274. data/lib/hecks/ports.rb +27 -0
  275. data/lib/hecks/projections/diagrams.rb +413 -0
  276. data/lib/hecks/projections/ir.rb +18 -0
  277. data/lib/hecks/projections/model/deviations.rb +98 -0
  278. data/lib/hecks/projections/model.rb +145 -0
  279. data/lib/hecks/projections/oidc.rb +110 -0
  280. data/lib/hecks/projections/parser_table.rb +159 -0
  281. data/lib/hecks/projections/reference.rb +38 -0
  282. data/lib/hecks/projections/shape.rb +31 -0
  283. data/lib/hecks/projections/statements.rb +110 -0
  284. data/lib/hecks/projections/vocabulary.rb +100 -0
  285. data/lib/hecks/projections.rb +32 -0
  286. data/lib/hecks/projector/cli_projector.rb +437 -0
  287. data/lib/hecks/projector/docs_projector.rb +321 -0
  288. data/lib/hecks/projector/exporter.rb +141 -0
  289. data/lib/hecks/projector/ir_projector.rb +18 -0
  290. data/lib/hecks/projector/narrate_projector.rb +243 -0
  291. data/lib/hecks/projector/target.rb +97 -0
  292. data/lib/hecks/projector.rb +186 -0
  293. data/lib/hecks/query_ir.rb +392 -0
  294. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  295. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  296. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  297. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  298. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  299. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  300. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  301. data/lib/hecks/query_specification/common/null_policy.rb +80 -0
  302. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  303. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  304. data/lib/hecks/query_specification/common/options.rb +32 -0
  305. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  306. data/lib/hecks/query_specification/common/specification.rb +10 -0
  307. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  308. data/lib/hecks/query_specification/field_path.rb +89 -0
  309. data/lib/hecks/query_specification/hop_path.rb +130 -0
  310. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  311. data/lib/hecks/query_specification.rb +14 -0
  312. data/lib/hecks/rendering.rb +48 -0
  313. data/lib/hecks/router/namespace_installer.rb +157 -0
  314. data/lib/hecks/router.rb +70 -0
  315. data/lib/hecks/runtime/caller.rb +50 -0
  316. data/lib/hecks/runtime/capability_graph.rb +44 -0
  317. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +124 -0
  318. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +248 -0
  319. data/lib/hecks/runtime/command_interpreter.rb +413 -0
  320. data/lib/hecks/runtime/command_rules/admissibility.rb +307 -0
  321. data/lib/hecks/runtime/command_rules/arithmetic.rb +254 -0
  322. data/lib/hecks/runtime/command_rules/authorization.rb +64 -0
  323. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  324. data/lib/hecks/runtime/command_rules/references.rb +181 -0
  325. data/lib/hecks/runtime/command_rules.rb +28 -0
  326. data/lib/hecks/runtime/dependency_planning.rb +231 -0
  327. data/lib/hecks/runtime/dispatcher.rb +286 -0
  328. data/lib/hecks/runtime/entity_element.rb +253 -0
  329. data/lib/hecks/runtime/entity_interpreter.rb +228 -0
  330. data/lib/hecks/runtime/era_check.rb +136 -0
  331. data/lib/hecks/runtime/era_guard/shape_diff.rb +128 -0
  332. data/lib/hecks/runtime/era_guard.rb +193 -0
  333. data/lib/hecks/runtime/era_tamper.rb +61 -0
  334. data/lib/hecks/runtime/errors.rb +100 -0
  335. data/lib/hecks/runtime/event.rb +51 -0
  336. data/lib/hecks/runtime/identity.rb +128 -0
  337. data/lib/hecks/runtime/instance.rb +120 -0
  338. data/lib/hecks/runtime/interpreting.rb +69 -0
  339. data/lib/hecks/runtime/loader.rb +143 -0
  340. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  341. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  342. data/lib/hecks/runtime/query_interpreter.rb +254 -0
  343. data/lib/hecks/runtime/reaction_invocation.rb +245 -0
  344. data/lib/hecks/runtime/read_model_interpreter.rb +266 -0
  345. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  346. data/lib/hecks/runtime/reference_hop.rb +99 -0
  347. data/lib/hecks/runtime/refusal_wording.rb +124 -0
  348. data/lib/hecks/runtime/registry/saga_persistence.rb +70 -0
  349. data/lib/hecks/runtime/registry/verification.rb +158 -0
  350. data/lib/hecks/runtime/registry.rb +218 -0
  351. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  352. data/lib/hecks/runtime/routing.rb +96 -0
  353. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  354. data/lib/hecks/runtime/saga_interpreter.rb +301 -0
  355. data/lib/hecks/runtime/storage_shape.rb +120 -0
  356. data/lib/hecks/runtime/tenant_check.rb +84 -0
  357. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  358. data/lib/hecks/runtime/value/admission.rb +112 -0
  359. data/lib/hecks/runtime/value/coercion.rb +500 -0
  360. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  361. data/lib/hecks/runtime/value.rb +125 -0
  362. data/lib/hecks/runtime.rb +95 -0
  363. data/lib/hecks/translation/audit/approval_digest.rb +31 -0
  364. data/lib/hecks/translation/audit/layer_one.rb +37 -0
  365. data/lib/hecks/translation/audit/layer_two.rb +77 -0
  366. data/lib/hecks/translation/audit/unfed_report.rb +42 -0
  367. data/lib/hecks/translation/audit.rb +70 -0
  368. data/lib/hecks/translation/reattest.rb +72 -0
  369. data/lib/hecks/translation/rule_compiler.rb +120 -0
  370. data/lib/hecks/translation/scaffold/differ.rb +183 -0
  371. data/lib/hecks/translation/scaffold/renderer.rb +41 -0
  372. data/lib/hecks/translation/scaffold/writer.rb +28 -0
  373. data/lib/hecks/translation/scaffold.rb +30 -0
  374. data/lib/hecks/translation.rb +11 -0
  375. data/lib/hecks/version.rb +16 -0
  376. data/lib/hecks/vocabulary.rb +213 -0
  377. data/lib/hecks.rb +118 -6
  378. metadata +401 -222
  379. data/bin/hecks +0 -7
  380. data/bin/hecks-package +0 -65
  381. data/bin/hecks_console +0 -12
  382. data/bin/hecks_serverless +0 -6
  383. data/lib/cli/build.rb +0 -14
  384. data/lib/cli/command_runner.rb +0 -28
  385. data/lib/cli/console.rb +0 -10
  386. data/lib/cli/generate.rb +0 -37
  387. data/lib/cli/hecks-cli.rb +0 -27
  388. data/lib/cli/test.rb +0 -57
  389. data/lib/console/commands.rb +0 -8
  390. data/lib/console/hecks-console.rb +0 -1
  391. data/lib/packager/README.md +0 -0
  392. data/lib/packager/app_runner.rb +0 -21
  393. data/lib/packager/args.rb +0 -26
  394. data/lib/packager/compatibility/fixnum.rb +0 -6
  395. data/lib/packager/hecks.rb +0 -39
  396. data/lib/packager/query_runner.rb +0 -21
  397. data/lib/packager/resources/Dockerfile +0 -11
  398. data/lib/packager/resources/app_binary +0 -7
  399. data/lib/packager/resources/bundle_config +0 -3
  400. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  401. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  402. data/lib/serverless/Domain +0 -32
  403. data/lib/serverless/cli.rb +0 -75
  404. data/lib/serverless/resources/command_name.js +0 -5
  405. data/lib/serverless/resources/environment.js +0 -7
  406. data/lib/serverless/resources/handler.js.tt +0 -28
  407. data/lib/serverless/resources/run_binary.js +0 -22
  408. data/lib/serverless/resources/serverless.yml +0 -20
@@ -0,0 +1,286 @@
1
+ require_relative "errors"
2
+ require_relative "refusal_wording"
3
+ require_relative "caller"
4
+ require_relative "routing"
5
+ require_relative "command_rules"
6
+ require_relative "command_interpreter"
7
+ require_relative "entity_interpreter"
8
+ require_relative "query_interpreter"
9
+ require_relative "read_model_interpreter"
10
+ require_relative "policy_interpreter"
11
+ require_relative "saga_interpreter"
12
+ require_relative "../naming"
13
+
14
+ module Hecks
15
+ module Runtime
16
+ class Dispatcher
17
+ MAX_REACTION_DEPTH = 5
18
+
19
+ Result = Struct.new(:verb, :instance, :events, :execution_plan, :persistence_outcome, keyword_init: true) do
20
+ # `instance` is nil for a port operation dispatched by verb (below)
21
+ # — nothing was hydrated or saved, the same reason
22
+ # `PortOperationInterpreter#emit`'s own comment gives for sourcing
23
+ # `id:` off the operation's reference attribute instead. `&.`, not
24
+ # a raised error: a caller that dispatches a port verb and then
25
+ # asks this Result for `.id`/`.state` made a category error the
26
+ # domain itself already told it about (there is no record here),
27
+ # not a crash-worthy one.
28
+ def id = instance&.id
29
+ def state = instance&.to_h
30
+
31
+ def to_s
32
+ announced = events.empty? ? "no events" : events.map(&:name).join(", ")
33
+ "#{verb} → #{instance.inspect} | #{announced}"
34
+ end
35
+
36
+ def inspect = "#<Result #{self}>"
37
+ end
38
+
39
+ attr_reader :registry
40
+
41
+ def initialize(registry)
42
+ @registry = registry
43
+ rules = CommandRules.new(registry)
44
+ @commands = CommandInterpreter.new(registry, rules: rules)
45
+ @port_ops = PortOperationInterpreter.new(registry, rules: rules)
46
+ @entities = EntityInterpreter.new(registry, rules: rules)
47
+ @queries = QueryInterpreter.new(registry)
48
+ @read_models = ReadModelInterpreter.new(registry)
49
+ @policies = PolicyInterpreter.new(registry, door: self)
50
+ @sagas = SagaInterpreter.new(registry, door: self)
51
+ end
52
+
53
+ def events = @registry.event_log
54
+
55
+ def reactions = @registry.reaction_log
56
+
57
+ def sagas = @registry.saga_log
58
+ def saga_dispatches = @registry.saga_dispatch_log
59
+ def policy_dispatches = @registry.policy_dispatch_log
60
+ def verbs = @registry.verbs
61
+
62
+ def dispatch(verb, to: nil, with: nil, saga_correlation: nil, **legacy_args)
63
+ domain, aggregate_name, command_name = parse(verb)
64
+ aggregate = resolve_aggregate(domain, aggregate_name, verb)
65
+
66
+ instance, announced, execution_plan, persistence_outcome =
67
+ if command_name.include?(".")
68
+ head, sub = command_name.split(".", 2)
69
+ port = aggregate.port(head)
70
+ # A PORT OPERATION, reached by the SAME verb shape an entity
71
+ # command already uses ("Domain::Aggregate.Head.Rest") — ports
72
+ # are checked first, so an aggregate that ever declared both a
73
+ # port and an entity of the same name would resolve to the
74
+ # port; no domain in this corpus does, and `dispatch_port`'s
75
+ # own header already named this as an open wire-spelling
76
+ # question this resolves, not silently avoids. No `instance`
77
+ # comes back — nothing is hydrated or saved by a port
78
+ # operation (`PortOperationInterpreter`'s own header) — so
79
+ # `Result#id`/`#state` are nil-safe (above) for exactly this
80
+ # path.
81
+ if port
82
+ operation = port.operation(sub) ||
83
+ raise(UnknownVerb, RefusalWording.render("UnknownVerb", "port_no_operation",
84
+ port: head, operation: sub.inspect))
85
+ route, args = port_invocation(aggregate, operation, to: to, with: with, legacy: legacy_args)
86
+ [nil, @port_ops.call(domain, aggregate, operation, args, route: route), nil, nil]
87
+ else
88
+ entity_depth = command_name.split(".").size - 1
89
+ route = Routing.envelope(to, entity_depth: entity_depth)
90
+ @entities.call(domain, aggregate, command_name, legacy_args, route: route, with: with)
91
+ end
92
+ else
93
+ command = aggregate.command(command_name) ||
94
+ raise(UnknownVerb, RefusalWording.render("UnknownVerb", "aggregate_no_command",
95
+ aggregate: aggregate_name, command: command_name.inspect))
96
+ args = Routing.payload(command, with: with, legacy: legacy_args)
97
+ route = Routing.envelope(to)
98
+ @commands.call(domain, aggregate, command, args, saga_correlation, route: route)
99
+ end
100
+
101
+ # Correlation is SET AT CONSTRUCTION now, not merged on here —
102
+ # it is part of the transaction, known from this method's own
103
+ # argument before a single event exists. It used to be stamped
104
+ # onto already-emitted events, which is what kept an event
105
+ # mutable after it had happened.
106
+ #
107
+ # The ordering this note used to guard still holds, and more
108
+ # simply: `SagaInterpreter#advance` runs on THIS domain's
109
+ # `announced` events within this very call, and finds the
110
+ # correlation already there because it was never absent.
111
+
112
+ announced.each { |event| @policies.react(event, domain) }
113
+
114
+ announced.each { |event| @sagas.advance(event, domain) }
115
+
116
+ Result.new(verb: verb, instance: instance, events: announced,
117
+ execution_plan: execution_plan, persistence_outcome: persistence_outcome)
118
+ end
119
+
120
+ # "IF THIS WERE DISPATCHED RIGHT NOW, WOULD IT SUCCEED" — the same
121
+ # pipeline #dispatch itself runs (arguments coerced, givens checked,
122
+ # mutations applied IN MEMORY, ensures checked against the settled
123
+ # result), except `step_save`/`step_emit` never run, and neither do
124
+ # policies or sagas afterward: nothing here is committed, so nothing
125
+ # should react to it. Built for exactly the shape a whole-board
126
+ # postcondition needs to be tested against (a downstream project's
127
+ # own chess domain, checking "does this move leave my own king in
128
+ # check" — the alternative was dispatching a real, unrelated piece's
129
+ # own move purely to trigger the check, which then had to avoid
130
+ # interfering with the very position being tested).
131
+ #
132
+ # RAISES THE SAME REFUSALS #dispatch does — a DomainRefusal
133
+ # subclass propagates normally, so a caller checking "would this be
134
+ # legal" writes the identical rescue clause a real dispatch already
135
+ # needs; this returns `true` only when nothing was refused.
136
+ #
137
+ # NEVER A PORT VERB — `PortOperationInterpreter`'s own side effects
138
+ # (an external gateway call, say) have no meaningful in-memory-only
139
+ # form, so this refuses one outright rather than silently running
140
+ # it for real, which "dry" would otherwise quietly lie about.
141
+ def dry_run?(verb, **args)
142
+ domain, aggregate_name, command_name = parse(verb)
143
+ aggregate = resolve_aggregate(domain, aggregate_name, verb)
144
+
145
+ if command_name.include?(".")
146
+ head, = command_name.split(".", 2)
147
+ if aggregate.port(head)
148
+ raise WiringError,
149
+ "#{verb} names a port operation — dry_run? has no in-memory form for one, " \
150
+ "only for aggregate and entity commands"
151
+ end
152
+
153
+ @entities.call(domain, aggregate, command_name, args, dry_run: true)
154
+ else
155
+ command = aggregate.command(command_name) ||
156
+ raise(UnknownVerb, RefusalWording.render("UnknownVerb", "aggregate_no_command",
157
+ aggregate: aggregate_name, command: command_name.inspect))
158
+ @commands.call(domain, aggregate, command, args, dry_run: true)
159
+ end
160
+
161
+ true
162
+ end
163
+
164
+ # THE DOOR AN ADAPTER OUTSIDE THE BLUEBOOK CALLS THROUGH — never the
165
+ # domain itself. `port_name`/`operation_name` are separate arguments
166
+ # rather than one packed verb string on purpose: there is no established
167
+ # wire spelling for "domain, aggregate, port, operation" yet, and
168
+ # inventing one is a bigger decision than this call needs to make.
169
+ #
170
+ # No adapter-to-port binding lookup happens here — that is
171
+ # `Hecks.adapter`'s existing job (unchanged by this), and wiring "which
172
+ # adapter may call this port" through is the next piece, not this one.
173
+ def dispatch_port(domain, aggregate_name, port_name, operation_name, to: nil, with: nil, **legacy_args)
174
+ aggregate = resolve_aggregate(domain, aggregate_name, "#{domain}::#{aggregate_name}.#{port_name}.#{operation_name}")
175
+ port = aggregate.port(port_name) ||
176
+ raise(UnknownVerb, "#{aggregate_name} has no port #{port_name.inspect}")
177
+ operation = port.operation(operation_name) ||
178
+ raise(UnknownVerb, "#{port_name} has no operation #{operation_name.inspect}")
179
+
180
+ route, args = port_invocation(aggregate, operation, to: to, with: with, legacy: legacy_args)
181
+ announced = @port_ops.call(domain, aggregate, operation, args, route: route)
182
+
183
+ announced.each { |event| @policies.react(event, domain) }
184
+ announced.each { |event| @sagas.advance(event, domain) }
185
+
186
+ announced
187
+ end
188
+
189
+ def port_invocation(aggregate, operation, to:, with:, legacy:)
190
+ legacy = legacy.dup
191
+ identity = operation.identity_attribute(aggregate.hecks_name)
192
+ if to.nil? && identity && legacy.key?(identity.name)
193
+ to = legacy.delete(identity.name)
194
+ elsif to.nil? && operation.to == aggregate.hecks_name
195
+ # `to:`-DECLARED OPERATIONS carry no Reference-typed attribute at
196
+ # all (PortOperationBuilder#initialize's own comment on why —
197
+ # genuine routing metadata, not an attribute), so `identity`
198
+ # above is always nil for these; this is the second, purely
199
+ # additive lookup they need instead. The routing value sits in
200
+ # a PLAIN external-fact attribute, named for the owning
201
+ # aggregate's own identified_by field — the domain author's job
202
+ # to match, same discipline reference_to's own `as:` always
203
+ # required. Composite identity (more than one identified_by
204
+ # component) isn't attempted here — `.first` only, no domain in
205
+ # the real corpus has needed more for a port operation yet.
206
+ #
207
+ # READ, NOT deleted — unlike the Reference-attribute branch
208
+ # above, this is a genuine declared operation attribute (Rust's
209
+ # own comment: "declare only external facts with attribute"),
210
+ # not synthetic routing-only state; the operation's own
211
+ # attributes still expect to find it in the payload a few steps
212
+ # later (refuse_absent_arguments), and a real, live
213
+ # AbsentArgument confirmed this the hard way before `[]`
214
+ # replaced `delete`.
215
+ identity_name = Array(aggregate.identified_by).first
216
+ to = legacy[identity_name] if identity_name && legacy.key?(identity_name)
217
+ end
218
+
219
+ route = Routing.envelope(to)
220
+ raise TypeMismatch, "#{operation.hecks_name} requires its receiving aggregate in to:" unless route
221
+
222
+ [route, Routing.payload(operation, with: with, legacy: legacy)]
223
+ end
224
+ private :port_invocation
225
+
226
+ def query(verb, **args)
227
+ domain, query_name = verb.to_s.split(".", 2)
228
+ if query_name && !domain.include?("::")
229
+ bluebook = @registry.bluebook(domain) ||
230
+ raise(UnknownVerb, RefusalWording.render("UnknownVerb", "no_domain", domain: domain.inspect, verb: verb))
231
+ model = bluebook.read_model(query_name) ||
232
+ raise(UnknownVerb, RefusalWording.render("UnknownVerb", "no_read_model",
233
+ domain: domain, query: query_name.inspect))
234
+ return @read_models.call(domain, model, args)
235
+ end
236
+
237
+ domain, aggregate_name, query_name = parse(verb)
238
+ aggregate = resolve_aggregate(domain, aggregate_name, verb)
239
+
240
+ @queries.call(domain, aggregate, query_name, args)
241
+ end
242
+
243
+ # The same ask, answered by the reference interpreter alone — never
244
+ # the bound adapter's native hook. Read models have no reference
245
+ # twin, so only the aggregate-query form answers here; the fuzzer's
246
+ # query oracle diffs this against #query's answer.
247
+ def reference_query(verb, **args)
248
+ domain, aggregate_name, query_name = parse(verb)
249
+ aggregate = resolve_aggregate(domain, aggregate_name, verb)
250
+
251
+ @queries.reference_call(domain, aggregate, query_name, args)
252
+ end
253
+
254
+ # A reaction is the SYSTEM acting, not the caller who happened to be
255
+ # on the stack when the triggering command ran — the ambient caller
256
+ # is cleared for the reaction's own dispatch, so a triggering
257
+ # caller's role can neither satisfy nor block a reaction command it
258
+ # has nothing to do with (Runtime::Caller.without).
259
+ def reenter(verb, saga_correlation: nil, **args)
260
+ depth = @reaction_depth.to_i
261
+ @reaction_depth = depth + 1
262
+ Caller.without { dispatch(verb, saga_correlation: saga_correlation, **args) }
263
+ ensure
264
+ @reaction_depth = depth
265
+ end
266
+
267
+ def reaction_depth_reached? = @reaction_depth.to_i >= MAX_REACTION_DEPTH
268
+ def max_reaction_depth = MAX_REACTION_DEPTH
269
+
270
+ private
271
+
272
+ def parse(verb)
273
+ Naming.split_verb(verb) ||
274
+ raise(UnknownVerb, RefusalWording.render("UnknownVerb", "not_fully_qualified", verb: verb.inspect))
275
+ end
276
+
277
+ def resolve_aggregate(domain, aggregate_name, verb)
278
+ bluebook = @registry.bluebook(domain) ||
279
+ raise(UnknownVerb, RefusalWording.render("UnknownVerb", "no_domain", domain: domain.inspect, verb: verb))
280
+ bluebook.aggregate(aggregate_name) ||
281
+ raise(UnknownVerb, RefusalWording.render("UnknownVerb", "no_aggregate",
282
+ domain: domain, aggregate: aggregate_name.inspect))
283
+ end
284
+ end
285
+ end
286
+ end
@@ -0,0 +1,253 @@
1
+ require_relative "../naming"
2
+ require_relative "../freezer"
3
+ require_relative "value"
4
+ require_relative "refusal_wording"
5
+ require_relative "errors"
6
+ require_relative "identity"
7
+
8
+ module Hecks
9
+ module Runtime
10
+ # ONE ENTITY ELEMENT, LOCATED AND MUTATED — the walk-and-write half of
11
+ # dispatching into a piece an aggregate holds, factored out of
12
+ # `EntityInterpreter` so a SECOND caller (`CommandInterpreter`'s own
13
+ # `delegate_to_entity` step) can locate and mutate the SAME element the
14
+ # same way, against an aggregate record it already holds in memory
15
+ # rather than one freshly loaded from a repository. `EntityInterpreter`
16
+ # keeps its own dispatch order and Context; this is the part underneath
17
+ # that both now share, so it exists in exactly one place rather than two
18
+ # that could only ever drift (the same reasoning `Runtime::Identity`'s
19
+ # own header gives for the join/dig/reading trio it centralizes).
20
+ #
21
+ # `rules` (a `CommandRules` instance) is passed explicitly throughout
22
+ # rather than closed over, since this module has no instance of its own
23
+ # to hold one.
24
+ module EntityElement
25
+ module_function
26
+
27
+ # ONE HOP PER CHAIN ENTRY. `container` starts as `instance` (the root
28
+ # aggregate record) and becomes each just-located element in turn —
29
+ # Dispatch's own element is found INSIDE the Handler element
30
+ # `locate_chain` located the step before, never inside `instance`
31
+ # directly. `owner` is whichever construct's OWN attribute declares
32
+ # the list being searched (Handler declares `dispatches` ; the root
33
+ # aggregate declares `handlers`) — `root_aggregate` stays the ROOT
34
+ # the whole way through instead, passed to `element_of` separately,
35
+ # because coercion (`Value.for_attribute`) resolves value objects
36
+ # against the root's own namespace only ; an entity must never
37
+ # answer `.value_object` (Entity's own header comment) so handing
38
+ # it an intermediate owner instead would break every VO-typed
39
+ # identity field a nested entity declares.
40
+ #
41
+ # `route`, if given, is the routing envelope's own entity chain
42
+ # (`ctx.route.entities`, one entry per hop) — the routing/payload
43
+ # split's own consequence: an entity's own identity may arrive via
44
+ # `to:` rather than duplicated into `args`, so each hop is offered
45
+ # its routed identity ahead of falling back to `args`.
46
+ def locate_chain(root_aggregate, chain, instance, args, command_name, route = nil)
47
+ container = instance
48
+ owner = root_aggregate
49
+ chain.each_with_index do |entity, index|
50
+ container = element_of(root_aggregate, owner, entity, command_name, container, args,
51
+ route&.entities&.fetch(index))
52
+ owner = entity
53
+ end
54
+ container
55
+ end
56
+
57
+ # ONE ELEMENT, MATCHED ON EVERY PART OF ITS IDENTITY — not just the first.
58
+ # A piece's identity may be several paths, the same shape a head's can be,
59
+ # so a dispatch that names the element has to supply every part and every
60
+ # part has to agree with the stored one. `routed_identity`, when given,
61
+ # matches by the element's own minted identity string directly instead
62
+ # (`element_identity`, below) — the routing envelope already resolved
63
+ # which element it means, so re-deriving `wants` from `args` would be
64
+ # redundant at best and wrong if `args` no longer carries that identity
65
+ # at all.
66
+ def element_of(root_aggregate, owner, entity, command_name, container, args, routed_identity = nil)
67
+ entity_name = entity.hecks_name
68
+ list_attr = owner.attributes.find { |a| a.list? && a.type.to_s == entity_name } ||
69
+ raise(UnknownVerb, RefusalWording.render("UnknownVerb", "entity_holds_no_list",
70
+ aggregate: owner.hecks_name, entity: entity_name))
71
+
72
+ wants = unless routed_identity
73
+ entity.identity_paths.map do |path|
74
+ head = path.to_s.split(".").first.to_sym
75
+ raw = args[head] ||
76
+ raise(NotFound, RefusalWording.render("NotFound", "entity_element_no_identity",
77
+ command: command_name, entity: entity_name,
78
+ identity: Identity.reading(entity)))
79
+
80
+ [head, path, Value.for_attribute(root_aggregate, entity.attribute(head), raw)]
81
+ end
82
+ end
83
+
84
+ original = Array(container[list_attr.name])
85
+ position = if routed_identity
86
+ original.find_index { |element| element_identity(entity, element).to_s == routed_identity.to_s }
87
+ else
88
+ original.find_index { |el| wants.all? { |head, _path, want| el[head] == want } }
89
+ end
90
+ unless position
91
+ raise NotFound, RefusalWording.render(
92
+ "NotFound", "entity_element_missing",
93
+ entity: entity_name, identity: Identity.reading(entity),
94
+ wants: wants&.map { |_h, path, want| Identity.scalar(path, want) }&.join(", "),
95
+ aggregate: owner.hecks_name,
96
+ parent_id: container.respond_to?(:id) ? container.id.inspect : Rendering.describe(container)
97
+ )
98
+ end
99
+
100
+ # ONE LEVEL DEEPER THAN Instance#dup, for the same reason: a list
101
+ # attribute holds Hashes, and `apply_to_element` mutates the found
102
+ # one IN PLACE — the update mechanism for an entity, not a bug. But
103
+ # in place means aliased with the adapter's own record until this
104
+ # copies the array and the target element before handing either
105
+ # back, and writes the fresh array into `container` so the copy is
106
+ # what persists on success and NOTHING aliased survives a refusal.
107
+ # `container[list_attr.name] = copied` reaches `instance` itself
108
+ # when this is the FIRST hop, and reaches the (already copied)
109
+ # PARENT element when it is a later one — either way it is the
110
+ # SAME already-fresh object `locate_chain` is about to hand back
111
+ # as `container` for the next hop, so nothing further has to
112
+ # propagate a write back up the chain by hand.
113
+ copied = original.dup
114
+ element = copied[position].dup
115
+ copied[position] = element
116
+ container[list_attr.name] = copied
117
+ element
118
+ end
119
+
120
+ # THE ELEMENT'S OWN IDENTITY, joined from its parts — the entity-level
121
+ # twin of `Identity.of`, reading off the STORED ELEMENT (a Hash) rather
122
+ # than a dispatch payload. An id is a SCALAR, and the PATH is how it is
123
+ # reached — never by opening a value object and taking whatever single
124
+ # field is inside. That unwrapping is gone from the language: a piece
125
+ # that does not name its fields is refused when the bluebook loads ("an
126
+ # entity says what it is known by", "an identity part names something"),
127
+ # so by the time a dispatch arrives here there is always a path to dig.
128
+ def element_identity(entity, element)
129
+ parts = entity.identity_paths.map do |path|
130
+ head = path.to_s.split(".").first.to_sym
131
+ Identity.scalar(path, element[head])
132
+ end
133
+
134
+ Naming.identity(parts)
135
+ end
136
+
137
+ # S17, ADR 0026 — `:append`/`:remove`/`:multiply`/`:clamp`, an
138
+ # entity-scoped mirror of `CommandInterpreter::MutationApplier
139
+ # #apply`'s own four (that module's own header names each one's
140
+ # origin). Missing until now — an entity-owned command declaring
141
+ # `sets :some_list, append: {...}` matched no `when` here and
142
+ # silently no-opped, the one place this language otherwise
143
+ # refuses what it cannot check applying nothing instead. `Member`/
144
+ # `Dispatch` (S17) are the first real callers: both need to
145
+ # append a value-object-typed element (`Pair`/`Binding`) onto a
146
+ # list attribute THEY OWN, once they become entities of
147
+ # `ValueObject`/`ProcessManager` rather than separate aggregates.
148
+ #
149
+ # `:increment`/`:decrement`/`:multiply` ALSO fixed here, found
150
+ # while proving this method against a real fixture: they wrapped
151
+ # `amount` unconditionally whenever `attribute` existed, the same
152
+ # asymmetric-wrapping shape `MutationApplier#rewrap_arithmetic_
153
+ # result`'s own comment documents fixing at the aggregate level
154
+ # (migration plan task 9) — a phantom-created VO-typed field's
155
+ # `current` reads back a raw, unwrapped default, `amount` was
156
+ # wrapped anyway, and the two sides of one arithmetic call
157
+ # disagreed on Value-ness. Confirmed live, not theoretical: this
158
+ # method's own fixture (TaggedList.Bump, a VO-typed `count` with
159
+ # `default: 0`) raised exactly this TypeMismatch on its first
160
+ # real run.
161
+ def apply_to_element(rules, aggregate, entity, element, mutation, args)
162
+ case mutation.op
163
+ when :set
164
+ value = rules.resolve_source(mutation.source, args)
165
+ attribute = entity.attribute(mutation.target)
166
+ element[mutation.target] = attribute ? Value.for_attribute(aggregate, attribute, value) : value
167
+ when :append
168
+ element[mutation.target] = appended_to_element(aggregate, entity, element, mutation, args)
169
+ when :remove
170
+ element[mutation.target] = removed_from_element(rules, aggregate, entity, element, mutation, args)
171
+ when :increment, :decrement
172
+ attribute = entity.attribute(mutation.target)
173
+ amount = rules.resolve_source(mutation.source, args)
174
+ current = element[mutation.target]
175
+ amount = Value.for_attribute(aggregate, attribute, amount) if attribute && current.is_a?(Value)
176
+ result = rules.arithmetic(current, amount, mutation.target, rules.sign_of(mutation.op))
177
+ element[mutation.target] = rewrap_arithmetic_result(aggregate, attribute, current, result)
178
+ when :multiply
179
+ attribute = entity.attribute(mutation.target)
180
+ amount = rules.resolve_source(mutation.source, args)
181
+ current = element[mutation.target]
182
+ amount = Value.for_attribute(aggregate, attribute, amount) if attribute && current.is_a?(Value)
183
+ result = rules.multiply(current, amount, mutation.target)
184
+ element[mutation.target] = rewrap_arithmetic_result(aggregate, attribute, current, result)
185
+ when :clamp
186
+ element[mutation.target] = rules.clamp(element[mutation.target], mutation.source, mutation.target)
187
+ else
188
+ # The aggregate-level twin's own backstop
189
+ # (MutationApplier#apply), for the same reason: applying
190
+ # nothing and refusing nothing would be the one silent
191
+ # no-op in a language that otherwise refuses what it cannot
192
+ # check.
193
+ raise WiringError, "no entity mutation applier handles :#{mutation.op} — add one before declaring it"
194
+ end
195
+ end
196
+
197
+ # `MutationApplier#rewrap_arithmetic_result`'s own entity-scoped
198
+ # twin, byte-for-byte the same fix — see that method's own
199
+ # comment for the full "phantom-field asymmetric wrapping" story.
200
+ # A no-op whenever `current` was already a Value (the arithmetic
201
+ # call already returned one) or the mutation targets no declared
202
+ # attribute at all.
203
+ def rewrap_arithmetic_result(aggregate, attribute, current, result)
204
+ return result if current.is_a?(Value) || attribute.nil? || result.is_a?(Value)
205
+
206
+ Value.for_attribute(aggregate, attribute, result)
207
+ end
208
+
209
+ # `MutationApplier#resolve_append_source`'s own entity-scoped
210
+ # twin — a caller-supplied ARG first, falling back to the
211
+ # ELEMENT's own current field (never the parent instance's) when
212
+ # it isn't one.
213
+ def resolve_element_append_source(source, element, args)
214
+ return source unless source.is_a?(Symbol)
215
+ return args[source] if args.key?(source)
216
+
217
+ element[source]
218
+ end
219
+
220
+ # `MutationApplier#appended`'s own entity-scoped twin. VALUE-
221
+ # OBJECT elements only — an entity's own list, appended to by an
222
+ # entity-owned command, holds a value object (`Member.pairs`'
223
+ # own `Pair`, `Dispatch.with_spec`'s own `Binding`) the same way
224
+ # every real corpus append does; entity-in-entity nesting (a
225
+ # list of ANOTHER entity, owned by this one) is out of scope —
226
+ # `MutationApplier#entity_element`'s own fallback is deliberately
227
+ # not mirrored here, since nothing in this language's own
228
+ # `EntityBuilder` can declare a nested entity to need it (see
229
+ # S17's own scoping note on why Dispatch flattens under
230
+ # ProcessManager instead of nesting under Handler).
231
+ def appended_to_element(aggregate, entity, element, mutation, args)
232
+ fields = mutation.source.transform_values { |source| resolve_element_append_source(source, element, args) }
233
+ element_type = entity.attribute(mutation.target)&.type
234
+ value_object = aggregate.value_object(element_type)
235
+ value_object&.attributes&.each do |attribute|
236
+ fields[attribute.name] = Value.scalar(fields[attribute.name]) if fields[attribute.name].is_a?(Value)
237
+ end
238
+ appended = value_object ? Value.build(value_object, fields, aggregate) : fields
239
+ Freezer.deep(Array(element[mutation.target]) + [appended])
240
+ end
241
+
242
+ # `MutationApplier#removed`'s own entity-scoped twin — matches by
243
+ # VALUE EQUALITY, element-wise, the same "so a concurrent Add can
244
+ # never be lost" reasoning that method's own comment gives.
245
+ def removed_from_element(rules, aggregate, entity, element, mutation, args)
246
+ value = rules.resolve_source(mutation.source, args)
247
+ attribute = entity.attribute(mutation.target)
248
+ value = Value.for_attribute(aggregate, attribute, value) if attribute
249
+ Array(element[mutation.target]).reject { |candidate| candidate == value }
250
+ end
251
+ end
252
+ end
253
+ end