hecks 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) 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.hecksagon +19 -0
  112. data/lib/hecks/doc/reference.rb +185 -16
  113. data/lib/hecks/embryonaut_bluebook.rb +32 -9
  114. data/lib/hecks/facade/handle.rb +76 -3
  115. data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
  116. data/lib/hecks/forms/field_shape.rb +3 -0
  117. data/lib/hecks/forms/page.rb +14 -0
  118. data/lib/hecks/forms/port_argument.rb +12 -0
  119. data/lib/hecks/forms/query_form_renderer.rb +63 -0
  120. data/lib/hecks/forms/record_renderer.rb +58 -0
  121. data/lib/hecks/forms/record_table.rb +27 -0
  122. data/lib/hecks/forms/reference_options.rb +24 -0
  123. data/lib/hecks/forms/value_object_shape.rb +10 -0
  124. data/lib/hecks/fqn.rb +58 -0
  125. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  126. data/lib/hecks/framework/oidc.json +15 -0
  127. data/lib/hecks/framework.rb +43 -20
  128. data/lib/hecks/freezer.rb +17 -1
  129. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
  130. data/lib/hecks/fuzzing/combination_miner.rb +59 -0
  131. data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
  132. data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
  133. data/lib/hecks/fuzzing/differential.rb +34 -0
  134. data/lib/hecks/fuzzing/domain_generator.rb +188 -11
  135. data/lib/hecks/fuzzing/era_boundary.rb +45 -15
  136. data/lib/hecks/fuzzing/form_census.rb +86 -0
  137. data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
  138. data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
  139. data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
  140. data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
  141. data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
  142. data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
  143. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
  144. data/lib/hecks/fuzzing/properties/guards.rb +44 -0
  145. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
  146. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
  147. data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
  148. data/lib/hecks/fuzzing/properties/querying.rb +68 -14
  149. data/lib/hecks/fuzzing/properties.rb +24 -15
  150. data/lib/hecks/fuzzing/qa_settings.rb +12 -0
  151. data/lib/hecks/fuzzing/replay.rb +137 -29
  152. data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
  153. data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
  154. data/lib/hecks/fuzzing/self_consistency.rb +189 -40
  155. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
  156. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
  157. data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
  158. data/lib/hecks/fuzzing/shrinker.rb +114 -2
  159. data/lib/hecks/fuzzing/structural_skips.rb +18 -4
  160. data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
  161. data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
  162. data/lib/hecks/fuzzing/value_generator.rb +98 -10
  163. data/lib/hecks/grammar/evolve.rb +178 -2
  164. data/lib/hecks/grammar.rb +46 -0
  165. data/lib/hecks/ir.rb +38 -7
  166. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  167. data/lib/hecks/literal.rb +32 -0
  168. data/lib/hecks/naming.rb +88 -7
  169. data/lib/hecks/ports/access_control.rb +5 -10
  170. data/lib/hecks/ports/authorization.rb +3 -6
  171. data/lib/hecks/ports/identity_assignment.rb +1 -2
  172. data/lib/hecks/ports/identity_resolution.rb +1 -2
  173. data/lib/hecks/ports/key_vault.port +6 -0
  174. data/lib/hecks/ports/key_vault.rb +58 -0
  175. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
  176. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
  177. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
  178. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
  179. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
  180. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  181. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
  182. data/lib/hecks/ports/query/in_memory.rb +39 -6
  183. data/lib/hecks/ports/query/ordering.rb +15 -0
  184. data/lib/hecks/ports.rb +1 -0
  185. data/lib/hecks/projections/bootstrap_table.rb +43 -8
  186. data/lib/hecks/projections/diagrams.rb +243 -7
  187. data/lib/hecks/projections/glossary/html.rb +88 -0
  188. data/lib/hecks/projections/glossary/markdown.rb +47 -0
  189. data/lib/hecks/projections/glossary/mermaid.rb +48 -0
  190. data/lib/hecks/projections/glossary/sentences.rb +105 -0
  191. data/lib/hecks/projections/glossary.rb +161 -19
  192. data/lib/hecks/projections/model/deviations.rb +44 -0
  193. data/lib/hecks/projections/model.rb +51 -1
  194. data/lib/hecks/projections/oidc.rb +40 -3
  195. data/lib/hecks/projections/parser_table.rb +49 -8
  196. data/lib/hecks/projections/reference.rb +12 -0
  197. data/lib/hecks/projections/rust_vocabulary.rb +219 -16
  198. data/lib/hecks/projections/shape.rb +8 -0
  199. data/lib/hecks/projections/statements.rb +63 -16
  200. data/lib/hecks/projections/vocabulary.rb +17 -0
  201. data/lib/hecks/projector/cli_projector.rb +218 -10
  202. data/lib/hecks/projector/docs_projector.rb +145 -19
  203. data/lib/hecks/projector/exporter.rb +65 -11
  204. data/lib/hecks/projector/ir_projector.rb +6 -0
  205. data/lib/hecks/projector/narrate_projector.rb +136 -15
  206. data/lib/hecks/projector/target.rb +29 -5
  207. data/lib/hecks/projector.rb +74 -6
  208. data/lib/hecks/query_ir.rb +47 -0
  209. data/lib/hecks/query_specification/common/null_policy.rb +5 -3
  210. data/lib/hecks/rendering.rb +6 -0
  211. data/lib/hecks/router/namespace_installer.rb +13 -0
  212. data/lib/hecks/router.rb +55 -0
  213. data/lib/hecks/runtime/aggregate_lock.rb +9 -0
  214. data/lib/hecks/runtime/boot_gates.rb +18 -0
  215. data/lib/hecks/runtime/caller.rb +32 -0
  216. data/lib/hecks/runtime/capability_graph.rb +11 -0
  217. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
  218. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
  219. data/lib/hecks/runtime/command_interpreter.rb +42 -17
  220. data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
  221. data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
  222. data/lib/hecks/runtime/command_rules/references.rb +118 -28
  223. data/lib/hecks/runtime/dependency_planning.rb +45 -0
  224. data/lib/hecks/runtime/dispatcher.rb +28 -50
  225. data/lib/hecks/runtime/entity_element.rb +161 -8
  226. data/lib/hecks/runtime/entity_interpreter.rb +44 -9
  227. data/lib/hecks/runtime/errors.rb +18 -4
  228. data/lib/hecks/runtime/event.rb +10 -5
  229. data/lib/hecks/runtime/identity.rb +71 -3
  230. data/lib/hecks/runtime/instance.rb +67 -7
  231. data/lib/hecks/runtime/interpreting.rb +13 -5
  232. data/lib/hecks/runtime/invocation.rb +118 -36
  233. data/lib/hecks/runtime/loader.rb +94 -8
  234. data/lib/hecks/runtime/outbox.rb +145 -7
  235. data/lib/hecks/runtime/policy_interpreter.rb +22 -9
  236. data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
  237. data/lib/hecks/runtime/query_interpreter.rb +40 -12
  238. data/lib/hecks/runtime/reaction_invocation.rb +53 -8
  239. data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
  240. data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
  241. data/lib/hecks/runtime/reference_hop.rb +42 -0
  242. data/lib/hecks/runtime/refusal_wording.rb +50 -0
  243. data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
  244. data/lib/hecks/runtime/registry/verification.rb +119 -4
  245. data/lib/hecks/runtime/registry.rb +157 -4
  246. data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
  247. data/lib/hecks/runtime/routing.rb +27 -2
  248. data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
  249. data/lib/hecks/runtime/saga_interpreter.rb +27 -13
  250. data/lib/hecks/runtime/tenant_check.rb +26 -6
  251. data/lib/hecks/runtime/tenant_scope.rb +18 -0
  252. data/lib/hecks/runtime/value/coercion.rb +255 -33
  253. data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
  254. data/lib/hecks/runtime/value.rb +50 -0
  255. data/lib/hecks/runtime.rb +32 -0
  256. data/lib/hecks/storehouse.rb +305 -9
  257. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  258. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  259. data/lib/hecks/version.rb +1 -1
  260. data/lib/hecks.rb +79 -1
  261. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
  262. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
  263. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
  264. metadata +11 -5
  265. data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
  266. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
  267. data/lib/hecks/deprecation.rb +0 -95
@@ -15,10 +15,10 @@ module Hecks
15
15
  # own header, and this repo's `.rubocop_todo.yml` for the merge that
16
16
  # made the split necessary). Cross-calls into `Coercion`'s own
17
17
  # `for_attribute`/`build`/`fields_for`/`value_object_for`/
18
- # `trusting_stored_state?` work unqualified here exactly as they did
19
- # before the split, because both modules land on the same `Value`
20
- # singleton class once extended — `self` never has to know which file
21
- # a sibling method actually lives in.
18
+ # `trusting_stored_state?` work unqualified here, exactly as they do
19
+ # within `Coercion` itself, because both modules land on the same
20
+ # `Value` singleton class once extended — `self` never has to know
21
+ # which file a sibling method actually lives in.
22
22
  module EntityListCoercion
23
23
  # S17, ADR 0026 — searches the whole entity tree, not only the
24
24
  # root's own direct children. `aggregate` here is always the
@@ -31,6 +31,12 @@ module Hecks
31
31
  # a nested entity — Dispatch, inside Handler — is not a direct
32
32
  # child of the root the way Handler itself is, and a plain
33
33
  # `aggregate.entities.find` stops one level short of it.
34
+ #
35
+ # @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct to
36
+ # search, and every entity nested under it
37
+ # @param name [String] the entity's declared `hecks_name` to find
38
+ # @return [Bluebook::Entity, nil] the matching entity anywhere in the tree, or
39
+ # nil if none matches
34
40
  def find_entity(construct, name)
35
41
  construct.entities.each do |candidate|
36
42
  return candidate if candidate.hecks_name == name
@@ -41,39 +47,54 @@ module Hecks
41
47
  nil
42
48
  end
43
49
 
50
+ # Hydrates a `list_of` attribute's offered value — each element of a
51
+ # whole-list offering, or a single remove-target value, depending on
52
+ # `attribute.type` and `value`'s own shape.
53
+ #
44
54
  # Frozen through: a list read back out of the store is an answer,
45
55
  # not a handle on what is stored.
46
56
  #
47
- # ADR 0047 — this used to bail (`return value unless entity`) the
48
- # moment `attribute.type` named a value object rather than an
49
- # entity, handing back the raw, un-hydrated argument untouched.
50
- # A `sets :field` mutation sourced from a whole-array argument (as
51
- # opposed to element-by-element `append:`) went straight through
57
+ # ADR 0047 — bailing (`return value unless entity`) the moment
58
+ # `attribute.type` names a value object rather than an entity would
59
+ # hand back the raw, un-hydrated argument untouched. A `sets :field`
60
+ # mutation sourced from a whole-array argument (as opposed to
61
+ # element-by-element `append:`) goes straight through
52
62
  # `for_attribute`'s `:list` branch, so `Banking::CardPayment.
53
- # Authorize`'s own `sets :tags` (`list_of(Tag)`) stored plain
63
+ # Authorize`'s own `sets :tags` (`list_of(Tag)`) would store plain
54
64
  # Ruby Hashes as its `tags` elements forever — never a real
55
65
  # `Value`, never through `Tag`'s own `pattern:`/`invariant`
56
66
  # checks. `remove:`'s `==` comparison (a real `Value` against a
57
- # raw `Hash`) then always failed, since `Hash#==` refuses anything
58
- # that isn't itself a compatible Hash — the bug ADR 0047 traces in
59
- # full. Delegating to `hydrate_value_object_list` below closes
60
- # that gap the same way the entity branch already worked: build a
61
- # real, validated `Value` per element, reusing `for_attribute`'s
62
- # own composite-construction path rather than inventing a second
63
- # one.
64
- # BUG#32 (QualityControl ledger) — `remove:`'s own single-target
65
- # value used to fall straight into `Array(value).map { ... }`
66
- # below unguarded, unlike this method's own delegated
67
- # `hydrate_value_object_list` sibling, whose `value.is_a?(Array)`
68
- # check exists for exactly this shape (see that method's own
69
- # comment). For an entity-typed list, `remove:`'s scalar target
70
- # (`Ledger.Void`'s `sequence: EntrySequence`) is never a Hash, so
71
- # `Array(2)` merely wrapped it as `[2]` rather than shredding it —
72
- # but the wrapping itself was still wrong: `MutationApplier#
73
- # removed`'s `element == value` then compared a stored `Entry`
74
- # Hash against a one-element Array, which can never be `==` a
75
- # Hash, so nothing was ever removed and nothing ever refused
76
- # either. Routed to `hydrate_entity_identity` instead, below.
67
+ # raw `Hash`) would then always fail, since `Hash#==` refuses
68
+ # anything that isn't itself a compatible Hash — the bug ADR 0047
69
+ # traces in full. Delegating to `hydrate_value_object_list` below
70
+ # closes that gap the same way the entity branch already works:
71
+ # build a real, validated `Value` per element, reusing
72
+ # `for_attribute`'s own composite-construction path rather than
73
+ # inventing a second one.
74
+ # BUG#32 (QualityControl ledger) — without this, `remove:`'s own
75
+ # single-target value would fall straight into
76
+ # `Array(value).map { ... }` below unguarded, unlike this method's
77
+ # own delegated `hydrate_value_object_list` sibling, whose
78
+ # `value.is_a?(Array)` check exists for exactly this shape (see
79
+ # that method's own comment). For an entity-typed list, `remove:`'s
80
+ # scalar target (`Ledger.Void`'s `sequence: EntrySequence`) is
81
+ # never a Hash, so `Array(2)` would merely wrap it as `[2]` rather
82
+ # than shredding it — but the wrapping itself would still be wrong:
83
+ # `MutationApplier#removed`'s `element == value` would then compare
84
+ # a stored `Entry` Hash against a one-element Array, which can
85
+ # never be `==` a Hash, so nothing would ever be removed and
86
+ # nothing would ever refuse either. Routed to `hydrate_entity_identity`
87
+ # instead, below.
88
+ #
89
+ # @param aggregate [Bluebook::Aggregate] the root aggregate `attribute` is
90
+ # declared on
91
+ # @param attribute [Bluebook::Attribute] the `list_of` attribute being hydrated
92
+ # @param value [Array<Hash, Object>, Object] the offered value: a whole-list
93
+ # Array (a `sets`/hydrate load) or a single remove-target value
94
+ # @return [Object] a frozen Array of hydrated elements when `value` is a
95
+ # whole-list Array and `attribute.type` names an entity; otherwise whatever
96
+ # `hydrate_entity_identity` (a non-Array value naming an entity) or
97
+ # `hydrate_value_object_list` (`attribute.type` naming no entity) returns
77
98
  def hydrate_entity_list(aggregate, attribute, value)
78
99
  entity = find_entity(aggregate, attribute.type.to_s)
79
100
  return hydrate_value_object_list(aggregate, attribute, value) unless entity
@@ -127,10 +148,23 @@ module Hecks
127
148
  # same pre-existing, narrower gap BUG#13 documented and left open,
128
149
  # not widened here.
129
150
  #
151
+ # Refuses a whole-list offering that misnames its own entities' identities —
152
+ # a duplicate identity across two elements, or an element missing one.
153
+ #
130
154
  # Skipped under `trusting_stored_state?`, the same guard `validate!`
131
155
  # already gives every value object (C6.3): a record already
132
156
  # written is trusted as it was, so tightening this check can never
133
157
  # make an old, already-persisted record unreadable.
158
+ #
159
+ # @param aggregate [Bluebook::Aggregate] the aggregate `entity` belongs to,
160
+ # named in a refusal
161
+ # @param entity [Bluebook::Entity] the entity type `elements` are shaped as
162
+ # @param elements [Array<Hash, Object>] the hydrated whole-list offering; a
163
+ # non-Hash element is skipped, not checked
164
+ # @return [void]
165
+ # @raise [Runtime::TypeMismatch] if `entity` declares a single-field identity
166
+ # and an element offers no value for it
167
+ # @raise [Runtime::AlreadyExists] if two elements offer the same identity value
134
168
  def check_entity_list_identities(aggregate, entity, elements)
135
169
  identity = entity.identified_by
136
170
  return unless identity
@@ -183,9 +217,20 @@ module Hecks
183
217
  # "nothing in this corpus needs it yet" boundary
184
218
  # `MutationApplier#check_entity_collision`'s own header already
185
219
  # draws for entity identity elsewhere in this runtime.
220
+ # Coerces a `remove:` target against an entity's own single-field
221
+ # identity type, rather than the entity's full shape.
222
+ #
186
223
  # `list_element_match?` treats an uncoerced value the same way
187
224
  # it always treated the pre-fix wrapped Array: never a match, a
188
225
  # documented no-op rather than a crash.
226
+ #
227
+ # @param aggregate [Bluebook::Aggregate] the aggregate `entity` belongs to
228
+ # @param entity [Bluebook::Entity] the entity type `value` addresses
229
+ # @param value [Object] the offered remove-target value
230
+ # @return [Runtime::Value, Object] `value` already coerced (returned unchanged);
231
+ # the value coerced against the identity field's declared type when `entity`
232
+ # has a single identity head and a matching attribute; `value` unchanged
233
+ # otherwise (no identity head, a composite identity, or no matching attribute)
189
234
  def hydrate_entity_identity(aggregate, entity, value)
190
235
  return value if value.is_a?(self)
191
236
 
@@ -198,6 +243,9 @@ module Hecks
198
243
  for_attribute(aggregate, field, value)
199
244
  end
200
245
 
246
+ # Hydrates a `list_of` attribute's offered value when `attribute.type`
247
+ # names a value object rather than an entity.
248
+ #
201
249
  # The value-object sibling of the entity branch above: an element
202
250
  # already shaped like the target `Value` (or a `Hash`/scalar that
203
251
  # `fields_for` can still open) is rebuilt through the same `build`
@@ -225,6 +273,17 @@ module Hecks
225
273
  # `value.is_a?(Array)` up front (true only for a genuine whole-list
226
274
  # `sets`/hydrate load) keeps the single-target shape a single
227
275
  # target, hydrated the same way, never listified.
276
+ #
277
+ # @param aggregate [Bluebook::Aggregate] the aggregate `attribute` is declared
278
+ # on, or a construct that does not respond to `value_object` (an entity), in
279
+ # which case `value` passes through unchanged
280
+ # @param attribute [Bluebook::Attribute] the `list_of` attribute being hydrated
281
+ # @param value [Array<Hash, Object>, Object] the offered value: a whole-list
282
+ # Array, or a single remove-target value
283
+ # @return [Object] `value` unchanged when `aggregate` does not respond to
284
+ # `value_object`, or `attribute.type` names no value object; otherwise a
285
+ # frozen Array of built `Value`s for a whole-list Array, or a single built
286
+ # `Value` (via `hydrate_value_object_element`) otherwise
228
287
  def hydrate_value_object_list(aggregate, attribute, value)
229
288
  return value unless aggregate.respond_to?(:value_object)
230
289
 
@@ -237,6 +296,19 @@ module Hecks
237
296
  Freezer.deep(hydrated)
238
297
  end
239
298
 
299
+ # Rebuilds one `list_of` element into a real, validated `Value`, unless it
300
+ # already is one of `value_object`'s own type.
301
+ #
302
+ # @param aggregate [Bluebook::Aggregate] the aggregate `value_object` is
303
+ # resolved against
304
+ # @param attribute [Bluebook::Attribute] the `list_of` attribute `element`
305
+ # belongs to
306
+ # @param value_object [Class] the `Bluebook::ValueObject` subclass `element`
307
+ # is built as
308
+ # @param element [Object] the raw element to build: a Hash of fields, a scalar
309
+ # `fields_for` can still open, or an already-built `Value`
310
+ # @return [Runtime::Value] `element` unchanged if it is already a `Value` of
311
+ # `value_object`'s own type; otherwise a freshly built one
240
312
  def hydrate_value_object_element(aggregate, attribute, value_object, element)
241
313
  return element if element.is_a?(self) && element.type_name == value_object.hecks_name
242
314
 
@@ -36,26 +36,64 @@ module Hecks
36
36
  # identity to change over — `with` already answers a new one rather
37
37
  # than mutating — so freezing it through is what it always claimed
38
38
  # to be.
39
+ #
40
+ # @param value_object [Bluebook::ValueObject] the declared type this instance is one of
41
+ # @param fields [Hash] the type's fields, keyed by name (String or Symbol); deep-frozen
42
+ # and stored with Symbol keys
39
43
  def initialize(value_object, fields)
40
44
  @value_object = value_object
41
45
  @fields = Freezer.deep(fields.transform_keys(&:to_sym))
42
46
  freeze
43
47
  end
44
48
 
49
+ # Reads the declared type's own name.
50
+ #
51
+ # @return [String] the value object's `hecks_name`
45
52
  def type_name = @value_object.hecks_name
53
+
54
+ # Reads one field.
55
+ #
56
+ # @param field [String, Symbol] the field name; `:value` reads the sole field of a
57
+ # single-attribute value object, whatever it is actually named
58
+ # @return [Object, nil] the field's coerced value; nil if the field is not held
46
59
  def [](field) = @fields[resolve_field(field)]
60
+
61
+ # Answers whether this value object holds the named field.
62
+ #
63
+ # @param field [String, Symbol] the field name; `:value` resolves the same way `[]` does
64
+ # @return [Boolean] true when the field is held
47
65
  def key?(field) = @fields.key?(resolve_field(field))
48
66
  def to_h = @fields.transform_values { |value| self.class.materialize(value) }
67
+
68
+ # Renders this value object as JSON, through the same shape `to_h` builds.
69
+ #
70
+ # @return [String] a JSON object of the materialized fields
49
71
  def to_json(*) = JSON.generate(to_h)
50
72
 
51
73
  def ==(other)
52
74
  other.is_a?(self.class) && other.type_name == type_name && other.to_h == to_h
53
75
  end
54
76
 
77
+ # Builds a new value object of the same type with one field replaced, re-validated.
78
+ #
79
+ # @param field [String, Symbol] the field to replace; `:value` resolves the same way
80
+ # `[]` does
81
+ # @param value [Object] the field's new, uncoerced value
82
+ # @return [Runtime::Value] a new instance of the same type, with `field` replaced
83
+ # @raise [Runtime::TypeMismatch] if the new fields do not satisfy the type's declared
84
+ # shape (an unknown field, a missing required one, a wrong numeric type, …)
85
+ # @raise [Runtime::InvariantViolation] if the new fields violate one of the type's own
86
+ # invariants, or are not a member of its closed set
55
87
  def with(field, value)
56
88
  self.class.build(@value_object, @fields.merge(resolve_field(field) => value))
57
89
  end
58
90
 
91
+ # Recursively converts a `Runtime::Value` (and any nested inside a Hash or Array) to
92
+ # plain data.
93
+ #
94
+ # @param value [Object] the value to materialize; anything that is not a `Runtime::Value`,
95
+ # Array or Hash passes through unchanged
96
+ # @return [Object] `value` with every nested `Runtime::Value` replaced by its own `to_h`
59
97
  def self.materialize(value)
60
98
  case value
61
99
  when self then value.to_h
@@ -77,6 +115,12 @@ module Hecks
77
115
  # used only for a `group_by`-declared head's own rows — grouping
78
116
  # needs a real scalar to key by regardless, so a report already
79
117
  # asking for that gets the unwrap for free.
118
+ #
119
+ # @param value [Object] the value to materialize; anything that is not a `Runtime::Value`,
120
+ # Array or Hash passes through unchanged
121
+ # @return [Object] `value` with every nested `Runtime::Value` replaced by its own sole
122
+ # field's value (recursively unwrapped), or by its own `Hash` of fields when it has
123
+ # more than one
80
124
  def self.materialize_unwrapped(value)
81
125
  case value
82
126
  when self
@@ -112,6 +156,12 @@ module Hecks
112
156
  # started guessing domain semantics would need to keep guessing
113
157
  # forever, once per shape of "gone" any caller ever invents. The
114
158
  # caller filters and sorts the result; this only groups it.
159
+ #
160
+ # @param rows [Array<Runtime::Value>] a `list_of` attribute's own elements, in append order
161
+ # @param key [Symbol] the method to call on each row to find "the same logical thing"
162
+ # across entries (typically a field reader)
163
+ # @return [Array<Runtime::Value>] one row per distinct `key` value, each the latest row
164
+ # that had it
115
165
  def self.latest_by(rows, key)
116
166
  rows.to_h { |row| [row.public_send(key), row] }.values
117
167
  end
data/lib/hecks/runtime.rb CHANGED
@@ -68,17 +68,40 @@ module Hecks
68
68
 
69
69
  # Load a bluebook directory and return the Dispatcher bound to it.
70
70
  # `install_facade:`, `environment:` — see Loader.boot.
71
+ #
72
+ # @param path [String] path to a domain directory, or a file inside one
73
+ # @param shared [String, nil] a shared-root override; see `Loader.boot`
74
+ # @param install_facade [Boolean] whether to install the `Widget::Item.Add`-style
75
+ # Ruby facade constants for this boot
76
+ # @param environment [String, nil] the environment name passed through to
77
+ # `Adapters::Folder#load_domain`
78
+ # @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the dispatcher bound
79
+ # to the booted domain
71
80
  def boot(path, shared: nil, install_facade: true, environment: nil)
72
81
  Loader.boot(path, shared: shared, install_facade: install_facade, environment: environment)
73
82
  end
74
83
 
75
84
  # `paths` form — see Loader.boot_files.
85
+ #
86
+ # @param paths [String, Array<String>] one or more file paths within the domain
87
+ # to load, instead of the whole directory
88
+ # @param shared [String, nil] a shared-root override; see `Loader.boot_files`
89
+ # @param install_facade [Boolean] whether to install the `Widget::Item.Add`-style
90
+ # Ruby facade constants for this boot
91
+ # @param environment [String, nil] the environment name passed through to the
92
+ # selected-file loader
93
+ # @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the dispatcher bound
94
+ # to the booted domain
76
95
  def boot_files(paths, shared: nil, install_facade: true, environment: nil)
77
96
  Loader.boot_files(paths, shared: shared, install_facade: install_facade, environment: environment)
78
97
  end
79
98
 
80
99
  # Bind the ambient registry for the duration of the block, restoring
81
100
  # whatever was there before. Nesting is safe ; a raise still restores.
101
+ #
102
+ # @param registry [Runtime::Registry] the registry to make current for the block
103
+ # @yield the code that should see `registry` as `current_registry`
104
+ # @return [Object] the block's result
82
105
  def with_registry(registry)
83
106
  previous = @current_registry
84
107
  @current_registry = registry
@@ -104,6 +127,15 @@ module Hecks
104
127
  # `RoleAssignment`'s own `scope`, not just its `role_name`. See
105
128
  # `Runtime::Caller::Current`'s own header for why this lives here
106
129
  # rather than as a command-level DSL construct.
130
+ # @param role [String, Symbol] the role to check the caller against
131
+ # @param actor_id [String, nil] who is calling, checked against a real Governance
132
+ # `RoleAssignment` when given; string-equality only against `role` when nil
133
+ # @param as_of [Integer, nil] Unix epoch seconds to check a matching `RoleAssignment`'s
134
+ # own `starts_at` against; unchecked when nil
135
+ # @param scope [String, nil] the scope to check a matching `RoleAssignment`'s own
136
+ # `scope` against; unchecked when nil
137
+ # @yield the code to run with this caller bound
138
+ # @return [Object] the block's result
107
139
  def as_caller(role:, actor_id: nil, as_of: nil, scope: nil, &)
108
140
  Caller.as(role: role, actor_id: actor_id, as_of: as_of, scope: scope, &)
109
141
  end