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
@@ -5,16 +5,18 @@ module Hecks
5
5
  module Projector
6
6
  # A bluebook, projected as prose an SME can read back and confirm.
7
7
  #
8
- # **What this is for**. `DocsProjector` already answers "what can I call and
9
- # what does it want" for the person implementing against a domain —
10
- # tables of arguments, shapes, refusal reasons. That is the wrong
11
- # register for the person who can actually say whether the domain is
12
- # right: the subject-matter expert who knows what an account is and has
13
- # never read a markdown table in their life. This projects the same IR
14
- # as sentences instead — "Debit — take money out. Issued by a Teller. It
15
- # only goes through if the balance covers it." — so a domain can be
16
- # read back to the person who can validate it without them learning the
17
- # DSL first.
8
+ # ## What this is for
9
+ #
10
+ # `DocsProjector` already answers "what can I call and what does it
11
+ # want" for the person implementing against a domain — tables of
12
+ # arguments, shapes, refusal reasons. That is the wrong register for
13
+ # the person who can actually say whether the domain is right: the
14
+ # subject-matter expert who knows what an account is and has never
15
+ # read a markdown table in their life. This projects the same IR as
16
+ # sentences instead — "Debit — take money out. Issued by a Teller.
17
+ # It only goes through if the balance covers it." — so a domain can
18
+ # be read back to the person who can validate it without them
19
+ # learning the DSL first.
18
20
  #
19
21
  # Same source, same guarantee `DocsProjector` gives: nothing here is
20
22
  # invented. Every sentence quotes a `description`, `goal`, or `given`
@@ -24,17 +26,30 @@ module Hecks
24
26
  # (`Projector.call(:narrate, bluebook: ...)`), same aggregate-scoping
25
27
  # via `options[:aggregate]`.
26
28
  #
27
- # **What it does not do**: replace `DocsProjector`. A shape table still says
28
- # "id of a Customer" more precisely than any sentence would, and an
29
- # implementer still wants that. This is the other document the same IR
30
- # is owed — one written for the reader who is being asked "is this
31
- # right?", not "how do I call it?"
29
+ # ## What it does not do
30
+ #
31
+ # Replace `DocsProjector`. A shape table still says "id of a
32
+ # Customer" more precisely than any sentence would, and an
33
+ # implementer still wants that. This is the other document the same
34
+ # IR is owed — one written for the reader who is being asked "is
35
+ # this right?", not "how do I call it?"
32
36
  module NarrateProjector
33
37
  module_function
34
38
 
39
+ # Projects `bluebook` as prose an SME can read back and confirm.
40
+ #
35
41
  # `options[:heading]` sets the top heading level, exactly as
36
42
  # `DocsProjector` does — so this, too, can be spliced into a larger
37
43
  # document rather than always starting at H1.
44
+ #
45
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter to narrate
46
+ # @param options [Hash] optional inputs
47
+ # @option options [Integer, String] :heading the top heading level; defaults to 1
48
+ # @option options [String, Symbol, nil] :aggregate narrows the narrative to one
49
+ # aggregate, omitting the chapter intro and reactions sections
50
+ # @return [String] the narrative, as Markdown prose, ending in a newline
51
+ # @raise [Runtime::NotFound] if `options[:aggregate]` names no aggregate `bluebook`
52
+ # declares
38
53
  def call(bluebook:, options: {})
39
54
  depth = (options[:heading] || 1).to_i
40
55
  only = options[:aggregate]
@@ -50,6 +65,12 @@ module Hecks
50
65
 
51
66
  # ── the chapter ───────────────────────────────────────────────────
52
67
 
68
+ # Narrates the chapter-level intro: its vision, classification, former
69
+ # name, and the aggregates it's told through.
70
+ #
71
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter to narrate
72
+ # @param depth [Integer] the heading level for the chapter's own title
73
+ # @return [String] the intro's Markdown prose
53
74
  def chapter_intro(bluebook, depth)
54
75
  parts = [DocsProjector.h(depth, bluebook.name)]
55
76
  parts << bluebook.vision if bluebook.vision
@@ -66,6 +87,12 @@ module Hecks
66
87
 
67
88
  # ── one aggregate ─────────────────────────────────────────────────
68
89
 
90
+ # Narrates one aggregate: its description, identity, references,
91
+ # lifecycle, commands, queries, and nested entities.
92
+ #
93
+ # @param aggregate [Bluebook::Aggregate] the aggregate to narrate
94
+ # @param depth [Integer] the heading level for the aggregate's own title
95
+ # @return [String] the aggregate's Markdown prose
69
96
  def aggregate_narrative(aggregate, depth)
70
97
  parts = [DocsProjector.h(depth, aggregate.hecks_name)]
71
98
  parts << aggregate.description if aggregate.description
@@ -86,12 +113,23 @@ module Hecks
86
113
  parts.compact.join("\n\n")
87
114
  end
88
115
 
116
+ # Names how a holder is identified.
117
+ #
118
+ # @param holder [Bluebook::Aggregate, Bluebook::Entity] the holder to describe
119
+ # @return [String, nil] a sentence naming `holder`'s identity fields, or nil if
120
+ # it declares none
89
121
  def identity_sentence(holder)
90
122
  return nil if holder.identity_heads.empty?
91
123
 
92
124
  "Every #{holder.hecks_name} is identified by its #{to_sentence_list(holder.identity_heads.map { |h| "`#{h}`" })}."
93
125
  end
94
126
 
127
+ # Narrates one entity nested under `aggregate`.
128
+ #
129
+ # @param aggregate [Bluebook::Aggregate] the entity's own owning aggregate
130
+ # @param entity [Bluebook::Entity] the entity to narrate
131
+ # @param depth [Integer] the heading level for the entity's own title
132
+ # @return [String] the entity's Markdown prose
95
133
  def entity_narrative(aggregate, entity, depth)
96
134
  parts = [DocsProjector.h(depth, "#{entity.hecks_name} (within #{aggregate.hecks_name})")]
97
135
  parts << entity.description if entity.description
@@ -106,6 +144,11 @@ module Hecks
106
144
 
107
145
  # ── the machine ───────────────────────────────────────────────────
108
146
 
147
+ # Narrates a holder's own lifecycle transitions.
148
+ #
149
+ # @param holder [Bluebook::Aggregate, Bluebook::Entity] the holder to describe
150
+ # @return [String, nil] a sentence per lifecycle transition, or nil if `holder`
151
+ # declares no lifecycle
109
152
  def lifecycle_narrative(holder)
110
153
  lifecycle = holder.lifecycle or return nil
111
154
 
@@ -120,6 +163,13 @@ module Hecks
120
163
 
121
164
  # ── the verbs ─────────────────────────────────────────────────────
122
165
 
166
+ # Narrates every command a holder declares.
167
+ #
168
+ # @param holder [Bluebook::Aggregate, Bluebook::Entity] the holder whose
169
+ # commands to narrate
170
+ # @param depth [Integer] the heading level for the "What can happen" section
171
+ # @return [String, nil] the section's Markdown prose, or nil if `holder`
172
+ # declares no command
123
173
  def verbs_narrative(holder, depth)
124
174
  return nil if holder.commands.empty?
125
175
 
@@ -136,6 +186,10 @@ module Hecks
136
186
  # sentence depends on anything before it. Same nil-or-string +
137
187
  # `compact.join` shape `aggregate_narrative`/`entity_narrative`
138
188
  # already use above for the identical reason.
189
+ #
190
+ # @param command [Bluebook::Command] the command to narrate
191
+ # @param holder [Bluebook::Aggregate, Bluebook::Entity] `command`'s own holder
192
+ # @return [String] the command's own paragraph
139
193
  def command_paragraph(command, holder)
140
194
  [
141
195
  command_headline_sentence(command),
@@ -153,10 +207,18 @@ module Hecks
153
207
  # exactly as declared, not recased to fit mid-sentence, because the
154
208
  # promise this whole projector makes is that a sentence here is a
155
209
  # sentence the chapter actually wrote.
210
+ #
211
+ # @param command [Bluebook::Command] the command to name
212
+ # @return [String] the command's own bold name, plus its goal if it declares one
156
213
  def command_headline_sentence(command)
157
214
  "**#{command.hecks_name}**#{command.goal ? " — #{command.goal}." : '.'}"
158
215
  end
159
216
 
217
+ # Names who issues a command.
218
+ #
219
+ # @param command [Bluebook::Command] the command to describe
220
+ # @return [String, nil] a sentence naming who issues `command`, or nil if it
221
+ # declares no role
160
222
  def command_role_sentence(command)
161
223
  return nil unless command.role
162
224
 
@@ -168,12 +230,21 @@ module Hecks
168
230
  # `Command#acts_on`'s own comment), so reading it directly here would
169
231
  # tell an SME that `LedgerEntry.Amend` brings a new ledger entry into
170
232
  # being, which is exactly backwards.
233
+ # @param command [Bluebook::Command] the command to check
234
+ # @param holder [Bluebook::Aggregate, Bluebook::Entity] `command`'s own holder
235
+ # @return [String, nil] a sentence saying `command` creates `holder`, or nil if
236
+ # `command` acts on an existing record instead
171
237
  def command_creation_sentence(command, holder)
172
238
  return nil unless command.acts_on.nil?
173
239
 
174
240
  "This is how a new #{holder.hecks_name} comes into being."
175
241
  end
176
242
 
243
+ # Lists a command's own non-reference arguments.
244
+ #
245
+ # @param command [Bluebook::Command] the command to describe
246
+ # @return [String, nil] a sentence listing `command`'s non-reference arguments,
247
+ # or nil if it declares none
177
248
  def command_arguments_sentence(command)
178
249
  arguments = command.attributes.reject(&:reference?)
179
250
  return nil if arguments.empty?
@@ -181,6 +252,11 @@ module Hecks
181
252
  "It takes #{to_sentence_list(arguments.map { |a| Forms::Humanize.label(a.name.to_s).downcase })}."
182
253
  end
183
254
 
255
+ # Names what a command references.
256
+ #
257
+ # @param command [Bluebook::Command] the command to describe
258
+ # @return [String, nil] a sentence naming what `command` references, or nil if
259
+ # it declares no reference argument
184
260
  def command_references_sentence(command)
185
261
  refs = command.attributes.select(&:reference?)
186
262
  return nil if refs.empty?
@@ -188,6 +264,12 @@ module Hecks
188
264
  "It's aimed at one existing #{to_sentence_list(refs.map { |r| r.type.target_name })}, by id."
189
265
  end
190
266
 
267
+ # Lists a command's own required conditions.
268
+ #
269
+ # @param command [Bluebook::Command] the command to describe
270
+ # @param holder [Bluebook::Aggregate, Bluebook::Entity] `command`'s own holder
271
+ # @return [String, nil] a sentence listing every required condition, or nil if
272
+ # `command` declares none
191
273
  def command_conditions_sentence(command, holder)
192
274
  conditions = conditions_of(command, holder)
193
275
  return nil if conditions.empty?
@@ -195,6 +277,11 @@ module Hecks
195
277
  "It only goes through if #{conditions.join('; ')}."
196
278
  end
197
279
 
280
+ # Lists what a command guarantees when it succeeds.
281
+ #
282
+ # @param command [Bluebook::Command] the command to describe
283
+ # @return [String, nil] a sentence listing `command`'s own `ensures`, or nil if
284
+ # it declares none
198
285
  def command_guarantees_sentence(command)
199
286
  guarantees = command.ensures.map(&:description)
200
287
  return nil if guarantees.empty?
@@ -202,6 +289,11 @@ module Hecks
202
289
  "When it succeeds: #{guarantees.join('; ')}."
203
290
  end
204
291
 
292
+ # Names what a command records.
293
+ #
294
+ # @param command [Bluebook::Command] the command to describe
295
+ # @return [String, nil] a sentence naming what `command` records, or nil if it
296
+ # emits nothing
205
297
  def command_emits_sentence(command)
206
298
  return nil if command.emits.empty?
207
299
 
@@ -214,6 +306,10 @@ module Hecks
214
306
  # `given`s), but kept positive rather than phrased as a refusal
215
307
  # reason. "Refused unless not X" is a sentence a reader has to
216
308
  # invert in their head; "only goes through if X" is not.
309
+ #
310
+ # @param command [Bluebook::Command] the command to gather conditions for
311
+ # @param holder [Bluebook::Aggregate, Bluebook::Entity] `command`'s own holder
312
+ # @return [Array<String>] every required condition, stated positively
217
313
  def conditions_of(command, holder)
218
314
  conditions = []
219
315
 
@@ -234,6 +330,12 @@ module Hecks
234
330
 
235
331
  # ── the reads ─────────────────────────────────────────────────────
236
332
 
333
+ # Narrates every query as one line per question.
334
+ #
335
+ # @param queries [Array<Bluebook::Query>] the queries to narrate
336
+ # @param holder_name [String] the queries' own holder's name
337
+ # @param depth [Integer] the heading level for the "Questions you can ask" section
338
+ # @return [String, nil] the section's Markdown prose, or nil if `queries` is empty
237
339
  def queries_narrative(queries, holder_name, depth)
238
340
  return nil if queries.empty?
239
341
 
@@ -257,12 +359,23 @@ module Hecks
257
359
  "#{header}\n\n#{lines.join("\n")}"
258
360
  end
259
361
 
362
+ # Translates a query comparator into plain English.
363
+ #
364
+ # @param comparator [String, Symbol] a where-clause comparator, such as `:eq`
365
+ # @return [String] the comparator in plain words, or `comparator.to_s` verbatim
366
+ # for one this file has no rendering rule for
260
367
  def op_words(comparator)
261
368
  { eq: "is", lt: "under", lte: "at most", gt: "over", gte: "at least" }[comparator.to_s.to_sym] || comparator.to_s
262
369
  end
263
370
 
264
371
  # ── what happens on its own ───────────────────────────────────────
265
372
 
373
+ # Narrates what happens on its own: every policy and every saga.
374
+ #
375
+ # @param bluebook [Bluebook::Behaviour::Chapter] the chapter to narrate
376
+ # @param depth [Integer] the heading level for the "Reactions" section
377
+ # @return [String, nil] the section's Markdown prose, or nil if `bluebook`
378
+ # declares no policy and no process manager
266
379
  def reactions_narrative(bluebook, depth)
267
380
  return nil if bluebook.policies.empty? && bluebook.process_managers.empty?
268
381
 
@@ -286,8 +399,16 @@ module Hecks
286
399
 
287
400
  # Both now live in `Naming` (a second projection, the glossary,
288
401
  # needed them); kept here as names so this file reads as it did.
402
+ #
403
+ # @param items [Array<#to_s>] items to join, in order
404
+ # @param conj [String] conjunction placed before the last item
405
+ # @return [String] the joined sentence fragment, `""` for an empty `items`
289
406
  def to_sentence_list(items, conj: "and") = Naming.to_sentence_list(items, conj: conj)
290
407
 
408
+ # Picks the English indefinite article for a word.
409
+ #
410
+ # @param word [String, Symbol] the word the article precedes
411
+ # @return [String] `"a"` or `"an"`
291
412
  def a_or_an(word) = Naming.a_or_an(word)
292
413
  end
293
414
  end
@@ -55,11 +55,11 @@ module Hecks
55
55
  #
56
56
  # A capability says what a construct can do; this says what it must
57
57
  # carry. `:vocabulary` needs a chapter declaring a Vocabulary
58
- # aggregate, `:parser_table` one declaring Syntax — and both used
59
- # to state that as a `raise` in their own body, which is a
60
- # requirement written as behaviour instead of declared. Stated
61
- # here, the registry refuses before the projection runs and the
62
- # projection stops carrying a guard about its own admission.
58
+ # aggregate, `:parser_table` one declaring Syntax — declared here
59
+ # rather than as a `raise` in the projection's own body, so the
60
+ # requirement is stated instead of written as behaviour. The
61
+ # registry refuses before the projection runs, so the projection
62
+ # itself no longer carries a guard about its own admission.
63
63
  # `emits:` says what kind of artifact comes back.
64
64
  #
65
65
  # `:artifact` (the default) is one thing — a Hash, or a String.
@@ -70,24 +70,61 @@ module Hecks
70
70
  # an ordinary Hash is guesswork — `{"name" => "Pizzas"}` is
71
71
  # indistinguishable from a one-file tree — so `write` asks what the
72
72
  # projection said rather than inspecting what it returned.
73
- def projects_as(key, requires: nil, declares: nil, emits: :artifact)
74
- @projection_emits = emits
75
- @projection_key = key.to_sym
76
- @projection_declares = Array(declares)
77
- @projection_requires = Array(requires)
73
+ # Registers `self` as a projection target under `key`, and stores the
74
+ # capability and aggregate requirements `admits!` later checks against.
75
+ #
76
+ # @param key [String, Symbol] the key to register `self` under (converted to a symbol)
77
+ # @param requires [Module, Array<Module>, nil] capability module(s) a construct must
78
+ # satisfy; nil or empty means `Bluebook::Behaviour::Chapter`
79
+ # @param declares [String, Symbol, Array<String, Symbol>, nil] aggregate name(s) the
80
+ # chapter must declare; nil means none
81
+ # @param emits [Symbol] the kind of artifact `self` returns: `:artifact` (the default,
82
+ # a single Hash or String) or `:files` (a path => contents tree)
83
+ # @param needs_world [Boolean] whether `self` is an export — a target that reads a
84
+ # domain's `.world`/`.hecksagon` bindings, not only its declaration. `false` (the
85
+ # default) for an ordinary projection; `true` refuses a `Projector.call` made with
86
+ # no `world:`.
87
+ # @return [Symbol] the registered key
88
+ def projects_as(key, requires: nil, declares: nil, emits: :artifact, needs_world: false)
89
+ @projection_emits = emits
90
+ @projection_key = key.to_sym
91
+ @projection_declares = Array(declares)
92
+ @projection_requires = Array(requires)
93
+ @projection_needs_world = needs_world
78
94
  Projector.register(@projection_key, self)
79
95
  @projection_key
80
96
  end
81
97
 
98
+ # Gives the key this target registered under.
99
+ #
100
+ # @return [Symbol, nil] the key given to `projects_as`, or nil before it is called
82
101
  def projection_key = @projection_key
83
102
 
84
103
  # Empty means "a chapter" — resolved here rather than as a default
85
104
  # argument, because Behaviour::Chapter is not loaded yet when this
86
105
  # file is.
106
+ #
107
+ # @return [Array<String, Symbol>] aggregate names the chapter must declare; empty
108
+ # for none
87
109
  def projection_declares = @projection_declares || []
88
110
 
111
+ # Gives the kind of artifact this target returns.
112
+ #
113
+ # @return [Symbol] the kind of artifact `self` returns: `:artifact` or `:files`
89
114
  def projection_emits = @projection_emits || :artifact
90
115
 
116
+ # Tells whether this target is an export — one that needs a domain's
117
+ # `.world`/`.hecksagon` bindings, passed as `Projector.call`'s `world:`,
118
+ # rather than only its declaration.
119
+ #
120
+ # @return [Boolean] the value given to `projects_as`' `needs_world:`, or false
121
+ # before `projects_as` is called
122
+ def projection_needs_world? = @projection_needs_world || false
123
+
124
+ # Names the capability module(s) a construct must satisfy, defaulting
125
+ # to plain chapter-hood when `projects_as` named none.
126
+ #
127
+ # @return [Array<Module>] required capability modules
91
128
  def projection_requires
92
129
  req = @projection_requires
93
130
  req.nil? || req.empty? ? [Bluebook::Behaviour::Chapter] : req
@@ -17,8 +17,11 @@ module Hecks
17
17
  # every real projection target (Rust/UL/OIDC all project one domain at
18
18
  # a time), and deliberately narrower than `Exporter.call`'s own
19
19
  # multi-domain shape.
20
- # Three kinds of "PROJECT", told apart by what they need as input.
21
- # Only the first belongs in this registry.
20
+ #
21
+ # ## Three kinds of "project"
22
+ #
23
+ # Told apart by what they need as input. Only the first belongs in this
24
+ # registry.
22
25
  #
23
26
  # a projection takes a chapter's declaration and answers something
24
27
  # that describes the domain: its IR, its storage shape, an OIDC scope
@@ -28,11 +31,13 @@ module Hecks
28
31
  #
29
32
  # an export takes a declaration and its bindings and answers
30
33
  # something that is the domain, running elsewhere — rust/project.rb's
31
- # generated crate, the WASM artifact, the SAM template
32
- # bin/project_deploy renders. It needs the `.world`/`.hecksagon` a
33
- # projection never looks at, because a running system has to know how
34
- # it is wired. That is the whole reason bin/project_deploy cannot use
35
- # this protocol: `call(bluebook:, options:)` has no channel for it.
34
+ # generated crate, the WASM artifact, the CloudFormation template
35
+ # `lib/hecks/projections/deploy` renders. It needs the
36
+ # `.world`/`.hecksagon` a projection never looks at, because a
37
+ # running system has to know how it is wired. A target that needs
38
+ # this declares `needs_world: true` (`Target#projects_as`) and reads
39
+ # `options.fetch(:world)`; `Projector.call`'s own `world:` keyword is
40
+ # what carries it, merged into `options` before the target ever runs.
36
41
  #
37
42
  # a state projection takes records — a domain after dispatch — and is
38
43
  # a read-model question wearing the same word.
@@ -42,8 +47,10 @@ module Hecks
42
47
  # dispatches. Converting it into this registry would be a category
43
48
  # error, however much its name suggests otherwise.
44
49
  #
45
- # **One word, three other meanings** — worth naming too, because grepping
46
- # "projection" turns all of these up and none is the above:
50
+ # ## One word, three other meanings
51
+ #
52
+ # Worth naming too, because grepping "projection" turns all of these up
53
+ # and none is the above:
47
54
  #
48
55
  # Ports::Projection read-model catch-up, events folded into state
49
56
  # bin/project forces that catch-up by hand
@@ -61,24 +68,48 @@ module Hecks
61
68
  # deliberately unguarded: a spec re-registering a stub under the same
62
69
  # name between examples is the ordinary case, not a footgun to fence
63
70
  # against.
71
+ # Adds `projector` to the registry under `name`, replacing anything
72
+ # already registered there.
73
+ #
74
+ # @param name [String, Symbol] the key `projector` is looked up by (converted to a symbol)
75
+ # @param projector [Module, Class, #call] anything answering `call(bluebook:, options:)`
76
+ # @return [void]
64
77
  def register(name, projector)
65
78
  registry[name.to_sym] = projector
66
79
  end
67
80
 
81
+ # Runs the projector registered under `name` against `bluebook`,
82
+ # after refusing a construct it does not admit.
83
+ #
68
84
  # `bluebook:` is kept as the keyword because it is the shipped
69
85
  # spelling and every existing caller uses it — but what it accepts is
70
86
  # any construct that emits IR, and `admits!` is what decides whether
71
87
  # this target can actually take the one handed over.
72
- def call(name, bluebook:, options: {})
88
+ #
89
+ # @param name [String, Symbol] the registered projector's key
90
+ # @param bluebook [Bluebook::Behaviour::Chapter, Hecks::IR] the chapter or
91
+ # IR-emitting construct to project
92
+ # @param options [Hash] projector-specific options, passed through unchanged
93
+ # @param world [Bluebook::World, nil] the domain's `.world`/`.hecksagon` bindings,
94
+ # required by an export (`needs_world: true`); nil for an ordinary projection.
95
+ # Merged into `options[:world]` before the target runs — the target never
96
+ # receives it as a separate argument.
97
+ # @return [Object] whatever the projector's own `call` returns: typically a
98
+ # `Hash`/`String` artifact, or a `Hash{String => String}` file tree
99
+ # @raise [UnknownProjector] if no projector is registered under `name`
100
+ # @raise [WrongConstruct] if `bluebook` lacks a capability or aggregate the
101
+ # projector requires, or if the projector needs `world:` and none is given
102
+ def call(name, bluebook:, options: {}, world: nil)
73
103
  projector = registry.fetch(name.to_sym) do
74
104
  raise UnknownProjector, "no projector registered for #{name.inspect} — registered: #{registered.sort.inspect}"
75
105
  end
76
- admits!(name, projector, bluebook)
77
- projector.call(bluebook: bluebook, options: options)
106
+ admits!(name, projector, bluebook, world)
107
+ projector.call(bluebook: bluebook, options: world ? options.merge(world: world) : options)
78
108
  end
79
109
 
80
- # A projection names the capabilities it needs; this refuses a
81
- # construct that lacks one, before the projector runs.
110
+ # Refuses `construct` if it lacks a capability or declared aggregate
111
+ # `projector` requires. A projection names the capabilities it needs;
112
+ # this is what enforces that, before the projector runs.
82
113
  #
83
114
  # One check covers both shapes. An ordinary construct includes its
84
115
  # capabilities and a class-shaped one — Command, Entity, ValueObject
@@ -87,7 +118,21 @@ module Hecks
87
118
  # started as two checks on the assumption it would not; a spec
88
119
  # asserting the assumption failed, which is the only reason the
89
120
  # redundant half was noticed.
90
- def admits!(name, projector, construct)
121
+ #
122
+ # @param name [String, Symbol] the projector's registered key, used in the message
123
+ # when refusing
124
+ # @param projector [Module, Class, #call] the target being checked; consulted for
125
+ # `projection_requires`, `projection_declares`, and `projection_needs_world?` when
126
+ # it answers them
127
+ # @param construct [Bluebook::Behaviour::Chapter, Hecks::IR] the chapter or
128
+ # IR-emitting construct offered to the projector
129
+ # @param world [Bluebook::World, nil] the `world:` given to `Projector.call`, checked
130
+ # against the target's own `needs_world:` declaration
131
+ # @return [void]
132
+ # @raise [WrongConstruct] if `construct` lacks a required capability, the chapter it
133
+ # is declares no aggregate the projector needs, or the projector needs `world:`
134
+ # and `world` is nil
135
+ def admits!(name, projector, construct, world = nil)
91
136
  needed = projector.respond_to?(:projection_requires) ? projector.projection_requires : []
92
137
  missing = needed.reject { |capability| capable?(construct, capability) }
93
138
  unless missing.empty?
@@ -98,25 +143,49 @@ module Hecks
98
143
 
99
144
  declared = projector.respond_to?(:projection_declares) ? projector.projection_declares : []
100
145
  absent = declared.reject { |named| construct.aggregate(named) }
101
- return if absent.empty?
146
+ unless absent.empty?
147
+ raise WrongConstruct,
148
+ "#{name.inspect} needs a chapter declaring #{absent.join(' and ')}; " \
149
+ "#{construct.name} declares no such aggregate."
150
+ end
151
+
152
+ return unless projector.respond_to?(:projection_needs_world?) && projector.projection_needs_world? && world.nil?
102
153
 
103
- raise WrongConstruct,
104
- "#{name.inspect} needs a chapter declaring #{absent.join(' and ')}; " \
105
- "#{construct.name} declares no such aggregate."
154
+ raise WrongConstruct, "#{name.inspect} needs .world/.hecksagon bindings — pass world: to Projector.call."
106
155
  end
107
156
 
157
+ # Tells whether `construct` has the capability `admits!` requires of it.
158
+ #
159
+ # @param construct [Bluebook::Behaviour::Chapter, Hecks::IR] the construct to check
160
+ # @param capability [Module] the capability module to check for
161
+ # @return [Boolean] true if `construct` is a `capability`
108
162
  def capable?(construct, capability) = construct.is_a?(capability)
109
163
 
110
164
  # What kind of artifact a registered target emits — asked of the
111
165
  # projection rather than inferred from what it returned.
166
+ #
167
+ # @param name [String, Symbol] the registered projector's key
168
+ # @return [Symbol] `:files` for a path => contents tree, `:artifact` (the
169
+ # default, including for an unregistered `name`) for a single Hash or String
112
170
  def emits_for(name)
113
171
  projector = registry.fetch(name.to_sym) { return :artifact }
114
172
  projector.respond_to?(:projection_emits) ? projector.projection_emits : :artifact
115
173
  end
116
174
 
175
+ # Tells whether a projector is registered under `name`.
176
+ #
177
+ # @param name [String, Symbol] the key to look up
178
+ # @return [Boolean] true if a projector is registered under `name`
117
179
  def registered?(name) = registry.key?(name.to_sym)
180
+
181
+ # Lists every key currently registered.
182
+ #
183
+ # @return [Array<Symbol>] every key currently registered
118
184
  def registered = registry.keys
119
185
 
186
+ # Gives the live registry, initializing it on first use.
187
+ #
188
+ # @return [Hash{Symbol => Module, Class, #call}] the live key => projector registry
120
189
  def registry
121
190
  @registry ||= {}
122
191
  end
@@ -126,6 +195,10 @@ module Hecks
126
195
  # (`:oidc`). Both resolve here, so the constant form is added
127
196
  # surface rather than a replacement — every `Projector.call(:ir, ...)`
128
197
  # written before this existed keeps working untouched.
198
+ #
199
+ # @param target [String, Symbol, #projection_key] a registered key, or a target
200
+ # that declared its own key with `Target#projects_as`
201
+ # @return [String, Symbol] the key to call the target under
129
202
  def key_for(target)
130
203
  return target.projection_key if target.respond_to?(:projection_key) && target.projection_key
131
204
 
@@ -141,6 +214,11 @@ module Hecks
141
214
  # than from inspecting the artifact. A Hash of path => contents and a
142
215
  # Hash that simply happens to hold strings are the same object to
143
216
  # Ruby; only the projection knows which it meant.
217
+ #
218
+ # @param artifact [Hash, String] the projector's output; a file tree when `as: :files`
219
+ # @param out [String] the path to write to; a directory when `as: :files`
220
+ # @param as [Symbol] `:files` to write a tree, anything else to write one file
221
+ # @return [String, Array<String>] the path written, or every path written when `as: :files`
144
222
  def write(artifact, out, as: :artifact)
145
223
  return write_tree(artifact, out) if as == :files
146
224
 
@@ -150,6 +228,10 @@ module Hecks
150
228
 
151
229
  # Answers the paths written, in the order given — so a caller can
152
230
  # report what happened without re-deriving it from the tree.
231
+ #
232
+ # @param files [Hash{String => String}] relative path => contents
233
+ # @param directory [String] the directory to write the tree under
234
+ # @return [Array<String>] each file's full written path, in `files`' order
153
235
  def write_tree(files, directory)
154
236
  require "fileutils"
155
237
  files.map do |relative, contents|