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,299 @@
1
+ require_relative "interpreting"
2
+ require_relative "../naming"
3
+ require_relative "../rendering"
4
+ require_relative "errors"
5
+ require_relative "identity"
6
+ require_relative "instance"
7
+ require_relative "value"
8
+ require_relative "refusal_wording"
9
+ require_relative "routing"
10
+ require_relative "dependency_planning"
11
+ require_relative "../ports/persistence/execution"
12
+ require_relative "entity_element"
13
+ require_relative "command_interpreter/argument_gate"
14
+
15
+ module Hecks
16
+ module Runtime
17
+ class EntityInterpreter
18
+ include Interpreting
19
+ # THE SAME PAYLOAD GATE aggregate commands and port operations already
20
+ # run — bug audit H1 (docs/audits/2026-08-10-main-bug-audit.md): this
21
+ # class used to run NEITHER refuse_unknown_arguments NOR
22
+ # refuse_absent_arguments, on a comment claiming "an entity inherits
23
+ # its aggregate's own gate." Nothing on the entity dispatch path ever
24
+ # ran one — confirmed live, `LedgerEntry.Reverse` accepted an
25
+ # unrecognized `bogus_arg:` outright, and dispatching it with no
26
+ # `narrative:` silently overwrote the stored narrative with `nil`
27
+ # (`sets :narrative`'s bare self-referential form reads `args[:narrative]`
28
+ # unconditionally). See `step_refuse_unknown_arguments`/
29
+ # `step_refuse_absent_arguments`, below, for how the shared gate is
30
+ # reused rather than reimplemented.
31
+ include CommandInterpreter::ArgumentGate
32
+
33
+ attr_reader :registry
34
+
35
+ # THE DECLARED ORDER, HAND-TYPED — mirrors Vocabulary::EntityDispatchOrder
36
+ # (language/bluebook/vocabulary.bluebook:217-232), held equal to it by
37
+ # spec/vocabulary_conformance_spec.rb the same way CommandInterpreter's
38
+ # own DISPATCH_ORDER is; see that constant's doc comment for why this is
39
+ # hand-typed rather than read live off the meta-domain at every dispatch.
40
+ # `refuse_unknown_arguments`/`refuse_absent_arguments` now lead it, same
41
+ # position `AggregateDispatchOrder` holds them at (H1, above) — the only
42
+ # remaining difference from the aggregate order is no
43
+ # `assign_creation_attributes` (an entity is never created through this
44
+ # path).
45
+ DISPATCH_ORDER = Hecks::Vocabulary.symbols("EntityDispatchOrder")
46
+
47
+ # Same safety valve as `CommandInterpreter::MAX_STALE_WRITE_RETRIES` —
48
+ # see that constant's own comment.
49
+ MAX_STALE_WRITE_RETRIES = 5
50
+
51
+ # `instance` is the PARENT aggregate record (what gets saved and
52
+ # returned) ; `element`/`view` are the entity piece itself — `view`
53
+ # wraps `element` as it stood at `locate_element`, pre-mutation, and
54
+ # `enforce_ensures` builds its own settled wrapper off `element` as it
55
+ # stands after, the same split the original sequential code made.
56
+ #
57
+ # `chain` — S17, ADR 0026 — every entity the dotted verb passes
58
+ # through, root-first (`[Handler, Dispatch]` for `Handler.Dispatch.
59
+ # Bind`) ; `entity`/`entity_name` stay the CHAIN'S OWN LAST entry,
60
+ # the one a command actually belongs to and a mutation actually
61
+ # targets, so every step written before this ADR (enforce_givens,
62
+ # apply_mutations, advance_lifecycle, element_identity, ...) reads
63
+ # exactly as it always has. Only `locate_element` walks the chain.
64
+ Context = Struct.new(:domain, :aggregate, :entity, :entity_name, :command, :command_name,
65
+ :args, :repository, :instance, :chain, :element, :view, :transition,
66
+ :old_element, :result, :route, :plan, :persistence_outcome, :dry_run)
67
+
68
+ def initialize(registry, rules:)
69
+ @registry = registry
70
+ @rules = rules
71
+ end
72
+
73
+ # `dry_run:` — CommandInterpreter#call's own twin, see that method's
74
+ # comment for the shared reasoning (Dispatcher#dry_run?'s own entry
75
+ # point). `step_save`/`step_emit` are the only two steps here that
76
+ # read it either.
77
+ # RETRIES THE WHOLE METHOD BODY on `StaleWrite` — same reasoning as
78
+ # `CommandInterpreter#call`'s own retry: a fresh `ctx`, a fresh
79
+ # `step_hydrate_parent`/`step_locate_element` re-reading current
80
+ # state.
81
+ def call(domain, aggregate, dotted, legacy_args, route: nil, with: nil, dry_run: false)
82
+ *entity_names, command_name = dotted.to_s.split(".")
83
+ if entity_names.empty?
84
+ raise UnknownVerb, RefusalWording.render("UnknownVerb", "entity_unknown",
85
+ aggregate: aggregate.hecks_name, entity: dotted.to_s.inspect)
86
+ end
87
+
88
+ chain = walk_entity_chain(aggregate, entity_names)
89
+ entity = chain.last
90
+ command = entity.command(command_name) ||
91
+ raise(UnknownVerb, RefusalWording.render("UnknownVerb", "entity_no_command",
92
+ entity: entity.hecks_name, command: command_name.inspect))
93
+
94
+ args = Routing.payload(command, with: with, legacy: legacy_args)
95
+ attempt = 0
96
+ begin
97
+ ctx = Context.new(domain, aggregate, entity, entity_names.join("."), command, command_name, args)
98
+ ctx.chain = chain
99
+ ctx.route = route
100
+ ctx.dry_run = dry_run
101
+ ctx.plan = DependencyPlanning::Analyzer.call(aggregate: entity, command: command)
102
+ # RESOLVED HERE, ONCE — see CommandInterpreter#call's own comment;
103
+ # `step_hydrate_parent` reads `ctx.repository` without re-fetching.
104
+ ctx.repository = @registry.repository(domain, aggregate)
105
+ lock_id = Identity.best_effort(aggregate, args, route)
106
+ run_dispatch_order_with_isolation(DISPATCH_ORDER, ctx, lock_key_id: lock_id)
107
+ [ctx.instance, ctx.result, ctx.plan, ctx.persistence_outcome]
108
+ rescue StaleWrite
109
+ attempt += 1
110
+ retry if attempt < MAX_STALE_WRITE_RETRIES
111
+ raise
112
+ end
113
+ end
114
+
115
+ private
116
+
117
+ # ONE HOP PER DOTTED SEGMENT — `ProcessManager.Handler.Dispatch.Bind`
118
+ # (once the dispatcher has already stripped "Domain::Aggregate.")
119
+ # walks Handler off the aggregate, then Dispatch off Handler, each
120
+ # step reading `.entities` exactly the way the single-level case
121
+ # always did — a nested entity is "structurally interchangeable
122
+ # with an aggregate" (Entity's own header) for precisely this
123
+ # reason. Two levels is what Handler/Dispatch need today ; nothing
124
+ # here assumes it stops at two.
125
+ def walk_entity_chain(aggregate, entity_names)
126
+ owner = aggregate
127
+ entity_names.map do |name|
128
+ found = owner.entities.find { |piece| piece.hecks_name == name } ||
129
+ raise(UnknownVerb, RefusalWording.render("UnknownVerb", "entity_unknown",
130
+ aggregate: owner.hecks_name, entity: name.inspect))
131
+ owner = found
132
+ found
133
+ end
134
+ end
135
+
136
+ # `extra_identity_heads:` — every entity `ctx.chain` walks through, not
137
+ # just the root aggregate `ArgumentGate` already knows about. A
138
+ # two-hop dispatch (`Handler.Dispatch.Bind`) is addressed by BOTH
139
+ # hops' own identity, each read straight out of `args` by
140
+ # `EntityElement#element_of` — refusing those as unknown would refuse
141
+ # every legitimate nested-entity dispatch there is, the same reasoning
142
+ # `ArgumentGate#refuse_unknown_arguments`'s own header gives for `:id`/
143
+ # the root's `identity_heads`.
144
+ def step_refuse_unknown_arguments(ctx)
145
+ step(:refuse_unknown_arguments) {
146
+ refuse_unknown_arguments(ctx.domain, ctx.aggregate, ctx.command, ctx.args,
147
+ extra_identity_heads: ctx.chain.flat_map(&:identity_heads))
148
+ }
149
+ end
150
+
151
+ # No `aggregate:` exemption to pass — that kwarg exists only for a
152
+ # port operation's own self-address (`ArgumentGate#refuse_absent_
153
+ # arguments`'s own comment); an entity command's chain identity never
154
+ # reaches `command.attributes` in the first place (resolved as
155
+ # addressing above, not as a declared fact), so there is nothing here
156
+ # for the exemption to need to strip.
157
+ def step_refuse_absent_arguments(ctx)
158
+ step(:refuse_absent_arguments) { refuse_absent_arguments(ctx.command, ctx.args) }
159
+ end
160
+
161
+ def step_normalize_args(ctx)
162
+ ctx.args = step(:normalize_args) { normalize_args(ctx.aggregate, ctx.command, ctx.args) }
163
+ end
164
+
165
+ def step_refuse_role_mismatch(ctx)
166
+ step(:refuse_role_mismatch) { @rules.refuse_role_mismatch(ctx.command, ctx.domain) }
167
+ end
168
+
169
+ def step_resolve_references(ctx)
170
+ step(:resolve_references) { @rules.resolve_references(ctx.domain, ctx.command, ctx.args) }
171
+ end
172
+
173
+ def step_hydrate_parent(ctx)
174
+ # `ctx.repository` is resolved once, in `#call`, before the
175
+ # isolation decision — not here any more.
176
+ ctx.instance = step(:hydrate_parent) {
177
+ parent(ctx.repository, ctx.aggregate, ctx.entity_name, ctx.command_name, ctx.args, ctx.route)
178
+ }
179
+ end
180
+
181
+ def step_locate_element(ctx)
182
+ ctx.element = step(:locate_element) {
183
+ EntityElement.locate_chain(ctx.aggregate, ctx.chain, ctx.instance, ctx.args, ctx.command_name, ctx.route)
184
+ }
185
+ # `view` was hydrated ONCE, here, into its OWN state hash
186
+ # (Value.hydrate builds a fresh Hash — never aliased with `element`)
187
+ # — exactly right for enforce_givens, which must read pre-mutation.
188
+ ctx.view = Instance.new(aggregate: ctx.entity, id: EntityElement.element_identity(ctx.entity, ctx.element).to_s,
189
+ state: ctx.element)
190
+ end
191
+
192
+ def step_enforce_givens(ctx)
193
+ step(:enforce_givens) {
194
+ @rules.enforce_givens(ctx.view, ctx.command, ctx.args, domain: ctx.domain, declaring: ctx.entity, parent: ctx.instance)
195
+ }
196
+ end
197
+
198
+ def step_admissible_transition(ctx)
199
+ ctx.transition = step(:admissible_transition) { @rules.admissible_transition(ctx.entity, ctx.command, ctx.view) }
200
+ end
201
+
202
+ def step_apply_mutations(ctx)
203
+ ctx.old_element = ctx.element.dup unless ctx.command.ensures.empty?
204
+ step(:apply_mutations) {
205
+ ctx.command.mutations.each { |mutation|
206
+ EntityElement.apply_to_element(@rules, ctx.aggregate, ctx.entity, ctx.element, mutation, ctx.args)
207
+ }
208
+ }
209
+ end
210
+
211
+ def step_advance_lifecycle(ctx)
212
+ return unless ctx.transition
213
+
214
+ step(:advance_lifecycle) { ctx.element[ctx.entity.lifecycle.field] = ctx.transition.target }
215
+ end
216
+
217
+ # An ensures reads the SETTLED record, so it needs a view hydrated from
218
+ # `element` as it stands now, mutations included — unlike `view` above,
219
+ # built once and read pre-mutation by enforce_givens.
220
+ def step_enforce_ensures(ctx)
221
+ step(:enforce_ensures) do
222
+ settled = Instance.new(aggregate: ctx.entity, id: ctx.view.id, state: ctx.element)
223
+ @rules.enforce_ensures(settled, ctx.command, ctx.args, old: ctx.old_element, domain: ctx.domain, parent: ctx.instance)
224
+ end
225
+ end
226
+
227
+ # THE PARENT AGGREGATE's own invariants — `ctx.instance` is the
228
+ # parent record an entity mutation writes into (this file's own
229
+ # `Context` comment), the SAME boundary an aggregate-level
230
+ # invariant guards regardless of which interpreter changed it. No
231
+ # separate "entity invariant" exists (S10, ADR 0025 scopes
232
+ # `invariant` to the aggregate only) — see `Admissibility#
233
+ # enforce_invariants`'s own comment.
234
+ def step_enforce_invariants(ctx)
235
+ step(:enforce_invariants) { @rules.enforce_invariants(ctx.instance, ctx.aggregate, domain: ctx.domain) }
236
+ end
237
+
238
+ # `dry_run:` skips this — see CommandInterpreter#step_save's own
239
+ # comment, same reasoning and the same precedent
240
+ # (`step_assign_creation_attributes`'s own conditional-skip).
241
+ def step_save(ctx)
242
+ return if ctx.dry_run
243
+
244
+ step(:save) do
245
+ @rules.resolve_state_references(ctx.domain, ctx.aggregate, ctx.instance.state)
246
+ # `expected_version:` — see CommandInterpreter#step_save's own
247
+ # comment: nil for a repository that isn't CAS-capable, or an
248
+ # instance never read from storage, either of which falls
249
+ # through to a plain save inside `AppendOnly#save`.
250
+ ctx.persistence_outcome = ctx.repository.save(ctx.instance, expected_version: ctx.instance.version)
251
+ if ctx.persistence_outcome.status == :stale
252
+ # NOT a `RefusalWording.render` call — see
253
+ # `CommandInterpreter#step_save`'s identical branch and
254
+ # `Runtime::StaleWrite`'s own comment.
255
+ raise(StaleWrite,
256
+ "#{ctx.command.hecks_name} on #{ctx.aggregate.hecks_name} " \
257
+ "(#{Identity.reading(ctx.aggregate)}: #{Rendering.describe(ctx.instance.id)}) lost a race — " \
258
+ "another write committed against this record after it was read")
259
+ end
260
+ end
261
+ end
262
+
263
+ # `dry_run:` skips this too — nothing was committed, so `ctx.result`
264
+ # stays nil and `Dispatcher#dry_run?` never reads it.
265
+ def step_emit(ctx)
266
+ return if ctx.dry_run
267
+
268
+ ctx.result = step(:emit) { @rules.emit(ctx.command, ctx.domain, ctx.aggregate, ctx.instance, ctx.args, ctx.repository) }
269
+ end
270
+
271
+ # THE PARENT AGGREGATE, addressed exactly as `CommandInterpreter#hydrate`
272
+ # addresses one acting on itself — derive from the declared identity first
273
+ # (`Identity.of`), and let a bare `id:` name an already-derived record when
274
+ # the identity itself is not what the caller is holding.
275
+ def parent(repository, aggregate, entity_name, command_name, args, route = nil)
276
+ parent_id = route&.aggregate ||
277
+ Identity.of(aggregate, args) ||
278
+ Identity.from(aggregate, args, :id) ||
279
+ raise(NotFound, RefusalWording.render("NotFound", "entity_parent_no_identity",
280
+ command: command_name, aggregate: aggregate.hecks_name,
281
+ entity: entity_name, identity: Identity.reading(aggregate)))
282
+ found = repository.find(parent_id) ||
283
+ raise(NotFound, RefusalWording.render("NotFound", "record_missing",
284
+ aggregate: aggregate.hecks_name,
285
+ identity: Identity.reading(aggregate),
286
+ offered: Rendering.describe(parent_id)))
287
+ found.dup
288
+ end
289
+
290
+ # `locate_chain`/`element_of`/`element_identity`/`apply_to_element` and
291
+ # their own helpers used to live here — moved to `Runtime::EntityElement`
292
+ # (see that file's own header) so `CommandInterpreter`'s own
293
+ # `delegate_to_entity` step can locate and mutate the same element the
294
+ # same way, against an aggregate record already held in memory. `call`,
295
+ # above, and every `step_*` method reach them through that module now;
296
+ # nothing about the STEPS themselves changed.
297
+ end
298
+ end
299
+ end
@@ -0,0 +1,122 @@
1
+ require_relative "value/invariant_violation"
2
+ require_relative "../vocabulary"
3
+
4
+ module Hecks
5
+ module Runtime
6
+ class UnknownVerb < StandardError; end
7
+ class EnsuresNotMet < StandardError; end
8
+ class GivenNotMet < StandardError; end
9
+ class NotFound < StandardError; end
10
+ class LifecycleRefused < StandardError; end
11
+ class TypeMismatch < StandardError; end
12
+ # An argument the command does not declare. Sibling of TypeMismatch : that one
13
+ # is the right name carrying the wrong thing, this one is a name the command
14
+ # never had. Both are the payload gate refusing before any rule runs.
15
+ class UnknownArgument < StandardError; end
16
+ # The third of the trio, and the one that was missing : a name the command DOES
17
+ # declare, absent. TypeMismatch is the right name carrying the wrong thing,
18
+ # UnknownArgument a name that was never declared, AbsentArgument a declared name
19
+ # that never arrived. Between them they say a command takes exactly the
20
+ # arguments it declares — no others, and all of them.
21
+ class AbsentArgument < StandardError; end
22
+ # A creating command whose derived identity already names a record. The
23
+ # record it would have overwritten stands ; the dispatch that tried to
24
+ # mint a second one over it is what refuses.
25
+ class AlreadyExists < StandardError; end
26
+ # A declared, non-optional attribute a record predates — added since
27
+ # it was written, with no default: to fill it and no translation
28
+ # declaring what an old record should read there. `GuardState`
29
+ # (command_rules/admissibility.rb) is the one place this is raised: a
30
+ # `given`/`ensures`/`invariant` that reads the field would otherwise
31
+ # evaluate against a value nobody wrote, which is the same silent-
32
+ # wrong-answer class as an unpopulated projection reading "not
33
+ # active" (ADR 0025, "Added attributes and absence"). An OPTIONAL
34
+ # attribute in the same spot reads nil instead — that is what
35
+ # optional means, and this refusal is deliberately narrower than the
36
+ # nil-read it sits beside, not a replacement for it.
37
+ class AttributeAbsent < StandardError; end
38
+ # THE SAME SILENT-WRONG-ANSWER CLASS AS ABOVE, one line up — a
39
+ # `projects` field (S12, ADR 0025) this record predates, or that no
40
+ # rebuild sweep has populated yet, read by a `given`/`ensures`/
41
+ # `invariant` as though it carried a real value. `GuardState` is
42
+ # the one place this is raised, the same way AttributeAbsent is —
43
+ # a DECLARED field the record does not yet carry, distinguished
44
+ # from that one only in WHY: an ordinary attribute is absent
45
+ # because nobody backfilled it, a projected field is absent
46
+ # because nobody has swept it yet.
47
+ class ProjectionAbsent < StandardError; end
48
+ # A query or read model declares `authorize policy, tenant: :field` and
49
+ # the caller did not pass that field — the one half of `authorize` this
50
+ # runtime can enforce without a caller-identity system: the boundary
51
+ # itself, not whether the caller actually holds `policy`. See
52
+ # Runtime::TenantScope.
53
+ class Unauthorized < StandardError; end
54
+ # `corrects` names a past event this record must have already emitted
55
+ # (CommandBuilder#corrects_impl's own comment) — a fact the expression
56
+ # evaluator cannot check (it is not a predicate over the record's OWN
57
+ # fields, it is "did this exact record ever announce this"), so it is
58
+ # raised structurally, the same way AlreadyExists/NotFound are, rather
59
+ # than being expressible as an ordinary `given`. Raised by
60
+ # `CommandRules::Admissibility#enforce_correction_target`.
61
+ class NothingToCorrect < StandardError; end
62
+
63
+ # A RUNTIME FAULT, NOT A DOMAIN REFUSAL — deliberately absent from
64
+ # `DOMAIN_REFUSALS` below and from `vocabulary.bluebook`'s own
65
+ # `DomainRefusal` list. Raised when an optimistic-concurrency CAS write
66
+ # (`AppendOnly#save`'s `expected_version:`) finds the stored version has
67
+ # moved since this instance was read — someone else's write committed
68
+ # in between. `CommandInterpreter#call`/`EntityInterpreter#call` catch
69
+ # this themselves and retry the whole dispatch from a fresh hydrate, so
70
+ # `enforce_givens` re-evaluates against the now-current state; a caller
71
+ # only ever sees this escape when every retry is exhausted under
72
+ # sustained contention — an operational condition (many concurrent
73
+ # writers hammering one aggregate), not a business rule a domain author
74
+ # declared. See docs/decisions/ (concurrency-control ADR).
75
+ class StaleWrite < StandardError; end
76
+
77
+ # A Lambda-routed domain's own refusal (rust/host, `Runtime::
78
+ # RemoteDispatcher`), carrying Rust's own refusal text verbatim —
79
+ # NOT yet mapped back to the specific matching class above
80
+ # (GivenNotMet vs. EnsuresNotMet vs. ...), a real, known,
81
+ # documented gap: the WASM projector's own event/refusal-wording
82
+ # parity work (ADR 0021) makes the TEXT match Ruby's, but nothing
83
+ # yet parses that text back into a typed Ruby exception the way a
84
+ # local dispatch already raises one directly. Callers that only
85
+ # need "the domain said no" (not which specific rule) are
86
+ # unaffected; callers pattern-matching a SPECIFIC refusal class
87
+ # against a Lambda-routed domain are the ones this gap would bite.
88
+ class RemoteRefusal < StandardError; end
89
+
90
+ # The domain saying NO — the errors a reaction may legitimately meet and
91
+ # record as an undelivered outcome. A policy whose target refuses is a fact
92
+ # about the domain ; the originating command still stands.
93
+ #
94
+ # Everything ELSE is a defect : a NoMethodError in an interpreter, a
95
+ # NameError from a missing constant, a TypeError from a bad assumption. A
96
+ # blanket `rescue StandardError` used to fold both into one line —
97
+ # `delivered: false, reason: "..."` — so a crash in the runtime was
98
+ # indistinguishable from a rule doing its job, and read as normal operation
99
+ # in the log.
100
+ #
101
+ # UnknownVerb IS one of these, and deliberately : a cross-domain policy
102
+ # (`across "Notifications"`) fires in deployments where that domain is not
103
+ # loaded, and recording the undelivered reaction rather than raising is the
104
+ # design — spec/policy_spec states it in so many words, "records a reaction
105
+ # it cannot deliver rather than swallowing it".
106
+ # InvariantViolation belongs here and was missing. A value object refusing
107
+ # its own rule is the domain saying no as plainly as a given is — but the
108
+ # class is declared over in value.rb and never made the list, so the policy
109
+ # and saga interpreters, which rescue exactly these, would let it propagate
110
+ # as though the RUNTIME had broken. A reaction whose target violates an
111
+ # invariant is declined, not crashed. Found by spec/domain_refusal_spec on
112
+ # its first run : every corpus refusal must be a class named here, and 23
113
+ # of banking's were InvariantViolation.
114
+ # THE NAMES COME FROM THE LANGUAGE, the classes from this module.
115
+ # `DomainRefusal` declares WHICH refusals are the domain's own —
116
+ # a rule the caller broke — as against a runtime fault. Resolving
117
+ # each name here means a refusal declared but never defined fails
118
+ # at load with a NameError, rather than being quietly absent from
119
+ # a list nothing re-checks.
120
+ DOMAIN_REFUSALS = Hecks::Vocabulary.fetch("DomainRefusal").map { |name| const_get(name) }.freeze
121
+ end
122
+ end
@@ -0,0 +1,51 @@
1
+ require "time"
2
+
3
+ module Hecks
4
+ module Runtime
5
+ # `correlation` is NOT on the wire — `to_h` below deliberately omits it,
6
+ # the same as `bin/run`'s own event projection does. It is runtime
7
+ # bookkeeping stamped by `Dispatcher#dispatch` when a saga leg's own
8
+ # dispatch causes this event (see `SagaInterpreter#deliver_saga_dispatch`
9
+ # and `#saga_correlation`) : a Hash of `correlation_head` -> correlation
10
+ # value, so an event caused by one saga cannot be misread by an unrelated
11
+ # one correlating on a different field. Absent for any event no saga
12
+ # dispatch caused, which is most of them.
13
+ Event = Struct.new(:name, :aggregate, :id, :payload, :occurred_at, :correlation, keyword_init: true) do
14
+ # AN EMITTED EVENT IS A RECORD OF SOMETHING THAT HAPPENED, and a
15
+ # mutable audit trail is not one. The PAYLOAD — the domain fact the
16
+ # event carries — is frozen THROUGH on emission: freezing the Hash
17
+ # alone would leave every value in it editable in place, which is
18
+ # the shape all four previous freezing bugs had.
19
+ #
20
+ # THE WHOLE EVENT, not just its payload. Correlation used to be
21
+ # merged onto already-emitted events by `Dispatcher#dispatch`, which
22
+ # is what kept an event writable after it had happened; it is set at
23
+ # construction now, because it is part of the transaction and known
24
+ # from `dispatch`'s own argument before anything is emitted.
25
+ #
26
+ # The LOG stays appendable: new events are still recorded. It is
27
+ # each event that stops changing once it exists.
28
+ def emit!
29
+ Freezer.deep(payload)
30
+ Freezer.deep(correlation)
31
+ freeze
32
+ end
33
+
34
+ def to_h
35
+ {
36
+ name: name,
37
+ aggregate: aggregate,
38
+ id: id,
39
+ payload: payload,
40
+ occurred_at: occurred_at
41
+ }
42
+ end
43
+
44
+ def to_s
45
+ "#{name}(#{aggregate}##{id}) #{payload.inspect}"
46
+ end
47
+
48
+ def inspect = "#<Event #{self}>"
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,156 @@
1
+ require_relative "../naming"
2
+ require_relative "value"
3
+
4
+ module Hecks
5
+ module Runtime
6
+ # THE SCALAR AN IDENTITY PATH NAMES.
7
+ #
8
+ # An identity is DECLARED as a path — `identified_by :number` — and
9
+ # this is the one place that reads one. It follows the path and nothing else.
10
+ #
11
+ # What it replaced was `Value.identifier`, which opened a one-field value
12
+ # object and took whatever was inside : that let `identified_by :number` pass
13
+ # for an identity, with the runtime guessing which field had been meant. The
14
+ # guess is gone. A declaration that names no field is now refused when the
15
+ # bluebook loads (`an aggregate that is identified names a field`, `an entity
16
+ # is known by a field`), so by the time anything is dispatched there is
17
+ # always a path here to follow.
18
+ #
19
+ # Usage:
20
+ #
21
+ # Identity.scalar("number.value", account_number_value_object) # => "acct-1"
22
+ #
23
+ module Identity
24
+ module_function
25
+
26
+ # A hash read that decides which spelling of a key answers by
27
+ # PRESENCE, never by `||` — a bare `||` treats a genuinely-held
28
+ # `false` the same as an absent key and falls through to the other
29
+ # spelling, landing on `nil` instead of the real, stored answer.
30
+ def hash_lookup(hash, key)
31
+ sym = key.to_sym
32
+ hash.key?(sym) ? hash[sym] : hash[key]
33
+ end
34
+
35
+ # The head names the ATTRIBUTE and is consumed by whoever looked the value
36
+ # up; what is left is the walk down into it. A path with no fields to walk
37
+ # — an aggregate that declares no identity and falls back to `id` — hands
38
+ # back what it was given, because there is nothing declared to dig for.
39
+ def scalar(path, held)
40
+ _head, *fields = path.to_s.split(".")
41
+ return held if fields.empty?
42
+
43
+ fields.reduce(Value.materialize(held)) do |dug, field|
44
+ dug.is_a?(Hash) ? hash_lookup(dug, field) : nil
45
+ end
46
+ end
47
+
48
+ # THE IDENTITY IS THE JOIN OF ITS PARTS, in declaration order. Shared by
49
+ # `CommandInterpreter` (an aggregate acting on itself) and
50
+ # `EntityInterpreter` (a piece addressed through its aggregate) — a piece
51
+ # declares an identity the same shape a head does, so it derives one the
52
+ # same way. `construct` answers `identity_paths` / `identity_heads` /
53
+ # `attribute` (an Aggregate or an Entity, either one) ; `value_owner`
54
+ # answers for coercion (`Value.for_attribute`'s first argument), which for
55
+ # an entity is its OWNING aggregate — an entity's value objects resolve
56
+ # through the aggregate's namespace, not its own.
57
+ #
58
+ # A part the payload does not carry makes the WHOLE identity unresolvable,
59
+ # rather than half of one. Half an identity names nothing, and joining what
60
+ # did arrive would silently name a different record on every dispatch — the
61
+ # precise failure that minting an id caused, arrived at by another road.
62
+ def of(construct, args, value_owner: construct)
63
+ paths = construct.identity_paths
64
+ return nil if paths.empty?
65
+
66
+ parts = paths.map { |path| from(construct, args, path, value_owner: value_owner) }
67
+ # A BLANK PART NAMES NOTHING, the same as an ABSENT one — AN ID IS A
68
+ # SCALAR, and "" is not a fact about anything. This used to check only
69
+ # `nil?`, so a canonical text extracted as "" (an expression whose
70
+ # source did not survive extraction) resolved to a REAL, empty-string
71
+ # identity — a record addressable by an id no caller could have meant.
72
+ return nil if parts.any? { |part| part.nil? || (part.respond_to?(:empty?) && part.empty?) }
73
+
74
+ Naming.identity(parts)
75
+ end
76
+
77
+ # A path digs into the value object that carries the identity, so what is
78
+ # stored is the SCALAR inside it rather than the object serialised whole.
79
+ def from(construct, args, key, value_owner: construct)
80
+ return nil unless key
81
+
82
+ head, *rest = key.to_s.split(".")
83
+ head = head.to_sym
84
+ return nil unless args.key?(head)
85
+
86
+ unless rest.empty?
87
+ held = args[head]
88
+ held = held.to_h if held.respond_to?(:to_h)
89
+ # AN ID IS ALWAYS A SCALAR. The path says WHICH FIELD carries it, so a
90
+ # caller may hand that field's value straight over — a string or a
91
+ # number, never a serialised object. Only a value object that actually
92
+ # arrived whole has to be opened.
93
+ return held.to_s unless held.is_a?(Hash)
94
+
95
+ return rest.reduce(held) { |h, f| h.is_a?(Hash) ? hash_lookup(h, f) : nil }&.to_s
96
+ end
97
+
98
+ # Coerced against the identity ATTRIBUTE only when the caller actually
99
+ # named it. A saga addresses an aggregate by its correlation key, and
100
+ # that key carries the id ALREADY RESOLVED — coercing "w1" against a
101
+ # WireReference asked the caller to pass fields for a value object they
102
+ # never mentioned.
103
+ attribute = construct.identity_heads.include?(head) ? construct.attribute(head) : nil
104
+ raw = args[head]
105
+ return raw unless attribute
106
+
107
+ # AN ID IS ALWAYS A SCALAR — same contract the dotted branch above
108
+ # already keeps, just reached a different way here: a BARE
109
+ # (undotted) identity path names one of THIS construct's own
110
+ # declared attributes directly, and when that attribute's type is
111
+ # a value object (Translation's own compound `identified_by
112
+ # :domain, :from, :to`, each typed `TranslationDomainName`/
113
+ # `TranslationEraName`), `Value.for_attribute` coerces it into a
114
+ # real single-field Value wrapper — never unwrapped before this,
115
+ # so `Naming.identity`'s own plain `Array#join` (`Naming.identity`'s
116
+ # own header: parts must already be scalars) fell through to
117
+ # Ruby's default `Object#to_s`, leaking a raw, run-to-run-random
118
+ # memory address (`#<Hecks::Runtime::Value:0x...>`) into
119
+ # every refusal quoting this identity — found live via bin/fuzz on
120
+ # the self-hosted "translation" domain (replay_is_deterministic:
121
+ # the SAME address never repeats, so two replays of the
122
+ # identical steps produced different histories the moment a
123
+ # Translation went missing). `materialize_unwrapped` is the
124
+ # SAME single-field-VO-recurses-to-its-bare-scalar helper
125
+ # `read_model_interpreter.rb` already uses for exactly this
126
+ # unwrap; passthrough for anything that isn't a Value at all.
127
+ Value.materialize_unwrapped(Value.for_attribute(value_owner, attribute, raw)).to_s
128
+ end
129
+
130
+ # How an identity READS when the runtime has to name it in a refusal — the
131
+ # paths as they were declared, so the message quotes the bluebook back.
132
+ def reading(construct)
133
+ construct.identity_paths.join(", ")
134
+ end
135
+
136
+ # BEST-EFFORT, FOR A LOCK KEY ONLY — `Runtime::AggregateLock`'s own
137
+ # per-record striping needs SOME id to key on before dispatch has run
138
+ # far enough to hydrate for real, so this walks the identical chain
139
+ # `CommandInterpreter#hydrate_existing`/`#hydrate_prior_or_initial`
140
+ # and `EntityInterpreter#parent` already use to locate the real
141
+ # record — but wrapped to never raise. Choosing which Mutex to hold
142
+ # must never itself become a crash. `nil` means "could not resolve
143
+ # from the raw, pre-normalized payload this runs against" — the
144
+ # caller locks by aggregate type alone in that case (coarser, still
145
+ # correct, just less concurrent).
146
+ def best_effort(construct, args, route = nil, reference_key: nil)
147
+ route&.aggregate ||
148
+ of(construct, args) ||
149
+ from(construct, args, :id) ||
150
+ (reference_key && from(construct, args, reference_key))
151
+ rescue StandardError
152
+ nil
153
+ end
154
+ end
155
+ end
156
+ end