hecks 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +97 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +3 -2
- data/lib/hecks/adapters/driven/postgres.rb +25 -14
- 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 +17 -1
- 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 +2 -1
- 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/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 +19 -472
- 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 +11 -3
- 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 +40 -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 +23 -13
- 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/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/projector/cli_projector.rb +49 -29
- data/lib/hecks/projector/docs_projector.rb +21 -12
- data/lib/hecks/projector/exporter.rb +10 -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/null_policy.rb +6 -2
- data/lib/hecks/query_specification/common/options.rb +17 -5
- 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 +29 -2
- 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 +26 -3
- 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 +207 -48
- 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 +14 -2
|
@@ -8,6 +8,13 @@ require_relative "../query_specification/field_path"
|
|
|
8
8
|
|
|
9
9
|
module Hecks
|
|
10
10
|
module Runtime
|
|
11
|
+
# Interprets one declared `read_model`: resolves each of its
|
|
12
|
+
# `include`d heads (root first, then the rest in dependency order),
|
|
13
|
+
# matches non-root heads to already-projected rows by reference
|
|
14
|
+
# field, and applies group_by/count/median reduction where declared.
|
|
15
|
+
# Prefers a native SQLite escape hatch (#query_read_model) when the
|
|
16
|
+
# model is simple enough for one; otherwise runs the whole join
|
|
17
|
+
# in-process against loaded records.
|
|
11
18
|
class ReadModelInterpreter
|
|
12
19
|
def initialize(registry) = @registry = registry
|
|
13
20
|
|
|
@@ -17,6 +24,19 @@ module Hecks
|
|
|
17
24
|
|
|
18
25
|
private
|
|
19
26
|
|
|
27
|
+
# ROOT-FIRST, THEN THE SQLITE ESCAPE HATCH, THEN THE JOIN LOOP —
|
|
28
|
+
# each step's own comment names a real, previously-shipped bug the
|
|
29
|
+
# current ORDER fixes (the reference/TenantScope refusal ordering
|
|
30
|
+
# above, the root-first head processing below). Splitting this
|
|
31
|
+
# into smaller methods would scatter that ordering across method
|
|
32
|
+
# boundaries where a future editor could silently break it, and
|
|
33
|
+
# would force threading `rootless`/`reference_id`/`eligible`/
|
|
34
|
+
# `projected`/`rows_by_as` between the pieces as parameters and
|
|
35
|
+
# return values.
|
|
36
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
37
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
38
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
39
|
+
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
20
40
|
def project(domain, model, args)
|
|
21
41
|
bluebook = @registry.bluebook(domain)
|
|
22
42
|
rootless = model.reference_target.nil?
|
|
@@ -46,7 +66,10 @@ module Hecks
|
|
|
46
66
|
# than narrowed.
|
|
47
67
|
unless rootless || model.group_by.any? || model.count? || model.median_field
|
|
48
68
|
repository = @registry.read_repository(domain, bluebook.aggregate(model.reference_target))
|
|
49
|
-
|
|
69
|
+
if repository.respond_to?(:query_read_model) && repository.adapter.respond_to?(:query_read_model)
|
|
70
|
+
return repository.query_read_model(domain, model, args,
|
|
71
|
+
bluebook)
|
|
72
|
+
end
|
|
50
73
|
end
|
|
51
74
|
|
|
52
75
|
# ROOT FIRST, ALWAYS — regardless of `include` order in the
|
|
@@ -103,7 +126,7 @@ module Hecks
|
|
|
103
126
|
# Declared order preserved in the OUTPUT — only the
|
|
104
127
|
# computation above needed reordering, not what a caller sees
|
|
105
128
|
# back.
|
|
106
|
-
heads = model.aggregate_heads.
|
|
129
|
+
heads = model.aggregate_heads.to_h { |head| [head[:as], rows_by_as[head[:as]]] }
|
|
107
130
|
grouped_head = group_by_target(model, bluebook)
|
|
108
131
|
reduced_head = aggregation_target(model, bluebook)
|
|
109
132
|
[heads.each_with_object({}) do |(as, value), out|
|
|
@@ -343,7 +366,7 @@ module Hecks
|
|
|
343
366
|
.map(&:name)
|
|
344
367
|
end
|
|
345
368
|
|
|
346
|
-
def matching(records) = records.select
|
|
369
|
+
def matching(records, &) = records.select(&).sort_by(&:id)
|
|
347
370
|
def row(record) = record.to_h
|
|
348
371
|
end
|
|
349
372
|
end
|
|
@@ -13,99 +13,105 @@ module Hecks
|
|
|
13
13
|
# every dispatch, the same reason Expression::Evaluator::
|
|
14
14
|
# SIGN_TEST_OPERATORS is hand-typed beside Vocabulary::SignTest.
|
|
15
15
|
module RefusalWording
|
|
16
|
+
# `Layout/HashAlignment`'s repo-wide `table` style (.rubocop.yml)
|
|
17
|
+
# would force every template string below onto the SAME column —
|
|
18
|
+
# matching the widest key — leaving the longer wordings almost no
|
|
19
|
+
# room to wrap under Layout/LineLength's own 130-column limit.
|
|
20
|
+
# Disabled for exactly this one hash literal (a single statement, so
|
|
21
|
+
# disable-next rather than a disable/enable pair).
|
|
22
|
+
# rubocop:disable-next Layout/HashAlignment
|
|
16
23
|
TEMPLATES = {
|
|
17
24
|
%w[NotFound creating_no_identity] =>
|
|
18
|
-
|
|
25
|
+
"{command} creates a {aggregate} — pass {identity}:",
|
|
19
26
|
%w[AlreadyExists creating_duplicate] =>
|
|
20
|
-
|
|
27
|
+
"{command} creates a {aggregate} that already exists — {identity} {offered}",
|
|
21
28
|
%w[AlreadyExists entity_duplicate] =>
|
|
22
|
-
|
|
29
|
+
"a {entity} already exists on {aggregate} — {identity} {offered}",
|
|
23
30
|
%w[NotFound acting_no_identity] =>
|
|
24
|
-
|
|
31
|
+
"{command} acts on an existing {aggregate} — pass {identity}:",
|
|
25
32
|
%w[NotFound record_missing] =>
|
|
26
|
-
|
|
33
|
+
"no {aggregate} with {identity} {offered}",
|
|
27
34
|
%w[NotFound entity_parent_no_identity] =>
|
|
28
|
-
|
|
35
|
+
"{command} acts on a {aggregate}'s {entity} — pass {identity}:",
|
|
29
36
|
%w[UnknownVerb entity_unknown] =>
|
|
30
|
-
|
|
37
|
+
"{aggregate} has no entity {entity}",
|
|
31
38
|
%w[NotFound entity_element_no_identity] =>
|
|
32
|
-
|
|
39
|
+
"{command} acts on one {entity} — pass {identity}:",
|
|
33
40
|
%w[NotFound entity_element_missing] =>
|
|
34
|
-
|
|
41
|
+
"no {entity} with {identity} {wants} on {aggregate} {parent_id}",
|
|
35
42
|
%w[NotFound reference_target_missing] =>
|
|
36
|
-
|
|
43
|
+
"no {target} with {heads} {key}",
|
|
37
44
|
%w[NotFound read_model_reference_missing] =>
|
|
38
|
-
|
|
45
|
+
"no {aggregate} with reference {offered}",
|
|
39
46
|
%w[TypeMismatch read_model_object_reference] =>
|
|
40
|
-
|
|
47
|
+
"{query} refused — a reference is an id, and {field} arrived as an object",
|
|
41
48
|
%w[UnknownVerb no_query] =>
|
|
42
|
-
|
|
49
|
+
"{aggregate} has no query {query}",
|
|
43
50
|
%w[UnknownVerb entity_query_missing] =>
|
|
44
|
-
|
|
51
|
+
"{entity} has no query {query}",
|
|
45
52
|
%w[UnknownVerb entity_holds_no_list] =>
|
|
46
|
-
|
|
53
|
+
"{aggregate} holds no list of {entity}",
|
|
47
54
|
%w[UnknownVerb entity_no_command] =>
|
|
48
|
-
|
|
55
|
+
"{entity} has no command {command}",
|
|
49
56
|
%w[UnknownVerb aggregate_no_command] =>
|
|
50
|
-
|
|
57
|
+
"{aggregate} has no command {command}",
|
|
51
58
|
%w[UnknownVerb port_no_operation] =>
|
|
52
|
-
|
|
59
|
+
"{port} has no operation {operation}",
|
|
53
60
|
%w[UnknownVerb no_domain] =>
|
|
54
|
-
|
|
61
|
+
"no domain {domain} loaded (verb {verb})",
|
|
55
62
|
%w[UnknownVerb no_read_model] =>
|
|
56
|
-
|
|
63
|
+
"{domain} has no read model {query}",
|
|
57
64
|
%w[UnknownVerb not_fully_qualified] =>
|
|
58
|
-
|
|
65
|
+
"{verb} is not a fully-qualified verb (Domain::Aggregate.Command)",
|
|
59
66
|
%w[UnknownVerb no_aggregate] =>
|
|
60
|
-
|
|
67
|
+
"{domain} has no aggregate {aggregate}",
|
|
61
68
|
%w[LifecycleRefused transition_blocked] =>
|
|
62
|
-
|
|
69
|
+
"{command} refused — {field} is {current}, and {command} moves it only from {allowed}",
|
|
63
70
|
%w[TypeMismatch value_object_shape] =>
|
|
64
|
-
|
|
71
|
+
"{name} is a {type} — pass its fields as an object, not {offered}",
|
|
65
72
|
%w[TypeMismatch reference_as_object] =>
|
|
66
|
-
|
|
73
|
+
"{command} refused — a reference is an id, and {attribute} arrived as an object{known_by}",
|
|
67
74
|
%w[TypeMismatch multi_field_scalar] =>
|
|
68
|
-
|
|
75
|
+
"{type} has multiple fields and cannot stand in for a scalar",
|
|
69
76
|
%w[TypeMismatch composite_identity] =>
|
|
70
|
-
|
|
77
|
+
"{type} is a composite identity — an identity must have exactly one field",
|
|
71
78
|
%w[TypeMismatch numeric_field] =>
|
|
72
|
-
|
|
79
|
+
"{type}.{field} expects {expected}, got {offered}",
|
|
73
80
|
%w[TypeMismatch non_finite_field] =>
|
|
74
|
-
|
|
81
|
+
"{type}.{field} must be a finite number, got {offered}",
|
|
82
|
+
%w[TypeMismatch integer_range] =>
|
|
83
|
+
"{type}.{field} must fit in a 64-bit integer, got {offered}",
|
|
75
84
|
%w[TypeMismatch pattern_mismatch] =>
|
|
76
|
-
|
|
85
|
+
"{type}.{field} must match {pattern}, got {offered}",
|
|
77
86
|
%w[TypeMismatch arithmetic_amount] =>
|
|
78
|
-
|
|
87
|
+
"{op} of {target} needs an Integer, got {offered}",
|
|
79
88
|
%w[TypeMismatch arithmetic_current] =>
|
|
80
|
-
|
|
89
|
+
"{op} of {target} needs an Integer {target}, got {offered}",
|
|
81
90
|
%w[TypeMismatch arithmetic_shared_field] =>
|
|
82
|
-
|
|
91
|
+
"{op} of {target} needs a value object with one shared Integer field",
|
|
83
92
|
%w[UnknownArgument unknown_args] =>
|
|
84
|
-
|
|
93
|
+
"{command} does not declare {unknown} — it takes {declared}",
|
|
85
94
|
%w[AbsentArgument absent_args] =>
|
|
86
|
-
|
|
95
|
+
"{command} was not given {absent} — it takes {declared}",
|
|
87
96
|
%w[InvariantViolation closed_set_member] =>
|
|
88
|
-
|
|
97
|
+
"{type} admits {admitted} — got {offered}",
|
|
89
98
|
%w[InvariantViolation value_object_invariant] =>
|
|
90
|
-
|
|
99
|
+
"{name} invariant violated — {description} (given {offered})",
|
|
91
100
|
%w[InvariantViolation admits_declared_set] =>
|
|
92
|
-
|
|
101
|
+
"{name} admits {admits} — {admitted} — got {offered}",
|
|
93
102
|
%w[InvariantViolation undeclared_set] =>
|
|
94
|
-
|
|
95
|
-
|
|
103
|
+
"{name} admits {admits}, which this chapter does not declare — a closed set is named Aggregate::SetName, and it must " \
|
|
104
|
+
"be one the bluebook actually holds",
|
|
96
105
|
%w[Unauthorized tenant_required] =>
|
|
97
|
-
|
|
98
|
-
"{field} this ask is scoped to",
|
|
106
|
+
"{query} declares authorize with tenant: {field} — pass {field}: to name which {field} this ask is scoped to",
|
|
99
107
|
%w[Unauthorized role_mismatch] =>
|
|
100
|
-
|
|
108
|
+
"{command} refused — role: {role}, and the caller stated {caller_role}",
|
|
101
109
|
%w[AttributeAbsent absent_read] =>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
"default: ...), or declare it optional: true",
|
|
110
|
+
"{aggregate} {field} is absent on this record — declared, not optional, and added since it was written. Backfill it " \
|
|
111
|
+
"in a translation (backfill :{field}, default: ...), or declare it optional: true",
|
|
105
112
|
%w[ProjectionAbsent absent_read] =>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
"or read {reference}.{remote_field} directly if this rule cannot wait"
|
|
113
|
+
"{aggregate} {field} is not yet projected on this record — declared via projects :{field}, but no rebuild sweep has " \
|
|
114
|
+
"populated it. Run the sweep, or read {reference}.{remote_field} directly if this rule cannot wait"
|
|
109
115
|
}.freeze
|
|
110
116
|
|
|
111
117
|
module_function
|
|
@@ -42,7 +42,9 @@ module Hecks
|
|
|
42
42
|
# why reusing that one would deadlock.
|
|
43
43
|
def saga_persistence(domain)
|
|
44
44
|
key = domain.to_s
|
|
45
|
-
@saga_persistence[key] || @saga_persistence_mutex.synchronize
|
|
45
|
+
@saga_persistence[key] || @saga_persistence_mutex.synchronize do
|
|
46
|
+
@saga_persistence[key] ||= resolve_saga_persistence(key)
|
|
47
|
+
end
|
|
46
48
|
end
|
|
47
49
|
|
|
48
50
|
# WALKS EVERY LOADED DOMAIN, repopulating `saga_instances` from
|
|
@@ -45,6 +45,7 @@ module Hecks
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
warn_undurable_sagas!(hexagon)
|
|
48
|
+
warn_undurable_outbox!(hexagon)
|
|
48
49
|
end
|
|
49
50
|
self
|
|
50
51
|
end
|
|
@@ -61,10 +62,10 @@ module Hecks
|
|
|
61
62
|
)
|
|
62
63
|
adapter_class(name)
|
|
63
64
|
self
|
|
64
|
-
rescue WiringError =>
|
|
65
|
+
rescue WiringError => e
|
|
65
66
|
raise WiringError,
|
|
66
67
|
"the default persistence adapter (#{name}) is not usable, so an " \
|
|
67
|
-
"aggregate with no bind could not be given one: #{
|
|
68
|
+
"aggregate with no bind could not be given one: #{e.message}"
|
|
68
69
|
end
|
|
69
70
|
|
|
70
71
|
def check_verb(bind)
|
|
@@ -227,6 +228,42 @@ module Hecks
|
|
|
227
228
|
# refusing the boot outright would break a choice an author made
|
|
228
229
|
# deliberately. What a deploy needs is for the gap to be loud and
|
|
229
230
|
# undeniable, not for local dev/test to become impossible.
|
|
231
|
+
# THE OUTBOX'S TWIN OF `warn_undurable_sagas!` — a domain that
|
|
232
|
+
# declares anything a commit could owe a reaction to (a policy
|
|
233
|
+
# listening to one of its events, or a process manager) but is
|
|
234
|
+
# bound to an adapter with no outbox (`AppendOnly#outbox?`) gets
|
|
235
|
+
# reactions the pre-outbox way: run inline, lost on a crash
|
|
236
|
+
# between commit and reaction. A warning, not a refusal, for the
|
|
237
|
+
# reason `warn_undurable_sagas!` gives — and Memory HAS an outbox
|
|
238
|
+
# (in-process, like everything else it holds), so a dev/test
|
|
239
|
+
# boot stays quiet; this speaks up for the file/remote adapters
|
|
240
|
+
# that persist state durably but hand reactions to nothing.
|
|
241
|
+
def warn_undurable_outbox!(hexagon)
|
|
242
|
+
bluebook_ir = bluebook(hexagon.domain)
|
|
243
|
+
return unless bluebook_ir
|
|
244
|
+
return if bluebook_ir.process_managers.empty? && !any_policy_listens_to?(bluebook_ir)
|
|
245
|
+
|
|
246
|
+
anchor = bluebook_ir.aggregates.first or return
|
|
247
|
+
bind = Ports::Persistence::BindingPolicy.resolve(self, hexagon.domain, anchor)
|
|
248
|
+
return if adapter_class(bind.adapter) <= Ports::Persistence::RemoteRuntime
|
|
249
|
+
return if repository(hexagon.domain, anchor).outbox?
|
|
250
|
+
|
|
251
|
+
warn "[hecks] #{hexagon.domain} declares policies/process_managers but its persistence adapter " \
|
|
252
|
+
"(#{bind.adapter}) has no outbox — reactions run inline and a crash between a command's commit " \
|
|
253
|
+
"and its reactions loses them silently. Bind SqlitePersistence or Postgres for a durable outbox " \
|
|
254
|
+
"(see Runtime::Outbox), or accept in-process-only reactions on purpose."
|
|
255
|
+
rescue WiringError
|
|
256
|
+
nil
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def any_policy_listens_to?(bluebook_ir)
|
|
260
|
+
emitted = bluebook_ir.aggregates.flat_map do |aggregate|
|
|
261
|
+
aggregate.commands.flat_map(&:emits) +
|
|
262
|
+
aggregate.ports.flat_map { |port| port.operations.flat_map { |op| [*op.emits, op.answers, op.refuses] } }
|
|
263
|
+
end.compact.map(&:to_s)
|
|
264
|
+
@bluebooks.each_value.any? { |candidate| candidate.policies.any? { |policy| emitted.include?(policy.event_name.to_s) } }
|
|
265
|
+
end
|
|
266
|
+
|
|
230
267
|
def warn_undurable_sagas!(hexagon)
|
|
231
268
|
bluebook_ir = bluebook(hexagon.domain)
|
|
232
269
|
return unless bluebook_ir
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require_relative "registry/verification"
|
|
2
2
|
require_relative "registry/saga_persistence"
|
|
3
|
+
require_relative "outbox"
|
|
3
4
|
|
|
4
5
|
module Hecks
|
|
5
6
|
module Runtime
|
|
@@ -87,8 +88,16 @@ module Hecks
|
|
|
87
88
|
# reason).
|
|
88
89
|
@saga_persistence = {}
|
|
89
90
|
@saga_persistence_mutex = Mutex.new
|
|
91
|
+
@outbox = Outbox::Relay.new(self)
|
|
90
92
|
end
|
|
91
93
|
|
|
94
|
+
# THE OUTBOX RELAY — one per registry, for its whole life (built
|
|
95
|
+
# here, never swapped, so no thread ever sees a different one).
|
|
96
|
+
# It can enqueue from the moment the registry exists; a Dispatcher
|
|
97
|
+
# attaches the interpreters that let it deliver. See
|
|
98
|
+
# `Runtime::Outbox`.
|
|
99
|
+
attr_reader :outbox
|
|
100
|
+
|
|
92
101
|
# THE BUILDER STAYS OPEN FOR THE LIFE OF THIS REGISTRY, keyed by chapter
|
|
93
102
|
# name — see the comment on `BluebookBuilder.build`. A chapter split across
|
|
94
103
|
# several files (`language/bluebook/*.bluebook`, all `Hecks.bluebook "Bluebook"`)
|
|
@@ -201,6 +210,7 @@ module Hecks
|
|
|
201
210
|
@saga_instances.clear
|
|
202
211
|
@repositories = {}
|
|
203
212
|
@projection_repositories = {}
|
|
213
|
+
@outbox.log.clear
|
|
204
214
|
rehydrate_sagas!
|
|
205
215
|
self
|
|
206
216
|
end
|
|
@@ -249,13 +259,13 @@ module Hecks
|
|
|
249
259
|
# authoritative bind" check is what actually catches a genuine
|
|
250
260
|
# double-bind; this merge only concatenates, it does not itself
|
|
251
261
|
# decide which of two binds for the same aggregate wins.
|
|
252
|
-
def merge_hecksagons(
|
|
262
|
+
def merge_hecksagons(base, overlay)
|
|
253
263
|
Bluebook::Hecksagon.new(
|
|
254
|
-
domain:
|
|
255
|
-
binds:
|
|
256
|
-
subscriptions:
|
|
257
|
-
framework_members:
|
|
258
|
-
vendored_bluebooks:
|
|
264
|
+
domain: base.domain,
|
|
265
|
+
binds: base.binds + overlay.binds,
|
|
266
|
+
subscriptions: base.subscriptions + overlay.subscriptions,
|
|
267
|
+
framework_members: base.framework_members + overlay.framework_members,
|
|
268
|
+
vendored_bluebooks: base.vendored_bluebooks + overlay.vendored_bluebooks
|
|
259
269
|
)
|
|
260
270
|
end
|
|
261
271
|
|
|
@@ -266,12 +276,12 @@ module Hecks
|
|
|
266
276
|
# also declares REPLACES that key's whole resolved hash (the same
|
|
267
277
|
# all-or-nothing shape `WorldBuilder#method_missing` already builds
|
|
268
278
|
# each entry as), it does not deep-merge field by field within it.
|
|
269
|
-
def merge_worlds(
|
|
279
|
+
def merge_worlds(base, overlay)
|
|
270
280
|
Bluebook::World.new(
|
|
271
|
-
domain:
|
|
272
|
-
realm:
|
|
273
|
-
latest:
|
|
274
|
-
settings:
|
|
281
|
+
domain: base.domain,
|
|
282
|
+
realm: overlay.realm || base.realm,
|
|
283
|
+
latest: overlay.latest || base.latest,
|
|
284
|
+
settings: base.settings.merge(overlay.settings)
|
|
275
285
|
)
|
|
276
286
|
end
|
|
277
287
|
end
|
|
@@ -81,7 +81,9 @@ module Hecks
|
|
|
81
81
|
# Lambda that has no way to represent its lineage history at
|
|
82
82
|
# all.
|
|
83
83
|
adapter_name = Ports::Persistence::BindingPolicy.resolve(@registry, domain, aggregate).adapter
|
|
84
|
-
|
|
84
|
+
unless @registry.adapter_class(adapter_name) <= Ports::Persistence::RemoteRuntime
|
|
85
|
+
return @local.dispatch(verb, saga_correlation: saga_correlation, **args)
|
|
86
|
+
end
|
|
85
87
|
|
|
86
88
|
response = @client.dispatch(verb, args)
|
|
87
89
|
|
|
@@ -19,74 +19,87 @@ module Hecks
|
|
|
19
19
|
def envelope(to, entity_depth: 0)
|
|
20
20
|
return nil if to.nil?
|
|
21
21
|
|
|
22
|
-
aggregate, entities =
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
unless unknown.empty?
|
|
26
|
-
raise TypeMismatch, "to: does not recognize #{unknown.sort.join(', ')}"
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
[hash[:aggregate], entity_identities(hash)]
|
|
30
|
-
else
|
|
31
|
-
[to, []]
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
if aggregate.nil? || aggregate.to_s.empty?
|
|
35
|
-
raise TypeMismatch, "to: must name the receiving aggregate identity"
|
|
36
|
-
end
|
|
22
|
+
aggregate, entities = to.is_a?(Hash) ? parse_envelope_hash(to) : [to, []]
|
|
23
|
+
|
|
24
|
+
raise TypeMismatch, "to: must name the receiving aggregate identity" if aggregate.nil? || aggregate.to_s.empty?
|
|
37
25
|
if entities.size != entity_depth
|
|
38
26
|
raise TypeMismatch,
|
|
39
27
|
"to: for an entity command needs #{entity_depth} entity " \
|
|
40
28
|
"#{entity_depth == 1 ? 'identity' : 'identities'} after the aggregate — got #{entities.size}"
|
|
41
29
|
end
|
|
42
|
-
if entities.any?
|
|
43
|
-
|
|
30
|
+
raise TypeMismatch, "to: contains a blank entity identity" if entities.any? do |identity|
|
|
31
|
+
identity.nil? || identity.to_s.empty?
|
|
44
32
|
end
|
|
45
33
|
|
|
46
34
|
Envelope.new(aggregate: aggregate, entities: entities)
|
|
47
35
|
end
|
|
48
36
|
|
|
37
|
+
# The Hash-shaped half of `to:` — pulled out of `envelope` because
|
|
38
|
+
# it is a self-contained parse (raises on an unrecognized key, then
|
|
39
|
+
# returns the pair) with no dependency on anything `envelope` does
|
|
40
|
+
# afterward; the validations that follow apply the same way
|
|
41
|
+
# whichever branch produced `aggregate`/`entities`.
|
|
42
|
+
def parse_envelope_hash(to)
|
|
43
|
+
hash = to.transform_keys(&:to_sym)
|
|
44
|
+
unknown = hash.keys - %i[aggregate entity entities]
|
|
45
|
+
raise TypeMismatch, "to: does not recognize #{unknown.sort.join(', ')}" unless unknown.empty?
|
|
46
|
+
|
|
47
|
+
[hash[:aggregate], entity_identities(hash)]
|
|
48
|
+
end
|
|
49
|
+
private_class_method :parse_envelope_hash
|
|
50
|
+
|
|
49
51
|
# `with:` is deliberately strict. Compatibility-only calls still pass
|
|
50
52
|
# loose keyword arguments through the old addressing gate, but a caller
|
|
51
53
|
# choosing the explicit envelope cannot smuggle receiver identity back
|
|
52
54
|
# into the payload.
|
|
53
55
|
def payload(command, with:, legacy:)
|
|
54
56
|
if with && !legacy.empty?
|
|
55
|
-
raise TypeMismatch,
|
|
57
|
+
raise TypeMismatch,
|
|
58
|
+
"dispatch takes command facts in with:, not both with: and loose keyword arguments"
|
|
56
59
|
end
|
|
57
60
|
|
|
58
61
|
return legacy unless with
|
|
59
|
-
unless with.is_a?(Hash)
|
|
60
|
-
raise TypeMismatch, "with: must be a hash of command facts"
|
|
61
|
-
end
|
|
62
|
+
raise TypeMismatch, "with: must be a hash of command facts" unless with.is_a?(Hash)
|
|
62
63
|
|
|
63
64
|
offered = with.transform_keys(&:to_sym)
|
|
64
65
|
declared = command.attributes.map { |attribute| attribute.name.to_sym }
|
|
66
|
+
refuse_unknown_facts!(command, offered, declared)
|
|
67
|
+
refuse_absent_facts!(command, offered, declared)
|
|
68
|
+
offered
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The two `with:` shape checks `payload` runs in sequence — pulled
|
|
72
|
+
# out because each is a self-contained "compute a difference, raise
|
|
73
|
+
# if non-empty" rule with no dependency on the other. Order stays
|
|
74
|
+
# unknown-before-absent, exactly as inline: `payload` calls them in
|
|
75
|
+
# that order, so a fact that is both unknown AND leaves something
|
|
76
|
+
# else absent still raises UnknownArgument first, same as before.
|
|
77
|
+
def refuse_unknown_facts!(command, offered, declared)
|
|
65
78
|
unknown = (offered.keys - declared).sort
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
79
|
+
return if unknown.empty?
|
|
80
|
+
|
|
81
|
+
reading = declared.empty? ? "none" : declared.join(", ")
|
|
82
|
+
raise UnknownArgument,
|
|
83
|
+
RefusalWording.render("UnknownArgument", "unknown_args",
|
|
84
|
+
command: command.hecks_name, unknown: unknown.join(", "),
|
|
85
|
+
declared: reading)
|
|
86
|
+
end
|
|
87
|
+
private_class_method :refuse_unknown_facts!
|
|
73
88
|
|
|
89
|
+
def refuse_absent_facts!(command, offered, declared)
|
|
74
90
|
absent = command.attributes.reject(&:optional?).map { |attribute| attribute.name.to_sym } - offered.keys
|
|
75
|
-
|
|
76
|
-
reading = declared.empty? ? "none" : declared.join(", ")
|
|
77
|
-
raise AbsentArgument,
|
|
78
|
-
RefusalWording.render("AbsentArgument", "absent_args",
|
|
79
|
-
command: command.hecks_name, absent: absent.sort.join(", "),
|
|
80
|
-
declared: reading)
|
|
81
|
-
end
|
|
91
|
+
return if absent.empty?
|
|
82
92
|
|
|
83
|
-
|
|
93
|
+
reading = declared.empty? ? "none" : declared.join(", ")
|
|
94
|
+
raise AbsentArgument,
|
|
95
|
+
RefusalWording.render("AbsentArgument", "absent_args",
|
|
96
|
+
command: command.hecks_name, absent: absent.sort.join(", "),
|
|
97
|
+
declared: reading)
|
|
84
98
|
end
|
|
99
|
+
private_class_method :refuse_absent_facts!
|
|
85
100
|
|
|
86
101
|
def entity_identities(hash)
|
|
87
|
-
if hash.key?(:entities) && hash.key?(:entity)
|
|
88
|
-
raise TypeMismatch, "to: takes entity: or entities:, not both"
|
|
89
|
-
end
|
|
102
|
+
raise TypeMismatch, "to: takes entity: or entities:, not both" if hash.key?(:entities) && hash.key?(:entity)
|
|
90
103
|
|
|
91
104
|
hash.key?(:entities) ? Array(hash[:entities]) : Array(hash[:entity])
|
|
92
105
|
end
|
|
@@ -14,8 +14,8 @@ module Hecks
|
|
|
14
14
|
# correlation key even IS is representation-dependent (the object
|
|
15
15
|
# itself? its serialised text?). `:"end_to_end.value"` reads the one
|
|
16
16
|
# field with a single unambiguous rendering.
|
|
17
|
-
def saga_correlation(
|
|
18
|
-
path =
|
|
17
|
+
def saga_correlation(process_manager, event)
|
|
18
|
+
path = process_manager.correlates_by.to_s.split(".")
|
|
19
19
|
# A LATER EVENT MAY ALREADY HOLD THE SCALAR. `reference.value` digs a
|
|
20
20
|
# value object's field out of a FRESH declaration (TransferRequested's
|
|
21
21
|
# `reference` IS a TransferReference) — but a downstream event this
|
|
@@ -45,7 +45,7 @@ module Hecks
|
|
|
45
45
|
# Keyed by `correlation_head`
|
|
46
46
|
# rather than a bare scalar so an event stamped by one saga cannot be
|
|
47
47
|
# misread by an unrelated one correlating on a different field.
|
|
48
|
-
stamped = event.correlation && event.correlation[
|
|
48
|
+
stamped = event.correlation && event.correlation[process_manager.correlation_head.to_s]
|
|
49
49
|
return stamped unless stamped.nil? || stamped.to_s.empty?
|
|
50
50
|
|
|
51
51
|
# A SELF-REFERENCING LEG carries the correlation forward under its
|
|
@@ -69,7 +69,7 @@ module Hecks
|
|
|
69
69
|
# not by which dispatch convention the caller happened to use.
|
|
70
70
|
#
|
|
71
71
|
# GATED, still — a manually-dispatched command on a wholly
|
|
72
|
-
# UNRELATED aggregate can share an event NAME this
|
|
72
|
+
# UNRELATED aggregate can share an event NAME this process_manager happens to
|
|
73
73
|
# handle (`Drawer.Take` also emits "Taken", the same name a
|
|
74
74
|
# SAGA-DISPATCHED leg uses) with nothing this saga should read as
|
|
75
75
|
# its own conversation. What makes a leg genuinely
|
|
@@ -79,17 +79,17 @@ module Hecks
|
|
|
79
79
|
# name: `OnboardingCase.identity_heads` really does include
|
|
80
80
|
# `:reference`, `correlates_by :"reference.value"`'s own head ;
|
|
81
81
|
# `Drawer.identity_heads` is `[:number]`, nowhere close.
|
|
82
|
-
self_identified?(
|
|
82
|
+
self_identified?(process_manager, event) ? event.id : nil
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
-
def self_identified?(
|
|
85
|
+
def self_identified?(process_manager, event)
|
|
86
86
|
domain, bare_name = event.aggregate.to_s.split("::", 2)
|
|
87
87
|
return false unless bare_name
|
|
88
88
|
|
|
89
89
|
construct = @registry.bluebook(domain)&.aggregate(bare_name)
|
|
90
90
|
return false unless construct
|
|
91
91
|
|
|
92
|
-
construct.identity_heads.map(&:to_s).include?(
|
|
92
|
+
construct.identity_heads.map(&:to_s).include?(process_manager.correlation_head.to_s)
|
|
93
93
|
end
|
|
94
94
|
end
|
|
95
95
|
end
|