hecks 1.4.0 → 1.5.1
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.bluebook +105 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/deploy/oidc.json +5 -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/compliance.bluebook +221 -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 +85 -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/deploy/fargate.rb +666 -0
- data/lib/hecks/projections/deploy/lambda.rb +2423 -0
- data/lib/hecks/projections/deploy/shared.rb +624 -0
- 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/projections.rb +3 -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 +47 -10
- data/lib/hecks/projector.rb +101 -19
- 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 +14 -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
- data/lib/hecks/framework/bluebook/compliance.bluebook +0 -1
|
@@ -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
|
|
@@ -10,6 +10,9 @@ module Hecks
|
|
|
10
10
|
# naming them; the dispatcher itself builds an Invocation instead.
|
|
11
11
|
module Routing
|
|
12
12
|
Envelope = Struct.new(:aggregate, :entities, keyword_init: true) do
|
|
13
|
+
# @param aggregate [String, #to_s] the receiving aggregate's identity
|
|
14
|
+
# @param entities [Array<String, #to_s>, nil] the entity-hop identities after the
|
|
15
|
+
# aggregate, in call order; empty for an aggregate-level command
|
|
13
16
|
def initialize(aggregate:, entities: [])
|
|
14
17
|
super(aggregate: aggregate.to_s, entities: Array(entities).map(&:to_s).freeze)
|
|
15
18
|
freeze
|
|
@@ -18,12 +21,34 @@ module Hecks
|
|
|
18
21
|
|
|
19
22
|
module_function
|
|
20
23
|
|
|
24
|
+
# Resolves a `to:` argument into a routing envelope. A thin delegator to
|
|
25
|
+
# `Invocation.route`; the dispatcher itself builds an `Invocation` instead.
|
|
26
|
+
#
|
|
27
|
+
# @param to [String, Hash, nil] a bare aggregate identity, or a Hash with
|
|
28
|
+
# `aggregate:` and `entity:`/`entities:`; nil for no receiver
|
|
29
|
+
# @param entity_depth [Integer] the number of entity-hop identities the call expects
|
|
30
|
+
# @return [Routing::Envelope, nil] the resolved envelope, or nil when `to` is nil
|
|
31
|
+
# @raise [Runtime::TypeMismatch] if `to` is malformed or its entity count does not
|
|
32
|
+
# match `entity_depth`
|
|
21
33
|
def envelope(to, entity_depth: 0) = Invocation.route(to, entity_depth: entity_depth)
|
|
22
34
|
|
|
35
|
+
# Resolves a command's offered facts into its flat args hash. A thin
|
|
36
|
+
# delegator to `Invocation.facts_for` for any caller still naming it.
|
|
37
|
+
#
|
|
23
38
|
# Loaded by invocation.rb (which requires this file), so `Invocation`
|
|
24
39
|
# is always defined by the time either method runs.
|
|
25
|
-
|
|
26
|
-
|
|
40
|
+
#
|
|
41
|
+
# @param command [Class] a `Bluebook::Command` subclass (or port-operation class)
|
|
42
|
+
# responding to `hecks_name` and `attributes`
|
|
43
|
+
# @param with [Hash, nil] the command's facts, keyed by attribute name; nil when the
|
|
44
|
+
# caller offers `flat` instead
|
|
45
|
+
# @param flat [Hash] the command's facts as a flat args hash, used when `with` is nil
|
|
46
|
+
# @return [Hash{String, Symbol => Object}] the offered facts by attribute name; an
|
|
47
|
+
# attribute offered as nil is kept as nil
|
|
48
|
+
# @raise [Runtime::TypeMismatch] if both `with` and a non-empty `flat` are given, if
|
|
49
|
+
# `with` is not a Hash, or if `with` names an unknown or omits a required attribute
|
|
50
|
+
def payload(command, with:, flat:)
|
|
51
|
+
facts = Invocation.facts_for(command, with: with, flat: flat)
|
|
27
52
|
Invocation.new(verb: nil, target: nil, facts: facts).to_args
|
|
28
53
|
end
|
|
29
54
|
end
|
|
@@ -50,18 +50,16 @@ module Hecks
|
|
|
50
50
|
|
|
51
51
|
# A self-referencing leg carries the correlation forward under its
|
|
52
52
|
# own emitting record's identity — `event.id`, not a field dug back
|
|
53
|
-
# out of the payload
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
# lines at all): the saga silently stopped advancing, forever, for
|
|
64
|
-
# exactly the leg this tier exists to correlate.
|
|
53
|
+
# out of the payload, because an addressing key is not a fact the
|
|
54
|
+
# payload should carry: routing is kept separate from payload
|
|
55
|
+
# (`to:`/`with:`, the facade's own `Handle#run` always uses it), so
|
|
56
|
+
# `own_key` (the aggregate's own reference-key convention, "wire",
|
|
57
|
+
# "transfer") never rides along in the payload unfiltered. Digging
|
|
58
|
+
# for it there would read an empty Hash for any self-referencing
|
|
59
|
+
# leg with no other declared attributes (`OnboardingCase.Clear`,
|
|
60
|
+
# `.Decline` — no `attribute` lines at all), silently stopping the
|
|
61
|
+
# saga from advancing, forever, for exactly the leg this tier
|
|
62
|
+
# exists to correlate.
|
|
65
63
|
#
|
|
66
64
|
# `event.id` says the identical thing this tier always meant —
|
|
67
65
|
# "the record that just emitted this event, by its own identity" —
|
|
@@ -28,14 +28,29 @@ module Hecks
|
|
|
28
28
|
|
|
29
29
|
attr_reader :registry
|
|
30
30
|
|
|
31
|
+
# @param registry [Runtime::Registry] the booted registry whose declared
|
|
32
|
+
# process managers and saga persistence this interpreter runs against
|
|
33
|
+
# @param door [Runtime::Dispatcher] the dispatcher a saga leg's own dispatch
|
|
34
|
+
# re-enters through
|
|
31
35
|
def initialize(registry, door:)
|
|
32
36
|
@registry = registry
|
|
33
37
|
@door = door
|
|
34
38
|
end
|
|
35
39
|
|
|
40
|
+
# Runs `domain`'s declared process managers against `event`: begins,
|
|
41
|
+
# advances or ends each matching saga instance, checkpointing durably
|
|
42
|
+
# before its own dispatches run.
|
|
43
|
+
#
|
|
36
44
|
# `only:` — one process manager, the outbox relay's way of running
|
|
37
45
|
# exactly the consumer a row names (`Runtime::Outbox::Relay#
|
|
38
46
|
# run_consumer`); nil advances every manager the domain declares.
|
|
47
|
+
#
|
|
48
|
+
# @param event [Runtime::Event] the just-emitted event to react to
|
|
49
|
+
# @param domain [String, Symbol] the domain whose declared process managers
|
|
50
|
+
# are checked
|
|
51
|
+
# @param only [Bluebook::ProcessManager, nil] one process manager to run
|
|
52
|
+
# exactly, instead of every manager `domain` declares
|
|
53
|
+
# @return [void]
|
|
39
54
|
def advance(event, domain, only: nil)
|
|
40
55
|
bluebook = @registry.bluebook(domain)
|
|
41
56
|
return unless bluebook
|
|
@@ -56,12 +71,11 @@ module Hecks
|
|
|
56
71
|
# could otherwise interleave their writes out of order, silently
|
|
57
72
|
# reordering a saga's own transition history — worse for the
|
|
58
73
|
# adapters with no locking of their own (Heki) than for Postgres.
|
|
59
|
-
# `deep_copy` guards against
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
# itself calls `JSON.generate` on it.
|
|
74
|
+
# `deep_copy` guards against a real bug shape (over-freezing a live,
|
|
75
|
+
# still-mutated Hash) — never hand a persistence adapter the same
|
|
76
|
+
# object `advance_saga`/`unwind` go on to mutate in place;
|
|
77
|
+
# round-tripping through JSON is also what guarantees the value is
|
|
78
|
+
# safe for every adapter that itself calls `JSON.generate` on it.
|
|
65
79
|
# `pending:` — see saga_pending_dispatch.rb. Injected into the
|
|
66
80
|
# written copy of memory only, never into `instance[:memory]`
|
|
67
81
|
# itself: every other reader of a live instance's memory
|
|
@@ -423,7 +437,7 @@ module Hecks
|
|
|
423
437
|
# Same non-reentrancy reasoning as `advance_saga`'s own comment —
|
|
424
438
|
# the mutex covers only the check-and-mutate-and-checkpoint step.
|
|
425
439
|
advanced = @registry.saga_mutex.synchronize do
|
|
426
|
-
# The compensating leg is selected by (REFUSED
|
|
440
|
+
# The compensating leg is selected by (`REFUSED`, current state)
|
|
427
441
|
# too — C10.3, one rule for every leg.
|
|
428
442
|
handler = process_manager.handler_for(REFUSED, instance[:state])
|
|
429
443
|
unless handler
|
|
@@ -507,9 +521,9 @@ module Hecks
|
|
|
507
521
|
end
|
|
508
522
|
|
|
509
523
|
# BUG#6 — unconditionally the saga's own home domain, never inferred
|
|
510
|
-
# from `command_name`'s own shape.
|
|
511
|
-
# `::` after `Naming.command_ref`'s own rewrite
|
|
512
|
-
# domain-qualified" and left alone
|
|
524
|
+
# from `command_name`'s own shape. Guessing from a leftover
|
|
525
|
+
# `::` after `Naming.command_ref`'s own rewrite (read as "already
|
|
526
|
+
# domain-qualified" and left alone) cannot actually
|
|
513
527
|
# tell a genuinely cross-domain reference (`Banking::Account::
|
|
514
528
|
# Debit` -> one `::` survives) apart from a same-domain entity
|
|
515
529
|
# command reference (`Manifest::Slot::Fill` -> one `::` survives
|
|
@@ -519,11 +533,11 @@ module Hecks
|
|
|
519
533
|
# (confirmed against `Naming.command_ref`'s own rewrite: it only
|
|
520
534
|
# ever strips the last `::`, so the count of what remains is blind
|
|
521
535
|
# to why it's there). Picking the cross-domain reading unconditionally
|
|
522
|
-
#
|
|
536
|
+
# would leave `qa/stress_domains/waybill`'s own `Packing` saga dispatching
|
|
523
537
|
# `Manifest::Slot::Fill` — an entity command in its own domain —
|
|
524
|
-
# unprefixed, so `Naming.split_verb` read "Manifest" as a domain
|
|
538
|
+
# unprefixed, so `Naming.split_verb` would read "Manifest" as a domain
|
|
525
539
|
# name instead of this chapter's own aggregate, and the dispatch
|
|
526
|
-
#
|
|
540
|
+
# would fail with `UnknownVerb`, silently recorded as an ordinary
|
|
527
541
|
# domain refusal rather than surfacing as the real bug it is.
|
|
528
542
|
#
|
|
529
543
|
# The fix mirrors `PolicyInterpreter#deliver`'s own mechanism,
|