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,70 @@
1
+ module Hecks
2
+ # The public, project-wide dispatch door. It owns address resolution only;
3
+ # each discovered Bluebook keeps its own runtime and persistence bindings.
4
+ class Router
5
+ class UnknownAddress < StandardError; end
6
+ class WrongVerbKind < StandardError; end
7
+ class NotBooted < StandardError; end
8
+ class AmbiguousShortRoute < StandardError; end
9
+
10
+ attr_reader :register
11
+
12
+ def self.load(root) = new(Bluebook::ProjectLoader.load(root))
13
+
14
+ # Install one project router for ordinary application calls. The explicit
15
+ # `Router.load` API remains useful for tests and embedded hosts.
16
+ def self.boot(root)
17
+ router = load(root)
18
+ router.install_namespace!
19
+ @default = router
20
+ end
21
+
22
+ def self.default
23
+ @default || raise(NotBooted, "no project router is booted — call Hecks::Router.boot(root) first")
24
+ end
25
+
26
+ def self.dispatch(address, **args) = default.dispatch(address, **args)
27
+ def self.query(address, **args) = default.query(address, **args)
28
+
29
+ def initialize(register)
30
+ @register = register
31
+ end
32
+
33
+ def available = register.entries.values
34
+
35
+ def install_namespace! = NamespaceInstaller.new(self).install!
36
+
37
+ def resolve(address)
38
+ fqn = Fqn.parse(address)
39
+ raise UnknownAddress, "router addresses require a realm: #{address.inspect}" unless fqn.realm
40
+
41
+ register.fetch(fqn.to_s)
42
+ rescue KeyError
43
+ raise UnknownAddress, "no Bluebook route for #{address.inspect}"
44
+ end
45
+
46
+ def dispatch(address, **args)
47
+ entry = resolve(address)
48
+ raise WrongVerbKind, "#{address.inspect} names a query; use #query" unless entry.command?
49
+
50
+ entry.dispatcher.dispatch(local_verb(entry), **args)
51
+ end
52
+
53
+ def query(address, **args)
54
+ entry = resolve(address)
55
+ raise WrongVerbKind, "#{address.inspect} names a command; use #dispatch" unless entry.query?
56
+
57
+ entry.dispatcher.query(local_verb(entry), **args)
58
+ end
59
+
60
+ private
61
+
62
+ def local_verb(entry)
63
+ return "#{entry.fqn.domain}.#{entry.declared_verb}" unless entry.fqn.aggregate
64
+
65
+ "#{entry.fqn.domain}::#{entry.fqn.aggregate}.#{entry.declared_verb}"
66
+ end
67
+ end
68
+ end
69
+
70
+ require_relative "router/namespace_installer"
@@ -0,0 +1,50 @@
1
+ module Hecks
2
+ module Runtime
3
+ # WHO IS DISPATCHING, bound for the duration of a block — the ambient
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
7
+ # time, which a future per-request caller (docs/rails-integration.md's
8
+ # still-unbuilt `WebDoor`) needs to bind safely under concurrency.
9
+ #
10
+ # A child `Thread.new` spawned mid-block does not inherit this — plain
11
+ # Ruby thread-local semantics — but nothing in this codebase spawns a
12
+ # worker thread mid-dispatch today, so this is a fact worth naming
13
+ # rather than a gap worth solving.
14
+ module Caller
15
+ # `actor_id` is OPTIONAL, on top of the role that has always been
16
+ # here — a caller that states only a role (every caller before this)
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
19
+ # `CommandRules::Authorization` check a real Governance
20
+ # `RoleAssignment` instead, once the command's domain has Governance
21
+ # attached — see that module's own header for the full split.
22
+ Current = Struct.new(:role, :actor_id, keyword_init: true)
23
+
24
+ module_function
25
+
26
+ def current = Thread.current[:hecks_caller]
27
+
28
+ def as(role:, actor_id: nil)
29
+ previous = Thread.current[:hecks_caller]
30
+ Thread.current[:hecks_caller] = Current.new(role: role.to_s, actor_id: actor_id&.to_s)
31
+ yield
32
+ ensure
33
+ Thread.current[:hecks_caller] = previous
34
+ end
35
+
36
+ # A REACTION IS THE SYSTEM ACTING, not the caller who happened to be
37
+ # on the stack when the triggering command ran — `Dispatcher#reenter`
38
+ # clears the ambient caller around a reaction's own dispatch so a
39
+ # triggering caller's role can neither satisfy nor block a reaction
40
+ # command it has nothing to do with.
41
+ def without
42
+ previous = Thread.current[:hecks_caller]
43
+ Thread.current[:hecks_caller] = nil
44
+ yield
45
+ ensure
46
+ Thread.current[:hecks_caller] = previous
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,44 @@
1
+ module Hecks
2
+ module Runtime
3
+ # WHICH PORTS A BOOT CAN ACTUALLY FULFILL, read off the registry it already
4
+ # holds. `registry.ports` names every port a domain declared a dependency
5
+ # on ; `registry.adapters` names every adapter wired to implement one — the
6
+ # same `adapter.port == port.name` match `Ports::Extraction` and
7
+ # `Ports::IdentityGeneration` already make for themselves, one at a time,
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
10
+ # about rather than something a live dispatch discovers by refusing.
11
+ #
12
+ # `cycles` answers `[]`, always, and honestly: nothing in this port model
13
+ # lets one port depend on another — a port names a verb and a signal, an
14
+ # adapter names the port it implements, and neither can point at a third
15
+ # port. There is no edge for a cycle to be made of.
16
+ class CapabilityGraph
17
+ def initialize(registry)
18
+ @registry = registry
19
+ end
20
+
21
+ # { port name => [adapter name, ...] }, for every port the registry
22
+ # declares — including the ports nothing implements, so a caller can
23
+ # tell "declared, unfulfilled" from "never declared at all".
24
+ def fulfillments
25
+ @fulfillments ||= @registry.ports.each_key.to_h { |name| [name, adapters_for(name)] }
26
+ end
27
+
28
+ # The ports with zero adapters bound — the gap `Runtime::WiringError`
29
+ # would otherwise only surface at the moment something tries to dispatch
30
+ # through one.
31
+ def unfulfilled
32
+ fulfillments.select { |_, adapters| adapters.empty? }.keys
33
+ end
34
+
35
+ def cycles = []
36
+
37
+ private
38
+
39
+ def adapters_for(port_name)
40
+ @registry.adapters.values.select { |adapter| adapter.port == port_name }.map(&:name)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,124 @@
1
+ require_relative "../../naming"
2
+ require_relative "../errors"
3
+ require_relative "../refusal_wording"
4
+
5
+ module Hecks
6
+ module Runtime
7
+ class CommandInterpreter
8
+ # The payload gate: a command takes the arguments it declares — all of
9
+ # them, and no others.
10
+ module ArgumentGate
11
+ private
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.
17
+ #
18
+ # The keys that are legitimately not attributes are the ones that ADDRESS
19
+ # the aggregate rather than describe it : `id`, whatever the aggregate is
20
+ # identified by, and the reference key of the root a command reaches
21
+ # through. Refusing those would refuse every dispatch there is.
22
+ def refuse_unknown_arguments(domain, aggregate, command, args)
23
+ addressing = [:id, *aggregate.identity_heads, reference_key(command)] + correlation_keys(domain)
24
+ known = (command.attributes.map(&:name) + addressing).compact.map(&:to_sym)
25
+ # SORTED. Payload order is whatever the caller happened to write, and
26
+ # refusal wording is contract — pinned byte-for-byte by the corpus, so
27
+ # it cannot depend on hash iteration order.
28
+ unknown = (args.keys.map(&:to_sym) - known).sort
29
+ return if unknown.empty?
30
+
31
+ raise UnknownArgument,
32
+ RefusalWording.render("UnknownArgument", "unknown_args",
33
+ command: command.hecks_name, unknown: unknown.join(", "),
34
+ declared: declared_reading(command))
35
+ end
36
+
37
+ # And it takes ALL of them. The other half of the same sentence, missing
38
+ # until fuzz went looking : a name the command never declared was refused,
39
+ # while a name it DID declare could simply be left out.
40
+ #
41
+ # `Customer.Register` without its `name` used to be refused — but by
42
+ # ACCIDENT, and with a lie for a message. `then_set :name, to: :name` found
43
+ # nothing to resolve, passed the literal symbol on, and coercion reported
44
+ # `name is a PersonName — pass its fields as an object`, which describes a
45
+ # mistake the caller did not make. The real mistake — an argument simply
46
+ # missing — was never the one named, and nothing refused it on purpose.
47
+ #
48
+ # No command attribute anywhere in the corpus carries a default — checked,
49
+ # all eight chapters, zero — so there is no optional argument for this to
50
+ # step on. Every declared attribute is a fact the command needs.
51
+ #
52
+ # `aggregate:` — PRESENT ONLY FOR A PORT OPERATION. An aggregate
53
+ # command's own self-address never reaches this list at all
54
+ # (`CommandBuilder#reference_to`'s bare self-reference mints no
55
+ # attribute to be absent) — but `PortOperationBuilder#reference_to`
56
+ # ALWAYS mints one (this file's own header on `PortOperation`: "no
57
+ # creates?/acts_on distinction to protect"), because an operation
58
+ # historically had no OTHER way to say which record it addressed.
59
+ # `to:` is that other way now (`Dispatcher#port_invocation`
60
+ # promotes the identity attribute's own value out of the payload
61
+ # and into routing) — which left the attribute still DECLARED,
62
+ # still non-optional, and now never present in `args` at all: every
63
+ # operation with an identity attribute refused its own well-formed
64
+ # calls, dispatched exactly the way `to:` intends. Exempted here on
65
+ # the same terms an aggregate command's self-address always was —
66
+ # an address, not a fact the operation still needs handed back.
67
+ def refuse_absent_arguments(command, args, aggregate: nil)
68
+ given = args.keys.map(&:to_sym)
69
+ exempt = aggregate && command.respond_to?(:identity_attribute) &&
70
+ command.identity_attribute(aggregate.hecks_name)&.name
71
+ required = command.attributes.reject(&:optional?).map { |attribute| attribute.name.to_sym } - [exempt]
72
+ # SORTED, for the same reason the unknown list is : refusal wording is
73
+ # contract, and a pinned wording cannot depend on the order a set
74
+ # difference happens to be computed in.
75
+ absent = (required - given).sort
76
+ return if absent.empty?
77
+
78
+ raise AbsentArgument,
79
+ RefusalWording.render("AbsentArgument", "absent_args",
80
+ command: command.hecks_name, absent: absent.join(", "),
81
+ declared: declared_reading(command))
82
+ end
83
+
84
+ # A COMMAND THAT DECLARES NOTHING STILL HAS TO SAY SO. `Account
85
+ # .Freeze` is `reference_to Account` and no attributes at all, so
86
+ # `{declared}` rendered empty and the sentence trailed off mid-
87
+ # clause : "Freeze does not declare standing — it takes ". Read
88
+ # against a real refusal that shape is worse than unhelpful — it
89
+ # looks like the message itself is broken, right where a caller
90
+ # is already trying to work out what went wrong.
91
+ #
92
+ # Only the empty case changes. Every command that declares even
93
+ # one attribute renders exactly as it did before, because refusal
94
+ # wording is contract and pinned byte-for-byte by the corpus (see
95
+ # `refuse_unknown_arguments`' own note on sorting for why).
96
+ def declared_reading(command)
97
+ declared = command.attributes.map(&:name)
98
+ declared.empty? ? "none" : declared.join(", ")
99
+ end
100
+
101
+ # What a process manager correlates by is ROUTING, not description. A saga
102
+ # threads its correlation key through every leg it dispatches so the event
103
+ # each leg emits carries it and the next step can be correlated — so the key
104
+ # arrives on commands that never declare it, and legitimately.
105
+ #
106
+ # This is the weakest part of the gate. Correlation is the SAGA's business,
107
+ # and the better shape is for the saga to stamp its own key onto the event
108
+ # it caused rather than smuggle it through the command's payload. Until it
109
+ # does, refusing the key here would break every saga in the corpus.
110
+ def correlation_keys(domain)
111
+ Array(@registry.bluebook(domain)&.process_managers)
112
+ .filter_map { |saga| saga.correlates_by && saga.correlation_head }
113
+ end
114
+
115
+ def reference_key(command)
116
+ target = command.references.to_s
117
+ return nil if target.empty?
118
+
119
+ Naming.reference_key(target)
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,248 @@
1
+ require_relative "../value"
2
+
3
+ module Hecks
4
+ module Runtime
5
+ class CommandInterpreter
6
+ # How a command's declared mutations land on the instance in hand —
7
+ # set, append (with value-object or entity elements), and the
8
+ # arithmetic pair.
9
+ module MutationApplier
10
+ private
11
+
12
+ def assign_creation_attributes(instance, aggregate, command, args)
13
+ command.attributes.each do |attr|
14
+ next unless aggregate.attribute(attr.name)
15
+ next unless args.key?(attr.name)
16
+
17
+ instance[attr.name] = Value.for(aggregate, attr.name, args[attr.name])
18
+ end
19
+ end
20
+
21
+ def apply(instance, aggregate, mutation, args)
22
+ case mutation.op
23
+ when :set
24
+ value = mutation.source.is_a?(StateRef) ? instance[mutation.source.name] : @rules.resolve_source(mutation.source, args)
25
+ instance[mutation.target] = Value.for(aggregate, mutation.target, value)
26
+ when :append
27
+ instance[mutation.target] = appended(instance, aggregate, mutation, args)
28
+ when :increment, :decrement
29
+ amount = @rules.resolve_source(mutation.source, args)
30
+ attribute = aggregate.attribute(mutation.target)
31
+ current = instance[mutation.target]
32
+ # Vendored fix, not (yet) upstream hecks (migration plan
33
+ # task 9): see #rewrap_arithmetic_result's own comment below
34
+ # -- `amount` is wrapped ONLY when `current` already is, not
35
+ # merely because the target attribute exists.
36
+ amount = Value.for_attribute(aggregate, attribute, amount) if attribute && current.is_a?(Value)
37
+ result = @rules.arithmetic(current, amount, mutation.target, @rules.sign_of(mutation.op))
38
+ instance[mutation.target] = rewrap_arithmetic_result(aggregate, attribute, current, result)
39
+ # Vendored addition, not (yet) upstream hecks (migration
40
+ # plan task 4): remove -- the list-removal counterpart to
41
+ # append, matching an element by value (plan.bluebook's
42
+ # RemoveDependency/DeactivateSprint: "a concurrent Add can
43
+ # never be lost").
44
+ when :remove
45
+ instance[mutation.target] = removed(instance, aggregate, mutation, args)
46
+ # Vendored addition, not (yet) upstream hecks (migration
47
+ # plan task 4, i106): multiply/clamp, the scale/bound pair
48
+ # alongside increment/decrement's add/subtract pair -- see
49
+ # CommandRules::Arithmetic#multiply/#clamp's own comments.
50
+ when :multiply
51
+ amount = @rules.resolve_source(mutation.source, args)
52
+ attribute = aggregate.attribute(mutation.target)
53
+ current = instance[mutation.target]
54
+ amount = Value.for_attribute(aggregate, attribute, amount) if attribute && current.is_a?(Value)
55
+ result = @rules.multiply(current, amount, mutation.target)
56
+ instance[mutation.target] = rewrap_arithmetic_result(aggregate, attribute, current, result)
57
+ # Vendored addition, not (yet) upstream hecks (migration
58
+ # plan task 4, i106): clamp -- bounds the current value into
59
+ # [min, max]. mutation.source is always a literal [min, max]
60
+ # pair, never an argument reference -- resolve_source would be
61
+ # a no-op for an Array (it only special-cases Symbol), so it's
62
+ # read straight.
63
+ when :clamp
64
+ instance[mutation.target] = @rules.clamp(instance[mutation.target], mutation.source, mutation.target)
65
+ # `delegates_to` — CommandBuilder#delegates_to's own comment gives
66
+ # the full reasoning for storing it as a mutation at all. A REAL
67
+ # no-op here, not a gap: it targets no field on THIS instance —
68
+ # `CommandInterpreter#step_delegate_to_entity`, a LATER step in
69
+ # the same dispatch, is what actually applies it, against the
70
+ # target entity element `EntityElement.apply_to_element` reaches,
71
+ # never through this method.
72
+ when :delegate
73
+ nil
74
+ else
75
+ # Every declared op has a `when` above — this is not a real
76
+ # runtime path today, only a backstop against the day one
77
+ # more mutation kind reaches the grammar and this method,
78
+ # unlike every other one it could reach, does not: applying
79
+ # nothing and refusing nothing would be the one silent
80
+ # no-op in a language that otherwise refuses what it cannot
81
+ # check.
82
+ raise Runtime::WiringError, "no mutation applier handles :#{mutation.op} — add one before declaring it"
83
+ end
84
+ end
85
+
86
+ # A CALLER-SUPPLIED ARG, FIRST -- an append's own field can also
87
+ # name something the SUBJECT ALREADY KNOWS about itself, falling
88
+ # back to the AGGREGATE'S OWN CURRENT FIELD when it isn't one — a
89
+ # caller appending at the end without computing or supplying a
90
+ # position (`position: :next_position`, a field the command never
91
+ # declares as an argument at all). `args.key?`, not a truthiness
92
+ # check on the value — an explicitly-nil argument still counts as
93
+ # "the caller named it," same distinction `assign_creation_attributes`
94
+ # already draws.
95
+ #
96
+ # Extracted to its own method (pure refactor, ternary -> early
97
+ # return, no behavior change) alongside `remove`'s own addition
98
+ # below, purely for readability at this point in the file.
99
+ def resolve_append_source(source, instance, args)
100
+ # `state(:field)` — the record's own value, never an argument
101
+ # (`Literal::StateRef`'s own comment).
102
+ return instance[source.name] if source.is_a?(StateRef)
103
+ return source unless source.is_a?(Symbol)
104
+ return args[source] if args.key?(source)
105
+
106
+ instance[source]
107
+ end
108
+
109
+ def appended(instance, aggregate, mutation, args)
110
+ fields = mutation.source.transform_values { |source| resolve_append_source(source, instance, args) }
111
+ element_type = aggregate.attribute(mutation.target)&.type
112
+ value_object = aggregate.value_object(element_type)
113
+ if value_object
114
+ value_object.attributes.each do |attribute|
115
+ held = fields[attribute.name]
116
+ # A SINGLE-FIELD VALUE unwraps to its scalar so it bridges
117
+ # into the element's own (differently named) wrapper; a
118
+ # MULTI-FIELD one (a `state(:en_passant_square)` Square
119
+ # copied off the record) has no scalar to stand in for it
120
+ # and is handed across whole — `Value.for_attribute` keeps a
121
+ # value of the element field's own type as it is.
122
+ fields[attribute.name] = Value.scalar(held) if held.is_a?(Value) && held.to_h.size == 1
123
+ end
124
+ end
125
+ element = value_object ? Value.build(value_object, fields,
126
+ aggregate) : entity_element(aggregate, element_type, instance[mutation.target],
127
+ fields)
128
+
129
+ # FROZEN, like every other value the domain hands back. An
130
+ # appended list used to come back mutable, so a caller could
131
+ # push straight into an aggregate's own state after the
132
+ # dispatch had finished.
133
+ Freezer.deep(Array(instance[mutation.target]) + [element])
134
+ end
135
+
136
+ # Vendored addition, not (yet) upstream hecks (migration plan
137
+ # task 4): the removal counterpart to #appended -- matches by
138
+ # VALUE EQUALITY, element-wise, no read-modify-write (plan.
139
+ # bluebook's own words: "so a concurrent Add can never be lost").
140
+ # `mutation.source` is a single field reference (`:dependency`),
141
+ # unlike append's field-map -- resolved and Value-coerced the
142
+ # SAME way increment/decrement already coerce their own amount,
143
+ # so the comparison is against a like-shaped Value, not a raw
144
+ # scalar against a wrapped one.
145
+ def removed(instance, aggregate, mutation, args)
146
+ value = @rules.resolve_source(mutation.source, args)
147
+ attribute = aggregate.attribute(mutation.target)
148
+ value = Value.for_attribute(aggregate, attribute, value) if attribute
149
+ Array(instance[mutation.target]).reject { |element| element == value }
150
+ end
151
+
152
+ # Vendored fix, not (yet) upstream hecks (migration plan
153
+ # task 9): #apply's `:increment`/`:decrement`/`:multiply`
154
+ # branches used to wrap `amount` into a `Value` unconditionally
155
+ # whenever the target attribute existed, never checking whether
156
+ # `current` (the field's OWN existing value, read straight off
157
+ # `instance[mutation.target]`) was ALSO wrapped -- the two sides
158
+ # of the same arithmetic call could disagree on Value-ness. On a
159
+ # PHANTOM-CREATED field this is the common case, not an edge
160
+ # one: `Instance.defaults`/`#default_for` leaves a VO-typed
161
+ # attribute with no declared `default:` genuinely absent (nil),
162
+ # and `CommandRules::Arithmetic#arithmetic`/`#multiply`'s own
163
+ # `current ||= 0` then turns that nil into a RAW, unwrapped
164
+ # Integer `0` -- so `current.is_a?(Value) && amount.is_a?(Value)`
165
+ # read false even though `amount` (correctly wrapped by the old
166
+ # unconditional line) genuinely held a valid, correctly-typed
167
+ # number, and the primitive path's `unless amount.is_a?(Numeric)`
168
+ # guard refused it as a TYPE MISMATCH the caller never made --
169
+ # an artifact of this method's own asymmetric wrapping, not bad
170
+ # input.
171
+ #
172
+ # Fixed at the call site (above) by wrapping `amount` ONLY when
173
+ # `current` is ALREADY a `Value` -- so an established VO-typed
174
+ # field (a multi-field Money balance, say, already hydrated from
175
+ # a prior save) keeps going through
176
+ # `Arithmetic#arithmetic_value_object` exactly as before (the
177
+ # "already-Value-wrapped field still mutates correctly" case
178
+ # this fix must not regress), while a phantom field's raw
179
+ # `current` and raw `amount` both take the plain-Numeric path
180
+ # together. This method closes the other half: the plain-Numeric
181
+ # path returns a bare Ruby number, and if the attribute is
182
+ # itself VO-typed (the norm), that raw result needs the SAME
183
+ # wrap `:set` already gives its own resolved value (`Value.for`)
184
+ # before it's stored, so a field's stored shape doesn't depend
185
+ # on which dispatch happened to mutate it first. A no-op
186
+ # whenever `current` was already a Value (the VO branch already
187
+ # returns one) or the mutation targets no declared attribute at
188
+ # all.
189
+ def rewrap_arithmetic_result(aggregate, attribute, current, result)
190
+ return result if current.is_a?(Value) || attribute.nil? || result.is_a?(Value)
191
+
192
+ Value.for_attribute(aggregate, attribute, result)
193
+ end
194
+
195
+ def entity_element(aggregate, element_type, current, fields)
196
+ entity = aggregate.entities.find { |piece| piece.hecks_name == element_type.to_s }
197
+ return fields unless entity
198
+
199
+ entity.attributes.each do |attribute|
200
+ next unless fields.key?(attribute.name)
201
+
202
+ fields[attribute.name] = Value.for_attribute(aggregate, attribute, fields[attribute.name])
203
+ end
204
+ if entity.identified_by && !fields.key?(entity.identified_by)
205
+ attribute = entity.attribute(entity.identified_by)
206
+ fields[entity.identified_by] = Value.from_identifier(aggregate, attribute, Array(current).size + 1)
207
+ else
208
+ check_entity_collision(aggregate, entity, current, fields)
209
+ end
210
+ fields[entity.lifecycle.field] ||= entity.lifecycle.default if entity.lifecycle
211
+ fields
212
+ end
213
+
214
+ # THE SAME CHECK #hydrate GIVES EVERY CREATING AGGREGATE COMMAND
215
+ # (`repository.find(id)`, above this file in command_interpreter.rb),
216
+ # one level down. Reached only on the two branches #entity_element
217
+ # does NOT auto-mint: a CALLER-SUPPLIED identity (the field is
218
+ # already in the append's own field map, so the `if` above skips
219
+ # it) or a COMPOSITE one (`entity.identified_by` is nil for those —
220
+ # Runtime::Identified#derive_identity — so the `if` above is false
221
+ # unconditionally). Neither used to check the sibling list at all:
222
+ # a second LogVisit with the same date+sequence, or a second
223
+ # IssueKey with the same serial, appended a silent duplicate — worse
224
+ # than an ordinary duplicate row, because EntityInterpreter#element_of's
225
+ # `find_index` always matches the FIRST match, so the second becomes
226
+ # permanently unaddressable by any later command.
227
+ #
228
+ # Auto-minted entities never reach here — `identity_heads` for them
229
+ # is still checked at mint time by construction (`current.size + 1`
230
+ # can only repeat if something `remove:`s from the list between
231
+ # mints, which no real domain does today), so they can't be flagged
232
+ # by mistake.
233
+ def check_entity_collision(aggregate, entity, current, fields)
234
+ heads = entity.identity_heads
235
+ return if heads.empty?
236
+
237
+ collision = Array(current).find { |element| heads.all? { |head| element[head] == fields[head] } }
238
+ return unless collision
239
+
240
+ raise(AlreadyExists, RefusalWording.render("AlreadyExists", "entity_duplicate",
241
+ entity: entity.hecks_name, aggregate: aggregate.hecks_name,
242
+ identity: Identity.reading(entity),
243
+ offered: heads.map { |head| Rendering.describe(fields[head]) }.join(", ")))
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end