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
@@ -38,13 +38,13 @@ module Hecks
38
38
  # never closes it. Real domain activity is unaffected; the saga's
39
39
  # own tracking of it is not. Left named rather than redesigning a
40
40
  # corpus fixture that is not this checker's to redesign.
41
- # S7, ADR 0025 — the ExternalSettlement finding this used to
42
- # allowlist is gone, not just quieted: its "sent" state was a
43
- # `state "x"` line never named by any handler's own from:/to:, a
41
+ # S7, ADR 0025 — an ExternalSettlement finding once lived here as an
42
+ # allowlist entry and is gone now, not just quieted: its "sent" state
43
+ # was a `state "x"` line never named by any handler's own from:/to:, a
44
44
  # pure declaration-drift artifact. States are derived from the
45
45
  # transitions that name them now (ProcessManagerBuilder#derived_
46
46
  # states), so a state nothing ever transitions into or out of no
47
- # longer exists to be unreachable — the finding this allowlisted
47
+ # longer exists to be unreachable — the finding that entry allowlisted
48
48
  # cannot occur any more, by construction.
49
49
  #
50
50
  # "banking"/NotifyOnClosure, FlagKeyReturn — gone from here, moved
@@ -65,7 +65,7 @@ module Hecks
65
65
  # in that domain's own declaration.
66
66
  ALLOWED_FINDINGS = {
67
67
  # QualityControl was the first domain in this corpus to trigger an
68
- # `asks`/`tells` port operation from a `policy`, and used to carry
68
+ # `asks`/`tells` port operation from a `policy`, and once carried
69
69
  # two entries here for it — both gone now, not just quieted:
70
70
  #
71
71
  # `deaf_policy` (ClearOnPass, RefuseOnFail, RecordTheIssue,
@@ -79,17 +79,17 @@ module Hecks
79
79
  #
80
80
  # `unknown_trigger` (FileWhenSubmitted, AskOnceMore) — BUG#23 — was
81
81
  # never actually a `Naming`/`PolicyBuilder` defect, confirmed by
82
- # tracing the real dispatch path rather than assuming the comment
83
- # that used to sit here: `Naming.command_ref`'s bare-constant
82
+ # tracing the real dispatch path rather than trusting a stale
83
+ # comment that once sat here: `Naming.command_ref`'s bare-constant
84
84
  # rewrite does leave `trigger Ticket::IssueTracker::File` (aggregate,
85
85
  # port, operation) as "Ticket::IssueTracker.File", a leftover `::`
86
86
  # past the aggregate — but `PolicyInterpreter#deliver` re-qualifies
87
87
  # every trigger with this domain's own name before dispatch
88
88
  # ("QualityControl::Ticket::IssueTracker.File"), and `Naming.
89
89
  # split_verb` already folds that reintroduced `::` into the
90
- # dot-joined tail correctly (fixed for `ReactionInvocation#
91
- # resolve_target`, PR #520, predating this entry's own removal) —
92
- # confirmed live: a real dispatch through `Ticket.Submit` fires
90
+ # dot-joined tail correctly (the same fix `ReactionInvocation#
91
+ # resolve_target` already carries) — confirmed live: a real
92
+ # dispatch through `Ticket.Submit` fires
93
93
  # `IssueFiled`/`TicketFiled` exactly as declared. The actual gap was
94
94
  # entirely in this checker: `verbs_of` never enumerated a port
95
95
  # operation as a triggerable verb at all, and `policy_findings`
@@ -102,6 +102,9 @@ module Hecks
102
102
 
103
103
  module_function
104
104
 
105
+ # Runs every static model check over `bluebook` and returns what it
106
+ # finds.
107
+ #
105
108
  # `hecksagon:`/`known_domains:` — both optional, both `nil`-safe
106
109
  # (every existing caller with no sibling hecksagon, or checking one
107
110
  # domain in isolation, behaves exactly as before). `hecksagon` is
@@ -118,14 +121,34 @@ module Hecks
118
121
  # `rust_target:`/`strict:` — both default false, both only change the
119
122
  # severity of `rust_reserved_name` findings (see
120
123
  # `rust_reserved_name_findings`); every other finding is unaffected.
121
- def call(bluebook, hecksagon: nil, known_domains: nil, rust_target: false, strict: false)
124
+ #
125
+ # @param bluebook [Bluebook::Chapter] the assembled chapter to check
126
+ # @param hecksagon [Bluebook::Hecksagon, nil] this bluebook's own
127
+ # sibling wiring file, if the caller loaded one
128
+ # @param known_domains [Set<String>, nil] every bluebook/hecksagon
129
+ # name the caller has booted anywhere in this corpus scan
130
+ # @param global_emitted_events [Set<String>, nil] every bare event
131
+ # name any domain the caller has booted this run emits — lets a
132
+ # same-domain-target policy (e.g. one built by `translates`)
133
+ # reacting to a genuinely foreign event avoid a false `deaf_policy`
134
+ # finding; `nil` (every pre-existing call site) keeps today's
135
+ # single-domain-only check
136
+ # @param rust_target [Boolean] whether this domain has a real Rust
137
+ # target, raising `rust_reserved_name` findings to error
138
+ # @param strict [Boolean] whether to raise every `rust_reserved_name`
139
+ # finding to error regardless of `rust_target`
140
+ # @return [Array<Finding>] every finding this bluebook triggers,
141
+ # across its lifecycles, sagas, policies, and Rust-reserved names
142
+ def call(bluebook, hecksagon: nil, known_domains: nil, global_emitted_events: nil, rust_target: false, strict: false)
122
143
  findings = []
123
144
  bluebook.aggregates.each do |aggregate|
124
145
  findings.concat(lifecycle_findings(aggregate, aggregate))
125
146
  aggregate.entities.each { |entity| findings.concat(lifecycle_findings(aggregate, entity)) }
126
147
  end
127
148
  bluebook.process_managers.each { |process_manager| findings.concat(saga_findings(bluebook, process_manager)) }
128
- bluebook.policies.each { |policy| findings.concat(policy_findings(bluebook, policy, hecksagon, known_domains)) }
149
+ bluebook.policies.each do |policy|
150
+ findings.concat(policy_findings(bluebook, policy, hecksagon, known_domains, global_emitted_events))
151
+ end
129
152
  findings.concat(rust_reserved_name_findings(domain_name: bluebook.name,
130
153
  aggregate_names: bluebook.aggregates.map(&:hecks_name),
131
154
  rust_target: rust_target, strict: strict))
@@ -155,6 +178,18 @@ module Hecks
155
178
  #
156
179
  # The module-name transform is `downcase`, the one both generators
157
180
  # apply to an aggregate name and to an attached chapter's name.
181
+ #
182
+ # @param domain_name [String, Symbol, nil] the domain's own name, or
183
+ # `nil` to skip the domain-level check
184
+ # @param aggregate_names [Array<String, Symbol>] every aggregate name
185
+ # to check
186
+ # @param rust_target [Boolean] whether this domain has a real Rust
187
+ # target, raising severity to error
188
+ # @param strict [Boolean] whether to raise severity to error
189
+ # regardless of `rust_target`
190
+ # @return [Array<Finding>] one `:rust_reserved_name` finding per
191
+ # aggregate or domain name that collides with a Rust or Cargo
192
+ # reserved word
158
193
  def rust_reserved_name_findings(domain_name: nil, aggregate_names: [], rust_target: false, strict: false)
159
194
  severity = rust_target || strict ? :error : :warning
160
195
  keywords = Hecks::Vocabulary.fetch("RustReservedWord")
@@ -171,6 +206,17 @@ module Hecks
171
206
  findings
172
207
  end
173
208
 
209
+ # Checks the domain's own module/Cargo-feature name, one level up
210
+ # from `rust_reserved_name_findings`'s own per-aggregate check.
211
+ #
212
+ # @param domain_name [String, Symbol] the domain's own name
213
+ # @param keywords [Array<String>] the admitted `RustReservedWord`
214
+ # vocabulary rows
215
+ # @param severity [Symbol] the finding severity to use, `:error` or
216
+ # `:warning`
217
+ # @return [Array<Finding>] a single-element array holding a
218
+ # `:rust_reserved_name` finding, or `[]` if `domain_name` collides
219
+ # with no reserved word or Cargo key
174
220
  def domain_reserved_name_findings(domain_name, keywords, severity)
175
221
  module_name = rust_module_name(domain_name)
176
222
  table = if keywords.include?(module_name)
@@ -185,10 +231,27 @@ module Hecks
185
231
  "rename the domain")]
186
232
  end
187
233
 
234
+ # Derives the Rust module identifier both generators derive from a
235
+ # name.
236
+ #
237
+ # @param name [String, Symbol] an aggregate or domain name
238
+ # @return [String] the Rust module identifier both generators derive
239
+ # from `name`
188
240
  def rust_module_name(name) = name.to_s.downcase
189
241
 
190
242
  # ── lifecycles (aggregate and entity — a piece may declare one too) ──
191
243
 
244
+ # Runs every lifecycle-shaped model check for one declaring
245
+ # construct's own lifecycle, if it has one.
246
+ #
247
+ # @param aggregate [Bluebook::Aggregate] the root aggregate `declaring`
248
+ # belongs to
249
+ # @param declaring [Bluebook::Aggregate, Bluebook::Entity] the
250
+ # construct whose own lifecycle to check — `aggregate` itself, or
251
+ # one of its entities
252
+ # @return [Array<Finding>] `[]` if `declaring` has no lifecycle;
253
+ # otherwise every `unknown_command`/`unreachable_state`/
254
+ # `dead_transition`/`stuck_state` finding its lifecycle triggers
192
255
  def lifecycle_findings(aggregate, declaring)
193
256
  lifecycle = declaring.lifecycle
194
257
  return [] unless lifecycle
@@ -207,6 +270,15 @@ module Hecks
207
270
  findings
208
271
  end
209
272
 
273
+ # Finds every declared state a lifecycle's own reachability walk
274
+ # never reaches.
275
+ #
276
+ # @param lifecycle [Bluebook::Lifecycle] the lifecycle being checked
277
+ # @param full [Array<String>] every state `full_states` declares
278
+ # @param reached [Set<String>] every state `reachable_states` reaches
279
+ # @param subject [String] the finding's own subject label
280
+ # @return [Array<Finding>] one `:unreachable_state` finding per
281
+ # declared state that `reached` does not cover
210
282
  def unreachable_state_findings(lifecycle, full, reached, subject)
211
283
  (full - reached.to_a).map do |state|
212
284
  Finding.new(kind: :unreachable_state, severity: :error, subject: subject,
@@ -215,6 +287,13 @@ module Hecks
215
287
  end
216
288
  end
217
289
 
290
+ # Finds every constrained transition that can never fire.
291
+ #
292
+ # @param lifecycle [Bluebook::Lifecycle] the lifecycle being checked
293
+ # @param reached [Set<String>] every state `reachable_states` reaches
294
+ # @param subject [String] the finding's own subject label
295
+ # @return [Array<Finding>] one `:dead_transition` finding per
296
+ # constrained transition whose `from:` states are all unreached
218
297
  def dead_transition_findings(lifecycle, reached, subject)
219
298
  lifecycle.transitions.filter_map do |command, transition|
220
299
  next unless transition.constrained?
@@ -226,6 +305,14 @@ module Hecks
226
305
  end
227
306
  end
228
307
 
308
+ # Finds every reached state with no transition ever leaving it.
309
+ #
310
+ # @param lifecycle [Bluebook::Lifecycle] the lifecycle being checked
311
+ # @param reached [Set<String>] every state `reachable_states` reaches
312
+ # @param subject [String] the finding's own subject label
313
+ # @return [Array<Finding>] one `:stuck_state` finding per reached,
314
+ # non-exempt state with no transition ever leaving it (skipped
315
+ # entirely when any transition is unconstrained)
229
316
  def stuck_state_findings(lifecycle, reached, subject)
230
317
  any_unconstrained = lifecycle.transitions.any? { |_, t| !t.constrained? }
231
318
  (reached - terminal_exempt(lifecycle)).filter_map do |state|
@@ -238,6 +325,15 @@ module Hecks
238
325
  end
239
326
  end
240
327
 
328
+ # Finds every transition named after a command this construct does
329
+ # not declare.
330
+ #
331
+ # @param lifecycle [Bluebook::Lifecycle] the lifecycle being checked
332
+ # @param commands [Array<String>] every command name the declaring
333
+ # construct actually declares
334
+ # @param subject [String] the finding's own subject label
335
+ # @return [Array<Finding>] one `:unknown_command` finding per
336
+ # transition named after a command `commands` does not list
241
337
  def unknown_transition_commands(lifecycle, commands, subject)
242
338
  lifecycle.transitions.filter_map do |command, _transition|
243
339
  next if commands.include?(command)
@@ -251,6 +347,10 @@ module Hecks
251
347
  # from-only state (declared nowhere as a target) is real and is
252
348
  # exactly the hole `Lifecycle#states` leaves: it answers default
253
349
  # plus targets only.
350
+ #
351
+ # @param lifecycle [Bluebook::Lifecycle] the lifecycle being checked
352
+ # @return [Array<String>] every state `lifecycle` declares, in any
353
+ # role (default, target, or from), unique
254
354
  def full_states(lifecycle)
255
355
  (
256
356
  [lifecycle.default] +
@@ -262,6 +362,10 @@ module Hecks
262
362
  # Least fixpoint from the default state: an unconstrained
263
363
  # transition always fires, from wherever the machine is ; a
264
364
  # constrained one fires once any of its named sources is reached.
365
+ #
366
+ # @param lifecycle [Bluebook::Lifecycle] the lifecycle being checked
367
+ # @return [Set<String>] every state reachable from `lifecycle`'s own
368
+ # default state
265
369
  def reachable_states(lifecycle)
266
370
  reached = Set.new([lifecycle.default])
267
371
  loop do
@@ -299,6 +403,10 @@ module Hecks
299
403
  # should still warn (see spec/fixtures/model_check/lifecycle_
300
404
  # findings.bluebook's own Widget::Part, which stays warned on
301
405
  # purpose).
406
+ #
407
+ # @param lifecycle [Bluebook::Lifecycle] the lifecycle being checked
408
+ # @return [Array<String>] `[lifecycle.default]` when it is exempt from
409
+ # the stuck-state warning, `[]` otherwise
302
410
  def terminal_exempt(lifecycle)
303
411
  return [] if lifecycle.transitions.empty?
304
412
 
@@ -308,6 +416,15 @@ module Hecks
308
416
 
309
417
  # ── process managers / sagas ──────────────────────────────────────
310
418
 
419
+ # Runs every saga-shaped model check for one process manager.
420
+ #
421
+ # @param bluebook [Bluebook::Chapter] the assembled chapter
422
+ # `process_manager` belongs to
423
+ # @param process_manager [Bluebook::ProcessManager] the process
424
+ # manager to check
425
+ # @return [Array<Finding>] every `deaf_trigger`/`unreachable_pm_state`/
426
+ # `deaf_handler`/`unknown_dispatch`/`unarmed_compensation`/
427
+ # `dead_compensation` finding this process manager triggers
311
428
  def saga_findings(bluebook, process_manager)
312
429
  emitted = emitted_events(bluebook)
313
430
  # (domain, aggregate, command) triples, not raw strings — see
@@ -328,6 +445,15 @@ module Hecks
328
445
  findings
329
446
  end
330
447
 
448
+ # Finds every declared `starts_on`/`ends_on` event this domain never
449
+ # emits.
450
+ #
451
+ # @param process_manager [Bluebook::ProcessManager] the process
452
+ # manager being checked
453
+ # @param emitted [Array<String>] every bare event name this domain
454
+ # actually emits
455
+ # @return [Array<Finding>] one `:deaf_trigger` finding per declared
456
+ # `starts_on`/`ends_on` event this domain never emits
331
457
  def deaf_trigger_findings(process_manager, emitted)
332
458
  [process_manager.starts_on, process_manager.ends_on].compact.filter_map do |event|
333
459
  next if emitted.include?(bare(event))
@@ -338,6 +464,14 @@ module Hecks
338
464
  end
339
465
  end
340
466
 
467
+ # Finds every declared state no handler chain ever reaches.
468
+ #
469
+ # @param process_manager [Bluebook::ProcessManager] the process
470
+ # manager being checked
471
+ # @param reached [Set<String>] every state `pm_reachable_states`
472
+ # reaches
473
+ # @return [Array<Finding>] one `:unreachable_pm_state` finding per
474
+ # declared state that `reached` does not cover
341
475
  def unreachable_pm_state_findings(process_manager, reached)
342
476
  (Array(process_manager.states) - reached.to_a).map do |state|
343
477
  Finding.new(kind: :unreachable_pm_state, severity: :error, subject: process_manager.name,
@@ -352,11 +486,25 @@ module Hecks
352
486
  # check reads only this handler plus the domain-wide emitted/verbs
353
487
  # sets saga_findings already resolved once, no state shared between
354
488
  # handlers.
489
+ #
490
+ # @param bluebook [Bluebook::Chapter] the assembled chapter
491
+ # `process_manager` belongs to
492
+ # @param process_manager [Bluebook::ProcessManager] the process
493
+ # manager `handler` belongs to
494
+ # @param emitted [Array<String>] every bare event name this domain
495
+ # actually emits
496
+ # @param verbs [Array<Array(String, String, String)>] every command
497
+ # this domain declares, parsed as `Naming.split_verb`
498
+ # `[domain, aggregate, command]` triples
499
+ # @param handler [Bluebook::ProcessManagerHandler] the handler to
500
+ # check
501
+ # @return [Array<Finding>] every `deaf_handler`/`unknown_dispatch`/
502
+ # `unarmed_compensation` finding this handler triggers
355
503
  def handler_findings(bluebook, process_manager, emitted, verbs, handler)
356
504
  findings = []
357
505
 
358
- # The compensating leg answers REFUSED, a synthetic trigger no
359
- # command ever emits by name (ProcessManager::REFUSED) — not
506
+ # The compensating leg answers `REFUSED`, a synthetic trigger no
507
+ # command ever emits by name (`ProcessManager::REFUSED`) — not
360
508
  # a deaf handler, the one handler this domain's own events can
361
509
  # never satisfy on purpose.
362
510
  if handler.event_type != ProcessManager::REFUSED && !emitted.include?(bare(handler.event_type))
@@ -367,16 +515,16 @@ module Hecks
367
515
 
368
516
  handler.dispatches.each do |dispatch|
369
517
  # **Always this domain** — same fix, same reason, as `SagaInterpreter
370
- # #qualified` (BUG#6). This used to guess: a dispatch whose own
371
- # `command_name` still carried a leftover `::` after `Naming.
372
- # command_ref`'s own rewrite was read as "already qualified" and
373
- # left alone — the exact same string-shape ambiguity that
518
+ # #qualified` (BUG#6). Guessing instead — reading a dispatch whose
519
+ # own `command_name` still carries a leftover `::` after `Naming.
520
+ # command_ref`'s own rewrite as "already qualified" and leaving it
521
+ # alone — would hit the exact same string-shape ambiguity that
374
522
  # `SagaInterpreter#qualified`'s own comment explains at length
375
523
  # (a same-domain entity command reference and a genuinely
376
524
  # cross-domain one are textually indistinguishable after that
377
525
  # rewrite). Confirmed against the entire corpus, same as that
378
526
  # fix: no saga anywhere ever dispatches genuinely cross-domain,
379
- # so this checker now qualifies exactly the way the runtime
527
+ # so this checker qualifies exactly the way the runtime
380
528
  # actually dispatches — unconditionally against `bluebook.name`
381
529
  # — instead of maintaining its own, independently-wrong copy of
382
530
  # the same guess.
@@ -410,7 +558,7 @@ module Hecks
410
558
  # shape of the real bug this whole feature closes ("the
411
559
  # reversal was written and never armed"), caught at build/
412
560
  # model-check time instead of discovered in production. No
413
- # handler anywhere answers REFUSED (`process_manager.saga?` false) means
561
+ # handler anywhere answers `REFUSED` (`process_manager.saga?` false) means
414
562
  # `SagaInterpreter#unwind` never runs for this process
415
563
  # manager at all, so a declared `compensates` is structurally
416
564
  # unreachable — not a warning about style, a dead declaration.
@@ -426,6 +574,16 @@ module Hecks
426
574
  findings
427
575
  end
428
576
 
577
+ # Checks whether a saga's own compensation leaves an unreachable
578
+ # state.
579
+ #
580
+ # @param process_manager [Bluebook::ProcessManager] the process
581
+ # manager being checked
582
+ # @param reached [Set<String>] every state `pm_reachable_states`
583
+ # reaches
584
+ # @return [Array<Finding>] a single-element array holding a
585
+ # `:dead_compensation` finding, or `[]` if `process_manager` has no
586
+ # saga or its own compensation leaves a reached state
429
587
  def dead_compensation_findings(process_manager, reached)
430
588
  return [] unless process_manager.saga? && !reached.include?(process_manager.saga.from_state)
431
589
 
@@ -435,13 +593,21 @@ module Hecks
435
593
  end
436
594
 
437
595
  # A handler edge is only usable in the closure if it can actually
438
- # fire — REFUSED always can (it is a compensation trigger, not an
596
+ # fire — `REFUSED` always can (it is a compensation trigger, not an
439
597
  # event), and any other handler needs its event genuinely emitted.
440
598
  # Without this, a deaf handler's declared from_state -> to_state
441
599
  # pair reads as connected even though nothing can ever traverse
442
600
  # it, which would hide exactly the states this walk exists to
443
601
  # catch (a state only "reachable" through a handler that itself
444
602
  # never fires).
603
+ #
604
+ # @param process_manager [Bluebook::ProcessManager] the process
605
+ # manager being checked
606
+ # @param emitted [Array<String>] every bare event name this domain
607
+ # actually emits
608
+ # @return [Set<String>] every state reachable from
609
+ # `process_manager`'s own first declared state, through handlers
610
+ # that can actually fire
445
611
  def pm_reachable_states(process_manager, emitted)
446
612
  return Set.new if Array(process_manager.states).empty?
447
613
 
@@ -463,7 +629,21 @@ module Hecks
463
629
 
464
630
  # ── policies ───────────────────────────────────────────────────────
465
631
 
466
- def policy_findings(bluebook, policy, hecksagon, known_domains)
632
+ # Runs every same-domain policy check, or defers to
633
+ # `cross_domain_policy_findings` for a policy that targets another
634
+ # domain.
635
+ #
636
+ # @param bluebook [Bluebook::Chapter] the assembled chapter `policy`
637
+ # belongs to
638
+ # @param policy [Bluebook::Policy] the policy to check
639
+ # @param hecksagon [Bluebook::Hecksagon, nil] this bluebook's own
640
+ # sibling wiring file, if the caller loaded one
641
+ # @param known_domains [Set<String>, nil] every bluebook/hecksagon
642
+ # name the caller has booted anywhere in this corpus scan
643
+ # @return [Array<Finding>] every `deaf_policy`/`unknown_trigger`
644
+ # finding this policy triggers, or `cross_domain_policy_findings`'s
645
+ # own return for a cross-domain policy
646
+ def policy_findings(bluebook, policy, hecksagon, known_domains, global_emitted_events = nil)
467
647
  return cross_domain_policy_findings(policy, hecksagon, known_domains) if policy.target_domain
468
648
 
469
649
  emitted = emitted_events(bluebook)
@@ -480,7 +660,18 @@ module Hecks
480
660
  # prior aggregate-scoped policy (ReviewOnFreeze) is also cross-domain
481
661
  # (`across "Compliance"`), which exits this method one line above
482
662
  # before the mismatch is ever reached.
483
- unless emitted.include?(policy.event_name)
663
+ # `global_emitted_events` — a `translates` (hecksagon-level)
664
+ # reaction is BUILT as the exact same same-domain-target `Policy`
665
+ # an ordinary `policy` block is (no `target_domain`, so it never
666
+ # reaches `cross_domain_policy_findings` above), but its whole
667
+ # point is to react to a FOREIGN domain's own event — this
668
+ # domain's own `emitted_events(bluebook)` was never going to
669
+ # contain it. Checked only as a FALLBACK, after the local check
670
+ # already failed, so a real same-domain typo still gets flagged
671
+ # exactly as before whenever the caller has no cross-domain set
672
+ # to offer (nil — every existing call site, unchanged) or the
673
+ # event genuinely isn't emitted anywhere this run examined.
674
+ unless emitted.include?(policy.event_name) || global_emitted_events&.include?(policy.event_name)
484
675
  findings << Finding.new(kind: :deaf_policy, severity: :error, subject: policy.name,
485
676
  message: "on #{policy.on_event.inspect}, which no command in this domain emits")
486
677
  end
@@ -536,9 +727,20 @@ module Hecks
536
727
  # DDD vocabulary (Shared Kernel, Customer/Supplier) lives here, in
537
728
  # the finding's own name and this comment, and in prose docs — not
538
729
  # in the grammar.
730
+ #
731
+ # @param policy [Bluebook::Policy] the cross-domain policy to check
732
+ # @param hecksagon [Bluebook::Hecksagon, nil] this bluebook's own
733
+ # sibling wiring file, if the caller loaded one
734
+ # @param known_domains [Set<String>, nil] every bluebook/hecksagon
735
+ # name the caller has booted anywhere in this corpus scan
736
+ # @return [Array<Finding>] `expected_undelivered_findings`'s own
737
+ # return for a policy declared `expect_undelivered`; otherwise every
738
+ # `contradictory_relationship`/`unacknowledged_relationship`/
739
+ # `unknown_target_domain` finding this policy triggers
539
740
  def cross_domain_policy_findings(policy, hecksagon, known_domains)
540
741
  return expected_undelivered_findings(policy, hecksagon, known_domains) if policy.expect_undelivered
541
- return [] unless hecksagon # no sibling hecksagon loaded — nothing to check a relationship against.
742
+ # No sibling hecksagon loaded — nothing to check a relationship against.
743
+ return [] unless hecksagon
542
744
 
543
745
  target = policy.target_domain
544
746
  findings = []
@@ -601,6 +803,16 @@ module Hecks
601
803
  # the reaction can be delivered, and the declaration is now a lie.
602
804
  # `known_domains` is nil for a single-target run, which can then only
603
805
  # check the hecksagon half.
806
+ #
807
+ # @param policy [Bluebook::Policy] the `expect_undelivered` policy to
808
+ # check
809
+ # @param hecksagon [Bluebook::Hecksagon, nil] this bluebook's own
810
+ # sibling wiring file, if the caller loaded one
811
+ # @param known_domains [Set<String>, nil] every bluebook/hecksagon
812
+ # name the caller has booted anywhere in this corpus scan
813
+ # @return [Array<Finding>] a single-element array holding a
814
+ # `:stale_undelivered_expectation` finding, or `[]` if the target
815
+ # remains genuinely unreachable
604
816
  def expected_undelivered_findings(policy, hecksagon, known_domains)
605
817
  target = policy.target_domain
606
818
  reached = []
@@ -641,6 +853,11 @@ module Hecks
641
853
  # read both. `.compact` because an inbound operation's `.answers`/
642
854
  # `.refuses` are always nil (there is no channel back to tell), which
643
855
  # would otherwise seed every emitted-events set with a stray nil.
856
+ #
857
+ # @param bluebook [Bluebook::Chapter] the assembled chapter to
858
+ # enumerate
859
+ # @return [Array<String>] every bare event name this domain's
860
+ # commands and port operations emit, answer, or refuse, unique
644
861
  def emitted_events(bluebook)
645
862
  aggregate_emits = bluebook.aggregates.flat_map do |aggregate|
646
863
  aggregate.commands.map(&:emits) +
@@ -659,6 +876,11 @@ module Hecks
659
876
  # instead. Pulled out of `emitted_events` above purely to keep that
660
877
  # method's own branching low enough to read at a glance — every
661
878
  # port, aggregate-owned or chapter-level, asks this the same way.
879
+ #
880
+ # @param ports [Array<Bluebook::DomainPort>] the ports to enumerate
881
+ # @return [Array<String, nil>] every event name each operation emits,
882
+ # answers, or refuses; `nil` entries included (compacted by the
883
+ # caller)
662
884
  def port_operation_events(ports)
663
885
  ports.flat_map { |port| port.operations.flat_map { |op| [*op.emits, op.answers, op.refuses] } }
664
886
  end
@@ -667,6 +889,12 @@ module Hecks
667
889
  # carries and fuzzing/sequence_generator/catalog.rb builds
668
890
  # independently for the same reason: a saga dispatch and a fuzzer
669
891
  # step both have to name a verb the same way the door does.
892
+ #
893
+ # @param bluebook [Bluebook::Chapter] the assembled chapter to
894
+ # enumerate
895
+ # @return [Array<String>] every command's own fully-qualified verb,
896
+ # `"Domain::Aggregate.Command"` or
897
+ # `"Domain::Aggregate.Entity.Command"`
670
898
  def verbs_of(bluebook)
671
899
  bluebook.aggregates.flat_map do |aggregate|
672
900
  verbs = aggregate.commands.map { |command| "#{bluebook.name}::#{aggregate.hecks_name}.#{command.hecks_name}" }
@@ -687,6 +915,11 @@ module Hecks
687
915
  # Only an aggregate's own ports (`aggregate.ports`) are in scope here
688
916
  # — a policy's `trigger` always names one aggregate, never a chapter-
689
917
  # level port with no owner to address through.
918
+ #
919
+ # @param bluebook [Bluebook::Chapter] the assembled chapter to
920
+ # enumerate
921
+ # @return [Array<String>] every aggregate-owned port operation's own
922
+ # fully-qualified verb, `"Domain::Aggregate.Port.Operation"`
690
923
  def port_verbs_of(bluebook)
691
924
  bluebook.aggregates.flat_map do |aggregate|
692
925
  aggregate.ports.flat_map do |port|
@@ -703,10 +936,21 @@ module Hecks
703
936
  # spellings of the same verb as strings (see `policy_findings`'s own
704
937
  # `unknown_trigger` check for why that comparison has to happen this
705
938
  # way, not as `include?` on a raw string).
939
+ #
940
+ # @param bluebook [Bluebook::Chapter] the assembled chapter to
941
+ # enumerate
942
+ # @return [Set<Array(String, String, String)>] every triggerable
943
+ # verb, parsed as `Naming.split_verb` `[domain, aggregate,
944
+ # command]` triples
706
945
  def triggerable_verbs(bluebook)
707
946
  (verbs_of(bluebook) + port_verbs_of(bluebook)).to_set { |verb| Naming.split_verb(verb) }
708
947
  end
709
948
 
949
+ # Strips a domain qualifier off an event name.
950
+ #
951
+ # @param event [String, Symbol] an event name, qualified
952
+ # (`"Domain::Event"`) or bare
953
+ # @return [String] `event`'s own bare name, past the last `::`
710
954
  def bare(event) = event.to_s.split("::").last
711
955
  end
712
956
  end
@@ -72,6 +72,10 @@ module Hecks
72
72
  # methods agreeing about a shared cursor" — exactly what the walk's
73
73
  # own comment says a cleverer spelling would obscure.
74
74
  # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
75
+ #
76
+ # @param pattern [String, Symbol, #to_s] the declared `pattern:` regex source
77
+ # @return [Rejection, nil] the reason the pattern is refused, or `nil` if it is
78
+ # entirely within the portable subset
75
79
  def validate(pattern)
76
80
  chars = pattern.to_s.chars
77
81
  index = 0
@@ -145,8 +149,17 @@ module Hecks
145
149
  possessive: "possessive quantifier"
146
150
  }.freeze
147
151
 
152
+ # Builds the rejection for one refused construct.
153
+ #
154
+ # @param key [Symbol] a key of `CONSTRUCTS`/`REASONS`, such as `:lookahead`
155
+ # @return [Rejection] the construct's name and the reason it is refused
148
156
  def refuse(key) = Rejection.new(CONSTRUCTS.fetch(key), REASONS.fetch(key))
149
157
 
158
+ # Says whether a POSIX bracket class (`[:digit:]` and friends) starts at `index`.
159
+ #
160
+ # @param chars [Array<String>] the pattern, split into characters
161
+ # @param index [Integer] the position to check
162
+ # @return [Boolean] whether a POSIX bracket class starts at `index`
150
163
  def posix_class_at?(chars, index)
151
164
  return false unless chars[index] == "[" && chars[index + 1] == ":"
152
165
 
@@ -156,9 +169,13 @@ module Hecks
156
169
  end
157
170
 
158
171
  # A possessive quantifier is `*+`, `++`, `?+`, or a bounded `{n}`/{n,m}`
159
- # immediately followed by `+` — only checked OUTSIDE a character class,
172
+ # immediately followed by `+` — only checked outside a character class,
160
173
  # where `*`, `+`, `?`, `{`, `}` are quantifier syntax rather than
161
174
  # literal characters.
175
+ #
176
+ # @param chars [Array<String>] the pattern, split into characters
177
+ # @param index [Integer] the position to check
178
+ # @return [Boolean] whether a possessive quantifier starts at `index`
162
179
  def possessive_at?(chars, index)
163
180
  return true if %w[* + ?].include?(chars[index]) && chars[index + 1] == "+"
164
181
  return false unless chars[index] == "{"
@@ -169,6 +186,11 @@ module Hecks
169
186
 
170
187
  # Length of a `{n}` / `{n,}` / `{n,m}` bound starting at `index`, or nil
171
188
  # if what's there isn't one.
189
+ #
190
+ # @param chars [Array<String>] the pattern, split into characters
191
+ # @param index [Integer] the position the bound is expected to start at
192
+ # @return [Integer, nil] the bound's length in characters, or `nil` if `index`
193
+ # does not start a `{n}`/`{n,}`/`{n,m}` bound
172
194
  def bounded_quantifier_length(chars, index)
173
195
  cursor = index + 1
174
196
  digit_seen = false
@@ -69,6 +69,10 @@ module Hecks
69
69
  # per-dispatch saga compensation instead — a deliberate choice, not
70
70
  # an accidental collision.
71
71
  Saga = Struct.new(:trigger, :from_state, :to_state, :compensations, keyword_init: true) do
72
+ # Lists the commands this saga sends to undo a refused leg.
73
+ #
74
+ # @return [Array<String>] the name of each compensating command, in the order
75
+ # `compensations` lists them
72
76
  def undoes = compensations.map(&:command_name)
73
77
 
74
78
  def to_s = "#{trigger} → #{to_state} (#{undoes.join(', ')})"
@@ -120,6 +124,15 @@ module Hecks
120
124
 
121
125
  attr_reader :name, :correlates_by, :starts_on, :ends_on, :states, :handlers
122
126
 
127
+ # @param name [String, Symbol] the process manager's declared name
128
+ # @param correlates_by [Symbol, nil] the payload field a triggering event's
129
+ # instances are correlated by
130
+ # @param starts_on [String, nil] the event that starts a new instance
131
+ # @param ends_on [String, nil] the event that ends an instance
132
+ # @param states [Array<String>] the declared states this procedure's instances pass
133
+ # through
134
+ # @param handlers [Array<Bluebook::ProcessManagerHandler>] the declared handler rows,
135
+ # one per (event, state) leg
123
136
  def initialize(name:, correlates_by: nil, starts_on: nil, ends_on: nil,
124
137
  states: [], handlers: [])
125
138
  @name = name.to_s
@@ -8,10 +8,15 @@ module Hecks
8
8
 
9
9
  attr_reader :root
10
10
 
11
+ # @param root [String] the directory to search under
11
12
  def initialize(root)
12
13
  @root = File.expand_path(root)
13
14
  end
14
15
 
16
+ # Walks `root` for every directory that holds `.bluebook` files.
17
+ #
18
+ # @return [Array<String>] the absolute path of each `bluebook` directory found,
19
+ # sorted; a directory named in `SKIPPED_DIRECTORIES` is pruned from the walk
15
20
  def bluebook_directories
16
21
  found = []
17
22
  Find.find(root) do |path|