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
@@ -8,11 +8,11 @@ module Hecks
8
8
  # What a construct needs that the language cannot say, and how a claim that a
9
9
  # field needs no assembling is checked.
10
10
  #
11
- # `derived:` used to be a list of names, and a list of names is a promise with
12
- # nobody holding it. The coverage gate only asked whether a field was accounted
13
- # for — so writing `derived: %i[version]` would have satisfied it while dropping
14
- # a chapter's version in silence, which is the exact shape of every defect this
15
- # arc has found. Naming a field derived is a claim, and a claim needs a kind:
11
+ # Naming a field derived is a claim, and a claim needs a kind, not just a name
12
+ # in a list. A bare list of names is a promise with nobody holding it: the
13
+ # coverage gate only asks whether a field is accounted for, so `derived:
14
+ # %i[version]` would satisfy it while dropping a chapter's version in
15
+ # silence — the exact shape of every defect this arc has found.
16
16
  #
17
17
  # :parent the containment tree supplies it — a `*_id`, or one of
18
18
  # the named pointers below. Checked against the name.
@@ -32,6 +32,10 @@ module Hecks
32
32
  # shape, because a list needs a reader per element and a folded field is
33
33
  # gathered rather than fetched. Same pattern as `rows`, in the other
34
34
  # direction: declare the exceptions, default the rest.
35
+ #
36
+ # @param key [String, Symbol] the field's declaration key, as `fields` names it
37
+ # @return [Symbol, Array, nil] the reader named in `reads` for `key`, or `nil`
38
+ # when the field reads through the default single-cell reader
35
39
  def reader(key) = Hash(reads)[key.to_sym]
36
40
 
37
41
  # How an appendable list becomes rows the walk can offer. A list absent from
@@ -39,10 +43,24 @@ module Hecks
39
43
  # because the IR keeps a shape the language does not — a transition whose
40
44
  # `from` is a list is several rows, an append binds several fields at once,
41
45
  # an open map is one row per entry.
46
+ #
47
+ # @param list [String, Symbol] the appendable list's field name
48
+ # @return [Symbol, nil] the shaper method named in `rows` for `list`, or `nil`
49
+ # when the list reads straight off the node instead
42
50
  def shaper(list) = Hash(rows)[list.to_sym]
43
51
 
52
+ # Whether `field` is one this contract accounts for, stored or derived.
53
+ #
54
+ # @param field [Symbol] the field name to check
55
+ # @return [Boolean] whether `field` is a key of `fields` or of `derived`
44
56
  def declares?(field) = fields.key?(field) || derived.key?(field)
45
57
 
58
+ # Names the kind of derived claim `field` makes, if any.
59
+ #
60
+ # @param field [Symbol] the derived field name to check
61
+ # @return [Symbol, Array, nil] the kind `derived` names for `field` — `:parent`,
62
+ # `:children`, `:elsewhere`, `:walk`, an `[:computed, method]` or `[:folded,
63
+ # object, member]` pair — or `nil` when `field` is not derived
46
64
  def kind_of(field) = derived[field]
47
65
 
48
66
  # The fields the walk supplies — every `derived: { field => :walk }` claim.
@@ -50,20 +68,27 @@ module Hecks
50
68
  # judge can order siblings, but no constructor takes one. This is the one
51
69
  # place that fact is stated ; `Specializer` and `Model::Deviations` read it
52
70
  # here rather than each keeping their own `%i[position]`.
71
+ #
72
+ # @return [Array<Symbol>] every field name whose derived kind is `:walk`
53
73
  def walked = derived.select { |_field, kind| kind == :walk }.keys
54
74
 
55
75
  # Where a folded field actually lives, as [object, member].
56
76
  #
57
77
  # `[:folded, :lifecycle, :field]` says the language's `state_field` is the
58
- # `field` of the IR's one Lifecycle. That is the same fact `Readings` used to
59
- # state a second time as `node.lifecycle&.field` — so saying it once here
60
- # drives both directions: the walk reads the member on the way in, and the
61
- # reconstruction gathers the members back into the object on the way out.
78
+ # `field` of the IR's one Lifecycle. `Readings` would otherwise have to state
79
+ # that fact a second time as `node.lifecycle&.field` — saying it once here
80
+ # drives both directions instead: the walk reads the member on the way in, and
81
+ # the reconstruction gathers the members back into the object on the way out.
62
82
  #
63
83
  # A nil member means the fold has no single member to name — `rows` is a
64
84
  # count of what `closed_set` and `members` hold between them, and `options`
65
85
  # spreads across eight keys. Those keep their own code, and the gate still
66
86
  # checks the object they name is real.
87
+ #
88
+ # @param field [Symbol] the derived field name to check
89
+ # @return [Array(Symbol, Symbol), Array(Array<Symbol>, nil), nil] the `[object,
90
+ # member]` pair the field folds into — `member` is `nil` when the fold has
91
+ # no single member to name — or `nil` when `field` does not fold at all
67
92
  def folded(field)
68
93
  kind = derived[field]
69
94
  return nil unless kind.is_a?(Array) && kind.first == :folded
@@ -81,6 +106,10 @@ module Hecks
81
106
  #
82
107
  # So a computed field is one the holder answers and the constructor does not
83
108
  # accept. `query_name` qualifies (`Naming.snake(name)`) ; `version` cannot.
109
+ #
110
+ # @param method [Symbol] the field name to check
111
+ # @return [Boolean] whether `holder` answers `method` but its constructor does
112
+ # not accept it as a keyword
84
113
  def computes?(method)
85
114
  return false unless holder
86
115
  return false unless answers?(method)
@@ -88,10 +117,22 @@ module Hecks
88
117
  !accepts?(method)
89
118
  end
90
119
 
120
+ # Whether `holder` answers `method`, checked the way that fits how it is
121
+ # built: `respond_to?` for a `:declare` holder (a class, queried live) and
122
+ # `method_defined?` for a `:new` holder (an instance method, checked without
123
+ # building one).
124
+ #
125
+ # @param method [Symbol] the method name to check
126
+ # @return [Boolean] whether `holder` answers `method`
91
127
  def answers?(method)
92
128
  make == :declare ? holder.respond_to?(method) : holder.method_defined?(method)
93
129
  end
94
130
 
131
+ # Whether the holder's constructor takes `keyword` as a keyword argument.
132
+ #
133
+ # @param keyword [Symbol] the keyword to check
134
+ # @return [Boolean] whether the holder's constructor (`.declare` or
135
+ # `#initialize`) accepts `keyword` as an optional or required keyword
95
136
  def accepts?(keyword)
96
137
  builder = make == :declare ? holder.method(:declare) : holder.instance_method(:initialize)
97
138
 
@@ -109,13 +150,18 @@ module Hecks
109
150
  # owner Entity's own text twin of that link, which is why
110
151
  # Entity's contract claims `owner: :parent`
111
152
  #
112
- # It used to read `%i[owner shape handler]` here while the model's copy read
113
- # `owner aggregate bluebook`: `shape` and `handler` were Member's and
114
- # Dispatch's parent fields before S17 (ADR 0026) made both nested entities,
115
- # and nothing noticed the two lists had stopped agreeing. spec/assembly_spec
116
- # now derives this set from `Plan` and the contracts and fails on any drift.
153
+ # `shape` and `handler` are not on this list — S17 (ADR 0026) made Member and
154
+ # Dispatch nested entities, so neither is a parent field any more the way
155
+ # `owner` is. `spec/assembly_spec` derives this set from `Plan` and the
156
+ # contracts directly and fails on any drift, rather than trusting this list to
157
+ # stay in sync with a second hand-written copy kept by the model generator.
117
158
  PARENT_POINTERS = %i[aggregate bluebook owner].freeze
118
159
 
160
+ # Whether `field` is a parent pointer that does not end in `_id`.
161
+ #
162
+ # @param field [String, Symbol] the field name to check
163
+ # @return [Boolean] whether `field` ends with `_id`, or is one of
164
+ # `PARENT_POINTERS`
119
165
  def self.parent_pointer?(field)
120
166
  field.to_s.end_with?("_id") || PARENT_POINTERS.include?(field.to_sym)
121
167
  end
@@ -131,6 +177,12 @@ module Hecks
131
177
  Bluebook: %i[normalisations]
132
178
  }.freeze
133
179
 
180
+ # Whether `field` is allow-listed as describing something other than
181
+ # `category`'s own construct — see `ELSEWHERE`.
182
+ #
183
+ # @param category [String, Symbol] the construct category name
184
+ # @param field [Symbol] the field name to check
185
+ # @return [Boolean] whether `field` is on `category`'s `ELSEWHERE` entry
134
186
  def self.elsewhere?(category, field)
135
187
  Array(ELSEWHERE[category.to_sym]).include?(field)
136
188
  end
@@ -24,18 +24,23 @@ module Hecks
24
24
  # about whether a declaration is admissible — the language settled that on the
25
25
  # way in. This is only how a spelling becomes an object again.
26
26
  #
27
- # Why a table and not a method per category. The judge used to carry one
28
- # hand-written branch per category, and the price was fourteen verbs the
29
- # language declared and the walk never offered — every rule hanging off them
30
- # decoration, and nothing red, because a branch that does not exist cannot
31
- # fail. An assembler with a method per category is the same shape. So the
32
- # table is checked against the language by spec/assembly_spec: a field the
33
- # language declares that no contract consumes is a failure, not a silence.
27
+ # Why a table and not a method per category: a hand-written branch per
28
+ # category can decorate a verb the language declares without the walk ever
29
+ # offering it, and nothing goes red, because a branch that does not exist
30
+ # cannot fail. An assembler with a method per category is the same shape.
31
+ # The table avoids it because spec/assembly_spec checks it against the
32
+ # language: a field the language declares that no contract consumes is a
33
+ # failure, not a silence.
34
34
  #
35
35
  # The `derived` list is how a field says it needs no assembling — a parent
36
36
  # pointer the containment tree already knows, or something computed from what
37
37
  # is here (`query_name` is `Naming.snake(name)`, `creates?` is whether a verb
38
38
  # names a root). Naming one is a claim, and the coverage gate holds it.
39
+ #
40
+ # @param category [String, Symbol] the construct category name, such as
41
+ # `"Aggregate"` or `:Command`
42
+ # @return [Bluebook::Assembly::Contract] the category's field contract
43
+ # @raise [KeyError] if no contract is registered for `category`
39
44
  def self.contract(category) = CONTRACTS.fetch(category.to_s)
40
45
 
41
46
  CONTRACTS = {
@@ -163,13 +168,13 @@ module Hecks
163
168
  fields: {
164
169
  name: [:name, :plain],
165
170
  description: [:description, :plain],
166
- # A list of paths, exactly as an aggregate's is. The two used to differ
167
- # — a Symbol here and a String there, which byte equality with `to_h`
168
- # could not see because both render as a string, so the assembled graph
169
- # got a String and `element_of` looked up `args["sequence"]` in a
170
- # symbol-keyed payload and found nothing: "Reverse acts on one
171
- # LedgerEntry — pass sequence:", while passing sequence. There is one
172
- # spelling now, and no room left for that difference.
171
+ # A list of paths, exactly as an aggregate's is, with no room left for
172
+ # the two to differ. A Symbol here and a String there would look
173
+ # identical to `to_h`'s byte-equality check, since both render as a
174
+ # string, while diverging at runtime: the assembled graph would carry a
175
+ # String, and `element_of`'s lookup of `args["sequence"]` in a
176
+ # symbol-keyed payload would find nothing — refusing "Reverse acts on
177
+ # one LedgerEntry — pass sequence:" while sequence was in fact passed.
173
178
  identified_by: [:identified_by, :plain],
174
179
  attributes: [:attributes, [:each, :shape_field]],
175
180
  # ADR 0028 — the same shape Aggregate's own `preconditions`
@@ -263,11 +268,11 @@ module Hecks
263
268
  # S17, ADR 0026 — Dispatch is a genuine entity now, nested under
264
269
  # Handler (`entity "Dispatch"`, process_manager.bluebook) — two
265
270
  # levels deep, "no life outside its Handler" (the ADR's own
266
- # words). `command_name` alone used to be Dispatch's own
267
- # identity, until items #151/#152 (`process_manager.bluebook`'s
268
- # own `DispatchPosition` comment) found it collided the instant a
269
- # real handler fanned the same command out more than once —
270
- # `position` now joins it, walk-minted the same way every other
271
+ # words). `command_name` alone is not Dispatch's own identity —
272
+ # items #151/#152 (`process_manager.bluebook`'s own
273
+ # `DispatchPosition` comment) found it collides the instant a
274
+ # real handler fans the same command out more than once, so
275
+ # `position` joins it, walk-minted the same way every other
271
276
  # category's own `position` is (`derived: { position: :walk }`,
272
277
  # the same entry ProcessManager's own contract carries above) —
273
278
  # never a stored field on `DispatchSpec` itself, exactly like
@@ -9,7 +9,7 @@ module Hecks
9
9
  # is that Shapes rebuilds hashes and this rebuilds objects, so it has to
10
10
  # recover types rather than just strings.
11
11
  #
12
- # Encoding losses are the LARGEST family of bug in this codebase, and every
12
+ # Encoding losses are the largest family of bug in this codebase, and every
13
13
  # member has the same shape: reading an object where `to_h` holds a spelling.
14
14
  # So each method below names the spelling it inverts.
15
15
  module Marks
@@ -18,6 +18,14 @@ module Hecks
18
18
  # `Attribute#to_h` spells a type with `to_s`, so a reference arrives as
19
19
  # "Reference<Customer>" and has to become an edge again. Everything else is
20
20
  # a name and stays one.
21
+ #
22
+ # @param field [Hash{Symbol => Object}] one attribute's declared row, keyed
23
+ # `:type` (String, a plain type name or `"Reference<Target>"`), `:name`
24
+ # (String), `:list` and `:optional` (read as truthy), `:default` (Object,
25
+ # nil), `:pattern` (String, nil), `:admits` (String, nil) and
26
+ # `:relationship` (String, nil), each the raw declared value
27
+ # @return [Bluebook::Attribute] the attribute, with `type` resolved to a
28
+ # `Bluebook::Reference` when the row spelled one
21
29
  def attribute(field)
22
30
  type = field[:type].to_s
23
31
  target = type[/\AReference<(.+)>\z/, 1]
@@ -50,11 +58,18 @@ module Hecks
50
58
  # The same shape. Two names because a head's field and a verb's argument are
51
59
  # different things in the language even though the IR keeps one class for
52
60
  # both — `Aggregate.Attribute` and `Command.Argument` are separate verbs.
61
+ #
62
+ # @param field [Hash{Symbol => Object}] see `attribute`
63
+ # @return [Bluebook::Attribute] see `attribute`
53
64
  def shape_field(field) = attribute(field)
54
65
 
55
66
  # One part of an identity. It goes in as a row so the language can hold an
56
67
  # ordered list of them, and comes back out as the path it always was —
57
68
  # a String, because `identity_paths` splits paths and never symbols.
69
+ #
70
+ # @param part [Hash{Symbol => Object}] one identity-path row, keyed `:value`
71
+ # (Object, read via `to_s`)
72
+ # @return [String] the path segment
58
73
  def identity_path(part) = part[:value].to_s
59
74
 
60
75
  # A member's fields — an open map, which is why Member is its own root in
@@ -64,6 +79,11 @@ module Hecks
64
79
  # and the language stores them as text: `member code: "JPY", minor_units: 0`
65
80
  # came back with a minor_units of "0", and a closed set that admits the string
66
81
  # would refuse the number the caller passes.
82
+ #
83
+ # @param pairs [Array<Array(String, Object)>] one member's own field pairs,
84
+ # each `[field_name, raw_value]` as declared
85
+ # @return [Hash{Symbol => Object}] the member's fields, keyed by field name,
86
+ # each value read back to its native type through `unmark_scalar`
67
87
  def member(pairs)
68
88
  pairs.to_h { |key, value| [key.to_sym, unmark_scalar(value)] }
69
89
  end
@@ -71,6 +91,12 @@ module Hecks
71
91
  # A read model's gathered head. The keys must be symbols whichever way the
72
92
  # declaration arrived, and `as` must be one too: it names the reader the
73
93
  # projection answers to, and `ReadModel#to_h` spells it `to_s`.
94
+ #
95
+ # @param row [Hash{Symbol => Object}] one aggregate-head row, keyed
96
+ # `:aggregate` (String), `:as` (String, the reader name) and `:many`
97
+ # (Boolean)
98
+ # @return [Hash{Symbol => Object}] the head, with every key a Symbol and
99
+ # `:as` read back to a Symbol
74
100
  def head(row)
75
101
  row.to_h { |key, value| [key.to_sym, key.to_sym == :as ? value.to_sym : value] }
76
102
  end
@@ -78,12 +104,20 @@ module Hecks
78
104
  # A group_by field's own name — the builder's native shape is already
79
105
  # `{field: :symbol}`, so this matches it rather than leaving `field`
80
106
  # as the String `Shapes#group_by_field` reads back.
107
+ #
108
+ # @param row [Hash{Symbol => String}] one group-by row, keyed `:field`
109
+ # @return [Hash{field: Symbol}] the row with `:field` read back to a Symbol
81
110
  def group_by_field(row) = { field: row[:field].to_sym }
82
111
 
83
112
  # A scalar that was written as itself rather than inspected — a member's
84
113
  # value, where the language holds text and the type has to be read back from
85
114
  # the shape of it. Unlike `read`, a bare word stays a String here, because
86
115
  # a closed set admits words far more often than symbols.
116
+ #
117
+ # @param value [Object] the raw declared scalar, read via `to_s`
118
+ # @return [true, false, Integer, Float, String] `true`/`false` for those
119
+ # literal words, a number when the text matches a bare integer or decimal
120
+ # pattern, or the string itself otherwise
87
121
  def unmark_scalar(value)
88
122
  text = value.to_s
89
123
  return true if text == "true"
@@ -97,12 +131,32 @@ module Hecks
97
131
  # A saga's argument bindings. Each value rides Literal's spelling, which
98
132
  # marks a Symbol with a leading colon — lose it and an argument reads as a
99
133
  # string of the same name.
134
+ #
135
+ # @param with [Array<Array(String, String)>, nil] the declared bindings,
136
+ # each `[argument_name, literal_spelling]` pair, or `nil` for none
137
+ # @return [Hash{Symbol => Object}] the bindings, keyed by argument name,
138
+ # each value read back through `Literal.read`
100
139
  def bindings(with) = Array(with).to_h { |key, value| [key.to_sym, read(value)] }
101
140
 
141
+ # Builds one aggregate or value object invariant from its declared row.
142
+ #
143
+ # @param rule [Hash{Symbol => String}] one invariant's declared row, keyed
144
+ # `:description` and `:canonical` (the canonical-form rendering of the
145
+ # rule's expression)
146
+ # @return [Bluebook::Invariant] the invariant, with `predicate`/`ast` left
147
+ # `nil` — `Expression::Evaluator` parses `canonical` on demand when
148
+ # neither is set
102
149
  def invariant(rule)
103
150
  Invariant.new(description: rule[:description], canonical: rule[:canonical])
104
151
  end
105
152
 
153
+ # Builds one command precondition from its declared row.
154
+ #
155
+ # @param rule [Hash{Symbol => String}] one given's declared row, keyed
156
+ # `:description` and `:canonical` (the canonical-form rendering of the
157
+ # rule's expression)
158
+ # @return [Bluebook::Given] the given, with `predicate`/`ast` left `nil` —
159
+ # `Expression::Evaluator` parses `canonical` on demand when neither is set
106
160
  def given(rule)
107
161
  Given.new(description: rule[:description], canonical: rule[:canonical])
108
162
  end
@@ -111,6 +165,11 @@ module Hecks
111
165
  # All three fields are identifiers, unlike Invariant/Given's own
112
166
  # free text, so — like `attribute`'s own `name`/`type` below —
113
167
  # they come back as Symbols.
168
+ #
169
+ # @param row [Hash{Symbol => String}] one projected-field's declared row,
170
+ # keyed `:name`, `:reference` and `:remote_field` (each an identifier)
171
+ # @return [Bluebook::ProjectedField] the projected field, with each
172
+ # identifier read back to a Symbol
114
173
  def projected_field(row)
115
174
  ProjectedField.new(name: row[:name].to_sym, reference: row[:reference].to_sym,
116
175
  remote_field: row[:remote_field].to_sym)
@@ -122,6 +181,14 @@ module Hecks
122
181
  # Symbol, wearing its colon) or a literal — the distinction that is the
123
182
  # whole reason `append: { direction: "out" }` was once indistinguishable
124
183
  # from an argument named `out`.
184
+ #
185
+ # @param change [Hash{Symbol => Object}] one mutation's declared row, keyed
186
+ # `:target` (String), `:op` (String, such as `"set"`, `"append"`,
187
+ # `"delegate"` or `"corrects"`), `:fields` (Array, for the append-shaped
188
+ # ops) and `:source` (Hash, for any other op)
189
+ # @return [Bluebook::Mutation] the mutation, with `source` a
190
+ # `Hash{Symbol => Object}` of field bindings for an append-shaped op, or
191
+ # `classified`'s own result for any other
125
192
  def mutation(change)
126
193
  target = change[:target].to_sym
127
194
  op = change[:op].to_sym
@@ -135,12 +202,27 @@ module Hecks
135
202
  Mutation.new(target: target, op: op, source: classified(change[:source]))
136
203
  end
137
204
 
205
+ # Reads an append mutation's field bindings back off their declared rows.
206
+ #
207
+ # @param fields [Array<Array(String, String)>, nil] the append's declared
208
+ # field bindings, each `[field_name, literal_spelling]` pair, or `nil`
209
+ # for none
210
+ # @return [Hash{Symbol => Object}] the bindings, keyed by field name, each
211
+ # value read back through `Literal.read`
138
212
  def appended(fields)
139
213
  Array(fields).to_h { |field, source| [field.to_sym, read(source)] }
140
214
  end
141
215
 
142
216
  # A set reads one thing, and `classified_source` said which: an argument by
143
217
  # name, or a literal by value.
218
+ #
219
+ # @param source [Hash{Symbol => Object}, nil] the mutation's declared
220
+ # source, keyed `:kind` (String, `"argument"`, `"state"` or a literal
221
+ # kind) plus `:name` (String, for `"argument"`/`"state"`) or `:value`
222
+ # (Object, for a literal), or `nil` for no source
223
+ # @return [Symbol, Hecks::StateRef, Object, nil] the command argument name,
224
+ # a state self-reference, the literal value, or `nil` when `source` is
225
+ # `nil`
144
226
  def classified(source)
145
227
  return nil if source.nil?
146
228
 
@@ -155,19 +237,22 @@ module Hecks
155
237
  #
156
238
  # A where-clause value, a saga's argument bindings, an append binding, a
157
239
  # limit: all of them ride Literal's self-describing form, so all of them
158
- # come back through here. There were two readers (`read` and `unmark`)
159
- # that disagreed about quoted strings and numbers, and which one a call
160
- # site got was a coin toss the comments had to keep apologising for.
161
- #
162
- # An object literal is the one that bit. A saga leg binds `narrative: {
163
- # text: "transfer out" }` — a value object's fields written inline — and
164
- # `to_s` on a Hash used to be its inspect form, so it came back as text.
165
- # Read as a string it reached the runtime as `"{:text=>\"transfer out\"}"`,
166
- # coercion refused it, the debit leg was never delivered, and the whole
167
- # settlement wire stopped: banking emitted TransferRequested five times and
168
- # TransferDebited never. A whole-history replay gate caught what every other
169
- # gate missed, because a saga that silently does nothing looks exactly like
170
- # a saga with nothing to do.
240
+ # come back through this one reader, never a second one that might
241
+ # disagree about a quoted string or a number.
242
+ #
243
+ # An object literal is the one that bites hardest. A saga leg binds
244
+ # `narrative: { text: "transfer out" }` — a value object's fields written
245
+ # inline — and reading it as a plain string rather than through `Literal`
246
+ # would reach the runtime as `"{:text=>\"transfer out\"}"`, which coercion
247
+ # refuses: the debit leg is never delivered, and the whole settlement wire
248
+ # stops — banking emits TransferRequested five times and TransferDebited
249
+ # never. A whole-history replay gate is what catches this, because a saga
250
+ # that silently does nothing looks exactly like a saga with nothing to do.
251
+ #
252
+ # @param value [String, Object] wire spelling produced by `Literal.render`,
253
+ # or a bare word
254
+ # @return [nil, true, false, Integer, Float, Symbol, Hecks::StateRef,
255
+ # String, Hash, Array] the value read back — see `Literal.read`
171
256
  def read(value) = Literal.read(value)
172
257
 
173
258
  # `target:` (ADR 0055) — read straight off the wire, unconverted:
@@ -180,12 +265,25 @@ module Hecks
180
265
  # declared `on:` — `clause[:target]`/`declared[:target]` reads
181
266
  # `nil` for a missing key exactly like an explicit `nil` would,
182
267
  # so this is additive, not a migration.
268
+ #
269
+ # @param clause [Hash{Symbol => Object}] one where-clause's declared row,
270
+ # keyed `:field` (String), `:op` (String, a comparator name), `:value`
271
+ # (String, Literal's spelling) and `:target` (String, `nil` when absent)
272
+ # @return [QuerySpecification::Common::WhereClause] the clause, with `op`
273
+ # read to a Symbol and `value` read back through `Literal.read`
183
274
  def where_clause(clause)
184
275
  QuerySpecification::Common::WhereClause.new(
185
276
  field: clause[:field], op: clause[:op].to_sym, value: read(clause[:value]), target: clause[:target]
186
277
  )
187
278
  end
188
279
 
280
+ # Builds a query's declared ordering, if it declares one.
281
+ #
282
+ # @param declared [Hash{Symbol => Object}, nil] the declared order-by row,
283
+ # keyed `:field` (String), `:direction` (String) and `:target` (String,
284
+ # `nil` when absent), or `nil` when the construct declares no ordering
285
+ # @return [QuerySpecification::Common::OrderBy, nil] the order-by, with
286
+ # `direction` read to a Symbol, or `nil` when `declared` is `nil`
189
287
  def order_by(declared)
190
288
  return nil unless declared
191
289
 
@@ -194,6 +292,14 @@ module Hecks
194
292
  )
195
293
  end
196
294
 
295
+ # Builds a query's declared limit, if it declares one.
296
+ #
297
+ # @param declared [Hash{Symbol => Object}, nil] the declared limit row,
298
+ # keyed `:value` (String, Literal's spelling) and `:target` (String,
299
+ # `nil` when absent), or `nil` when the construct declares no limit
300
+ # @return [QuerySpecification::Common::LimitSpec, nil] the limit, with
301
+ # `value` read back through `Literal.read`, or `nil` when `declared` is
302
+ # `nil`
197
303
  def limit(declared)
198
304
  return nil unless declared
199
305
 
@@ -219,6 +325,20 @@ module Hecks
219
325
  # with `to_s`, so the colon is not there to strip.
220
326
  SYMBOLIC = %i[mode policy tenant].freeze
221
327
 
328
+ # Builds one query specification option from its declared row and `OPTIONS`.
329
+ #
330
+ # @param name [Symbol] the option's name, a key of `OPTIONS` (`:offset`,
331
+ # `:cursor`, `:null_semantics`, `:authorization` or `:inspection`)
332
+ # @param declared [Hash{Symbol => Object}, nil] the option's declared row,
333
+ # or `nil` when the construct does not declare this option
334
+ # @return [QuerySpecification::Common::OffsetSpec,
335
+ # QuerySpecification::Common::CursorSpec,
336
+ # QuerySpecification::Common::NullSemantics,
337
+ # QuerySpecification::Common::AuthorizationSpec,
338
+ # QuerySpecification::Common::InspectionSpec, nil] the option built
339
+ # through the holder `OPTIONS` names for `name`, or `nil` when `declared`
340
+ # is `nil`
341
+ # @raise [KeyError] if `name` is not a key of `OPTIONS`
222
342
  def option(name, declared)
223
343
  return nil if declared.nil?
224
344
 
@@ -226,6 +346,15 @@ module Hecks
226
346
  holder.new(**Hash(declared).to_h { |key, value| [key, option_value(key, value, marked)] })
227
347
  end
228
348
 
349
+ # Reads one option member's declared value back to its native type.
350
+ #
351
+ # @param key [Symbol] the option member's name being read
352
+ # @param value [Object, nil] the declared value for that member
353
+ # @param marked [Array<Symbol>] the members of this option that ride
354
+ # Literal's spelling, from `OPTIONS`
355
+ # @return [Object, nil] `value` read back through `Literal.read` when
356
+ # `key` is marked, a Symbol when `key` is one of `SYMBOLIC`, the raw
357
+ # `value` otherwise, or `nil` when `value` is `nil`
229
358
  def option_value(key, value, marked)
230
359
  return nil if value.nil?
231
360
  return read(value) if marked.include?(key)
@@ -27,10 +27,23 @@ module Hecks
27
27
  # (searched recursively, the same reason `Value::Coercion#find_
28
28
  # entity` does: a nested entity, like `Dispatch` inside
29
29
  # `Handler`, is not a direct child of any aggregate either).
30
+ #
31
+ # @param chapter [Bluebook::Chapter] the chapter to search
32
+ # @param name [String, Symbol] the construct's declared name
33
+ # @return [Bluebook::Aggregate, Class, nil] the aggregate, or the entity class
34
+ # (a `Bluebook::Entity` subclass) nested anywhere under one, or `nil` if
35
+ # `chapter` declares no construct by that name
30
36
  def construct_for(chapter, name)
31
37
  chapter.aggregate(name) || chapter.aggregates.filter_map { |a| find_entity(a, name) }.first
32
38
  end
33
39
 
40
+ # Searches a construct's own entities, recursively, for one by name.
41
+ #
42
+ # @param construct [Bluebook::Aggregate, Class] the aggregate, or entity class,
43
+ # to search under
44
+ # @param name [String, Symbol] the entity's declared name
45
+ # @return [Class, nil] the entity class (a `Bluebook::Entity` subclass), or `nil`
46
+ # if none of `construct`'s nested entities has that name
34
47
  def find_entity(construct, name)
35
48
  construct.entities.each do |candidate|
36
49
  return candidate if candidate.hecks_name == name
@@ -52,6 +65,12 @@ module Hecks
52
65
  # So the skip reads the category's own walk claims (`Contract#walked`)
53
66
  # rather than restating `position` here — Handler, which has no
54
67
  # walk-minted position, skips nothing.
68
+ #
69
+ # @param category [String, Symbol] the construct category's name, such as
70
+ # `"Aggregate"` or `"Command"`
71
+ # @return [Hash{Symbol => Array(Symbol, Symbol)}] a `contracts.rb`-shaped
72
+ # `fields:` table, one `field => [field, :plain]` entry per scalar,
73
+ # non-reference, non-walked attribute the language declares for `category`
55
74
  def fields_for(category)
56
75
  language = construct_for(MetaValidator.grammar_registry.bluebook("Bluebook"), category.to_s)
57
76
  walked = Assembly.contract(category).walked
@@ -8,6 +8,8 @@ module Hecks
8
8
  # into the graph the runtime runs: IR aggregates with their verbs, value
9
9
  # objects, entities and asks, owned by the chapter that declares them.
10
10
  #
11
+ # ## Why a hash, not a runtime
12
+ #
11
13
  # It takes a hash, not a runtime, on purpose. That makes it a pure inverse of
12
14
  # `to_h` and testable without the meta-domain in the picture at all:
13
15
  #
@@ -17,23 +19,39 @@ module Hecks
17
19
  # Feed it the reconstruction instead and the same code assembles what the
18
20
  # language holds — the only difference being where the declarations came from.
19
21
  #
22
+ # ## One table, not a method per category
23
+ #
20
24
  # Every field is read from one table. There is no method per category here:
21
25
  # `Contracts` names what the language cannot say about a construct, `Build`
22
- # reads it, and the coverage gate holds the table to the language. The first
23
- # draft of this file did have a method each, which is the shape the judge used
24
- # to have — and the price of that shape was fourteen verbs the language declared
25
- # and nothing ever offered.
26
+ # reads it, and the coverage gate holds the table to the language. A method
27
+ # per category can decorate a verb the language declares without ever
28
+ # offering it; the table cannot, because `spec/assembly_spec` checks every
29
+ # field the language declares against it.
30
+ #
31
+ # ## What stays hand-written
26
32
  #
27
- # What stays hand-written is the containment: which construct holds which.
28
- # That is not a field table. (The runtime surface is no longer built here at
29
- # all — the door is a per-boot projection, facade/surface.rb.)
33
+ # The containment — which construct holds which — stays hand-written; it is
34
+ # not a field table. The runtime surface is no longer built here at all — the
35
+ # door is a per-boot projection, facade/surface.rb.
30
36
  class Assembly
37
+ # Builds the graph the runtime runs from one chapter's declared hash.
38
+ #
39
+ # @param declaration [Hash{Symbol => Object}] a chapter's declarations, in the
40
+ # shape `Chapter#to_h` spells
41
+ # @return [Bluebook::Chapter] the assembled chapter, with its aggregates, read
42
+ # models and process managers built and wired
31
43
  def self.call(declaration) = new(declaration).bluebook
32
44
 
45
+ # @param declaration [Hash{Symbol => Object}] a chapter's declarations, in the
46
+ # shape `Chapter#to_h` spells
33
47
  def initialize(declaration)
34
48
  @declaration = declaration
35
49
  end
36
50
 
51
+ # Assembles this instance's declared hash into the chapter's runtime graph.
52
+ #
53
+ # @return [Bluebook::Chapter] the assembled chapter, with its aggregates, read
54
+ # models and process managers built and wired
37
55
  def bluebook
38
56
  aggregates = Array(@declaration[:aggregates]).map { |row| AggregateAssembly.new(row).aggregate }
39
57
  models = Array(@declaration[:read_models]).map { |row| Build.call("ReadModel", row) }