hecks 1.4.0 → 1.5.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 +65 -0
- data/lib/hecks/adapters/driven/folder.rb +73 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
- data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
- data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +93 -2
- data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +35 -7
- data/lib/hecks/adapters/driven/lambda.rb +43 -2
- data/lib/hecks/adapters/driven/local_storage.rb +67 -1
- data/lib/hecks/adapters/driven/memory.rb +13 -13
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
- data/lib/hecks/adapters/driven/prism.rb +46 -7
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- data/lib/hecks/adapters/driven/system_clock.rb +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
- data/lib/hecks/adapters/driven.rb +2 -0
- data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
- data/lib/hecks/behaviors/dsl.rb +58 -0
- data/lib/hecks/behaviors/expectations.rb +168 -7
- data/lib/hecks/behaviors/ir.rb +11 -0
- data/lib/hecks/behaviors/rspec.rb +8 -0
- data/lib/hecks/behaviors/runner.rb +19 -0
- data/lib/hecks/behaviors.rb +8 -0
- data/lib/hecks/bluebook/aggregate.rb +30 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
- data/lib/hecks/bluebook/assembly/build.rb +25 -4
- data/lib/hecks/bluebook/assembly/contract.rb +66 -14
- data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
- data/lib/hecks/bluebook/assembly/marks.rb +143 -14
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
- data/lib/hecks/bluebook/assembly.rb +25 -7
- data/lib/hecks/bluebook/attribute.rb +17 -3
- data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
- data/lib/hecks/bluebook/behaviour/command.rb +33 -3
- data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
- data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
- data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
- data/lib/hecks/bluebook/behaviour/query.rb +5 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
- data/lib/hecks/bluebook/chapter.rb +23 -0
- data/lib/hecks/bluebook/command.rb +53 -8
- data/lib/hecks/bluebook/domain_port.rb +25 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
- data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
- data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
- data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
- data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
- data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
- data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
- data/lib/hecks/bluebook/entity.rb +29 -0
- data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
- data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
- data/lib/hecks/bluebook/expression/resolver.rb +320 -77
- data/lib/hecks/bluebook/hexagon.rb +34 -0
- data/lib/hecks/bluebook/lifecycle.rb +11 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
- data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
- data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator.rb +180 -84
- data/lib/hecks/bluebook/model_check.rb +268 -24
- data/lib/hecks/bluebook/pattern_subset.rb +23 -1
- data/lib/hecks/bluebook/process_manager.rb +13 -0
- data/lib/hecks/bluebook/project_discovery.rb +5 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +44 -0
- data/lib/hecks/bluebook/query.rb +27 -0
- data/lib/hecks/bluebook/read_model.rb +21 -1
- data/lib/hecks/bluebook/reference.rb +21 -8
- data/lib/hecks/bluebook/smoke_test.rb +29 -6
- data/lib/hecks/bluebook/synthesizer.rb +34 -0
- data/lib/hecks/bluebook/translation.rb +30 -1
- data/lib/hecks/bluebook/value_object.rb +23 -5
- data/lib/hecks/bluebook.rb +3 -4
- data/lib/hecks/codemod.rb +107 -20
- data/lib/hecks/construct.rb +15 -1
- data/lib/hecks/corpus.rb +146 -25
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/doc/reference.rb +185 -16
- data/lib/hecks/embryonaut_bluebook.rb +32 -9
- data/lib/hecks/facade/handle.rb +76 -3
- data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
- data/lib/hecks/forms/field_shape.rb +3 -0
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/port_argument.rb +12 -0
- data/lib/hecks/forms/query_form_renderer.rb +63 -0
- data/lib/hecks/forms/record_renderer.rb +58 -0
- data/lib/hecks/forms/record_table.rb +27 -0
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +10 -0
- data/lib/hecks/fqn.rb +58 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +43 -20
- data/lib/hecks/freezer.rb +17 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
- data/lib/hecks/fuzzing/combination_miner.rb +59 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
- data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
- data/lib/hecks/fuzzing/differential.rb +34 -0
- data/lib/hecks/fuzzing/domain_generator.rb +188 -11
- data/lib/hecks/fuzzing/era_boundary.rb +45 -15
- data/lib/hecks/fuzzing/form_census.rb +86 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
- data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
- data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
- data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
- data/lib/hecks/fuzzing/properties/guards.rb +44 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
- data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
- data/lib/hecks/fuzzing/properties/querying.rb +68 -14
- data/lib/hecks/fuzzing/properties.rb +24 -15
- data/lib/hecks/fuzzing/qa_settings.rb +12 -0
- data/lib/hecks/fuzzing/replay.rb +137 -29
- data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
- data/lib/hecks/fuzzing/self_consistency.rb +189 -40
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
- data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
- data/lib/hecks/fuzzing/shrinker.rb +114 -2
- data/lib/hecks/fuzzing/structural_skips.rb +18 -4
- data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
- data/lib/hecks/fuzzing/value_generator.rb +98 -10
- data/lib/hecks/grammar/evolve.rb +178 -2
- data/lib/hecks/grammar.rb +46 -0
- data/lib/hecks/ir.rb +38 -7
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/literal.rb +32 -0
- data/lib/hecks/naming.rb +88 -7
- data/lib/hecks/ports/access_control.rb +5 -10
- data/lib/hecks/ports/authorization.rb +3 -6
- data/lib/hecks/ports/identity_assignment.rb +1 -2
- data/lib/hecks/ports/identity_resolution.rb +1 -2
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +58 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
- data/lib/hecks/ports/query/in_memory.rb +39 -6
- data/lib/hecks/ports/query/ordering.rb +15 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +43 -8
- data/lib/hecks/projections/diagrams.rb +243 -7
- data/lib/hecks/projections/glossary/html.rb +88 -0
- data/lib/hecks/projections/glossary/markdown.rb +47 -0
- data/lib/hecks/projections/glossary/mermaid.rb +48 -0
- data/lib/hecks/projections/glossary/sentences.rb +105 -0
- data/lib/hecks/projections/glossary.rb +161 -19
- data/lib/hecks/projections/model/deviations.rb +44 -0
- data/lib/hecks/projections/model.rb +51 -1
- data/lib/hecks/projections/oidc.rb +40 -3
- data/lib/hecks/projections/parser_table.rb +49 -8
- data/lib/hecks/projections/reference.rb +12 -0
- data/lib/hecks/projections/rust_vocabulary.rb +219 -16
- data/lib/hecks/projections/shape.rb +8 -0
- data/lib/hecks/projections/statements.rb +63 -16
- data/lib/hecks/projections/vocabulary.rb +17 -0
- data/lib/hecks/projector/cli_projector.rb +218 -10
- data/lib/hecks/projector/docs_projector.rb +145 -19
- data/lib/hecks/projector/exporter.rb +65 -11
- data/lib/hecks/projector/ir_projector.rb +6 -0
- data/lib/hecks/projector/narrate_projector.rb +136 -15
- data/lib/hecks/projector/target.rb +29 -5
- data/lib/hecks/projector.rb +74 -6
- data/lib/hecks/query_ir.rb +47 -0
- data/lib/hecks/query_specification/common/null_policy.rb +5 -3
- data/lib/hecks/rendering.rb +6 -0
- data/lib/hecks/router/namespace_installer.rb +13 -0
- data/lib/hecks/router.rb +55 -0
- data/lib/hecks/runtime/aggregate_lock.rb +9 -0
- data/lib/hecks/runtime/boot_gates.rb +18 -0
- data/lib/hecks/runtime/caller.rb +32 -0
- data/lib/hecks/runtime/capability_graph.rb +11 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
- data/lib/hecks/runtime/command_interpreter.rb +42 -17
- data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
- data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
- data/lib/hecks/runtime/command_rules/references.rb +118 -28
- data/lib/hecks/runtime/dependency_planning.rb +45 -0
- data/lib/hecks/runtime/dispatcher.rb +28 -50
- data/lib/hecks/runtime/entity_element.rb +161 -8
- data/lib/hecks/runtime/entity_interpreter.rb +44 -9
- data/lib/hecks/runtime/errors.rb +18 -4
- data/lib/hecks/runtime/event.rb +10 -5
- data/lib/hecks/runtime/identity.rb +71 -3
- data/lib/hecks/runtime/instance.rb +67 -7
- data/lib/hecks/runtime/interpreting.rb +13 -5
- data/lib/hecks/runtime/invocation.rb +118 -36
- data/lib/hecks/runtime/loader.rb +94 -8
- data/lib/hecks/runtime/outbox.rb +145 -7
- data/lib/hecks/runtime/policy_interpreter.rb +22 -9
- data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
- data/lib/hecks/runtime/query_interpreter.rb +40 -12
- data/lib/hecks/runtime/reaction_invocation.rb +53 -8
- data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
- data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
- data/lib/hecks/runtime/reference_hop.rb +42 -0
- data/lib/hecks/runtime/refusal_wording.rb +50 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
- data/lib/hecks/runtime/registry/verification.rb +119 -4
- data/lib/hecks/runtime/registry.rb +157 -4
- data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
- data/lib/hecks/runtime/routing.rb +27 -2
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
- data/lib/hecks/runtime/saga_interpreter.rb +27 -13
- data/lib/hecks/runtime/tenant_check.rb +26 -6
- data/lib/hecks/runtime/tenant_scope.rb +18 -0
- data/lib/hecks/runtime/value/coercion.rb +255 -33
- data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
- data/lib/hecks/runtime/value.rb +50 -0
- data/lib/hecks/runtime.rb +32 -0
- data/lib/hecks/storehouse.rb +305 -9
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +79 -1
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
- metadata +11 -5
- data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
- data/lib/hecks/deprecation.rb +0 -95
|
@@ -10,9 +10,19 @@ module Hecks
|
|
|
10
10
|
# loading, and the smaller checks are also called piecemeal by the
|
|
11
11
|
# repository factory.
|
|
12
12
|
module Verification
|
|
13
|
+
# Runs the whole wiring gate against this registry's loaded bluebooks,
|
|
14
|
+
# hexagons, ports and adapters.
|
|
15
|
+
#
|
|
16
|
+
# @return [Runtime::Registry] self
|
|
17
|
+
# @raise [Runtime::WiringError] if a bind names an undeclared aggregate, an
|
|
18
|
+
# adapter cannot satisfy its port's verb or declared `answers`, a world
|
|
19
|
+
# setting names a field its adapter does not declare, the default adapter
|
|
20
|
+
# is unusable, or a command declares a role with no authorization provider
|
|
21
|
+
# attached
|
|
13
22
|
def verify!
|
|
14
23
|
verify_default_adapter!
|
|
15
24
|
verify_singleton_port_answers!
|
|
25
|
+
refuse_cross_package_bluebook_merge!
|
|
16
26
|
|
|
17
27
|
@hecksagons.each_value do |hexagon|
|
|
18
28
|
refuse_ungoverned_roles!(hexagon)
|
|
@@ -50,6 +60,13 @@ module Hecks
|
|
|
50
60
|
self
|
|
51
61
|
end
|
|
52
62
|
|
|
63
|
+
# Checks that the framework-wide default persistence adapter (used by any
|
|
64
|
+
# aggregate left out of an explicit bind list) is itself wired correctly.
|
|
65
|
+
#
|
|
66
|
+
# @return [Runtime::Registry] self
|
|
67
|
+
# @raise [Runtime::WiringError] if the default adapter cannot satisfy the
|
|
68
|
+
# persistence port's verb, is missing a declared `answers` method, or has
|
|
69
|
+
# no Ruby implementation
|
|
53
70
|
def verify_default_adapter!
|
|
54
71
|
name = Ports::Persistence::DEFAULT_ADAPTER
|
|
55
72
|
|
|
@@ -68,6 +85,14 @@ module Hecks
|
|
|
68
85
|
"aggregate with no bind could not be given one: #{e.message}"
|
|
69
86
|
end
|
|
70
87
|
|
|
88
|
+
# Checks that `bind`'s adapter implements the port it names and satisfies
|
|
89
|
+
# the verb the bind declares.
|
|
90
|
+
#
|
|
91
|
+
# @param bind [Bluebook::Bind] the bind to check
|
|
92
|
+
# @return [void]
|
|
93
|
+
# @raise [Runtime::WiringError] if `bind`'s adapter is unknown, declares an
|
|
94
|
+
# unknown port, is missing a declared `answers` method, or cannot satisfy
|
|
95
|
+
# `bind`'s own verb
|
|
71
96
|
def check_verb(bind)
|
|
72
97
|
port = port_for(bind)
|
|
73
98
|
check_answers(port, bind.adapter)
|
|
@@ -85,6 +110,13 @@ module Hecks
|
|
|
85
110
|
# actually call. `answers` is optional per port (an empty list is
|
|
86
111
|
# today's pre-existing behavior, unchecked), so this only ever
|
|
87
112
|
# tightens a port that opted in.
|
|
113
|
+
#
|
|
114
|
+
# @param port [Bluebook::Port] the port whose declared `answers` methods
|
|
115
|
+
# `adapter_name` must respond to
|
|
116
|
+
# @param adapter_name [String] the adapter's declared name to check
|
|
117
|
+
# @return [void]
|
|
118
|
+
# @raise [Runtime::WiringError] if `adapter_name` has no Ruby implementation,
|
|
119
|
+
# or its implementation does not respond to one of `port.answers`
|
|
88
120
|
def check_answers(port, adapter_name)
|
|
89
121
|
answers = Array(port.answers)
|
|
90
122
|
return if answers.empty?
|
|
@@ -114,6 +146,12 @@ module Hecks
|
|
|
114
146
|
# exactly one adapter, wired, missing a method `answers` names.
|
|
115
147
|
PER_AGGREGATE_PORTS = %w[persistence projection loading].freeze
|
|
116
148
|
|
|
149
|
+
# Checks every singleton port (not per-aggregate-bound) with exactly one
|
|
150
|
+
# wired adapter against its own declared `answers` methods.
|
|
151
|
+
#
|
|
152
|
+
# @return [Runtime::Registry] self
|
|
153
|
+
# @raise [Runtime::WiringError] if a singleton port's one wired adapter is
|
|
154
|
+
# missing one of its declared `answers` methods
|
|
117
155
|
def verify_singleton_port_answers!
|
|
118
156
|
@ports.each_value do |port|
|
|
119
157
|
next if PER_AGGREGATE_PORTS.include?(port.name)
|
|
@@ -127,6 +165,16 @@ module Hecks
|
|
|
127
165
|
self
|
|
128
166
|
end
|
|
129
167
|
|
|
168
|
+
# Checks that every setting `settings` declares (besides `:adapter`) is a
|
|
169
|
+
# field `bind`'s adapter actually admits.
|
|
170
|
+
#
|
|
171
|
+
# @param bind [Bluebook::Bind] the bind naming the adapter to check against;
|
|
172
|
+
# a no-op if its adapter is unknown
|
|
173
|
+
# @param settings [Hash{Symbol => Object}] the world's declared settings for
|
|
174
|
+
# this bind
|
|
175
|
+
# @return [void]
|
|
176
|
+
# @raise [Runtime::WiringError] if `settings` declares a field `bind`'s
|
|
177
|
+
# adapter does not declare
|
|
130
178
|
def check_settings(bind, settings)
|
|
131
179
|
adapter = @adapters[bind.adapter]
|
|
132
180
|
return unless adapter
|
|
@@ -141,6 +189,12 @@ module Hecks
|
|
|
141
189
|
"Add the field to the adapter, or remove it from the world."
|
|
142
190
|
end
|
|
143
191
|
|
|
192
|
+
# Finds the port `bind`'s adapter declares.
|
|
193
|
+
#
|
|
194
|
+
# @param bind [Bluebook::Bind] the bind naming the adapter to look up
|
|
195
|
+
# @return [Bluebook::Port] the port `bind`'s adapter declares
|
|
196
|
+
# @raise [Runtime::WiringError] if `bind` names an unknown adapter, or one
|
|
197
|
+
# declaring an unknown port
|
|
144
198
|
def port_for(bind)
|
|
145
199
|
adapter = @adapters[bind.adapter]
|
|
146
200
|
raise WiringError, "unknown adapter #{bind.adapter.inspect}" unless adapter
|
|
@@ -149,6 +203,12 @@ module Hecks
|
|
|
149
203
|
raise(WiringError, "adapter #{bind.adapter} declares unknown port #{adapter.port.inspect}")
|
|
150
204
|
end
|
|
151
205
|
|
|
206
|
+
# Finds the Ruby module implementing the adapter declared `name`.
|
|
207
|
+
#
|
|
208
|
+
# @param name [String] the adapter's declared name, such as `"PostgresEra"`
|
|
209
|
+
# @return [Module] the adapter module or class under `Hecks::Adapters`
|
|
210
|
+
# @raise [Runtime::WiringError] if no Ruby implementation named `name` exists
|
|
211
|
+
# under `Hecks::Adapters`
|
|
152
212
|
def adapter_class(name)
|
|
153
213
|
Adapters.const_get(name)
|
|
154
214
|
rescue NameError
|
|
@@ -164,9 +224,9 @@ module Hecks
|
|
|
164
224
|
# unchecked, exactly the defect ADR 0025 §9 names ("role gates
|
|
165
225
|
# access control by exact string equality ... Governance ...
|
|
166
226
|
# connected to none of it"). Checked here, at `verify!` — recovered
|
|
167
|
-
# and moved, not new: this
|
|
168
|
-
# build time (Bluebook::DSL::HecksagonBuilder#build),
|
|
169
|
-
# the moment a domain
|
|
227
|
+
# and moved, not new: running this per-block, at hecksagon
|
|
228
|
+
# build time (Bluebook::DSL::HecksagonBuilder#build), breaks
|
|
229
|
+
# the moment a domain is split across multiple hecksagon
|
|
170
230
|
# blocks (base + an `environments/<name>.hecksagon` overlay,
|
|
171
231
|
# Runtime::Loader.boot's `environment:` — see its own comment for
|
|
172
232
|
# the recovery provenance): every block but the one declaring
|
|
@@ -179,7 +239,7 @@ module Hecks
|
|
|
179
239
|
# correct (a check against an incomplete, not-yet-merged
|
|
180
240
|
# hecksagon can never see the real final shape).
|
|
181
241
|
#
|
|
182
|
-
# A provider is
|
|
242
|
+
# A provider is recognised by its declaration, not its name —
|
|
183
243
|
# `authorization_provider_for` answers for the domain's own
|
|
184
244
|
# chapter too, so Governance (which declares `provides
|
|
185
245
|
# "authorization"`) passes here because of what it declares, and
|
|
@@ -217,6 +277,61 @@ module Hecks
|
|
|
217
277
|
bluebook_ir.aggregates.flat_map { |aggregate| aggregate.commands + aggregate.entities.flat_map(&:commands) }
|
|
218
278
|
end
|
|
219
279
|
|
|
280
|
+
# TWO UNRELATED PACKAGES, ONE CHAPTER NAME BY COINCIDENCE — the
|
|
281
|
+
# real risk `Registry#bluebook_sources` exists to catch (found
|
|
282
|
+
# live: a stale `vendor/hecksagain` fork's own copy of Governance/
|
|
283
|
+
# Identity/Deploy, still reachable on 4 consuming apps' own load
|
|
284
|
+
# paths alongside the real gem). `BluebookBuilder.build`'s own
|
|
285
|
+
# accumulation (several files declaring the SAME chapter name ON
|
|
286
|
+
# PURPOSE — `lib/hecks/language/bluebook/*.bluebook` all open
|
|
287
|
+
# `Hecks.bluebook "Bluebook"`) is never touched here — that merge
|
|
288
|
+
# stays unconditional, checked only AFTER every file has loaded,
|
|
289
|
+
# the same "check the merged final result once" timing
|
|
290
|
+
# `refuse_ungoverned_roles!` already uses and for the same reason
|
|
291
|
+
# (a check against an incomplete load can never see the real
|
|
292
|
+
# shape). What distinguishes intentional accumulation from
|
|
293
|
+
# coincidence is PACKAGE ROOT, not file identity: files a real
|
|
294
|
+
# gemspec or a `vendor/` boundary already treats as one unit are
|
|
295
|
+
# expected to share a name; files from two DIFFERENT roots never
|
|
296
|
+
# legitimately do.
|
|
297
|
+
def refuse_cross_package_bluebook_merge!
|
|
298
|
+
@bluebook_sources.each do |name, paths|
|
|
299
|
+
roots = paths.map { |path| package_root_for(path) }.uniq
|
|
300
|
+
next if roots.size <= 1
|
|
301
|
+
|
|
302
|
+
raise WiringError,
|
|
303
|
+
"#{name.inspect} is declared by more than one package: #{roots.join(' and ')} — " \
|
|
304
|
+
"these are two unrelated sources sharing a chapter name by coincidence, not one " \
|
|
305
|
+
"domain split across files, and merging their declarations into one chapter is " \
|
|
306
|
+
"almost certainly a stale/vendored copy left on the load path (paths: " \
|
|
307
|
+
"#{paths.join(', ')})"
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# THE NEAREST BOUNDARY A PATH ALREADY BELONGS TO — a real
|
|
312
|
+
# gemspec (this IS a package, whatever depends on it or vendors
|
|
313
|
+
# it), or a bare `vendor/` path component, treated as its OWN
|
|
314
|
+
# root regardless of what gemspec might sit above it: vendored
|
|
315
|
+
# code should never be considered "the same package" as whatever
|
|
316
|
+
# it's vendored into, even when nothing else marks the boundary.
|
|
317
|
+
# Neither found, the path's own directory is the root — two
|
|
318
|
+
# files with no closer marker only "belong together" if they are
|
|
319
|
+
# literally the same file.
|
|
320
|
+
def package_root_for(path)
|
|
321
|
+
return path.to_s if path.nil?
|
|
322
|
+
|
|
323
|
+
dir = File.dirname(File.expand_path(path))
|
|
324
|
+
loop do
|
|
325
|
+
return "vendor:#{dir}" if File.basename(dir) == "vendor"
|
|
326
|
+
return dir if Dir.glob(File.join(dir, "*.gemspec")).any?
|
|
327
|
+
|
|
328
|
+
parent = File.dirname(dir)
|
|
329
|
+
return dir if parent == dir
|
|
330
|
+
|
|
331
|
+
dir = parent
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
220
335
|
# A domain that declares a `process_manager` but whose
|
|
221
336
|
# `saga_persistence` resolves to `NULL_SAGA_STORE` (no anchor
|
|
222
337
|
# aggregate, a RemoteRuntime-shaped adapter, an adapter that
|
|
@@ -16,12 +16,17 @@ module Hecks
|
|
|
16
16
|
|
|
17
17
|
attr_reader :root, :bluebooks, :hecksagons, :ports, :adapters, :worlds, :event_log,
|
|
18
18
|
:reaction_log, :saga_log, :saga_instances, :translations, :saga_mutex,
|
|
19
|
-
:saga_dispatch_log, :policy_dispatch_log
|
|
19
|
+
:saga_dispatch_log, :policy_dispatch_log, :bluebook_sources,
|
|
20
|
+
:pending_privacy_markings
|
|
20
21
|
|
|
22
|
+
# @param root [String, nil] the booting project's root directory, the base
|
|
23
|
+
# a shared ports/adapters root and a `.world`'s own relative paths resolve
|
|
24
|
+
# against; nil for a registry with no such root
|
|
21
25
|
def initialize(root: nil)
|
|
22
26
|
@root = root
|
|
23
27
|
@bluebooks = {}
|
|
24
|
-
@
|
|
28
|
+
@bluebook_sources = {}
|
|
29
|
+
@hecksagons = {}
|
|
25
30
|
@ports = {}
|
|
26
31
|
@adapters = {}
|
|
27
32
|
@worlds = {}
|
|
@@ -29,6 +34,14 @@ module Hecks
|
|
|
29
34
|
@event_log = []
|
|
30
35
|
@reaction_log = []
|
|
31
36
|
@saga_log = []
|
|
37
|
+
# A DECLARATIVE FACT, NOT YET A DISPATCHED ONE — `AggregateDoor#
|
|
38
|
+
# mark_sensitive` (called from a `.hecksagon` file, the same way
|
|
39
|
+
# `port`/`persisted_by` already are) appends here at
|
|
40
|
+
# hecksagon-build time; `Runtime::Loader.boot`'s own post-dispatcher
|
|
41
|
+
# step turns each entry into a real `Privacy::Marking.Mark`,
|
|
42
|
+
# idempotently, the same "declared here, taken effect once boot
|
|
43
|
+
# actually has a dispatcher" shape `redrive_outbox!` already has.
|
|
44
|
+
@pending_privacy_markings = []
|
|
32
45
|
# **Additive, Ruby-only** — never merged into saga_log/reaction_log.
|
|
33
46
|
# rust/src/kernel/orchestrate.rs ports those two arrays' exact
|
|
34
47
|
# shape byte-for-byte (spec/rust_conformance_spec.rb's own
|
|
@@ -112,6 +125,14 @@ module Hecks
|
|
|
112
125
|
# several files (`language/bluebook/*.bluebook`, all `Hecks.bluebook "Bluebook"`)
|
|
113
126
|
# needs its declarations to accumulate into one builder rather than each
|
|
114
127
|
# file minting its own and silently discarding the one before.
|
|
128
|
+
#
|
|
129
|
+
# @param name [String, Symbol] the chapter name the builder accumulates
|
|
130
|
+
# declarations for
|
|
131
|
+
# @yield the block that mints a fresh builder, called only the first time
|
|
132
|
+
# `name` is asked for
|
|
133
|
+
# @yieldreturn [Bluebook::DSL::BluebookBuilder] a fresh builder for `name`
|
|
134
|
+
# @return [Bluebook::DSL::BluebookBuilder] the builder already open for
|
|
135
|
+
# `name`, or the block's freshly minted one on the first call
|
|
115
136
|
def bluebook_builder(name)
|
|
116
137
|
@bluebook_builders[name.to_s] ||= yield
|
|
117
138
|
end
|
|
@@ -130,20 +151,53 @@ module Hecks
|
|
|
130
151
|
# registry/saga_persistence.rb) there is no concurrent caller for
|
|
131
152
|
# `Hecks/ThreadSharedIvarMutation` to actually be warning about here.
|
|
132
153
|
# rubocop:disable Hecks/ThreadSharedIvarMutation
|
|
154
|
+
# Registers a loaded chapter, keyed by its own declared name.
|
|
155
|
+
#
|
|
156
|
+
# @param item [Bluebook::Chapter] the loaded, judged chapter
|
|
157
|
+
# @return [Bluebook::Chapter] `item`, unchanged
|
|
133
158
|
def add_bluebook(item) = @bluebooks[item.name] = item
|
|
134
159
|
|
|
160
|
+
# PROVENANCE, SIDE-CHANNEL — which real `.bluebook` file(s)
|
|
161
|
+
# contributed to a chapter name, never part of the exported IR (a
|
|
162
|
+
# boot-time loading fact, not a domain fact) and never Rust-mirrored
|
|
163
|
+
# (the same "additive, Ruby-only" shape `@translations` above already
|
|
164
|
+
# is). Legitimate accumulation (several files declaring the SAME
|
|
165
|
+
# chapter name on purpose — `lib/hecks/language/bluebook/*.bluebook`
|
|
166
|
+
# all open `Hecks.bluebook "Bluebook"`) pushes more than one path
|
|
167
|
+
# here too; that alone is not a problem. What this exists to let
|
|
168
|
+
# `refuse_cross_package_bluebook_merge!` (registry/verification.rb)
|
|
169
|
+
# catch is TWO UNRELATED PACKAGES accumulating into the same name by
|
|
170
|
+
# coincidence — a stale vendored fork's own copy of a real gem's
|
|
171
|
+
# chapter, still reachable on the load path, silently merging its
|
|
172
|
+
# aggregates into the real one via this exact accumulation mechanism.
|
|
173
|
+
def record_bluebook_source(name, path)
|
|
174
|
+
(@bluebook_sources[name.to_s] ||= []) << path
|
|
175
|
+
end
|
|
176
|
+
|
|
135
177
|
# Merged, not replaced — recovered, not new (see Runtime::Loader
|
|
136
178
|
# .boot's own comment for the provenance). A domain's hecksagon can
|
|
137
179
|
# now load in more than one block for the same domain (base file
|
|
138
180
|
# plus an `environments/<name>.hecksagon` overlay), and the second
|
|
139
181
|
# block should add to what the first declared, not silently
|
|
140
182
|
# discard it.
|
|
183
|
+
#
|
|
184
|
+
# @param item [Bluebook::Hecksagon] the declared wiring to register
|
|
185
|
+
# @return [void]
|
|
141
186
|
def add_hecksagon(item)
|
|
142
187
|
existing = @hecksagons[item.domain]
|
|
143
188
|
@hecksagons[item.domain] = existing ? merge_hecksagons(existing, item) : item
|
|
144
189
|
end
|
|
145
190
|
|
|
191
|
+
# Registers a loaded port, keyed by its own declared name.
|
|
192
|
+
#
|
|
193
|
+
# @param item [Bluebook::Port, Bluebook::DomainPort] the loaded port
|
|
194
|
+
# @return [Bluebook::Port, Bluebook::DomainPort] `item`, unchanged
|
|
146
195
|
def add_port(item) = @ports[item.name] = item
|
|
196
|
+
|
|
197
|
+
# Registers a loaded adapter, keyed by its own declared name.
|
|
198
|
+
#
|
|
199
|
+
# @param item [Bluebook::Adapter] the loaded, judged adapter
|
|
200
|
+
# @return [Bluebook::Adapter] `item`, unchanged
|
|
147
201
|
def add_adapter(item) = @adapters[item.name] = item
|
|
148
202
|
|
|
149
203
|
# Merged, not replaced — the same generalization for `World` that
|
|
@@ -156,12 +210,40 @@ module Hecks
|
|
|
156
210
|
# bare verb key and the `"verb:adapter"` qualified key point at the
|
|
157
211
|
# same resolved hash) — an overlay's key wins over the base's same
|
|
158
212
|
# key; a key only the base declares survives untouched.
|
|
213
|
+
#
|
|
214
|
+
# @param item [Bluebook::World] the declared world settings to register
|
|
215
|
+
# @return [void]
|
|
159
216
|
def add_world(item)
|
|
160
217
|
existing = @worlds[item.domain]
|
|
161
218
|
@worlds[item.domain] = existing ? merge_worlds(existing, item) : item
|
|
162
219
|
end
|
|
163
220
|
|
|
221
|
+
# Registers a loaded translation.
|
|
222
|
+
#
|
|
223
|
+
# @param item [Bluebook::Translation] the loaded, judged translation
|
|
224
|
+
# @return [Array<Bluebook::Translation>] every translation registered so far,
|
|
225
|
+
# `item` last
|
|
164
226
|
def add_translation(item) = @translations << item
|
|
227
|
+
|
|
228
|
+
# Declares one attribute of one domain's own aggregate sensitive — called from a
|
|
229
|
+
# terminal `has_<category>(readable_by:)` on a `Bluebook::DSL::AttributePath`
|
|
230
|
+
# (reached by chaining off a bare `Domain::Aggregate` inside a `.hecksagon` file
|
|
231
|
+
# being `Kernel.load`ed, or off an already-installed `AggregateDoor`), same timing
|
|
232
|
+
# (and same thread-safety argument, above) as `add_bluebook`/`add_port`. Recorded,
|
|
233
|
+
# not dispatched: `Runtime::Loader.boot`'s own `seed_privacy_markings!` turns each
|
|
234
|
+
# entry into a real `Privacy::Marking.Mark` once a dispatcher exists.
|
|
235
|
+
#
|
|
236
|
+
# @param domain [String] the marked attribute's own aggregate FQN, e.g.
|
|
237
|
+
# `"Lifeadelics::Registration"`
|
|
238
|
+
# @param attribute_path [String] the dotted path within that aggregate, e.g.
|
|
239
|
+
# `"attendee.medications"`
|
|
240
|
+
# @param category [String] the marking's own sensitivity category, e.g. `"phi"`
|
|
241
|
+
# @param readable_by [String] the Governance role a read must hold, unredacted
|
|
242
|
+
# @return [void]
|
|
243
|
+
def add_pending_privacy_marking(domain:, attribute_path:, category:, readable_by:)
|
|
244
|
+
@pending_privacy_markings << { domain: domain, attribute_path: attribute_path,
|
|
245
|
+
category: category, readable_by: readable_by }
|
|
246
|
+
end
|
|
165
247
|
# rubocop:enable Hecks/ThreadSharedIvarMutation
|
|
166
248
|
|
|
167
249
|
# {domain name => era ordinal} as resolved by the boot-time era
|
|
@@ -176,10 +258,30 @@ module Hecks
|
|
|
176
258
|
# an old checkout only — see `initialize`'s own comment on it.
|
|
177
259
|
attr_reader :superseded_eras
|
|
178
260
|
|
|
261
|
+
# Finds a loaded chapter by name.
|
|
262
|
+
#
|
|
263
|
+
# @param name [String, Symbol] the chapter's declared name
|
|
264
|
+
# @return [Bluebook::Chapter, nil] the chapter, or nil if none is registered
|
|
265
|
+
# under `name`
|
|
179
266
|
def bluebook(name) = @bluebooks[name.to_s]
|
|
267
|
+
|
|
268
|
+
# Finds a domain's registered wiring by name.
|
|
269
|
+
#
|
|
270
|
+
# @param name [String, Symbol] the domain name
|
|
271
|
+
# @return [Bluebook::Hecksagon, nil] the domain's wiring, or nil if none is
|
|
272
|
+
# registered under `name`
|
|
180
273
|
def hecksagon(name) = @hecksagons[name.to_s]
|
|
274
|
+
|
|
275
|
+
# Finds a domain's registered world settings by name.
|
|
276
|
+
#
|
|
277
|
+
# @param name [String, Symbol] the domain name
|
|
278
|
+
# @return [Bluebook::World, nil] the domain's world, or nil if none is
|
|
279
|
+
# registered under `name`
|
|
181
280
|
def world(name) = @worlds[name.to_s]
|
|
182
281
|
|
|
282
|
+
# Every verb every loaded chapter declares, sorted.
|
|
283
|
+
#
|
|
284
|
+
# @return [Array<String>] every declared verb, across every loaded chapter
|
|
183
285
|
def verbs = @bluebooks.values.flat_map(&:verbs).sort
|
|
184
286
|
|
|
185
287
|
# The chapter that answers a role check for `domain` — the domain's
|
|
@@ -188,6 +290,10 @@ module Hecks
|
|
|
188
290
|
# every check for the literal name "Governance": Governance is
|
|
189
291
|
# recognised by what it declares, and a chapter that declares the
|
|
190
292
|
# same thing is recognised the same way.
|
|
293
|
+
#
|
|
294
|
+
# @param domain [String, Symbol] the domain whose role checks are being resolved
|
|
295
|
+
# @return [Bluebook::Chapter, nil] the chapter that answers `domain`'s role
|
|
296
|
+
# checks, or nil if none does
|
|
191
297
|
def authorization_provider_for(domain)
|
|
192
298
|
names = [domain.to_s, *Array(hecksagon(domain)&.framework_members)]
|
|
193
299
|
names.filter_map { |name| bluebook(name) }
|
|
@@ -195,10 +301,24 @@ module Hecks
|
|
|
195
301
|
end
|
|
196
302
|
|
|
197
303
|
# Every loaded chapter declaring `provides "authorization"`.
|
|
304
|
+
#
|
|
305
|
+
# @return [Array<Bluebook::Chapter>] every loaded chapter that provides
|
|
306
|
+
# authorization
|
|
198
307
|
def authorization_providers
|
|
199
308
|
@bluebooks.values.select { |chapter| chapter.provides?(Bluebook::Capabilities::AUTHORIZATION) }
|
|
200
309
|
end
|
|
201
310
|
|
|
311
|
+
# Resolves and memoizes `aggregate`'s authoritative repository.
|
|
312
|
+
#
|
|
313
|
+
# @param domain [String, Symbol] name of the domain `aggregate` belongs to
|
|
314
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate to resolve a repository for
|
|
315
|
+
# @return [Persistence::AppendOnly] repository over the aggregate's authoritative
|
|
316
|
+
# adapter, or over a `Memory` adapter when the domain declares no hecksagon
|
|
317
|
+
# @raise [Runtime::WiringError] if the aggregate has no authoritative bind, more than
|
|
318
|
+
# one, or a bind with a role this port does not support; or if the bound adapter is
|
|
319
|
+
# unknown, answers a different verb, is given a setting it does not declare, has no
|
|
320
|
+
# Ruby implementation, or lacks a method its port's `answers` list or the
|
|
321
|
+
# append-only contract (`append`, `project`, `entries`) requires
|
|
202
322
|
def repository(domain, aggregate)
|
|
203
323
|
@repositories[[domain.to_s, aggregate.hecks_name]] ||= Ports::Persistence.repository(self, domain, aggregate)
|
|
204
324
|
end
|
|
@@ -216,8 +336,8 @@ module Hecks
|
|
|
216
336
|
# that store again, the way `Loader.boot_files` does after
|
|
217
337
|
# `verify!`.
|
|
218
338
|
#
|
|
219
|
-
# What this is for: a test runner that
|
|
220
|
-
# test to get isolation (`Behaviors::Expectations.run_one`) — ~2s a
|
|
339
|
+
# What this is for: a test runner that would otherwise boot a runtime
|
|
340
|
+
# per test to get isolation (`Behaviors::Expectations.run_one`) — ~2s a
|
|
221
341
|
# boot, 76 chess behaviours = two and a half minutes of booting the
|
|
222
342
|
# same two files — can now boot once and reset between tests.
|
|
223
343
|
#
|
|
@@ -229,6 +349,8 @@ module Hecks
|
|
|
229
349
|
# single-threaded example loop. No production dispatch path calls
|
|
230
350
|
# this at all — a live Puma worker pool never resets a registry out
|
|
231
351
|
# from under itself mid-flight.
|
|
352
|
+
#
|
|
353
|
+
# @return [Runtime::Registry] self
|
|
232
354
|
# rubocop:disable-next Hecks/ThreadSharedIvarMutation
|
|
233
355
|
def reset_runtime_state!
|
|
234
356
|
@event_log.clear
|
|
@@ -250,6 +372,17 @@ module Hecks
|
|
|
250
372
|
# gives, just without a lazy `||=` race on standing it up.
|
|
251
373
|
attr_reader :capability_graph
|
|
252
374
|
|
|
375
|
+
# Resolves and memoizes the repository to read `aggregate` from — a caught-up
|
|
376
|
+
# projection when one is bound and current, otherwise the authoritative repository.
|
|
377
|
+
#
|
|
378
|
+
# @param domain [String, Symbol] name of the domain `aggregate` belongs to
|
|
379
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate to resolve a read
|
|
380
|
+
# repository for
|
|
381
|
+
# @return [Persistence::AppendOnly] the projection repository when one is bound
|
|
382
|
+
# and caught up with the authoritative store; the authoritative repository
|
|
383
|
+
# otherwise
|
|
384
|
+
# @raise [Runtime::WiringError] if the authoritative or projection bind cannot
|
|
385
|
+
# be resolved
|
|
253
386
|
def read_repository(domain, aggregate)
|
|
254
387
|
key = [domain.to_s, aggregate.hecks_name]
|
|
255
388
|
binding = Ports::Projection.binds_for(self, domain, aggregate).first
|
|
@@ -264,6 +397,15 @@ module Hecks
|
|
|
264
397
|
projection_current?(projection, authoritative) ? projection : authoritative
|
|
265
398
|
end
|
|
266
399
|
|
|
400
|
+
# Reports whether `projection`'s own journal entries and rows agree with
|
|
401
|
+
# `authoritative`'s, entry-for-entry.
|
|
402
|
+
#
|
|
403
|
+
# @param projection [Persistence::AppendOnly] the projection repository to check
|
|
404
|
+
# @param authoritative [Persistence::AppendOnly] the authoritative repository to
|
|
405
|
+
# check `projection` against
|
|
406
|
+
# @return [Boolean] true when `projection` holds the same entries and rows as
|
|
407
|
+
# `authoritative`, in the same order; false on any mismatch, or if comparing
|
|
408
|
+
# them raises
|
|
267
409
|
def projection_current?(projection, authoritative)
|
|
268
410
|
projected_entries = projection.entries
|
|
269
411
|
source_entries = authoritative.entries
|
|
@@ -288,6 +430,12 @@ module Hecks
|
|
|
288
430
|
# authoritative bind" check is what actually catches a genuine
|
|
289
431
|
# double-bind; this merge only concatenates, it does not itself
|
|
290
432
|
# decide which of two binds for the same aggregate wins.
|
|
433
|
+
#
|
|
434
|
+
# @param base [Bluebook::Hecksagon] the domain's already-registered wiring
|
|
435
|
+
# @param overlay [Bluebook::Hecksagon] the newly loaded block's own wiring to
|
|
436
|
+
# fold in
|
|
437
|
+
# @return [Bluebook::Hecksagon] a new wiring with every list-shaped fact
|
|
438
|
+
# concatenated, `base` then `overlay`
|
|
291
439
|
def merge_hecksagons(base, overlay)
|
|
292
440
|
Bluebook::Hecksagon.new(
|
|
293
441
|
domain: base.domain,
|
|
@@ -305,6 +453,11 @@ module Hecks
|
|
|
305
453
|
# also declares replaces that key's whole resolved hash (the same
|
|
306
454
|
# all-or-nothing shape `WorldBuilder#method_missing` already builds
|
|
307
455
|
# each entry as), it does not deep-merge field by field within it.
|
|
456
|
+
#
|
|
457
|
+
# @param base [Bluebook::World] the domain's already-registered world
|
|
458
|
+
# @param overlay [Bluebook::World] the newly loaded block's own world to fold in
|
|
459
|
+
# @return [Bluebook::World] a new world with `overlay`'s scalars winning when
|
|
460
|
+
# present, and `settings` shallow-merged, `overlay`'s keys winning
|
|
308
461
|
def merge_worlds(base, overlay)
|
|
309
462
|
Bluebook::World.new(
|
|
310
463
|
domain: base.domain,
|
|
@@ -16,7 +16,7 @@ module Hecks
|
|
|
16
16
|
# closely enough that everything built on top of a dispatcher —
|
|
17
17
|
# `Handle`, `AggregateDoor`, `Facade::Surface` — needs no changes
|
|
18
18
|
# at all: `Handle#run`'s entire contract is
|
|
19
|
-
# `@dispatcher.dispatch("#{fqn}.#{command}",
|
|
19
|
+
# `@dispatcher.dispatch("#{fqn}.#{command}", to: @id, with: args).instance.state`,
|
|
20
20
|
# and both classes answer that identically.
|
|
21
21
|
#
|
|
22
22
|
# **Reads delegate, writes don't**. `query`/`reference_query` hand off
|
|
@@ -31,12 +31,27 @@ module Hecks
|
|
|
31
31
|
# against incomplete local state and then merely persisted.
|
|
32
32
|
class RemoteDispatcher
|
|
33
33
|
Result = Struct.new(:verb, :instance, :events, keyword_init: true) do
|
|
34
|
+
# Reads the identity of the record the dispatch settled on.
|
|
35
|
+
#
|
|
36
|
+
# @return [String] the settled record's identity
|
|
34
37
|
def id = instance.id
|
|
38
|
+
|
|
39
|
+
# Reads the settled record's attributes as one Hash.
|
|
40
|
+
#
|
|
41
|
+
# @return [Hash{Symbol => Object}] the settled record's attributes, `:id` merged
|
|
42
|
+
# in last
|
|
35
43
|
def state = instance.to_h
|
|
36
44
|
end
|
|
37
45
|
|
|
38
46
|
attr_reader :registry
|
|
39
47
|
|
|
48
|
+
# @param registry [Runtime::Registry] the booted registry this dispatcher fronts;
|
|
49
|
+
# read-side calls (`query`/`reference_query`) delegate to a local `Dispatcher`
|
|
50
|
+
# built over the same registry
|
|
51
|
+
# @param region [String] the AWS region the routed Lambda function lives in
|
|
52
|
+
# @param function [String, nil] the `.world`'s own `dispatched_by("Lambda")`
|
|
53
|
+
# function name, when the deployment stack is not named `hecks-<domain>`; nil
|
|
54
|
+
# resolves the function name from `ENV["DOMAIN_NAME"]` or `registry.root`
|
|
40
55
|
def initialize(registry, region: "us-east-1", function: nil)
|
|
41
56
|
@registry = registry
|
|
42
57
|
# `File.basename(registry.root)`, not `bluebooks.keys.first` —
|
|
@@ -62,14 +77,55 @@ module Hecks
|
|
|
62
77
|
@local = Dispatcher.new(registry)
|
|
63
78
|
end
|
|
64
79
|
|
|
65
|
-
#
|
|
66
|
-
#
|
|
80
|
+
# Dispatches a command by verb, routing to the local `Dispatcher` or the
|
|
81
|
+
# remote Lambda depending on the aggregate's bound adapter.
|
|
82
|
+
#
|
|
83
|
+
# Same shape as `Dispatcher#dispatch_flat` — everything but
|
|
84
|
+
# `saga_correlation:` is forwarded through unread, `to:`/`with:`
|
|
85
|
+
# included, and lifted out downstream by whichever path actually
|
|
86
|
+
# dispatches (`@local.dispatch_flat` locally, the flat wire form
|
|
87
|
+
# remotely). Not the strict `to:`/`with:`-only door `Dispatcher#
|
|
88
|
+
# dispatch` is — see that class's own comment for why this file
|
|
89
|
+
# never had one.
|
|
90
|
+
#
|
|
91
|
+
# @param verb [String] the fully qualified verb, `"Domain::Aggregate.Command"`
|
|
92
|
+
# or `"Domain::Aggregate.Entity.Command"`
|
|
93
|
+
# @param saga_correlation [Hash, nil] correlation head => value, stamped on every
|
|
94
|
+
# emitted event when a saga leg causes this dispatch; nil otherwise
|
|
95
|
+
# @param args [Hash] the facts, plus optional `:to`/`:with` keys, read the same
|
|
96
|
+
# way `dispatch_flat` reads them
|
|
97
|
+
# @return [RemoteDispatcher::Result] the verb, settled instance and emitted events
|
|
98
|
+
# @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a
|
|
99
|
+
# domain or aggregate that is not declared
|
|
100
|
+
# @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when a
|
|
101
|
+
# locally-routed aggregate refuses the call
|
|
102
|
+
# @raise [Runtime::StaleWrite] if concurrent local writers beat this one through
|
|
103
|
+
# every retry, for a locally-routed aggregate
|
|
104
|
+
# @raise [Runtime::WiringError] if the aggregate's adapter or repository cannot be
|
|
105
|
+
# resolved, or the remote call is accepted but reports no mutation for it
|
|
106
|
+
# @raise [Runtime::RemoteRefusal] if the routed Lambda refuses the call
|
|
67
107
|
def dispatch(verb, saga_correlation: nil, **args)
|
|
68
|
-
Dispatcher.deprecate_loose_facts(args.except(:to, :with))
|
|
69
108
|
dispatch_flat(verb, args.merge(saga_correlation: saga_correlation))
|
|
70
109
|
end
|
|
71
110
|
|
|
72
|
-
#
|
|
111
|
+
# Routes a dispatch to the local `Dispatcher` when the aggregate's bound
|
|
112
|
+
# adapter is not remote-backed (`Ports::Persistence::RemoteRuntime`),
|
|
113
|
+
# otherwise dispatches through the routed Lambda. Same flat-facts wire
|
|
114
|
+
# form as `Dispatcher#dispatch_flat`.
|
|
115
|
+
#
|
|
116
|
+
# @param verb [String] the fully qualified verb, in any shape `dispatch` accepts
|
|
117
|
+
# @param args [Hash] the facts, plus optional Symbol keys `:to`, `:with` and
|
|
118
|
+
# `:saga_correlation`, read as `dispatch`'s keywords of the same names; not mutated
|
|
119
|
+
# @return [RemoteDispatcher::Result] the verb, settled instance and emitted events
|
|
120
|
+
# @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a
|
|
121
|
+
# domain or aggregate that is not declared
|
|
122
|
+
# @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when a
|
|
123
|
+
# locally-routed aggregate refuses the call
|
|
124
|
+
# @raise [Runtime::StaleWrite] if concurrent local writers beat this one through
|
|
125
|
+
# every retry, for a locally-routed aggregate
|
|
126
|
+
# @raise [Runtime::WiringError] if the aggregate's adapter or repository cannot be
|
|
127
|
+
# resolved, or the remote call is accepted but reports no mutation for it
|
|
128
|
+
# @raise [Runtime::RemoteRefusal] if the routed Lambda refuses the call
|
|
73
129
|
def dispatch_flat(verb, args = {})
|
|
74
130
|
args = args.dup
|
|
75
131
|
saga_correlation = args.delete(:saga_correlation)
|
|
@@ -125,16 +181,46 @@ module Hecks
|
|
|
125
181
|
Result.new(verb: verb, instance: instance, events: step_events(response))
|
|
126
182
|
end
|
|
127
183
|
|
|
184
|
+
# Delegates to the local `Dispatcher` built over the same registry — see the
|
|
185
|
+
# class comment on why reads, unlike writes, need no remote-specific logic.
|
|
186
|
+
#
|
|
187
|
+
# @param verb [String, Symbol] the query's verb, in one of `Dispatcher#query`'s
|
|
188
|
+
# three shapes
|
|
189
|
+
# @param args [Hash{Symbol => Object}] the query's declared arguments
|
|
190
|
+
# @return [Array<Hash>] see `Dispatcher#query`'s own return
|
|
191
|
+
# @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a
|
|
192
|
+
# domain, aggregate, entity, query or read model that is not declared
|
|
193
|
+
# @raise [Runtime::NotFound] if a read model's root reference names no record
|
|
194
|
+
# @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its declared type
|
|
195
|
+
# @raise [KeyError] if a rooted read model is asked without its reference argument
|
|
196
|
+
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
128
197
|
def query(verb, **args) = @local.query(verb, **args)
|
|
198
|
+
|
|
199
|
+
# Delegates to the local `Dispatcher` built over the same registry — see the
|
|
200
|
+
# class comment on why reads, unlike writes, need no remote-specific logic.
|
|
201
|
+
#
|
|
202
|
+
# @param verb [String] the fully qualified query verb, `"Domain::Aggregate.Query"`
|
|
203
|
+
# or `"Domain::Aggregate.Entity.Query"`
|
|
204
|
+
# @param args [Hash{Symbol => Object}] the query's declared arguments
|
|
205
|
+
# @return [Array<Hash>] one Hash per matching record, its state with `:id` merged
|
|
206
|
+
# in last; for an entity query, one Hash per matching element
|
|
207
|
+
# @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a
|
|
208
|
+
# domain, aggregate, entity or query that is not declared
|
|
209
|
+
# @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its declared type
|
|
210
|
+
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
129
211
|
def reference_query(verb, **args) = @local.reference_query(verb, **args)
|
|
130
212
|
|
|
131
|
-
#
|
|
213
|
+
# Fetches the full domain's event history, on every call — `{"read":
|
|
132
214
|
# true}` replays the whole journal (Phase 1's `dispatch::read`),
|
|
133
215
|
# so its own `events` array already is the complete log, the
|
|
134
216
|
# same thing `@registry.event_log` would answer for a local
|
|
135
217
|
# dispatch. Not cached: `AggregateDoor.events`/`Handle#events`
|
|
136
218
|
# are not called in this codebase's own hot paths today: if that
|
|
137
219
|
# changes, caching belongs here, not in every caller.
|
|
220
|
+
#
|
|
221
|
+
# @return [Array<Runtime::Event>] every event in the routed Lambda's domain
|
|
222
|
+
# journal, oldest first, with `occurred_at` always nil (the kernel is
|
|
223
|
+
# timestamp-free by design)
|
|
138
224
|
def events
|
|
139
225
|
@client.read.fetch("events", []).map { |e| build_event(e) }
|
|
140
226
|
end
|