hecks 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +7 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +10 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
  7. data/lib/hecks/adapters/driven/heki/journal.rb +60 -2
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
  9. data/lib/hecks/adapters/driven/heki.rb +13 -7
  10. data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
  12. data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
  13. data/lib/hecks/adapters/driven/lambda.rb +39 -33
  14. data/lib/hecks/adapters/driven/local_storage.rb +17 -10
  15. data/lib/hecks/adapters/driven/memory.rb +205 -9
  16. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
  17. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  18. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  19. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  20. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  21. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  22. data/lib/hecks/adapters/driven/postgres_era.adapter +5 -0
  23. data/lib/hecks/adapters/driven/prism.rb +4 -4
  24. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  25. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  26. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  27. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  28. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  29. data/lib/hecks/adapters/driven.rb +4 -4
  30. data/lib/hecks/adapters/driving/github_webhook.rb +145 -0
  31. data/lib/hecks/behaviors/dsl.rb +2 -2
  32. data/lib/hecks/behaviors/expectations.rb +51 -23
  33. data/lib/hecks/behaviors/ir.rb +1 -1
  34. data/lib/hecks/behaviors/rspec.rb +1 -1
  35. data/lib/hecks/behaviors/runner.rb +2 -2
  36. data/lib/hecks/behaviors.rb +1 -1
  37. data/lib/hecks/bluebook/aggregate.rb +13 -13
  38. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
  39. data/lib/hecks/bluebook/assembly/build.rb +1 -1
  40. data/lib/hecks/bluebook/assembly/contract.rb +39 -16
  41. data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
  42. data/lib/hecks/bluebook/assembly/marks.rb +18 -18
  43. data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
  44. data/lib/hecks/bluebook/assembly.rb +9 -9
  45. data/lib/hecks/bluebook/attribute.rb +9 -9
  46. data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
  47. data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
  48. data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
  49. data/lib/hecks/bluebook/behaviour/command.rb +23 -23
  50. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -3
  51. data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
  52. data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
  53. data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
  54. data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
  55. data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
  56. data/lib/hecks/bluebook/behaviour/query.rb +1 -1
  57. data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
  58. data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
  59. data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
  60. data/lib/hecks/bluebook/capabilities.rb +27 -0
  61. data/lib/hecks/bluebook/chapter.rb +28 -9
  62. data/lib/hecks/bluebook/command.rb +12 -12
  63. data/lib/hecks/bluebook/domain_port.rb +9 -9
  64. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  65. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  66. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
  67. data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
  68. data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
  69. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
  70. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
  71. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  72. data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
  73. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  74. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  75. data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
  76. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  77. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
  78. data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
  79. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  80. data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
  81. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  82. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  83. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
  84. data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
  85. data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
  86. data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
  87. data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
  88. data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
  89. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  90. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  91. data/lib/hecks/bluebook/entity.rb +11 -11
  92. data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
  93. data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
  94. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
  95. data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
  96. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
  97. data/lib/hecks/bluebook/expression/resolver.rb +60 -62
  98. data/lib/hecks/bluebook/hexagon.rb +1 -1
  99. data/lib/hecks/bluebook/lifecycle.rb +1 -1
  100. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
  101. data/lib/hecks/bluebook/meta_validator/judge.rb +122 -86
  102. data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
  103. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
  104. data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
  105. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
  106. data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
  107. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
  108. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
  109. data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
  110. data/lib/hecks/bluebook/meta_validator.rb +70 -70
  111. data/lib/hecks/bluebook/model_check.rb +301 -84
  112. data/lib/hecks/bluebook/pattern_subset.rb +9 -9
  113. data/lib/hecks/bluebook/policy.rb +15 -13
  114. data/lib/hecks/bluebook/process_manager.rb +14 -14
  115. data/lib/hecks/bluebook/project_register.rb +6 -6
  116. data/lib/hecks/bluebook/query.rb +4 -4
  117. data/lib/hecks/bluebook/read_model.rb +14 -14
  118. data/lib/hecks/bluebook/reference.rb +8 -8
  119. data/lib/hecks/bluebook/smoke_test.rb +19 -19
  120. data/lib/hecks/bluebook/synthesizer.rb +12 -12
  121. data/lib/hecks/bluebook/translation.rb +4 -4
  122. data/lib/hecks/bluebook/value_object.rb +6 -6
  123. data/lib/hecks/bluebook.rb +2 -2
  124. data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
  125. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
  126. data/lib/hecks/codemod.rb +36 -35
  127. data/lib/hecks/construct.rb +6 -6
  128. data/lib/hecks/corpus.rb +317 -0
  129. data/lib/hecks/deprecation.rb +95 -0
  130. data/lib/hecks/doc/reference.rb +19 -19
  131. data/lib/hecks/embryonaut_bluebook.rb +11 -11
  132. data/lib/hecks/facade/cli_door.rb +69 -10
  133. data/lib/hecks/facade/cli_runner.rb +105 -24
  134. data/lib/hecks/facade/command_request.rb +23 -0
  135. data/lib/hecks/facade/handle.rb +79 -32
  136. data/lib/hecks/facade/json_door.rb +106 -25
  137. data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
  138. data/lib/hecks/facade/surface/chapter.rb +26 -17
  139. data/lib/hecks/facade/surface.rb +16 -3
  140. data/lib/hecks/facade.rb +15 -4
  141. data/lib/hecks/forms/app.rb +46 -30
  142. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  143. data/lib/hecks/forms/field_renderer.rb +142 -6
  144. data/lib/hecks/forms/field_shape.rb +183 -20
  145. data/lib/hecks/forms/html.rb +51 -7
  146. data/lib/hecks/forms/index_renderer.rb +14 -2
  147. data/lib/hecks/forms/params.rb +120 -23
  148. data/lib/hecks/forms/port_argument.rb +2 -2
  149. data/lib/hecks/forms/query_form_renderer.rb +2 -2
  150. data/lib/hecks/forms/record_renderer.rb +2 -2
  151. data/lib/hecks/forms/record_table.rb +1 -1
  152. data/lib/hecks/forms/value_object_shape.rb +3 -3
  153. data/lib/hecks/forms.rb +24 -4
  154. data/lib/hecks/fqn.rb +1 -1
  155. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  156. data/lib/hecks/framework.rb +48 -17
  157. data/lib/hecks/freezer.rb +11 -11
  158. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
  159. data/lib/hecks/fuzzing/combination_miner.rb +119 -0
  160. data/lib/hecks/fuzzing/concurrent_dispatch.rb +361 -0
  161. data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
  162. data/lib/hecks/fuzzing/differential.rb +158 -0
  163. data/lib/hecks/fuzzing/domain_generator.rb +694 -0
  164. data/lib/hecks/fuzzing/era_boundary.rb +124 -0
  165. data/lib/hecks/fuzzing/form_census.rb +199 -0
  166. data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
  167. data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
  168. data/lib/hecks/fuzzing/isolated_boot.rb +226 -38
  169. data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
  170. data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
  171. data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
  172. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
  173. data/lib/hecks/fuzzing/properties/guards.rb +129 -26
  174. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
  175. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
  176. data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
  177. data/lib/hecks/fuzzing/properties/querying.rb +17 -17
  178. data/lib/hecks/fuzzing/properties.rb +84 -39
  179. data/lib/hecks/fuzzing/qa_settings.rb +152 -0
  180. data/lib/hecks/fuzzing/replay.rb +224 -114
  181. data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
  182. data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
  183. data/lib/hecks/fuzzing/self_consistency.rb +676 -0
  184. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
  189. data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
  190. data/lib/hecks/fuzzing/shrinker.rb +197 -0
  191. data/lib/hecks/fuzzing/structural_skips.rb +39 -0
  192. data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
  193. data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
  194. data/lib/hecks/fuzzing/value_generator.rb +66 -14
  195. data/lib/hecks/fuzzing.rb +11 -0
  196. data/lib/hecks/grammar/evolve.rb +10 -10
  197. data/lib/hecks/grammar.rb +7 -7
  198. data/lib/hecks/ir.rb +13 -13
  199. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  200. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  201. data/lib/hecks/language/bluebook/vocabulary.bluebook +365 -15
  202. data/lib/hecks/language/oidc.json +5 -0
  203. data/lib/hecks/literal.rb +9 -9
  204. data/lib/hecks/naming.rb +89 -21
  205. data/lib/hecks/ports/access_control.rb +58 -2
  206. data/lib/hecks/ports/agent/answers.rb +83 -6
  207. data/lib/hecks/ports/agent.rb +119 -35
  208. data/lib/hecks/ports/authentication.rb +44 -4
  209. data/lib/hecks/ports/authorization.rb +53 -11
  210. data/lib/hecks/ports/clock.rb +42 -23
  211. data/lib/hecks/ports/extraction.rb +16 -0
  212. data/lib/hecks/ports/identity_assignment.rb +24 -2
  213. data/lib/hecks/ports/identity_generation.rb +17 -3
  214. data/lib/hecks/ports/identity_resolution.rb +18 -1
  215. data/lib/hecks/ports/loading.rb +4 -0
  216. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  217. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  218. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  219. data/lib/hecks/ports/persistence/execution.rb +4 -0
  220. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  221. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  222. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  223. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  224. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  225. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  226. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  227. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  228. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  229. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  230. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  231. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +160 -58
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +44 -6
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +338 -83
  242. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  243. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  244. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  245. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
  246. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  247. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  248. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  249. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
  250. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
  251. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
  252. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  253. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  254. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  255. data/lib/hecks/ports/persistence/repository_factory.rb +29 -6
  256. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  257. data/lib/hecks/ports/persistence.rb +36 -1
  258. data/lib/hecks/ports/projection.rb +61 -7
  259. data/lib/hecks/ports/query/in_memory.rb +3 -3
  260. data/lib/hecks/ports/query/ordering.rb +6 -6
  261. data/lib/hecks/ports/query.rb +35 -0
  262. data/lib/hecks/projections/bootstrap_table.rb +112 -0
  263. data/lib/hecks/projections/diagrams.rb +75 -75
  264. data/lib/hecks/projections/glossary/html.rb +250 -0
  265. data/lib/hecks/projections/glossary/markdown.rb +105 -0
  266. data/lib/hecks/projections/glossary/mermaid.rb +110 -0
  267. data/lib/hecks/projections/glossary/page.css +271 -0
  268. data/lib/hecks/projections/glossary/page.js +72 -0
  269. data/lib/hecks/projections/glossary/sections.rb +17 -0
  270. data/lib/hecks/projections/glossary/sentences.rb +205 -0
  271. data/lib/hecks/projections/glossary.rb +214 -286
  272. data/lib/hecks/projections/ir.rb +1 -1
  273. data/lib/hecks/projections/model/deviations.rb +18 -17
  274. data/lib/hecks/projections/model.rb +25 -21
  275. data/lib/hecks/projections/oidc.rb +7 -7
  276. data/lib/hecks/projections/parser_table.rb +5 -5
  277. data/lib/hecks/projections/reference.rb +3 -3
  278. data/lib/hecks/projections/rust_vocabulary.rb +443 -0
  279. data/lib/hecks/projections/shape.rb +2 -2
  280. data/lib/hecks/projections/statements.rb +11 -11
  281. data/lib/hecks/projections/vocabulary.rb +9 -9
  282. data/lib/hecks/projections.rb +5 -3
  283. data/lib/hecks/projector/cli_projector.rb +29 -29
  284. data/lib/hecks/projector/docs_projector.rb +13 -13
  285. data/lib/hecks/projector/exporter.rb +42 -21
  286. data/lib/hecks/projector/ir_projector.rb +1 -1
  287. data/lib/hecks/projector/narrate_projector.rb +15 -22
  288. data/lib/hecks/projector/target.rb +13 -13
  289. data/lib/hecks/projector.rb +15 -15
  290. data/lib/hecks/query_ir.rb +47 -47
  291. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  292. data/lib/hecks/query_specification/common/comparison.rb +132 -24
  293. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  294. data/lib/hecks/query_specification/common/null_policy.rb +57 -13
  295. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  296. data/lib/hecks/query_specification/common/options.rb +25 -0
  297. data/lib/hecks/query_specification/field_path.rb +69 -15
  298. data/lib/hecks/query_specification/hop_path.rb +57 -20
  299. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  300. data/lib/hecks/rendering.rb +3 -3
  301. data/lib/hecks/router/namespace_installer.rb +3 -3
  302. data/lib/hecks/router.rb +1 -1
  303. data/lib/hecks/runtime/aggregate_lock.rb +11 -11
  304. data/lib/hecks/runtime/boot_gates.rb +3 -3
  305. data/lib/hecks/runtime/caller.rb +8 -8
  306. data/lib/hecks/runtime/capability_graph.rb +2 -2
  307. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
  308. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +64 -67
  309. data/lib/hecks/runtime/command_interpreter.rb +159 -102
  310. data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
  311. data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
  312. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  313. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  314. data/lib/hecks/runtime/command_rules/references.rb +94 -19
  315. data/lib/hecks/runtime/command_rules.rb +3 -0
  316. data/lib/hecks/runtime/dependency_planning.rb +11 -11
  317. data/lib/hecks/runtime/dispatcher.rb +283 -90
  318. data/lib/hecks/runtime/entity_element.rb +282 -46
  319. data/lib/hecks/runtime/entity_interpreter.rb +160 -69
  320. data/lib/hecks/runtime/errors.rb +19 -19
  321. data/lib/hecks/runtime/event.rb +6 -6
  322. data/lib/hecks/runtime/identity.rb +22 -22
  323. data/lib/hecks/runtime/instance.rb +39 -14
  324. data/lib/hecks/runtime/interpreting.rb +12 -12
  325. data/lib/hecks/runtime/invocation.rb +276 -0
  326. data/lib/hecks/runtime/loader.rb +14 -14
  327. data/lib/hecks/runtime/outbox.rb +23 -23
  328. data/lib/hecks/runtime/policy_interpreter.rb +54 -54
  329. data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
  330. data/lib/hecks/runtime/query_interpreter.rb +111 -56
  331. data/lib/hecks/runtime/reaction_invocation.rb +76 -9
  332. data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
  333. data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
  334. data/lib/hecks/runtime/reference_hop.rb +6 -6
  335. data/lib/hecks/runtime/refusal_wording.rb +92 -112
  336. data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
  337. data/lib/hecks/runtime/registry/verification.rb +36 -26
  338. data/lib/hecks/runtime/registry.rb +56 -27
  339. data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
  340. data/lib/hecks/runtime/routing.rb +10 -88
  341. data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
  342. data/lib/hecks/runtime/saga_interpreter.rb +93 -56
  343. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  344. data/lib/hecks/runtime/tenant_check.rb +9 -9
  345. data/lib/hecks/runtime/tenant_scope.rb +5 -5
  346. data/lib/hecks/runtime/value/admission.rb +75 -30
  347. data/lib/hecks/runtime/value/coercion.rb +379 -226
  348. data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
  349. data/lib/hecks/runtime/value.rb +28 -23
  350. data/lib/hecks/runtime.rb +7 -7
  351. data/lib/hecks/storehouse.rb +64 -64
  352. data/lib/hecks/version.rb +3 -3
  353. data/lib/hecks/vocabulary.rb +207 -5
  354. data/lib/hecks.rb +13 -11
  355. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
  356. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
  357. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
  358. metadata +43 -2
@@ -16,24 +16,34 @@ module Hecks
16
16
  # historical bluebook text under old grammar defaults. Both are real,
17
17
  # independent, currently-shipped consumers.
18
18
  #
19
- # A prior version of this module ALSO drove its own top-level check
20
- # `check!`/`check_bluebook!`, walking a registry and reading/writing a
21
- # held snapshot under `data/eras/*.bluebook` duplicating, on its own,
22
- # the same per-aggregate walk `CoverageCheck` already performs against
23
- # `PostgresEra`'s own DB-held shapes. Nothing in production ever called
24
- # it (only a direct unit spec did); deleted rather than kept unwired,
25
- # per ADR 0032. Wanted again, it's rebuilt informed by `CoverageCheck`'s
26
- # real orchestration, not resurrected from here.
19
+ # There is deliberately no top-level `check!`/`check_bluebook!` here
20
+ # walking a registry and reading/writing a held snapshot under
21
+ # `data/eras/*.bluebook`: it would duplicate, on its own, the same
22
+ # per-aggregate walk `CoverageCheck` already performs against
23
+ # `PostgresEra`'s own DB-held shapes, and nothing in production would
24
+ # call it an unwired driver is not kept, per ADR 0032. If one is
25
+ # wanted, it is built informed by `CoverageCheck`'s real orchestration.
27
26
  module EraGuard
28
27
  extend ShapeDiff
29
28
 
30
29
  module_function
31
30
 
31
+ # Refuses the boot when a held aggregate is gone and no translation says where it went.
32
+ #
32
33
  # An aggregate that existed in the held text and answers to no
33
34
  # current name — renamed silently, with nothing declaring `was:` to
34
35
  # explain where its data went — is exactly the disease this guards
35
36
  # against, and a plain per-aggregate diff would never see it: the
36
37
  # current aggregate simply has no held counterpart to compare to.
38
+ #
39
+ # @param registry [Runtime::Registry] the registry whose declared translations are
40
+ # searched for a `was:` or a `retired` entry naming the held aggregate
41
+ # @param bluebook [Bluebook::Chapter] the bluebook booting now
42
+ # @param held_bluebook [Bluebook::Chapter] the bluebook parsed from the held era's text
43
+ # @return [void]
44
+ # @raise [Runtime::WiringError] if a held aggregate matches no current aggregate by
45
+ # name, no current aggregate's translation declares it as `was:`, and no translation
46
+ # for this domain retires it
37
47
  def check_vanished_aggregates!(registry, bluebook, held_bluebook)
38
48
  held_bluebook.aggregates.each do |held_aggregate|
39
49
  claimed = bluebook.aggregates.any? do |aggregate|
@@ -54,9 +64,18 @@ module Hecks
54
64
  end
55
65
  end
56
66
 
67
+ # Raises the refusal naming every changed path no translation rule explains.
68
+ #
57
69
  # The Layer-1 coverage refusal — one wording, shared with whoever
58
70
  # calls it (today, `PostgresEra::LineageManager::CoverageCheck`'s
59
71
  # own mint-time coverage check).
72
+ #
73
+ # @param bluebook [Bluebook::Chapter] the bluebook booting now, named in the message
74
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose shape changed
75
+ # @param uncovered [Array<String>] the unexplained paths, as `uncovered_attributes`
76
+ # returns them; must not be empty, since the first one seeds the suggested rule
77
+ # @return [void] never returns; always raises
78
+ # @raise [Runtime::WiringError] always, carrying the refusal wording
60
79
  def refuse_uncovered!(bluebook, aggregate, uncovered)
61
80
  raise WiringError,
62
81
  "cannot boot #{bluebook.name}::#{aggregate.name}: its shape changed and " \
@@ -65,9 +84,18 @@ module Hecks
65
84
  "#{suggestion(uncovered.first)}."
66
85
  end
67
86
 
87
+ # Raises the refusal naming every new required attribute an existing record cannot fill.
88
+ #
68
89
  # The addition-side sibling of refuse_uncovered! above — same
69
90
  # wording shape, different cause: nothing vanished or changed type,
70
91
  # something new arrived that an existing record has no way to hold.
92
+ #
93
+ # @param bluebook [Bluebook::Chapter] the bluebook booting now, named in the message
94
+ # @param aggregate [Bluebook::Aggregate] the aggregate that gained the attributes
95
+ # @param unsafe [Array<Symbol>] the attribute names, as `unsafe_additions` returns them;
96
+ # must not be empty, since the first one seeds the suggested `backfill`
97
+ # @return [void] never returns; always raises
98
+ # @raise [Runtime::WiringError] always, carrying the refusal wording
71
99
  def refuse_unsafe_addition!(bluebook, aggregate, unsafe)
72
100
  raise WiringError,
73
101
  "cannot boot #{bluebook.name}::#{aggregate.name}: #{unsafe.map { |name| ":#{name}" }.join(', ')} " \
@@ -77,8 +105,18 @@ module Hecks
77
105
  "`backfill :#{unsafe.first}, default: ...`."
78
106
  end
79
107
 
108
+ # Renders a path the way a translation file spells it.
109
+ #
110
+ # @param path [String] a bare attribute name or a dotted value-object member path
111
+ # @return [String] the path quoted (`"price.currency"`) when dotted, otherwise as a
112
+ # Symbol literal (`:cost`)
80
113
  def render_path(path) = path.include?(".") ? path.inspect : ":#{path}"
81
114
 
115
+ # Proposes the translation rules that would explain one uncovered path.
116
+ #
117
+ # @param path [String] a bare attribute name or a dotted value-object member path
118
+ # @return [String] backticked example rules: `move`/`drop` for a dotted path,
119
+ # `rename`/`drop` for a bare name
82
120
  def suggestion(path)
83
121
  if path.include?(".")
84
122
  "`move #{path.inspect}, to: #{path.inspect}` or `drop #{path.inspect}`"
@@ -90,60 +128,79 @@ module Hecks
90
128
  # Parses held source into its own IR, in a scratch registry so a past
91
129
  # era's text never touches the one actually booting.
92
130
  #
93
- # NORMAL PARSE FIRST, shadow only as a FALLBACKnot shadow-parsing
94
- # unconditionally, which is what this used to do. A handful of DSL
131
+ # Normal parse first, shadow only as a fallbacknever shadow-parsing
132
+ # unconditionally. A handful of DSL
95
133
  # defaults fork on `MetaValidator.shadow_parsing?` for a reason
96
- # that has NOTHING to do with syntax the live grammar can no longer
134
+ # that has nothing to do with syntax the live grammar can no longer
97
135
  # read at all (`identified_by { }`, `belongs_to`, `has_one`,
98
136
  # `has_many` — genuinely removed spellings, exactly what shadow-
99
137
  # parsing exists to keep readable): `reference_to`'s own default
100
138
  # mint name (`default_reference_name`, attribute_collector.rb)
101
- # changed from `_id`-suffixed to bare under ADR 0025, and THAT fork
102
- # applies even to text using nothing but current, live syntax.
103
- # Held text minted under the CURRENT grammar — every real era in
104
- # this corpus today, since nothing has ever minted a second one —
105
- # parses fine normally; only the reference-naming DEFAULT differed
106
- # once shadow mode engaged unconditionally, so it silently
107
- # reconstructed a DIFFERENT shape (and hash) than a fresh parse of
139
+ # is bare under ADR 0025 and `_id`-suffixed under shadow mode, and
140
+ # that fork applies even to text using nothing but current, live
141
+ # syntax. Held text minted under the current grammar — every real
142
+ # era in this corpus today, since nothing has ever minted a second
143
+ # one — parses fine normally; under unconditional shadow mode only
144
+ # the reference-naming default differs, so it silently
145
+ # reconstructs a different shape (and hash) than a fresh parse of
108
146
  # the identical text — the same text hashing two different ways
109
147
  # depending on which code path read it, breaking `ensure_named!`'s
110
148
  # own from/to edge lookup with a spurious "no translation edge
111
149
  # covers it" refusal that has nothing to do with any real
112
150
  # translation gap.
113
151
  #
114
- # A normal parse can only ever SUCCEED on text the live grammar
152
+ # A normal parse can only ever succeed on text the live grammar
115
153
  # fully understands — there is no way for it to silently produce a
116
154
  # wrong-but-plausible answer for genuinely legacy text, since every
117
155
  # removed spelling refuses loudly (`Malformed`) rather than
118
156
  # degrading. So: try normal first — if the ordinary grammar reads
119
- # this text without complaint, that IS the canonical, unambiguous
157
+ # this text without complaint, that is the canonical, unambiguous
120
158
  # interpretation, the same one `label_of`/`mint_hash` on the same
121
159
  # source text always computes, whoever's asking. Only on a
122
160
  # `Malformed` refusal — the one signal that actually means "this
123
161
  # spelling doesn't exist anymore" — fall back to the legacy
124
- # grammar, exactly as before this change. Any OTHER exception (a
162
+ # grammar. Any other exception (a
125
163
  # genuine syntax error, an unrelated validation refusal) propagates
126
164
  # unchanged; swallowing it here to retry under shadow mode would
127
165
  # risk masking a real defect in the held text behind a confusing
128
166
  # second failure instead of the original, more specific one.
129
167
  #
130
168
  # `MetaValidator.while_shadow_parsing` (ADR 0025, docs/dsl-work-
131
- # slices.md's S0a) is what makes the fallback a LEGACY grammar
169
+ # slices.md's S0a) is what makes the fallback a legacy grammar
132
170
  # rather than just a second copy of today's: it stops
133
171
  # `BluebookBuilder.build` from judging this text against the
134
- # grammar as it stands NOW, which is the one thing that would make
135
- # a removed spelling refuse HISTORY the day it is removed from
172
+ # grammar as it stands now, which is the one thing that would make
173
+ # a removed spelling refuse history the day it is removed from
136
174
  # live source. The scratch registry is throwaway either way —
137
175
  # nothing here is dispatched against or exposed to the real one —
138
176
  # so skipping the judge/assemble round-trip changes nothing this
139
177
  # method reads: `shape`, `uncovered_attributes`, and friends only
140
178
  # ever ask the built IR for its own structure.
179
+ #
180
+ # @param source [String] the held bluebook text to evaluate
181
+ # @param path [String] the file path the text is evaluated as, which the predicate
182
+ # extractor reads from disk; callers pass a tempfile holding the same text
183
+ # @return [Bluebook::Chapter, nil] the first bluebook the text declares; nil when it
184
+ # declares none
185
+ # @raise [Bluebook::DSL::Malformed] if the text parses under neither the live grammar
186
+ # nor the legacy one
187
+ # @raise [SyntaxError] if the text is not valid Ruby
141
188
  def shadow_parse(source, path)
142
189
  parse_bluebook(source, path, shadow: false)
143
190
  rescue Hecks::Bluebook::DSL::Malformed
144
191
  parse_bluebook(source, path, shadow: true)
145
192
  end
146
193
 
194
+ # Evaluates bluebook text once, in a throwaway registry, under one chosen grammar.
195
+ #
196
+ # @param source [String] the bluebook text to evaluate
197
+ # @param path [String] the file path reported to `Kernel.eval` as the text's origin
198
+ # @param shadow [Boolean] true evaluates inside `MetaValidator.while_shadow_parsing`
199
+ # (the legacy grammar); false evaluates under the live grammar
200
+ # @return [Bluebook::Chapter, nil] the first bluebook the text registered in the scratch
201
+ # registry; nil when it declares none
202
+ # @raise [Bluebook::DSL::Malformed] if the chosen grammar refuses the text
203
+ # @raise [SyntaxError] if the text is not valid Ruby
147
204
  def parse_bluebook(source, path, shadow:)
148
205
  scratch = Registry.new
149
206
  loading = Ports::Loading.bootstrap
@@ -6,41 +6,52 @@ require_relative "storage_shape"
6
6
  module Hecks
7
7
  module Runtime
8
8
  # The refusal wording for a held era whose digest no longer matches its
9
- # frozen text — the digest mismatch alone is what DETECTS tampering (a
9
+ # frozen text — the digest mismatch alone is what detects tampering (a
10
10
  # plain SHA256 comparison over raw bytes, unrelated to any of this);
11
11
  # this only supplies the wording once that's already fired.
12
12
  #
13
- # LIVES HERE, NOT UNDER `Ports::Persistence` — `project` below directly
13
+ # Lives here, not under `Ports::Persistence` — `project` below directly
14
14
  # calls `Runtime::EraGuard.shadow_parse`/`Runtime::StorageShape.project`
15
15
  # (DSL-execution machinery), the "an adapter/port reaches into the
16
16
  # runtime instead of being handed already-computed data" shape that
17
17
  # has caused trouble here before. A capability this dependent on the
18
- # runtime is a runtime-owned one that the Postgres adapter CALLS, not
18
+ # runtime is a runtime-owned one that the Postgres adapter calls, not
19
19
  # a ports-level module that happens to reach sideways into it.
20
20
  #
21
- # Used to also distinguish a cosmetic edit from a real shape change
22
- # here, by re-parsing the edited text a pure quality-of-message
23
- # nicety, not a safety property, since dropped: every tamper refusal
24
- # reaches the same generic wording now, the one this already fell to
25
- # whenever it couldn't classify an edit anyway. An operator judges
26
- # "did this matter" themselves,
27
- # reading the still-archived original an anomalous recovery moment
28
- # already, not a normal boot path.
21
+ # Every tamper refusal reaches the same generic wording, cosmetic edit
22
+ # or real shape change alike: telling the two apart by re-parsing the
23
+ # edited text is a pure quality-of-message nicety, not a safety
24
+ # property, and an edit that cannot be classified has to fall to the
25
+ # generic wording anyway. An operator judges "did this matter"
26
+ # themselves, reading the still-archived original — an anomalous
27
+ # recovery moment already, not a normal boot path.
29
28
  module EraTamper
30
29
  module_function
31
30
 
31
+ # Words the boot refusal for a held era text whose digest no longer matches.
32
+ #
33
+ # @param domain [String] name of the domain whose era was edited
34
+ # @param ordinal [Integer] the edited era's ordinal in `hecks_eras`
35
+ # @return [String] the refusal message, ready to raise as a `Runtime::WiringError`
32
36
  def refusal(domain:, ordinal:)
33
37
  "cannot boot #{domain}: the held text of era #{ordinal} was edited after it was frozen — " \
34
38
  "held era texts are storage facts; restore the original text, or reset the data"
35
39
  end
36
40
 
37
- # The storage-shape projection of a bluebook text, JSON-normalized
38
- # for structural comparison against a stored projection; nil when
39
- # the text does not load. ALWAYS parsed through a fresh tempfile,
40
- # never the held file's own path: the predicate extractor caches
41
- # source by path, and a held path whose content has changed (the
42
- # very situation this module exists for) would hand it stale
43
- # lines.
41
+ # Parses a bluebook text and returns its storage-shape projection.
42
+ #
43
+ # The projection is JSON-normalized for structural comparison
44
+ # against a stored projection; nil when the text does not load.
45
+ # Always parsed through a fresh tempfile, never the held file's own
46
+ # path: the predicate extractor caches source by path, and a held
47
+ # path whose content has changed (the very situation this module
48
+ # exists for) would hand it stale lines.
49
+ #
50
+ # @param text [String] the bluebook source to parse
51
+ # @param _source_path [String, nil] ignored; the text is always parsed from a tempfile
52
+ # @return [Hash{String => Object}, nil] `StorageShape.project`'s Hash after a JSON
53
+ # round-trip; nil when the text declares no bluebook, or when parsing or projecting
54
+ # raises any `StandardError` or `SyntaxError`
44
55
  def project(text, _source_path = nil)
45
56
  file = Tempfile.new(["hecks-tamper-", ".bluebook"])
46
57
  begin
@@ -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