hecks 0.2.0 → 1.0.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 (419) 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 +427 -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 +101 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +56 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +99 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +65 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +179 -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 +145 -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 +88 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +207 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +438 -0
  31. data/lib/hecks/adapters/driven/postgres_era.adapter +17 -0
  32. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  33. data/lib/hecks/adapters/driven/prism.rb +80 -0
  34. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  35. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  36. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  37. data/lib/hecks/adapters/driven/sqlite/codec.rb +80 -0
  38. data/lib/hecks/adapters/driven/sqlite/projection.rb +172 -0
  39. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +192 -0
  40. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  41. data/lib/hecks/adapters/driven/sqlite.rb +306 -0
  42. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  43. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  44. data/lib/hecks/adapters/driven.rb +54 -0
  45. data/lib/hecks/adapters.rb +6 -0
  46. data/lib/hecks/behaviors/dsl.rb +125 -0
  47. data/lib/hecks/behaviors/expectations.rb +340 -0
  48. data/lib/hecks/behaviors/ir.rb +31 -0
  49. data/lib/hecks/behaviors/rspec.rb +42 -0
  50. data/lib/hecks/behaviors/runner.rb +96 -0
  51. data/lib/hecks/behaviors.rb +25 -0
  52. data/lib/hecks/bluebook/aggregate.rb +108 -0
  53. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  54. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  55. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  56. data/lib/hecks/bluebook/assembly/contracts.rb +438 -0
  57. data/lib/hecks/bluebook/assembly/marks.rb +228 -0
  58. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  59. data/lib/hecks/bluebook/assembly.rb +91 -0
  60. data/lib/hecks/bluebook/attribute.rb +96 -0
  61. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  62. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  63. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  64. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  65. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  66. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  67. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  68. data/lib/hecks/bluebook/behaviour/lifecycle.rb +68 -0
  69. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  70. data/lib/hecks/bluebook/behaviour/process_manager.rb +51 -0
  71. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  72. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  73. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  74. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  75. data/lib/hecks/bluebook/chapter.rb +78 -0
  76. data/lib/hecks/bluebook/command.rb +124 -0
  77. data/lib/hecks/bluebook/domain_port.rb +102 -0
  78. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  79. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +1018 -0
  80. data/lib/hecks/bluebook/dsl/attribute_collector.rb +348 -0
  81. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  82. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1087 -0
  83. data/lib/hecks/bluebook/dsl/command_builder.rb +714 -0
  84. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  85. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  86. data/lib/hecks/bluebook/dsl/entity_builder.rb +430 -0
  87. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  88. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  89. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  90. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  91. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  92. data/lib/hecks/bluebook/dsl/policy_builder.rb +135 -0
  93. data/lib/hecks/bluebook/dsl/port_builder.rb +39 -0
  94. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  95. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +306 -0
  96. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  97. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  98. data/lib/hecks/bluebook/dsl/rule_reference.rb +173 -0
  99. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  100. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  101. data/lib/hecks/bluebook/dsl/word_gate.rb +228 -0
  102. data/lib/hecks/bluebook/dsl/world_builder.rb +117 -0
  103. data/lib/hecks/bluebook/dsl.rb +45 -0
  104. data/lib/hecks/bluebook/entity.rb +103 -0
  105. data/lib/hecks/bluebook/expression/canonical_form.rb +123 -0
  106. data/lib/hecks/bluebook/expression/evaluator.rb +304 -0
  107. data/lib/hecks/bluebook/expression/projection.json +218 -0
  108. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  109. data/lib/hecks/bluebook/expression/resolver.rb +781 -0
  110. data/lib/hecks/bluebook/expression.rb +13 -0
  111. data/lib/hecks/bluebook/hexagon.rb +60 -0
  112. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  113. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  114. data/lib/hecks/bluebook/meta_validator/judge.rb +621 -0
  115. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  116. data/lib/hecks/bluebook/meta_validator/port_judge.rb +51 -0
  117. data/lib/hecks/bluebook/meta_validator/readings.rb +360 -0
  118. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +351 -0
  119. data/lib/hecks/bluebook/meta_validator/shapes.rb +266 -0
  120. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  121. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  122. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  123. data/lib/hecks/bluebook/meta_validator.rb +542 -0
  124. data/lib/hecks/bluebook/model_check.rb +445 -0
  125. data/lib/hecks/bluebook/pattern_subset.rb +184 -0
  126. data/lib/hecks/bluebook/policy.rb +41 -0
  127. data/lib/hecks/bluebook/process_manager.rb +128 -0
  128. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  129. data/lib/hecks/bluebook/project_loader.rb +40 -0
  130. data/lib/hecks/bluebook/project_register.rb +82 -0
  131. data/lib/hecks/bluebook/query.rb +61 -0
  132. data/lib/hecks/bluebook/read_model.rb +109 -0
  133. data/lib/hecks/bluebook/reference.rb +74 -0
  134. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  135. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  136. data/lib/hecks/bluebook/translation.rb +92 -0
  137. data/lib/hecks/bluebook/value_object.rb +66 -0
  138. data/lib/hecks/bluebook.rb +74 -0
  139. data/lib/hecks/codemod.rb +342 -0
  140. data/lib/hecks/construct.rb +71 -0
  141. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  142. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  143. data/lib/hecks/deploy/oidc.json +18 -0
  144. data/lib/hecks/doc/reference.rb +410 -0
  145. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  146. data/lib/hecks/facade/cli_door.rb +119 -0
  147. data/lib/hecks/facade/cli_runner.rb +190 -0
  148. data/lib/hecks/facade/command_request.rb +105 -0
  149. data/lib/hecks/facade/handle.rb +173 -0
  150. data/lib/hecks/facade/json_door.rb +166 -0
  151. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  152. data/lib/hecks/facade/surface/chapter.rb +107 -0
  153. data/lib/hecks/facade/surface.rb +48 -0
  154. data/lib/hecks/facade.rb +44 -0
  155. data/lib/hecks/forms/app.rb +341 -0
  156. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  157. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  158. data/lib/hecks/forms/field_renderer.rb +177 -0
  159. data/lib/hecks/forms/field_shape.rb +232 -0
  160. data/lib/hecks/forms/html.rb +84 -0
  161. data/lib/hecks/forms/index_renderer.rb +35 -0
  162. data/lib/hecks/forms/page.rb +157 -0
  163. data/lib/hecks/forms/params.rb +161 -0
  164. data/lib/hecks/forms/port_argument.rb +46 -0
  165. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  166. data/lib/hecks/forms/record_renderer.rb +119 -0
  167. data/lib/hecks/forms/record_table.rb +68 -0
  168. data/lib/hecks/forms/reference_options.rb +30 -0
  169. data/lib/hecks/forms/value_object_shape.rb +46 -0
  170. data/lib/hecks/forms.rb +54 -0
  171. data/lib/hecks/fqn.rb +94 -0
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  175. data/lib/hecks/framework/bluebook/governance.bluebook +145 -0
  176. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  177. data/lib/hecks/framework/oidc.json +39 -0
  178. data/lib/hecks/framework.rb +90 -0
  179. data/lib/hecks/freezer.rb +67 -0
  180. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
  181. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  182. data/lib/hecks/fuzzing/isolated_boot.rb +286 -0
  183. data/lib/hecks/fuzzing/properties.rb +1192 -0
  184. data/lib/hecks/fuzzing/replay.rb +668 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +80 -0
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +250 -0
  189. data/lib/hecks/fuzzing/sequence_generator.rb +131 -0
  190. data/lib/hecks/fuzzing/value_generator.rb +200 -0
  191. data/lib/hecks/fuzzing.rb +12 -0
  192. data/lib/hecks/grammar/evolve.rb +324 -0
  193. data/lib/hecks/grammar/expression.bluebook +420 -0
  194. data/lib/hecks/grammar/expression_operators.json +1648 -0
  195. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  196. data/lib/hecks/grammar/oidc.json +69 -0
  197. data/lib/hecks/grammar/translation.bluebook +203 -0
  198. data/lib/hecks/grammar.rb +178 -0
  199. data/lib/hecks/ir.rb +126 -0
  200. data/lib/hecks/language/adapter.bluebook +116 -0
  201. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  202. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  203. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  204. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  205. data/lib/hecks/language/bluebook/command.bluebook +471 -0
  206. data/lib/hecks/language/bluebook/entity.bluebook +392 -0
  207. data/lib/hecks/language/bluebook/policy.bluebook +189 -0
  208. data/lib/hecks/language/bluebook/process_manager.bluebook +381 -0
  209. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  210. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  211. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  212. data/lib/hecks/language/bluebook/syntax.bluebook +448 -0
  213. data/lib/hecks/language/bluebook/vocabulary.bluebook +379 -0
  214. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  215. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  216. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  217. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  218. data/lib/hecks/language/oidc.json +333 -0
  219. data/lib/hecks/language/port.bluebook +120 -0
  220. data/lib/hecks/language/translation/translation.bluebook +110 -0
  221. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  222. data/lib/hecks/language/world/wiring.bluebook +62 -0
  223. data/lib/hecks/language/world/world.bluebook +84 -0
  224. data/lib/hecks/literal.rb +125 -0
  225. data/lib/hecks/naming.rb +174 -0
  226. data/lib/hecks/ports/access_control.port +9 -0
  227. data/lib/hecks/ports/access_control.rb +62 -0
  228. data/lib/hecks/ports/agent/answers.rb +104 -0
  229. data/lib/hecks/ports/agent.port +8 -0
  230. data/lib/hecks/ports/agent.rb +167 -0
  231. data/lib/hecks/ports/authentication.port +6 -0
  232. data/lib/hecks/ports/authentication.rb +50 -0
  233. data/lib/hecks/ports/authorization.port +7 -0
  234. data/lib/hecks/ports/authorization.rb +62 -0
  235. data/lib/hecks/ports/clock.port +5 -0
  236. data/lib/hecks/ports/clock.rb +62 -0
  237. data/lib/hecks/ports/extraction.port +5 -0
  238. data/lib/hecks/ports/extraction.rb +37 -0
  239. data/lib/hecks/ports/identity_assignment.port +5 -0
  240. data/lib/hecks/ports/identity_assignment.rb +45 -0
  241. data/lib/hecks/ports/identity_generation.port +5 -0
  242. data/lib/hecks/ports/identity_generation.rb +49 -0
  243. data/lib/hecks/ports/identity_resolution.port +5 -0
  244. data/lib/hecks/ports/identity_resolution.rb +40 -0
  245. data/lib/hecks/ports/loading.port +4 -0
  246. data/lib/hecks/ports/loading.rb +13 -0
  247. data/lib/hecks/ports/persistence/append_only.rb +138 -0
  248. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  249. data/lib/hecks/ports/persistence/execution.rb +23 -0
  250. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  251. data/lib/hecks/ports/persistence/plugin.rb +54 -0
  252. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +169 -0
  253. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +128 -0
  254. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +161 -0
  255. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +61 -0
  256. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +304 -0
  257. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +171 -0
  258. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +190 -0
  259. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +478 -0
  260. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +166 -0
  261. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +314 -0
  262. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +168 -0
  263. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +170 -0
  264. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +134 -0
  265. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +137 -0
  266. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +89 -0
  267. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +85 -0
  268. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  269. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +132 -0
  270. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +74 -0
  271. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +758 -0
  272. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +120 -0
  273. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +31 -0
  274. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +46 -0
  275. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +102 -0
  276. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +46 -0
  277. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +70 -0
  278. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +72 -0
  279. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +120 -0
  280. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +183 -0
  281. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +41 -0
  282. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +28 -0
  283. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +30 -0
  284. data/lib/hecks/ports/persistence/plugins/era/translation.rb +11 -0
  285. data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
  286. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  287. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  288. data/lib/hecks/ports/persistence.port +4 -0
  289. data/lib/hecks/ports/persistence.rb +39 -0
  290. data/lib/hecks/ports/projection.port +4 -0
  291. data/lib/hecks/ports/projection.rb +96 -0
  292. data/lib/hecks/ports/query/in_memory.rb +60 -0
  293. data/lib/hecks/ports/query/ordering.rb +41 -0
  294. data/lib/hecks/ports/query.rb +36 -0
  295. data/lib/hecks/ports.rb +27 -0
  296. data/lib/hecks/projections/diagrams.rb +642 -0
  297. data/lib/hecks/projections/ir.rb +18 -0
  298. data/lib/hecks/projections/model/deviations.rb +98 -0
  299. data/lib/hecks/projections/model.rb +145 -0
  300. data/lib/hecks/projections/oidc.rb +110 -0
  301. data/lib/hecks/projections/parser_table.rb +159 -0
  302. data/lib/hecks/projections/reference.rb +38 -0
  303. data/lib/hecks/projections/shape.rb +44 -0
  304. data/lib/hecks/projections/statements.rb +110 -0
  305. data/lib/hecks/projections/vocabulary.rb +100 -0
  306. data/lib/hecks/projections.rb +32 -0
  307. data/lib/hecks/projector/cli_projector.rb +446 -0
  308. data/lib/hecks/projector/docs_projector.rb +321 -0
  309. data/lib/hecks/projector/exporter.rb +158 -0
  310. data/lib/hecks/projector/ir_projector.rb +18 -0
  311. data/lib/hecks/projector/narrate_projector.rb +243 -0
  312. data/lib/hecks/projector/target.rb +97 -0
  313. data/lib/hecks/projector.rb +186 -0
  314. data/lib/hecks/query_ir.rb +411 -0
  315. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  316. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  317. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  318. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  319. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  320. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  321. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  322. data/lib/hecks/query_specification/common/null_policy.rb +92 -0
  323. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  324. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  325. data/lib/hecks/query_specification/common/options.rb +32 -0
  326. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  327. data/lib/hecks/query_specification/common/specification.rb +10 -0
  328. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  329. data/lib/hecks/query_specification/field_path.rb +107 -0
  330. data/lib/hecks/query_specification/hop_path.rb +132 -0
  331. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  332. data/lib/hecks/query_specification.rb +14 -0
  333. data/lib/hecks/rendering.rb +48 -0
  334. data/lib/hecks/router/namespace_installer.rb +157 -0
  335. data/lib/hecks/router.rb +70 -0
  336. data/lib/hecks/runtime/aggregate_lock.rb +45 -0
  337. data/lib/hecks/runtime/boot_gates.rb +41 -0
  338. data/lib/hecks/runtime/caller.rb +66 -0
  339. data/lib/hecks/runtime/capability_graph.rb +44 -0
  340. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +135 -0
  341. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +260 -0
  342. data/lib/hecks/runtime/command_interpreter.rb +497 -0
  343. data/lib/hecks/runtime/command_rules/admissibility.rb +357 -0
  344. data/lib/hecks/runtime/command_rules/arithmetic.rb +260 -0
  345. data/lib/hecks/runtime/command_rules/authorization.rb +65 -0
  346. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  347. data/lib/hecks/runtime/command_rules/references.rb +189 -0
  348. data/lib/hecks/runtime/command_rules.rb +28 -0
  349. data/lib/hecks/runtime/dependency_planning.rb +245 -0
  350. data/lib/hecks/runtime/dispatcher.rb +301 -0
  351. data/lib/hecks/runtime/entity_element.rb +253 -0
  352. data/lib/hecks/runtime/entity_interpreter.rb +299 -0
  353. data/lib/hecks/runtime/errors.rb +122 -0
  354. data/lib/hecks/runtime/event.rb +51 -0
  355. data/lib/hecks/runtime/identity.rb +156 -0
  356. data/lib/hecks/runtime/instance.rb +175 -0
  357. data/lib/hecks/runtime/interpreting.rb +90 -0
  358. data/lib/hecks/runtime/loader.rb +184 -0
  359. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  360. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  361. data/lib/hecks/runtime/query_interpreter.rb +286 -0
  362. data/lib/hecks/runtime/reaction_invocation.rb +253 -0
  363. data/lib/hecks/runtime/read_model_interpreter.rb +341 -0
  364. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  365. data/lib/hecks/runtime/reference_hop.rb +99 -0
  366. data/lib/hecks/runtime/refusal_wording.rb +126 -0
  367. data/lib/hecks/runtime/registry/saga_persistence.rb +142 -0
  368. data/lib/hecks/runtime/registry/verification.rb +246 -0
  369. data/lib/hecks/runtime/registry.rb +279 -0
  370. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  371. data/lib/hecks/runtime/routing.rb +96 -0
  372. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  373. data/lib/hecks/runtime/saga_interpreter.rb +503 -0
  374. data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
  375. data/lib/hecks/runtime/tenant_check.rb +84 -0
  376. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  377. data/lib/hecks/runtime/value/admission.rb +130 -0
  378. data/lib/hecks/runtime/value/coercion.rb +565 -0
  379. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  380. data/lib/hecks/runtime/value.rb +125 -0
  381. data/lib/hecks/runtime.rb +107 -0
  382. data/lib/hecks/storehouse.rb +632 -0
  383. data/lib/hecks/version.rb +16 -0
  384. data/lib/hecks/vocabulary.rb +218 -0
  385. data/lib/hecks.rb +123 -6
  386. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
  387. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
  388. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
  389. metadata +412 -222
  390. data/bin/hecks +0 -7
  391. data/bin/hecks-package +0 -65
  392. data/bin/hecks_console +0 -12
  393. data/bin/hecks_serverless +0 -6
  394. data/lib/cli/build.rb +0 -14
  395. data/lib/cli/command_runner.rb +0 -28
  396. data/lib/cli/console.rb +0 -10
  397. data/lib/cli/generate.rb +0 -37
  398. data/lib/cli/hecks-cli.rb +0 -27
  399. data/lib/cli/test.rb +0 -57
  400. data/lib/console/commands.rb +0 -8
  401. data/lib/console/hecks-console.rb +0 -1
  402. data/lib/packager/README.md +0 -0
  403. data/lib/packager/app_runner.rb +0 -21
  404. data/lib/packager/args.rb +0 -26
  405. data/lib/packager/compatibility/fixnum.rb +0 -6
  406. data/lib/packager/hecks.rb +0 -39
  407. data/lib/packager/query_runner.rb +0 -21
  408. data/lib/packager/resources/Dockerfile +0 -11
  409. data/lib/packager/resources/app_binary +0 -7
  410. data/lib/packager/resources/bundle_config +0 -3
  411. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  412. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  413. data/lib/serverless/Domain +0 -32
  414. data/lib/serverless/cli.rb +0 -75
  415. data/lib/serverless/resources/command_name.js +0 -5
  416. data/lib/serverless/resources/environment.js +0 -7
  417. data/lib/serverless/resources/handler.js.tt +0 -28
  418. data/lib/serverless/resources/run_binary.js +0 -22
  419. data/lib/serverless/resources/serverless.yml +0 -20
@@ -0,0 +1,357 @@
1
+ require_relative "../../bluebook/expression/evaluator"
2
+ require_relative "../../rendering"
3
+ require_relative "../errors"
4
+ require_relative "../refusal_wording"
5
+ require_relative "../value"
6
+ require_relative "../instance"
7
+
8
+ module Hecks
9
+ module Runtime
10
+ class CommandRules
11
+ # Whether a command may run at all: its declared givens, and the
12
+ # lifecycle transition it asks for.
13
+ module Admissibility
14
+ # WRAPS `subject` so a guard can read a DECLARED-but-storage-absent
15
+ # optional attribute as nil, not "cannot resolve." `Instance
16
+ # #hydrate_with_defaults` deliberately leaves one absent rather
17
+ # than nil-filled — "an attribute with no default stays absent,
18
+ # exactly as stored," that file's own comment — which is right
19
+ # for storage fidelity but wrong for a `given`/`ensures` reading
20
+ # an optional field a record predates (measured, not assumed: a
21
+ # real Item.Promote crashed on `!promoted` against a record from
22
+ # before `promoted` existed). Defensive on purpose — a `subject`
23
+ # with no `.aggregate` (an entity's own view/settled wrapper,
24
+ # entity_interpreter.rb's own callers) just degrades to today's
25
+ # exact behavior, zero risk to a path this bug was never measured
26
+ # against.
27
+ #
28
+ # NIL IS NOT THE ONLY OUTCOME, though. The Item.Promote fix above
29
+ # covered an OPTIONAL field — the honest case, where absence is
30
+ # exactly what optional means. It also, as a side effect, let a
31
+ # NON-optional field predating a record read nil the same way,
32
+ # which is the `ne:`/array-`in:` bug class applied to rule
33
+ # evaluation: a predicate silently answers against a value nobody
34
+ # ever wrote (ADR 0025, "Added attributes and absence"). `[]`
35
+ # below narrows the nil-read back to what it was built for —
36
+ # optional stays nil, non-optional raises a named refusal
37
+ # identifying the field, so a rule that reads it fails loud
38
+ # instead of quietly wrong.
39
+ class GuardState
40
+ def initialize(instance)
41
+ @instance = instance
42
+ @declared = instance.respond_to?(:aggregate) ? instance.aggregate.attributes.to_h { |a| [a.name, a] } : {}
43
+ # S12, ADR 0025 — a SEPARATE index, the same reason
44
+ # `Aggregate#projected_fields` is a separate IR collection
45
+ # rather than folded into `attributes` (see that field's
46
+ # own comment): a projected field's absence means
47
+ # something different from an ordinary attribute's, so it
48
+ # needs its own refusal below, not `AttributeAbsent`'s.
49
+ # `projects` is AGGREGATE-scoped only — an entity's own
50
+ # `instance.aggregate` answers the ENTITY construct here
51
+ # (EntityInterpreter's own subject), which declares no
52
+ # `projected_fields` of its own, hence the extra guard
53
+ # `@declared` above does not need.
54
+ owner = instance.aggregate if instance.respond_to?(:aggregate)
55
+ @projected = owner.respond_to?(:projected_fields) ? owner.projected_fields.to_h { |f| [f.name, f] } : {}
56
+ end
57
+
58
+ def key?(name) = @declared.key?(name.to_sym) || @projected.key?(name.to_sym) || @instance.key?(name)
59
+
60
+ def [](name)
61
+ return @instance[name] if @instance.key?(name)
62
+
63
+ projected = @projected[name.to_sym]
64
+ return raise_projection_absent(projected) if projected
65
+
66
+ attribute = @declared[name.to_sym]
67
+ return nil if attribute.nil? || attribute.optional?
68
+
69
+ raise AttributeAbsent,
70
+ RefusalWording.render("AttributeAbsent", "absent_read",
71
+ aggregate: @instance.aggregate.hecks_name, field: name)
72
+ end
73
+
74
+ private
75
+
76
+ def raise_projection_absent(projected)
77
+ raise ProjectionAbsent,
78
+ RefusalWording.render("ProjectionAbsent", "absent_read",
79
+ aggregate: @instance.aggregate.hecks_name, field: projected.name,
80
+ reference: projected.reference, remote_field: projected.remote_field)
81
+ end
82
+ end
83
+ private_constant :GuardState
84
+
85
+ # `domain:` is only needed to dereference a reference-typed field
86
+ # (`customer.status`) — see References#dereference. `owner` is the
87
+ # declaring aggregate/entity when `subject` carries one (an
88
+ # entity's pre-mutation `view` does, same as an aggregate's
89
+ # `Instance`); a `subject` with no `.aggregate` just hydrates
90
+ # nothing from state, same as GuardState degrades above.
91
+ #
92
+ # MERGE ORDER MATTERS, and it is NOT "args always win": an
93
+ # unaliased command-level reference dereferences under a
94
+ # different name than the argument holds (`account_id` the arg,
95
+ # `account` the hydrated key — no collision, order is moot). An
96
+ # ALIASED one (`reference_to Customer, as: :customer`) hydrates
97
+ # under the SAME name the argument itself holds — `customer` is
98
+ # both the raw id an arg puts there and the key `customer.status`
99
+ # expects to dig into. If `args` merged last, the raw id (a
100
+ # String) would win and `.status` on a String is where a fuzzer
101
+ # found this — TypeError, not a refusal. Command-level
102
+ # dereferencing is the one thing that is SUPPOSED to override
103
+ # its own source argument for exactly this reason; args still
104
+ # wins over stored OWNER state (unchanged from before this fix).
105
+ # `parent:` is an entity command's OWN parent aggregate record
106
+ # (EntityInterpreter's `ctx.instance` — "the PARENT aggregate
107
+ # record", its own doc comment) — the entity's containment, not a
108
+ # declared reference attribute, so it doesn't come from
109
+ # `dereference`'s attribute scan the way `owner`'s do. Hydrated
110
+ # the same shape regardless: the parent's own state, MERGED so
111
+ # the dereferenced hash wins over the raw reference it replaces
112
+ # (ADR 0025 dropped the `_id` suffix that used to keep the two
113
+ # apart by name, so `parent.state.merge(dereference(...))` is
114
+ # now load-bearing, not redundant) — plus ITS OWN references
115
+ # dereferenced one level in, so `parent.customer.status` (a
116
+ # parent aggregate reaching ITS OWN customer) resolves the same
117
+ # way `account.customer.status` does for a command-level reference.
118
+ # nil for an aggregate command — CommandInterpreter never passes it.
119
+ def enforce_givens(subject, command, args, domain:, declaring: nil, parent: nil)
120
+ state = GuardState.new(subject)
121
+ # A RULE MAY ONLY READ WITHIN ITS OWN AGGREGATE BOUNDARY (S12,
122
+ # ADR 0025) — `subject`'s own STORED references are no longer
123
+ # dereferenced here at all. What used to be a live query against
124
+ # another aggregate's own repository is now just `subject`'s own
125
+ # state: a `projects :customer_status, from: :"customer.status"`
126
+ # field is a REGULAR stored attribute, already present in
127
+ # `subject`/`state` with no hydration step needed. `dereference`
128
+ # is still called on `command`/`args`, below — that is a
129
+ # DIFFERENT case the ADR explicitly keeps in bounds ("its command
130
+ # arguments"): a reference-typed ARGUMENT this dispatch was just
131
+ # handed (`Dispute`'s own `disputed_by`, say) has nothing stored
132
+ # to project yet, so resolving it here, once, synchronously with
133
+ # THIS command's own admission, is not the live-query-against-
134
+ # another-aggregate's-stored-state pattern the boundary rule
135
+ # forbids.
136
+ attrs = args.merge(dereference(domain, command, args))
137
+ attrs = attrs.merge(parent: parent.state) if parent
138
+ command.givens.each do |given|
139
+ next if Bluebook::Expression::Evaluator.call(given.canonical, state, attrs)
140
+
141
+ raise GivenNotMet, "#{command.hecks_name} refused — #{given.description}"
142
+ end
143
+
144
+ enforce_lifecycle_guard(declaring, command, subject) if declaring
145
+ end
146
+
147
+ # `corrects` — CommandBuilder#corrects_impl's own comment. NOT
148
+ # expressible as an ordinary `given`: "has this exact record
149
+ # already emitted this exact event" is not a predicate over the
150
+ # record's OWN fields, it is a fact about the event log, so it is
151
+ # raised structurally here, the same way NotFound/AlreadyExists
152
+ # are, rather than through the expression evaluator. The build-
153
+ # time half — does ANYTHING in this aggregate ever emit the named
154
+ # event at all — is `AggregateBuilder#seal_correction_targets`;
155
+ # this is the dispatch-time half — has THIS record actually done
156
+ # so yet.
157
+ def enforce_correction_target(instance, aggregate, command, domain:)
158
+ command.mutations.each do |mutation|
159
+ next unless mutation.op == :corrects
160
+
161
+ event_key = "#{domain}::#{aggregate.hecks_name}"
162
+ event_name = mutation.target.to_s
163
+ next if @registry.event_log.any? do |event|
164
+ event.name == event_name && event.aggregate == event_key && event.id == instance.id
165
+ end
166
+
167
+ raise NothingToCorrect,
168
+ "#{command.hecks_name} refused — corrects #{event_name}, but " \
169
+ "#{event_key} ##{instance.id} has never emitted it"
170
+ end
171
+ end
172
+
173
+ # LIFECYCLE STATE AS A COMMAND GUARD (S10, ADR 0025) — `command
174
+ # "Debit", from: "open"` checked here, folded into the SAME
175
+ # dispatch step `given` already runs at (both are preconditions,
176
+ # evaluated before any mutation) rather than earning its own
177
+ # DISPATCH_ORDER entry. A GUARD, never a transition: it names no
178
+ # target state and `step_advance_lifecycle` never sees it — see
179
+ # `admissible_transition`, right below, for the transition this
180
+ # is deliberately NOT reusing (its own `StateTransition#target`
181
+ # is required, and a guard-only command has none to give it).
182
+ def enforce_lifecycle_guard(declaring, command, subject)
183
+ return unless command.from
184
+
185
+ lifecycle = declaring.lifecycle
186
+ current = Value.scalar(subject[lifecycle.field]).to_s
187
+ return if Array(command.from).include?(current)
188
+
189
+ # ROUTED THROUGH RefusalWording's OWN "transition_blocked"
190
+ # TEMPLATE — the same one #admissible_transition, right below,
191
+ # already raises LifecycleRefused through for the same
192
+ # refusal class. This used to hand-roll its own wording
193
+ # inline ("...only runs from..." vs. the template's "...moves
194
+ # it only from...") — two shapes for one refusal kind, so
195
+ # anything string-matching a LifecycleRefused message (a
196
+ # property, a spec, a caller) had to know both existed rather
197
+ # than one.
198
+ raise LifecycleRefused,
199
+ RefusalWording.render("LifecycleRefused", "transition_blocked",
200
+ command: command.hecks_name, field: lifecycle.field,
201
+ current: Rendering.describe(current),
202
+ allowed: Array(command.from).map(&:inspect).join(" or "))
203
+ end
204
+
205
+ # The far side of the contract: evaluated against the SETTLED record
206
+ # — after mutations and the lifecycle move, before anything persists
207
+ # — with `old` carrying the state as the givens saw it. Injected into
208
+ # the attrs at evaluation time only; the payload gate never sees it.
209
+ #
210
+ # `old` — and every dispatch ARGUMENT — wins over a same-named STATE
211
+ # field in expression scope (Resolver#fetch checks attrs first). An
212
+ # ensures naming a field the command also takes as an argument (or,
213
+ # on an entity, a field that doubles as the addressing argument
214
+ # element_of reads) will read the ARGUMENT, not the settled value.
215
+ # Not new to ensures — `given` lives under the same rule — but an
216
+ # ensures is more likely to collide, since it typically re-reads a
217
+ # field the command just took in to mutate it.
218
+ def enforce_ensures(subject, command, args, old:, domain:, parent: nil)
219
+ state = GuardState.new(subject)
220
+ # S12, ADR 0025 — same boundary reasoning as enforce_givens
221
+ # above: `subject`'s own stored references are no longer
222
+ # dereferenced here; a `projects`-maintained field is already
223
+ # part of `state`. `command`/`args` still dereferences — a
224
+ # fresh reference-typed ARGUMENT stays in bounds.
225
+ # `old` still wins over everything, unchanged.
226
+ attrs = args.merge(dereference(domain, command, args))
227
+ attrs = attrs.merge(parent: parent.state) if parent
228
+ attrs = attrs.merge(old: old)
229
+ command.ensures.each do |rule|
230
+ next if Bluebook::Expression::Evaluator.call(rule.canonical, state, attrs)
231
+
232
+ raise EnsuresNotMet, "#{command.hecks_name} refused — #{rule.description}"
233
+ end
234
+ end
235
+
236
+ # THE AGGREGATE BOUNDARY, checked after every command, before
237
+ # save (S10, ADR 0025 — "Rules") — the same point `enforce_
238
+ # ensures` already checks at, and for the same reason: an
239
+ # invariant is a claim about the SETTLED record, not the
240
+ # command that produced it, so it reads no `args`/`old` at all,
241
+ # only the record's own state. `subject` here is
242
+ # always the AGGREGATE's own instance — `CommandInterpreter`
243
+ # passes its own `ctx.instance`, and `EntityInterpreter` passes
244
+ # the PARENT record (`ctx.instance`, not the element), since an
245
+ # entity mutation changes data inside the SAME aggregate
246
+ # boundary the invariant guards; there is no separate "entity
247
+ # invariant" to check the piece's own view against.
248
+ #
249
+ # NO `dereference` (S12, ADR 0025) — an invariant may only read
250
+ # `subject`'s own boundary, same rule `enforce_givens`/
251
+ # `enforce_ensures` now hold to. No invariant in the corpus has
252
+ # ever read across a `reference_to` (verified before this
253
+ # change), so this is not a migration, just closing the same
254
+ # capability off here that was already unused.
255
+ def enforce_invariants(subject, aggregate, domain:)
256
+ state = GuardState.new(subject)
257
+ attrs = {}
258
+ aggregate.invariants.each do |invariant|
259
+ next if Bluebook::Expression::Evaluator.call(invariant.canonical, state, attrs)
260
+
261
+ raise InvariantViolation, "#{aggregate.hecks_name} refused — #{invariant.description}"
262
+ end
263
+
264
+ check_entity_invariants(aggregate, subject, domain: domain)
265
+ end
266
+
267
+ # A PIECE'S OWN SHAPE RULE, checked against EVERY INSTANCE the
268
+ # aggregate holds — not a separate boundary from the aggregate's
269
+ # own invariants just above (same two checkpoints: after every
270
+ # mutation, before save), just a WIDER one: the aggregate's own
271
+ # consistency includes each of its pieces individually looking
272
+ # right, the same way `ValueObject#invariants` already checks
273
+ # each of ITS OWN instances one construct up. RECURSES into
274
+ # nested pieces (S17, ADR 0026 — Dispatch inside Handler) the
275
+ # same way `check_entity_invariants`'s own caller recurses
276
+ # nowhere else needs to, since a piece's `entities` are already
277
+ # exactly as reachable as an aggregate's.
278
+ #
279
+ # `list_attr` reuses the EXACT lookup `EntityInterpreter#
280
+ # element_of` already makes to locate a SINGLE addressed
281
+ # element by identity — this reads every element instead, but
282
+ # the "which field on the owner holds this piece's own
283
+ # instances" question is the identical one. A piece declaring
284
+ # invariants that nothing on its owner actually holds (no
285
+ # matching list attribute) is a static-analysis gap for a
286
+ # future gate, not a runtime concern here — `next` past it
287
+ # rather than raising mid-enforcement for an unrelated command.
288
+ def check_entity_invariants(owner_construct, owner_instance, domain:)
289
+ owner_construct.entities.each do |entity|
290
+ next if entity.invariants.empty?
291
+
292
+ list_attr = owner_construct.attributes.find { |a| a.list? && a.type.to_s == entity.hecks_name }
293
+ next unless list_attr
294
+
295
+ Array(owner_instance[list_attr.name]).each do |element|
296
+ wrapped = Instance.new(aggregate: entity, id: nil, state: element)
297
+ element_state = GuardState.new(wrapped)
298
+ # NO `dereference` (S12, ADR 0025) — same boundary rule as
299
+ # enforce_invariants above; `parent` (the owner's own
300
+ # state, projected fields included) stays readable.
301
+ attrs = { parent: owner_instance.state }
302
+
303
+ entity.invariants.each do |invariant|
304
+ next if Bluebook::Expression::Evaluator.call(invariant.canonical, element_state, attrs)
305
+
306
+ raise InvariantViolation, "#{entity.hecks_name} refused — #{invariant.description}"
307
+ end
308
+
309
+ check_entity_invariants(entity, wrapped, domain: domain)
310
+ end
311
+ end
312
+ end
313
+
314
+ def admissible_transition(declaring, command, subject)
315
+ lifecycle = declaring.lifecycle
316
+ return nil unless lifecycle
317
+
318
+ candidates = lifecycle.transitions_for(command.hecks_name)
319
+ return nil if candidates.empty?
320
+
321
+ # `Value.scalar` unwrap -- vendored addition, not (yet)
322
+ # upstream hecks (migration plan task 9): a VO-typed
323
+ # lifecycle field (the norm, not the exception, per this
324
+ # corpus's own no-primitive-envy convention) holds a real
325
+ # `Runtime::Value` here, and a bare `.to_s` on that hit Ruby's
326
+ # default `Object#to_s` instead of unwrapping the inner
327
+ # scalar first -- `current` came back as a raw object-pointer
328
+ # string (`"#<Hecks::Runtime::Value:0x...>"`) that could
329
+ # never match any declared `from` state, so EVERY transition
330
+ # on a VO-typed lifecycle field refused unconditionally, and
331
+ # when it refused the message leaked the pointer too.
332
+ # Confirmed live via `Plan::Task.Complete` (status defaults to
333
+ # `TaskStatus`, a single-field VO), not inferred. Reuses
334
+ # `Value.scalar` -- this file's own third candidate for "how
335
+ # to unwrap a Value/Hash-shaped field," already built and
336
+ # already documented for exactly this job ("rendering a value
337
+ # object into a column or a message, where there is no path
338
+ # to consult," `value/coercion.rb`'s own comment) -- rather
339
+ # than inventing a second unwrap helper beside `Resolver#
340
+ # unwrap_scalar`'s bare-comparison one. Duck-typed the same
341
+ # way : a bare, non-VO lifecycle field passes through
342
+ # unchanged (`Value.scalar` only opens a `Value` instance).
343
+ current = Value.scalar(subject[lifecycle.field]).to_s
344
+ admitted = candidates.find { |t| !t.constrained? || Array(t.from).include?(current) }
345
+ return admitted if admitted
346
+
347
+ allowed = candidates.flat_map { |t| Array(t.from) }.uniq
348
+ raise LifecycleRefused,
349
+ RefusalWording.render("LifecycleRefused", "transition_blocked",
350
+ command: command.hecks_name, field: lifecycle.field,
351
+ current: Rendering.describe(current),
352
+ allowed: allowed.map(&:inspect).join(" or "))
353
+ end
354
+ end
355
+ end
356
+ end
357
+ end
@@ -0,0 +1,260 @@
1
+ require_relative "../../rendering"
2
+ require_relative "../errors"
3
+ require_relative "../refusal_wording"
4
+ require_relative "../value"
5
+
6
+ module Hecks
7
+ module Runtime
8
+ class CommandRules
9
+ # The arithmetic half of mutation: what a source resolves to, and how
10
+ # increment/decrement land on an Integer or a one-numeric-field value
11
+ # object.
12
+ module Arithmetic
13
+ # The ops Runtime::CommandInterpreter applies. Declared the same way in
14
+ # Vocabulary::MutationOp (language/bluebook/vocabulary.bluebook) —
15
+ # spec/vocabulary_conformance_spec holds the two tables equal, so
16
+ # increment/decrement's sign cannot drift from what the language says
17
+ # each op means. set/append carry no sign — they do no arithmetic.
18
+ MutationOp = Struct.new(:name, :sign, keyword_init: true)
19
+
20
+ MUTATION_OPS = [
21
+ MutationOp.new(name: "set", sign: nil),
22
+ MutationOp.new(name: "append", sign: nil),
23
+ MutationOp.new(name: "increment", sign: 1),
24
+ MutationOp.new(name: "decrement", sign: -1),
25
+ # Vendored addition, not (yet) upstream hecks (migration
26
+ # plan task 4, i106): multiply/clamp carry no sign -- like
27
+ # set/append, they do no add-or-subtract arithmetic (multiply
28
+ # scales, clamp bounds). See #multiply/#clamp below.
29
+ MutationOp.new(name: "multiply", sign: nil),
30
+ MutationOp.new(name: "clamp", sign: nil),
31
+ # Vendored addition, not (yet) upstream hecks (migration
32
+ # plan task 4): remove -- carries no sign, like set/append; it
33
+ # matches a list element by value rather than doing arithmetic.
34
+ # Declared here so this table stays exactly what
35
+ # Vocabulary::MutationOp declares (spec/vocabulary_conformance_spec
36
+ # holds the two equal) -- MutationApplier's own `when :remove`
37
+ # branch (mutation_applier.rb) never calls #sign_of, so this was
38
+ # a declared-vocabulary gap, not a behaviour gap.
39
+ MutationOp.new(name: "remove", sign: nil),
40
+ # Vendored addition, not (yet) upstream hecks —
41
+ # CommandBuilder#delegates_to's own comment gives the full
42
+ # reasoning; carries no sign, like set/append/remove — it does
43
+ # no arithmetic, only a synchronous handoff into one nested
44
+ # entity command. Declared here so this table stays exactly
45
+ # what Vocabulary::MutationOp declares — MutationApplier's own
46
+ # `when :delegate` branch (mutation_applier.rb) never calls
47
+ # #sign_of either, same as `remove`'s own note above.
48
+ MutationOp.new(name: "delegate", sign: nil),
49
+ # CommandBuilder#corrects_impl's own comment gives the full
50
+ # reasoning — a command amending a past event rather than
51
+ # acting fresh. Carries no sign, like delegate: it does no
52
+ # arithmetic of its own; the record's actual change, if any,
53
+ # is an ordinary `sets` declared alongside it.
54
+ MutationOp.new(name: "corrects", sign: nil)
55
+ ].freeze
56
+
57
+ # A mutation's source is either the NAME OF AN ARGUMENT or a LITERAL, and
58
+ # the two are told apart by type : a Symbol is always a name, a String or a
59
+ # number is always a value. Checked across all eight chapters — `to: :name`
60
+ # and `to: "sold"`, never a Symbol meant as a value.
61
+ #
62
+ # `&& args.key?(source)` used to guard the lookup, and that guard is what
63
+ # made an ABSENT argument fall through to `source` and return THE SYMBOL
64
+ # ITSELF as the value. `Customer.Register` without its `name` set name to
65
+ # the literal `:name`, coercion met a Symbol where a PersonName belonged,
66
+ # and the refusal read "name is a PersonName — pass its fields as an
67
+ # object, not :name" — a message describing a mistake the caller had not
68
+ # made. The real mistake, an absent argument, was never the one refused,
69
+ # which is what fuzz surfaced.
70
+ #
71
+ # Absent now resolves to nil. Whether it should be REFUSED instead
72
+ # is a separate question — the language cannot yet say which arguments are
73
+ # optional, and the meta-domain has plenty that are.
74
+ def resolve_source(source, args)
75
+ return args[source] if source.is_a?(Symbol)
76
+
77
+ source
78
+ end
79
+
80
+ def arithmetic(current, amount, target, sign)
81
+ op = sign.positive? ? "increment" : "decrement"
82
+ current ||= 0
83
+
84
+ if current.is_a?(Value) && amount.is_a?(Value)
85
+ return arithmetic_value_object(current, amount, target, sign, op)
86
+ end
87
+
88
+ # `current` genuinely absent (no declared default, never set) and
89
+ # `amount` arrives VO-wrapped — a real command argument typed the
90
+ # same as the attribute, but with nothing to combine field-by-
91
+ # field against yet (that is what `arithmetic_value_object`,
92
+ # above, is for once BOTH sides carry real fields). Before this,
93
+ # falling straight to `unless amount.is_a?(Numeric)` below
94
+ # refused with "increment needs an Integer, got 500" — true of
95
+ # nothing: 500 is exactly the Integer it asked for, just still
96
+ # wearing the Money wrapper the command's own declared attribute
97
+ # type put it in. Unwrapped here, the same shape #clamp already
98
+ # falls through to for an absent VO-typed attribute
99
+ # (`current ||= 0`, then a raw scalar) — the mutation applier
100
+ # re-wraps the raw result into the declared VO type on write,
101
+ # the same way it already does for clamp's own result.
102
+ amount = unwrap_single_numeric_field(amount) if amount.is_a?(Value)
103
+
104
+ # Widened from Integer to Numeric (migration plan task 4, i106):
105
+ # miette's organ math increments a Float (`increment: 0.02`) --
106
+ # the raw, non-value-object path only ever mattered for Integer
107
+ # counters before this corpus existed. Integer stays the common
108
+ # case; Float is now accepted the same way.
109
+ unless amount.is_a?(Numeric)
110
+ raise TypeMismatch, RefusalWording.render("TypeMismatch", "arithmetic_amount",
111
+ op: op, target: target, offered: Rendering.describe(amount))
112
+ end
113
+ unless current.is_a?(Numeric)
114
+ raise TypeMismatch, RefusalWording.render("TypeMismatch", "arithmetic_current",
115
+ op: op, target: target, offered: Rendering.describe(current))
116
+ end
117
+
118
+ current + (sign * amount)
119
+ end
120
+
121
+ def arithmetic_value_object(current, amount, target, sign, op)
122
+ current_fields = current.to_h
123
+ amount_fields = amount.to_h
124
+ # Widened from Integer to Numeric -- see #arithmetic's own
125
+ # comment. A synthesised value-object wrapper around a bare
126
+ # Float attribute (miette's Synapse#strength, auto-wrapped per
127
+ # Part 3a's "bare primitives forbidden" finding) lands here as
128
+ # a one-Float-field Value exactly the way a one-Integer-field
129
+ # Value already did.
130
+ shared_numeric = current_fields.keys.select do |field|
131
+ current_fields[field].is_a?(Numeric) && amount_fields[field].is_a?(Numeric)
132
+ end
133
+ unless shared_numeric.size == 1
134
+ raise TypeMismatch,
135
+ RefusalWording.render("TypeMismatch", "arithmetic_shared_field", op: op, target: target)
136
+ end
137
+
138
+ field = shared_numeric.first
139
+ current.with(field, current[field] + (sign * amount[field]))
140
+ end
141
+
142
+ # Not a bare `.find(...)&.sign || -1` — that silently answered
143
+ # DECREMENT'S sign for BOTH an op this table has never heard of
144
+ # AND a declared, real op that simply carries no sign at all
145
+ # (set/append/multiply/clamp/remove — see MUTATION_OPS above).
146
+ # Callers today only ever reach this for :increment/:decrement
147
+ # (both MutationApplier#apply and EntityInterpreter#
148
+ # apply_to_element gate every other op through their own `case`
149
+ # first, each with its own loud WiringError backstop), so this
150
+ # raise is not a real runtime path yet — it is the same
151
+ # backstop one level down, in case a future caller reaches
152
+ # #sign_of directly for an op that was never meant to have one.
153
+ def sign_of(op)
154
+ MUTATION_OPS.find { |candidate| candidate.name == op.to_s }&.sign ||
155
+ raise(WiringError, "no sign declared for mutation op #{op.inspect} — add one before calling #sign_of")
156
+ end
157
+
158
+ # Vendored addition, not (yet) upstream hecks (migration plan
159
+ # task 4, i106): `current * amount` -- the scaling counterpart to
160
+ # increment/decrement's add/subtract. Same raw-vs-value-object
161
+ # branch shape as #arithmetic/#arithmetic_value_object, reused
162
+ # rather than duplicated verb-for-verb (a `Proc` picks the actual
163
+ # arithmetic; everything else -- the Value unwrap/rewrap, the
164
+ # TypeMismatch refusals -- is identical to the additive pair).
165
+ def multiply(current, amount, target)
166
+ current ||= 0
167
+
168
+ if current.is_a?(Value) && amount.is_a?(Value)
169
+ return combine_value_object(current, amount, target, "multiply") { |c, a| c * a }
170
+ end
171
+
172
+ # Same absent-`current`, VO-wrapped-`amount` gap as `#arithmetic`
173
+ # — see that method's own comment.
174
+ amount = unwrap_single_numeric_field(amount) if amount.is_a?(Value)
175
+
176
+ unless amount.is_a?(Numeric) && current.is_a?(Numeric)
177
+ raise TypeMismatch, RefusalWording.render("TypeMismatch", "arithmetic_amount",
178
+ op: "multiply", target: target,
179
+ offered: Rendering.describe(current.is_a?(Numeric) ? amount : current))
180
+ end
181
+
182
+ current * amount
183
+ end
184
+
185
+ # Vendored addition, not (yet) upstream hecks (migration plan
186
+ # task 4, i106): bound the CURRENT value into `[min, max]` -- no
187
+ # "amount" to combine, so it does not go through
188
+ # #arithmetic/#multiply's shared-numeric-field matching at all;
189
+ # it clamps whichever single numeric field the wrapping value
190
+ # object carries (a synthesised wrapper always carries exactly
191
+ # one, per Part 3a's auto-synthesis).
192
+ def clamp(current, bounds, target)
193
+ min, max = bounds
194
+ # THE SAME `current ||= 0` #arithmetic/#multiply both give a
195
+ # PHANTOM (never-set) numeric field, one line up from each —
196
+ # this was the one arithmetic op that didn't, so a VO-typed
197
+ # attribute with no declared `default:` (genuinely absent,
198
+ # `Instance.defaults`/`#default_for`) hit TypeMismatch on the
199
+ # FIRST clamp. (#arithmetic/#multiply's OWN absent-current gap
200
+ # was a real, separate bug this comment used to describe wrong —
201
+ # they did not "silently treat the same absent field as zero";
202
+ # they raised too, blaming a perfectly valid `amount` for not
203
+ # being an Integer when it was one, just still Money-wrapped.
204
+ # Fixed alongside this one — see #unwrap_single_numeric_field.)
205
+ current ||= 0
206
+ if current.is_a?(Value)
207
+ fields = current.to_h
208
+ field = fields.keys.find { |f| fields[f].is_a?(Numeric) } or
209
+ raise TypeMismatch, RefusalWording.render("TypeMismatch", "arithmetic_current",
210
+ op: "clamp", target: target, offered: Rendering.describe(current))
211
+ return current.with(field, fields[field].clamp(min, max))
212
+ end
213
+
214
+ unless current.is_a?(Numeric)
215
+ raise TypeMismatch, RefusalWording.render("TypeMismatch", "arithmetic_current",
216
+ op: "clamp", target: target, offered: Rendering.describe(current))
217
+ end
218
+
219
+ current.clamp(min, max)
220
+ end
221
+
222
+ private
223
+
224
+ # `amount` arrives VO-wrapped whenever the command's own declared
225
+ # attribute type says so (a real `Money`, not a bare Integer) —
226
+ # true whether or not `current` has ever been set. Only meaningful
227
+ # to call once `current` is known NOT to be a Value itself (the
228
+ # `current.is_a?(Value) && amount.is_a?(Value)` branch, above in
229
+ # both callers, already owns the case where both sides carry real
230
+ # fields to combine). Refuses rather than guesses when more than
231
+ # one field is numeric — genuinely ambiguous which one an absent
232
+ # `current` should be treated as zero for, the same reasoning
233
+ # `combine_value_object`'s own `shared_numeric.size == 1` check
234
+ # already holds to when both sides ARE present.
235
+ def unwrap_single_numeric_field(value)
236
+ fields = value.to_h
237
+ numeric_fields = fields.keys.select { |field| fields[field].is_a?(Numeric) }
238
+ return value unless numeric_fields.size == 1
239
+
240
+ fields[numeric_fields.first]
241
+ end
242
+
243
+ def combine_value_object(current, amount, target, op)
244
+ current_fields = current.to_h
245
+ amount_fields = amount.to_h
246
+ shared_numeric = current_fields.keys.select do |field|
247
+ current_fields[field].is_a?(Numeric) && amount_fields[field].is_a?(Numeric)
248
+ end
249
+ unless shared_numeric.size == 1
250
+ raise TypeMismatch,
251
+ RefusalWording.render("TypeMismatch", "arithmetic_shared_field", op: op, target: target)
252
+ end
253
+
254
+ field = shared_numeric.first
255
+ current.with(field, yield(current[field], amount[field]))
256
+ end
257
+ end
258
+ end
259
+ end
260
+ end