hecks 1.4.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/claude_code.rb +65 -0
- data/lib/hecks/adapters/driven/folder.rb +73 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
- data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
- data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +93 -2
- data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +35 -7
- data/lib/hecks/adapters/driven/lambda.rb +43 -2
- data/lib/hecks/adapters/driven/local_storage.rb +67 -1
- data/lib/hecks/adapters/driven/memory.rb +13 -13
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
- data/lib/hecks/adapters/driven/prism.rb +46 -7
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- data/lib/hecks/adapters/driven/system_clock.rb +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
- data/lib/hecks/adapters/driven.rb +2 -0
- data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
- data/lib/hecks/behaviors/dsl.rb +58 -0
- data/lib/hecks/behaviors/expectations.rb +168 -7
- data/lib/hecks/behaviors/ir.rb +11 -0
- data/lib/hecks/behaviors/rspec.rb +8 -0
- data/lib/hecks/behaviors/runner.rb +19 -0
- data/lib/hecks/behaviors.rb +8 -0
- data/lib/hecks/bluebook/aggregate.rb +30 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
- data/lib/hecks/bluebook/assembly/build.rb +25 -4
- data/lib/hecks/bluebook/assembly/contract.rb +66 -14
- data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
- data/lib/hecks/bluebook/assembly/marks.rb +143 -14
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
- data/lib/hecks/bluebook/assembly.rb +25 -7
- data/lib/hecks/bluebook/attribute.rb +17 -3
- data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
- data/lib/hecks/bluebook/behaviour/command.rb +33 -3
- data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
- data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
- data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
- data/lib/hecks/bluebook/behaviour/query.rb +5 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
- data/lib/hecks/bluebook/chapter.rb +23 -0
- data/lib/hecks/bluebook/command.rb +53 -8
- data/lib/hecks/bluebook/domain_port.rb +25 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
- data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
- data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
- data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
- data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
- data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
- data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
- data/lib/hecks/bluebook/entity.rb +29 -0
- data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
- data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
- data/lib/hecks/bluebook/expression/resolver.rb +320 -77
- data/lib/hecks/bluebook/hexagon.rb +34 -0
- data/lib/hecks/bluebook/lifecycle.rb +11 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
- data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
- data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator.rb +180 -84
- data/lib/hecks/bluebook/model_check.rb +268 -24
- data/lib/hecks/bluebook/pattern_subset.rb +23 -1
- data/lib/hecks/bluebook/process_manager.rb +13 -0
- data/lib/hecks/bluebook/project_discovery.rb +5 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +44 -0
- data/lib/hecks/bluebook/query.rb +27 -0
- data/lib/hecks/bluebook/read_model.rb +21 -1
- data/lib/hecks/bluebook/reference.rb +21 -8
- data/lib/hecks/bluebook/smoke_test.rb +29 -6
- data/lib/hecks/bluebook/synthesizer.rb +34 -0
- data/lib/hecks/bluebook/translation.rb +30 -1
- data/lib/hecks/bluebook/value_object.rb +23 -5
- data/lib/hecks/bluebook.rb +3 -4
- data/lib/hecks/codemod.rb +107 -20
- data/lib/hecks/construct.rb +15 -1
- data/lib/hecks/corpus.rb +146 -25
- data/lib/hecks/deploy/bluebook/deploy.bluebook +105 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/deploy/oidc.json +5 -0
- data/lib/hecks/doc/reference.rb +185 -16
- data/lib/hecks/embryonaut_bluebook.rb +32 -9
- data/lib/hecks/facade/handle.rb +76 -3
- data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
- data/lib/hecks/forms/field_shape.rb +3 -0
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/port_argument.rb +12 -0
- data/lib/hecks/forms/query_form_renderer.rb +63 -0
- data/lib/hecks/forms/record_renderer.rb +58 -0
- data/lib/hecks/forms/record_table.rb +27 -0
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +10 -0
- data/lib/hecks/fqn.rb +58 -0
- data/lib/hecks/framework/bluebook/compliance.bluebook +221 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +43 -20
- data/lib/hecks/freezer.rb +17 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
- data/lib/hecks/fuzzing/combination_miner.rb +59 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
- data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
- data/lib/hecks/fuzzing/differential.rb +34 -0
- data/lib/hecks/fuzzing/domain_generator.rb +188 -11
- data/lib/hecks/fuzzing/era_boundary.rb +45 -15
- data/lib/hecks/fuzzing/form_census.rb +86 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
- data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
- data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
- data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
- data/lib/hecks/fuzzing/properties/guards.rb +44 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
- data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
- data/lib/hecks/fuzzing/properties/querying.rb +68 -14
- data/lib/hecks/fuzzing/properties.rb +24 -15
- data/lib/hecks/fuzzing/qa_settings.rb +12 -0
- data/lib/hecks/fuzzing/replay.rb +137 -29
- data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
- data/lib/hecks/fuzzing/self_consistency.rb +189 -40
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
- data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
- data/lib/hecks/fuzzing/shrinker.rb +114 -2
- data/lib/hecks/fuzzing/structural_skips.rb +18 -4
- data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
- data/lib/hecks/fuzzing/value_generator.rb +98 -10
- data/lib/hecks/grammar/evolve.rb +178 -2
- data/lib/hecks/grammar.rb +46 -0
- data/lib/hecks/ir.rb +38 -7
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/literal.rb +32 -0
- data/lib/hecks/naming.rb +88 -7
- data/lib/hecks/ports/access_control.rb +5 -10
- data/lib/hecks/ports/authorization.rb +3 -6
- data/lib/hecks/ports/identity_assignment.rb +1 -2
- data/lib/hecks/ports/identity_resolution.rb +1 -2
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +85 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
- data/lib/hecks/ports/query/in_memory.rb +39 -6
- data/lib/hecks/ports/query/ordering.rb +15 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +43 -8
- data/lib/hecks/projections/deploy/fargate.rb +666 -0
- data/lib/hecks/projections/deploy/lambda.rb +2423 -0
- data/lib/hecks/projections/deploy/shared.rb +624 -0
- data/lib/hecks/projections/diagrams.rb +243 -7
- data/lib/hecks/projections/glossary/html.rb +88 -0
- data/lib/hecks/projections/glossary/markdown.rb +47 -0
- data/lib/hecks/projections/glossary/mermaid.rb +48 -0
- data/lib/hecks/projections/glossary/sentences.rb +105 -0
- data/lib/hecks/projections/glossary.rb +161 -19
- data/lib/hecks/projections/model/deviations.rb +44 -0
- data/lib/hecks/projections/model.rb +51 -1
- data/lib/hecks/projections/oidc.rb +40 -3
- data/lib/hecks/projections/parser_table.rb +49 -8
- data/lib/hecks/projections/reference.rb +12 -0
- data/lib/hecks/projections/rust_vocabulary.rb +219 -16
- data/lib/hecks/projections/shape.rb +8 -0
- data/lib/hecks/projections/statements.rb +63 -16
- data/lib/hecks/projections/vocabulary.rb +17 -0
- data/lib/hecks/projections.rb +3 -0
- data/lib/hecks/projector/cli_projector.rb +218 -10
- data/lib/hecks/projector/docs_projector.rb +145 -19
- data/lib/hecks/projector/exporter.rb +65 -11
- data/lib/hecks/projector/ir_projector.rb +6 -0
- data/lib/hecks/projector/narrate_projector.rb +136 -15
- data/lib/hecks/projector/target.rb +47 -10
- data/lib/hecks/projector.rb +101 -19
- data/lib/hecks/query_ir.rb +47 -0
- data/lib/hecks/query_specification/common/null_policy.rb +5 -3
- data/lib/hecks/rendering.rb +6 -0
- data/lib/hecks/router/namespace_installer.rb +13 -0
- data/lib/hecks/router.rb +55 -0
- data/lib/hecks/runtime/aggregate_lock.rb +9 -0
- data/lib/hecks/runtime/boot_gates.rb +18 -0
- data/lib/hecks/runtime/caller.rb +32 -0
- data/lib/hecks/runtime/capability_graph.rb +11 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
- data/lib/hecks/runtime/command_interpreter.rb +42 -17
- data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
- data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
- data/lib/hecks/runtime/command_rules/references.rb +118 -28
- data/lib/hecks/runtime/dependency_planning.rb +45 -0
- data/lib/hecks/runtime/dispatcher.rb +28 -50
- data/lib/hecks/runtime/entity_element.rb +161 -8
- data/lib/hecks/runtime/entity_interpreter.rb +44 -9
- data/lib/hecks/runtime/errors.rb +18 -4
- data/lib/hecks/runtime/event.rb +10 -5
- data/lib/hecks/runtime/identity.rb +71 -3
- data/lib/hecks/runtime/instance.rb +67 -7
- data/lib/hecks/runtime/interpreting.rb +13 -5
- data/lib/hecks/runtime/invocation.rb +118 -36
- data/lib/hecks/runtime/loader.rb +94 -8
- data/lib/hecks/runtime/outbox.rb +145 -7
- data/lib/hecks/runtime/policy_interpreter.rb +22 -9
- data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
- data/lib/hecks/runtime/query_interpreter.rb +40 -12
- data/lib/hecks/runtime/reaction_invocation.rb +53 -8
- data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
- data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
- data/lib/hecks/runtime/reference_hop.rb +42 -0
- data/lib/hecks/runtime/refusal_wording.rb +50 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
- data/lib/hecks/runtime/registry/verification.rb +119 -4
- data/lib/hecks/runtime/registry.rb +157 -4
- data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
- data/lib/hecks/runtime/routing.rb +27 -2
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
- data/lib/hecks/runtime/saga_interpreter.rb +27 -13
- data/lib/hecks/runtime/tenant_check.rb +26 -6
- data/lib/hecks/runtime/tenant_scope.rb +18 -0
- data/lib/hecks/runtime/value/coercion.rb +255 -33
- data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
- data/lib/hecks/runtime/value.rb +50 -0
- data/lib/hecks/runtime.rb +32 -0
- data/lib/hecks/storehouse.rb +305 -9
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +79 -1
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
- metadata +14 -5
- data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
- data/lib/hecks/deprecation.rb +0 -95
- data/lib/hecks/framework/bluebook/compliance.bluebook +0 -1
|
@@ -31,7 +31,7 @@ module Hecks
|
|
|
31
31
|
# both directions.
|
|
32
32
|
DISPATCH_ORDER = Hecks::Vocabulary.symbols("AggregateDispatchOrder")
|
|
33
33
|
|
|
34
|
-
#
|
|
34
|
+
# A last-resort safety valve, not the normal outcome path — see
|
|
35
35
|
# `Runtime::StaleWrite`'s own comment. Two concurrent writers
|
|
36
36
|
# against one aggregate resolve through exactly one retry in the
|
|
37
37
|
# ordinary case (the loser's retried hydrate reads the winner's now-
|
|
@@ -41,14 +41,17 @@ module Hecks
|
|
|
41
41
|
# two-writer case.
|
|
42
42
|
MAX_STALE_WRITE_RETRIES = 5
|
|
43
43
|
|
|
44
|
-
# Every cross-step local
|
|
45
|
-
#
|
|
44
|
+
# Every cross-step local held in one place, now that the sequence is
|
|
45
|
+
# data-driven rather than `call`'s own literal method-call sequence —
|
|
46
46
|
# `result` and `transition`/`old_state` default to nil until the step
|
|
47
|
-
# that sets them runs, same as
|
|
47
|
+
# that sets them runs, the same as unset locals would be before that point.
|
|
48
48
|
Context = Struct.new(:domain, :aggregate, :command, :args, :repository, :instance, :transition, :old_state,
|
|
49
49
|
:result, :correlation, :route, :plan, :strategy, :persistence_outcome, :pending_delegation,
|
|
50
50
|
:dry_run, :correction_bindings, :outbox_rows, :invocation)
|
|
51
51
|
|
|
52
|
+
# @param registry [Runtime::Registry] the booted registry this interpreter reads
|
|
53
|
+
# @param rules [Runtime::CommandRules] the shared rules engine (admissibility,
|
|
54
|
+
# references, arithmetic, authorization, emission) dispatch runs through
|
|
52
55
|
def initialize(registry, rules:)
|
|
53
56
|
@registry = registry
|
|
54
57
|
@rules = rules
|
|
@@ -69,6 +72,27 @@ module Hecks
|
|
|
69
72
|
# `invocation` — the `Runtime::Invocation` `Dispatcher` built for this
|
|
70
73
|
# call. `ctx.args` is `invocation.to_args` (the same Hash routing
|
|
71
74
|
# always handed this method), `ctx.route` its `target`.
|
|
75
|
+
#
|
|
76
|
+
# @param domain [String, Symbol] the domain `aggregate` belongs to
|
|
77
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate the command acts on
|
|
78
|
+
# @param command [Class] the command class (`Bluebook::Command` subclass) to dispatch
|
|
79
|
+
# @param invocation [Runtime::Invocation] the invocation `Dispatcher` built for
|
|
80
|
+
# this call
|
|
81
|
+
# @param correlation [Hash{Symbol => Object}, nil] correlation head => value,
|
|
82
|
+
# stamped on every emitted event when a saga leg causes this dispatch; nil
|
|
83
|
+
# otherwise
|
|
84
|
+
# @param dry_run [Boolean] whether to run every step through validation without
|
|
85
|
+
# saving, emitting or enqueueing
|
|
86
|
+
# @return [Array(Runtime::Instance, Array<Runtime::Event>,
|
|
87
|
+
# Runtime::DependencyPlanning::Plan, Ports::Persistence::Execution,
|
|
88
|
+
# Array<Runtime::Outbox::Row>)] the settled instance, emitted events,
|
|
89
|
+
# execution plan, persistence outcome and outbox rows — the last
|
|
90
|
+
# three nil on a dry run, which skips save/emit/outbox
|
|
91
|
+
# @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when a
|
|
92
|
+
# given/ensures/invariant/authorization/admissibility rule refuses
|
|
93
|
+
# @raise [Runtime::StaleWrite] if concurrent writers beat this one through every
|
|
94
|
+
# retry (`MAX_STALE_WRITE_RETRIES`)
|
|
95
|
+
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
72
96
|
def call(domain, aggregate, command, invocation, correlation = nil, dry_run: false)
|
|
73
97
|
args = invocation.to_args
|
|
74
98
|
route = invocation.target
|
|
@@ -273,9 +297,9 @@ module Hecks
|
|
|
273
297
|
# `ensures`/`enforce_invariants`/`save` steps still run after
|
|
274
298
|
# this one. The target's emission is parked and performed by
|
|
275
299
|
# `step_emit`, after the parent committed — where every other
|
|
276
|
-
# command's events are emitted too. (
|
|
277
|
-
# leg's events
|
|
278
|
-
# the parent could refuse.)
|
|
300
|
+
# command's events are emitted too. (Without parking it here, the
|
|
301
|
+
# entity leg's events would be on the event log and in the adapter
|
|
302
|
+
# before the parent could refuse.)
|
|
279
303
|
ctx.pending_delegation = [target_command, target_args]
|
|
280
304
|
end
|
|
281
305
|
end
|
|
@@ -350,7 +374,7 @@ module Hecks
|
|
|
350
374
|
|
|
351
375
|
def persist_instance(ctx)
|
|
352
376
|
if ctx.strategy == DependencyPlanning::ATOMIC_PUT
|
|
353
|
-
#
|
|
377
|
+
# A second creation is not a fresh one — see
|
|
354
378
|
# hydrate_complete_state's own comment; the
|
|
355
379
|
# same refusal, on the same terms, for the
|
|
356
380
|
# complete-state path. `insert_only:` asks the
|
|
@@ -441,8 +465,9 @@ module Hecks
|
|
|
441
465
|
# `:delegate` mutation.
|
|
442
466
|
if ctx.pending_delegation
|
|
443
467
|
target_command, target_args = ctx.pending_delegation
|
|
444
|
-
# The same dispatch, so the same correlation —
|
|
445
|
-
#
|
|
468
|
+
# The same dispatch, so the same correlation — without
|
|
469
|
+
# threading `ctx.correlation` through, a saga-driven door's
|
|
470
|
+
# events would lose their stamp here.
|
|
446
471
|
next @rules.emit(target_command, ctx.domain, ctx.aggregate, ctx.instance, target_args, ctx.repository,
|
|
447
472
|
ctx.correlation)
|
|
448
473
|
end
|
|
@@ -562,7 +587,7 @@ module Hecks
|
|
|
562
587
|
aggregate: aggregate.hecks_name,
|
|
563
588
|
identity: identity_reading(aggregate)))
|
|
564
589
|
|
|
565
|
-
#
|
|
590
|
+
# A second creation is not a fresh one — `creates?` on an identity
|
|
566
591
|
# a record already exists under refuses (`AlreadyExists`) rather
|
|
567
592
|
# than silently overwriting it, the same refusal `hydrate_prior_
|
|
568
593
|
# or_initial`'s own body gives for its own complete-but-state-
|
|
@@ -604,7 +629,7 @@ module Hecks
|
|
|
604
629
|
identity: identity_reading(aggregate)))
|
|
605
630
|
found = repository.find(id)
|
|
606
631
|
|
|
607
|
-
#
|
|
632
|
+
# A second creation is not a fresh one — see hydrate_complete_
|
|
608
633
|
# state's own comment; the same refusal, on the same terms, for
|
|
609
634
|
# a complete-but-state-dependent command (one with a `given`
|
|
610
635
|
# reading its own prior state, which is what routes here instead
|
|
@@ -614,10 +639,10 @@ module Hecks
|
|
|
614
639
|
# prior state this branch exists to supply), so only a genuine
|
|
615
640
|
# creation reusing an already-occupied identity is a duplicate.
|
|
616
641
|
# `SafeDepositBox.Rent` is exactly this shape — `given("box is
|
|
617
|
-
# vacant")` makes it state-dependent, so
|
|
618
|
-
# silently hydrate the existing box as "prior
|
|
619
|
-
# for the wrong reason (not vacant) instead of
|
|
620
|
-
# (already exists).
|
|
642
|
+
# vacant")` makes it state-dependent, so without this check a
|
|
643
|
+
# second Rent would silently hydrate the existing box as "prior
|
|
644
|
+
# state" and refuse for the wrong reason (not vacant) instead of
|
|
645
|
+
# the right one (already exists).
|
|
621
646
|
if found && command.creates?
|
|
622
647
|
raise(AlreadyExists, RefusalWording.render_site("AlreadyExists", "creating_duplicate",
|
|
623
648
|
command: command.hecks_name, aggregate: aggregate.hecks_name,
|
|
@@ -628,7 +653,7 @@ module Hecks
|
|
|
628
653
|
found ? found.dup : Instance.new(aggregate: aggregate, id: id, args: args)
|
|
629
654
|
end
|
|
630
655
|
|
|
631
|
-
#
|
|
656
|
+
# The join, the dig, and the reading — all shared with `EntityInterpreter`
|
|
632
657
|
# now, in `Runtime::Identity`, rather than kept as two copies that could
|
|
633
658
|
# only ever drift. See that module for the reasoning ; these three stay
|
|
634
659
|
# here, at the old names, purely so nothing below has to change.
|
|
@@ -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
|