hecks 1.0.2 → 1.2.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/claude_code.rb +3 -3
- data/lib/hecks/adapters/driven/d1.rb +48 -28
- data/lib/hecks/adapters/driven/folder.rb +10 -3
- data/lib/hecks/adapters/driven/google_authentication.rb +2 -4
- data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +3 -5
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +4 -1
- data/lib/hecks/adapters/driven/lambda.rb +2 -2
- data/lib/hecks/adapters/driven/local_storage.adapter +3 -0
- data/lib/hecks/adapters/driven/local_storage.rb +120 -0
- data/lib/hecks/adapters/driven/memory.rb +48 -3
- data/lib/hecks/adapters/driven/postgres/codec.rb +3 -1
- data/lib/hecks/adapters/driven/postgres/outbox.rb +100 -0
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +57 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +22 -2
- data/lib/hecks/adapters/driven/postgres.rb +45 -31
- data/lib/hecks/adapters/driven/prism.rb +11 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +45 -20
- data/lib/hecks/adapters/driven/sqlite/codec.rb +3 -1
- data/lib/hecks/adapters/driven/sqlite/projection.rb +22 -3
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +23 -1
- data/lib/hecks/adapters/driven/sqlite.rb +87 -8
- data/lib/hecks/adapters/driven.rb +4 -0
- data/lib/hecks/adapters.rb +4 -0
- data/lib/hecks/behaviors/dsl.rb +7 -0
- data/lib/hecks/behaviors/expectations.rb +32 -4
- data/lib/hecks/behaviors/rspec.rb +2 -0
- data/lib/hecks/behaviors/runner.rb +4 -0
- data/lib/hecks/bluebook/aggregate.rb +6 -5
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +4 -4
- data/lib/hecks/bluebook/assembly/build.rb +6 -3
- data/lib/hecks/bluebook/assembly/contract.rb +5 -1
- data/lib/hecks/bluebook/assembly/contracts.rb +4 -0
- data/lib/hecks/bluebook/assembly/marks.rb +15 -4
- data/lib/hecks/bluebook/assembly.rb +1 -3
- data/lib/hecks/bluebook/attribute.rb +6 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +18 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +17 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +53 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +1 -1
- data/lib/hecks/bluebook/command.rb +6 -5
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +4 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +524 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +29 -477
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +14 -6
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +5 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +848 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +25 -784
- data/lib/hecks/bluebook/dsl/command_builder.rb +53 -13
- data/lib/hecks/bluebook/dsl/const_shim.rb +6 -0
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +20 -7
- data/lib/hecks/bluebook/dsl/entity_builder.rb +27 -6
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +12 -2
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +8 -1
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +24 -12
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +40 -1
- data/lib/hecks/bluebook/dsl/policy_builder.rb +10 -1
- data/lib/hecks/bluebook/dsl/port_builder.rb +6 -1
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +33 -12
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +56 -12
- data/lib/hecks/bluebook/dsl/query_builder.rb +7 -1
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +142 -19
- data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -1
- data/lib/hecks/bluebook/dsl/translation_builder.rb +13 -2
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +8 -1
- data/lib/hecks/bluebook/dsl/word_gate.rb +5 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +13 -3
- data/lib/hecks/bluebook/entity.rb +5 -3
- data/lib/hecks/bluebook/expression/ast_json.rb +88 -9
- data/lib/hecks/bluebook/expression/ast_reader.rb +107 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +7 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +64 -11
- data/lib/hecks/bluebook/expression/projection.json +12 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +4 -3
- data/lib/hecks/bluebook/expression/resolver.rb +83 -11
- data/lib/hecks/bluebook/expression.rb +1 -0
- data/lib/hecks/bluebook/hexagon.rb +10 -0
- data/lib/hecks/bluebook/lifecycle.rb +9 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +10 -4
- data/lib/hecks/bluebook/meta_validator/plan.rb +4 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/shapes.rb +3 -3
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +7 -4
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +42 -3
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -1
- data/lib/hecks/bluebook/meta_validator.rb +5 -5
- data/lib/hecks/bluebook/model_check.rb +119 -82
- data/lib/hecks/bluebook/pattern_subset.rb +9 -0
- data/lib/hecks/bluebook/policy.rb +2 -1
- data/lib/hecks/bluebook/process_manager.rb +6 -0
- data/lib/hecks/bluebook/project_register.rb +45 -25
- data/lib/hecks/bluebook/query.rb +4 -0
- data/lib/hecks/bluebook/read_model.rb +2 -2
- data/lib/hecks/bluebook/smoke_test.rb +5 -0
- data/lib/hecks/bluebook/synthesizer.rb +2 -1
- data/lib/hecks/bluebook/translation.rb +3 -0
- data/lib/hecks/bluebook/value_object.rb +2 -2
- data/lib/hecks/codemod.rb +13 -0
- data/lib/hecks/doc/reference.rb +17 -11
- data/lib/hecks/facade/cli_runner.rb +2 -2
- data/lib/hecks/facade/command_request.rb +11 -11
- data/lib/hecks/facade/handle.rb +3 -3
- data/lib/hecks/facade/surface/aggregate_door.rb +52 -38
- data/lib/hecks/facade.rb +4 -0
- data/lib/hecks/forms/app.rb +10 -6
- data/lib/hecks/forms/command_form_renderer.rb +3 -3
- data/lib/hecks/forms/field_renderer.rb +14 -9
- data/lib/hecks/forms/field_shape.rb +7 -2
- data/lib/hecks/forms/html.rb +5 -2
- data/lib/hecks/forms/index_renderer.rb +1 -1
- data/lib/hecks/forms/page.rb +7 -3
- data/lib/hecks/forms/query_form_renderer.rb +3 -2
- data/lib/hecks/forms/record_renderer.rb +22 -16
- data/lib/hecks/forms.rb +4 -0
- data/lib/hecks/fqn.rb +21 -9
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +30 -8
- data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -1
- data/lib/hecks/fuzzing/isolated_boot.rb +14 -6
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +264 -0
- data/lib/hecks/fuzzing/properties/guards.rb +251 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +351 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +99 -0
- data/lib/hecks/fuzzing/properties/querying.rb +206 -0
- data/lib/hecks/fuzzing/properties.rb +41 -1065
- data/lib/hecks/fuzzing/replay.rb +60 -16
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +6 -2
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +2 -1
- data/lib/hecks/fuzzing/sequence_generator.rb +0 -1
- data/lib/hecks/fuzzing/value_generator.rb +2 -2
- data/lib/hecks/grammar/evolve.rb +34 -10
- data/lib/hecks/grammar/expression_operators.json +124 -0
- data/lib/hecks/grammar.rb +15 -5
- data/lib/hecks/ir.rb +9 -0
- data/lib/hecks/language/bluebook/aggregate.bluebook +4 -2
- data/lib/hecks/language/bluebook/command.bluebook +2 -1
- data/lib/hecks/language/bluebook/entity.bluebook +2 -1
- data/lib/hecks/language/bluebook/projection.bluebook +2 -1
- data/lib/hecks/language/bluebook/query.bluebook +4 -2
- data/lib/hecks/language/bluebook/shape.bluebook +2 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -0
- data/lib/hecks/literal.rb +18 -8
- data/lib/hecks/naming.rb +6 -3
- data/lib/hecks/ports/access_control.rb +1 -1
- data/lib/hecks/ports/agent.rb +5 -3
- data/lib/hecks/ports/authentication.rb +3 -2
- data/lib/hecks/ports/authorization.rb +1 -1
- data/lib/hecks/ports/clock.rb +1 -1
- data/lib/hecks/ports/extraction.rb +5 -1
- data/lib/hecks/ports/identity_assignment.rb +1 -1
- data/lib/hecks/ports/identity_generation.rb +1 -1
- data/lib/hecks/ports/identity_resolution.rb +1 -1
- data/lib/hecks/ports/loading.rb +4 -1
- data/lib/hecks/ports/persistence/append_only.rb +51 -4
- data/lib/hecks/ports/persistence/binding_policy.rb +1 -1
- data/lib/hecks/ports/persistence/plugin.rb +3 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -1
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +5 -5
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +14 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +4 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +80 -16
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +23 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +43 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +25 -4
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +49 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +10 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +49 -38
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +77 -18
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +19 -21
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +27 -9
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +9 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +25 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +2 -6
- data/lib/hecks/ports/persistence/plugins/era.rb +3 -1
- data/lib/hecks/ports/persistence/remote_runtime.rb +1 -1
- data/lib/hecks/ports/persistence.rb +7 -3
- data/lib/hecks/ports/projection.rb +13 -5
- data/lib/hecks/ports/query/in_memory.rb +8 -2
- data/lib/hecks/ports/query.rb +1 -3
- data/lib/hecks/ports.rb +2 -0
- data/lib/hecks/projections/diagrams.rb +11 -3
- data/lib/hecks/projections/glossary.rb +374 -0
- data/lib/hecks/projections/ir.rb +2 -0
- data/lib/hecks/projections/model/deviations.rb +2 -1
- data/lib/hecks/projections/model.rb +15 -10
- data/lib/hecks/projections/vocabulary.rb +1 -3
- data/lib/hecks/projections.rb +1 -0
- data/lib/hecks/projector/cli_projector.rb +78 -42
- data/lib/hecks/projector/docs_projector.rb +21 -12
- data/lib/hecks/projector/exporter.rb +34 -1
- data/lib/hecks/projector/narrate_projector.rb +83 -25
- data/lib/hecks/projector.rb +2 -2
- data/lib/hecks/query_ir.rb +60 -36
- data/lib/hecks/query_specification/common/authorization_spec.rb +1 -1
- data/lib/hecks/query_specification/common/comparators.rb +4 -0
- data/lib/hecks/query_specification/common/comparison.rb +8 -1
- data/lib/hecks/query_specification/common/dsl.rb +5 -0
- data/lib/hecks/query_specification/common/limit_spec.rb +9 -2
- data/lib/hecks/query_specification/common/null_policy.rb +6 -2
- data/lib/hecks/query_specification/common/offset_spec.rb +9 -2
- data/lib/hecks/query_specification/common/options.rb +17 -5
- data/lib/hecks/query_specification/common/order_by.rb +9 -2
- data/lib/hecks/query_specification/common/where_clause.rb +15 -2
- data/lib/hecks/query_specification/field_path.rb +4 -4
- data/lib/hecks/query_specification/read_model/specification.rb +5 -2
- data/lib/hecks/router/namespace_installer.rb +22 -10
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +57 -22
- data/lib/hecks/runtime/command_interpreter.rb +104 -65
- data/lib/hecks/runtime/command_rules/admissibility.rb +26 -8
- data/lib/hecks/runtime/command_rules/arithmetic.rb +35 -14
- data/lib/hecks/runtime/dependency_planning.rb +33 -21
- data/lib/hecks/runtime/dispatcher.rb +41 -7
- data/lib/hecks/runtime/entity_element.rb +25 -6
- data/lib/hecks/runtime/entity_interpreter.rb +28 -15
- data/lib/hecks/runtime/errors.rb +28 -2
- data/lib/hecks/runtime/instance.rb +9 -3
- data/lib/hecks/runtime/interpreting.rb +45 -9
- data/lib/hecks/runtime/loader.rb +21 -0
- data/lib/hecks/runtime/outbox.rb +365 -0
- data/lib/hecks/runtime/policy_interpreter.rb +32 -17
- data/lib/hecks/runtime/query_interpreter.rb +36 -16
- data/lib/hecks/runtime/reaction_invocation.rb +35 -22
- data/lib/hecks/runtime/read_model_interpreter.rb +31 -7
- data/lib/hecks/runtime/reference_hop.rb +1 -1
- data/lib/hecks/runtime/refusal_wording.rb +55 -49
- data/lib/hecks/runtime/registry/saga_persistence.rb +3 -1
- data/lib/hecks/runtime/registry/verification.rb +39 -2
- data/lib/hecks/runtime/registry.rb +21 -11
- data/lib/hecks/runtime/remote_dispatcher.rb +3 -1
- data/lib/hecks/runtime/routing.rb +52 -39
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +7 -7
- data/lib/hecks/runtime/saga_interpreter.rb +119 -90
- data/lib/hecks/runtime/saga_pending_dispatch.rb +6 -3
- data/lib/hecks/runtime/tenant_scope.rb +7 -0
- data/lib/hecks/runtime/value/coercion.rb +271 -49
- data/lib/hecks/runtime/value.rb +1 -1
- data/lib/hecks/runtime.rb +7 -2
- data/lib/hecks/storehouse.rb +15 -8
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +1 -0
- data/lib/hecks.rb +17 -13
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +1 -1
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +1 -1
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +1 -1
- metadata +16 -2
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module QuerySpecification
|
|
3
3
|
module Common
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
# `target` — which many-side `include`d aggregate this clause applies
|
|
5
|
+
# to on a `read_model` with more than one (ADR 0055's own `on:`,
|
|
6
|
+
# `ReadModelBuilder#where_impl`). Always `nil` for a plain `Query`'s
|
|
7
|
+
# own `where` (that builder never overrides `where_impl` to accept
|
|
8
|
+
# `on:`), and `nil` for a `read_model` with a single many-side head,
|
|
9
|
+
# where naming one is unnecessary. `to_h` omits the key entirely
|
|
10
|
+
# rather than emitting `target: nil` — the same "absent, not null"
|
|
11
|
+
# convention `count`/`median_field` already established
|
|
12
|
+
# (`lib/hecks/bluebook/read_model.rb`), so every read model that
|
|
13
|
+
# never uses `on:` keeps its existing wire shape byte-identical.
|
|
14
|
+
WhereClause = Struct.new(:field, :op, :value, :target, keyword_init: true) do
|
|
15
|
+
def to_h
|
|
16
|
+
base = { field: field.to_s, op: op.to_s, value: QuerySpecification.render_value(value) }
|
|
17
|
+
target ? base.merge(target: target.to_s) : base
|
|
18
|
+
end
|
|
6
19
|
end
|
|
7
20
|
end
|
|
8
21
|
end
|
|
@@ -85,8 +85,8 @@ module Hecks
|
|
|
85
85
|
# or a value object carrying at least one numeric member. A dotted
|
|
86
86
|
# path must land on a numeric primitive itself — the convention does
|
|
87
87
|
# not reach through a named path, it IS the absence of one.
|
|
88
|
-
def numeric?(attribute, segments, &
|
|
89
|
-
leaf = leaf_attribute(attribute, segments, &
|
|
88
|
+
def numeric?(attribute, segments, &)
|
|
89
|
+
leaf = leaf_attribute(attribute, segments, &)
|
|
90
90
|
return false if leaf.nil? || leaf.list? || leaf.reference?
|
|
91
91
|
return true if NUMERIC_PRIMITIVES.include?(leaf.type.to_s)
|
|
92
92
|
return false unless segments.empty?
|
|
@@ -98,8 +98,8 @@ module Hecks
|
|
|
98
98
|
# A dotted path must end on a SCALAR member — landing on a value
|
|
99
99
|
# object would hand SQL a JSON object where the reference
|
|
100
100
|
# interpreter unwraps a hash, and the two would answer differently.
|
|
101
|
-
def scalar_leaf?(attribute, segments, &
|
|
102
|
-
leaf = leaf_attribute(attribute, segments, &
|
|
101
|
+
def scalar_leaf?(attribute, segments, &)
|
|
102
|
+
leaf = leaf_attribute(attribute, segments, &)
|
|
103
103
|
!leaf.nil? && !leaf.list? && !leaf.reference? && SCALAR_PRIMITIVES.include?(leaf.type.to_s)
|
|
104
104
|
end
|
|
105
105
|
end
|
|
@@ -3,11 +3,14 @@ require_relative "../common/options"
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module QuerySpecification
|
|
5
5
|
module ReadModel
|
|
6
|
+
# The read-model-specific specification: Common::Options' shared
|
|
7
|
+
# query attributes plus `joins`, the one thing a read model
|
|
8
|
+
# declares that a plain Query does not.
|
|
6
9
|
class Specification < Common::Options
|
|
7
10
|
attr_reader :joins
|
|
8
11
|
|
|
9
|
-
def initialize(joins: [], **
|
|
10
|
-
super(**
|
|
12
|
+
def initialize(joins: [], **)
|
|
13
|
+
super(**)
|
|
11
14
|
@joins = joins
|
|
12
15
|
end
|
|
13
16
|
|
|
@@ -7,12 +7,19 @@ module Hecks
|
|
|
7
7
|
# resolution stays in Router; this object only adapts it to constants and
|
|
8
8
|
# method calls.
|
|
9
9
|
class NamespaceInstaller
|
|
10
|
+
# Returned by the `.options(version:)` method installed on each
|
|
11
|
+
# namespace module — forwards any command/query verb called on it
|
|
12
|
+
# to the router, pinned to this specific FQN version rather than
|
|
13
|
+
# the router's default resolution.
|
|
10
14
|
class OptionsProxy
|
|
11
15
|
def initialize(router:, realm:, domain:, aggregate:, options:)
|
|
12
16
|
unknown = options.keys - [:version]
|
|
13
17
|
raise ArgumentError, "unknown router options: #{unknown.join(', ')}" unless unknown.empty?
|
|
14
18
|
|
|
15
|
-
@router
|
|
19
|
+
@router = router
|
|
20
|
+
@realm = realm
|
|
21
|
+
@domain = domain
|
|
22
|
+
@aggregate = aggregate
|
|
16
23
|
@version = options[:version]&.to_s
|
|
17
24
|
end
|
|
18
25
|
|
|
@@ -103,12 +110,12 @@ module Hecks
|
|
|
103
110
|
|
|
104
111
|
target.define_singleton_method(:find) do |id|
|
|
105
112
|
found = dispatcher.registry.repository(domain, ir).find(id)
|
|
106
|
-
found && Facade::Handle.new(dispatcher: dispatcher, domain: domain,
|
|
113
|
+
found && Facade::Handle.new(dispatcher: dispatcher, domain: domain, aggregate: ir, instance: found)
|
|
107
114
|
end
|
|
108
115
|
|
|
109
116
|
target.define_singleton_method(:all) do
|
|
110
117
|
dispatcher.registry.repository(domain, ir).all.map do |instance|
|
|
111
|
-
Facade::Handle.new(dispatcher: dispatcher, domain: domain,
|
|
118
|
+
Facade::Handle.new(dispatcher: dispatcher, domain: domain, aggregate: ir, instance: instance)
|
|
112
119
|
end
|
|
113
120
|
end
|
|
114
121
|
end
|
|
@@ -118,16 +125,17 @@ module Hecks
|
|
|
118
125
|
current_entries.reject { |entry| entry.fqn.aggregate.nil? }
|
|
119
126
|
.group_by { |entry| [entry.fqn.aggregate, entry.fqn.verb] }
|
|
120
127
|
.each do |(aggregate, verb), candidates|
|
|
121
|
-
shortcut_target(aggregate).define_singleton_method(verb)
|
|
128
|
+
shortcut_target(aggregate).define_singleton_method(verb) do |**args|
|
|
122
129
|
installer.send(:dispatch_short, candidates, **args)
|
|
123
|
-
|
|
130
|
+
end
|
|
124
131
|
end
|
|
125
132
|
end
|
|
126
133
|
|
|
127
134
|
def dispatch_short(candidates, **args)
|
|
128
135
|
if candidates.length > 1
|
|
129
136
|
shown = candidates.map { |entry| entry.fqn.to_s }.sort.join(", ")
|
|
130
|
-
aggregate
|
|
137
|
+
aggregate = candidates.first.fqn.aggregate
|
|
138
|
+
verb = candidates.first.fqn.verb
|
|
131
139
|
raise AmbiguousShortRoute, "#{aggregate}.#{verb} is ambiguous — choose one of: #{shown}"
|
|
132
140
|
end
|
|
133
141
|
|
|
@@ -137,8 +145,10 @@ module Hecks
|
|
|
137
145
|
|
|
138
146
|
def shortcut_target(aggregate)
|
|
139
147
|
constant = Object.const_get(aggregate, false) if Object.const_defined?(aggregate, false)
|
|
140
|
-
|
|
141
|
-
|
|
148
|
+
if constant && !constant.is_a?(Module)
|
|
149
|
+
raise NameError,
|
|
150
|
+
"cannot install Bluebook shortcut #{aggregate}: it is not a module"
|
|
151
|
+
end
|
|
142
152
|
|
|
143
153
|
constant || Object.const_set(aggregate, Module.new)
|
|
144
154
|
end
|
|
@@ -146,8 +156,10 @@ module Hecks
|
|
|
146
156
|
def namespace_for(realm, domain, aggregate)
|
|
147
157
|
[realm, domain, aggregate].compact.reduce(Object) do |parent, name|
|
|
148
158
|
constant = parent.const_get(name, false) if parent.const_defined?(name, false)
|
|
149
|
-
|
|
150
|
-
|
|
159
|
+
if constant && !constant.is_a?(Module)
|
|
160
|
+
raise NameError,
|
|
161
|
+
"cannot install Bluebook route under #{parent}::#{name}: it is not a module"
|
|
162
|
+
end
|
|
151
163
|
|
|
152
164
|
constant || parent.const_set(name, Module.new)
|
|
153
165
|
end
|
|
@@ -18,17 +18,39 @@ module Hecks
|
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
# A CASE STATEMENT OVER A CLOSED, DECLARED SET — every mutation op
|
|
22
|
+
# the grammar can emit gets its own branch, including the `else`
|
|
23
|
+
# backstop for the day a new op reaches this method undeclared (see
|
|
24
|
+
# its own comment). Splitting each branch into its own method would
|
|
25
|
+
# not reduce what a reader has to hold at once (each op's own
|
|
26
|
+
# comment already explains why IT is shaped the way it is) and
|
|
27
|
+
# would obscure that the set is closed and exhaustive.
|
|
28
|
+
# rubocop:disable Lint/DuplicateBranch -- :delegate and :corrects
|
|
29
|
+
# both no-op here, for two unrelated documented reasons (see each
|
|
30
|
+
# branch's own comment below); merging would blur that distinction.
|
|
31
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
32
|
+
#
|
|
33
|
+
# `pre` — THE PRE-DISPATCH STATE (C4.2): every source below reads
|
|
34
|
+
# it, every target is written to `instance`. A command's effects
|
|
35
|
+
# are one update set; declaration order carries no meaning, and
|
|
36
|
+
# build refuses a field written twice (`CommandBuilder#
|
|
37
|
+
# refuse_duplicate_targets!`).
|
|
38
|
+
def apply(instance, aggregate, mutation, args, pre = instance)
|
|
22
39
|
case mutation.op
|
|
23
40
|
when :set
|
|
24
|
-
value = mutation.source.is_a?(StateRef)
|
|
41
|
+
value = if mutation.source.is_a?(StateRef)
|
|
42
|
+
pre[mutation.source.name]
|
|
43
|
+
else
|
|
44
|
+
@rules.resolve_source(mutation.source,
|
|
45
|
+
args)
|
|
46
|
+
end
|
|
25
47
|
instance[mutation.target] = Value.for(aggregate, mutation.target, value)
|
|
26
48
|
when :append
|
|
27
|
-
instance[mutation.target] = appended(
|
|
49
|
+
instance[mutation.target] = appended(pre, aggregate, mutation, args)
|
|
28
50
|
when :increment, :decrement
|
|
29
51
|
amount = @rules.resolve_source(mutation.source, args)
|
|
30
52
|
attribute = aggregate.attribute(mutation.target)
|
|
31
|
-
current =
|
|
53
|
+
current = pre[mutation.target]
|
|
32
54
|
# Vendored fix, not (yet) upstream hecks (migration plan
|
|
33
55
|
# task 9): see #rewrap_arithmetic_result's own comment below
|
|
34
56
|
# -- `amount` is wrapped ONLY when `current` already is, not
|
|
@@ -42,7 +64,7 @@ module Hecks
|
|
|
42
64
|
# RemoveDependency/DeactivateSprint: "a concurrent Add can
|
|
43
65
|
# never be lost").
|
|
44
66
|
when :remove
|
|
45
|
-
instance[mutation.target] = removed(
|
|
67
|
+
instance[mutation.target] = removed(pre, aggregate, mutation, args)
|
|
46
68
|
# Vendored addition, not (yet) upstream hecks (migration
|
|
47
69
|
# plan task 4, i106): multiply/clamp, the scale/bound pair
|
|
48
70
|
# alongside increment/decrement's add/subtract pair -- see
|
|
@@ -50,7 +72,7 @@ module Hecks
|
|
|
50
72
|
when :multiply
|
|
51
73
|
amount = @rules.resolve_source(mutation.source, args)
|
|
52
74
|
attribute = aggregate.attribute(mutation.target)
|
|
53
|
-
current =
|
|
75
|
+
current = pre[mutation.target]
|
|
54
76
|
amount = Value.for_attribute(aggregate, attribute, amount) if attribute && current.is_a?(Value)
|
|
55
77
|
result = @rules.multiply(current, amount, mutation.target)
|
|
56
78
|
instance[mutation.target] = rewrap_arithmetic_result(aggregate, attribute, current, result)
|
|
@@ -61,7 +83,7 @@ module Hecks
|
|
|
61
83
|
# a no-op for an Array (it only special-cases Symbol), so it's
|
|
62
84
|
# read straight.
|
|
63
85
|
when :clamp
|
|
64
|
-
instance[mutation.target] = @rules.clamp(
|
|
86
|
+
instance[mutation.target] = @rules.clamp(pre[mutation.target], mutation.source, mutation.target)
|
|
65
87
|
# `delegates_to` — CommandBuilder#delegates_to's own comment gives
|
|
66
88
|
# the full reasoning for storing it as a mutation at all. A REAL
|
|
67
89
|
# no-op here, not a gap: it targets no field on THIS instance —
|
|
@@ -94,6 +116,8 @@ module Hecks
|
|
|
94
116
|
raise Runtime::WiringError, "no mutation applier handles :#{mutation.op} — add one before declaring it"
|
|
95
117
|
end
|
|
96
118
|
end
|
|
119
|
+
# rubocop:enable Lint/DuplicateBranch
|
|
120
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
97
121
|
|
|
98
122
|
# A CALLER-SUPPLIED ARG, FIRST -- an append's own field can also
|
|
99
123
|
# name something the SUBJECT ALREADY KNOWS about itself, falling
|
|
@@ -122,21 +146,23 @@ module Hecks
|
|
|
122
146
|
fields = mutation.source.transform_values { |source| resolve_append_source(source, instance, args) }
|
|
123
147
|
element_type = aggregate.attribute(mutation.target)&.type
|
|
124
148
|
value_object = aggregate.value_object(element_type)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
fields[attribute.name] = Value.scalar(held) if held.is_a?(Value) && held.to_h.size == 1
|
|
135
|
-
end
|
|
149
|
+
value_object&.attributes&.each do |attribute|
|
|
150
|
+
held = fields[attribute.name]
|
|
151
|
+
# A SINGLE-FIELD VALUE unwraps to its scalar so it bridges
|
|
152
|
+
# into the element's own (differently named) wrapper; a
|
|
153
|
+
# MULTI-FIELD one (a `state(:en_passant_square)` Square
|
|
154
|
+
# copied off the record) has no scalar to stand in for it
|
|
155
|
+
# and is handed across whole — `Value.for_attribute` keeps a
|
|
156
|
+
# value of the element field's own type as it is.
|
|
157
|
+
fields[attribute.name] = Value.scalar(held) if held.is_a?(Value) && held.to_h.size == 1
|
|
136
158
|
end
|
|
137
|
-
element =
|
|
138
|
-
|
|
139
|
-
|
|
159
|
+
element = if value_object
|
|
160
|
+
Value.build(value_object, fields,
|
|
161
|
+
aggregate)
|
|
162
|
+
else
|
|
163
|
+
entity_element(aggregate, element_type, instance[mutation.target],
|
|
164
|
+
fields)
|
|
165
|
+
end
|
|
140
166
|
|
|
141
167
|
# FROZEN, like every other value the domain hands back. An
|
|
142
168
|
# appended list used to come back mutable, so a caller could
|
|
@@ -215,7 +241,7 @@ module Hecks
|
|
|
215
241
|
end
|
|
216
242
|
if entity.identified_by && !fields.key?(entity.identified_by)
|
|
217
243
|
attribute = entity.attribute(entity.identified_by)
|
|
218
|
-
fields[entity.identified_by] = Value.from_identifier(aggregate, attribute,
|
|
244
|
+
fields[entity.identified_by] = Value.from_identifier(aggregate, attribute, next_identity(current, entity))
|
|
219
245
|
else
|
|
220
246
|
check_entity_collision(aggregate, entity, current, fields)
|
|
221
247
|
end
|
|
@@ -223,6 +249,15 @@ module Hecks
|
|
|
223
249
|
fields
|
|
224
250
|
end
|
|
225
251
|
|
|
252
|
+
# THE MINTED IDENTITY IS ONE PAST THE HIGHEST HELD (C4.5) — not
|
|
253
|
+
# `size + 1`, which repeats an identity the moment the list has
|
|
254
|
+
# ever shrunk. One strategy for the language, so the IR declares
|
|
255
|
+
# none; the Rust generators mint by the same rule.
|
|
256
|
+
def next_identity(current, entity)
|
|
257
|
+
held = Array(current).map { |element| Value.scalar(element[entity.identified_by]).to_i }
|
|
258
|
+
held.max.to_i + 1
|
|
259
|
+
end
|
|
260
|
+
|
|
226
261
|
# THE SAME CHECK #hydrate GIVES EVERY CREATING AGGREGATE COMMAND
|
|
227
262
|
# (`repository.find(id)`, above this file in command_interpreter.rb),
|
|
228
263
|
# one level down. Reached only on the two branches #entity_element
|
|
@@ -47,8 +47,8 @@ module Hecks
|
|
|
47
47
|
# `result` and `transition`/`old_state` default to nil until the step
|
|
48
48
|
# that sets them runs, same as they were unset locals before that point.
|
|
49
49
|
Context = Struct.new(:domain, :aggregate, :command, :args, :repository, :instance, :transition, :old_state,
|
|
50
|
-
:result, :correlation, :route, :plan, :strategy, :persistence_outcome, :
|
|
51
|
-
:dry_run, :correction_bindings)
|
|
50
|
+
:result, :correlation, :route, :plan, :strategy, :persistence_outcome, :pending_delegation,
|
|
51
|
+
:dry_run, :correction_bindings, :outbox_rows)
|
|
52
52
|
|
|
53
53
|
def initialize(registry, rules:)
|
|
54
54
|
@registry = registry
|
|
@@ -82,7 +82,7 @@ module Hecks
|
|
|
82
82
|
ctx.repository = @registry.repository(domain, aggregate)
|
|
83
83
|
lock_id = Identity.best_effort(aggregate, args, route, reference_key: reference_key(command))
|
|
84
84
|
run_dispatch_order_with_isolation(DISPATCH_ORDER, ctx, lock_key_id: lock_id)
|
|
85
|
-
[ctx.instance, ctx.result, ctx.plan, ctx.persistence_outcome]
|
|
85
|
+
[ctx.instance, ctx.result, ctx.plan, ctx.persistence_outcome, ctx.outbox_rows]
|
|
86
86
|
rescue StaleWrite
|
|
87
87
|
attempt += 1
|
|
88
88
|
retry if attempt < MAX_STALE_WRITE_RETRIES
|
|
@@ -116,7 +116,7 @@ module Hecks
|
|
|
116
116
|
# `ctx.repository` is resolved once, in `#call`, before the
|
|
117
117
|
# isolation decision (lock vs. CAS+retry) — not here any more.
|
|
118
118
|
ctx.strategy = ctx.plan.strategy_for(capabilities: ctx.repository.capabilities)
|
|
119
|
-
ctx.instance = step(:hydrate)
|
|
119
|
+
ctx.instance = step(:hydrate) do
|
|
120
120
|
if ctx.plan.complete_state? && ctx.plan.state_independent?
|
|
121
121
|
hydrate_complete_state(ctx.repository, ctx.aggregate, ctx.command, ctx.args, ctx.route, ctx.strategy)
|
|
122
122
|
elsif ctx.plan.complete_state?
|
|
@@ -126,11 +126,11 @@ module Hecks
|
|
|
126
126
|
else
|
|
127
127
|
hydrate_existing(ctx.repository, ctx.aggregate, ctx.command, ctx.args, ctx.route)
|
|
128
128
|
end
|
|
129
|
-
|
|
129
|
+
end
|
|
130
130
|
end
|
|
131
131
|
|
|
132
132
|
def step_enforce_givens(ctx)
|
|
133
|
-
step(:enforce_givens)
|
|
133
|
+
step(:enforce_givens) do
|
|
134
134
|
# STRUCTURAL, before the declared givens — the same ordering
|
|
135
135
|
# NotFound/AlreadyExists already get at hydration: "does the
|
|
136
136
|
# fact this command's corrects names even exist" is not a
|
|
@@ -142,7 +142,7 @@ module Hecks
|
|
|
142
142
|
ctx.correction_bindings = @rules.enforce_correction_target(ctx.instance, ctx.aggregate, ctx.command, domain: ctx.domain)
|
|
143
143
|
@rules.enforce_givens(ctx.instance, ctx.command, ctx.args, domain: ctx.domain,
|
|
144
144
|
declaring: ctx.aggregate, parent: ctx.instance, correction: ctx.correction_bindings)
|
|
145
|
-
|
|
145
|
+
end
|
|
146
146
|
end
|
|
147
147
|
|
|
148
148
|
def step_admissible_transition(ctx)
|
|
@@ -161,11 +161,17 @@ module Hecks
|
|
|
161
161
|
# arithmetic via Value#with, append builds a new array), never
|
|
162
162
|
# edit a held value in place.
|
|
163
163
|
ctx.old_state = ctx.instance.state.dup unless ctx.command.ensures.empty?
|
|
164
|
-
step(:apply_mutations)
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
164
|
+
step(:apply_mutations) do
|
|
165
|
+
# ONE UPDATE SET OVER THE PRE-DISPATCH STATE (C4.2, docs/
|
|
166
|
+
# semantics/bluebook-semantics.md): every effect's sources read
|
|
167
|
+
# `pre` — the state as it was before this command — and its
|
|
168
|
+
# target is written to the candidate; declaration order carries
|
|
169
|
+
# no meaning, and build refuses a field written twice.
|
|
170
|
+
pre = ctx.instance.state.dup
|
|
171
|
+
ctx.command.mutations.each do |mutation|
|
|
172
|
+
apply(ctx.instance, ctx.aggregate, mutation, ctx.args, pre)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
169
175
|
end
|
|
170
176
|
|
|
171
177
|
def step_advance_lifecycle(ctx)
|
|
@@ -188,11 +194,21 @@ module Hecks
|
|
|
188
194
|
# `step_locate_element`/`step_apply_mutations` mutate their OWN
|
|
189
195
|
# freshly-loaded copy — the only difference is WHICH already-in-
|
|
190
196
|
# memory record gets handed in.
|
|
197
|
+
#
|
|
198
|
+
# Reimplements the entity command pipeline's own order (givens,
|
|
199
|
+
# transition, mutations, ensures, emit) inline, deliberately — see
|
|
200
|
+
# the comment above on why this must run strictly between this
|
|
201
|
+
# command's own mutations and its ensures/invariants/save. Splitting
|
|
202
|
+
# it would scatter that exact ordering across method boundaries and
|
|
203
|
+
# force threading element/view/transition/old_element/settled
|
|
204
|
+
# through as parameters — the `with:` bug documented just below is
|
|
205
|
+
# exactly the kind of ordering/plumbing mistake that shape invites.
|
|
206
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
191
207
|
def step_delegate_to_entity(ctx)
|
|
192
208
|
delegation = ctx.command.mutations.find { |mutation| mutation.op == :delegate }
|
|
193
209
|
return unless delegation
|
|
194
210
|
|
|
195
|
-
step(:delegate_to_entity)
|
|
211
|
+
step(:delegate_to_entity) do
|
|
196
212
|
entity_name, _dot, command_name = delegation.target.to_s.rpartition(".")
|
|
197
213
|
entity = ctx.aggregate.entities.find { |e| e.hecks_name == entity_name } ||
|
|
198
214
|
raise(WiringError, "#{ctx.command.hecks_name} delegates_to #{entity_name}." \
|
|
@@ -227,23 +243,32 @@ module Hecks
|
|
|
227
243
|
transition = @rules.admissible_transition(entity, target_command, view)
|
|
228
244
|
|
|
229
245
|
old_element = target_command.ensures.empty? ? nil : element.dup
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
246
|
+
pre = element.dup # C4.2 — the update set reads the element as it was
|
|
247
|
+
target_command.mutations.each do |mutation|
|
|
248
|
+
EntityElement.apply_to_element(@rules, ctx.aggregate, entity, element, mutation, target_args, pre)
|
|
249
|
+
end
|
|
233
250
|
element[entity.lifecycle.field] = transition.target if transition
|
|
234
251
|
|
|
235
252
|
settled = Instance.new(aggregate: entity, id: view.id, state: element)
|
|
236
253
|
@rules.enforce_ensures(settled, target_command, target_args, old: old_element, domain: ctx.domain, parent: ctx.instance)
|
|
237
254
|
|
|
238
|
-
|
|
239
|
-
|
|
255
|
+
# NOT emitted here — C7.2 (docs/semantics/bluebook-semantics.md):
|
|
256
|
+
# a refused command records nothing, and the parent's own
|
|
257
|
+
# `ensures`/`enforce_invariants`/`save` steps still run after
|
|
258
|
+
# this one. The target's emission is parked and performed by
|
|
259
|
+
# `step_emit`, after the parent committed — where every other
|
|
260
|
+
# command's events are emitted too. (Before this, the entity
|
|
261
|
+
# leg's events were on the event log and in the adapter before
|
|
262
|
+
# the parent could refuse.)
|
|
263
|
+
ctx.pending_delegation = [target_command, target_args]
|
|
264
|
+
end
|
|
240
265
|
end
|
|
241
266
|
|
|
242
267
|
def step_enforce_ensures(ctx)
|
|
243
|
-
step(:enforce_ensures)
|
|
268
|
+
step(:enforce_ensures) do
|
|
244
269
|
@rules.enforce_ensures(ctx.instance, ctx.command, ctx.args, old: ctx.old_state,
|
|
245
270
|
domain: ctx.domain, parent: ctx.instance, correction: ctx.correction_bindings || {})
|
|
246
|
-
|
|
271
|
+
end
|
|
247
272
|
end
|
|
248
273
|
|
|
249
274
|
def step_enforce_invariants(ctx)
|
|
@@ -261,41 +286,49 @@ module Hecks
|
|
|
261
286
|
step(:save) do
|
|
262
287
|
@rules.resolve_state_references(ctx.domain, ctx.aggregate, ctx.instance.state)
|
|
263
288
|
seed_projected_fields(ctx)
|
|
264
|
-
ctx.persistence_outcome =
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
289
|
+
ctx.persistence_outcome = persist_instance(ctx)
|
|
290
|
+
raise_for_persistence_outcome!(ctx)
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def persist_instance(ctx)
|
|
295
|
+
if ctx.strategy == DependencyPlanning::ATOMIC_PUT
|
|
296
|
+
# A SECOND CREATION IS NOT A FRESH ONE — see
|
|
297
|
+
# hydrate_complete_state's own comment; the
|
|
298
|
+
# same refusal, on the same terms, for the
|
|
299
|
+
# complete-state path. `insert_only:` asks the
|
|
300
|
+
# ADAPTER to decide and refuse ATOMICALLY
|
|
301
|
+
# (never writing a `creates?` command over an
|
|
302
|
+
# identity that already names a record) rather
|
|
303
|
+
# than this interpreter reading the record
|
|
304
|
+
# first to check — a `repository.find` before
|
|
305
|
+
# every atomic_put would be exactly the read
|
|
306
|
+
# this strategy exists to skip.
|
|
307
|
+
ctx.repository.atomic_put(ctx.instance, insert_only: ctx.command.creates?)
|
|
308
|
+
else
|
|
309
|
+
# `expected_version:` is `ctx.instance.version` — nil for a
|
|
310
|
+
# brand-new record (never read from storage) or when the
|
|
311
|
+
# repository isn't CAS-capable, either of which falls straight
|
|
312
|
+
# through to a plain, unconditional save inside `AppendOnly#save`.
|
|
313
|
+
ctx.repository.save(ctx.instance, expected_version: ctx.instance.version)
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def raise_for_persistence_outcome!(ctx)
|
|
318
|
+
if ctx.persistence_outcome.status == :conflicted
|
|
319
|
+
raise(AlreadyExists, RefusalWording.render("AlreadyExists", "creating_duplicate",
|
|
320
|
+
command: ctx.command.hecks_name, aggregate: ctx.aggregate.hecks_name,
|
|
321
|
+
identity: identity_reading(ctx.aggregate),
|
|
322
|
+
offered: Rendering.describe(ctx.instance.id)))
|
|
323
|
+
elsif ctx.persistence_outcome.status == :stale
|
|
324
|
+
# NOT a `RefusalWording.render` call — this is not a declared
|
|
325
|
+
# vocabulary refusal, just a plain, informative message. See
|
|
326
|
+
# `Runtime::StaleWrite`'s own comment: caught by `#call`'s
|
|
327
|
+
# retry loop, re-raised only once retries are exhausted.
|
|
328
|
+
raise(StaleWrite,
|
|
329
|
+
"#{ctx.command.hecks_name} on #{ctx.aggregate.hecks_name} " \
|
|
330
|
+
"(#{identity_reading(ctx.aggregate)}: #{Rendering.describe(ctx.instance.id)}) lost a race — " \
|
|
331
|
+
"another write committed against this record after it was read")
|
|
299
332
|
end
|
|
300
333
|
end
|
|
301
334
|
|
|
@@ -334,9 +367,10 @@ module Hecks
|
|
|
334
367
|
|
|
335
368
|
# A DELEGATING COMMAND EMITS NOTHING OF ITS OWN (`CommandBuilder#build`'s
|
|
336
369
|
# own guard refuses declaring `emits` alongside `delegates_to`) — its
|
|
337
|
-
# result IS
|
|
338
|
-
#
|
|
339
|
-
# into `@rules.emit` for a command with no
|
|
370
|
+
# result IS the target entity command's own `emits`, parked by
|
|
371
|
+
# `step_delegate_to_entity` and emitted HERE, after save (C7.2), not
|
|
372
|
+
# a second, empty call into `@rules.emit` for a command with no
|
|
373
|
+
# announced events at all.
|
|
340
374
|
#
|
|
341
375
|
# `dry_run:` skips this too, same reasoning as `step_save` — nothing
|
|
342
376
|
# was committed, so `ctx.result` stays nil and `Dispatcher#dry_run?`
|
|
@@ -344,15 +378,20 @@ module Hecks
|
|
|
344
378
|
def step_emit(ctx)
|
|
345
379
|
return if ctx.dry_run
|
|
346
380
|
|
|
347
|
-
ctx.result = step(:emit)
|
|
348
|
-
# `ctx.
|
|
349
|
-
# and only when this command carries a
|
|
350
|
-
#
|
|
351
|
-
|
|
352
|
-
|
|
381
|
+
ctx.result = step(:emit) do
|
|
382
|
+
# `ctx.pending_delegation` is only ever set by
|
|
383
|
+
# `step_delegate_to_entity`, and only when this command carries a
|
|
384
|
+
# `:delegate` mutation.
|
|
385
|
+
if ctx.pending_delegation
|
|
386
|
+
target_command, target_args = ctx.pending_delegation
|
|
387
|
+
# The same dispatch, so the same correlation — a saga-driven
|
|
388
|
+
# door's events used to lose their stamp here.
|
|
389
|
+
next @rules.emit(target_command, ctx.domain, ctx.aggregate, ctx.instance, target_args, ctx.repository,
|
|
390
|
+
ctx.correlation)
|
|
391
|
+
end
|
|
353
392
|
|
|
354
393
|
@rules.emit(ctx.command, ctx.domain, ctx.aggregate, ctx.instance, ctx.args, ctx.repository, ctx.correlation)
|
|
355
|
-
|
|
394
|
+
end
|
|
356
395
|
end
|
|
357
396
|
|
|
358
397
|
def hydrate_existing(repository, aggregate, command, args, route = nil)
|
|
@@ -143,9 +143,12 @@ module Hecks
|
|
|
143
143
|
attrs = attrs.merge(parent: parent.state) if parent
|
|
144
144
|
attrs = attrs.merge(correction) unless correction.empty?
|
|
145
145
|
command.givens.each do |given|
|
|
146
|
-
next if Bluebook::Expression::Evaluator.
|
|
146
|
+
next if Bluebook::Expression::Evaluator.call_rule(given, state, attrs)
|
|
147
147
|
|
|
148
|
-
raise GivenNotMet
|
|
148
|
+
raise GivenNotMet.new(
|
|
149
|
+
"#{command.hecks_name} refused — #{given.description}",
|
|
150
|
+
detail: Bluebook::Expression::Evaluator.comparison_detail(given.canonical, state, attrs)
|
|
151
|
+
)
|
|
149
152
|
end
|
|
150
153
|
|
|
151
154
|
enforce_lifecycle_guard(declaring, command, subject) if declaring
|
|
@@ -177,6 +180,16 @@ module Hecks
|
|
|
177
180
|
# one, made deliberately: `as:` reads as "the instance being
|
|
178
181
|
# corrected," which is naturally the latest fact on record, not
|
|
179
182
|
# an arbitrary one.
|
|
183
|
+
# C9.2 (docs/semantics/bluebook-semantics.md) — a correction target
|
|
184
|
+
# is judged against the record's DURABLE history: the events the
|
|
185
|
+
# aggregate's own store recorded (`AppendOnly#events`), which
|
|
186
|
+
# survive a restart the way the Rust kernel's persisted
|
|
187
|
+
# `emitted_<event>` flag does. The in-process log is the fallback
|
|
188
|
+
# only for an adapter that records no readable history.
|
|
189
|
+
def correction_history(domain, aggregate)
|
|
190
|
+
@registry.repository(domain, aggregate).events || @registry.event_log
|
|
191
|
+
end
|
|
192
|
+
|
|
180
193
|
def enforce_correction_target(instance, aggregate, command, domain:)
|
|
181
194
|
bindings = {}
|
|
182
195
|
command.mutations.each do |mutation|
|
|
@@ -184,8 +197,8 @@ module Hecks
|
|
|
184
197
|
|
|
185
198
|
event_key = "#{domain}::#{aggregate.hecks_name}"
|
|
186
199
|
event_name = mutation.target.to_s
|
|
187
|
-
corrected =
|
|
188
|
-
event.name == event_name && event.aggregate == event_key && event.id == instance.id
|
|
200
|
+
corrected = correction_history(domain, aggregate).reverse.find do |event|
|
|
201
|
+
event.name == event_name && event.aggregate == event_key && event.id.to_s == instance.id.to_s
|
|
189
202
|
end
|
|
190
203
|
|
|
191
204
|
unless corrected
|
|
@@ -257,12 +270,17 @@ module Hecks
|
|
|
257
270
|
# one) wins right alongside it — a settled-record ensures can
|
|
258
271
|
# reference the correction target exactly as freely as a
|
|
259
272
|
# pre-mutation given already can.
|
|
260
|
-
|
|
273
|
+
# C2.3 (docs/semantics/bluebook-semantics.md) — an ensures reads
|
|
274
|
+
# the SETTLED STATE first: an argument that shares a field's
|
|
275
|
+
# name does not shadow the candidate here (it does in a given,
|
|
276
|
+
# C2.2), so `sets :note` + `ensures { note == ... }` judges what
|
|
277
|
+
# landed, and `old.<field>` remains the pre-state.
|
|
278
|
+
attrs = args.reject { |name, _| subject.key?(name) }.merge(dereference(domain, command, args))
|
|
261
279
|
attrs = attrs.merge(parent: parent.state) if parent
|
|
262
280
|
attrs = attrs.merge(correction) unless correction.empty?
|
|
263
281
|
attrs = attrs.merge(old: old)
|
|
264
282
|
command.ensures.each do |rule|
|
|
265
|
-
next if Bluebook::Expression::Evaluator.
|
|
283
|
+
next if Bluebook::Expression::Evaluator.call_rule(rule, state, attrs)
|
|
266
284
|
|
|
267
285
|
raise EnsuresNotMet, "#{command.hecks_name} refused — #{rule.description}"
|
|
268
286
|
end
|
|
@@ -291,7 +309,7 @@ module Hecks
|
|
|
291
309
|
state = GuardState.new(subject)
|
|
292
310
|
attrs = {}
|
|
293
311
|
aggregate.invariants.each do |invariant|
|
|
294
|
-
next if Bluebook::Expression::Evaluator.
|
|
312
|
+
next if Bluebook::Expression::Evaluator.call_rule(invariant, state, attrs)
|
|
295
313
|
|
|
296
314
|
raise InvariantViolation, "#{aggregate.hecks_name} refused — #{invariant.description}"
|
|
297
315
|
end
|
|
@@ -336,7 +354,7 @@ module Hecks
|
|
|
336
354
|
attrs = { parent: owner_instance.state }
|
|
337
355
|
|
|
338
356
|
entity.invariants.each do |invariant|
|
|
339
|
-
next if Bluebook::Expression::Evaluator.
|
|
357
|
+
next if Bluebook::Expression::Evaluator.call_rule(invariant, element_state, attrs)
|
|
340
358
|
|
|
341
359
|
raise InvariantViolation, "#{entity.hecks_name} refused — #{invariant.description}"
|
|
342
360
|
end
|