hecks 1.4.0 → 1.5.1

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 (275) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +65 -0
  3. data/lib/hecks/adapters/driven/folder.rb +73 -0
  4. data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
  5. data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
  6. data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
  7. data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
  8. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  9. data/lib/hecks/adapters/driven/heki.rb +93 -2
  10. data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
  12. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  13. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  14. data/lib/hecks/adapters/driven/lambda/client.rb +35 -7
  15. data/lib/hecks/adapters/driven/lambda.rb +43 -2
  16. data/lib/hecks/adapters/driven/local_storage.rb +67 -1
  17. data/lib/hecks/adapters/driven/memory.rb +13 -13
  18. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
  19. data/lib/hecks/adapters/driven/prism.rb +46 -7
  20. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  21. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  22. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  23. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  24. data/lib/hecks/adapters/driven.rb +2 -0
  25. data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
  26. data/lib/hecks/behaviors/dsl.rb +58 -0
  27. data/lib/hecks/behaviors/expectations.rb +168 -7
  28. data/lib/hecks/behaviors/ir.rb +11 -0
  29. data/lib/hecks/behaviors/rspec.rb +8 -0
  30. data/lib/hecks/behaviors/runner.rb +19 -0
  31. data/lib/hecks/behaviors.rb +8 -0
  32. data/lib/hecks/bluebook/aggregate.rb +30 -0
  33. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
  34. data/lib/hecks/bluebook/assembly/build.rb +25 -4
  35. data/lib/hecks/bluebook/assembly/contract.rb +66 -14
  36. data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
  37. data/lib/hecks/bluebook/assembly/marks.rb +143 -14
  38. data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
  39. data/lib/hecks/bluebook/assembly.rb +25 -7
  40. data/lib/hecks/bluebook/attribute.rb +17 -3
  41. data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
  42. data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
  43. data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
  44. data/lib/hecks/bluebook/behaviour/command.rb +33 -3
  45. data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
  46. data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
  47. data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
  48. data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
  49. data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
  50. data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
  51. data/lib/hecks/bluebook/behaviour/query.rb +5 -0
  52. data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
  53. data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
  54. data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
  55. data/lib/hecks/bluebook/chapter.rb +23 -0
  56. data/lib/hecks/bluebook/command.rb +53 -8
  57. data/lib/hecks/bluebook/domain_port.rb +25 -0
  58. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
  59. data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
  60. data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
  61. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
  62. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
  63. data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
  64. data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
  65. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
  66. data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
  67. data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
  68. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
  69. data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
  70. data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
  71. data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
  72. data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
  73. data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
  74. data/lib/hecks/bluebook/entity.rb +29 -0
  75. data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
  76. data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
  77. data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
  78. data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
  79. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
  80. data/lib/hecks/bluebook/expression/resolver.rb +320 -77
  81. data/lib/hecks/bluebook/hexagon.rb +34 -0
  82. data/lib/hecks/bluebook/lifecycle.rb +11 -0
  83. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
  84. data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
  85. data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
  86. data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
  87. data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
  88. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
  89. data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
  90. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
  91. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
  92. data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
  93. data/lib/hecks/bluebook/meta_validator.rb +180 -84
  94. data/lib/hecks/bluebook/model_check.rb +268 -24
  95. data/lib/hecks/bluebook/pattern_subset.rb +23 -1
  96. data/lib/hecks/bluebook/process_manager.rb +13 -0
  97. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  98. data/lib/hecks/bluebook/project_loader.rb +40 -0
  99. data/lib/hecks/bluebook/project_register.rb +44 -0
  100. data/lib/hecks/bluebook/query.rb +27 -0
  101. data/lib/hecks/bluebook/read_model.rb +21 -1
  102. data/lib/hecks/bluebook/reference.rb +21 -8
  103. data/lib/hecks/bluebook/smoke_test.rb +29 -6
  104. data/lib/hecks/bluebook/synthesizer.rb +34 -0
  105. data/lib/hecks/bluebook/translation.rb +30 -1
  106. data/lib/hecks/bluebook/value_object.rb +23 -5
  107. data/lib/hecks/bluebook.rb +3 -4
  108. data/lib/hecks/codemod.rb +107 -20
  109. data/lib/hecks/construct.rb +15 -1
  110. data/lib/hecks/corpus.rb +146 -25
  111. data/lib/hecks/deploy/bluebook/deploy.bluebook +105 -0
  112. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  113. data/lib/hecks/deploy/oidc.json +5 -0
  114. data/lib/hecks/doc/reference.rb +185 -16
  115. data/lib/hecks/embryonaut_bluebook.rb +32 -9
  116. data/lib/hecks/facade/handle.rb +76 -3
  117. data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
  118. data/lib/hecks/forms/field_shape.rb +3 -0
  119. data/lib/hecks/forms/page.rb +14 -0
  120. data/lib/hecks/forms/port_argument.rb +12 -0
  121. data/lib/hecks/forms/query_form_renderer.rb +63 -0
  122. data/lib/hecks/forms/record_renderer.rb +58 -0
  123. data/lib/hecks/forms/record_table.rb +27 -0
  124. data/lib/hecks/forms/reference_options.rb +24 -0
  125. data/lib/hecks/forms/value_object_shape.rb +10 -0
  126. data/lib/hecks/fqn.rb +58 -0
  127. data/lib/hecks/framework/bluebook/compliance.bluebook +221 -0
  128. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  129. data/lib/hecks/framework/oidc.json +15 -0
  130. data/lib/hecks/framework.rb +43 -20
  131. data/lib/hecks/freezer.rb +17 -1
  132. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
  133. data/lib/hecks/fuzzing/combination_miner.rb +59 -0
  134. data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
  135. data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
  136. data/lib/hecks/fuzzing/differential.rb +34 -0
  137. data/lib/hecks/fuzzing/domain_generator.rb +188 -11
  138. data/lib/hecks/fuzzing/era_boundary.rb +45 -15
  139. data/lib/hecks/fuzzing/form_census.rb +86 -0
  140. data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
  141. data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
  142. data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
  143. data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
  144. data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
  145. data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
  146. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
  147. data/lib/hecks/fuzzing/properties/guards.rb +44 -0
  148. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
  149. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
  150. data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
  151. data/lib/hecks/fuzzing/properties/querying.rb +68 -14
  152. data/lib/hecks/fuzzing/properties.rb +24 -15
  153. data/lib/hecks/fuzzing/qa_settings.rb +12 -0
  154. data/lib/hecks/fuzzing/replay.rb +137 -29
  155. data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
  156. data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
  157. data/lib/hecks/fuzzing/self_consistency.rb +189 -40
  158. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
  159. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
  160. data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
  161. data/lib/hecks/fuzzing/shrinker.rb +114 -2
  162. data/lib/hecks/fuzzing/structural_skips.rb +18 -4
  163. data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
  164. data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
  165. data/lib/hecks/fuzzing/value_generator.rb +98 -10
  166. data/lib/hecks/grammar/evolve.rb +178 -2
  167. data/lib/hecks/grammar.rb +46 -0
  168. data/lib/hecks/ir.rb +38 -7
  169. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  170. data/lib/hecks/literal.rb +32 -0
  171. data/lib/hecks/naming.rb +88 -7
  172. data/lib/hecks/ports/access_control.rb +5 -10
  173. data/lib/hecks/ports/authorization.rb +3 -6
  174. data/lib/hecks/ports/identity_assignment.rb +1 -2
  175. data/lib/hecks/ports/identity_resolution.rb +1 -2
  176. data/lib/hecks/ports/key_vault.port +6 -0
  177. data/lib/hecks/ports/key_vault.rb +85 -0
  178. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
  179. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
  180. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
  181. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
  182. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
  183. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  184. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
  185. data/lib/hecks/ports/query/in_memory.rb +39 -6
  186. data/lib/hecks/ports/query/ordering.rb +15 -0
  187. data/lib/hecks/ports.rb +1 -0
  188. data/lib/hecks/projections/bootstrap_table.rb +43 -8
  189. data/lib/hecks/projections/deploy/fargate.rb +666 -0
  190. data/lib/hecks/projections/deploy/lambda.rb +2423 -0
  191. data/lib/hecks/projections/deploy/shared.rb +624 -0
  192. data/lib/hecks/projections/diagrams.rb +243 -7
  193. data/lib/hecks/projections/glossary/html.rb +88 -0
  194. data/lib/hecks/projections/glossary/markdown.rb +47 -0
  195. data/lib/hecks/projections/glossary/mermaid.rb +48 -0
  196. data/lib/hecks/projections/glossary/sentences.rb +105 -0
  197. data/lib/hecks/projections/glossary.rb +161 -19
  198. data/lib/hecks/projections/model/deviations.rb +44 -0
  199. data/lib/hecks/projections/model.rb +51 -1
  200. data/lib/hecks/projections/oidc.rb +40 -3
  201. data/lib/hecks/projections/parser_table.rb +49 -8
  202. data/lib/hecks/projections/reference.rb +12 -0
  203. data/lib/hecks/projections/rust_vocabulary.rb +219 -16
  204. data/lib/hecks/projections/shape.rb +8 -0
  205. data/lib/hecks/projections/statements.rb +63 -16
  206. data/lib/hecks/projections/vocabulary.rb +17 -0
  207. data/lib/hecks/projections.rb +3 -0
  208. data/lib/hecks/projector/cli_projector.rb +218 -10
  209. data/lib/hecks/projector/docs_projector.rb +145 -19
  210. data/lib/hecks/projector/exporter.rb +65 -11
  211. data/lib/hecks/projector/ir_projector.rb +6 -0
  212. data/lib/hecks/projector/narrate_projector.rb +136 -15
  213. data/lib/hecks/projector/target.rb +47 -10
  214. data/lib/hecks/projector.rb +101 -19
  215. data/lib/hecks/query_ir.rb +47 -0
  216. data/lib/hecks/query_specification/common/null_policy.rb +5 -3
  217. data/lib/hecks/rendering.rb +6 -0
  218. data/lib/hecks/router/namespace_installer.rb +13 -0
  219. data/lib/hecks/router.rb +55 -0
  220. data/lib/hecks/runtime/aggregate_lock.rb +9 -0
  221. data/lib/hecks/runtime/boot_gates.rb +18 -0
  222. data/lib/hecks/runtime/caller.rb +32 -0
  223. data/lib/hecks/runtime/capability_graph.rb +11 -0
  224. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
  225. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
  226. data/lib/hecks/runtime/command_interpreter.rb +42 -17
  227. data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
  228. data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
  229. data/lib/hecks/runtime/command_rules/references.rb +118 -28
  230. data/lib/hecks/runtime/dependency_planning.rb +45 -0
  231. data/lib/hecks/runtime/dispatcher.rb +28 -50
  232. data/lib/hecks/runtime/entity_element.rb +161 -8
  233. data/lib/hecks/runtime/entity_interpreter.rb +44 -9
  234. data/lib/hecks/runtime/errors.rb +18 -4
  235. data/lib/hecks/runtime/event.rb +10 -5
  236. data/lib/hecks/runtime/identity.rb +71 -3
  237. data/lib/hecks/runtime/instance.rb +67 -7
  238. data/lib/hecks/runtime/interpreting.rb +13 -5
  239. data/lib/hecks/runtime/invocation.rb +118 -36
  240. data/lib/hecks/runtime/loader.rb +94 -8
  241. data/lib/hecks/runtime/outbox.rb +145 -7
  242. data/lib/hecks/runtime/policy_interpreter.rb +22 -9
  243. data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
  244. data/lib/hecks/runtime/query_interpreter.rb +40 -12
  245. data/lib/hecks/runtime/reaction_invocation.rb +53 -8
  246. data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
  247. data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
  248. data/lib/hecks/runtime/reference_hop.rb +42 -0
  249. data/lib/hecks/runtime/refusal_wording.rb +50 -0
  250. data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
  251. data/lib/hecks/runtime/registry/verification.rb +119 -4
  252. data/lib/hecks/runtime/registry.rb +157 -4
  253. data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
  254. data/lib/hecks/runtime/routing.rb +27 -2
  255. data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
  256. data/lib/hecks/runtime/saga_interpreter.rb +27 -13
  257. data/lib/hecks/runtime/tenant_check.rb +26 -6
  258. data/lib/hecks/runtime/tenant_scope.rb +18 -0
  259. data/lib/hecks/runtime/value/coercion.rb +255 -33
  260. data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
  261. data/lib/hecks/runtime/value.rb +50 -0
  262. data/lib/hecks/runtime.rb +32 -0
  263. data/lib/hecks/storehouse.rb +305 -9
  264. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  265. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  266. data/lib/hecks/version.rb +1 -1
  267. data/lib/hecks.rb +79 -1
  268. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
  269. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
  270. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
  271. metadata +14 -5
  272. data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
  273. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
  274. data/lib/hecks/deprecation.rb +0 -95
  275. data/lib/hecks/framework/bluebook/compliance.bluebook +0 -1
@@ -32,6 +32,12 @@ module Hecks
32
32
  # `save` only runs when a projected value would differ from what
33
33
  # is already stored, so re-running a sweep with nothing having
34
34
  # moved on the target side touches the append log not at all.
35
+ #
36
+ # @param registry [Runtime::Registry] the booted registry to read repositories from
37
+ # @param domain [String] the domain the aggregate belongs to
38
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose `projected_fields` to
39
+ # refresh
40
+ # @return [Integer] the number of records actually changed and saved
35
41
  def call(registry, domain, aggregate)
36
42
  return 0 if aggregate.projected_fields.empty?
37
43
 
@@ -39,6 +45,16 @@ module Hecks
39
45
  repository.all.count { |record| refresh(registry, domain, aggregate, record, repository) }
40
46
  end
41
47
 
48
+ # Refreshes one record's own projected fields in place, saving it if any changed.
49
+ #
50
+ # @param registry [Runtime::Registry] the booted registry to read the target's
51
+ # repository from
52
+ # @param domain [String] the domain the aggregate belongs to
53
+ # @param aggregate [Bluebook::Aggregate] the aggregate `record` is an instance of
54
+ # @param record [Runtime::Instance] the record to refresh, mutated in place
55
+ # @param repository [Ports::Persistence::AppendOnly] the repository to save `record`
56
+ # through when it changes
57
+ # @return [Boolean] true if any projected field's value changed and `record` was saved
42
58
  def refresh(registry, domain, aggregate, record, repository)
43
59
  changed = false
44
60
 
@@ -55,10 +71,22 @@ module Hecks
55
71
  changed
56
72
  end
57
73
 
74
+ # Reads the current value of one projected field's remote target field.
75
+ #
58
76
  # `nil` when the reference itself does not resolve in this
59
77
  # chapter (a cross-domain target left "unfollowed" the same way
60
78
  # References#dereference already leaves one) or when the record
61
79
  # names no target at all — an optional reference nobody set.
80
+ #
81
+ # @param registry [Runtime::Registry] the booted registry to read the target's
82
+ # repository from
83
+ # @param domain [String] the domain the aggregate belongs to
84
+ # @param aggregate [Bluebook::Aggregate] the aggregate declaring `field`
85
+ # @param record [Runtime::Instance] the record holding the reference to follow
86
+ # @param field [Bluebook::ProjectedField] the projected field to resolve
87
+ # @return [Object, nil] the target record's own `field.remote_field` value; nil if the
88
+ # reference type does not resolve, the record names no target, or the target record
89
+ # cannot be found
62
90
  def remote_value(registry, domain, aggregate, record, field)
63
91
  target = aggregate.attribute(field.reference)&.type&.resolve
64
92
  return nil unless target
@@ -31,6 +31,21 @@ module Hecks
31
31
  module ReferenceHop
32
32
  module_function
33
33
 
34
+ # Folds every hop clause in `declared.wheres` into a synthetic local `in` clause.
35
+ #
36
+ # @param declared [Bluebook::Query, Runtime::TenantScope::Scoped,
37
+ # QuerySpecification::Common::Options] the declared query specification to fold hop
38
+ # clauses of
39
+ # @param args [Hash] the query's arguments, read when resolving each hop's own query
40
+ # @param registry [Runtime::Registry] the booted registry to resolve each hop's target
41
+ # repository from
42
+ # @param domain [String] the domain `aggregate` belongs to
43
+ # @param aggregate [Bluebook::Aggregate] the aggregate `declared` queries
44
+ # @return [Bluebook::Query, Runtime::TenantScope::Scoped, QuerySpecification::Common::
45
+ # Options, Hecks::Runtime::ReferenceHop::Folded] `declared` unchanged when it has no
46
+ # hop clauses; otherwise a `Folded` wrapper whose `#wheres` replaces each hop clause
47
+ # with its folded `in` clause
48
+ # @raise [Runtime::WiringError] if a hop's target no longer resolves (see `fold`)
34
49
  def apply(declared, args, registry:, domain:, aggregate:)
35
50
  hopped, local = declared.wheres.partition { |clause| QuerySpecification::HopPath.hop_head?(clause.field, aggregate.attributes) }
36
51
  return declared if hopped.empty?
@@ -39,6 +54,18 @@ module Hecks
39
54
  Folded.new(declared, local + folded)
40
55
  end
41
56
 
57
+ # Folds one hop clause into a synthetic `in` clause over the hop attribute's own ids.
58
+ #
59
+ # @param clause [QuerySpecification::Common::WhereClause] the hop clause to fold; its
60
+ # `field` names the hop path, dotted past the first segment
61
+ # @param args [Hash] the query's arguments, read when resolving the inner query
62
+ # @param registry [Runtime::Registry] the booted registry to resolve the hop's target
63
+ # repository from
64
+ # @param domain [String] the domain `aggregate` belongs to
65
+ # @param aggregate [Bluebook::Aggregate] the aggregate `clause` is declared against
66
+ # @return [QuerySpecification::Common::WhereClause] a synthetic `in` clause on the hop
67
+ # attribute's name, whose value is every id the inner clause admits on the target
68
+ # @raise [Runtime::WiringError] if the hop's target aggregate no longer resolves
42
69
  def fold(clause, args, registry:, domain:, aggregate:)
43
70
  step = QuerySpecification::HopPath.next_hop(clause.field, aggregate.attributes)
44
71
  hop, rest = step
@@ -70,6 +97,16 @@ module Hecks
70
97
  # aggregate is actually bound to (which may not be the engine
71
98
  # the outer aggregate is bound to at all) rather than by a
72
99
  # second reading of the comparators.
100
+ #
101
+ # @param domain [String] the domain `target` belongs to
102
+ # @param target [Bluebook::Aggregate] the hop's target aggregate to query
103
+ # @param wheres [Array<QuerySpecification::Common::WhereClause>] the inner clause(s) to
104
+ # run against `target`
105
+ # @param args [Hash] the outer query's arguments, read when resolving the inner query
106
+ # @param registry [Runtime::Registry] the booted registry to resolve `target`'s
107
+ # repository from
108
+ # @return [Array<String>] every distinct id the inner clause(s) admit on `target`
109
+ # @raise [Runtime::WiringError] if a hop nested inside `wheres` no longer resolves
73
110
  def matching_ids(domain, target, wheres, args, registry:)
74
111
  spec = apply(QuerySpecification::Common::Options.new(wheres: wheres), args,
75
112
  registry: registry, domain: domain, aggregate: target)
@@ -87,6 +124,11 @@ module Hecks
87
124
  # same reason: SimpleDelegator only intercepts calls made
88
125
  # directly on the wrapper.
89
126
  class Folded < SimpleDelegator
127
+ # @param declared [Bluebook::Query, Runtime::TenantScope::Scoped,
128
+ # QuerySpecification::Common::Options] the wrapped query specification, delegated to
129
+ # for everything but `#wheres`
130
+ # @param wheres [Array<QuerySpecification::Common::WhereClause>] the replacement
131
+ # where-clauses, hop clauses folded to synthetic `in` clauses
90
132
  def initialize(declared, wheres)
91
133
  super(declared)
92
134
  @wheres = wheres
@@ -31,9 +31,18 @@ module Hecks
31
31
 
32
32
  module_function
33
33
 
34
+ # Renders `refusal`/`site`'s template, substituting `values` verbatim.
35
+ #
34
36
  # Plain text substitution, never expression syntax — a template is
35
37
  # read, not evaluated. Values arrive already formatted; prefer
36
38
  # `render_site`, which formats them off the declared rows.
39
+ #
40
+ # @param refusal [String] the `DomainRefusal` class name, such as `"UnknownArgument"`
41
+ # @param site [String] the template site within `refusal`, such as `"unknown_args"`
42
+ # @param values [Hash{Symbol => #to_s}] each `{name}` placeholder's already-formatted
43
+ # replacement text
44
+ # @return [String] the rendered refusal message
45
+ # @raise [KeyError] if no `RefusalTemplate` row declares `refusal`/`site`
37
46
  def render(refusal, site, **values)
38
47
  substitute(template(refusal, site), values)
39
48
  end
@@ -46,6 +55,16 @@ module Hecks
46
55
  # RefusalWording.render_site("UnknownArgument", "unknown_args",
47
56
  # command: "Close", unknown: [:parcel], declared: [])
48
57
  # # => "Close does not declare parcel — it takes none"
58
+ #
59
+ # @param refusal [String] the `DomainRefusal` class name, such as `"UnknownArgument"`
60
+ # @param site [String] the template site within `refusal`, such as `"unknown_args"`
61
+ # @param arguments [Hash{Symbol => Object}] raw values, one per `RefusalSiteArgument`
62
+ # row declared for `refusal`/`site`; formatted per row before substitution
63
+ # @return [String] the rendered refusal message
64
+ # @raise [ArgumentError] if `arguments` is missing a declared argument or offers one
65
+ # `refusal`/`site` does not declare
66
+ # @raise [KeyError] if no `RefusalTemplate`/`RefusalSiteArgument` rows declare
67
+ # `refusal`/`site`
49
68
  def render_site(refusal, site, **arguments)
50
69
  specs = argument_rows(refusal, site)
51
70
  declared = specs.map { |spec| spec["argument"].to_sym }
@@ -62,6 +81,12 @@ module Hecks
62
81
  # `render_site` without the registry lookups: a template, its
63
82
  # argument rows, and raw values. The Rust projection calls this with
64
83
  # the chapter's own rows to compute the expected wording it pins.
84
+ #
85
+ # @param template [String] the raw template, `{name}` placeholders unsubstituted
86
+ # @param specs [Array<Hash>] the `RefusalSiteArgument` rows to format `arguments`
87
+ # against, one per declared argument name
88
+ # @param arguments [Hash{Symbol => Object}] raw values, one per entry in `specs`
89
+ # @return [String] the rendered refusal message
65
90
  def render_with(template, specs, arguments)
66
91
  values = specs.to_h do |spec|
67
92
  name = spec["argument"].to_sym
@@ -73,6 +98,12 @@ module Hecks
73
98
  # One argument, written the way its row says. A list is sorted first
74
99
  # (before quoting), then each item quoted, then joined; an empty list
75
100
  # reads `when_empty`. A scalar is quoted or taken as its own text.
101
+ #
102
+ # @param spec [Hash] the argument's `RefusalSiteArgument` row (`"shape"`, `"quoting"`,
103
+ # and, for a list, `"sorted"`, `"separator"`, `"when_empty"`)
104
+ # @param value [Object, Array] the raw value to format; an Array (or anything
105
+ # `Array()`-coercible) for a `"list"`-shaped spec, a scalar otherwise
106
+ # @return [String] the formatted text
76
107
  def format_argument(spec, value)
77
108
  inspect = spec.fetch("quoting") == "inspect"
78
109
  return inspect ? value.inspect : value.to_s unless spec.fetch("shape") == "list"
@@ -83,10 +114,22 @@ module Hecks
83
114
  items.empty? ? spec.fetch("when_empty") : items.join(spec.fetch("separator"))
84
115
  end
85
116
 
117
+ # Replaces each `{name}` placeholder in `template` with its value's text.
118
+ #
119
+ # @param template [String] the raw template, `{name}` placeholders unsubstituted
120
+ # @param values [Hash{Symbol => #to_s}] each placeholder name mapped to its
121
+ # replacement text
122
+ # @return [String] `template` with every `{name}` placeholder substituted
86
123
  def substitute(template, values)
87
124
  values.reduce(template) { |text, (key, value)| text.gsub("{#{key}}", value.to_s) }
88
125
  end
89
126
 
127
+ # Looks up the raw template text declared for one refusal site.
128
+ #
129
+ # @param refusal [String] the `DomainRefusal` class name, such as `"UnknownArgument"`
130
+ # @param site [String] the template site within `refusal`, such as `"unknown_args"`
131
+ # @return [String] the raw `RefusalTemplate` text, `{name}` placeholders unsubstituted
132
+ # @raise [KeyError] if no `RefusalTemplate` row declares `refusal`/`site`
90
133
  def template(refusal, site)
91
134
  TEMPLATES.fetch([refusal, site]) do
92
135
  raise KeyError, "no refusal template for #{refusal}/#{site} — declare it in " \
@@ -94,9 +137,16 @@ module Hecks
94
137
  end
95
138
  end
96
139
 
140
+ # The declared arguments for one refusal site.
141
+ #
97
142
  # Read lazily, not into a constant: bin/project_vocabulary boots
98
143
  # `hecks` (and so this file) before it writes the table a newly
99
144
  # declared site's rows live in.
145
+ #
146
+ # @param refusal [String] the `DomainRefusal` class name, such as `"UnknownArgument"`
147
+ # @param site [String] the template site within `refusal`, such as `"unknown_args"`
148
+ # @return [Array<Hash>] the `RefusalSiteArgument` rows declared for `refusal`/`site`
149
+ # @raise [KeyError] if no `RefusalSiteArgument` rows declare `refusal`/`site`
100
150
  def argument_rows(refusal, site)
101
151
  @argument_rows ||= Hecks::Vocabulary.rows("RefusalSiteArgument")
102
152
  .group_by { |row| [row["refusal"], row["site"]] }
@@ -40,6 +40,15 @@ module Hecks
40
40
  # state lands in). Double-checked locking against a dedicated mutex
41
41
  # — never `@saga_mutex` — see `Registry#initialize`'s own comment for
42
42
  # why reusing that one would deadlock.
43
+ # Resolves and memoizes the adapter a domain's sagas persist through.
44
+ #
45
+ # @param domain [String, Symbol] the domain to resolve saga persistence for
46
+ # @return [Adapters::Heki, Adapters::Postgres, Adapters::Sqlite, Adapters::D1,
47
+ # Ports::Persistence::Plugins::Era::PostgresEra, Ports::Persistence::NullSagaStore]
48
+ # the same adapter instance the domain's anchor aggregate persists through, when it
49
+ # implements `save_saga`; `NULL_SAGA_STORE` for a domain with no anchor aggregate, an
50
+ # adapter that does not implement the capability, a `RemoteRuntime`-shaped adapter,
51
+ # or a `Runtime::WiringError` resolving the anchor's own bind
43
52
  def saga_persistence(domain)
44
53
  key = domain.to_s
45
54
  @saga_persistence[key] || @saga_persistence_mutex.synchronize do
@@ -67,6 +76,8 @@ module Hecks
67
76
  # points inside `@saga_mutex.synchronize` blocks (`saga_interpreter.
68
77
  # rb`), which genuinely do and are guarded accordingly.
69
78
  # rubocop:disable-next Hecks/ThreadSharedIvarMutation
79
+ #
80
+ # @return [Hecks::Runtime::Registry] self
70
81
  def rehydrate_sagas!
71
82
  @hecksagons.each_key do |domain|
72
83
  saga_persistence(domain).each_saga do |process_manager, correlation, state, memory, completed_compensations = []|
@@ -10,9 +10,19 @@ module Hecks
10
10
  # loading, and the smaller checks are also called piecemeal by the
11
11
  # repository factory.
12
12
  module Verification
13
+ # Runs the whole wiring gate against this registry's loaded bluebooks,
14
+ # hexagons, ports and adapters.
15
+ #
16
+ # @return [Runtime::Registry] self
17
+ # @raise [Runtime::WiringError] if a bind names an undeclared aggregate, an
18
+ # adapter cannot satisfy its port's verb or declared `answers`, a world
19
+ # setting names a field its adapter does not declare, the default adapter
20
+ # is unusable, or a command declares a role with no authorization provider
21
+ # attached
13
22
  def verify!
14
23
  verify_default_adapter!
15
24
  verify_singleton_port_answers!
25
+ refuse_cross_package_bluebook_merge!
16
26
 
17
27
  @hecksagons.each_value do |hexagon|
18
28
  refuse_ungoverned_roles!(hexagon)
@@ -50,6 +60,13 @@ module Hecks
50
60
  self
51
61
  end
52
62
 
63
+ # Checks that the framework-wide default persistence adapter (used by any
64
+ # aggregate left out of an explicit bind list) is itself wired correctly.
65
+ #
66
+ # @return [Runtime::Registry] self
67
+ # @raise [Runtime::WiringError] if the default adapter cannot satisfy the
68
+ # persistence port's verb, is missing a declared `answers` method, or has
69
+ # no Ruby implementation
53
70
  def verify_default_adapter!
54
71
  name = Ports::Persistence::DEFAULT_ADAPTER
55
72
 
@@ -68,6 +85,14 @@ module Hecks
68
85
  "aggregate with no bind could not be given one: #{e.message}"
69
86
  end
70
87
 
88
+ # Checks that `bind`'s adapter implements the port it names and satisfies
89
+ # the verb the bind declares.
90
+ #
91
+ # @param bind [Bluebook::Bind] the bind to check
92
+ # @return [void]
93
+ # @raise [Runtime::WiringError] if `bind`'s adapter is unknown, declares an
94
+ # unknown port, is missing a declared `answers` method, or cannot satisfy
95
+ # `bind`'s own verb
71
96
  def check_verb(bind)
72
97
  port = port_for(bind)
73
98
  check_answers(port, bind.adapter)
@@ -85,6 +110,13 @@ module Hecks
85
110
  # actually call. `answers` is optional per port (an empty list is
86
111
  # today's pre-existing behavior, unchecked), so this only ever
87
112
  # tightens a port that opted in.
113
+ #
114
+ # @param port [Bluebook::Port] the port whose declared `answers` methods
115
+ # `adapter_name` must respond to
116
+ # @param adapter_name [String] the adapter's declared name to check
117
+ # @return [void]
118
+ # @raise [Runtime::WiringError] if `adapter_name` has no Ruby implementation,
119
+ # or its implementation does not respond to one of `port.answers`
88
120
  def check_answers(port, adapter_name)
89
121
  answers = Array(port.answers)
90
122
  return if answers.empty?
@@ -114,6 +146,12 @@ module Hecks
114
146
  # exactly one adapter, wired, missing a method `answers` names.
115
147
  PER_AGGREGATE_PORTS = %w[persistence projection loading].freeze
116
148
 
149
+ # Checks every singleton port (not per-aggregate-bound) with exactly one
150
+ # wired adapter against its own declared `answers` methods.
151
+ #
152
+ # @return [Runtime::Registry] self
153
+ # @raise [Runtime::WiringError] if a singleton port's one wired adapter is
154
+ # missing one of its declared `answers` methods
117
155
  def verify_singleton_port_answers!
118
156
  @ports.each_value do |port|
119
157
  next if PER_AGGREGATE_PORTS.include?(port.name)
@@ -127,6 +165,16 @@ module Hecks
127
165
  self
128
166
  end
129
167
 
168
+ # Checks that every setting `settings` declares (besides `:adapter`) is a
169
+ # field `bind`'s adapter actually admits.
170
+ #
171
+ # @param bind [Bluebook::Bind] the bind naming the adapter to check against;
172
+ # a no-op if its adapter is unknown
173
+ # @param settings [Hash{Symbol => Object}] the world's declared settings for
174
+ # this bind
175
+ # @return [void]
176
+ # @raise [Runtime::WiringError] if `settings` declares a field `bind`'s
177
+ # adapter does not declare
130
178
  def check_settings(bind, settings)
131
179
  adapter = @adapters[bind.adapter]
132
180
  return unless adapter
@@ -141,6 +189,12 @@ module Hecks
141
189
  "Add the field to the adapter, or remove it from the world."
142
190
  end
143
191
 
192
+ # Finds the port `bind`'s adapter declares.
193
+ #
194
+ # @param bind [Bluebook::Bind] the bind naming the adapter to look up
195
+ # @return [Bluebook::Port] the port `bind`'s adapter declares
196
+ # @raise [Runtime::WiringError] if `bind` names an unknown adapter, or one
197
+ # declaring an unknown port
144
198
  def port_for(bind)
145
199
  adapter = @adapters[bind.adapter]
146
200
  raise WiringError, "unknown adapter #{bind.adapter.inspect}" unless adapter
@@ -149,6 +203,12 @@ module Hecks
149
203
  raise(WiringError, "adapter #{bind.adapter} declares unknown port #{adapter.port.inspect}")
150
204
  end
151
205
 
206
+ # Finds the Ruby module implementing the adapter declared `name`.
207
+ #
208
+ # @param name [String] the adapter's declared name, such as `"PostgresEra"`
209
+ # @return [Module] the adapter module or class under `Hecks::Adapters`
210
+ # @raise [Runtime::WiringError] if no Ruby implementation named `name` exists
211
+ # under `Hecks::Adapters`
152
212
  def adapter_class(name)
153
213
  Adapters.const_get(name)
154
214
  rescue NameError
@@ -164,9 +224,9 @@ module Hecks
164
224
  # unchecked, exactly the defect ADR 0025 §9 names ("role gates
165
225
  # access control by exact string equality ... Governance ...
166
226
  # connected to none of it"). Checked here, at `verify!` — recovered
167
- # and moved, not new: this used to run per-block, at hecksagon
168
- # build time (Bluebook::DSL::HecksagonBuilder#build), which broke
169
- # the moment a domain could be split across multiple hecksagon
227
+ # and moved, not new: running this per-block, at hecksagon
228
+ # build time (Bluebook::DSL::HecksagonBuilder#build), breaks
229
+ # the moment a domain is split across multiple hecksagon
170
230
  # blocks (base + an `environments/<name>.hecksagon` overlay,
171
231
  # Runtime::Loader.boot's `environment:` — see its own comment for
172
232
  # the recovery provenance): every block but the one declaring
@@ -179,7 +239,7 @@ module Hecks
179
239
  # correct (a check against an incomplete, not-yet-merged
180
240
  # hecksagon can never see the real final shape).
181
241
  #
182
- # A provider is RECOGNISED by its declaration, not its name —
242
+ # A provider is recognised by its declaration, not its name —
183
243
  # `authorization_provider_for` answers for the domain's own
184
244
  # chapter too, so Governance (which declares `provides
185
245
  # "authorization"`) passes here because of what it declares, and
@@ -217,6 +277,61 @@ module Hecks
217
277
  bluebook_ir.aggregates.flat_map { |aggregate| aggregate.commands + aggregate.entities.flat_map(&:commands) }
218
278
  end
219
279
 
280
+ # TWO UNRELATED PACKAGES, ONE CHAPTER NAME BY COINCIDENCE — the
281
+ # real risk `Registry#bluebook_sources` exists to catch (found
282
+ # live: a stale `vendor/hecksagain` fork's own copy of Governance/
283
+ # Identity/Deploy, still reachable on 4 consuming apps' own load
284
+ # paths alongside the real gem). `BluebookBuilder.build`'s own
285
+ # accumulation (several files declaring the SAME chapter name ON
286
+ # PURPOSE — `lib/hecks/language/bluebook/*.bluebook` all open
287
+ # `Hecks.bluebook "Bluebook"`) is never touched here — that merge
288
+ # stays unconditional, checked only AFTER every file has loaded,
289
+ # the same "check the merged final result once" timing
290
+ # `refuse_ungoverned_roles!` already uses and for the same reason
291
+ # (a check against an incomplete load can never see the real
292
+ # shape). What distinguishes intentional accumulation from
293
+ # coincidence is PACKAGE ROOT, not file identity: files a real
294
+ # gemspec or a `vendor/` boundary already treats as one unit are
295
+ # expected to share a name; files from two DIFFERENT roots never
296
+ # legitimately do.
297
+ def refuse_cross_package_bluebook_merge!
298
+ @bluebook_sources.each do |name, paths|
299
+ roots = paths.map { |path| package_root_for(path) }.uniq
300
+ next if roots.size <= 1
301
+
302
+ raise WiringError,
303
+ "#{name.inspect} is declared by more than one package: #{roots.join(' and ')} — " \
304
+ "these are two unrelated sources sharing a chapter name by coincidence, not one " \
305
+ "domain split across files, and merging their declarations into one chapter is " \
306
+ "almost certainly a stale/vendored copy left on the load path (paths: " \
307
+ "#{paths.join(', ')})"
308
+ end
309
+ end
310
+
311
+ # THE NEAREST BOUNDARY A PATH ALREADY BELONGS TO — a real
312
+ # gemspec (this IS a package, whatever depends on it or vendors
313
+ # it), or a bare `vendor/` path component, treated as its OWN
314
+ # root regardless of what gemspec might sit above it: vendored
315
+ # code should never be considered "the same package" as whatever
316
+ # it's vendored into, even when nothing else marks the boundary.
317
+ # Neither found, the path's own directory is the root — two
318
+ # files with no closer marker only "belong together" if they are
319
+ # literally the same file.
320
+ def package_root_for(path)
321
+ return path.to_s if path.nil?
322
+
323
+ dir = File.dirname(File.expand_path(path))
324
+ loop do
325
+ return "vendor:#{dir}" if File.basename(dir) == "vendor"
326
+ return dir if Dir.glob(File.join(dir, "*.gemspec")).any?
327
+
328
+ parent = File.dirname(dir)
329
+ return dir if parent == dir
330
+
331
+ dir = parent
332
+ end
333
+ end
334
+
220
335
  # A domain that declares a `process_manager` but whose
221
336
  # `saga_persistence` resolves to `NULL_SAGA_STORE` (no anchor
222
337
  # aggregate, a RemoteRuntime-shaped adapter, an adapter that