hecks 1.2.0 → 1.4.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 (358) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +7 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +10 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
  7. data/lib/hecks/adapters/driven/heki/journal.rb +60 -2
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
  9. data/lib/hecks/adapters/driven/heki.rb +13 -7
  10. data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
  12. data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
  13. data/lib/hecks/adapters/driven/lambda.rb +39 -33
  14. data/lib/hecks/adapters/driven/local_storage.rb +17 -10
  15. data/lib/hecks/adapters/driven/memory.rb +205 -9
  16. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
  17. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  18. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  19. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  20. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  21. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  22. data/lib/hecks/adapters/driven/postgres_era.adapter +5 -0
  23. data/lib/hecks/adapters/driven/prism.rb +4 -4
  24. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  25. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  26. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  27. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  28. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  29. data/lib/hecks/adapters/driven.rb +4 -4
  30. data/lib/hecks/adapters/driving/github_webhook.rb +145 -0
  31. data/lib/hecks/behaviors/dsl.rb +2 -2
  32. data/lib/hecks/behaviors/expectations.rb +51 -23
  33. data/lib/hecks/behaviors/ir.rb +1 -1
  34. data/lib/hecks/behaviors/rspec.rb +1 -1
  35. data/lib/hecks/behaviors/runner.rb +2 -2
  36. data/lib/hecks/behaviors.rb +1 -1
  37. data/lib/hecks/bluebook/aggregate.rb +13 -13
  38. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
  39. data/lib/hecks/bluebook/assembly/build.rb +1 -1
  40. data/lib/hecks/bluebook/assembly/contract.rb +39 -16
  41. data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
  42. data/lib/hecks/bluebook/assembly/marks.rb +18 -18
  43. data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
  44. data/lib/hecks/bluebook/assembly.rb +9 -9
  45. data/lib/hecks/bluebook/attribute.rb +9 -9
  46. data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
  47. data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
  48. data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
  49. data/lib/hecks/bluebook/behaviour/command.rb +23 -23
  50. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -3
  51. data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
  52. data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
  53. data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
  54. data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
  55. data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
  56. data/lib/hecks/bluebook/behaviour/query.rb +1 -1
  57. data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
  58. data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
  59. data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
  60. data/lib/hecks/bluebook/capabilities.rb +27 -0
  61. data/lib/hecks/bluebook/chapter.rb +28 -9
  62. data/lib/hecks/bluebook/command.rb +12 -12
  63. data/lib/hecks/bluebook/domain_port.rb +9 -9
  64. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  65. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  66. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
  67. data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
  68. data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
  69. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
  70. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
  71. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  72. data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
  73. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  74. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  75. data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
  76. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  77. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
  78. data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
  79. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  80. data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
  81. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  82. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  83. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
  84. data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
  85. data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
  86. data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
  87. data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
  88. data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
  89. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  90. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  91. data/lib/hecks/bluebook/entity.rb +11 -11
  92. data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
  93. data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
  94. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
  95. data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
  96. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
  97. data/lib/hecks/bluebook/expression/resolver.rb +60 -62
  98. data/lib/hecks/bluebook/hexagon.rb +1 -1
  99. data/lib/hecks/bluebook/lifecycle.rb +1 -1
  100. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
  101. data/lib/hecks/bluebook/meta_validator/judge.rb +122 -86
  102. data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
  103. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
  104. data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
  105. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
  106. data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
  107. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
  108. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
  109. data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
  110. data/lib/hecks/bluebook/meta_validator.rb +70 -70
  111. data/lib/hecks/bluebook/model_check.rb +301 -84
  112. data/lib/hecks/bluebook/pattern_subset.rb +9 -9
  113. data/lib/hecks/bluebook/policy.rb +15 -13
  114. data/lib/hecks/bluebook/process_manager.rb +14 -14
  115. data/lib/hecks/bluebook/project_register.rb +6 -6
  116. data/lib/hecks/bluebook/query.rb +4 -4
  117. data/lib/hecks/bluebook/read_model.rb +14 -14
  118. data/lib/hecks/bluebook/reference.rb +8 -8
  119. data/lib/hecks/bluebook/smoke_test.rb +19 -19
  120. data/lib/hecks/bluebook/synthesizer.rb +12 -12
  121. data/lib/hecks/bluebook/translation.rb +4 -4
  122. data/lib/hecks/bluebook/value_object.rb +6 -6
  123. data/lib/hecks/bluebook.rb +2 -2
  124. data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
  125. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
  126. data/lib/hecks/codemod.rb +36 -35
  127. data/lib/hecks/construct.rb +6 -6
  128. data/lib/hecks/corpus.rb +317 -0
  129. data/lib/hecks/deprecation.rb +95 -0
  130. data/lib/hecks/doc/reference.rb +19 -19
  131. data/lib/hecks/embryonaut_bluebook.rb +11 -11
  132. data/lib/hecks/facade/cli_door.rb +69 -10
  133. data/lib/hecks/facade/cli_runner.rb +105 -24
  134. data/lib/hecks/facade/command_request.rb +23 -0
  135. data/lib/hecks/facade/handle.rb +79 -32
  136. data/lib/hecks/facade/json_door.rb +106 -25
  137. data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
  138. data/lib/hecks/facade/surface/chapter.rb +26 -17
  139. data/lib/hecks/facade/surface.rb +16 -3
  140. data/lib/hecks/facade.rb +15 -4
  141. data/lib/hecks/forms/app.rb +46 -30
  142. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  143. data/lib/hecks/forms/field_renderer.rb +142 -6
  144. data/lib/hecks/forms/field_shape.rb +183 -20
  145. data/lib/hecks/forms/html.rb +51 -7
  146. data/lib/hecks/forms/index_renderer.rb +14 -2
  147. data/lib/hecks/forms/params.rb +120 -23
  148. data/lib/hecks/forms/port_argument.rb +2 -2
  149. data/lib/hecks/forms/query_form_renderer.rb +2 -2
  150. data/lib/hecks/forms/record_renderer.rb +2 -2
  151. data/lib/hecks/forms/record_table.rb +1 -1
  152. data/lib/hecks/forms/value_object_shape.rb +3 -3
  153. data/lib/hecks/forms.rb +24 -4
  154. data/lib/hecks/fqn.rb +1 -1
  155. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  156. data/lib/hecks/framework.rb +48 -17
  157. data/lib/hecks/freezer.rb +11 -11
  158. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
  159. data/lib/hecks/fuzzing/combination_miner.rb +119 -0
  160. data/lib/hecks/fuzzing/concurrent_dispatch.rb +361 -0
  161. data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
  162. data/lib/hecks/fuzzing/differential.rb +158 -0
  163. data/lib/hecks/fuzzing/domain_generator.rb +694 -0
  164. data/lib/hecks/fuzzing/era_boundary.rb +124 -0
  165. data/lib/hecks/fuzzing/form_census.rb +199 -0
  166. data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
  167. data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
  168. data/lib/hecks/fuzzing/isolated_boot.rb +226 -38
  169. data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
  170. data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
  171. data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
  172. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
  173. data/lib/hecks/fuzzing/properties/guards.rb +129 -26
  174. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
  175. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
  176. data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
  177. data/lib/hecks/fuzzing/properties/querying.rb +17 -17
  178. data/lib/hecks/fuzzing/properties.rb +84 -39
  179. data/lib/hecks/fuzzing/qa_settings.rb +152 -0
  180. data/lib/hecks/fuzzing/replay.rb +224 -114
  181. data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
  182. data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
  183. data/lib/hecks/fuzzing/self_consistency.rb +676 -0
  184. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
  189. data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
  190. data/lib/hecks/fuzzing/shrinker.rb +197 -0
  191. data/lib/hecks/fuzzing/structural_skips.rb +39 -0
  192. data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
  193. data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
  194. data/lib/hecks/fuzzing/value_generator.rb +66 -14
  195. data/lib/hecks/fuzzing.rb +11 -0
  196. data/lib/hecks/grammar/evolve.rb +10 -10
  197. data/lib/hecks/grammar.rb +7 -7
  198. data/lib/hecks/ir.rb +13 -13
  199. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  200. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  201. data/lib/hecks/language/bluebook/vocabulary.bluebook +365 -15
  202. data/lib/hecks/language/oidc.json +5 -0
  203. data/lib/hecks/literal.rb +9 -9
  204. data/lib/hecks/naming.rb +89 -21
  205. data/lib/hecks/ports/access_control.rb +58 -2
  206. data/lib/hecks/ports/agent/answers.rb +83 -6
  207. data/lib/hecks/ports/agent.rb +119 -35
  208. data/lib/hecks/ports/authentication.rb +44 -4
  209. data/lib/hecks/ports/authorization.rb +53 -11
  210. data/lib/hecks/ports/clock.rb +42 -23
  211. data/lib/hecks/ports/extraction.rb +16 -0
  212. data/lib/hecks/ports/identity_assignment.rb +24 -2
  213. data/lib/hecks/ports/identity_generation.rb +17 -3
  214. data/lib/hecks/ports/identity_resolution.rb +18 -1
  215. data/lib/hecks/ports/loading.rb +4 -0
  216. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  217. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  218. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  219. data/lib/hecks/ports/persistence/execution.rb +4 -0
  220. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  221. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  222. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  223. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  224. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  225. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  226. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  227. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  228. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  229. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  230. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  231. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +160 -58
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +44 -6
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +338 -83
  242. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  243. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  244. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  245. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
  246. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  247. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  248. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  249. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
  250. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
  251. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
  252. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  253. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  254. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  255. data/lib/hecks/ports/persistence/repository_factory.rb +29 -6
  256. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  257. data/lib/hecks/ports/persistence.rb +36 -1
  258. data/lib/hecks/ports/projection.rb +61 -7
  259. data/lib/hecks/ports/query/in_memory.rb +3 -3
  260. data/lib/hecks/ports/query/ordering.rb +6 -6
  261. data/lib/hecks/ports/query.rb +35 -0
  262. data/lib/hecks/projections/bootstrap_table.rb +112 -0
  263. data/lib/hecks/projections/diagrams.rb +75 -75
  264. data/lib/hecks/projections/glossary/html.rb +250 -0
  265. data/lib/hecks/projections/glossary/markdown.rb +105 -0
  266. data/lib/hecks/projections/glossary/mermaid.rb +110 -0
  267. data/lib/hecks/projections/glossary/page.css +271 -0
  268. data/lib/hecks/projections/glossary/page.js +72 -0
  269. data/lib/hecks/projections/glossary/sections.rb +17 -0
  270. data/lib/hecks/projections/glossary/sentences.rb +205 -0
  271. data/lib/hecks/projections/glossary.rb +214 -286
  272. data/lib/hecks/projections/ir.rb +1 -1
  273. data/lib/hecks/projections/model/deviations.rb +18 -17
  274. data/lib/hecks/projections/model.rb +25 -21
  275. data/lib/hecks/projections/oidc.rb +7 -7
  276. data/lib/hecks/projections/parser_table.rb +5 -5
  277. data/lib/hecks/projections/reference.rb +3 -3
  278. data/lib/hecks/projections/rust_vocabulary.rb +443 -0
  279. data/lib/hecks/projections/shape.rb +2 -2
  280. data/lib/hecks/projections/statements.rb +11 -11
  281. data/lib/hecks/projections/vocabulary.rb +9 -9
  282. data/lib/hecks/projections.rb +5 -3
  283. data/lib/hecks/projector/cli_projector.rb +29 -29
  284. data/lib/hecks/projector/docs_projector.rb +13 -13
  285. data/lib/hecks/projector/exporter.rb +42 -21
  286. data/lib/hecks/projector/ir_projector.rb +1 -1
  287. data/lib/hecks/projector/narrate_projector.rb +15 -22
  288. data/lib/hecks/projector/target.rb +13 -13
  289. data/lib/hecks/projector.rb +15 -15
  290. data/lib/hecks/query_ir.rb +47 -47
  291. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  292. data/lib/hecks/query_specification/common/comparison.rb +132 -24
  293. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  294. data/lib/hecks/query_specification/common/null_policy.rb +57 -13
  295. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  296. data/lib/hecks/query_specification/common/options.rb +25 -0
  297. data/lib/hecks/query_specification/field_path.rb +69 -15
  298. data/lib/hecks/query_specification/hop_path.rb +57 -20
  299. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  300. data/lib/hecks/rendering.rb +3 -3
  301. data/lib/hecks/router/namespace_installer.rb +3 -3
  302. data/lib/hecks/router.rb +1 -1
  303. data/lib/hecks/runtime/aggregate_lock.rb +11 -11
  304. data/lib/hecks/runtime/boot_gates.rb +3 -3
  305. data/lib/hecks/runtime/caller.rb +8 -8
  306. data/lib/hecks/runtime/capability_graph.rb +2 -2
  307. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
  308. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +64 -67
  309. data/lib/hecks/runtime/command_interpreter.rb +159 -102
  310. data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
  311. data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
  312. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  313. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  314. data/lib/hecks/runtime/command_rules/references.rb +94 -19
  315. data/lib/hecks/runtime/command_rules.rb +3 -0
  316. data/lib/hecks/runtime/dependency_planning.rb +11 -11
  317. data/lib/hecks/runtime/dispatcher.rb +283 -90
  318. data/lib/hecks/runtime/entity_element.rb +282 -46
  319. data/lib/hecks/runtime/entity_interpreter.rb +160 -69
  320. data/lib/hecks/runtime/errors.rb +19 -19
  321. data/lib/hecks/runtime/event.rb +6 -6
  322. data/lib/hecks/runtime/identity.rb +22 -22
  323. data/lib/hecks/runtime/instance.rb +39 -14
  324. data/lib/hecks/runtime/interpreting.rb +12 -12
  325. data/lib/hecks/runtime/invocation.rb +276 -0
  326. data/lib/hecks/runtime/loader.rb +14 -14
  327. data/lib/hecks/runtime/outbox.rb +23 -23
  328. data/lib/hecks/runtime/policy_interpreter.rb +54 -54
  329. data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
  330. data/lib/hecks/runtime/query_interpreter.rb +111 -56
  331. data/lib/hecks/runtime/reaction_invocation.rb +76 -9
  332. data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
  333. data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
  334. data/lib/hecks/runtime/reference_hop.rb +6 -6
  335. data/lib/hecks/runtime/refusal_wording.rb +92 -112
  336. data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
  337. data/lib/hecks/runtime/registry/verification.rb +36 -26
  338. data/lib/hecks/runtime/registry.rb +56 -27
  339. data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
  340. data/lib/hecks/runtime/routing.rb +10 -88
  341. data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
  342. data/lib/hecks/runtime/saga_interpreter.rb +93 -56
  343. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  344. data/lib/hecks/runtime/tenant_check.rb +9 -9
  345. data/lib/hecks/runtime/tenant_scope.rb +5 -5
  346. data/lib/hecks/runtime/value/admission.rb +75 -30
  347. data/lib/hecks/runtime/value/coercion.rb +379 -226
  348. data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
  349. data/lib/hecks/runtime/value.rb +28 -23
  350. data/lib/hecks/runtime.rb +7 -7
  351. data/lib/hecks/storehouse.rb +64 -64
  352. data/lib/hecks/version.rb +3 -3
  353. data/lib/hecks/vocabulary.rb +207 -5
  354. data/lib/hecks.rb +13 -11
  355. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
  356. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
  357. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
  358. metadata +43 -2
@@ -11,7 +11,7 @@ module Hecks
11
11
  module Ports
12
12
  module Persistence
13
13
  module Plugins
14
- # ADR 0033 — requiring THIS FILE is installing the plugin. Nothing in
14
+ # ADR 0033 — requiring this file is installing the plugin. Nothing in
15
15
  # Hecks core requires it; an app that binds `PostgresEra`, or wants
16
16
  # schema-translation support at all, requires it explicitly — the
17
17
  # same shape every adapter-specific spec fixture already uses to
@@ -19,6 +19,8 @@ module Hecks
19
19
  module Era
20
20
  module_function
21
21
 
22
+ # Registers this plugin's era gates on one boot's gate list.
23
+ #
22
24
  # `Runtime::Loader.run_boot_gates!` asks every loaded persistence
23
25
  # plugin to contribute here, generically — it never mentions
24
26
  # `EraCheck` or "era" by name. Two gates, both `:pre_verify`:
@@ -29,11 +31,19 @@ module Hecks
29
31
  # unchanged) — this is the rich, adapter-aware version of that
30
32
  # check; `Runtime::Loader`'s own structural backstop (plain
31
33
  # `Bluebook::Translation` data, no plugin-specific class) only
32
- # ever fires when NO persistence plugin is loaded at all.
34
+ # ever fires when no persistence plugin is loaded at all.
33
35
  #
34
36
  # `:era_check` — conditional, exactly ADR 0031's own gate,
35
37
  # unchanged: registered only when this registry has an aggregate
36
38
  # actually bound to a lineage-capable adapter.
39
+ #
40
+ # @param registry [Runtime::Registry] the registry being booted, asked whether any
41
+ # bluebook's first aggregate is bound to a lineage-capable adapter
42
+ # @param gates [Runtime::BootGates] this boot's gate list, registered onto in place
43
+ # @return [Runtime::BootGates, nil] `gates` when `:era_check` was registered; nil when
44
+ # the registry binds nothing lineage-capable, so only `:era_compute_rules` was added
45
+ # @raise [Runtime::WiringError] if an aggregate's persistence binding is missing,
46
+ # ambiguous, or carries an unsupported role
37
47
  def contribute_boot_gates(registry, gates)
38
48
  gates.register(:era_compute_rules, lambda { |reg, _dir|
39
49
  Runtime::EraCheck.check_compute_rules_for_registry!(reg)
@@ -3,12 +3,12 @@ require_relative "../../runtime/registry"
3
3
  module Hecks
4
4
  module Ports
5
5
  module Persistence
6
- # THE OTHER SHAPE AN ADAPTER CAN BE. `AppendOnly` names one shape
6
+ # The other shape an adapter can be. `AppendOnly` names one shape
7
7
  # already — something that stores bytes, locally (Postgres, SQLite,
8
8
  # Heki) or over the network (D1), doesn't matter, the point is it
9
9
  # does real local interpretation and has real entries to replay.
10
10
  # This names the second, different shape: not "storage reached
11
- # remotely," but the real INTERPRETER itself living behind a call
11
+ # remotely," but the real interpreter itself living behind a call
12
12
  # boundary — `append`/`project` don't do partial local work and
13
13
  # then fail, they raise unconditionally, because there is no local
14
14
  # write-ahead log to have; `entries` is always `[]` for the same
@@ -28,6 +28,10 @@ module Hecks
28
28
  # `Runtime::RemoteDispatcher`'s own use) instead of comparing
29
29
  # adapter names by string.
30
30
  module RemoteRuntime
31
+ # Refuses every local write; `project` is an alias and refuses the same way.
32
+ #
33
+ # @return [void] never returns
34
+ # @raise [Runtime::WiringError] always, pointing the caller at `Runtime::RemoteDispatcher`
31
35
  def append(*)
32
36
  raise Runtime::WiringError,
33
37
  "#{self.class.name} is a remote-runtime delegate — dispatch through " \
@@ -35,6 +39,9 @@ module Hecks
35
39
  end
36
40
  alias project append
37
41
 
42
+ # Reports an empty journal, since a remote-runtime delegate keeps no local log.
43
+ #
44
+ # @return [Array] always `[]`
38
45
  def entries = []
39
46
  end
40
47
  end
@@ -1,27 +1,50 @@
1
1
  require_relative "append_only"
2
+ require_relative "codec_boundary"
2
3
 
3
4
  module Hecks
4
5
  module Ports
5
6
  module Persistence
6
7
  # Turns a declared adapter binding plus its world configuration into a
7
8
  # concrete repository. Selection policy stays out of adapter creation.
9
+ #
10
+ # Every adapter it builds is guarded (`CodecBoundary.guard!`) before
11
+ # anything else touches it — `recover!` included — so no adapter a
12
+ # runtime reaches can build an `Instance` from undecoded state.
8
13
  module RepositoryFactory
9
14
  module_function
10
15
 
16
+ # Instantiates the adapter a bind names, guards it, and wraps it as a repository.
17
+ #
18
+ # @param registry [Runtime::Registry] the registry supplying the adapter class, the
19
+ # domain's world settings, its resolved eras and the root path
20
+ # @param domain [String, Symbol] name of the domain the aggregate belongs to
21
+ # @param aggregate [Bluebook::Aggregate] the aggregate to persist
22
+ # @param bind [Bluebook::Bind] the bind naming the adapter, as `BindingPolicy.resolve`
23
+ # or the projection port chose it
24
+ # @param recover [Boolean] true to replay the journal through `project` before returning
25
+ # @param settings_verb [String] the verb whose world settings configure the adapter;
26
+ # `"persisted_by"` by default, `Ports::Projection::VERB` for a projection
27
+ # @return [Persistence::AppendOnly] the repository over the guarded adapter
28
+ # @raise [Runtime::WiringError] if the bind's verb or settings fail the registry's
29
+ # checks, no `Hecks::Adapters` constant matches the adapter name, or the adapter
30
+ # lacks `append`, `project` or `entries`
11
31
  def build(registry, domain, aggregate, bind, recover: true, settings_verb: VERB)
12
32
  registry.check_verb(bind)
13
33
  settings = (registry.world(domain)&.for_binding(settings_verb, bind.adapter) || {})
14
34
  .reject { |key, _| key.to_sym == :role }
15
35
  registry.check_settings(bind, settings)
16
- # The domain and the resolved era ride along after the
17
- # declared-settings check: a lineage adapter journals per
18
- # DOMAIN and writes into its own ERA's partition neither of
19
- # which a world's settings carry.
36
+ # The domain, the resolved era, and (for an old checkout) the era
37
+ # that superseded it ride along after the declared-settings
38
+ # check: a lineage adapter journals per domain, writes into its
39
+ # own era's partition, and refuses to write at all once that era
40
+ # is superseded (`PostgresEra#append`, BUG#24) — none of which a
41
+ # world's settings carry.
20
42
  adapter = registry.adapter_class(bind.adapter)
21
43
  .new(aggregate: aggregate,
22
- settings: settings.merge(domain: domain.to_s, era: registry.resolved_eras[domain.to_s]),
44
+ settings: settings.merge(domain: domain.to_s, era: registry.resolved_eras[domain.to_s],
45
+ superseded_by: registry.superseded_eras[domain.to_s]),
23
46
  root: registry.root)
24
- repository = AppendOnly.new(adapter)
47
+ repository = AppendOnly.new(CodecBoundary.guard!(adapter))
25
48
  recover ? repository.recover! : repository
26
49
  end
27
50
  end
@@ -0,0 +1,319 @@
1
+ require "json"
2
+ require_relative "../../runtime/value"
3
+
4
+ module Hecks
5
+ module Ports
6
+ module Persistence
7
+ # One spelling of an aggregate's state across the store boundary
8
+ # (Phase 2, Track A, PR A2). This codec is the single, IR-driven
9
+ # answer every adapter converges on, because adapters left to decode
10
+ # their own way disagree: symbolizing the top level only (a Heki head,
11
+ # a journal reader), symbolizing deep (a SQL or Lambda head), or never
12
+ # serializing at all (Memory's shallow `state.dup`).
13
+ # spec/ports/persistence_legacy_decode_spec.rb pins that bytes written
14
+ # without the codec still decode to the one canonical shape. Every
15
+ # adapter writes through `encode` and reads through `decode`
16
+ # (Memory through `copy`; PR A3), and `CodecBoundary` — installed on every
17
+ # adapter `RepositoryFactory.build` makes — refuses an `Instance`
18
+ # built inside an adapter call from state `decoded?` rejects.
19
+ #
20
+ # ## The three operations
21
+ #
22
+ # - `encode` — canonical and JSON-ready: string keys at every depth,
23
+ # `Runtime::Value`s materialized, only JSON scalars at the leaves.
24
+ # Needs no IR (JSON has one spelling), but takes it for symmetry.
25
+ # - `decode` — walks the aggregate's IR: attributes, value objects
26
+ # (their fields, recursively), `list_of` value objects and entities
27
+ # (entity fields, nested entities, the entity's own lifecycle),
28
+ # references, the lifecycle field, and projected fields. A declared
29
+ # key becomes a symbol at every depth, whichever spelling arrived.
30
+ # - `copy` — `decode(encode(state))`: what a durable adapter would hand
31
+ # back, for Memory, with no JSON text in between.
32
+ #
33
+ # ## What decode never does
34
+ #
35
+ # It never invents a key. A declared field absent from the stored
36
+ # state stays absent — not a present nil — because the runtime reads
37
+ # absence as "this record predates the field": `Instance.
38
+ # hydrate_with_defaults` fills a declared `default:` only when the key
39
+ # is missing (spec/runtime/hydrate_defaults_spec.rb), Era translation
40
+ # backfills only `unless state.key?` (era/lineage.rb#translate), and a
41
+ # required declared-but-absent field reads as a named refusal rather
42
+ # than nil (spec/runtime/attribute_absence_spec.rb). A present nil
43
+ # would silently suppress all three. For the same reason it never
44
+ # drops a key, nil or not: a stored nil stays a stored nil.
45
+ #
46
+ # It never touches an undeclared key's value — a retired field, or a
47
+ # member a value object no longer declares, is exactly what an Era
48
+ # translation (rename/move/drop) still has to read. Its key keeps its
49
+ # spelling below the top level; at the top level every key is a
50
+ # symbol, declared or not, because every adapter has always
51
+ # symbolized the top level and `Lineage#translate` reads retired
52
+ # top-level names as symbols.
53
+ #
54
+ # ## When both spellings arrive
55
+ #
56
+ # When a hash carries both spellings of one declared key, the symbol
57
+ # spelling wins: it can only have been written by Ruby after the
58
+ # string one was read.
59
+ module StateCodec
60
+ JSON_SCALARS = [String, Integer, Float, TrueClass, FalseClass, NilClass].freeze
61
+
62
+ module_function
63
+
64
+ # Converts state into its canonical JSON-ready form for a durable adapter to write.
65
+ #
66
+ # @param _aggregate [Bluebook::Aggregate, Bluebook::Entity] unused; taken so `encode`
67
+ # and `decode` have one signature
68
+ # @param state [Hash, Runtime::Value, nil] the state to store; nil for a delete entry
69
+ # @return [Hash{String => Object}, nil] a new Hash with String keys at every depth and
70
+ # only JSON scalars, Arrays and Hashes below; nil when `state` is nil
71
+ def encode(_aggregate, state) = encode_value(state)
72
+
73
+ # Respells stored state into the declared shape by walking the aggregate's IR.
74
+ #
75
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct whose
76
+ # declarations name the keys to symbolize
77
+ # @param raw [Hash, Object, nil] parsed stored state, with keys in either spelling
78
+ # @return [Hash{Symbol => Object}, Object, nil] a new Hash with every top-level key and
79
+ # every declared nested key a Symbol; anything that is not a Hash is returned as given
80
+ def decode(aggregate, raw)
81
+ return raw unless raw.is_a?(Hash)
82
+
83
+ fields = declared_top_level(aggregate)
84
+ decode_hash(aggregate, fields, raw, symbolize_undeclared: true)
85
+ end
86
+
87
+ # Deep-copies state into the shape a durable adapter would read back, without JSON text.
88
+ #
89
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct whose
90
+ # declarations name the keys to symbolize
91
+ # @param state [Hash, Runtime::Value, nil] live state, such as `Instance#state`
92
+ # @return [Hash{Symbol => Object}, nil] `decode(encode(state))`, sharing no Hash or
93
+ # Array with `state`; nil when `state` is nil
94
+ def copy(aggregate, state) = decode(aggregate, encode(aggregate, state))
95
+
96
+ # Maps every declared top-level field name to its attribute.
97
+ #
98
+ # The field walk Sqlite::Codec#persisted_fields does for columns,
99
+ # as name => Attribute (nil for the lifecycle field and projected
100
+ # fields: bare scalars with no attribute of their own). The same
101
+ # three sources, the same precedence — an attribute that happens to
102
+ # share the lifecycle's or a projected field's name keeps its type.
103
+ # An entity (no `projected_fields` of its own) gets its entity
104
+ # field set — `decoded?` is asked about any `Instance`, and an
105
+ # entity is "structurally interchangeable with an aggregate".
106
+ #
107
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct to walk
108
+ # @return [Hash{Symbol => Bluebook::Attribute, nil}] a new Hash of field name to
109
+ # attribute; nil marks the lifecycle field or a projected field with no attribute
110
+ def declared_top_level(aggregate)
111
+ fields = entity_fields(aggregate)
112
+ return fields unless aggregate.respond_to?(:projected_fields)
113
+
114
+ aggregate.projected_fields.each { |field| fields[field.name.to_sym] = nil unless fields.key?(field.name.to_sym) }
115
+ fields
116
+ end
117
+
118
+ # Checks whether state already has the shape `decode` produces.
119
+ #
120
+ # Whether `decode` would hand `state` back unchanged — every
121
+ # top-level key a Symbol, every declared key below it a Symbol, no
122
+ # hash carrying both spellings of a declared key. Allocates
123
+ # nothing; `CodecBoundary` asks it of every `Instance` an adapter
124
+ # builds. A `Runtime::Value` (hydrated state, a save's own entry)
125
+ # already is the declared shape, so it answers true.
126
+ #
127
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct the state
128
+ # belongs to
129
+ # @param state [Hash, Runtime::Value, nil] the state to inspect
130
+ # @return [Boolean] true when `state` is decoded, and for anything that is not a Hash
131
+ def decoded?(aggregate, state)
132
+ return true unless state.is_a?(Hash)
133
+
134
+ hash_decoded?(aggregate, declared_top_level(aggregate), state, top: true)
135
+ end
136
+
137
+ # ── encode ──────────────────────────────────────────────────────
138
+
139
+ # Encodes one value, recursing through Hashes, Arrays and `Runtime::Value`s.
140
+ #
141
+ # @param value [Object] any state value; a `Runtime::Value` is encoded as its `to_h`
142
+ # @return [Hash{String => Object}, Array, String, Integer, Float, Boolean, nil] the
143
+ # JSON-ready form; a leaf outside `JSON_SCALARS` (a Symbol, a Time) becomes whatever
144
+ # a `JSON.generate` then `JSON.parse` round trip makes of it
145
+ def encode_value(value)
146
+ case value
147
+ when Runtime::Value then encode_value(value.to_h)
148
+ when Hash then value.each_with_object({}) { |(key, inner), out| out[key.to_s] = encode_value(inner) }
149
+ when Array then value.map { |inner| encode_value(inner) }
150
+ when *JSON_SCALARS then value
151
+ # A Symbol, a Time, anything else: exactly what `JSON.generate`
152
+ # then `JSON.parse` would make of it, so Memory's copy and a
153
+ # durable adapter's row agree on the leaf too.
154
+ else JSON.parse(JSON.generate([value])).first
155
+ end
156
+ end
157
+
158
+ # ── decode ──────────────────────────────────────────────────────
159
+
160
+ # Decodes one Hash level, symbolizing its declared keys and recursing into their values.
161
+ #
162
+ # `fields` is name => Attribute-or-nil for this level. Key order is
163
+ # kept; a string key is skipped when the same hash also holds its
164
+ # symbol spelling, so the symbol wins wherever either one sits.
165
+ #
166
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the root construct, through
167
+ # which nested value objects and entities resolve
168
+ # @param fields [Hash{Symbol => Bluebook::Attribute, nil}] the fields declared at
169
+ # this level
170
+ # @param raw [Hash] the stored Hash for this level
171
+ # @param symbolize_undeclared [Boolean] true to symbolize undeclared keys too, as the
172
+ # top level does; false to leave their spelling alone
173
+ # @return [Hash] a new Hash in `raw`'s key order
174
+ def decode_hash(aggregate, fields, raw, symbolize_undeclared: false)
175
+ raw.each_with_object({}) do |(key, value), out|
176
+ name = key.to_s.to_sym
177
+ next if key.is_a?(String) && raw.key?(name)
178
+
179
+ if fields.key?(name)
180
+ out[name] = decode_field(aggregate, fields[name], value)
181
+ else
182
+ out[symbolize_undeclared ? name : key] = value
183
+ end
184
+ end
185
+ end
186
+
187
+ # Decodes the stored value of one declared field.
188
+ #
189
+ # One declared field's value, by the four shapes `Value::Coercion`
190
+ # names (scalar / list / optional / composite). nil, a reference (a
191
+ # bare id or a list of them), and a scalar pass through untouched; a
192
+ # value object or entity recurses only when the stored value really
193
+ # is the Hash/Array its declaration says — anything else (a legacy
194
+ # bare scalar for a one-field value object) is left for hydration.
195
+ #
196
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the root construct, through
197
+ # which the field's type resolves
198
+ # @param attribute [Bluebook::Attribute, nil] the field's declaration; nil for a
199
+ # lifecycle or projected field
200
+ # @param value [Object, nil] the stored value
201
+ # @return [Object, nil] `value` itself when nothing needs respelling, otherwise a new
202
+ # Hash or Array of decoded composites
203
+ def decode_field(aggregate, attribute, value)
204
+ return value if attribute.nil? || value.nil? || attribute.reference?
205
+
206
+ if attribute.list?
207
+ return value unless value.is_a?(Array)
208
+
209
+ value.map { |element| decode_composite(aggregate, attribute.type.to_s, element) }
210
+ else
211
+ decode_composite(aggregate, attribute.type.to_s, value)
212
+ end
213
+ end
214
+
215
+ # Decodes one stored value object or entity, looked up by type name.
216
+ #
217
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the root construct that
218
+ # declares, or whose chapter declares, the type
219
+ # @param type [String] the declared type name, such as `"Address"`
220
+ # @param value [Object] the stored value
221
+ # @return [Hash, Object] a new decoded Hash; `value` unchanged when it is not a Hash or
222
+ # `type` names neither an entity nor an unambiguous value object
223
+ def decode_composite(aggregate, type, value)
224
+ return value unless value.is_a?(Hash)
225
+
226
+ entity = Runtime::Value.find_entity(aggregate, type)
227
+ return decode_hash(aggregate, entity_fields(entity), value) if entity
228
+
229
+ value_object = Runtime::Value.value_object_for(aggregate, type)
230
+ return value unless value_object
231
+
232
+ decode_hash(aggregate, value_object.attributes.to_h { |field| [field.name, field] }, value)
233
+ end
234
+
235
+ # Maps an entity's (or aggregate's) attribute names, plus its lifecycle field, to
236
+ # their attributes.
237
+ #
238
+ # An entity is "structurally interchangeable with an aggregate"
239
+ # (behaviour/entity.rb): its attributes plus its own lifecycle field.
240
+ # Its value objects and nested entities resolve through the root
241
+ # aggregate, the same way `EntityListCoercion#hydrate_entity_list`
242
+ # resolves them.
243
+ #
244
+ # @param entity [Bluebook::Entity, Bluebook::Aggregate] the construct to walk
245
+ # @return [Hash{Symbol => Bluebook::Attribute, nil}] a new Hash of field name to
246
+ # attribute; nil marks a lifecycle field that no attribute declares
247
+ def entity_fields(entity)
248
+ fields = entity.attributes.to_h { |attribute| [attribute.name, attribute] }
249
+ lifecycle = entity.lifecycle
250
+ fields[lifecycle.field.to_sym] = nil if lifecycle && !fields.key?(lifecycle.field.to_sym)
251
+ fields
252
+ end
253
+
254
+ # ── decoded? ────────────────────────────────────────────────────
255
+
256
+ # Checks one Hash level for keys `decode` would respell.
257
+ #
258
+ # The mirror of `decode_hash`: a key `decode` would respell (any
259
+ # non-Symbol at the top, a non-Symbol declared key below it) means
260
+ # "not decoded"; an undeclared nested key keeps whatever spelling
261
+ # it has, exactly as `decode` keeps it.
262
+ #
263
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the root construct, through
264
+ # which nested types resolve
265
+ # @param fields [Hash{Symbol => Bluebook::Attribute, nil}] the fields declared at
266
+ # this level
267
+ # @param hash [Hash] the Hash to inspect
268
+ # @param top [Boolean] true for the top level, where every key must be a Symbol
269
+ # @return [Boolean] true when this level and every declared composite below it is
270
+ # decoded
271
+ def hash_decoded?(aggregate, fields, hash, top: false)
272
+ hash.all? do |key, value|
273
+ if key.is_a?(Symbol)
274
+ !fields.key?(key) || field_decoded?(aggregate, fields[key], value)
275
+ else
276
+ !top && !fields.key?(key.to_s.to_sym)
277
+ end
278
+ end
279
+ end
280
+
281
+ # Checks the stored value of one declared field, the mirror of `decode_field`.
282
+ #
283
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the root construct, through
284
+ # which the field's type resolves
285
+ # @param attribute [Bluebook::Attribute, nil] the field's declaration; nil for a
286
+ # lifecycle or projected field
287
+ # @param value [Object, nil] the value to inspect
288
+ # @return [Boolean] true when `decode_field` would leave `value` as it is; always true
289
+ # for nil, a reference, an undeclared attribute, or a list that is not an Array
290
+ def field_decoded?(aggregate, attribute, value)
291
+ return true if attribute.nil? || value.nil? || attribute.reference?
292
+ return composite_decoded?(aggregate, attribute.type.to_s, value) unless attribute.list?
293
+
294
+ !value.is_a?(Array) || value.all? { |element| composite_decoded?(aggregate, attribute.type.to_s, element) }
295
+ end
296
+
297
+ # Checks one stored value object or entity, the mirror of `decode_composite`.
298
+ #
299
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the root construct that
300
+ # declares, or whose chapter declares, the type
301
+ # @param type [String] the declared type name
302
+ # @param value [Object] the value to inspect
303
+ # @return [Boolean] true when `value` is not a Hash, `type` names neither an entity nor
304
+ # an unambiguous value object, or every declared key in it is a Symbol
305
+ def composite_decoded?(aggregate, type, value)
306
+ return true unless value.is_a?(Hash)
307
+
308
+ entity = Runtime::Value.find_entity(aggregate, type)
309
+ return hash_decoded?(aggregate, entity_fields(entity), value) if entity
310
+
311
+ value_object = Runtime::Value.value_object_for(aggregate, type)
312
+ return true unless value_object
313
+
314
+ hash_decoded?(aggregate, value_object.attributes.to_h { |field| [field.name, field] }, value)
315
+ end
316
+ end
317
+ end
318
+ end
319
+ end
@@ -15,6 +15,7 @@ require_relative "persistence/append_only"
15
15
  require_relative "persistence/execution"
16
16
  require_relative "persistence/remote_runtime"
17
17
  require_relative "persistence/null_saga_store"
18
+ require_relative "persistence/state_codec"
18
19
 
19
20
  module Hecks
20
21
  module Ports
@@ -25,16 +26,50 @@ module Hecks
25
26
  module Persistence
26
27
  module_function
27
28
 
28
- # The public persistence port owns only authoritative aggregate heads.
29
+ # Builds the repository that reads and writes one aggregate's authoritative store.
30
+ #
31
+ # The public persistence port owns only authoritative aggregate heads. The repository
32
+ # comes back already recovered: every journal entry has been re-projected.
33
+ #
34
+ # @param registry [Runtime::Registry] the booted registry holding the domain's
35
+ # hecksagon, world and adapters
36
+ # @param domain [String, Symbol] name of the domain the aggregate belongs to
37
+ # @param aggregate [Bluebook::Aggregate] the aggregate to persist
38
+ # @return [Persistence::AppendOnly] repository over the aggregate's authoritative
39
+ # adapter, or over a `Memory` adapter when the domain declares no hecksagon
40
+ # @raise [Runtime::WiringError] if the aggregate has no authoritative bind, more than
41
+ # one, or a bind with a role this port does not support; or if the bound adapter is
42
+ # unknown, answers a different verb, is given a setting it does not declare, has no
43
+ # Ruby implementation, or lacks a method its port's `answers` list or the
44
+ # append-only contract (`append`, `project`, `entries`) requires
29
45
  def repository(registry, domain, aggregate)
30
46
  authoritative = BindingPolicy.resolve(registry, domain, aggregate)
31
47
  RepositoryFactory.build(registry, domain, aggregate, authoritative)
32
48
  end
33
49
 
50
+ # Resolves an aggregate's authoritative bind, paired with an always-empty Array.
51
+ #
52
+ # @param registry [Runtime::Registry] the booted registry holding the domain's hecksagon
53
+ # @param domain [String, Symbol] name of the domain the aggregate belongs to
54
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose binding is wanted
55
+ # @return [Array(Bluebook::Bind, Array)] the authoritative `persisted_by` bind (the
56
+ # default `Memory` bind when the domain declares no hecksagon), then an Array that
57
+ # is always `[]`
58
+ # @raise [Runtime::WiringError] if the aggregate has no authoritative bind, more than
59
+ # one, or a bind with a role this port does not support
34
60
  def binds_for(registry, domain, aggregate)
35
61
  [BindingPolicy.resolve(registry, domain, aggregate), []]
36
62
  end
37
63
 
64
+ # Resolves the one bind naming an aggregate's authoritative store.
65
+ #
66
+ # @param registry [Runtime::Registry] the booted registry holding the domain's hecksagon
67
+ # @param domain [String, Symbol] name of the domain the aggregate belongs to
68
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose binding is wanted
69
+ # @return [Bluebook::Bind] the authoritative `persisted_by` bind, or the default
70
+ # `Memory` bind when the domain declares no hecksagon
71
+ # @raise [Runtime::WiringError] if the aggregate has no authoritative bind, more than
72
+ # one, or a bind with a role this port does not support
38
73
  def bind_for(registry, domain, aggregate)
39
74
  binds_for(registry, domain, aggregate).first
40
75
  end
@@ -12,10 +12,32 @@ module Hecks
12
12
 
13
13
  module_function
14
14
 
15
+ # Lists the `projected_by` binds a domain's hecksagon declares for one aggregate.
16
+ #
17
+ # @param registry [Runtime::Registry] the booted registry to search
18
+ # @param domain [String, Symbol] name of the domain the aggregate belongs to
19
+ # @param aggregate [Bluebook::Aggregate] the aggregate to find projection binds for
20
+ # @return [Array<Bluebook::Bind>] the binds naming the aggregate, or failing that the
21
+ # hecksagon's aggregate-less `projected_by` binds; `[]` if there are none or the
22
+ # domain declares no hecksagon
15
23
  def binds_for(registry, domain, aggregate)
16
24
  registry.hecksagon(domain)&.binds_for(aggregate.hecks_name, VERB) || []
17
25
  end
18
26
 
27
+ # Builds a worker that catches one projection's store up to its authoritative journal.
28
+ #
29
+ # @param registry [Runtime::Registry] the booted registry to resolve binds against
30
+ # @param domain [String, Symbol] name of the domain the aggregate belongs to
31
+ # @param aggregate [Bluebook::Aggregate] the aggregate being projected
32
+ # @param policy [Symbol, String] `:refresh` (reset the store and rebuild it) or
33
+ # `:strict` (refuse on divergent history) — see `Worker::VALID_POLICIES`
34
+ # @return [Ports::Projection::Worker, nil] a worker for the aggregate's first declared
35
+ # bind, or nil if it has no projection bind
36
+ # @raise [Runtime::WiringError] if the aggregate's authoritative persistence bind does
37
+ # not resolve (see `Persistence.repository`), or the projection's adapter is unknown,
38
+ # answers a different verb, is given a setting it does not declare, has no Ruby
39
+ # implementation, or lacks a method the append-only contract requires
40
+ # @raise [ArgumentError] if `policy` is not one of `Worker::VALID_POLICIES`
19
41
  def worker(registry, domain, aggregate, policy: :refresh)
20
42
  bind = binds_for(registry, domain, aggregate).first
21
43
  return unless bind
@@ -32,23 +54,32 @@ module Hecks
32
54
  # separate process or scheduler, never from the command-side write
33
55
  # path.
34
56
  class Worker
57
+ # @return [Persistence::AppendOnly] the projection store this worker catches up
35
58
  attr_reader :projection
36
59
 
37
60
  # The only two policies anything in this codebase ever passes
38
61
  # (`bin/project`, every spec) — there is no third, legitimate
39
- # "lenient append" policy on record anywhere. Before this, any
40
- # value OTHER than the exact symbol `:strict` silently fell
41
- # through the `consistent?` check below and appended onto
42
- # divergent history without a word not just a real typo like
43
- # `:strikt`, but a caller-supplied String `"strict"` too (this
44
- # duck-typed fine via `policy.to_sym`, but that was luck, not a
45
- # contract: nothing here declared what a valid policy even was).
62
+ # "lenient append" policy on record anywhere. This list is the
63
+ # contract for what a valid policy is: `catch_up!` enforces
64
+ # history agreement only for the exact symbol `:strict`, so any
65
+ # other unrecognised value a typo like `:strikt` would fall
66
+ # through the `consistent?` check below and append onto
67
+ # divergent history without a word. A caller-supplied String
68
+ # `"strict"` is accepted on purpose, normalised by `policy.to_sym`
69
+ # in `initialize` before it is checked against this list, not by
70
+ # the accident of duck typing.
46
71
  # Refusing loudly at construction, once, for anything outside
47
72
  # this list turns a silent no-op into an immediate, named error
48
73
  # — the "refuse rather than silently skip" reading of L1, since
49
74
  # `:strict` really is meant to be the only enforcing contract.
50
75
  VALID_POLICIES = %i[refresh strict].freeze
51
76
 
77
+ # @param authoritative [Persistence::AppendOnly] the authoritative repository whose
78
+ # journal the projection is caught up to
79
+ # @param projection [Persistence::AppendOnly] the projection store being caught up
80
+ # @param policy [Symbol, String] one of `VALID_POLICIES`, as a Symbol or its String
81
+ # spelling
82
+ # @raise [ArgumentError] if `policy` is not one of `VALID_POLICIES`
52
83
  def initialize(authoritative, projection, policy: :refresh)
53
84
  @authoritative = authoritative
54
85
  @projection = projection
@@ -60,8 +91,20 @@ module Hecks
60
91
  end
61
92
  end
62
93
 
94
+ # Appends and projects every authoritative entry the projection store lacks.
95
+ #
63
96
  # Invoke from a separate process or scheduler. The command-side write
64
97
  # path never calls this method.
98
+ #
99
+ # Under `:refresh` the store is reset first and rebuilt from the whole journal. Under
100
+ # `:strict` the store's own entries must be a prefix of the authoritative journal
101
+ # (same operation, id and state, in order); only the entries after that prefix are
102
+ # replayed.
103
+ #
104
+ # @return [Persistence::AppendOnly] the projection store, now caught up
105
+ # @raise [Runtime::WiringError] under `:strict`, if the projection's history is not a
106
+ # prefix of the authoritative journal; under `:refresh`, if the projection's
107
+ # adapter cannot `reset!`
65
108
  def catch_up!
66
109
  entries = Queue.new(@authoritative).entries
67
110
  present = @projection.entries
@@ -79,6 +122,10 @@ module Hecks
79
122
  @projection
80
123
  end
81
124
 
125
+ # Counts the entries the projection store holds, which is how far it has caught up.
126
+ #
127
+ # @return [Integer] number of journal entries already appended to the projection
128
+ # store; 0 for a store that has never caught up
82
129
  def checkpoint = @projection.entries.length
83
130
 
84
131
  private
@@ -96,7 +143,14 @@ module Hecks
96
143
  # It is committed before a worker sees it; projection entries are the
97
144
  # worker's durable checkpoint, so delivery is at-least-once and safe to replay.
98
145
  class Queue
146
+ # @param authoritative [Persistence::AppendOnly] the authoritative repository to read
147
+ # entries from
99
148
  def initialize(authoritative) = @authoritative = authoritative
149
+
150
+ # Reads the whole authoritative journal, the work a projection worker replays from.
151
+ #
152
+ # @return [Array<Persistence::Entry>] every committed entry, oldest first; `[]` for
153
+ # an empty journal
100
154
  def entries = @authoritative.entries
101
155
  end
102
156
  end
@@ -27,7 +27,7 @@ module Hecks
27
27
  field = declared.order_by&.field
28
28
  matched = Ordering.apply(matched, declared.order_by, declared.null_semantics,
29
29
  identity: ->(record) { record.id.to_s }) { |record| comparable(FieldPath.dig(record, field)) }
30
- # OFFSET FIRST, THEN LIMIT — the order SQL means by `LIMIT n
30
+ # **Offset first, then limit** — the order SQL means by `LIMIT n
31
31
  # OFFSET m`, which is what `SqlQueryBuilder` emits and therefore
32
32
  # what every SQL-backed aggregate already answers. Written the
33
33
  # other way round here, and the two engines disagreed on the
@@ -38,7 +38,7 @@ module Hecks
38
38
  #
39
39
  # It gets worse the further you page, which is the case nobody
40
40
  # writing the first page ever sees: at `limit 10, offset 10`,
41
- # taking ten and then dropping ten leaves NOTHING, so page two
41
+ # taking ten and then dropping ten leaves nothing, so page two
42
42
  # of a memory-backed query came back empty however many rows
43
43
  # were really there.
44
44
  matched = matched.drop(resolve(declared.offset.value, args).to_i) if declared.offset
@@ -50,7 +50,7 @@ module Hecks
50
50
  # QuerySpecification::Common::Comparison — this module and
51
51
  # Runtime::QueryInterpreter used to carry a copy each, and the two
52
52
  # drifted (see that file's own comment for what it cost). What
53
- # stays here is how a value is REACHED for this path: a registry
53
+ # stays here is how a value is reached for this path: a registry
54
54
  # arrives as an argument rather than as instance state, and the
55
55
  # field is dug through FieldPath before it arrives.
56
56
  def holds?(clause, held, args, registry: nil)