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
|
@@ -37,6 +37,8 @@ module Hecks
|
|
|
37
37
|
# identifying the field, so a rule that reads it fails loud
|
|
38
38
|
# instead of quietly wrong.
|
|
39
39
|
class GuardState
|
|
40
|
+
# @param instance [Runtime::Instance] the record a `given`/`ensures`/
|
|
41
|
+
# `invariant` rule reads state from
|
|
40
42
|
def initialize(instance)
|
|
41
43
|
@instance = instance
|
|
42
44
|
@declared = instance.respond_to?(:aggregate) ? instance.aggregate.attributes.to_h { |a| [a.name, a] } : {}
|
|
@@ -55,8 +57,23 @@ module Hecks
|
|
|
55
57
|
@projected = owner.respond_to?(:projected_fields) ? owner.projected_fields.to_h { |f| [f.name, f] } : {}
|
|
56
58
|
end
|
|
57
59
|
|
|
60
|
+
# Reports whether `name` is a declared attribute, a projected field, or
|
|
61
|
+
# a key `instance`'s own state actually holds.
|
|
62
|
+
#
|
|
63
|
+
# @param name [String, Symbol] the field name to check
|
|
64
|
+
# @return [Boolean] true if `name` is readable one of those three ways
|
|
58
65
|
def key?(name) = @declared.key?(name.to_sym) || @projected.key?(name.to_sym) || @instance.key?(name)
|
|
59
66
|
|
|
67
|
+
# Reads one field, the way a `given`/`ensures`/`invariant` rule expects
|
|
68
|
+
# absence to read.
|
|
69
|
+
#
|
|
70
|
+
# @param name [String, Symbol] the field name to read
|
|
71
|
+
# @return [Object, nil] the stored value; nil for a declared, optional
|
|
72
|
+
# attribute the record predates
|
|
73
|
+
# @raise [Runtime::AttributeAbsent] if `name` is a declared, non-optional
|
|
74
|
+
# attribute the record's own state does not hold
|
|
75
|
+
# @raise [Runtime::ProjectionAbsent] if `name` is a `projects` field the
|
|
76
|
+
# record's own state does not hold
|
|
60
77
|
def [](name)
|
|
61
78
|
return @instance[name] if @instance.key?(name)
|
|
62
79
|
|
|
@@ -101,7 +118,7 @@ module Hecks
|
|
|
101
118
|
# found this — TypeError, not a refusal. Command-level
|
|
102
119
|
# dereferencing is the one thing that is supposed to override
|
|
103
120
|
# its own source argument for exactly this reason; args still
|
|
104
|
-
# wins over stored owner state
|
|
121
|
+
# wins over stored owner state.
|
|
105
122
|
# `parent:` is an entity command's own parent aggregate record
|
|
106
123
|
# (EntityInterpreter's `ctx.instance` — "the parent aggregate
|
|
107
124
|
# record", its own doc comment) — the entity's containment, not a
|
|
@@ -109,9 +126,9 @@ module Hecks
|
|
|
109
126
|
# `dereference`'s attribute scan the way `owner`'s do. Hydrated
|
|
110
127
|
# the same shape regardless: the parent's own state, merged so
|
|
111
128
|
# the dereferenced hash wins over the raw reference it replaces
|
|
112
|
-
# (ADR 0025
|
|
129
|
+
# (ADR 0025 drops the `_id` suffix that once kept the two
|
|
113
130
|
# apart by name, so `parent.state.merge(dereference(...))` is
|
|
114
|
-
#
|
|
131
|
+
# load-bearing, not redundant) — plus its own references
|
|
115
132
|
# dereferenced one level in, so `parent.customer.status` (a
|
|
116
133
|
# parent aggregate reaching its own customer) resolves the same
|
|
117
134
|
# way `account.customer.status` does for a command-level reference.
|
|
@@ -122,14 +139,34 @@ module Hecks
|
|
|
122
139
|
# `enforce_ensures`, below — it is a fresh local binding a
|
|
123
140
|
# `corrects` command introduces, not a real argument/state field
|
|
124
141
|
# a caller could collide with by accident.
|
|
142
|
+
#
|
|
143
|
+
# @param subject [Runtime::Instance] the pre-mutation record a `given` is
|
|
144
|
+
# checked against
|
|
145
|
+
# @param command [Class] the command class (`Bluebook::Command` subclass)
|
|
146
|
+
# whose declared `givens` are checked
|
|
147
|
+
# @param args [Hash{String, Symbol => Object}] the offered command arguments,
|
|
148
|
+
# readable by a given
|
|
149
|
+
# @param domain [String, Symbol] the domain a reference-typed argument is
|
|
150
|
+
# dereferenced in
|
|
151
|
+
# @param declaring [Bluebook::Aggregate, Bluebook::Entity, nil] the construct
|
|
152
|
+
# `command` is declared on; also checks `enforce_lifecycle_guard` when given
|
|
153
|
+
# @param parent [Runtime::Instance, nil] an entity command's own parent
|
|
154
|
+
# aggregate record, readable as `parent.*`; nil for an aggregate command
|
|
155
|
+
# @param correction [Hash{Symbol => Object}] the `{as_name => payload}`
|
|
156
|
+
# bindings a `corrects` command's located old event offers
|
|
157
|
+
# @return [void]
|
|
158
|
+
# @raise [Runtime::GivenNotMet] if a declared `given` does not hold
|
|
159
|
+
# @raise [Runtime::LifecycleRefused] if `declaring` is given and the command's
|
|
160
|
+
# own `from:` guard refuses the record's current lifecycle state
|
|
161
|
+
# @raise [Bluebook::Expression::EvaluationError] if a given's own rule cannot
|
|
162
|
+
# be evaluated (an unresolvable field, a bad comparison)
|
|
125
163
|
def enforce_givens(subject, command, args, domain:, declaring: nil, parent: nil, correction: {})
|
|
126
164
|
state = GuardState.new(subject)
|
|
127
165
|
# A rule may only read within its own aggregate boundary (S12,
|
|
128
166
|
# ADR 0025) — `subject`'s own stored references are no longer
|
|
129
|
-
# dereferenced here at all
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
# field is a regular stored attribute, already present in
|
|
167
|
+
# dereferenced here at all: a `projects :customer_status, from:
|
|
168
|
+
# :"customer.status"` field is just `subject`'s own
|
|
169
|
+
# field, a regular stored attribute, already present in
|
|
133
170
|
# `subject`/`state` with no hydration step needed. `dereference`
|
|
134
171
|
# is still called on `command`/`args`, below — that is a
|
|
135
172
|
# different case the ADR explicitly keeps in bounds ("its command
|
|
@@ -180,16 +217,64 @@ module Hecks
|
|
|
180
217
|
# one, made deliberately: `as:` reads as "the instance being
|
|
181
218
|
# corrected," which is naturally the latest fact on record, not
|
|
182
219
|
# an arbitrary one.
|
|
220
|
+
# Reads the durable event history a `corrects` mutation is judged against.
|
|
221
|
+
#
|
|
183
222
|
# C9.2 (docs/semantics/bluebook-semantics.md) — a correction target
|
|
184
223
|
# is judged against the record's durable history: the events the
|
|
185
224
|
# aggregate's own store recorded (`AppendOnly#events`), which
|
|
186
225
|
# survive a restart the way the Rust kernel's persisted
|
|
187
226
|
# `emitted_<event>` flag does. The in-process log is the fallback
|
|
188
227
|
# only for an adapter that records no readable history.
|
|
228
|
+
#
|
|
229
|
+
# @param domain [String, Symbol] the domain `aggregate` belongs to
|
|
230
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate whose repository is read
|
|
231
|
+
# @return [Array<Runtime::Event>] the aggregate's own durably recorded events
|
|
232
|
+
# if its repository keeps a readable log, otherwise the registry's in-process
|
|
233
|
+
# event log
|
|
189
234
|
def correction_history(domain, aggregate)
|
|
190
235
|
@registry.repository(domain, aggregate).events || @registry.event_log
|
|
191
236
|
end
|
|
192
237
|
|
|
238
|
+
# Locates each `:corrects` mutation's own already-emitted target event, and
|
|
239
|
+
# binds every `as:`-named one for `given`/`ensures` to reference.
|
|
240
|
+
#
|
|
241
|
+
# `corrects` — CommandBuilder#corrects_impl's own comment. Not
|
|
242
|
+
# expressible as an ordinary `given`: "has this exact record
|
|
243
|
+
# already emitted this exact event" is not a predicate over the
|
|
244
|
+
# record's own fields, it is a fact about the event log, so it is
|
|
245
|
+
# raised structurally here, the same way NotFound/AlreadyExists
|
|
246
|
+
# are, rather than through the expression evaluator. The build-
|
|
247
|
+
# time half — does anything in this aggregate ever emit the named
|
|
248
|
+
# event at all — is `AggregateBuilder#seal_correction_targets`;
|
|
249
|
+
# this is the dispatch-time half — has this record actually done
|
|
250
|
+
# so yet.
|
|
251
|
+
#
|
|
252
|
+
# Also locates the matched event now, not just its existence, and
|
|
253
|
+
# returns a `{as_name => payload}` bindings hash — one entry per
|
|
254
|
+
# `:corrects` mutation that named an `as:` — so `given`/`ensures`
|
|
255
|
+
# on a corrects-bearing command can reference the located old
|
|
256
|
+
# event by that name, the same shape `enforce_ensures`'s own
|
|
257
|
+
# `old:` binding already has (CommandBuilder#corrects_impl's own
|
|
258
|
+
# comment: `as:` was stored, from the start, specifically to be
|
|
259
|
+
# wired into the evaluator once a real runtime consumer existed —
|
|
260
|
+
# this is that consumer). `.reverse.find` — the most recent
|
|
261
|
+
# matching event, if this record has somehow emitted the same
|
|
262
|
+
# correction target more than once; the prior existence-only
|
|
263
|
+
# check never had to make this choice, so it's a genuinely new
|
|
264
|
+
# one, made deliberately: `as:` reads as "the instance being
|
|
265
|
+
# corrected," which is naturally the latest fact on record, not
|
|
266
|
+
# an arbitrary one.
|
|
267
|
+
#
|
|
268
|
+
# @param instance [Runtime::Instance] the record being corrected
|
|
269
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate `instance` belongs to
|
|
270
|
+
# @param command [Class] the command class (`Bluebook::Command` subclass)
|
|
271
|
+
# whose `:corrects` mutations are located
|
|
272
|
+
# @param domain [String, Symbol] the domain `aggregate` belongs to
|
|
273
|
+
# @return [Hash{Symbol => Object}] the located event's payload, keyed by
|
|
274
|
+
# each `:corrects` mutation's own `as:` name; empty for a mutation with
|
|
275
|
+
# no `as:`
|
|
276
|
+
# @raise [Runtime::NothingToCorrect] if a `:corrects` mutation names an
|
|
277
|
+
# event `instance` has never emitted
|
|
193
278
|
def enforce_correction_target(instance, aggregate, command, domain:)
|
|
194
279
|
bindings = {}
|
|
195
280
|
command.mutations.each do |mutation|
|
|
@@ -222,6 +307,16 @@ module Hecks
|
|
|
222
307
|
# `admissible_transition`, right below, for the transition this
|
|
223
308
|
# is deliberately not reusing (its own `StateTransition#target`
|
|
224
309
|
# is required, and a guard-only command has none to give it).
|
|
310
|
+
#
|
|
311
|
+
# @param declaring [Bluebook::Aggregate, Bluebook::Entity] the construct
|
|
312
|
+
# whose lifecycle field is checked
|
|
313
|
+
# @param command [Class] the command class (`Bluebook::Command` subclass)
|
|
314
|
+
# whose `from:` guard is checked
|
|
315
|
+
# @param subject [Runtime::Instance] the pre-mutation record to read the
|
|
316
|
+
# current lifecycle state off
|
|
317
|
+
# @return [void]
|
|
318
|
+
# @raise [Runtime::LifecycleRefused] if `command` declares `from:` and the
|
|
319
|
+
# record's current lifecycle state is not one of them
|
|
225
320
|
def enforce_lifecycle_guard(declaring, command, subject)
|
|
226
321
|
return unless command.from
|
|
227
322
|
|
|
@@ -232,12 +327,12 @@ module Hecks
|
|
|
232
327
|
# Routed through RefusalWording's own "transition_blocked"
|
|
233
328
|
# template — the same one #admissible_transition, right below,
|
|
234
329
|
# already raises LifecycleRefused through for the same
|
|
235
|
-
# refusal class
|
|
330
|
+
# refusal class, rather than hand-rolling its own wording
|
|
236
331
|
# inline ("...only runs from..." vs. the template's "...moves
|
|
237
|
-
# it only from...")
|
|
332
|
+
# it only from..."), two shapes for one refusal kind, so
|
|
238
333
|
# anything string-matching a LifecycleRefused message (a
|
|
239
|
-
# property, a spec, a caller)
|
|
240
|
-
# than one.
|
|
334
|
+
# property, a spec, a caller) would have to know both existed
|
|
335
|
+
# rather than one.
|
|
241
336
|
raise LifecycleRefused,
|
|
242
337
|
RefusalWording.render_site("LifecycleRefused", "transition_blocked",
|
|
243
338
|
command: command.hecks_name, field: lifecycle.field,
|
|
@@ -258,6 +353,25 @@ module Hecks
|
|
|
258
353
|
# Not new to ensures — `given` lives under the same rule — but an
|
|
259
354
|
# ensures is more likely to collide, since it typically re-reads a
|
|
260
355
|
# field the command just took in to mutate it.
|
|
356
|
+
#
|
|
357
|
+
# @param subject [Runtime::Instance] the settled, post-mutation record an
|
|
358
|
+
# `ensures` is checked against
|
|
359
|
+
# @param command [Class] the command class (`Bluebook::Command` subclass)
|
|
360
|
+
# whose declared `ensures` are checked
|
|
361
|
+
# @param args [Hash{String, Symbol => Object}] the offered command arguments,
|
|
362
|
+
# readable by an ensures unless `subject` shares the same field name
|
|
363
|
+
# @param old [Hash{Symbol => Object}, nil] the pre-mutation state, readable
|
|
364
|
+
# as `old.*`; nil when `command` declares no ensures (dup skipped upstream)
|
|
365
|
+
# @param domain [String, Symbol] the domain a reference-typed argument is
|
|
366
|
+
# dereferenced in
|
|
367
|
+
# @param parent [Runtime::Instance, nil] an entity command's own parent
|
|
368
|
+
# aggregate record, readable as `parent.*`; nil for an aggregate command
|
|
369
|
+
# @param correction [Hash{Symbol => Object}] the `{as_name => payload}`
|
|
370
|
+
# bindings a `corrects` command's located old event offers
|
|
371
|
+
# @return [void]
|
|
372
|
+
# @raise [Runtime::EnsuresNotMet] if a declared `ensures` does not hold
|
|
373
|
+
# @raise [Bluebook::Expression::EvaluationError] if an ensures's own rule
|
|
374
|
+
# cannot be evaluated (an unresolvable field, a bad comparison)
|
|
261
375
|
def enforce_ensures(subject, command, args, old:, domain:, parent: nil, correction: {})
|
|
262
376
|
state = GuardState.new(subject)
|
|
263
377
|
# S12, ADR 0025 — same boundary reasoning as enforce_givens
|
|
@@ -302,9 +416,21 @@ module Hecks
|
|
|
302
416
|
# No `dereference` (S12, ADR 0025) — an invariant may only read
|
|
303
417
|
# `subject`'s own boundary, same rule `enforce_givens`/
|
|
304
418
|
# `enforce_ensures` now hold to. No invariant in the corpus has
|
|
305
|
-
# ever read across a `reference_to` (verified
|
|
306
|
-
#
|
|
307
|
-
#
|
|
419
|
+
# ever read across a `reference_to` (verified by grep), so this
|
|
420
|
+
# is not a migration, just closing the same capability off here
|
|
421
|
+
# that was already unused.
|
|
422
|
+
#
|
|
423
|
+
# @param subject [Runtime::Instance] the settled aggregate record an
|
|
424
|
+
# invariant is checked against
|
|
425
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate whose declared
|
|
426
|
+
# `invariants` are checked
|
|
427
|
+
# @param domain [String, Symbol] the domain `aggregate` belongs to, passed
|
|
428
|
+
# through to `check_entity_invariants`
|
|
429
|
+
# @return [void]
|
|
430
|
+
# @raise [Runtime::InvariantViolation] if a declared invariant does not hold,
|
|
431
|
+
# on `aggregate` itself or any of its entities
|
|
432
|
+
# @raise [Bluebook::Expression::EvaluationError] if an invariant's own rule
|
|
433
|
+
# cannot be evaluated (an unresolvable field, a bad comparison)
|
|
308
434
|
def enforce_invariants(subject, aggregate, domain:)
|
|
309
435
|
state = GuardState.new(subject)
|
|
310
436
|
attrs = {}
|
|
@@ -338,6 +464,17 @@ module Hecks
|
|
|
338
464
|
# matching list attribute) is a static-analysis gap for a
|
|
339
465
|
# future gate, not a runtime concern here — `next` past it
|
|
340
466
|
# rather than raising mid-enforcement for an unrelated command.
|
|
467
|
+
#
|
|
468
|
+
# @param owner_construct [Bluebook::Aggregate, Bluebook::Entity] the
|
|
469
|
+
# construct whose nested entities are checked
|
|
470
|
+
# @param owner_instance [Runtime::Instance] the settled record holding
|
|
471
|
+
# `owner_construct`'s own entity lists
|
|
472
|
+
# @param domain [String, Symbol] the domain `owner_construct` belongs to
|
|
473
|
+
# @return [void]
|
|
474
|
+
# @raise [Runtime::InvariantViolation] if a declared entity invariant does
|
|
475
|
+
# not hold, on any element or its own nested entities
|
|
476
|
+
# @raise [Bluebook::Expression::EvaluationError] if an invariant's own rule
|
|
477
|
+
# cannot be evaluated (an unresolvable field, a bad comparison)
|
|
341
478
|
def check_entity_invariants(owner_construct, owner_instance, domain:)
|
|
342
479
|
owner_construct.entities.each do |entity|
|
|
343
480
|
next if entity.invariants.empty?
|
|
@@ -364,6 +501,20 @@ module Hecks
|
|
|
364
501
|
end
|
|
365
502
|
end
|
|
366
503
|
|
|
504
|
+
# Finds the lifecycle transition `command` admits from `subject`'s current
|
|
505
|
+
# state, if `declaring` declares a lifecycle and `command` moves it.
|
|
506
|
+
#
|
|
507
|
+
# @param declaring [Bluebook::Aggregate, Bluebook::Entity] the construct
|
|
508
|
+
# whose lifecycle is checked
|
|
509
|
+
# @param command [Class] the command class (`Bluebook::Command` subclass)
|
|
510
|
+
# to find a declared transition for
|
|
511
|
+
# @param subject [Runtime::Instance] the pre-mutation record to read the
|
|
512
|
+
# current lifecycle state off
|
|
513
|
+
# @return [Bluebook::StateTransition, nil] the admitted transition, or nil
|
|
514
|
+
# when `declaring` has no lifecycle or `command` declares no transition
|
|
515
|
+
# @raise [Runtime::LifecycleRefused] if `command` declares one or more
|
|
516
|
+
# transitions, each constrained by its own `from:`, and none admits the
|
|
517
|
+
# record's current lifecycle state
|
|
367
518
|
def admissible_transition(declaring, command, subject)
|
|
368
519
|
lifecycle = declaring.lifecycle
|
|
369
520
|
return nil unless lifecycle
|
|
@@ -279,6 +279,9 @@ module Hecks
|
|
|
279
279
|
bounded(current * amount, "multiply", current, amount, "*")
|
|
280
280
|
end
|
|
281
281
|
|
|
282
|
+
# Bounds an attribute's current value into `[min, max]`, on a bare number or on
|
|
283
|
+
# the one numeric field the wrapping value object carries.
|
|
284
|
+
#
|
|
282
285
|
# Vendored addition, not (yet) upstream hecks (migration plan
|
|
283
286
|
# task 4, i106): bound the current value into `[min, max]` -- no
|
|
284
287
|
# "amount" to combine, so it does not go through
|
|
@@ -286,6 +289,15 @@ module Hecks
|
|
|
286
289
|
# it clamps whichever single numeric field the wrapping value
|
|
287
290
|
# object carries (a synthesised wrapper always carries exactly
|
|
288
291
|
# one, per Part 3a's auto-synthesis).
|
|
292
|
+
#
|
|
293
|
+
# @param current [Numeric, Runtime::Value, nil] the attribute's pre-dispatch value; nil
|
|
294
|
+
# (never set) counts as 0
|
|
295
|
+
# @param bounds [Array<Numeric>] the two-element `[min, max]` range to clamp into
|
|
296
|
+
# @param target [Symbol, String] name of the attribute, used only to word a refusal
|
|
297
|
+
# @return [Numeric, Runtime::Value] the clamped value: a `Runtime::Value` when
|
|
298
|
+
# `current` is one, otherwise a bare number the caller re-wraps
|
|
299
|
+
# @raise [Runtime::TypeMismatch] if `current` is a value object with no single
|
|
300
|
+
# numeric field, or is neither numeric nor a value object
|
|
289
301
|
def clamp(current, bounds, target)
|
|
290
302
|
min, max = bounds
|
|
291
303
|
# The same `current ||= 0` #arithmetic/#multiply both give a
|
|
@@ -294,11 +306,11 @@ module Hecks
|
|
|
294
306
|
# attribute with no declared `default:` (genuinely absent,
|
|
295
307
|
# `Instance.defaults`/`#default_for`) hit TypeMismatch on the
|
|
296
308
|
# first clamp. (#arithmetic/#multiply's own absent-current gap
|
|
297
|
-
#
|
|
298
|
-
#
|
|
299
|
-
#
|
|
300
|
-
#
|
|
301
|
-
#
|
|
309
|
+
# is a real, separate bug: they do not silently treat an absent
|
|
310
|
+
# field as zero; they raise too, blaming a perfectly valid
|
|
311
|
+
# `amount` for not being an Integer when it is one, just still
|
|
312
|
+
# Money-wrapped. Fixed alongside this one — see
|
|
313
|
+
# #unwrap_single_numeric_field.)
|
|
302
314
|
current ||= 0
|
|
303
315
|
if current.is_a?(Value)
|
|
304
316
|
fields = current.to_h
|
|
@@ -23,6 +23,15 @@ module Hecks
|
|
|
23
23
|
# Shared by CommandInterpreter and EntityInterpreter — an entity command
|
|
24
24
|
# can declare a reference-typed attribute the same way an aggregate
|
|
25
25
|
# command can, even though nothing in the real corpus does yet.
|
|
26
|
+
#
|
|
27
|
+
# @param domain [String, Symbol] the domain the referenced aggregate is
|
|
28
|
+
# resolved in
|
|
29
|
+
# @param command [Class] the command or port-operation class (`Bluebook::Command`
|
|
30
|
+
# or `PortOperation` subclass) whose reference-typed attributes are checked
|
|
31
|
+
# @param args [Hash{Symbol => Object}] the offered, already-coerced argument values
|
|
32
|
+
# @return [void]
|
|
33
|
+
# @raise [Runtime::NotFound] if a reference-typed argument names an identity that
|
|
34
|
+
# does not exist in its target aggregate's repository
|
|
26
35
|
def resolve_references(domain, command, args)
|
|
27
36
|
command.attributes.each do |attribute|
|
|
28
37
|
next unless attribute.reference?
|
|
@@ -41,6 +50,20 @@ module Hecks
|
|
|
41
50
|
# Structural references are checked again against the settled state.
|
|
42
51
|
# This is what makes a `has_many` declared on an aggregate honest even
|
|
43
52
|
# when a command supplies its list through an ordinary typed argument.
|
|
53
|
+
#
|
|
54
|
+
# @param domain [String, Symbol] the domain the referenced aggregate is
|
|
55
|
+
# resolved in
|
|
56
|
+
# @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct
|
|
57
|
+
# whose reference-typed attributes are checked against `state`
|
|
58
|
+
# @param state [Hash{Symbol => Object}] the settled, post-mutation state to
|
|
59
|
+
# check every reference and relationship cardinality against
|
|
60
|
+
# @return [void]
|
|
61
|
+
# @raise [Runtime::TypeMismatch] if a required `has_one`/`belongs_to`
|
|
62
|
+
# relationship holds nil
|
|
63
|
+
# @raise [Runtime::NotFound] if a reference-typed field names an identity
|
|
64
|
+
# that does not exist in its target aggregate's repository
|
|
65
|
+
# @raise [Runtime::Unauthorized] if a referenced record belongs to a
|
|
66
|
+
# different tenant than `construct`'s own record
|
|
44
67
|
def resolve_state_references(domain, construct, state)
|
|
45
68
|
own_tenant_field = tenant_field_for(construct)
|
|
46
69
|
|
|
@@ -73,6 +96,14 @@ module Hecks
|
|
|
73
96
|
# checking only command arguments would let a required relationship be
|
|
74
97
|
# persisted as nil. `has_many` admits zero members, so its empty list is
|
|
75
98
|
# already a valid cardinality and needs no presence refusal.
|
|
99
|
+
#
|
|
100
|
+
# @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct
|
|
101
|
+
# `attribute` is declared on, named in a refusal
|
|
102
|
+
# @param attribute [Bluebook::Attribute] the reference-typed attribute to check
|
|
103
|
+
# @param held [Object, nil] the field's settled value
|
|
104
|
+
# @return [void]
|
|
105
|
+
# @raise [Runtime::TypeMismatch] if `attribute` declares a required (non-optional,
|
|
106
|
+
# non-list) `has_one`/`belongs_to` relationship and `held` is nil
|
|
76
107
|
def validate_relationship_cardinality(construct, attribute, held)
|
|
77
108
|
return if attribute.relationship.nil? || attribute.list?
|
|
78
109
|
return unless held.nil? && !attribute.optional?
|
|
@@ -84,15 +115,31 @@ module Hecks
|
|
|
84
115
|
end
|
|
85
116
|
|
|
86
117
|
# The reference resolves itself — through the chapter's own IR, so the
|
|
87
|
-
# bluebook's declared heads are the index
|
|
88
|
-
# name out of "Reference<Customer>" and
|
|
89
|
-
# `registry.bluebook(domain).aggregates` for it
|
|
90
|
-
# target through Ruby's constant tree, a class thrown away for its
|
|
91
|
-
# the moment it was found.
|
|
118
|
+
# bluebook's declared heads are the index, rather than regexing the
|
|
119
|
+
# target's name out of "Reference<Customer>" and searching
|
|
120
|
+
# `registry.bluebook(domain).aggregates` for it, or reaching the
|
|
121
|
+
# target through Ruby's constant tree, a class thrown away for its
|
|
122
|
+
# `.ir` the moment it was found.
|
|
123
|
+
#
|
|
124
|
+
# @param attribute [Bluebook::Attribute] the reference-typed attribute to resolve
|
|
125
|
+
# @return [Bluebook::Aggregate, nil] the target aggregate the reference names,
|
|
126
|
+
# or nil if it cannot be resolved
|
|
127
|
+
# @raise [Bluebook::DSL::Malformed] if the reference does not know which
|
|
128
|
+
# aggregate declares it
|
|
92
129
|
def referenced_aggregate(attribute)
|
|
93
130
|
attribute.type.resolve
|
|
94
131
|
end
|
|
95
132
|
|
|
133
|
+
# Refuses a reference-typed value whose target identity does not exist.
|
|
134
|
+
#
|
|
135
|
+
# @param domain [String, Symbol] the domain `target` is resolved in
|
|
136
|
+
# @param target [Bluebook::Aggregate] the referenced aggregate
|
|
137
|
+
# @param held [Object, Array<Object>] the offered value: a single reference,
|
|
138
|
+
# or, when `list:` is true, an Array of them
|
|
139
|
+
# @param list [Boolean] whether `held` is list-shaped
|
|
140
|
+
# @return [void]
|
|
141
|
+
# @raise [Runtime::NotFound] if any referenced identity does not exist in
|
|
142
|
+
# `target`'s repository
|
|
96
143
|
def validate_reference_values(domain, target, held, list:)
|
|
97
144
|
values = list ? Array(held) : [held]
|
|
98
145
|
values.each do |value|
|
|
@@ -132,6 +179,22 @@ module Hecks
|
|
|
132
179
|
# — one change, both interpreters covered, no new vocabulary step
|
|
133
180
|
# to keep in sync with `Vocabulary::AggregateDispatchOrder`/
|
|
134
181
|
# `EntityDispatchOrder`.
|
|
182
|
+
#
|
|
183
|
+
# @param domain [String, Symbol] the domain `target` is resolved in
|
|
184
|
+
# @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct
|
|
185
|
+
# `attribute` is declared on, named in a refusal
|
|
186
|
+
# @param attribute [Bluebook::Attribute] the reference-typed attribute being
|
|
187
|
+
# checked
|
|
188
|
+
# @param target [Bluebook::Aggregate] the referenced aggregate
|
|
189
|
+
# @param held [Object, Array<Object>] the offered value: a single reference,
|
|
190
|
+
# or, when `attribute.list?` is true, an Array of them
|
|
191
|
+
# @param state [Hash{Symbol => Object}] `construct`'s own settled state, read
|
|
192
|
+
# for its own tenant field's value
|
|
193
|
+
# @param own_tenant_field [Symbol, nil] `construct`'s own declared tenant field;
|
|
194
|
+
# a no-op if nil or absent from `state`
|
|
195
|
+
# @return [void]
|
|
196
|
+
# @raise [Runtime::Unauthorized] if a referenced record's own tenant field
|
|
197
|
+
# disagrees with `construct`'s
|
|
135
198
|
def enforce_tenant_boundary(domain, construct, attribute, target, held, state, own_tenant_field)
|
|
136
199
|
return unless own_tenant_field && state.key?(own_tenant_field)
|
|
137
200
|
|
|
@@ -172,11 +235,19 @@ module Hecks
|
|
|
172
235
|
# (`Entity.queries` is always empty in the real corpus), so this
|
|
173
236
|
# answers `nil` for every entity without needing to special-case
|
|
174
237
|
# one.
|
|
238
|
+
#
|
|
239
|
+
# @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct
|
|
240
|
+
# whose declared queries are searched
|
|
241
|
+
# @return [Symbol, nil] the tenant-scoping field name, or nil if `construct`
|
|
242
|
+
# declares no query with `authorize policy, tenant: :field`
|
|
175
243
|
def tenant_field_for(construct)
|
|
176
244
|
authorization = construct.queries.filter_map(&:authorization).find(&:tenant)
|
|
177
245
|
authorization&.tenant&.to_sym
|
|
178
246
|
end
|
|
179
247
|
|
|
248
|
+
# Renders a reference-typed value as the plain string key its target
|
|
249
|
+
# record is looked up by.
|
|
250
|
+
#
|
|
180
251
|
# `value` is the referenced record's own id, exactly as `Identity.of`
|
|
181
252
|
# would build it for that record — a bare scalar for a single-field
|
|
182
253
|
# identity (the overwhelming common case; Banking's own plain
|
|
@@ -184,18 +255,23 @@ module Hecks
|
|
|
184
255
|
# materialize_unwrapped` is a no-op passthrough here), or a
|
|
185
256
|
# `Naming.identity`-joined string for a compound one
|
|
186
257
|
# (`belongs_to Translation, as: :translation_ref` — Translation's
|
|
187
|
-
# own `identified_by :domain, :from, :to`, three fields).
|
|
188
|
-
#
|
|
189
|
-
# Value hit Ruby's default `Object#to_s` (a raw,
|
|
190
|
-
# memory address) instead of joining the record's
|
|
191
|
-
# live via bin/fuzz on the self-hosted "translation"
|
|
192
|
-
# (replay_is_deterministic), the same class of gap `Identity.from`
|
|
258
|
+
# own `identified_by :domain, :from, :to`, three fields). Without
|
|
259
|
+
# `materialize_unwrapped`, plain `value.to_s` on that compound case's
|
|
260
|
+
# own coerced Value would hit Ruby's default `Object#to_s` (a raw,
|
|
261
|
+
# run-to-run-random memory address) instead of joining the record's
|
|
262
|
+
# real id — found live via bin/fuzz on the self-hosted "translation"
|
|
263
|
+
# domain (replay_is_deterministic), the same class of gap `Identity.from`
|
|
193
264
|
# already had for a compound `identified_by`'s own bare (undotted)
|
|
194
265
|
# attribute paths. `materialize_unwrapped` recurses a multi-
|
|
195
266
|
# attribute value object to a plain Hash keyed by attribute name,
|
|
196
267
|
# in declaration order — `Naming.identity` on `.values` reproduces
|
|
197
268
|
# the identical join `Identity.of` itself would produce for the
|
|
198
269
|
# same fields.
|
|
270
|
+
#
|
|
271
|
+
# @param value [Object] the offered reference value: a `Runtime::Value`, a
|
|
272
|
+
# scalar, or anything `Value.materialize_unwrapped` can open
|
|
273
|
+
# @return [String] the key to look the referenced record up by; empty when
|
|
274
|
+
# `value` names no id
|
|
199
275
|
def reference_key(value)
|
|
200
276
|
unwrapped = Value.materialize_unwrapped(value)
|
|
201
277
|
return Naming.identity(unwrapped.values).to_s if unwrapped.is_a?(Hash)
|
|
@@ -203,8 +279,8 @@ module Hecks
|
|
|
203
279
|
unwrapped.to_s
|
|
204
280
|
end
|
|
205
281
|
|
|
206
|
-
#
|
|
207
|
-
# `given`/`ensures` — `disputed_by.status`, say, `CardPayment
|
|
282
|
+
# Hydrates a command argument's own related record, reachable by name
|
|
283
|
+
# from `given`/`ensures` — `disputed_by.status`, say, `CardPayment
|
|
208
284
|
# .Dispute`'s own fresh `Reference<Customer>` argument — without
|
|
209
285
|
# teaching the pure expression evaluator anything about
|
|
210
286
|
# repositories. The lookup happens here, once, before evaluation;
|
|
@@ -213,23 +289,21 @@ module Hecks
|
|
|
213
289
|
#
|
|
214
290
|
# `owner` narrowed to `command` only (S12, ADR 0025 — "rules
|
|
215
291
|
# confined to their own aggregate boundary"): dereferencing the
|
|
216
|
-
# declaring aggregate/entity's own stored `reference_to`
|
|
217
|
-
#
|
|
218
|
-
#
|
|
219
|
-
# `
|
|
220
|
-
#
|
|
221
|
-
#
|
|
222
|
-
#
|
|
223
|
-
#
|
|
224
|
-
#
|
|
225
|
-
#
|
|
226
|
-
#
|
|
227
|
-
#
|
|
228
|
-
# against an already-persisted reference. `enforce_givens`/
|
|
292
|
+
# declaring aggregate/entity's own stored `reference_to` is not part
|
|
293
|
+
# of this method's job — a cross-aggregate fact a rule needs has to
|
|
294
|
+
# be a `projects`-maintained local field (`AggregateBuilder#
|
|
295
|
+
# projects_impl`'s own comment), already present in `subject`'s own
|
|
296
|
+
# state, no hydration needed, rather than a live query against
|
|
297
|
+
# another aggregate's own repository on every `given`/`ensures`/
|
|
298
|
+
# `invariant` run. A reference-typed command argument stays in
|
|
299
|
+
# bounds, though — the ADR's own boundary list names "its command
|
|
300
|
+
# arguments" as readable, and nothing is stored yet for a fresh
|
|
301
|
+
# argument to project from; resolving it once here, synchronous
|
|
302
|
+
# with this command's own admission, is a different shape from a
|
|
303
|
+
# live query against an already-persisted reference. `enforce_givens`/
|
|
229
304
|
# `enforce_ensures` are this method's only two remaining callers,
|
|
230
305
|
# both passing `command`/`args`, never a `subject`'s own
|
|
231
|
-
# aggregate — verified
|
|
232
|
-
# assumed.
|
|
306
|
+
# aggregate — verified by grep, not assumed.
|
|
233
307
|
#
|
|
234
308
|
# Recurses into what it finds, so a chain deeper than one hop
|
|
235
309
|
# still resolves in one pass. Depth-bounded rather than cycle-
|
|
@@ -239,6 +313,22 @@ module Hecks
|
|
|
239
313
|
DEREFERENCE_DEPTH = 4
|
|
240
314
|
private_constant :DEREFERENCE_DEPTH
|
|
241
315
|
|
|
316
|
+
# Hydrates `owner`'s own reference-typed attributes into the records they name.
|
|
317
|
+
#
|
|
318
|
+
# @param domain [String, Symbol] the domain a referenced aggregate is
|
|
319
|
+
# resolved in
|
|
320
|
+
# @param owner [Class, Bluebook::Aggregate, Bluebook::Entity, nil] the
|
|
321
|
+
# construct whose reference-typed attributes are dereferenced; nil (or a
|
|
322
|
+
# depth of zero) short-circuits to an empty Hash
|
|
323
|
+
# @param source [Hash{Symbol => Object}] the offered payload to read each
|
|
324
|
+
# reference-typed attribute's raw id from
|
|
325
|
+
# @param depth [Integer] how many more hops to recurse into a resolved
|
|
326
|
+
# reference's own reference-typed fields
|
|
327
|
+
# @return [Hash{Symbol => Object}] one entry per resolved reference-typed
|
|
328
|
+
# attribute, keyed by the attribute name with a trailing `_id` stripped,
|
|
329
|
+
# valued at the referenced record's state merged with its own dereferenced
|
|
330
|
+
# references; an attribute with no id, an unresolvable target, or no
|
|
331
|
+
# matching record contributes nothing
|
|
242
332
|
def dereference(domain, owner, source, depth: DEREFERENCE_DEPTH)
|
|
243
333
|
return {} if depth <= 0 || owner.nil?
|
|
244
334
|
|
|
@@ -17,12 +17,28 @@ module Hecks
|
|
|
17
17
|
:unresolved_dependencies,
|
|
18
18
|
keyword_init: true
|
|
19
19
|
) do
|
|
20
|
+
# Reports whether every owner field this command could touch is a known,
|
|
21
|
+
# deterministic write.
|
|
22
|
+
#
|
|
23
|
+
# @return [Boolean] `complete_state`
|
|
20
24
|
def complete_state? = complete_state
|
|
25
|
+
|
|
26
|
+
# Reports whether the command needs no prior state at all (implies `complete_state?`).
|
|
27
|
+
#
|
|
28
|
+
# @return [Boolean] `state_independent`
|
|
21
29
|
def state_independent? = state_independent
|
|
22
30
|
|
|
31
|
+
# Chooses the dispatch strategy this plan's proof and the adapter's own
|
|
32
|
+
# capabilities together allow.
|
|
33
|
+
#
|
|
23
34
|
# Capability negotiation is correctness-first: an optimization is
|
|
24
35
|
# selected only when both the semantic proof and adapter capability
|
|
25
36
|
# are present. This is planning data only; no runtime path calls it yet.
|
|
37
|
+
#
|
|
38
|
+
# @param capabilities [Array<String, Symbol>] the repository's declared capabilities
|
|
39
|
+
# @return [Symbol] `DependencyPlanning::ATOMIC_PUT` when the plan is complete,
|
|
40
|
+
# state-independent, and the adapter declares that capability;
|
|
41
|
+
# `DependencyPlanning::TRANSACTIONAL_FALLBACK` otherwise
|
|
26
42
|
def strategy_for(capabilities: [])
|
|
27
43
|
return TRANSACTIONAL_FALLBACK unless complete_state? && state_independent?
|
|
28
44
|
return TRANSACTIONAL_FALLBACK unless capabilities.map(&:to_sym).include?(ATOMIC_PUT)
|
|
@@ -38,13 +54,27 @@ module Hecks
|
|
|
38
54
|
module ExpressionReads
|
|
39
55
|
module_function
|
|
40
56
|
|
|
57
|
+
# Finds every dotted path a canonical expression reads, without evaluating it.
|
|
58
|
+
#
|
|
41
59
|
# Read the same parsed canonical-expression nodes the evaluator uses.
|
|
42
60
|
# A generic Struct walk keeps this additive when the expression grammar
|
|
43
61
|
# gains a composed node; only Lookup nodes carry domain dependencies.
|
|
62
|
+
#
|
|
63
|
+
# @param canonical [String] the canonical expression text, such as a rule's `canonical`
|
|
64
|
+
# @return [Array<String>] the dotted paths the expression reads
|
|
44
65
|
def paths(canonical)
|
|
45
66
|
collect(Bluebook::Expression::Evaluator.parse(canonical), Set.new)
|
|
46
67
|
end
|
|
47
68
|
|
|
69
|
+
# Walks one parsed expression node, collecting the dotted paths it reads.
|
|
70
|
+
#
|
|
71
|
+
# @param node [Object] a node from `Bluebook::Expression::Evaluator.parse` — a
|
|
72
|
+
# `Bluebook::Expression::Resolver::Lookup`, a `Bluebook::Expression::Resolver::
|
|
73
|
+
# BlockPredicate`, a `Struct` composed of further nodes, an `Array` of them, or any
|
|
74
|
+
# other value (a literal), which contributes no paths
|
|
75
|
+
# @param bound_names [Set<String>] names locally bound by an enclosing block
|
|
76
|
+
# predicate's own parameter, excluded from the result rather than reported as reads
|
|
77
|
+
# @return [Array<String>] the dotted paths read under `node`, excluding `bound_names`
|
|
48
78
|
def collect(node, bound_names)
|
|
49
79
|
case node
|
|
50
80
|
when Bluebook::Expression::Resolver::Lookup
|
|
@@ -87,8 +117,20 @@ module Hecks
|
|
|
87
117
|
# unresolved regardless of correctness. Defaults to `aggregate`
|
|
88
118
|
# (a no-op) for the plain-aggregate case — `CommandInterpreter`'s
|
|
89
119
|
# own call site never needed to change.
|
|
120
|
+
#
|
|
121
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct the command
|
|
122
|
+
# is dispatched against; an entity for an entity-owned command
|
|
123
|
+
# @param command [Bluebook::Command] the command to analyze
|
|
124
|
+
# @param root_aggregate [Bluebook::Aggregate] the owning aggregate a `parent.*` read
|
|
125
|
+
# resolves against; defaults to `aggregate` for a plain-aggregate command
|
|
126
|
+
# @return [Hecks::Runtime::DependencyPlanning::Plan] the derived, frozen plan
|
|
90
127
|
def self.call(aggregate:, command:, root_aggregate: aggregate) = new(aggregate, command, root_aggregate).call
|
|
91
128
|
|
|
129
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct the command
|
|
130
|
+
# is dispatched against
|
|
131
|
+
# @param command [Bluebook::Command] the command to analyze
|
|
132
|
+
# @param root_aggregate [Bluebook::Aggregate] the owning aggregate `parent.*` reads
|
|
133
|
+
# resolve against; defaults to `aggregate`
|
|
92
134
|
def initialize(aggregate, command, root_aggregate = aggregate)
|
|
93
135
|
@aggregate = aggregate
|
|
94
136
|
@command = command
|
|
@@ -126,6 +168,9 @@ module Hecks
|
|
|
126
168
|
@unresolved = Set.new
|
|
127
169
|
end
|
|
128
170
|
|
|
171
|
+
# Runs the analysis and derives the command's dependency plan.
|
|
172
|
+
#
|
|
173
|
+
# @return [Hecks::Runtime::DependencyPlanning::Plan] the derived, frozen plan
|
|
129
174
|
def call
|
|
130
175
|
analyze_initial_state
|
|
131
176
|
analyze_mutations
|