hecks 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +65 -0
  3. data/lib/hecks/adapters/driven/folder.rb +73 -0
  4. data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
  5. data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
  6. data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
  7. data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
  8. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  9. data/lib/hecks/adapters/driven/heki.rb +93 -2
  10. data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
  12. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  13. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  14. data/lib/hecks/adapters/driven/lambda/client.rb +35 -7
  15. data/lib/hecks/adapters/driven/lambda.rb +43 -2
  16. data/lib/hecks/adapters/driven/local_storage.rb +67 -1
  17. data/lib/hecks/adapters/driven/memory.rb +13 -13
  18. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
  19. data/lib/hecks/adapters/driven/prism.rb +46 -7
  20. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  21. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  22. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  23. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  24. data/lib/hecks/adapters/driven.rb +2 -0
  25. data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
  26. data/lib/hecks/behaviors/dsl.rb +58 -0
  27. data/lib/hecks/behaviors/expectations.rb +168 -7
  28. data/lib/hecks/behaviors/ir.rb +11 -0
  29. data/lib/hecks/behaviors/rspec.rb +8 -0
  30. data/lib/hecks/behaviors/runner.rb +19 -0
  31. data/lib/hecks/behaviors.rb +8 -0
  32. data/lib/hecks/bluebook/aggregate.rb +30 -0
  33. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
  34. data/lib/hecks/bluebook/assembly/build.rb +25 -4
  35. data/lib/hecks/bluebook/assembly/contract.rb +66 -14
  36. data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
  37. data/lib/hecks/bluebook/assembly/marks.rb +143 -14
  38. data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
  39. data/lib/hecks/bluebook/assembly.rb +25 -7
  40. data/lib/hecks/bluebook/attribute.rb +17 -3
  41. data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
  42. data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
  43. data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
  44. data/lib/hecks/bluebook/behaviour/command.rb +33 -3
  45. data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
  46. data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
  47. data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
  48. data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
  49. data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
  50. data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
  51. data/lib/hecks/bluebook/behaviour/query.rb +5 -0
  52. data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
  53. data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
  54. data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
  55. data/lib/hecks/bluebook/chapter.rb +23 -0
  56. data/lib/hecks/bluebook/command.rb +53 -8
  57. data/lib/hecks/bluebook/domain_port.rb +25 -0
  58. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
  59. data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
  60. data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
  61. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
  62. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
  63. data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
  64. data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
  65. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
  66. data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
  67. data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
  68. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
  69. data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
  70. data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
  71. data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
  72. data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
  73. data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
  74. data/lib/hecks/bluebook/entity.rb +29 -0
  75. data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
  76. data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
  77. data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
  78. data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
  79. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
  80. data/lib/hecks/bluebook/expression/resolver.rb +320 -77
  81. data/lib/hecks/bluebook/hexagon.rb +34 -0
  82. data/lib/hecks/bluebook/lifecycle.rb +11 -0
  83. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
  84. data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
  85. data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
  86. data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
  87. data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
  88. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
  89. data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
  90. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
  91. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
  92. data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
  93. data/lib/hecks/bluebook/meta_validator.rb +180 -84
  94. data/lib/hecks/bluebook/model_check.rb +268 -24
  95. data/lib/hecks/bluebook/pattern_subset.rb +23 -1
  96. data/lib/hecks/bluebook/process_manager.rb +13 -0
  97. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  98. data/lib/hecks/bluebook/project_loader.rb +40 -0
  99. data/lib/hecks/bluebook/project_register.rb +44 -0
  100. data/lib/hecks/bluebook/query.rb +27 -0
  101. data/lib/hecks/bluebook/read_model.rb +21 -1
  102. data/lib/hecks/bluebook/reference.rb +21 -8
  103. data/lib/hecks/bluebook/smoke_test.rb +29 -6
  104. data/lib/hecks/bluebook/synthesizer.rb +34 -0
  105. data/lib/hecks/bluebook/translation.rb +30 -1
  106. data/lib/hecks/bluebook/value_object.rb +23 -5
  107. data/lib/hecks/bluebook.rb +3 -4
  108. data/lib/hecks/codemod.rb +107 -20
  109. data/lib/hecks/construct.rb +15 -1
  110. data/lib/hecks/corpus.rb +146 -25
  111. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  112. data/lib/hecks/doc/reference.rb +185 -16
  113. data/lib/hecks/embryonaut_bluebook.rb +32 -9
  114. data/lib/hecks/facade/handle.rb +76 -3
  115. data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
  116. data/lib/hecks/forms/field_shape.rb +3 -0
  117. data/lib/hecks/forms/page.rb +14 -0
  118. data/lib/hecks/forms/port_argument.rb +12 -0
  119. data/lib/hecks/forms/query_form_renderer.rb +63 -0
  120. data/lib/hecks/forms/record_renderer.rb +58 -0
  121. data/lib/hecks/forms/record_table.rb +27 -0
  122. data/lib/hecks/forms/reference_options.rb +24 -0
  123. data/lib/hecks/forms/value_object_shape.rb +10 -0
  124. data/lib/hecks/fqn.rb +58 -0
  125. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  126. data/lib/hecks/framework/oidc.json +15 -0
  127. data/lib/hecks/framework.rb +43 -20
  128. data/lib/hecks/freezer.rb +17 -1
  129. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
  130. data/lib/hecks/fuzzing/combination_miner.rb +59 -0
  131. data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
  132. data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
  133. data/lib/hecks/fuzzing/differential.rb +34 -0
  134. data/lib/hecks/fuzzing/domain_generator.rb +188 -11
  135. data/lib/hecks/fuzzing/era_boundary.rb +45 -15
  136. data/lib/hecks/fuzzing/form_census.rb +86 -0
  137. data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
  138. data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
  139. data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
  140. data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
  141. data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
  142. data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
  143. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
  144. data/lib/hecks/fuzzing/properties/guards.rb +44 -0
  145. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
  146. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
  147. data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
  148. data/lib/hecks/fuzzing/properties/querying.rb +68 -14
  149. data/lib/hecks/fuzzing/properties.rb +24 -15
  150. data/lib/hecks/fuzzing/qa_settings.rb +12 -0
  151. data/lib/hecks/fuzzing/replay.rb +137 -29
  152. data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
  153. data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
  154. data/lib/hecks/fuzzing/self_consistency.rb +189 -40
  155. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
  156. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
  157. data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
  158. data/lib/hecks/fuzzing/shrinker.rb +114 -2
  159. data/lib/hecks/fuzzing/structural_skips.rb +18 -4
  160. data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
  161. data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
  162. data/lib/hecks/fuzzing/value_generator.rb +98 -10
  163. data/lib/hecks/grammar/evolve.rb +178 -2
  164. data/lib/hecks/grammar.rb +46 -0
  165. data/lib/hecks/ir.rb +38 -7
  166. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  167. data/lib/hecks/literal.rb +32 -0
  168. data/lib/hecks/naming.rb +88 -7
  169. data/lib/hecks/ports/access_control.rb +5 -10
  170. data/lib/hecks/ports/authorization.rb +3 -6
  171. data/lib/hecks/ports/identity_assignment.rb +1 -2
  172. data/lib/hecks/ports/identity_resolution.rb +1 -2
  173. data/lib/hecks/ports/key_vault.port +6 -0
  174. data/lib/hecks/ports/key_vault.rb +58 -0
  175. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
  176. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
  177. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
  178. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
  179. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
  180. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  181. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
  182. data/lib/hecks/ports/query/in_memory.rb +39 -6
  183. data/lib/hecks/ports/query/ordering.rb +15 -0
  184. data/lib/hecks/ports.rb +1 -0
  185. data/lib/hecks/projections/bootstrap_table.rb +43 -8
  186. data/lib/hecks/projections/diagrams.rb +243 -7
  187. data/lib/hecks/projections/glossary/html.rb +88 -0
  188. data/lib/hecks/projections/glossary/markdown.rb +47 -0
  189. data/lib/hecks/projections/glossary/mermaid.rb +48 -0
  190. data/lib/hecks/projections/glossary/sentences.rb +105 -0
  191. data/lib/hecks/projections/glossary.rb +161 -19
  192. data/lib/hecks/projections/model/deviations.rb +44 -0
  193. data/lib/hecks/projections/model.rb +51 -1
  194. data/lib/hecks/projections/oidc.rb +40 -3
  195. data/lib/hecks/projections/parser_table.rb +49 -8
  196. data/lib/hecks/projections/reference.rb +12 -0
  197. data/lib/hecks/projections/rust_vocabulary.rb +219 -16
  198. data/lib/hecks/projections/shape.rb +8 -0
  199. data/lib/hecks/projections/statements.rb +63 -16
  200. data/lib/hecks/projections/vocabulary.rb +17 -0
  201. data/lib/hecks/projector/cli_projector.rb +218 -10
  202. data/lib/hecks/projector/docs_projector.rb +145 -19
  203. data/lib/hecks/projector/exporter.rb +65 -11
  204. data/lib/hecks/projector/ir_projector.rb +6 -0
  205. data/lib/hecks/projector/narrate_projector.rb +136 -15
  206. data/lib/hecks/projector/target.rb +29 -5
  207. data/lib/hecks/projector.rb +74 -6
  208. data/lib/hecks/query_ir.rb +47 -0
  209. data/lib/hecks/query_specification/common/null_policy.rb +5 -3
  210. data/lib/hecks/rendering.rb +6 -0
  211. data/lib/hecks/router/namespace_installer.rb +13 -0
  212. data/lib/hecks/router.rb +55 -0
  213. data/lib/hecks/runtime/aggregate_lock.rb +9 -0
  214. data/lib/hecks/runtime/boot_gates.rb +18 -0
  215. data/lib/hecks/runtime/caller.rb +32 -0
  216. data/lib/hecks/runtime/capability_graph.rb +11 -0
  217. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
  218. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
  219. data/lib/hecks/runtime/command_interpreter.rb +42 -17
  220. data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
  221. data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
  222. data/lib/hecks/runtime/command_rules/references.rb +118 -28
  223. data/lib/hecks/runtime/dependency_planning.rb +45 -0
  224. data/lib/hecks/runtime/dispatcher.rb +28 -50
  225. data/lib/hecks/runtime/entity_element.rb +161 -8
  226. data/lib/hecks/runtime/entity_interpreter.rb +44 -9
  227. data/lib/hecks/runtime/errors.rb +18 -4
  228. data/lib/hecks/runtime/event.rb +10 -5
  229. data/lib/hecks/runtime/identity.rb +71 -3
  230. data/lib/hecks/runtime/instance.rb +67 -7
  231. data/lib/hecks/runtime/interpreting.rb +13 -5
  232. data/lib/hecks/runtime/invocation.rb +118 -36
  233. data/lib/hecks/runtime/loader.rb +94 -8
  234. data/lib/hecks/runtime/outbox.rb +145 -7
  235. data/lib/hecks/runtime/policy_interpreter.rb +22 -9
  236. data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
  237. data/lib/hecks/runtime/query_interpreter.rb +40 -12
  238. data/lib/hecks/runtime/reaction_invocation.rb +53 -8
  239. data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
  240. data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
  241. data/lib/hecks/runtime/reference_hop.rb +42 -0
  242. data/lib/hecks/runtime/refusal_wording.rb +50 -0
  243. data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
  244. data/lib/hecks/runtime/registry/verification.rb +119 -4
  245. data/lib/hecks/runtime/registry.rb +157 -4
  246. data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
  247. data/lib/hecks/runtime/routing.rb +27 -2
  248. data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
  249. data/lib/hecks/runtime/saga_interpreter.rb +27 -13
  250. data/lib/hecks/runtime/tenant_check.rb +26 -6
  251. data/lib/hecks/runtime/tenant_scope.rb +18 -0
  252. data/lib/hecks/runtime/value/coercion.rb +255 -33
  253. data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
  254. data/lib/hecks/runtime/value.rb +50 -0
  255. data/lib/hecks/runtime.rb +32 -0
  256. data/lib/hecks/storehouse.rb +305 -9
  257. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  258. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  259. data/lib/hecks/version.rb +1 -1
  260. data/lib/hecks.rb +79 -1
  261. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
  262. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
  263. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
  264. metadata +11 -5
  265. data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
  266. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
  267. data/lib/hecks/deprecation.rb +0 -95
@@ -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,6 +70,17 @@ 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
+ # 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
+ # @return [Symbol] the registered key
73
84
  def projects_as(key, requires: nil, declares: nil, emits: :artifact)
74
85
  @projection_emits = emits
75
86
  @projection_key = key.to_sym
@@ -79,15 +90,28 @@ module Hecks
79
90
  @projection_key
80
91
  end
81
92
 
93
+ # Gives the key this target registered under.
94
+ #
95
+ # @return [Symbol, nil] the key given to `projects_as`, or nil before it is called
82
96
  def projection_key = @projection_key
83
97
 
84
98
  # Empty means "a chapter" — resolved here rather than as a default
85
99
  # argument, because Behaviour::Chapter is not loaded yet when this
86
100
  # file is.
101
+ #
102
+ # @return [Array<String, Symbol>] aggregate names the chapter must declare; empty
103
+ # for none
87
104
  def projection_declares = @projection_declares || []
88
105
 
106
+ # Gives the kind of artifact this target returns.
107
+ #
108
+ # @return [Symbol] the kind of artifact `self` returns: `:artifact` or `:files`
89
109
  def projection_emits = @projection_emits || :artifact
90
110
 
111
+ # Names the capability module(s) a construct must satisfy, defaulting
112
+ # to plain chapter-hood when `projects_as` named none.
113
+ #
114
+ # @return [Array<Module>] required capability modules
91
115
  def projection_requires
92
116
  req = @projection_requires
93
117
  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
@@ -42,8 +45,10 @@ module Hecks
42
45
  # dispatches. Converting it into this registry would be a category
43
46
  # error, however much its name suggests otherwise.
44
47
  #
45
- # **One word, three other meanings** — worth naming too, because grepping
46
- # "projection" turns all of these up and none is the above:
48
+ # ## One word, three other meanings
49
+ #
50
+ # Worth naming too, because grepping "projection" turns all of these up
51
+ # and none is the above:
47
52
  #
48
53
  # Ports::Projection read-model catch-up, events folded into state
49
54
  # bin/project forces that catch-up by hand
@@ -61,14 +66,33 @@ module Hecks
61
66
  # deliberately unguarded: a spec re-registering a stub under the same
62
67
  # name between examples is the ordinary case, not a footgun to fence
63
68
  # against.
69
+ # Adds `projector` to the registry under `name`, replacing anything
70
+ # already registered there.
71
+ #
72
+ # @param name [String, Symbol] the key `projector` is looked up by (converted to a symbol)
73
+ # @param projector [Module, Class, #call] anything answering `call(bluebook:, options:)`
74
+ # @return [void]
64
75
  def register(name, projector)
65
76
  registry[name.to_sym] = projector
66
77
  end
67
78
 
79
+ # Runs the projector registered under `name` against `bluebook`,
80
+ # after refusing a construct it does not admit.
81
+ #
68
82
  # `bluebook:` is kept as the keyword because it is the shipped
69
83
  # spelling and every existing caller uses it — but what it accepts is
70
84
  # any construct that emits IR, and `admits!` is what decides whether
71
85
  # this target can actually take the one handed over.
86
+ #
87
+ # @param name [String, Symbol] the registered projector's key
88
+ # @param bluebook [Bluebook::Behaviour::Chapter, Hecks::IR] the chapter or
89
+ # IR-emitting construct to project
90
+ # @param options [Hash] projector-specific options, passed through unchanged
91
+ # @return [Object] whatever the projector's own `call` returns: typically a
92
+ # `Hash`/`String` artifact, or a `Hash{String => String}` file tree
93
+ # @raise [UnknownProjector] if no projector is registered under `name`
94
+ # @raise [WrongConstruct] if `bluebook` lacks a capability or aggregate the
95
+ # projector requires
72
96
  def call(name, bluebook:, options: {})
73
97
  projector = registry.fetch(name.to_sym) do
74
98
  raise UnknownProjector, "no projector registered for #{name.inspect} — registered: #{registered.sort.inspect}"
@@ -77,8 +101,9 @@ module Hecks
77
101
  projector.call(bluebook: bluebook, options: options)
78
102
  end
79
103
 
80
- # A projection names the capabilities it needs; this refuses a
81
- # construct that lacks one, before the projector runs.
104
+ # Refuses `construct` if it lacks a capability or declared aggregate
105
+ # `projector` requires. A projection names the capabilities it needs;
106
+ # this is what enforces that, before the projector runs.
82
107
  #
83
108
  # One check covers both shapes. An ordinary construct includes its
84
109
  # capabilities and a class-shaped one — Command, Entity, ValueObject
@@ -87,6 +112,16 @@ module Hecks
87
112
  # started as two checks on the assumption it would not; a spec
88
113
  # asserting the assumption failed, which is the only reason the
89
114
  # redundant half was noticed.
115
+ #
116
+ # @param name [String, Symbol] the projector's registered key, used in the message
117
+ # when refusing
118
+ # @param projector [Module, Class, #call] the target being checked; consulted for
119
+ # `projection_requires` and `projection_declares` when it answers them
120
+ # @param construct [Bluebook::Behaviour::Chapter, Hecks::IR] the chapter or
121
+ # IR-emitting construct offered to the projector
122
+ # @return [void]
123
+ # @raise [WrongConstruct] if `construct` lacks a required capability, or the
124
+ # chapter it is declares no aggregate the projector needs
90
125
  def admits!(name, projector, construct)
91
126
  needed = projector.respond_to?(:projection_requires) ? projector.projection_requires : []
92
127
  missing = needed.reject { |capability| capable?(construct, capability) }
@@ -105,18 +140,38 @@ module Hecks
105
140
  "#{construct.name} declares no such aggregate."
106
141
  end
107
142
 
143
+ # Tells whether `construct` has the capability `admits!` requires of it.
144
+ #
145
+ # @param construct [Bluebook::Behaviour::Chapter, Hecks::IR] the construct to check
146
+ # @param capability [Module] the capability module to check for
147
+ # @return [Boolean] true if `construct` is a `capability`
108
148
  def capable?(construct, capability) = construct.is_a?(capability)
109
149
 
110
150
  # What kind of artifact a registered target emits — asked of the
111
151
  # projection rather than inferred from what it returned.
152
+ #
153
+ # @param name [String, Symbol] the registered projector's key
154
+ # @return [Symbol] `:files` for a path => contents tree, `:artifact` (the
155
+ # default, including for an unregistered `name`) for a single Hash or String
112
156
  def emits_for(name)
113
157
  projector = registry.fetch(name.to_sym) { return :artifact }
114
158
  projector.respond_to?(:projection_emits) ? projector.projection_emits : :artifact
115
159
  end
116
160
 
161
+ # Tells whether a projector is registered under `name`.
162
+ #
163
+ # @param name [String, Symbol] the key to look up
164
+ # @return [Boolean] true if a projector is registered under `name`
117
165
  def registered?(name) = registry.key?(name.to_sym)
166
+
167
+ # Lists every key currently registered.
168
+ #
169
+ # @return [Array<Symbol>] every key currently registered
118
170
  def registered = registry.keys
119
171
 
172
+ # Gives the live registry, initializing it on first use.
173
+ #
174
+ # @return [Hash{Symbol => Module, Class, #call}] the live key => projector registry
120
175
  def registry
121
176
  @registry ||= {}
122
177
  end
@@ -126,6 +181,10 @@ module Hecks
126
181
  # (`:oidc`). Both resolve here, so the constant form is added
127
182
  # surface rather than a replacement — every `Projector.call(:ir, ...)`
128
183
  # written before this existed keeps working untouched.
184
+ #
185
+ # @param target [String, Symbol, #projection_key] a registered key, or a target
186
+ # that declared its own key with `Target#projects_as`
187
+ # @return [String, Symbol] the key to call the target under
129
188
  def key_for(target)
130
189
  return target.projection_key if target.respond_to?(:projection_key) && target.projection_key
131
190
 
@@ -141,6 +200,11 @@ module Hecks
141
200
  # than from inspecting the artifact. A Hash of path => contents and a
142
201
  # Hash that simply happens to hold strings are the same object to
143
202
  # Ruby; only the projection knows which it meant.
203
+ #
204
+ # @param artifact [Hash, String] the projector's output; a file tree when `as: :files`
205
+ # @param out [String] the path to write to; a directory when `as: :files`
206
+ # @param as [Symbol] `:files` to write a tree, anything else to write one file
207
+ # @return [String, Array<String>] the path written, or every path written when `as: :files`
144
208
  def write(artifact, out, as: :artifact)
145
209
  return write_tree(artifact, out) if as == :files
146
210
 
@@ -150,6 +214,10 @@ module Hecks
150
214
 
151
215
  # Answers the paths written, in the order given — so a caller can
152
216
  # report what happened without re-deriving it from the tree.
217
+ #
218
+ # @param files [Hash{String => String}] relative path => contents
219
+ # @param directory [String] the directory to write the tree under
220
+ # @return [Array<String>] each file's full written path, in `files`' order
153
221
  def write_tree(files, directory)
154
222
  require "fileutils"
155
223
  files.map do |relative, contents|
@@ -37,6 +37,11 @@ module Hecks
37
37
 
38
38
  module_function
39
39
 
40
+ # Reads the fields the language declares for one construct kind, from the grammar itself.
41
+ #
42
+ # @param name [String] a `CONSTRUCTS` key, such as `"Aggregate"`
43
+ # @return [Array<Symbol>] the attribute names the self-hosted meta-domain
44
+ # grammar declares for `name`
40
45
  def meta_declared(name)
41
46
  Hecks::Bluebook::MetaValidator.grammar_registry
42
47
  .bluebook("Bluebook").aggregate(name).attributes.map(&:name)
@@ -47,6 +52,11 @@ module Hecks
47
52
  # for it — the same comparison spec/model_shape_conformance_spec.rb
48
53
  # makes, reusing its own Deviations data so this can never silently
49
54
  # drift from what that gate actually checks.
55
+ #
56
+ # @param name [String] a `CONSTRUCTS` key, such as `"Aggregate"`
57
+ # @return [Hash{Symbol => Object}] `:name`, `:declared`, `:emitted`, plus
58
+ # `:missing_from_ruby` and `:unaccounted_in_ruby` — each an `Array<Symbol>`
59
+ # @raise [ArgumentError] if `name` is not a `CONSTRUCTS` key
50
60
  def construct_diff(name)
51
61
  klass = CONSTRUCTS.fetch(name) do
52
62
  raise ArgumentError, "no such construct #{name.inspect} — known: #{CONSTRUCTS.keys.join(', ')}"
@@ -72,6 +82,10 @@ module Hecks
72
82
  missing_from_ruby: accounted - emitted, unaccounted_in_ruby: unaccounted }
73
83
  end
74
84
 
85
+ # Diffs one or every construct kind at once.
86
+ #
87
+ # @param names [Array<String>] `CONSTRUCTS` keys to diff, every construct when empty
88
+ # @return [Array<Hash>] one `construct_diff` result per name
75
89
  def constructs(names = [])
76
90
  targets = names.empty? ? CONSTRUCTS.keys : names
77
91
  targets.map { |name| construct_diff(name) }
@@ -96,6 +110,10 @@ module Hecks
96
110
  # already a distinct object, whether it was block-declared or
97
111
  # bare-referenced — object identity carries no signal past that
98
112
  # point, for any construct, not just this one).
113
+ #
114
+ # @param registry [Runtime::Registry] the booted registry to walk
115
+ # @param chapter_name [String, nil] one chapter to walk, every booted chapter when nil
116
+ # @return [Array<Rule>]
99
117
  def collect_rules(registry, chapter_name = nil)
100
118
  rules = []
101
119
  chapters = chapter_name ? [registry.bluebook(chapter_name)] : registry.bluebooks.values
@@ -158,6 +176,10 @@ module Hecks
158
176
  # local_givens` reads it directly to group `collect_rules`' own
159
177
  # output by owner itself, the same reading `duplicates`' own
160
178
  # `declaration_count` makes.
179
+ #
180
+ # @param location [String] a `Rule#location`
181
+ # @return [String] the owning construct's path, with `" (declared)"` or a trailing
182
+ # `.CommandName` segment stripped
161
183
  def owner_of(location)
162
184
  return location.sub(/ \(declared\)\z/, "") if location.end_with?(" (declared)")
163
185
 
@@ -188,6 +210,12 @@ module Hecks
188
210
  # `domains: []` means "the self-hosted meta-domain only" — pass real
189
211
  # domain directories explicitly to include them, or `nil` (the
190
212
  # default) for meta-domain plus every real example.
213
+ #
214
+ # @param domains [Array<String>, nil] domain root directories to scan; every real
215
+ # example (`Codemod::EXAMPLE_ROOTS`) when nil, none beyond the meta-domain when `[]`
216
+ # @param include_meta [Boolean] whether to also scan the self-hosted meta-domain
217
+ # @return [Array<Hash>] one entry per duplicate group: `:kind`, `:description`,
218
+ # `:canonical`, and `:locations` (`Array<String>`)
191
219
  def duplicates(domains: nil, include_meta: true)
192
220
  domains ||= Codemod::EXAMPLE_ROOTS
193
221
  all_rules = []
@@ -260,6 +288,9 @@ module Hecks
260
288
  # case above, before assuming a group naming two pieces under
261
289
  # different aggregates is fresh duplication rather than an already-
262
290
  # resolved chapter-wide reference.
291
+ #
292
+ # @param rules [Array<Rule>] one duplicate-key group's own rules
293
+ # @return [Integer] how many of `rules` are real, independent declarations
263
294
  def declaration_count(rules)
264
295
  declared = rules.select { |r| r.location.end_with?(" (declared)") }
265
296
  declared_owners = declared.to_set { |r| owner_of(r.location) }
@@ -282,6 +313,8 @@ module Hecks
282
313
  # `bin/hecks_query_ir_mcp` (an MCP tool result, itself a text
283
314
  # block) want the identical human-readable rendering; only the
284
315
  # outer framing differs (plain stdout vs. a JSON-RPC content array).
316
+ # @param diffs [Array<Hash>] `constructs`' own output
317
+ # @return [String] the human-readable rendering
285
318
  def format_constructs(diffs)
286
319
  diffs.map do |diff|
287
320
  lines = ["== #{diff[:name]} =="]
@@ -328,6 +361,12 @@ module Hecks
328
361
  # existing gate (`model_shape_conformance_spec.rb`,
329
362
  # `assembly_spec.rb`, `meta_domain_coverage_spec.rb`) before trusting
330
363
  # a `false` here as a real gap.
364
+ # @param name [String] a `CONSTRUCTS` key, such as `"Aggregate"`
365
+ # @param field [String, Symbol] the declared field to check propagation for
366
+ # @return [Hash{Symbol => Object}] `:name`, `:field`, and `:touchpoints` — an
367
+ # `Array<Hash>` of `:touchpoint` (String) and `:present` (Boolean, or nil when the
368
+ # touchpoint does not apply to `name`)
369
+ # @raise [ArgumentError] if `name` is not a `CONSTRUCTS` key
331
370
  def impact_preview(name, field)
332
371
  CONSTRUCTS.fetch(name) { raise ArgumentError, "no such construct #{name.inspect} — known: #{CONSTRUCTS.keys.join(', ')}" }
333
372
  field = field.to_s
@@ -402,6 +441,10 @@ module Hecks
402
441
  end
403
442
  private_class_method :rust_mentions?
404
443
 
444
+ # Renders one field's touchpoint checklist as text.
445
+ #
446
+ # @param preview [Hash] `impact_preview`'s own output
447
+ # @return [String] the human-readable rendering
405
448
  def format_impact_preview(preview)
406
449
  lines = ["== #{preview[:name]}##{preview[:field]} =="]
407
450
  preview[:touchpoints].each do |t|
@@ -421,6 +464,10 @@ module Hecks
421
464
  lines.join("\n")
422
465
  end
423
466
 
467
+ # Renders the duplicate-rule groups as text.
468
+ #
469
+ # @param groups [Array<Hash>] `duplicates`' own output
470
+ # @return [String] the human-readable rendering
424
471
  def format_duplicates(groups)
425
472
  return "no duplicate given/invariant/ensures rule found" if groups.empty?
426
473