hecks 1.3.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 (355) 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 +5 -4
  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/prism.rb +4 -4
  23. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  24. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  25. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  26. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  27. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  28. data/lib/hecks/adapters/driven.rb +4 -4
  29. data/lib/hecks/adapters/driving/github_webhook.rb +18 -18
  30. data/lib/hecks/behaviors/dsl.rb +2 -2
  31. data/lib/hecks/behaviors/expectations.rb +23 -23
  32. data/lib/hecks/behaviors/ir.rb +1 -1
  33. data/lib/hecks/behaviors/rspec.rb +1 -1
  34. data/lib/hecks/behaviors/runner.rb +2 -2
  35. data/lib/hecks/behaviors.rb +1 -1
  36. data/lib/hecks/bluebook/aggregate.rb +13 -13
  37. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
  38. data/lib/hecks/bluebook/assembly/build.rb +1 -1
  39. data/lib/hecks/bluebook/assembly/contract.rb +39 -16
  40. data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
  41. data/lib/hecks/bluebook/assembly/marks.rb +18 -18
  42. data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
  43. data/lib/hecks/bluebook/assembly.rb +9 -9
  44. data/lib/hecks/bluebook/attribute.rb +9 -9
  45. data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
  46. data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
  47. data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
  48. data/lib/hecks/bluebook/behaviour/command.rb +23 -23
  49. data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -7
  50. data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
  51. data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
  52. data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
  53. data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
  54. data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
  55. data/lib/hecks/bluebook/behaviour/query.rb +1 -1
  56. data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
  57. data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
  58. data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
  59. data/lib/hecks/bluebook/capabilities.rb +27 -0
  60. data/lib/hecks/bluebook/chapter.rb +28 -9
  61. data/lib/hecks/bluebook/command.rb +12 -12
  62. data/lib/hecks/bluebook/domain_port.rb +9 -9
  63. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  64. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  65. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
  66. data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
  67. data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
  68. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
  69. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
  70. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  71. data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
  72. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  73. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  74. data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
  75. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  76. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
  77. data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
  78. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  79. data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
  80. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  81. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  82. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
  83. data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
  84. data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
  85. data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
  86. data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
  87. data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
  88. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  89. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  90. data/lib/hecks/bluebook/entity.rb +11 -11
  91. data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
  92. data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
  93. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
  94. data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
  95. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
  96. data/lib/hecks/bluebook/expression/resolver.rb +60 -62
  97. data/lib/hecks/bluebook/hexagon.rb +1 -1
  98. data/lib/hecks/bluebook/lifecycle.rb +1 -1
  99. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
  100. data/lib/hecks/bluebook/meta_validator/judge.rb +101 -87
  101. data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
  102. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
  103. data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
  104. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
  105. data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
  106. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
  107. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
  108. data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
  109. data/lib/hecks/bluebook/meta_validator.rb +70 -70
  110. data/lib/hecks/bluebook/model_check.rb +170 -84
  111. data/lib/hecks/bluebook/pattern_subset.rb +9 -9
  112. data/lib/hecks/bluebook/policy.rb +15 -13
  113. data/lib/hecks/bluebook/process_manager.rb +14 -14
  114. data/lib/hecks/bluebook/project_register.rb +6 -6
  115. data/lib/hecks/bluebook/query.rb +4 -4
  116. data/lib/hecks/bluebook/read_model.rb +14 -14
  117. data/lib/hecks/bluebook/reference.rb +8 -8
  118. data/lib/hecks/bluebook/smoke_test.rb +19 -19
  119. data/lib/hecks/bluebook/synthesizer.rb +12 -12
  120. data/lib/hecks/bluebook/translation.rb +4 -4
  121. data/lib/hecks/bluebook/value_object.rb +6 -6
  122. data/lib/hecks/bluebook.rb +2 -2
  123. data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
  124. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
  125. data/lib/hecks/codemod.rb +36 -35
  126. data/lib/hecks/construct.rb +6 -6
  127. data/lib/hecks/corpus.rb +317 -0
  128. data/lib/hecks/deprecation.rb +95 -0
  129. data/lib/hecks/doc/reference.rb +19 -19
  130. data/lib/hecks/embryonaut_bluebook.rb +11 -11
  131. data/lib/hecks/facade/cli_door.rb +69 -10
  132. data/lib/hecks/facade/cli_runner.rb +105 -24
  133. data/lib/hecks/facade/command_request.rb +23 -0
  134. data/lib/hecks/facade/handle.rb +79 -32
  135. data/lib/hecks/facade/json_door.rb +106 -25
  136. data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
  137. data/lib/hecks/facade/surface/chapter.rb +26 -17
  138. data/lib/hecks/facade/surface.rb +16 -3
  139. data/lib/hecks/facade.rb +15 -4
  140. data/lib/hecks/forms/app.rb +46 -30
  141. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  142. data/lib/hecks/forms/field_renderer.rb +142 -6
  143. data/lib/hecks/forms/field_shape.rb +180 -19
  144. data/lib/hecks/forms/html.rb +51 -7
  145. data/lib/hecks/forms/index_renderer.rb +14 -2
  146. data/lib/hecks/forms/params.rb +120 -23
  147. data/lib/hecks/forms/port_argument.rb +2 -2
  148. data/lib/hecks/forms/query_form_renderer.rb +2 -2
  149. data/lib/hecks/forms/record_renderer.rb +2 -2
  150. data/lib/hecks/forms/record_table.rb +1 -1
  151. data/lib/hecks/forms/value_object_shape.rb +3 -3
  152. data/lib/hecks/forms.rb +24 -4
  153. data/lib/hecks/fqn.rb +1 -1
  154. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  155. data/lib/hecks/framework.rb +48 -17
  156. data/lib/hecks/freezer.rb +11 -11
  157. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
  158. data/lib/hecks/fuzzing/combination_miner.rb +119 -0
  159. data/lib/hecks/fuzzing/concurrent_dispatch.rb +140 -45
  160. data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
  161. data/lib/hecks/fuzzing/differential.rb +158 -0
  162. data/lib/hecks/fuzzing/domain_generator.rb +694 -0
  163. data/lib/hecks/fuzzing/era_boundary.rb +37 -18
  164. data/lib/hecks/fuzzing/form_census.rb +35 -20
  165. data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
  166. data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
  167. data/lib/hecks/fuzzing/isolated_boot.rb +62 -62
  168. data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
  169. data/lib/hecks/fuzzing/persistence_parity.rb +19 -21
  170. data/lib/hecks/fuzzing/properties/corrections.rb +9 -9
  171. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +154 -49
  172. data/lib/hecks/fuzzing/properties/guards.rb +42 -42
  173. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
  174. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +22 -35
  175. data/lib/hecks/fuzzing/properties/outbox.rb +24 -24
  176. data/lib/hecks/fuzzing/properties/querying.rb +17 -17
  177. data/lib/hecks/fuzzing/properties.rb +64 -40
  178. data/lib/hecks/fuzzing/qa_settings.rb +152 -0
  179. data/lib/hecks/fuzzing/replay.rb +114 -114
  180. data/lib/hecks/fuzzing/rotation_priority.rb +10 -10
  181. data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
  182. data/lib/hecks/fuzzing/self_consistency.rb +141 -112
  183. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +35 -35
  184. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  185. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  186. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  187. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +58 -31
  188. data/lib/hecks/fuzzing/sequence_generator.rb +111 -23
  189. data/lib/hecks/fuzzing/shrinker.rb +197 -0
  190. data/lib/hecks/fuzzing/structural_skips.rb +23 -130
  191. data/lib/hecks/fuzzing/sweep_depth.rb +4 -4
  192. data/lib/hecks/fuzzing/target_capabilities.rb +49 -18
  193. data/lib/hecks/fuzzing/value_generator.rb +14 -14
  194. data/lib/hecks/fuzzing.rb +5 -0
  195. data/lib/hecks/grammar/evolve.rb +10 -10
  196. data/lib/hecks/grammar.rb +7 -7
  197. data/lib/hecks/ir.rb +13 -13
  198. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  199. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  200. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  201. data/lib/hecks/language/oidc.json +5 -0
  202. data/lib/hecks/literal.rb +9 -9
  203. data/lib/hecks/naming.rb +25 -25
  204. data/lib/hecks/ports/access_control.rb +58 -2
  205. data/lib/hecks/ports/agent/answers.rb +83 -6
  206. data/lib/hecks/ports/agent.rb +119 -35
  207. data/lib/hecks/ports/authentication.rb +44 -4
  208. data/lib/hecks/ports/authorization.rb +53 -11
  209. data/lib/hecks/ports/clock.rb +42 -23
  210. data/lib/hecks/ports/extraction.rb +16 -0
  211. data/lib/hecks/ports/identity_assignment.rb +24 -2
  212. data/lib/hecks/ports/identity_generation.rb +17 -3
  213. data/lib/hecks/ports/identity_resolution.rb +18 -1
  214. data/lib/hecks/ports/loading.rb +4 -0
  215. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  216. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  217. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  218. data/lib/hecks/ports/persistence/execution.rb +4 -0
  219. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  220. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  221. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  222. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  223. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  224. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  225. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  226. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  227. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  228. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  229. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  230. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  231. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  241. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  242. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  243. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  244. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
  245. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  246. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  247. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  248. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
  249. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
  250. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
  251. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  252. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  253. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  254. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  255. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  256. data/lib/hecks/ports/persistence.rb +36 -1
  257. data/lib/hecks/ports/projection.rb +61 -7
  258. data/lib/hecks/ports/query/in_memory.rb +3 -3
  259. data/lib/hecks/ports/query/ordering.rb +6 -6
  260. data/lib/hecks/ports/query.rb +35 -0
  261. data/lib/hecks/projections/bootstrap_table.rb +112 -0
  262. data/lib/hecks/projections/diagrams.rb +75 -75
  263. data/lib/hecks/projections/glossary/html.rb +3 -3
  264. data/lib/hecks/projections/glossary/markdown.rb +3 -3
  265. data/lib/hecks/projections/glossary/mermaid.rb +1 -1
  266. data/lib/hecks/projections/glossary/sections.rb +1 -1
  267. data/lib/hecks/projections/glossary/sentences.rb +5 -5
  268. data/lib/hecks/projections/glossary.rb +15 -15
  269. data/lib/hecks/projections/ir.rb +1 -1
  270. data/lib/hecks/projections/model/deviations.rb +18 -17
  271. data/lib/hecks/projections/model.rb +25 -21
  272. data/lib/hecks/projections/oidc.rb +7 -7
  273. data/lib/hecks/projections/parser_table.rb +5 -5
  274. data/lib/hecks/projections/reference.rb +3 -3
  275. data/lib/hecks/projections/rust_vocabulary.rb +443 -0
  276. data/lib/hecks/projections/shape.rb +2 -2
  277. data/lib/hecks/projections/statements.rb +11 -11
  278. data/lib/hecks/projections/vocabulary.rb +9 -9
  279. data/lib/hecks/projections.rb +5 -3
  280. data/lib/hecks/projector/cli_projector.rb +29 -29
  281. data/lib/hecks/projector/docs_projector.rb +13 -13
  282. data/lib/hecks/projector/exporter.rb +42 -21
  283. data/lib/hecks/projector/ir_projector.rb +1 -1
  284. data/lib/hecks/projector/narrate_projector.rb +11 -11
  285. data/lib/hecks/projector/target.rb +13 -13
  286. data/lib/hecks/projector.rb +15 -15
  287. data/lib/hecks/query_ir.rb +47 -47
  288. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  289. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  290. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  291. data/lib/hecks/query_specification/common/null_policy.rb +57 -13
  292. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  293. data/lib/hecks/query_specification/common/options.rb +25 -0
  294. data/lib/hecks/query_specification/field_path.rb +69 -15
  295. data/lib/hecks/query_specification/hop_path.rb +57 -20
  296. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  297. data/lib/hecks/rendering.rb +3 -3
  298. data/lib/hecks/router/namespace_installer.rb +3 -3
  299. data/lib/hecks/router.rb +1 -1
  300. data/lib/hecks/runtime/aggregate_lock.rb +11 -11
  301. data/lib/hecks/runtime/boot_gates.rb +3 -3
  302. data/lib/hecks/runtime/caller.rb +8 -8
  303. data/lib/hecks/runtime/capability_graph.rb +2 -2
  304. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
  305. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +42 -63
  306. data/lib/hecks/runtime/command_interpreter.rb +159 -102
  307. data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
  308. data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
  309. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  310. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  311. data/lib/hecks/runtime/command_rules/references.rb +35 -35
  312. data/lib/hecks/runtime/command_rules.rb +3 -0
  313. data/lib/hecks/runtime/dependency_planning.rb +11 -11
  314. data/lib/hecks/runtime/dispatcher.rb +283 -90
  315. data/lib/hecks/runtime/entity_element.rb +151 -67
  316. data/lib/hecks/runtime/entity_interpreter.rb +108 -82
  317. data/lib/hecks/runtime/errors.rb +19 -19
  318. data/lib/hecks/runtime/event.rb +6 -6
  319. data/lib/hecks/runtime/identity.rb +22 -22
  320. data/lib/hecks/runtime/instance.rb +39 -14
  321. data/lib/hecks/runtime/interpreting.rb +12 -12
  322. data/lib/hecks/runtime/invocation.rb +276 -0
  323. data/lib/hecks/runtime/loader.rb +14 -14
  324. data/lib/hecks/runtime/outbox.rb +23 -23
  325. data/lib/hecks/runtime/policy_interpreter.rb +54 -54
  326. data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
  327. data/lib/hecks/runtime/query_interpreter.rb +55 -64
  328. data/lib/hecks/runtime/reaction_invocation.rb +20 -20
  329. data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
  330. data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
  331. data/lib/hecks/runtime/reference_hop.rb +6 -6
  332. data/lib/hecks/runtime/refusal_wording.rb +92 -115
  333. data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
  334. data/lib/hecks/runtime/registry/verification.rb +36 -26
  335. data/lib/hecks/runtime/registry.rb +45 -28
  336. data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
  337. data/lib/hecks/runtime/routing.rb +10 -153
  338. data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
  339. data/lib/hecks/runtime/saga_interpreter.rb +64 -64
  340. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  341. data/lib/hecks/runtime/tenant_check.rb +9 -9
  342. data/lib/hecks/runtime/tenant_scope.rb +5 -5
  343. data/lib/hecks/runtime/value/admission.rb +75 -30
  344. data/lib/hecks/runtime/value/coercion.rb +318 -127
  345. data/lib/hecks/runtime/value/entity_list_coercion.rb +34 -34
  346. data/lib/hecks/runtime/value.rb +21 -21
  347. data/lib/hecks/runtime.rb +7 -7
  348. data/lib/hecks/storehouse.rb +64 -64
  349. data/lib/hecks/version.rb +3 -3
  350. data/lib/hecks/vocabulary.rb +205 -4
  351. data/lib/hecks.rb +13 -11
  352. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
  353. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
  354. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
  355. metadata +22 -2
@@ -12,28 +12,32 @@ module Hecks
12
12
  # the declared routing `version:` — is excluded, so editing behavior
13
13
  # never bumps an era and editing shape always does.
14
14
  #
15
- # Three attribute facts on the wire are constraints on PERSISTED
16
- # VALUES, not behavior, and are still excluded — each decided, not
15
+ # ## Constraints left out on purpose
16
+ #
17
+ # Three attribute facts on the wire are constraints on persisted
18
+ # values, not behavior, and are still excluded — each decided, not
17
19
  # overlooked:
18
20
  #
19
21
  # `optional` — required-ness is enforced at dispatch; stored rows
20
22
  # are never re-validated on read, so flipping it
21
23
  # strands nothing already written. Excluded.
22
- # `pattern` — same argument: a fact about what may be WRITTEN
24
+ # `pattern` — same argument: a fact about what may be written
23
25
  # next, not about what was stored. Excluded.
24
26
  # `admits` — the sharpest of the three: narrowing a closed set
25
- # CAN strand stored rows outside it, and the wire
26
- # carries only the set's NAME, so a set whose members
27
+ # can strand stored rows outside it, and the wire
28
+ # carries only the set's name, so a set whose members
27
29
  # changed under a stable name is invisible even to a
28
30
  # projection that included the fact (the same lesson
29
31
  # recursive value-object drift taught). Excluded, and
30
- # NAMED as a gap: constraint tightening has no
32
+ # named as a gap: constraint tightening has no
31
33
  # translation-rule vocabulary to acknowledge it yet,
32
34
  # so including it would mint era bumps nothing can
33
35
  # explain. When the translation language grows a
34
36
  # constraint-acknowledgment rule, `admits` (by member
35
37
  # list, not by name) is first in line, and that
36
- # change bumps FORM_VERSION.
38
+ # change bumps `FORM_VERSION`.
39
+ #
40
+ # ## Structural comparison over the dump form
37
41
  #
38
42
  # Projection runs over the canonical dump form (`to_h`, JSON
39
43
  # round-tripped), so a verdict depends only on the IR — never on live
@@ -41,6 +45,11 @@ module Hecks
41
45
  module StorageShape
42
46
  module_function
43
47
 
48
+ # Reduces a bluebook to the parts of its IR that decide what stored data looks like.
49
+ #
50
+ # @param bluebook [Bluebook::Chapter] the bluebook whose `to_h` dump is projected
51
+ # @return [Hash{String => Object}] `"name"` (the bluebook's name) and `"aggregates"`, an
52
+ # Array of the Hashes `project_aggregate` builds, sorted by aggregate name
44
53
  def project(bluebook)
45
54
  domain = JSON.parse(JSON.generate(bluebook.to_h))
46
55
  {
@@ -51,30 +60,55 @@ module Hecks
51
60
  }
52
61
  end
53
62
 
63
+ # Compares two bluebooks by storage shape alone, ignoring every behavioral difference.
64
+ #
65
+ # @param held [Bluebook::Chapter] the bluebook a held era was minted from
66
+ # @param current [Bluebook::Chapter] the bluebook booting now
67
+ # @return [Boolean] true when both project to an equal structure
54
68
  def same?(held, current) = project(held) == project(current)
55
69
 
56
- # The canonical serialization the Ruby scaffold hashes at MINT time
70
+ # The canonical serialization the Ruby scaffold hashes at mint time
57
71
  # — the one moment identity is computed. Nothing ever recomputes a
58
72
  # stored era name to verify it, so this form can evolve freely.
73
+ #
74
+ # @param bluebook [Bluebook::Chapter] the bluebook to serialize
75
+ # @return [String] compact JSON text of `project(bluebook)`
59
76
  def canonical(bluebook) = JSON.generate(project(bluebook))
60
77
 
78
+ # Computes the era identity of a bluebook's storage shape.
79
+ #
80
+ # @param bluebook [Bluebook::Chapter] the bluebook being minted
81
+ # @return [String] 64 lowercase hex characters, the SHA-256 of `canonical(bluebook)`
61
82
  def mint_hash(bluebook) = Digest::SHA256.hexdigest(canonical(bluebook))
62
83
 
63
84
  LABEL_LENGTH = 6
85
+
86
+ # Shortens the era identity to the label translation edges and refusals use.
87
+ #
88
+ # @param bluebook [Bluebook::Chapter] the bluebook being minted
89
+ # @return [String] the first `LABEL_LENGTH` hex characters of `mint_hash(bluebook)`
64
90
  def mint_label(bluebook) = mint_hash(bluebook)[0, LABEL_LENGTH]
65
91
 
66
92
  # The version of the canonical serialization above. Minted-once
67
93
  # means a stored name stays valid across form changes — but only
68
- # if each name records WHICH form minted it, so v1-named and
94
+ # if each name records which form minted it, so v1-named and
69
95
  # v2-named eras coexist legibly. Stored beside every minted hash
70
96
  # (names.tsv fourth field / hecks_eras.canon_form); bump this in
71
97
  # the same change that alters project/canonical output.
72
98
  FORM_VERSION = 1
73
99
 
100
+ # Projects one dumped aggregate down to its identity, lifecycle field and attributes.
101
+ #
102
+ # @param aggregate [Hash{String => Object}] one entry of the dumped bluebook's
103
+ # `"aggregates"` list, string-keyed after the JSON round-trip
104
+ # @return [Hash{String => Object}] `"name"`, `"identity"` (Array<String> of declared
105
+ # identity paths, `[]` when none is declared), `"lifecycle_field"` (String, or nil
106
+ # without a lifecycle) and `"attributes"` (Array of `project_attribute` Hashes, sorted
107
+ # by name)
74
108
  def project_aggregate(aggregate)
75
109
  {
76
110
  "name" => aggregate["name"],
77
- # The declared identity paths, AS A LIST, in declaration order —
111
+ # The declared identity paths, as a list, in declaration order —
78
112
  # order is semantic (the paths join in order to form the id).
79
113
  # No "id" fallback: an aggregate that declares nothing has [],
80
114
  # and that is a real declared state, distinct from an aggregate
@@ -87,6 +121,16 @@ module Hecks
87
121
  }
88
122
  end
89
123
 
124
+ # Projects one dumped attribute to its name, cardinality and full type signature.
125
+ #
126
+ # @param aggregate [Hash{String => Object}] the dumped aggregate that owns the attribute,
127
+ # searched for the value objects and entities its type may name
128
+ # @param attribute [Hash{String => Object}] the dumped attribute, read for `"name"`,
129
+ # `"list"` and `"type"`
130
+ # @param seen [Array<String>] type names already being expanded, which stops a
131
+ # self-referencing type from recursing forever
132
+ # @return [Hash{String => Object}] `"name"` (String), `"list"` (Boolean) and `"type"`
133
+ # (whatever `type_signature` returns)
90
134
  def project_attribute(aggregate, attribute, seen)
91
135
  {
92
136
  "name" => attribute["name"].to_s,
@@ -99,6 +143,14 @@ module Hecks
99
143
  # members' full signatures for a value object or entity — so two
100
144
  # attributes with the same declared type name but different
101
145
  # internals are never mistaken for unchanged.
146
+ #
147
+ # @param aggregate [Hash{String => Object}] the dumped aggregate whose value objects and
148
+ # entities are searched for `type_name`
149
+ # @param type_name [String] the attribute's declared type name
150
+ # @param seen [Array<String>] type names already being expanded
151
+ # @return [String, Hash{String => Object}] `type_name` itself for a primitive or a type
152
+ # already in `seen`; otherwise `"type"` plus `"members"`, the member attributes'
153
+ # `project_attribute` Hashes sorted by name
102
154
  def type_signature(aggregate, type_name, seen)
103
155
  container = nested_type(aggregate, type_name)
104
156
  return type_name if container.nil? || seen.include?(type_name)
@@ -111,6 +163,12 @@ module Hecks
111
163
  }
112
164
  end
113
165
 
166
+ # Looks a type name up among a dumped aggregate's value objects, then its entities.
167
+ #
168
+ # @param aggregate [Hash{String => Object}] the dumped aggregate to search
169
+ # @param type_name [String] the declared type name to find
170
+ # @return [Hash{String => Object}, nil] the dumped value object or entity; nil when the
171
+ # name is a primitive or belongs to nothing this aggregate declares
114
172
  def nested_type(aggregate, type_name)
115
173
  (aggregate["value_objects"] || []).find { |vo| vo["name"] == type_name } ||
116
174
  (aggregate["entities"] || []).find { |entity| entity["name"] == type_name }
@@ -7,21 +7,27 @@ module Hecks
7
7
  # ── the human gate, made real ──────────────────────────────────
8
8
  #
9
9
  # For the five portable rule kinds the mint verifies the edge
10
- # mechanically (Layer 2 IS the cross-execution equivalence gate).
10
+ # mechanically (Layer 2 is the cross-execution equivalence gate).
11
11
  # A compute has no mechanical verification — the Layer-3 sample a
12
12
  # human reads is the only one there is — so a compute edge cannot
13
13
  # mint until someone has run `bin/translation_audit … --approve`.
14
14
  #
15
- # The approval binds to WHAT WAS ACTUALLY REVIEWED, on both axes:
15
+ # The approval binds to what was actually reviewed, on both axes:
16
16
  # a content digest of the parsed edge (change the edge's meaning
17
17
  # and the approval lapses; a comment does not), and the journal's
18
- # high-water ordinal at review time, recorded IN the target
18
+ # high-water ordinal at review time, recorded in the target
19
19
  # database — a token in the repo could not bind to the data, and
20
20
  # samples reviewed against staging or against production-as-of-T
21
21
  # say nothing about a database that has moved on. Mint stays
22
22
  # non-interactive and its lock stays short; the human decision
23
23
  # happens in the audit tool, where the samples are.
24
24
  module ApprovalDigest
25
+ # Fingerprints a parsed translation edge, so an approval lapses when the edge's
26
+ # meaning changes but not when only a comment does.
27
+ #
28
+ # @param edge [Bluebook::Translation] the parsed translation edge
29
+ # @return [String] 64 lowercase hex characters: the SHA-256 of the edge's exported
30
+ # JSON (`Projector::Exporter.translation_hash`)
25
31
  def edge_digest(edge)
26
32
  Digest::SHA256.hexdigest(JSON.generate(Projector::Exporter.translation_hash(edge)))
27
33
  end
@@ -8,11 +8,18 @@ module Hecks
8
8
  module Audit
9
9
  # Layer 1 — from the bluebook alone: every translated state must
10
10
  # pass the new era's types, value-object invariants, and lifecycle.
11
- # This is also where a NEW, stricter invariant that old records
11
+ # This is also where a new, stricter invariant that old records
12
12
  # violate surfaces — there is no "grandfather old records"
13
13
  # construct, and the remedy is relaxing the invariant or explicit
14
14
  # remediation, never a translation rule.
15
15
  module LayerOne
16
+ # Hydrates every translated state as a current-era instance and records each one
17
+ # the era's types, invariants or lifecycle refuse.
18
+ #
19
+ # @param violations [Array<String>] collector this method appends messages to
20
+ # @param aggregate [Bluebook::Aggregate] the current era's IR for the aggregate
21
+ # @param after [Hash{String => Hash}] translated state per record id, as parsed JSON
22
+ # @return [void]
16
23
  def layer_one!(violations, aggregate, after)
17
24
  after.each do |id, state|
18
25
  symbolized = JSON.parse(JSON.generate(state), symbolize_names: true)
@@ -25,7 +32,7 @@ module Hecks
25
32
  # legitimately declared just as a `from:` (a terminal
26
33
  # transition's source, never anyone's target) is real and
27
34
  # reachable but invisible to it. `ModelCheck.full_states`
28
- # is the full declared set (default, every target, AND
35
+ # is the full declared set (default, every target, and
29
36
  # every from) that `fuzzing/properties.rb`'s own replay
30
37
  # check already uses for this identical question — see its
31
38
  # comment on this same hole.
@@ -7,8 +7,11 @@ module Hecks
7
7
  # Layer 2 — from the edge alone: per-rule value preservation, no
8
8
  # leftover source keys, and id-set conservation across the edge.
9
9
  module LayerTwo
10
+ # Checks one aggregate's records across the edge for id conservation and, where a
11
+ # per-record comparison is possible, agreement with the reference transform.
12
+ #
10
13
  # Per-rule value preservation and leftover source keys are checked
11
- # against the reference transform IN FULL — never rule by rule in
14
+ # against the reference transform in full — never rule by rule in
12
15
  # isolation, because rules interact (a rename whose value a later
13
16
  # move partially consumes preserves exactly what the transform
14
17
  # says it preserves, no more). This makes every mint a run of the
@@ -17,10 +20,20 @@ module Hecks
17
20
  # transform produces `expected`; they must agree byte-for-byte on
18
21
  # every path a compute doesn't own. Compute paths are exempt — the
19
22
  # SQL is their only implementation, and the Layer-3 sample is
20
- # their only review. A rekeyed aggregate is exempt from this WHOLE
23
+ # their only review. A rekeyed aggregate is exempt from this whole
21
24
  # per-record check, for the same reason and one more: there is no
22
25
  # old-id → new-id correspondence to look `after` up by once the id
23
26
  # itself is what changed.
27
+ #
28
+ # @param violations [Array<String>] collector this method appends messages to
29
+ # @param aggregate [Bluebook::Aggregate] the current era's IR for the aggregate
30
+ # @param declared [Bluebook::TranslationAggregate, nil] this edge's rules for the
31
+ # aggregate; nil limits the check to id conservation
32
+ # @param before [Hash{String => Hash}] source state per record id, as parsed JSON
33
+ # @param after [Hash{String => Hash}] translated state per record id, as parsed JSON
34
+ # @return [void]
35
+ # @raise [Runtime::WiringError] if the reference transform cannot translate a `before`
36
+ # state: a convert meets an unmapped value, or a move nests under a non-Hash
24
37
  def layer_two!(violations, aggregate, declared, before, after)
25
38
  rekeyed = declared && !declared.rekeys.empty?
26
39
 
@@ -38,15 +51,25 @@ module Hecks
38
51
  check_value_preservation!(violations, aggregate, declared, before, after)
39
52
  end
40
53
 
54
+ # Records a violation when the edge loses, gains or collides record ids.
55
+ #
41
56
  # Record-count / id-set conservation — one of the two properties
42
57
  # this module's own header names, and independent of the other
43
58
  # (per-rule value preservation, below): it needs only `before`,
44
59
  # `after`, and whether the edge rekeyed, never the declared rules
45
60
  # themselves.
61
+ #
62
+ # @param violations [Array<String>] collector this method appends at most one message to
63
+ # @param aggregate [Bluebook::Aggregate] the aggregate, named in the message
64
+ # @param before [Hash{String => Hash}] source state per record id
65
+ # @param after [Hash{String => Hash}] translated state per record id
66
+ # @param rekeyed [Boolean, nil] truthy when the edge rekeys, which compares record
67
+ # counts instead of id sets
68
+ # @return [void]
46
69
  def check_id_conservation!(violations, aggregate, before, after, rekeyed:)
47
- # A rekey legitimately changes the id SET (that's the entire
70
+ # A rekey legitimately changes the id set (that's the entire
48
71
  # point) — set-equality would flag every honest rekey as data
49
- # loss. What must still hold is RECORD COUNT: a botched rekey
72
+ # loss. What must still hold is record count: a botched rekey
50
73
  # colliding two distinct old ids onto one new id, or dropping one
51
74
  # (its SQL returning NULL), shows up as the count going down —
52
75
  # caught here without needing to track the old→new mapping
@@ -87,21 +110,32 @@ module Hecks
87
110
  end
88
111
  end
89
112
 
113
+ # Round-trips state through JSON so both sides of the comparison share one spelling.
114
+ #
115
+ # @param state [Hash, nil] a state with Symbol or String keys
116
+ # @return [Hash{String => Object}, nil] a deep copy with String keys at every depth
90
117
  def normalize(state) = JSON.parse(JSON.generate(state))
91
118
 
119
+ # Removes the paths a compute owns from a normalized state, in place.
120
+ #
92
121
  # Exempts exactly the paths a compute owns, not the whole
93
122
  # top-level attribute it happens to live under. A bare path
94
123
  # ("price_cents") is itself the compute's entire value — dropping
95
124
  # the whole top-level key is correct, there's nothing else there
96
- # to check. A DOTTED path ("price.cents") only owns that one
125
+ # to check. A dotted path ("price.cents") only owns that one
97
126
  # member of the value object it reaches into; every sibling
98
127
  # member (e.g. "price.currency") is untouched by the compute and
99
128
  # must stay subject to the equivalence check below. Blanket-
100
- # dropping the whole top-level key for a dotted compute used to
129
+ # dropping the whole top-level key for a dotted compute would
101
130
  # exempt the entire attribute — silent data loss elsewhere in the
102
131
  # same value object (a migration that nulls or drops a sibling
103
- # field) produced zero violations, defeating the one gate whose
104
- # entire purpose is to catch exactly that.
132
+ # field) would produce zero violations, defeating the one gate
133
+ # whose entire purpose is to catch exactly that.
134
+ #
135
+ # @param state [Hash{String => Object}] a normalized state; mutated
136
+ # @param paths [Array<String>] bare (`"price_cents"`) or dotted (`"price.cents"`)
137
+ # compute paths
138
+ # @return [Hash{String => Object}] `state` itself, with those paths deleted
105
139
  def strip_compute_paths(state, paths)
106
140
  paths.each do |path|
107
141
  segments = path.split(".")
@@ -4,9 +4,11 @@ module Hecks
4
4
  # New-era attributes that nothing feeds: absent from every
5
5
  # translated state, produced by no rule, and carrying no default.
6
6
  # A report, not a violation — the remedy is a `default:` on the
7
- # attribute, and the loud refusal for a REQUIRED one comes from
7
+ # attribute, and the loud refusal for a required one comes from
8
8
  # Layer 1 the moment an invariant reads it.
9
9
  module UnfedReport
10
+ # Lists the current era's attributes that no rule, default or translated record feeds.
11
+ #
10
12
  # `fed` build-up plus one ordered guard chain per attribute
11
13
  # (already fed, has a default, actually present in some record, no
12
14
  # records to check at all) answering a single question — "is
@@ -14,6 +16,13 @@ module Hecks
14
16
  # rules out one way the answer is "not unfed after all" before the
15
17
  # next is even worth asking. Splitting the guards out would just
16
18
  # turn each into a same-shaped one-line predicate call.
19
+ #
20
+ # @param aggregate [Bluebook::Aggregate] the current era's IR for the aggregate
21
+ # @param declared [Bluebook::TranslationAggregate, nil] this edge's rules; the
22
+ # destinations of its renames, moves, converts and computes count as fed
23
+ # @param after [Hash{String => Hash}] translated state per record id
24
+ # @return [Array<String>] names of unfed attributes, in declaration order; `[]` when
25
+ # every attribute is fed or `after` holds no records
17
26
  # rubocop:disable-next Metrics/CyclomaticComplexity
18
27
  # rubocop:disable-next Metrics/PerceivedComplexity
19
28
  def unfed(aggregate, declared, after)
@@ -36,6 +45,12 @@ module Hecks
36
45
  end
37
46
  end
38
47
 
48
+ # Reads a dotted path out of a state whose keys may be Strings or Symbols.
49
+ #
50
+ # @param state [Hash, nil] the state to read
51
+ # @param path [String, Symbol] a bare or dotted path, such as `"price.cents"`
52
+ # @return [Object, nil] the value held at the path, `false` included; nil when `state`
53
+ # is nil, a segment is absent, or a segment's parent is not a Hash
39
54
  def dig_path(state, path)
40
55
  return nil if state.nil?
41
56
 
@@ -12,10 +12,14 @@ module Hecks
12
12
  # rules), audit/layer_two.rb (the edge against the reference
13
13
  # transform), audit/unfed_report.rb (what nothing feeds), and
14
14
  # audit/approval_digest.rb (the human gate's binding). Layer 3 — the
15
- # before/after sample a HUMAN approves, intent not being derivable —
15
+ # before/after sample a human approves, intent not being derivable —
16
16
  # is assembled right here in `check`.
17
17
  module Audit
18
18
  Verdict = Struct.new(:violations, :dropped, :unfed, :samples, keyword_init: true) do
19
+ # Reports whether the mechanical layers found nothing to refuse.
20
+ #
21
+ # @return [Boolean] true when `violations` is empty; `dropped`, `unfed` and `samples`
22
+ # are reports and never affect the answer
19
23
  def ok? = violations.empty?
20
24
  end
21
25
 
@@ -28,9 +32,20 @@ module Hecks
28
32
 
29
33
  module_function
30
34
 
31
- # aggregate the CURRENT era's IR; declared this edge's rules
32
- # for it (may be nil); before/after — {id => state hash}, source
33
- # era's latest vs translated.
35
+ # Audits one aggregate's records across one translation edge and gathers what a
36
+ # human reviewer needs to see.
37
+ #
38
+ # @param aggregate [Bluebook::Aggregate] the current era's IR for the aggregate
39
+ # @param declared [Bluebook::TranslationAggregate, nil] this edge's rules for the
40
+ # aggregate; nil when the edge declares none
41
+ # @param before [Hash{String => Hash}] the source era's latest state per record id, as
42
+ # parsed JSON
43
+ # @param after [Hash{String => Hash}] the translated state per record id, as parsed JSON
44
+ # @return [Translation::Audit::Verdict] `violations` (Layer 1 and 2 messages), `dropped`
45
+ # (declared drop paths as Strings), `unfed` (attribute names nothing feeds) and
46
+ # `samples` (the Layer 3 before/after pairs)
47
+ # @raise [Runtime::WiringError] if Layer 2's reference transform cannot translate a
48
+ # `before` state: a convert meets an unmapped value, or a move nests under a non-Hash
34
49
  def check(aggregate:, declared:, before:, after:)
35
50
  violations = []
36
51
 
@@ -45,22 +60,38 @@ module Hecks
45
60
  )
46
61
  end
47
62
 
63
+ # Picks the first `SAMPLE_SIZE` records, by sorted id, for a human to compare.
64
+ #
65
+ # @param declared [Bluebook::TranslationAggregate, nil] this edge's rules; a rekey among
66
+ # them switches to `rekeyed_samples`
67
+ # @param before [Hash{String => Hash}] source state per record id
68
+ # @param after [Hash{String => Hash}] translated state per record id
69
+ # @return [Array<Hash{Symbol => Object}>] Hashes with `:id`, `:before` and `:after`;
70
+ # `:after` is nil for an id missing from `after`
48
71
  def samples_for(declared, before, after)
49
72
  return rekeyed_samples(before, after) if declared && !declared.rekeys.empty?
50
73
 
51
74
  before.keys.sort.first(SAMPLE_SIZE).map { |id| { id: id, before: before[id], after: after[id] } }
52
75
  end
53
76
 
77
+ # Samples both sides of a rekeying edge separately instead of pairing them by id.
78
+ #
54
79
  # A rekey changes the id itself, so `before`'s and `after`'s
55
80
  # keyspaces share nothing — pairing by matching id (the ordinary
56
81
  # path above) would show every `after` as nil, telling a human
57
82
  # nothing. Nothing in-process can compute the old→new
58
83
  # correspondence either (the rekey's SQL is its only
59
84
  # implementation, same as compute's own). Shown side by side
60
- # instead, each labelled by its OWN id: real records going in,
85
+ # instead, each labelled by its own id: real records going in,
61
86
  # real records coming out — not claimed to correspond one-to-one,
62
87
  # but enough for the human this rule's only verification depends
63
88
  # on to actually see real shapes and values, not a wall of null.
89
+ #
90
+ # @param before [Hash{String => Hash}] source state per old record id
91
+ # @param after [Hash{String => Hash}] translated state per new record id
92
+ # @return [Array<Hash{Symbol => Object}>] up to `SAMPLE_SIZE` Hashes whose `:id` ends in
93
+ # `" (before)"` with `:after` nil, then up to `SAMPLE_SIZE` ending in `" (after)"` with
94
+ # `:before` nil
64
95
  def rekeyed_samples(before, after)
65
96
  before.keys.sort.first(SAMPLE_SIZE).map { |id| { id: "#{id} (before)", before: before[id], after: nil } } +
66
97
  after.keys.sort.first(SAMPLE_SIZE).map { |id| { id: "#{id} (after)", before: nil, after: after[id] } }
@@ -6,19 +6,33 @@ require_relative "../storage_shape"
6
6
  module Hecks
7
7
  module Translation
8
8
  # The question re-attestation must answer before anything else: did
9
- # the edit change the era's SHAPE, or only its text? Cosmetic edits
9
+ # the edit change the era's shape, or only its text? Cosmetic edits
10
10
  # (comments, whitespace, behavior) re-freeze safely; a shape change
11
11
  # would retroactively redefine what era N *meant* for data already
12
12
  # written under it, and refuses hard — there is no --accept past
13
13
  # this guard.
14
14
  #
15
15
  # This does not violate minted-once. That prohibition exists so
16
- # boot-time RECOGNITION never depends on canonicalization stability;
16
+ # boot-time recognition never depends on canonicalization stability;
17
17
  # this is operator-initiated repair, where a false negative is a
18
18
  # loud refusal, never a silent misread.
19
19
  module Reattest
20
20
  module_function
21
21
 
22
+ # Decides whether an edited era text still projects to the shape the era was frozen
23
+ # with, refusing the edit when it does not.
24
+ #
25
+ # @param domain [String] the domain's name, used in refusal messages
26
+ # @param ordinal [Integer] the era's ordinal, used in refusal messages
27
+ # @param text [String] the held era's bluebook source as it now stands
28
+ # @param stored_hash [String, nil] the era's minted shape hash (SHA-256 hex); nil for an
29
+ # era that was never named
30
+ # @param stored_projection [Hash{String => Object}, nil] the era's stored
31
+ # `Runtime::StorageShape.project` result as parsed JSON; nil for a store without one
32
+ # @return [Symbol] `:cosmetic` when the shape is unchanged, `:unnamed` when neither a
33
+ # projection nor a hash is stored so no comparison is possible
34
+ # @raise [Runtime::WiringError] if `text` does not load as a bluebook, or it projects to
35
+ # a shape other than the stored one
22
36
  def shape_guard!(domain:, ordinal:, text:, stored_hash:, stored_projection: nil)
23
37
  bluebook = shadow(text)
24
38
  unless bluebook
@@ -27,7 +41,7 @@ module Hecks
27
41
  "bluebook — a held era text is bootable source; restore a loadable text"
28
42
  end
29
43
 
30
- # The stored PROJECTION is the preferred comparison: structural,
44
+ # The stored projection is the preferred comparison: structural,
31
45
  # version-free, and the same mechanism every boot trusts — so
32
46
  # this guard never depends on canonicalization stability, and a
33
47
  # future canonical-form version cannot make cosmetic edits to
@@ -57,6 +71,12 @@ module Hecks
57
71
  "under it; restore a text with the original shape"
58
72
  end
59
73
 
74
+ # Parses bluebook source in a scratch registry, through a temporary file that is
75
+ # removed afterwards, without touching the live registry.
76
+ #
77
+ # @param source [String] bluebook source text
78
+ # @return [Bluebook::Chapter, nil] the first bluebook the source declares; nil when it
79
+ # declares none or fails to parse or load for any reason
60
80
  def shadow(source)
61
81
  file = Tempfile.new(["hecks-reattest-", ".bluebook"])
62
82
  file.write(source)
@@ -2,23 +2,22 @@ require "json"
2
2
 
3
3
  module Hecks
4
4
  module Translation
5
- # The closed, PURE half of Postgres's own SQL compiler
6
- # (adapters/driven/postgres_era/lineage/head_compiler.rb) — the part
7
- # that turns one TranslationAggregate's declared rules into a
8
- # jsonb-transforming SQL expression. No database connection, no
9
- # watermark, no era chain, no catalog lookup: those stay exactly
10
- # where they were, in head_compiler.rb's own per-mint assembly,
11
- # which calls into this module instead of defining these methods
12
- # itself.
5
+ # The closed, pure half of Postgres's own SQL compiler
6
+ # (ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb)
7
+ # — the part that turns one TranslationAggregate's declared rules into
8
+ # a jsonb-transforming SQL expression. No database connection, no
9
+ # watermark, no era chain, no catalog lookup: those live in
10
+ # head_compiler.rb's own per-mint assembly, which calls into this
11
+ # module instead of defining these methods itself.
13
12
  #
14
- # Extracted here — not left as private methods on
15
- # Adapters::PostgresEra::Lineage — so a SECOND, adapter-agnostic
13
+ # A module of its own — not private methods on
14
+ # Adapters::PostgresEra::Lineage — so a second, adapter-agnostic
16
15
  # caller (Exporter.translation_aggregate's build-time SQL export,
17
16
  # feeding rust/host's own future boot-time mint) can call the exact
18
- # SAME code Ruby's own mint path runs, not a hand-ported duplicate
19
- # that could silently drift the way `Exporter.translation_hash`
20
- # drifted from `hecks_eras`/`hecks_approvals`' real schema before
21
- # this file existed (rekeys/backfills were missing for years).
17
+ # same code Ruby's own mint path runs, not a hand-ported duplicate
18
+ # that could silently drift the way a hand-kept
19
+ # `Exporter.translation_hash` can drift from `hecks_eras`/
20
+ # `hecks_approvals`' real schema (leaving out rekeys and backfills).
22
21
  module RuleCompiler
23
22
  module_function
24
23
 
@@ -51,7 +50,7 @@ module Hecks
51
50
  expression
52
51
  end
53
52
 
54
- # Whether THIS edge's declared rules for this aggregate include a
53
+ # Whether this edge's declared rules for this aggregate include a
55
54
  # rekey — checked directly off the raw IR object, the same way
56
55
  # every other rule kind is already read in `compile_rules`
57
56
  # (`declared.computes`, `declared.moves`, ...), not through the
@@ -60,16 +59,16 @@ module Hecks
60
59
  # module builds SQL straight off the IR either way.
61
60
  def rekeyed?(declared) = declared && !declared.rekeys.empty?
62
61
 
63
- # THE ONLY TWO PLACES `aggregate_id` NEEDS TO CHANGE — guarded so
62
+ # The only two places `aggregate_id` needs to change — guarded so
64
63
  # the generated SQL for the overwhelming common case (no rekey
65
64
  # declared) stays the bare `aggregate_id` passthrough it always
66
- # was — this CASE only appears in an edge that actually declares
65
+ # was — this case only appears in an edge that actually declares
67
66
  # one.
68
67
  def id_case(guard, declared)
69
68
  "CASE WHEN #{guard} THEN #{compile_id_expression(declared)} ELSE aggregate_id END AS aggregate_id"
70
69
  end
71
70
 
72
- # THE REKEY'S OWN SQL — reading `state` directly, not the
71
+ # The rekey's own SQL — reading `state` directly, not the
73
72
  # progressively-built `expression` chain `compile_compute` reads
74
73
  # from. A rekey doesn't consume or move any field the way a move
75
74
  # or compute does, so there is no same-edge rename/move ordering
@@ -96,7 +95,7 @@ module Hecks
96
95
  "LATERAL (SELECT (__s ->> #{text_literal(from)}) AS #{quote(from)}) __fields)"
97
96
  end
98
97
 
99
- # `PG::Connection.quote_ident` needs the `pg` gem LOADED, not
98
+ # `PG::Connection.quote_ident` needs the `pg` gem loaded, not
100
99
  # connected — required here, lazily, the same "a domain that
101
100
  # never wires PostgresEra should never need the gem" reasoning
102
101
  # `PostgresEra.connect_for`'s own `require "pg"` already holds
@@ -32,7 +32,7 @@ module Hecks
32
32
  end
33
33
 
34
34
  # A vanished aggregate whose full shape reappears under exactly one
35
- # new name was renamed. `retired` is only confident when NOTHING
35
+ # new name was renamed. `retired` is only confident when nothing
36
36
  # remains it could plausibly have become — a vanished aggregate
37
37
  # beside an unmatched new one might be a rename-plus-reshape, and
38
38
  # writing `retired` there would be a guess that strands data.
@@ -61,7 +61,7 @@ module Hecks
61
61
  end
62
62
 
63
63
  # Every changed path in one aggregate, resolved into rules by
64
- # signature matching over the FULL path set — top-level names and
64
+ # signature matching over the full path set — top-level names and
65
65
  # dotted members alike. Unique signature pair: a rename (both
66
66
  # top-level) or a move (any dotted end). Same path, same members,
67
67
  # new type name: a retype. Anything else: unresolved, carrying its
@@ -81,7 +81,7 @@ module Hecks
81
81
  appeared = appeared_paths(held_attrs, current_attrs, retyped)
82
82
 
83
83
  # Mutates `rules` in place (not build-and-concat, like the two
84
- # passes above) because it must READ its own earlier writes:
84
+ # passes above) because it must read its own earlier writes:
85
85
  # `taken`, below, is recomputed from `rules` at the top of every
86
86
  # iteration, so a target this same loop already claimed for an
87
87
  # earlier vanished path is excluded from a later one. Passing a
@@ -92,7 +92,7 @@ module Hecks
92
92
  end
93
93
 
94
94
  # retype pass: same attribute name, same member structure, the
95
- # TYPE's own name changed
95
+ # type's own name changed
96
96
  def retype_rules(held_attrs, current_attrs)
97
97
  (held_attrs.keys & current_attrs.keys).filter_map do |name|
98
98
  held = held_attrs[name]
@@ -166,7 +166,7 @@ module Hecks
166
166
  paths
167
167
  end
168
168
 
169
- # THE ONE THING `attribute_rules` COULD NOT SEE BEFORE — the shape
169
+ # The one thing `attribute_rules` could not see before — the shape
170
170
  # projection already carries `"identity"` (`StorageShape
171
171
  # .project_aggregate`), it was simply never read here. An
172
172
  # unresolved placeholder, not a guess: `coverage_check.rb`'s own
@@ -8,7 +8,7 @@ module Hecks
8
8
  module Writer
9
9
  # The edge file, regenerated in place when one for the same shape
10
10
  # pair already exists (matched textually — an unresolved file
11
- # cannot be LOADED to ask, that being the whole point of
11
+ # cannot be loaded to ask, that being the whole point of
12
12
  # unresolved).
13
13
  def write!(directory, edge)
14
14
  translations_dir = File.join(directory, "translations")
@@ -6,13 +6,13 @@ module Hecks
6
6
  module Translation
7
7
  # The scaffold writes translations; humans resolve ambiguity. It
8
8
  # diffs the held era's storage-shape projection against the current
9
- # one (scaffold/differ.rb) and WRITES the edge file
9
+ # one (scaffold/differ.rb) and writes the edge file
10
10
  # (scaffold/renderer.rb, scaffold/writer.rb): confident rules inline
11
11
  # (unique signature pairs → rename/move, aggregate renames → `was:`,
12
12
  # type renames with identical members → retype, aggregates gone
13
13
  # without a successor → retired), ambiguities as parse-refusing
14
14
  # `unresolved` constructs — never comments, so an unresolved file can
15
- # only boot into a refusal, never a guess. It NEVER proposes a
15
+ # only boot into a refusal, never a guess. It never proposes a
16
16
  # `compute` (a computation takes domain judgment no mechanical diff
17
17
  # can infer) — an empty-candidate `unresolved` is what points the
18
18
  # author there. It never proposes a `drop` either: data loss is a