hecks 1.0.2 → 1.1.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 (239) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +3 -3
  3. data/lib/hecks/adapters/driven/d1.rb +48 -28
  4. data/lib/hecks/adapters/driven/folder.rb +10 -3
  5. data/lib/hecks/adapters/driven/google_authentication.rb +2 -4
  6. data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
  7. data/lib/hecks/adapters/driven/heki.rb +3 -5
  8. data/lib/hecks/adapters/driven/in_memory_ordering.rb +4 -1
  9. data/lib/hecks/adapters/driven/lambda.rb +2 -2
  10. data/lib/hecks/adapters/driven/local_storage.adapter +3 -0
  11. data/lib/hecks/adapters/driven/local_storage.rb +120 -0
  12. data/lib/hecks/adapters/driven/memory.rb +48 -3
  13. data/lib/hecks/adapters/driven/postgres/codec.rb +3 -1
  14. data/lib/hecks/adapters/driven/postgres/outbox.rb +97 -0
  15. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +3 -2
  16. data/lib/hecks/adapters/driven/postgres.rb +25 -14
  17. data/lib/hecks/adapters/driven/prism.rb +11 -0
  18. data/lib/hecks/adapters/driven/sql_query_builder.rb +45 -20
  19. data/lib/hecks/adapters/driven/sqlite/codec.rb +3 -1
  20. data/lib/hecks/adapters/driven/sqlite/projection.rb +17 -1
  21. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +23 -1
  22. data/lib/hecks/adapters/driven/sqlite.rb +87 -8
  23. data/lib/hecks/adapters/driven.rb +4 -0
  24. data/lib/hecks/adapters.rb +4 -0
  25. data/lib/hecks/behaviors/dsl.rb +7 -0
  26. data/lib/hecks/behaviors/expectations.rb +32 -4
  27. data/lib/hecks/behaviors/rspec.rb +2 -0
  28. data/lib/hecks/behaviors/runner.rb +4 -0
  29. data/lib/hecks/bluebook/aggregate.rb +6 -5
  30. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +4 -4
  31. data/lib/hecks/bluebook/assembly/build.rb +6 -3
  32. data/lib/hecks/bluebook/assembly/contract.rb +5 -1
  33. data/lib/hecks/bluebook/assembly/contracts.rb +4 -0
  34. data/lib/hecks/bluebook/assembly/marks.rb +2 -1
  35. data/lib/hecks/bluebook/assembly.rb +1 -3
  36. data/lib/hecks/bluebook/attribute.rb +6 -0
  37. data/lib/hecks/bluebook/behaviour/policy.rb +18 -0
  38. data/lib/hecks/bluebook/behaviour/process_manager.rb +17 -1
  39. data/lib/hecks/bluebook/behaviour/traits.rb +1 -1
  40. data/lib/hecks/bluebook/command.rb +6 -5
  41. data/lib/hecks/bluebook/dsl/adapter_builder.rb +4 -1
  42. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +524 -0
  43. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +19 -472
  44. data/lib/hecks/bluebook/dsl/attribute_collector.rb +14 -6
  45. data/lib/hecks/bluebook/dsl/binding_proxy.rb +5 -0
  46. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +848 -0
  47. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +25 -784
  48. data/lib/hecks/bluebook/dsl/command_builder.rb +53 -13
  49. data/lib/hecks/bluebook/dsl/const_shim.rb +6 -0
  50. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +20 -7
  51. data/lib/hecks/bluebook/dsl/entity_builder.rb +27 -6
  52. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +12 -2
  53. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +8 -1
  54. data/lib/hecks/bluebook/dsl/identity_declaration.rb +24 -12
  55. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +40 -1
  56. data/lib/hecks/bluebook/dsl/policy_builder.rb +10 -1
  57. data/lib/hecks/bluebook/dsl/port_builder.rb +6 -1
  58. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +33 -12
  59. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +56 -12
  60. data/lib/hecks/bluebook/dsl/query_builder.rb +7 -1
  61. data/lib/hecks/bluebook/dsl/read_model_builder.rb +11 -3
  62. data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -1
  63. data/lib/hecks/bluebook/dsl/translation_builder.rb +13 -2
  64. data/lib/hecks/bluebook/dsl/value_object_builder.rb +8 -1
  65. data/lib/hecks/bluebook/dsl/word_gate.rb +5 -0
  66. data/lib/hecks/bluebook/dsl/world_builder.rb +13 -3
  67. data/lib/hecks/bluebook/entity.rb +5 -3
  68. data/lib/hecks/bluebook/expression/ast_json.rb +88 -9
  69. data/lib/hecks/bluebook/expression/ast_reader.rb +107 -0
  70. data/lib/hecks/bluebook/expression/canonical_form.rb +7 -0
  71. data/lib/hecks/bluebook/expression/evaluator.rb +64 -11
  72. data/lib/hecks/bluebook/expression/projection.json +12 -0
  73. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +4 -3
  74. data/lib/hecks/bluebook/expression/resolver.rb +83 -11
  75. data/lib/hecks/bluebook/expression.rb +1 -0
  76. data/lib/hecks/bluebook/hexagon.rb +10 -0
  77. data/lib/hecks/bluebook/lifecycle.rb +9 -0
  78. data/lib/hecks/bluebook/meta_validator/judge.rb +10 -4
  79. data/lib/hecks/bluebook/meta_validator/plan.rb +4 -0
  80. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -1
  81. data/lib/hecks/bluebook/meta_validator/shapes.rb +3 -3
  82. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +7 -4
  83. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +42 -3
  84. data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -1
  85. data/lib/hecks/bluebook/meta_validator.rb +5 -5
  86. data/lib/hecks/bluebook/model_check.rb +119 -82
  87. data/lib/hecks/bluebook/pattern_subset.rb +9 -0
  88. data/lib/hecks/bluebook/policy.rb +2 -1
  89. data/lib/hecks/bluebook/process_manager.rb +6 -0
  90. data/lib/hecks/bluebook/project_register.rb +45 -25
  91. data/lib/hecks/bluebook/query.rb +4 -0
  92. data/lib/hecks/bluebook/read_model.rb +2 -2
  93. data/lib/hecks/bluebook/smoke_test.rb +5 -0
  94. data/lib/hecks/bluebook/synthesizer.rb +2 -1
  95. data/lib/hecks/bluebook/translation.rb +3 -0
  96. data/lib/hecks/bluebook/value_object.rb +2 -2
  97. data/lib/hecks/codemod.rb +13 -0
  98. data/lib/hecks/doc/reference.rb +17 -11
  99. data/lib/hecks/facade/cli_runner.rb +2 -2
  100. data/lib/hecks/facade/command_request.rb +11 -11
  101. data/lib/hecks/facade/handle.rb +3 -3
  102. data/lib/hecks/facade/surface/aggregate_door.rb +52 -38
  103. data/lib/hecks/facade.rb +4 -0
  104. data/lib/hecks/forms/app.rb +10 -6
  105. data/lib/hecks/forms/command_form_renderer.rb +3 -3
  106. data/lib/hecks/forms/field_renderer.rb +14 -9
  107. data/lib/hecks/forms/field_shape.rb +7 -2
  108. data/lib/hecks/forms/html.rb +5 -2
  109. data/lib/hecks/forms/index_renderer.rb +1 -1
  110. data/lib/hecks/forms/page.rb +7 -3
  111. data/lib/hecks/forms/query_form_renderer.rb +3 -2
  112. data/lib/hecks/forms/record_renderer.rb +22 -16
  113. data/lib/hecks/forms.rb +4 -0
  114. data/lib/hecks/fqn.rb +21 -9
  115. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +30 -8
  116. data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -1
  117. data/lib/hecks/fuzzing/isolated_boot.rb +14 -6
  118. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +264 -0
  119. data/lib/hecks/fuzzing/properties/guards.rb +251 -0
  120. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +351 -0
  121. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +99 -0
  122. data/lib/hecks/fuzzing/properties/querying.rb +206 -0
  123. data/lib/hecks/fuzzing/properties.rb +41 -1065
  124. data/lib/hecks/fuzzing/replay.rb +60 -16
  125. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +6 -2
  126. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +2 -1
  127. data/lib/hecks/fuzzing/sequence_generator.rb +0 -1
  128. data/lib/hecks/fuzzing/value_generator.rb +2 -2
  129. data/lib/hecks/grammar/evolve.rb +34 -10
  130. data/lib/hecks/grammar/expression_operators.json +124 -0
  131. data/lib/hecks/grammar.rb +15 -5
  132. data/lib/hecks/ir.rb +9 -0
  133. data/lib/hecks/language/bluebook/aggregate.bluebook +4 -2
  134. data/lib/hecks/language/bluebook/command.bluebook +2 -1
  135. data/lib/hecks/language/bluebook/entity.bluebook +2 -1
  136. data/lib/hecks/language/bluebook/projection.bluebook +2 -1
  137. data/lib/hecks/language/bluebook/query.bluebook +4 -2
  138. data/lib/hecks/language/bluebook/shape.bluebook +2 -1
  139. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -0
  140. data/lib/hecks/literal.rb +18 -8
  141. data/lib/hecks/naming.rb +6 -3
  142. data/lib/hecks/ports/access_control.rb +1 -1
  143. data/lib/hecks/ports/agent.rb +5 -3
  144. data/lib/hecks/ports/authentication.rb +3 -2
  145. data/lib/hecks/ports/authorization.rb +1 -1
  146. data/lib/hecks/ports/clock.rb +1 -1
  147. data/lib/hecks/ports/extraction.rb +5 -1
  148. data/lib/hecks/ports/identity_assignment.rb +1 -1
  149. data/lib/hecks/ports/identity_generation.rb +1 -1
  150. data/lib/hecks/ports/identity_resolution.rb +1 -1
  151. data/lib/hecks/ports/loading.rb +4 -1
  152. data/lib/hecks/ports/persistence/append_only.rb +40 -4
  153. data/lib/hecks/ports/persistence/binding_policy.rb +1 -1
  154. data/lib/hecks/ports/persistence/plugin.rb +3 -0
  155. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -1
  156. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +5 -5
  157. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +14 -5
  158. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +4 -0
  159. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +1 -1
  160. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +80 -16
  161. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +23 -6
  162. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +43 -5
  163. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +25 -4
  164. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +49 -9
  165. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +2 -2
  166. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +10 -0
  167. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +49 -38
  168. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +23 -13
  169. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +19 -21
  170. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +27 -9
  171. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +9 -0
  172. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +25 -10
  173. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +2 -6
  174. data/lib/hecks/ports/persistence/plugins/era.rb +3 -1
  175. data/lib/hecks/ports/persistence/remote_runtime.rb +1 -1
  176. data/lib/hecks/ports/persistence.rb +7 -3
  177. data/lib/hecks/ports/projection.rb +13 -5
  178. data/lib/hecks/ports/query/in_memory.rb +8 -2
  179. data/lib/hecks/ports/query.rb +1 -3
  180. data/lib/hecks/ports.rb +2 -0
  181. data/lib/hecks/projections/diagrams.rb +11 -3
  182. data/lib/hecks/projections/ir.rb +2 -0
  183. data/lib/hecks/projections/model/deviations.rb +2 -1
  184. data/lib/hecks/projections/model.rb +15 -10
  185. data/lib/hecks/projections/vocabulary.rb +1 -3
  186. data/lib/hecks/projector/cli_projector.rb +49 -29
  187. data/lib/hecks/projector/docs_projector.rb +21 -12
  188. data/lib/hecks/projector/exporter.rb +10 -1
  189. data/lib/hecks/projector/narrate_projector.rb +83 -25
  190. data/lib/hecks/projector.rb +2 -2
  191. data/lib/hecks/query_ir.rb +60 -36
  192. data/lib/hecks/query_specification/common/authorization_spec.rb +1 -1
  193. data/lib/hecks/query_specification/common/comparators.rb +4 -0
  194. data/lib/hecks/query_specification/common/comparison.rb +8 -1
  195. data/lib/hecks/query_specification/common/dsl.rb +5 -0
  196. data/lib/hecks/query_specification/common/null_policy.rb +6 -2
  197. data/lib/hecks/query_specification/common/options.rb +17 -5
  198. data/lib/hecks/query_specification/field_path.rb +4 -4
  199. data/lib/hecks/query_specification/read_model/specification.rb +5 -2
  200. data/lib/hecks/router/namespace_installer.rb +22 -10
  201. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +57 -22
  202. data/lib/hecks/runtime/command_interpreter.rb +104 -65
  203. data/lib/hecks/runtime/command_rules/admissibility.rb +26 -8
  204. data/lib/hecks/runtime/command_rules/arithmetic.rb +35 -14
  205. data/lib/hecks/runtime/dependency_planning.rb +33 -21
  206. data/lib/hecks/runtime/dispatcher.rb +41 -7
  207. data/lib/hecks/runtime/entity_element.rb +25 -6
  208. data/lib/hecks/runtime/entity_interpreter.rb +28 -15
  209. data/lib/hecks/runtime/errors.rb +28 -2
  210. data/lib/hecks/runtime/instance.rb +9 -3
  211. data/lib/hecks/runtime/interpreting.rb +29 -2
  212. data/lib/hecks/runtime/loader.rb +21 -0
  213. data/lib/hecks/runtime/outbox.rb +365 -0
  214. data/lib/hecks/runtime/policy_interpreter.rb +32 -17
  215. data/lib/hecks/runtime/query_interpreter.rb +36 -16
  216. data/lib/hecks/runtime/reaction_invocation.rb +35 -22
  217. data/lib/hecks/runtime/read_model_interpreter.rb +26 -3
  218. data/lib/hecks/runtime/reference_hop.rb +1 -1
  219. data/lib/hecks/runtime/refusal_wording.rb +55 -49
  220. data/lib/hecks/runtime/registry/saga_persistence.rb +3 -1
  221. data/lib/hecks/runtime/registry/verification.rb +39 -2
  222. data/lib/hecks/runtime/registry.rb +21 -11
  223. data/lib/hecks/runtime/remote_dispatcher.rb +3 -1
  224. data/lib/hecks/runtime/routing.rb +52 -39
  225. data/lib/hecks/runtime/saga_interpreter/correlation.rb +7 -7
  226. data/lib/hecks/runtime/saga_interpreter.rb +119 -90
  227. data/lib/hecks/runtime/saga_pending_dispatch.rb +6 -3
  228. data/lib/hecks/runtime/tenant_scope.rb +7 -0
  229. data/lib/hecks/runtime/value/coercion.rb +207 -48
  230. data/lib/hecks/runtime/value.rb +1 -1
  231. data/lib/hecks/runtime.rb +7 -2
  232. data/lib/hecks/storehouse.rb +15 -8
  233. data/lib/hecks/version.rb +1 -1
  234. data/lib/hecks/vocabulary.rb +1 -0
  235. data/lib/hecks.rb +17 -13
  236. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +1 -1
  237. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +1 -1
  238. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +1 -1
  239. metadata +14 -2
@@ -1,5 +1,3 @@
1
- require "set"
2
-
3
1
  module Hecks
4
2
  module Bluebook
5
3
  # Lightweight formal methods over the IR — the same family as TLA+/
@@ -89,7 +87,7 @@ module Hecks
89
87
  findings.concat(lifecycle_findings(aggregate, aggregate))
90
88
  aggregate.entities.each { |entity| findings.concat(lifecycle_findings(aggregate, entity)) }
91
89
  end
92
- bluebook.process_managers.each { |pm| findings.concat(saga_findings(bluebook, pm)) }
90
+ bluebook.process_managers.each { |process_manager| findings.concat(saga_findings(bluebook, process_manager)) }
93
91
  bluebook.policies.each { |policy| findings.concat(policy_findings(bluebook, policy, hecksagon, known_domains)) }
94
92
  findings
95
93
  end
@@ -103,38 +101,46 @@ module Hecks
103
101
  subject = declaring.equal?(aggregate) ? aggregate.hecks_name : "#{aggregate.hecks_name}::#{declaring.hecks_name}"
104
102
  commands = Array(declaring.commands).map(&:hecks_name)
105
103
 
106
- findings = []
107
- findings.concat(unknown_transition_commands(lifecycle, commands, subject))
108
-
109
104
  full = full_states(lifecycle)
110
105
  reached = reachable_states(lifecycle)
111
106
 
112
- (full - reached.to_a).each do |state|
113
- findings << Finding.new(kind: :unreachable_state, severity: :error, subject: subject,
114
- message: "#{state.inspect} is declared (in a transition's from: or target) " \
115
- "but no path from #{lifecycle.default.inspect} ever reaches it")
107
+ findings = []
108
+ findings.concat(unknown_transition_commands(lifecycle, commands, subject))
109
+ findings.concat(unreachable_state_findings(lifecycle, full, reached, subject))
110
+ findings.concat(dead_transition_findings(lifecycle, reached, subject))
111
+ findings.concat(stuck_state_findings(lifecycle, reached, subject))
112
+ findings
113
+ end
114
+
115
+ def unreachable_state_findings(lifecycle, full, reached, subject)
116
+ (full - reached.to_a).map do |state|
117
+ Finding.new(kind: :unreachable_state, severity: :error, subject: subject,
118
+ message: "#{state.inspect} is declared (in a transition's from: or target) " \
119
+ "but no path from #{lifecycle.default.inspect} ever reaches it")
116
120
  end
121
+ end
117
122
 
118
- lifecycle.transitions.each do |command, transition|
123
+ def dead_transition_findings(lifecycle, reached, subject)
124
+ lifecycle.transitions.filter_map do |command, transition|
119
125
  next unless transition.constrained?
120
126
  next if Array(transition.from).any? { |source| reached.include?(source) }
121
127
 
122
- findings << Finding.new(kind: :dead_transition, severity: :error, subject: subject,
123
- message: "#{command} from #{Array(transition.from).inspect} can never fire — " \
124
- "none of those states is ever reached")
128
+ Finding.new(kind: :dead_transition, severity: :error, subject: subject,
129
+ message: "#{command} from #{Array(transition.from).inspect} can never fire — " \
130
+ "none of those states is ever reached")
125
131
  end
132
+ end
126
133
 
134
+ def stuck_state_findings(lifecycle, reached, subject)
127
135
  any_unconstrained = lifecycle.transitions.any? { |_, t| !t.constrained? }
128
- (reached - terminal_exempt(lifecycle)).each do |state|
136
+ (reached - terminal_exempt(lifecycle)).filter_map do |state|
129
137
  next if any_unconstrained
130
138
  next if lifecycle.transitions.any? { |_, t| t.constrained? && Array(t.from).include?(state) }
131
139
 
132
- findings << Finding.new(kind: :stuck_state, severity: :warning, subject: subject,
133
- message: "#{state.inspect} is reached but no transition ever leaves it — " \
134
- "fine if that is meant to be terminal")
140
+ Finding.new(kind: :stuck_state, severity: :warning, subject: subject,
141
+ message: "#{state.inspect} is reached but no transition ever leaves it — " \
142
+ "fine if that is meant to be terminal")
135
143
  end
136
-
137
- findings
138
144
  end
139
145
 
140
146
  def unknown_transition_commands(lifecycle, commands, subject)
@@ -165,6 +171,11 @@ module Hecks
165
171
  reached = Set.new([lifecycle.default])
166
172
  loop do
167
173
  grown = false
174
+ # `transitions` is an Array of [from, transition]-shaped entries
175
+ # (Lifecycle#transitions), not a Hash — Style/HashEachMethods'
176
+ # `each_value` rewrite assumed otherwise from the block shape
177
+ # alone. False positive.
178
+ # rubocop:disable-next Style/HashEachMethods
168
179
  lifecycle.transitions.each do |_, transition|
169
180
  next if reached.include?(transition.target)
170
181
  next if transition.constrained? && Array(transition.from).none? { |source| reached.include?(source) }
@@ -202,79 +213,105 @@ module Hecks
202
213
 
203
214
  # ── process managers / sagas ──────────────────────────────────────
204
215
 
205
- def saga_findings(bluebook, pm)
216
+ def saga_findings(bluebook, process_manager)
217
+ emitted = emitted_events(bluebook)
218
+ verbs = verbs_of(bluebook)
219
+ reached = pm_reachable_states(process_manager, emitted)
220
+
206
221
  findings = []
207
- emitted = emitted_events(bluebook)
208
- verbs = verbs_of(bluebook)
222
+ findings.concat(deaf_trigger_findings(process_manager, emitted))
223
+ findings.concat(unreachable_pm_state_findings(process_manager, reached))
224
+ process_manager.handlers.each do |handler|
225
+ findings.concat(handler_findings(bluebook, process_manager, emitted, verbs, handler))
226
+ end
227
+ findings.concat(dead_compensation_findings(process_manager, reached))
228
+ findings
229
+ end
209
230
 
210
- [pm.starts_on, pm.ends_on].compact.each do |event|
231
+ def deaf_trigger_findings(process_manager, emitted)
232
+ [process_manager.starts_on, process_manager.ends_on].compact.filter_map do |event|
211
233
  next if emitted.include?(bare(event))
212
234
 
213
- findings << Finding.new(kind: :deaf_trigger, severity: :error, subject: pm.name,
214
- message: "starts_on/ends_on names #{event.inspect}, which no command in this " \
215
- "domain emits")
235
+ Finding.new(kind: :deaf_trigger, severity: :error, subject: process_manager.name,
236
+ message: "starts_on/ends_on names #{event.inspect}, which no command in this " \
237
+ "domain emits")
216
238
  end
239
+ end
217
240
 
218
- reached = pm_reachable_states(pm, emitted)
219
- (Array(pm.states) - reached.to_a).each do |state|
220
- findings << Finding.new(kind: :unreachable_pm_state, severity: :error, subject: pm.name,
221
- message: "#{state.inspect} is declared but no handler chain from " \
222
- "#{pm.states.first.inspect} ever reaches it")
241
+ def unreachable_pm_state_findings(process_manager, reached)
242
+ (Array(process_manager.states) - reached.to_a).map do |state|
243
+ Finding.new(kind: :unreachable_pm_state, severity: :error, subject: process_manager.name,
244
+ message: "#{state.inspect} is declared but no handler chain from " \
245
+ "#{process_manager.states.first.inspect} ever reaches it")
223
246
  end
247
+ end
224
248
 
225
- pm.handlers.each do |handler|
226
- # The compensating leg answers REFUSED, a synthetic trigger no
227
- # command ever emits by name (ProcessManager::REFUSED) not
228
- # a deaf handler, the one handler this domain's own events can
229
- # never satisfy on purpose.
230
- if handler.event_type != ProcessManager::REFUSED && !emitted.include?(bare(handler.event_type))
231
- findings << Finding.new(kind: :deaf_handler, severity: :error, subject: pm.name,
232
- message: "a handler answers #{handler.event_type.inspect}, which no command " \
233
- "in this domain emits")
234
- end
249
+ # ONE HANDLER'S OWN FINDINGS — deaf_handler, unknown_dispatch (one
250
+ # per dispatch), and unarmed_compensation (one per compensating
251
+ # dispatch), pulled out of saga_findings' own handler loop; each
252
+ # check reads only this handler plus the domain-wide emitted/verbs
253
+ # sets saga_findings already resolved once, no state shared BETWEEN
254
+ # handlers.
255
+ def handler_findings(bluebook, process_manager, emitted, verbs, handler)
256
+ findings = []
235
257
 
236
- handler.dispatches.each do |dispatch|
237
- # SAME-DOMAIN, same as `SagaInterpreter#qualified`a dispatch
238
- # naming no domain at all (the ordinary shape a bare command
239
- # constant now produces, S6) means THIS one, and is compared
240
- # against `verbs_of`'s own fully-qualified spelling qualified
241
- # the identical way, not left bare to miss it on a technicality.
242
- qualified = dispatch.command_name.include?("::") ? dispatch.command_name : "#{bluebook.name}::#{dispatch.command_name}"
243
- next if verbs.include?(qualified)
244
-
245
- findings << Finding.new(kind: :unknown_dispatch, severity: :error, subject: pm.name,
246
- message: "dispatches #{dispatch.command_name.inspect}, which this domain " \
247
- "declares no command at — cross-domain dispatch is out of this " \
248
- "checker's scope, same as CommandRules#resolve_references")
249
- end
258
+ # The compensating leg answers REFUSED, a synthetic trigger no
259
+ # command ever emits by name (ProcessManager::REFUSED) not
260
+ # a deaf handler, the one handler this domain's own events can
261
+ # never satisfy on purpose.
262
+ if handler.event_type != ProcessManager::REFUSED && !emitted.include?(bare(handler.event_type))
263
+ findings << Finding.new(kind: :deaf_handler, severity: :error, subject: process_manager.name,
264
+ message: "a handler answers #{handler.event_type.inspect}, which no command " \
265
+ "in this domain emits")
266
+ end
250
267
 
251
- # A `compensates` DECLARED WITH NOWHERE TO EVER FIRE — the exact
252
- # shape of the real bug this whole feature closes ("the
253
- # reversal was written and never armed"), caught at build/
254
- # model-check time instead of discovered in production. No
255
- # handler anywhere answers REFUSED (`pm.saga?` false) means
256
- # `SagaInterpreter#unwind` never runs for this process
257
- # manager at all, so a declared `compensates` is structurally
258
- # unreachable — not a warning about style, a dead declaration.
259
- if !pm.saga? && handler.dispatches.any?(&:compensates)
260
- handler.dispatches.select(&:compensates).each do |dispatch|
261
- findings << Finding.new(kind: :unarmed_compensation, severity: :error, subject: pm.name,
262
- message: "#{dispatch.command_name} compensates #{dispatch.compensates.command_name}, " \
263
- "but no handler anywhere in this saga answers a refusal — the " \
264
- "compensation is declared and can never fire")
265
- end
266
- end
268
+ handler.dispatches.each do |dispatch|
269
+ # SAME-DOMAIN, same as `SagaInterpreter#qualified` a dispatch
270
+ # naming no domain at all (the ordinary shape a bare command
271
+ # constant now produces, S6) means THIS one, and is compared
272
+ # against `verbs_of`'s own fully-qualified spelling qualified
273
+ # the identical way, not left bare to miss it on a technicality.
274
+ qualified = if dispatch.command_name.include?("::")
275
+ dispatch.command_name
276
+ else
277
+ "#{bluebook.name}::#{dispatch.command_name}"
278
+ end
279
+ next if verbs.include?(qualified)
280
+
281
+ findings << Finding.new(kind: :unknown_dispatch, severity: :error, subject: process_manager.name,
282
+ message: "dispatches #{dispatch.command_name.inspect}, which this domain " \
283
+ "declares no command at — cross-domain dispatch is out of this " \
284
+ "checker's scope, same as CommandRules#resolve_references")
267
285
  end
268
286
 
269
- if pm.saga? && !reached.include?(pm.saga.from_state)
270
- findings << Finding.new(kind: :dead_compensation, severity: :error, subject: pm.name,
271
- message: "the compensation leaves #{pm.saga.from_state.inspect}, which no " \
272
- "handler chain ever reaches a refusal here can never fire it")
287
+ # A `compensates` DECLARED WITH NOWHERE TO EVER FIRE — the exact
288
+ # shape of the real bug this whole feature closes ("the
289
+ # reversal was written and never armed"), caught at build/
290
+ # model-check time instead of discovered in production. No
291
+ # handler anywhere answers REFUSED (`process_manager.saga?` false) means
292
+ # `SagaInterpreter#unwind` never runs for this process
293
+ # manager at all, so a declared `compensates` is structurally
294
+ # unreachable — not a warning about style, a dead declaration.
295
+ if !process_manager.saga? && handler.dispatches.any?(&:compensates)
296
+ handler.dispatches.select(&:compensates).each do |dispatch|
297
+ findings << Finding.new(kind: :unarmed_compensation, severity: :error, subject: process_manager.name,
298
+ message: "#{dispatch.command_name} compensates #{dispatch.compensates.command_name}, " \
299
+ "but no handler anywhere in this saga answers a refusal — the " \
300
+ "compensation is declared and can never fire")
301
+ end
273
302
  end
274
303
 
275
304
  findings
276
305
  end
277
306
 
307
+ def dead_compensation_findings(process_manager, reached)
308
+ return [] unless process_manager.saga? && !reached.include?(process_manager.saga.from_state)
309
+
310
+ [Finding.new(kind: :dead_compensation, severity: :error, subject: process_manager.name,
311
+ message: "the compensation leaves #{process_manager.saga.from_state.inspect}, which no " \
312
+ "handler chain ever reaches — a refusal here can never fire it")]
313
+ end
314
+
278
315
  # A handler edge is only usable in the closure if it can actually
279
316
  # FIRE — REFUSED always can (it is a compensation trigger, not an
280
317
  # event), and any other handler needs its event genuinely emitted.
@@ -283,13 +320,13 @@ module Hecks
283
320
  # it, which would hide exactly the states this walk exists to
284
321
  # catch (a state only "reachable" through a handler that itself
285
322
  # never fires).
286
- def pm_reachable_states(pm, emitted)
287
- return Set.new if Array(pm.states).empty?
323
+ def pm_reachable_states(process_manager, emitted)
324
+ return Set.new if Array(process_manager.states).empty?
288
325
 
289
- reached = Set.new([pm.states.first])
326
+ reached = Set.new([process_manager.states.first])
290
327
  loop do
291
328
  grown = false
292
- pm.handlers.each do |handler|
329
+ process_manager.handlers.each do |handler|
293
330
  next unless handler.event_type == ProcessManager::REFUSED || emitted.include?(bare(handler.event_type))
294
331
  next unless reached.include?(handler.from_state)
295
332
  next if reached.include?(handler.to_state)
@@ -446,9 +483,9 @@ module Hecks
446
483
  bluebook.aggregates.flat_map do |aggregate|
447
484
  verbs = aggregate.commands.map { |command| "#{bluebook.name}::#{aggregate.hecks_name}.#{command.hecks_name}" }
448
485
  verbs + aggregate.entities.flat_map do |entity|
449
- entity.commands.map { |command|
486
+ entity.commands.map do |command|
450
487
  "#{bluebook.name}::#{aggregate.hecks_name}.#{entity.hecks_name}.#{command.hecks_name}"
451
- }
488
+ end
452
489
  end
453
490
  end
454
491
  end
@@ -63,6 +63,15 @@ module Hecks
63
63
  # characters "(?=" and says nothing about lookahead — which is why this
64
64
  # steps over each backslash pair rather than matching the pattern as a
65
65
  # whole.
66
+ # One character walk is the subset's DEFINITION (see the method
67
+ # comment above): each construct-check is a branch in a single
68
+ # ordered pass sharing `index`/`in_class`/`class_start`. Splitting
69
+ # the branches into separate methods would force those three cursor
70
+ # variables to thread as parameters/return values between them,
71
+ # turning "the subset is this walk" into "the subset is these
72
+ # methods agreeing about a shared cursor" — exactly what the walk's
73
+ # own comment says a cleverer spelling would obscure.
74
+ # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
66
75
  def validate(pattern)
67
76
  chars = pattern.to_s.chars
68
77
  index = 0
@@ -16,7 +16,8 @@ module Hecks
16
16
  target_domain: :target_domain,
17
17
  where: :where,
18
18
  for_each: :for_each,
19
- with_spec: -> { with_spec.map { |key, value| [key.to_s, Bluebook.render_value(value)] } }
19
+ with_spec: -> { with_spec.map { |key, value| [key.to_s, Bluebook.render_value(value)] } },
20
+ where_ast: -> { where_ast }
20
21
  )
21
22
 
22
23
  attr_reader :name, :on_event, :trigger_command, :target_domain, :where, :for_each, :with_spec
@@ -74,6 +74,12 @@ module Hecks
74
74
  def to_s = "#{trigger} → #{to_state} (#{undoes.join(', ')})"
75
75
  end
76
76
 
77
+ # The built form of a `process_manager "Name" do ... end` block,
78
+ # produced by `DSL::ProcessManagerBuilder` — its start/end events,
79
+ # correlation field, derived states, and handler rows. Its
80
+ # compensation half (`saga`, `Behaviour::ProcessManager#saga`) is
81
+ # DERIVED from the handler answering `REFUSED`, below, not declared as
82
+ # its own construct.
77
83
  class ProcessManager
78
84
  # The BLUEBOOK's name for this construct, asked the same way of a class
79
85
  # that has crossed over and of an IR object that has not. Collapses into
@@ -35,34 +35,56 @@ module Hecks
35
35
  "#{bluebook.name} in #{directory} declares latest #{world.latest.inspect}, not #{bluebook.version.inspect}"
36
36
  end
37
37
 
38
- bluebook.aggregates.each do |aggregate|
39
- aggregate.commands.each do |command|
40
- add(Fqn.command(realm: realm, domain: bluebook.name, version: bluebook.version,
41
- aggregate: aggregate.hecks_name, command: command.hecks_name), directory, dispatcher, command.hecks_name, bluebook.version) if bluebook.version
42
- add(Fqn.command(realm: realm, domain: bluebook.name, aggregate: aggregate.hecks_name,
43
- command: command.hecks_name), directory, dispatcher, command.hecks_name, bluebook.version) if current?(
44
- bluebook, world
45
- )
46
- end
47
- aggregate.queries.each do |query|
48
- name = Naming.snake(query.name)
49
- add(Fqn.query(realm: realm, domain: bluebook.name, version: bluebook.version,
50
- aggregate: aggregate.hecks_name, query: name), directory, dispatcher, query.name, bluebook.version) if bluebook.version
51
- add(Fqn.query(realm: realm, domain: bluebook.name, aggregate: aggregate.hecks_name,
52
- query: name), directory, dispatcher, query.name, bluebook.version) if current?(bluebook, world)
53
- end
38
+ bluebook.aggregates.each { |aggregate| register_aggregate!(aggregate, bluebook, world, realm, directory, dispatcher) }
39
+ bluebook.read_models.each { |model| register_read_model!(model, bluebook, world, realm, directory, dispatcher) }
40
+ end
41
+ self
42
+ end
43
+
44
+ private
45
+
46
+ # Split out of `register` per-aggregate: same versioned-then-current
47
+ # registration shape as `register_read_model!`, just applied to a
48
+ # command/query pair instead of a single report. No state threads
49
+ # back to `register` each `add` call is independent.
50
+ def register_aggregate!(aggregate, bluebook, world, realm, directory, dispatcher)
51
+ aggregate.commands.each do |command|
52
+ if bluebook.version
53
+ add(Fqn.command(realm: realm, domain: bluebook.name, version: bluebook.version,
54
+ aggregate: aggregate.hecks_name, command: command.hecks_name),
55
+ directory, dispatcher, command.hecks_name, bluebook.version)
54
56
  end
55
- bluebook.read_models.each do |model|
57
+ if current?(bluebook, world)
58
+ add(Fqn.command(realm: realm, domain: bluebook.name, aggregate: aggregate.hecks_name,
59
+ command: command.hecks_name), directory, dispatcher, command.hecks_name, bluebook.version)
60
+ end
61
+ end
62
+ aggregate.queries.each do |query|
63
+ name = Naming.snake(query.name)
64
+ if bluebook.version
56
65
  add(Fqn.query(realm: realm, domain: bluebook.name, version: bluebook.version,
57
- query: model.query_name), directory, dispatcher, model.query_name, bluebook.version) if bluebook.version
58
- add(Fqn.query(realm: realm, domain: bluebook.name, query: model.query_name),
59
- directory, dispatcher, model.query_name, bluebook.version) if current?(bluebook, world)
66
+ aggregate: aggregate.hecks_name, query: name), directory, dispatcher, query.name, bluebook.version)
67
+ end
68
+ if current?(bluebook, world)
69
+ add(Fqn.query(realm: realm, domain: bluebook.name, aggregate: aggregate.hecks_name,
70
+ query: name), directory, dispatcher, query.name, bluebook.version)
60
71
  end
61
72
  end
62
- self
63
73
  end
64
74
 
65
- private
75
+ # Split out of `register` per-read-model: registers the versioned FQN
76
+ # (if this bluebook declares a version) and the current/unversioned
77
+ # FQN (if this bluebook is the world's latest) for one report.
78
+ def register_read_model!(model, bluebook, world, realm, directory, dispatcher)
79
+ if bluebook.version
80
+ add(Fqn.query(realm: realm, domain: bluebook.name, version: bluebook.version,
81
+ query: model.query_name), directory, dispatcher, model.query_name, bluebook.version)
82
+ end
83
+ if current?(bluebook, world)
84
+ add(Fqn.query(realm: realm, domain: bluebook.name, query: model.query_name),
85
+ directory, dispatcher, model.query_name, bluebook.version)
86
+ end
87
+ end
66
88
 
67
89
  # THE ACTUAL "more than one tenant" MOMENT — Runtime::TenantCheck's
68
90
  # own header names this table as the one place real multitenancy
@@ -93,9 +115,7 @@ module Hecks
93
115
 
94
116
  def add(fqn, directory, dispatcher, declared_verb, domain_version)
95
117
  existing = entries[fqn.to_s]
96
- if existing
97
- raise DuplicateFqn, "#{fqn} is declared in both #{existing.source_directory} and #{directory}"
98
- end
118
+ raise DuplicateFqn, "#{fqn} is declared in both #{existing.source_directory} and #{directory}" if existing
99
119
 
100
120
  entries[fqn.to_s] = Entry.new(
101
121
  fqn: fqn, source_directory: directory, dispatcher: dispatcher,
@@ -1,6 +1,10 @@
1
1
  require_relative "behaviour/query"
2
2
 
3
3
  module Hecks
4
+ # Reopened here only to add `render_value` below — the pinned
5
+ # wire-literal rendering (`Literal.render`) the `emits_ir` Procs
6
+ # throughout this chapter reach for. See lib/hecks/bluebook.rb's own
7
+ # header for what `Bluebook` is as a whole.
4
8
  module Bluebook
5
9
  def self.render_value(value) = Literal.render(value)
6
10
 
@@ -35,8 +35,8 @@ module Hecks
35
35
  # `reference_target.nil?` stays a real, checkable fact for the
36
36
  # interpreter instead of silently becoming `""`.
37
37
  def initialize(name:, description: nil, reference_name: nil, reference_target: nil, aggregate_heads: [],
38
- group_by: [], count: nil, median_field: nil, **options)
39
- super(joins: aggregate_heads, **options)
38
+ group_by: [], count: nil, median_field: nil, **)
39
+ super(joins: aggregate_heads, **)
40
40
  @name = name.to_s
41
41
  @hecks_name = @name
42
42
  @description = description
@@ -118,6 +118,11 @@ module Hecks
118
118
  # the time that aggregate's own turn comes, the same reason
119
119
  # `Synthesizer#args_for`'s `created` map is threaded through in
120
120
  # this same order.
121
+ # `created` and `failures` thread through both the aggregate loop and the
122
+ # read_models loop below in the declaration order the comment above already
123
+ # documents; splitting would turn that ordering into an implicit contract on
124
+ # parameter/return passing instead of one visible sequence.
125
+ # rubocop:disable-next Metrics/AbcSize
121
126
  def smoke_domain(dispatcher, domain)
122
127
  chapter = dispatcher.registry.bluebook(domain)
123
128
  return [] unless chapter
@@ -64,7 +64,8 @@ module Hecks
64
64
  chapter.aggregates.filter_map { |a| a.value_object(type_name) }.first
65
65
  return "smoke-test" unless value_object
66
66
 
67
- return value_object.members.first.to_h if value_object.respond_to?(:closed_set?) && value_object.closed_set? && value_object.members.any?
67
+ closed_set = value_object.respond_to?(:closed_set?) && value_object.closed_set? && value_object.members.any?
68
+ return value_object.members.first.to_h if closed_set
68
69
 
69
70
  value_object.attributes.to_h { |field| [field.name, field_value_for(chapter, aggregate, field.type)] }
70
71
  end
@@ -11,6 +11,9 @@ module Hecks
11
11
  # Paths follow `TranslationMove`'s convention (dotted reaches a VO
12
12
  # member); `values` maps every old-era value that can appear to its
13
13
  # new-era replacement.
14
+ # `:values` shadows Struct#values on purpose (same reasoning as
15
+ # AttributeCollector::OneOf) — no caller reads the built-in behavior.
16
+ # rubocop:disable-next Lint/StructNewOverride
14
17
  TranslationConvert = Struct.new(:from, :to, :values)
15
18
 
16
19
  # A value object's or entity's own TYPE NAME changed with its member
@@ -3,7 +3,7 @@ require_relative "expression/ast_json"
3
3
 
4
4
  module Hecks
5
5
  module Bluebook
6
- Invariant = Struct.new(:description, :canonical, :predicate, keyword_init: true)
6
+ Invariant = Struct.new(:description, :canonical, :predicate, :ast, keyword_init: true)
7
7
 
8
8
  # A value object — a DECLARATION HOLDER, never instantiated.
9
9
  #
@@ -37,7 +37,7 @@ module Hecks
37
37
  # `Expression::AstJson`'s own header — built for `rust/host`'s
38
38
  # own mint-time invariant check (`reference_validate.rs`), which
39
39
  # has no kernel crate to parse `canonical` with.
40
- invariants: -> { invariants.map { |rule| { description: rule.description, canonical: rule.canonical, ast: Expression::AstJson.emit_predicate(rule.canonical) } } },
40
+ invariants: -> { invariants.map { |rule| Expression::AstJson.rule_row(rule) } },
41
41
  closed_set: :closed_set?,
42
42
  # THE FIELD NAME IS STRINGIFIED, NEVER THE VALUE. A `member` row can
43
43
  # hold any of the scalar types an attribute declares — `Integer 84`
data/lib/hecks/codemod.rb CHANGED
@@ -221,7 +221,11 @@ module Hecks
221
221
  # loop for why it can't batch) — splitting it across methods just
222
222
  # to satisfy the line count would scatter state four ways for no
223
223
  # readability gain.
224
+ # rubocop:disable-next Metrics/AbcSize
224
225
  # rubocop:disable-next Metrics/BlockLength
226
+ # rubocop:disable-next Metrics/CyclomaticComplexity
227
+ # rubocop:disable-next Metrics/MethodLength
228
+ # rubocop:disable-next Metrics/PerceivedComplexity
225
229
  def run_example_domains(results, dry_run)
226
230
  Codemod::EXAMPLE_ROOTS.each do |domain_dir|
227
231
  bluebook_files = Dir.glob(File.join(domain_dir, "bluebook", "*.bluebook"))
@@ -286,6 +290,15 @@ module Hecks
286
290
  # changes the IR, so the running state's own export should always
287
291
  # still equal the pristine original after each kept edit, by
288
292
  # definition, no moving target needed.
293
+ # Same shape and same reason as run_example_domains just above (see
294
+ # its own comment) — the write/verify/revert sequence, PER
295
+ # CANDIDATE (this method's own comment explains why it cannot
296
+ # batch), is one coherent unit; splitting it would scatter
297
+ # before_meta/live_meta/applied_by_file state across methods for
298
+ # no gain.
299
+ # rubocop:disable-next Metrics/AbcSize
300
+ # rubocop:disable-next Metrics/CyclomaticComplexity
301
+ # rubocop:disable-next Metrics/PerceivedComplexity
289
302
  def run_meta_domain(results, dry_run)
290
303
  before_meta = Codemod.boot_meta
291
304
  meta_candidates = @find_candidates.call(Codemod.meta_registry)
@@ -114,7 +114,7 @@ module Hecks
114
114
  by `bin/reference` — do not edit inside the markers. The prose
115
115
  between them is hand-written and survives regeneration.*
116
116
  #{GENERATED_END}
117
- #{preamble.empty? ? '' : "\n#{preamble}\n"}
117
+ #{"\n#{preamble}\n" unless preamble.empty?}
118
118
  #{sections.join("\n")}
119
119
  PAGE
120
120
  end
@@ -145,7 +145,7 @@ module Hecks
145
145
  ## #{row[:word]}
146
146
 
147
147
  #{generated_begin(row[:word])}
148
- #{spellings}#{facts.empty? ? '' : " — #{facts.join(', ')}"}
148
+ #{spellings}#{" — #{facts.join(', ')}" unless facts.empty?}
149
149
  #{table}#{GENERATED_END}
150
150
 
151
151
  #{prose_or_sentinel(prose)}
@@ -181,7 +181,7 @@ module Hecks
181
181
  name = arg[:at].to_s.empty? ? "`#{arg[:named]}:`" : "positional #{arg[:at]}"
182
182
  lines << "| #{name} | #{arg[:kind]} | #{arg[:required]} | #{arg[:fills]} |"
183
183
  end
184
- lines.join("\n") + "\n"
184
+ "#{lines.join("\n")}\n"
185
185
  end
186
186
 
187
187
  def render_index
@@ -210,6 +210,11 @@ module Hecks
210
210
  # region existed has no generated marker ahead of its first `## `,
211
211
  # so nothing is collecting when that heading arrives and no empty
212
212
  # preamble is invented — the older shape reads back unchanged.
213
+ # A single-pass line-scanning state machine (current/collecting/
214
+ # buffer/in_fence) — each branch mutates shared local state that
215
+ # carries into the NEXT iteration, so splitting per branch would
216
+ # mean passing all four back and forth by reference every line.
217
+ # rubocop:disable-next Metrics/PerceivedComplexity
213
218
  def harvest(text)
214
219
  prose = {}
215
220
  current = PREAMBLE
@@ -263,7 +268,7 @@ module Hecks
263
268
  end
264
269
 
265
270
  def guide_index(root)
266
- paths = Dir.glob(File.join(root, "docs/implemented/guides/*.md")).sort
271
+ paths = Dir.glob(File.join(root, "docs/implemented/guides/*.md"))
267
272
  .reject { |p| %w[AUTHORING.md].include?(File.basename(p)) }
268
273
  lines = paths.map do |path|
269
274
  heading = File.foreach(path).find { |line| line.start_with?("# ") }
@@ -273,7 +278,7 @@ module Hecks
273
278
  lines.join("\n")
274
279
  end
275
280
 
276
- def reference_index(root)
281
+ def reference_index(_root)
277
282
  count = contexts.size
278
283
  "[The DSL reference](docs/implemented/reference/index.md) — #{count} contexts, generated from " \
279
284
  "the aggregate-local tables under `lib/hecks/language/` and held to them by " \
@@ -333,11 +338,11 @@ module Hecks
333
338
  end
334
339
 
335
340
  def corpus_roster(root)
336
- dirs = Dir.glob(File.join(root, "examples/*/")).sort
341
+ dirs = Dir.glob(File.join(root, "examples/*/"))
337
342
  lines = dirs.filter_map do |dir|
338
343
  name = File.basename(dir.chomp("/"))
339
- bluebooks = Dir.glob(File.join(dir, "bluebook/*.bluebook")).sort
340
- bluebooks = Dir.glob(File.join(dir, "*.bluebook")).sort if bluebooks.empty?
344
+ bluebooks = Dir.glob(File.join(dir, "bluebook/*.bluebook"))
345
+ bluebooks = Dir.glob(File.join(dir, "*.bluebook")) if bluebooks.empty?
341
346
  next if bluebooks.empty?
342
347
 
343
348
  vision = bluebooks.filter_map { |bluebook| File.read(bluebook)[/vision\s+"([^"]*)"/, 1] }.first
@@ -364,7 +369,7 @@ module Hecks
364
369
  # example — a word whose only "example" is invisible or inert is a
365
370
  # word still shipping on its prose alone, which is the thing this
366
371
  # gate exists to refuse.
367
- EXAMPLE_FENCE = /^```ruby(?: bluebook| boot)?[ \t]*$/.freeze
372
+ EXAMPLE_FENCE = /^```ruby(?: bluebook| boot)?[ \t]*$/
368
373
 
369
374
  def exemplified?(prose) = prose.to_s.match?(EXAMPLE_FENCE)
370
375
 
@@ -378,12 +383,13 @@ module Hecks
378
383
  # `harvest` already rejects empty prose and the TODO sentinel, so a
379
384
  # word with nothing written for it arrives here with a nil.
380
385
  def live_words(directory)
381
- contexts.flat_map { |context|
386
+ rows = contexts.flat_map do |context|
382
387
  path = File.join(directory, page_name(context))
383
388
  prose = File.exist?(path) ? harvest(File.read(path)) : {}
384
389
  keywords.select { |row| row[:context] == context && live?(row) }
385
390
  .map { |row| [row[:word], context, prose[row[:word]]] }
386
- }.uniq { |word, context, _| [word, context] }
391
+ end
392
+ rows.uniq { |word, context, _| [word, context] }
387
393
  end
388
394
 
389
395
  def name_of(word, context) = "#{word} (#{context})"