hecks 1.4.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +65 -0
  3. data/lib/hecks/adapters/driven/folder.rb +73 -0
  4. data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
  5. data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
  6. data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
  7. data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
  8. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  9. data/lib/hecks/adapters/driven/heki.rb +93 -2
  10. data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
  12. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  13. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  14. data/lib/hecks/adapters/driven/lambda/client.rb +35 -7
  15. data/lib/hecks/adapters/driven/lambda.rb +43 -2
  16. data/lib/hecks/adapters/driven/local_storage.rb +67 -1
  17. data/lib/hecks/adapters/driven/memory.rb +13 -13
  18. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
  19. data/lib/hecks/adapters/driven/prism.rb +46 -7
  20. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  21. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  22. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  23. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  24. data/lib/hecks/adapters/driven.rb +2 -0
  25. data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
  26. data/lib/hecks/behaviors/dsl.rb +58 -0
  27. data/lib/hecks/behaviors/expectations.rb +168 -7
  28. data/lib/hecks/behaviors/ir.rb +11 -0
  29. data/lib/hecks/behaviors/rspec.rb +8 -0
  30. data/lib/hecks/behaviors/runner.rb +19 -0
  31. data/lib/hecks/behaviors.rb +8 -0
  32. data/lib/hecks/bluebook/aggregate.rb +30 -0
  33. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
  34. data/lib/hecks/bluebook/assembly/build.rb +25 -4
  35. data/lib/hecks/bluebook/assembly/contract.rb +66 -14
  36. data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
  37. data/lib/hecks/bluebook/assembly/marks.rb +143 -14
  38. data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
  39. data/lib/hecks/bluebook/assembly.rb +25 -7
  40. data/lib/hecks/bluebook/attribute.rb +17 -3
  41. data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
  42. data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
  43. data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
  44. data/lib/hecks/bluebook/behaviour/command.rb +33 -3
  45. data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
  46. data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
  47. data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
  48. data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
  49. data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
  50. data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
  51. data/lib/hecks/bluebook/behaviour/query.rb +5 -0
  52. data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
  53. data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
  54. data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
  55. data/lib/hecks/bluebook/chapter.rb +23 -0
  56. data/lib/hecks/bluebook/command.rb +53 -8
  57. data/lib/hecks/bluebook/domain_port.rb +25 -0
  58. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
  59. data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
  60. data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
  61. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
  62. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
  63. data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
  64. data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
  65. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
  66. data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
  67. data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
  68. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
  69. data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
  70. data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
  71. data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
  72. data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
  73. data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
  74. data/lib/hecks/bluebook/entity.rb +29 -0
  75. data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
  76. data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
  77. data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
  78. data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
  79. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
  80. data/lib/hecks/bluebook/expression/resolver.rb +320 -77
  81. data/lib/hecks/bluebook/hexagon.rb +34 -0
  82. data/lib/hecks/bluebook/lifecycle.rb +11 -0
  83. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
  84. data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
  85. data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
  86. data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
  87. data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
  88. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
  89. data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
  90. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
  91. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
  92. data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
  93. data/lib/hecks/bluebook/meta_validator.rb +180 -84
  94. data/lib/hecks/bluebook/model_check.rb +268 -24
  95. data/lib/hecks/bluebook/pattern_subset.rb +23 -1
  96. data/lib/hecks/bluebook/process_manager.rb +13 -0
  97. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  98. data/lib/hecks/bluebook/project_loader.rb +40 -0
  99. data/lib/hecks/bluebook/project_register.rb +44 -0
  100. data/lib/hecks/bluebook/query.rb +27 -0
  101. data/lib/hecks/bluebook/read_model.rb +21 -1
  102. data/lib/hecks/bluebook/reference.rb +21 -8
  103. data/lib/hecks/bluebook/smoke_test.rb +29 -6
  104. data/lib/hecks/bluebook/synthesizer.rb +34 -0
  105. data/lib/hecks/bluebook/translation.rb +30 -1
  106. data/lib/hecks/bluebook/value_object.rb +23 -5
  107. data/lib/hecks/bluebook.rb +3 -4
  108. data/lib/hecks/codemod.rb +107 -20
  109. data/lib/hecks/construct.rb +15 -1
  110. data/lib/hecks/corpus.rb +146 -25
  111. data/lib/hecks/deploy/bluebook/deploy.bluebook +105 -0
  112. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  113. data/lib/hecks/deploy/oidc.json +5 -0
  114. data/lib/hecks/doc/reference.rb +185 -16
  115. data/lib/hecks/embryonaut_bluebook.rb +32 -9
  116. data/lib/hecks/facade/handle.rb +76 -3
  117. data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
  118. data/lib/hecks/forms/field_shape.rb +3 -0
  119. data/lib/hecks/forms/page.rb +14 -0
  120. data/lib/hecks/forms/port_argument.rb +12 -0
  121. data/lib/hecks/forms/query_form_renderer.rb +63 -0
  122. data/lib/hecks/forms/record_renderer.rb +58 -0
  123. data/lib/hecks/forms/record_table.rb +27 -0
  124. data/lib/hecks/forms/reference_options.rb +24 -0
  125. data/lib/hecks/forms/value_object_shape.rb +10 -0
  126. data/lib/hecks/fqn.rb +58 -0
  127. data/lib/hecks/framework/bluebook/compliance.bluebook +221 -0
  128. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  129. data/lib/hecks/framework/oidc.json +15 -0
  130. data/lib/hecks/framework.rb +43 -20
  131. data/lib/hecks/freezer.rb +17 -1
  132. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
  133. data/lib/hecks/fuzzing/combination_miner.rb +59 -0
  134. data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
  135. data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
  136. data/lib/hecks/fuzzing/differential.rb +34 -0
  137. data/lib/hecks/fuzzing/domain_generator.rb +188 -11
  138. data/lib/hecks/fuzzing/era_boundary.rb +45 -15
  139. data/lib/hecks/fuzzing/form_census.rb +86 -0
  140. data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
  141. data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
  142. data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
  143. data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
  144. data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
  145. data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
  146. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
  147. data/lib/hecks/fuzzing/properties/guards.rb +44 -0
  148. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
  149. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
  150. data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
  151. data/lib/hecks/fuzzing/properties/querying.rb +68 -14
  152. data/lib/hecks/fuzzing/properties.rb +24 -15
  153. data/lib/hecks/fuzzing/qa_settings.rb +12 -0
  154. data/lib/hecks/fuzzing/replay.rb +137 -29
  155. data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
  156. data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
  157. data/lib/hecks/fuzzing/self_consistency.rb +189 -40
  158. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
  159. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
  160. data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
  161. data/lib/hecks/fuzzing/shrinker.rb +114 -2
  162. data/lib/hecks/fuzzing/structural_skips.rb +18 -4
  163. data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
  164. data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
  165. data/lib/hecks/fuzzing/value_generator.rb +98 -10
  166. data/lib/hecks/grammar/evolve.rb +178 -2
  167. data/lib/hecks/grammar.rb +46 -0
  168. data/lib/hecks/ir.rb +38 -7
  169. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  170. data/lib/hecks/literal.rb +32 -0
  171. data/lib/hecks/naming.rb +88 -7
  172. data/lib/hecks/ports/access_control.rb +5 -10
  173. data/lib/hecks/ports/authorization.rb +3 -6
  174. data/lib/hecks/ports/identity_assignment.rb +1 -2
  175. data/lib/hecks/ports/identity_resolution.rb +1 -2
  176. data/lib/hecks/ports/key_vault.port +6 -0
  177. data/lib/hecks/ports/key_vault.rb +85 -0
  178. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
  179. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
  180. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
  181. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
  182. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
  183. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  184. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
  185. data/lib/hecks/ports/query/in_memory.rb +39 -6
  186. data/lib/hecks/ports/query/ordering.rb +15 -0
  187. data/lib/hecks/ports.rb +1 -0
  188. data/lib/hecks/projections/bootstrap_table.rb +43 -8
  189. data/lib/hecks/projections/deploy/fargate.rb +666 -0
  190. data/lib/hecks/projections/deploy/lambda.rb +2423 -0
  191. data/lib/hecks/projections/deploy/shared.rb +624 -0
  192. data/lib/hecks/projections/diagrams.rb +243 -7
  193. data/lib/hecks/projections/glossary/html.rb +88 -0
  194. data/lib/hecks/projections/glossary/markdown.rb +47 -0
  195. data/lib/hecks/projections/glossary/mermaid.rb +48 -0
  196. data/lib/hecks/projections/glossary/sentences.rb +105 -0
  197. data/lib/hecks/projections/glossary.rb +161 -19
  198. data/lib/hecks/projections/model/deviations.rb +44 -0
  199. data/lib/hecks/projections/model.rb +51 -1
  200. data/lib/hecks/projections/oidc.rb +40 -3
  201. data/lib/hecks/projections/parser_table.rb +49 -8
  202. data/lib/hecks/projections/reference.rb +12 -0
  203. data/lib/hecks/projections/rust_vocabulary.rb +219 -16
  204. data/lib/hecks/projections/shape.rb +8 -0
  205. data/lib/hecks/projections/statements.rb +63 -16
  206. data/lib/hecks/projections/vocabulary.rb +17 -0
  207. data/lib/hecks/projections.rb +3 -0
  208. data/lib/hecks/projector/cli_projector.rb +218 -10
  209. data/lib/hecks/projector/docs_projector.rb +145 -19
  210. data/lib/hecks/projector/exporter.rb +65 -11
  211. data/lib/hecks/projector/ir_projector.rb +6 -0
  212. data/lib/hecks/projector/narrate_projector.rb +136 -15
  213. data/lib/hecks/projector/target.rb +47 -10
  214. data/lib/hecks/projector.rb +101 -19
  215. data/lib/hecks/query_ir.rb +47 -0
  216. data/lib/hecks/query_specification/common/null_policy.rb +5 -3
  217. data/lib/hecks/rendering.rb +6 -0
  218. data/lib/hecks/router/namespace_installer.rb +13 -0
  219. data/lib/hecks/router.rb +55 -0
  220. data/lib/hecks/runtime/aggregate_lock.rb +9 -0
  221. data/lib/hecks/runtime/boot_gates.rb +18 -0
  222. data/lib/hecks/runtime/caller.rb +32 -0
  223. data/lib/hecks/runtime/capability_graph.rb +11 -0
  224. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
  225. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
  226. data/lib/hecks/runtime/command_interpreter.rb +42 -17
  227. data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
  228. data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
  229. data/lib/hecks/runtime/command_rules/references.rb +118 -28
  230. data/lib/hecks/runtime/dependency_planning.rb +45 -0
  231. data/lib/hecks/runtime/dispatcher.rb +28 -50
  232. data/lib/hecks/runtime/entity_element.rb +161 -8
  233. data/lib/hecks/runtime/entity_interpreter.rb +44 -9
  234. data/lib/hecks/runtime/errors.rb +18 -4
  235. data/lib/hecks/runtime/event.rb +10 -5
  236. data/lib/hecks/runtime/identity.rb +71 -3
  237. data/lib/hecks/runtime/instance.rb +67 -7
  238. data/lib/hecks/runtime/interpreting.rb +13 -5
  239. data/lib/hecks/runtime/invocation.rb +118 -36
  240. data/lib/hecks/runtime/loader.rb +94 -8
  241. data/lib/hecks/runtime/outbox.rb +145 -7
  242. data/lib/hecks/runtime/policy_interpreter.rb +22 -9
  243. data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
  244. data/lib/hecks/runtime/query_interpreter.rb +40 -12
  245. data/lib/hecks/runtime/reaction_invocation.rb +53 -8
  246. data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
  247. data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
  248. data/lib/hecks/runtime/reference_hop.rb +42 -0
  249. data/lib/hecks/runtime/refusal_wording.rb +50 -0
  250. data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
  251. data/lib/hecks/runtime/registry/verification.rb +119 -4
  252. data/lib/hecks/runtime/registry.rb +157 -4
  253. data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
  254. data/lib/hecks/runtime/routing.rb +27 -2
  255. data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
  256. data/lib/hecks/runtime/saga_interpreter.rb +27 -13
  257. data/lib/hecks/runtime/tenant_check.rb +26 -6
  258. data/lib/hecks/runtime/tenant_scope.rb +18 -0
  259. data/lib/hecks/runtime/value/coercion.rb +255 -33
  260. data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
  261. data/lib/hecks/runtime/value.rb +50 -0
  262. data/lib/hecks/runtime.rb +32 -0
  263. data/lib/hecks/storehouse.rb +305 -9
  264. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  265. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  266. data/lib/hecks/version.rb +1 -1
  267. data/lib/hecks.rb +79 -1
  268. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
  269. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
  270. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
  271. metadata +14 -5
  272. data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
  273. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
  274. data/lib/hecks/deprecation.rb +0 -95
  275. data/lib/hecks/framework/bluebook/compliance.bluebook +0 -1
@@ -17,25 +17,29 @@ module Hecks
17
17
  # "no, that's wrong". A banker, a support rep, the CEO and an engineer
18
18
  # read the same page.
19
19
  #
20
- # **Three things follow from that audience**. First, no type jargon: a
21
- # term is a term, not "an Aggregate" or "a Value Object", and every
22
- # identifier is spelled as a person says it (`Naming.words`: `ATMCard`
23
- # is "ATM card"). Second, nothing invented: every sentence is either
24
- # the domain author's own words (a `description`, a `goal`, an
25
- # invariant) or built mechanically from a declared fact ("Recorded
26
- # after Freeze account") — the same discipline `DocsProjector` holds
27
- # to; a wrong sentence is worse than a missing one. Third, grouped
28
- # under the aggregate each term belongs to, A to Z within it — a
29
- # reader thinks "what does Account mean" before "what starts with A",
30
- # and once there reads the way a dictionary reads.
20
+ # ## Three things follow from that audience
31
21
  #
32
- # **Two files from one source**. `glossary.md` is the document — every
33
- # construct in it renders on GitHub as-is (headings, blockquotes,
34
- # ```mermaid fences, lists, in-page links). `html/index.html` is that
35
- # exact Markdown string rendered into a page with a navigation rail;
36
- # it is built FROM the Markdown, not beside it, so the two cannot
37
- # drift. `bin/project_glossary` writes both to `<domain>/glossary/` beside
38
- # the bluebook (examples/banking, examples/pizzas, and the QA ledger in qa/).
22
+ # First, no type jargon: a term is a term, not "an Aggregate" or "a
23
+ # Value Object", and every identifier is spelled as a person says it
24
+ # (`Naming.words`: `ATMCard` is "ATM card"). Second, nothing invented:
25
+ # every sentence is either the domain author's own words (a
26
+ # `description`, a `goal`, an invariant) or built mechanically from a
27
+ # declared fact ("Recorded after Freeze account") — the same
28
+ # discipline `DocsProjector` holds to; a wrong sentence is worse than
29
+ # a missing one. Third, grouped under the aggregate each term belongs
30
+ # to, A to Z within it — a reader thinks "what does Account mean"
31
+ # before "what starts with A", and once there reads the way a
32
+ # dictionary reads.
33
+ #
34
+ # ## Two files from one source
35
+ #
36
+ # `glossary.md` is the document — every construct in it renders on
37
+ # GitHub as-is (headings, blockquotes, ```mermaid fences, lists,
38
+ # in-page links). `html/index.html` is that exact Markdown string
39
+ # rendered into a page with a navigation rail; it is built from the
40
+ # Markdown, not beside it, so the two cannot drift. `bin/project_glossary`
41
+ # writes both to `<domain>/glossary/` beside the bluebook (examples/banking,
42
+ # examples/pizzas, and the QA ledger in qa/).
39
43
  #
40
44
  # Projector.call(:glossary, bluebook: <the Bluebook chapter>)
41
45
  # # => { "glossary.md" => "...", "html/index.html" => "..." }
@@ -59,6 +63,14 @@ module Hecks
59
63
 
60
64
  module_function
61
65
 
66
+ # Projects `bluebook`'s glossary as a Markdown document and the HTML
67
+ # page built from it.
68
+ #
69
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter to render a
70
+ # glossary for
71
+ # @param options [Hash] unused; accepted to satisfy the registry's call shape
72
+ # @return [Hash{String => String}] `"glossary.md"` and `"html/index.html"`,
73
+ # each mapped to its rendered content
62
74
  def call(bluebook:, options: {})
63
75
  markdown = Markdown.render(document(bluebook))
64
76
  { "glossary.md" => markdown, "html/index.html" => Html.render(markdown) }
@@ -66,6 +78,11 @@ module Hecks
66
78
 
67
79
  # ── the document ─────────────────────────────────────────────────
68
80
 
81
+ # Gathers every term, groups it into sections, assigns headings and
82
+ # slugs, and builds the index links resolve through.
83
+ #
84
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter to render
85
+ # @return [Document] the fully assembled document, ready for `Markdown.render`
69
86
  def document(bluebook)
70
87
  sections = sections(bluebook, entries(bluebook))
71
88
  Slugs.assign!(bluebook, sections)
@@ -74,6 +91,12 @@ module Hecks
74
91
 
75
92
  # Aggregates first, a to Z by their spoken name ("Account" before
76
93
  # "ATM card"), then the three groups nothing homes to one aggregate.
94
+ #
95
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter declaring the
96
+ # aggregates to group by
97
+ # @param entries [Array<Entry>] every gathered term
98
+ # @return [Array<Section>] one section per aggregate, sorted, followed by
99
+ # any of the Roles/Read models/Reactions sections that have entries
77
100
  def sections(bluebook, entries)
78
101
  grouped = entries.group_by(&:section)
79
102
  list = bluebook.aggregates.sort_by { |aggregate| Naming.words(aggregate.hecks_name).downcase }.map do |aggregate|
@@ -87,10 +110,16 @@ module Hecks
87
110
  list
88
111
  end
89
112
 
90
- # A HEADWORD is qualified only when it would repeat within its own
113
+ # A headword is qualified only when it would repeat within its own
91
114
  # section — "Open" the action and "Open (the list)" the question;
92
115
  # "Return (key issuance)" beside another Return — never numbered
93
116
  # (Chicago 18.9, MDN's disambiguation pages: qualify the headword).
117
+ #
118
+ # @param entries [Array<Entry>] the section's own entries
119
+ # @param section_name [String] the entries' own section name, for telling
120
+ # an entry's own section apart from a borrowed one when qualifying
121
+ # @return [Array<Entry>] `entries`, headword-assigned, sorted by headword
122
+ # then kind
94
123
  def with_headwords(entries, section_name)
95
124
  entries.each { |entry| entry.headword = Naming.words(entry.name) }
96
125
  entries.group_by(&:headword).each_value do |group|
@@ -101,6 +130,14 @@ module Hecks
101
130
  entries.sort_by { |entry| [entry.headword.downcase, entry.kind.to_s] }
102
131
  end
103
132
 
133
+ # Disambiguates one entry's headword from the rest of its group.
134
+ #
135
+ # @param entry [Entry] the entry to qualify
136
+ # @param group [Array<Entry>] every entry sharing `entry`'s plain headword
137
+ # @param section_name [String] the section `entry` and `group` are being
138
+ # rendered under
139
+ # @return [String] `entry`'s headword, qualified with "(the list)" or its
140
+ # own holder's name when needed to tell it apart from the rest of `group`
104
141
  def qualified(entry, group, section_name)
105
142
  base = Naming.words(entry.name)
106
143
  return "#{base} (the list)" if entry.kind == :query && group.any? { |other| other.kind == :command }
@@ -114,6 +151,10 @@ module Hecks
114
151
  # An entity can carry its own commands and queries too — walked the
115
152
  # same one level down `DocsProjector` and `Projections::Diagrams`
116
153
  # already walk it.
154
+ #
155
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter to walk
156
+ # @return [Array<Bluebook::Aggregate, Bluebook::Entity>] every aggregate, each
157
+ # immediately followed by its own nested entities
117
158
  def holders(bluebook)
118
159
  bluebook.aggregates.flat_map { |aggregate| [aggregate, *aggregate.entities] }
119
160
  end
@@ -121,6 +162,10 @@ module Hecks
121
162
  # Every holder's own aggregate, one hop or zero — an aggregate maps
122
163
  # to itself, an entity to whichever aggregate declared it. The one
123
164
  # fact the grouping is built on.
165
+ #
166
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter to walk
167
+ # @return [Hash{String => String}] every aggregate and entity name, mapped to
168
+ # its own owning aggregate's name
124
169
  def holder_aggregate(bluebook)
125
170
  bluebook.aggregates.each_with_object({}) do |aggregate, map|
126
171
  map[aggregate.hecks_name] = aggregate.hecks_name
@@ -131,14 +176,29 @@ module Hecks
131
176
  # Every event's raisers — an event is never declared, only emitted,
132
177
  # so its home is whichever aggregate the first command that raises
133
178
  # it belongs to; a policy or saga reacting to it inherits that home.
179
+ #
180
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter to walk
181
+ # @return [Hash{String => Array<Array(Bluebook::Aggregate, Bluebook::Command)>}]
182
+ # every emitted event name, mapped to `[holder, command]` pairs for every
183
+ # command that raises it; a name absent from the Hash raises nothing
134
184
  def event_raisers(bluebook)
135
185
  holders(bluebook).each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |holder, map|
136
186
  holder.commands.each { |command| command.emits.each { |event| map[event] << [holder, command] } }
137
187
  end
138
188
  end
139
189
 
190
+ # Strips a dotted name down to its last segment.
191
+ #
192
+ # @param qualified [String, Symbol] a dotted or bare name, such as
193
+ # `"Handler.Dispatch"` or `"Freeze"`
194
+ # @return [String] the name after the last `.`, or the whole name if it has none
140
195
  def bare(qualified) = qualified.to_s.split(".").last
141
196
 
197
+ # Gathers every term the glossary carries, ungrouped and unordered.
198
+ #
199
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter to gather terms from
200
+ # @return [Array<Entry>] one entry per entity, value object, command, query,
201
+ # event, policy, saga, role, and read model
142
202
  def entries(bluebook)
143
203
  homes = holder_aggregate(bluebook)
144
204
  raisers = event_raisers(bluebook)
@@ -156,6 +216,10 @@ module Hecks
156
216
  entries
157
217
  end
158
218
 
219
+ # Gathers one entry per entity.
220
+ #
221
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter to gather entities from
222
+ # @return [Array<Entry>] one `:entity`-kind entry per entity
159
223
  def entity_entries(bluebook)
160
224
  bluebook.aggregates.flat_map do |aggregate|
161
225
  aggregate.entities.map do |entity|
@@ -167,6 +231,11 @@ module Hecks
167
231
 
168
232
  # Aggregates only — an entity declares no value objects of its own
169
233
  # (`Bluebook::Entity` deliberately does not answer `value_objects`).
234
+ #
235
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter to gather value
236
+ # objects from
237
+ # @return [Array<Entry>] one `:value_object`-kind entry per aggregate-declared
238
+ # value object
170
239
  def value_object_entries(bluebook)
171
240
  bluebook.aggregates.flat_map do |aggregate|
172
241
  aggregate.value_objects.map do |value_object|
@@ -176,6 +245,14 @@ module Hecks
176
245
  end
177
246
  end
178
247
 
248
+ # Gathers one entry per command and query.
249
+ #
250
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter to gather commands
251
+ # and queries from
252
+ # @param homes [Hash{String => String}] every holder's name, mapped to its
253
+ # owning aggregate's name (as `holder_aggregate` builds)
254
+ # @return [Array<Entry>] one `:command`-kind or `:query`-kind entry per command
255
+ # and query, commands first
179
256
  def verb_entries(bluebook, homes)
180
257
  holders(bluebook).flat_map do |holder|
181
258
  commands = holder.commands.map do |command|
@@ -190,6 +267,15 @@ module Hecks
190
267
  end
191
268
  end
192
269
 
270
+ # Gathers one entry per raised event.
271
+ #
272
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter declaring the
273
+ # policies checked for reactions
274
+ # @param raisers [Hash{String => Array<Array(Bluebook::Aggregate, Bluebook::Command)>}]
275
+ # every event name, mapped to its raising `[holder, command]` pairs (as
276
+ # `event_raisers` builds)
277
+ # @param home_of [Proc, #call] answers an event name's owning aggregate name, or nil
278
+ # @return [Array<Entry>] one `:event`-kind entry per raised event
193
279
  def event_entries(bluebook, raisers, home_of)
194
280
  raisers.map do |event, raised_by|
195
281
  reactions = bluebook.policies.select { |policy| bare(policy.on_event) == event }
@@ -198,6 +284,11 @@ module Hecks
198
284
  end
199
285
  end
200
286
 
287
+ # Gathers one entry per declared policy.
288
+ #
289
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter to gather policies from
290
+ # @param home_of [Proc, #call] answers an event name's owning aggregate name, or nil
291
+ # @return [Array<Entry>] one `:policy`-kind entry per declared policy
201
292
  def policy_entries(bluebook, home_of)
202
293
  bluebook.policies.map do |policy|
203
294
  Entry.new(name: policy.name, kind: :policy, section: home_of.call(bare(policy.on_event)),
@@ -205,6 +296,11 @@ module Hecks
205
296
  end
206
297
  end
207
298
 
299
+ # Gathers one entry per declared process manager.
300
+ #
301
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter to gather sagas from
302
+ # @param home_of [Proc, #call] answers an event name's owning aggregate name, or nil
303
+ # @return [Array<Entry>] one `:saga`-kind entry per declared process manager
208
304
  def saga_entries(bluebook, home_of)
209
305
  bluebook.process_managers.map do |saga|
210
306
  shape = saga.to_h
@@ -216,6 +312,9 @@ module Hecks
216
312
  # Cross-cutting by nature — `System` and `Customer` issue commands
217
313
  # across half the aggregates here — so a role belongs to no single
218
314
  # one and gets its own section.
315
+ #
316
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter to gather roles from
317
+ # @return [Array<Entry>] one `:role`-kind entry per distinct role named by a command
219
318
  def role_entries(bluebook)
220
319
  by_role = Hash.new { |hash, key| hash[key] = [] }
221
320
  holders(bluebook).each do |holder|
@@ -228,6 +327,10 @@ module Hecks
228
327
 
229
328
  # A read model joins heads from more than one aggregate — its own
230
329
  # header says so — so it belongs to none of them.
330
+ #
331
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter to gather read
332
+ # models from
333
+ # @return [Array<Entry>] one `:read_model`-kind entry per declared read model
231
334
  def read_model_entries(bluebook)
232
335
  bluebook.read_models.map do |read_model|
233
336
  Entry.new(name: read_model.name, kind: :read_model, section: READ_MODELS, facts: { read_model: read_model })
@@ -244,8 +347,20 @@ module Hecks
244
347
  module Slugs
245
348
  module_function
246
349
 
350
+ # Reproduces GitHub's own heading-to-slug transform.
351
+ #
352
+ # @param text [String] the heading text to slugify
353
+ # @return [String] `text` lowercased, stripped of punctuation, spaces turned
354
+ # to hyphens
247
355
  def github(text) = text.to_s.downcase.gsub(/[^\p{Word}\- ]/, "").tr(" ", "-")
248
356
 
357
+ # Assigns every section's and every entry's own slug, in document order.
358
+ #
359
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter the document
360
+ # titles itself after
361
+ # @param sections [Array<Section>] the sections to assign slugs to, in the
362
+ # order they render
363
+ # @return [void]
249
364
  def assign!(bluebook, sections)
250
365
  seen = Hash.new(0)
251
366
  take = lambda do |text|
@@ -267,6 +382,10 @@ module Hecks
267
382
  # (a cross-domain command like `Notifications.Send`) answers nil,
268
383
  # and the sentence says it in words with no link.
269
384
  class Index
385
+ # Builds the lookup table every `link` call resolves through.
386
+ #
387
+ # @param sections [Array<Section>] the document's own sections, already
388
+ # built and slugged
270
389
  def initialize(sections)
271
390
  @by_key = {}
272
391
  sections.each do |section|
@@ -275,6 +394,12 @@ module Hecks
275
394
  end
276
395
  end
277
396
 
397
+ # Builds one entry's lookup key.
398
+ #
399
+ # @param entry [Entry] the entry to key
400
+ # @return [Array(Symbol, String, String), Array(Symbol, String)] `[kind, within,
401
+ # name]` for a command, query, or value object (which can share a bare name
402
+ # across holders); `[kind, name]` otherwise
278
403
  def key_of(entry)
279
404
  case entry.kind
280
405
  when :command, :query, :value_object then [entry.kind, entry.within, entry.name]
@@ -282,6 +407,14 @@ module Hecks
282
407
  end
283
408
  end
284
409
 
410
+ # Looks up one entry or aggregate section by its key.
411
+ #
412
+ # @param kind [Symbol] the entry's kind, or `:aggregate` for a section
413
+ # @param name [String] the entry's or aggregate's name
414
+ # @param within [String, nil] the holder's name, for a command, query, or
415
+ # value object; nil otherwise
416
+ # @return [Entry, Section, nil] the matching entry or aggregate section, or
417
+ # nil if the chapter declares none
285
418
  def [](kind, name, within: nil)
286
419
  @by_key[within ? [kind, within, name] : [kind, name]]
287
420
  end
@@ -289,6 +422,15 @@ module Hecks
289
422
  # A Markdown link to a term's own heading, or the plain words when
290
423
  # the chapter declares no such term. `label:` overrides the link
291
424
  # text for a sentence that has to tell two same-named terms apart.
425
+ #
426
+ # @param kind [Symbol] the target's kind, or `:aggregate` for a section
427
+ # @param name [String] the target's or aggregate's name
428
+ # @param within [String, nil] the holder's name, for a command, query, or
429
+ # value object; nil otherwise
430
+ # @param label [String, nil] link text to use instead of the target's own
431
+ # headword or title
432
+ # @return [String] a `[text](#slug)` Markdown link, or the plain words when
433
+ # the chapter declares no such term
292
434
  def link(kind, name, within: nil, label: nil)
293
435
  target = self[kind, name, within: within]
294
436
  return label || Naming.words(name) unless target
@@ -80,19 +80,63 @@ module Hecks
80
80
  # first. Entity spells its own (separate, non-colliding) text twin
81
81
  # of the parent link `owner`. Not restated here: the one list is
82
82
  # `Assembly::PARENT_POINTERS`, which the assembly gate reads too.
83
+ # Tells whether `field` is a parent pointer, minted bare rather than
84
+ # declared like an ordinary field.
85
+ #
86
+ # @param field [String, Symbol] the field name to check
87
+ # @return [Boolean] true if `field` is a parent pointer (`Assembly::PARENT_POINTERS`,
88
+ # or any name ending `_id`)
83
89
  def parent_ref?(field) = Hecks::Bluebook::Assembly.parent_pointer?(field)
84
90
 
85
91
  # The judge's own fields, never the model's — read off the
86
92
  # category's contract (`derived: { position: :walk }`), not restated.
93
+ #
94
+ # @param name [String] the construct's name, an `Assembly::CONTRACTS` key
95
+ # @return [Array<Symbol>] fields the assembly judge derives by walking, for this construct
96
+ # @raise [KeyError] if `name` has no assembly contract
87
97
  def judge_only(name) = Hecks::Bluebook::Assembly.contract(name).walked
88
98
 
89
99
  # The tables that carry a reason answer with names only when the
90
100
  # caller wants the set rather than the explanations.
101
+ #
102
+ # @param name [String] the construct's name, an `OFF_THE_WIRE` key
103
+ # @return [Array<Symbol>] fields declared but deliberately not emitted for `name`;
104
+ # empty if `name` has none
91
105
  def off_the_wire(name) = OFF_THE_WIRE.fetch(name, {}).keys
106
+
107
+ # Names `name`'s model-only fields.
108
+ #
109
+ # @param name [String] the construct's name, a `COMPUTED` key
110
+ # @return [Array<Symbol>] model-only fields computed rather than declared for
111
+ # `name`; empty if `name` has none
92
112
  def computed(name) = COMPUTED.fetch(name, {}).keys
113
+
114
+ # Names `name`'s containment edges.
115
+ #
116
+ # @param name [String] the construct's name, a `CONTAINED` key
117
+ # @return [Array<Symbol>] fields the model holds that the grammar declares as
118
+ # containment edges instead, for `name`; empty if `name` has none
93
119
  def contained(name) = CONTAINED.fetch(name, [])
120
+
121
+ # Names `name`'s folded fields and what each gathers.
122
+ #
123
+ # @param name [String] the construct's name, a `FOLDED` key
124
+ # @return [Hash{Symbol => Array<Symbol>}] each folded field for `name`, mapped to
125
+ # the declared fields it gathers; empty if `name` has none
94
126
  def folded(name) = FOLDED.fetch(name, {})
127
+
128
+ # Names `name`'s unpacked fields and what each opens into.
129
+ #
130
+ # @param name [String] the construct's name, an `UNPACKED` key
131
+ # @return [Hash{Symbol => Array<Symbol>}] each declared field for `name` that opens
132
+ # into several model fields, mapped to those fields; empty if `name` has none
95
133
  def unpacked(name) = UNPACKED.fetch(name, {})
134
+
135
+ # Names `name`'s dynamic-tail fields.
136
+ #
137
+ # @param name [String] the construct's name, a `DYNAMIC_TAIL` key
138
+ # @return [Array<Symbol>] fields for `name` emitted by `to_h`'s own merge rather
139
+ # than by `emits_ir`; empty if `name` has none
96
140
  def dynamic_tail(name) = DYNAMIC_TAIL.fetch(name, [])
97
141
  end
98
142
  end
@@ -17,7 +17,7 @@ module Hecks
17
17
  # written — the chapter looked generatable and was not, until its
18
18
  # `@hecks_root`, ports table and child stamping moved behind `settle`.
19
19
  #
20
- # The HOST manifest is the honest part. The grammar states the fields;
20
+ # The `HOST` manifest is the honest part. The grammar states the fields;
21
21
  # it does not state which constructs are Ruby classes rather than
22
22
  # instances, what a constructor's defaults are, or how a value is
23
23
  # coerced on the way in. Those are facts about Ruby, not about
@@ -65,10 +65,26 @@ module Hecks
65
65
 
66
66
  module_function
67
67
 
68
+ # Renders every `HOST`-listed construct's holding half.
69
+ #
70
+ # @param bluebook [Bluebook::Chapter] the "Bluebook" chapter itself (the language
71
+ # describing its own constructs), not a domain being projected
72
+ # @param options [Hash{Symbol => Object}] ignored; present to satisfy the
73
+ # `Projector::Target` calling convention
74
+ # @return [Hash{String => String}] each construct's output filename mapped to its
75
+ # rendered Ruby source
68
76
  def call(bluebook:, options: {})
69
77
  HOST.to_h { |name, host| [host.fetch(:file), render(bluebook, name, host)] }
70
78
  end
71
79
 
80
+ # Renders one construct's class: emission, readers and constructor, wrapped in
81
+ # the generated-file header and namespace.
82
+ #
83
+ # @param bluebook [Bluebook::Chapter] the "Bluebook" chapter the construct's own
84
+ # attributes are read from
85
+ # @param name [String] the construct's name, a key into `HOST` (`"Policy"`, ...)
86
+ # @param host [Hash{Symbol => Object}] `name`'s `HOST` entry
87
+ # @return [String] the rendered class source, ready to write to `host[:file]`
72
88
  def render(bluebook, name, host)
73
89
  <<~RUBY
74
90
  # Generated — projected from the language's own #{name} aggregate.
@@ -98,6 +114,11 @@ module Hecks
98
114
  # A computed field (Deviations::COMPUTED) is emitted too — it is
99
115
  # model-only by definition, so it rides after the declared fields
100
116
  # and must have a `renders` entry, there being nothing to `send`.
117
+ #
118
+ # @param bluebook [Bluebook::Chapter] the "Bluebook" chapter the construct's own
119
+ # attributes are read from
120
+ # @param name [String] the construct's name, a key into `HOST`
121
+ # @return [String] the rendered `emits_ir(...)` call, one field per line
101
122
  def emits(bluebook, name)
102
123
  fields = emitted_fields(bluebook, name) + Deviations.computed(name)
103
124
  renders = HOST.fetch(name).fetch(:renders, {})
@@ -110,6 +131,13 @@ module Hecks
110
131
  # deviation the tables account for. The generator and
111
132
  # spec/model_shape_conformance_spec compute this the same way, from
112
133
  # the same tables, which is the point of the tables being in lib.
134
+ #
135
+ # @param bluebook [Bluebook::Chapter] the "Bluebook" chapter the construct's own
136
+ # attributes are read from
137
+ # @param name [String] the construct's name, an aggregate on `bluebook`
138
+ # @return [Array<Symbol>] the declared attribute names, minus every field
139
+ # `Deviations` marks as parent-ref, judge-only, off-the-wire, dynamic-tail,
140
+ # folded, or unpacked
113
141
  def emitted_fields(bluebook, name)
114
142
  bluebook.aggregate(name).attributes.map(&:name)
115
143
  .reject { |f| Deviations.parent_ref?(f) } -
@@ -120,6 +148,11 @@ module Hecks
120
148
  Deviations.unpacked(name).keys
121
149
  end
122
150
 
151
+ # Renders the `attr_reader` line for every emitted field, plus an `attr_accessor`
152
+ # for each field the model deliberately keeps off the wire.
153
+ #
154
+ # @param host [Hash{Symbol => Object}] the construct's `HOST` entry
155
+ # @return [String] the rendered reader/accessor lines, one construct's worth
123
156
  def readers(host)
124
157
  lines = ["attr_reader #{host.fetch(:readers).map { |r| ":#{r}" }.join(', ')}"]
125
158
  accessors = host.fetch(:accessors, [])
@@ -137,6 +170,11 @@ module Hecks
137
170
  end).join("\n")
138
171
  end
139
172
 
173
+ # Renders the `initialize` that assigns every declared field, coerced as `HOST`
174
+ # states, and calls `settle` unless the construct opts out.
175
+ #
176
+ # @param host [Hash{Symbol => Object}] the construct's `HOST` entry
177
+ # @return [String] the rendered `def initialize ... end` block
140
178
  def constructor(host)
141
179
  args = host.fetch(:defaults).map { |f, d| d ? "#{f}: #{d}" : "#{f}:" }.join(", ")
142
180
  body = host.fetch(:defaults).keys.map do |f|
@@ -147,7 +185,19 @@ module Hecks
147
185
  "def initialize(#{args})\n#{body.join("\n")}\nend"
148
186
  end
149
187
 
188
+ # Indents every non-blank line of a rendered block, for nesting it inside the
189
+ # class body.
190
+ #
191
+ # @param text [String] the block to indent
192
+ # @param by [Integer] how many spaces to prefix each non-blank line with
193
+ # @return [String] the indented text
150
194
  def indent(text, by) = text.lines.map { |l| l.strip.empty? ? l : (" " * by) + l }.join
195
+
196
+ # Wraps prose into `# `-prefixed comment lines, for a reader-declared reason
197
+ # rendered back into the generated file.
198
+ #
199
+ # @param text [String] the prose to wrap
200
+ # @return [String] the wrapped text, its lines joined by `"\n# "`
151
201
  def wrap(text) = text.scan(/.{1,62}(?:\s|$)/).map(&:strip).join("\n# ")
152
202
  end
153
203
  end
@@ -7,7 +7,8 @@ module Hecks
7
7
  # an identity provider has to know about this domain before it can
8
8
  # issue a token that means anything here.
9
9
  #
10
- # The artifact half of something already half-built.
10
+ # ## The artifact half of something already half-built
11
+ #
11
12
  # `spec/oidc_projection_spec.rb` covers the integration half — verified
12
13
  # claims in, `IdentityResolution.resolve` → `Authorization.holds_role?`
13
14
  # → a dispatch scoped by `Hecks.as_caller(role:)`. That half enforces
@@ -24,8 +25,9 @@ module Hecks
24
25
  # `Ports::Authorization.holds_role?` compares against a real
25
26
  # `Governance::RoleAssignment`.
26
27
  #
27
- # Roles come from the commands, not from governance. A command's own
28
- # `role "Compliance officer"` is in the bluebook IR
28
+ # ## Roles come from the commands, not from governance
29
+ #
30
+ # A command's own `role "Compliance officer"` is in the bluebook IR
29
31
  # (`Command#role`), whereas `uses_framework "Governance"` is
30
32
  # declared in the `.hecksagon` — which `call(bluebook:, options:)`
31
33
  # cannot see at all. Reading the commands is both the only thing
@@ -47,6 +49,13 @@ module Hecks
47
49
  # `audience:` overrides the domain name, for the ordinary case
48
50
  # where the IdP's registered audience is a URL rather than a bare
49
51
  # chapter name.
52
+ #
53
+ # @param bluebook [Bluebook::Chapter] the chapter being projected
54
+ # @param options [Hash{Symbol => Object}] `:audience` (String, Symbol, nil)
55
+ # overrides the manifest's `"audience"` value
56
+ # @return [Hash{String => Object}] `"audience"` (String), `"scopes"` (see
57
+ # `scopes_for`) and `"roles"` (`Array<String>`, every distinct declared role,
58
+ # sorted)
50
59
  def call(bluebook:, options: {})
51
60
  scopes = scopes_for(bluebook)
52
61
 
@@ -67,11 +76,22 @@ module Hecks
67
76
  # through `Dispatcher#dispatch`'s dotted `Entity.Command` routing
68
77
  # is real and callable, so a manifest that never names it can
69
78
  # never grant a client a scope for it either.
79
+ #
80
+ # @param bluebook [Bluebook::Chapter] the chapter being projected
81
+ # @return [Array<Hash{String => Object}>] every command's scope entry (see
82
+ # `command_scopes`), sorted by `"scope"`
70
83
  def scopes_for(bluebook)
71
84
  bluebook.aggregates.flat_map { |aggregate| aggregate_scopes(bluebook, aggregate) }
72
85
  .sort_by { |scope| scope["scope"] }
73
86
  end
74
87
 
88
+ # Projects one aggregate's own commands and every entity nested inside it.
89
+ #
90
+ # @param bluebook [Bluebook::Chapter] the aggregate's owning chapter, for the
91
+ # verb and scope prefixes
92
+ # @param aggregate [Bluebook::Aggregate] the aggregate being projected
93
+ # @return [Array<Hash{String => Object}>] the aggregate's and its entities'
94
+ # scope entries (see `command_scopes`)
75
95
  def aggregate_scopes(bluebook, aggregate)
76
96
  verb_prefix = "#{bluebook.name}::#{aggregate.hecks_name}"
77
97
  scope_prefix = "#{Naming.snake(bluebook.name)}:#{Naming.snake(aggregate.hecks_name)}"
@@ -84,6 +104,14 @@ module Hecks
84
104
  # inside `Handler`), so this recurses the same way `Chapter#verbs`
85
105
  # now does. The verb and scope prefixes grow in lockstep, each
86
106
  # `.`-joined the same way its own kind already was.
107
+ #
108
+ # @param entity [Bluebook::Entity] the entity being projected
109
+ # @param verb_prefix [String] the enclosing aggregate or entity's own verb
110
+ # prefix, extended with this entity's name
111
+ # @param scope_prefix [String] the enclosing aggregate or entity's own scope
112
+ # prefix, extended with this entity's snake-cased name
113
+ # @return [Array<Hash{String => Object}>] this entity's and its nested entities'
114
+ # scope entries (see `command_scopes`)
87
115
  def entity_scopes(entity, verb_prefix, scope_prefix)
88
116
  verb_prefix = "#{verb_prefix}.#{entity.hecks_name}"
89
117
  scope_prefix = "#{scope_prefix}.#{Naming.snake(entity.hecks_name)}"
@@ -96,6 +124,15 @@ module Hecks
96
124
  # spelled in, and snake_cased through the same `Naming.snake` the
97
125
  # facade uses to name a command's own door method, so a scope and
98
126
  # the Ruby call that satisfies it cannot drift apart.
127
+ #
128
+ # @param commands [Array<Bluebook::Command>] the commands to project
129
+ # @param verb_prefix [String] the owning aggregate or entity's fully-qualified
130
+ # verb prefix
131
+ # @param scope_prefix [String] the owning aggregate or entity's snake-cased
132
+ # scope prefix
133
+ # @return [Array<Hash{String => Object}>] one entry per command: `"scope"`
134
+ # (String), `"verb"` (String), `"role"` (String, `nil` if the command
135
+ # declares none)
99
136
  def command_scopes(commands, verb_prefix, scope_prefix)
100
137
  commands.map do |command|
101
138
  {