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
@@ -1,7 +1,7 @@
1
1
  module Hecks
2
2
  module Ports
3
3
  module Persistence
4
- # THE NO-OP SAGA STORE — what `Registry#saga_persistence` hands
4
+ # **The no-op saga store** — what `Registry#saga_persistence` hands
5
5
  # back for a domain whose resolved adapter doesn't implement the
6
6
  # (optional) saga-persistence capability: Memory, deliberately
7
7
  # (sagas stay in-memory-only, exactly as they always have), and
@@ -14,8 +14,19 @@ module Hecks
14
14
  # `Ports::Persistence::AppendOnly` already gives adapters that
15
15
  # don't implement `reset!`/`events`/`record_event`.
16
16
  class NullSagaStore
17
+ # Accepts and discards a saga checkpoint, whatever keywords it carries.
18
+ #
19
+ # @return [nil] always; nothing is stored
17
20
  def save_saga(**) = nil
21
+
22
+ # Accepts and ignores a request to forget a saga.
23
+ #
24
+ # @return [nil] always; there is nothing to delete
18
25
  def delete_saga(**) = nil
26
+
27
+ # Yields nothing, because no saga is ever stored here.
28
+ #
29
+ # @return [nil] always; a block, if given, is never called
19
30
  def each_saga(*) = nil
20
31
  end
21
32
 
@@ -8,17 +8,17 @@ module Hecks
8
8
  # a plugin makes itself known as a side effect of being `require`d, the
9
9
  # same way an adapter's own `.rb` file already registers itself
10
10
  # (`Runtime::Registry#add_adapter`) rather than core listing adapter
11
- # names anywhere. `register_plugin` is the ENTIRE contract a plugin
11
+ # names anywhere. `register_plugin` is the entire contract a plugin
12
12
  # must satisfy: anything responding to `contribute_boot_gates(registry,
13
13
  # gates)` (a no-op is a valid implementation).
14
14
  #
15
- # This is deliberately NOT the same registry `Hecks::Projector` uses
15
+ # This is deliberately not the same registry `Hecks::Projector` uses
16
16
  # (ADR 0027) or `Runtime::BootGates` (ADR 0031) — each of those is
17
17
  # scoped to its own seam (IR-in/artifact-out; per-boot phased gates).
18
18
  # A shared base is deferred until a third registry actually wants one.
19
19
  #
20
- # NAME-KEYED, PROCESS-WIDE, NOT PER-BOOT — unlike `BootGates` (one
21
- # instance per `Loader.boot` call, because gate REGISTRATION is
20
+ # **Name-keyed, process-wide, not per-boot** — unlike `BootGates` (one
21
+ # instance per `Loader.boot` call, because gate registration is
22
22
  # capability-conditional per registry), a persistence plugin is either
23
23
  # `require`d into this process or it isn't; there is no "this boot's
24
24
  # registry doesn't need it" case to isolate against, so a plain
@@ -27,18 +27,36 @@ module Hecks
27
27
  @plugins = {}
28
28
 
29
29
  class << self
30
+ # Adds a plugin under a name, replacing any plugin already registered under it.
31
+ #
32
+ # @param name [Symbol, String] the plugin's name, such as `:era`; stored as a Symbol
33
+ # @param plugin [Object] anything responding to
34
+ # `contribute_boot_gates(registry, gates)`
35
+ # @return [Object] the plugin just registered
30
36
  def register(name, plugin)
31
37
  @plugins[name.to_sym] = plugin
32
38
  end
33
39
 
40
+ # Reports whether a plugin of that name has been required into this process.
41
+ #
42
+ # @param name [Symbol, String] the plugin's name
43
+ # @return [Boolean] true when a plugin is registered under `name`
34
44
  def registered?(name)
35
45
  @plugins.key?(name.to_sym)
36
46
  end
37
47
 
48
+ # Yields every registered plugin, in registration order.
49
+ #
50
+ # @yieldparam plugin [Object] a registered plugin
51
+ # @return [Hash{Symbol => Object}, Enumerator] the plugin table when a block is
52
+ # given, otherwise an Enumerator over the plugins
38
53
  def each(&)
39
54
  @plugins.each_value(&)
40
55
  end
41
56
 
57
+ # Reports whether any persistence plugin is loaded at all.
58
+ #
59
+ # @return [Boolean] true when at least one plugin is registered
42
60
  def any? = !@plugins.empty?
43
61
  end
44
62
  end
@@ -48,9 +66,29 @@ module Hecks
48
66
  # directly for a plugin's own file to call at require-time.
49
67
  module_function
50
68
 
69
+ # Registers a persistence plugin; a plugin's own file calls this when it is required.
70
+ #
71
+ # @param name [Symbol, String] the plugin's name, such as `:era`
72
+ # @param plugin [Object] anything responding to `contribute_boot_gates(registry, gates)`
73
+ # @return [Object] the plugin just registered
51
74
  def register_plugin(name, plugin) = Plugin.register(name, plugin)
75
+
76
+ # Reports whether the named persistence plugin is loaded in this process.
77
+ #
78
+ # @param name [Symbol, String] the plugin's name
79
+ # @return [Boolean] true when a plugin is registered under `name`
52
80
  def plugin?(name) = Plugin.registered?(name)
81
+
82
+ # Yields every loaded persistence plugin, in registration order.
83
+ #
84
+ # @yieldparam plugin [Object] a registered plugin
85
+ # @return [Hash{Symbol => Object}, Enumerator] the plugin table when a block is given,
86
+ # otherwise an Enumerator over the plugins
53
87
  def each_plugin(&) = Plugin.each(&)
88
+
89
+ # Reports whether any persistence plugin is loaded in this process.
90
+ #
91
+ # @return [Boolean] true when at least one plugin is registered
54
92
  def plugins_loaded? = Plugin.any?
55
93
  end
56
94
  end
@@ -7,7 +7,7 @@ require_relative "../../../../runtime/registry"
7
7
 
8
8
  module Hecks
9
9
  module Runtime
10
- # The boot-time era gate, run for the adapters that HAVE eras — the
10
+ # The boot-time era gate, run for the adapters that have eras — the
11
11
  # lineage-capable ones. An era is a fact about stored data that some
12
12
  # adapter can carry across a shape change; an adapter that cannot
13
13
  # translate has no era to hold, and holding one for it would record
@@ -15,7 +15,7 @@ module Hecks
15
15
  #
16
16
  # The whole domain is handed to the capable adapter's own era check
17
17
  # (Postgres: hold, recognize, mint, or refuse toward the scaffold),
18
- # which keeps its era facts as rows BESIDE its data. That
18
+ # which keeps its era facts as rows beside its data. That
19
19
  # co-location is load-bearing — a watermark only means something
20
20
  # against the journal it was cut from, and an approval recorded
21
21
  # anywhere but the reviewed database binds to nothing.
@@ -31,50 +31,84 @@ module Hecks
31
31
  module EraCheck
32
32
  module_function
33
33
 
34
- # EACH BLUEBOOK'S OWN SOURCE, not one file read once and reused
34
+ # Runs both halves of the era gate over every bluebook in a registry.
35
+ #
36
+ # Each bluebook's own source, not one file read once and reused
35
37
  # for every bluebook in the registry — true as long as a domain
36
38
  # directory only ever held exactly one, and silently wrong the
37
39
  # moment `uses_framework` made a second, differently-sourced
38
40
  # bluebook (Governance, Identity — `lib/hecks/framework/bluebook/`,
39
41
  # not the domain's own directory) share a boot with the first.
40
42
  # Caught the hard way: three domains booted together, one real
41
- # source text (the domain's own), and every OTHER domain's era-1
42
- # held THAT text under its own name — a shadow-parse of it later
43
+ # source text (the domain's own), and every other domain's era-1
44
+ # held that text under its own name — a shadow-parse of it later
43
45
  # reconstructs a completely different shape, and every boot after
44
46
  # the first refuses toward a scaffold that was never the real
45
47
  # drift.
48
+ #
49
+ # @param registry [Runtime::Registry] the registry being booted
50
+ # @param directory [String] path of the domain's own bluebook directory, searched for
51
+ # each bluebook's `.bluebook` source
52
+ # @return [void]
53
+ # @raise [Runtime::WiringError] if a compute rule is bound to an adapter that is not
54
+ # lineage-capable, a persistence binding cannot be resolved, a lineage-bound bluebook
55
+ # has no findable source, or the adapter's own `era_check!` refuses the boot
46
56
  def check!(registry, directory)
47
57
  check_compute_rules_for_registry!(registry)
48
58
  check_lineage!(registry, directory)
49
59
  end
50
60
 
61
+ # Refuses the boot when any bluebook's compute rule is bound away from Postgres.
62
+ #
51
63
  # The domain-agnostic half, split out for ADR 0031's boot-gate
52
64
  # registry: a compute rule requires Postgres whatever adapter is
53
- # actually bound, so this must run for EVERY registry, the same way
65
+ # actually bound, so this must run for every registry, the same way
54
66
  # `registry.verify!` does — it is not conditional on any adapter
55
67
  # being lineage-capable, and must never be skipped by
56
68
  # `check_lineage!`'s own capability gate below.
69
+ #
70
+ # @param registry [Runtime::Registry] the registry whose bluebooks are all checked
71
+ # @return [void]
72
+ # @raise [Runtime::WiringError] if an aggregate carrying a compute rule is bound to an
73
+ # adapter that is not lineage-capable, or its persistence binding cannot be resolved
57
74
  def check_compute_rules_for_registry!(registry)
58
75
  registry.bluebooks.each_value { |bluebook| check_compute_rules!(registry, bluebook) }
59
76
  end
60
77
 
78
+ # Hands every bluebook, with its own source text, to its adapter's era check.
79
+ #
61
80
  # The capability-gated half — ADR 0031's registered `:era_check`
62
81
  # gate. Registration is conditional on `lineage_capable_registry?`;
63
82
  # `check_bluebook!` below still carries its own per-bluebook
64
83
  # `lineage_capable?` return-early, unchanged, for a registry with a
65
84
  # mix of lineage-capable and plain-adapter bluebooks.
85
+ #
86
+ # @param registry [Runtime::Registry] the registry being booted
87
+ # @param directory [String] path of the domain's own bluebook directory
88
+ # @return [void]
89
+ # @raise [Runtime::WiringError] if a persistence binding cannot be resolved, a
90
+ # lineage-bound bluebook has no findable source, or the adapter's own `era_check!`
91
+ # refuses the boot
66
92
  def check_lineage!(registry, directory)
67
93
  registry.bluebooks.each_value do |bluebook|
68
- check_bluebook!(registry, bluebook, source_text_for(bluebook, directory), directory: directory)
94
+ check_bluebook!(registry, bluebook, source_text_for(bluebook, directory, registry: registry), directory: directory)
69
95
  end
70
96
  end
71
97
 
98
+ # Decides whether a registry binds anything that carries eras.
99
+ #
72
100
  # The `:era_check` gate's own registration predicate: true iff at
73
101
  # least one bluebook's own anchor (first) aggregate resolves to a
74
102
  # lineage-capable adapter — mirrors `check_bluebook!`'s existing
75
103
  # per-bluebook anchor check, just asked once, up front, of the
76
104
  # whole registry, so a registry with nothing lineage-capable bound
77
105
  # anywhere never registers the gate at all.
106
+ #
107
+ # @param registry [Runtime::Registry] the registry whose bluebooks are asked
108
+ # @return [Boolean] true when some bluebook's first aggregate is bound to a
109
+ # lineage-capable adapter; false for an empty registry or one with no such binding
110
+ # @raise [Runtime::WiringError] if a first aggregate's persistence binding is missing,
111
+ # ambiguous, or carries an unsupported role
78
112
  def lineage_capable_registry?(registry)
79
113
  registry.bluebooks.each_value.any? do |bluebook|
80
114
  first = bluebook.aggregates.first
@@ -84,38 +118,170 @@ module Hecks
84
118
  end
85
119
  end
86
120
 
121
+ # Reads the source text one bluebook was declared in, wherever that source lives.
122
+ #
87
123
  # The domain's own directory first, matched by name — a real app's
88
124
  # directory may hold more than one file once `uses_framework`
89
125
  # exists, so ".first" alone can no longer be trusted, the exact
90
126
  # way it silently wasn't the day this was found: three domains
91
127
  # booted together, one real source text read once (the domain's
92
- # own, ".first"'d), and every OTHER domain's era-1 held THAT text
128
+ # own, ".first"'d), and every other domain's era-1 held that text
93
129
  # under its own name — a later shadow-parse of it reconstructs a
94
130
  # completely different shape, and every boot after the first
95
131
  # refuses toward a scaffold that was never the real drift.
96
132
  #
97
- # A single-file directory whose one file names something ELSE
133
+ # A single-file directory whose one file names something else
98
134
  # falls back to it anyway (a fixture may legitimately name its
99
- # file differently from the `Hecks.bluebook` it declares) — UNLESS
100
- # this bluebook is a known framework member, in which case that
101
- # one file is certainly some OTHER domain's, not this one's, and
102
- # the framework registry is asked instead the only other place a
103
- # bluebook in this registry could have come from, per
104
- # `uses_framework`.
105
- def source_text_for(bluebook, directory)
135
+ # file differently from the `Hecks.bluebook` it declares) — unless
136
+ # this bluebook is a known framework member or a vendored
137
+ # embryonaut bluebook (see `vendored_source_for` below), in which
138
+ # case that one file is certainly some other domain's, not this
139
+ # one's, and the real registry is asked instead the only other
140
+ # two places a bluebook in this registry could have come from, per
141
+ # `uses_framework` and `uses_embryonaut_bluebook`.
142
+ #
143
+ # The bug this guards against, found live: `Framework.members` was
144
+ # the only exclusion checked here, so a domain attaching a
145
+ # vendored bluebook instead (`uses_embryonaut_bluebook`, which has
146
+ # no equivalent registry — see embryonaut_bluebook.rb's own
147
+ # header) fell straight through the single-file fallback: a
148
+ # directory holding exactly one `.bluebook` file (the target
149
+ # domain's own) handed that same text back for the vendored
150
+ # bluebook's era check too. PostgresEra minted era 1 for the
151
+ # vendored domain with the target's own source stamped as its
152
+ # `held_text` — the label computed from it, too — so the very
153
+ # next boot re-derived the vendored domain's real shape, found it
154
+ # didn't match what got (wrongly) stored, and refused to boot
155
+ # toward a scaffold for drift that never actually happened.
156
+ # `registry:` is what lets this check the one thing
157
+ # `Framework.members` cannot: whether some hecksagon in this
158
+ # registry declared `uses_embryonaut_bluebook` for this exact
159
+ # bluebook name, the same way `EmbryonautBluebook.load!` itself
160
+ # already resolves the vendored package's own directory.
161
+ #
162
+ # @param bluebook [Bluebook::Chapter] the bluebook whose source is wanted
163
+ # @param directory [String] path of the domain's own bluebook directory
164
+ # @param registry [Runtime::Registry, nil] the registry asked whether some hecksagon
165
+ # vendored this bluebook; nil skips the vendored lookup
166
+ # @return [String, nil] the UTF-8 text of every matching file joined with `"\n"`; nil
167
+ # when no source file could be found
168
+ def source_text_for(bluebook, directory, registry: nil)
106
169
  domain_files = Dir[File.join(directory, "*.bluebook")]
107
- own = domain_files.select do |path|
108
- File.foreach(path, encoding: "UTF-8").any? do |line|
109
- line.match?(/\A\s*Hecks\.bluebook\s+#{Regexp.escape(bluebook.name.inspect)}/)
110
- end
111
- end
112
- own = domain_files if own.empty? && domain_files.size == 1 && !Framework.members.key?(bluebook.name)
113
- own = [Framework.members[bluebook.name]].compact if own.empty?
170
+ own = domain_files.select { |path| declares_bluebook?(path, bluebook.name) }
171
+ own = fallback_source_files(bluebook, directory, domain_files, registry) if own.empty?
114
172
  return if own.empty?
115
173
 
116
174
  own.map { |path| File.read(path, encoding: "UTF-8") }.join("\n")
117
175
  end
118
176
 
177
+ # Scans a file for a `Hecks.bluebook "<name>"` line opening the named bluebook.
178
+ #
179
+ # @param path [String] path of the `.bluebook` file to scan, read as UTF-8
180
+ # @param bluebook_name [String] the declared bluebook name, matched in its `inspect`
181
+ # (double-quoted) spelling
182
+ # @return [Boolean] true when some line, after leading whitespace, starts with
183
+ # `Hecks.bluebook` followed by that quoted name
184
+ def declares_bluebook?(path, bluebook_name)
185
+ File.foreach(path, encoding: "UTF-8").any? do |line|
186
+ line.match?(/\A\s*Hecks\.bluebook\s+#{Regexp.escape(bluebook_name.inspect)}/)
187
+ end
188
+ end
189
+
190
+ # Picks the files to read for a bluebook no file in the domain directory declares.
191
+ #
192
+ # Only reached once nothing in the domain's own directory actually
193
+ # declares this bluebook by name. Three remaining sources, in
194
+ # order: a genuinely single-file domain directory whose one file
195
+ # just happens to name something else (but only when this
196
+ # bluebook isn't already known to come from somewhere else
197
+ # entirely — the exact guard the bug below was missing half of);
198
+ # a framework member; a vendored embryonaut bluebook.
199
+ #
200
+ # @param bluebook [Bluebook::Chapter] the bluebook whose source is wanted
201
+ # @param directory [String] path of the domain's own bluebook directory
202
+ # @param domain_files [Array<String>] paths of every `.bluebook` file in `directory`
203
+ # @param registry [Runtime::Registry, nil] the registry asked for a vendored name; nil
204
+ # skips the vendored lookup
205
+ # @return [Array<String>] file paths to read, in load order; `[]` when the bluebook
206
+ # comes from none of the three sources
207
+ def fallback_source_files(bluebook, directory, domain_files, registry)
208
+ vendored_name = registry && vendored_bluebook_name_for(registry, bluebook.name)
209
+ framework_path = Framework.members[bluebook.name]
210
+
211
+ if domain_files.size == 1 && !framework_path && !vendored_name
212
+ domain_files
213
+ elsif framework_path
214
+ [framework_path]
215
+ elsif vendored_name
216
+ vendored_source_for(directory, vendored_name)
217
+ else
218
+ []
219
+ end
220
+ end
221
+
222
+ # Recovers the vendored package name behind a bluebook, if some hecksagon vendored it.
223
+ #
224
+ # The name `uses_embryonaut_bluebook` was actually called with —
225
+ # recovered from whichever hecksagon in this registry recorded it
226
+ # (`HecksagonBuilder#uses_embryonaut_bluebook`'s own
227
+ # `@vendored_bluebooks`), matched the same way
228
+ # `EmbryonautBluebook.load!` itself decides idempotency: the
229
+ # vendored name Pascal-cases to this bluebook's own declared name.
230
+ # Nil for an ordinary bluebook nothing ever vendored.
231
+ #
232
+ # @param registry [Runtime::Registry] the registry whose hecksagons are searched
233
+ # @param bluebook_name [String] the bluebook's declared Pascal-case name
234
+ # @return [String, nil] the package name as written in `uses_embryonaut_bluebook`; nil
235
+ # when no hecksagon vendored a package whose name Pascal-cases to `bluebook_name`
236
+ def vendored_bluebook_name_for(registry, bluebook_name)
237
+ registry.hecksagons.each_value do |hecksagon|
238
+ match = hecksagon.vendored_bluebooks.find { |name| Naming.pascal(name) == bluebook_name }
239
+ return match if match
240
+ end
241
+ nil
242
+ end
243
+
244
+ # Lists the `.bluebook` files a vendored embryonaut package ships.
245
+ #
246
+ # The same path `EmbryonautBluebook.load!` itself resolves from —
247
+ # `<registry.root>/vendor/embryonaut_bluebooks/<name>/bluebook/`,
248
+ # rebuilt here from `directory` (the domain's own bluebook
249
+ # directory, always `registry.root`'s immediate child — see
250
+ # `Runtime::Loader.boot`) rather than threading `registry.root`
251
+ # through as a second parameter. Every `.bluebook` file the
252
+ # package ships, in the same `Dir.glob` order actually loaded at
253
+ # boot time — order matters here because this text is later
254
+ # re-parsed whole (`EraCheck::shadow`) to reconstruct the shape a
255
+ # held era claims, and that reconstruction must see the files in
256
+ # the order that actually built the live shape.
257
+ #
258
+ # @param directory [String] path of the domain's own bluebook directory, whose parent
259
+ # holds `vendor/embryonaut_bluebooks`
260
+ # @param name [String] the vendored package name, as `vendored_bluebook_name_for`
261
+ # returns it
262
+ # @return [Array<String>] paths of the package's `.bluebook` files in `Dir.glob` order;
263
+ # `[]` when the package directory is missing or empty
264
+ def vendored_source_for(directory, name)
265
+ dir = File.join(File.dirname(directory), "vendor", "embryonaut_bluebooks", name, "bluebook")
266
+ Dir.glob(File.join(dir, "*.bluebook"))
267
+ end
268
+
269
+ # Hands one bluebook to its adapter's `era_check!`, when that adapter carries eras.
270
+ #
271
+ # The bluebook's first aggregate is the anchor: its binding alone decides
272
+ # which adapter is asked, and a bluebook with no aggregates, or one bound
273
+ # to an adapter that is not lineage-capable, is skipped without a word.
274
+ #
275
+ # @param registry [Runtime::Registry] the registry being booted
276
+ # @param bluebook [Bluebook::Chapter] the bluebook to check
277
+ # @param current_text [String, nil] the bluebook's source, as `source_text_for` found
278
+ # it; nil means no source file was found
279
+ # @param directory [String, nil] the domain's bluebook directory, named in the
280
+ # missing-source refusal and passed through to the adapter
281
+ # @return [void]
282
+ # @raise [Runtime::WiringError] if the first aggregate's binding cannot be resolved,
283
+ # `current_text` is nil for a lineage-bound bluebook, or the adapter's `era_check!`
284
+ # refuses the boot
119
285
  def check_bluebook!(registry, bluebook, current_text, directory: nil)
120
286
  first = bluebook.aggregates.first
121
287
  return unless first
@@ -136,10 +302,19 @@ module Hecks
136
302
  )
137
303
  end
138
304
 
139
- # The per-rule capability gate NOT an era fact, and so it
305
+ # Refuses the boot when one bluebook's compute rule is bound away from Postgres.
306
+ #
307
+ # The per-rule capability gate — not an era fact, and so it
140
308
  # survives on every adapter: a compute rule's SQL is its only
141
309
  # implementation, so an aggregate carrying one cannot boot
142
310
  # anywhere but Postgres, whatever any shape comparison would say.
311
+ #
312
+ # @param registry [Runtime::Registry] the registry holding the declared translations
313
+ # and the aggregate's bindings
314
+ # @param bluebook [Bluebook::Chapter] the bluebook whose aggregates are checked
315
+ # @return [void]
316
+ # @raise [Runtime::WiringError] if an aggregate carrying a compute rule is bound to an
317
+ # adapter that is not lineage-capable, or its persistence binding cannot be resolved
143
318
  def check_compute_rules!(registry, bluebook)
144
319
  bluebook.aggregates.each do |aggregate|
145
320
  lineage = Ports::Persistence::Lineage.for(registry, bluebook.name, aggregate)
@@ -152,14 +327,32 @@ module Hecks
152
327
  end
153
328
  end
154
329
 
330
+ # Names the adapter an aggregate's authoritative persistence bind points at.
331
+ #
332
+ # @param registry [Runtime::Registry] the registry holding the domain's hecksagon
333
+ # @param domain [String, Symbol] name of the domain the aggregate belongs to
334
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose binding is resolved
335
+ # @return [String] the bound adapter's name, such as `"PostgresEra"`; `"Memory"` when
336
+ # the domain declares no hecksagon
337
+ # @raise [Runtime::WiringError] if the aggregate has no persistence bind, more than one
338
+ # authoritative bind, or a bind with a role the port does not support
155
339
  def adapter_for(registry, domain, aggregate)
156
340
  Ports::Persistence::BindingPolicy.resolve(registry, domain, aggregate).adapter
157
341
  end
158
342
 
159
- # The capability idiom: an adapter CLASS that answers
343
+ # Asks a named adapter whether it carries eras.
344
+ #
345
+ # The capability idiom: an adapter class that answers
160
346
  # lineage_capable? with true carries eras and may act on drift
161
347
  # (translate, fork, merge). Postgres alone does today; the seam is
162
348
  # what lets a second one arrive without touching this file.
349
+ #
350
+ # @param registry [Runtime::Registry] the registry whose `adapters` must list the name
351
+ # @param adapter_name [String] the adapter's name, as `adapter_for` returns it
352
+ # @return [Boolean] the adapter's own `lineage_capable?` answer; false when the registry
353
+ # does not list the adapter, the adapter does not respond to `lineage_capable?`, or
354
+ # any `StandardError` is raised on the way (including an adapter with no Ruby
355
+ # implementation)
163
356
  def lineage_capable?(registry, adapter_name)
164
357
  adapter_class = registry.adapters[adapter_name] && registry.adapter_class(adapter_name)
165
358
  adapter_class.respond_to?(:lineage_capable?) && adapter_class.lineage_capable?
@@ -6,16 +6,32 @@ module Hecks
6
6
  # from the IR it is handed, which is why the Postgres mint path can
7
7
  # call it too.
8
8
  module ShapeDiff
9
+ # Computes an aggregate's structural signature, for comparing two eras of it.
10
+ #
11
+ # @param aggregate [Bluebook::Aggregate] the aggregate to describe
12
+ # @return [Array<Array>] one `[name, signature]` pair per attribute, sorted by name;
13
+ # `name` is a Symbol and `signature` is whatever `attribute_signature` returns
9
14
  def shape(aggregate)
10
15
  aggregate.attributes.map do |attribute|
11
16
  [attribute.name, attribute_signature(aggregate, attribute.type)]
12
17
  end.sort_by(&:first)
13
18
  end
14
19
 
20
+ # Expands a declared type into a signature that exposes its members.
21
+ #
15
22
  # A plain type name for a primitive; `[type_name, member_signatures]`
16
23
  # for a value object or entity, walked recursively — so two
17
24
  # attributes with the same declared type name but a different
18
25
  # internal shape are never mistaken for unchanged.
26
+ #
27
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose value objects and entities
28
+ # are searched for `type_name`
29
+ # @param type_name [String, Bluebook::Reference] the attribute's declared type
30
+ # @param seen [Array<String, Bluebook::Reference>] types already being expanded, which
31
+ # stops a self-referencing type from recursing forever
32
+ # @return [String, Bluebook::Reference, Array] `type_name` itself for a primitive, a
33
+ # reference, or a type already in `seen`; otherwise `[type_name, members]`, where
34
+ # `members` is an Array of `[member_name, signature]` pairs sorted by member name
19
35
  def attribute_signature(aggregate, type_name, seen = [])
20
36
  container = nested_type(aggregate, type_name)
21
37
  return type_name if container.nil? || seen.include?(type_name)
@@ -26,13 +42,21 @@ module Hecks
26
42
  [type_name, members]
27
43
  end
28
44
 
45
+ # Looks a type name up among an aggregate's value objects, then its entities.
46
+ #
47
+ # @param aggregate [Bluebook::Aggregate] the aggregate to search
48
+ # @param type_name [String, Bluebook::Reference] the declared type to find
49
+ # @return [Bluebook::ValueObject, Bluebook::Entity, nil] the matching declaration; nil
50
+ # when the type is a primitive, a reference, or nothing this aggregate declares
29
51
  def nested_type(aggregate, type_name)
30
52
  aggregate.value_object(type_name) || aggregate.entities.find { |entity| entity.name == type_name }
31
53
  end
32
54
 
55
+ # Lists the new attributes that would leave an existing record with a required gap.
56
+ #
33
57
  # Attributes present now that the held shape never had — the
34
- # addition side of drift, which `uncovered_attributes` above never
35
- # looks at, since it only walks the HELD shape (vanish/retype).
58
+ # addition side of drift, which `uncovered_attributes` below never
59
+ # looks at, since it only walks the held shape (vanish/retype).
36
60
  # Most additions are free (ADR 0025, "Added attributes and
37
61
  # absence"): a default:, a list_of (frozen []), or a value object
38
62
  # whose fields all default fill an existing record automatically
@@ -40,11 +64,20 @@ module Hecks
40
64
  # non-optional attribute with no way to fill itself — can leave an
41
65
  # existing record with the field genuinely absent, and that is
42
66
  # what this reports: unfilled by a declared translation's own
43
- # `backfill`, OR by a move/convert that lands an OLD field inside
67
+ # `backfill`, or by a move/convert that lands an old field inside
44
68
  # this brand-new attribute (`Lineage#fills?` — the destination-
45
69
  # side question, never `explains?`'s source-side one, since a
46
70
  # rename from Crate to Bin can introduce a top-level attribute
47
71
  # name that never existed to have "vanished").
72
+ #
73
+ # @param aggregate [Bluebook::Aggregate] the aggregate as currently declared
74
+ # @param held_aggregate [Bluebook::Aggregate] the same aggregate as the held era's text
75
+ # declares it
76
+ # @param lineage [Ports::Persistence::Lineage, nil] the edge's rules for this aggregate; nil
77
+ # when the edge declares none, so nothing can fill an addition
78
+ # @return [Array<Symbol>] names of new, non-optional attributes with no default, no
79
+ # list cardinality, no fully-defaulted value object and no rule filling them; `[]`
80
+ # when every addition is safe
48
81
  def unsafe_additions(aggregate, held_aggregate, lineage)
49
82
  held_names = held_aggregate.attributes.map(&:name)
50
83
 
@@ -55,9 +88,17 @@ module Hecks
55
88
  .map(&:name)
56
89
  end
57
90
 
58
- # THE FOUR-ROW TABLE, as a predicate over the fourth row only the
91
+ # Decides whether a new attribute could be genuinely absent from an existing record.
92
+ #
93
+ # The four-row table, as a predicate over the fourth row only — the
59
94
  # other three (default:, list_of, a fully-defaulted value object)
60
95
  # all fill an existing record for free and never reach here.
96
+ #
97
+ # @param aggregate [Bluebook::Aggregate] the aggregate declaring the attribute, searched
98
+ # for the value object its type may name
99
+ # @param attribute [Bluebook::Attribute] the newly added attribute
100
+ # @return [Boolean] false when the attribute is optional, has a default, is a list, or
101
+ # is a value object whose every field has a default; true otherwise
61
102
  def possibly_absent?(aggregate, attribute)
62
103
  return false if attribute.optional?
63
104
  return false unless attribute.default.nil?
@@ -69,14 +110,24 @@ module Hecks
69
110
  true
70
111
  end
71
112
 
113
+ # Lists the held paths that vanished or changed type with no rule explaining them.
114
+ #
72
115
  # Paths the translation needs to explain: attributes that vanished
73
116
  # by name, attributes that kept their name but changed type (a
74
117
  # `convert` is what lets that be declared at all), and — recursing
75
- # into a same-named, same-typed value object — its OWN members
118
+ # into a same-named, same-typed value object — its own members
76
119
  # vanishing or changing type one level down, reported as a dotted
77
120
  # path ("price.currency"). A pure addition, at any depth, never
78
121
  # needs covering; only vanish-or-retype does, matching the
79
122
  # top-level rule at every depth.
123
+ #
124
+ # @param aggregate [Bluebook::Aggregate] the aggregate as currently declared
125
+ # @param held_aggregate [Bluebook::Aggregate] the same aggregate as the held era's text
126
+ # declares it
127
+ # @param lineage [Ports::Persistence::Lineage, nil] the edge's rules for this aggregate; nil
128
+ # when the edge declares none, so every vanished or retyped path is reported
129
+ # @return [Array<String>] bare attribute names and dotted member paths such as
130
+ # `"price.currency"`; `[]` when the whole diff is explained
80
131
  def uncovered_attributes(aggregate, held_aggregate, lineage)
81
132
  paths = held_aggregate.attributes.flat_map do |held_attribute|
82
133
  current_attribute = aggregate.attribute(held_attribute.name)
@@ -90,20 +141,37 @@ module Hecks
90
141
  paths.reject { |path| lineage.explains?(path) }
91
142
  end
92
143
 
93
- # `held_type`/`current_type` are type NAMES, resolved against each
94
- # side's OWN value_object AND entity declarations — neither is ever
144
+ # Compares one path's held and current types, recursing into shared members.
145
+ #
146
+ # `held_type`/`current_type` are type names, resolved against each
147
+ # side's own value_object and entity declarations — neither is ever
95
148
  # nested in the DSL, only in the type graph, so both are always
96
149
  # looked up flat off their respective aggregate. A `list_of` entity
97
- # is only reached here to DETECT a member vanish-or-retype; there is
150
+ # is only reached here to detect a member vanish-or-retype; there is
98
151
  # no per-element translation machinery yet (`move`/`convert`/`drop`
99
152
  # only reach into a single nested hash, not each element of an
100
153
  # array) — the only way to satisfy a refusal on an entity path
101
154
  # today is a top-level `drop` of the whole list attribute, which
102
155
  # `explains?` already recognizes as covering everything nested
103
156
  # under it. Blunt, but loud beats silent.
157
+ #
158
+ # @param path [String] the bare or dotted path being compared, reported on a mismatch
159
+ # @param held_type [String, Bluebook::Reference] the type the held era declares there
160
+ # @param current_type [String, Bluebook::Reference] the type declared there now
161
+ # @param held_aggregate [Bluebook::Aggregate] the held aggregate, searched for
162
+ # `held_type`'s declaration
163
+ # @param aggregate [Bluebook::Aggregate] the current aggregate, searched for
164
+ # `current_type`'s declaration
165
+ # @param lineage [Ports::Persistence::Lineage, nil] the edge's rules, asked whether a retype
166
+ # pairs the two type names; nil means no retype is accepted
167
+ # @param seen [Array<String, Bluebook::Reference>] current types already being
168
+ # expanded, which stops a self-referencing type from recursing forever
169
+ # @return [Array<String>] `[path]` when the types differ with no retype declared; the
170
+ # dotted paths of members that vanished or changed type beneath it; `[]` when
171
+ # nothing differs or either type is not a value object or entity
104
172
  def diff_type(path, held_type, current_type, held_aggregate, aggregate, lineage, seen = [])
105
173
  if held_type != current_type
106
- # A declared retype says the two TYPE names mean the same shape
174
+ # A declared retype says the two type names mean the same shape
107
175
  # — accept the pair, but still recurse into the members so a
108
176
  # member drift hiding beneath the rename is caught by name.
109
177
  return [path] unless lineage&.retype?(held_type, current_type)