hecks 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (373) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +72 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +83 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
  7. data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
  9. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  10. data/lib/hecks/adapters/driven/heki.rb +106 -9
  11. data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
  12. data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
  13. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  15. data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
  16. data/lib/hecks/adapters/driven/lambda.rb +82 -35
  17. data/lib/hecks/adapters/driven/local_storage.rb +83 -10
  18. data/lib/hecks/adapters/driven/memory.rb +205 -9
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  24. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  25. data/lib/hecks/adapters/driven/prism.rb +50 -11
  26. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  31. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  32. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  33. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  34. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  35. data/lib/hecks/adapters/driven.rb +6 -4
  36. data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
  37. data/lib/hecks/behaviors/dsl.rb +60 -2
  38. data/lib/hecks/behaviors/expectations.rb +190 -29
  39. data/lib/hecks/behaviors/ir.rb +12 -1
  40. data/lib/hecks/behaviors/rspec.rb +9 -1
  41. data/lib/hecks/behaviors/runner.rb +21 -2
  42. data/lib/hecks/behaviors.rb +9 -1
  43. data/lib/hecks/bluebook/aggregate.rb +43 -13
  44. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
  45. data/lib/hecks/bluebook/assembly/build.rb +26 -5
  46. data/lib/hecks/bluebook/assembly/contract.rb +98 -23
  47. data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
  48. data/lib/hecks/bluebook/assembly/marks.rb +159 -30
  49. data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
  50. data/lib/hecks/bluebook/assembly.rb +32 -14
  51. data/lib/hecks/bluebook/attribute.rb +26 -12
  52. data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
  53. data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
  54. data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
  55. data/lib/hecks/bluebook/behaviour/command.rb +55 -25
  56. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
  57. data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
  58. data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
  59. data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
  60. data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
  61. data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
  62. data/lib/hecks/bluebook/behaviour/query.rb +6 -1
  63. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
  64. data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
  65. data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
  66. data/lib/hecks/bluebook/capabilities.rb +27 -0
  67. data/lib/hecks/bluebook/chapter.rb +51 -9
  68. data/lib/hecks/bluebook/command.rb +62 -17
  69. data/lib/hecks/bluebook/domain_port.rb +34 -9
  70. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  71. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  72. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
  73. data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
  74. data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
  75. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
  76. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
  77. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  78. data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
  79. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  80. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  81. data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
  82. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  83. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
  84. data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
  85. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  86. data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
  87. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  88. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  89. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
  90. data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
  91. data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
  92. data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
  93. data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
  94. data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
  95. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  96. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  97. data/lib/hecks/bluebook/entity.rb +40 -11
  98. data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
  99. data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
  100. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
  101. data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
  102. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
  103. data/lib/hecks/bluebook/expression/resolver.rb +369 -128
  104. data/lib/hecks/bluebook/hexagon.rb +35 -1
  105. data/lib/hecks/bluebook/lifecycle.rb +12 -1
  106. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
  107. data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
  108. data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
  109. data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
  110. data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
  111. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
  112. data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
  113. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
  114. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
  115. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
  116. data/lib/hecks/bluebook/meta_validator.rb +235 -139
  117. data/lib/hecks/bluebook/model_check.rb +434 -104
  118. data/lib/hecks/bluebook/pattern_subset.rb +32 -10
  119. data/lib/hecks/bluebook/policy.rb +15 -13
  120. data/lib/hecks/bluebook/process_manager.rb +27 -14
  121. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  122. data/lib/hecks/bluebook/project_loader.rb +40 -0
  123. data/lib/hecks/bluebook/project_register.rb +50 -6
  124. data/lib/hecks/bluebook/query.rb +31 -4
  125. data/lib/hecks/bluebook/read_model.rb +35 -15
  126. data/lib/hecks/bluebook/reference.rb +26 -13
  127. data/lib/hecks/bluebook/smoke_test.rb +46 -23
  128. data/lib/hecks/bluebook/synthesizer.rb +46 -12
  129. data/lib/hecks/bluebook/translation.rb +34 -5
  130. data/lib/hecks/bluebook/value_object.rb +29 -11
  131. data/lib/hecks/bluebook.rb +5 -6
  132. data/lib/hecks/codemod.rb +138 -50
  133. data/lib/hecks/construct.rb +21 -7
  134. data/lib/hecks/corpus.rb +438 -0
  135. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  136. data/lib/hecks/doc/reference.rb +200 -31
  137. data/lib/hecks/embryonaut_bluebook.rb +38 -15
  138. data/lib/hecks/facade/cli_door.rb +69 -10
  139. data/lib/hecks/facade/cli_runner.rb +105 -24
  140. data/lib/hecks/facade/command_request.rb +23 -0
  141. data/lib/hecks/facade/handle.rb +155 -35
  142. data/lib/hecks/facade/json_door.rb +106 -25
  143. data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
  144. data/lib/hecks/facade/surface/chapter.rb +26 -17
  145. data/lib/hecks/facade/surface.rb +16 -3
  146. data/lib/hecks/facade.rb +15 -4
  147. data/lib/hecks/forms/app.rb +46 -30
  148. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  149. data/lib/hecks/forms/field_renderer.rb +142 -6
  150. data/lib/hecks/forms/field_shape.rb +183 -19
  151. data/lib/hecks/forms/html.rb +51 -7
  152. data/lib/hecks/forms/index_renderer.rb +14 -2
  153. data/lib/hecks/forms/page.rb +14 -0
  154. data/lib/hecks/forms/params.rb +120 -23
  155. data/lib/hecks/forms/port_argument.rb +14 -2
  156. data/lib/hecks/forms/query_form_renderer.rb +65 -2
  157. data/lib/hecks/forms/record_renderer.rb +60 -2
  158. data/lib/hecks/forms/record_table.rb +28 -1
  159. data/lib/hecks/forms/reference_options.rb +24 -0
  160. data/lib/hecks/forms/value_object_shape.rb +13 -3
  161. data/lib/hecks/forms.rb +24 -4
  162. data/lib/hecks/fqn.rb +59 -1
  163. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  164. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  165. data/lib/hecks/framework/oidc.json +15 -0
  166. data/lib/hecks/framework.rb +79 -25
  167. data/lib/hecks/freezer.rb +27 -11
  168. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
  169. data/lib/hecks/fuzzing/combination_miner.rb +178 -0
  170. data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
  171. data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
  172. data/lib/hecks/fuzzing/differential.rb +192 -0
  173. data/lib/hecks/fuzzing/domain_generator.rb +871 -0
  174. data/lib/hecks/fuzzing/era_boundary.rb +68 -19
  175. data/lib/hecks/fuzzing/form_census.rb +121 -20
  176. data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
  177. data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
  178. data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
  179. data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
  180. data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
  181. data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
  182. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
  183. data/lib/hecks/fuzzing/properties/guards.rb +86 -42
  184. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
  185. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
  186. data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
  187. data/lib/hecks/fuzzing/properties/querying.rb +82 -28
  188. data/lib/hecks/fuzzing/properties.rb +84 -51
  189. data/lib/hecks/fuzzing/qa_settings.rb +164 -0
  190. data/lib/hecks/fuzzing/replay.rb +241 -133
  191. data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
  192. data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
  193. data/lib/hecks/fuzzing/self_consistency.rb +315 -137
  194. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
  195. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  196. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  197. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  198. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
  199. data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
  200. data/lib/hecks/fuzzing/shrinker.rb +309 -0
  201. data/lib/hecks/fuzzing/structural_skips.rb +37 -130
  202. data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
  203. data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
  204. data/lib/hecks/fuzzing/value_generator.rb +110 -22
  205. data/lib/hecks/fuzzing.rb +5 -0
  206. data/lib/hecks/grammar/evolve.rb +188 -12
  207. data/lib/hecks/grammar.rb +53 -7
  208. data/lib/hecks/ir.rb +51 -20
  209. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  210. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  211. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  212. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  213. data/lib/hecks/language/oidc.json +5 -0
  214. data/lib/hecks/literal.rb +41 -9
  215. data/lib/hecks/naming.rb +112 -31
  216. data/lib/hecks/ports/access_control.rb +53 -2
  217. data/lib/hecks/ports/agent/answers.rb +83 -6
  218. data/lib/hecks/ports/agent.rb +119 -35
  219. data/lib/hecks/ports/authentication.rb +44 -4
  220. data/lib/hecks/ports/authorization.rb +50 -11
  221. data/lib/hecks/ports/clock.rb +42 -23
  222. data/lib/hecks/ports/extraction.rb +16 -0
  223. data/lib/hecks/ports/identity_assignment.rb +23 -2
  224. data/lib/hecks/ports/identity_generation.rb +17 -3
  225. data/lib/hecks/ports/identity_resolution.rb +17 -1
  226. data/lib/hecks/ports/key_vault.port +6 -0
  227. data/lib/hecks/ports/key_vault.rb +58 -0
  228. data/lib/hecks/ports/loading.rb +4 -0
  229. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  230. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  231. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  232. data/lib/hecks/ports/persistence/execution.rb +4 -0
  233. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  234. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  235. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  236. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  237. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  238. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  239. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  250. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  251. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  252. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
  253. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  254. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  255. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  256. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  257. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  258. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
  259. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  260. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  261. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  262. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
  263. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
  264. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  265. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
  266. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  267. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  268. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  269. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  270. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  271. data/lib/hecks/ports/persistence.rb +36 -1
  272. data/lib/hecks/ports/projection.rb +61 -7
  273. data/lib/hecks/ports/query/in_memory.rb +41 -8
  274. data/lib/hecks/ports/query/ordering.rb +21 -6
  275. data/lib/hecks/ports/query.rb +35 -0
  276. data/lib/hecks/ports.rb +1 -0
  277. data/lib/hecks/projections/bootstrap_table.rb +147 -0
  278. data/lib/hecks/projections/diagrams.rb +315 -79
  279. data/lib/hecks/projections/glossary/html.rb +91 -3
  280. data/lib/hecks/projections/glossary/markdown.rb +50 -3
  281. data/lib/hecks/projections/glossary/mermaid.rb +49 -1
  282. data/lib/hecks/projections/glossary/sections.rb +1 -1
  283. data/lib/hecks/projections/glossary/sentences.rb +110 -5
  284. data/lib/hecks/projections/glossary.rb +173 -31
  285. data/lib/hecks/projections/ir.rb +1 -1
  286. data/lib/hecks/projections/model/deviations.rb +62 -17
  287. data/lib/hecks/projections/model.rb +75 -21
  288. data/lib/hecks/projections/oidc.rb +45 -8
  289. data/lib/hecks/projections/parser_table.rb +53 -12
  290. data/lib/hecks/projections/reference.rb +15 -3
  291. data/lib/hecks/projections/rust_vocabulary.rb +646 -0
  292. data/lib/hecks/projections/shape.rb +10 -2
  293. data/lib/hecks/projections/statements.rb +69 -22
  294. data/lib/hecks/projections/vocabulary.rb +26 -9
  295. data/lib/hecks/projections.rb +5 -3
  296. data/lib/hecks/projector/cli_projector.rb +245 -37
  297. data/lib/hecks/projector/docs_projector.rb +154 -28
  298. data/lib/hecks/projector/exporter.rb +104 -29
  299. data/lib/hecks/projector/ir_projector.rb +7 -1
  300. data/lib/hecks/projector/narrate_projector.rb +144 -23
  301. data/lib/hecks/projector/target.rb +42 -18
  302. data/lib/hecks/projector.rb +86 -18
  303. data/lib/hecks/query_ir.rb +94 -47
  304. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  305. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  306. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  307. data/lib/hecks/query_specification/common/null_policy.rb +61 -15
  308. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  309. data/lib/hecks/query_specification/common/options.rb +25 -0
  310. data/lib/hecks/query_specification/field_path.rb +69 -15
  311. data/lib/hecks/query_specification/hop_path.rb +57 -20
  312. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  313. data/lib/hecks/rendering.rb +9 -3
  314. data/lib/hecks/router/namespace_installer.rb +16 -3
  315. data/lib/hecks/router.rb +56 -1
  316. data/lib/hecks/runtime/aggregate_lock.rb +20 -11
  317. data/lib/hecks/runtime/boot_gates.rb +21 -3
  318. data/lib/hecks/runtime/caller.rb +40 -8
  319. data/lib/hecks/runtime/capability_graph.rb +13 -2
  320. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
  321. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
  322. data/lib/hecks/runtime/command_interpreter.rb +195 -113
  323. data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
  324. data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
  325. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  326. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  327. data/lib/hecks/runtime/command_rules/references.rb +145 -55
  328. data/lib/hecks/runtime/command_rules.rb +3 -0
  329. data/lib/hecks/runtime/dependency_planning.rb +56 -11
  330. data/lib/hecks/runtime/dispatcher.rb +263 -92
  331. data/lib/hecks/runtime/entity_element.rb +304 -67
  332. data/lib/hecks/runtime/entity_interpreter.rb +149 -88
  333. data/lib/hecks/runtime/errors.rb +37 -23
  334. data/lib/hecks/runtime/event.rb +15 -10
  335. data/lib/hecks/runtime/identity.rb +91 -23
  336. data/lib/hecks/runtime/instance.rb +103 -18
  337. data/lib/hecks/runtime/interpreting.rb +24 -16
  338. data/lib/hecks/runtime/invocation.rb +358 -0
  339. data/lib/hecks/runtime/loader.rb +105 -19
  340. data/lib/hecks/runtime/outbox.rb +164 -26
  341. data/lib/hecks/runtime/policy_interpreter.rb +73 -60
  342. data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
  343. data/lib/hecks/runtime/query_interpreter.rb +93 -74
  344. data/lib/hecks/runtime/reaction_invocation.rb +73 -28
  345. data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
  346. data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
  347. data/lib/hecks/runtime/reference_hop.rb +48 -6
  348. data/lib/hecks/runtime/refusal_wording.rb +142 -115
  349. data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
  350. data/lib/hecks/runtime/registry/verification.rb +153 -28
  351. data/lib/hecks/runtime/registry.rb +202 -32
  352. data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
  353. data/lib/hecks/runtime/routing.rb +36 -154
  354. data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
  355. data/lib/hecks/runtime/saga_interpreter.rb +90 -76
  356. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  357. data/lib/hecks/runtime/tenant_check.rb +33 -13
  358. data/lib/hecks/runtime/tenant_scope.rb +23 -5
  359. data/lib/hecks/runtime/value/admission.rb +75 -30
  360. data/lib/hecks/runtime/value/coercion.rb +555 -142
  361. data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
  362. data/lib/hecks/runtime/value.rb +71 -21
  363. data/lib/hecks/runtime.rb +39 -7
  364. data/lib/hecks/storehouse.rb +368 -72
  365. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  366. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  367. data/lib/hecks/version.rb +3 -3
  368. data/lib/hecks/vocabulary.rb +205 -4
  369. data/lib/hecks.rb +91 -11
  370. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
  371. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
  372. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
  373. metadata +28 -2
@@ -15,9 +15,17 @@ module Hecks
15
15
  # `ancestor_name` is the declared name a rename came from (matches
16
16
  # the held bluebook's own aggregate names); `ancestor_storage_name`
17
17
  # is its derived, snake_case file/table name. Both are nil unless
18
- # the aggregate itself was renamed.
18
+ # the edge declares a `was:` name that differs from the aggregate's own.
19
19
  attr_reader :ancestor_name, :ancestor_storage_name
20
20
 
21
+ # Builds the rules of the first declared translation that mentions an aggregate.
22
+ #
23
+ # @param registry [Runtime::Registry] the registry whose declared translations are
24
+ # searched, in declaration order
25
+ # @param domain [String, Symbol] name of the domain the aggregate belongs to
26
+ # @param aggregate [Bluebook::Aggregate] the aggregate as currently declared
27
+ # @return [Ports::Persistence::Lineage, nil] the rules of the first translation for `domain`
28
+ # that names the aggregate; nil when none does
21
29
  def self.for(registry, domain, aggregate)
22
30
  translation = registry.translations.find do |candidate|
23
31
  candidate.domain == domain.to_s && candidate.for_aggregate(aggregate.name)
@@ -27,9 +35,18 @@ module Hecks
27
35
  from_declared(translation.for_aggregate(aggregate.name), aggregate.name)
28
36
  end
29
37
 
30
- # One SPECIFIC edge's rules for one aggregate — what the mint
38
+ # Builds the rules one declared translation edge carries for one aggregate.
39
+ #
40
+ # One specific edge's rules for one aggregate — what the mint
31
41
  # path uses, where `for` would happily answer with whichever
32
42
  # edge in the registry mentioned the aggregate first.
43
+ #
44
+ # @param declared [Bluebook::TranslationAggregate, nil] the edge's entry for the
45
+ # aggregate, as `Bluebook::Translation#for_aggregate` returns it; nil when the edge
46
+ # does not mention the aggregate
47
+ # @param aggregate_name [String, Symbol] the aggregate's current name, compared with
48
+ # `declared.was` to decide whether the edge renames the aggregate itself
49
+ # @return [Ports::Persistence::Lineage, nil] the edge's rules; nil when `declared` is nil
33
50
  def self.from_declared(declared, aggregate_name)
34
51
  return nil unless declared
35
52
 
@@ -43,6 +60,24 @@ module Hecks
43
60
  ancestor_name: ancestor_name, ancestor_storage_name: ancestor_storage_name)
44
61
  end
45
62
 
63
+ # @param renames [Hash{Symbol => Symbol}] old top-level attribute name to new name
64
+ # @param moves [Array<Bluebook::TranslationMove>] fields crossing a value-object
65
+ # boundary, each a `from`/`to` pair of bare or dotted paths
66
+ # @param converts [Array<Bluebook::TranslationConvert>] moves whose value is replaced
67
+ # through an exhaustive `values` lookup table
68
+ # @param drops [Array<Symbol>] bare or dotted paths whose data is deliberately discarded
69
+ # @param retypes [Array<Bluebook::TranslationRetype>] value-object or entity type names
70
+ # declared to mean the same shape
71
+ # @param computes [Array<Bluebook::TranslationCompute>] SQL-only rules, never applied
72
+ # in process
73
+ # @param rekeys [Array<Bluebook::TranslationRekey>] SQL-only identity rewrites; only
74
+ # the first is read
75
+ # @param backfills [Array<Bluebook::TranslationBackfill>] defaults for new top-level
76
+ # attributes an old entry lacks
77
+ # @param ancestor_name [String, nil] the aggregate's declared name before the edge;
78
+ # nil unless the edge renames the aggregate itself
79
+ # @param ancestor_storage_name [String, nil] snake_case storage name of
80
+ # `ancestor_name`; nil unless the edge renames the aggregate itself
46
81
  def initialize(renames, moves = [], converts = [], drops = [], retypes: [], computes: [], rekeys: [],
47
82
  backfills: [], ancestor_name: nil, ancestor_storage_name: nil)
48
83
  @renames = renames
@@ -57,37 +92,60 @@ module Hecks
57
92
  @ancestor_storage_name = ancestor_storage_name
58
93
  end
59
94
 
60
- # Whether any rule in this edge is a compute — the one rule kind
61
- # with no in-process implementation at all. An aggregate carrying
62
- # one refuses to boot anywhere but Postgres, per-rule and by name,
63
- # before the general drift machinery says anything vaguer.
95
+ # Reports whether any rule in this edge is a compute.
96
+ #
97
+ # Compute is the one rule kind with no in-process implementation at
98
+ # all. An aggregate carrying one refuses to boot anywhere but
99
+ # Postgres, per-rule and by name, before the general drift
100
+ # machinery says anything vaguer.
101
+ #
102
+ # @return [Boolean] true when the edge declares at least one compute rule
64
103
  def computes? = !@computes.empty?
65
104
 
66
- # THE SINGLE SOURCE OF TRUTH for "does this edge rekey this
105
+ # Reports whether this edge rewrites the aggregate's identity.
106
+ #
107
+ # The single source of truth for "does this edge rekey this
67
108
  # aggregate" — every consumer (coverage_check.rb's identity gate,
68
109
  # minter.rb's approval gate, layer_two.rb's audit, head_compiler.rb's
69
- # SQL compilation) asks THIS, never re-derives it from `declared`
110
+ # SQL compilation) asks this, never re-derives it from `declared`
70
111
  # independently. One accessor to change if what a rekey rule means
71
112
  # ever needs to change, not four call sites in four files.
113
+ #
114
+ # @return [Boolean] true when the edge declares at least one rekey rule
72
115
  def rekey? = !@rekeys.empty?
73
116
 
117
+ # Returns the SQL expression the edge's first rekey rule declares.
118
+ #
74
119
  # The rekey's own SQL — first-and-only rule, same one-per-aggregate
75
120
  # assumption `compute` makes about its own list where it matters
76
121
  # (an edge with more than one is a DSL-level decision, not
77
122
  # something this reader arbitrates).
123
+ #
124
+ # @return [String, nil] the first rekey rule's SQL; nil when the edge declares no rekey
78
125
  def rekey_sql = @rekeys.first&.sql
79
126
 
80
- # The reference semantics for the five PORTABLE rule kinds —
127
+ # Rewrites one journal entry's state from the held shape into the current one.
128
+ #
129
+ # The reference semantics for the five portable rule kinds —
81
130
  # rename, move, convert, drop, and the aggregate-level `was:`.
82
131
  # `retype` moves nothing (stored state never carries a type name)
83
132
  # and `compute` is deliberately not applied here: its SQL is its
84
133
  # only implementation, so this transform neither imitates nor
85
134
  # checks it — the source field passes through untouched, and the
86
135
  # audit verifies compute output against the matview alone.
136
+ #
137
+ # @param entry [Ports::Persistence::Entry] the entry as stored: `state` has Symbol
138
+ # top-level keys and String keys inside a nested value-object Hash
139
+ # @return [Ports::Persistence::Entry] a new entry with a deep-copied, translated state
140
+ # and the same `operation`, `id` and `mirrors`; `entry` itself, untouched, when it
141
+ # is not a save or carries no state
142
+ # @raise [Runtime::WiringError] if a convert meets a value missing from its `values`
143
+ # table, or a move or convert would nest under a destination already holding a
144
+ # non-Hash value
87
145
  def translate(entry)
88
146
  return entry unless entry.save? && entry.state
89
147
 
90
- # Deep, not shallow: a move or convert reaches INTO a nested
148
+ # Deep, not shallow: a move or convert reaches into a nested
91
149
  # value-object hash, and a shallow dup would quietly mutate
92
150
  # the caller's copy of the original entry.
93
151
  state = deep_dup(entry.state)
@@ -95,24 +153,26 @@ module Hecks
95
153
  @moves.each { |move| apply_move(state, move) }
96
154
  @converts.each { |convert| apply_convert(state, convert) }
97
155
  @drops.each { |name| apply_drop(state, name) }
98
- # LAST, and only where nothing already answered — a backfill
156
+ # Last, and only where nothing already answered — a backfill
99
157
  # fills the gap a rename/move/convert left untouched, never
100
158
  # overwrites a value that already made it across.
101
159
  @backfills.each { |backfill| state[backfill.name] = backfill.default unless state.key?(backfill.name) }
102
160
  Entry.new(operation: entry.operation, id: entry.id, state: state, mirrors: entry.mirrors)
103
161
  end
104
162
 
163
+ # Reports whether some rule accounts for a held path that vanished or changed type.
164
+ #
105
165
  # Whether this translation names `path` as an old key it accounts
106
166
  # for — the rename, move, or convert it came from, or an explicit
107
167
  # drop. `path` is a bare name ("cost") or a dotted value-object
108
- # member ("price.currency"); a rule covering the WHOLE top-level
168
+ # member ("price.currency"); a rule covering the whole top-level
109
169
  # attribute (a rename, a top-level move/convert/drop) also covers
110
170
  # anything nested under it, since the whole value travels or goes
111
- # away together. Used to catch a field — or a value object's own
112
- # member — that vanished (or silently changed type) without
113
- # anything explaining it, even when some OTHER field is covered.
171
+ # away together. This is what catches a field — or a value object's
172
+ # own member — that vanished (or silently changed type) without
173
+ # anything explaining it, even when some other field is covered.
114
174
  #
115
- # `backfills` matches on the WHOLE name only, never a dotted
175
+ # `backfills` matches on the whole name only, never a dotted
116
176
  # prefix — a backfill names a top-level attribute that is new
117
177
  # outright (nothing to be a prefix of on the held side), unlike
118
178
  # every rule above it, which explains a path that existed and
@@ -121,9 +181,14 @@ module Hecks
121
181
  # One `||` chain over a closed, fixed set of rule kinds (renames,
122
182
  # moves, converts, drops, computes, backfills) — the same six this
123
183
  # file's other methods enumerate. Splitting each disjunct into its
124
- # own predicate would scatter one question ("does ANY rule explain
184
+ # own predicate would scatter one question ("does any rule explain
125
185
  # this path") across six same-shaped methods with nothing else to
126
186
  # do.
187
+ #
188
+ # @param path [String, Symbol] a bare attribute name or a dotted value-object member
189
+ # path on the held side
190
+ # @return [Boolean] true when a rename, move, convert, drop or compute names the path or
191
+ # its top-level attribute as its source, or a backfill names the top-level attribute
127
192
  # rubocop:disable-next Metrics/CyclomaticComplexity
128
193
  # rubocop:disable-next Metrics/PerceivedComplexity
129
194
  def explains?(path)
@@ -138,31 +203,36 @@ module Hecks
138
203
  @backfills.any? { |backfill| backfill.name.to_s == top }
139
204
  end
140
205
 
141
- # THE DESTINATION-SIDE TWIN of `explains?` above, which only ever
142
- # asks about a rule's SOURCE. `unsafe_additions` asks a different
206
+ # Reports whether some rule gives an existing record a value at a new attribute.
207
+ #
208
+ # The destination-side twin of `explains?` above, which only ever
209
+ # asks about a rule's source. `unsafe_additions` asks a different
143
210
  # question — not "was this vanished path accounted for" but "does
144
211
  # an existing record end up with a value here" — and a move or
145
- # convert whose `to:` lands a old field inside a BRAND-NEW
212
+ # convert whose `to:` lands a old field inside a brand-new
146
213
  # top-level attribute (`weight` becoming `contents.weight` when
147
214
  # `Contents` did not exist before) fills that attribute for an
148
215
  # existing record exactly as a `backfill` would, even though
149
216
  # nothing named `contents` explains any vanished path. `compute`
150
217
  # counts on the same terms `explains?` already grants it
151
218
  # elsewhere in this file — Postgres-only and audited, not
152
- # actually applied by THIS method, the same gap the vanish side
219
+ # actually applied by this method, the same gap the vanish side
153
220
  # already lives with.
154
221
  #
155
- # `@renames.value?` belongs here too, and used to be missing: a
156
- # bare `rename :cost, to: :amount` is the plainest possible
157
- # covering rule there is (`translate` above applies it
158
- # unconditionally, no lookup table, no per-record ambiguity —
159
- # simpler than a move or convert, which both got their `fills?`
160
- # entry from the start), and its absence meant `unsafe_additions`
161
- # reported the new name as an unexplained required addition on
162
- # EVERY rename-only edge, the single most common translation
163
- # shape there is. `explains?` already checked the source side
164
- # (`@renames.key?`); `fills?` is the symmetric destination-side
165
- # check that was never added alongside it.
222
+ # `@renames.value?` belongs here too: a bare
223
+ # `rename :cost, to: :amount` is the plainest possible covering
224
+ # rule there is (`translate` above applies it unconditionally, no
225
+ # lookup table, no per-record ambiguity — simpler than a move or
226
+ # convert), and without it `unsafe_additions` reports the new name
227
+ # as an unexplained required addition on every rename-only edge,
228
+ # the single most common translation shape there is. `explains?`
229
+ # checks the source side (`@renames.key?`); this is the symmetric
230
+ # destination-side check.
231
+ #
232
+ # @param path [String, Symbol] the name of a top-level attribute new in the current
233
+ # shape
234
+ # @return [Boolean] true when a rename, move, convert or compute lands a value in that
235
+ # attribute, or a backfill names it
166
236
  def fills?(path)
167
237
  path = path.to_s
168
238
 
@@ -173,11 +243,18 @@ module Hecks
173
243
  @backfills.any? { |backfill| backfill.name.to_s == path }
174
244
  end
175
245
 
176
- # Whether a declared retype says the pair of TYPE names means the
177
- # same shape — a value object or entity whose own name changed
246
+ # Reports whether a declared retype pairs two type names as the same shape.
247
+ #
248
+ # A retype covers a value object or entity whose own name changed
178
249
  # with its members intact. Nothing in the stored data carries the
179
250
  # type name, so this never moves a value; it only satisfies the
180
251
  # era diff's literal type-name comparison.
252
+ #
253
+ # @param held_type [String, Bluebook::Reference] the type the held era declares,
254
+ # compared by its `to_s`
255
+ # @param current_type [String, Bluebook::Reference] the type declared now, compared by
256
+ # its `to_s`
257
+ # @return [Boolean] true when some retype rule runs from `held_type` to `current_type`
181
258
  def retype?(held_type, current_type)
182
259
  @retypes.any? { |retype| retype.from == held_type.to_s && retype.to == current_type.to_s }
183
260
  end
@@ -193,25 +270,25 @@ module Hecks
193
270
  end
194
271
 
195
272
  # M27 (docs/audits/2026-08-10-main-bug-audit.md,
196
- # docs/audits/2026-08-11-bug-triage.md) — SIMULTANEOUS, not
273
+ # docs/audits/2026-08-11-bug-triage.md) — simultaneous, not
197
274
  # sequential: `state[new] = state.delete(old)` per rename, run
198
- # one rule at a time against the SAME hash it was reading from,
275
+ # one rule at a time against the same hash it was reading from,
199
276
  # loses data the instant one rule's destination is another
200
- # rule's source. A swap (`rename :a, to: :b` alongside
201
- # `rename :b, to: :a`) on `{a: 1, b: 2}` used to produce
202
- # `{a: 1}` — the first rule wrote `b: 1` over the real `b: 2`
203
- # before the second rule ever got a chance to read it, and the
204
- # value the whole edge was supposed to preserve (2, moved to
205
- # `:a`) was gone. The standard fix: snapshot every rule's OLD
206
- # key and value from `state` FIRST, then remove every old key
207
- # and only THEN write every new key — a rename never reads a
277
+ # rule's source. Applied sequentially, a swap (`rename :a, to: :b`
278
+ # alongside `rename :b, to: :a`) on `{a: 1, b: 2}` produces
279
+ # `{a: 1}` — the first rule writes `b: 1` over the real `b: 2`
280
+ # before the second rule ever gets a chance to read it, and the
281
+ # value the whole edge is supposed to preserve (2, moved to
282
+ # `:a`) is gone. The standard fix: snapshot every rule's old
283
+ # key and value from `state` first, then remove every old key
284
+ # and only then write every new key — a rename never reads a
208
285
  # key this same pass has already written to, so a swap or a
209
286
  # longer chain applies as one permutation, not a sequence of
210
287
  # edits each stepping on the last.
211
288
  def apply_renames(state, renames)
212
289
  snapshot = renames.filter_map { |old_name, new_name| [old_name, new_name, state[old_name]] if state.key?(old_name) }
213
290
  snapshot.each { |old_name, _new_name, _value| state.delete(old_name) }
214
- # NOT combinable (Style/CombinableLoops is disabled repo-wide, see
291
+ # Not combinable (Style/CombinableLoops is disabled repo-wide, see
215
292
  # .rubocop.yml, for exactly this reason): a swap (:a<->:b) needs
216
293
  # every delete done before any write, or the first rename's write
217
294
  # becomes the second rename's delete target — see this method's
@@ -236,8 +313,16 @@ module Hecks
236
313
  end
237
314
 
238
315
  # A dotted path's first segment is a top-level (symbol) key; a
239
- # second segment reaches into a value-object member, stored with a
240
- # string key exactly as the adapter's own journal reader loaded it.
316
+ # second segment reaches into a value-object member by its string
317
+ # key — the spelling a raw stored row carries. Translation runs on
318
+ # raw rows, before the state codec decodes anything (PR A3): its
319
+ # only caller (`Translation::Audit::LayerTwo`) feeds it
320
+ # head-snapshot rows straight out of `JSON.parse`, and the
321
+ # PostgresEra head applies the same rules in SQL before
322
+ # `PostgresEra#decode` ever sees the jsonb. Decode is always the
323
+ # last step, so an undeclared (retired) member this rule has to
324
+ # read is still exactly as it was written, and never something an
325
+ # adapter's `entries` — decoded, deep-symbol — would be fed here.
241
326
  def apply_move(state, move)
242
327
  old_top, old_member = move.from.split(".", 2)
243
328
  new_top, new_member = move.to.split(".", 2)
@@ -282,19 +367,18 @@ module Hecks
282
367
  [value, true]
283
368
  end
284
369
 
285
- # ADVERSARIAL FINDING, not a hypothetical: a destination whose
286
- # top segment ALREADY holds a value — most commonly a reference,
287
- # stored as a bare scalar id — used to be silently clobbered with
288
- # an empty hash the moment a dotted destination needed to nest
289
- # under it (`state[top] ||= {}` only guards nil/false, so a
290
- # truthy non-Hash sailed straight through to `state[top][member] =`,
291
- # i.e. `"team-1"["detail"] =`, which is String#[]= and raised an
292
- # unrelated-looking IndexError). Whether it crashed or silently
293
- # replaced the value, this is a `drop` that never declared
294
- # itself — the one thing this language exists to make explicit
295
- # (see apply_convert's own refusal above, the same shape). Refuse
296
- # by name instead, on both sides: the SQL half (hecks_tr_insert)
297
- # raises the identical wording.
370
+ # Adversarial finding, not a hypothetical: a destination whose
371
+ # top segment already holds a value — most commonly a reference,
372
+ # stored as a bare scalar id — must not be nested under when a
373
+ # dotted destination needs it (`state[top] ||= {}` alone only
374
+ # guards nil/false, so a truthy non-Hash sails straight through
375
+ # to `state[top][member] =`, i.e. `"team-1"["detail"] =`, which
376
+ # is String#[]= and raises an unrelated-looking IndexError).
377
+ # Whether it crashes or silently replaces the value, that is a
378
+ # `drop` that never declared itself — the one thing this language
379
+ # exists to make explicit (see apply_convert's own refusal above,
380
+ # the same shape). Refuse by name instead, on both sides: the SQL
381
+ # half (hecks_tr_insert) raises the identical wording.
298
382
  def insert(state, top, member, value, rule:)
299
383
  return state[top] = value unless member
300
384
 
@@ -11,12 +11,20 @@ module Hecks
11
11
  # version, recording a Layer-3 approval, and minting an era's name
12
12
  # once (never recomputed after).
13
13
  module EraStore
14
+ # Lists every era the domain holds, oldest first, refusing any whose text was edited.
15
+ #
14
16
  # Every held text is verified against its raw-byte digest on the
15
17
  # way out — an edited storage fact refuses rather than silently
16
- # reporting "no drift". This is NOT the era name (which hashes
18
+ # reporting "no drift". This is not the era name (which hashes
17
19
  # the canonical projection, minted once, never recomputed): it
18
- # is a plain integrity check over bytes, and a row from before
19
- # the check existed is backfilled, not refused.
20
+ # is a plain integrity check over bytes, and a row that carries
21
+ # no digest yet is backfilled, not refused.
22
+ #
23
+ # @return [Array<Hash{Symbol => Object}>] one Hash per era in ordinal order, `[]` when
24
+ # the domain holds none; keys are `:ordinal` (Integer), `:hash` and `:label` (String,
25
+ # nil until `mint_name!` names the era), `:held_text` (String) and `:watermark`
26
+ # (Integer journal ordinal the era was cut at, nil for era 1)
27
+ # @raise [Runtime::WiringError] if a held text no longer matches its stored digest
20
28
  def eras
21
29
  @db.exec_params(
22
30
  "SELECT ordinal, hash, label, held_text, watermark, held_digest, held_projection::text " \
@@ -29,8 +37,15 @@ module Hecks
29
37
  end
30
38
  end
31
39
 
40
+ # Reads one era's stored row without running the integrity check that `eras` applies.
41
+ #
32
42
  # The unverified row — what bin/reattest_era shows an operator
33
43
  # after the integrity check fires.
44
+ #
45
+ # @param ordinal [Integer] ordinal of the era to read
46
+ # @return [Hash{Symbol => String, nil}, nil] `:held_text`, `:held_digest` (SHA-256 hex,
47
+ # nil if never stored), `:hash` (nil until minted) and `:held_projection` (JSON text,
48
+ # nil if never stored); nil when the domain holds no such era
34
49
  def raw_era(ordinal)
35
50
  rows = @db.exec_params(
36
51
  "SELECT held_text, held_digest, hash, held_projection::text FROM hecks_eras WHERE domain = $1 AND ordinal = $2",
@@ -42,10 +57,17 @@ module Hecks
42
57
  held_projection: rows[0]["held_projection"] }
43
58
  end
44
59
 
45
- # The recovery path: tamper-EVIDENCE (against accident and
60
+ # Accepts an era's held text as it now stands, replacing its stored digest.
61
+ #
62
+ # The recovery path: tamper-evidence (against accident and
46
63
  # drift, not adversaries) gets resolved by a human acknowledging
47
64
  # the text as it now stands — recorded in an append-only
48
- # attestation table, never by a bare psql UPDATE.
65
+ # attestation table, never by a bare psql `UPDATE`.
66
+ #
67
+ # @param ordinal [Integer] ordinal of the era to re-attest
68
+ # @return [String] the new SHA-256 hex digest of the held text
69
+ # @raise [Runtime::WiringError] if the domain holds no era with that ordinal
70
+ # @raise [PG::Error] if Postgres refuses the attestation DDL or a write
49
71
  def reattest!(ordinal)
50
72
  era = raw_era(ordinal)
51
73
  raise Runtime::WiringError, "#{@domain} holds no era #{ordinal} to re-attest" unless era
@@ -72,6 +94,19 @@ module Hecks
72
94
  fresh
73
95
  end
74
96
 
97
+ # Checks one held text against its stored digest, storing the digest if the row has none.
98
+ #
99
+ # A text that passes (or that had no digest to fail against) also gets
100
+ # `backfill_frozen_facts!`; a text that fails gets nothing written.
101
+ #
102
+ # @param ordinal [Integer] ordinal of the era the text belongs to
103
+ # @param text [String] the era's held bluebook source text
104
+ # @param stored_digest [String, nil] SHA-256 hex digest stored with the row; nil when
105
+ # the row carries none yet
106
+ # @param stored_projection_json [String, nil] the row's stored projection as JSON text;
107
+ # nil when the row carries none yet
108
+ # @return [void]
109
+ # @raise [Runtime::WiringError] if `stored_digest` is present and does not match `text`
75
110
  def verify_integrity!(ordinal, text, stored_digest, stored_projection_json)
76
111
  digest = Digest::SHA256.hexdigest(text)
77
112
  if stored_digest.nil?
@@ -90,9 +125,17 @@ module Hecks
90
125
  raise Runtime::WiringError, Runtime::EraTamper.refusal(domain: @domain, ordinal: ordinal)
91
126
  end
92
127
 
93
- # A verified-authentic text backfills what older rows lack: its
128
+ # Fills in the archive copy and the stored projection an era row is missing.
129
+ #
130
+ # A verified-authentic text backfills what a row may lack: its
94
131
  # projection and its archive copy. Never run on a text that
95
132
  # failed verification — that would bless the edit.
133
+ #
134
+ # @param ordinal [Integer] ordinal of the era the text belongs to
135
+ # @param text [String] the era's verified held source text
136
+ # @param stored_projection_json [String, nil] the row's stored projection as JSON text;
137
+ # nil makes this derive one from `text` and store it, unless `text` does not load
138
+ # @return [void]
96
139
  def backfill_frozen_facts!(ordinal, text, stored_projection_json)
97
140
  archive_text!(ordinal, text)
98
141
  return if stored_projection_json
@@ -107,11 +150,27 @@ module Hecks
107
150
  )
108
151
  end
109
152
 
153
+ # Reports the ordinal of the newest era the domain holds.
154
+ #
155
+ # @return [Integer] the highest held ordinal, or 1 when no era is held yet
156
+ # @raise [Runtime::WiringError] if a held text no longer matches its stored digest
110
157
  def current_era
111
158
  held = eras
112
159
  held.empty? ? 1 : held.last[:ordinal]
113
160
  end
114
161
 
162
+ # Holds a domain's first source text as era 1 and makes era 1 the writable era.
163
+ #
164
+ # Call only while the domain holds no era — every caller checks `eras.empty?` first.
165
+ # An existing era 1 row is kept (`ON CONFLICT DO NOTHING`), but `advance_era!(1)` runs
166
+ # regardless, so calling this on a domain past era 1 would roll the write fence back.
167
+ #
168
+ # @param text [String] the bluebook source text to freeze as era 1
169
+ # @param projection [Hash{String => Object}, nil] the storage-shape projection of the
170
+ # bluebook, as `Runtime::StorageShape.project` returns it; nil stores none
171
+ # @return [void]
172
+ # @raise [PG::Error] if Postgres refuses the insert or the policy change, as it does
173
+ # for a role that does not own the journal
115
174
  def hold_first!(text, projection: nil)
116
175
  @db.exec_params(
117
176
  "INSERT INTO hecks_eras (domain, ordinal, held_text, held_digest, held_projection) " \
@@ -124,6 +183,15 @@ module Hecks
124
183
  advance_era!(1)
125
184
  end
126
185
 
186
+ # Copies one version of an era's text into `hecks_era_texts`, keyed by its digest.
187
+ #
188
+ # A version already archived is left alone (`ON CONFLICT DO NOTHING`), so every
189
+ # distinct text an era ever held survives a re-attestation.
190
+ #
191
+ # @param ordinal [Integer] ordinal of the era the text belongs to
192
+ # @param text [String] the source text to archive
193
+ # @return [void]
194
+ # @raise [PG::Error] if Postgres refuses the insert
127
195
  def archive_text!(ordinal, text)
128
196
  @db.exec_params(
129
197
  "INSERT INTO hecks_era_texts (domain, ordinal, digest, held_text) VALUES ($1, $2, $3, $4) " \
@@ -132,10 +200,19 @@ module Hecks
132
200
  )
133
201
  end
134
202
 
135
- # A Layer-3 approval, recorded IN the database it was reviewed
203
+ # Records that a human approved a translation edge's audit samples.
204
+ #
205
+ # A Layer-3 approval, recorded in the database it was reviewed
136
206
  # against — bound to the edge's parsed content and the journal's
137
207
  # high-water ordinal at review time. The latest row for a shape
138
208
  # pair wins (re-approval supersedes).
209
+ #
210
+ # @param from [String] label of the era shape the edge leaves
211
+ # @param to [String] label of the era shape the edge leads to
212
+ # @param edge_digest [String] SHA-256 hex digest of the edge's parsed content, as
213
+ # `Translation::Audit.edge_digest` computes it
214
+ # @return [void]
215
+ # @raise [PG::Error] if Postgres refuses the insert
139
216
  def record_approval!(from:, to:, edge_digest:)
140
217
  @db.exec_params(
141
218
  "INSERT INTO hecks_approvals (domain, from_label, to_label, edge_digest, reviewed_ordinal) " \
@@ -144,6 +221,13 @@ module Hecks
144
221
  )
145
222
  end
146
223
 
224
+ # Finds the most recent approval recorded for the edge between two era shapes.
225
+ #
226
+ # @param from [String] label of the era shape the edge leaves
227
+ # @param to [String] label of the era shape the edge leads to
228
+ # @return [Hash{Symbol => Object}, nil] `:edge_digest` (String, SHA-256 hex of the edge
229
+ # that was reviewed) and `:reviewed_ordinal` (Integer, the journal's highest ordinal at
230
+ # review time); nil when no approval is recorded for the pair
147
231
  def approval_for(from:, to:)
148
232
  rows = @db.exec_params(
149
233
  "SELECT edge_digest, reviewed_ordinal FROM hecks_approvals " \
@@ -155,8 +239,16 @@ module Hecks
155
239
  { edge_digest: rows[0]["edge_digest"], reviewed_ordinal: rows[0]["reviewed_ordinal"].to_i }
156
240
  end
157
241
 
158
- # Era identity is minted ONCE and stored; nothing ever recomputes
242
+ # Stores an era's hash and label, only if the era has no hash yet.
243
+ #
244
+ # Era identity is minted once and stored; nothing ever recomputes
159
245
  # a stored name to verify it.
246
+ #
247
+ # @param ordinal [Integer] ordinal of the era to name
248
+ # @param hash [String] the era's shape hash, SHA-256 hex
249
+ # @param label [String] the short label edges and refusals use, a prefix of `hash`
250
+ # @return [void]
251
+ # @raise [PG::Error] if Postgres refuses the update
160
252
  def mint_name!(ordinal, hash, label)
161
253
  @db.exec_params(
162
254
  "UPDATE hecks_eras SET hash = $3, label = $4, canon_form = $5 " \
@@ -165,6 +257,9 @@ module Hecks
165
257
  )
166
258
  end
167
259
 
260
+ # Reads the journal's high-water mark across every era's partition.
261
+ #
262
+ # @return [Integer] the highest ordinal in the journal, or 0 when the journal is empty
168
263
  def last_ordinal
169
264
  @db.exec("SELECT COALESCE(max(ordinal), 0) AS o FROM #{quoted_journal}")[0]["o"].to_i
170
265
  end