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
@@ -31,20 +31,34 @@ module Hecks
31
31
  # holding it when they mint a brand-new record). See
32
32
  # `materialize_identity!` for why a composite identity needs it.
33
33
  #
34
- # `hydrate:` — ON BY DEFAULT, and every existing caller keeps getting
34
+ # `hydrate:` — on by default, and every existing caller keeps getting
35
35
  # exactly what it always got: `state` re-walked through
36
36
  # `hydrate_with_defaults` (declared defaults filled, every attribute
37
37
  # re-coerced through `Value.for_attribute`, an entity list's every
38
38
  # element rebuilt and re-validated). `false` is for exactly one
39
39
  # caller (`Adapters::Memory#build_instance`, judge-bootstrapping
40
40
  # only — see its own header) that already knows `state` needs none
41
- # of that: it is a shallow dup of an ALREADY-hydrated, ALREADY-
41
+ # of that: it is a shallow dup of an already-hydrated, already-
42
42
  # validated live `Instance`'s own state, not a raw value pulled off
43
43
  # a wire. Skipping the re-walk is what turns a `list_of` entity's Nth
44
44
  # save from O(N) (re-hydrating every element saved so far, for every
45
45
  # save) into O(1) — the quadratic cost `Adapters::Memory`'s own
46
46
  # header traces start to finish. `CodecBoundary.check_state!` still
47
47
  # runs either way ; only the re-hydration is skipped.
48
+ #
49
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct this
50
+ # record's state is declared by
51
+ # @param id [String, nil] this record's identity; nil for a record whose identity
52
+ # is not yet resolvable (e.g. an entity element hydrated before its own view exists)
53
+ # @param state [Hash{Symbol => Object}, nil] the record's stored (or partial)
54
+ # attribute values; nil for a brand-new record, hydrated entirely from declared
55
+ # defaults
56
+ # @param args [Hash{Symbol => Object}, nil] the original command payload, offered
57
+ # only by a fresh creation; materializes a composite identity's own head
58
+ # attributes
59
+ # @param hydrate [Boolean] whether `state` is re-walked through
60
+ # `hydrate_with_defaults`; false only for a caller that already knows `state` is
61
+ # already hydrated and validated
48
62
  def initialize(aggregate:, id:, state: nil, args: nil, hydrate: true)
49
63
  @aggregate = aggregate
50
64
  @id = id
@@ -68,6 +82,12 @@ module Hecks
68
82
  # written — arrives filled instead of nil. Only declared defaults
69
83
  # fill in; an attribute with no default stays absent, exactly as
70
84
  # stored.
85
+ #
86
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct whose
87
+ # declared attributes and defaults `state` is hydrated against
88
+ # @param state [Hash{Symbol => Object}] the raw stored (or partial) attribute values
89
+ # @return [Hash{Symbol => Object}] `state` coerced through every declared attribute,
90
+ # with any missing declared-default attribute filled in
71
91
  def self.hydrate_with_defaults(aggregate, state)
72
92
  hydrated = Value.hydrate(aggregate, state)
73
93
  defaults(aggregate).each do |name, value|
@@ -76,6 +96,13 @@ module Hecks
76
96
  hydrated
77
97
  end
78
98
 
99
+ # Builds a fresh record's starting state: every declared attribute's
100
+ # default value, an empty frozen Array for a `list_of` attribute, and
101
+ # the lifecycle field's declared starting value when `aggregate` has one.
102
+ #
103
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct whose
104
+ # declared attributes and lifecycle are read
105
+ # @return [Hash{Symbol => Object}] the default state, keyed by attribute name
79
106
  def self.defaults(aggregate)
80
107
  state = aggregate.attributes.to_h do |attr|
81
108
  # Frozen, like a list that has had something appended to it.
@@ -88,6 +115,16 @@ module Hecks
88
115
  state
89
116
  end
90
117
 
118
+ # Resolves one attribute's default value: its own declared `default:`
119
+ # if it has one, otherwise a value object built entirely from its own
120
+ # members' defaults, when every member declares one.
121
+ #
122
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct
123
+ # `attribute` belongs to
124
+ # @param attribute [Bluebook::Attribute] the attribute to resolve a default for
125
+ # @return [Runtime::Value, Object, nil] the coerced default value; nil when
126
+ # `attribute` declares no default and either names no value object or names
127
+ # one with a member that itself declares no default
91
128
  def self.default_for(aggregate, attribute)
92
129
  return Value.for_attribute(aggregate, attribute, attribute.default) unless attribute.default.nil?
93
130
  # An entity's members hydrate through the same path but an entity
@@ -100,10 +137,24 @@ module Hecks
100
137
  Value.build(value_object, {}, aggregate)
101
138
  end
102
139
 
140
+ # Reads one state field by name.
141
+ #
142
+ # @param name [String, Symbol] the declared attribute name to read
143
+ # @return [Object, nil] the field's current value; nil if `name` is not a key
144
+ # of `state`
103
145
  def [](name) = @state[name.to_sym]
104
146
 
147
+ # Reports whether `state` holds a value for `name`.
148
+ #
149
+ # @param name [String, Symbol] the declared attribute name to check
150
+ # @return [Boolean] true if `state` has a key for `name`
105
151
  def key?(name) = @state.key?(name.to_sym)
106
152
 
153
+ # Writes one state field by name.
154
+ #
155
+ # @param name [String, Symbol] the declared attribute name to write
156
+ # @param value [Object] the value to store
157
+ # @return [Object] `value`, unchanged
107
158
  def []=(name, value)
108
159
  @state[name.to_sym] = value
109
160
  end
@@ -118,6 +169,8 @@ module Hecks
118
169
  @state.key?(name) || super
119
170
  end
120
171
 
172
+ # Renders this record's wire/storage shape, state plus its identity.
173
+ #
121
174
  # `id: @id` last, not first — see Facade::Handle#to_h's own comment
122
175
  # for the full story (the same fix, landed there first): an
123
176
  # aggregate free to declare its own attribute literally named `id`
@@ -125,8 +178,13 @@ module Hecks
125
178
  # attribute's own wrapped value object sitting in `@state[:id]` —
126
179
  # merging `@state` on top of `{ id: @id }` let it silently clobber
127
180
  # the correct bare identity. `@id` merged last always wins.
181
+ #
182
+ # @return [Hash{Symbol => Object}] `state`, with `id` merged in last
128
183
  def to_h = @state.merge(id: @id)
129
184
 
185
+ # Copies this record, deep enough that a mutation on the copy cannot
186
+ # reach the original's own state.
187
+ #
130
188
  # A copy a mutation may touch. Every adapter but Memory hands `find`
131
189
  # a freshly-decoded Instance already; Memory's holds the record it
132
190
  # eventually saves — the same state Hash, aliased. Before `ensures`
@@ -139,6 +197,8 @@ module Hecks
139
197
  # record through this, never through the adapter's own return value
140
198
  # directly, so a refused ensures leaves the stored record untouched
141
199
  # regardless of which adapter is holding it.
200
+ #
201
+ # @return [Runtime::Instance] a copy of this record, with its own state Hash
142
202
  def dup
143
203
  copy = super
144
204
  copy.state = @state.dup
@@ -157,11 +217,11 @@ module Hecks
157
217
  # `identified_by` to fall back to `:id` for — `@aggregate.identified_by`
158
218
  # is nil the moment there is more than one head (`Behaviour::Identified
159
219
  # #derive_identity`), so the single-head branch below never runs for
160
- # it at all. A creating command that declares those heads as ordinary
161
- # attributes but doesn't also `sets` them (redundant with the identity
162
- # the command's own args already named) used to persist every head as
163
- # nil — the id correctly named the record, but the record's own
164
- # attributes forgot what named it.
220
+ # it at all. Without this, a creating command that declares those heads
221
+ # as ordinary attributes but doesn't also `sets` them (redundant with
222
+ # the identity the command's own args already named) would persist
223
+ # every head as nil — the id correctly naming the record, but the
224
+ # record's own attributes forgetting what named it.
165
225
  #
166
226
  # Filled from `args`, never from splitting `@id` back apart — the
167
227
  # same reason the single-head branch below won't guess a multi-path
@@ -8,11 +8,19 @@ module Hecks
8
8
  # in CommandInterpreter and EntityInterpreter, where they could only
9
9
  # ever drift.
10
10
  module Interpreting
11
+ # Ruby's module-inclusion hook. Gives `interpreter` its own `trace`
12
+ # accessor, since a class including this module needs its own copy
13
+ # rather than one shared across every interpreter.
14
+ #
11
15
  # Each including interpreter gets its own `trace` — set by a spec to
12
16
  # observe dispatch order (Vocabulary::AggregateDispatchOrder and
13
17
  # Vocabulary::EntityDispatchOrder in language/bluebook/vocabulary.bluebook);
14
18
  # nil in production, always — one array push and a nil check per step
15
19
  # is the entire cost of leaving this in.
20
+ #
21
+ # @param interpreter [Class] the class (`CommandInterpreter`, `EntityInterpreter`)
22
+ # including this module
23
+ # @return [void]
16
24
  def self.included(interpreter)
17
25
  interpreter.singleton_class.attr_accessor :trace
18
26
  end
@@ -32,9 +40,9 @@ module Hecks
32
40
  # read off its own generated Vocabulary::*DispatchOrder table —
33
41
  # vocabulary.bluebook, via lib/hecks/vocabulary.rb) by `send`ing
34
42
  # each declared step name against the including interpreter's own
35
- # `step_<name>` handler, in declared order. What used to be `call`'s own
36
- # literal sequence of method calls is now data driving that sequence —
37
- # tracing a real dispatch and comparing it to the declaration is
43
+ # `step_<name>` handler, in declared order. Data drives that sequence,
44
+ # rather than `call` spelling it out as a literal sequence of method
45
+ # calls — tracing a real dispatch and comparing it to the declaration is
38
46
  # tautological once `call` mechanically follows the declaration; a
39
47
  # conditional step (assign_creation_attributes, advance_lifecycle) still
40
48
  # has to guard itself at the top of its own handler and skip tracing
@@ -116,8 +124,8 @@ module Hecks
116
124
  # refuse_absent_arguments are separate DISPATCH_ORDER steps now (the
117
125
  # declared vocabulary lists all three as flat, sequential members, not
118
126
  # one nesting the other two), and EntityInterpreter never had them here
119
- # at all (an entity inherits its aggregate's own gate). One copy,
120
- # shared, rather than the two identical ones that used to drift.
127
+ # at all (an entity inherits its aggregate's own gate). One shared
128
+ # copy, rather than two identical ones that can only ever drift apart.
121
129
  def normalize_args(aggregate, command, args)
122
130
  coerce_declared_arguments(aggregate, command, args)
123
131
  end
@@ -9,7 +9,8 @@ module Hecks
9
9
  # (`target`, a `Routing::Envelope` or nil), and every fact the caller did
10
10
  # or did not offer. Built once per dispatch by `Invocation.from_call`, the
11
11
  # only place the runtime interprets the shape of a call (`to:` vs `with:`
12
- # vs loose keyword arguments, a port operation's reference attribute
12
+ # vs the flat facts hash `dispatch_flat` carries, a port operation's
13
+ # reference attribute
13
14
  # lifted into `to:`). `Runtime::Routing.envelope`/`.payload` delegate here
14
15
  # and no longer hold that logic themselves.
15
16
  #
@@ -21,12 +22,12 @@ module Hecks
21
22
  # - `Invocation::Present` — the key was offered with a value
22
23
  #
23
24
  # Keys are kept exactly as offered: `with:` keys are symbolized (as they
24
- # always were), loose keyword arguments keep whatever key the caller
25
- # used. Undeclared keys a caller offered are kept too — refusing them is
25
+ # always were), a flat facts hash keeps whatever key the wire used. Undeclared keys a caller
26
+ # offered are kept too — refusing them is
26
27
  # still `refuse_unknown_arguments`' job, a dispatch step, not this one.
27
28
  #
28
- # PR I1 changes no behavior: every interpreter still reads `ctx.args`,
29
- # which is `#to_args` — the same Hash `Routing.payload` used to return.
29
+ # I1 changes no behavior: every interpreter still reads `ctx.args`,
30
+ # which is `#to_args` — the same Hash `Routing.payload` returns.
30
31
  # Roadmap I2 moves the Ruby `decode_arguments` step onto `facts` itself.
31
32
  Invocation = Data.define(:verb, :target, :facts)
32
33
 
@@ -41,6 +42,7 @@ module Hecks
41
42
  # The class of the two frozen marker singletons below — never
42
43
  # instantiated anywhere else.
43
44
  class Marker
45
+ # @param name [String] the marker's own label, used by #inspect/#to_s
44
46
  def initialize(name)
45
47
  @name = name
46
48
  freeze
@@ -54,22 +56,51 @@ module Hecks
54
56
  Absent = Marker.new("Absent")
55
57
  Null = Marker.new("Null")
56
58
 
59
+ # @param verb [String] the fully qualified verb this invocation dispatches
60
+ # @param target [Routing::Envelope, nil] the resolved receiver, or nil when
61
+ # the facts carry the identity themselves
62
+ # @param facts [Hash{String, Symbol => Object}] every candidate fact, keyed by
63
+ # offered (or declared) name; frozen, deep-duped, before being stored
57
64
  def initialize(verb:, target:, facts:)
58
65
  super(verb: verb, target: target, facts: facts.dup.freeze)
59
66
  end
60
67
 
68
+ # Reads the fact recorded under `name`.
69
+ #
61
70
  # The fact recorded under `name` — `Absent` for a name never offered
62
71
  # and never declared either.
72
+ #
73
+ # @param name [String, Symbol] the fact name to look up
74
+ # @return [Invocation::Present, Invocation::Null, Invocation::Absent] the
75
+ # recorded marker for `name`
63
76
  def fact(name) = facts.fetch(name, Absent)
64
77
 
78
+ # Reports whether `name` was offered with a real, non-nil value.
79
+ #
80
+ # @param name [String, Symbol] the fact name to check
81
+ # @return [Boolean] true if `name` was offered with a non-nil value
65
82
  def present?(name) = fact(name).is_a?(Present)
83
+
84
+ # Reports whether `name` was offered with an explicit nil.
85
+ #
86
+ # @param name [String, Symbol] the fact name to check
87
+ # @return [Boolean] true if `name` was offered with an explicit nil
66
88
  def null?(name) = fact(name).equal?(Null)
89
+
90
+ # Reports whether `name` was never offered.
91
+ #
92
+ # @param name [String, Symbol] the fact name to check
93
+ # @return [Boolean] true if `name` was never offered (and, if declared, left out)
67
94
  def absent?(name) = fact(name).equal?(Absent)
68
95
 
69
96
  # A Present fact's value; nil for an explicit Null. Raises KeyError for
70
97
  # an Absent fact — "never offered" has no value, and answering nil
71
98
  # would re-conflate it with an explicit null, the very ambiguity this
72
99
  # type exists to remove. Ask `absent?`/`present?` first.
100
+ #
101
+ # @param name [String, Symbol] the fact name to read
102
+ # @return [Object, nil] the offered value; nil for an explicit Null fact
103
+ # @raise [KeyError] if `name` names an Absent fact (never offered)
73
104
  def value(name)
74
105
  case (found = fact(name))
75
106
  when Present then found.value
@@ -78,9 +109,12 @@ module Hecks
78
109
  end
79
110
  end
80
111
 
81
- # The legacy args hash, byte for byte what `Routing.payload` returned
112
+ # The flat args hash, byte for byte what `Routing.payload` returned
82
113
  # before this type existed: offered keys in offered order, Absent keys
83
114
  # omitted, Null keys mapped to nil. A fresh Hash every call.
115
+ #
116
+ # @return [Hash{String, Symbol => Object}] the offered facts by name; a fact
117
+ # offered as nil is kept as nil
84
118
  def to_args
85
119
  facts.each_with_object({}) do |(name, found), args|
86
120
  next if found.equal?(Absent)
@@ -90,7 +124,7 @@ module Hecks
90
124
  end
91
125
 
92
126
  class << self
93
- # **The one reading of a call's shape**. `receiver:` picks which of the
127
+ # The one reading of a call's shape. `receiver:` picks which of the
94
128
  # three dispatch shapes this is, because each has always checked its
95
129
  # parts in its own order and a malformed call's refusal depends on
96
130
  # that order:
@@ -107,24 +141,57 @@ module Hecks
107
141
  # the point in that order where the caller always resolved it.
108
142
  # `aggregate:` is the owning aggregate construct, read for `:port`
109
143
  # only.
110
- def from_call(verb, to:, with:, legacy:, receiver: :aggregate, entity_depth: 0, aggregate: nil, &declaring)
144
+ #
145
+ # @param verb [String] the fully qualified verb being dispatched
146
+ # @param to [String, Hash, nil] a bare aggregate identity, or a Hash with
147
+ # `aggregate:` and `entity:`/`entities:`; nil for no explicit receiver
148
+ # @param with [Hash, nil] the command's facts, keyed by attribute name; nil
149
+ # when the caller offers `flat` instead
150
+ # @param flat [Hash] the command's facts as a flat args hash, used when `with`
151
+ # is nil; for `:port`, may also carry the receiver identity under a
152
+ # reference attribute's own name
153
+ # @param receiver [Symbol] which of the three dispatch shapes this is:
154
+ # `:aggregate`, `:entity` or `:port`
155
+ # @param entity_depth [Integer] the number of entity-hop identities `to:`
156
+ # must carry, for `:entity` only
157
+ # @param aggregate [Bluebook::Aggregate, nil] the owning aggregate construct,
158
+ # read for `:port` only
159
+ # @yieldreturn [Class] the declaring command or port-operation class (anything
160
+ # with `hecks_name` and `attributes`)
161
+ # @return [Runtime::Invocation] the built invocation
162
+ # @raise [Runtime::TypeMismatch] if `to:` is malformed, its entity count is
163
+ # wrong, or (`:port` only) the operation resolves no receiving aggregate
164
+ # @raise [Runtime::UnknownArgument] if `with:` names an attribute the command
165
+ # does not declare
166
+ # @raise [Runtime::AbsentArgument] if `with:` omits a required attribute
167
+ # @raise [ArgumentError] if `receiver` is none of `:aggregate`, `:entity` or `:port`
168
+ def from_call(verb, to:, with:, flat:, receiver: :aggregate, entity_depth: 0, aggregate: nil, &declaring)
111
169
  case receiver
112
170
  when :aggregate
113
171
  command = declaring.call
114
- facts = facts_for(command, with: with, legacy: legacy)
172
+ facts = facts_for(command, with: with, flat: flat)
115
173
  new(verb: verb, target: route(to), facts: facts)
116
174
  when :entity
117
175
  target = route(to, entity_depth: entity_depth)
118
176
  command = declaring.call
119
- new(verb: verb, target: target, facts: facts_for(command, with: with, legacy: legacy))
177
+ new(verb: verb, target: target, facts: facts_for(command, with: with, flat: flat))
120
178
  when :port
121
- port_call(verb, aggregate, declaring.call, to: to, with: with, legacy: legacy)
179
+ port_call(verb, aggregate, declaring.call, to: to, with: with, flat: flat)
122
180
  else
123
181
  raise ArgumentError, "unknown receiver #{receiver.inspect}"
124
182
  end
125
183
  end
126
184
 
127
185
  # `to:` as a `Routing::Envelope`, or nil when no `to:` was given.
186
+ #
187
+ # @param to [String, Hash, nil] a bare aggregate identity, or a Hash with
188
+ # `aggregate:` and `entity:`/`entities:`; nil for no receiver
189
+ # @param entity_depth [Integer] the number of entity-hop identities `to`
190
+ # must carry
191
+ # @return [Routing::Envelope, nil] the resolved envelope, or nil when `to`
192
+ # is nil
193
+ # @raise [Runtime::TypeMismatch] if `to` is malformed, or its entity count
194
+ # does not match `entity_depth`
128
195
  def route(to, entity_depth: 0)
129
196
  return nil if to.nil?
130
197
 
@@ -146,8 +213,23 @@ module Hecks
146
213
  # The offered facts for `declaring`, as Absent/Null/Present — offered
147
214
  # keys first, in offered order, then every declared attribute that was
148
215
  # not offered, as Absent.
149
- def facts_for(declaring, with:, legacy:)
150
- offered = offered_facts(declaring, with: with, legacy: legacy)
216
+ #
217
+ # @param declaring [Class] the command or port-operation class (anything
218
+ # with `hecks_name` and `attributes`) whose declared attributes are read
219
+ # @param with [Hash, nil] the command's facts, keyed by attribute name; nil
220
+ # when the caller offers `flat` instead
221
+ # @param flat [Hash] the command's facts as a flat args hash, used when `with`
222
+ # is nil
223
+ # @return [Hash{String, Symbol => Invocation::Absent, Invocation::Null,
224
+ # Invocation::Present}] each candidate fact keyed by its offered (or
225
+ # declared) name
226
+ # @raise [Runtime::TypeMismatch] if both `with` and a non-empty `flat` are
227
+ # given, or if `with` is not a Hash
228
+ # @raise [Runtime::UnknownArgument] if `with:` names an attribute `declaring`
229
+ # does not declare
230
+ # @raise [Runtime::AbsentArgument] if `with:` omits a required attribute
231
+ def facts_for(declaring, with:, flat:)
232
+ offered = offered_facts(declaring, with: with, flat: flat)
151
233
  facts = offered.each_with_object({}) do |(name, value), found|
152
234
  found[name] = value.nil? ? Null : Present.new(value: value)
153
235
  end
@@ -160,10 +242,10 @@ module Hecks
160
242
 
161
243
  private
162
244
 
163
- # **The port operation shape** — formerly `Dispatcher#port_invocation`.
245
+ # **The port operation shape** — moved from `Dispatcher#port_invocation`.
164
246
  #
165
247
  # A Reference-typed attribute naming the owning aggregate is routing,
166
- # not a fact: lifted out of the loose kwargs into `to:` when no `to:`
248
+ # not a fact: lifted out of the flat facts into `to:` when no `to:`
167
249
  # was given. A `to:`-declared operation carries no Reference
168
250
  # attribute at all (PortOperationBuilder#initialize's own comment),
169
251
  # so its receiver is read — not removed — from the plain attribute
@@ -173,30 +255,30 @@ module Hecks
173
255
  # with attribute"; a real AbsentArgument confirmed this before `[]`
174
256
  # replaced `delete`). Composite identity is not attempted — `.first`
175
257
  # only, no domain in the corpus needs more for a port operation.
176
- def port_call(verb, aggregate, operation, to:, with:, legacy:)
177
- legacy = legacy.dup
258
+ def port_call(verb, aggregate, operation, to:, with:, flat:)
259
+ flat = flat.dup
178
260
  identity = operation.identity_attribute(aggregate.hecks_name)
179
- if to.nil? && identity && legacy.key?(identity.name)
180
- to = legacy.delete(identity.name)
261
+ if to.nil? && identity && flat.key?(identity.name)
262
+ to = flat.delete(identity.name)
181
263
  elsif to.nil? && operation.to == aggregate.hecks_name
182
264
  identity_name = Array(aggregate.identified_by).first
183
- to = legacy[identity_name] if identity_name && legacy.key?(identity_name)
265
+ to = flat[identity_name] if identity_name && flat.key?(identity_name)
184
266
  end
185
267
 
186
268
  target = route(to)
187
269
  raise TypeMismatch, "#{operation.hecks_name} requires its receiving aggregate in to:" unless target
188
270
 
189
- new(verb: verb, target: target, facts: facts_for(operation, with: with, legacy: legacy))
271
+ new(verb: verb, target: target, facts: facts_for(operation, with: with, flat: flat))
190
272
  end
191
273
 
192
274
  # BUG#7 — a non-Hash `to:` must be a String, matching Rust's
193
275
  # `RoutingEnvelope::from_json` (kernel/routing.rs), which refuses
194
276
  # anything neither a JSON string nor object before the domain payload
195
277
  # is examined. Surfaced on `examples/roster`'s `Mark`, whose own
196
- # attribute is literally named `to`: a flat-kwargs dispatch steals
197
- # that key into this parameter, and an out-of-range Integer used to be
198
- # accepted as the aggregate identity, leaving Mark's `to` fact absent
199
- # (AbsentArgument in Ruby, TypeMismatch in Rust). See
278
+ # attribute is literally named `to`: a flat-facts dispatch steals
279
+ # that key into this parameter, and without this check an out-of-range
280
+ # Integer would be accepted as the aggregate identity, leaving Mark's
281
+ # `to` fact absent (AbsentArgument in Ruby, TypeMismatch in Rust). See
200
282
  # spec/runtime/routing_envelope_shape_spec.rb.
201
283
  def scalar_envelope(to)
202
284
  return [to, []] if to.is_a?(String)
@@ -214,11 +296,11 @@ module Hecks
214
296
 
215
297
  # BUG#18 — an entity route naming no entity (`entities: []`, or
216
298
  # neither key) refuses here, unconditionally, before entity_depth is
217
- # consulted: for an aggregate-level command (depth 0) `[].size == 0`
218
- # used to satisfy the depth check and let the degenerate Hash reach
219
- # the command's own validation. Rust's `RoutingEnvelope::from_json`
220
- # always refused it at this point. A bare aggregate identity String
221
- # remains the ordinary aggregate-command shape.
299
+ # consulted: for an aggregate-level command (depth 0), without this,
300
+ # `[].size == 0` would satisfy the depth check and let the degenerate
301
+ # Hash reach the command's own validation. Rust's `RoutingEnvelope::
302
+ # from_json` always refused it at this point. A bare aggregate identity
303
+ # String remains the ordinary aggregate-command shape.
222
304
  def entity_identities(hash)
223
305
  raise TypeMismatch, "to: takes entity: or entities:, not both" if hash.key?(:entities) && hash.key?(:entity)
224
306
 
@@ -230,16 +312,16 @@ module Hecks
230
312
 
231
313
  # `with:` is deliberately strict: a caller choosing the explicit
232
314
  # envelope cannot smuggle receiver identity back into the payload,
233
- # and may not mix it with loose keyword arguments. Without `with:`
234
- # (nil or false) the loose keyword arguments are the facts, unread —
315
+ # and may not mix it with a flat facts hash. Without `with:`
316
+ # (nil or false) the flat facts are the facts, unread —
235
317
  # whether `to:` was given never enters this decision (BUG#17).
236
- def offered_facts(declaring, with:, legacy:)
237
- if with && !legacy.empty?
318
+ def offered_facts(declaring, with:, flat:)
319
+ if with && !flat.empty?
238
320
  raise TypeMismatch,
239
- "dispatch takes command facts in with:, not both with: and loose keyword arguments"
321
+ "dispatch takes command facts in with:, not both with: and a flat facts hash"
240
322
  end
241
323
 
242
- return legacy unless with
324
+ return flat unless with
243
325
  raise TypeMismatch, "with: must be a hash of command facts" unless with.is_a?(Hash)
244
326
 
245
327
  offered = with.transform_keys(&:to_sym)
@@ -37,6 +37,20 @@ module Hecks
37
37
  # its own env var name and passes the resulting string straight
38
38
  # through, e.g. `Hecks.boot(path, environment:
39
39
  # ENV.fetch("MYAPP_ENV", "development"))`.
40
+ #
41
+ # @param path [String] a domain directory to boot, resolved through
42
+ # `Ports::Loading#bluebook_directory`
43
+ # @param shared [String, nil] an explicit shared ports/adapters root override; nil
44
+ # resolves it by walking up from the domain directory
45
+ # @param install_facade [Boolean] whether to install the `Widget::Item.Add(...)`
46
+ # facade sugar
47
+ # @param environment [String, nil] the environment overlay to load after the
48
+ # domain's own files (e.g. `"production"`); nil loads none
49
+ # @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted dispatcher
50
+ # @raise [Errno::ENOENT] if `path` names neither a domain directory nor one holding
51
+ # a `bluebook/` subdirectory
52
+ # @raise [Runtime::WiringError] if a boot gate finds a wiring problem (an undeclared
53
+ # bind, a compute/rekey rule with no persistence plugin loaded, …)
40
54
  def self.boot(path, shared: nil, install_facade: true, environment: nil)
41
55
  loading = Ports::Loading.bootstrap
42
56
  directory = loading.bluebook_directory(path)
@@ -52,6 +66,7 @@ module Hecks
52
66
  run_boot_gates!(registry, directory)
53
67
  dispatcher = dispatcher_for(registry)
54
68
  redrive_outbox!(dispatcher)
69
+ seed_privacy_markings!(dispatcher, registry)
55
70
  install_facade ? bind_runtime(dispatcher) : dispatcher
56
71
  end
57
72
 
@@ -63,12 +78,43 @@ module Hecks
63
78
  # provably never started; `claimed` rows are surfaced, never
64
79
  # auto-redriven — see `Runtime::Outbox`. A remote dispatcher has
65
80
  # no local stores to scan.
81
+ #
82
+ # @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the
83
+ # just-booted dispatcher; a no-op unless it exposes an `outbox`
84
+ # @return [void]
66
85
  def self.redrive_outbox!(dispatcher)
67
86
  return unless dispatcher.respond_to?(:outbox)
68
87
 
69
88
  dispatcher.outbox.redrive!
70
89
  end
71
90
 
91
+ # Turns every `.hecksagon`-declared `AggregateDoor#mark_sensitive` fact into a real
92
+ # `Privacy::Marking.Mark` — after the dispatcher exists (dispatch needs a bound
93
+ # repository), idempotent across reboots (a marking already present is never
94
+ # re-dispatched, the same "provably never started" restraint `redrive_outbox!`
95
+ # holds itself to above). A no-op when nothing declared one, or when the domain
96
+ # never attached Privacy at all.
97
+ #
98
+ # @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the just-booted
99
+ # dispatcher `Privacy::Marking.Mark` dispatches through
100
+ # @param registry [Runtime::Registry] the just-booted registry `pending_privacy_markings`
101
+ # was recorded on
102
+ # @return [void]
103
+ def self.seed_privacy_markings!(dispatcher, registry)
104
+ return if registry.pending_privacy_markings.empty?
105
+ return unless registry.bluebook("Privacy")
106
+
107
+ already_marked_by_domain = registry.pending_privacy_markings.map { |marking| marking[:domain] }.uniq.to_h do |domain|
108
+ [domain, dispatcher.query("Privacy::Marking.ForDomain", domain: domain).map { |row| row[:attribute_path][:value] }]
109
+ end
110
+
111
+ registry.pending_privacy_markings.each do |marking|
112
+ next if already_marked_by_domain[marking[:domain]].include?(marking[:attribute_path])
113
+
114
+ dispatcher.dispatch_flat("Privacy::Marking.Mark", marking)
115
+ end
116
+ end
117
+
72
118
  # The explicit-file form — `paths` names the exact bluebook/hecksagon/
73
119
  # world files to boot, in place, wherever they actually live. `boot`
74
120
  # above only ever takes a directory and globs it; that is the right
@@ -91,6 +137,18 @@ module Hecks
91
137
  # first real path in `paths` — genuinely on disk, not a copy — so
92
138
  # every downstream path (`EraCheck`, `persisted_by`, `shared_root`)
93
139
  # resolves exactly as an ordinary directory boot's would.
140
+ #
141
+ # @param paths [Array<String>, String] the exact bluebook/hecksagon/world file paths
142
+ # to boot, in the order they should load
143
+ # @param shared [String, nil] an explicit shared ports/adapters root override; nil
144
+ # resolves it by walking up from `File.dirname` of the first path
145
+ # @param install_facade [Boolean] whether to install the `Widget::Item.Add(...)`
146
+ # facade sugar
147
+ # @param environment [String, nil] the environment overlay to load after the
148
+ # named files (e.g. `"production"`); nil loads none
149
+ # @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted dispatcher
150
+ # @raise [Runtime::WiringError] if a boot gate finds a wiring problem (an undeclared
151
+ # bind, a compute/rekey rule with no persistence plugin loaded, …)
94
152
  def self.boot_files(paths, shared: nil, install_facade: true, environment: nil)
95
153
  loading = Ports::Loading.bootstrap
96
154
  files = Array(paths).map { |path| File.expand_path(path) }
@@ -109,9 +167,11 @@ module Hecks
109
167
  install_facade ? bind_runtime(dispatcher) : dispatcher
110
168
  end
111
169
 
112
- # ADR 0031 — replaces two previously-hardcoded, unconditional calls
113
- # with a per-boot `BootGates` instance holding exactly the gates this
114
- # registry's own bound adapters have a capability for. Ordering is
170
+ # Runs every registered boot gate against `registry`, in order.
171
+ #
172
+ # ADR 0031 — a per-boot `BootGates` instance holds exactly the gates this
173
+ # registry's own bound adapters have a capability for, rather than two
174
+ # hardcoded, unconditional calls. Ordering is
115
175
  # preserved: era-checking (when a persistence plugin contributes one)
116
176
  # still runs before `verify!`, saga rehydration still runs after
117
177
  # (conservative — see `SagaPersistence#rehydrate_sagas!`'s own
@@ -124,6 +184,13 @@ module Hecks
124
184
  # `:post_verify` gates generically; `:saga_rehydration` is the one
125
185
  # gate core still registers directly, because ADR 0031 already
126
186
  # proved it's not era-specific.
187
+ #
188
+ # @param registry [Runtime::Registry] the registry mid-boot, with its bluebooks,
189
+ # hecksagons, ports, adapters and worlds already loaded
190
+ # @param directory [String] the domain directory the gates report against in a refusal
191
+ # @return [Runtime::BootGates] the gates instance built and run for this boot
192
+ # @raise [Runtime::WiringError] if `registry.verify!` or any contributed gate finds
193
+ # a wiring problem
127
194
  def self.run_boot_gates!(registry, directory)
128
195
  gates = BootGates.new
129
196
  Ports::Persistence.each_plugin { |plugin| plugin.contribute_boot_gates(registry, gates) }
@@ -149,6 +216,12 @@ module Hecks
149
216
  # the real, adapter-aware version of this check and refuses by name
150
217
  # ("...is bound to Memory") long before this ever would; this only
151
218
  # fires when nothing did, because nothing was loaded to.
219
+ #
220
+ # @param registry [Runtime::Registry] the registry mid-boot, whose declared
221
+ # `translations` are checked
222
+ # @return [void]
223
+ # @raise [Runtime::WiringError] if any translation declares a `computes`/`rekeys`
224
+ # rule while no persistence plugin is loaded to interpret it
152
225
  def self.check_compute_rules_backstop!(registry)
153
226
  return if Ports::Persistence.plugins_loaded?
154
227
 
@@ -183,6 +256,12 @@ module Hecks
183
256
  # this landed. A domain opts in explicitly, the same way
184
257
  # `persisted_by("PostgresEra")` is never inferred from anything
185
258
  # else either.
259
+ #
260
+ # @param registry [Runtime::Registry] the just-booted registry, whose just-booted
261
+ # domain's `.world` is checked for `dispatched_by("Lambda")`
262
+ # @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] a `RemoteDispatcher`
263
+ # bound to `registry` if the domain declares `dispatched_by("Lambda")`, a plain
264
+ # `Dispatcher` otherwise
186
265
  def self.dispatcher_for(registry)
187
266
  domain = registry.bluebooks.keys.first
188
267
  settings = registry.world(domain)&.for_verb("dispatched_by") || {}
@@ -191,11 +270,18 @@ module Hecks
191
270
  RemoteDispatcher.new(registry, region: settings.fetch(:region, "us-east-1"), function: settings[:function])
192
271
  end
193
272
 
194
- # The door is installed here, not stamped. This used to write the
195
- # dispatcher onto every aggregate's class (`ruby_class.runtime =`) — the
196
- # class-level global that made two boots in one process share one
197
- # name. The facade's modules close over this dispatcher instead, so the
198
- # binding lives in the surface a boot installs, not on anything shared.
273
+ # Installs the facade sugar (`Widget::Item.Add(...)`), closed over
274
+ # `dispatcher`, and hands `dispatcher` back.
275
+ #
276
+ # The door is installed here, not stamped: the facade's modules close
277
+ # over this dispatcher, so the binding lives in the surface a boot
278
+ # installs rather than in a class-level global (`ruby_class.runtime =`
279
+ # on every aggregate's class) that would make two boots in one process
280
+ # share one name.
281
+ #
282
+ # @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the
283
+ # just-booted dispatcher the facade should call into
284
+ # @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] `dispatcher`, unchanged
199
285
  def self.bind_runtime(dispatcher)
200
286
  Facade::Surface.install(dispatcher)
201
287
  dispatcher