hecks 2.8.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (886) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +50 -7
  3. data/lib/hecks/adapters/driven/claude_code.rb +4 -104
  4. data/lib/hecks/adapters/driven/d1.rb +44 -237
  5. data/lib/hecks/adapters/driven/folder.rb +17 -76
  6. data/lib/hecks/adapters/driven/google_authentication.rb +15 -49
  7. data/lib/hecks/adapters/driven/governance_authorization.rb +23 -70
  8. data/lib/hecks/adapters/driven/heki/journal.rb +8 -41
  9. data/lib/hecks/adapters/driven/heki/saga_store.rb +12 -54
  10. data/lib/hecks/adapters/driven/heki/snapshot.rb +4 -12
  11. data/lib/hecks/adapters/driven/heki.rb +13 -42
  12. data/lib/hecks/adapters/driven/identity_registry.rb +3 -14
  13. data/lib/hecks/adapters/driven/in_memory_ordering.rb +9 -26
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +2 -14
  15. data/lib/hecks/adapters/driven/lambda/client.rb +10 -51
  16. data/lib/hecks/adapters/driven/lambda.rb +10 -84
  17. data/lib/hecks/adapters/driven/local_storage.rb +5 -40
  18. data/lib/hecks/adapters/driven/memory.rb +32 -100
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +5 -21
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +6 -28
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +3 -12
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +10 -32
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +34 -108
  24. data/lib/hecks/adapters/driven/postgres/shared_connection.rb +180 -0
  25. data/lib/hecks/adapters/driven/postgres.rb +149 -136
  26. data/lib/hecks/adapters/driven/postgres_era.adapter +4 -15
  27. data/lib/hecks/adapters/driven/prism.rb +48 -38
  28. data/lib/hecks/adapters/driven/sql_query_builder.rb +21 -121
  29. data/lib/hecks/adapters/driven/sqlite/codec.rb +8 -40
  30. data/lib/hecks/adapters/driven/sqlite/projection.rb +26 -82
  31. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +40 -84
  32. data/lib/hecks/adapters/driven/sqlite.rb +128 -65
  33. data/lib/hecks/adapters/driven/tenant_provisioner.rb +76 -62
  34. data/lib/hecks/adapters/driven.rb +5 -36
  35. data/lib/hecks/adapters/driving/github_webhook.rb +8 -76
  36. data/lib/hecks/behaviors/dsl.rb +7 -25
  37. data/lib/hecks/behaviors/expectations.rb +32 -142
  38. data/lib/hecks/behaviors/ir.rb +7 -20
  39. data/lib/hecks/behaviors/rspec.rb +6 -17
  40. data/lib/hecks/behaviors/runner.rb +10 -34
  41. data/lib/hecks/behaviors.rb +4 -8
  42. data/lib/hecks/bench/cli.rb +62 -0
  43. data/lib/hecks/bench/environment.rb +71 -0
  44. data/lib/hecks/bench/postgres_probe.rb +30 -0
  45. data/lib/hecks/bench/report.rb +76 -0
  46. data/lib/hecks/bench/ruby_runner.rb +93 -0
  47. data/lib/hecks/bench/run.rb +55 -0
  48. data/lib/hecks/bench/rust_runner.rb +101 -0
  49. data/lib/hecks/bench/stats.rb +46 -0
  50. data/lib/hecks/bench/suite.rb +98 -0
  51. data/lib/hecks/bench/workload.rb +89 -0
  52. data/lib/hecks/bench.rb +17 -0
  53. data/lib/hecks/bluebook/aggregate.rb +9 -75
  54. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +9 -38
  55. data/lib/hecks/bluebook/assembly/build.rb +4 -15
  56. data/lib/hecks/bluebook/assembly/contract.rb +15 -136
  57. data/lib/hecks/bluebook/assembly/contracts.rb +56 -174
  58. data/lib/hecks/bluebook/assembly/marks.rb +31 -223
  59. data/lib/hecks/bluebook/assembly/specializer.rb +6 -52
  60. data/lib/hecks/bluebook/assembly.rb +6 -45
  61. data/lib/hecks/bluebook/attribute.rb +12 -61
  62. data/lib/hecks/bluebook/behaviour/aggregate.rb +39 -39
  63. data/lib/hecks/bluebook/behaviour/attribute.rb +3 -14
  64. data/lib/hecks/bluebook/behaviour/chapter.rb +14 -42
  65. data/lib/hecks/bluebook/behaviour/command.rb +14 -66
  66. data/lib/hecks/bluebook/behaviour/domain_port.rb +11 -20
  67. data/lib/hecks/bluebook/behaviour/entity.rb +7 -27
  68. data/lib/hecks/bluebook/behaviour/hexagon.rb +23 -41
  69. data/lib/hecks/bluebook/behaviour/lifecycle.rb +15 -40
  70. data/lib/hecks/bluebook/behaviour/policy.rb +23 -52
  71. data/lib/hecks/bluebook/behaviour/process_manager.rb +30 -57
  72. data/lib/hecks/bluebook/behaviour/read_model.rb +25 -31
  73. data/lib/hecks/bluebook/behaviour/traits.rb +27 -64
  74. data/lib/hecks/bluebook/behaviour/value_object.rb +5 -24
  75. data/lib/hecks/bluebook/capabilities.rb +17 -49
  76. data/lib/hecks/bluebook/chapter.rb +16 -50
  77. data/lib/hecks/bluebook/command.rb +23 -81
  78. data/lib/hecks/bluebook/domain_port.rb +49 -67
  79. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +50 -229
  80. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +50 -377
  81. data/lib/hecks/bluebook/dsl/attribute_collector.rb +33 -204
  82. data/lib/hecks/bluebook/dsl/binding_proxy.rb +16 -69
  83. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +96 -708
  84. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +63 -169
  85. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +3 -1
  86. data/lib/hecks/bluebook/dsl/command_builder.rb +74 -546
  87. data/lib/hecks/bluebook/dsl/const_shim.rb +8 -41
  88. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +42 -89
  89. data/lib/hecks/bluebook/dsl/entity_builder.rb +44 -330
  90. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +20 -269
  91. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +148 -197
  92. data/lib/hecks/bluebook/dsl/identity_declaration.rb +13 -88
  93. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +6 -20
  94. data/lib/hecks/bluebook/dsl/policy_builder.rb +16 -88
  95. data/lib/hecks/bluebook/dsl/port_builder.rb +2 -6
  96. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +22 -96
  97. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +31 -184
  98. data/lib/hecks/bluebook/dsl/query_builder.rb +38 -43
  99. data/lib/hecks/bluebook/dsl/read_model_builder.rb +75 -201
  100. data/lib/hecks/bluebook/dsl/rule_reference.rb +22 -163
  101. data/lib/hecks/bluebook/dsl/translation_builder.rb +28 -114
  102. data/lib/hecks/bluebook/dsl/value_object_builder.rb +10 -124
  103. data/lib/hecks/bluebook/dsl/word_gate.rb +16 -162
  104. data/lib/hecks/bluebook/dsl/world_builder.rb +46 -111
  105. data/lib/hecks/bluebook/entity.rb +13 -71
  106. data/lib/hecks/bluebook/expression/ast_json.rb +24 -190
  107. data/lib/hecks/bluebook/expression/ast_reader.rb +12 -54
  108. data/lib/hecks/bluebook/expression/canonical_form.rb +11 -88
  109. data/lib/hecks/bluebook/expression/evaluator.rb +47 -295
  110. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +30 -147
  111. data/lib/hecks/bluebook/expression/resolver.rb +39 -661
  112. data/lib/hecks/bluebook/hexagon.rb +18 -33
  113. data/lib/hecks/bluebook/lifecycle.rb +5 -16
  114. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -7
  115. data/lib/hecks/bluebook/meta_validator/judge.rb +111 -361
  116. data/lib/hecks/bluebook/meta_validator/plan.rb +33 -162
  117. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -7
  118. data/lib/hecks/bluebook/meta_validator/readings.rb +56 -342
  119. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +25 -176
  120. data/lib/hecks/bluebook/meta_validator/shapes.rb +53 -267
  121. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +47 -306
  122. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +7 -43
  123. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +246 -0
  124. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -18
  125. data/lib/hecks/bluebook/meta_validator.rb +87 -352
  126. data/lib/hecks/bluebook/model_check/client_profile.rb +6 -21
  127. data/lib/hecks/bluebook/model_check.rb +122 -596
  128. data/lib/hecks/bluebook/pattern_subset.rb +49 -69
  129. data/lib/hecks/bluebook/process_manager.rb +13 -83
  130. data/lib/hecks/bluebook/project_loader.rb +3 -39
  131. data/lib/hecks/bluebook/project_register.rb +12 -60
  132. data/lib/hecks/bluebook/query.rb +27 -51
  133. data/lib/hecks/bluebook/read_model.rb +57 -81
  134. data/lib/hecks/bluebook/reference.rb +7 -41
  135. data/lib/hecks/bluebook/smoke_test.rb +9 -99
  136. data/lib/hecks/bluebook/synthesizer.rb +13 -62
  137. data/lib/hecks/bluebook/translation.rb +26 -62
  138. data/lib/hecks/bluebook/value_object.rb +11 -36
  139. data/lib/hecks/bluebook.rb +5 -22
  140. data/lib/hecks/cache_dir.rb +4 -23
  141. data/lib/hecks/canonical_json.rb +30 -0
  142. data/lib/hecks/chapters.rb +102 -0
  143. data/lib/hecks/cli/behaviors.rb +61 -0
  144. data/lib/hecks/cli/canonicalise.rb +25 -0
  145. data/lib/hecks/cli/check_engine_agreement.rb +32 -0
  146. data/lib/hecks/cli/check_era.rb +88 -0
  147. data/lib/hecks/cli/console.rb +66 -0
  148. data/lib/hecks/cli/deploy_template_diff.rb +40 -0
  149. data/lib/hecks/cli/doc_coverage.rb +32 -0
  150. data/lib/hecks/cli/document.rb +5 -23
  151. data/lib/hecks/cli/expression_projection.rb +41 -0
  152. data/lib/hecks/cli/follow.rb +88 -0
  153. data/lib/hecks/cli/history.rb +44 -0
  154. data/lib/hecks/cli/ir.rb +4 -6
  155. data/lib/hecks/cli/mcp.rb +5 -6
  156. data/lib/hecks/cli/model_check.rb +24 -88
  157. data/lib/hecks/cli/pattern_cases.rb +47 -0
  158. data/lib/hecks/cli/present.rb +39 -0
  159. data/lib/hecks/cli/project_cli.rb +223 -64
  160. data/lib/hecks/cli/project_diagrams.rb +4 -10
  161. data/lib/hecks/cli/project_glossary.rb +44 -0
  162. data/lib/hecks/cli/reference.rb +46 -0
  163. data/lib/hecks/cli/refresh_projections.rb +44 -0
  164. data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +150 -0
  165. data/lib/hecks/cli/release.rb +70 -0
  166. data/lib/hecks/cli/release_gem.rb +59 -0
  167. data/lib/hecks/cli/rspec_io_parallel_files.rb +137 -0
  168. data/lib/hecks/cli/rspec_shard_files.rb +66 -0
  169. data/lib/hecks/cli/run.rb +15 -61
  170. data/lib/hecks/cli/seed_semantics_corpus.rb +71 -0
  171. data/lib/hecks/cli/serve_query_ir_mcp.rb +30 -0
  172. data/lib/hecks/cli/shape.rb +70 -0
  173. data/lib/hecks/cli/smoke_http.rb +230 -0
  174. data/lib/hecks/cli/smoke_test.rb +7 -19
  175. data/lib/hecks/cli/spec_example.rb +27 -0
  176. data/lib/hecks/cli/statements.rb +32 -0
  177. data/lib/hecks/cli/stores.rb +4 -6
  178. data/lib/hecks/cli/stress_concurrency_specs.rb +177 -0
  179. data/lib/hecks/cli.rb +216 -73
  180. data/lib/hecks/codemod.rb +324 -0
  181. data/lib/hecks/construct.rb +8 -34
  182. data/lib/hecks/corpus.rb +423 -0
  183. data/lib/hecks/deploy/bluebook/deploy.bluebook +617 -158
  184. data/lib/hecks/deploy/bluebook/deploy.hecksagon +41 -11
  185. data/lib/hecks/deploy/oidc.json +82 -2
  186. data/lib/hecks/doc/reference.rb +351 -0
  187. data/lib/hecks/doors/cli_door.rb +196 -0
  188. data/lib/hecks/doors/cli_runner.rb +297 -0
  189. data/lib/hecks/{facade → doors}/command_request.rb +14 -34
  190. data/lib/hecks/doors/handle.rb +206 -0
  191. data/lib/hecks/doors/json_door.rb +132 -0
  192. data/lib/hecks/doors/launcher_options.rb +129 -0
  193. data/lib/hecks/{cli → doors}/mcp_door.rb +25 -120
  194. data/lib/hecks/doors/mcp_door_scope.rb +97 -0
  195. data/lib/hecks/doors/ruby_door/aggregate_door.rb +162 -0
  196. data/lib/hecks/doors/ruby_door/chapter.rb +72 -0
  197. data/lib/hecks/doors/ruby_door.rb +77 -0
  198. data/lib/hecks/doors.rb +61 -0
  199. data/lib/hecks/embryonaut_bluebook/lock.rb +3 -35
  200. data/lib/hecks/embryonaut_bluebook/shape.rb +1 -17
  201. data/lib/hecks/embryonaut_bluebook/vendor.rb +8 -53
  202. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +4 -19
  203. data/lib/hecks/embryonaut_bluebook.rb +5 -72
  204. data/lib/hecks/engine_agreement.rb +156 -0
  205. data/lib/hecks/facade.rb +23 -51
  206. data/lib/hecks/forms/app.rb +21 -76
  207. data/lib/hecks/forms/banking_presentation.rb +50 -0
  208. data/lib/hecks/forms/command_form_renderer.rb +10 -29
  209. data/lib/hecks/forms/field_renderer.rb +17 -156
  210. data/lib/hecks/forms/field_shape.rb +31 -222
  211. data/lib/hecks/forms/html.rb +12 -47
  212. data/lib/hecks/forms/index_renderer.rb +1 -4
  213. data/lib/hecks/forms/page.rb +3 -9
  214. data/lib/hecks/forms/params.rb +14 -60
  215. data/lib/hecks/forms/port_argument.rb +2 -19
  216. data/lib/hecks/forms/query_form_renderer.rb +4 -79
  217. data/lib/hecks/forms/record_renderer.rb +8 -75
  218. data/lib/hecks/forms/record_table.rb +7 -50
  219. data/lib/hecks/forms/reference_options.rb +3 -30
  220. data/lib/hecks/forms/value_object_shape.rb +8 -35
  221. data/lib/hecks/forms.rb +5 -33
  222. data/lib/hecks/fqn.rb +5 -12
  223. data/lib/hecks/framework/bluebook/compliance.bluebook +16 -65
  224. data/lib/hecks/framework/bluebook/console_settings.bluebook +47 -134
  225. data/lib/hecks/framework/bluebook/framework.hecksagon +5 -14
  226. data/lib/hecks/framework/bluebook/governance.bluebook +13 -34
  227. data/lib/hecks/framework/bluebook/identity.bluebook +11 -26
  228. data/lib/hecks/framework/bluebook/privacy.bluebook +8 -33
  229. data/lib/hecks/framework.rb +11 -80
  230. data/lib/hecks/freezer.rb +5 -39
  231. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +393 -0
  232. data/lib/hecks/fuzzing/combination_miner.rb +86 -0
  233. data/lib/hecks/fuzzing/concurrent_dispatch.rb +214 -0
  234. data/lib/hecks/fuzzing/coverage_campaign.rb +143 -0
  235. data/lib/hecks/fuzzing/differential.rb +156 -0
  236. data/lib/hecks/fuzzing/domain_generator.rb +615 -0
  237. data/lib/hecks/fuzzing/era_boundary.rb +71 -0
  238. data/lib/hecks/fuzzing/form_census.rb +136 -0
  239. data/lib/hecks/fuzzing/generated_domain_check.rb +93 -0
  240. data/lib/hecks/fuzzing/invalid_value_generator.rb +64 -0
  241. data/lib/hecks/fuzzing/isolated_boot.rb +301 -0
  242. data/lib/hecks/fuzzing/nondeterministic.rb +51 -0
  243. data/lib/hecks/fuzzing/outside_world.rb +45 -0
  244. data/lib/hecks/fuzzing/persistence_parity.rb +102 -0
  245. data/lib/hecks/fuzzing/properties/corrections.rb +88 -0
  246. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +314 -0
  247. data/lib/hecks/fuzzing/properties/guards.rb +190 -0
  248. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +300 -0
  249. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +93 -0
  250. data/lib/hecks/fuzzing/properties/outbox.rb +89 -0
  251. data/lib/hecks/fuzzing/properties/querying.rb +175 -0
  252. data/lib/hecks/fuzzing/properties.rb +162 -0
  253. data/lib/hecks/fuzzing/qa_settings.rb +123 -0
  254. data/lib/hecks/fuzzing/replay.rb +505 -0
  255. data/lib/hecks/fuzzing/rotation_priority.rb +48 -0
  256. data/lib/hecks/fuzzing/rust_gap_manifest.rb +108 -0
  257. data/lib/hecks/fuzzing/self_consistency.rb +339 -0
  258. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +385 -0
  259. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +132 -0
  260. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +95 -0
  261. data/lib/hecks/fuzzing/sequence_generator/picker.rb +77 -0
  262. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +109 -0
  263. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +237 -0
  264. data/lib/hecks/fuzzing/sequence_generator.rb +189 -0
  265. data/lib/hecks/fuzzing/shrinker.rb +166 -0
  266. data/lib/hecks/fuzzing/structural_skips.rb +33 -0
  267. data/lib/hecks/fuzzing/sweep_depth.rb +43 -0
  268. data/lib/hecks/fuzzing/target_capabilities.rb +163 -0
  269. data/lib/hecks/fuzzing/value_generator.rb +216 -0
  270. data/lib/hecks/fuzzing.rb +23 -0
  271. data/lib/hecks/grammar/evolve.rb +336 -0
  272. data/lib/hecks/grammar/expression.bluebook +15 -40
  273. data/lib/hecks/grammar/translation.bluebook +3 -6
  274. data/lib/hecks/grammar.rb +16 -41
  275. data/lib/hecks/hecks/adapters/codebase/codemods.rb +45 -0
  276. data/lib/hecks/hecks/adapters/codebase/conformance.rb +71 -0
  277. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +202 -0
  278. data/lib/hecks/hecks/adapters/codebase/gem_registry.rb +80 -0
  279. data/lib/hecks/hecks/adapters/codebase/kernel_tables.rb +54 -0
  280. data/lib/hecks/hecks/adapters/codebase/language.rb +240 -0
  281. data/lib/hecks/hecks/adapters/codebase/npm_registry.rb +102 -0
  282. data/lib/hecks/hecks/adapters/codebase/publishing.rb +142 -0
  283. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +67 -0
  284. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +90 -0
  285. data/lib/hecks/hecks/adapters/codebase/ruby_child.rb +83 -0
  286. data/lib/hecks/hecks/adapters/codebase/secret_vault.rb +36 -0
  287. data/lib/hecks/hecks/adapters/codebase/source_tree.adapter +7 -0
  288. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +189 -0
  289. data/lib/hecks/hecks/adapters/codebase/sqlite_fixture.rb +71 -0
  290. data/lib/hecks/hecks/adapters/codebase/style.rb +171 -0
  291. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +55 -0
  292. data/lib/hecks/hecks/adapters/codebase/test_suite.rb +165 -0
  293. data/lib/hecks/hecks/adapters/codebase/tree.rb +111 -0
  294. data/lib/hecks/hecks/adapters/console_capture.rb +79 -0
  295. data/lib/hecks/hecks/adapters/deploy_toolchain.adapter +7 -0
  296. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +108 -0
  297. data/lib/hecks/hecks/adapters/git.adapter +7 -0
  298. data/lib/hecks/hecks/adapters/git.rb +102 -0
  299. data/lib/hecks/hecks/adapters/host_http.adapter +7 -0
  300. data/lib/hecks/hecks/adapters/host_http.rb +65 -0
  301. data/lib/hecks/hecks/adapters/in_process_boot.adapter +7 -0
  302. data/lib/hecks/hecks/adapters/in_process_boot.rb +197 -0
  303. data/lib/hecks/hecks/adapters/in_process_operations.rb +202 -0
  304. data/lib/hecks/hecks/adapters/journal_store/changes.rb +143 -0
  305. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +145 -0
  306. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +132 -0
  307. data/lib/hecks/hecks/adapters/journal_store/examination.rb +135 -0
  308. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +163 -0
  309. data/lib/hecks/hecks/adapters/journal_store/readings.rb +187 -0
  310. data/lib/hecks/hecks/adapters/journal_store.adapter +7 -0
  311. data/lib/hecks/hecks/adapters/journal_store.rb +64 -0
  312. data/lib/hecks/hecks/adapters/local_files.adapter +7 -0
  313. data/lib/hecks/hecks/adapters/local_files.rb +46 -0
  314. data/lib/hecks/hecks/adapters/pg_admin.adapter +7 -0
  315. data/lib/hecks/hecks/adapters/pg_admin.rb +245 -0
  316. data/lib/hecks/hecks/adapters/process_pool.adapter +7 -0
  317. data/lib/hecks/hecks/adapters/process_pool.rb +119 -0
  318. data/lib/hecks/hecks/adapters/rust_toolchain.adapter +7 -0
  319. data/lib/hecks/hecks/adapters/rust_toolchain.rb +179 -0
  320. data/lib/hecks/hecks/adapters/rust_workspace.rb +150 -0
  321. data/lib/hecks/hecks/adapters/shell.adapter +7 -0
  322. data/lib/hecks/hecks/adapters/shell.rb +49 -0
  323. data/lib/hecks/hecks/adapters/terminal.adapter +7 -0
  324. data/lib/hecks/hecks/adapters/terminal.rb +74 -0
  325. data/lib/hecks/hecks/codebase.behaviors +172 -0
  326. data/lib/hecks/hecks/codebase.bluebook +2453 -0
  327. data/lib/hecks/hecks/context_map.hecksagon +13 -0
  328. data/lib/hecks/hecks/custodian.behaviors +179 -0
  329. data/lib/hecks/hecks/custodian.bluebook +1847 -0
  330. data/lib/hecks/hecks/deploy.behaviors +74 -0
  331. data/lib/hecks/hecks/deploy.world +12 -0
  332. data/lib/hecks/hecks/environments/memory.world +10 -0
  333. data/lib/hecks/hecks/governance.world +7 -0
  334. data/lib/hecks/hecks/hecks.behaviors +62 -0
  335. data/lib/hecks/hecks/hecks.bluebook +124 -0
  336. data/lib/hecks/hecks/hecks.hecksagon +390 -0
  337. data/lib/hecks/hecks/hecks.world +42 -0
  338. data/lib/hecks/ir.rb +14 -83
  339. data/lib/hecks/kernel_capabilities.rb +194 -0
  340. data/lib/hecks/language/adapter.bluebook +2 -7
  341. data/lib/hecks/language/bluebook/aggregate.bluebook +56 -221
  342. data/lib/hecks/language/bluebook/attaches/paging.bluebook +6 -15
  343. data/lib/hecks/language/bluebook/bluebook.bluebook +26 -83
  344. data/lib/hecks/language/bluebook/bluebook.hecksagon +3 -15
  345. data/lib/hecks/language/bluebook/command.bluebook +27 -144
  346. data/lib/hecks/language/bluebook/entity.bluebook +24 -97
  347. data/lib/hecks/language/bluebook/policy.bluebook +16 -67
  348. data/lib/hecks/language/bluebook/process_manager.bluebook +28 -151
  349. data/lib/hecks/language/bluebook/projection.bluebook +126 -60
  350. data/lib/hecks/language/bluebook/query.bluebook +19 -43
  351. data/lib/hecks/language/bluebook/shape.bluebook +23 -73
  352. data/lib/hecks/language/bluebook/syntax.bluebook +51 -262
  353. data/lib/hecks/language/bluebook/vocabulary.bluebook +83 -198
  354. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +3 -10
  355. data/lib/hecks/language/hecksagon/domain_port.bluebook +14 -19
  356. data/lib/hecks/language/hecksagon/hecksagon.bluebook +26 -25
  357. data/lib/hecks/language/hecksagon/port_operation.bluebook +3 -14
  358. data/lib/hecks/language/oidc.json +35 -0
  359. data/lib/hecks/language/port.bluebook +4 -17
  360. data/lib/hecks/language/translation/translation.bluebook +3 -9
  361. data/lib/hecks/language/translation/translation_aggregate.bluebook +5 -24
  362. data/lib/hecks/language/world/wiring.bluebook +1 -3
  363. data/lib/hecks/language/world/world.bluebook +5 -7
  364. data/lib/hecks/literal.rb +8 -50
  365. data/lib/hecks/mcp_stdio_guard.rb +4 -41
  366. data/lib/hecks/naming.rb +29 -137
  367. data/lib/hecks/persistence_legacy_fixture/regenerate.rb +161 -0
  368. data/lib/hecks/persistence_legacy_fixture.rb +113 -0
  369. data/lib/hecks/ports/access_control.rb +7 -23
  370. data/lib/hecks/ports/agent/answers.rb +5 -23
  371. data/lib/hecks/ports/agent.rb +47 -172
  372. data/lib/hecks/ports/authentication.rb +12 -39
  373. data/lib/hecks/ports/authorization.rb +16 -43
  374. data/lib/hecks/ports/clock.rb +5 -45
  375. data/lib/hecks/ports/extraction.rb +7 -12
  376. data/lib/hecks/ports/identity_assignment.rb +8 -29
  377. data/lib/hecks/ports/identity_generation.rb +6 -29
  378. data/lib/hecks/ports/identity_resolution.rb +2 -10
  379. data/lib/hecks/ports/key_vault.rb +9 -45
  380. data/lib/hecks/ports/persistence/append_only.rb +61 -64
  381. data/lib/hecks/ports/persistence/binding_policy.rb +4 -14
  382. data/lib/hecks/ports/persistence/codec_boundary.rb +20 -80
  383. data/lib/hecks/ports/persistence/null_saga_store.rb +4 -20
  384. data/lib/hecks/ports/persistence/plugin.rb +13 -42
  385. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +44 -144
  386. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +15 -115
  387. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +20 -85
  388. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +6 -25
  389. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +18 -24
  390. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +55 -181
  391. data/lib/hecks/ports/persistence/plugins/era/postgres_era/events.rb +77 -0
  392. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +12 -122
  393. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +28 -128
  394. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +73 -474
  395. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +31 -124
  396. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +45 -219
  397. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +13 -108
  398. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +19 -76
  399. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +9 -49
  400. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +6 -123
  401. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +23 -55
  402. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +14 -72
  403. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +7 -16
  404. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +43 -127
  405. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +13 -45
  406. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +131 -555
  407. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +13 -63
  408. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +227 -0
  409. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +2 -17
  410. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +3 -14
  411. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +13 -58
  412. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -17
  413. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +9 -26
  414. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +50 -47
  415. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -124
  416. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +34 -123
  417. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +5 -12
  418. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +3 -9
  419. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -14
  420. data/lib/hecks/ports/persistence/plugins/era/translation.rb +1 -0
  421. data/lib/hecks/ports/persistence/plugins/era.rb +4 -23
  422. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +7 -23
  423. data/lib/hecks/ports/persistence/postgres_dump.rb +4 -35
  424. data/lib/hecks/ports/persistence/remote_runtime.rb +2 -24
  425. data/lib/hecks/ports/persistence/repository_factory.rb +8 -27
  426. data/lib/hecks/ports/persistence/state_codec.rb +47 -108
  427. data/lib/hecks/ports/persistence.rb +7 -14
  428. data/lib/hecks/ports/projection.rb +42 -84
  429. data/lib/hecks/ports/query/in_memory.rb +10 -58
  430. data/lib/hecks/ports/query/ordering.rb +11 -32
  431. data/lib/hecks/ports/query.rb +14 -37
  432. data/lib/hecks/ports.rb +2 -8
  433. data/lib/hecks/projection_files.rb +285 -0
  434. data/lib/hecks/projections/bootstrap_table.rb +12 -44
  435. data/lib/hecks/projections/deploy/fargate/assembly.rb +2 -15
  436. data/lib/hecks/projections/deploy/fargate/cdn.rb +1 -48
  437. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +2 -11
  438. data/lib/hecks/projections/deploy/fargate/check.rb +5 -14
  439. data/lib/hecks/projections/deploy/fargate/containers.rb +3 -78
  440. data/lib/hecks/projections/deploy/fargate/extras.rb +3 -44
  441. data/lib/hecks/projections/deploy/fargate/monitoring.rb +0 -44
  442. data/lib/hecks/projections/deploy/fargate/settings.rb +2 -54
  443. data/lib/hecks/projections/deploy/fargate/yaml.rb +13 -58
  444. data/lib/hecks/projections/deploy/fargate.rb +40 -207
  445. data/lib/hecks/projections/deploy/lambda.rb +191 -708
  446. data/lib/hecks/projections/deploy/preview/access_yaml.rb +1 -33
  447. data/lib/hecks/projections/deploy/preview/containers.rb +6 -111
  448. data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
  449. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +4 -56
  450. data/lib/hecks/projections/deploy/preview/script.rb +2 -14
  451. data/lib/hecks/projections/deploy/preview/settings.rb +3 -33
  452. data/lib/hecks/projections/deploy/preview/task_yaml.rb +2 -38
  453. data/lib/hecks/projections/deploy/preview/template.rb +3 -59
  454. data/lib/hecks/projections/deploy/preview.rb +2 -67
  455. data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +1 -1
  456. data/lib/hecks/projections/deploy/scripts/expected-era.header +1 -1
  457. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +3 -3
  458. data/lib/hecks/projections/deploy/scripts/settings.rb +3 -25
  459. data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +1 -1
  460. data/lib/hecks/projections/deploy/scripts.rb +4 -57
  461. data/lib/hecks/projections/deploy/shared.rb +48 -147
  462. data/lib/hecks/projections/deploy/smoke.rb +4 -61
  463. data/lib/hecks/projections/deploy/template_diff/comparer.rb +3 -25
  464. data/lib/hecks/projections/deploy/template_diff/loader.rb +2 -11
  465. data/lib/hecks/projections/deploy/template_diff.rb +5 -57
  466. data/lib/hecks/projections/diagrams.rb +22 -584
  467. data/lib/hecks/projections/glossary/html.rb +14 -130
  468. data/lib/hecks/projections/glossary/markdown.rb +8 -69
  469. data/lib/hecks/projections/glossary/mermaid.rb +9 -66
  470. data/lib/hecks/projections/glossary/sensitivity.rb +5 -34
  471. data/lib/hecks/projections/glossary/sentences.rb +14 -159
  472. data/lib/hecks/projections/glossary.rb +30 -222
  473. data/lib/hecks/projections/ir.rb +3 -10
  474. data/lib/hecks/projections/model/deviations.rb +16 -43
  475. data/lib/hecks/projections/model.rb +12 -51
  476. data/lib/hecks/projections/oidc.rb +12 -56
  477. data/lib/hecks/projections/parser_table.rb +13 -32
  478. data/lib/hecks/projections/reference.rb +4 -18
  479. data/lib/hecks/projections/rust_vocabulary.rb +13 -251
  480. data/lib/hecks/projections/shape.rb +3 -29
  481. data/lib/hecks/projections/statements.rb +7 -86
  482. data/lib/hecks/projections/vocabulary.rb +6 -22
  483. data/lib/hecks/projections.rb +2 -16
  484. data/lib/hecks/projector/cli_projector.rb +154 -400
  485. data/lib/hecks/projector/docs_projector.rb +10 -201
  486. data/lib/hecks/projector/exporter.rb +42 -134
  487. data/lib/hecks/projector/ir_projector.rb +2 -9
  488. data/lib/hecks/projector/narrate_projector.rb +15 -189
  489. data/lib/hecks/projector/target.rb +13 -76
  490. data/lib/hecks/projector.rb +30 -121
  491. data/lib/hecks/quality_control/adapters/agent.adapter +7 -0
  492. data/lib/hecks/quality_control/adapters/agent.rb +58 -0
  493. data/lib/hecks/quality_control/adapters/angle_tools.adapter +7 -0
  494. data/lib/hecks/quality_control/adapters/angle_tools.rb +15 -0
  495. data/lib/hecks/quality_control/adapters/clearance_recorder.rb +36 -0
  496. data/lib/hecks/quality_control/adapters/clearance_tools.adapter +7 -0
  497. data/lib/hecks/quality_control/adapters/clearance_tools.rb +17 -0
  498. data/lib/hecks/quality_control/adapters/git_pr.adapter +7 -0
  499. data/lib/hecks/quality_control/adapters/git_pr.rb +226 -0
  500. data/lib/hecks/quality_control/adapters/github_checks.adapter +9 -0
  501. data/lib/hecks/quality_control/adapters/github_checks.rb +83 -0
  502. data/lib/hecks/quality_control/adapters/github_ci_webhook.rb +56 -0
  503. data/lib/hecks/quality_control/adapters/github_issues.adapter +7 -0
  504. data/lib/hecks/quality_control/adapters/github_issues.rb +49 -0
  505. data/lib/hecks/quality_control/adapters/qa_tool.rb +59 -0
  506. data/lib/hecks/quality_control/adapters/sweep_tools.adapter +7 -0
  507. data/lib/hecks/quality_control/adapters/sweep_tools.rb +51 -0
  508. data/lib/hecks/quality_control/adapters/target_tools.adapter +7 -0
  509. data/lib/hecks/quality_control/adapters/target_tools.rb +43 -0
  510. data/lib/hecks/quality_control/cli/child.rb +45 -0
  511. data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +45 -0
  512. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +248 -0
  513. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +163 -0
  514. data/lib/hecks/quality_control/cli/qa_generated_domains.rb +450 -0
  515. data/lib/hecks/quality_control/cli/qa_log_bug.rb +195 -0
  516. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +220 -0
  517. data/lib/hecks/quality_control/cli/qa_open_pr.rb +305 -0
  518. data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +154 -0
  519. data/lib/hecks/quality_control/cli/qa_postgres_role.rb +159 -0
  520. data/lib/hecks/quality_control/cli/qa_pr_check.rb +232 -0
  521. data/lib/hecks/quality_control/cli/qa_seed_angles.rb +142 -0
  522. data/lib/hecks/quality_control/cli/qa_seed_targets.rb +69 -0
  523. data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +280 -0
  524. data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +329 -0
  525. data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +92 -0
  526. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +117 -0
  527. data/lib/hecks/quality_control/cli/qa_sweep/parsing.rb +175 -0
  528. data/lib/hecks/quality_control/cli/qa_sweep/release_mode.rb +60 -0
  529. data/lib/hecks/quality_control/cli/qa_sweep.rb +611 -0
  530. data/lib/hecks/quality_control/cli/qa_tick.rb +274 -0
  531. data/lib/hecks/quality_control/quality_control.bluebook +2224 -0
  532. data/lib/hecks/quality_control/quality_control.hecksagon +13 -0
  533. data/lib/hecks/quality_control/quality_control.ports.hecksagon +54 -0
  534. data/lib/hecks/query_ir.rb +346 -0
  535. data/lib/hecks/query_ir_mcp.rb +198 -0
  536. data/lib/hecks/query_specification/common/comparators.rb +5 -26
  537. data/lib/hecks/query_specification/common/comparison.rb +35 -143
  538. data/lib/hecks/query_specification/common/dsl.rb +8 -24
  539. data/lib/hecks/query_specification/common/null_policy.rb +20 -70
  540. data/lib/hecks/query_specification/common/options.rb +2 -6
  541. data/lib/hecks/query_specification/common/where_clause.rb +2 -10
  542. data/lib/hecks/query_specification/field_path.rb +37 -77
  543. data/lib/hecks/query_specification/hop_path.rb +12 -76
  544. data/lib/hecks/release/gem_pin.rb +2 -26
  545. data/lib/hecks/release/runner/ci_publisher.rb +5 -8
  546. data/lib/hecks/release/runner/clean_tree.rb +42 -0
  547. data/lib/hecks/release/runner/commands.rb +2 -4
  548. data/lib/hecks/release/runner/console.rb +6 -0
  549. data/lib/hecks/release/runner/gem_publisher.rb +15 -9
  550. data/lib/hecks/release/runner/npm_publisher.rb +9 -53
  551. data/lib/hecks/release/runner/preflight.rb +29 -20
  552. data/lib/hecks/release/runner/published.rb +8 -26
  553. data/lib/hecks/release/runner/tagger.rb +31 -23
  554. data/lib/hecks/release/runner.rb +60 -38
  555. data/lib/hecks/rendering.rb +4 -26
  556. data/lib/hecks/router/namespace_installer.rb +15 -39
  557. data/lib/hecks/router.rb +4 -8
  558. data/lib/hecks/runtime/adapter_lookup.rb +124 -0
  559. data/lib/hecks/runtime/aggregate_lock.rb +8 -33
  560. data/lib/hecks/runtime/boot_gates.rb +3 -18
  561. data/lib/hecks/runtime/caller.rb +19 -59
  562. data/lib/hecks/runtime/capability_graph.rb +5 -21
  563. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +16 -78
  564. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +27 -160
  565. data/lib/hecks/runtime/command_interpreter.rb +124 -306
  566. data/lib/hecks/runtime/command_rules/admissibility.rb +95 -366
  567. data/lib/hecks/runtime/command_rules/arithmetic.rb +66 -196
  568. data/lib/hecks/runtime/command_rules/authorization.rb +13 -45
  569. data/lib/hecks/runtime/command_rules/emission.rb +8 -21
  570. data/lib/hecks/runtime/command_rules/references.rb +23 -202
  571. data/lib/hecks/runtime/dependency_planning.rb +50 -104
  572. data/lib/hecks/runtime/dispatcher.rb +134 -243
  573. data/lib/hecks/runtime/entity_element.rb +40 -462
  574. data/lib/hecks/runtime/entity_interpreter.rb +64 -219
  575. data/lib/hecks/runtime/errors.rb +32 -115
  576. data/lib/hecks/runtime/event.rb +3 -25
  577. data/lib/hecks/runtime/identity.rb +18 -154
  578. data/lib/hecks/runtime/instance.rb +18 -150
  579. data/lib/hecks/runtime/interpreting.rb +14 -70
  580. data/lib/hecks/runtime/invocation.rb +28 -164
  581. data/lib/hecks/runtime/loader.rb +109 -194
  582. data/lib/hecks/runtime/outbox.rb +39 -262
  583. data/lib/hecks/runtime/policy_interpreter.rb +43 -202
  584. data/lib/hecks/runtime/port_operation_interpreter.rb +28 -97
  585. data/lib/hecks/runtime/query_interpreter.rb +122 -187
  586. data/lib/hecks/runtime/reaction_invocation.rb +36 -138
  587. data/lib/hecks/runtime/reaction_outcome.rb +89 -0
  588. data/lib/hecks/runtime/read_model_interpreter.rb +117 -208
  589. data/lib/hecks/runtime/rebuild_sweep.rb +8 -54
  590. data/lib/hecks/runtime/reference_hop.rb +8 -81
  591. data/lib/hecks/runtime/refusal_wording.rb +28 -87
  592. data/lib/hecks/runtime/registry/saga_persistence.rb +10 -102
  593. data/lib/hecks/runtime/registry/verification.rb +213 -227
  594. data/lib/hecks/runtime/registry/world_defaults.rb +4 -53
  595. data/lib/hecks/runtime/registry.rb +156 -247
  596. data/lib/hecks/runtime/remote_dispatcher.rb +68 -154
  597. data/lib/hecks/runtime/routing.rb +4 -15
  598. data/lib/hecks/runtime/saga_interpreter/correlation.rb +14 -61
  599. data/lib/hecks/runtime/saga_interpreter.rb +79 -263
  600. data/lib/hecks/runtime/saga_pending_dispatch.rb +2 -43
  601. data/lib/hecks/runtime/tenant_check.rb +6 -68
  602. data/lib/hecks/runtime/tenant_scope.rb +3 -28
  603. data/lib/hecks/runtime/value/admission.rb +16 -67
  604. data/lib/hecks/runtime/value/coercion.rb +162 -760
  605. data/lib/hecks/runtime/value/entity_list_coercion.rb +26 -219
  606. data/lib/hecks/runtime/value/field_checks.rb +172 -0
  607. data/lib/hecks/runtime/value.rb +15 -84
  608. data/lib/hecks/runtime.rb +35 -69
  609. data/lib/hecks/rust_build/conformance.rb +158 -0
  610. data/lib/hecks/rust_build/conformance_fuzz.rb +71 -0
  611. data/lib/hecks/rust_build/coverage.rb +218 -0
  612. data/lib/hecks/rust_build/native_build.rb +124 -0
  613. data/lib/hecks/rust_build/project_rust/root_mod.rb +120 -0
  614. data/lib/hecks/rust_build/project_rust.rb +332 -0
  615. data/lib/hecks/rust_build/wasm.rb +105 -0
  616. data/lib/hecks/rust_build/wasm_browser.rb +113 -0
  617. data/lib/hecks/rust_build.rb +144 -0
  618. data/lib/hecks/storehouse.rb +155 -582
  619. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +10 -34
  620. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +6 -23
  621. data/lib/hecks/three_zero/forms.yml +127 -0
  622. data/lib/hecks/three_zero.rb +12 -0
  623. data/lib/hecks/tools/argument_gate_matrix.rb +316 -0
  624. data/lib/hecks/tools/comment_style.rb +1070 -0
  625. data/lib/hecks/tools/corpus_report.rb +87 -0
  626. data/lib/hecks/tools/deploy_recipe.rb +255 -0
  627. data/lib/hecks/tools/deploy_recipe_lint.rb +282 -0
  628. data/lib/hecks/tools/drop_implicit_append_fields.rb +95 -0
  629. data/lib/hecks/tools/era_projection_backfill.rb +94 -0
  630. data/lib/hecks/tools/era_reattest.rb +146 -0
  631. data/lib/hecks/tools/evolve_run.rb +189 -0
  632. data/lib/hecks/tools/fuzz_sweep.rb +373 -0
  633. data/lib/hecks/tools/heki_compaction.rb +100 -0
  634. data/lib/hecks/tools/hoist_local_givens.rb +246 -0
  635. data/lib/hecks/tools/journal_compaction.rb +89 -0
  636. data/lib/hecks/tools/oidc_manifests.rb +70 -0
  637. data/lib/hecks/tools/query_ir_run.rb +81 -0
  638. data/lib/hecks/tools/regeneration_run.rb +166 -0
  639. data/lib/hecks/tools/rust_comment_style.rb +642 -0
  640. data/lib/hecks/tools/sequence_script.rb +62 -0
  641. data/lib/hecks/tools/tail_merge.rb +117 -0
  642. data/lib/hecks/tools/tenant_provisioning.rb +142 -0
  643. data/lib/hecks/tools/translation_audit.rb +208 -0
  644. data/lib/hecks/tools/translation_scaffold.rb +131 -0
  645. data/lib/hecks/tools.rb +86 -0
  646. data/lib/hecks/vendoring/git_environment.rb +2 -8
  647. data/lib/hecks/vendoring/git_source.rb +4 -10
  648. data/lib/hecks/vendoring.rb +7 -46
  649. data/lib/hecks/version.rb +3 -14
  650. data/lib/hecks.rb +52 -90
  651. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -46
  652. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +11 -80
  653. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +10 -99
  654. data/qa/settings.yml +66 -0
  655. data/rust/.cargo/config.toml +6 -0
  656. data/rust/.gitignore +2 -0
  657. data/rust/Cargo.lock +54 -0
  658. data/rust/Cargo.toml +61 -0
  659. data/rust/build/.cargo/config.toml +3 -0
  660. data/rust/build/.gitignore +1 -0
  661. data/rust/build/Cargo.lock +7 -0
  662. data/rust/build/Cargo.toml +19 -0
  663. data/rust/build/src/build_artifact.rs +47 -0
  664. data/rust/build/src/cargo_sync.rs +248 -0
  665. data/rust/build/src/json.rs +421 -0
  666. data/rust/build/src/lineage_pass.rs +285 -0
  667. data/rust/build/src/main.rs +51 -0
  668. data/rust/build/src/optional_pass.rs +150 -0
  669. data/rust/build/src/pipeline.rs +264 -0
  670. data/rust/build/src/reserved_names.rs +76 -0
  671. data/rust/build/src/resolve.rs +124 -0
  672. data/rust/build/src/root.rs +25 -0
  673. data/rust/build/src/sidecars.rs +73 -0
  674. data/rust/build/src/subprocess.rs +51 -0
  675. data/rust/build/src/tmp.rs +31 -0
  676. data/rust/codegen/.cargo/config.toml +3 -0
  677. data/rust/codegen/.gitignore +1 -0
  678. data/rust/codegen/Cargo.lock +7 -0
  679. data/rust/codegen/Cargo.toml +20 -0
  680. data/rust/codegen/src/attr.rs +32 -0
  681. data/rust/codegen/src/bridging.rs +481 -0
  682. data/rust/codegen/src/commands.rs +1226 -0
  683. data/rust/codegen/src/constraints.rs +109 -0
  684. data/rust/codegen/src/dependency_planning.rs +209 -0
  685. data/rust/codegen/src/domain_generator.rs +1288 -0
  686. data/rust/codegen/src/exemplar.rs +230 -0
  687. data/rust/codegen/src/expr_emitter.rs +87 -0
  688. data/rust/codegen/src/fielded.rs +192 -0
  689. data/rust/codegen/src/hecks_naming.rs +33 -0
  690. data/rust/codegen/src/json.rs +335 -0
  691. data/rust/codegen/src/json_codec.rs +697 -0
  692. data/rust/codegen/src/literal.rs +207 -0
  693. data/rust/codegen/src/main.rs +290 -0
  694. data/rust/codegen/src/manifest.rs +227 -0
  695. data/rust/codegen/src/mutations.rs +784 -0
  696. data/rust/codegen/src/naming.rs +286 -0
  697. data/rust/codegen/src/ports.rs +208 -0
  698. data/rust/codegen/src/prelude.rs +116 -0
  699. data/rust/codegen/src/queries.rs +786 -0
  700. data/rust/codegen/src/reactions.rs +532 -0
  701. data/rust/codegen/src/read_models.rs +697 -0
  702. data/rust/codegen/src/reference_specs.rs +40 -0
  703. data/rust/codegen/src/registry.rs +966 -0
  704. data/rust/codegen/src/reserved_names.rs +76 -0
  705. data/rust/codegen/src/shared.rs +93 -0
  706. data/rust/codegen/src/skip_reason.rs +25 -0
  707. data/rust/codegen/src/types.rs +362 -0
  708. data/rust/host/.cargo/config.toml +3 -0
  709. data/rust/host/.gitignore +1 -0
  710. data/rust/host/Cargo.lock +4297 -0
  711. data/rust/host/Cargo.toml +61 -0
  712. data/rust/host/HECKS_RELEASE +1 -0
  713. data/rust/host/rds-ca-bundle.pem +2736 -0
  714. data/rust/host/src/api.rs +1596 -0
  715. data/rust/host/src/approval.rs +652 -0
  716. data/rust/host/src/auth.rs +1287 -0
  717. data/rust/host/src/bin/expr_harness.rs +46 -0
  718. data/rust/host/src/bin/lineage_harness.rs +119 -0
  719. data/rust/host/src/bin/mint_harness.rs +135 -0
  720. data/rust/host/src/checkout.rs +265 -0
  721. data/rust/host/src/dispatch.rs +1443 -0
  722. data/rust/host/src/expr_json.rs +1117 -0
  723. data/rust/host/src/field_hints.rs +52 -0
  724. data/rust/host/src/ir.rs +733 -0
  725. data/rust/host/src/journal.rs +874 -0
  726. data/rust/host/src/lambda_client.rs +378 -0
  727. data/rust/host/src/log.rs +147 -0
  728. data/rust/host/src/main.rs +474 -0
  729. data/rust/host/src/mint.rs +1865 -0
  730. data/rust/host/src/payments/keystore.rs +330 -0
  731. data/rust/host/src/payments/tests.rs +1400 -0
  732. data/rust/host/src/payments.rs +767 -0
  733. data/rust/host/src/presentation.rs +698 -0
  734. data/rust/host/src/presentation_write.rs +1431 -0
  735. data/rust/host/src/query_step.rs +149 -0
  736. data/rust/host/src/rate_limit.rs +904 -0
  737. data/rust/host/src/reference_transform.rs +241 -0
  738. data/rust/host/src/reference_validate.rs +338 -0
  739. data/rust/host/src/resend.rs +296 -0
  740. data/rust/host/src/secrets.rs +106 -0
  741. data/rust/host/src/server.rs +663 -0
  742. data/rust/host/src/storage_shape.rs +175 -0
  743. data/rust/host/src/test_pg.rs +40 -0
  744. data/rust/host/src/ui_schema.rs +1351 -0
  745. data/rust/host/src/wasm_runner.rs +191 -0
  746. data/rust/host/src/web/newsletter/cooldown.rs +126 -0
  747. data/rust/host/src/web/newsletter/tests.rs +104 -0
  748. data/rust/host/src/web/newsletter.rs +310 -0
  749. data/rust/host/src/web/newsletter_send.rs +357 -0
  750. data/rust/host/src/web/registrations.rs +1505 -0
  751. data/rust/host/src/web.rs +2937 -0
  752. data/rust/lsp/.gitignore +1 -0
  753. data/rust/lsp/Cargo.lock +7 -0
  754. data/rust/lsp/Cargo.toml +16 -0
  755. data/rust/lsp/README.md +133 -0
  756. data/rust/lsp/src/diagnostics.rs +165 -0
  757. data/rust/lsp/src/json.rs +385 -0
  758. data/rust/lsp/src/main.rs +399 -0
  759. data/rust/lsp/src/outline.rs +177 -0
  760. data/rust/lsp/src/rpc.rs +67 -0
  761. data/rust/parser/.cargo/config.toml +5 -0
  762. data/rust/parser/.gitignore +1 -0
  763. data/rust/parser/Cargo.lock +7 -0
  764. data/rust/parser/Cargo.toml +21 -0
  765. data/rust/parser/src/build/closed_sets.rs +27 -0
  766. data/rust/parser/src/build/identity.rs +276 -0
  767. data/rust/parser/src/build/mod.rs +12 -0
  768. data/rust/parser/src/build/naming.rs +141 -0
  769. data/rust/parser/src/build/pattern_subset.rs +161 -0
  770. data/rust/parser/src/build/query_derive.rs +72 -0
  771. data/rust/parser/src/build/query_inference.rs +269 -0
  772. data/rust/parser/src/build/query_options.rs +100 -0
  773. data/rust/parser/src/build/read_model.rs +94 -0
  774. data/rust/parser/src/build/references.rs +32 -0
  775. data/rust/parser/src/canonical.rs +114 -0
  776. data/rust/parser/src/diag.rs +55 -0
  777. data/rust/parser/src/emit.rs +953 -0
  778. data/rust/parser/src/expr/ast_json.rs +147 -0
  779. data/rust/parser/src/expr/evaluator.rs +81 -0
  780. data/rust/parser/src/expr/mod.rs +84 -0
  781. data/rust/parser/src/expr/resolver.rs +497 -0
  782. data/rust/parser/src/ir.rs +387 -0
  783. data/rust/parser/src/keywords.rs +585 -0
  784. data/rust/parser/src/lex.rs +662 -0
  785. data/rust/parser/src/main.rs +252 -0
  786. data/rust/parser/src/parse/aggregate.rs +612 -0
  787. data/rust/parser/src/parse/chapter.rs +517 -0
  788. data/rust/parser/src/parse/command.rs +637 -0
  789. data/rust/parser/src/parse/domain_port.rs +127 -0
  790. data/rust/parser/src/parse/entity.rs +527 -0
  791. data/rust/parser/src/parse/file.rs +67 -0
  792. data/rust/parser/src/parse/hecksagon.rs +231 -0
  793. data/rust/parser/src/parse/lifecycle.rs +162 -0
  794. data/rust/parser/src/parse/mod.rs +1262 -0
  795. data/rust/parser/src/parse/policy.rs +87 -0
  796. data/rust/parser/src/parse/process_manager.rs +305 -0
  797. data/rust/parser/src/parse/query.rs +100 -0
  798. data/rust/parser/src/parse/read_model.rs +214 -0
  799. data/rust/parser/src/parse/value_object.rs +250 -0
  800. data/rust/parser/src/ruby_value.rs +422 -0
  801. data/rust/project/bridging.rb +257 -0
  802. data/rust/project/commands.rb +877 -0
  803. data/rust/project/constraints.rb +86 -0
  804. data/rust/project/dependency_planning.rb +178 -0
  805. data/rust/project/domain_generator.rb +980 -0
  806. data/rust/project/exemplar.rb +148 -0
  807. data/rust/project/expr_emitter.rb +72 -0
  808. data/rust/project/fielded.rb +141 -0
  809. data/rust/project/json_codec.rb +641 -0
  810. data/rust/project/mutations.rb +721 -0
  811. data/rust/project/naming.rb +205 -0
  812. data/rust/project/ports.rb +65 -0
  813. data/rust/project/queries.rb +493 -0
  814. data/rust/project/reactions.rb +317 -0
  815. data/rust/project/read_models.rb +690 -0
  816. data/rust/project/reference_specs.rb +27 -0
  817. data/rust/project/registry.rb +396 -0
  818. data/rust/project/skip_reason.rb +29 -0
  819. data/rust/project/types.rb +225 -0
  820. data/rust/project/write_if_changed.rb +83 -0
  821. data/rust/project.rb +30 -0
  822. data/rust/project_rust_pipeline.rb +383 -0
  823. data/rust/src/exemplar/commands.rs +279 -0
  824. data/rust/src/exemplar/constraints.rs +18 -0
  825. data/rust/src/exemplar/fielded.rs +280 -0
  826. data/rust/src/exemplar/json.rs +277 -0
  827. data/rust/src/exemplar/mod.rs +17 -0
  828. data/rust/src/exemplar/mutations.rs +80 -0
  829. data/rust/src/exemplar/queries.rs +33 -0
  830. data/rust/src/exemplar/reactions.rs +88 -0
  831. data/rust/src/exemplar/read_models.rs +48 -0
  832. data/rust/src/exemplar/registry.rs +166 -0
  833. data/rust/src/exemplar/types.rs +44 -0
  834. data/rust/src/kernel/attribute_shapes/composite.rs +88 -0
  835. data/rust/src/kernel/attribute_shapes/list.rs +18 -0
  836. data/rust/src/kernel/attribute_shapes/mod.rs +44 -0
  837. data/rust/src/kernel/attribute_shapes/optional.rs +9 -0
  838. data/rust/src/kernel/attribute_shapes/scalar.rs +39 -0
  839. data/rust/src/kernel/cli.rs +585 -0
  840. data/rust/src/kernel/dispatch.rs +1090 -0
  841. data/rust/src/kernel/expr.rs +458 -0
  842. data/rust/src/kernel/expression_operators/arithmetic.rs +128 -0
  843. data/rust/src/kernel/expression_operators/comparison.rs +48 -0
  844. data/rust/src/kernel/expression_operators/enumeration.rs +232 -0
  845. data/rust/src/kernel/expression_operators/logical.rs +13 -0
  846. data/rust/src/kernel/expression_operators/membership.rs +18 -0
  847. data/rust/src/kernel/expression_operators/mod.rs +68 -0
  848. data/rust/src/kernel/expression_operators/pattern_match.rs +73 -0
  849. data/rust/src/kernel/expression_operators/positional.rs +126 -0
  850. data/rust/src/kernel/expression_operators/presence.rs +101 -0
  851. data/rust/src/kernel/expression_operators/sign_test.rs +16 -0
  852. data/rust/src/kernel/expression_operators/sized.rs +35 -0
  853. data/rust/src/kernel/expression_operators/text.rs +79 -0
  854. data/rust/src/kernel/expression_operators/to_string.rs +18 -0
  855. data/rust/src/kernel/json.rs +779 -0
  856. data/rust/src/kernel/mod.rs +133 -0
  857. data/rust/src/kernel/named_query.rs +219 -0
  858. data/rust/src/kernel/orchestrate.rs +1810 -0
  859. data/rust/src/kernel/pattern.rs +307 -0
  860. data/rust/src/kernel/query_comparators.rs +206 -0
  861. data/rust/src/kernel/query_ordering.rs +103 -0
  862. data/rust/src/kernel/read_model.rs +914 -0
  863. data/rust/src/kernel/reference_lookup.rs +149 -0
  864. data/rust/src/kernel/repository.rs +468 -0
  865. data/rust/src/kernel/routing.rs +612 -0
  866. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +118 -0
  867. data/rust/src/kernel/vocab/entity_dispatch_order.rs +114 -0
  868. data/rust/src/kernel/vocab/field_hint.rs +70 -0
  869. data/rust/src/kernel/vocab/mod.rs +20 -0
  870. data/rust/src/kernel/vocab/query_comparator.rs +65 -0
  871. data/rust/src/kernel/vocab/refusal_template.rs +1592 -0
  872. data/rust/src/lib.rs +8 -0
  873. data/rust/src/main.rs +16 -0
  874. data/rust/web/.gitignore +1 -0
  875. data/rust/web/Cargo.lock +119 -0
  876. data/rust/web/Cargo.toml +18 -0
  877. data/rust/web/src/lib.rs +8 -0
  878. metadata +478 -12
  879. data/lib/hecks/facade/cli_door.rb +0 -178
  880. data/lib/hecks/facade/cli_runner.rb +0 -271
  881. data/lib/hecks/facade/handle.rb +0 -293
  882. data/lib/hecks/facade/json_door.rb +0 -247
  883. data/lib/hecks/facade/surface/aggregate_door.rb +0 -222
  884. data/lib/hecks/facade/surface/chapter.rb +0 -116
  885. data/lib/hecks/facade/surface.rb +0 -61
  886. data/lib/hecks/mcp_door_scope.rb +0 -176
@@ -3,8 +3,10 @@ require "json"
3
3
  require_relative "../../../../adapters/driven/sql_query_builder"
4
4
  require_relative "../../../../adapters/driven/postgres/outbox"
5
5
  require_relative "../../../../adapters/driven/postgres/reconnect"
6
+ require_relative "../../../../adapters/driven/postgres/shared_connection"
6
7
  require_relative "postgres_era/lineage"
7
8
  require_relative "postgres_era/lineage_manager"
9
+ require_relative "postgres_era/events"
8
10
  require_relative "../../../../ports/persistence/append_only"
9
11
  require_relative "../../../../query_specification/common/order_by"
10
12
  require_relative "../../../../runtime/errors"
@@ -14,100 +16,45 @@ require_relative "../../../../runtime/registry"
14
16
 
15
17
  module Hecks
16
18
  module Adapters
17
- # The enforcement-grade persistence adapter — and the only one that
18
- # declares the lineage capability: it may act on shape drift
19
- # (translate, fork, merge) where every other adapter can only refuse
20
- # toward it. Sibling to the plain `Postgres` adapter (postgres.rb),
21
- # which is the same database with none of this machinery — pick
22
- # PostgresEra only once a domain actually needs to survive a shape
23
- # change live. See docs/implemented/postgres-era-adapter-split-plan.md for why
24
- # the two are split and what each one carries.
25
- #
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
31
- # sequence spanning partitions. Appends go there; nothing updates
32
- # or deletes a journal row (immutability by privilege — UPDATE and
33
- # DELETE revoked; a deployment's app role connects as a non-owner).
34
- # - Per aggregate, the head is derived: era 1 reads a plain view
35
- # (latest save per id); later eras read a view overlaying the
36
- # materialized, translated ancestor tail with live current-era
37
- # rows. `project` is therefore a no-op — old entries are never
38
- # rewritten, and the head is never a table anything writes.
39
- # - State is one jsonb column. jsonb normalizes key order (and drops
40
- # duplicate keys), so anything comparing stored state — the corpus
41
- # history gate above all — must compare canonicalized state, never
42
- # raw bytes; `bin/canonicalise` deep-sorts keys, which is exactly
43
- # why the gate survives this normalization.
44
- # - Query pushdown is the shared SqlQueryBuilder: every declared
45
- # operator compiles fully into SQL, or the query refuses loudly.
19
+ # Postgres adapter that journals each write per domain and derives every head by era; the
20
+ # only one that can translate, fork or merge on shape drift. Compare jsonb state canonicalized.
46
21
  class PostgresEra
47
22
  include SqlQueryBuilder
48
23
  include Adapters::PostgresOutbox
49
24
  include Adapters::PostgresReconnect
25
+ include Events
50
26
 
51
27
  attr_reader :aggregate
52
28
 
53
29
  # Names the optional persistence capabilities this adapter implements natively.
54
30
  #
55
- # `:cross_process_lock` tells `Interpreting#run_dispatch_order_with_isolation`
56
- # (runtime/interpreting.rb) this repository can hold a real
57
- # cross-process lock for the whole dispatch order itself, via
58
- # `with_write_lock` below — so it should use that instead of the
59
- # in-process `AggregateLock` `Mutex` every other non-CAS repository
60
- # falls back to. See ADR 0036: that in-process Mutex is invisible
61
- # to `rust/host` dispatching against the same PostgresEra-bound
62
- # tables from a separate OS process.
31
+ # `:cross_process_lock` lets dispatch hold `with_write_lock` instead of the in-process
32
+ # mutex, which `rust/host` in another OS process cannot see (ADR 0036). `:atomic_append`
33
+ # tells `RepositoryFactory.build` that `append` (below) already commits the snapshot in
34
+ # the same transaction as the journal row, so replaying the journal through `project` on
35
+ # boot has nothing left to fix up.
63
36
  #
64
- # @return [Array<Symbol>] always `[:atomic_put, :cross_process_lock]`
65
- def persistence_capabilities = %i[atomic_put cross_process_lock]
37
+ # @return [Array<Symbol>] always `[:atomic_put, :cross_process_lock, :atomic_append]`
38
+ def persistence_capabilities = %i[atomic_put cross_process_lock atomic_append]
66
39
 
67
40
  # Declares that this adapter can act on shape drift rather than only refuse.
68
41
  #
69
- # The capability idiom: only PostgresEra answers true, and only
70
- # PostgresEra carries an era_check! for the boot gate to delegate to.
71
- #
72
42
  # @return [Boolean] always true
73
43
  def self.lineage_capable? = true
74
44
 
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
78
- # full reasoning. `connect_for`'s own `schema:` setting (the
79
- # Storehouse shared-instance mechanism, already built, already
80
- # proven for eras) is what keeps two tenant boots' tables apart:
81
- # each boot's own `SET search_path` means every unqualified
82
- # reference this adapter and its lineage classes construct
83
- # resolves into that boot's own schema, never another tenant's —
84
- # proven for real, not assumed, by tenant_isolation_spec.rb, the
85
- # same discipline lineage_capable? already holds itself to.
45
+ # Declares that two tenant boots on separate `schema:` settings keep their tables apart.
86
46
  #
87
47
  # @return [Boolean] always true
88
48
  def self.tenant_capable? = true
89
49
 
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`).
50
+ # Resolves this boot's era, minting the next one when the shape drifted and a translation
51
+ # edge covers it. Delegates to `LineageManager.check!`.
93
52
  #
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
53
+ # @param settings [Hash{Symbol, String => Object}] persistence settings; needs `database`
54
+ # @param directory [String, nil] where `HECKS_SCAFFOLD=1` writes a translation edge
55
+ # @return [Integer, nil] the ordinal of the era this boot minted, or nil when none
56
+ # @raise [Runtime::WiringError] if the database is unreachable or the mint refuses
57
+ # @raise [Bluebook::DSL::Malformed] if a held era's text parses under no grammar
111
58
  def self.era_check!(registry:, bluebook:, current_text:, settings:, directory: nil)
112
59
  LineageManager.check!(
113
60
  registry: registry, bluebook: bluebook, current_text: current_text,
@@ -115,23 +62,11 @@ module Hecks
115
62
  )
116
63
  end
117
64
 
118
- # Reads one setting from a world's settings hash under either key spelling.
65
+ # Reads one setting under either its Symbol or String spelling.
119
66
  #
120
- # Both spellings of a setting are honored — a world's settings hash
121
- # may arrive symbol-keyed (built straight in Ruby) or string-keyed
122
- # (round-tripped through JSON), and a domain that names one is not
123
- # obligated to also skip the other. `key?` decides which spelling
124
- # actually exists, never `||` — `||` cannot tell a genuinely stored
125
- # `false` apart from an absent key, and would silently prefer the
126
- # other spelling (or `default`) instead of returning the real, held
127
- # answer. See Hecks::QuerySpecification::FieldPath#read for the same
128
- # discipline applied to stored state instead of settings.
67
+ # Uses `key?` rather than `||` so a stored `false` is not mistaken for an absent key.
129
68
  #
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
69
+ # @return [Object, nil] the stored value, or `default` when neither spelling is present
135
70
  def self.setting(settings, key, default: nil)
136
71
  return settings[key] if settings.key?(key)
137
72
 
@@ -141,20 +76,15 @@ module Hecks
141
76
  default
142
77
  end
143
78
 
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.
79
+ # Opens a connection to the declared database, selecting the declared `schema` if any.
80
+ # The caller owns the connection and closes it.
147
81
  #
148
82
  # @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`
83
+ # @param settings [Hash{Symbol, String => Object}] `database` is a name or `postgres://` URL
84
+ # @return [PG::Connection] an open connection
85
+ # @raise [Runtime::WiringError] if `database` is missing or Postgres refuses a statement
155
86
  def self.connect_for(name, settings)
156
- # **Lazy, on purpose** — same reasoning as Sqlite's own initialize:
157
- # a domain that never wires PostgresEra should never need the gem.
87
+ # Lazy so a domain that never wires PostgresEra does not need the pg gem.
158
88
  require "pg"
159
89
 
160
90
  declared = setting(settings, :database)
@@ -171,39 +101,17 @@ module Hecks
171
101
  PG.connect(dbname: declared)
172
102
  end
173
103
 
174
- # **Shared-instance isolation**. A domain that declares `schema` is
175
- # sharing its Postgres instance with other domains (the
176
- # storehouse) — every unqualified table/view/function reference
177
- # this adapter and its lineage classes ever construct resolves
178
- # through search_path, so this one SET is what makes ALTER
179
- # TABLE ... SET SCHEMA migrations transparent to the rest of the
180
- # adapter. A domain with no `schema` setting keeps Postgres's
181
- # own default search_path (public).
104
+ # A declared `schema` means the instance is shared; search_path makes every unqualified
105
+ # name this adapter and its lineage classes build resolve inside that schema.
182
106
  schema = setting(settings, :schema)
183
107
  if schema.to_s != ""
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
187
- # selected to create in", found live provisioning tenant_
188
- # isolation_spec.rb's own multi-schema fixture by hand.
189
- # `CREATE SCHEMA IF NOT EXISTS` is exactly the
190
- # same self-healing idempotency this adapter's own table/era
191
- # provisioning already holds itself to (see the comment right
192
- # below on `client_min_messages`) — a schema that already
193
- # exists is the ordinary case for every boot after the first,
194
- # not news.
108
+ # Idempotent: the schema may not exist yet on the first boot.
195
109
  connection.exec("CREATE SCHEMA IF NOT EXISTS #{connection.quote_ident(schema)}")
196
110
  connection.exec("SET search_path TO #{connection.quote_ident(schema)}")
197
111
  end
198
112
 
199
- # **Quiet on purpose**. Provisioning re-runs its own idempotent
200
- # `CREATE ... IF NOT EXISTS` checks on every boot — a schema that
201
- # already exists is the ordinary case, not news, and Postgres
202
- # surfaces every one as a notice by default. `bin/set-password`
203
- # boots a real registry just to mint an Identity, and nobody
204
- # setting a password needs to see a page of "relation ...
205
- # already exists, skipping" to do it. Warning and above (real
206
- # problems) still surface.
113
+ # Provisioning re-runs CREATE ... IF NOT EXISTS on every boot; silence the notices.
114
+ # Warnings and above still surface.
207
115
  connection.exec("SET client_min_messages = warning")
208
116
  connection
209
117
  rescue PG::Error => e
@@ -211,118 +119,53 @@ module Hecks
211
119
  "cannot bind PostgresEra at #{declared} for #{name}: #{e.message.strip}"
212
120
  end
213
121
 
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.
122
+ # Joins the process's shared connection for the declared database and schema, so every
123
+ # aggregate of a domain runs on one connection, then provisions the journal, this
124
+ # aggregate's head and field caches, and the event, saga and outbox tables. Every step is
125
+ # idempotent.
216
126
  #
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
127
+ # @param settings [Hash{Symbol, String => Object}] world settings plus what
128
+ # `RepositoryFactory.build` merges in: `domain`, `era` and `superseded_by`
129
+ # @raise [Runtime::WiringError] if `database` is missing or the connection is refused
225
130
  def initialize(aggregate:, settings: {}, root: nil)
226
131
  @aggregate = aggregate
227
132
  @settings = settings
228
- @db = self.class.connect_for(aggregate.name, settings)
229
- # The domain names the journal (one journal per lineage). The
230
- # factory injects it as the owning bluebook's own declared name
231
- # (`RepositoryFactory.build`'s own `domain` positional arg —
232
- # `bluebook.name`, confirmed at `outbox.rb`'s own call site —
233
- # never this default in real dispatch). This default exists only
234
- # for a directly-instantiated adapter (specs, consoles) that
235
- # skips the factory.
236
- #
237
- # When the aggregate does have an owning chapter (`hecks_owner`
238
- # set — true for any aggregate sealed through a real bluebook,
239
- # even if this adapter itself was built by hand), default to the
240
- # chapter's own declared PascalCase name. That's the same string
241
- # `rust/host`'s `pg_advisory_xact_lock` key derives from
242
- # (`HECKS_DOMAIN`, deploy-time-set to `world.domain`) — the
243
- # aggregate's own snake_case `storage_name` silently disagreed
244
- # with it (`hashtext` is byte-sensitive: `"chess"` and `"Chess"`
245
- # never collide), which is ADR 0036 Blocker 1.
246
- #
247
- # When there's no owning chapter at all (a bare
248
- # `AggregateBuilder` fixture built outside any real bluebook —
249
- # `hecks_owner` is only ever stamped by chapter construction, see
250
- # `traits.rb`'s `hecks_owner = self`) there is no chapter name to
251
- # match Rust against in the first place, so fall back to the
252
- # aggregate's own name.
133
+ @db = PostgresSharedConnection.for(aggregate.name, settings, connector: self.class)
134
+ # Journal name: the owning bluebook's declared name, matching the key rust/host derives its
135
+ # advisory lock from (ADR 0036). A bare aggregate with no owner falls back to its own name.
253
136
  @domain = self.class.setting(
254
137
  settings, :domain, default: aggregate.hecks_owner&.name || aggregate.storage_name
255
138
  ).to_s
256
139
  @lineage = Lineage.new(@db, @domain)
257
140
  @lineage.ensure_base!
258
- # The era gate resolves which era this boot is (an old checkout
259
- # boots a held-but-superseded era and keeps writing its own
260
- # partition); a directly-instantiated adapter defaults to the
261
- # newest.
262
- #
263
- # Not `self.class.setting(...)` here — RepositoryFactory#build
264
- # always merges `era: registry.resolved_eras[domain]` into
265
- # settings, so the key is genuinely present (not absent) for
266
- # any domain the era boot gate hasn't resolved yet (or that
267
- # doesn't have one at all), just holding `nil`. `setting`'s own
268
- # presence-over-truthiness discipline (correct for a field like
269
- # `:role`, where a stored `false` is a real, distinct answer
270
- # from "unset") does the wrong thing for `:era` specifically:
271
- # `nil` can never be a meaningful era override — only a real
272
- # ordinal or "not resolved, self-resolve" ever apply — so
273
- # coalescing here, not `setting`'s presence check, is what
274
- # actually honors this comment's own "defaults to the newest"
275
- # promise.
141
+ # Coalesce instead of `setting`: the factory always passes `era:`, nil until the boot gate
142
+ # resolves it, and nil can never be a real override.
276
143
  @era = settings.key?(:era) ? settings[:era] : settings["era"]
277
144
  @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.
145
+ # Non-nil only for a held-but-superseded boot. `append` and `atomic_put` refuse on it
146
+ # because a superuser walks through the RLS fence.
291
147
  @superseded_by = settings.key?(:superseded_by) ? settings[:superseded_by] : settings["superseded_by"]
292
- # Unconditional and idempotent, regardless of era — belt-and-
293
- # suspenders self-healing (compile_head! already ensures this for
294
- # a freshly-minted era's own name; ensure_first_head! for era 1's)
295
- # against any boot-ordering surprise, at the cost of one
296
- # CREATE TABLE IF NOT EXISTS nobody pays for twice.
148
+ # Idempotent self-healing against any boot-ordering surprise.
297
149
  @lineage.ensure_head_snapshot!(table, @era)
298
150
  @lineage.ensure_first_head!(table) if @era == 1
299
- # The read-cache side of the era workaround (Track C,
300
- # docs/implemented/postgres-era-adapter-split-plan.md §3) — one row-cache
301
- # table per `where`-field this aggregate's own declared queries
302
- # (and its entities' own) actually use, derived automatically
303
- # (principle 3 — no bluebook keyword), self-healing and
304
- # idempotent like everything else booted here. `@field_caches`
305
- # maps field -> cache-table name; `query` below consults it to
306
- # decide whether a declared query can skip the DISTINCT ON
307
- # reduction entirely.
151
+ # One row-cache table per where-field the aggregate's queries use; `query` consults it
152
+ # to skip the head reduction.
308
153
  @field_caches = ensure_field_caches!
309
154
  create_event_table!
310
155
  create_saga_table!
311
156
  create_outbox_table!
312
157
  end
313
158
 
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.
159
+ # Names this aggregate in storage: the journal's `aggregate` value and the stem of its
160
+ # head view, head snapshot and field-cache names.
316
161
  #
317
162
  # @return [String] the aggregate's snake_case storage name
318
163
  def table = @aggregate.storage_name
319
164
 
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.
165
+ # Reads one record's current state through the head view, translated to the current shape.
322
166
  #
323
167
  # @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)
168
+ # @return [Runtime::Instance, nil] the record, or nil when never written or deleted
326
169
  def find(id)
327
170
  result = @db.exec_params(%(SELECT id, state FROM #{quoted_head} WHERE id = $1), [id.to_s])
328
171
  return nil if result.ntuples.zero?
@@ -332,21 +175,12 @@ module Hecks
332
175
 
333
176
  # Lists every current record, ordered by id unless the caller names a field.
334
177
  #
335
- # order_by is a runtime value, not framework-authored bluebook source
336
- # like every other caller of order_expression — a query param off an
337
- # HTTP request, in the console's case. Whitelisted against the
338
- # aggregate's own real attributes (plus its lifecycle field) before
339
- # it ever reaches order_expression, unlike a declared query's
340
- # order_by, which the language itself already only lets name a real
341
- # attribute at parse time. Without this, an unknown field wouldn't
342
- # error — query_expression degrades a nil attribute to a harmless
343
- # no-op path — it would just silently sort by nothing.
178
+ # `order_by` is a runtime value (an HTTP param), so it is checked against the aggregate's
179
+ # attributes; an unknown field would otherwise sort by nothing without erroring.
344
180
  #
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
181
+ # @param order_by [String, Symbol, nil] an attribute, the lifecycle field, or a dotted path
182
+ # @param direction [Symbol, String] `desc` sorts descending; nulls sort first ascending
183
+ # @return [Array<Runtime::Instance>] every saved record
350
184
  # @raise [Runtime::WiringError] if `order_by` names no attribute of this aggregate
351
185
  def all(order_by: nil, direction: :asc)
352
186
  return @db.exec(%(SELECT id, state FROM #{quoted_head} ORDER BY id)).map { |row| instance(row) } unless order_by
@@ -366,51 +200,16 @@ module Hecks
366
200
  # @return [Integer] how many saved, undeleted records the head holds
367
201
  def count = @db.exec(%(SELECT COUNT(*) FROM #{quoted_head}))[0]["count"].to_i
368
202
 
369
- # Runs a declared query, looking candidate ids up in the field caches first when
370
- # its `where` clauses allow it.
203
+ # Runs a declared query, looking candidate ids up in the field caches when every `where`
204
+ # clause allows it and only then reading those ids from the head view.
371
205
  #
372
- # The two-phase shortcut (Track C, docs/implemented/postgres-era-adapter-
373
- # split-plan.md §3). `SqlQueryBuilder#query` (`super`, unmodified per
374
- # principle 2) always runs correctly here — it filters against
375
- # `head_view`, which is already the fully-reduced current state —
376
- # but for a domain that has minted a second era, that reduction
377
- # itself is the expensive part, and no index on the jsonb path
378
- # changes that (see field_cache.rb's own header for the SQL-
379
- # semantics reason why). When every `where` clause this query
380
- # declares either targets a cached field or is eligible to (an
381
- # ordinary comparator, not a null-vs-value special case — see
382
- # `cache_eligible?`), skip the reduction: look candidate ids up in
383
- # the cache table(s) first (cheap, indexed, no reduction involved),
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
387
- # comparison) is simply re-checked against `head_view` in the
388
- # second phase, exactly as `super` would have checked it anyway —
389
- # this can only ever narrow what phase two has to look at, never
390
- # change what a clause means.
391
- #
392
- # Falls back to `super` whenever no clause can be accelerated — a
393
- # query with no `where` at all (order_by-only — no cache table
394
- # exists for these, see field_cache.rb), a query whose only clauses
395
- # target fields with no cache table, or a domain that has never
396
- # minted a second era at all (`@field_caches` is never empty just
397
- # because era 1 has no reduction to skip — the cache tables still
398
- # exist and still accelerate era 1 the same way, but the fallback
399
- # path is already just as cheap there since head_view is the
400
- # snapshot table verbatim for era 1; skipping straight to `super`
401
- # in that case would be a valid future optimization, not attempted
402
- # here to keep this one code path correct for every era uniformly).
206
+ # The cache only narrows candidates; the head view stays authoritative, so results match
207
+ # `super`. Uncached or null-comparison clauses are re-checked against the head view.
403
208
  #
404
209
  # @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
210
+ # @param args [Hash{Symbol => Object}] caller-supplied values for named arguments
211
+ # @return [Array<Runtime::Instance>] the matching records, by id when no order is declared
212
+ # @raise [ArgumentError] if a clause uses an operator this dialect cannot compile
414
213
  def query(declared, args = {}, context: {})
415
214
  return super if @field_caches.empty? || declared.wheres.empty?
416
215
 
@@ -424,59 +223,27 @@ module Hecks
424
223
  head_phase(declared, uncached, ids, args)
425
224
  end
426
225
 
427
- # Runs a block inside one transaction holding the domain's cross-process write lock.
226
+ # Runs a block in one transaction holding the domain's cross-process write lock (ADR 0036).
428
227
  #
429
- # ADR 0036's actual fix — see `persistence_capabilities` above.
430
- # Wraps the whole dispatch order (hydrate through save), not just
431
- # `append`'s own transaction below: `lock_writes!` has to be held
432
- # before hydrate even starts, or two cross-process writers can
433
- # both hydrate unlocked and race for the write, each blind to the
434
- # other. `transaction` (via `include Adapters::PostgresOutbox`) is
435
- # already re-entrant — `append`/`atomic_put`'s own inner
436
- # `transaction do ... end`, deep inside the block below, joins
437
- # this same transaction instead of opening/committing its own, so
438
- # the advisory lock stays held until this whole block returns.
228
+ # The lock must be taken before hydrate, so it wraps the whole dispatch order; the
229
+ # nested `append` joins this transaction and the lock is held until the block returns.
439
230
  #
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
231
+ # @yield the dispatch order; an exception rolls the transaction back
443
232
  # @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
233
+ # @raise [PG::ConnectionBad] if the connection dropped; it reconnects, then re-raises
448
234
  def with_write_lock(&block) = transaction { lock_writes!; block.call } # rubocop:disable Style/Semicolon
449
235
 
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.
236
+ # Appends one entry to the journal and updates the head snapshot and every field cache,
237
+ # all in one transaction under the domain's write lock.
452
238
  #
453
- # The lock is held for the whole transaction, not just around the INSERT — the
454
- # ordinal is assigned by the column's own `nextval()` default, inside
455
- # this same statement, so the lock has to already be held before that
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
459
- # for why only that half of the race is closed.
460
- # The journal insert and the snapshot upsert/delete happen in the
461
- # same transaction — real ACID atomicity, not the append-then-
462
- # project two-step a file-based adapter needs a crash-recovery
463
- # replay for (see Heki). If this transaction commits, the snapshot
464
- # is already exactly as current as the journal; if it doesn't,
465
- # neither happened. `project` stays uninvolved on purpose — it
466
- # still runs, cheaply, during AppendOnly#recover!'s full replay on
467
- # every boot (see `project` below), and a second write there would
468
- # make that replay pay real DB cost for a snapshot that's already
469
- # correct. When `with_write_lock` above already drove this whole
470
- # dispatch, `lock_writes!` here is a harmless re-acquire of the
471
- # same already-held (per-session-reentrant) advisory lock; a bare
472
- # `repository.save` outside a full dispatch still takes it fresh.
239
+ # The lock precedes the INSERT because the ordinal comes from `nextval()` inside it. It is
240
+ # a different key from `mint_era!`'s, so it serializes writes, never a write against a mint.
241
+ # `project` stays out of it: AppendOnly#recover! already replays it on every boot.
473
242
  #
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
243
+ # @param entry [Ports::Persistence::Entry] the save or delete to journal
476
244
  # @return [Ports::Persistence::Entry] the same entry, unchanged
477
245
  # @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
246
+ # @raise [PG::Error] if Postgres refuses a statement, such as the era fence's RLS
480
247
  def append(entry)
481
248
  refuse_superseded_write!
482
249
  transaction do
@@ -487,16 +254,10 @@ module Hecks
487
254
  end
488
255
 
489
256
  # 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.
257
+ # Runs before the transaction so a stale checkout takes no lock; reads stay allowed.
496
258
  #
497
259
  # @return [nil] when this boot's era is current
498
260
  # @raise [Runtime::WiringError] if the boot gate marked this era superseded
499
- # (`superseded_by` in the settings)
500
261
  def refuse_superseded_write!
501
262
  return unless @superseded_by
502
263
 
@@ -507,19 +268,12 @@ module Hecks
507
268
  "reboot to write again."
508
269
  end
509
270
 
510
- # Saves an entry and reports whether it inserted or replaced, deciding that under the
511
- # same lock that guards the write.
512
- #
513
- # Outcome detection, journal append and every derived projection share
514
- # the same transaction and domain write lock. The lineage-aware head
515
- # determines whether this id is already visible; no repository `find`
516
- # occurs before entering this adapter-native operation.
271
+ # Saves an entry and reports whether it inserted or replaced, decided under the same lock
272
+ # that guards the write.
517
273
  #
518
274
  # @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
275
+ # @param insert_only [Boolean] when true, an id already in the head is left untouched
276
+ # @return [Symbol] `:inserted`, `:replaced`, or `:conflicted` when `insert_only` hit a record
523
277
  # @raise [Runtime::WiringError] if this boot's era has been superseded by a mint
524
278
  # @raise [PG::Error] if Postgres refuses a statement
525
279
  def atomic_put(entry, insert_only: false)
@@ -541,31 +295,22 @@ module Hecks
541
295
  status
542
296
  end
543
297
 
544
- # Builds the instance an entry describes, writing nothing.
545
- #
546
- # The head is derived — projecting is reading, so there is nothing
547
- # to write here. `append` above already keeps the snapshot the head
548
- # view reads from current, transactionally. The instance is still
549
- # built (and validated) so a save returns what every other adapter
550
- # returns.
298
+ # Builds the instance an entry describes, writing nothing; the head is derived, so
299
+ # projecting is reading.
551
300
  #
552
301
  # @param entry [Ports::Persistence::Entry] a journaled save or delete
553
302
  # @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`
303
+ # @raise [Runtime::WiringError] if the entry's state is still in its stored form
556
304
  def project(entry)
557
305
  return if entry.delete?
558
306
 
559
307
  Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: entry.state)
560
308
  end
561
309
 
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.
310
+ # Reads this aggregate's whole journal, every era, in write order. States are decoded but
311
+ # not translated: an ancestor era's row keeps the shape it was written in.
565
312
  #
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
313
+ # @return [Array<Ports::Persistence::Entry>] entries by ascending ordinal
569
314
  def entries
570
315
  @db.exec_params(
571
316
  "SELECT aggregate_id, operation, state, mirrors FROM #{@lineage.quoted_journal} " \
@@ -582,30 +327,15 @@ module Hecks
582
327
  end
583
328
  end
584
329
 
585
- # Deletes this aggregate's journal rows, refusing when row-level security silently
586
- # turns the DELETE into a no-op.
587
- #
588
- # The journal carries FORCE ROW LEVEL SECURITY with exactly two
589
- # policies — hecks_current_era's INSERT and hecks_read_all's
590
- # SELECT (advance_era! above) — and no DELETE policy at all, for
591
- # anyone. `FORCE` means even the table's own owner is fenced by
592
- # that (only an actual Postgres superuser or a role granted
593
- # BYPASSRLS sits above it — see lineage.rb's own header), so a
594
- # plain `DELETE ... WHERE aggregate = $1` from an ordinary
595
- # connection silently matches zero rows: no privilege error, no
596
- # exception, just a no-op that looks like success. Counting
597
- # before and comparing to what the DELETE itself reports is what
598
- # tells "nothing to delete" apart from "RLS silently ate the
599
- # delete" — the same row count, from the same statement, either
600
- # way, with no separate query racing the DELETE for an answer.
330
+ # Deletes this aggregate's journal rows, refusing when row-level security turns the DELETE
331
+ # into a silent no-op.
601
332
  #
602
- # Only the journal is cleared: head snapshots, field caches, the `events` table and
603
- # saga rows are left as they are.
333
+ # The journal forces RLS with no DELETE policy, so an ordinary connection matches zero rows
334
+ # without error; comparing the count before with the DELETE's own count exposes that.
335
+ # Head snapshots, field caches, `events` and saga rows are left as they are.
604
336
  #
605
337
  # @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
338
+ # @raise [Runtime::WiringError] if the journal held rows and the DELETE removed none
609
339
  def reset!
610
340
  before = @db.exec_params(
611
341
  "SELECT count(*) FROM #{@lineage.quoted_journal} WHERE aggregate = $1", [table]
@@ -621,86 +351,39 @@ module Hecks
621
351
  self
622
352
  end
623
353
 
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.
354
+ # Journals an instance's current state as a save entry. A direct-adapter convenience for
355
+ # specs and consoles; a runtime saves through `Ports::Persistence::AppendOnly`.
627
356
  #
628
357
  # @param instance [Runtime::Instance] the record to persist
629
358
  # @return [Runtime::Instance] a fresh instance built from the journaled state
630
359
  # @raise [Runtime::WiringError] if this boot's era has been superseded by a mint
631
- # @raise [PG::Error] if Postgres refuses a statement
632
360
  def save(instance)
633
361
  entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
634
362
  append(entry)
635
363
  project(entry)
636
364
  end
637
365
 
638
- # Journals a delete entry for an id and tombstones it in the head snapshot, without
639
- # checking first that the record exists.
366
+ # Journals a delete entry and tombstones the id in the head snapshot, without checking
367
+ # that the record exists.
640
368
  #
641
369
  # @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
370
+ # @return [true] always
643
371
  # @raise [Runtime::WiringError] if this boot's era has been superseded by a mint
644
- # @raise [PG::Error] if Postgres refuses a statement
645
372
  def delete(id)
646
373
  entry = Ports::Persistence::Entry.new(operation: "delete", id: id.to_s, state: nil)
647
374
  append(entry)
648
375
  true
649
376
  end
650
377
 
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
658
- def record_event(event)
659
- @db.exec_params(
660
- "INSERT INTO events (name, aggregate, aggregate_id, payload, occurred_at) VALUES ($1, $2, $3, $4, $5)",
661
- [event.name, event.aggregate, event.id.to_s, JSON.generate(event.payload), event.occurred_at]
662
- )
663
- end
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
670
- def events
671
- @db.exec("SELECT * FROM events ORDER BY id").map do |row|
672
- Runtime::Event.new(
673
- name: row["name"],
674
- aggregate: row["aggregate"],
675
- id: row["aggregate_id"],
676
- payload: JSON.parse(row["payload"], symbolize_names: true),
677
- occurred_at: row["occurred_at"]
678
- )
679
- end
680
- end
378
+ # Saga rows keep `domain` as a column so domains sharing one schema stay isolated. No lock
379
+ # here: `SagaInterpreter`'s own mutex already serializes in-process writers.
681
380
 
682
- # ── the optional saga-persistence capability (Ports::Persistence's
683
- # own three-method shape, §2) — one row per (domain, process_manager,
684
- # correlation), `domain` kept as an explicit column even under
685
- # schema isolation so two domains sharing one schema (neither
686
- # declares its own `schema`) still isolate correctly, matching
687
- # `hecks_eras`' own precedent (postgres/lineage/provisioning.rb).
688
- # No advisory lock of its own: every call here already runs inside
689
- # `SagaInterpreter`'s own mutex (§7) serializing in-process writers,
690
- # and gets the same cross-process safety an aggregate's own writes
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.
381
+ # Checkpoints one saga instance, replacing the row for the same (domain, process manager,
382
+ # correlation) if one exists.
695
383
  #
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
384
  # @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
385
+ # @param completed_compensations [Array] compensable legs already completed, stored as JSON
386
+ # @return [PG::Result] the upsert's result
704
387
  def save_saga(process_manager:, correlation:, state:, memory:, completed_compensations: [])
705
388
  @db.exec_params(
706
389
  "INSERT INTO hecks_saga_instances (domain, process_manager, correlation, state, memory, completed_compensations) " \
@@ -715,10 +398,7 @@ module Hecks
715
398
 
716
399
  # Removes a finished saga instance's checkpoint; a no-op when no such row exists.
717
400
  #
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
401
+ # @return [PG::Result] the DELETE's result
722
402
  def delete_saga(process_manager:, correlation:)
723
403
  @db.exec_params(
724
404
  "DELETE FROM hecks_saga_instances WHERE domain = $1 AND process_manager = $2 AND correlation = $3",
@@ -726,18 +406,10 @@ module Hecks
726
406
  )
727
407
  end
728
408
 
729
- # Yields every saga checkpoint stored for this domain, so a booting registry can
730
- # rehydrate its in-flight sagas.
409
+ # Yields every saga checkpoint stored for this domain, so a booting registry can rehydrate.
731
410
  #
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
411
+ # @yieldparam memory [Hash{Symbol => Object}] the saga's memory, keys symbolized
412
+ # @return [Enumerator, PG::Result] an enumerator when no block is given
741
413
  def each_saga
742
414
  return enum_for(:each_saga) unless block_given?
743
415
 
@@ -777,34 +449,14 @@ module Hecks
777
449
  "state = EXCLUDED.state WHERE #{quoted_head_snapshot}.ordinal < EXCLUDED.ordinal",
778
450
  [entry.id, ordinal, state_json]
779
451
  )
780
- # **Same transaction, same ordinal** — every field cache stays
781
- # exactly as current as the snapshot it's derived from, for
782
- # the identical reason `postgres_era.rb`'s own header comment
783
- # gives for the journal/snapshot pair: if this transaction
784
- # commits, every cache row is already correct; if it doesn't,
785
- # none of them changed.
452
+ # Same transaction and ordinal, so every cache is exactly as current as the snapshot.
786
453
  @field_caches.each do |field, cache_table|
787
454
  @lineage.upsert_field_cache_row!(cache_table, entry.id, ordinal, state_json, query_expression(field))
788
455
  end
789
456
  else
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
793
- # once an ancestor era is in the picture: for a record carried
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
796
- # outrank the ancestor matview's own (still-present, still
797
- # `save`) row, so `DISTINCT ON` picks the ancestor's row and
798
- # the "deleted" record keeps reading back forever. Upserting a
799
- # tombstone (`operation = 'delete'`, `state` NULL) instead
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
802
- # masked correctly" — the audit's own phrasing for why that
803
- # half never breaks) — it just carries `operation
804
- # = 'delete'` instead of `'save'`, so `head_view`'s own `WHERE
805
- # operation = 'save'` still correctly hides it. Ordinal-guarded
806
- # the same as every other upsert here, so an out-of-order
807
- # replay can never let a stale delete clobber a newer save.
457
+ # A tombstone row, not a bare DELETE: without one, an ancestor era's saved row would win
458
+ # the head's DISTINCT ON and the deleted record would read back. The ordinal guard keeps
459
+ # a stale replayed delete from clobbering a newer save.
808
460
  @db.exec_params(
809
461
  "INSERT INTO #{quoted_head_snapshot} (id, ordinal, operation, state) VALUES ($1, $2, 'delete', NULL) " \
810
462
  "ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal, operation = EXCLUDED.operation, " \
@@ -817,8 +469,6 @@ module Hecks
817
469
  ordinal
818
470
  end
819
471
 
820
- # ── SqlQueryBuilder's dialect hooks ─────────────────────────────
821
-
822
472
  def select_list = "id, state"
823
473
  def from_relation = quoted_head
824
474
  def dialect_name = "PostgresEra"
@@ -854,17 +504,11 @@ module Hecks
854
504
  @db.exec_params(sql, binds).map { |row| instance(row) }
855
505
  end
856
506
 
857
- # ── the rest of the dialect ─────────────────────────────────────
858
-
859
507
  def instance(row)
860
508
  Runtime::Instance.new(aggregate: @aggregate, id: row["id"], state: decode(row["state"]))
861
509
  end
862
510
 
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.
511
+ # Runs after SQL-side era translation, as the last step of every read.
868
512
  def decode(state_json)
869
513
  Ports::Persistence::StateCodec.decode(@aggregate, JSON.parse(state_json))
870
514
  end
@@ -878,11 +522,8 @@ module Hecks
878
522
  numeric_field?(field) ? "(#{expression})::numeric" : expression
879
523
  end
880
524
 
881
- # Postgres defaults to NULLS LAST on ASC; the port's in-memory
882
- # semantics (NullPolicy.order, which SQLite's own default happens
883
- # to match) put null rows FIRST ascending and LAST
884
- # descending. Compile the placement explicitly so a declared query
885
- # answers identically no matter which adapter serves it.
525
+ # Postgres defaults to NULLS LAST on ASC; place nulls explicitly (first ascending, last
526
+ # descending) so a declared query answers the same on every adapter.
886
527
  def order_clause(order_by, policy)
887
528
  direction = order_by.direction.to_s.downcase == "desc" ? "DESC" : "ASC"
888
529
  nulls = case policy&.mode.to_s
@@ -893,10 +534,7 @@ module Hecks
893
534
  "#{order_expression(order_by.field)} #{direction}#{nulls}, id #{direction}"
894
535
  end
895
536
 
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
899
- # as text: "900" above "1200".
537
+ # One shared walk decides numericness at any depth, so a nested path is not ordered as text.
900
538
  def numeric_field?(field)
901
539
  name, *path = field.to_s.split(".")
902
540
  QuerySpecification::FieldPath.numeric?(@aggregate.attribute(name), path) do |type|
@@ -904,31 +542,16 @@ module Hecks
904
542
  end
905
543
  end
906
544
 
907
- # `ARRAY[...]` of individually-escaped literals, never the hand-rolled
908
- # '{a,b,c}' array-literal syntax — a segment is a field or
909
- # value-object member name, and while today's callers only ever
910
- # pass schema-declared names, this method has no way to know
911
- # that, and the '{...}' form has no escaping at all: a segment
912
- # containing a single quote closes the string early and whatever
913
- # follows becomes live SQL. Measured, not assumed — a crafted
914
- # field name of `x}' = '' OR $1::text = $1::text -- ` made a
915
- # `where(secret: "public")` clause return every row regardless,
916
- # against the '{...}' form; the `ARRAY[]` form below closes it, verified
917
- # against the identical payload.
545
+ # `ARRAY[...]` of escaped literals, never the '{a,b}' syntax: that form has no escaping, so
546
+ # a quote in a segment would become live SQL.
918
547
  def jsonb_path(segments)
919
548
  "state #>> ARRAY[#{segments.map { |segment| text_literal(segment) }.join(', ')}]::text[]"
920
549
  end
921
550
 
922
551
  def text_literal(text) = "'#{text.to_s.gsub("'", "''")}'"
923
552
 
924
- # ── the field-cache read shortcut (Track C) ─────────────────────
925
-
926
- # Every declared `where`-field this aggregate's own queries and its
927
- # entities' own queries use, minus anything a cache table can't
928
- # represent (see `cacheable_field?`) — never `order_by`-only
929
- # fields, which never needed a cache in the first place (sorting
930
- # the reduced output was never blocked by the reduction; only
931
- # filtering was — see field_cache.rb's own header).
553
+ # Where-fields of the aggregate's and its entities' queries that a cache table can represent;
554
+ # order_by-only fields never needed a cache.
932
555
  def ensure_field_caches!
933
556
  cached_where_fields.to_h do |field|
934
557
  [field, @lineage.ensure_field_cache!(table, @era, field, query_expression(field))]
@@ -948,14 +571,7 @@ module Hecks
948
571
  @aggregate.queries + @aggregate.entities.flat_map(&:queries)
949
572
  end
950
573
 
951
- # List-typed fields are excluded, same boundary the plain `Postgres`
952
- # and `Sqlite`/`D1` adapters independently landed on for their own
953
- # automatic indexing: `contains` means element membership, and a
954
- # (id, ordinal, one value) cache row has nowhere to put more than
955
- # one element. Everything else — a plain scalar, the lifecycle
956
- # field, or a non-list value-object member path — reduces to
957
- # exactly one comparable value per id, which is the one shape this
958
- # cache table represents.
574
+ # List fields are excluded: a one-value cache row cannot hold `contains` membership.
959
575
  def cacheable_field?(field)
960
576
  name = field.to_s.split(".").first
961
577
  return true if @aggregate.lifecycle&.field.to_s == name
@@ -964,28 +580,16 @@ module Hecks
964
580
  !attribute.nil? && !attribute.list?
965
581
  end
966
582
 
967
- # A clause can be served by the cache when its field has a cache
968
- # table and it isn't the null-vs-value special case
969
- # `QuerySpecification::Common::NullPolicy` intercepts before
970
- # `where_clause` ever runs (see `query`'s own header comment) — a
971
- # clause that fails either check simply flows to `head_phase`
972
- # unaccelerated, exactly as `super` would have evaluated it.
583
+ # A clause is cache-served when its field has a cache table and it is not a null comparison,
584
+ # which `NullPolicy` intercepts before `where_clause`.
973
585
  def cache_eligible?(clause, value)
974
586
  @field_caches.key?(clause.field.to_s) &&
975
587
  QuerySpecification::Common::NullPolicy.sql_predicate(query_expression(clause.field, value: value), clause.op,
976
588
  value).nil?
977
589
  end
978
590
 
979
- # **Phase one** — candidate ids, no reduction touched. One SELECT per
980
- # cached clause against its own narrow (id, ordinal, value) table,
981
- # `INTERSECT`ed into the set that satisfies every cached clause at
982
- # once. Reuses `where_clause` (SqlQueryBuilder, private, already
983
- # mixed into this class) unchanged against the cache table's own
984
- # `value` column instead of a jsonb path expression — the exact
985
- # same operator compilation (`eq`/`ne`/`gt`/`gte`/`lt`/`lte`/`in`)
986
- # a live query already gets against the real column, so a cached
987
- # field supports every comparator `super` would have, not just the
988
- # `eq` example in the plan doc's own illustration.
591
+ # Phase one: candidate ids from the narrow cache tables, INTERSECTed. Reuses `where_clause`
592
+ # against each cache's `value` column, so every operator `super` supports works here.
989
593
  def cache_phase(cached)
990
594
  binds = []
991
595
  clauses = cached.map do |clause, value|
@@ -996,20 +600,8 @@ module Hecks
996
600
  @db.exec_params(clauses.join("\nINTERSECT\n"), binds).map { |row| row["id"] }
997
601
  end
998
602
 
999
- # **Phase two** — `head_view`, restricted to phase one's candidate ids
1000
- # plus whatever clauses phase one couldn't accelerate, applied
1001
- # exactly the way `SqlQueryBuilder#query` (`super`) already applies
1002
- # every clause today: against the fully-reduced view, which is
1003
- # already correct regardless of caching (see this file's own
1004
- # `query` comment — a cache is a speed shortcut, never a
1005
- # correctness fix; head_view was always safe to filter directly,
1006
- # just expensive to reduce in the first place). Duplicates a small
1007
- # slice of `SqlQueryBuilder#query`'s own tail assembly (order_by/
1008
- # limit/offset) rather than reaching into it — principle 2
1009
- # (docs/implemented/postgres-era-adapter-split-plan.md): SqlQueryBuilder stays
1010
- # untouched by the era workaround, so this stays local to
1011
- # PostgresEra rather than growing a shared hook only one adapter
1012
- # would ever call.
603
+ # Phase two: the head view restricted to phase one's ids plus the clauses it could not
604
+ # accelerate. Repeats the tail of `SqlQueryBuilder#query` so that module stays untouched.
1013
605
  def head_phase(declared, uncached, ids, args)
1014
606
  binds = []
1015
607
  clauses = ["id IN (#{ids.map { |id| placeholder(binds, id) }.join(', ')})"]
@@ -1034,19 +626,6 @@ module Hecks
1034
626
  execute_query(sql, binds)
1035
627
  end
1036
628
 
1037
- def create_event_table!
1038
- @db.exec(<<~SQL)
1039
- CREATE TABLE IF NOT EXISTS events (
1040
- id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
1041
- name text NOT NULL,
1042
- aggregate text NOT NULL,
1043
- aggregate_id text NOT NULL,
1044
- payload jsonb,
1045
- occurred_at text
1046
- )
1047
- SQL
1048
- end
1049
-
1050
629
  def create_saga_table!
1051
630
  @db.exec(<<~SQL)
1052
631
  CREATE TABLE IF NOT EXISTS hecks_saga_instances (
@@ -1060,10 +639,7 @@ module Hecks
1060
639
  PRIMARY KEY (domain, process_manager, correlation)
1061
640
  )
1062
641
  SQL
1063
- # `CREATE TABLE IF NOT EXISTS` above is a no-op against a table
1064
- # this same domain already created before this column existed —
1065
- # the same idiom `rust/host/src/journal.rs`'s own
1066
- # `sagas_backfilled` column addition already uses.
642
+ # Covers a table created before this column existed, where IF NOT EXISTS is a no-op.
1067
643
  @db.exec("ALTER TABLE hecks_saga_instances ADD COLUMN IF NOT EXISTS completed_compensations jsonb " \
1068
644
  "NOT NULL DEFAULT '[]'::jsonb")
1069
645
  end