hecks 0.3.0 → 1.0.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 (194) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/d1.rb +38 -14
  3. data/lib/hecks/adapters/driven/governance_authorization.rb +41 -2
  4. data/lib/hecks/adapters/driven/heki/journal.rb +7 -2
  5. data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
  6. data/lib/hecks/adapters/driven/heki/snapshot.rb +31 -4
  7. data/lib/hecks/adapters/driven/heki.rb +40 -9
  8. data/lib/hecks/adapters/driven/lambda.rb +16 -2
  9. data/lib/hecks/adapters/driven/postgres/codec.rb +8 -0
  10. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +45 -6
  11. data/lib/hecks/adapters/driven/postgres.rb +76 -29
  12. data/lib/hecks/adapters/driven/postgres_era.adapter +11 -0
  13. data/lib/hecks/adapters/driven/sqlite/codec.rb +17 -0
  14. data/lib/hecks/adapters/driven/sqlite/projection.rb +76 -9
  15. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +17 -5
  16. data/lib/hecks/adapters/driven/sqlite.rb +30 -9
  17. data/lib/hecks/adapters/driven.rb +19 -1
  18. data/lib/hecks/behaviors/dsl.rb +29 -0
  19. data/lib/hecks/behaviors/expectations.rb +62 -2
  20. data/lib/hecks/bluebook/assembly/contracts.rb +36 -7
  21. data/lib/hecks/bluebook/assembly/marks.rb +4 -3
  22. data/lib/hecks/bluebook/assembly.rb +14 -1
  23. data/lib/hecks/bluebook/behaviour/lifecycle.rb +18 -1
  24. data/lib/hecks/bluebook/behaviour/process_manager.rb +14 -1
  25. data/lib/hecks/bluebook/chapter.rb +21 -11
  26. data/lib/hecks/bluebook/command.rb +1 -1
  27. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +117 -5
  28. data/lib/hecks/bluebook/dsl/attribute_collector.rb +21 -0
  29. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +71 -2
  30. data/lib/hecks/bluebook/dsl/command_builder.rb +91 -3
  31. data/lib/hecks/bluebook/dsl/entity_builder.rb +129 -4
  32. data/lib/hecks/bluebook/dsl/policy_builder.rb +18 -3
  33. data/lib/hecks/bluebook/dsl/port_builder.rb +12 -3
  34. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +109 -10
  35. data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -0
  36. data/lib/hecks/bluebook/dsl/word_gate.rb +9 -2
  37. data/lib/hecks/bluebook/dsl/world_builder.rb +44 -4
  38. data/lib/hecks/bluebook/expression/canonical_form.rb +71 -3
  39. data/lib/hecks/bluebook/expression/evaluator.rb +50 -7
  40. data/lib/hecks/bluebook/expression/projection.json +48 -0
  41. data/lib/hecks/bluebook/expression/resolver.rb +161 -10
  42. data/lib/hecks/bluebook/hexagon.rb +1 -1
  43. data/lib/hecks/bluebook/meta_validator/judge.rb +78 -16
  44. data/lib/hecks/bluebook/meta_validator/port_judge.rb +4 -0
  45. data/lib/hecks/bluebook/meta_validator/readings.rb +14 -4
  46. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +42 -3
  47. data/lib/hecks/bluebook/meta_validator/shapes.rb +30 -10
  48. data/lib/hecks/bluebook/meta_validator.rb +103 -13
  49. data/lib/hecks/bluebook/model_check.rb +132 -5
  50. data/lib/hecks/bluebook/pattern_subset.rb +66 -2
  51. data/lib/hecks/bluebook/process_manager.rb +53 -11
  52. data/lib/hecks/bluebook/value_object.rb +9 -1
  53. data/lib/hecks/doc/reference.rb +22 -1
  54. data/lib/hecks/facade/cli_door.rb +6 -3
  55. data/lib/hecks/facade/json_door.rb +16 -4
  56. data/lib/hecks/forms/app.rb +47 -6
  57. data/lib/hecks/forms/command_form_renderer.rb +1 -1
  58. data/lib/hecks/forms/field_renderer.rb +11 -4
  59. data/lib/hecks/forms/html.rb +31 -0
  60. data/lib/hecks/forms/params.rb +30 -1
  61. data/lib/hecks/forms/port_argument.rb +46 -0
  62. data/lib/hecks/forms/record_renderer.rb +6 -2
  63. data/lib/hecks/forms/record_table.rb +6 -1
  64. data/lib/hecks/framework/bluebook/console_settings.bluebook +19 -19
  65. data/lib/hecks/framework/bluebook/governance.bluebook +26 -11
  66. data/lib/hecks/framework/bluebook/identity.bluebook +2 -2
  67. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
  68. data/lib/hecks/fuzzing/isolated_boot.rb +212 -18
  69. data/lib/hecks/fuzzing/properties.rb +52 -6
  70. data/lib/hecks/fuzzing/replay.rb +51 -18
  71. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +28 -2
  72. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +71 -6
  73. data/lib/hecks/fuzzing/sequence_generator.rb +5 -4
  74. data/lib/hecks/fuzzing/value_generator.rb +25 -2
  75. data/lib/hecks/grammar/evolve.rb +33 -0
  76. data/lib/hecks/grammar/expression.bluebook +8 -5
  77. data/lib/hecks/grammar/expression_operators.json +497 -1
  78. data/lib/hecks/language/bluebook/aggregate.bluebook +12 -12
  79. data/lib/hecks/language/bluebook/bluebook.bluebook +3 -3
  80. data/lib/hecks/language/bluebook/command.bluebook +42 -8
  81. data/lib/hecks/language/bluebook/entity.bluebook +86 -10
  82. data/lib/hecks/language/bluebook/policy.bluebook +21 -4
  83. data/lib/hecks/language/bluebook/process_manager.bluebook +135 -18
  84. data/lib/hecks/language/bluebook/projection.bluebook +6 -6
  85. data/lib/hecks/language/bluebook/query.bluebook +4 -4
  86. data/lib/hecks/language/bluebook/shape.bluebook +6 -6
  87. data/lib/hecks/language/bluebook/syntax.bluebook +12 -11
  88. data/lib/hecks/language/bluebook/vocabulary.bluebook +22 -7
  89. data/lib/hecks/language/oidc.json +20 -0
  90. data/lib/hecks/language/port.bluebook +30 -2
  91. data/lib/hecks/naming.rb +54 -1
  92. data/lib/hecks/ports/access_control.port +7 -2
  93. data/lib/hecks/ports/access_control.rb +1 -1
  94. data/lib/hecks/ports/agent.port +6 -2
  95. data/lib/hecks/ports/agent.rb +1 -1
  96. data/lib/hecks/ports/authentication.port +4 -2
  97. data/lib/hecks/ports/authentication.rb +1 -1
  98. data/lib/hecks/ports/authorization.port +5 -2
  99. data/lib/hecks/ports/authorization.rb +14 -11
  100. data/lib/hecks/ports/clock.port +3 -2
  101. data/lib/hecks/ports/clock.rb +1 -1
  102. data/lib/hecks/ports/extraction.port +3 -2
  103. data/lib/hecks/ports/extraction.rb +1 -1
  104. data/lib/hecks/ports/identity_assignment.port +3 -2
  105. data/lib/hecks/ports/identity_assignment.rb +1 -1
  106. data/lib/hecks/ports/identity_generation.port +3 -2
  107. data/lib/hecks/ports/identity_generation.rb +1 -1
  108. data/lib/hecks/ports/identity_resolution.port +3 -2
  109. data/lib/hecks/ports/identity_resolution.rb +1 -1
  110. data/lib/hecks/ports/persistence/append_only.rb +40 -4
  111. data/lib/hecks/ports/persistence/execution.rb +6 -1
  112. data/lib/hecks/ports/persistence/plugin.rb +54 -0
  113. data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_check.rb +41 -8
  114. data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_guard.rb +24 -56
  115. data/lib/hecks/ports/persistence/{lineage.rb → plugins/era/lineage.rb} +31 -4
  116. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/era_store.rb +3 -3
  117. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/head_compiler.rb +59 -10
  118. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/mint_transaction.rb +2 -2
  119. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/provisioning.rb +29 -1
  120. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/tail_merge.rb +11 -4
  121. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/transform_installer.rb +20 -0
  122. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage.rb +1 -1
  123. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/coverage_check.rb +5 -5
  124. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/era_resolver.rb +5 -2
  125. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/merge_coordinator.rb +2 -2
  126. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/minter.rb +4 -4
  127. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager.rb +2 -2
  128. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era.rb +116 -29
  129. data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/approval_digest.rb +1 -1
  130. data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/layer_one.rb +14 -5
  131. data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/layer_two.rb +31 -6
  132. data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/unfed_report.rb +5 -1
  133. data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/reattest.rb +3 -3
  134. data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/differ.rb +1 -1
  135. data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
  136. data/lib/hecks/ports/persistence.rb +1 -1
  137. data/lib/hecks/ports/projection.rb +20 -0
  138. data/lib/hecks/projections/diagrams.rb +230 -1
  139. data/lib/hecks/projections/shape.rb +15 -2
  140. data/lib/hecks/projector/cli_projector.rb +10 -1
  141. data/lib/hecks/projector/exporter.rb +19 -2
  142. data/lib/hecks/query_ir.rb +19 -0
  143. data/lib/hecks/query_specification/common/null_policy.rb +13 -1
  144. data/lib/hecks/query_specification/field_path.rb +20 -2
  145. data/lib/hecks/query_specification/hop_path.rb +7 -5
  146. data/lib/hecks/runtime/aggregate_lock.rb +45 -0
  147. data/lib/hecks/runtime/boot_gates.rb +41 -0
  148. data/lib/hecks/runtime/caller.rb +19 -3
  149. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +13 -2
  150. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -0
  151. data/lib/hecks/runtime/command_interpreter.rb +97 -13
  152. data/lib/hecks/runtime/command_rules/admissibility.rb +64 -14
  153. data/lib/hecks/runtime/command_rules/arithmetic.rb +7 -1
  154. data/lib/hecks/runtime/command_rules/authorization.rb +2 -1
  155. data/lib/hecks/runtime/command_rules/references.rb +27 -19
  156. data/lib/hecks/runtime/dependency_planning.rb +14 -0
  157. data/lib/hecks/runtime/dispatcher.rb +19 -4
  158. data/lib/hecks/runtime/entity_interpreter.rb +85 -14
  159. data/lib/hecks/runtime/errors.rb +22 -0
  160. data/lib/hecks/runtime/identity.rb +30 -2
  161. data/lib/hecks/runtime/instance.rb +59 -4
  162. data/lib/hecks/runtime/interpreting.rb +21 -0
  163. data/lib/hecks/runtime/loader.rb +59 -18
  164. data/lib/hecks/runtime/query_interpreter.rb +36 -4
  165. data/lib/hecks/runtime/reaction_invocation.rb +9 -1
  166. data/lib/hecks/runtime/read_model_interpreter.rb +76 -1
  167. data/lib/hecks/runtime/refusal_wording.rb +2 -0
  168. data/lib/hecks/runtime/registry/saga_persistence.rb +75 -3
  169. data/lib/hecks/runtime/registry/verification.rb +88 -0
  170. data/lib/hecks/runtime/registry.rb +69 -8
  171. data/lib/hecks/runtime/saga_interpreter.rb +215 -13
  172. data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
  173. data/lib/hecks/runtime/value/admission.rb +19 -1
  174. data/lib/hecks/runtime/value/coercion.rb +75 -10
  175. data/lib/hecks/runtime.rb +17 -5
  176. data/lib/hecks/storehouse.rb +632 -0
  177. data/lib/hecks/version.rb +1 -1
  178. data/lib/hecks/vocabulary.rb +6 -1
  179. data/lib/hecks.rb +7 -2
  180. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
  181. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
  182. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
  183. metadata +48 -37
  184. /data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_guard/shape_diff.rb +0 -0
  185. /data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_tamper.rb +0 -0
  186. /data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/field_cache.rb +0 -0
  187. /data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/resumable_backfill.rb +0 -0
  188. /data/lib/hecks/{runtime → ports/persistence/plugins/era}/storage_shape.rb +0 -0
  189. /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit.rb +0 -0
  190. /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/rule_compiler.rb +0 -0
  191. /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/renderer.rb +0 -0
  192. /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/writer.rb +0 -0
  193. /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold.rb +0 -0
  194. /data/lib/hecks/{translation.rb → ports/persistence/plugins/era/translation.rb} +0 -0
@@ -10,7 +10,8 @@ module Hecks
10
10
  include RuleReference
11
11
  include WordGate
12
12
 
13
- def initialize(name, chapter_named_givens: {}, chapter_pending_givens: [])
13
+ def initialize(name, chapter_named_givens: {}, chapter_pending_givens: [],
14
+ chapter_entity_named_givens: {}, chapter_entity_pending_givens: [])
14
15
  @name = name
15
16
  @value_objects = []
16
17
  @commands = []
@@ -37,6 +38,15 @@ module Hecks
37
38
  # here and `BluebookBuilder#resolve_pending_chapter_givens!`
38
39
  # for where it drains.
39
40
  @chapter_pending_givens = chapter_pending_givens
41
+ # ONE LEVEL WIDER STILL, PAST THE CHAPTER'S OWN AGGREGATE-LEVEL
42
+ # POOL — the chapter's own entity-scoped pool, threaded from
43
+ # `BluebookBuilder#aggregate_impl` the same way
44
+ # `@chapter_named_givens` is, and passed straight through
45
+ # (unchanged) to every top-level piece this aggregate builds
46
+ # (`#drain_pending!`). See `EntityBuilder#given_impl`'s own
47
+ # comment for what this closes.
48
+ @chapter_entity_named_givens = chapter_entity_named_givens
49
+ @chapter_entity_pending_givens = chapter_entity_pending_givens
40
50
  # DEFERRED CONSTRUCTION — `entity`/`command`/`query` push a
41
51
  # pending descriptor here instead of building immediately; see
42
52
  # `#drain_pending!`'s own comment for why.
@@ -419,6 +429,7 @@ module Hecks
419
429
  seal_defaults
420
430
  seal_lifecycle_guards
421
431
  seal_projected_fields
432
+ seal_correction_targets
422
433
 
423
434
  ir = Aggregate.new(
424
435
  name: @name,
@@ -445,8 +456,11 @@ module Hecks
445
456
  ir
446
457
  end
447
458
 
448
- def self.build(name, chapter_named_givens: {}, chapter_pending_givens: [], &block)
449
- builder = new(name, chapter_named_givens: chapter_named_givens, chapter_pending_givens: chapter_pending_givens)
459
+ def self.build(name, chapter_named_givens: {}, chapter_pending_givens: [],
460
+ chapter_entity_named_givens: {}, chapter_entity_pending_givens: [], &block)
461
+ builder = new(name, chapter_named_givens: chapter_named_givens, chapter_pending_givens: chapter_pending_givens,
462
+ chapter_entity_named_givens: chapter_entity_named_givens,
463
+ chapter_entity_pending_givens: chapter_entity_pending_givens)
450
464
  builder.instance_eval(&block) if block
451
465
  builder.build
452
466
  end
@@ -496,6 +510,9 @@ module Hecks
496
510
  owner_named_givens: @entity_named_givens,
497
511
  identity_name_prefix: "#{Naming.demodulise(@name)}#{Naming.demodulise(name)}",
498
512
  identity_value_object_installer: ->(value_object) { @value_objects << value_object },
513
+ aggregate_name: @name,
514
+ chapter_entity_named_givens: @chapter_entity_named_givens,
515
+ chapter_entity_pending_givens: @chapter_entity_pending_givens,
499
516
  &block)
500
517
  end
501
518
 
@@ -609,7 +626,17 @@ module Hecks
609
626
  # keys: a default that is a Hash is left to `Value.for_attribute`, which
610
627
  # is where a wrong FIELD belongs.
611
628
  def seal_defaults
612
- shapes = @value_objects.map { |shape| shape.hecks_name.to_s }
629
+ # `closed_sets` TOO, not only `@value_objects` the exact gap
630
+ # this method's own comment names: an inline `one_of(...)`
631
+ # synthesises its value object through `closed_sets`
632
+ # (AttributeCollector#synthesise_closed_set), never installed
633
+ # into `@value_objects` until `#build` merges them (see
634
+ # `#build`'s own `@value_objects + closed_sets`, and
635
+ # `declared_value_object`'s identical merge). Checking
636
+ # `@value_objects` alone made this exact attribute — a bare
637
+ # default on an inline closed set — invisible to the one
638
+ # check meant to catch it.
639
+ shapes = (@value_objects + closed_sets).map { |shape| shape.hecks_name.to_s }
613
640
 
614
641
  attributes.each do |attribute|
615
642
  next if attribute.default.nil? || attribute.default.is_a?(Hash)
@@ -683,7 +710,10 @@ module Hecks
683
710
  # #step_delegate_to_entity`, which refuses a real one that
684
711
  # names no such entity or command). Sealing THIS check
685
712
  # against it would refuse every delegating command outright.
686
- next if mutation.op == :delegate
713
+ # `:corrects` CommandBuilder#corrects_impl's own comment —
714
+ # targets an EVENT name, not a field either; checked instead
715
+ # by `seal_correction_targets`, below.
716
+ next if [:delegate, :corrects].include?(mutation.op)
687
717
  next if known.include?(mutation.target.to_sym)
688
718
 
689
719
  raise Malformed,
@@ -694,6 +724,88 @@ module Hecks
694
724
  end
695
725
  end
696
726
 
727
+ # `corrects` — CommandBuilder#corrects_impl's own comment. Runs
728
+ # once every command in the aggregate is known (the same reason
729
+ # this is a `seal_*` step rather than living in `corrects_impl`
730
+ # itself — a command cannot see its own siblings' `emits` while
731
+ # it is still being built). Two things are checked:
732
+ #
733
+ # 1. The named event must be something a SIBLING command here
734
+ # actually `emits` — naming an event nothing in this aggregate
735
+ # ever announces is a build-time authoring error. (Whether
736
+ # THIS record has actually emitted it YET is the dispatch-time
737
+ # half — CommandRules::Admissibility#enforce_correction_target.)
738
+ #
739
+ # 2. `reverses: true` derives the corrective `sets` from the
740
+ # ORIGINAL command's own mutations, rather than the author
741
+ # writing them — but only when every one of those mutations is
742
+ # STRUCTURALLY invertible with no runtime data: increment/
743
+ # decrement, same argument, opposite verb (`sign_for`'s own
744
+ # +1/-1 pair — CommandRules::Arithmetic applies `current +
745
+ # sign * amount`, so the SAME source with the OPPOSITE sign
746
+ # undoes it exactly). Nothing else qualifies today: `set` has
747
+ # no such rule at all — inverting it needs the SPECIFIC prior
748
+ # value at the moment the original fired, which is per-
749
+ # instance runtime data no build-time derivation can have;
750
+ # `multiply`/`clamp` are lossy by design (a clamped value's
751
+ # own pre-clamp magnitude is not recoverable from the mutation
752
+ # at all); `append`/`remove` LOOK symmetric but are not
753
+ # reliably so — `append`'s source is a per-field binding hash
754
+ # (`append: { name: :name, amount: :amount }`), `remove`'s is
755
+ # a single resolved value to match by equality
756
+ # (MutationApplier#removed), and collapsing one shape into the
757
+ # other correctly needs the target list's own value-object
758
+ # field names, not just the mutation's own recorded shape — a
759
+ # real gap, left for a follow-on round rather than guessed at
760
+ # here. Refuses rather than silently deriving something wrong
761
+ # — see docs/decisions/ for the ADR that draws this exact
762
+ # line.
763
+ def seal_correction_targets
764
+ inverse_op = { increment: :decrement, decrement: :increment }
765
+ emitted_by = Hash.new { |hash, key| hash[key] = [] }
766
+ @commands.each { |command| command.emits.each { |event_name| emitted_by[event_name] << command } }
767
+
768
+ @commands.each do |command|
769
+ correction = command.mutations.find { |mutation| mutation.op == :corrects }
770
+ next unless correction
771
+
772
+ event = correction.target
773
+ sources = emitted_by[event]
774
+ if sources.empty?
775
+ raise Malformed,
776
+ "#{@name}.#{command.hecks_name} corrects #{event.inspect}, but nothing " \
777
+ "declared on #{@name} ever emits it — corrects names a fact this " \
778
+ "aggregate actually announces, not an aspiration"
779
+ end
780
+
781
+ next unless correction.source[:reverses]
782
+
783
+ own_mutations = command.mutations.reject { |mutation| mutation.op == :corrects }
784
+ if own_mutations.any?
785
+ raise Malformed,
786
+ "#{@name}.#{command.hecks_name} declares both corrects #{event.inspect}, " \
787
+ "reverses: true AND its own sets — reverses: true means the correction " \
788
+ "is DERIVED; write one or the other, never both"
789
+ end
790
+
791
+ derived = sources.flat_map(&:mutations).reject { |mutation| mutation.op == :corrects }
792
+ unsupported = derived.reject { |mutation| inverse_op.key?(mutation.op) }
793
+ if unsupported.any?
794
+ raise Malformed,
795
+ "#{@name}.#{command.hecks_name} corrects #{event.inspect}, reverses: " \
796
+ "true, but the command(s) that emit it use " \
797
+ "#{unsupported.map(&:op).uniq.join(', ')} — not statically invertible " \
798
+ "(set needs the specific prior value, multiply/clamp are lossy) — " \
799
+ "declare the corrective sets by hand instead"
800
+ end
801
+
802
+ derived.each do |mutation|
803
+ command.mutations << Mutation.new(target: mutation.target, op: inverse_op.fetch(mutation.op),
804
+ source: mutation.source)
805
+ end
806
+ end
807
+ end
808
+
697
809
  # A query must ask about a field the aggregate actually HAS — the same
698
810
  # seal `then_set` gets, closing the same silence: a where over a field
699
811
  # nothing declares matches nothing and refuses nothing, forever, on
@@ -62,6 +62,8 @@ module Hecks
62
62
  admits: nil, one_of: nil)
63
63
  # moved to the language: FieldName invariant, on Root.Attribute
64
64
 
65
+ refuse_duplicate_attribute!(name)
66
+
65
67
  if type.equal?(UNSET)
66
68
  raise Malformed, "#{name} declares no type — attribute :#{name}, SomeType is required, " \
67
69
  "there is no default"
@@ -156,6 +158,7 @@ module Hecks
156
158
  private
157
159
 
158
160
  def relationship_attribute(target, kind, name, optional: false, list: false)
161
+ refuse_duplicate_attribute!(name)
159
162
  attributes << Attribute.new(
160
163
  name: name,
161
164
  type: Reference.new(target),
@@ -183,6 +186,24 @@ module Hecks
183
186
  "with the type-position one_of(...) instead"
184
187
  end
185
188
 
189
+ # A NAME DECLARED TWICE ON THE SAME OWNER IS TWO ATTRIBUTES SHARING
190
+ # ONE NAME, and nothing downstream disambiguates them — every
191
+ # reader that walks `attributes` looking for one by name
192
+ # (`seal_mutation_targets`, `seal_query_field`, `projects`'s own
193
+ # local check, `Instance#[]`, ...) uses `Array#find`/`any?`, which
194
+ # silently answers whichever declaration happens to come first and
195
+ # discards the second. Used to boot clean and stay that way : both
196
+ # declarations survived into the IR, one of them permanently
197
+ # unreachable by name. Refused HERE, at the one place every owner
198
+ # (Aggregate/Entity/Command/Query/PortOperation/ValueObject, each
199
+ # `include AttributeCollector`) mints an attribute through, rather
200
+ # than taught to each of those readers individually.
201
+ def refuse_duplicate_attribute!(name)
202
+ return unless attributes.any? { |attribute| attribute.name == name }
203
+
204
+ raise Malformed, "#{name} is declared twice — an attribute name is declared once, not twice"
205
+ end
206
+
186
207
  # A pattern is refused AT DECLARATION, not when a value first meets it :
187
208
  # a regex whose meaning depends on which engine reads it is a defect in
188
209
  # the bluebook, and a bluebook that loads is one whose patterns carry
@@ -28,6 +28,13 @@ module Hecks
28
28
  # what queues here and `#resolve_pending_chapter_givens!`,
29
29
  # below, for where it drains.
30
30
  @chapter_pending_givens = []
31
+ # ONE LEVEL WIDER STILL — the CHAPTER-WIDE, ENTITY-SCOPED pool
32
+ # (the piece analogue of `@chapter_named_givens`, above). See
33
+ # `EntityBuilder#given_impl`'s own comment for what this
34
+ # closes; `docs/implemented/resolution-rules/
35
+ # chapter-entity-given.md` for the full algorithm.
36
+ @chapter_entity_named_givens = {}
37
+ @chapter_entity_pending_givens = []
31
38
  end
32
39
 
33
40
  # Chapter metadata belongs to the composed folder, not whichever file
@@ -85,8 +92,10 @@ module Hecks
85
92
  # chapter's own top-level shape is written with it), so also
86
93
  # named in GenericDispatch::BOOTSTRAP_CALLS_FALLBACK.
87
94
  def aggregate_impl(name, &block)
88
- @aggregates << AggregateBuilder.build(name, chapter_named_givens: @chapter_named_givens,
89
- chapter_pending_givens: @chapter_pending_givens, &block)
95
+ @aggregates << AggregateBuilder.build(name, chapter_named_givens: @chapter_named_givens,
96
+ chapter_pending_givens: @chapter_pending_givens,
97
+ chapter_entity_named_givens: @chapter_entity_named_givens,
98
+ chapter_entity_pending_givens: @chapter_entity_pending_givens, &block)
90
99
  end
91
100
 
92
101
  # `read_model` is the word (ADR 0025 reverts `report` — the IR
@@ -143,6 +152,7 @@ module Hecks
143
152
  # `validate_assembled!` there — nothing downstream should ever
144
153
  # read an unresolved placeholder's fields.
145
154
  resolve_pending_chapter_givens! unless MetaValidator.deferring?
155
+ resolve_pending_chapter_entity_givens! unless MetaValidator.deferring?
146
156
 
147
157
  # A CHAPTER MAY BE SPLIT ACROSS FILES (see `self.build`'s own
148
158
  # comment). Every check below needs the WHOLE chapter present —
@@ -225,6 +235,47 @@ module Hecks
225
235
  end
226
236
  private :resolve_pending_chapter_given
227
237
 
238
+ # THE ENTITY-SCOPED ANALOGUE, one level down — see
239
+ # `#resolve_pending_chapter_givens!`'s own comment; identical
240
+ # shape, resolved against `@chapter_entity_named_givens` instead.
241
+ def resolve_pending_chapter_entity_givens!
242
+ @chapter_entity_pending_givens.each do |entry|
243
+ resolved = resolve_pending_chapter_entity_given(entry)
244
+ entry[:placeholder].description = resolved.description
245
+ entry[:placeholder].canonical = resolved.canonical
246
+ entry[:placeholder].predicate = resolved.predicate
247
+ end
248
+ @chapter_entity_pending_givens.clear
249
+ end
250
+
251
+ def resolve_pending_chapter_entity_given(entry)
252
+ description = entry[:description]
253
+ candidates = RuleReference.resolve_owner_keyed(@chapter_entity_named_givens, description)
254
+
255
+ if entry[:declared_by]
256
+ candidates[entry[:declared_by]] ||
257
+ raise(Malformed,
258
+ "#{entry[:entity]}'s given #{description.inspect} names no precondition " \
259
+ "#{entry[:declared_by]} declares in this chapter — #{entry[:declared_by]} " \
260
+ "either hasn't declared #{description.inspect}, or declared_by: named the " \
261
+ "wrong piece")
262
+ elsif candidates.size == 1
263
+ candidates.values.first
264
+ elsif candidates.empty?
265
+ raise(Malformed,
266
+ "#{entry[:entity]}'s given #{description.inspect} names no precondition " \
267
+ "any piece in this chapter ever declares — declare it once with a block " \
268
+ "(some piece's own given(#{description.inspect}) { ... })")
269
+ else
270
+ raise(Malformed,
271
+ "#{entry[:entity]}'s given #{description.inspect} is ambiguous across the " \
272
+ "chapter's own pieces — #{candidates.keys.join(', ')} each declare a DIFFERENT " \
273
+ "predicate under this same description; name which one with declared_by: (e.g. " \
274
+ "given(#{description.inspect}, declared_by: #{candidates.keys.first.inspect}))")
275
+ end
276
+ end
277
+ private :resolve_pending_chapter_entity_given
278
+
228
279
  # EVERY WHOLE-CHAPTER CHECK, IN ONE PLACE — the battery `#build`
229
280
  # used to run inline, now a pure function of an assembled
230
281
  # `Bluebook::Chapter` so `MetaValidator.judge_deferred!` can run
@@ -862,6 +913,24 @@ module Hecks
862
913
  # nothing further to check once it matches by name.
863
914
  return if remote_attribute.nil? && target.lifecycle&.field.to_s == plan.tail
864
915
 
916
+ # A PROJECTION MAY CHAIN THROUGH ANOTHER PROJECTION (S12, ADR
917
+ # 0025's own boundary rule, followed through) — `target`'s
918
+ # OWN projected fields live in `projected_fields`, a
919
+ # separate list from `attributes`, so a match there is
920
+ # invisible to the check above even though it names a real,
921
+ # always-current, stored field. `Transfer.projects
922
+ # :source_customer_status, from: :"source.customer_status"`
923
+ # reads Account's own already-projected `customer_status`
924
+ # this way — Account is one hop from Customer, Transfer is
925
+ # one hop from Account, and neither aggregate needs to know
926
+ # about the other's target two hops away. A projected
927
+ # field's remote value is always a scalar by construction
928
+ # (`RebuildSweep.remote_value` never copies a reference, a
929
+ # value object, or a list), so nothing further to check once
930
+ # it matches by name — same reasoning the lifecycle
931
+ # fallback just above already applies.
932
+ return if remote_attribute.nil? && target.projected_fields.any? { |f| f.name.to_s == plan.tail }
933
+
865
934
  unless remote_attribute
866
935
  raise Malformed,
867
936
  "#{aggregate.hecks_name}.projects :#{field.name} reads #{target.hecks_name}'s own " \
@@ -297,7 +297,7 @@ module Hecks
297
297
  end
298
298
 
299
299
  op, source = named.first
300
- @mutations << Mutation.new(target: target.to_sym, op: op, source: source)
300
+ @mutations << Mutation.new(target: target.to_sym, op: op, source: normalize_append_source(op, source))
301
301
  end
302
302
 
303
303
  # LEGACY UNDER SHADOW-PARSING (S0a's own bridge) — frozen era text
@@ -319,8 +319,23 @@ module Hecks
319
319
  # enforces it. This is the first rule to move ACROSS rather than be
320
320
  # duplicated : delete the declaration and an unnamed event is accepted,
321
321
  # which is what makes the meta-domain load-bearing rather than decorative.
322
+ #
323
+ # BARE CONSTANT ACCEPTED (ADR 0025, S6 — "events first-class"),
324
+ # `emits Account::AccountFrozen`, resolved through `ConstShim` the
325
+ # same way `trigger`/`dispatch` already resolve a command
326
+ # reference (`Naming.event_ref`, that method's own header). NOT
327
+ # yet a REQUIRED spelling, deliberately, unlike `trigger`/
328
+ # `dispatch`'s own quoted-text refusal: those were safe to refuse
329
+ # only because command references are already 100% migrated
330
+ # across the live corpus (verified 2026-08-27) — `emits`/`on`
331
+ # are not, so refusing the quoted form here would break every
332
+ # live `.bluebook` site this pass didn't touch, not just frozen
333
+ # era text `shadow_parse` exists to keep readable. Both forms
334
+ # are accepted in live source until a full corpus migration
335
+ # lands and the same refusal this file's `reference_to`/
336
+ # `trigger_impl` already carry can be added here safely.
322
337
  def emits(event_name)
323
- @emits << event_name.to_s
338
+ @emits << Naming.event_ref(event_name)
324
339
  end
325
340
 
326
341
  # THE RECORD'S OWN VALUE AS A MUTATION SOURCE — `sets :positions,
@@ -412,6 +427,57 @@ module Hecks
412
427
  @mutations << Mutation.new(target: target.to_s, op: :delegate, source: with)
413
428
  end
414
429
 
430
+ # A COMMAND DECLARING WHAT PAST FACT IT AMENDS — the append-only
431
+ # answer to "what if this record's history turns out to have been
432
+ # wrong": never rewrite the original event (the log stays exactly
433
+ # what it was), always append a NEW fact on top. `event` names the
434
+ # event this command corrects; `as:` optionally binds the located
435
+ # instance for a `given`/`ensures` to reference, the same shape
436
+ # `ensures`'s own `old` binding already has; `reason:` is not
437
+ # descriptive-only the way `goal` is — it is carried as data, the
438
+ # one thing an audit trail actually needs ("we corrected this, and
439
+ # here is why"), refused when blank the same way a `given`'s own
440
+ # description is required to say something.
441
+ #
442
+ # STORED AS A MUTATION, not a new Command field — see the
443
+ # KeywordSeed row's own comment (command.bluebook) for why: this
444
+ # is the exact same choice `delegates_to` already made, for the
445
+ # exact same reason. Rides the SAME multi-binding wire shape
446
+ # `append`/`delegate` use — `as:`/`reason:`/`reverses:` assembled
447
+ # by hand into one `source` hash, the way `sets_impl` assembles up
448
+ # to seven kwargs into one `named` hash above.
449
+ #
450
+ # `reverses: true` NAMES an intent to auto-derive the corrective
451
+ # `sets` from the original event's own mutations, rather than the
452
+ # author writing it — see `AggregateBuilder#seal_correction_targets`,
453
+ # where that derivation actually happens (it needs every sibling
454
+ # command in the aggregate already known, which this builder alone
455
+ # cannot see). MUTUALLY EXCLUSIVE with an explicit `sets` on the
456
+ # same command — two ways of saying the same thing is exactly the
457
+ # redundancy `sets`'s own omittable-`to:` rule refuses elsewhere.
458
+ #
459
+ # `as:` IS ALWAYS STORED AS TEXT, never left a bare Symbol —
460
+ # `Mutation#classified_source`/`#appended_fields` (Behaviour::
461
+ # Mutation) classify any bare Symbol field as `kind: "argument"`,
462
+ # meaning "resolve this against one of THIS command's own declared
463
+ # attributes at dispatch time" (append/delegate's own meaning for a
464
+ # Symbol). `as:` names no such thing — it is a plain label, not yet
465
+ # wired into the expression evaluator (a future round's work, once
466
+ # a real runtime consumer exists) — so coercing it to a String here
467
+ # keeps it out of that machinery entirely rather than silently
468
+ # miscategorised as an unresolvable argument reference.
469
+ def corrects_impl(event, as: nil, reason: nil, reverses: false)
470
+ if reason.to_s.strip.empty?
471
+ raise Malformed,
472
+ "#{@name}'s corrects #{event.inspect} names no reason — a correction " \
473
+ "is carried as data (an audit trail needs to say WHY), the same way a " \
474
+ "given's own description must say something"
475
+ end
476
+
477
+ @mutations << Mutation.new(target: event.to_s, op: :corrects,
478
+ source: { as: as&.to_s, reason: reason.to_s, reverses: reverses })
479
+ end
480
+
415
481
  def build
416
482
  resolve_implicit_attributes!
417
483
 
@@ -618,7 +684,29 @@ module Hecks
618
684
  end
619
685
 
620
686
  op, source = named.first
621
- @mutations << Mutation.new(target: target.to_sym, op: op, source: source)
687
+ @mutations << Mutation.new(target: target.to_sym, op: op, source: normalize_append_source(op, source))
688
+ end
689
+
690
+ # `append:` NORMALLY binds several fields at once (`append: {
691
+ # name: :name, amount: :amount }`) — `Mutation#appended_fields`/
692
+ # `MutationApplier#appended`/the meta-validator Judge's own
693
+ # `mutation_rows` all read `mutation.source` as a Hash
694
+ # unconditionally. A BARE value (`append: :single_field`, or any
695
+ # non-Hash literal) is the one-field shorthand: exactly what an
696
+ # explicit `append: { value: :single_field }` would have meant,
697
+ # named the same way a single-field value object's own implicit
698
+ # member already is (`MutationApplier#appended`'s own `:value`
699
+ # scalar-unwrap). Without this, that shorthand built a Mutation
700
+ # whose `source` was a bare Symbol, which crashed with a raw
701
+ # `NoMethodError` on `#transform_values` the moment anything
702
+ # downstream read it — at dispatch (`MutationApplier#appended`),
703
+ # at IR emission (`Mutation#appended_fields`), and in the
704
+ # meta-validator's own Judge (`Readings#mutation_rows`). #138.
705
+ def normalize_append_source(op, source)
706
+ return source unless op == :append
707
+ return source if source.is_a?(::Hash)
708
+
709
+ { value: source }
622
710
  end
623
711
  end
624
712
  end
@@ -11,7 +11,8 @@ module Hecks
11
11
  include WordGate
12
12
 
13
13
  def initialize(name, owner_value_objects: [], owner_named_givens: {},
14
- identity_name_prefix: nil, identity_value_object_installer: nil)
14
+ identity_name_prefix: nil, identity_value_object_installer: nil,
15
+ aggregate_name: nil, chapter_entity_named_givens: {}, chapter_entity_pending_givens: [])
15
16
  @name = name
16
17
  @commands = []
17
18
  @queries = []
@@ -30,6 +31,18 @@ module Hecks
30
31
  # reference reads from it via `CommandBuilder#
31
32
  # reference_named_given`.
32
33
  @owner_named_givens = owner_named_givens
34
+ # ONE LEVEL WIDER STILL — the CHAPTER-WIDE, ENTITY-SCOPED pool
35
+ # (the piece analogue of `AggregateBuilder#@chapter_named_givens`,
36
+ # one level down). `@aggregate_name` names THIS piece's own
37
+ # root, so the write-through below can key itself
38
+ # "AggregateName.EntityName" — the same dotted addressing
39
+ # convention `declared_by:` already uses chapter-wide, one
40
+ # level up. See `#given_impl`'s own comment for what this
41
+ # closes and `docs/implemented/resolution-rules/
42
+ # chapter-entity-given.md` for the full algorithm.
43
+ @aggregate_name = aggregate_name || Naming.demodulise(name)
44
+ @chapter_entity_named_givens = chapter_entity_named_givens
45
+ @chapter_entity_pending_givens = chapter_entity_pending_givens
33
46
  # DEFERRED CONSTRUCTION — see `AggregateBuilder#drain_pending!`'s
34
47
  # own comment; the identical mechanism, one level down, so a
35
48
  # nested piece's own commands (Dispatch inside Handler) see
@@ -150,7 +163,40 @@ module Hecks
150
163
  # magic.
151
164
  # RENAMED FROM `given` — item #13's full metaprogrammed dispatch
152
165
  # (slice 4b), same reasoning as reference_to_impl above.
153
- def given_impl(description, &predicate)
166
+ #
167
+ # BARE — NO BLOCK — REFERENCES ANOTHER PIECE'S OWN DECLARATION,
168
+ # ANYWHERE IN THE CHAPTER, not just a sibling under this same
169
+ # aggregate — one level wider than round 4's own cross-entity
170
+ # sharing, mirroring `AggregateBuilder#given_impl`'s own
171
+ # chapter-wide shape exactly one level down. Real, live corpus
172
+ # this closes: `Account::LedgerEntry` and `SafeDepositBox::Visit`
173
+ # — two pieces under two DIFFERENT aggregates — independently
174
+ # typed `given("customer is active") { parent.customer.status ==
175
+ # "active" }` byte for byte; neither the aggregate-level chapter
176
+ # pool (a DIFFERENT canonical — bare `customer.status`, the
177
+ # wrong scope for a piece's own command) nor the existing
178
+ # same-aggregate cross-entity pool (`@owner_named_givens`, scoped
179
+ # to ONE aggregate's own entity tree) could reach across the
180
+ # aggregate boundary. Resolved against `@chapter_entity_named_
181
+ # givens`, keyed "AggregateName.EntityName" — see
182
+ # `#reference_named_chapter_entity_given`'s own comment for the
183
+ # algorithm and `docs/implemented/resolution-rules/
184
+ # chapter-entity-given.md` for the full write-up.
185
+ #
186
+ # `declared_by:` is a PLAIN STRING ("Account.LedgerEntry"), not a
187
+ # constant — unlike `AggregateBuilder#given_impl`'s own
188
+ # `declared_by:`, which names a real aggregate constant. A piece
189
+ # has no first-class, independently-addressable reference
190
+ # anywhere in this language (only its owning aggregate does);
191
+ # inventing one to make this ONE argument spelling symmetrical
192
+ # with the aggregate-level word is a real, separate, unscoped
193
+ # feature this fix does not need — ships textual now, the same
194
+ # way `admits:` shipped textual before its own constant-bridge
195
+ # existed, revisited only if a genuine, separate need for
196
+ # constant-addressed pieces shows up later.
197
+ def given_impl(description, declared_by: nil, &predicate)
198
+ return reference_named_chapter_entity_given(description, declared_by: declared_by) unless predicate
199
+
154
200
  named = build_rule(Given, description, predicate, owner_name: @name, word: "given",
155
201
  extraction_failure: "its source could not be read, so no other runtime could ever evaluate it")
156
202
  @named_givens[description] = named
@@ -163,6 +209,17 @@ module Hecks
163
209
  # by coincidence, same as an aggregate-level given already
164
210
  # tolerates today).
165
211
  @owner_named_givens[description] ||= named
212
+ # WRITE-THROUGH, PER OWNER — the chapter-wide analogue of the
213
+ # line above, keyed by [description, this piece's own dotted
214
+ # "Aggregate.Entity" name] rather than description alone, the
215
+ # identical reasoning `AggregateBuilder#given_impl`'s own
216
+ # chapter write-through gives: two DIFFERENT pieces (anywhere
217
+ # in the chapter) independently declaring the SAME description
218
+ # are two DISTINCT candidates a later bare reference chooses
219
+ # between (via `declared_by:` once there is more than one),
220
+ # never silently merged into one slot.
221
+ @chapter_entity_named_givens[description] ||= {}
222
+ @chapter_entity_named_givens[description]["#{@aggregate_name}.#{@name}"] ||= named
166
223
  end
167
224
 
168
225
  # A PIECE'S OWN SHAPE RULE (S10, ADR 0025's own "Rules" shape,
@@ -203,16 +260,81 @@ module Hecks
203
260
  end
204
261
 
205
262
  def self.build(name, owner_value_objects: [], owner_named_givens: {},
206
- identity_name_prefix: nil, identity_value_object_installer: nil, &block)
263
+ identity_name_prefix: nil, identity_value_object_installer: nil,
264
+ aggregate_name: nil, chapter_entity_named_givens: {}, chapter_entity_pending_givens: [], &block)
207
265
  builder = new(name, owner_value_objects: owner_value_objects, owner_named_givens: owner_named_givens,
208
266
  identity_name_prefix: identity_name_prefix,
209
- identity_value_object_installer: identity_value_object_installer)
267
+ identity_value_object_installer: identity_value_object_installer,
268
+ aggregate_name: aggregate_name,
269
+ chapter_entity_named_givens: chapter_entity_named_givens,
270
+ chapter_entity_pending_givens: chapter_entity_pending_givens)
210
271
  builder.instance_eval(&block) if block
211
272
  builder.build
212
273
  end
213
274
 
214
275
  private
215
276
 
277
+ # PRIMITIVE 2 (RuleReference#resolve_owner_keyed) — the CHAPTER-
278
+ # WIDE, ENTITY-SCOPED analogue of `AggregateBuilder#
279
+ # reference_named_chapter_given`; the three branches below are
280
+ # this construct's OWN refusal wording, not shared, matching that
281
+ # method's own precedent (`declared_by:` only exists on `given`
282
+ # so far, at either scope). UNRESOLVED is deferred, not raised
283
+ # here — see `#pending_chapter_entity_given`, below.
284
+ #
285
+ # WRITES THROUGH TO `@owner_named_givens` TOO — not just
286
+ # `@named_givens` — or this piece resolving a description via the
287
+ # WIDER, chapter pool would leave the NARROWER, same-aggregate
288
+ # pool (`EntityBuilder#given_impl`'s own block-form write-through)
289
+ # never populated for this description, breaking any SIBLING
290
+ # piece's existing command-level bare reference
291
+ # (`CommandBuilder#reference_named_given`) that depends on it —
292
+ # real, live corpus: `SafeDepositBox::KeyIssuance.Return`'s own
293
+ # bare `given("customer is active")` resolves through
294
+ # `@owner_named_givens`, populated by `Visit`'s declaration
295
+ # whether `Visit` types the predicate itself OR (now) references
296
+ # `Account::LedgerEntry`'s instead — this write keeps that
297
+ # working unchanged either way, `||=` so nothing here overrides
298
+ # an actual local declaration if one is ever added later.
299
+ def reference_named_chapter_entity_given(description, declared_by:)
300
+ verify_resolves_via!("given", "Entity", "owner_keyed")
301
+ candidates = resolve_owner_keyed(@chapter_entity_named_givens, description)
302
+
303
+ named =
304
+ if declared_by
305
+ candidates[declared_by] || pending_chapter_entity_given(description, declared_by: declared_by)
306
+ elsif candidates.size == 1
307
+ candidates.values.first
308
+ elsif candidates.empty?
309
+ pending_chapter_entity_given(description, declared_by: nil)
310
+ else
311
+ raise(Malformed,
312
+ "#{@aggregate_name}::#{@name}'s given #{description.inspect} is ambiguous " \
313
+ "across the chapter's own pieces — #{candidates.keys.join(', ')} each declare " \
314
+ "a DIFFERENT predicate under this same description; name which one with " \
315
+ "declared_by: (e.g. given(#{description.inspect}, declared_by: " \
316
+ "#{candidates.keys.first.inspect}))")
317
+ end
318
+
319
+ @named_givens[description] = named
320
+ @owner_named_givens[description] ||= named
321
+ end
322
+
323
+ # A CHAPTER MAY BE SPLIT ACROSS FILES — the identical reason
324
+ # `AggregateBuilder#pending_chapter_given` defers rather than
325
+ # raising the moment a bare reference outruns what's loaded so
326
+ # far. Hands back a PLACEHOLDER `Given`, embedded by Ruby object
327
+ # reference in this piece's own `preconditions`, and queues the
328
+ # request in `@chapter_entity_pending_givens` —
329
+ # `BluebookBuilder#resolve_pending_chapter_entity_givens!`
330
+ # mutates it in place once every file in the chapter has loaded.
331
+ def pending_chapter_entity_given(description, declared_by:)
332
+ placeholder = Given.new(description: description, canonical: nil, predicate: nil)
333
+ @chapter_entity_pending_givens << { entity: "#{@aggregate_name}.#{@name}", description: description,
334
+ declared_by: declared_by, placeholder: placeholder }
335
+ placeholder
336
+ end
337
+
216
338
  # A PIECE'S OWN `one_of` LANDS ON ITS AGGREGATE. A type-position
217
339
  # `one_of("never_moved", "moved")` on an entity attribute
218
340
  # synthesizes a closed-set value object — and until this, that
@@ -257,6 +379,9 @@ module Hecks
257
379
  owner_named_givens: @owner_named_givens,
258
380
  identity_name_prefix: "#{@identity_name_prefix}#{Naming.demodulise(name)}",
259
381
  identity_value_object_installer: @identity_value_object_installer,
382
+ aggregate_name: @aggregate_name,
383
+ chapter_entity_named_givens: @chapter_entity_named_givens,
384
+ chapter_entity_pending_givens: @chapter_entity_pending_givens,
260
385
  &block)
261
386
  end
262
387
 
@@ -11,9 +11,24 @@ module Hecks
11
11
  @name = name
12
12
  end
13
13
 
14
- # `on` — item #13's full metaprogrammed dispatch, slice 1
15
- # (whole-project table-unification survey): a bare, kind-driven
16
- # coerce-and-assign, now executed by `GenericDispatch`.
14
+ # `on Account::AccountFrozen` — A BARE CONSTANT ACCEPTED (ADR
15
+ # 0025, S6 "events first-class"), resolved through `ConstShim`
16
+ # the same way `trigger`/`dispatch` already resolve a command
17
+ # reference (`Naming.event_ref`, that method's own header). NOT
18
+ # a required spelling yet, unlike `trigger`'s own quoted-text
19
+ # refusal — see `policy.bluebook`'s own KeywordSeed comment for
20
+ # why: event names aren't 100% migrated across the live corpus
21
+ # the way command references are, so both `on "Account.
22
+ # AccountFrozen"` (quoted) and `on Account::AccountFrozen`
23
+ # (bare) stay admitted until a full migration lands.
24
+ #
25
+ # RENAMED FROM the generic single-fill coercion — item #13's
26
+ # full metaprogrammed dispatch, slice 1 (whole-project
27
+ # table-unification survey), now overridden here the same way
28
+ # `trigger_impl` overrides its own generic default.
29
+ def on_impl(event_ref)
30
+ @on_event = Naming.event_ref(event_ref)
31
+ end
17
32
 
18
33
  # `with:` — WHAT THE TRIGGER IS GIVEN, when the event's own shape
19
34
  # is not it. Omitted, the whole event payload forwards verbatim,