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,307 @@
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
+ owner = subject.aggregate if subject.respond_to?(:aggregate)
122
+ attrs = dereference(domain, owner, subject).merge(args).merge(dereference(domain, command, args))
123
+ attrs = attrs.merge(parent: parent.state.merge(dereference(domain, parent.aggregate, parent.state))) if parent
124
+ command.givens.each do |given|
125
+ next if Bluebook::Expression::Evaluator.call(given.canonical, state, attrs)
126
+
127
+ raise GivenNotMet, "#{command.hecks_name} refused — #{given.description}"
128
+ end
129
+
130
+ enforce_lifecycle_guard(declaring, command, subject) if declaring
131
+ end
132
+
133
+ # LIFECYCLE STATE AS A COMMAND GUARD (S10, ADR 0025) — `command
134
+ # "Debit", from: "open"` checked here, folded into the SAME
135
+ # dispatch step `given` already runs at (both are preconditions,
136
+ # evaluated before any mutation) rather than earning its own
137
+ # DISPATCH_ORDER entry. A GUARD, never a transition: it names no
138
+ # target state and `step_advance_lifecycle` never sees it — see
139
+ # `admissible_transition`, right below, for the transition this
140
+ # is deliberately NOT reusing (its own `StateTransition#target`
141
+ # is required, and a guard-only command has none to give it).
142
+ def enforce_lifecycle_guard(declaring, command, subject)
143
+ return unless command.from
144
+
145
+ lifecycle = declaring.lifecycle
146
+ current = Value.scalar(subject[lifecycle.field]).to_s
147
+ return if Array(command.from).include?(current)
148
+
149
+ # ROUTED THROUGH RefusalWording's OWN "transition_blocked"
150
+ # TEMPLATE — the same one #admissible_transition, right below,
151
+ # already raises LifecycleRefused through for the same
152
+ # refusal class. This used to hand-roll its own wording
153
+ # inline ("...only runs from..." vs. the template's "...moves
154
+ # it only from...") — two shapes for one refusal kind, so
155
+ # anything string-matching a LifecycleRefused message (a
156
+ # property, a spec, a caller) had to know both existed rather
157
+ # than one.
158
+ raise LifecycleRefused,
159
+ RefusalWording.render("LifecycleRefused", "transition_blocked",
160
+ command: command.hecks_name, field: lifecycle.field,
161
+ current: Rendering.describe(current),
162
+ allowed: Array(command.from).map(&:inspect).join(" or "))
163
+ end
164
+
165
+ # The far side of the contract: evaluated against the SETTLED record
166
+ # — after mutations and the lifecycle move, before anything persists
167
+ # — with `old` carrying the state as the givens saw it. Injected into
168
+ # the attrs at evaluation time only; the payload gate never sees it.
169
+ #
170
+ # `old` — and every dispatch ARGUMENT — wins over a same-named STATE
171
+ # field in expression scope (Resolver#fetch checks attrs first). An
172
+ # ensures naming a field the command also takes as an argument (or,
173
+ # on an entity, a field that doubles as the addressing argument
174
+ # element_of reads) will read the ARGUMENT, not the settled value.
175
+ # Not new to ensures — `given` lives under the same rule — but an
176
+ # ensures is more likely to collide, since it typically re-reads a
177
+ # field the command just took in to mutate it.
178
+ def enforce_ensures(subject, command, args, old:, domain:, parent: nil)
179
+ state = GuardState.new(subject)
180
+ owner = subject.aggregate if subject.respond_to?(:aggregate)
181
+ # Same merge-order reasoning as enforce_givens above: an
182
+ # aliased command-level reference must override its own raw
183
+ # id argument, not the other way round. `old` still wins over
184
+ # everything, unchanged.
185
+ attrs = dereference(domain, owner, subject).merge(args).merge(dereference(domain, command, args))
186
+ attrs = attrs.merge(parent: parent.state.merge(dereference(domain, parent.aggregate, parent.state))) if parent
187
+ attrs = attrs.merge(old: old)
188
+ command.ensures.each do |rule|
189
+ next if Bluebook::Expression::Evaluator.call(rule.canonical, state, attrs)
190
+
191
+ raise EnsuresNotMet, "#{command.hecks_name} refused — #{rule.description}"
192
+ end
193
+ end
194
+
195
+ # THE AGGREGATE BOUNDARY, checked after every command, before
196
+ # save (S10, ADR 0025 — "Rules") — the same point `enforce_
197
+ # ensures` already checks at, and for the same reason: an
198
+ # invariant is a claim about the SETTLED record, not the
199
+ # command that produced it, so it reads no `args`/`old` at all,
200
+ # only the record's own (dereferenced) state. `subject` here is
201
+ # always the AGGREGATE's own instance — `CommandInterpreter`
202
+ # passes its own `ctx.instance`, and `EntityInterpreter` passes
203
+ # the PARENT record (`ctx.instance`, not the element), since an
204
+ # entity mutation changes data inside the SAME aggregate
205
+ # boundary the invariant guards; there is no separate "entity
206
+ # invariant" to check the piece's own view against.
207
+ def enforce_invariants(subject, aggregate, domain:)
208
+ state = GuardState.new(subject)
209
+ attrs = dereference(domain, aggregate, subject)
210
+ aggregate.invariants.each do |invariant|
211
+ next if Bluebook::Expression::Evaluator.call(invariant.canonical, state, attrs)
212
+
213
+ raise InvariantViolation, "#{aggregate.hecks_name} refused — #{invariant.description}"
214
+ end
215
+
216
+ check_entity_invariants(aggregate, subject, domain: domain)
217
+ end
218
+
219
+ # A PIECE'S OWN SHAPE RULE, checked against EVERY INSTANCE the
220
+ # aggregate holds — not a separate boundary from the aggregate's
221
+ # own invariants just above (same two checkpoints: after every
222
+ # mutation, before save), just a WIDER one: the aggregate's own
223
+ # consistency includes each of its pieces individually looking
224
+ # right, the same way `ValueObject#invariants` already checks
225
+ # each of ITS OWN instances one construct up. RECURSES into
226
+ # nested pieces (S17, ADR 0026 — Dispatch inside Handler) the
227
+ # same way `check_entity_invariants`'s own caller recurses
228
+ # nowhere else needs to, since a piece's `entities` are already
229
+ # exactly as reachable as an aggregate's.
230
+ #
231
+ # `list_attr` reuses the EXACT lookup `EntityInterpreter#
232
+ # element_of` already makes to locate a SINGLE addressed
233
+ # element by identity — this reads every element instead, but
234
+ # the "which field on the owner holds this piece's own
235
+ # instances" question is the identical one. A piece declaring
236
+ # invariants that nothing on its owner actually holds (no
237
+ # matching list attribute) is a static-analysis gap for a
238
+ # future gate, not a runtime concern here — `next` past it
239
+ # rather than raising mid-enforcement for an unrelated command.
240
+ def check_entity_invariants(owner_construct, owner_instance, domain:)
241
+ owner_construct.entities.each do |entity|
242
+ next if entity.invariants.empty?
243
+
244
+ list_attr = owner_construct.attributes.find { |a| a.list? && a.type.to_s == entity.hecks_name }
245
+ next unless list_attr
246
+
247
+ Array(owner_instance[list_attr.name]).each do |element|
248
+ wrapped = Instance.new(aggregate: entity, id: nil, state: element)
249
+ element_state = GuardState.new(wrapped)
250
+ attrs = dereference(domain, entity, wrapped)
251
+ .merge(parent: owner_instance.state.merge(dereference(domain, owner_construct, owner_instance)))
252
+
253
+ entity.invariants.each do |invariant|
254
+ next if Bluebook::Expression::Evaluator.call(invariant.canonical, element_state, attrs)
255
+
256
+ raise InvariantViolation, "#{entity.hecks_name} refused — #{invariant.description}"
257
+ end
258
+
259
+ check_entity_invariants(entity, wrapped, domain: domain)
260
+ end
261
+ end
262
+ end
263
+
264
+ def admissible_transition(declaring, command, subject)
265
+ lifecycle = declaring.lifecycle
266
+ return nil unless lifecycle
267
+
268
+ candidates = lifecycle.transitions_for(command.hecks_name)
269
+ return nil if candidates.empty?
270
+
271
+ # `Value.scalar` unwrap -- vendored addition, not (yet)
272
+ # upstream hecks (migration plan task 9): a VO-typed
273
+ # lifecycle field (the norm, not the exception, per this
274
+ # corpus's own no-primitive-envy convention) holds a real
275
+ # `Runtime::Value` here, and a bare `.to_s` on that hit Ruby's
276
+ # default `Object#to_s` instead of unwrapping the inner
277
+ # scalar first -- `current` came back as a raw object-pointer
278
+ # string (`"#<Hecks::Runtime::Value:0x...>"`) that could
279
+ # never match any declared `from` state, so EVERY transition
280
+ # on a VO-typed lifecycle field refused unconditionally, and
281
+ # when it refused the message leaked the pointer too.
282
+ # Confirmed live via `Plan::Task.Complete` (status defaults to
283
+ # `TaskStatus`, a single-field VO), not inferred. Reuses
284
+ # `Value.scalar` -- this file's own third candidate for "how
285
+ # to unwrap a Value/Hash-shaped field," already built and
286
+ # already documented for exactly this job ("rendering a value
287
+ # object into a column or a message, where there is no path
288
+ # to consult," `value/coercion.rb`'s own comment) -- rather
289
+ # than inventing a second unwrap helper beside `Resolver#
290
+ # unwrap_scalar`'s bare-comparison one. Duck-typed the same
291
+ # way : a bare, non-VO lifecycle field passes through
292
+ # unchanged (`Value.scalar` only opens a `Value` instance).
293
+ current = Value.scalar(subject[lifecycle.field]).to_s
294
+ admitted = candidates.find { |t| !t.constrained? || Array(t.from).include?(current) }
295
+ return admitted if admitted
296
+
297
+ allowed = candidates.flat_map { |t| Array(t.from) }.uniq
298
+ raise LifecycleRefused,
299
+ RefusalWording.render("LifecycleRefused", "transition_blocked",
300
+ command: command.hecks_name, field: lifecycle.field,
301
+ current: Rendering.describe(current),
302
+ allowed: allowed.map(&:inspect).join(" or "))
303
+ end
304
+ end
305
+ end
306
+ end
307
+ end
@@ -0,0 +1,254 @@
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
+ ].freeze
50
+
51
+ # A mutation's source is either the NAME OF AN ARGUMENT or a LITERAL, and
52
+ # the two are told apart by type : a Symbol is always a name, a String or a
53
+ # number is always a value. Checked across all eight chapters — `to: :name`
54
+ # and `to: "sold"`, never a Symbol meant as a value.
55
+ #
56
+ # `&& args.key?(source)` used to guard the lookup, and that guard is what
57
+ # made an ABSENT argument fall through to `source` and return THE SYMBOL
58
+ # ITSELF as the value. `Customer.Register` without its `name` set name to
59
+ # the literal `:name`, coercion met a Symbol where a PersonName belonged,
60
+ # and the refusal read "name is a PersonName — pass its fields as an
61
+ # object, not :name" — a message describing a mistake the caller had not
62
+ # made. The real mistake, an absent argument, was never the one refused,
63
+ # which is what fuzz surfaced.
64
+ #
65
+ # Absent now resolves to nil. Whether it should be REFUSED instead
66
+ # is a separate question — the language cannot yet say which arguments are
67
+ # optional, and the meta-domain has plenty that are.
68
+ def resolve_source(source, args)
69
+ return args[source] if source.is_a?(Symbol)
70
+
71
+ source
72
+ end
73
+
74
+ def arithmetic(current, amount, target, sign)
75
+ op = sign.positive? ? "increment" : "decrement"
76
+ current ||= 0
77
+
78
+ if current.is_a?(Value) && amount.is_a?(Value)
79
+ return arithmetic_value_object(current, amount, target, sign, op)
80
+ end
81
+
82
+ # `current` genuinely absent (no declared default, never set) and
83
+ # `amount` arrives VO-wrapped — a real command argument typed the
84
+ # same as the attribute, but with nothing to combine field-by-
85
+ # field against yet (that is what `arithmetic_value_object`,
86
+ # above, is for once BOTH sides carry real fields). Before this,
87
+ # falling straight to `unless amount.is_a?(Numeric)` below
88
+ # refused with "increment needs an Integer, got 500" — true of
89
+ # nothing: 500 is exactly the Integer it asked for, just still
90
+ # wearing the Money wrapper the command's own declared attribute
91
+ # type put it in. Unwrapped here, the same shape #clamp already
92
+ # falls through to for an absent VO-typed attribute
93
+ # (`current ||= 0`, then a raw scalar) — the mutation applier
94
+ # re-wraps the raw result into the declared VO type on write,
95
+ # the same way it already does for clamp's own result.
96
+ amount = unwrap_single_numeric_field(amount) if amount.is_a?(Value)
97
+
98
+ # Widened from Integer to Numeric (migration plan task 4, i106):
99
+ # miette's organ math increments a Float (`increment: 0.02`) --
100
+ # the raw, non-value-object path only ever mattered for Integer
101
+ # counters before this corpus existed. Integer stays the common
102
+ # case; Float is now accepted the same way.
103
+ unless amount.is_a?(Numeric)
104
+ raise TypeMismatch, RefusalWording.render("TypeMismatch", "arithmetic_amount",
105
+ op: op, target: target, offered: Rendering.describe(amount))
106
+ end
107
+ unless current.is_a?(Numeric)
108
+ raise TypeMismatch, RefusalWording.render("TypeMismatch", "arithmetic_current",
109
+ op: op, target: target, offered: Rendering.describe(current))
110
+ end
111
+
112
+ current + (sign * amount)
113
+ end
114
+
115
+ def arithmetic_value_object(current, amount, target, sign, op)
116
+ current_fields = current.to_h
117
+ amount_fields = amount.to_h
118
+ # Widened from Integer to Numeric -- see #arithmetic's own
119
+ # comment. A synthesised value-object wrapper around a bare
120
+ # Float attribute (miette's Synapse#strength, auto-wrapped per
121
+ # Part 3a's "bare primitives forbidden" finding) lands here as
122
+ # a one-Float-field Value exactly the way a one-Integer-field
123
+ # Value already did.
124
+ shared_numeric = current_fields.keys.select do |field|
125
+ current_fields[field].is_a?(Numeric) && amount_fields[field].is_a?(Numeric)
126
+ end
127
+ unless shared_numeric.size == 1
128
+ raise TypeMismatch,
129
+ RefusalWording.render("TypeMismatch", "arithmetic_shared_field", op: op, target: target)
130
+ end
131
+
132
+ field = shared_numeric.first
133
+ current.with(field, current[field] + (sign * amount[field]))
134
+ end
135
+
136
+ # Not a bare `.find(...)&.sign || -1` — that silently answered
137
+ # DECREMENT'S sign for BOTH an op this table has never heard of
138
+ # AND a declared, real op that simply carries no sign at all
139
+ # (set/append/multiply/clamp/remove — see MUTATION_OPS above).
140
+ # Callers today only ever reach this for :increment/:decrement
141
+ # (both MutationApplier#apply and EntityInterpreter#
142
+ # apply_to_element gate every other op through their own `case`
143
+ # first, each with its own loud WiringError backstop), so this
144
+ # raise is not a real runtime path yet — it is the same
145
+ # backstop one level down, in case a future caller reaches
146
+ # #sign_of directly for an op that was never meant to have one.
147
+ def sign_of(op)
148
+ MUTATION_OPS.find { |candidate| candidate.name == op.to_s }&.sign ||
149
+ raise(WiringError, "no sign declared for mutation op #{op.inspect} — add one before calling #sign_of")
150
+ end
151
+
152
+ # Vendored addition, not (yet) upstream hecks (migration plan
153
+ # task 4, i106): `current * amount` -- the scaling counterpart to
154
+ # increment/decrement's add/subtract. Same raw-vs-value-object
155
+ # branch shape as #arithmetic/#arithmetic_value_object, reused
156
+ # rather than duplicated verb-for-verb (a `Proc` picks the actual
157
+ # arithmetic; everything else -- the Value unwrap/rewrap, the
158
+ # TypeMismatch refusals -- is identical to the additive pair).
159
+ def multiply(current, amount, target)
160
+ current ||= 0
161
+
162
+ if current.is_a?(Value) && amount.is_a?(Value)
163
+ return combine_value_object(current, amount, target, "multiply") { |c, a| c * a }
164
+ end
165
+
166
+ # Same absent-`current`, VO-wrapped-`amount` gap as `#arithmetic`
167
+ # — see that method's own comment.
168
+ amount = unwrap_single_numeric_field(amount) if amount.is_a?(Value)
169
+
170
+ unless amount.is_a?(Numeric) && current.is_a?(Numeric)
171
+ raise TypeMismatch, RefusalWording.render("TypeMismatch", "arithmetic_amount",
172
+ op: "multiply", target: target,
173
+ offered: Rendering.describe(current.is_a?(Numeric) ? amount : current))
174
+ end
175
+
176
+ current * amount
177
+ end
178
+
179
+ # Vendored addition, not (yet) upstream hecks (migration plan
180
+ # task 4, i106): bound the CURRENT value into `[min, max]` -- no
181
+ # "amount" to combine, so it does not go through
182
+ # #arithmetic/#multiply's shared-numeric-field matching at all;
183
+ # it clamps whichever single numeric field the wrapping value
184
+ # object carries (a synthesised wrapper always carries exactly
185
+ # one, per Part 3a's auto-synthesis).
186
+ def clamp(current, bounds, target)
187
+ min, max = bounds
188
+ # THE SAME `current ||= 0` #arithmetic/#multiply both give a
189
+ # PHANTOM (never-set) numeric field, one line up from each —
190
+ # this was the one arithmetic op that didn't, so a VO-typed
191
+ # attribute with no declared `default:` (genuinely absent,
192
+ # `Instance.defaults`/`#default_for`) hit TypeMismatch on the
193
+ # FIRST clamp. (#arithmetic/#multiply's OWN absent-current gap
194
+ # was a real, separate bug this comment used to describe wrong —
195
+ # they did not "silently treat the same absent field as zero";
196
+ # they raised too, blaming a perfectly valid `amount` for not
197
+ # being an Integer when it was one, just still Money-wrapped.
198
+ # Fixed alongside this one — see #unwrap_single_numeric_field.)
199
+ current ||= 0
200
+ if current.is_a?(Value)
201
+ fields = current.to_h
202
+ field = fields.keys.find { |f| fields[f].is_a?(Numeric) } or
203
+ raise TypeMismatch, RefusalWording.render("TypeMismatch", "arithmetic_current",
204
+ op: "clamp", target: target, offered: Rendering.describe(current))
205
+ return current.with(field, fields[field].clamp(min, max))
206
+ end
207
+
208
+ unless current.is_a?(Numeric)
209
+ raise TypeMismatch, RefusalWording.render("TypeMismatch", "arithmetic_current",
210
+ op: "clamp", target: target, offered: Rendering.describe(current))
211
+ end
212
+
213
+ current.clamp(min, max)
214
+ end
215
+
216
+ private
217
+
218
+ # `amount` arrives VO-wrapped whenever the command's own declared
219
+ # attribute type says so (a real `Money`, not a bare Integer) —
220
+ # true whether or not `current` has ever been set. Only meaningful
221
+ # to call once `current` is known NOT to be a Value itself (the
222
+ # `current.is_a?(Value) && amount.is_a?(Value)` branch, above in
223
+ # both callers, already owns the case where both sides carry real
224
+ # fields to combine). Refuses rather than guesses when more than
225
+ # one field is numeric — genuinely ambiguous which one an absent
226
+ # `current` should be treated as zero for, the same reasoning
227
+ # `combine_value_object`'s own `shared_numeric.size == 1` check
228
+ # already holds to when both sides ARE present.
229
+ def unwrap_single_numeric_field(value)
230
+ fields = value.to_h
231
+ numeric_fields = fields.keys.select { |field| fields[field].is_a?(Numeric) }
232
+ return value unless numeric_fields.size == 1
233
+
234
+ fields[numeric_fields.first]
235
+ end
236
+
237
+ def combine_value_object(current, amount, target, op)
238
+ current_fields = current.to_h
239
+ amount_fields = amount.to_h
240
+ shared_numeric = current_fields.keys.select do |field|
241
+ current_fields[field].is_a?(Numeric) && amount_fields[field].is_a?(Numeric)
242
+ end
243
+ unless shared_numeric.size == 1
244
+ raise TypeMismatch,
245
+ RefusalWording.render("TypeMismatch", "arithmetic_shared_field", op: op, target: target)
246
+ end
247
+
248
+ field = shared_numeric.first
249
+ current.with(field, yield(current[field], amount[field]))
250
+ end
251
+ end
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,64 @@
1
+ require_relative "../caller"
2
+ require_relative "../errors"
3
+ require_relative "../refusal_wording"
4
+ require_relative "../../ports/authorization"
5
+
6
+ module Hecks
7
+ module Runtime
8
+ class CommandRules
9
+ # Whether the caller may run this command at all — a `role` mismatch,
10
+ # the one check that runs before any domain-state work, alongside the
11
+ # argument gate rather than after it.
12
+ #
13
+ # TWO CHECKS, NOT A REPLACEMENT — ADR 0025 §9's own caution against
14
+ # "silently downgrading `role` to documentation" cuts both ways: a
15
+ # caller who never named WHO they are (every caller before this) is
16
+ # checked exactly the way it always has been, string equality
17
+ # against the command's own `role`. Only a caller that ALSO binds an
18
+ # `actor_id` (`Hecks.as_caller(role:, actor_id:)`) reaches the
19
+ # real check — a live Governance::RoleAssignment lookup through
20
+ # `Ports::Authorization`, once the command's domain declares
21
+ # `uses_framework "Governance"` (`HecksagonBuilder` refuses any
22
+ # domain that declares a `role` and does not, at build time — see
23
+ # its own `refuse_ungoverned_roles!`). An identified caller is never
24
+ # let back through the string fallback: a real identity that holds
25
+ # no matching grant is refused, not waved through because it also
26
+ # happens to type the right word.
27
+ module Authorization
28
+ # OPT-IN, on BOTH sides. No caller bound: unchecked, exactly as
29
+ # today. No role declared: unchecked too — `role` is genuinely
30
+ # optional in this language (roughly a third of banking's own
31
+ # commands declare none), so a command that never named a role has
32
+ # nothing to check a caller against.
33
+ def refuse_role_mismatch(command, domain)
34
+ caller = Caller.current
35
+ return unless caller
36
+ return if command.role.to_s.empty?
37
+
38
+ authorized =
39
+ if caller.actor_id && governance_attached?(domain)
40
+ Ports::Authorization.holds_role?(registry, actor_id: caller.actor_id, role: command.role)
41
+ else
42
+ caller.role == command.role
43
+ end
44
+
45
+ return if authorized
46
+
47
+ raise Unauthorized, RefusalWording.render("Unauthorized", "role_mismatch",
48
+ command: command.hecks_name, role: command.role,
49
+ caller_role: caller.role)
50
+ end
51
+
52
+ private
53
+
54
+ # SELF-EXEMPT, like `HecksagonBuilder#refuse_ungoverned_roles!` —
55
+ # Governance IS the thing a `holds_role?` lookup runs against, so
56
+ # its own commands are always checked by the string fallback, not
57
+ # a lookup against itself.
58
+ def governance_attached?(domain)
59
+ domain.to_s == "Governance" || registry.hecksagon(domain)&.framework_members&.include?("Governance") || false
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,34 @@
1
+ require_relative "../event"
2
+
3
+ module Hecks
4
+ module Runtime
5
+ class CommandRules
6
+ # What a command's `emits` becomes: an Event in the registry's log,
7
+ # and — where the store can hold one — a recorded event beside the
8
+ # data it describes.
9
+ module Emission
10
+ # `correlation` arrives HERE rather than being merged onto the
11
+ # event afterwards. It is part of the transaction — known from
12
+ # `dispatch`'s own argument before anything is emitted — and an
13
+ # event that is still being written to is not yet a record of
14
+ # what happened. Setting it at construction is what lets the
15
+ # event be frozen the moment it exists.
16
+ def emit(command, domain, aggregate, instance, args, repository, correlation = nil)
17
+ command.emits.map do |event_name|
18
+ event = Event.new(
19
+ name: event_name,
20
+ aggregate: "#{domain}::#{aggregate.hecks_name}",
21
+ id: instance.id,
22
+ payload: args,
23
+ occurred_at: Time.now.utc.iso8601,
24
+ correlation: correlation
25
+ )
26
+ @registry.event_log << event.emit!
27
+ repository.record_event(event) if repository.respond_to?(:record_event)
28
+ event
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end