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
@@ -10,27 +10,30 @@ module Hecks
10
10
  # a sanity check that the domain still says what someone thinks it
11
11
  # says).
12
12
  #
13
- # **Never invents a sentence from nothing** — `DocsProjector`'s own
14
- # discipline, held here too: a relationship's sentence is built
15
- # mechanically from its own declared shape (holder, target,
16
- # relationship kind — the same facts `Projections::Diagrams`'s own
17
- # relationship_edge already reads), and an invariant's sentence is
18
- # the domain author's own `description`, capitalized and
19
- # punctuated, never paraphrased. If a fact has no author-written
20
- # description and no unambiguous mechanical phrasing, it doesn't
21
- # get a sentence here — a wrong sentence is worse than a missing
22
- # one.
13
+ # ## Never invents a sentence from nothing
23
14
  #
24
- # Reachable the same way every projection already is, no new facade
25
- # wiring needed: `Pizzas.project(Projections::Statements)` —
26
- # `Facade::Surface::Chapter#project`'s own comment already settled
15
+ # `DocsProjector`'s own discipline, held here too: a relationship's
16
+ # sentence is built mechanically from its own declared shape (holder,
17
+ # target, relationship kind — the same facts `Projections::Diagrams`'s
18
+ # own relationship_edge already reads), and an invariant's sentence is
19
+ # the domain author's own `description`, capitalized and punctuated,
20
+ # never paraphrased. If a fact has no author-written description and
21
+ # no unambiguous mechanical phrasing, it doesn't get a sentence here —
22
+ # a wrong sentence is worse than a missing one.
23
+ #
24
+ # ## Reachable like any projection
25
+ #
26
+ # No new facade wiring needed: `Pizzas.project(Projections::Statements)`
27
+ # — `Facade::Surface::Chapter#project`'s own comment already settled
27
28
  # this ("anything genuinely needing the graph is a projector, and a
28
29
  # projector is given it"). `bin/statements` is a thin, optional
29
30
  # convenience for reaching the same call from a shell.
30
31
  #
31
- # MVP scope: "has many"/"has a"/"belongs to"/"references" sentences
32
- # for every list or relationship attribute, and every invariant's
33
- # own description (aggregate-level and every nested value object's),
32
+ # ## MVP scope
33
+ #
34
+ # "has many"/"has a"/"belongs to"/"references" sentences for every
35
+ # list or relationship attribute, and every invariant's own
36
+ # description (aggregate-level and every nested value object's),
34
37
  # verbatim. Lifecycle transitions and command given/ensures read the
35
38
  # same way and are the natural next sentences — not built yet.
36
39
  module Statements
@@ -40,12 +43,30 @@ module Hecks
40
43
 
41
44
  module_function
42
45
 
46
+ # Projects every reachable fact in the chapter into its flat sentence list.
47
+ #
48
+ # @param bluebook [Bluebook::Chapter] the chapter being projected
49
+ # @param options [Hash{Symbol => Object}] ignored; present to satisfy the
50
+ # `Projector::Target` calling convention
51
+ # @return [Array<String>] one sentence per attribute relationship and invariant,
52
+ # in holder order
43
53
  def call(bluebook:, options: {})
44
54
  holders(bluebook).flat_map { |holder| statements_for(holder) }
45
55
  end
46
56
 
57
+ # Every construct that can carry its own attributes and invariants — an
58
+ # aggregate and each of its entities.
59
+ #
60
+ # @param bluebook [Bluebook::Chapter] the chapter being walked
61
+ # @return [Array<Bluebook::Aggregate, Bluebook::Entity>] every aggregate followed
62
+ # by its own entities, in declaration order
47
63
  def holders(bluebook) = bluebook.aggregates.flat_map { |aggregate| [aggregate, *aggregate.entities] }
48
64
 
65
+ # All of one holder's sentences: its attribute relationships, then its invariants.
66
+ #
67
+ # @param holder [Bluebook::Aggregate, Bluebook::Entity] the construct being
68
+ # projected
69
+ # @return [Array<String>] the holder's sentences
49
70
  def statements_for(holder)
50
71
  attribute_statements(holder) + invariant_statements(holder)
51
72
  end
@@ -60,10 +81,24 @@ module Hecks
60
81
  # name, because the field's own name is what the domain author
61
82
  # actually chose to call the collection — "toppings", not
62
83
  # "Topping".
84
+ # Projects every one of a holder's attributes that says something in plain
85
+ # English — a list, or a named relationship.
86
+ #
87
+ # @param holder [Bluebook::Aggregate, Bluebook::Entity] the construct whose
88
+ # attributes are being read
89
+ # @return [Array<String>] one sentence per list or relationship attribute; a
90
+ # plain scalar attribute contributes none
63
91
  def attribute_statements(holder)
64
92
  holder.attributes.filter_map { |attribute| attribute_statement(holder, attribute) }
65
93
  end
66
94
 
95
+ # Projects one attribute's relationship, if it has one worth stating.
96
+ #
97
+ # @param holder [Bluebook::Aggregate, Bluebook::Entity] the attribute's owning
98
+ # construct, named as the sentence's subject
99
+ # @param attribute [Bluebook::Attribute] the attribute being projected
100
+ # @return [String, nil] the sentence, or `nil` if `attribute` is neither a list
101
+ # nor a `has_one`/`belongs_to`/`reference_to` relationship
67
102
  def attribute_statement(holder, attribute)
68
103
  subject = "#{article(holder.hecks_name)} #{holder.hecks_name}"
69
104
  return "#{subject} has many #{attribute.name}." if attribute.list?
@@ -85,12 +120,20 @@ module Hecks
85
120
  # "university") because a construct name is always spelled as a
86
121
  # plain word, never an abbreviation read letter-by-letter or a
87
122
  # word starting with a consonant letter but a vowel sound.
123
+ #
124
+ # @param word [String] a construct name to prefix
125
+ # @return [String] `"An"` if `word` starts with a vowel letter, `"A"` otherwise
88
126
  def article(word) = word.to_s.match?(/\A[AEIOUaeiou]/) ? "An" : "A"
89
127
 
90
128
  # Invariants live in two places — directly on the holder (an
91
129
  # aggregate-level rule, checked after every command) and on every
92
130
  # value object nested inside it — `DocsProjector#rules_of`'s own
93
131
  # `value_object_for` lookup is the precedent for walking both.
132
+ #
133
+ # @param holder [Bluebook::Aggregate, Bluebook::Entity] the construct whose
134
+ # invariants, and whose value objects' invariants, are being read
135
+ # @return [Array<String>] one sentence per invariant, the holder's own first,
136
+ # then each nested value object's
94
137
  def invariant_statements(holder)
95
138
  own = Array(holder.respond_to?(:invariants) ? holder.invariants : [])
96
139
  nested = Array(holder.respond_to?(:value_objects) ? holder.value_objects : []).flat_map(&:invariants)
@@ -100,6 +143,10 @@ module Hecks
100
143
  # The domain author's own words, capitalized and punctuated —
101
144
  # nothing else. `invariant("a pizza is named")` already reads as
102
145
  # a sentence; this is the entire transformation.
146
+ #
147
+ # @param invariant [Bluebook::Invariant] the invariant being projected
148
+ # @return [String] the invariant's `description`, capitalized, ending with `.`,
149
+ # `!` or `?`
103
150
  def invariant_statement(invariant)
104
151
  text = invariant.description.to_s.strip
105
152
  text = "#{text[0].upcase}#{text[1..]}" if text[0]
@@ -40,6 +40,11 @@ module Hecks
40
40
 
41
41
  # The projector protocol. `options` is unused: a vocabulary table
42
42
  # has nothing to vary.
43
+ #
44
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter declaring the
45
+ # Vocabulary aggregate to project
46
+ # @param options [Hash] unused; accepted to satisfy the registry's call shape
47
+ # @return [String] the rendered `lib/hecks/vocabulary.rb` source
43
48
  def call(bluebook:, options: {}) = render(bluebook)
44
49
 
45
50
  # Full rows, not just the first field of each.
@@ -50,12 +55,24 @@ module Hecks
50
55
  # Taking the first field of those produced a list of thirty-nine
51
56
  # duplicated error names — well-formed and meaningless. So rows are
52
57
  # carried whole, and the terms are derived from them.
58
+ #
59
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter declaring the
60
+ # Vocabulary aggregate to read
61
+ # @return [Hash{String => Array<Hash{String => String}>}] each closed set's name,
62
+ # mapped to its member rows with every field stringified
53
63
  def tables(bluebook)
54
64
  bluebook.aggregate("Vocabulary").value_objects.to_h do |vo|
55
65
  [vo.hecks_name, vo.members.map { |row| row.to_h.transform_keys(&:to_s).transform_values(&:to_s) }]
56
66
  end
57
67
  end
58
68
 
69
+ # Renders `lib/hecks/vocabulary.rb`'s full source: the frozen `TABLES`
70
+ # and `TERMS` constants and the `fetch`/`rows`/`symbols`/`names` reader
71
+ # methods, built from `bluebook`'s declared closed sets.
72
+ #
73
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter declaring the
74
+ # Vocabulary aggregate to render
75
+ # @return [String] the generated Ruby source, ready to write to disk
59
76
  def render(bluebook)
60
77
  rows = tables(bluebook).sort_by(&:first).map do |name, members|
61
78
  " #{name.inspect} => [\n#{members.map { |row| " #{row.inspect}.freeze" }.join(",\n")}\n ].freeze"
@@ -10,10 +10,14 @@ module Hecks
10
10
  # then drifts — and the first thing to drift is the help text, which is the
11
11
  # only part anybody reads.
12
12
  #
13
- # What is projected, and what is not. This answers the surface — the verb
14
- # tree, the argument spec, the usage text — and nothing executes here. One
15
- # small generic runner (`bin/run`) boots a domain, asks for this, parses
16
- # against it and dispatches.
13
+ # ## What is projected
14
+ #
15
+ # This answers the surface only — the verb tree, the argument spec, the
16
+ # usage text — and nothing executes here. One small generic runner
17
+ # (`bin/run`) boots a domain, asks for this, parses against it and
18
+ # dispatches.
19
+ #
20
+ # ## Why project rather than generate
17
21
  #
18
22
  # The alternative was generating an executable per domain, which is what
19
23
  # `bin/project_rust` does for a whole runtime and would be the more
@@ -23,12 +27,13 @@ module Hecks
23
27
  # reads. Projecting the surface keeps one dispatcher and a help text that
24
28
  # cannot be stale, because it is computed at the moment it is printed.
25
29
  #
26
- # **The typing is the point**. A CLI hands everything over as a String.
27
- # `sequence.value=99` has to become the Integer 99 or the runtime refuses
28
- # it, and the only honest place to learn that is the value object's own
29
- # declared field type. A CLI that guessed — "it looks like a number" —
30
- # would send 99 for a version string of "99" and be wrong in a way nobody
31
- # could see.
30
+ # ## The typing is the point
31
+ #
32
+ # A CLI hands everything over as a String. `sequence.value=99` has to
33
+ # become the Integer 99 or the runtime refuses it, and the only honest
34
+ # place to learn that is the value object's own declared field type. A CLI
35
+ # that guessed — "it looks like a number" — would send 99 for a version
36
+ # string of "99" and be wrong in a way nobody could see.
32
37
  module CliProjector
33
38
  module_function
34
39
 
@@ -45,6 +50,17 @@ module Hecks
45
50
  # shell-safe where a `?` suffix would be eaten by globbing, and it makes
46
51
  # the collision impossible rather than detected. It also reads as what it
47
52
  # is: everything under `ask` changes nothing.
53
+ #
54
+ # @param bluebook [Bluebook::Chapter] the booted domain to project
55
+ # @param options [Hash{Symbol => Object}] `:program` (String, defaults to
56
+ # `"bin/run"`) is echoed into the usage text; `:verb` (String) and `:ask`
57
+ # (Boolean) select one verb's `--help` text instead of the full usage
58
+ # @return [Hash{Symbol => Object}] `:verbs` and `:questions` map each projected
59
+ # name to its spec hash; `:names` maps `:command`/`:question` to an alias table
60
+ # (short and full spelling both keying the full name); `:usage` is the
61
+ # pre-rendered help text
62
+ # @raise [Bluebook::DSL::Malformed] if two verbs project to the same command-line
63
+ # name
48
64
  def call(bluebook:, options: {})
49
65
  verbs = {}
50
66
  questions = {}
@@ -114,6 +130,10 @@ module Hecks
114
130
  # aggregates declaring `Close` keep `customer.close` and `account.close`,
115
131
  # which is the honest answer — a CLI that picked one would be choosing
116
132
  # for the caller.
133
+ #
134
+ # @param specs [Hash{String => Hash}] the verb or question map being projected,
135
+ # mutated in place: each spec hash gains a `:short` key
136
+ # @return [void]
117
137
  def shorten(specs)
118
138
  tails = specs.keys.group_by { |name| name.split(".").last }
119
139
  specs.each do |name, spec|
@@ -122,6 +142,12 @@ module Hecks
122
142
  end
123
143
  end
124
144
 
145
+ # Builds the alias table `CliRunner` resolves a typed name against.
146
+ #
147
+ # @param specs [Hash{String => Hash}] a verb or question map whose specs already
148
+ # carry `:short` (set by `shorten`)
149
+ # @return [Hash{String => String}] every accepted spelling — the full name and
150
+ # its `:short` form — mapped to the full name
125
151
  def aliases(specs)
126
152
  specs.each_with_object({}) do |(name, spec), map|
127
153
  map[name] = name
@@ -134,6 +160,13 @@ module Hecks
134
160
  # subcommands. Refused here rather than silently resolving to whichever
135
161
  # was walked first, which is how `Ticket.Filed` (a command) and
136
162
  # `Ticket.Filed` (a query) sat undetected until something flattened them.
163
+ #
164
+ # @param verbs [Hash{String => Hash}] the verb or question map being built,
165
+ # mutated in place
166
+ # @param name [String] the command-line name this spec claims
167
+ # @param spec [Hash{Symbol => Object}] the spec hash to store under `name`
168
+ # @return [void]
169
+ # @raise [Bluebook::DSL::Malformed] if `name` is already claimed by another verb
137
170
  def claim(verbs, name, spec)
138
171
  if verbs.key?(name)
139
172
  raise Bluebook::DSL::Malformed,
@@ -144,6 +177,15 @@ module Hecks
144
177
  verbs[name] = spec
145
178
  end
146
179
 
180
+ # Builds the dotted command-line name for one verb: `aggregate[.entity].verb`,
181
+ # snake-cased.
182
+ #
183
+ # @param aggregate [Bluebook::Aggregate] the verb's owning aggregate
184
+ # @param verb [Bluebook::Command, Bluebook::Query, Bluebook::PortOperation]
185
+ # the command, query or port operation being named
186
+ # @param entity [Bluebook::Entity, nil] the entity the verb is declared on, or `nil`
187
+ # for an aggregate-level verb
188
+ # @return [String] the dotted, snake-cased command-line name
147
189
  def name_for(aggregate, verb, entity = nil)
148
190
  parts = [Naming.snake(aggregate.hecks_name)]
149
191
  parts << Naming.snake(entity.hecks_name) if entity
@@ -151,6 +193,16 @@ module Hecks
151
193
  parts.join(".")
152
194
  end
153
195
 
196
+ # Builds the verb's fully-qualified language name, `Bluebook::Aggregate[.Entity].Verb`,
197
+ # for display in help text.
198
+ #
199
+ # @param bluebook [Bluebook::Chapter] the verb's owning chapter
200
+ # @param aggregate [Bluebook::Aggregate] the verb's owning aggregate
201
+ # @param verb [Bluebook::Command, Bluebook::Query, Bluebook::PortOperation]
202
+ # the command, query or port operation being named
203
+ # @param entity [Bluebook::Entity, nil] the entity the verb is declared on, or `nil`
204
+ # for an aggregate-level verb
205
+ # @return [String] the fully-qualified name, as the language spells it
154
206
  def fqn(bluebook, aggregate, verb, entity = nil)
155
207
  [bluebook.name, "::", aggregate.hecks_name, ".",
156
208
  entity ? "#{entity.hecks_name}." : "", verb.hecks_name].join
@@ -166,6 +218,14 @@ module Hecks
166
218
  #
167
219
  # `nil` — a creating command — takes none: there is no existing record
168
220
  # yet for `to=` to name.
221
+ #
222
+ # @param receiver [Symbol, nil] `:entity`, `:aggregate`, or `nil` for a creating
223
+ # command that names no existing record
224
+ # @param aggregate [Bluebook::Aggregate] the aggregate the record belongs to
225
+ # @param entity [Bluebook::Entity, nil] the entity to name, required when
226
+ # `receiver` is `:entity`
227
+ # @return [Array<Hash{Symbol => Object}>] the `to`/`to.aggregate`+`to.entity` option
228
+ # specs this receiver needs, or `[]` for `nil`
169
229
  def receiver_options(receiver, aggregate, entity)
170
230
  case receiver
171
231
  when :entity
@@ -185,6 +245,18 @@ module Hecks
185
245
 
186
246
  # ── one verb ──────────────────────────────────────────────────────
187
247
 
248
+ # Projects one command into the spec `verb_help` and `CliRunner` both read.
249
+ #
250
+ # @param bluebook [Bluebook::Chapter] the command's owning chapter
251
+ # @param aggregate [Bluebook::Aggregate] the command's owning aggregate
252
+ # @param entity [Bluebook::Entity, nil] the entity the command is declared on, or
253
+ # `nil` for an aggregate-level command
254
+ # @param command [Bluebook::Command] the command being projected
255
+ # @return [Hash{Symbol => Object}] `:verb` (fully-qualified name), `:kind` (`:command`),
256
+ # `:summary`, `:role` (String, nil if the command declares none), `:role_gated`
257
+ # (Boolean), `:creates` (Boolean), `:receiver` (`:entity`, `:aggregate` or `nil`),
258
+ # `:legacy_receiver` (`:id` or `nil`), `:legacy_arguments`, `:refusals`
259
+ # (see `refusals`) and `:arguments` (see `options_for`)
188
260
  def command_spec(bluebook, aggregate, entity, command)
189
261
  holder = entity || aggregate
190
262
  arguments = command.attributes.flat_map { |a| options_for(a, holder, aggregate) }
@@ -229,6 +301,16 @@ module Hecks
229
301
  # suite is red, and refuse only when rspec could not run. Somebody
230
302
  # reading `--help` should not have to open the hecksagon to find that
231
303
  # out.
304
+ #
305
+ # @param bluebook [Bluebook::Chapter] the port's owning chapter
306
+ # @param aggregate [Bluebook::Aggregate] the aggregate the port is declared on
307
+ # @param port [Bluebook::DomainPort] the port the operation is declared on
308
+ # @param operation [Bluebook::PortOperation] the operation being projected
309
+ # @return [Hash{Symbol => Object}] `:verb` (the `Aggregate.port.operation` name),
310
+ # `:kind` (`:command`), `:creates` (`false`), `:receiver` (`:aggregate`),
311
+ # `:refusals` (`[]`), `:role` (String, who calls whom through the port),
312
+ # `:role_gated` (`false`), `:summary` (see `port_summary`) and `:arguments`
313
+ # (see `options_for`)
232
314
  def port_spec(bluebook, aggregate, port, operation)
233
315
  arguments = receiver_options(:aggregate, aggregate, nil) +
234
316
  operation.attributes.flat_map { |a| options_for(a, aggregate, aggregate) }
@@ -260,6 +342,12 @@ module Hecks
260
342
  summary: port_summary(port, operation), arguments: arguments }
261
343
  end
262
344
 
345
+ # Names both endings a port operation can have: what an inbound `tells` records,
346
+ # or what an outbound `asks` came back with and what it said instead.
347
+ #
348
+ # @param port [Bluebook::DomainPort] the operation's owning port
349
+ # @param operation [Bluebook::PortOperation] the operation being summarized
350
+ # @return [String] one line, worded for `--help`
263
351
  def port_summary(port, operation)
264
352
  return "#{port.name} reports it; emits #{operation.emits.join(', ')}" unless operation.outbound?
265
353
 
@@ -268,6 +356,12 @@ module Hecks
268
356
 
269
357
  # A rootless report takes nothing; a rooted one takes the id of the
270
358
  # record it is a view of, under the name the model gave that reference.
359
+ #
360
+ # @param bluebook [Bluebook::Chapter] the report's owning chapter
361
+ # @param model [Bluebook::ReadModel] the report being projected
362
+ # @return [Hash{Symbol => Object}] `:verb` (`Chapter.ReportName`), `:kind`
363
+ # (`:query`), `:summary` and `:arguments` (`[]`, or one required id option
364
+ # named after the model's `reference_name` when it declares a `reference_target`)
271
365
  def report_spec(bluebook, model)
272
366
  arguments =
273
367
  if model.reference_target
@@ -281,6 +375,15 @@ module Hecks
281
375
  summary: model.description, arguments: arguments }
282
376
  end
283
377
 
378
+ # Projects one query into the spec `verb_help` and `CliRunner` both read.
379
+ #
380
+ # @param bluebook [Bluebook::Chapter] the query's owning chapter
381
+ # @param aggregate [Bluebook::Aggregate] the query's owning aggregate
382
+ # @param entity [Bluebook::Entity, nil] the entity the query is declared on, or
383
+ # `nil` for an aggregate-level query
384
+ # @param query [Bluebook::Query] the query being projected
385
+ # @return [Hash{Symbol => Object}] `:verb` (fully-qualified name), `:kind`
386
+ # (`:query`), `:summary` and `:arguments` (see `options_for`)
284
387
  def query_spec(bluebook, aggregate, entity, query)
285
388
  arguments = Array(query.to_h[:attributes]).flat_map do |declared|
286
389
  attribute = query.attributes.find { |a| a.name.to_s == declared[:name].to_s }
@@ -308,6 +411,18 @@ module Hecks
308
411
  # one-level CLI is not merely inconvenient, it is a machine for writing
309
412
  # malformed records into a real store, which is what it did on its first
310
413
  # run against the pizzas database.
414
+ #
415
+ # @param attribute [Bluebook::Attribute] the field being projected
416
+ # @param holder [Bluebook::Aggregate, Bluebook::Entity, Class] the construct
417
+ # `attribute` is declared on; a `Bluebook::ValueObject` subclass on a recursive call
418
+ # @param aggregate [Bluebook::Aggregate] the top-level aggregate, kept across recursion
419
+ # so a nested value object can still be found
420
+ # @param prefix [String, nil] the dotted path built so far, `nil` at the top level
421
+ # @param optional [Boolean, nil] whether an enclosing field already makes this one
422
+ # optional; `nil` defers to `attribute.optional?`
423
+ # @return [Array<Hash{Symbol => Object}>] one option spec per leaf field, each with
424
+ # `:path`, `:type`, `:required`, and optionally `:note`, `:enum`, `:pattern`,
425
+ # `:default`, `:list`
311
426
  def options_for(attribute, holder, aggregate, prefix = nil, optional = nil)
312
427
  path = [prefix, attribute.name].compact.join(".")
313
428
  optional ||= attribute.optional?
@@ -342,11 +457,27 @@ module Hecks
342
457
  fields.map { |option| option.merge(list: true, note: [option[:note], "repeatable"].compact.join("; ")) }
343
458
  end
344
459
 
460
+ # Projects a `reference_to` attribute as the id of the record it points at.
461
+ #
462
+ # @param attribute [Bluebook::Attribute] a `reference?` attribute
463
+ # @return [Hash{Symbol => Object}] `:path`, `:type` (`"String"`), `:required` and
464
+ # `:note`
345
465
  def reference_option(attribute)
346
466
  { path: attribute.name.to_s, type: "String", required: !attribute.optional?,
347
467
  note: "id of a #{attribute.type.target_name}" }
348
468
  end
349
469
 
470
+ # Projects one leaf field — a plain attribute, or one field of a flattened
471
+ # value object.
472
+ #
473
+ # @param path [String] the dotted option path this field is reached at
474
+ # @param field [Bluebook::Attribute] the field being projected
475
+ # @param optional [Boolean] whether this option may be omitted
476
+ # @param enum [Array<Object>] the closed set of values this field admits, `[]` for
477
+ # an open field
478
+ # @return [Hash{Symbol => Object}] `:path`, `:type`, `:required`, plus `:enum` when
479
+ # `enum` is non-empty, `:pattern` when `field` declares one, and `:default` when
480
+ # `field` declares a non-nil one
350
481
  def scalar_option(path, field, optional, enum: [])
351
482
  option = { path: path, type: field.type.to_s, required: !optional }
352
483
  option[:enum] = enum unless enum.empty?
@@ -355,12 +486,30 @@ module Hecks
355
486
  option
356
487
  end
357
488
 
489
+ # The values a `one_of` value object's field is closed to, for the option's
490
+ # `:enum`.
491
+ #
492
+ # @param value_object [Class] a `Bluebook::ValueObject` subclass
493
+ # @param field [Bluebook::Attribute] the value object's field being projected
494
+ # @return [Array<Object>] the distinct values every declared member gives this
495
+ # field, or `[]` when the value object declares no closed set
358
496
  def closed_members(value_object, field)
359
497
  return [] unless value_object.closed_set?
360
498
 
361
499
  value_object.members.filter_map { |member| member[field.name] }.uniq
362
500
  end
363
501
 
502
+ # Finds the value object type an attribute declares, if any — the fork between
503
+ # `scalar_option` (one option) and recursing into `options_for` (one option per
504
+ # field).
505
+ #
506
+ # @param attribute [Bluebook::Attribute] the field whose type is being resolved
507
+ # @param holder [Bluebook::Aggregate, Bluebook::Entity, Class, nil] the construct
508
+ # `attribute` is declared on, searched first
509
+ # @param aggregate [Bluebook::Aggregate, nil] the top-level aggregate, searched when
510
+ # `holder` does not declare the value object itself
511
+ # @return [Class, nil] the matching `Bluebook::ValueObject` subclass, or `nil` if
512
+ # `attribute`'s type names no value object in scope
364
513
  def value_object_for(attribute, holder, aggregate)
365
514
  [holder, aggregate].compact.each do |scope|
366
515
  next unless scope.respond_to?(:value_objects)
@@ -373,6 +522,13 @@ module Hecks
373
522
 
374
523
  # Every way this verb can say no, in the chapter's own words — printed
375
524
  # by `--help` before the caller spends a dispatch finding out.
525
+ #
526
+ # @param command [Bluebook::Command] the command whose refusals are being projected
527
+ # @param holder [Bluebook::Aggregate, Bluebook::Entity] the aggregate or entity the
528
+ # command's lifecycle guard, if any, is checked against
529
+ # @return [Array<String>] one sentence per way the command can refuse: a lifecycle
530
+ # state mismatch, a missing referenced record per `reference_to` attribute, and
531
+ # each declared `given`'s description, in that order
376
532
  def refusals(command, holder)
377
533
  out = []
378
534
  lifecycle = holder.lifecycle
@@ -386,6 +542,16 @@ module Hecks
386
542
 
387
543
  # ── the help ──────────────────────────────────────────────────────
388
544
 
545
+ # Renders the full verb/question table, or one verb's `--help` text when
546
+ # `options[:verb]` names one.
547
+ #
548
+ # @param bluebook [Bluebook::Chapter] the projected chapter, for its name and vision
549
+ # line
550
+ # @param verbs [Hash{String => Hash}] the projected, `shorten`ed command map
551
+ # @param questions [Hash{String => Hash}] the projected, `shorten`ed query map
552
+ # @param options [Hash{Symbol => Object}] `:program` (String), `:verb` (String, nil)
553
+ # and `:ask` (Boolean) — see `call`
554
+ # @return [String] the rendered help text
389
555
  def usage(bluebook, verbs, questions, options)
390
556
  program = options[:program] || "bin/run"
391
557
  only = options[:verb]
@@ -416,6 +582,12 @@ module Hecks
416
582
  out.join("\n")
417
583
  end
418
584
 
585
+ # Picks one verb whose full spelling differs from its short one, to show the
586
+ # caller both forms exist.
587
+ #
588
+ # @param verbs [Hash{String => Hash}] the projected, `shorten`ed command map
589
+ # @return [String] `"<short> is also <full>"`, or `""` if every verb's short form
590
+ # is its full name (a one-aggregate domain, where no name ever collides)
419
591
  def example_qualified(verbs)
420
592
  name, spec = verbs.find { |key, value| key != value[:short] } || verbs.first
421
593
  name ? "#{spec[:short]} is also #{name}" : ""
@@ -424,6 +596,9 @@ module Hecks
424
596
  # A query's `description` is written as a paragraph — it argues for why
425
597
  # the list is worth reading. A verb table wants the first sentence of
426
598
  # that argument; `--help` still prints the whole thing.
599
+ #
600
+ # @param text [String, nil] the full description, or `nil`
601
+ # @return [String] the text up to and including its first `.`, or `""` for `nil`
427
602
  def first_sentence(text)
428
603
  text.to_s.split(/(?<=\.)\s/).first.to_s
429
604
  end
@@ -434,6 +609,13 @@ module Hecks
434
609
  # below), so each is its own method returning the lines it
435
610
  # contributes — `[]` when it contributes none — concatenated in the
436
611
  # same order the original inline version built them in.
612
+ #
613
+ # @param program [String] how the caller was invoked, echoed in the invocation line
614
+ # @param name [String] the verb's short display name
615
+ # @param spec [Hash{Symbol => Object}] the verb or question's projected spec (see
616
+ # `command_spec`, `port_spec`, `report_spec` or `query_spec`)
617
+ # @param ask [Boolean] whether this is a question, reached through `ask`
618
+ # @return [String] the verb's full `--help` text
437
619
  def verb_help(program, name, spec, ask: false)
438
620
  out = verb_help_meta_lines(name, spec)
439
621
  out.concat(verb_help_invocation_lines(program, name, spec, ask))
@@ -442,6 +624,12 @@ module Hecks
442
624
  out.join("\n")
443
625
  end
444
626
 
627
+ # Renders the name/summary heading, plus the dispatched or read verb and issuing
628
+ # role when the spec declares them.
629
+ #
630
+ # @param name [String] the verb's short display name
631
+ # @param spec [Hash{Symbol => Object}] the verb or question's projected spec
632
+ # @return [Array<String>] the meta block's lines
445
633
  def verb_help_meta_lines(name, spec)
446
634
  out = ["#{name} — #{spec[:summary]}", ""]
447
635
  out << "dispatches #{spec[:verb]}" if spec[:kind] == :command
@@ -450,11 +638,26 @@ module Hecks
450
638
  out
451
639
  end
452
640
 
641
+ # Renders the one-line example invocation, with every argument's path as a
642
+ # placeholder.
643
+ #
644
+ # @param program [String] how the caller was invoked
645
+ # @param name [String] the verb's short display name
646
+ # @param spec [Hash{Symbol => Object}] the verb or question's projected spec, read
647
+ # for `:arguments`
648
+ # @param ask [Boolean] whether to show the `ask` form of the invocation
649
+ # @return [Array<String>] the invocation block's lines
453
650
  def verb_help_invocation_lines(program, name, spec, ask)
454
651
  invocation = ask ? "#{program} ask #{name}" : "#{program} #{name}"
455
652
  ["", " #{invocation}#{spec[:arguments].map { |a| " #{a[:path]}=…" }.join}", ""]
456
653
  end
457
654
 
655
+ # Renders one line per declared argument: its type, admitted values, pattern,
656
+ # default and whether it is optional.
657
+ #
658
+ # @param spec [Hash{Symbol => Object}] the verb or question's projected spec, read
659
+ # for `:arguments`
660
+ # @return [Array<String>] the argument block's lines, `[]` if the verb takes none
458
661
  def verb_help_argument_lines(spec)
459
662
  return [] if spec[:arguments].empty?
460
663
 
@@ -472,6 +675,11 @@ module Hecks
472
675
  lines << ""
473
676
  end
474
677
 
678
+ # Renders the "refused when:" block, one line per way the verb can say no.
679
+ #
680
+ # @param spec [Hash{Symbol => Object}] the verb or question's projected spec, read
681
+ # for `:refusals`
682
+ # @return [Array<String>] the refusal block's lines, `[]` if the verb declares none
475
683
  def verb_help_refusal_lines(spec)
476
684
  return [] if Array(spec[:refusals]).empty?
477
685