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.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/d1.rb +38 -14
- data/lib/hecks/adapters/driven/governance_authorization.rb +41 -2
- data/lib/hecks/adapters/driven/heki/journal.rb +7 -2
- data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
- data/lib/hecks/adapters/driven/heki/snapshot.rb +31 -4
- data/lib/hecks/adapters/driven/heki.rb +40 -9
- data/lib/hecks/adapters/driven/lambda.rb +16 -2
- data/lib/hecks/adapters/driven/postgres/codec.rb +8 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +45 -6
- data/lib/hecks/adapters/driven/postgres.rb +76 -29
- data/lib/hecks/adapters/driven/postgres_era.adapter +11 -0
- data/lib/hecks/adapters/driven/sqlite/codec.rb +17 -0
- data/lib/hecks/adapters/driven/sqlite/projection.rb +76 -9
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +17 -5
- data/lib/hecks/adapters/driven/sqlite.rb +30 -9
- data/lib/hecks/adapters/driven.rb +19 -1
- data/lib/hecks/behaviors/dsl.rb +29 -0
- data/lib/hecks/behaviors/expectations.rb +62 -2
- data/lib/hecks/bluebook/assembly/contracts.rb +36 -7
- data/lib/hecks/bluebook/assembly/marks.rb +4 -3
- data/lib/hecks/bluebook/assembly.rb +14 -1
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +18 -1
- data/lib/hecks/bluebook/behaviour/process_manager.rb +14 -1
- data/lib/hecks/bluebook/chapter.rb +21 -11
- data/lib/hecks/bluebook/command.rb +1 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +117 -5
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +21 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +71 -2
- data/lib/hecks/bluebook/dsl/command_builder.rb +91 -3
- data/lib/hecks/bluebook/dsl/entity_builder.rb +129 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +18 -3
- data/lib/hecks/bluebook/dsl/port_builder.rb +12 -3
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +109 -10
- data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -0
- data/lib/hecks/bluebook/dsl/word_gate.rb +9 -2
- data/lib/hecks/bluebook/dsl/world_builder.rb +44 -4
- data/lib/hecks/bluebook/expression/canonical_form.rb +71 -3
- data/lib/hecks/bluebook/expression/evaluator.rb +50 -7
- data/lib/hecks/bluebook/expression/projection.json +48 -0
- data/lib/hecks/bluebook/expression/resolver.rb +161 -10
- data/lib/hecks/bluebook/hexagon.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +78 -16
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +4 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +14 -4
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +42 -3
- data/lib/hecks/bluebook/meta_validator/shapes.rb +30 -10
- data/lib/hecks/bluebook/meta_validator.rb +103 -13
- data/lib/hecks/bluebook/model_check.rb +132 -5
- data/lib/hecks/bluebook/pattern_subset.rb +66 -2
- data/lib/hecks/bluebook/process_manager.rb +53 -11
- data/lib/hecks/bluebook/value_object.rb +9 -1
- data/lib/hecks/doc/reference.rb +22 -1
- data/lib/hecks/facade/cli_door.rb +6 -3
- data/lib/hecks/facade/json_door.rb +16 -4
- data/lib/hecks/forms/app.rb +47 -6
- data/lib/hecks/forms/command_form_renderer.rb +1 -1
- data/lib/hecks/forms/field_renderer.rb +11 -4
- data/lib/hecks/forms/html.rb +31 -0
- data/lib/hecks/forms/params.rb +30 -1
- data/lib/hecks/forms/port_argument.rb +46 -0
- data/lib/hecks/forms/record_renderer.rb +6 -2
- data/lib/hecks/forms/record_table.rb +6 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +19 -19
- data/lib/hecks/framework/bluebook/governance.bluebook +26 -11
- data/lib/hecks/framework/bluebook/identity.bluebook +2 -2
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +212 -18
- data/lib/hecks/fuzzing/properties.rb +52 -6
- data/lib/hecks/fuzzing/replay.rb +51 -18
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +28 -2
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +71 -6
- data/lib/hecks/fuzzing/sequence_generator.rb +5 -4
- data/lib/hecks/fuzzing/value_generator.rb +25 -2
- data/lib/hecks/grammar/evolve.rb +33 -0
- data/lib/hecks/grammar/expression.bluebook +8 -5
- data/lib/hecks/grammar/expression_operators.json +497 -1
- data/lib/hecks/language/bluebook/aggregate.bluebook +12 -12
- data/lib/hecks/language/bluebook/bluebook.bluebook +3 -3
- data/lib/hecks/language/bluebook/command.bluebook +42 -8
- data/lib/hecks/language/bluebook/entity.bluebook +86 -10
- data/lib/hecks/language/bluebook/policy.bluebook +21 -4
- data/lib/hecks/language/bluebook/process_manager.bluebook +135 -18
- data/lib/hecks/language/bluebook/projection.bluebook +6 -6
- data/lib/hecks/language/bluebook/query.bluebook +4 -4
- data/lib/hecks/language/bluebook/shape.bluebook +6 -6
- data/lib/hecks/language/bluebook/syntax.bluebook +12 -11
- data/lib/hecks/language/bluebook/vocabulary.bluebook +22 -7
- data/lib/hecks/language/oidc.json +20 -0
- data/lib/hecks/language/port.bluebook +30 -2
- data/lib/hecks/naming.rb +54 -1
- data/lib/hecks/ports/access_control.port +7 -2
- data/lib/hecks/ports/access_control.rb +1 -1
- data/lib/hecks/ports/agent.port +6 -2
- data/lib/hecks/ports/agent.rb +1 -1
- data/lib/hecks/ports/authentication.port +4 -2
- data/lib/hecks/ports/authentication.rb +1 -1
- data/lib/hecks/ports/authorization.port +5 -2
- data/lib/hecks/ports/authorization.rb +14 -11
- data/lib/hecks/ports/clock.port +3 -2
- data/lib/hecks/ports/clock.rb +1 -1
- data/lib/hecks/ports/extraction.port +3 -2
- data/lib/hecks/ports/extraction.rb +1 -1
- data/lib/hecks/ports/identity_assignment.port +3 -2
- data/lib/hecks/ports/identity_assignment.rb +1 -1
- data/lib/hecks/ports/identity_generation.port +3 -2
- data/lib/hecks/ports/identity_generation.rb +1 -1
- data/lib/hecks/ports/identity_resolution.port +3 -2
- data/lib/hecks/ports/identity_resolution.rb +1 -1
- data/lib/hecks/ports/persistence/append_only.rb +40 -4
- data/lib/hecks/ports/persistence/execution.rb +6 -1
- data/lib/hecks/ports/persistence/plugin.rb +54 -0
- data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_check.rb +41 -8
- data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_guard.rb +24 -56
- data/lib/hecks/ports/persistence/{lineage.rb → plugins/era/lineage.rb} +31 -4
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/era_store.rb +3 -3
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/head_compiler.rb +59 -10
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/mint_transaction.rb +2 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/provisioning.rb +29 -1
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/tail_merge.rb +11 -4
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/transform_installer.rb +20 -0
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage.rb +1 -1
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/coverage_check.rb +5 -5
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/era_resolver.rb +5 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/merge_coordinator.rb +2 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/minter.rb +4 -4
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager.rb +2 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era.rb +116 -29
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/approval_digest.rb +1 -1
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/layer_one.rb +14 -5
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/layer_two.rb +31 -6
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/unfed_report.rb +5 -1
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/reattest.rb +3 -3
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/differ.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
- data/lib/hecks/ports/persistence.rb +1 -1
- data/lib/hecks/ports/projection.rb +20 -0
- data/lib/hecks/projections/diagrams.rb +230 -1
- data/lib/hecks/projections/shape.rb +15 -2
- data/lib/hecks/projector/cli_projector.rb +10 -1
- data/lib/hecks/projector/exporter.rb +19 -2
- data/lib/hecks/query_ir.rb +19 -0
- data/lib/hecks/query_specification/common/null_policy.rb +13 -1
- data/lib/hecks/query_specification/field_path.rb +20 -2
- data/lib/hecks/query_specification/hop_path.rb +7 -5
- data/lib/hecks/runtime/aggregate_lock.rb +45 -0
- data/lib/hecks/runtime/boot_gates.rb +41 -0
- data/lib/hecks/runtime/caller.rb +19 -3
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +13 -2
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -0
- data/lib/hecks/runtime/command_interpreter.rb +97 -13
- data/lib/hecks/runtime/command_rules/admissibility.rb +64 -14
- data/lib/hecks/runtime/command_rules/arithmetic.rb +7 -1
- data/lib/hecks/runtime/command_rules/authorization.rb +2 -1
- data/lib/hecks/runtime/command_rules/references.rb +27 -19
- data/lib/hecks/runtime/dependency_planning.rb +14 -0
- data/lib/hecks/runtime/dispatcher.rb +19 -4
- data/lib/hecks/runtime/entity_interpreter.rb +85 -14
- data/lib/hecks/runtime/errors.rb +22 -0
- data/lib/hecks/runtime/identity.rb +30 -2
- data/lib/hecks/runtime/instance.rb +59 -4
- data/lib/hecks/runtime/interpreting.rb +21 -0
- data/lib/hecks/runtime/loader.rb +59 -18
- data/lib/hecks/runtime/query_interpreter.rb +36 -4
- data/lib/hecks/runtime/reaction_invocation.rb +9 -1
- data/lib/hecks/runtime/read_model_interpreter.rb +76 -1
- data/lib/hecks/runtime/refusal_wording.rb +2 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +75 -3
- data/lib/hecks/runtime/registry/verification.rb +88 -0
- data/lib/hecks/runtime/registry.rb +69 -8
- data/lib/hecks/runtime/saga_interpreter.rb +215 -13
- data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
- data/lib/hecks/runtime/value/admission.rb +19 -1
- data/lib/hecks/runtime/value/coercion.rb +75 -10
- data/lib/hecks/runtime.rb +17 -5
- data/lib/hecks/storehouse.rb +632 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +6 -1
- data/lib/hecks.rb +7 -2
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
- metadata +48 -37
- /data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_guard/shape_diff.rb +0 -0
- /data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_tamper.rb +0 -0
- /data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/field_cache.rb +0 -0
- /data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/resumable_backfill.rb +0 -0
- /data/lib/hecks/{runtime → ports/persistence/plugins/era}/storage_shape.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/rule_compiler.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/renderer.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/writer.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold.rb +0 -0
- /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: [],
|
|
449
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
89
|
-
chapter_pending_givens:
|
|
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
|
|
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
|
-
|
|
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,
|
|
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` —
|
|
15
|
-
#
|
|
16
|
-
#
|
|
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,
|