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
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
require_relative "errors"
|
|
2
|
-
require_relative "../deprecation"
|
|
3
2
|
require_relative "refusal_wording"
|
|
4
3
|
require_relative "caller"
|
|
5
4
|
require_relative "invocation"
|
|
@@ -121,22 +120,21 @@ module Hecks
|
|
|
121
120
|
# Runs one command, entity command or port operation, then every policy and saga reaction
|
|
122
121
|
# its events are owed.
|
|
123
122
|
#
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
#
|
|
128
|
-
#
|
|
123
|
+
# The receiver in `to:`, the facts in `with:`, and nothing else.
|
|
124
|
+
# Loose keyword facts — `dispatch(verb, amount: 5)`, one bag holding
|
|
125
|
+
# both the route and the payload — were deprecated in 1.3.x and are
|
|
126
|
+
# gone: Ruby now refuses them itself, by name ("unknown keyword:
|
|
127
|
+
# :amount"). Code holding a bag of data rather than written keywords
|
|
128
|
+
# calls `dispatch_flat` below; that door is not going anywhere.
|
|
129
129
|
#
|
|
130
130
|
# @param verb [String] the fully qualified verb: `"Domain::Aggregate.Command"`,
|
|
131
131
|
# `"Domain::Aggregate.Entity.Command"` or `"Domain::Aggregate.Port.Operation"`
|
|
132
132
|
# @param to [String, Hash, nil] the receiver: an aggregate identity, or an entity route
|
|
133
133
|
# Hash with `:aggregate` and one of `:entity`/`:entities`; nil when the facts carry
|
|
134
134
|
# the identity themselves
|
|
135
|
-
# @param with [Hash, nil] the command's facts, keyed by argument name (String or Symbol)
|
|
136
|
-
# may not be combined with loose keyword facts
|
|
135
|
+
# @param with [Hash, nil] the command's facts, keyed by argument name (String or Symbol)
|
|
137
136
|
# @param saga_correlation [Hash, nil] correlation head => value, stamped on every
|
|
138
137
|
# emitted event when a saga leg causes this dispatch; nil otherwise
|
|
139
|
-
# @param legacy_args [Hash{Symbol => Object}] deprecated loose keyword facts
|
|
140
138
|
# @return [Runtime::Dispatcher::Result] the verb, settled instance (nil for a port
|
|
141
139
|
# operation), emitted events, execution plan and persistence outcome
|
|
142
140
|
# @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a domain,
|
|
@@ -145,39 +143,20 @@ module Hecks
|
|
|
145
143
|
# the call (`GivenNotMet`, `TypeMismatch`, `Unauthorized`, `NotFound`, …)
|
|
146
144
|
# @raise [Runtime::StaleWrite] if concurrent writers beat this one through every retry
|
|
147
145
|
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def dispatch(verb, to: nil, with: nil, saga_correlation: nil, **legacy_args)
|
|
151
|
-
Dispatcher.deprecate_loose_facts(legacy_args)
|
|
152
|
-
dispatch_invocation(verb, to: to, with: with, saga_correlation: saga_correlation, legacy_args: legacy_args)
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
LEGACY_ARGS_REMOVAL = "1.5.0".freeze
|
|
156
|
-
LEGACY_ARGS_WARNING =
|
|
157
|
-
"passing command facts to dispatch as loose keyword arguments is deprecated and will be removed in " \
|
|
158
|
-
"hecks #{LEGACY_ARGS_REMOVAL} — pass them as `with: { ... }`, with the receiver identity in `to:` " \
|
|
159
|
-
"(bin/codemod_legacy_dispatch_args rewrites existing callers)".freeze
|
|
160
|
-
|
|
161
|
-
# Warns, once per call site, that a dispatch passed its facts as loose keyword arguments.
|
|
162
|
-
#
|
|
163
|
-
# @param legacy_args [Hash] the loose keyword facts a dispatch received; empty means
|
|
164
|
-
# nothing deprecated was used, and nothing is warned
|
|
165
|
-
# @return [nil] always, whether or not a warning was issued
|
|
166
|
-
# @raise [Deprecation::Error] if `legacy_args` is not empty where
|
|
167
|
-
# `Deprecation.raise_on!(:legacy_dispatch_args)` applies
|
|
168
|
-
def self.deprecate_loose_facts(legacy_args)
|
|
169
|
-
Deprecation.call(:legacy_dispatch_args, LEGACY_ARGS_WARNING) unless legacy_args.empty?
|
|
146
|
+
def dispatch(verb, to: nil, with: nil, saga_correlation: nil)
|
|
147
|
+
dispatch_invocation(verb, to: to, with: with, saga_correlation: saga_correlation, flat: {})
|
|
170
148
|
end
|
|
171
149
|
|
|
172
150
|
# Dispatches a verb whose receiver and facts arrive together in one flat Hash.
|
|
173
151
|
#
|
|
174
|
-
# **The flat-facts wire form** — one Hash, not keywords
|
|
175
|
-
#
|
|
176
|
-
#
|
|
177
|
-
#
|
|
178
|
-
#
|
|
152
|
+
# **The flat-facts wire form** — one Hash, not keywords: the shape
|
|
153
|
+
# `spec/corpus/*.json` steps, the Rust kernel's `cli.rs` contract, a
|
|
154
|
+
# reaction without a `with:` projection, and the self-hosted
|
|
155
|
+
# meta-domain all carry, where the receiver's identity is one of the
|
|
156
|
+
# keys because that is how the wire spells it. Routes exactly as the
|
|
157
|
+
# removed keyword door did: a Symbol `:to`, `:with` or
|
|
179
158
|
# `:saga_correlation` key is lifted out as that keyword, everything
|
|
180
|
-
# else is a fact (so a String "to" key stays a fact).
|
|
159
|
+
# else is a fact (so a String "to" key stays a fact, as it did).
|
|
181
160
|
# Framework code that replays data calls this; application code
|
|
182
161
|
# calls `dispatch(verb, to:, with:)`.
|
|
183
162
|
#
|
|
@@ -196,10 +175,10 @@ module Hecks
|
|
|
196
175
|
to = facts.delete(:to)
|
|
197
176
|
with = facts.delete(:with)
|
|
198
177
|
saga_correlation = facts.delete(:saga_correlation)
|
|
199
|
-
dispatch_invocation(verb, to: to, with: with, saga_correlation: saga_correlation,
|
|
178
|
+
dispatch_invocation(verb, to: to, with: with, saga_correlation: saga_correlation, flat: facts)
|
|
200
179
|
end
|
|
201
180
|
|
|
202
|
-
def dispatch_invocation(verb, to:, with:, saga_correlation:,
|
|
181
|
+
def dispatch_invocation(verb, to:, with:, saga_correlation:, flat:)
|
|
203
182
|
domain, aggregate_name, command_name = parse(verb)
|
|
204
183
|
aggregate = resolve_aggregate(domain, aggregate_name, verb)
|
|
205
184
|
|
|
@@ -222,12 +201,12 @@ module Hecks
|
|
|
222
201
|
operation = port.operation(sub) ||
|
|
223
202
|
raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "port_no_operation",
|
|
224
203
|
port: head, operation: sub))
|
|
225
|
-
invocation = Invocation.from_call(verb, to: to, with: with,
|
|
204
|
+
invocation = Invocation.from_call(verb, to: to, with: with, flat: flat,
|
|
226
205
|
receiver: :port, aggregate: aggregate) { operation }
|
|
227
206
|
[nil, @port_ops.call(domain, aggregate, operation, invocation), nil, nil, :enqueue]
|
|
228
207
|
else
|
|
229
208
|
resolution = nil
|
|
230
|
-
invocation = Invocation.from_call(verb, to: to, with: with,
|
|
209
|
+
invocation = Invocation.from_call(verb, to: to, with: with, flat: flat,
|
|
231
210
|
receiver: :entity, entity_depth: command_name.count(".")) do
|
|
232
211
|
(resolution = EntityInterpreter::Resolution.of(aggregate, command_name)).command
|
|
233
212
|
end
|
|
@@ -235,7 +214,7 @@ module Hecks
|
|
|
235
214
|
end
|
|
236
215
|
else
|
|
237
216
|
command = command_of(aggregate, aggregate_name, command_name)
|
|
238
|
-
invocation = Invocation.from_call(verb, to: to, with: with,
|
|
217
|
+
invocation = Invocation.from_call(verb, to: to, with: with, flat: flat) { command }
|
|
239
218
|
@commands.call(domain, aggregate, command, invocation, saga_correlation)
|
|
240
219
|
end
|
|
241
220
|
|
|
@@ -328,13 +307,13 @@ module Hecks
|
|
|
328
307
|
# `to:`/`with:` are not keywords of this method — a key named
|
|
329
308
|
# either is an ordinary fact here (BUG#131), so both go in as nil.
|
|
330
309
|
resolution = nil
|
|
331
|
-
invocation = Invocation.from_call(verb, to: nil, with: nil,
|
|
310
|
+
invocation = Invocation.from_call(verb, to: nil, with: nil, flat: args, receiver: :entity) do
|
|
332
311
|
(resolution = EntityInterpreter::Resolution.of(aggregate, command_name)).command
|
|
333
312
|
end
|
|
334
313
|
@entities.call(domain, aggregate, resolution, invocation, dry_run: true)
|
|
335
314
|
else
|
|
336
315
|
command = command_of(aggregate, aggregate_name, command_name)
|
|
337
|
-
invocation = Invocation.from_call(verb, to: nil, with: nil,
|
|
316
|
+
invocation = Invocation.from_call(verb, to: nil, with: nil, flat: args) { command }
|
|
338
317
|
@commands.call(domain, aggregate, command, invocation, dry_run: true)
|
|
339
318
|
end
|
|
340
319
|
|
|
@@ -360,7 +339,9 @@ module Hecks
|
|
|
360
339
|
# @param to [String, Hash, nil] the receiving aggregate's identity; when nil it is read
|
|
361
340
|
# from the facts, by the operation's reference or identity attribute
|
|
362
341
|
# @param with [Hash, nil] the operation's facts, keyed by argument name
|
|
363
|
-
# @param
|
|
342
|
+
# @param flat [Hash] the wire form `dispatch_flat` takes, for the driving adapter
|
|
343
|
+
# holding a decoded webhook rather than written keywords; it is where the operation's
|
|
344
|
+
# own reference attribute is read from and lifted into `to:` when `to:` is nil
|
|
364
345
|
# @return [Array<Runtime::Event>] the events the operation announced: one per declared
|
|
365
346
|
# `emits` for an inbound operation, the one answering or refusing event for an
|
|
366
347
|
# outbound one
|
|
@@ -369,10 +350,7 @@ module Hecks
|
|
|
369
350
|
# is malformed
|
|
370
351
|
# @raise [Runtime::NotFound] if the receiving aggregate record does not exist
|
|
371
352
|
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
372
|
-
|
|
373
|
-
# `Deprecation.raise_on!(:legacy_dispatch_args)` applies
|
|
374
|
-
def dispatch_port(domain, aggregate_name, port_name, operation_name, to: nil, with: nil, **legacy_args)
|
|
375
|
-
Dispatcher.deprecate_loose_facts(legacy_args)
|
|
353
|
+
def dispatch_port(domain, aggregate_name, port_name, operation_name, to: nil, with: nil, flat: {})
|
|
376
354
|
aggregate = resolve_aggregate(domain, aggregate_name, "#{domain}::#{aggregate_name}.#{port_name}.#{operation_name}")
|
|
377
355
|
port = aggregate.port(port_name) ||
|
|
378
356
|
raise(UnknownVerb, "#{aggregate_name} has no port #{port_name.inspect}")
|
|
@@ -380,7 +358,7 @@ module Hecks
|
|
|
380
358
|
raise(UnknownVerb, "#{port_name} has no operation #{operation_name.inspect}")
|
|
381
359
|
|
|
382
360
|
invocation = Invocation.from_call("#{domain}::#{aggregate_name}.#{port_name}.#{operation_name}",
|
|
383
|
-
to: to, with: with,
|
|
361
|
+
to: to, with: with, flat: flat,
|
|
384
362
|
receiver: :port, aggregate: aggregate) { operation }
|
|
385
363
|
announced = @port_ops.call(domain, aggregate, operation, invocation)
|
|
386
364
|
|
|
@@ -33,6 +33,9 @@ module Hecks
|
|
|
33
33
|
UNMATCHABLE = Object.new.freeze
|
|
34
34
|
private_constant :UNMATCHABLE
|
|
35
35
|
|
|
36
|
+
# Walks `chain`, one hop per entry, and returns the located element (or
|
|
37
|
+
# `instance` itself, when `chain` is empty).
|
|
38
|
+
#
|
|
36
39
|
# **One hop per chain entry**. `container` starts as `instance` (the root
|
|
37
40
|
# aggregate record) and becomes each just-located element in turn —
|
|
38
41
|
# Dispatch's own element is found inside the Handler element
|
|
@@ -52,6 +55,22 @@ module Hecks
|
|
|
52
55
|
# split's own consequence: an entity's own identity may arrive via
|
|
53
56
|
# `to:` rather than duplicated into `args`, so each hop is offered
|
|
54
57
|
# its routed identity ahead of falling back to `args`.
|
|
58
|
+
#
|
|
59
|
+
# @param root_aggregate [Bluebook::Aggregate] the root aggregate record's
|
|
60
|
+
# own construct, held constant across every hop for coercion
|
|
61
|
+
# @param chain [Array<Bluebook::Entity>] the entity chain to walk, root-first
|
|
62
|
+
# @param instance [Runtime::Instance] the root aggregate record to walk from
|
|
63
|
+
# @param args [Hash{String, Symbol => Object}] the offered command arguments,
|
|
64
|
+
# read for each hop's own identity when `route` does not supply it
|
|
65
|
+
# @param command_name [String] the command name, quoted in a refusal
|
|
66
|
+
# @param route [Runtime::Routing::Envelope, nil] the call's resolved routing
|
|
67
|
+
# envelope, if any; its own `entities` supply each hop's identity first
|
|
68
|
+
# @return [Runtime::Instance, Hash{Symbol => Object}] the located element;
|
|
69
|
+
# `instance` itself, unchanged, when `chain` is empty
|
|
70
|
+
# @raise [Runtime::UnknownVerb] if a hop's owner declares no list attribute
|
|
71
|
+
# for that entity
|
|
72
|
+
# @raise [Runtime::NotFound] if a hop's identity is absent from `args`, or no
|
|
73
|
+
# element matches it
|
|
55
74
|
def locate_chain(root_aggregate, chain, instance, args, command_name, route = nil)
|
|
56
75
|
container = instance
|
|
57
76
|
owner = root_aggregate
|
|
@@ -81,6 +100,24 @@ module Hecks
|
|
|
81
100
|
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
82
101
|
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
83
102
|
# rubocop:disable-next Metrics/MethodLength
|
|
103
|
+
#
|
|
104
|
+
# @param root_aggregate [Bluebook::Aggregate] the root aggregate record's own
|
|
105
|
+
# construct, used for value-object coercion
|
|
106
|
+
# @param owner [Bluebook::Aggregate, Bluebook::Entity] the construct whose own
|
|
107
|
+
# attribute declares the list `entity` is searched in
|
|
108
|
+
# @param entity [Bluebook::Entity] the entity type being located
|
|
109
|
+
# @param command_name [String] the command name, quoted in a refusal
|
|
110
|
+
# @param container [Runtime::Instance, Hash{Symbol => Object}] the record or
|
|
111
|
+
# element holding the list to search
|
|
112
|
+
# @param args [Hash{String, Symbol => Object}] the offered command arguments,
|
|
113
|
+
# read for the element's own identity when `routed_identity` is nil
|
|
114
|
+
# @param routed_identity [String, nil] the routing envelope's own identity
|
|
115
|
+
# string for this hop, matched directly instead of deriving one from `args`
|
|
116
|
+
# @return [Hash{Symbol => Object}] a fresh copy of the located element; the
|
|
117
|
+
# owning list inside `container` is replaced with a fresh copy too
|
|
118
|
+
# @raise [Runtime::UnknownVerb] if `owner` declares no list attribute for `entity`
|
|
119
|
+
# @raise [Runtime::NotFound] if an identity part is absent from `args`
|
|
120
|
+
# (`routed_identity` nil only), or no element matches
|
|
84
121
|
def element_of(root_aggregate, owner, entity, command_name, container, args, routed_identity = nil)
|
|
85
122
|
entity_name = entity.hecks_name
|
|
86
123
|
list_attr = owner.attributes.find { |a| a.list? && a.type.to_s == entity_name } ||
|
|
@@ -173,6 +210,12 @@ module Hecks
|
|
|
173
210
|
# that does not name its fields is refused when the bluebook loads ("an
|
|
174
211
|
# entity says what it is known by", "an identity part names something"),
|
|
175
212
|
# so by the time a dispatch arrives here there is always a path to dig.
|
|
213
|
+
#
|
|
214
|
+
# @param entity [Bluebook::Entity] the entity type whose identity paths
|
|
215
|
+
# are read
|
|
216
|
+
# @param element [Hash{Symbol => Object}] the stored element to read an
|
|
217
|
+
# identity off
|
|
218
|
+
# @return [String] the element's identity, joined from its declared parts
|
|
176
219
|
def element_identity(entity, element)
|
|
177
220
|
parts = entity.identity_paths.map do |path|
|
|
178
221
|
head = path.to_s.split(".").first.to_sym
|
|
@@ -215,8 +258,32 @@ module Hecks
|
|
|
215
258
|
# intentional mirroring, which is what lets the two be diffed
|
|
216
259
|
# against each other when one gets a fix the other needs too.
|
|
217
260
|
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
261
|
+
# Applies one declared mutation to an entity element, in place.
|
|
262
|
+
#
|
|
218
263
|
# `pre` — the element as it was before this command (C4.2): every
|
|
219
264
|
# read below goes through it, every write lands on `element`.
|
|
265
|
+
#
|
|
266
|
+
# @param rules [Runtime::CommandRules] the shared rules engine `resolve_source`/
|
|
267
|
+
# `arithmetic`/`multiply`/`clamp`/`sign_of` are read through
|
|
268
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
|
|
269
|
+
# coercion
|
|
270
|
+
# @param entity [Bluebook::Entity] the entity type `element` is an instance of
|
|
271
|
+
# @param element [Hash{Symbol => Object}] the element to mutate; written in place
|
|
272
|
+
# @param mutation [Bluebook::Mutation] the declared mutation to apply
|
|
273
|
+
# @param args [Hash{String, Symbol => Object}] the offered command arguments,
|
|
274
|
+
# a mutation's source may read from
|
|
275
|
+
# @param pre [Hash{Symbol => Object}] the element as it stood before this
|
|
276
|
+
# command; every read goes through this, every write lands on `element`
|
|
277
|
+
# @return [void]
|
|
278
|
+
# @raise [Runtime::TypeMismatch] if a mutation's own value cannot be coerced,
|
|
279
|
+
# or an arithmetic op's operands are not numeric or compatible value objects
|
|
280
|
+
# @raise [Runtime::InvariantViolation] if a coerced or arithmetic-derived value
|
|
281
|
+
# object breaks one of its own invariants
|
|
282
|
+
# @raise [Runtime::AlreadyExists] if an `:append` mints a nested entity whose
|
|
283
|
+
# identity collides with an existing element
|
|
284
|
+
# @raise [Bluebook::Expression::EvaluationError] if an arithmetic op's product
|
|
285
|
+
# does not fit a signed 64-bit Integer, or is a non-finite Float
|
|
286
|
+
# @raise [Runtime::WiringError] if `mutation.op` names no handled mutation kind
|
|
220
287
|
def apply_to_element(rules, aggregate, entity, element, mutation, args, pre = element)
|
|
221
288
|
case mutation.op
|
|
222
289
|
when :set
|
|
@@ -269,22 +336,47 @@ module Hecks
|
|
|
269
336
|
end
|
|
270
337
|
end
|
|
271
338
|
|
|
339
|
+
# Rewraps a plain-Numeric arithmetic result into `attribute`'s own declared
|
|
340
|
+
# value-object type, when the arithmetic itself ran unwrapped.
|
|
341
|
+
#
|
|
272
342
|
# `MutationApplier#rewrap_arithmetic_result`'s own entity-scoped
|
|
273
343
|
# twin, byte-for-byte the same fix — see that method's own
|
|
274
344
|
# comment for the full "phantom-field asymmetric wrapping" story.
|
|
275
345
|
# A no-op whenever `current` was already a Value (the arithmetic
|
|
276
346
|
# call already returned one) or the mutation targets no declared
|
|
277
347
|
# attribute at all.
|
|
348
|
+
#
|
|
349
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
|
|
350
|
+
# coercion
|
|
351
|
+
# @param attribute [Bluebook::Attribute, nil] the mutated field's own declared
|
|
352
|
+
# attribute; a no-op when nil (targets no declared attribute)
|
|
353
|
+
# @param current [Object] the field's pre-mutation value, as read off `pre`
|
|
354
|
+
# @param result [Object] the arithmetic op's own result
|
|
355
|
+
# @return [Runtime::Value, Object] `result` unchanged when already a `Value`,
|
|
356
|
+
# `current` is a `Value`, or `attribute` is nil; otherwise `result` coerced
|
|
357
|
+
# into `attribute`'s own declared type
|
|
278
358
|
def rewrap_arithmetic_result(aggregate, attribute, current, result)
|
|
279
359
|
return result if current.is_a?(Value) || attribute.nil? || result.is_a?(Value)
|
|
280
360
|
|
|
281
361
|
Value.for_attribute(aggregate, attribute, result)
|
|
282
362
|
end
|
|
283
363
|
|
|
364
|
+
# Resolves an `:append` mutation's own field source against the offered
|
|
365
|
+
# arguments, falling back to the element's own current field.
|
|
366
|
+
#
|
|
284
367
|
# `MutationApplier#resolve_append_source`'s own entity-scoped
|
|
285
368
|
# twin — a caller-supplied arg first, falling back to the
|
|
286
369
|
# element's own current field (never the parent instance's) when
|
|
287
370
|
# it isn't one.
|
|
371
|
+
#
|
|
372
|
+
# @param source [Symbol, Object] the mutation's own field source: a Symbol
|
|
373
|
+
# names a command argument or, failing that, an element field; anything
|
|
374
|
+
# else is returned as is
|
|
375
|
+
# @param element [Hash{Symbol => Object}] the element to fall back to reading
|
|
376
|
+
# from
|
|
377
|
+
# @param args [Hash{String, Symbol => Object}] the offered command arguments
|
|
378
|
+
# @return [Object, nil] the named argument's value; failing that, the named
|
|
379
|
+
# element field's value; `source` itself when it is not a Symbol
|
|
288
380
|
def resolve_element_append_source(source, element, args)
|
|
289
381
|
return source unless source.is_a?(Symbol)
|
|
290
382
|
return args[source] if args.key?(source)
|
|
@@ -314,6 +406,20 @@ module Hecks
|
|
|
314
406
|
# every declared attribute the append mapping doesn't name gets its
|
|
315
407
|
# own default the same way a fresh aggregate's own attributes
|
|
316
408
|
# already do (`Instance.defaults`), whichever branch built `fields`.
|
|
409
|
+
#
|
|
410
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
|
|
411
|
+
# coercion
|
|
412
|
+
# @param entity [Bluebook::Entity] the entity type `element`'s mutated list
|
|
413
|
+
# attribute belongs to
|
|
414
|
+
# @param element [Hash{Symbol => Object}] the element being appended to, read
|
|
415
|
+
# as the source for a field the mutation's own map does not supply directly
|
|
416
|
+
# @param mutation [Bluebook::Mutation] the declared `:append` mutation
|
|
417
|
+
# @param args [Hash{String, Symbol => Object}] the offered command arguments
|
|
418
|
+
# @return [Array] `element[mutation.target]`'s existing elements, frozen deep,
|
|
419
|
+
# with the newly built element (a `Runtime::Value`, or a Hash for an entity
|
|
420
|
+
# or untyped element) appended last
|
|
421
|
+
# @raise [Runtime::AlreadyExists] if the appended element is a nested entity
|
|
422
|
+
# whose identity collides with an existing one
|
|
317
423
|
def appended_to_element(aggregate, entity, element, mutation, args)
|
|
318
424
|
fields = mutation.source.transform_values { |source| resolve_element_append_source(source, element, args) }
|
|
319
425
|
element_type = entity.attribute(mutation.target)&.type
|
|
@@ -389,6 +495,15 @@ module Hecks
|
|
|
389
495
|
# what "the default" means. Additive only — a key `fields` already
|
|
390
496
|
# holds (the append mapping, an auto-minted identity, a lifecycle
|
|
391
497
|
# default) is never overwritten.
|
|
498
|
+
#
|
|
499
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
|
|
500
|
+
# coercion
|
|
501
|
+
# @param entity [Bluebook::Entity] the entity type whose declared attributes
|
|
502
|
+
# are defaulted
|
|
503
|
+
# @param fields [Hash{Symbol => Object}] the element fields already resolved;
|
|
504
|
+
# written in place
|
|
505
|
+
# @return [Hash{Symbol => Object}] `fields`, with every declared attribute it
|
|
506
|
+
# did not already hold filled with its own default
|
|
392
507
|
def fill_declared_defaults(aggregate, entity, fields)
|
|
393
508
|
entity.attributes.each do |attribute|
|
|
394
509
|
next if fields.key?(attribute.name)
|
|
@@ -404,6 +519,18 @@ module Hecks
|
|
|
404
519
|
# the list this targets is itself entity-typed — see
|
|
405
520
|
# `list_element_match?`, below, which both this and
|
|
406
521
|
# `MutationApplier#removed` now share.
|
|
522
|
+
#
|
|
523
|
+
# @param rules [Runtime::CommandRules] the shared rules engine `resolve_source`
|
|
524
|
+
# is read through
|
|
525
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
|
|
526
|
+
# coercion
|
|
527
|
+
# @param entity [Bluebook::Entity] the entity type `element`'s mutated list
|
|
528
|
+
# attribute belongs to
|
|
529
|
+
# @param element [Hash{Symbol => Object}] the element being removed from
|
|
530
|
+
# @param mutation [Bluebook::Mutation] the declared `:remove` mutation
|
|
531
|
+
# @param args [Hash{String, Symbol => Object}] the offered command arguments
|
|
532
|
+
# @return [Array] `element[mutation.target]`'s existing elements, with any
|
|
533
|
+
# matching the resolved remove target left out
|
|
407
534
|
def removed_from_element(rules, aggregate, entity, element, mutation, args)
|
|
408
535
|
value = rules.resolve_source(mutation.source, args)
|
|
409
536
|
attribute = entity.attribute(mutation.target)
|
|
@@ -443,6 +570,17 @@ module Hecks
|
|
|
443
570
|
# "matches" means — the same reasoning this file's own header
|
|
444
571
|
# gives for centralizing `locate_chain`/`element_of` once rather
|
|
445
572
|
# than twice.
|
|
573
|
+
#
|
|
574
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate, to resolve
|
|
575
|
+
# whether `attribute`'s own type is an entity
|
|
576
|
+
# @param attribute [Bluebook::Attribute, nil] the list attribute `element`
|
|
577
|
+
# belongs to
|
|
578
|
+
# @param element [Object] the stored list element to check: a Hash for an
|
|
579
|
+
# entity-typed list, a `Runtime::Value` otherwise
|
|
580
|
+
# @param value [Object] the `remove:` target to match `element` against
|
|
581
|
+
# @return [Boolean] whole-value equality for a non-entity-typed list;
|
|
582
|
+
# identity-field equality for an entity-typed one with a single identity
|
|
583
|
+
# head; false for a composite or absent identity
|
|
446
584
|
def list_element_match?(aggregate, attribute, element, value)
|
|
447
585
|
entity = attribute&.list? ? Value.find_entity(aggregate, attribute.type.to_s) : nil
|
|
448
586
|
return element == value unless entity
|
|
@@ -453,24 +591,27 @@ module Hecks
|
|
|
453
591
|
element.is_a?(Hash) && element[head] == value
|
|
454
592
|
end
|
|
455
593
|
|
|
456
|
-
#
|
|
594
|
+
# Refuses a caller-supplied or composite identity that already names an
|
|
595
|
+
# element on `current`.
|
|
596
|
+
#
|
|
597
|
+
# BUG#13 — the same check #hydrate gives every creating
|
|
457
598
|
# aggregate command (`repository.find(id)`,
|
|
458
|
-
# `command_interpreter.rb`), one level down.
|
|
459
|
-
# `MutationApplier` (mutation_applier.rb), called
|
|
599
|
+
# `command_interpreter.rb`), one level down. Shared with
|
|
600
|
+
# `MutationApplier` (mutation_applier.rb), called from
|
|
460
601
|
# `#entity_element` — an aggregate's own entity list (`Workspace.
|
|
461
|
-
# boards`, `Ledger.entries`)
|
|
602
|
+
# boards`, `Ledger.entries`) — and, moved here (BUG#145), from `#appended_
|
|
462
603
|
# to_element`, above — an entity's own nested entity list one hop
|
|
463
|
-
# further in (`Board.cards`) —
|
|
464
|
-
#
|
|
604
|
+
# further in (`Board.cards`) — so neither call site reimplements
|
|
605
|
+
# it a second time, the same way `#list_element_
|
|
465
606
|
# match?` already avoids that split for `remove:`.
|
|
466
607
|
#
|
|
467
608
|
# Reached, at the aggregate-owned call site, only on the two
|
|
468
609
|
# branches that do not auto-mint: a caller-supplied identity (the
|
|
469
610
|
# field is already in the append's own field map) or a composite
|
|
470
611
|
# one (`entity.identified_by` is nil for those — Runtime::
|
|
471
|
-
# Identified#derive_identity).
|
|
612
|
+
# Identified#derive_identity). Without this, neither checks the sibling
|
|
472
613
|
# list at all: a second LogVisit with the same date+sequence, or a
|
|
473
|
-
# second IssueKey with the same serial,
|
|
614
|
+
# second IssueKey with the same serial, would append a silent
|
|
474
615
|
# duplicate — worse than an ordinary duplicate row, because
|
|
475
616
|
# `EntityElement#element_of`'s own `find_index` always matches the
|
|
476
617
|
# first match, so the second becomes permanently unaddressable by
|
|
@@ -491,6 +632,18 @@ module Hecks
|
|
|
491
632
|
# hops up). Used for that naming purpose only (`owner.hecks_name`) —
|
|
492
633
|
# never for `Value`/namespace resolution, which is why an `Entity`
|
|
493
634
|
# (not just an `Aggregate`) is a valid thing to pass here.
|
|
635
|
+
#
|
|
636
|
+
# @param owner [Bluebook::Aggregate, Bluebook::Entity] the construct named
|
|
637
|
+
# in a refusal as what the duplicate "already exists on"
|
|
638
|
+
# @param entity [Bluebook::Entity] the entity type being checked for a
|
|
639
|
+
# colliding identity
|
|
640
|
+
# @param current [Array<Hash>, Object] the entity's own existing elements;
|
|
641
|
+
# coerced through `Array()`, so a single element or nil is also accepted
|
|
642
|
+
# @param fields [Hash{Symbol => Object}] the new element's own fields, whose
|
|
643
|
+
# identity heads are checked against every element in `current`
|
|
644
|
+
# @return [void]
|
|
645
|
+
# @raise [Runtime::AlreadyExists] if `current` already holds an element
|
|
646
|
+
# whose identity heads match `fields`'s own
|
|
494
647
|
def check_entity_collision(owner, entity, current, fields)
|
|
495
648
|
heads = entity.identity_heads
|
|
496
649
|
return if heads.empty?
|
|
@@ -24,8 +24,8 @@ module Hecks
|
|
|
24
24
|
class EntityInterpreter
|
|
25
25
|
include Interpreting
|
|
26
26
|
# The same payload gate aggregate commands and port operations already
|
|
27
|
-
# run — bug audit H1 (docs/audits/2026-08-10-main-bug-audit.md):
|
|
28
|
-
#
|
|
27
|
+
# run — bug audit H1 (docs/audits/2026-08-10-main-bug-audit.md): without
|
|
28
|
+
# this, this class runs neither refuse_unknown_arguments nor
|
|
29
29
|
# refuse_absent_arguments, on a comment claiming "an entity inherits
|
|
30
30
|
# its aggregate's own gate." Nothing on the entity dispatch path ever
|
|
31
31
|
# ran one — confirmed live, `LedgerEntry.Reverse` accepted an
|
|
@@ -83,7 +83,19 @@ module Hecks
|
|
|
83
83
|
# reads the declaring command — after `to:` is parsed, before the
|
|
84
84
|
# facts are.
|
|
85
85
|
Resolution = Data.define(:entity_names, :chain, :command_name, :command) do
|
|
86
|
+
# Resolves a dotted entity verb against `aggregate` into the entity
|
|
87
|
+
# chain it walks and the command it names.
|
|
88
|
+
#
|
|
86
89
|
# Refuses UnknownVerb for an unknown entity or command.
|
|
90
|
+
#
|
|
91
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate the dotted verb
|
|
92
|
+
# is resolved against
|
|
93
|
+
# @param dotted [String, Symbol] the entity verb, with the leading
|
|
94
|
+
# "Domain::Aggregate." already stripped (e.g. `"Handler.Dispatch.Bind"`)
|
|
95
|
+
# @return [EntityInterpreter::Resolution] the resolved entity names, chain,
|
|
96
|
+
# command name and command class
|
|
97
|
+
# @raise [Runtime::UnknownVerb] if `dotted` names no entity, or the located
|
|
98
|
+
# entity declares no such command
|
|
87
99
|
def self.of(aggregate, dotted)
|
|
88
100
|
*entity_names, command_name = dotted.to_s.split(".")
|
|
89
101
|
if entity_names.empty?
|
|
@@ -119,6 +131,9 @@ module Hecks
|
|
|
119
131
|
private_class_method :walk
|
|
120
132
|
end
|
|
121
133
|
|
|
134
|
+
# @param registry [Runtime::Registry] the booted registry this interpreter reads
|
|
135
|
+
# @param rules [Runtime::CommandRules] the shared rules engine (admissibility,
|
|
136
|
+
# references, arithmetic, authorization, emission) dispatch runs through
|
|
122
137
|
def initialize(registry, rules:)
|
|
123
138
|
@registry = registry
|
|
124
139
|
@rules = rules
|
|
@@ -136,6 +151,26 @@ module Hecks
|
|
|
136
151
|
# `resolution` is #resolve's answer; `invocation` the
|
|
137
152
|
# `Runtime::Invocation` `Dispatcher` built — `ctx.args` is its
|
|
138
153
|
# `to_args`, `ctx.route` its `target`.
|
|
154
|
+
#
|
|
155
|
+
# @param domain [String, Symbol] the domain `aggregate` belongs to
|
|
156
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate owning the entity
|
|
157
|
+
# chain the command targets
|
|
158
|
+
# @param resolution [EntityInterpreter::Resolution] the resolved entity chain
|
|
159
|
+
# and command, as `Resolution.of` builds it
|
|
160
|
+
# @param invocation [Runtime::Invocation] the invocation `Dispatcher` built for
|
|
161
|
+
# this call
|
|
162
|
+
# @param dry_run [Boolean] whether to run every step through validation without
|
|
163
|
+
# saving, emitting or enqueueing
|
|
164
|
+
# @return [Array(Runtime::Instance, Array<Runtime::Event>,
|
|
165
|
+
# Runtime::DependencyPlanning::Plan, Ports::Persistence::Execution,
|
|
166
|
+
# Array<Runtime::Outbox::Row>)] the settled parent aggregate instance, emitted
|
|
167
|
+
# events, execution plan, persistence outcome and outbox rows — the last
|
|
168
|
+
# three nil on a dry run, which skips save/emit/outbox
|
|
169
|
+
# @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when a
|
|
170
|
+
# given/ensures/invariant/authorization/admissibility rule refuses
|
|
171
|
+
# @raise [Runtime::StaleWrite] if concurrent writers beat this one through every
|
|
172
|
+
# retry (`MAX_STALE_WRITE_RETRIES`)
|
|
173
|
+
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
139
174
|
def call(domain, aggregate, resolution, invocation, dry_run: false)
|
|
140
175
|
chain = resolution.chain
|
|
141
176
|
entity = chain.last
|
|
@@ -239,7 +274,7 @@ module Hecks
|
|
|
239
274
|
# command's `corrects` names even exist" is checked here too, once,
|
|
240
275
|
# before the entity's own `given`s.
|
|
241
276
|
#
|
|
242
|
-
#
|
|
277
|
+
# Admissibility is checked against the parent/root, not the entity —
|
|
243
278
|
# deliberately `ctx.instance`/`ctx.aggregate` (the parent aggregate
|
|
244
279
|
# record and the root aggregate construct), never `ctx.view`/
|
|
245
280
|
# `ctx.entity` (the entity's own pre-mutation view/construct). This
|
|
@@ -399,12 +434,12 @@ module Hecks
|
|
|
399
434
|
end
|
|
400
435
|
|
|
401
436
|
# `locate_chain`/`element_of`/`element_identity`/`apply_to_element` and
|
|
402
|
-
# their own helpers
|
|
403
|
-
# (see that file's own header)
|
|
404
|
-
# `delegate_to_entity` step can locate and
|
|
405
|
-
# same way, against an aggregate record
|
|
406
|
-
# above, and every `step_*` method
|
|
407
|
-
#
|
|
437
|
+
# their own helpers live in `Runtime::EntityElement`
|
|
438
|
+
# (see that file's own header), shared rather than kept here, so
|
|
439
|
+
# `CommandInterpreter`'s own `delegate_to_entity` step can locate and
|
|
440
|
+
# mutate the same element the same way, against an aggregate record
|
|
441
|
+
# already held in memory. `call`, above, and every `step_*` method
|
|
442
|
+
# reach them through that module.
|
|
408
443
|
end
|
|
409
444
|
end
|
|
410
445
|
end
|
data/lib/hecks/runtime/errors.rb
CHANGED
|
@@ -2,6 +2,10 @@ require_relative "value/invariant_violation"
|
|
|
2
2
|
require_relative "../vocabulary"
|
|
3
3
|
|
|
4
4
|
module Hecks
|
|
5
|
+
# What runs a booted domain: dispatch, the command/entity/query/policy/saga
|
|
6
|
+
# interpreters, the registry a boot assembles, and the errors below —
|
|
7
|
+
# everything downstream of a `.bluebook`/`.hecksagon`/`.world` declaration.
|
|
8
|
+
# See `lib/hecks/runtime.rb` for the module's own facade and boot entry points.
|
|
5
9
|
module Runtime
|
|
6
10
|
class UnknownVerb < StandardError; end
|
|
7
11
|
class EnsuresNotMet < StandardError; end
|
|
@@ -21,11 +25,21 @@ module Hecks
|
|
|
21
25
|
class GivenNotMet < StandardError
|
|
22
26
|
attr_reader :detail
|
|
23
27
|
|
|
28
|
+
# @param message [String, nil] the refusal text, pinned byte-for-byte across specs
|
|
29
|
+
# @param detail [String, nil] the failing comparison's resolved operands ("left: X,
|
|
30
|
+
# right: Y"), or nil when the given's top-level shape is not a bare comparison
|
|
24
31
|
def initialize(message = nil, detail: nil)
|
|
25
32
|
super(message)
|
|
26
33
|
@detail = detail
|
|
27
34
|
end
|
|
28
35
|
|
|
36
|
+
# Renders `message` with `detail` appended, for a human reading an unhandled refusal.
|
|
37
|
+
#
|
|
38
|
+
# @param highlight [Boolean] unused; accepted for compatibility with `Exception#
|
|
39
|
+
# detailed_message`'s own signature
|
|
40
|
+
# @param opts [Hash] unused; accepted for compatibility with `Exception#
|
|
41
|
+
# detailed_message`'s own signature
|
|
42
|
+
# @return [String] `message`, with `" (#{detail})"` appended when `detail` is present
|
|
29
43
|
def detailed_message(highlight: false, **opts)
|
|
30
44
|
base = super
|
|
31
45
|
detail ? "#{base} (#{detail})" : base
|
|
@@ -119,10 +133,10 @@ module Hecks
|
|
|
119
133
|
#
|
|
120
134
|
# Everything else is a defect : a NoMethodError in an interpreter, a
|
|
121
135
|
# NameError from a missing constant, a TypeError from a bad assumption. A
|
|
122
|
-
# blanket `rescue StandardError`
|
|
123
|
-
# `delivered: false, reason: "..."` —
|
|
124
|
-
# indistinguishable from a rule doing its job,
|
|
125
|
-
# in the log.
|
|
136
|
+
# blanket `rescue StandardError` would fold both into one line —
|
|
137
|
+
# `delivered: false, reason: "..."` — making a crash in the runtime
|
|
138
|
+
# indistinguishable from a rule doing its job, reading as normal
|
|
139
|
+
# operation in the log.
|
|
126
140
|
#
|
|
127
141
|
# UnknownVerb is one of these, and deliberately : a cross-domain policy
|
|
128
142
|
# (`across "Notifications"`) fires in deployments where that domain is not
|
data/lib/hecks/runtime/event.rb
CHANGED
|
@@ -11,20 +11,25 @@ module Hecks
|
|
|
11
11
|
# one correlating on a different field. Absent for any event no saga
|
|
12
12
|
# dispatch caused, which is most of them.
|
|
13
13
|
Event = Struct.new(:name, :aggregate, :id, :payload, :occurred_at, :correlation, keyword_init: true) do
|
|
14
|
+
# Freezes the event deep, so nothing about it can change after it happens.
|
|
15
|
+
#
|
|
14
16
|
# An emitted event is a record of something that happened, and a
|
|
15
17
|
# mutable audit trail is not one. The payload — the domain fact the
|
|
16
18
|
# event carries — is frozen through on emission: freezing the Hash
|
|
17
19
|
# alone would leave every value in it editable in place, which is
|
|
18
20
|
# the shape all four previous freezing bugs had.
|
|
19
21
|
#
|
|
20
|
-
# The whole event, not just its payload. Correlation
|
|
21
|
-
# merged
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
22
|
+
# The whole event, not just its payload. Correlation is set at
|
|
23
|
+
# construction rather than merged in here by `Dispatcher#dispatch`
|
|
24
|
+
# after the event already exists, because it is part of the
|
|
25
|
+
# transaction and known from `dispatch`'s own argument before
|
|
26
|
+
# anything is emitted — that is what keeps an event immutable once
|
|
27
|
+
# it exists.
|
|
25
28
|
#
|
|
26
29
|
# The log stays appendable: new events are still recorded. It is
|
|
27
30
|
# each event that stops changing once it exists.
|
|
31
|
+
#
|
|
32
|
+
# @return [void]
|
|
28
33
|
def emit!
|
|
29
34
|
Freezer.deep(payload)
|
|
30
35
|
Freezer.deep(correlation)
|