hecks 1.3.0 → 1.5.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 (373) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +72 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +83 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
  7. data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
  9. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  10. data/lib/hecks/adapters/driven/heki.rb +106 -9
  11. data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
  12. data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
  13. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  15. data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
  16. data/lib/hecks/adapters/driven/lambda.rb +82 -35
  17. data/lib/hecks/adapters/driven/local_storage.rb +83 -10
  18. data/lib/hecks/adapters/driven/memory.rb +205 -9
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  24. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  25. data/lib/hecks/adapters/driven/prism.rb +50 -11
  26. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  31. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  32. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  33. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  34. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  35. data/lib/hecks/adapters/driven.rb +6 -4
  36. data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
  37. data/lib/hecks/behaviors/dsl.rb +60 -2
  38. data/lib/hecks/behaviors/expectations.rb +190 -29
  39. data/lib/hecks/behaviors/ir.rb +12 -1
  40. data/lib/hecks/behaviors/rspec.rb +9 -1
  41. data/lib/hecks/behaviors/runner.rb +21 -2
  42. data/lib/hecks/behaviors.rb +9 -1
  43. data/lib/hecks/bluebook/aggregate.rb +43 -13
  44. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
  45. data/lib/hecks/bluebook/assembly/build.rb +26 -5
  46. data/lib/hecks/bluebook/assembly/contract.rb +98 -23
  47. data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
  48. data/lib/hecks/bluebook/assembly/marks.rb +159 -30
  49. data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
  50. data/lib/hecks/bluebook/assembly.rb +32 -14
  51. data/lib/hecks/bluebook/attribute.rb +26 -12
  52. data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
  53. data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
  54. data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
  55. data/lib/hecks/bluebook/behaviour/command.rb +55 -25
  56. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
  57. data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
  58. data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
  59. data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
  60. data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
  61. data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
  62. data/lib/hecks/bluebook/behaviour/query.rb +6 -1
  63. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
  64. data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
  65. data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
  66. data/lib/hecks/bluebook/capabilities.rb +27 -0
  67. data/lib/hecks/bluebook/chapter.rb +51 -9
  68. data/lib/hecks/bluebook/command.rb +62 -17
  69. data/lib/hecks/bluebook/domain_port.rb +34 -9
  70. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  71. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  72. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
  73. data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
  74. data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
  75. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
  76. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
  77. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  78. data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
  79. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  80. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  81. data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
  82. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  83. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
  84. data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
  85. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  86. data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
  87. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  88. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  89. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
  90. data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
  91. data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
  92. data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
  93. data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
  94. data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
  95. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  96. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  97. data/lib/hecks/bluebook/entity.rb +40 -11
  98. data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
  99. data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
  100. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
  101. data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
  102. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
  103. data/lib/hecks/bluebook/expression/resolver.rb +369 -128
  104. data/lib/hecks/bluebook/hexagon.rb +35 -1
  105. data/lib/hecks/bluebook/lifecycle.rb +12 -1
  106. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
  107. data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
  108. data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
  109. data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
  110. data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
  111. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
  112. data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
  113. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
  114. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
  115. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
  116. data/lib/hecks/bluebook/meta_validator.rb +235 -139
  117. data/lib/hecks/bluebook/model_check.rb +434 -104
  118. data/lib/hecks/bluebook/pattern_subset.rb +32 -10
  119. data/lib/hecks/bluebook/policy.rb +15 -13
  120. data/lib/hecks/bluebook/process_manager.rb +27 -14
  121. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  122. data/lib/hecks/bluebook/project_loader.rb +40 -0
  123. data/lib/hecks/bluebook/project_register.rb +50 -6
  124. data/lib/hecks/bluebook/query.rb +31 -4
  125. data/lib/hecks/bluebook/read_model.rb +35 -15
  126. data/lib/hecks/bluebook/reference.rb +26 -13
  127. data/lib/hecks/bluebook/smoke_test.rb +46 -23
  128. data/lib/hecks/bluebook/synthesizer.rb +46 -12
  129. data/lib/hecks/bluebook/translation.rb +34 -5
  130. data/lib/hecks/bluebook/value_object.rb +29 -11
  131. data/lib/hecks/bluebook.rb +5 -6
  132. data/lib/hecks/codemod.rb +138 -50
  133. data/lib/hecks/construct.rb +21 -7
  134. data/lib/hecks/corpus.rb +438 -0
  135. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  136. data/lib/hecks/doc/reference.rb +200 -31
  137. data/lib/hecks/embryonaut_bluebook.rb +38 -15
  138. data/lib/hecks/facade/cli_door.rb +69 -10
  139. data/lib/hecks/facade/cli_runner.rb +105 -24
  140. data/lib/hecks/facade/command_request.rb +23 -0
  141. data/lib/hecks/facade/handle.rb +155 -35
  142. data/lib/hecks/facade/json_door.rb +106 -25
  143. data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
  144. data/lib/hecks/facade/surface/chapter.rb +26 -17
  145. data/lib/hecks/facade/surface.rb +16 -3
  146. data/lib/hecks/facade.rb +15 -4
  147. data/lib/hecks/forms/app.rb +46 -30
  148. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  149. data/lib/hecks/forms/field_renderer.rb +142 -6
  150. data/lib/hecks/forms/field_shape.rb +183 -19
  151. data/lib/hecks/forms/html.rb +51 -7
  152. data/lib/hecks/forms/index_renderer.rb +14 -2
  153. data/lib/hecks/forms/page.rb +14 -0
  154. data/lib/hecks/forms/params.rb +120 -23
  155. data/lib/hecks/forms/port_argument.rb +14 -2
  156. data/lib/hecks/forms/query_form_renderer.rb +65 -2
  157. data/lib/hecks/forms/record_renderer.rb +60 -2
  158. data/lib/hecks/forms/record_table.rb +28 -1
  159. data/lib/hecks/forms/reference_options.rb +24 -0
  160. data/lib/hecks/forms/value_object_shape.rb +13 -3
  161. data/lib/hecks/forms.rb +24 -4
  162. data/lib/hecks/fqn.rb +59 -1
  163. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  164. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  165. data/lib/hecks/framework/oidc.json +15 -0
  166. data/lib/hecks/framework.rb +79 -25
  167. data/lib/hecks/freezer.rb +27 -11
  168. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
  169. data/lib/hecks/fuzzing/combination_miner.rb +178 -0
  170. data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
  171. data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
  172. data/lib/hecks/fuzzing/differential.rb +192 -0
  173. data/lib/hecks/fuzzing/domain_generator.rb +871 -0
  174. data/lib/hecks/fuzzing/era_boundary.rb +68 -19
  175. data/lib/hecks/fuzzing/form_census.rb +121 -20
  176. data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
  177. data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
  178. data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
  179. data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
  180. data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
  181. data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
  182. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
  183. data/lib/hecks/fuzzing/properties/guards.rb +86 -42
  184. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
  185. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
  186. data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
  187. data/lib/hecks/fuzzing/properties/querying.rb +82 -28
  188. data/lib/hecks/fuzzing/properties.rb +84 -51
  189. data/lib/hecks/fuzzing/qa_settings.rb +164 -0
  190. data/lib/hecks/fuzzing/replay.rb +241 -133
  191. data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
  192. data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
  193. data/lib/hecks/fuzzing/self_consistency.rb +315 -137
  194. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
  195. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  196. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  197. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  198. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
  199. data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
  200. data/lib/hecks/fuzzing/shrinker.rb +309 -0
  201. data/lib/hecks/fuzzing/structural_skips.rb +37 -130
  202. data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
  203. data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
  204. data/lib/hecks/fuzzing/value_generator.rb +110 -22
  205. data/lib/hecks/fuzzing.rb +5 -0
  206. data/lib/hecks/grammar/evolve.rb +188 -12
  207. data/lib/hecks/grammar.rb +53 -7
  208. data/lib/hecks/ir.rb +51 -20
  209. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  210. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  211. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  212. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  213. data/lib/hecks/language/oidc.json +5 -0
  214. data/lib/hecks/literal.rb +41 -9
  215. data/lib/hecks/naming.rb +112 -31
  216. data/lib/hecks/ports/access_control.rb +53 -2
  217. data/lib/hecks/ports/agent/answers.rb +83 -6
  218. data/lib/hecks/ports/agent.rb +119 -35
  219. data/lib/hecks/ports/authentication.rb +44 -4
  220. data/lib/hecks/ports/authorization.rb +50 -11
  221. data/lib/hecks/ports/clock.rb +42 -23
  222. data/lib/hecks/ports/extraction.rb +16 -0
  223. data/lib/hecks/ports/identity_assignment.rb +23 -2
  224. data/lib/hecks/ports/identity_generation.rb +17 -3
  225. data/lib/hecks/ports/identity_resolution.rb +17 -1
  226. data/lib/hecks/ports/key_vault.port +6 -0
  227. data/lib/hecks/ports/key_vault.rb +58 -0
  228. data/lib/hecks/ports/loading.rb +4 -0
  229. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  230. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  231. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  232. data/lib/hecks/ports/persistence/execution.rb +4 -0
  233. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  234. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  235. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  236. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  237. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  238. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  239. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  250. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  251. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  252. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
  253. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  254. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  255. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  256. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  257. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  258. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
  259. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  260. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  261. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  262. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
  263. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
  264. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  265. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
  266. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  267. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  268. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  269. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  270. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  271. data/lib/hecks/ports/persistence.rb +36 -1
  272. data/lib/hecks/ports/projection.rb +61 -7
  273. data/lib/hecks/ports/query/in_memory.rb +41 -8
  274. data/lib/hecks/ports/query/ordering.rb +21 -6
  275. data/lib/hecks/ports/query.rb +35 -0
  276. data/lib/hecks/ports.rb +1 -0
  277. data/lib/hecks/projections/bootstrap_table.rb +147 -0
  278. data/lib/hecks/projections/diagrams.rb +315 -79
  279. data/lib/hecks/projections/glossary/html.rb +91 -3
  280. data/lib/hecks/projections/glossary/markdown.rb +50 -3
  281. data/lib/hecks/projections/glossary/mermaid.rb +49 -1
  282. data/lib/hecks/projections/glossary/sections.rb +1 -1
  283. data/lib/hecks/projections/glossary/sentences.rb +110 -5
  284. data/lib/hecks/projections/glossary.rb +173 -31
  285. data/lib/hecks/projections/ir.rb +1 -1
  286. data/lib/hecks/projections/model/deviations.rb +62 -17
  287. data/lib/hecks/projections/model.rb +75 -21
  288. data/lib/hecks/projections/oidc.rb +45 -8
  289. data/lib/hecks/projections/parser_table.rb +53 -12
  290. data/lib/hecks/projections/reference.rb +15 -3
  291. data/lib/hecks/projections/rust_vocabulary.rb +646 -0
  292. data/lib/hecks/projections/shape.rb +10 -2
  293. data/lib/hecks/projections/statements.rb +69 -22
  294. data/lib/hecks/projections/vocabulary.rb +26 -9
  295. data/lib/hecks/projections.rb +5 -3
  296. data/lib/hecks/projector/cli_projector.rb +245 -37
  297. data/lib/hecks/projector/docs_projector.rb +154 -28
  298. data/lib/hecks/projector/exporter.rb +104 -29
  299. data/lib/hecks/projector/ir_projector.rb +7 -1
  300. data/lib/hecks/projector/narrate_projector.rb +144 -23
  301. data/lib/hecks/projector/target.rb +42 -18
  302. data/lib/hecks/projector.rb +86 -18
  303. data/lib/hecks/query_ir.rb +94 -47
  304. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  305. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  306. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  307. data/lib/hecks/query_specification/common/null_policy.rb +61 -15
  308. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  309. data/lib/hecks/query_specification/common/options.rb +25 -0
  310. data/lib/hecks/query_specification/field_path.rb +69 -15
  311. data/lib/hecks/query_specification/hop_path.rb +57 -20
  312. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  313. data/lib/hecks/rendering.rb +9 -3
  314. data/lib/hecks/router/namespace_installer.rb +16 -3
  315. data/lib/hecks/router.rb +56 -1
  316. data/lib/hecks/runtime/aggregate_lock.rb +20 -11
  317. data/lib/hecks/runtime/boot_gates.rb +21 -3
  318. data/lib/hecks/runtime/caller.rb +40 -8
  319. data/lib/hecks/runtime/capability_graph.rb +13 -2
  320. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
  321. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
  322. data/lib/hecks/runtime/command_interpreter.rb +195 -113
  323. data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
  324. data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
  325. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  326. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  327. data/lib/hecks/runtime/command_rules/references.rb +145 -55
  328. data/lib/hecks/runtime/command_rules.rb +3 -0
  329. data/lib/hecks/runtime/dependency_planning.rb +56 -11
  330. data/lib/hecks/runtime/dispatcher.rb +263 -92
  331. data/lib/hecks/runtime/entity_element.rb +304 -67
  332. data/lib/hecks/runtime/entity_interpreter.rb +149 -88
  333. data/lib/hecks/runtime/errors.rb +37 -23
  334. data/lib/hecks/runtime/event.rb +15 -10
  335. data/lib/hecks/runtime/identity.rb +91 -23
  336. data/lib/hecks/runtime/instance.rb +103 -18
  337. data/lib/hecks/runtime/interpreting.rb +24 -16
  338. data/lib/hecks/runtime/invocation.rb +358 -0
  339. data/lib/hecks/runtime/loader.rb +105 -19
  340. data/lib/hecks/runtime/outbox.rb +164 -26
  341. data/lib/hecks/runtime/policy_interpreter.rb +73 -60
  342. data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
  343. data/lib/hecks/runtime/query_interpreter.rb +93 -74
  344. data/lib/hecks/runtime/reaction_invocation.rb +73 -28
  345. data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
  346. data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
  347. data/lib/hecks/runtime/reference_hop.rb +48 -6
  348. data/lib/hecks/runtime/refusal_wording.rb +142 -115
  349. data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
  350. data/lib/hecks/runtime/registry/verification.rb +153 -28
  351. data/lib/hecks/runtime/registry.rb +202 -32
  352. data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
  353. data/lib/hecks/runtime/routing.rb +36 -154
  354. data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
  355. data/lib/hecks/runtime/saga_interpreter.rb +90 -76
  356. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  357. data/lib/hecks/runtime/tenant_check.rb +33 -13
  358. data/lib/hecks/runtime/tenant_scope.rb +23 -5
  359. data/lib/hecks/runtime/value/admission.rb +75 -30
  360. data/lib/hecks/runtime/value/coercion.rb +555 -142
  361. data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
  362. data/lib/hecks/runtime/value.rb +71 -21
  363. data/lib/hecks/runtime.rb +39 -7
  364. data/lib/hecks/storehouse.rb +368 -72
  365. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  366. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  367. data/lib/hecks/version.rb +3 -3
  368. data/lib/hecks/vocabulary.rb +205 -4
  369. data/lib/hecks.rb +91 -11
  370. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
  371. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
  372. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
  373. metadata +28 -2
@@ -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
@@ -18,6 +18,19 @@ module Hecks
18
18
 
19
19
  module_function
20
20
 
21
+ # Filters, orders and pages `records` against a declared query specification.
22
+ #
23
+ # @param records [Array<Runtime::Instance, Hash>] the candidate records to filter —
24
+ # an Instance, a Value, or a plain row hash per record (see `FieldPath.dig`)
25
+ # @param declared [QuerySpecification::Common::Options,
26
+ # Bluebook::Behaviour::ReadModel::FilteredOptions] the specification providing
27
+ # `wheres`, `order_by`, `offset` and `limit`
28
+ # @param args [Hash{Symbol => Object}] bound values for any Symbol placeholder in a
29
+ # where clause, `offset`, or `limit`
30
+ # @param registry [Runtime::Registry, nil] the booted registry, passed through to a
31
+ # registry-aware comparison; nil when none is available
32
+ # @return [Array<Runtime::Instance, Hash>] the matching records, ordered and paged;
33
+ # the same element type as `records`
21
34
  def execute(records, declared, args = {}, registry: nil)
22
35
  matched = records.select do |record|
23
36
  declared.wheres.all? do |clause|
@@ -27,7 +40,7 @@ module Hecks
27
40
  field = declared.order_by&.field
28
41
  matched = Ordering.apply(matched, declared.order_by, declared.null_semantics,
29
42
  identity: ->(record) { record.id.to_s }) { |record| comparable(FieldPath.dig(record, field)) }
30
- # OFFSET FIRST, THEN LIMIT — the order SQL means by `LIMIT n
43
+ # **Offset first, then limit** — the order SQL means by `LIMIT n
31
44
  # OFFSET m`, which is what `SqlQueryBuilder` emits and therefore
32
45
  # what every SQL-backed aggregate already answers. Written the
33
46
  # other way round here, and the two engines disagreed on the
@@ -38,7 +51,7 @@ module Hecks
38
51
  #
39
52
  # It gets worse the further you page, which is the case nobody
40
53
  # writing the first page ever sees: at `limit 10, offset 10`,
41
- # taking ten and then dropping ten leaves NOTHING, so page two
54
+ # taking ten and then dropping ten leaves nothing, so page two
42
55
  # of a memory-backed query came back empty however many rows
43
56
  # were really there.
44
57
  matched = matched.drop(resolve(declared.offset.value, args).to_i) if declared.offset
@@ -47,18 +60,38 @@ module Hecks
47
60
  end
48
61
 
49
62
  # The comparator table itself lives in
50
- # QuerySpecification::Common::Comparison — this module and
51
- # Runtime::QueryInterpreter used to carry a copy each, and the two
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
54
- # arrives as an argument rather than as instance state, and the
55
- # field is dug through FieldPath before it arrives.
63
+ # QuerySpecification::Common::Comparison, not duplicated per
64
+ # caller — this module and Runtime::QueryInterpreter drifted
65
+ # when each carried its own copy (see that file's own comment
66
+ # for what it cost). What stays here is how a value is reached
67
+ # for this path: a registry arrives as an argument rather than
68
+ # as instance state, and the field is dug through FieldPath
69
+ # before it arrives.
70
+ # @param clause [QuerySpecification::Common::WhereClause] the declared comparison
71
+ # @param held [Object, nil] the record's own value for `clause.field`
72
+ # @param args [Hash{Symbol => Object}] bound values for a Symbol-named `clause.value`
73
+ # @param registry [Runtime::Registry, nil] used only by `none_in_state`; see
74
+ # `Comparison.holds?`
75
+ # @return [Boolean] whether the comparison holds
76
+ # @raise [Runtime::WiringError] if `clause.op` names no comparator in
77
+ # `Comparison`'s table, or `none_in_state`'s target aggregate has no wired repository
56
78
  def holds?(clause, held, args, registry: nil)
57
79
  Comparison.holds?(clause.op, held, comparable(resolve(clause.value, args)), registry: registry)
58
80
  end
59
81
 
82
+ # Resolves a where-clause value, looking up a Symbol placeholder in `args`.
83
+ #
84
+ # @param value [Object] a literal value, or a Symbol naming a key in `args`
85
+ # @param args [Hash{Symbol => Object}] bound argument values
86
+ # @return [Object] `args[value]` when `value` is a Symbol, else `value` unchanged
60
87
  def resolve(value, args) = value.is_a?(Symbol) ? args[value] : value
61
88
 
89
+ # Normalises a value into the shape `Comparison` compares against.
90
+ #
91
+ # @param value [Runtime::Value, Hash, Object, nil] a held or wanted value; a
92
+ # `Runtime::Value` is read through its `to_h`
93
+ # @return [Object, nil] the sole numeric member, or the sole member, of a Hash-shaped
94
+ # value; otherwise `value` unchanged
62
95
  def comparable(value) = Comparison.comparable(value)
63
96
  end
64
97
  end
@@ -3,30 +3,45 @@ require_relative "../../query_specification/common/null_policy"
3
3
  module Hecks
4
4
  module Ports
5
5
  module Query
6
- # What order an ask ANSWERS IN — the meaning of the ask, not a property
6
+ # What order an ask answers in — the meaning of the ask, not a property
7
7
  # of the store that happens to hold it. Declared here once so an adapter
8
8
  # may satisfy it natively but never redefine it : SQLite pushes both
9
9
  # tiers into SQL (NullPolicy.sql_order renders `field DIR, id DIR`),
10
10
  # while Heki and Memory have no query engine and delegate straight back
11
11
  # to InMemory.
12
12
  #
13
- # Two tiers, in this order : the DECLARED order_by when there is one,
14
- # then IDENTITY, always. The identity tier is what makes an ask total.
13
+ # Two tiers, in this order : the declared order_by when there is one,
14
+ # then identity, always. The identity tier is what makes an ask total.
15
15
  # Without it, an ask with no order_by — or a declared order with tied
16
16
  # keys — hands back whatever order the store happened to hold, and
17
17
  # store order was quietly standing in for a rule while every
18
18
  # hand-written query in the corpus stayed green : not one of them
19
19
  # had a tie for store order to decide.
20
20
  #
21
- # An adapter that pushes ordering down MUST push limit down with it.
21
+ # An adapter that pushes ordering down must push limit down with it.
22
22
  # Re-ordering a page the store already cut would be a top-N of the
23
23
  # wrong N — the one way this can be got quietly, expensively wrong.
24
24
  module Ordering
25
25
  module_function
26
26
 
27
+ # Orders an Array of rows by the declared order, with identity as the total-order
28
+ # tiebreaker.
29
+ #
30
+ # @param rows [Array<Object>] the rows to order; any shape `identity` and the block
31
+ # can read (a `Runtime::Instance`, a plain Hash row, …)
32
+ # @param order_by [QuerySpecification::Common::OrderBy, nil] the declared order;
33
+ # nil orders by identity alone
34
+ # @param null_semantics [QuerySpecification::Common::NullSemantics, nil] where a
35
+ # nil-valued row sorts, passed to `NullPolicy.order`; nil uses its native default
36
+ # @param identity [Proc] yields a row, returns its comparable identity value, used
37
+ # as the stable tiebreaker
38
+ # @yieldparam row [Object] one row being ordered
39
+ # @yieldreturn [Object, nil] the row's comparable value for `order_by`'s field
40
+ # @return [Array<Object>] `rows`, ordered by `order_by` then identity (identity
41
+ # alone when `order_by` is nil)
27
42
  def apply(rows, order_by, null_semantics = nil, identity:, &value_of)
28
- # STABLE, because sort_by is not : two rows whose identity ties would
29
- # otherwise swap arbitrarily, and a tier meant to REMOVE store-dependence
43
+ # Stable, because sort_by is not : two rows whose identity ties would
44
+ # otherwise swap arbitrarily, and a tier meant to remove store-dependence
30
45
  # would be adding a coin flip of its own.
31
46
  rows = rows.each_with_index.sort_by { |row, index| [identity.call(row), index] }.map(&:first)
32
47
  return rows unless order_by