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
@@ -30,6 +30,10 @@ module Hecks
30
30
 
31
31
  module_function
32
32
 
33
+ # Renders `glossary.md`'s Markdown source as a self-contained HTML page.
34
+ #
35
+ # @param markdown [String] `glossary.md`'s own rendered Markdown source
36
+ # @return [String] the complete `html/index.html` source
33
37
  def render(markdown)
34
38
  blocks = parse(markdown)
35
39
  slugs = heading_slugs(blocks)
@@ -40,6 +44,12 @@ module Hecks
40
44
 
41
45
  # ── reading the Markdown ─────────────────────────────────────────
42
46
 
47
+ # Parses the subset of Markdown the glossary projector emits into
48
+ # structured blocks.
49
+ #
50
+ # @param markdown [String] the Markdown source to parse
51
+ # @return [Array<Block>] one block per heading, quote, list, mermaid
52
+ # fence, or paragraph, in document order
43
53
  def parse(markdown)
44
54
  lines = markdown.lines.map(&:chomp)
45
55
  blocks = []
@@ -70,6 +80,11 @@ module Hecks
70
80
 
71
81
  # The mermaid source between the opening fence at `index` and its
72
82
  # closing one, and the index just past that.
83
+ #
84
+ # @param lines [Array<String>] the document's lines
85
+ # @param index [Integer] the opening ` ```mermaid ` fence's line index
86
+ # @return [Array(Block, Integer)] the parsed `:mermaid` block, and the line
87
+ # index just past its closing fence
73
88
  def fence(lines, index)
74
89
  close = ((index + 1)...lines.size).find { |at| lines[at] == "```" } || lines.size
75
90
  [Block.new(type: :mermaid, text: lines[(index + 1)...close].join("\n")), close + 1]
@@ -77,6 +92,13 @@ module Hecks
77
92
 
78
93
  # The consecutive lines from `index` that satisfy the block, and
79
94
  # the index just past them.
95
+ #
96
+ # @param lines [Array<String>] the document's lines
97
+ # @param index [Integer] the line index to start scanning from
98
+ # @yieldparam text [String] one candidate line
99
+ # @yieldreturn [Boolean] whether that line belongs to the run
100
+ # @return [Array(Array<String>, Integer)] the consecutive matching lines, and
101
+ # the line index just past them
80
102
  def run(lines, index)
81
103
  taken = []
82
104
  while index < lines.size && yield(lines[index])
@@ -93,6 +115,10 @@ module Hecks
93
115
  # members, so two "### Open" headings in different sections would
94
116
  # otherwise be one key, and the first would answer with the
95
117
  # second's "-1" slug, leaving `#open` with nothing to land on.
118
+ #
119
+ # @param blocks [Array<Block>] every parsed block, in document order
120
+ # @return [Hash{Block => String}] each heading block, mapped to its GitHub-style
121
+ # slug, keyed by object identity
96
122
  def heading_slugs(blocks)
97
123
  seen = Hash.new(0)
98
124
  slugs = {}.compare_by_identity
@@ -106,6 +132,11 @@ module Hecks
106
132
 
107
133
  # Everything before the first `##` is the front matter; each `##`
108
134
  # opens a section that runs to the next.
135
+ #
136
+ # @param blocks [Array<Block>] every parsed block, in document order
137
+ # @return [Array<Hash{Symbol => Object}>] one Hash per section: `:heading`
138
+ # (the `Block`, or nil for the leading front matter) and `:blocks` (its
139
+ # own `Array<Block>`)
109
140
  def split_sections(blocks)
110
141
  sections = [{ heading: nil, blocks: [] }]
111
142
  blocks.each do |block|
@@ -117,6 +148,13 @@ module Hecks
117
148
 
118
149
  # ── writing the page ─────────────────────────────────────────────
119
150
 
151
+ # Renders the full HTML page shell around the navigation rail and content.
152
+ #
153
+ # @param title [String] the document's `#` heading text
154
+ # @param sections [Array<Hash{Symbol => Object}>] every `##` section, as
155
+ # `split_sections` builds (the front matter excluded by the caller)
156
+ # @param slugs [Hash{Block => String}] every heading block's own slug
157
+ # @return [String] the complete HTML page source
120
158
  def page(title, sections, slugs)
121
159
  front, *rest = sections
122
160
  <<~HTML
@@ -148,6 +186,14 @@ module Hecks
148
186
  HTML
149
187
  end
150
188
 
189
+ # Renders the navigation rail: the domain name, a search box, and one
190
+ # link per section.
191
+ #
192
+ # @param title [String] the document's `#` heading text, `"Domain — Glossary"`
193
+ # @param sections [Array<Hash{Symbol => Object}>] every `##` section (the
194
+ # front matter excluded)
195
+ # @param slugs [Hash{Block => String}] every heading block's own slug
196
+ # @return [String] the rail's HTML source
151
197
  def rail(title, sections, slugs)
152
198
  domain = title.split(" — ").first
153
199
  items = sections.map do |section|
@@ -167,6 +213,10 @@ module Hecks
167
213
  HTML
168
214
  end
169
215
 
216
+ # Renders the page's `<header>`: the title, vision, lede, and overview diagram.
217
+ #
218
+ # @param blocks [Array<Block>] the front matter's own blocks
219
+ # @return [String] the header's HTML source
170
220
  def front_matter(blocks)
171
221
  parts = ["<header>"]
172
222
  blocks.each do |block|
@@ -185,6 +235,11 @@ module Hecks
185
235
  # rules list), then its terms — each `###` opens an article that
186
236
  # runs to the next. A bold-only paragraph is the caption of
187
237
  # whatever figure or list follows it.
238
+ #
239
+ # @param section [Hash{Symbol => Object}] one `##` section, as `split_sections`
240
+ # builds
241
+ # @param slugs [Hash{Block => String}] every heading block's own slug
242
+ # @return [String] the section's `<section>` HTML source
188
243
  def section_html(section, slugs)
189
244
  heading = section[:heading]
190
245
  state = { parts: [%(<section id="#{slugs[heading]}">), "<h2>#{inline(heading.text)}</h2>"],
@@ -195,6 +250,15 @@ module Hecks
195
250
  state[:parts].join("\n")
196
251
  end
197
252
 
253
+ # Renders one block into `state[:parts]`, mutating `state` as it goes.
254
+ #
255
+ # @param block [Block] the block to render
256
+ # @param state [Hash{Symbol => Object}] the section's own render state:
257
+ # `:parts` (the growing `Array<String>` of HTML), `:caption` (a pending
258
+ # bold-only paragraph's text, or nil), `:in_terms` (whether a `###` term
259
+ # article is currently open)
260
+ # @param slugs [Hash{Block => String}] every heading block's own slug
261
+ # @return [void]
198
262
  def section_block(block, state, slugs)
199
263
  parts = state[:parts]
200
264
  case block.type
@@ -214,6 +278,13 @@ module Hecks
214
278
  end
215
279
  end
216
280
 
281
+ # Renders one paragraph, either capturing it as a pending caption (a
282
+ # bold-only line) or appending it to `state[:parts]`.
283
+ #
284
+ # @param text [String] the paragraph's own text
285
+ # @param state [Hash{Symbol => Object}] the section's own render state (see
286
+ # `section_block`); `:caption` and `:parts` may be mutated
287
+ # @return [void]
217
288
  def paragraph(text, state)
218
289
  if text.match?(/\A\*\*[^*]+\*\*\z/)
219
290
  state[:caption] = text.delete("*")
@@ -223,6 +294,11 @@ module Hecks
223
294
  end
224
295
  end
225
296
 
297
+ # Renders a mermaid block as a `<figure>`, with an optional caption.
298
+ #
299
+ # @param block [Block] the `:mermaid` block to render
300
+ # @param caption [String, nil] the figure's caption text, or nil for none
301
+ # @return [String] the `<figure>`'s HTML source
226
302
  def figure(block, caption)
227
303
  parts = ["<figure>"]
228
304
  parts << "<figcaption>#{escape(caption)}</figcaption>" if caption
@@ -232,10 +308,18 @@ module Hecks
232
308
 
233
309
  # ── inline text ──────────────────────────────────────────────────
234
310
 
311
+ # Escapes text for HTML.
312
+ #
313
+ # @param text [Object, nil] the value to escape, rendered with `to_s`
314
+ # @return [String] `text`, HTML-escaped
235
315
  def escape(text) = Forms::Escape.html(text)
236
316
 
237
317
  # Escape first, so nothing in the prose ever becomes a tag; then
238
318
  # the three inline forms the Markdown uses, on the escaped text.
319
+ #
320
+ # @param text [String] the inline text to render
321
+ # @return [String] `text`, HTML-escaped, with `[x](#y)` links, `**bold**` and
322
+ # `*italic*` spans converted to their HTML equivalents
239
323
  def inline(text)
240
324
  escape(text)
241
325
  .gsub(/\[([^\]]+)\]\(#([^)]+)\)/) { %(<a href="##{Regexp.last_match(2)}">#{Regexp.last_match(1)}</a>) }
@@ -243,6 +327,10 @@ module Hecks
243
327
  .gsub(/\*(.+?)\*/) { "<em>#{Regexp.last_match(1)}</em>" }
244
328
  end
245
329
 
330
+ # Reads one static asset file from beside this file.
331
+ #
332
+ # @param name [String] the asset's filename, such as `"page.css"`
333
+ # @return [String] the asset file's contents, read from beside this file
246
334
  def asset(name) = File.read(File.join(__dir__, name))
247
335
  end
248
336
  end
@@ -39,14 +39,27 @@ module Hecks
39
39
 
40
40
  module_function
41
41
 
42
+ # Titles the document after its chapter.
43
+ #
44
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter to title after
45
+ # @return [String] the document's `#` heading text
42
46
  def title(bluebook) = "#{bluebook.name} — Glossary"
43
47
 
48
+ # Renders the full `glossary.md` document.
49
+ #
50
+ # @param document [Glossary::Document] the assembled document to render
51
+ # @return [String] the complete Markdown source, ending in a newline
44
52
  def render(document)
45
53
  parts = [header(document.bluebook)]
46
54
  parts += document.sections.map { |section| section_text(section, document) }
47
55
  "#{parts.join("\n\n")}\n"
48
56
  end
49
57
 
58
+ # Renders the document's title, vision blockquote, lede, and overview map.
59
+ #
60
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter to render a
61
+ # header for
62
+ # @return [String] the header's Markdown source
50
63
  def header(bluebook)
51
64
  parts = ["# #{title(bluebook)}"]
52
65
  parts << "> #{bluebook.vision}" if bluebook.vision
@@ -55,6 +68,13 @@ module Hecks
55
68
  parts.join("\n\n")
56
69
  end
57
70
 
71
+ # Renders one `##` section: its opening (an aggregate's, or a standing
72
+ # group's blurb), followed by every term inside it.
73
+ #
74
+ # @param section [Glossary::Section] the section to render
75
+ # @param document [Glossary::Document] the document `section` belongs to,
76
+ # for its bluebook and link index
77
+ # @return [String] the section's Markdown source
58
78
  def section_text(section, document)
59
79
  parts = ["## #{section.title}"]
60
80
  parts += section.aggregate ? opening(section.aggregate, document.bluebook) : ["> #{STANDING[section.name]}"]
@@ -64,6 +84,12 @@ module Hecks
64
84
 
65
85
  # What an aggregate is, what it can be, how it fits and moves,
66
86
  # and what is always true of it — before a single term.
87
+ #
88
+ # @param aggregate [Bluebook::Aggregate] the aggregate to render an opening for
89
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter `aggregate` belongs
90
+ # to, for drawing its context diagram
91
+ # @return [Array<String>] the opening's Markdown blocks, one per paragraph or
92
+ # diagram, not yet joined
67
93
  def opening(aggregate, bluebook)
68
94
  parts = []
69
95
  parts << "> #{aggregate.description}" if aggregate.description
@@ -82,6 +108,12 @@ module Hecks
82
108
  # one thing this page never shows as an identifier — so each is
83
109
  # replaced with how it is said, whole-word, and nothing else in
84
110
  # the sentence is touched.
111
+ #
112
+ # @param aggregate [Bluebook::Aggregate] the aggregate to gather rules for
113
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter to read every
114
+ # construct name from, for de-identifying relationship sentences
115
+ # @return [Array<String>] `aggregate`'s (and its entities') attribute and
116
+ # invariant statements, spoken and deduplicated
85
117
  def always_true(aggregate, bluebook)
86
118
  names = bluebook.aggregates.flat_map { |other| [other.hecks_name, *other.entities.map(&:hecks_name)] }
87
119
  [aggregate, *aggregate.entities].flat_map do |holder|
@@ -90,14 +122,29 @@ module Hecks
90
122
  end.uniq
91
123
  end
92
124
 
125
+ # Replaces every whole-word construct name in `sentence` with how it is said.
126
+ #
127
+ # @param sentence [String] the sentence to de-identify
128
+ # @param names [Array<String>] every construct name to replace, whole-word
129
+ # @return [String] `sentence` with each name in `names` replaced by
130
+ # `Naming.words(name)`
93
131
  def spoken(sentence, names)
94
132
  names.reduce(sentence) { |text, name| text.gsub(/\b#{Regexp.escape(name)}\b/, Naming.words(name)) }
95
133
  end
96
134
 
135
+ # Renders one term's `###` heading and its paragraphs.
136
+ #
137
+ # @param entry [Glossary::Entry] the term to render
138
+ # @param index [Glossary::Index] the document's link index
139
+ # @return [String] the term's Markdown source
97
140
  def term_text(entry, index)
98
141
  ["### #{entry.headword}", *Sentences.paragraphs(entry, index)].join("\n\n")
99
142
  end
100
143
 
144
+ # Wraps a Mermaid diagram's source in a fenced code block.
145
+ #
146
+ # @param source [String] a Mermaid diagram's own source
147
+ # @return [String] `source` wrapped in a ` ```mermaid ` fence
101
148
  def fence(source) = "```mermaid\n#{source}\n```"
102
149
  end
103
150
  end
@@ -25,6 +25,10 @@ module Hecks
25
25
  module Mermaid
26
26
  module_function
27
27
 
28
+ # Draws the whole chapter's aggregates and which points at which.
29
+ #
30
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter to draw
31
+ # @return [String] a Mermaid `flowchart LR` source
28
32
  def map(bluebook)
29
33
  names = bluebook.aggregates.map(&:hecks_name)
30
34
  lines = ["flowchart LR"]
@@ -39,6 +43,12 @@ module Hecks
39
43
  lines.join("\n")
40
44
  end
41
45
 
46
+ # Draws one aggregate, its entities, and its neighbours.
47
+ #
48
+ # @param aggregate [Bluebook::Aggregate] the aggregate to focus on
49
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter to find
50
+ # neighbouring aggregates in
51
+ # @return [String] a Mermaid `flowchart LR` source
42
52
  def context(aggregate, bluebook)
43
53
  focus = aggregate.hecks_name
44
54
  lines = ["flowchart LR", node(focus, focus: true)]
@@ -58,6 +68,11 @@ module Hecks
58
68
  lines.uniq.join("\n")
59
69
  end
60
70
 
71
+ # Draws the states a holder can be in and what moves it between them.
72
+ #
73
+ # @param holder [Bluebook::Aggregate, Bluebook::Entity] the holder whose
74
+ # lifecycle to draw
75
+ # @return [String] a Mermaid `stateDiagram-v2` source
61
76
  def lifecycle(holder)
62
77
  lifecycle = holder.lifecycle
63
78
  lines = ["stateDiagram-v2"]
@@ -74,15 +89,33 @@ module Hecks
74
89
 
75
90
  # ── the facts drawn ──────────────────────────────────────────────
76
91
 
92
+ # Names a holder's own reference attributes.
93
+ #
94
+ # @param holder [Bluebook::Aggregate, Bluebook::Entity] the holder to
95
+ # read reference attributes from
96
+ # @return [Array<Bluebook::Attribute>] `holder`'s own reference attributes
77
97
  def references(holder) = holder.attributes.select(&:reference?)
78
98
 
79
99
  # An entity is held as `list_of(LedgerEntry)` — the attribute
80
100
  # whose element type names one of the aggregate's own entities.
101
+ #
102
+ # @param aggregate [Bluebook::Aggregate] the aggregate to read entity
103
+ # attributes from
104
+ # @return [Array<Bluebook::Attribute>] attributes whose element type is one
105
+ # of `aggregate`'s own entities
81
106
  def entities(aggregate)
82
107
  names = aggregate.entities.map(&:hecks_name)
83
108
  aggregate.attributes.select { |attribute| !attribute.reference? && names.include?(attribute.type.to_s) }
84
109
  end
85
110
 
111
+ # Finds every other aggregate that references `aggregate`.
112
+ #
113
+ # @param aggregate [Bluebook::Aggregate] the aggregate to find pointers to
114
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter to search other
115
+ # aggregates in
116
+ # @return [Array<Array(Bluebook::Aggregate, Bluebook::Attribute)>] every other
117
+ # aggregate's own reference attribute that targets `aggregate`, paired
118
+ # with its holder
86
119
  def pointing_at(aggregate, bluebook)
87
120
  bluebook.aggregates.reject { |other| other.equal?(aggregate) }.flat_map do |other|
88
121
  references(other).select { |attribute| attribute.type.target_name == aggregate.hecks_name }
@@ -94,13 +127,28 @@ module Hecks
94
127
 
95
128
  # A node id the page source can carry without an identifier in
96
129
  # it — `n_atm_card`, never `n_ATMCard`.
130
+ #
131
+ # @param name [String, Symbol] the construct name to derive an id from
132
+ # @return [String] a Mermaid-safe node id, such as `"n_atm_card"`
97
133
  def id(name) = "n_#{Naming.snake(name).gsub(/[^a-z0-9_]/, '_')}"
98
134
 
135
+ # Renders one Mermaid node declaration.
136
+ #
137
+ # @param name [String, Symbol] the node's construct name
138
+ # @param focus [Boolean] whether to mark this node with the `focus` CSS class
139
+ # @return [String] a Mermaid node declaration line
99
140
  def node(name, focus: false)
100
141
  line = " #{id(name)}[\"#{Naming.words(name)}\"]"
101
142
  focus ? "#{line}:::focus" : line
102
143
  end
103
144
 
145
+ # Renders one Mermaid edge declaration.
146
+ #
147
+ # @param from [String, Symbol] the source node's construct name
148
+ # @param attribute [Bluebook::Attribute] the attribute the edge is labelled after
149
+ # @param to [String, Symbol] the target node's construct name
150
+ # @return [String] a Mermaid edge declaration line, labelled with the
151
+ # attribute's spoken name
104
152
  def edge(from, attribute, to)
105
153
  " #{id(from)} -->|\"#{Naming.words(attribute.name).downcase}\"| #{id(to)}"
106
154
  end
@@ -30,6 +30,11 @@ module Hecks
30
30
 
31
31
  # The paragraphs under a term's headword — the definition first,
32
32
  # then, when the term carries rules, one "Always true: …" line.
33
+ #
34
+ # @param entry [Glossary::Entry] the term to render
35
+ # @param index [Glossary::Index] the document's link index
36
+ # @return [Array<String>] one or more paragraphs, compacted of any that
37
+ # have nothing to say (such as an entity with no lifecycle)
33
38
  def paragraphs(entry, index)
34
39
  facts = entry.facts
35
40
  case entry.kind
@@ -47,10 +52,18 @@ module Hecks
47
52
 
48
53
  # An aggregate's lede or an entity's entry: what it is, then the
49
54
  # states it can be in.
55
+ #
56
+ # @param holder [Bluebook::Aggregate, Bluebook::Entity] the holder to describe
57
+ # @return [Array<String>] `holder`'s description, then its lifecycle sentence;
58
+ # either or both may be absent
50
59
  def holder_paragraphs(holder)
51
60
  [holder.description, holder.lifecycle && lifecycle_sentence(holder.lifecycle)].compact
52
61
  end
53
62
 
63
+ # Describes a lifecycle's starting state and every state it can reach.
64
+ #
65
+ # @param lifecycle [Bluebook::Lifecycle] the state machine to describe
66
+ # @return [String] what state it starts in and every state it can reach
54
67
  def lifecycle_sentence(lifecycle)
55
68
  states = ([lifecycle.default] + lifecycle.transitions.map { |_name, transition| transition.target }).uniq
56
69
  "Starts out #{spoken(lifecycle.default)}. " \
@@ -58,8 +71,19 @@ module Hecks
58
71
  end
59
72
 
60
73
  # A state name is spelled `awaiting_credit`; said, it is "awaiting credit".
74
+ #
75
+ # @param state [String, Symbol] a state name
76
+ # @return [String] `state` with underscores turned to spaces
61
77
  def spoken(state) = state.to_s.tr("_", " ")
62
78
 
79
+ # Renders a value object's definition, and its rules if it has any.
80
+ #
81
+ # @param value_object [Bluebook::ValueObject] the value object to describe
82
+ # @param index [Glossary::Index] the document's link index
83
+ # @param within [String, nil] the value object's own holder's name, for
84
+ # resolving a nested value object's own type link
85
+ # @return [Array<String>] the value object's definition sentence, then its
86
+ # "Always true: …" rules line; the rules line is absent when it has none
63
87
  def value_object_paragraphs(value_object, index, within)
64
88
  rules = value_object.invariants.map { |invariant| Statements.invariant_statement(invariant) }
65
89
  [value_object_sentence(value_object, index, within), rules_line(rules)].compact
@@ -67,6 +91,11 @@ module Hecks
67
91
 
68
92
  # A one-field object whose field is just "value" is its type —
69
93
  # "Text.", "A whole number." — the field name would add nothing.
94
+ #
95
+ # @param value_object [Bluebook::ValueObject] the value object to describe
96
+ # @param index [Glossary::Index] the document's link index
97
+ # @param within [String, nil] the value object's own holder's name
98
+ # @return [String] the value object's definition sentence
70
99
  def value_object_sentence(value_object, index, within)
71
100
  return closed_set_sentence(value_object.members) if value_object.closed_set?
72
101
  return "A marker with no details of its own." if value_object.attributes.empty?
@@ -82,10 +111,22 @@ module Hecks
82
111
 
83
112
  # "amount (a whole number)" — the field as the author named it,
84
113
  # then what kind of thing goes in it.
114
+ #
115
+ # @param field [Bluebook::Attribute] the field to describe
116
+ # @param index [Glossary::Index] the document's link index
117
+ # @param within [String, nil] the field's own holder's name
118
+ # @return [String] the field's name, spoken, with its type in parentheses
85
119
  def field_phrase(field, index, within)
86
120
  "#{Naming.words(field.name).downcase} (#{type_words(field, index, within)})"
87
121
  end
88
122
 
123
+ # Describes one field's type in plain words.
124
+ #
125
+ # @param field [Bluebook::Attribute] the field to describe
126
+ # @param index [Glossary::Index] the document's link index
127
+ # @param within [String, nil] the field's own holder's name
128
+ # @return [String] the field's type in plain words, or a link to its value
129
+ # object's own headword; "a list of …" when the field is a list
89
130
  def type_words(field, index, within)
90
131
  type = field.type.to_s
91
132
  inner = TYPE_WORDS[type] || index.link(:value_object, type, within: within)
@@ -96,6 +137,9 @@ module Hecks
96
137
  # whose rows carry more (StatementFrequency's cadence plus a
97
138
  # retention and a fee) leads with the first field and keeps the
98
139
  # rest beside it, so no row loses what makes it distinct.
140
+ #
141
+ # @param members [Array<Hash{Symbol => Object}>] the closed set's member rows
142
+ # @return [String] "One of: …" (multi-field rows) or "One of …" (single-field)
99
143
  def closed_set_sentence(members)
100
144
  if members.first && members.first.size > 1
101
145
  rows = members.map do |row|
@@ -112,6 +156,10 @@ module Hecks
112
156
  # code." — the rules as their author wrote them, kept out of the
113
157
  # definition sentence (a rule hidden inside a definition is a rule
114
158
  # a reader misses).
159
+ #
160
+ # @param rules [Array<String>] rendered invariant sentences
161
+ # @return [String, nil] "Always true: …" joining every rule, or nil if
162
+ # `rules` is empty
115
163
  def rules_line(rules)
116
164
  return nil if rules.empty?
117
165
 
@@ -119,6 +167,11 @@ module Hecks
119
167
  "Always true: #{clauses.join('; ')}."
120
168
  end
121
169
 
170
+ # Describes a command's goal and who does it.
171
+ #
172
+ # @param command [Bluebook::Command] the command to describe
173
+ # @return [String, nil] the command's goal and/or its role sentence, or nil
174
+ # if it declares neither
122
175
  def command_sentence(command)
123
176
  parts = []
124
177
  parts << with_period(command.goal) if command.goal
@@ -126,6 +179,14 @@ module Hecks
126
179
  parts.empty? ? nil : parts.join(" ")
127
180
  end
128
181
 
182
+ # Describes when an event is recorded and what it prompts.
183
+ #
184
+ # @param facts [Hash{Symbol => Object}] an `:event`-kind entry's facts:
185
+ # `:raised_by` (`Array<Array(Bluebook::Aggregate, Bluebook::Command)>`) and
186
+ # `:policies` (`Array<Bluebook::Policy>`)
187
+ # @param index [Glossary::Index] the document's link index
188
+ # @return [String] which commands record the event, and, when any policy
189
+ # reacts to it, which ones it prompts
129
190
  def event_sentence(facts, index)
130
191
  raisers = command_links(facts[:raised_by], index)
131
192
  sentence = "Recorded after #{Naming.to_sentence_list(raisers, conj: 'or')}."
@@ -138,6 +199,10 @@ module Hecks
138
199
  # account, once for each row of Open for customer." — a
139
200
  # cross-domain trigger (`across "Compliance"`) is spoken as words
140
201
  # with the domain named, since nothing here to link to exists.
202
+ #
203
+ # @param policy [Bluebook::Policy] the policy to describe
204
+ # @param index [Glossary::Index] the document's link index
205
+ # @return [String] when the policy fires and what it asks for
141
206
  def policy_sentence(policy, index)
142
207
  holder, command = split_trigger(policy.trigger_command)
143
208
  asked = if policy.target_domain
@@ -154,6 +219,13 @@ module Hecks
154
219
  "#{sentence}."
155
220
  end
156
221
 
222
+ # Describes when a saga begins and ends, and its states along the way.
223
+ #
224
+ # @param shape [Hash{Symbol => Object}] a process manager's `to_h`, read for
225
+ # `:starts_on`, `:ends_on`, and `:states`
226
+ # @param index [Glossary::Index] the document's link index
227
+ # @return [String] when the saga begins and ends, and, when it declares any,
228
+ # the states it can be in along the way
157
229
  def saga_sentence(shape, index)
158
230
  sentence = "Begins when #{index.link(:event, bare(shape[:starts_on]))} happens " \
159
231
  "and ends when #{index.link(:event, bare(shape[:ends_on]))} happens."
@@ -166,6 +238,11 @@ module Hecks
166
238
  # never "Can credit and debit": banking's System role raises
167
239
  # `Debited` and `Credited`, and "can … debited" is a wrong
168
240
  # sentence. The headwords are already what people say.
241
+ #
242
+ # @param issues [Array<Array(Bluebook::Aggregate, Bluebook::Command)>] the
243
+ # role's own `[holder, command]` pairs
244
+ # @param index [Glossary::Index] the document's link index
245
+ # @return [String] "Responsible for …", naming every command
169
246
  def role_sentence(issues, index)
170
247
  "Responsible for #{Naming.to_sentence_list(command_links(issues, index))}."
171
248
  end
@@ -174,6 +251,12 @@ module Hecks
174
251
  # — a role responsible for CardPayment's Reverse and Transfer's
175
252
  # Reverse is responsible for "Reverse (card payment)" and
176
253
  # "Reverse (transfer)", not for "Reverse" twice.
254
+ #
255
+ # @param issues [Array<Array(Bluebook::Aggregate, Bluebook::Command)>] each
256
+ # `[holder, command]` pair to link
257
+ # @param index [Glossary::Index] the document's link index
258
+ # @return [Array<String>] one Markdown link per distinct `[holder, command]`
259
+ # pair, qualified with the holder's name when the command name repeats
177
260
  def command_links(issues, index)
178
261
  issues = issues.uniq { |holder, command| [holder.hecks_name, command.hecks_name] }
179
262
  repeated = issues.map { |_holder, command| command.hecks_name }.tally.select { |_name, count| count > 1 }
@@ -187,17 +270,39 @@ module Hecks
187
270
 
188
271
  # ── small carpentry ─────────────────────────────────────────────
189
272
 
273
+ # Splits a dotted `Holder.command` trigger into its two parts.
274
+ #
275
+ # @param dotted [String, Symbol] a `"Holder.command"` reference
276
+ # @return [Array(String, String)] `[holder, command]`
190
277
  def split_trigger(dotted)
191
278
  holder, _dot, command = dotted.to_s.rpartition(".")
192
279
  [holder, command]
193
280
  end
194
281
 
282
+ # Strips a dotted name down to its last segment.
283
+ #
284
+ # @param qualified [String, Symbol] a dotted or bare name, such as
285
+ # `"Handler.Dispatch"` or `"Freeze"`
286
+ # @return [String] the name after the last `.`, or the whole name if it has none
195
287
  def bare(qualified) = qualified.to_s.split(".").last
196
288
 
289
+ # Ensures text ends in sentence-ending punctuation.
290
+ #
291
+ # @param text [String, Symbol, nil] the text to punctuate
292
+ # @return [String] `text`, stripped, with a trailing `.` added if it has no
293
+ # sentence-ending punctuation already
197
294
  def with_period(text) = text.to_s.strip.end_with?(".", "!", "?") ? text.to_s.strip : "#{text.to_s.strip}."
198
295
 
296
+ # Lowercases a leading capital letter.
297
+ #
298
+ # @param text [String] the text to adjust
299
+ # @return [String] `text` with a leading uppercase letter lowercased
199
300
  def lower_first(text) = text.sub(/\A[[:upper:]]/, &:downcase)
200
301
 
302
+ # Uppercases a leading lowercase letter.
303
+ #
304
+ # @param text [String] the text to adjust
305
+ # @return [String] `text` with a leading lowercase letter uppercased
201
306
  def upper_first(text) = text.sub(/\A[[:lower:]]/, &:upcase)
202
307
  end
203
308
  end