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
@@ -15,7 +15,7 @@ require_relative "../../../../runtime/registry"
15
15
  module Hecks
16
16
  module Adapters
17
17
  # The enforcement-grade persistence adapter — and the only one that
18
- # declares the LINEAGE capability: it may act on shape drift
18
+ # declares the lineage capability: it may act on shape drift
19
19
  # (translate, fork, merge) where every other adapter can only refuse
20
20
  # toward it. Sibling to the plain `Postgres` adapter (postgres.rb),
21
21
  # which is the same database with none of this machinery — pick
@@ -23,19 +23,22 @@ module Hecks
23
23
  # change live. See docs/implemented/postgres-era-adapter-split-plan.md for why
24
24
  # the two are split and what each one carries.
25
25
  #
26
- # Storage model (see postgres_era/lineage.rb for the DDL):
27
- # - One journal per DOMAIN, list-partitioned by era, one ordinal
26
+ # ## Storage model
27
+ #
28
+ # See postgres_era/lineage.rb for the DDL.
29
+ #
30
+ # - One journal per domain, list-partitioned by era, one ordinal
28
31
  # sequence spanning partitions. Appends go there; nothing updates
29
32
  # or deletes a journal row (immutability by privilege — UPDATE and
30
33
  # DELETE revoked; a deployment's app role connects as a non-owner).
31
- # - Per aggregate, the HEAD is derived: era 1 reads a plain view
34
+ # - Per aggregate, the head is derived: era 1 reads a plain view
32
35
  # (latest save per id); later eras read a view overlaying the
33
36
  # materialized, translated ancestor tail with live current-era
34
37
  # rows. `project` is therefore a no-op — old entries are never
35
38
  # rewritten, and the head is never a table anything writes.
36
- # - State is ONE jsonb column. jsonb normalizes key order (and drops
39
+ # - State is one jsonb column. jsonb normalizes key order (and drops
37
40
  # duplicate keys), so anything comparing stored state — the corpus
38
- # history gate above all — must compare CANONICALIZED state, never
41
+ # history gate above all — must compare canonicalized state, never
39
42
  # raw bytes; `bin/canonicalise` deep-sorts keys, which is exactly
40
43
  # why the gate survives this normalization.
41
44
  # - Query pushdown is the shared SqlQueryBuilder: every declared
@@ -47,21 +50,31 @@ module Hecks
47
50
 
48
51
  attr_reader :aggregate
49
52
 
53
+ # Names the optional persistence capabilities this adapter implements natively.
54
+ #
50
55
  # `:cross_process_lock` tells `Interpreting#run_dispatch_order_with_isolation`
51
- # (runtime/interpreting.rb) this repository can hold a REAL
56
+ # (runtime/interpreting.rb) this repository can hold a real
52
57
  # cross-process lock for the whole dispatch order itself, via
53
58
  # `with_write_lock` below — so it should use that instead of the
54
59
  # in-process `AggregateLock` `Mutex` every other non-CAS repository
55
60
  # falls back to. See ADR 0036: that in-process Mutex is invisible
56
61
  # to `rust/host` dispatching against the same PostgresEra-bound
57
62
  # tables from a separate OS process.
63
+ #
64
+ # @return [Array<Symbol>] always `[:atomic_put, :cross_process_lock]`
58
65
  def persistence_capabilities = %i[atomic_put cross_process_lock]
59
66
 
67
+ # Declares that this adapter can act on shape drift rather than only refuse.
68
+ #
60
69
  # The capability idiom: only PostgresEra answers true, and only
61
70
  # PostgresEra carries an era_check! for the boot gate to delegate to.
71
+ #
72
+ # @return [Boolean] always true
62
73
  def self.lineage_capable? = true
63
74
 
64
- # TENANT-CAPABLE see Runtime::TenantCheck's own header for the
75
+ # Declares that two tenant boots of this adapter keep their tables apart.
76
+ #
77
+ # **Tenant-capable** — see Runtime::TenantCheck's own header for the
65
78
  # full reasoning. `connect_for`'s own `schema:` setting (the
66
79
  # Storehouse shared-instance mechanism, already built, already
67
80
  # proven for eras) is what keeps two tenant boots' tables apart:
@@ -70,8 +83,31 @@ module Hecks
70
83
  # resolves into that boot's own schema, never another tenant's —
71
84
  # proven for real, not assumed, by tenant_isolation_spec.rb, the
72
85
  # same discipline lineage_capable? already holds itself to.
86
+ #
87
+ # @return [Boolean] always true
73
88
  def self.tenant_capable? = true
74
89
 
90
+ # Resolves which era this boot is, minting the next one when the shape has drifted
91
+ # and a covering translation edge exists. The boot gate's entry point; the work is
92
+ # `LineageManager.check!` (see `LineageManager::EraResolver`).
93
+ #
94
+ # @param registry [Runtime::Registry] the booting registry; receives the resolved
95
+ # ordinal in `resolved_eras` and, for a superseded boot, `superseded_eras`
96
+ # @param bluebook [Bluebook::Chapter] the domain being booted
97
+ # @param current_text [String] the domain's bluebook source as it stands on disk
98
+ # @param settings [Hash{Symbol, String => Object}] the world's persistence settings
99
+ # for this binding; `database` is required, `schema`, `role` and
100
+ # `allow_superuser` are honored
101
+ # @param directory [String, nil] the domain's bluebook directory, where
102
+ # `HECKS_SCAFFOLD=1` writes a translation edge; nil disables scaffolding
103
+ # @return [Integer, nil] the ordinal of the era this boot minted, or nil when it
104
+ # minted nothing (first boot, quiet reboot, or a held-but-superseded shape)
105
+ # @raise [Runtime::WiringError] if the database cannot be reached, the connection
106
+ # is a superuser without `allow_superuser`, a held text fails its integrity
107
+ # check, or the shape drifted and the mint refuses (no edge, a stale or forked
108
+ # edge, an unapproved compute or rekey, uncovered drift, or a failed audit)
109
+ # @raise [Bluebook::DSL::Malformed] if a held era's text parses under neither the
110
+ # current nor the legacy grammar
75
111
  def self.era_check!(registry:, bluebook:, current_text:, settings:, directory: nil)
76
112
  LineageManager.check!(
77
113
  registry: registry, bluebook: bluebook, current_text: current_text,
@@ -79,15 +115,23 @@ module Hecks
79
115
  )
80
116
  end
81
117
 
82
- # BOTH SPELLINGS OF A SETTING ARE HONORED — a world's settings hash
118
+ # Reads one setting from a world's settings hash under either key spelling.
119
+ #
120
+ # Both spellings of a setting are honored — a world's settings hash
83
121
  # may arrive symbol-keyed (built straight in Ruby) or string-keyed
84
122
  # (round-tripped through JSON), and a domain that names one is not
85
123
  # obligated to also skip the other. `key?` decides which spelling
86
124
  # actually exists, never `||` — `||` cannot tell a genuinely stored
87
125
  # `false` apart from an absent key, and would silently prefer the
88
- # OTHER spelling (or `default`) instead of returning the real, held
126
+ # other spelling (or `default`) instead of returning the real, held
89
127
  # answer. See Hecks::QuerySpecification::FieldPath#read for the same
90
128
  # discipline applied to stored state instead of settings.
129
+ #
130
+ # @param settings [Hash{Symbol, String => Object}] the settings hash to read
131
+ # @param key [Symbol, String] the setting name; tried as given, then as a String
132
+ # @param default [Object, nil] what to return when neither spelling is a key
133
+ # @return [Object, nil] the stored value, including a stored `false` or `nil`, or
134
+ # `default` when the key is absent under both spellings
91
135
  def self.setting(settings, key, default: nil)
92
136
  return settings[key] if settings.key?(key)
93
137
 
@@ -97,8 +141,19 @@ module Hecks
97
141
  default
98
142
  end
99
143
 
144
+ # Opens a connection to the database a world declares, creating and selecting the
145
+ # declared `schema` and silencing sub-warning notices. The caller owns the
146
+ # connection and closes it.
147
+ #
148
+ # @param name [String] the domain or aggregate name, used only in refusal messages
149
+ # @param settings [Hash{Symbol, String => Object}] the world's settings; `database`
150
+ # is a database name or a `postgres://`/`postgresql://` URL, `schema` is optional
151
+ # @return [PG::Connection] an open connection with `search_path` set when a schema
152
+ # is declared
153
+ # @raise [Runtime::WiringError] if the settings declare no `database`, or Postgres
154
+ # refuses the connection, the `CREATE SCHEMA` or either `SET`
100
155
  def self.connect_for(name, settings)
101
- # LAZY, ON PURPOSE — same reasoning as Sqlite's own initialize:
156
+ # **Lazy, on purpose** — same reasoning as Sqlite's own initialize:
102
157
  # a domain that never wires PostgresEra should never need the gem.
103
158
  require "pg"
104
159
 
@@ -116,38 +171,38 @@ module Hecks
116
171
  PG.connect(dbname: declared)
117
172
  end
118
173
 
119
- # SHARED-INSTANCE ISOLATION. A domain that declares `schema` is
174
+ # **Shared-instance isolation**. A domain that declares `schema` is
120
175
  # sharing its Postgres instance with other domains (the
121
176
  # storehouse) — every unqualified table/view/function reference
122
177
  # this adapter and its lineage classes ever construct resolves
123
178
  # through search_path, so this one SET is what makes ALTER
124
179
  # TABLE ... SET SCHEMA migrations transparent to the rest of the
125
180
  # adapter. A domain with no `schema` setting keeps Postgres's
126
- # own default search_path (public), same as before this existed.
181
+ # own default search_path (public).
127
182
  schema = setting(settings, :schema)
128
183
  if schema.to_s != ""
129
- # THE SCHEMA ITSELF, IDEMPOTENTLYa domain naming a `schema:`
130
- # nobody has created yet used to fail on its FIRST table-
131
- # creation attempt with Postgres's own "no schema has been
184
+ # **The schema itself, idempotently**without this, a domain
185
+ # naming a `schema:` nobody has created yet fails on its first
186
+ # table-creation attempt with Postgres's own "no schema has been
132
187
  # selected to create in", found live provisioning tenant_
133
- # isolation_spec.rb's own multi-schema fixture by hand before
134
- # this existed. `CREATE SCHEMA IF NOT EXISTS` is exactly the
188
+ # isolation_spec.rb's own multi-schema fixture by hand.
189
+ # `CREATE SCHEMA IF NOT EXISTS` is exactly the
135
190
  # same self-healing idempotency this adapter's own table/era
136
191
  # provisioning already holds itself to (see the comment right
137
192
  # below on `client_min_messages`) — a schema that already
138
- # exists is the ORDINARY case for every boot after the first,
193
+ # exists is the ordinary case for every boot after the first,
139
194
  # not news.
140
195
  connection.exec("CREATE SCHEMA IF NOT EXISTS #{connection.quote_ident(schema)}")
141
196
  connection.exec("SET search_path TO #{connection.quote_ident(schema)}")
142
197
  end
143
198
 
144
- # QUIET ON PURPOSE. Provisioning re-runs its own idempotent
199
+ # **Quiet on purpose**. Provisioning re-runs its own idempotent
145
200
  # `CREATE ... IF NOT EXISTS` checks on every boot — a schema that
146
- # already exists is the ORDINARY case, not news, and Postgres
147
- # surfaces every one as a NOTICE by default. `bin/set-password`
201
+ # already exists is the ordinary case, not news, and Postgres
202
+ # surfaces every one as a notice by default. `bin/set-password`
148
203
  # boots a real registry just to mint an Identity, and nobody
149
204
  # setting a password needs to see a page of "relation ...
150
- # already exists, skipping" to do it. WARNING and above (real
205
+ # already exists, skipping" to do it. Warning and above (real
151
206
  # problems) still surface.
152
207
  connection.exec("SET client_min_messages = warning")
153
208
  connection
@@ -156,6 +211,17 @@ module Hecks
156
211
  "cannot bind PostgresEra at #{declared} for #{name}: #{e.message.strip}"
157
212
  end
158
213
 
214
+ # Connects, provisions the domain's journal, this aggregate's head and field
215
+ # caches, and the event, saga and outbox tables. Every step is idempotent.
216
+ #
217
+ # @param aggregate [Bluebook::Aggregate] the aggregate this repository persists
218
+ # @param settings [Hash{Symbol, String => Object}] the world's settings plus what
219
+ # `RepositoryFactory.build` merges in: `domain` (journal name), `era` (the
220
+ # resolved ordinal, nil to self-resolve to the newest) and `superseded_by` (the
221
+ # ordinal that superseded this boot's era, nil for a current-era boot)
222
+ # @param root [String, nil] the registry root every adapter is offered; unused here
223
+ # @raise [Runtime::WiringError] if the settings declare no `database`, the
224
+ # connection is refused, or a held era text fails its integrity check
159
225
  def initialize(aggregate:, settings: {}, root: nil)
160
226
  @aggregate = aggregate
161
227
  @settings = settings
@@ -168,7 +234,7 @@ module Hecks
168
234
  # for a directly-instantiated adapter (specs, consoles) that
169
235
  # skips the factory.
170
236
  #
171
- # When the aggregate DOES have an owning chapter (`hecks_owner`
237
+ # When the aggregate does have an owning chapter (`hecks_owner`
172
238
  # set — true for any aggregate sealed through a real bluebook,
173
239
  # even if this adapter itself was built by hand), default to the
174
240
  # chapter's own declared PascalCase name. That's the same string
@@ -183,20 +249,20 @@ module Hecks
183
249
  # `hecks_owner` is only ever stamped by chapter construction, see
184
250
  # `traits.rb`'s `hecks_owner = self`) there is no chapter name to
185
251
  # match Rust against in the first place, so fall back to the
186
- # aggregate's own name, same as before this ADR.
252
+ # aggregate's own name.
187
253
  @domain = self.class.setting(
188
254
  settings, :domain, default: aggregate.hecks_owner&.name || aggregate.storage_name
189
255
  ).to_s
190
256
  @lineage = Lineage.new(@db, @domain)
191
257
  @lineage.ensure_base!
192
- # The era gate resolves which era this boot IS (an old checkout
258
+ # The era gate resolves which era this boot is (an old checkout
193
259
  # boots a held-but-superseded era and keeps writing its own
194
260
  # partition); a directly-instantiated adapter defaults to the
195
261
  # newest.
196
262
  #
197
- # NOT `self.class.setting(...)` here — RepositoryFactory#build
263
+ # Not `self.class.setting(...)` here — RepositoryFactory#build
198
264
  # always merges `era: registry.resolved_eras[domain]` into
199
- # settings, so the key is genuinely PRESENT (not absent) for
265
+ # settings, so the key is genuinely present (not absent) for
200
266
  # any domain the era boot gate hasn't resolved yet (or that
201
267
  # doesn't have one at all), just holding `nil`. `setting`'s own
202
268
  # presence-over-truthiness discipline (correct for a field like
@@ -209,6 +275,20 @@ module Hecks
209
275
  # promise.
210
276
  @era = settings.key?(:era) ? settings[:era] : settings["era"]
211
277
  @era ||= @lineage.current_era
278
+ # The in-process half of the era fence. `EraResolver.check!` sets
279
+ # `registry.superseded_eras[domain]` for a held-but-superseded
280
+ # boot only, and `RepositoryFactory.build` merges it in here as
281
+ # `superseded_by:` — so this is nil for every current-era boot and
282
+ # for a directly-instantiated adapter (specs, consoles), which,
283
+ # like `era:` above, self-resolves rather than being told. When
284
+ # set, `append`/`atomic_put` refuse before issuing an INSERT (see
285
+ # `refuse_superseded_write!`): the RLS fence already refuses the
286
+ # same write for an ordinary role, but a superuser walks through
287
+ # RLS (BUG#24), and this checkout's own knowledge that it is stale
288
+ # is the one guard no role attribute can void. Same coalescing as
289
+ # `era:` — the key is always present from the factory, holding nil
290
+ # for the ordinary case.
291
+ @superseded_by = settings.key?(:superseded_by) ? settings[:superseded_by] : settings["superseded_by"]
212
292
  # Unconditional and idempotent, regardless of era — belt-and-
213
293
  # suspenders self-healing (compile_head! already ensures this for
214
294
  # a freshly-minted era's own name; ensure_first_head! for era 1's)
@@ -216,7 +296,7 @@ module Hecks
216
296
  # CREATE TABLE IF NOT EXISTS nobody pays for twice.
217
297
  @lineage.ensure_head_snapshot!(table, @era)
218
298
  @lineage.ensure_first_head!(table) if @era == 1
219
- # THE READ-CACHE SIDE OF THE ERA WORKAROUND (Track C,
299
+ # The read-cache side of the era workaround (Track C,
220
300
  # docs/implemented/postgres-era-adapter-split-plan.md §3) — one row-cache
221
301
  # table per `where`-field this aggregate's own declared queries
222
302
  # (and its entities' own) actually use, derived automatically
@@ -231,8 +311,18 @@ module Hecks
231
311
  create_outbox_table!
232
312
  end
233
313
 
314
+ # Names this aggregate in storage: the journal's `aggregate` column value and the
315
+ # stem of its head view, head snapshot and field-cache names.
316
+ #
317
+ # @return [String] the aggregate's snake_case storage name
234
318
  def table = @aggregate.storage_name
235
319
 
320
+ # Reads one record's current state through the lineage-aware head view, so a record
321
+ # written under an ancestor era comes back translated to the current shape.
322
+ #
323
+ # @param id [String, Object] the record's identity, compared as `id.to_s`
324
+ # @return [Runtime::Instance, nil] the stored record, or nil when the head holds no
325
+ # saved row for that id (never written, or deleted)
236
326
  def find(id)
237
327
  result = @db.exec_params(%(SELECT id, state FROM #{quoted_head} WHERE id = $1), [id.to_s])
238
328
  return nil if result.ntuples.zero?
@@ -240,7 +330,9 @@ module Hecks
240
330
  instance(result[0])
241
331
  end
242
332
 
243
- # order_by IS A RUNTIME VALUE, not framework-authored bluebook source
333
+ # Lists every current record, ordered by id unless the caller names a field.
334
+ #
335
+ # order_by is a runtime value, not framework-authored bluebook source
244
336
  # like every other caller of order_expression — a query param off an
245
337
  # HTTP request, in the console's case. Whitelisted against the
246
338
  # aggregate's own real attributes (plus its lifecycle field) before
@@ -249,6 +341,13 @@ module Hecks
249
341
  # attribute at parse time. Without this, an unknown field wouldn't
250
342
  # error — query_expression degrades a nil attribute to a harmless
251
343
  # no-op path — it would just silently sort by nothing.
344
+ #
345
+ # @param order_by [String, Symbol, nil] an attribute name, the lifecycle field, or a
346
+ # dotted path whose first segment is one of those; nil orders by id
347
+ # @param direction [Symbol, String] `desc` in any case sorts descending, anything
348
+ # else ascending; nulls sort first ascending and last descending
349
+ # @return [Array<Runtime::Instance>] every saved record, `[]` when there are none
350
+ # @raise [Runtime::WiringError] if `order_by` names no attribute of this aggregate
252
351
  def all(order_by: nil, direction: :asc)
253
352
  return @db.exec(%(SELECT id, state FROM #{quoted_head} ORDER BY id)).map { |row| instance(row) } unless order_by
254
353
 
@@ -262,9 +361,15 @@ module Hecks
262
361
  @db.exec(%(SELECT id, state FROM #{quoted_head} ORDER BY #{order_clause(spec, nil)})).map { |row| instance(row) }
263
362
  end
264
363
 
364
+ # Counts current records in SQL against the head view, without loading any state.
365
+ #
366
+ # @return [Integer] how many saved, undeleted records the head holds
265
367
  def count = @db.exec(%(SELECT COUNT(*) FROM #{quoted_head}))[0]["count"].to_i
266
368
 
267
- # THE TWO-PHASE SHORTCUT (Track C, docs/implemented/postgres-era-adapter-
369
+ # Runs a declared query, looking candidate ids up in the field caches first when
370
+ # its `where` clauses allow it.
371
+ #
372
+ # The two-phase shortcut (Track C, docs/implemented/postgres-era-adapter-
268
373
  # split-plan.md §3). `SqlQueryBuilder#query` (`super`, unmodified per
269
374
  # principle 2) always runs correctly here — it filters against
270
375
  # `head_view`, which is already the fully-reduced current state —
@@ -276,25 +381,36 @@ module Hecks
276
381
  # ordinary comparator, not a null-vs-value special case — see
277
382
  # `cache_eligible?`), skip the reduction: look candidate ids up in
278
383
  # the cache table(s) first (cheap, indexed, no reduction involved),
279
- # then read ONLY those ids' current state from `head_view` — safe
280
- # THROUGH the reduction because `id` is its own partition key. Any
281
- # clause that ISN'T cache-eligible (an uncached field, or a null
384
+ # then read only those ids' current state from `head_view` — safe
385
+ # through the reduction because `id` is its own partition key. Any
386
+ # clause that isn't cache-eligible (an uncached field, or a null
282
387
  # comparison) is simply re-checked against `head_view` in the
283
388
  # second phase, exactly as `super` would have checked it anyway —
284
- # this can only ever NARROW what phase two has to look at, never
389
+ # this can only ever narrow what phase two has to look at, never
285
390
  # change what a clause means.
286
391
  #
287
- # FALLS BACK TO `super` WHENEVER NO CLAUSE CAN BE ACCELERATED — a
392
+ # Falls back to `super` whenever no clause can be accelerated — a
288
393
  # query with no `where` at all (order_by-only — no cache table
289
394
  # exists for these, see field_cache.rb), a query whose only clauses
290
395
  # target fields with no cache table, or a domain that has never
291
396
  # minted a second era at all (`@field_caches` is never empty just
292
397
  # because era 1 has no reduction to skip — the cache tables still
293
398
  # exist and still accelerate era 1 the same way, but the fallback
294
- # path is already just as cheap there since head_view IS the
399
+ # path is already just as cheap there since head_view is the
295
400
  # snapshot table verbatim for era 1; skipping straight to `super`
296
- # in that case would be a valid FUTURE optimization, not attempted
401
+ # in that case would be a valid future optimization, not attempted
297
402
  # here to keep this one code path correct for every era uniformly).
403
+ #
404
+ # @param declared [Bluebook::Query] the declared query, or a delegator wrapping one
405
+ # (tenant scoping and reference hops fold extra `where` clauses in that way)
406
+ # @param args [Hash{Symbol => Object}] caller-supplied values for the clauses, limit
407
+ # and offset that name an argument
408
+ # @param context [Hash] execution context the query port passes to every adapter;
409
+ # unused here
410
+ # @return [Array<Runtime::Instance>] the matching records in declared order, or by
411
+ # id when the query declares none; `[]` when nothing matches
412
+ # @raise [ArgumentError] if a clause uses an operator this dialect cannot compile,
413
+ # or `contains` on a list of multi-field value objects
298
414
  def query(declared, args = {}, context: {})
299
415
  return super if @field_caches.empty? || declared.wheres.empty?
300
416
 
@@ -308,27 +424,41 @@ module Hecks
308
424
  head_phase(declared, uncached, ids, args)
309
425
  end
310
426
 
427
+ # Runs a block inside one transaction holding the domain's cross-process write lock.
428
+ #
311
429
  # ADR 0036's actual fix — see `persistence_capabilities` above.
312
- # Wraps the WHOLE dispatch order (hydrate through save), not just
430
+ # Wraps the whole dispatch order (hydrate through save), not just
313
431
  # `append`'s own transaction below: `lock_writes!` has to be held
314
432
  # before hydrate even starts, or two cross-process writers can
315
433
  # both hydrate unlocked and race for the write, each blind to the
316
434
  # other. `transaction` (via `include Adapters::PostgresOutbox`) is
317
435
  # already re-entrant — `append`/`atomic_put`'s own inner
318
436
  # `transaction do ... end`, deep inside the block below, joins
319
- # this SAME transaction instead of opening/committing its own, so
437
+ # this same transaction instead of opening/committing its own, so
320
438
  # the advisory lock stays held until this whole block returns.
439
+ #
440
+ # @yield the dispatch order to run under the lock; an exception rolls the
441
+ # transaction back
442
+ # @yieldreturn [Object] whatever the dispatch order produces
443
+ # @return [Object] the block's own value
444
+ # @raise [PG::ConnectionBad] if the connection dropped; the adapter reconnects first,
445
+ # then re-raises so the caller decides whether to retry
446
+ # @raise [Runtime::WiringError] if the connection dropped and the reconnect attempt
447
+ # (`connect_for`) is refused as well
321
448
  def with_write_lock(&block) = transaction { lock_writes!; block.call } # rubocop:disable Style/Semicolon
322
449
 
323
- # HELD FOR THE WHOLE TRANSACTION, not just around the INSERT the
450
+ # Appends one entry to the journal and brings the head snapshot and every field
451
+ # cache up to date, all in one transaction under the domain's write lock.
452
+ #
453
+ # The lock is held for the whole transaction, not just around the INSERT — the
324
454
  # ordinal is assigned by the column's own `nextval()` default, inside
325
455
  # this same statement, so the lock has to already be held before that
326
- # default evaluates. A DIFFERENT key from `mint_era!`/`merge_tail!`'s
327
- # `hecks_eras:domain` : this serializes plain writes against EACH
328
- # OTHER, never against a mint. See postgres/lineage.rb's own comment
456
+ # default evaluates. A different key from `mint_era!`/`merge_tail!`'s
457
+ # `hecks_eras:domain` : this serializes plain writes against each
458
+ # other, never against a mint. See postgres/lineage.rb's own comment
329
459
  # for why only that half of the race is closed.
330
460
  # The journal insert and the snapshot upsert/delete happen in the
331
- # SAME transaction — real ACID atomicity, not the append-then-
461
+ # same transaction — real ACID atomicity, not the append-then-
332
462
  # project two-step a file-based adapter needs a crash-recovery
333
463
  # replay for (see Heki). If this transaction commits, the snapshot
334
464
  # is already exactly as current as the journal; if it doesn't,
@@ -340,7 +470,15 @@ module Hecks
340
470
  # dispatch, `lock_writes!` here is a harmless re-acquire of the
341
471
  # same already-held (per-session-reentrant) advisory lock; a bare
342
472
  # `repository.save` outside a full dispatch still takes it fresh.
473
+ #
474
+ # @param entry [Ports::Persistence::Entry] the save or delete to journal; `state`
475
+ # is decoded domain state, encoded through `StateCodec` on the way in
476
+ # @return [Ports::Persistence::Entry] the same entry, unchanged
477
+ # @raise [Runtime::WiringError] if this boot's era has been superseded by a mint
478
+ # @raise [PG::Error] if Postgres refuses a statement, such as the era fence's
479
+ # row-level security rejecting the INSERT
343
480
  def append(entry)
481
+ refuse_superseded_write!
344
482
  transaction do
345
483
  lock_writes!
346
484
  append_and_project!(entry)
@@ -348,11 +486,44 @@ module Hecks
348
486
  entry
349
487
  end
350
488
 
489
+ # Refuses a write from a checkout whose era a later mint has superseded.
490
+ #
491
+ # Before the transaction, before the lock, before the INSERT — a
492
+ # superseded checkout takes nothing and touches nothing. Reads are
493
+ # deliberately untouched: `EraResolver.check!`'s own contract for an
494
+ # old checkout is "may keep booting and reading, but may not keep
495
+ # writing", and the head views it reads through are its own era's.
496
+ #
497
+ # @return [nil] when this boot's era is current
498
+ # @raise [Runtime::WiringError] if the boot gate marked this era superseded
499
+ # (`superseded_by` in the settings)
500
+ def refuse_superseded_write!
501
+ return unless @superseded_by
502
+
503
+ raise Runtime::WiringError,
504
+ "cannot write #{table} for #{@domain}: this checkout booted era #{@era}, which era " \
505
+ "#{@superseded_by} has superseded — its shape was replaced by a mint, and a write here would " \
506
+ "land in a partition no newer head reads. Reads still work; pull the current bluebook and " \
507
+ "reboot to write again."
508
+ end
509
+
510
+ # Saves an entry and reports whether it inserted or replaced, deciding that under the
511
+ # same lock that guards the write.
512
+ #
351
513
  # Outcome detection, journal append and every derived projection share
352
- # the SAME transaction and domain write lock. The lineage-aware head
514
+ # the same transaction and domain write lock. The lineage-aware head
353
515
  # determines whether this id is already visible; no repository `find`
354
516
  # occurs before entering this adapter-native operation.
517
+ #
518
+ # @param entry [Ports::Persistence::Entry] the save to journal
519
+ # @param insert_only [Boolean] when true, an id the head already shows is left
520
+ # untouched and nothing is journaled
521
+ # @return [Symbol] `:inserted`, `:replaced`, or `:conflicted` when `insert_only` met
522
+ # an existing record
523
+ # @raise [Runtime::WiringError] if this boot's era has been superseded by a mint
524
+ # @raise [PG::Error] if Postgres refuses a statement
355
525
  def atomic_put(entry, insert_only: false)
526
+ refuse_superseded_write!
356
527
  status = nil
357
528
  transaction do
358
529
  lock_writes!
@@ -370,17 +541,31 @@ module Hecks
370
541
  status
371
542
  end
372
543
 
373
- # The head is DERIVED projecting is reading, so there is nothing
544
+ # Builds the instance an entry describes, writing nothing.
545
+ #
546
+ # The head is derived — projecting is reading, so there is nothing
374
547
  # to write here. `append` above already keeps the snapshot the head
375
548
  # view reads from current, transactionally. The instance is still
376
549
  # built (and validated) so a save returns what every other adapter
377
550
  # returns.
551
+ #
552
+ # @param entry [Ports::Persistence::Entry] a journaled save or delete
553
+ # @return [Runtime::Instance, nil] the saved record, or nil for a delete entry
554
+ # @raise [Runtime::WiringError] if the entry's state is still in its stored
555
+ # (undecoded) form; see `Ports::Persistence::CodecBoundary`
378
556
  def project(entry)
379
557
  return if entry.delete?
380
558
 
381
559
  Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: entry.state)
382
560
  end
383
561
 
562
+ # Reads this aggregate's whole journal, every era, in the order it was written.
563
+ # States come back decoded but untranslated: an ancestor era's row keeps the shape
564
+ # it was written in.
565
+ #
566
+ # @return [Array<Ports::Persistence::Entry>] saves and deletes by ascending ordinal;
567
+ # a delete's `state` is nil, `mirrors` is nil when none were recorded, and a row
568
+ # with no recorded operation reads as `"save"`; `[]` for an empty journal
384
569
  def entries
385
570
  @db.exec_params(
386
571
  "SELECT aggregate_id, operation, state, mirrors FROM #{@lineage.quoted_journal} " \
@@ -391,16 +576,19 @@ module Hecks
391
576
  Ports::Persistence::Entry.new(
392
577
  operation: row["operation"] || "save",
393
578
  id: row["aggregate_id"],
394
- state: state&.transform_keys(&:to_sym),
579
+ state: Ports::Persistence::StateCodec.decode(@aggregate, state),
395
580
  mirrors: row["mirrors"] && JSON.parse(row["mirrors"])
396
581
  )
397
582
  end
398
583
  end
399
584
 
585
+ # Deletes this aggregate's journal rows, refusing when row-level security silently
586
+ # turns the DELETE into a no-op.
587
+ #
400
588
  # The journal carries FORCE ROW LEVEL SECURITY with exactly two
401
589
  # policies — hecks_current_era's INSERT and hecks_read_all's
402
590
  # SELECT (advance_era! above) — and no DELETE policy at all, for
403
- # anyone. FORCE means even the table's own owner is fenced by
591
+ # anyone. `FORCE` means even the table's own owner is fenced by
404
592
  # that (only an actual Postgres superuser or a role granted
405
593
  # BYPASSRLS sits above it — see lineage.rb's own header), so a
406
594
  # plain `DELETE ... WHERE aggregate = $1` from an ordinary
@@ -410,6 +598,14 @@ module Hecks
410
598
  # tells "nothing to delete" apart from "RLS silently ate the
411
599
  # delete" — the same row count, from the same statement, either
412
600
  # way, with no separate query racing the DELETE for an answer.
601
+ #
602
+ # Only the journal is cleared: head snapshots, field caches, the `events` table and
603
+ # saga rows are left as they are.
604
+ #
605
+ # @return [Adapters::PostgresEra] this adapter
606
+ # @raise [Runtime::WiringError] if the journal held rows for this aggregate and the
607
+ # DELETE removed none, which means the connection is neither a superuser nor
608
+ # granted BYPASSRLS
413
609
  def reset!
414
610
  before = @db.exec_params(
415
611
  "SELECT count(*) FROM #{@lineage.quoted_journal} WHERE aggregate = $1", [table]
@@ -425,18 +621,40 @@ module Hecks
425
621
  self
426
622
  end
427
623
 
624
+ # Journals an instance's current state as a save entry. The direct-adapter
625
+ # convenience for specs and consoles; a runtime saves through
626
+ # `Ports::Persistence::AppendOnly`, which builds the entry itself.
627
+ #
628
+ # @param instance [Runtime::Instance] the record to persist
629
+ # @return [Runtime::Instance] a fresh instance built from the journaled state
630
+ # @raise [Runtime::WiringError] if this boot's era has been superseded by a mint
631
+ # @raise [PG::Error] if Postgres refuses a statement
428
632
  def save(instance)
429
633
  entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
430
634
  append(entry)
431
635
  project(entry)
432
636
  end
433
637
 
638
+ # Journals a delete entry for an id and tombstones it in the head snapshot, without
639
+ # checking first that the record exists.
640
+ #
641
+ # @param id [String, Object] the record's identity, journaled as `id.to_s`
642
+ # @return [true] always, whether or not a record had that id
643
+ # @raise [Runtime::WiringError] if this boot's era has been superseded by a mint
644
+ # @raise [PG::Error] if Postgres refuses a statement
434
645
  def delete(id)
435
646
  entry = Ports::Persistence::Entry.new(operation: "delete", id: id.to_s, state: nil)
436
647
  append(entry)
437
648
  true
438
649
  end
439
650
 
651
+ # Stores one emitted event durably in the `events` table, which every aggregate
652
+ # bound to this database and schema shares.
653
+ #
654
+ # @param event [Runtime::Event] the event to record; `payload` is stored as JSON,
655
+ # `occurred_at` as text (an ISO 8601 UTC string, or nil), and `correlation` is
656
+ # not stored
657
+ # @return [PG::Result] the INSERT's result, which carries no rows
440
658
  def record_event(event)
441
659
  @db.exec_params(
442
660
  "INSERT INTO events (name, aggregate, aggregate_id, payload, occurred_at) VALUES ($1, $2, $3, $4, $5)",
@@ -444,6 +662,11 @@ module Hecks
444
662
  )
445
663
  end
446
664
 
665
+ # Reads every recorded event in the order it was recorded. The `events` table is
666
+ # shared, so this is not limited to this adapter's aggregate.
667
+ #
668
+ # @return [Array<Runtime::Event>] events with symbol-keyed `payload`, `occurred_at`
669
+ # as stored text and `correlation` nil; `[]` when none are recorded
447
670
  def events
448
671
  @db.exec("SELECT * FROM events ORDER BY id").map do |row|
449
672
  Runtime::Event.new(
@@ -456,16 +679,28 @@ module Hecks
456
679
  end
457
680
  end
458
681
 
459
- # ── the OPTIONAL saga-persistence capability (Ports::Persistence's
682
+ # ── the optional saga-persistence capability (Ports::Persistence's
460
683
  # own three-method shape, §2) — one row per (domain, process_manager,
461
684
  # correlation), `domain` kept as an explicit column even under
462
685
  # schema isolation so two domains sharing one schema (neither
463
686
  # declares its own `schema`) still isolate correctly, matching
464
687
  # `hecks_eras`' own precedent (postgres/lineage/provisioning.rb).
465
688
  # No advisory lock of its own: every call here already runs inside
466
- # `SagaInterpreter`'s own mutex (§7) serializing IN-PROCESS writers,
467
- # and gets the SAME cross-process safety an aggregate's own writes
689
+ # `SagaInterpreter`'s own mutex (§7) serializing in-process writers,
690
+ # and gets the same cross-process safety an aggregate's own writes
468
691
  # get from this adapter — no better, no worse.
692
+
693
+ # Checkpoints one saga instance, replacing the row for the same
694
+ # (domain, process manager, correlation) if one exists.
695
+ #
696
+ # @param process_manager [String, Symbol] the process manager's name
697
+ # @param correlation [String, Object] the instance's correlation value, stored as
698
+ # `correlation.to_s`
699
+ # @param state [String, Symbol] the saga's current state name
700
+ # @param memory [Hash] the saga's memory, stored as JSON
701
+ # @param completed_compensations [Array] the ledger of compensable legs already
702
+ # completed, stored as JSON; `[]` when there are none
703
+ # @return [PG::Result] the upsert's result, which carries no rows
469
704
  def save_saga(process_manager:, correlation:, state:, memory:, completed_compensations: [])
470
705
  @db.exec_params(
471
706
  "INSERT INTO hecks_saga_instances (domain, process_manager, correlation, state, memory, completed_compensations) " \
@@ -478,6 +713,12 @@ module Hecks
478
713
  )
479
714
  end
480
715
 
716
+ # Removes a finished saga instance's checkpoint; a no-op when no such row exists.
717
+ #
718
+ # @param process_manager [String, Symbol] the process manager's name
719
+ # @param correlation [String, Object] the instance's correlation value, matched as
720
+ # `correlation.to_s`
721
+ # @return [PG::Result] the DELETE's result, which carries no rows
481
722
  def delete_saga(process_manager:, correlation:)
482
723
  @db.exec_params(
483
724
  "DELETE FROM hecks_saga_instances WHERE domain = $1 AND process_manager = $2 AND correlation = $3",
@@ -485,6 +726,18 @@ module Hecks
485
726
  )
486
727
  end
487
728
 
729
+ # Yields every saga checkpoint stored for this domain, so a booting registry can
730
+ # rehydrate its in-flight sagas.
731
+ #
732
+ # @yieldparam process_manager [String] the process manager's name
733
+ # @yieldparam correlation [String] the instance's correlation value
734
+ # @yieldparam state [String] the saga's state name
735
+ # @yieldparam memory [Hash{Symbol => Object}] the saga's memory, keys symbolized at
736
+ # every depth
737
+ # @yieldparam completed_compensations [Array] the completed-compensation ledger,
738
+ # `[]` when the row stores none
739
+ # @return [Enumerator, PG::Result] an enumerator over the same five values when no
740
+ # block is given; otherwise the query result, which callers ignore
488
741
  def each_saga
489
742
  return enum_for(:each_saga) unless block_given?
490
743
 
@@ -509,7 +762,7 @@ module Hecks
509
762
  end
510
763
 
511
764
  def append_and_project!(entry)
512
- state_json = entry.state && JSON.generate(entry.state)
765
+ state_json = entry.state && JSON.generate(Ports::Persistence::StateCodec.encode(@aggregate, entry.state))
513
766
  ordinal = @db.exec_params(
514
767
  "INSERT INTO #{@lineage.quoted_journal} (era, aggregate, aggregate_id, operation, state, mirrors) " \
515
768
  "VALUES ($1, $2, $3, $4, $5, $6) RETURNING ordinal",
@@ -524,7 +777,7 @@ module Hecks
524
777
  "state = EXCLUDED.state WHERE #{quoted_head_snapshot}.ordinal < EXCLUDED.ordinal",
525
778
  [entry.id, ordinal, state_json]
526
779
  )
527
- # SAME TRANSACTION, SAME ORDINAL — every field cache stays
780
+ # **Same transaction, same ordinal** — every field cache stays
528
781
  # exactly as current as the snapshot it's derived from, for
529
782
  # the identical reason `postgres_era.rb`'s own header comment
530
783
  # gives for the journal/snapshot pair: if this transaction
@@ -534,20 +787,20 @@ module Hecks
534
787
  @lineage.upsert_field_cache_row!(cache_table, entry.id, ordinal, state_json, query_expression(field))
535
788
  end
536
789
  else
537
- # A TOMBSTONE ROW, NOT A BARE DELETE — H3 (docs/audits/2026-08-
538
- # 10-main-bug-audit.md). `DELETE FROM head_snapshot` used to be
539
- # the whole story here, which is correct in isolation but wrong
790
+ # A tombstone row, not a bare DELETE — H3 (docs/audits/2026-08-
791
+ # 10-main-bug-audit.md). A plain `DELETE FROM head_snapshot` is
792
+ # correct in isolation but wrong
540
793
  # once an ancestor era is in the picture: for a record carried
541
- # into this era from an ancestor, removing this era's row left
542
- # NOTHING on the current-era side of `compile_head!`'s union to
794
+ # into this era from an ancestor, removing this era's row leaves
795
+ # nothing on the current-era side of `compile_head!`'s union to
543
796
  # outrank the ancestor matview's own (still-present, still
544
- # `save`) row, so `DISTINCT ON` picked the ancestor's row and
545
- # the "deleted" record kept reading back forever. Upserting a
797
+ # `save`) row, so `DISTINCT ON` picks the ancestor's row and
798
+ # the "deleted" record keeps reading back forever. Upserting a
546
799
  # tombstone (`operation = 'delete'`, `state` NULL) instead
547
- # means this era always has ITS OWN newest-ordinal row for the
548
- # id, exactly like a real re-save already did ("re-saves are
800
+ # means this era always has its own newest-ordinal row for the
801
+ # id, exactly like a real re-save does ("re-saves are
549
802
  # masked correctly" — the audit's own phrasing for why that
550
- # half of this was never broken) — it just carries `operation
803
+ # half never breaks) — it just carries `operation
551
804
  # = 'delete'` instead of `'save'`, so `head_view`'s own `WHERE
552
805
  # operation = 'save'` still correctly hides it. Ordinal-guarded
553
806
  # the same as every other upsert here, so an out-of-order
@@ -607,11 +860,13 @@ module Hecks
607
860
  Runtime::Instance.new(aggregate: @aggregate, id: row["id"], state: decode(row["state"]))
608
861
  end
609
862
 
863
+ # Through the state codec (PR A3), after any SQL-side era
864
+ # translation has already run: the head view hands back the
865
+ # translated jsonb, and decoding it is the last step, the same as
866
+ # every other adapter's read. A never-seeded projected field is
867
+ # simply absent from the blob, and stays absent.
610
868
  def decode(state_json)
611
- # Deep symbols, exactly what the Sqlite adapter's per-column
612
- # `symbolize_names:` decode produces — value-object members and
613
- # list elements arrive symbol-keyed either way.
614
- JSON.parse(state_json, symbolize_names: true)
869
+ Ports::Persistence::StateCodec.decode(@aggregate, JSON.parse(state_json))
615
870
  end
616
871
 
617
872
  def quote_ident(name) = PG::Connection.quote_ident(name.to_s)
@@ -638,9 +893,9 @@ module Hecks
638
893
  "#{order_expression(order_by.field)} #{direction}#{nulls}, id #{direction}"
639
894
  end
640
895
 
641
- # One shared walk decides numericness at ANY depth — this used to
642
- # inspect only the first nested segment, so a two-level path
643
- # (pizza.price_cents.cents) skipped the ::numeric cast and ordered
896
+ # One shared walk decides numericness at any depth — inspecting
897
+ # only the first nested segment would let a two-level path
898
+ # (pizza.price_cents.cents) skip the ::numeric cast and order
644
899
  # as text: "900" above "1200".
645
900
  def numeric_field?(field)
646
901
  name, *path = field.to_s.split(".")
@@ -649,8 +904,8 @@ module Hecks
649
904
  end
650
905
  end
651
906
 
652
- # ARRAY[...] of individually-escaped literals, never the hand-rolled
653
- # '{a,b,c}' array-literal SYNTAX — a segment is a field or
907
+ # `ARRAY[...]` of individually-escaped literals, never the hand-rolled
908
+ # '{a,b,c}' array-literal syntax — a segment is a field or
654
909
  # value-object member name, and while today's callers only ever
655
910
  # pass schema-declared names, this method has no way to know
656
911
  # that, and the '{...}' form has no escaping at all: a segment
@@ -658,7 +913,7 @@ module Hecks
658
913
  # follows becomes live SQL. Measured, not assumed — a crafted
659
914
  # field name of `x}' = '' OR $1::text = $1::text -- ` made a
660
915
  # `where(secret: "public")` clause return every row regardless,
661
- # against the OLD form; the ARRAY[] form below closes it, verified
916
+ # against the '{...}' form; the `ARRAY[]` form below closes it, verified
662
917
  # against the identical payload.
663
918
  def jsonb_path(segments)
664
919
  "state #>> ARRAY[#{segments.map { |segment| text_literal(segment) }.join(', ')}]::text[]"
@@ -668,7 +923,7 @@ module Hecks
668
923
 
669
924
  # ── the field-cache read shortcut (Track C) ─────────────────────
670
925
 
671
- # EVERY declared `where`-field this aggregate's own queries and its
926
+ # Every declared `where`-field this aggregate's own queries and its
672
927
  # entities' own queries use, minus anything a cache table can't
673
928
  # represent (see `cacheable_field?`) — never `order_by`-only
674
929
  # fields, which never needed a cache in the first place (sorting
@@ -693,10 +948,10 @@ module Hecks
693
948
  @aggregate.queries + @aggregate.entities.flat_map(&:queries)
694
949
  end
695
950
 
696
- # LIST-TYPED FIELDS ARE EXCLUDED, same boundary the plain `Postgres`
951
+ # List-typed fields are excluded, same boundary the plain `Postgres`
697
952
  # and `Sqlite`/`D1` adapters independently landed on for their own
698
953
  # automatic indexing: `contains` means element membership, and a
699
- # (id, ordinal, ONE value) cache row has nowhere to put more than
954
+ # (id, ordinal, one value) cache row has nowhere to put more than
700
955
  # one element. Everything else — a plain scalar, the lifecycle
701
956
  # field, or a non-list value-object member path — reduces to
702
957
  # exactly one comparable value per id, which is the one shape this
@@ -710,7 +965,7 @@ module Hecks
710
965
  end
711
966
 
712
967
  # A clause can be served by the cache when its field has a cache
713
- # table AND it isn't the null-vs-value special case
968
+ # table and it isn't the null-vs-value special case
714
969
  # `QuerySpecification::Common::NullPolicy` intercepts before
715
970
  # `where_clause` ever runs (see `query`'s own header comment) — a
716
971
  # clause that fails either check simply flows to `head_phase`
@@ -721,11 +976,11 @@ module Hecks
721
976
  value).nil?
722
977
  end
723
978
 
724
- # PHASE ONE — candidate ids, no reduction touched. One SELECT per
979
+ # **Phase one** — candidate ids, no reduction touched. One SELECT per
725
980
  # cached clause against its own narrow (id, ordinal, value) table,
726
981
  # `INTERSECT`ed into the set that satisfies every cached clause at
727
982
  # once. Reuses `where_clause` (SqlQueryBuilder, private, already
728
- # mixed into this class) UNCHANGED against the cache table's own
983
+ # mixed into this class) unchanged against the cache table's own
729
984
  # `value` column instead of a jsonb path expression — the exact
730
985
  # same operator compilation (`eq`/`ne`/`gt`/`gte`/`lt`/`lte`/`in`)
731
986
  # a live query already gets against the real column, so a cached
@@ -741,12 +996,12 @@ module Hecks
741
996
  @db.exec_params(clauses.join("\nINTERSECT\n"), binds).map { |row| row["id"] }
742
997
  end
743
998
 
744
- # PHASE TWO — `head_view`, restricted to phase one's candidate ids
745
- # PLUS whatever clauses phase one couldn't accelerate, applied
999
+ # **Phase two** — `head_view`, restricted to phase one's candidate ids
1000
+ # plus whatever clauses phase one couldn't accelerate, applied
746
1001
  # exactly the way `SqlQueryBuilder#query` (`super`) already applies
747
1002
  # every clause today: against the fully-reduced view, which is
748
1003
  # already correct regardless of caching (see this file's own
749
- # `query` comment — a cache is a SPEED shortcut, never a
1004
+ # `query` comment — a cache is a speed shortcut, never a
750
1005
  # correctness fix; head_view was always safe to filter directly,
751
1006
  # just expensive to reduce in the first place). Duplicates a small
752
1007
  # slice of `SqlQueryBuilder#query`'s own tail assembly (order_by/