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,193 @@
1
+ require "fileutils"
2
+ require_relative "era_guard/shape_diff"
3
+ require_relative "../bluebook/dsl/malformed"
4
+ require_relative "../bluebook/meta_validator"
5
+ require_relative "../naming"
6
+ require_relative "../ports/loading"
7
+ require_relative "../ports/persistence/lineage"
8
+ require_relative "era_check"
9
+ require_relative "registry"
10
+
11
+ module Hecks
12
+ module Runtime
13
+ # Detects when a bluebook's storage shape has drifted from what its data
14
+ # was written under — a rename or restructure with nothing to explain
15
+ # it. Refuses at boot, before a command can run into the gap and raise
16
+ # something that only makes sense once you already suspect a rename.
17
+ #
18
+ # The held source is a snapshot of the bluebook text as it stood the
19
+ # last time its shape was accepted. It updates only when a drift is
20
+ # seen AND covered by a translation — never on every boot, and never
21
+ # silently when a rename has nothing to explain it.
22
+ module EraGuard
23
+ extend ShapeDiff
24
+
25
+ module_function
26
+
27
+ def check!(registry, directory)
28
+ registry.bluebooks.each_value { |bluebook| check_bluebook!(registry, bluebook, directory) }
29
+ end
30
+
31
+ def check_bluebook!(registry, bluebook, directory)
32
+ current_text = EraCheck.source_text_for(bluebook, directory)
33
+ return unless current_text
34
+
35
+ era_dir = File.join(registry.root, "data", "eras")
36
+ held_path = File.join(era_dir, "#{Naming.snake(bluebook.name)}.bluebook")
37
+
38
+ FileUtils.mkdir_p(era_dir)
39
+ unless File.exist?(held_path)
40
+ File.write(held_path, current_text)
41
+ return
42
+ end
43
+
44
+ held_bluebook = shadow_parse(File.read(held_path, encoding: "UTF-8"), held_path)
45
+ drifted = false
46
+
47
+ bluebook.aggregates.each do |aggregate|
48
+ lineage = Ports::Persistence::Lineage.for(registry, bluebook.name, aggregate)
49
+ held_aggregate = held_bluebook.aggregate(lineage&.ancestor_name || aggregate.name)
50
+ next unless held_aggregate
51
+ next if shape(aggregate) == shape(held_aggregate)
52
+
53
+ drifted = true
54
+ uncovered = uncovered_attributes(aggregate, held_aggregate, lineage)
55
+ refuse_uncovered!(bluebook, aggregate, uncovered) unless uncovered.empty?
56
+
57
+ unsafe = unsafe_additions(aggregate, held_aggregate, lineage)
58
+ refuse_unsafe_addition!(bluebook, aggregate, unsafe) unless unsafe.empty?
59
+ end
60
+
61
+ check_vanished_aggregates!(registry, bluebook, held_bluebook)
62
+ File.write(held_path, current_text) if drifted
63
+ end
64
+
65
+ # An aggregate that existed in the held text and answers to no
66
+ # current name — renamed silently, with nothing declaring `was:` to
67
+ # explain where its data went — is exactly the disease this guards
68
+ # against, and a plain per-aggregate diff would never see it: the
69
+ # current aggregate simply has no held counterpart to compare to.
70
+ def check_vanished_aggregates!(registry, bluebook, held_bluebook)
71
+ held_bluebook.aggregates.each do |held_aggregate|
72
+ claimed = bluebook.aggregates.any? do |aggregate|
73
+ aggregate.name == held_aggregate.name ||
74
+ registry.translations.any? do |translation|
75
+ translation.domain == bluebook.name &&
76
+ translation.for_aggregate(aggregate.name)&.was == held_aggregate.name
77
+ end
78
+ end
79
+ claimed ||= registry.translations.any? do |translation|
80
+ translation.domain == bluebook.name && translation.retired.include?(held_aggregate.name)
81
+ end
82
+ next if claimed
83
+
84
+ raise WiringError,
85
+ "cannot boot #{bluebook.name}: #{held_aggregate.name} existed and now doesn't, " \
86
+ "and nothing declares was: #{held_aggregate.name.inspect} to explain where its data went."
87
+ end
88
+ end
89
+
90
+ # The Layer-1 coverage refusal — one wording, whoever detects the
91
+ # gap (the file-store boot check here, or Postgres's mint path).
92
+ def refuse_uncovered!(bluebook, aggregate, uncovered)
93
+ raise WiringError,
94
+ "cannot boot #{bluebook.name}::#{aggregate.name}: its shape changed and " \
95
+ "#{uncovered.map { |path| render_path(path) }.join(', ')} #{uncovered.size == 1 ? 'is' : 'are'} not " \
96
+ "explained by any rename, move, convert, retype, or drop. Update bluebook/translations/*.bluebook, e.g. " \
97
+ "#{suggestion(uncovered.first)}."
98
+ end
99
+
100
+ # The addition-side sibling of refuse_uncovered! above — same
101
+ # wording shape, different cause: nothing vanished or changed type,
102
+ # something new arrived that an existing record has no way to hold.
103
+ def refuse_unsafe_addition!(bluebook, aggregate, unsafe)
104
+ raise WiringError,
105
+ "cannot boot #{bluebook.name}::#{aggregate.name}: #{unsafe.map { |name| ":#{name}" }.join(', ')} " \
106
+ "#{unsafe.size == 1 ? 'is new and required' : 'are new and required'}, with no default: to fill " \
107
+ "an existing record and no translation explaining what one should read there. Give it a " \
108
+ "default:, make it optional: true or list_of, or declare bluebook/translations/*.bluebook, e.g. " \
109
+ "`backfill :#{unsafe.first}, default: ...`."
110
+ end
111
+
112
+ def render_path(path) = path.include?(".") ? path.inspect : ":#{path}"
113
+
114
+ def suggestion(path)
115
+ if path.include?(".")
116
+ "`move #{path.inspect}, to: #{path.inspect}` or `drop #{path.inspect}`"
117
+ else
118
+ "`rename :#{path}, to: :new_name` or `drop :#{path}`"
119
+ end
120
+ end
121
+
122
+ # Parses held source into its own IR, in a scratch registry so a past
123
+ # era's text never touches the one actually booting.
124
+ #
125
+ # NORMAL PARSE FIRST, shadow only as a FALLBACK — not shadow-parsing
126
+ # unconditionally, which is what this used to do. A handful of DSL
127
+ # defaults fork on `MetaValidator.shadow_parsing?` for a reason
128
+ # that has NOTHING to do with syntax the live grammar can no longer
129
+ # read at all (`identified_by { }`, `belongs_to`, `has_one`,
130
+ # `has_many` — genuinely removed spellings, exactly what shadow-
131
+ # parsing exists to keep readable): `reference_to`'s own default
132
+ # mint name (`default_reference_name`, attribute_collector.rb)
133
+ # changed from `_id`-suffixed to bare under ADR 0025, and THAT fork
134
+ # applies even to text using nothing but current, live syntax.
135
+ # Held text minted under the CURRENT grammar — every real era in
136
+ # this corpus today, since nothing has ever minted a second one —
137
+ # parses fine normally; only the reference-naming DEFAULT differed
138
+ # once shadow mode engaged unconditionally, so it silently
139
+ # reconstructed a DIFFERENT shape (and hash) than a fresh parse of
140
+ # the identical text — the same text hashing two different ways
141
+ # depending on which code path read it, breaking `ensure_named!`'s
142
+ # own from/to edge lookup with a spurious "no translation edge
143
+ # covers it" refusal that has nothing to do with any real
144
+ # translation gap.
145
+ #
146
+ # A normal parse can only ever SUCCEED on text the live grammar
147
+ # fully understands — there is no way for it to silently produce a
148
+ # wrong-but-plausible answer for genuinely legacy text, since every
149
+ # removed spelling refuses loudly (`Malformed`) rather than
150
+ # degrading. So: try normal first — if the ordinary grammar reads
151
+ # this text without complaint, that IS the canonical, unambiguous
152
+ # interpretation, the same one `label_of`/`mint_hash` on the same
153
+ # source text always computes, whoever's asking. Only on a
154
+ # `Malformed` refusal — the one signal that actually means "this
155
+ # spelling doesn't exist anymore" — fall back to the legacy
156
+ # grammar, exactly as before this change. Any OTHER exception (a
157
+ # genuine syntax error, an unrelated validation refusal) propagates
158
+ # unchanged; swallowing it here to retry under shadow mode would
159
+ # risk masking a real defect in the held text behind a confusing
160
+ # second failure instead of the original, more specific one.
161
+ #
162
+ # `MetaValidator.while_shadow_parsing` (ADR 0025, docs/dsl-work-
163
+ # slices.md's S0a) is what makes the fallback a LEGACY grammar
164
+ # rather than just a second copy of today's: it stops
165
+ # `BluebookBuilder.build` from judging this text against the
166
+ # grammar as it stands NOW, which is the one thing that would make
167
+ # a removed spelling refuse HISTORY the day it is removed from
168
+ # live source. The scratch registry is throwaway either way —
169
+ # nothing here is dispatched against or exposed to the real one —
170
+ # so skipping the judge/assemble round-trip changes nothing this
171
+ # method reads: `shape`, `uncovered_attributes`, and friends only
172
+ # ever ask the built IR for its own structure.
173
+ def shadow_parse(source, path)
174
+ parse_bluebook(source, path, shadow: false)
175
+ rescue Hecks::Bluebook::DSL::Malformed
176
+ parse_bluebook(source, path, shadow: true)
177
+ end
178
+
179
+ def parse_bluebook(source, path, shadow:)
180
+ scratch = Registry.new
181
+ loading = Ports::Loading.bootstrap
182
+ run = lambda do
183
+ Hecks.with_registry(scratch) do
184
+ loading.load_library
185
+ Kernel.eval(source, TOPLEVEL_BINDING, path, 1)
186
+ end
187
+ end
188
+ shadow ? Hecks::Bluebook::MetaValidator.while_shadow_parsing(&run) : run.call
189
+ scratch.bluebooks.values.first
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,61 @@
1
+ require "json"
2
+ require "tempfile"
3
+ require_relative "era_guard"
4
+ require_relative "storage_shape"
5
+
6
+ module Hecks
7
+ module Runtime
8
+ # The refusal wording for a held era whose digest no longer matches its
9
+ # frozen text — the digest mismatch alone is what DETECTS tampering (a
10
+ # plain SHA256 comparison over raw bytes, unrelated to any of this);
11
+ # this only supplies the wording once that's already fired.
12
+ #
13
+ # LIVES HERE, NOT UNDER `Ports::Persistence` — `project` below directly
14
+ # calls `Runtime::EraGuard.shadow_parse`/`Runtime::StorageShape.project`
15
+ # (DSL-execution machinery), the "an adapter/port reaches into the
16
+ # runtime instead of being handed already-computed data" shape that
17
+ # has caused trouble here before. A capability this dependent on the
18
+ # runtime is a runtime-owned one that the Postgres adapter CALLS, not
19
+ # a ports-level module that happens to reach sideways into it.
20
+ #
21
+ # Used to also distinguish a cosmetic edit from a real shape change
22
+ # here, by re-parsing the edited text — a pure quality-of-message
23
+ # nicety, not a safety property, since dropped: every tamper refusal
24
+ # reaches the same generic wording now, the one this already fell to
25
+ # whenever it couldn't classify an edit anyway. An operator judges
26
+ # "did this matter" themselves,
27
+ # reading the still-archived original — an anomalous recovery moment
28
+ # already, not a normal boot path.
29
+ module EraTamper
30
+ module_function
31
+
32
+ def refusal(domain:, ordinal:)
33
+ "cannot boot #{domain}: the held text of era #{ordinal} was edited after it was frozen — " \
34
+ "held era texts are storage facts; restore the original text, or reset the data"
35
+ end
36
+
37
+ # The storage-shape projection of a bluebook text, JSON-normalized
38
+ # for structural comparison against a stored projection; nil when
39
+ # the text does not load. ALWAYS parsed through a fresh tempfile,
40
+ # never the held file's own path: the predicate extractor caches
41
+ # source by path, and a held path whose content has changed (the
42
+ # very situation this module exists for) would hand it stale
43
+ # lines.
44
+ def project(text, _source_path = nil)
45
+ file = Tempfile.new(["hecks-tamper-", ".bluebook"])
46
+ begin
47
+ file.write(text)
48
+ file.flush
49
+ bluebook = EraGuard.shadow_parse(text, file.path)
50
+ ensure
51
+ file.close!
52
+ end
53
+ return nil unless bluebook
54
+
55
+ JSON.parse(JSON.generate(StorageShape.project(bluebook)))
56
+ rescue StandardError, SyntaxError
57
+ nil
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,100 @@
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
+
55
+ # A Lambda-routed domain's own refusal (rust/host, `Runtime::
56
+ # RemoteDispatcher`), carrying Rust's own refusal text verbatim —
57
+ # NOT yet mapped back to the specific matching class above
58
+ # (GivenNotMet vs. EnsuresNotMet vs. ...), a real, known,
59
+ # documented gap: the WASM projector's own event/refusal-wording
60
+ # parity work (ADR 0021) makes the TEXT match Ruby's, but nothing
61
+ # yet parses that text back into a typed Ruby exception the way a
62
+ # local dispatch already raises one directly. Callers that only
63
+ # need "the domain said no" (not which specific rule) are
64
+ # unaffected; callers pattern-matching a SPECIFIC refusal class
65
+ # against a Lambda-routed domain are the ones this gap would bite.
66
+ class RemoteRefusal < StandardError; end
67
+
68
+ # The domain saying NO — the errors a reaction may legitimately meet and
69
+ # record as an undelivered outcome. A policy whose target refuses is a fact
70
+ # about the domain ; the originating command still stands.
71
+ #
72
+ # Everything ELSE is a defect : a NoMethodError in an interpreter, a
73
+ # NameError from a missing constant, a TypeError from a bad assumption. A
74
+ # blanket `rescue StandardError` used to fold both into one line —
75
+ # `delivered: false, reason: "..."` — so a crash in the runtime was
76
+ # indistinguishable from a rule doing its job, and read as normal operation
77
+ # in the log.
78
+ #
79
+ # UnknownVerb IS one of these, and deliberately : a cross-domain policy
80
+ # (`across "Notifications"`) fires in deployments where that domain is not
81
+ # loaded, and recording the undelivered reaction rather than raising is the
82
+ # design — spec/policy_spec states it in so many words, "records a reaction
83
+ # it cannot deliver rather than swallowing it".
84
+ # InvariantViolation belongs here and was missing. A value object refusing
85
+ # its own rule is the domain saying no as plainly as a given is — but the
86
+ # class is declared over in value.rb and never made the list, so the policy
87
+ # and saga interpreters, which rescue exactly these, would let it propagate
88
+ # as though the RUNTIME had broken. A reaction whose target violates an
89
+ # invariant is declined, not crashed. Found by spec/domain_refusal_spec on
90
+ # its first run : every corpus refusal must be a class named here, and 23
91
+ # of banking's were InvariantViolation.
92
+ # THE NAMES COME FROM THE LANGUAGE, the classes from this module.
93
+ # `DomainRefusal` declares WHICH refusals are the domain's own —
94
+ # a rule the caller broke — as against a runtime fault. Resolving
95
+ # each name here means a refusal declared but never defined fails
96
+ # at load with a NameError, rather than being quietly absent from
97
+ # a list nothing re-checks.
98
+ DOMAIN_REFUSALS = Hecks::Vocabulary.fetch("DomainRefusal").map { |name| const_get(name) }.freeze
99
+ end
100
+ 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,128 @@
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
+ # The head names the ATTRIBUTE and is consumed by whoever looked the value
27
+ # up; what is left is the walk down into it. A path with no fields to walk
28
+ # — an aggregate that declares no identity and falls back to `id` — hands
29
+ # back what it was given, because there is nothing declared to dig for.
30
+ def scalar(path, held)
31
+ _head, *fields = path.to_s.split(".")
32
+ return held if fields.empty?
33
+
34
+ fields.reduce(Value.materialize(held)) do |dug, field|
35
+ dug.is_a?(Hash) ? (dug[field.to_sym] || dug[field]) : nil
36
+ end
37
+ end
38
+
39
+ # THE IDENTITY IS THE JOIN OF ITS PARTS, in declaration order. Shared by
40
+ # `CommandInterpreter` (an aggregate acting on itself) and
41
+ # `EntityInterpreter` (a piece addressed through its aggregate) — a piece
42
+ # declares an identity the same shape a head does, so it derives one the
43
+ # same way. `construct` answers `identity_paths` / `identity_heads` /
44
+ # `attribute` (an Aggregate or an Entity, either one) ; `value_owner`
45
+ # answers for coercion (`Value.for_attribute`'s first argument), which for
46
+ # an entity is its OWNING aggregate — an entity's value objects resolve
47
+ # through the aggregate's namespace, not its own.
48
+ #
49
+ # A part the payload does not carry makes the WHOLE identity unresolvable,
50
+ # rather than half of one. Half an identity names nothing, and joining what
51
+ # did arrive would silently name a different record on every dispatch — the
52
+ # precise failure that minting an id caused, arrived at by another road.
53
+ def of(construct, args, value_owner: construct)
54
+ paths = construct.identity_paths
55
+ return nil if paths.empty?
56
+
57
+ parts = paths.map { |path| from(construct, args, path, value_owner: value_owner) }
58
+ # A BLANK PART NAMES NOTHING, the same as an ABSENT one — AN ID IS A
59
+ # SCALAR, and "" is not a fact about anything. This used to check only
60
+ # `nil?`, so a canonical text extracted as "" (an expression whose
61
+ # source did not survive extraction) resolved to a REAL, empty-string
62
+ # identity — a record addressable by an id no caller could have meant.
63
+ return nil if parts.any? { |part| part.nil? || (part.respond_to?(:empty?) && part.empty?) }
64
+
65
+ Naming.identity(parts)
66
+ end
67
+
68
+ # A path digs into the value object that carries the identity, so what is
69
+ # stored is the SCALAR inside it rather than the object serialised whole.
70
+ def from(construct, args, key, value_owner: construct)
71
+ return nil unless key
72
+
73
+ head, *rest = key.to_s.split(".")
74
+ head = head.to_sym
75
+ return nil unless args.key?(head)
76
+
77
+ unless rest.empty?
78
+ held = args[head]
79
+ held = held.to_h if held.respond_to?(:to_h)
80
+ # AN ID IS ALWAYS A SCALAR. The path says WHICH FIELD carries it, so a
81
+ # caller may hand that field's value straight over — a string or a
82
+ # number, never a serialised object. Only a value object that actually
83
+ # arrived whole has to be opened.
84
+ return held.to_s unless held.is_a?(Hash)
85
+
86
+ return rest.reduce(held) { |h, f| h.is_a?(Hash) ? (h[f.to_sym] || h[f]) : nil }&.to_s
87
+ end
88
+
89
+ # Coerced against the identity ATTRIBUTE only when the caller actually
90
+ # named it. A saga addresses an aggregate by its correlation key, and
91
+ # that key carries the id ALREADY RESOLVED — coercing "w1" against a
92
+ # WireReference asked the caller to pass fields for a value object they
93
+ # never mentioned.
94
+ attribute = construct.identity_heads.include?(head) ? construct.attribute(head) : nil
95
+ raw = args[head]
96
+ return raw unless attribute
97
+
98
+ # AN ID IS ALWAYS A SCALAR — same contract the dotted branch above
99
+ # already keeps, just reached a different way here: a BARE
100
+ # (undotted) identity path names one of THIS construct's own
101
+ # declared attributes directly, and when that attribute's type is
102
+ # a value object (Translation's own compound `identified_by
103
+ # :domain, :from, :to`, each typed `TranslationDomainName`/
104
+ # `TranslationEraName`), `Value.for_attribute` coerces it into a
105
+ # real single-field Value wrapper — never unwrapped before this,
106
+ # so `Naming.identity`'s own plain `Array#join` (`Naming.identity`'s
107
+ # own header: parts must already be scalars) fell through to
108
+ # Ruby's default `Object#to_s`, leaking a raw, run-to-run-random
109
+ # memory address (`#<Hecks::Runtime::Value:0x...>`) into
110
+ # every refusal quoting this identity — found live via bin/fuzz on
111
+ # the self-hosted "translation" domain (replay_is_deterministic:
112
+ # the SAME address never repeats, so two replays of the
113
+ # identical steps produced different histories the moment a
114
+ # Translation went missing). `materialize_unwrapped` is the
115
+ # SAME single-field-VO-recurses-to-its-bare-scalar helper
116
+ # `read_model_interpreter.rb` already uses for exactly this
117
+ # unwrap; passthrough for anything that isn't a Value at all.
118
+ Value.materialize_unwrapped(Value.for_attribute(value_owner, attribute, raw)).to_s
119
+ end
120
+
121
+ # How an identity READS when the runtime has to name it in a refusal — the
122
+ # paths as they were declared, so the message quotes the bluebook back.
123
+ def reading(construct)
124
+ construct.identity_paths.join(", ")
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,120 @@
1
+ require_relative "value"
2
+
3
+ module Hecks
4
+ module Runtime
5
+ class Instance
6
+ attr_reader :aggregate, :id
7
+ attr_accessor :state
8
+
9
+ def initialize(aggregate:, id:, state: nil)
10
+ @aggregate = aggregate
11
+ @id = id
12
+ @state = state ? self.class.hydrate_with_defaults(aggregate, state) : self.class.defaults(aggregate)
13
+ materialize_identity!
14
+ end
15
+
16
+ # Loading existing state runs the same default-fill a fresh instance
17
+ # gets: an attribute the record predates — a newly-required field
18
+ # with a declared default:, a list added since the record was
19
+ # written — arrives filled instead of nil. Only declared defaults
20
+ # fill in; an attribute with no default stays absent, exactly as
21
+ # stored.
22
+ def self.hydrate_with_defaults(aggregate, state)
23
+ hydrated = Value.hydrate(aggregate, state)
24
+ defaults(aggregate).each do |name, value|
25
+ hydrated[name] = value unless value.nil? || hydrated.key?(name)
26
+ end
27
+ hydrated
28
+ end
29
+
30
+ def self.defaults(aggregate)
31
+ state = aggregate.attributes.each_with_object({}) do |attr, acc|
32
+ # FROZEN, like a list that has had something appended to it.
33
+ # An untouched list is the easiest one to miss and the easiest
34
+ # to mutate: nothing has replaced it yet, so a caller pushing
35
+ # into it writes straight into the aggregate's own state.
36
+ acc[attr.name] = attr.list? ? Freezer.deep([]) : default_for(aggregate, attr)
37
+ end
38
+ state[aggregate.lifecycle.field.to_sym] = aggregate.lifecycle.default if aggregate.lifecycle
39
+ state
40
+ end
41
+
42
+ def self.default_for(aggregate, attribute)
43
+ return Value.for_attribute(aggregate, attribute, attribute.default) unless attribute.default.nil?
44
+ # An entity's members hydrate through the same path but an entity
45
+ # declares no value objects of its own — nothing to default-build.
46
+ return nil unless aggregate.respond_to?(:value_object)
47
+
48
+ value_object = aggregate.value_object(attribute.type)
49
+ return nil unless value_object && value_object.attributes.all? { |field| !field.default.nil? }
50
+
51
+ Value.build(value_object, {}, aggregate)
52
+ end
53
+
54
+ def [](name) = @state[name.to_sym]
55
+
56
+ def key?(name) = @state.key?(name.to_sym)
57
+
58
+ def []=(name, value)
59
+ @state[name.to_sym] = value
60
+ end
61
+
62
+ def method_missing(name, *args)
63
+ return @state[name] if @state.key?(name)
64
+
65
+ super
66
+ end
67
+
68
+ def respond_to_missing?(name, include_private = false)
69
+ @state.key?(name) || super
70
+ end
71
+
72
+ # `id: @id` LAST, not first — see Facade::Handle#to_h's own comment
73
+ # for the full story (the same fix, landed there first): an
74
+ # aggregate free to declare its own attribute literally named `id`
75
+ # (BurningManPrep's `Item`, `attribute :id, ItemId`) has that
76
+ # attribute's own wrapped value object sitting in `@state[:id]` —
77
+ # merging `@state` on top of `{ id: @id }` let it silently clobber
78
+ # the correct bare identity. `@id` merged last always wins.
79
+ def to_h = @state.merge(id: @id)
80
+
81
+ # A COPY A MUTATION MAY TOUCH. Every adapter but Memory hands `find`
82
+ # a freshly-decoded Instance already; Memory's holds the record it
83
+ # eventually saves — the SAME state Hash, aliased. Before `ensures`
84
+ # existed, nothing could refuse between apply_mutations and save, so
85
+ # that aliasing was invisible: a dispatch either ran to completion or
86
+ # raised before touching state at all. `ensures` is the first refusal
87
+ # to sit AFTER mutation, and it found the bug the moment it did — an
88
+ # in-memory record left half-mutated by a dispatch that then refused.
89
+ # `command_interpreter`/`entity_interpreter` hydrate an EXISTING
90
+ # record through this, never through the adapter's own return value
91
+ # directly, so a refused ensures leaves the stored record untouched
92
+ # regardless of which adapter is holding it.
93
+ def dup
94
+ copy = super
95
+ copy.state = @state.dup
96
+ copy
97
+ end
98
+
99
+ def inspect
100
+ fields = @state.map { |k, v| "#{k}=#{v.inspect}" }.join(" ")
101
+ "#<#{@aggregate.hecks_name} #{@id} #{fields}>"
102
+ end
103
+
104
+ private
105
+
106
+ def materialize_identity!
107
+ identity = @aggregate.identified_by || :id
108
+ attribute = @aggregate.attribute(identity)
109
+ return unless attribute && @state[identity].nil?
110
+ # Several paths under one head mean the identifier is a display key,
111
+ # not a reversible serialization of the structured value object. A
112
+ # creating command supplies that object explicitly; persisted state
113
+ # hydrates it from storage. Never guess by splitting the joined id.
114
+ return if @aggregate.identity_heads.one? && @aggregate.identity_paths.size > 1
115
+
116
+ @state[identity] = Value.from_identifier(@aggregate, attribute, @id)
117
+ end
118
+ end
119
+ end
120
+ end