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
@@ -7,6 +7,8 @@ require_relative "../ports/persistence/binding_policy"
7
7
 
8
8
  # Hecks::Behaviors::Expectations
9
9
  #
10
+ # ## What it does
11
+ #
10
12
  # One test case, start to finish: take the suite's own runtime (booted
11
13
  # once for exactly what its `loads` names, reset to nothing between
12
14
  # tests — `runtime_for`), replay `setup` dispatches, dispatch (or query)
@@ -14,6 +16,8 @@ require_relative "../ports/persistence/binding_policy"
14
16
  # runner.rb the same way the file-count/sweep concern is split from a
15
17
  # single test's own execution.
16
18
  #
19
+ # ## Deviations from a prior port
20
+ #
17
21
  # Two deliberate deviations from a prior port of this same idea (read
18
22
  # before writing this, not reinvented):
19
23
  #
@@ -46,6 +50,15 @@ module Hecks
46
50
  REFUSAL_CLASSES = Hecks::Runtime::DOMAIN_REFUSALS
47
51
  SPECIAL_KEYS = %i[ok refused emits count].freeze
48
52
 
53
+ # Runs one test case: replays its `setup` dispatches, dispatches (or queries)
54
+ # the command under test, and checks its `expect`.
55
+ #
56
+ # @param test [Behaviors::TestCase] the test case to run
57
+ # @param suite [Behaviors::BehaviorsSuite] the suite `test` belongs to
58
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher, nil] an
59
+ # already-booted runtime to reuse and reset; nil boots (or reuses the cached
60
+ # boot of) `suite.loads` via `runtime_for`
61
+ # @return [Result] the test's pass, fail, or error outcome
49
62
  def run_one(test, suite, runtime: nil)
50
63
  runtime ||= runtime_for(suite)
51
64
  runtime.registry.reset_runtime_state!
@@ -85,6 +98,14 @@ module Hecks
85
98
  RUNTIMES_LOCK = Mutex.new
86
99
  private_constant :RUNTIMES, :RUNTIMES_LOCK
87
100
 
101
+ # Boots (or reuses the cached boot of) the runtime a suite's `loads` names.
102
+ #
103
+ # @param suite [Behaviors::BehaviorsSuite] the suite whose `loads` files boot
104
+ # the runtime
105
+ # @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the cached or
106
+ # freshly booted, Memory-only-guarded runtime
107
+ # @raise [Malformed] if any aggregate the suite boots is not bound to the
108
+ # default (Memory) adapter
88
109
  def runtime_for(suite)
89
110
  files = Array(suite.loads).map { |path| File.expand_path(path) }
90
111
  key = files.map { |file| [file, File.exist?(file) ? File.mtime(file).to_f : nil] }
@@ -104,12 +125,25 @@ module Hecks
104
125
  # of guard `BindingPolicy` already applies to a missing bind — the
105
126
  # project's identity is refusing bad wiring up front, not
106
127
  # discovering it mid-suite.
128
+ #
129
+ # @param files [Array<String>] absolute paths to the bluebook/hecksagon/world
130
+ # files to boot
131
+ # @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted,
132
+ # Memory-only-guarded runtime
133
+ # @raise [Malformed] if any aggregate is bound to a non-Memory adapter
107
134
  def boot_and_guard(files)
108
135
  runtime = Hecks::Runtime::Loader.boot_files(files, install_facade: false)
109
136
  guard_memory_only!(runtime)
110
137
  runtime
111
138
  end
112
139
 
140
+ # Refuses a runtime where any aggregate is bound to anything other than the
141
+ # default (Memory) adapter, so tests can never leak state or touch a real store.
142
+ #
143
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted
144
+ # runtime to check
145
+ # @return [void]
146
+ # @raise [Malformed] if any aggregate is bound to a non-Memory adapter
113
147
  def guard_memory_only!(runtime)
114
148
  runtime.registry.bluebooks.each_value do |bluebook|
115
149
  bluebook.aggregates.each do |aggregate|
@@ -127,6 +161,15 @@ module Hecks
127
161
  end
128
162
  end
129
163
 
164
+ # Qualifies and dispatches (or queries) the command under test, catching a
165
+ # domain refusal as the test's own outcome rather than an error.
166
+ #
167
+ # @param test [Behaviors::TestCase] the test case to run
168
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the suite's
169
+ # booted runtime
170
+ # @param bluebooks [Array<Bluebook::Chapter>] every chapter the suite booted,
171
+ # searched to qualify a bare command/query name
172
+ # @return [Result] the test's pass, fail, or error outcome
130
173
  def run_tested(test, runtime, bluebooks)
131
174
  verb = qualify(test.tests_command, test.on_aggregate, bluebooks, kind: test.kind)
132
175
 
@@ -139,6 +182,13 @@ module Hecks
139
182
  check_refusal(test, e)
140
183
  end
141
184
 
185
+ # Dispatches the command under test and checks its `expect`.
186
+ #
187
+ # @param test [Behaviors::TestCase] the test case to run
188
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the suite's
189
+ # booted runtime
190
+ # @param verb [String] the command's dotted FQN
191
+ # @return [Result] the test's pass or fail outcome
142
192
  def run_command(test, runtime, verb)
143
193
  before = runtime.registry.event_log.length
144
194
  result = dispatch_command(runtime, verb, test.input)
@@ -170,6 +220,15 @@ module Hecks
170
220
  # `emits:` saw MoveCountBumped in the same test). The repository
171
221
  # holds the settled record; read it back by the id the dispatch
172
222
  # itself answered with.
223
+ #
224
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the suite's
225
+ # booted runtime
226
+ # @param verb [String] the dispatched command's dotted FQN
227
+ # @param result [Runtime::Dispatcher::Result, Runtime::RemoteDispatcher::Result]
228
+ # the dispatch's own result
229
+ # @return [Hash{Symbol => Object}] the settled record's current state, read
230
+ # back from the repository; `result.state` (or `{}`) when the result has no
231
+ # id or the repository no longer has that aggregate/record
173
232
  def settled_state(runtime, verb, result)
174
233
  return result.state || {} unless result.respond_to?(:id) && result.id
175
234
 
@@ -202,13 +261,24 @@ module Hecks
202
261
  # expects a command's own declared attributes at the top level,
203
262
  # not a port operation's already-wrapped `to:`/`with:` shape.
204
263
  #
205
- # This used to rely on `resolve_target` raising `UnknownVerb` for
206
- # any port-operation verb — true only so long as nothing else ever
207
- # asked it to resolve one. Now that a `policy` can legitimately
208
- # `trigger` a port operation (`ReactionInvocation#resolve_target`'s
209
- # own port-operation branch), that raise is gone, so this checks
210
- # for a port operation directly instead of leaning on a refusal
211
- # that no longer happens.
264
+ # Checks for a port operation directly rather than leaning on
265
+ # `resolve_target` to raise `UnknownVerb` for one — a refusal that held
266
+ # only so long as nothing else ever asked it to resolve a port-operation
267
+ # verb. Now that a `policy` can legitimately `trigger` a port operation
268
+ # (`ReactionInvocation#resolve_target`'s own port-operation branch),
269
+ # that raise no longer happens, so this checks directly instead.
270
+ #
271
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the suite's
272
+ # booted runtime
273
+ # @param verb [String] the command's dotted FQN, or a port operation's
274
+ # @param args [Hash{Symbol => Object}] the command's (or setup's) facts, mixing
275
+ # receiver identity and declared arguments
276
+ # @return [Runtime::Dispatcher::Result, Runtime::RemoteDispatcher::Result] the
277
+ # dispatch's own result
278
+ # @raise [Runtime::UnknownVerb] if `verb` is not fully qualified or names an
279
+ # undeclared domain, aggregate, command, entity or port operation
280
+ # @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when the
281
+ # domain refuses the call
212
282
  def dispatch_command(runtime, verb, args)
213
283
  return runtime.dispatch_flat(verb, args) if port_operation?(runtime, verb)
214
284
 
@@ -231,6 +301,11 @@ module Hecks
231
301
  # `ReactionInvocation#resolve_target` both already check — a bare
232
302
  # domain/aggregate lookup plus a port-name lookup, no command
233
303
  # resolution needed since all this asks is whether one exists.
304
+ #
305
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the suite's
306
+ # booted runtime
307
+ # @param verb [String] the verb to check, dotted FQN shaped
308
+ # @return [Boolean] true if `verb` names a port operation on a declared aggregate
234
309
  def port_operation?(runtime, verb)
235
310
  domain, aggregate_name, command_path = Naming.split_verb(verb)
236
311
  return false unless command_path
@@ -242,6 +317,13 @@ module Hecks
242
317
  rest && !!aggregate.port(head)
243
318
  end
244
319
 
320
+ # Runs the query under test and checks its `expect`.
321
+ #
322
+ # @param test [Behaviors::TestCase] the test case to run
323
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the suite's
324
+ # booted runtime
325
+ # @param verb [String] the query's dotted FQN
326
+ # @return [Result] the test's pass or fail outcome
245
327
  def run_query(test, runtime, verb)
246
328
  rows = runtime.query(verb, **test.input)
247
329
 
@@ -267,6 +349,11 @@ module Hecks
267
349
  check_ok(test) || check_fields(test, row) || pass_result(test)
268
350
  end
269
351
 
352
+ # Whether `test.expect` names a field, not just one of the special keys.
353
+ #
354
+ # @param test [Behaviors::TestCase] the test case to check
355
+ # @return [Boolean] true if `test.expect` has any key besides `ok`, `refused`,
356
+ # `emits` or `count`
270
357
  def field_expectations?(test)
271
358
  test.expect.keys.any? { |key| !SPECIAL_KEYS.include?(key) }
272
359
  end
@@ -277,6 +364,11 @@ module Hecks
277
364
  # ambiguous about which row it's describing. `check_fields` itself
278
365
  # stays row-shaped (it already is, for `run_command`'s settled
279
366
  # state); this picks which row it reads.
367
+ #
368
+ # @param test [Behaviors::TestCase] the test case, for the fail message
369
+ # @param rows [Array<Hash>, Hash] the query's own result
370
+ # @return [Hash, Result] `rows` itself when it isn't an Array; its one row when
371
+ # it holds exactly one; otherwise a `fail_result` naming the row count
280
372
  def query_row(test, rows)
281
373
  return rows unless rows.is_a?(Array)
282
374
 
@@ -288,6 +380,11 @@ module Hecks
288
380
  end
289
381
  end
290
382
 
383
+ # Checks an `expect ok: true` expectation.
384
+ #
385
+ # @param test [Behaviors::TestCase] the test case to check
386
+ # @return [Result, nil] nil if `test.expect` has no `ok` key or expects `true`;
387
+ # a `fail_result` if it names anything else
291
388
  def check_ok(test)
292
389
  return unless test.expect.key?(:ok)
293
390
 
@@ -297,6 +394,13 @@ module Hecks
297
394
  fail_result(test, "expect ok: only accepts true — got #{expected.inspect}")
298
395
  end
299
396
 
397
+ # Checks every field-name expectation in `test.expect` against `state`.
398
+ #
399
+ # @param test [Behaviors::TestCase] the test case to check
400
+ # @param state [Hash{Symbol, String => Object}] the settled record's (or query
401
+ # row's) fields
402
+ # @return [Result, nil] nil if every expected field matches; a `fail_result` for
403
+ # the first field that names none of `state`'s keys or doesn't match
300
404
  def check_fields(test, state)
301
405
  test.expect.each do |key, expected|
302
406
  next if SPECIAL_KEYS.include?(key)
@@ -314,6 +418,12 @@ module Hecks
314
418
  nil
315
419
  end
316
420
 
421
+ # Checks a caught domain refusal against `test.expect[:refused]`.
422
+ #
423
+ # @param test [Behaviors::TestCase] the test case to check
424
+ # @param error [StandardError] the caught refusal, a member of `REFUSAL_CLASSES`
425
+ # @return [Result] a `pass_result` if `test` expected this refusal's message, an
426
+ # `error_result` if it expected none, or a `fail_result` if the message doesn't match
317
427
  def check_refusal(test, error)
318
428
  expected = test.expect[:refused]
319
429
  return error_result(test, "unexpected refusal (#{error.class}): #{error.message}") unless expected
@@ -333,6 +443,11 @@ module Hecks
333
443
  # field always comes back as a `Hecks::Runtime::Value`;
334
444
  # normalizing both sides to the same bare-scalar-or-plain-hash
335
445
  # shape is the one comparison that accepts either spelling.
446
+ #
447
+ # @param value [Object] a stored field's value, or an `expect` value to compare
448
+ # it against
449
+ # @return [Object] the bare underlying value: unwrapped from a `Runtime::Value`,
450
+ # or from a `{value: ...}` Hash; unchanged otherwise
336
451
  def normalize(value)
337
452
  return Hecks::Runtime::Value.materialize_unwrapped(value) if value.is_a?(Hecks::Runtime::Value)
338
453
  return normalize(value[:value]) if value.is_a?(Hash) && value.keys == [:value]
@@ -347,6 +462,15 @@ module Hecks
347
462
  # the tested command — `setup` never receives it, see the DSL
348
463
  # contract) narrows the search to one aggregate by name instead of
349
464
  # searching all of them.
465
+ #
466
+ # @param command [String, Symbol] a bare verb, or an already-dotted FQN
467
+ # @param on_aggregate [String, Symbol, nil] the aggregate to search, or nil to
468
+ # search every aggregate of every bluebook
469
+ # @param bluebooks [Array<Bluebook::Chapter>] every chapter the suite booted
470
+ # @param kind [Symbol] `:command` or `:query`
471
+ # @return [String] `command` unchanged if already dotted, otherwise its resolved
472
+ # dotted FQN
473
+ # @raise [ArgumentError] if no aggregate declares `command`, or more than one does
350
474
  def qualify(command, on_aggregate, bluebooks, kind:)
351
475
  return command.to_s if command.to_s.include?(".")
352
476
 
@@ -354,9 +478,19 @@ module Hecks
354
478
  disambiguate_qualified_name(candidates, command, kind, bluebooks)
355
479
  end
356
480
 
481
+ # Finds every aggregate that could be what a bare command/query name refers to.
482
+ #
357
483
  # **The search** — every (bluebook, aggregate) pair that declares a
358
484
  # command/query named `command`, narrowed to `on_aggregate` by name
359
485
  # when given.
486
+ #
487
+ # @param command [String, Symbol] the bare verb to search for
488
+ # @param on_aggregate [String, Symbol, nil] the aggregate to search, or nil to
489
+ # search every aggregate of every bluebook
490
+ # @param bluebooks [Array<Bluebook::Chapter>] every chapter the suite booted
491
+ # @param kind [Symbol] `:command` or `:query`
492
+ # @return [Array<Array(Bluebook::Chapter, Bluebook::Aggregate)>] every matching
493
+ # (chapter, aggregate) pair
360
494
  def qualify_candidates(command, on_aggregate, bluebooks, kind)
361
495
  members = kind == :query ? :queries : :commands
362
496
  pairs =
@@ -368,9 +502,20 @@ module Hecks
368
502
  pairs.select { |_, agg| agg.public_send(members).any? { |m| m.hecks_name == command.to_s } }
369
503
  end
370
504
 
505
+ # Resolves a search's candidates to exactly one dotted FQN, or refuses.
506
+ #
371
507
  # **The report** — zero candidates and more-than-one candidates both
372
508
  # refuse (with a different message); exactly one resolves to its
373
509
  # dotted FQN.
510
+ #
511
+ # @param candidates [Array<Array(Bluebook::Chapter, Bluebook::Aggregate)>] the
512
+ # matching (chapter, aggregate) pairs found by `qualify_candidates`
513
+ # @param command [String, Symbol] the bare verb that was searched for
514
+ # @param kind [Symbol] `:command` or `:query`, for the refusal message
515
+ # @param bluebooks [Array<Bluebook::Chapter>] every chapter the suite booted,
516
+ # for the refusal message
517
+ # @return [String] the one candidate's dotted FQN
518
+ # @raise [ArgumentError] if `candidates` is empty, or holds more than one
374
519
  def disambiguate_qualified_name(candidates, command, kind, bluebooks)
375
520
  case candidates.size
376
521
  when 0
@@ -386,8 +531,24 @@ module Hecks
386
531
  end
387
532
  end
388
533
 
534
+ # Builds a passing result.
535
+ #
536
+ # @param test [Behaviors::TestCase] the test that passed
537
+ # @return [Result] a `:pass` result
389
538
  def pass_result(test) = Result.new(description: test.description, status: :pass, message: nil)
539
+
540
+ # Builds a failing result.
541
+ #
542
+ # @param test [Behaviors::TestCase] the test whose expectation was not met
543
+ # @param message [String] what was expected versus what happened
544
+ # @return [Result] a `:fail` result
390
545
  def fail_result(test, message) = Result.new(description: test.description, status: :fail, message: message)
546
+
547
+ # Builds an errored result.
548
+ #
549
+ # @param test [Behaviors::TestCase] the test that could not run to a conclusion
550
+ # @param message [String] what went wrong
551
+ # @return [Result] an `:error` result
391
552
  def error_result(test, message) = Result.new(description: test.description, status: :error, message: message)
392
553
 
393
554
  Result = Struct.new(:description, :status, :message, keyword_init: true)
@@ -8,18 +8,29 @@
8
8
  # round-trip machinery a real bluebook construct does. It is a test
9
9
  # artifact a runner reads on demand, not a domain a boot needs.
10
10
  module Hecks
11
+ # The `.behaviors` toolkit: the DSL a `.behaviors` file is written against
12
+ # (`Hecks.behaviors`, dsl.rb), the plain-Struct IR it builds (this file), the runner
13
+ # that boots a suite's domain and checks each test (runner.rb, expectations.rb), and
14
+ # an rspec shim for a consuming app's own suite (rspec.rb).
11
15
  module Behaviors
12
16
  TestSetup = Struct.new(:command, :args, keyword_init: true)
13
17
 
14
18
  TestCase = Struct.new(:description, :tests_command, :on_aggregate, :kind,
15
19
  :setups, :input, :expect, keyword_init: true) do
20
+ # Tells whether `tests_command` is already a fully qualified verb name.
21
+ #
16
22
  # An already-dotted tests_command is a literal FQN; otherwise `on:`
17
23
  # composes with the domain name resolved once setups/the tested
18
24
  # dispatch actually run (see Expectations — the domain isn't known
19
25
  # until `loads` is booted, so it's a runtime concern, not a field
20
26
  # here).
27
+ #
28
+ # @return [Boolean] true when `tests_command` contains a `.`
21
29
  def dotted? = tests_command.to_s.include?(".")
22
30
 
31
+ # Tells whether this test case exercises a query rather than a command.
32
+ #
33
+ # @return [Boolean] true when `kind` is `:query`
23
34
  def query? = kind == :query
24
35
  end
25
36
 
@@ -21,6 +21,14 @@ module Hecks
21
21
  module RSpec
22
22
  module_function
23
23
 
24
+ # Parses one `.behaviors` file and registers an rspec example group for it, one
25
+ # `it` per test named by the test's own description.
26
+ #
27
+ # @param path [String] the `.behaviors` file's path
28
+ # @return [void]
29
+ # @raise [RuntimeError] not raised by this call itself; wraps `parsed.parse_error`'s
30
+ # message and is raised only when rspec later runs the generated "loads without a
31
+ # parse error" example, so it surfaces as that example's failure
24
32
  def describe_file(path)
25
33
  parsed = Behaviors.parse(path)
26
34
 
@@ -45,6 +45,10 @@ module Hecks
45
45
  # suite from whatever loaded before it in a sweep (a real bug in a
46
46
  # prior port of this idea: compared only against nil, so after the
47
47
  # first successful file in a sweep it stayed non-nil forever).
48
+ # @param path [String] the `.behaviors` file's path
49
+ # @return [Behaviors::ParseResult] `suite` holding the built `BehaviorsSuite` and
50
+ # `parse_error` nil on success; `suite` nil and `parse_error` a String describing
51
+ # a raised exception, or the file loading without calling `Hecks.behaviors`
48
52
  def parse(path)
49
53
  path = File.expand_path(path)
50
54
  previous_path = loading_path
@@ -66,6 +70,11 @@ module Hecks
66
70
  end
67
71
 
68
72
  # One `.behaviors` file → `FileResult`, every test actually run.
73
+ #
74
+ # @param path [String] the `.behaviors` file's path
75
+ # @return [Behaviors::FileResult] `parse_error` and empty `runs` on a parse
76
+ # failure; otherwise `parse_error` nil and `runs` one `Expectations::Result`
77
+ # per test
69
78
  def run(path)
70
79
  parsed = parse(path)
71
80
  return FileResult.new(path: parsed.path, parse_error: parsed.parse_error, runs: []) if parsed.parse_error
@@ -78,12 +87,22 @@ module Hecks
78
87
  # it actually found — a sweep that goes green without saying how
79
88
  # much it looked at is indistinguishable from one that found
80
89
  # nothing to look at.
90
+ #
91
+ # @param dir [String] the directory to search, recursively, for `.behaviors` files
92
+ # @return [Behaviors::SweepResult] `files_swept` the count found, `files` one
93
+ # `FileResult` per file, and `summary` the aggregate counts `summarize` returns
81
94
  def run_all(dir)
82
95
  files = Dir.glob(File.join(dir, "**", "*.behaviors"))
83
96
  results = files.map { |path| run(path) }
84
97
  SweepResult.new(root: dir, files_swept: files.size, files: results, summary: summarize(results))
85
98
  end
86
99
 
100
+ # Tallies a sweep's results into counts by outcome.
101
+ #
102
+ # @param results [Array<Behaviors::FileResult>] the swept files' results
103
+ # @return [Hash{Symbol => Integer}] `:files` the file count, `:parse_errors` files
104
+ # that failed to parse, `:total` tests actually run across every file, `:passed`,
105
+ # `:failed` and `:errored` counting each `Expectations::Result#status`
87
106
  def summarize(results)
88
107
  runs = results.flat_map(&:runs)
89
108
  {
@@ -13,6 +13,14 @@ module Hecks
13
13
  # behaviors suite is a test artifact a runner reads on demand, never
14
14
  # a thing a live domain boot needs, so it has no business landing in
15
15
  # `Runtime.current_registry`.
16
+ #
17
+ # @param name [String] the suite's declared name
18
+ # @yield the suite's body, evaluated against a `Behaviors::BehaviorsBuilder`
19
+ # @return [Behaviors::BehaviorsSuite] the built suite, also stashed as
20
+ # `Behaviors.last_suite`
21
+ # @raise [Behaviors::LoadOutsideRunner] if called outside `Behaviors.run(path)`
22
+ # @raise [Behaviors::Malformed] if the suite declares no `vision` or `loads`, or
23
+ # any of its `test` blocks is malformed
16
24
  def behaviors(name, &)
17
25
  path = Behaviors.loading_path or
18
26
  raise Behaviors::LoadOutsideRunner,
@@ -80,6 +80,36 @@ module Hecks
80
80
  # Assigns what the language declares, then hands off to the
81
81
  # behaviour's own `settle` — derived identity, name indexes and
82
82
  # owner stamping, none of which the declaration states.
83
+ #
84
+ # @param name [String, Symbol] the aggregate's declared name
85
+ # @param description [String, nil] the aggregate's declared prose description
86
+ # @param attributes [Array<Bluebook::Attribute>] the aggregate's declared fields
87
+ # @param value_objects [Array<Class>] the value object classes (`Bluebook::ValueObject`
88
+ # subclasses) declared on this aggregate
89
+ # @param commands [Array<Class>] the command classes (`Bluebook::Command` subclasses)
90
+ # declared on this aggregate
91
+ # @param invariants [Array<Bluebook::Invariant>] the aggregate-level rules that must
92
+ # always hold
93
+ # @param preconditions [Array<Bluebook::Given>] the aggregate's own named `given`s, a
94
+ # referencing command's own resolved `givens` entry can point back at
95
+ # @param projected_fields [Array<Bluebook::ProjectedField>] the declared `projects`
96
+ # fields, copied from another aggregate's own state by the rebuild sweep
97
+ # @param identified_by [String, Symbol, Array<String, Symbol>] the identity path(s)
98
+ # this aggregate is addressed by
99
+ # @param lifecycle [Bluebook::Lifecycle, nil] the aggregate's declared state machine,
100
+ # or `nil` if it declares none
101
+ # @param entities [Array<Class>] the entity classes (`Bluebook::Entity` subclasses)
102
+ # nested directly under this aggregate
103
+ # @param queries [Array<Bluebook::Query>] the queries declared directly on this
104
+ # aggregate
105
+ # @param policies [Array<Bluebook::Policy>] the reactions hoisted onto this aggregate
106
+ # from the chapter that assembled it
107
+ # @param ports [Array<Bluebook::DomainPort>] the aggregate-scoped ports attached after
108
+ # this aggregate was built
109
+ # @param reference_targets [Array<String>] the name of every aggregate this one points
110
+ # at with an aggregate-level `reference_to`
111
+ # @param provenance [Object, nil] the aggregate's declared canonical source, captured
112
+ # exactly as written, or `nil` if it declares none
83
113
  def initialize(name:, description: nil, attributes: [], value_objects: [],
84
114
  commands: [], invariants: [], preconditions: [], projected_fields: [], identified_by: [], lifecycle: nil,
85
115
  entities: [], queries: [], policies: [], ports: [], reference_targets: [],
@@ -10,10 +10,17 @@ module Hecks
10
10
  # It decides nothing. Whether a declaration is admissible was settled on the
11
11
  # way in, by the language.
12
12
  class AggregateAssembly
13
+ # @param row [Hash{Symbol => Object}] one declared aggregate's raw contract data,
14
+ # as `@declaration[:aggregates]` carries it
13
15
  def initialize(row)
14
16
  @row = row
15
17
  end
16
18
 
19
+ # Builds the aggregate's whole owner-chain graph — its value objects, commands,
20
+ # entities, queries and lifecycle — from its raw declaration.
21
+ #
22
+ # @return [Bluebook::Aggregate] the built aggregate, with every reference stamped
23
+ # to resolve against it
17
24
  def aggregate
18
25
  shapes = Array(@row[:value_objects]).map { |shape| value_object(shape) }
19
26
  commands = Array(@row[:commands]).map { |verb| Build.call("Command", verb) }
@@ -3,10 +3,10 @@ module Hecks
3
3
  class Assembly
4
4
  # One way to build a construct, for every construct.
5
5
  #
6
- # There used to be a method per category here — `value_object(row)`,
7
- # `command(row)`, `policy(row)` — each one gathering the same keywords the
8
- # contract already names. This reads the contract instead, so adding a field
9
- # to the language and forgetting to assemble it is caught by the coverage gate
6
+ # Not a method per category — `value_object(row)`, `command(row)`,
7
+ # `policy(row)`, each gathering the same keywords the contract already
8
+ # names. This reads the contract instead, so adding a field to the
9
+ # language and forgetting to assemble it is caught by the coverage gate
10
10
  # rather than by nobody.
11
11
  #
12
12
  # `make` is the only branch, and it is a real one: a construct that became a
@@ -16,6 +16,16 @@ module Hecks
16
16
  module Build
17
17
  module_function
18
18
 
19
+ # Builds one construct from its declared row, reading which keywords it takes
20
+ # and how to read each one off `Assembly.contract(category)`.
21
+ #
22
+ # @param category [String] the contract's category name, such as `"Aggregate"`
23
+ # or `"Command"`
24
+ # @param row [Hash{Symbol => Object}] the construct's own declared row
25
+ # @param extra [Hash{Symbol => Object}] keywords the caller supplies directly,
26
+ # such as already-built children the contract itself cannot derive
27
+ # @return [Object] the built construct: an instance for a category whose
28
+ # `make` is `:new`, or a class for one whose `make` is `:declare`
19
29
  def call(category, row, extra = {})
20
30
  contract = Assembly.contract(category)
21
31
  keywords = contract.fields.to_h { |keyword, (key, reader)| [keyword, read(reader, row[key])] }
@@ -23,12 +33,23 @@ module Hecks
23
33
  holder(contract).public_send(contract.make, **keywords, **extra)
24
34
  end
25
35
 
36
+ # Resolves the class or module a contract's construct is built through.
37
+ #
38
+ # @param contract [Bluebook::Assembly::Contract] the category's field contract
39
+ # @return [Module] the holder that answers `contract.make` (`.new` or `.declare`)
40
+ # @raise [ArgumentError] if the contract names no holder to build through
26
41
  def holder(contract)
27
42
  contract.holder or raise ArgumentError, "#{contract} holds nothing that can be built"
28
43
  end
29
44
 
30
45
  # A reader is a Marks method, a list of them, or one of three spellings that
31
46
  # need no decoding at all.
47
+ #
48
+ # @param reader [Symbol, Array, nil] the contract field's reader: `:plain`,
49
+ # `:identity`, `:flag`, `[:each, marks_method]`, `[:option, name]`, or a bare
50
+ # `Marks` method name
51
+ # @param value [Object] the raw declared value to read
52
+ # @return [Object] the value read through `reader`
32
53
  def read(reader, value)
33
54
  case reader
34
55
  when :plain then value