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
@@ -79,7 +79,7 @@ module Hecks
79
79
  rescue Runtime::NotFound, Runtime::TypeMismatch => e
80
80
  # A BAD ARGUMENT AND A MISSING RECORD BOTH LAND HERE, and both want the
81
81
  # same next step: read what the verb actually takes.
82
- ["#{e.message}\n\n #{program} #{asking ? 'ask ' : ''}#{name} --help", 1]
82
+ ["#{e.message}\n\n #{program} #{'ask ' if asking}#{name} --help", 1]
83
83
  rescue *Runtime::DOMAIN_REFUSALS => e
84
84
  # THE REFUSAL IS THE PRODUCT — the chapter's own sentence, verbatim.
85
85
  [e.message, 1]
@@ -177,7 +177,7 @@ module Hecks
177
177
 
178
178
  lines = ["no such #{asking ? 'question' : 'verb'}: #{name}"]
179
179
  lines += ["", "did you mean:", *near.first(5).map { |candidate| " #{candidate}" }] unless near.empty?
180
- lines += ["", " #{program}#{asking ? ' ask' : ''} for the full list"]
180
+ lines += ["", " #{program}#{' ask' if asking} for the full list"]
181
181
  lines.join("\n")
182
182
  end
183
183
 
@@ -18,9 +18,7 @@ module Hecks
18
18
 
19
19
  def normalize(input, receiver:, legacy_receiver: nil)
20
20
  request = symbolize(input)
21
- unless request.is_a?(Hash)
22
- raise Runtime::TypeMismatch, "a command request must be a hash"
23
- end
21
+ raise Runtime::TypeMismatch, "a command request must be a hash" unless request.is_a?(Hash)
24
22
 
25
23
  route, facts = split(request, receiver: receiver, legacy_receiver: legacy_receiver)
26
24
  validate_route!(route, receiver)
@@ -54,9 +52,7 @@ module Hecks
54
52
  def take_legacy_route(flat, receiver, legacy_receiver)
55
53
  return unless legacy_receiver
56
54
 
57
- if receiver == :aggregate
58
- return flat.delete(legacy_receiver.to_sym)
59
- end
55
+ return flat.delete(legacy_receiver.to_sym) if receiver == :aggregate
60
56
 
61
57
  return unless receiver == :entity && legacy_receiver.is_a?(Hash)
62
58
 
@@ -68,23 +64,27 @@ module Hecks
68
64
  end
69
65
  private_class_method :take_legacy_route
70
66
 
67
+ # A case dispatch over the three closed receiver kinds (nil,
68
+ # :aggregate, :entity) plus the impossible-kind backstop — each
69
+ # branch is its own self-contained validation for that one kind.
70
+ # rubocop:disable-next Metrics/CyclomaticComplexity
71
71
  def validate_route!(route, receiver)
72
72
  case receiver
73
73
  when nil
74
74
  raise Runtime::TypeMismatch, "this command does not take a receiver in to:" unless route.nil?
75
75
  when :aggregate
76
76
  if route.nil? || route.to_s.empty? || route.is_a?(Hash)
77
- raise Runtime::TypeMismatch, "to: must name the receiving aggregate identity"
77
+ raise Runtime::TypeMismatch,
78
+ "to: must name the receiving aggregate identity"
78
79
  end
79
80
  when :entity
80
- unless route.is_a?(Hash)
81
- raise Runtime::TypeMismatch, "to: for an entity command must contain aggregate: and entity:"
82
- end
81
+ raise Runtime::TypeMismatch, "to: for an entity command must contain aggregate: and entity:" unless route.is_a?(Hash)
83
82
 
84
83
  extra = route.keys - [:aggregate, :entity]
85
84
  missing = [:aggregate, :entity].select { |key| route[key].nil? || route[key].to_s.empty? }
86
85
  unless extra.empty? && missing.empty?
87
- raise Runtime::TypeMismatch, "to: for an entity command must contain only aggregate: and entity:"
86
+ raise Runtime::TypeMismatch,
87
+ "to: for an entity command must contain only aggregate: and entity:"
88
88
  end
89
89
  else
90
90
  raise ArgumentError, "unknown receiver kind #{receiver.inspect}"
@@ -18,10 +18,10 @@ module Hecks
18
18
  class Handle
19
19
  attr_reader :id
20
20
 
21
- def initialize(dispatcher:, domain:, ir:, instance:)
21
+ def initialize(dispatcher:, domain:, aggregate:, instance:)
22
22
  @dispatcher = dispatcher
23
23
  @domain = domain
24
- @ir = ir
24
+ @ir = aggregate
25
25
  @id = instance.id
26
26
  @state = instance.state
27
27
  define_reference_accessors
@@ -72,7 +72,7 @@ module Hecks
72
72
  # A declared field not yet written arrives here too (nil, the way a
73
73
  # defined reader answered). Verbs are NOT handled here — see
74
74
  # `define_verb_methods` for why.
75
- def method_missing(name, *args, **kwargs, &block)
75
+ def method_missing(name, *args, **kwargs, &)
76
76
  return @state[name] if @state.key?(name) || reader?(name)
77
77
 
78
78
  super
@@ -13,14 +13,25 @@ module Hecks
13
13
  # `persisted_by`-style binding collector a `.hecksagon` lands on, and
14
14
  # the aggregate-scoped `port` a `.hecksagon` lands on beside it.
15
15
  module AggregateDoor
16
- def aggregate_module(dispatcher, domain, ir)
17
- fqn = "#{domain}::#{ir.hecks_name}"
16
+ # One method building one `door` module's ~20 singleton methods
17
+ # looks like it splits along each `define_singleton_method` call,
18
+ # but three of those blocks (`:port`, `:method_missing`,
19
+ # `:const_missing`) are a single cross-referencing essay on the
20
+ # stale-facade-across-boots hazard — each one's comment explicitly
21
+ # points at "below"/"above" as part of the SAME method. Splitting
22
+ # into helper methods wouldn't break anything at runtime (no
23
+ # shared mutable state beyond the closed-over args, which just
24
+ # become parameters), but it would sever that narrative across
25
+ # method boundaries for no functional gain.
26
+ # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
27
+ def aggregate_module(dispatcher, domain, aggregate)
28
+ fqn = "#{domain}::#{aggregate.hecks_name}"
18
29
  door = Module.new
19
30
 
20
- ir.attributes.each do |attribute|
31
+ aggregate.attributes.each do |attribute|
21
32
  next unless RESERVED.include?(attribute.name.to_sym)
22
33
 
23
- warn "[hecks] #{ir.hecks_name}##{attribute.name} shadows a built-in — no reader defined"
34
+ warn "[hecks] #{aggregate.hecks_name}##{attribute.name} shadows a built-in — no reader defined"
24
35
  end
25
36
 
26
37
  # A creating verb is a MODULE method returning the new record in hand ;
@@ -33,9 +44,9 @@ module Hecks
33
44
  # accounts, a real same-aggregate collision this corpus already
34
45
  # has) — the suffix is what makes both nameable at all, not
35
46
  # merely a style choice.
36
- ir.commands.select(&:creates?).each do |command|
47
+ aggregate.commands.select(&:creates?).each do |command|
37
48
  door.define_singleton_method("#{Naming.snake(command.hecks_name)}!") do |**args|
38
- Handle.new(dispatcher: dispatcher, domain: domain, ir: ir,
49
+ Handle.new(dispatcher: dispatcher, domain: domain, aggregate: aggregate,
39
50
  instance: dispatcher.dispatch("#{fqn}.#{command.hecks_name}", with: args).instance)
40
51
  end
41
52
  end
@@ -46,14 +57,14 @@ module Hecks
46
57
  # about the way `!` does for a command. Answers the raw row
47
58
  # array `dispatcher.query` itself answers, the same shape
48
59
  # `runtime.query("#{fqn}.Name")` already gave.
49
- ir.queries.each do |query|
60
+ aggregate.queries.each do |query|
50
61
  door.define_singleton_method(Naming.snake(query.hecks_name)) do |**args|
51
62
  dispatcher.query("#{fqn}.#{query.hecks_name}", **args)
52
63
  end
53
64
  end
54
65
 
55
66
  door.define_singleton_method(:fqn) { fqn }
56
- door.define_singleton_method(:ir) { ir }
67
+ door.define_singleton_method(:ir) { aggregate }
57
68
 
58
69
  # THE SAME VERB THE CHAPTER ANSWERS, one level down. Every
59
70
  # construct emits its own IR (Hecks::IR), so an aggregate
@@ -67,39 +78,41 @@ module Hecks
67
78
  # `{"aggregates" => []}` for an aggregate.
68
79
  door.define_singleton_method(:project) do |target, out: nil, **options|
69
80
  key = Projector.key_for(target)
70
- artifact = Projector.call(key, bluebook: ir, options: options)
81
+ artifact = Projector.call(key, bluebook: aggregate, options: options)
71
82
  return artifact unless out
72
83
 
73
84
  Projector.write(artifact, out, as: Projector.emits_for(key))
74
85
  end
75
- door.define_singleton_method(:repository) { dispatcher.registry.repository(domain, ir) }
76
- door.define_singleton_method(:commands) { ir.commands.map { |c| "#{Naming.snake(c.hecks_name)}!" }.sort }
77
- door.define_singleton_method(:queries) { ir.queries.map { |q| Naming.snake(q.hecks_name) }.sort }
86
+ door.define_singleton_method(:repository) { dispatcher.registry.repository(domain, aggregate) }
87
+ door.define_singleton_method(:commands) do
88
+ aggregate.commands.map { |c| "#{Naming.snake(c.hecks_name)}!" }.sort
89
+ end
90
+ door.define_singleton_method(:queries) { aggregate.queries.map { |q| Naming.snake(q.hecks_name) }.sort }
78
91
  # ONE AGGREGATE'S USAGE DOCUMENT — the same projection the chapter
79
92
  # answers with, narrowed to this head. `commands` above already
80
93
  # answers "what can I call"; this answers "and what does each one
81
94
  # want, refuse, and guarantee", which is the rest of the question.
82
95
  door.define_singleton_method(:docs) do |**options|
83
96
  Projector.call(:docs, bluebook: dispatcher.registry.bluebook(domain),
84
- options: options.merge(aggregate: ir.hecks_name))
97
+ options: options.merge(aggregate: aggregate.hecks_name))
85
98
  end
86
99
  # ONE AGGREGATE, READ BACK IN ENGLISH — the same narrowing `:docs`
87
100
  # takes, aimed at `:narrate` instead.
88
101
  door.define_singleton_method(:narrate) do |**options|
89
102
  Projector.call(:narrate, bluebook: dispatcher.registry.bluebook(domain),
90
- options: options.merge(aggregate: ir.hecks_name))
103
+ options: options.merge(aggregate: aggregate.hecks_name))
91
104
  end
92
- door.define_singleton_method(:count) { dispatcher.registry.repository(domain, ir).count }
105
+ door.define_singleton_method(:count) { dispatcher.registry.repository(domain, aggregate).count }
93
106
  door.define_singleton_method(:events) { dispatcher.events.select { |event| event.aggregate == fqn } }
94
107
 
95
108
  door.define_singleton_method(:find) do |id|
96
- found = dispatcher.registry.repository(domain, ir).find(id)
97
- found && Handle.new(dispatcher: dispatcher, domain: domain, ir: ir, instance: found)
109
+ found = dispatcher.registry.repository(domain, aggregate).find(id)
110
+ found && Handle.new(dispatcher: dispatcher, domain: domain, aggregate: aggregate, instance: found)
98
111
  end
99
112
 
100
113
  door.define_singleton_method(:all) do |**opts|
101
- dispatcher.registry.repository(domain, ir).all(**opts).map do |instance|
102
- Handle.new(dispatcher: dispatcher, domain: domain, ir: ir, instance: instance)
114
+ dispatcher.registry.repository(domain, aggregate).all(**opts).map do |instance|
115
+ Handle.new(dispatcher: dispatcher, domain: domain, aggregate: aggregate, instance: instance)
103
116
  end
104
117
  end
105
118
 
@@ -109,20 +122,20 @@ module Hecks
109
122
  # `ConstShim`/`const_missing`, so `Pizzas::Pizza.port(...)` lands
110
123
  # HERE instead once any boot has run before.
111
124
  #
112
- # RE-RESOLVED, NOT THE CLOSED-OVER `ir` — this door can be a STALE
113
- # one, built by a boot from earlier in this same process, sitting
114
- # on the `Pizzas`/`Pizza` constants only because nothing has
115
- # re-installed them since. Attaching to this door's OWN `ir` would
116
- # attach the port to a discarded aggregate from that old boot,
117
- # invisible to the CURRENT one actually being loaded — silently,
118
- # the exact way `method_missing` below already has to avoid it for
119
- # a plain bind, via `HecksagonBuilder.collector` rather than
120
- # anything this door closes over. `Hecks.current_registry` is
121
- # the same "whichever boot is actually in progress" indirection,
125
+ # RE-RESOLVED, NOT THE CLOSED-OVER `aggregate` — this door can be a
126
+ # STALE one, built by a boot from earlier in this same process,
127
+ # sitting on the `Pizzas`/`Pizza` constants only because nothing
128
+ # has re-installed them since. Attaching to this door's OWN
129
+ # `aggregate` would attach the port to a discarded aggregate from
130
+ # that old boot, invisible to the CURRENT one actually being
131
+ # loaded — silently, the exact way `method_missing` below already
132
+ # has to avoid it for a plain bind, via `HecksagonBuilder.collector`
133
+ # rather than anything this door closes over. `Hecks.current_registry`
134
+ # is the same "whichever boot is actually in progress" indirection,
122
135
  # and `BindingProxy#port` already re-resolves through it the same
123
136
  # way — this is that door's fallback twin, not a shortcut past it.
124
137
  door.define_singleton_method(:port) do |name, &block|
125
- current = Hecks.current_registry&.bluebook(domain)&.aggregate(ir.hecks_name) or
138
+ current = Hecks.current_registry&.bluebook(domain)&.aggregate(aggregate.hecks_name) or
126
139
  raise Bluebook::DSL::Malformed, "#{fqn}.port(#{name.inspect}) called outside a boot"
127
140
 
128
141
  domain_port = Bluebook::DSL::ConstShim.with(->(const) { const }) do
@@ -163,16 +176,17 @@ module Hecks
163
176
  # const_missing directly, never `Object.const_missing`/`ConstShim::
164
177
  # Hook` at all: real modules resolve without ever calling that.
165
178
  #
166
- # `ir.hecks_name`, NOT the qualified `fqn` — a scoped reference has to
167
- # read the SAME either way, whether or not a stale door happens to be
168
- # sitting on this process from an earlier boot; qualifying it here
169
- # would make `Account::Debit`'s own meaning depend on incidental
170
- # process history, which is the exact instability S0b exists to
171
- # remove (docs/dsl-work-slices.md — "two domains in one registry" is
172
- # this file's own reason for being owned by that slice).
179
+ # `aggregate.hecks_name`, NOT the qualified `fqn` — a scoped
180
+ # reference has to read the SAME either way, whether or not a stale
181
+ # door happens to be sitting on this process from an earlier boot;
182
+ # qualifying it here would make `Account::Debit`'s own meaning
183
+ # depend on incidental process history, which is the exact
184
+ # instability S0b exists to remove (docs/dsl-work-slices.md — "two
185
+ # domains in one registry" is this file's own reason for being
186
+ # owned by that slice).
173
187
  door.define_singleton_method(:const_missing) do |name|
174
188
  resolver = Bluebook::DSL::ConstShim.resolver
175
- return resolver.call("#{ir.hecks_name}::#{name}") if resolver
189
+ return resolver.call("#{aggregate.hecks_name}::#{name}") if resolver
176
190
 
177
191
  super(name)
178
192
  end
data/lib/hecks/facade.rb CHANGED
@@ -15,6 +15,10 @@ module Hecks
15
15
  # what GENERATED retains is one small facade module per name, not a graph
16
16
  # per boot.
17
17
  module Namespace
18
+ # NOT frozen — a real registry, mutated below
19
+ # (`GENERATED[[container, name]] = value`). False positive for
20
+ # Style/MutableConstant.
21
+ # rubocop:disable-next Style/MutableConstant
18
22
  GENERATED = {}
19
23
 
20
24
  module_function
@@ -118,7 +118,8 @@ module Hecks
118
118
  return respond(405, "text/plain", "GET only") unless request.get?
119
119
 
120
120
  if format == "html"
121
- html("#{chapter.name}::#{aggregate.hecks_name}", RecordRenderer.index(registry: @registry, domain: chapter.name, aggregate: aggregate),
121
+ html("#{chapter.name}::#{aggregate.hecks_name}",
122
+ RecordRenderer.index(registry: @registry, domain: chapter.name, aggregate: aggregate),
122
123
  breadcrumbs: [[chapter.name, "/"], [aggregate.hecks_name, nil]])
123
124
  else
124
125
  instances = @registry.repository(chapter.name, aggregate).all
@@ -196,7 +197,7 @@ module Hecks
196
197
  end
197
198
 
198
199
  def command_json(request, domain, aggregate, command)
199
- return json(200, command.to_h) if request.get? && request.GET.reject { |k, _| k == "format" }.empty?
200
+ return json(200, command.to_h) if request.get? && request.GET.except("format").empty?
200
201
  return respond(405, "text/plain", "GET or POST only") unless request.post?
201
202
 
202
203
  _, envelope = submitted_command(request, aggregate, command)
@@ -213,7 +214,7 @@ module Hecks
213
214
  return respond(405, "text/plain", "GET only") unless request.get?
214
215
 
215
216
  params = request.GET
216
- asked = params.reject { |k, _| k == "format" }
217
+ asked = params.except("format")
217
218
  return query_json(domain, aggregate, query, asked) if format != "html"
218
219
 
219
220
  query_html(domain, aggregate, query, asked)
@@ -252,7 +253,7 @@ module Hecks
252
253
  def run_query(domain, aggregate, query, fields, asked)
253
254
  args = Params.extract(fields, asked)
254
255
  rows = @dispatcher.query("#{domain}::#{aggregate.hecks_name}.#{query.hecks_name}", **args)
255
- [rows.map { |row| Record.new(row[:id], row.reject { |k, _| k == :id }) }, nil]
256
+ [rows.map { |row| Record.new(row[:id], row.except(:id)) }, nil]
256
257
  # L10 (docs/audits/2026-08-10-main-bug-audit.md) — `Params.extract`
257
258
  # (params.rb's `extract_list`) reads a list-of-value-object line as
258
259
  # JSON (the honest fallback for a multi-attribute list element this
@@ -275,7 +276,8 @@ module Hecks
275
276
  # sites in this file (see aggregate_route's own comment).
276
277
  return json(200, instance.state.merge(id: instance.id)) if format != "html"
277
278
 
278
- html("#{domain}::#{aggregate.hecks_name} #{id}", RecordRenderer.show(registry: @registry, domain: domain, aggregate: aggregate, id: id),
279
+ html("#{domain}::#{aggregate.hecks_name} #{id}",
280
+ RecordRenderer.show(registry: @registry, domain: domain, aggregate: aggregate, id: id),
279
281
  breadcrumbs: [[domain, "/"], [aggregate.hecks_name, "/#{domain}/#{aggregate.hecks_name}.html"], [id, nil]])
280
282
  end
281
283
 
@@ -283,7 +285,9 @@ module Hecks
283
285
  return json(404, { error: "NotFound", message: "no #{aggregate.hecks_name} #{id}" }) if format != "html"
284
286
 
285
287
  respond(404, "text/html; charset=utf-8",
286
- Page.render(title: "not found", body: "<h1>Not found</h1><p>No #{Escape.html(aggregate.hecks_name)} #{Escape.html(id)}.</p>"))
288
+ Page.render(title: "not found",
289
+ body: "<h1>Not found</h1><p>No #{Escape.html(aggregate.hecks_name)} " \
290
+ "#{Escape.html(id)}.</p>"))
287
291
  end
288
292
 
289
293
  def find_aggregate(chapter, name)
@@ -57,9 +57,9 @@ module Hecks
57
57
  def self.header(domain, aggregate, command)
58
58
  <<~HTML
59
59
  <h1>#{Escape.html("#{domain}::#{aggregate.hecks_name}.#{command.hecks_name}")}</h1>
60
- #{command.role ? %(<span class="badge role" title="Declared on the command; this prototype dispatches with no caller bound, so the check does not run.">role: #{Escape.html(command.role)} (not enforced here)</span>) : ''}
61
- #{command.creates? ? %(<span class="badge">creates a new #{Escape.html(aggregate.hecks_name)}</span>) : ''}
62
- #{command.goal ? %(<p class="goal">#{Escape.html(command.goal)}</p>) : ''}
60
+ #{%(<span class="badge role" title="Declared on the command; this prototype dispatches with no caller bound, so the check does not run.">role: #{Escape.html(command.role)} (not enforced here)</span>) if command.role}
61
+ #{%(<span class="badge">creates a new #{Escape.html(aggregate.hecks_name)}</span>) if command.creates?}
62
+ #{%(<p class="goal">#{Escape.html(command.goal)}</p>) if command.goal}
63
63
  #{givens_callout(command)}
64
64
  HTML
65
65
  end
@@ -28,7 +28,7 @@ module Hecks
28
28
  def self.group(field, values, errors, reference_options, tag:, css: nil)
29
29
  inner = field.children.map { |child| render(child, values: values, errors: errors, reference_options: reference_options) }
30
30
  <<~HTML
31
- <#{tag}#{css ? %( class="#{css}") : ""}>
31
+ <#{tag}#{%( class="#{css}") if css}>
32
32
  <legend>#{Escape.html(field.label)}#{required_mark(field)}</legend>
33
33
  #{inner.join("\n")}
34
34
  </#{tag}>
@@ -43,7 +43,7 @@ module Hecks
43
43
  <label for="#{dom_id(field.path)}">#{Escape.html(field.label)}#{required_mark(field)}</label>
44
44
  <textarea id="#{dom_id(field.path)}" name="#{Escape.attr(field.path)}"
45
45
  #{aria(field)} placeholder="one #{Escape.attr(item.label.downcase)} per line">#{Escape.html(current)}</textarea>
46
- <span class="help" id="#{dom_id(field.path)}-help">One #{Escape.html(item.label.downcase)} per line#{item.leaf? ? '' : ' — each line is one JSON object'}.</span>
46
+ <span class="help" id="#{dom_id(field.path)}-help">One #{Escape.html(item.label.downcase)} per line#{' — each line is one JSON object' unless item.leaf?}.</span>
47
47
  </div>
48
48
  HTML
49
49
  end
@@ -64,11 +64,11 @@ module Hecks
64
64
 
65
65
  def self.wrap(field, body, error)
66
66
  <<~HTML
67
- <div class="field#{error ? ' has-error' : ''}">
68
- #{field.kind == :boolean ? '' : %(<label for="#{dom_id(field.path)}">#{Escape.html(field.label)}#{required_mark(field)}</label>)}
67
+ <div class="field#{' has-error' if error}">
68
+ #{%(<label for="#{dom_id(field.path)}">#{Escape.html(field.label)}#{required_mark(field)}</label>) unless field.kind == :boolean}
69
69
  #{body}
70
- #{field.help ? %(<span class="help" id="#{dom_id(field.path)}-help">#{Escape.html(field.help)}</span>) : ''}
71
- #{error ? %(<span class="help" role="alert">#{Escape.html(error)}</span>) : ''}
70
+ #{%(<span class="help" id="#{dom_id(field.path)}-help">#{Escape.html(field.help)}</span>) if field.help}
71
+ #{%(<span class="help" role="alert">#{Escape.html(error)}</span>) if error}
72
72
  </div>
73
73
  HTML
74
74
  end
@@ -114,7 +114,8 @@ module Hecks
114
114
  def self.select(field, value)
115
115
  selected = value || field.default
116
116
  options = field.options.map do |option_value, option_label|
117
- %(<option value="#{Escape.attr(option_value)}"#{option_value.to_s == selected.to_s ? ' selected' : ''}>#{Escape.html(option_label)}</option>)
117
+ %(<option value="#{Escape.attr(option_value)}"#{' selected' if option_value.to_s == selected.to_s}>) \
118
+ "#{Escape.html(option_label)}</option>"
118
119
  end
119
120
  blank = field.optional? ? %(<option value="">—</option>) : ""
120
121
  %(<select id="#{dom_id(field.path)}" name="#{Escape.attr(field.path)}" #{aria(field)}>#{blank}#{options.join}</select>)
@@ -124,9 +125,13 @@ module Hecks
124
125
  return input(field.tap { |f| f.html_type = "text" }, value) unless options && !options.empty?
125
126
 
126
127
  rendered = options.map do |id, label|
127
- %(<option value="#{Escape.attr(id)}"#{id.to_s == value.to_s ? ' selected' : ''}>#{Escape.html(label)}</option>)
128
+ %(<option value="#{Escape.attr(id)}"#{' selected' if id.to_s == value.to_s}>#{Escape.html(label)}</option>)
128
129
  end
129
- blank = field.optional? ? %(<option value="">—</option>) : %(<option value="" disabled#{value ? '' : ' selected'}>choose one…</option>)
130
+ blank = if field.optional?
131
+ %(<option value="">—</option>)
132
+ else
133
+ %(<option value="" disabled#{' selected' unless value}>choose one…</option>)
134
+ end
130
135
  %(<select id="#{dom_id(field.path)}" name="#{Escape.attr(field.path)}" #{aria(field)}>#{blank}#{rendered.join}</select>)
131
136
  end
132
137
 
@@ -24,6 +24,8 @@ module Hecks
24
24
  def required? = !optional
25
25
  end
26
26
 
27
+ # Turns a wire-spelled field or path segment into the plain-English text
28
+ # `FieldShape` embeds as a `Field`'s label or a group's legend.
27
29
  module Humanize
28
30
  # "given" -> "Given", "daily_limit" -> "Daily limit", "end_to_end" ->
29
31
  # "End to end" — the label a plain reader wants, not the wire spelling.
@@ -91,8 +93,11 @@ module Hecks
91
93
  def self.reference_field(attribute, common)
92
94
  target = attribute.type.resolve
93
95
  Field.new(**common, kind: :reference, html_type: "text", target_aggregate: target,
94
- help: target ? "References an existing #{target.hecks_name} by id." :
95
- "References an aggregate in another domain — enter its id.")
96
+ help: if target
97
+ "References an existing #{target.hecks_name} by id."
98
+ else
99
+ "References an aggregate in another domain — enter its id."
100
+ end)
96
101
  end
97
102
 
98
103
  # `admits:` names a closed set declared ELSEWHERE (`"Account::
@@ -69,7 +69,7 @@ module Hecks
69
69
  next if value.nil? || value == false
70
70
  next name.to_s.tr("_", "-") if value == true
71
71
 
72
- %(#{name.to_s.tr("_", "-")}="#{Escape.attr(value)}")
72
+ %(#{name.to_s.tr('_', '-')}="#{Escape.attr(value)}")
73
73
  end.join(" ")
74
74
  end
75
75
 
@@ -78,7 +78,10 @@ module Hecks
78
78
  rendered.empty? ? "<#{name}>" : "<#{name} #{rendered}>"
79
79
  end
80
80
 
81
- def self.void(name, **pairs) = open(name, **pairs) # self-closing tags read the same; HTML5 needs no slash
81
+ # `self.open(...)`, not bare `open(...)` this Html.open (an HTML tag
82
+ # renderer, right above) shadows Kernel#open safely either way, but the
83
+ # explicit receiver also settles Security/Open's static ambiguity.
84
+ def self.void(name, **pairs) = self.open(name, **pairs) # self-closing tags read the same; HTML5 needs no slash
82
85
  end
83
86
  end
84
87
  end
@@ -26,7 +26,7 @@ module Hecks
26
26
  HTML
27
27
  end
28
28
  <<~HTML
29
- <h2>#{Escape.html(name)}#{chapter.vision ? %( <span class="badge">#{Escape.html(chapter.vision)}</span>) : ''}</h2>
29
+ <h2>#{Escape.html(name)}#{%( <span class="badge">#{Escape.html(chapter.vision)}</span>) if chapter.vision}</h2>
30
30
  <ul class="verb-list">#{items.join}</ul>
31
31
  HTML
32
32
  end
@@ -40,12 +40,16 @@ module Hecks
40
40
  return "" if crumbs.empty?
41
41
 
42
42
  items = crumbs.map do |label, href|
43
- href ? %(<a href="#{Escape.attr(href)}">#{Escape.html(label)}</a>) : %(<span aria-current="page">#{Escape.html(label)}</span>)
43
+ if href
44
+ %(<a href="#{Escape.attr(href)}">#{Escape.html(label)}</a>)
45
+ else
46
+ %(<span aria-current="page">#{Escape.html(label)}</span>)
47
+ end
44
48
  end
45
49
  %(<nav class="breadcrumbs" aria-label="Breadcrumb">#{items.join(' <span class="sep">/</span> ')}</nav>)
46
50
  end
47
51
 
48
- STYLE = <<~CSS
52
+ STYLE = <<~CSS.freeze
49
53
  :root {
50
54
  --bg: #fbfaf8; --surface: #ffffff; --border: #e2ddd3; --text: #24211c;
51
55
  --muted: #6b6355; --accent: #8a5a2b; --accent-contrast: #fff8ef;
@@ -131,7 +135,7 @@ module Hecks
131
135
  # Decorative only — every form here already works with JS disabled
132
136
  # (plain `<form method=post>`/`method=get`), matching this repo's own
133
137
  # "no ERB, nothing scripted" bar for the domain layer itself.
134
- SCRIPT = <<~JS
138
+ SCRIPT = <<~JS.freeze
135
139
  document.addEventListener("click", (event) => {
136
140
  const button = event.target.closest("[data-copy]");
137
141
  if (!button) return;
@@ -38,7 +38,7 @@ module Hecks
38
38
  def self.header(domain, aggregate, query)
39
39
  <<~HTML
40
40
  <h1>#{Escape.html("#{domain}::#{aggregate.hecks_name}.#{query.hecks_name}")}</h1>
41
- #{query.description ? %(<p class="goal">#{Escape.html(query.description)}</p>) : ''}
41
+ #{%(<p class="goal">#{Escape.html(query.description)}</p>) if query.description}
42
42
  #{badges(query)}
43
43
  HTML
44
44
  end
@@ -89,7 +89,8 @@ module Hecks
89
89
  def self.error_banner(error)
90
90
  return "" unless error
91
91
 
92
- %(<div class="error-banner" role="alert"><p><strong>#{Escape.html(error.class.name.split('::').last)}</strong> — #{Escape.html(error.message)}</p></div>)
92
+ %(<div class="error-banner" role="alert"><p><strong>#{Escape.html(error.class.name.split('::').last)}</strong> — ) \
93
+ "#{Escape.html(error.message)}</p></div>"
93
94
  end
94
95
 
95
96
  def self.results_section(aggregate, results, domain)
@@ -15,7 +15,7 @@ module Hecks
15
15
  instances = registry.repository(domain, aggregate).all
16
16
  <<~HTML
17
17
  <h1>#{Escape.html("#{domain}::#{aggregate.hecks_name}")}</h1>
18
- #{aggregate.description ? %(<p class="goal">#{Escape.html(aggregate.description)}</p>) : ''}
18
+ #{%(<p class="goal">#{Escape.html(aggregate.description)}</p>) if aggregate.description}
19
19
  #{creating_links(domain, aggregate)}
20
20
  <h2>Records (#{instances.size})</h2>
21
21
  #{RecordTable.render(aggregate, instances, domain: domain)}
@@ -27,9 +27,10 @@ module Hecks
27
27
  creators = aggregate.commands.select(&:creates?)
28
28
  return "" if creators.empty?
29
29
 
30
- links = creators.map { |cmd|
31
- %(<a class="button" href="/#{domain}/#{aggregate.hecks_name}/#{cmd.hecks_name}.html">+ #{Escape.html(cmd.hecks_name)}</a>)
32
- }
30
+ links = creators.map do |cmd|
31
+ %(<a class="button" href="/#{domain}/#{aggregate.hecks_name}/#{cmd.hecks_name}.html">) \
32
+ "+ #{Escape.html(cmd.hecks_name)}</a>"
33
+ end
33
34
  %(<div class="actions">#{links.join}</div>)
34
35
  end
35
36
 
@@ -41,7 +42,7 @@ module Hecks
41
42
  current = aggregate.lifecycle && state[aggregate.lifecycle.field]
42
43
  <<~HTML
43
44
  <h1>#{Escape.html("#{domain}::#{aggregate.hecks_name}")} <span class="mono">#{Escape.html(id)}</span></h1>
44
- #{current ? %(<span class="badge role">#{Escape.html(aggregate.lifecycle.field)}: #{Escape.html(current)}</span>) : ''}
45
+ #{%(<span class="badge role">#{Escape.html(aggregate.lifecycle.field)}: #{Escape.html(current)}</span>) if current}
45
46
  #{state_table(state)}
46
47
  <h2>Commands</h2>
47
48
  #{command_links(domain, aggregate, id, current)}
@@ -50,9 +51,9 @@ module Hecks
50
51
  end
51
52
 
52
53
  def self.state_table(state)
53
- rows = state.map { |key, value|
54
+ rows = state.map do |key, value|
54
55
  "<tr><th>#{Escape.html(Humanize.label(key.to_s))}</th><td>#{Escape.html(render_value(value))}</td></tr>"
55
- }
56
+ end
56
57
  %(<div class="table-scroll"><table><tbody>#{rows.join}</tbody></table></div>)
57
58
  end
58
59
 
@@ -81,7 +82,8 @@ module Hecks
81
82
  # parameter), then the assembled href is attribute-escaped as
82
83
  # usual.
83
84
  href = "/#{domain}/#{aggregate.hecks_name}/#{cmd.hecks_name}.html?to=#{Escape.url(id)}"
84
- %(<li><a href="#{Escape.attr(href)}"><span>#{Escape.html(cmd.hecks_name)}</span><span class="kind">#{Escape.html(cmd.goal.to_s)}</span></a></li>)
85
+ %(<li><a href="#{Escape.attr(href)}"><span>#{Escape.html(cmd.hecks_name)}</span>) \
86
+ "<span class=\"kind\">#{Escape.html(cmd.goal.to_s)}</span></a></li>"
85
87
  end
86
88
  %(<ul class="verb-list">#{items.join}</ul>)
87
89
  end
@@ -97,7 +99,8 @@ module Hecks
97
99
  return "" if aggregate.queries.empty?
98
100
 
99
101
  items = aggregate.queries.map do |query|
100
- %(<li><a href="/#{domain}/#{aggregate.hecks_name}/#{query.hecks_name}.html"><span>#{Escape.html(query.hecks_name)}</span><span class="kind">query</span></a></li>)
102
+ %(<li><a href="/#{domain}/#{aggregate.hecks_name}/#{query.hecks_name}.html">) \
103
+ "<span>#{Escape.html(query.hecks_name)}</span><span class=\"kind\">query</span></a></li>"
101
104
  end
102
105
  %(<h2>Queries</h2><ul class="verb-list">#{items.join}</ul>)
103
106
  end
@@ -106,13 +109,16 @@ module Hecks
106
109
  commands = aggregate.commands.reject(&:creates?)
107
110
  return "" if commands.empty? && aggregate.queries.empty?
108
111
 
109
- cmd_items = commands.map { |c|
110
- %(<li><a href="/#{domain}/#{aggregate.hecks_name}/#{c.hecks_name}.html"><span>#{Escape.html(c.hecks_name)}</span><span class="kind">command</span></a></li>)
111
- }
112
- query_items = aggregate.queries.map { |q|
113
- %(<li><a href="/#{domain}/#{aggregate.hecks_name}/#{q.hecks_name}.html"><span>#{Escape.html(q.hecks_name)}</span><span class="kind">query</span></a></li>)
114
- }
115
- %(<h2>Every command &amp; query on #{Escape.html(aggregate.hecks_name)}</h2><ul class="verb-list">#{(cmd_items + query_items).join}</ul>)
112
+ cmd_items = commands.map do |c|
113
+ %(<li><a href="/#{domain}/#{aggregate.hecks_name}/#{c.hecks_name}.html">) \
114
+ "<span>#{Escape.html(c.hecks_name)}</span><span class=\"kind\">command</span></a></li>"
115
+ end
116
+ query_items = aggregate.queries.map do |q|
117
+ %(<li><a href="/#{domain}/#{aggregate.hecks_name}/#{q.hecks_name}.html">) \
118
+ "<span>#{Escape.html(q.hecks_name)}</span><span class=\"kind\">query</span></a></li>"
119
+ end
120
+ %(<h2>Every command &amp; query on #{Escape.html(aggregate.hecks_name)}</h2>) \
121
+ "<ul class=\"verb-list\">#{(cmd_items + query_items).join}</ul>"
116
122
  end
117
123
  end
118
124
  end
data/lib/hecks/forms.rb CHANGED
@@ -13,6 +13,10 @@ require "hecks"
13
13
  require_relative "forms/app"
14
14
 
15
15
  module Hecks
16
+ # Per-chapter `expose` configuration, keyed by chapter name and looked up
17
+ # by the Rack app at request time (`.config`) to decide which chapters'
18
+ # forms are switched on at all — see the file header above for what
19
+ # `expose` deliberately does and does not cover.
16
20
  module Forms
17
21
  # `expose`'s own declaration — "which chapters does this app expose" —
18
22
  # kept OUTSIDE the `Hecks.*` collector convention (`Hecks.bluebook`,