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
@@ -2,8 +2,13 @@ require_relative "word_gate"
2
2
  module Hecks
3
3
  module Bluebook
4
4
  module DSL
5
+ # The `command "Name" do ... end` receiver — collects a command's
6
+ # `role`/`goal`/`given`/`ensures`/`sets`/`emits`/`delegates_to`/
7
+ # `corrects` declarations, resolves implicit attributes a bare `sets
8
+ # :field` or `append:` self-reference imports from the owner
9
+ # (`#resolve_implicit_attributes!`), and builds the final `Command`.
5
10
  class CommandBuilder
6
- GRAMMAR_CONTEXT = "Command"
11
+ GRAMMAR_CONTEXT = "Command".freeze
7
12
 
8
13
  include AttributeCollector
9
14
  include RuleReference
@@ -67,10 +72,12 @@ module Hecks
67
72
  # declares a role), so also named in
68
73
  # `GenericDispatch::BOOTSTRAP_CALLS_FALLBACK`.
69
74
  def role_impl(value)
70
- raise Malformed,
71
- "#{@name} declares role twice — a command carries ONE " \
72
- "responsibility; the second would silently win and the " \
73
- "first would still look declared" if @role
75
+ if @role
76
+ raise Malformed,
77
+ "#{@name} declares role twice a command carries ONE " \
78
+ "responsibility; the second would silently win and the " \
79
+ "first would still look declared"
80
+ end
74
81
 
75
82
  @role = value
76
83
  end
@@ -104,7 +111,7 @@ module Hecks
104
111
  # one thing it exists to say. Transfer has said
105
112
  # `reference_to Account, as: :source` for as long as banking has existed;
106
113
  # this is the same sentence when the target happens to be the owner.
107
- return cross_reference(demodulised, as, optional) if as || demodulised.to_s != @owner.to_s
114
+ return cross_reference(demodulised, as, optional: optional) if as || demodulised.to_s != @owner.to_s
108
115
 
109
116
  if @references
110
117
  raise Malformed,
@@ -118,7 +125,7 @@ module Hecks
118
125
 
119
126
  private
120
127
 
121
- def cross_reference(target, as, optional = false)
128
+ def cross_reference(target, as, optional: false)
122
129
  attribute_impl(as || default_reference_name(target), Reference.new(target), optional: optional)
123
130
  end
124
131
 
@@ -308,8 +315,8 @@ module Hecks
308
315
  # dedicated, `status: "deprecated"` Keyword row (syntax.bluebook)
309
316
  # rather than living only as `sets`'s own `was:` — see that
310
317
  # row's own comment for why.
311
- def then_set_impl(target, positional_to = UNSET, **kwargs)
312
- return legacy_then_set(target, positional_to, **kwargs) if MetaValidator.shadow_parsing?
318
+ def then_set_impl(target, positional_to = UNSET, **)
319
+ return legacy_then_set(target, positional_to, **) if MetaValidator.shadow_parsing?
313
320
 
314
321
  raise Malformed, "#{@name}'s then_set is gone — sets is the word now"
315
322
  end
@@ -478,8 +485,13 @@ module Hecks
478
485
  source: { as: as&.to_s, reason: reason.to_s, reverses: reverses })
479
486
  end
480
487
 
488
+ # THE EFFECTS THAT WRITE A FIELD OF THE RECORD — `delegate` and
489
+ # `corrects` name a command and an event, never a field.
490
+ FIELD_EFFECTS = %i[set append remove increment decrement multiply clamp].freeze
491
+
481
492
  def build
482
493
  resolve_implicit_attributes!
494
+ refuse_duplicate_targets!
483
495
 
484
496
  delegation = @mutations.find { |mutation| mutation.op == :delegate }
485
497
  if delegation && (@mutations.size > 1 || @emits.any?)
@@ -515,6 +527,27 @@ module Hecks
515
527
 
516
528
  private
517
529
 
530
+ # C4.2 (docs/semantics/bluebook-semantics.md) — a command's effects
531
+ # are ONE UPDATE SET over the pre-dispatch state, so a field
532
+ # written twice has no meaning to give: last-wins would make
533
+ # declaration order significant, which the update set says it is
534
+ # not. Refused here, where the declaration can still be read whole.
535
+ def refuse_duplicate_targets!
536
+ return if MetaValidator.shadow_parsing? # frozen era text is history
537
+
538
+ seen = {}
539
+ @mutations.each do |mutation|
540
+ next unless FIELD_EFFECTS.include?(mutation.op)
541
+
542
+ if (earlier = seen[mutation.target.to_sym])
543
+ raise Malformed,
544
+ "#{@name} writes #{mutation.target} twice (#{earlier.op} and #{mutation.op}) — a command's " \
545
+ "effects are one update set over the pre-dispatch state, so each field is written at most once"
546
+ end
547
+ seen[mutation.target.to_sym] = mutation
548
+ end
549
+ end
550
+
518
551
  # RESOLUTION RULES — see `docs/resolution-rules/README.md` for the
519
552
  # precise, language-agnostic algorithm each of `resolve_bare_set!`/
520
553
  # `resolve_append_fields!` implements (`implicit-command-attributes.md`
@@ -563,8 +596,8 @@ module Hecks
563
596
  # it for an ordering accident this language has never required
564
597
  # authors to avoid. rubocop's Style/CombinableLoops can't see
565
598
  # that dependency — it only sees two `@mutations.each` calls
566
- # back to back — so it's disabled here, not satisfied.
567
- # rubocop:disable-next Style/CombinableLoops
599
+ # back to back — which is exactly why it's disabled repo-wide
600
+ # (see .rubocop.yml) rather than satisfied.
568
601
  @mutations.each { |mutation| refuse_unknown_argument_sources!(mutation) }
569
602
  end
570
603
 
@@ -674,6 +707,13 @@ module Hecks
674
707
  # the naive append; every OTHER field in the same hash did not,
675
708
  # caught by this codemod's own reboot-and-diff safety net rather
676
709
  # than silently landing wrong.
710
+ # `anchor` is a snapshot of `present`'s position, taken BEFORE
711
+ # `attributes.reject!` mutates the array below it — see this
712
+ # method's own header comment for why: a naive append-at-end
713
+ # silently scrambled real corpus field order (Keyword#was/
714
+ # Argument#variadic). Splitting the snapshot from the mutation it
715
+ # guards is exactly the ordering invariant that must not move.
716
+ # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
677
717
  def resolve_append_fields!(mutation)
678
718
  return unless mutation.source.is_a?(Hash)
679
719
 
@@ -755,8 +795,8 @@ module Hecks
755
795
  # downstream read it — at dispatch (`MutationApplier#appended`),
756
796
  # at IR emission (`Mutation#appended_fields`), and in the
757
797
  # meta-validator's own Judge (`Readings#mutation_rows`). #138.
758
- def normalize_append_source(op, source)
759
- return source unless op == :append
798
+ def normalize_append_source(oper, source)
799
+ return source unless oper == :append
760
800
  return source if source.is_a?(::Hash)
761
801
 
762
802
  { value: source }
@@ -1,6 +1,12 @@
1
1
  module Hecks
2
2
  module Bluebook
3
3
  module DSL
4
+ # A global (Object-level, via `Hook` prepended below) `const_missing`
5
+ # bridge — `.with(resolver) { ... }` installs a resolver for the
6
+ # dynamic extent of a block, so a bare, not-yet-declared constant
7
+ # inside a DSL block (a bareword type, `Domain::Aggregate`, ...)
8
+ # resolves to whatever that DSL context needs (a name, a
9
+ # `BindingProxy`, a `ScopedConstant`) instead of raising `NameError`.
4
10
  module ConstShim
5
11
  class << self
6
12
  attr_accessor :resolver
@@ -2,8 +2,15 @@ require_relative "word_gate"
2
2
  module Hecks
3
3
  module Bluebook
4
4
  module DSL
5
+ # Parses a `domain_port "Name" do ... end` block into a `DomainPort` —
6
+ # the inbound (`operation`/`tells`) and outbound (`asks`) operations a
7
+ # domain exchanges with the outside world. Falls back to building a
8
+ # plain `Port` (the same object `PortBuilder` builds) when the block is
9
+ # bare-verb shaped instead, so an existing top-level `.port` file can
10
+ # migrate to being parsed by this builder — see `legacy_bare_port:`'s
11
+ # own comment on `#initialize`.
5
12
  class DomainPortBuilder
6
- GRAMMAR_CONTEXT = "DomainPort"
13
+ GRAMMAR_CONTEXT = "DomainPort".freeze
7
14
 
8
15
  include WordGate
9
16
 
@@ -43,8 +50,8 @@ module Hecks
43
50
  # — the routing between the two spellings now lives in the table,
44
51
  # not in a Ruby `alias`. Not bootstrap-reachable (checked
45
52
  # directly), so no BOOTSTRAP_CALLS_FALLBACK entry needed.
46
- def tells_impl(name, to: nil, &block)
47
- @operations << PortOperationBuilder.build(name, to: to, owner: @owner, direction: :inbound, &block)
53
+ def tells_impl(name, to: nil, &)
54
+ @operations << PortOperationBuilder.build(name, to: to, owner: @owner, direction: :inbound, &)
48
55
  end
49
56
 
50
57
  # WHAT WE ASK OF THE OUTSIDE — the direction this language did not
@@ -57,8 +64,8 @@ module Hecks
57
64
  #
58
65
  # RENAMED FROM `asks` — item #13's full metaprogrammed dispatch
59
66
  # (slice 4c), same reasoning as tells_impl above.
60
- def asks_impl(name, to: nil, &block)
61
- @operations << PortOperationBuilder.build(name, to: to, owner: @owner, direction: :outbound, &block)
67
+ def asks_impl(name, to: nil, &)
68
+ @operations << PortOperationBuilder.build(name, to: to, owner: @owner, direction: :outbound, &)
62
69
  end
63
70
 
64
71
  # THE DRIVEN HALF OF THE SAME WORD. `operation`/`emits` translates an
@@ -101,9 +108,15 @@ module Hecks
101
108
  def answers(name) = @answers << name.to_sym
102
109
 
103
110
  def build
104
- raise Malformed, "#{@name} declares both a verb and operations — a port is one or the other, not both" if @verb && !@operations.empty?
111
+ if @verb && !@operations.empty?
112
+ raise Malformed,
113
+ "#{@name} declares both a verb and operations — a port is one or the other, not both"
114
+ end
105
115
 
106
- return MetaValidator.call_port(Port.new(name: @name, verb: @verb, signal: @signal, answers: @answers)) if @verb || (@legacy_bare_port && @operations.empty?)
116
+ if @verb || (@legacy_bare_port && @operations.empty?)
117
+ return MetaValidator.call_port(Port.new(name: @name, verb: @verb, signal: @signal,
118
+ answers: @answers))
119
+ end
107
120
 
108
121
  raise Malformed, "#{@name} declares no verb and no operations" if @operations.empty?
109
122
 
@@ -2,8 +2,16 @@ require_relative "word_gate"
2
2
  module Hecks
3
3
  module Bluebook
4
4
  module DSL
5
+ # Parses an `entity "Name" do ... end` block nested inside an aggregate
6
+ # (or another entity, ADR 0026 — "a piece nested inside a piece") into
7
+ # an `Entity` — attributes, relationships (`has_many`/`has_one`/
8
+ # `belongs_to`), its own nested commands/queries/lifecycle, and
9
+ # preconditions shared across them. Threads the owning aggregate's
10
+ # value-object and cross-entity `given` pools through unchanged, so a
11
+ # nested piece resolves identity and shared rules against the same
12
+ # aggregate-wide state a top-level piece does.
5
13
  class EntityBuilder
6
- GRAMMAR_CONTEXT = "Entity"
14
+ GRAMMAR_CONTEXT = "Entity".freeze
7
15
 
8
16
  include AttributeCollector
9
17
  include IdentityDeclaration
@@ -80,7 +88,8 @@ module Hecks
80
88
  # rubocop:disable Naming/PredicatePrefix
81
89
  def has_many_impl(type, as: nil, **options)
82
90
  unless options.empty?
83
- raise Malformed, "#{@name}.has_many takes no #{options.keys.first}: — an empty list already means none"
91
+ raise Malformed,
92
+ "#{@name}.has_many takes no #{options.keys.first}: — an empty list already means none"
84
93
  end
85
94
 
86
95
  plural = Naming.demodulise(type)
@@ -139,8 +148,8 @@ module Hecks
139
148
  @pending_entities << [name, block]
140
149
  end
141
150
 
142
- def lifecycle_impl(field, default:, &block)
143
- @lifecycle = LifecycleBuilder.build(field, default: default, &block)
151
+ def lifecycle_impl(field, default:, &)
152
+ @lifecycle = LifecycleBuilder.build(field, default: default, &)
144
153
  end
145
154
 
146
155
  # A PRECONDITION SHARED ACROSS THIS PIECE'S OWN COMMANDS, DECLARED
@@ -400,10 +409,22 @@ module Hecks
400
409
  # See `AggregateBuilder#seal_lifecycle_guards`'s own comment —
401
410
  # the identical check, one level down.
402
411
  def seal_lifecycle_guards
403
- return if @lifecycle
404
-
405
412
  @commands.each do |command|
413
+ if @lifecycle && !MetaValidator.shadow_parsing?
414
+ # C5.3 — the same refusal `AggregateBuilder::Sealing` gives
415
+ # an aggregate: a `sets` on the lifecycle field (frozen era
416
+ # text excepted, as there).
417
+ command.mutations.each do |mutation|
418
+ next if [:delegate, :corrects].include?(mutation.op)
419
+ next unless mutation.target.to_sym == @lifecycle.field.to_sym
420
+
421
+ raise Malformed,
422
+ "#{@name}.#{command.hecks_name} sets #{mutation.target}, #{@name}'s lifecycle field — " \
423
+ "a lifecycle field moves only by transition; declare one instead of setting it"
424
+ end
425
+ end
406
426
  next unless command.from
427
+ next if @lifecycle
407
428
 
408
429
  raise Malformed,
409
430
  "#{@name}.#{command.hecks_name} guards from: #{Array(command.from).inspect}, but " \
@@ -269,7 +269,8 @@ module Hecks
269
269
  # would silently execute as a no-op instead of raising the
270
270
  # same "not yet implemented" refusal a genuinely unmigrated
271
271
  # word already gets.
272
- raise Runtime::WiringError, "no dispatcher handles shape #{shape[:kind].inspect} — add one before shape_for can produce it"
272
+ raise Runtime::WiringError,
273
+ "no dispatcher handles shape #{shape[:kind].inspect} — add one before shape_for can produce it"
273
274
  end
274
275
  end
275
276
 
@@ -277,6 +278,13 @@ module Hecks
277
278
  # which of the four safe shapes (context, word) is, or `nil` if
278
279
  # it falls outside this slice's own verified scope. No argument
279
280
  # values are read here; this only ever looks at the table.
281
+ #
282
+ # An early-return classification chain: each `return` depends on
283
+ # locals (`calls`, `fills`, `arguments`, `arg`) computed by the
284
+ # checks before it. Splitting per shape would mean re-deriving or
285
+ # threading those locals across method boundaries for a
286
+ # classification that is only ever read top-to-bottom once, here.
287
+ # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
280
288
  def shape_for(context, word, rows)
281
289
  keyword = rows[:keywords].find { |k| k[:context] == context && k[:word] == word && k[:status] != "retired" }
282
290
  return nil unless keyword
@@ -333,7 +341,9 @@ module Hecks
333
341
  raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0)" unless args.empty?
334
342
 
335
343
  rows = MetaValidator::SyntaxBoot.call[:keywords]
336
- siblings = rows.count { |k| k[:context] == keyword[:context] && k[:fills] == keyword[:fills] && k[:status] != "retired" }
344
+ siblings = rows.count do |k|
345
+ k[:context] == keyword[:context] && k[:fills] == keyword[:fills] && k[:status] != "retired"
346
+ end
337
347
  value = siblings > 1 ? keyword[:word].to_sym : true
338
348
 
339
349
  builder.instance_variable_set(:"@#{keyword[:fills]}", value)
@@ -2,8 +2,15 @@ require_relative "word_gate"
2
2
  module Hecks
3
3
  module Bluebook
4
4
  module DSL
5
+ # Parses a `.hecksagon` file's top-level DSL block into a `Hecksagon`
6
+ # — a domain's own WIRING: which adapter binds to which verb
7
+ # (`persisted_by`, `projected_by`, ...), which framework/vendored
8
+ # bluebooks it attaches, which external events it subscribes to, and
9
+ # its bare chapter-root port. Kept separate from the bluebook itself
10
+ # (the domain's own declared MODEL) because wiring is an operational
11
+ # decision, not a fact the domain states about itself.
5
12
  class HecksagonBuilder
6
- GRAMMAR_CONTEXT = "Hecksagon"
13
+ GRAMMAR_CONTEXT = "Hecksagon".freeze
7
14
 
8
15
  include WordGate
9
16
 
@@ -39,14 +39,24 @@ module Hecks
39
39
  # and Entity Keyword rows name it in `calls:`. Bootstrap-
40
40
  # reachable (every self-hosted aggregate/entity declares an
41
41
  # identity), so in BOOTSTRAP_CALLS_FALLBACK for both contexts.
42
+ # Dispatches across the three live forms documented above (value-
43
+ # object + block, single type target, single/compound field
44
+ # target), each an early return that sets exactly one pending
45
+ # ivar. Splitting per form would need each branch's own `return`-
46
+ # with-nil semantics and the shared `@name`/`identity_pool`
47
+ # threaded back out as parameters, for no gain beyond what the
48
+ # three-forms comment above already documents.
49
+ # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
42
50
  def identified_by_impl(*targets, as: nil, &definition)
43
51
  return legacy_identified_by(*targets, as: as, &definition) if MetaValidator.shadow_parsing?
44
52
 
45
53
  refuse_second_identity!
46
54
 
47
55
  if definition
48
- raise Malformed,
49
- "#{@name}.identified_by cannot combine a value-object type with a block" unless targets.empty?
56
+ unless targets.empty?
57
+ raise Malformed,
58
+ "#{@name}.identified_by cannot combine a value-object type with a block"
59
+ end
50
60
 
51
61
  type_name = identity_value_object_name
52
62
  value_object =
@@ -56,7 +66,7 @@ module Hecks
56
66
  owner_value_objects: identity_pool,
57
67
  &definition
58
68
  )
59
- rescue NameError => error
69
+ rescue NameError => e
60
70
  # A REMOVED SPELLING MUST REFUSE LOUDLY, not degrade into a raw
61
71
  # Ruby error — the one contract `EraGuard.shadow_parse` leans
62
72
  # on to know a normal parse genuinely could not read this text
@@ -73,11 +83,9 @@ module Hecks
73
83
  # precisely this text.
74
84
  raise Malformed,
75
85
  "#{@name}.identified_by do ... end could not be read as a value-object " \
76
- "definition: #{error.message}"
86
+ "definition: #{e.message}"
77
87
  end
78
- if value_object.attributes.empty?
79
- raise Malformed, "#{@name}.identified_by do declares no identity attributes"
80
- end
88
+ raise Malformed, "#{@name}.identified_by do declares no identity attributes" if value_object.attributes.empty?
81
89
 
82
90
  install_identity_value_object!(value_object)
83
91
  @identity_type_pending = [value_object, as || :identity, attributes.size]
@@ -93,8 +101,10 @@ module Hecks
93
101
 
94
102
  if targets.one?
95
103
  field = targets.first
96
- raise Malformed,
97
- "#{@name}.identified_by takes no as: — name the declared field itself" if as
104
+ if as
105
+ raise Malformed,
106
+ "#{@name}.identified_by takes no as: — name the declared field itself"
107
+ end
98
108
  # Transitional compatibility: the self-hosted language and live
99
109
  # corpus still contain this form. Keep it readable until their
100
110
  # exemplar-led migration is complete; the final lifecycle cutover
@@ -170,8 +180,10 @@ module Hecks
170
180
  if identity_type?(target)
171
181
  @identity_type_pending = [target, as, attributes.size]
172
182
  else
173
- raise Malformed,
174
- "#{@name}.identified_by :#{target} takes no as: — as: only applies to identified_by ValueObject" if as
183
+ if as
184
+ raise Malformed,
185
+ "#{@name}.identified_by :#{target} takes no as: — as: only applies to identified_by ValueObject"
186
+ end
175
187
 
176
188
  @identity_field_pending = target
177
189
  end
@@ -180,7 +192,7 @@ module Hecks
180
192
 
181
193
  raise Malformed, "#{@name}.identified_by names no field" unless path
182
194
 
183
- paths = Ports::Extraction.canonical(path).to_s.split(" ").reject(&:empty?)
195
+ paths = Ports::Extraction.canonical(path).to_s.split.reject(&:empty?)
184
196
  raise Malformed, "#{@name}.identified_by names no field" if paths.empty?
185
197
 
186
198
  @identity_paths = paths
@@ -2,8 +2,13 @@ require_relative "word_gate"
2
2
  module Hecks
3
3
  module Bluebook
4
4
  module DSL
5
+ # Parses a `lifecycle :field, default: ... do transition ... end`
6
+ # block into a `Lifecycle` — the field an aggregate or entity's own
7
+ # state machine lives on, its starting value, and the `command =>
8
+ # target_state` transition table (with an optional `from:` guard)
9
+ # each `transition_impl` call adds a row to.
5
10
  class LifecycleBuilder
6
- GRAMMAR_CONTEXT = "Lifecycle"
11
+ GRAMMAR_CONTEXT = "Lifecycle".freeze
7
12
 
8
13
  include WordGate
9
14
 
@@ -30,6 +35,7 @@ module Hecks
30
35
  end
31
36
 
32
37
  def build
38
+ refuse_ambiguity!
33
39
  Lifecycle.new(field: @field, default: @default, transitions: @transitions)
34
40
  end
35
41
 
@@ -38,6 +44,39 @@ module Hecks
38
44
  builder.instance_eval(&block) if block
39
45
  builder.build
40
46
  end
47
+
48
+ private
49
+
50
+ # C5.3 (docs/semantics/bluebook-semantics.md) — two transitions for
51
+ # one command whose `from:` sets overlap (or where either has no
52
+ # `from:` at all) were silently first-wins; refused where the state
53
+ # machine can be read whole. Two transitions for one command from
54
+ # DISJOINT states are the legitimate shape (`match_transition`
55
+ # picks by the current state) and stay. A `from:` naming a state
56
+ # nothing declares is NOT refused here: it is a reachability
57
+ # finding `bin/model_check` already reports (unreachable state,
58
+ # dead transition), and a bluebook may declare it on purpose.
59
+ def refuse_ambiguity!
60
+ return if MetaValidator.shadow_parsing? # frozen era text is history
61
+
62
+ @transitions.each_with_index do |(command, transition), index|
63
+ earlier = @transitions[0...index].find do |other_command, other|
64
+ other_command == command && overlap?(other, transition)
65
+ end
66
+ next unless earlier
67
+
68
+ raise Malformed,
69
+ "lifecycle :#{@field} declares two transitions for #{command.inspect} from the same state " \
70
+ "(=> #{earlier.last.target.inspect} and => #{transition.target.inspect}) — which one fires " \
71
+ "would be declaration order; give them disjoint from: states"
72
+ end
73
+ end
74
+
75
+ def overlap?(one, other)
76
+ return true if one.from.nil? || other.from.nil?
77
+
78
+ Array(one.from).map(&:to_s).intersect?(Array(other.from).map(&:to_s))
79
+ end
41
80
  end
42
81
  end
43
82
  end
@@ -2,8 +2,14 @@ require_relative "word_gate"
2
2
  module Hecks
3
3
  module Bluebook
4
4
  module DSL
5
+ # Parses a `policy "Name" do ... end` block into a `Policy` — a
6
+ # reaction wired at chapter scope: `on` the event it watches, an
7
+ # optional `where` guard evaluated against the event's own payload, an
8
+ # optional `for_each` fan-out query, and the `trigger` command it
9
+ # dispatches (`with:` projecting the event's payload onto the
10
+ # command's own arguments when the two shapes differ).
5
11
  class PolicyBuilder
6
- GRAMMAR_CONTEXT = "Policy"
12
+ GRAMMAR_CONTEXT = "Policy".freeze
7
13
 
8
14
  include WordGate
9
15
 
@@ -100,6 +106,9 @@ module Hecks
100
106
  "could not be read, so no other runtime could ever evaluate it"
101
107
  end
102
108
 
109
+ # C3.6 — the same PatternSubset check every rule site gets.
110
+ Expression::AstJson.refuse_unshared_patterns!(Expression::AstJson.emit_predicate(canonical),
111
+ owner: @name, word: "where")
103
112
  @where = canonical
104
113
  end
105
114
 
@@ -2,8 +2,13 @@ require_relative "word_gate"
2
2
  module Hecks
3
3
  module Bluebook
4
4
  module DSL
5
+ # Parses a top-level `.port` file's `Hecks.port "Name" do verb "x";
6
+ # signal :effect end` body into a `Port` — the adapter-facing shape (one
7
+ # verb, one signal, an optional method contract in `answers`) a domain
8
+ # calls OUT through, as opposed to `DomainPortBuilder`'s own inbound/
9
+ # outbound operations.
5
10
  class PortBuilder
6
- GRAMMAR_CONTEXT = "Port"
11
+ GRAMMAR_CONTEXT = "Port".freeze
7
12
 
8
13
  include WordGate
9
14
 
@@ -2,8 +2,14 @@ require_relative "word_gate"
2
2
  module Hecks
3
3
  module Bluebook
4
4
  module DSL
5
+ # Parses one `operation`/`tells`/`asks` block inside a `domain_port`
6
+ # into a `PortOperation` — its own attributes (the external fact's
7
+ # payload) plus either the events it `emits` (inbound) or the
8
+ # `answers`/`refuses` pair naming its two possible outcomes (outbound).
9
+ # `to:` records which aggregate the operation routes to as dispatch
10
+ # metadata, never as an attribute — see `to:`'s own comment below.
5
11
  class PortOperationBuilder
6
- GRAMMAR_CONTEXT = "PortOperation"
12
+ GRAMMAR_CONTEXT = "PortOperation".freeze
7
13
 
8
14
  include AttributeCollector
9
15
  include WordGate
@@ -93,9 +99,11 @@ module Hecks
93
99
  # AN INBOUND OPERATION STILL HAS TO SAY SOMETHING. Only inbound: an
94
100
  # `asks` says it with `answers`/`refuses` instead, and
95
101
  # `refuse_wrong_words!` above has already insisted on both.
96
- raise Malformed,
97
- "#{@name} declares no emits — an operation with nothing to say " \
98
- "afterward is a call into nothing" if !outbound && @emits.empty?
102
+ if !outbound && @emits.empty?
103
+ raise Malformed,
104
+ "#{@name} declares no emits an operation with nothing to say " \
105
+ "afterward is a call into nothing"
106
+ end
99
107
 
100
108
  operation
101
109
  end
@@ -119,18 +127,31 @@ module Hecks
119
127
  # runtime cannot keep.
120
128
  def refuse_wrong_words!(outbound)
121
129
  if outbound
122
- raise Malformed, "#{@name} is an asks and declares emits — name its two endings with " \
123
- "answers and refuses instead" unless @emits.empty?
124
- raise Malformed, "#{@name} declares no answers an ask with no word for what came " \
125
- "back cannot be reacted to" unless @answers
126
- raise Malformed, "#{@name} declares no refuses — an ask that cannot fail is a call " \
127
- "into a system you do not control, pretending otherwise" unless @refuses
130
+ unless @emits.empty?
131
+ raise Malformed, "#{@name} is an asks and declares emits — name its two endings with " \
132
+ "answers and refuses instead"
133
+ end
134
+ unless @answers
135
+ raise Malformed, "#{@name} declares no answers an ask with no word for what came " \
136
+ "back cannot be reacted to"
137
+ end
138
+ unless @refuses
139
+ raise Malformed, "#{@name} declares no refuses — an ask that cannot fail is a call " \
140
+ "into a system you do not control, pretending otherwise"
141
+ end
128
142
  else
129
- raise Malformed, "#{@name} is a tells and declares #{@answers ? 'answers' : 'refuses'} — " \
130
- "an inbound fact has no channel back to whoever sent it" if @answers || @refuses
143
+ if @answers || @refuses
144
+ raise Malformed, "#{@name} is a tells and declares #{@answers ? 'answers' : 'refuses'} " \
145
+ "an inbound fact has no channel back to whoever sent it"
146
+ end
131
147
  end
132
148
  end
133
149
 
150
+ # `private` above (scoping the instance methods between it and here)
151
+ # doesn't reach a singleton method — correctly so: `.build` is this
152
+ # builder's real public entry point (DomainPortBuilder calls it),
153
+ # never meant to be private.
154
+ # rubocop:disable-next Lint/IneffectiveAccessModifier
134
155
  def self.build(name, to: nil, owner: nil, direction: :inbound, &block)
135
156
  builder = new(name, to: to, owner: owner, direction: direction)
136
157
  builder.instance_eval(&block) if block