hecks 1.0.2 → 1.1.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 +3 -3
- data/lib/hecks/adapters/driven/d1.rb +48 -28
- data/lib/hecks/adapters/driven/folder.rb +10 -3
- data/lib/hecks/adapters/driven/google_authentication.rb +2 -4
- data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +3 -5
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +4 -1
- data/lib/hecks/adapters/driven/lambda.rb +2 -2
- data/lib/hecks/adapters/driven/local_storage.adapter +3 -0
- data/lib/hecks/adapters/driven/local_storage.rb +120 -0
- data/lib/hecks/adapters/driven/memory.rb +48 -3
- data/lib/hecks/adapters/driven/postgres/codec.rb +3 -1
- data/lib/hecks/adapters/driven/postgres/outbox.rb +97 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +3 -2
- data/lib/hecks/adapters/driven/postgres.rb +25 -14
- data/lib/hecks/adapters/driven/prism.rb +11 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +45 -20
- data/lib/hecks/adapters/driven/sqlite/codec.rb +3 -1
- data/lib/hecks/adapters/driven/sqlite/projection.rb +17 -1
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +23 -1
- data/lib/hecks/adapters/driven/sqlite.rb +87 -8
- data/lib/hecks/adapters/driven.rb +4 -0
- data/lib/hecks/adapters.rb +4 -0
- data/lib/hecks/behaviors/dsl.rb +7 -0
- data/lib/hecks/behaviors/expectations.rb +32 -4
- data/lib/hecks/behaviors/rspec.rb +2 -0
- data/lib/hecks/behaviors/runner.rb +4 -0
- data/lib/hecks/bluebook/aggregate.rb +6 -5
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +4 -4
- data/lib/hecks/bluebook/assembly/build.rb +6 -3
- data/lib/hecks/bluebook/assembly/contract.rb +5 -1
- data/lib/hecks/bluebook/assembly/contracts.rb +4 -0
- data/lib/hecks/bluebook/assembly/marks.rb +2 -1
- data/lib/hecks/bluebook/assembly.rb +1 -3
- data/lib/hecks/bluebook/attribute.rb +6 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +18 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +17 -1
- data/lib/hecks/bluebook/behaviour/traits.rb +1 -1
- data/lib/hecks/bluebook/command.rb +6 -5
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +4 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +524 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +19 -472
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +14 -6
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +5 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +848 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +25 -784
- data/lib/hecks/bluebook/dsl/command_builder.rb +53 -13
- data/lib/hecks/bluebook/dsl/const_shim.rb +6 -0
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +20 -7
- data/lib/hecks/bluebook/dsl/entity_builder.rb +27 -6
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +12 -2
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +8 -1
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +24 -12
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +40 -1
- data/lib/hecks/bluebook/dsl/policy_builder.rb +10 -1
- data/lib/hecks/bluebook/dsl/port_builder.rb +6 -1
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +33 -12
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +56 -12
- data/lib/hecks/bluebook/dsl/query_builder.rb +7 -1
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +11 -3
- data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -1
- data/lib/hecks/bluebook/dsl/translation_builder.rb +13 -2
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +8 -1
- data/lib/hecks/bluebook/dsl/word_gate.rb +5 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +13 -3
- data/lib/hecks/bluebook/entity.rb +5 -3
- data/lib/hecks/bluebook/expression/ast_json.rb +88 -9
- data/lib/hecks/bluebook/expression/ast_reader.rb +107 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +7 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +64 -11
- data/lib/hecks/bluebook/expression/projection.json +12 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +4 -3
- data/lib/hecks/bluebook/expression/resolver.rb +83 -11
- data/lib/hecks/bluebook/expression.rb +1 -0
- data/lib/hecks/bluebook/hexagon.rb +10 -0
- data/lib/hecks/bluebook/lifecycle.rb +9 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +10 -4
- data/lib/hecks/bluebook/meta_validator/plan.rb +4 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/shapes.rb +3 -3
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +7 -4
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +42 -3
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -1
- data/lib/hecks/bluebook/meta_validator.rb +5 -5
- data/lib/hecks/bluebook/model_check.rb +119 -82
- data/lib/hecks/bluebook/pattern_subset.rb +9 -0
- data/lib/hecks/bluebook/policy.rb +2 -1
- data/lib/hecks/bluebook/process_manager.rb +6 -0
- data/lib/hecks/bluebook/project_register.rb +45 -25
- data/lib/hecks/bluebook/query.rb +4 -0
- data/lib/hecks/bluebook/read_model.rb +2 -2
- data/lib/hecks/bluebook/smoke_test.rb +5 -0
- data/lib/hecks/bluebook/synthesizer.rb +2 -1
- data/lib/hecks/bluebook/translation.rb +3 -0
- data/lib/hecks/bluebook/value_object.rb +2 -2
- data/lib/hecks/codemod.rb +13 -0
- data/lib/hecks/doc/reference.rb +17 -11
- data/lib/hecks/facade/cli_runner.rb +2 -2
- data/lib/hecks/facade/command_request.rb +11 -11
- data/lib/hecks/facade/handle.rb +3 -3
- data/lib/hecks/facade/surface/aggregate_door.rb +52 -38
- data/lib/hecks/facade.rb +4 -0
- data/lib/hecks/forms/app.rb +10 -6
- data/lib/hecks/forms/command_form_renderer.rb +3 -3
- data/lib/hecks/forms/field_renderer.rb +14 -9
- data/lib/hecks/forms/field_shape.rb +7 -2
- data/lib/hecks/forms/html.rb +5 -2
- data/lib/hecks/forms/index_renderer.rb +1 -1
- data/lib/hecks/forms/page.rb +7 -3
- data/lib/hecks/forms/query_form_renderer.rb +3 -2
- data/lib/hecks/forms/record_renderer.rb +22 -16
- data/lib/hecks/forms.rb +4 -0
- data/lib/hecks/fqn.rb +21 -9
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +30 -8
- data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -1
- data/lib/hecks/fuzzing/isolated_boot.rb +14 -6
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +264 -0
- data/lib/hecks/fuzzing/properties/guards.rb +251 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +351 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +99 -0
- data/lib/hecks/fuzzing/properties/querying.rb +206 -0
- data/lib/hecks/fuzzing/properties.rb +41 -1065
- data/lib/hecks/fuzzing/replay.rb +60 -16
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +6 -2
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +2 -1
- data/lib/hecks/fuzzing/sequence_generator.rb +0 -1
- data/lib/hecks/fuzzing/value_generator.rb +2 -2
- data/lib/hecks/grammar/evolve.rb +34 -10
- data/lib/hecks/grammar/expression_operators.json +124 -0
- data/lib/hecks/grammar.rb +15 -5
- data/lib/hecks/ir.rb +9 -0
- data/lib/hecks/language/bluebook/aggregate.bluebook +4 -2
- data/lib/hecks/language/bluebook/command.bluebook +2 -1
- data/lib/hecks/language/bluebook/entity.bluebook +2 -1
- data/lib/hecks/language/bluebook/projection.bluebook +2 -1
- data/lib/hecks/language/bluebook/query.bluebook +4 -2
- data/lib/hecks/language/bluebook/shape.bluebook +2 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -0
- data/lib/hecks/literal.rb +18 -8
- data/lib/hecks/naming.rb +6 -3
- data/lib/hecks/ports/access_control.rb +1 -1
- data/lib/hecks/ports/agent.rb +5 -3
- data/lib/hecks/ports/authentication.rb +3 -2
- data/lib/hecks/ports/authorization.rb +1 -1
- data/lib/hecks/ports/clock.rb +1 -1
- data/lib/hecks/ports/extraction.rb +5 -1
- data/lib/hecks/ports/identity_assignment.rb +1 -1
- data/lib/hecks/ports/identity_generation.rb +1 -1
- data/lib/hecks/ports/identity_resolution.rb +1 -1
- data/lib/hecks/ports/loading.rb +4 -1
- data/lib/hecks/ports/persistence/append_only.rb +40 -4
- data/lib/hecks/ports/persistence/binding_policy.rb +1 -1
- data/lib/hecks/ports/persistence/plugin.rb +3 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -1
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +5 -5
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +14 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +4 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +80 -16
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +23 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +43 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +25 -4
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +49 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +10 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +49 -38
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +23 -13
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +19 -21
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +27 -9
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +9 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +25 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +2 -6
- data/lib/hecks/ports/persistence/plugins/era.rb +3 -1
- data/lib/hecks/ports/persistence/remote_runtime.rb +1 -1
- data/lib/hecks/ports/persistence.rb +7 -3
- data/lib/hecks/ports/projection.rb +13 -5
- data/lib/hecks/ports/query/in_memory.rb +8 -2
- data/lib/hecks/ports/query.rb +1 -3
- data/lib/hecks/ports.rb +2 -0
- data/lib/hecks/projections/diagrams.rb +11 -3
- data/lib/hecks/projections/ir.rb +2 -0
- data/lib/hecks/projections/model/deviations.rb +2 -1
- data/lib/hecks/projections/model.rb +15 -10
- data/lib/hecks/projections/vocabulary.rb +1 -3
- data/lib/hecks/projector/cli_projector.rb +49 -29
- data/lib/hecks/projector/docs_projector.rb +21 -12
- data/lib/hecks/projector/exporter.rb +10 -1
- data/lib/hecks/projector/narrate_projector.rb +83 -25
- data/lib/hecks/projector.rb +2 -2
- data/lib/hecks/query_ir.rb +60 -36
- data/lib/hecks/query_specification/common/authorization_spec.rb +1 -1
- data/lib/hecks/query_specification/common/comparators.rb +4 -0
- data/lib/hecks/query_specification/common/comparison.rb +8 -1
- data/lib/hecks/query_specification/common/dsl.rb +5 -0
- data/lib/hecks/query_specification/common/null_policy.rb +6 -2
- data/lib/hecks/query_specification/common/options.rb +17 -5
- data/lib/hecks/query_specification/field_path.rb +4 -4
- data/lib/hecks/query_specification/read_model/specification.rb +5 -2
- data/lib/hecks/router/namespace_installer.rb +22 -10
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +57 -22
- data/lib/hecks/runtime/command_interpreter.rb +104 -65
- data/lib/hecks/runtime/command_rules/admissibility.rb +26 -8
- data/lib/hecks/runtime/command_rules/arithmetic.rb +35 -14
- data/lib/hecks/runtime/dependency_planning.rb +33 -21
- data/lib/hecks/runtime/dispatcher.rb +41 -7
- data/lib/hecks/runtime/entity_element.rb +25 -6
- data/lib/hecks/runtime/entity_interpreter.rb +28 -15
- data/lib/hecks/runtime/errors.rb +28 -2
- data/lib/hecks/runtime/instance.rb +9 -3
- data/lib/hecks/runtime/interpreting.rb +29 -2
- data/lib/hecks/runtime/loader.rb +21 -0
- data/lib/hecks/runtime/outbox.rb +365 -0
- data/lib/hecks/runtime/policy_interpreter.rb +32 -17
- data/lib/hecks/runtime/query_interpreter.rb +36 -16
- data/lib/hecks/runtime/reaction_invocation.rb +35 -22
- data/lib/hecks/runtime/read_model_interpreter.rb +26 -3
- data/lib/hecks/runtime/reference_hop.rb +1 -1
- data/lib/hecks/runtime/refusal_wording.rb +55 -49
- data/lib/hecks/runtime/registry/saga_persistence.rb +3 -1
- data/lib/hecks/runtime/registry/verification.rb +39 -2
- data/lib/hecks/runtime/registry.rb +21 -11
- data/lib/hecks/runtime/remote_dispatcher.rb +3 -1
- data/lib/hecks/runtime/routing.rb +52 -39
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +7 -7
- data/lib/hecks/runtime/saga_interpreter.rb +119 -90
- data/lib/hecks/runtime/saga_pending_dispatch.rb +6 -3
- data/lib/hecks/runtime/tenant_scope.rb +7 -0
- data/lib/hecks/runtime/value/coercion.rb +207 -48
- data/lib/hecks/runtime/value.rb +1 -1
- data/lib/hecks/runtime.rb +7 -2
- data/lib/hecks/storehouse.rb +15 -8
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +1 -0
- data/lib/hecks.rb +17 -13
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +1 -1
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +1 -1
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +1 -1
- metadata +14 -2
|
@@ -18,17 +18,39 @@ module Hecks
|
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
# A CASE STATEMENT OVER A CLOSED, DECLARED SET — every mutation op
|
|
22
|
+
# the grammar can emit gets its own branch, including the `else`
|
|
23
|
+
# backstop for the day a new op reaches this method undeclared (see
|
|
24
|
+
# its own comment). Splitting each branch into its own method would
|
|
25
|
+
# not reduce what a reader has to hold at once (each op's own
|
|
26
|
+
# comment already explains why IT is shaped the way it is) and
|
|
27
|
+
# would obscure that the set is closed and exhaustive.
|
|
28
|
+
# rubocop:disable Lint/DuplicateBranch -- :delegate and :corrects
|
|
29
|
+
# both no-op here, for two unrelated documented reasons (see each
|
|
30
|
+
# branch's own comment below); merging would blur that distinction.
|
|
31
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
32
|
+
#
|
|
33
|
+
# `pre` — THE PRE-DISPATCH STATE (C4.2): every source below reads
|
|
34
|
+
# it, every target is written to `instance`. A command's effects
|
|
35
|
+
# are one update set; declaration order carries no meaning, and
|
|
36
|
+
# build refuses a field written twice (`CommandBuilder#
|
|
37
|
+
# refuse_duplicate_targets!`).
|
|
38
|
+
def apply(instance, aggregate, mutation, args, pre = instance)
|
|
22
39
|
case mutation.op
|
|
23
40
|
when :set
|
|
24
|
-
value = mutation.source.is_a?(StateRef)
|
|
41
|
+
value = if mutation.source.is_a?(StateRef)
|
|
42
|
+
pre[mutation.source.name]
|
|
43
|
+
else
|
|
44
|
+
@rules.resolve_source(mutation.source,
|
|
45
|
+
args)
|
|
46
|
+
end
|
|
25
47
|
instance[mutation.target] = Value.for(aggregate, mutation.target, value)
|
|
26
48
|
when :append
|
|
27
|
-
instance[mutation.target] = appended(
|
|
49
|
+
instance[mutation.target] = appended(pre, aggregate, mutation, args)
|
|
28
50
|
when :increment, :decrement
|
|
29
51
|
amount = @rules.resolve_source(mutation.source, args)
|
|
30
52
|
attribute = aggregate.attribute(mutation.target)
|
|
31
|
-
current =
|
|
53
|
+
current = pre[mutation.target]
|
|
32
54
|
# Vendored fix, not (yet) upstream hecks (migration plan
|
|
33
55
|
# task 9): see #rewrap_arithmetic_result's own comment below
|
|
34
56
|
# -- `amount` is wrapped ONLY when `current` already is, not
|
|
@@ -42,7 +64,7 @@ module Hecks
|
|
|
42
64
|
# RemoveDependency/DeactivateSprint: "a concurrent Add can
|
|
43
65
|
# never be lost").
|
|
44
66
|
when :remove
|
|
45
|
-
instance[mutation.target] = removed(
|
|
67
|
+
instance[mutation.target] = removed(pre, aggregate, mutation, args)
|
|
46
68
|
# Vendored addition, not (yet) upstream hecks (migration
|
|
47
69
|
# plan task 4, i106): multiply/clamp, the scale/bound pair
|
|
48
70
|
# alongside increment/decrement's add/subtract pair -- see
|
|
@@ -50,7 +72,7 @@ module Hecks
|
|
|
50
72
|
when :multiply
|
|
51
73
|
amount = @rules.resolve_source(mutation.source, args)
|
|
52
74
|
attribute = aggregate.attribute(mutation.target)
|
|
53
|
-
current =
|
|
75
|
+
current = pre[mutation.target]
|
|
54
76
|
amount = Value.for_attribute(aggregate, attribute, amount) if attribute && current.is_a?(Value)
|
|
55
77
|
result = @rules.multiply(current, amount, mutation.target)
|
|
56
78
|
instance[mutation.target] = rewrap_arithmetic_result(aggregate, attribute, current, result)
|
|
@@ -61,7 +83,7 @@ module Hecks
|
|
|
61
83
|
# a no-op for an Array (it only special-cases Symbol), so it's
|
|
62
84
|
# read straight.
|
|
63
85
|
when :clamp
|
|
64
|
-
instance[mutation.target] = @rules.clamp(
|
|
86
|
+
instance[mutation.target] = @rules.clamp(pre[mutation.target], mutation.source, mutation.target)
|
|
65
87
|
# `delegates_to` — CommandBuilder#delegates_to's own comment gives
|
|
66
88
|
# the full reasoning for storing it as a mutation at all. A REAL
|
|
67
89
|
# no-op here, not a gap: it targets no field on THIS instance —
|
|
@@ -94,6 +116,8 @@ module Hecks
|
|
|
94
116
|
raise Runtime::WiringError, "no mutation applier handles :#{mutation.op} — add one before declaring it"
|
|
95
117
|
end
|
|
96
118
|
end
|
|
119
|
+
# rubocop:enable Lint/DuplicateBranch
|
|
120
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
97
121
|
|
|
98
122
|
# A CALLER-SUPPLIED ARG, FIRST -- an append's own field can also
|
|
99
123
|
# name something the SUBJECT ALREADY KNOWS about itself, falling
|
|
@@ -122,21 +146,23 @@ module Hecks
|
|
|
122
146
|
fields = mutation.source.transform_values { |source| resolve_append_source(source, instance, args) }
|
|
123
147
|
element_type = aggregate.attribute(mutation.target)&.type
|
|
124
148
|
value_object = aggregate.value_object(element_type)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
fields[attribute.name] = Value.scalar(held) if held.is_a?(Value) && held.to_h.size == 1
|
|
135
|
-
end
|
|
149
|
+
value_object&.attributes&.each do |attribute|
|
|
150
|
+
held = fields[attribute.name]
|
|
151
|
+
# A SINGLE-FIELD VALUE unwraps to its scalar so it bridges
|
|
152
|
+
# into the element's own (differently named) wrapper; a
|
|
153
|
+
# MULTI-FIELD one (a `state(:en_passant_square)` Square
|
|
154
|
+
# copied off the record) has no scalar to stand in for it
|
|
155
|
+
# and is handed across whole — `Value.for_attribute` keeps a
|
|
156
|
+
# value of the element field's own type as it is.
|
|
157
|
+
fields[attribute.name] = Value.scalar(held) if held.is_a?(Value) && held.to_h.size == 1
|
|
136
158
|
end
|
|
137
|
-
element =
|
|
138
|
-
|
|
139
|
-
|
|
159
|
+
element = if value_object
|
|
160
|
+
Value.build(value_object, fields,
|
|
161
|
+
aggregate)
|
|
162
|
+
else
|
|
163
|
+
entity_element(aggregate, element_type, instance[mutation.target],
|
|
164
|
+
fields)
|
|
165
|
+
end
|
|
140
166
|
|
|
141
167
|
# FROZEN, like every other value the domain hands back. An
|
|
142
168
|
# appended list used to come back mutable, so a caller could
|
|
@@ -215,7 +241,7 @@ module Hecks
|
|
|
215
241
|
end
|
|
216
242
|
if entity.identified_by && !fields.key?(entity.identified_by)
|
|
217
243
|
attribute = entity.attribute(entity.identified_by)
|
|
218
|
-
fields[entity.identified_by] = Value.from_identifier(aggregate, attribute,
|
|
244
|
+
fields[entity.identified_by] = Value.from_identifier(aggregate, attribute, next_identity(current, entity))
|
|
219
245
|
else
|
|
220
246
|
check_entity_collision(aggregate, entity, current, fields)
|
|
221
247
|
end
|
|
@@ -223,6 +249,15 @@ module Hecks
|
|
|
223
249
|
fields
|
|
224
250
|
end
|
|
225
251
|
|
|
252
|
+
# THE MINTED IDENTITY IS ONE PAST THE HIGHEST HELD (C4.5) — not
|
|
253
|
+
# `size + 1`, which repeats an identity the moment the list has
|
|
254
|
+
# ever shrunk. One strategy for the language, so the IR declares
|
|
255
|
+
# none; the Rust generators mint by the same rule.
|
|
256
|
+
def next_identity(current, entity)
|
|
257
|
+
held = Array(current).map { |element| Value.scalar(element[entity.identified_by]).to_i }
|
|
258
|
+
held.max.to_i + 1
|
|
259
|
+
end
|
|
260
|
+
|
|
226
261
|
# THE SAME CHECK #hydrate GIVES EVERY CREATING AGGREGATE COMMAND
|
|
227
262
|
# (`repository.find(id)`, above this file in command_interpreter.rb),
|
|
228
263
|
# one level down. Reached only on the two branches #entity_element
|
|
@@ -47,8 +47,8 @@ module Hecks
|
|
|
47
47
|
# `result` and `transition`/`old_state` default to nil until the step
|
|
48
48
|
# that sets them runs, same as they were unset locals before that point.
|
|
49
49
|
Context = Struct.new(:domain, :aggregate, :command, :args, :repository, :instance, :transition, :old_state,
|
|
50
|
-
:result, :correlation, :route, :plan, :strategy, :persistence_outcome, :
|
|
51
|
-
:dry_run, :correction_bindings)
|
|
50
|
+
:result, :correlation, :route, :plan, :strategy, :persistence_outcome, :pending_delegation,
|
|
51
|
+
:dry_run, :correction_bindings, :outbox_rows)
|
|
52
52
|
|
|
53
53
|
def initialize(registry, rules:)
|
|
54
54
|
@registry = registry
|
|
@@ -82,7 +82,7 @@ module Hecks
|
|
|
82
82
|
ctx.repository = @registry.repository(domain, aggregate)
|
|
83
83
|
lock_id = Identity.best_effort(aggregate, args, route, reference_key: reference_key(command))
|
|
84
84
|
run_dispatch_order_with_isolation(DISPATCH_ORDER, ctx, lock_key_id: lock_id)
|
|
85
|
-
[ctx.instance, ctx.result, ctx.plan, ctx.persistence_outcome]
|
|
85
|
+
[ctx.instance, ctx.result, ctx.plan, ctx.persistence_outcome, ctx.outbox_rows]
|
|
86
86
|
rescue StaleWrite
|
|
87
87
|
attempt += 1
|
|
88
88
|
retry if attempt < MAX_STALE_WRITE_RETRIES
|
|
@@ -116,7 +116,7 @@ module Hecks
|
|
|
116
116
|
# `ctx.repository` is resolved once, in `#call`, before the
|
|
117
117
|
# isolation decision (lock vs. CAS+retry) — not here any more.
|
|
118
118
|
ctx.strategy = ctx.plan.strategy_for(capabilities: ctx.repository.capabilities)
|
|
119
|
-
ctx.instance = step(:hydrate)
|
|
119
|
+
ctx.instance = step(:hydrate) do
|
|
120
120
|
if ctx.plan.complete_state? && ctx.plan.state_independent?
|
|
121
121
|
hydrate_complete_state(ctx.repository, ctx.aggregate, ctx.command, ctx.args, ctx.route, ctx.strategy)
|
|
122
122
|
elsif ctx.plan.complete_state?
|
|
@@ -126,11 +126,11 @@ module Hecks
|
|
|
126
126
|
else
|
|
127
127
|
hydrate_existing(ctx.repository, ctx.aggregate, ctx.command, ctx.args, ctx.route)
|
|
128
128
|
end
|
|
129
|
-
|
|
129
|
+
end
|
|
130
130
|
end
|
|
131
131
|
|
|
132
132
|
def step_enforce_givens(ctx)
|
|
133
|
-
step(:enforce_givens)
|
|
133
|
+
step(:enforce_givens) do
|
|
134
134
|
# STRUCTURAL, before the declared givens — the same ordering
|
|
135
135
|
# NotFound/AlreadyExists already get at hydration: "does the
|
|
136
136
|
# fact this command's corrects names even exist" is not a
|
|
@@ -142,7 +142,7 @@ module Hecks
|
|
|
142
142
|
ctx.correction_bindings = @rules.enforce_correction_target(ctx.instance, ctx.aggregate, ctx.command, domain: ctx.domain)
|
|
143
143
|
@rules.enforce_givens(ctx.instance, ctx.command, ctx.args, domain: ctx.domain,
|
|
144
144
|
declaring: ctx.aggregate, parent: ctx.instance, correction: ctx.correction_bindings)
|
|
145
|
-
|
|
145
|
+
end
|
|
146
146
|
end
|
|
147
147
|
|
|
148
148
|
def step_admissible_transition(ctx)
|
|
@@ -161,11 +161,17 @@ module Hecks
|
|
|
161
161
|
# arithmetic via Value#with, append builds a new array), never
|
|
162
162
|
# edit a held value in place.
|
|
163
163
|
ctx.old_state = ctx.instance.state.dup unless ctx.command.ensures.empty?
|
|
164
|
-
step(:apply_mutations)
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
164
|
+
step(:apply_mutations) do
|
|
165
|
+
# ONE UPDATE SET OVER THE PRE-DISPATCH STATE (C4.2, docs/
|
|
166
|
+
# semantics/bluebook-semantics.md): every effect's sources read
|
|
167
|
+
# `pre` — the state as it was before this command — and its
|
|
168
|
+
# target is written to the candidate; declaration order carries
|
|
169
|
+
# no meaning, and build refuses a field written twice.
|
|
170
|
+
pre = ctx.instance.state.dup
|
|
171
|
+
ctx.command.mutations.each do |mutation|
|
|
172
|
+
apply(ctx.instance, ctx.aggregate, mutation, ctx.args, pre)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
169
175
|
end
|
|
170
176
|
|
|
171
177
|
def step_advance_lifecycle(ctx)
|
|
@@ -188,11 +194,21 @@ module Hecks
|
|
|
188
194
|
# `step_locate_element`/`step_apply_mutations` mutate their OWN
|
|
189
195
|
# freshly-loaded copy — the only difference is WHICH already-in-
|
|
190
196
|
# memory record gets handed in.
|
|
197
|
+
#
|
|
198
|
+
# Reimplements the entity command pipeline's own order (givens,
|
|
199
|
+
# transition, mutations, ensures, emit) inline, deliberately — see
|
|
200
|
+
# the comment above on why this must run strictly between this
|
|
201
|
+
# command's own mutations and its ensures/invariants/save. Splitting
|
|
202
|
+
# it would scatter that exact ordering across method boundaries and
|
|
203
|
+
# force threading element/view/transition/old_element/settled
|
|
204
|
+
# through as parameters — the `with:` bug documented just below is
|
|
205
|
+
# exactly the kind of ordering/plumbing mistake that shape invites.
|
|
206
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
191
207
|
def step_delegate_to_entity(ctx)
|
|
192
208
|
delegation = ctx.command.mutations.find { |mutation| mutation.op == :delegate }
|
|
193
209
|
return unless delegation
|
|
194
210
|
|
|
195
|
-
step(:delegate_to_entity)
|
|
211
|
+
step(:delegate_to_entity) do
|
|
196
212
|
entity_name, _dot, command_name = delegation.target.to_s.rpartition(".")
|
|
197
213
|
entity = ctx.aggregate.entities.find { |e| e.hecks_name == entity_name } ||
|
|
198
214
|
raise(WiringError, "#{ctx.command.hecks_name} delegates_to #{entity_name}." \
|
|
@@ -227,23 +243,32 @@ module Hecks
|
|
|
227
243
|
transition = @rules.admissible_transition(entity, target_command, view)
|
|
228
244
|
|
|
229
245
|
old_element = target_command.ensures.empty? ? nil : element.dup
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
246
|
+
pre = element.dup # C4.2 — the update set reads the element as it was
|
|
247
|
+
target_command.mutations.each do |mutation|
|
|
248
|
+
EntityElement.apply_to_element(@rules, ctx.aggregate, entity, element, mutation, target_args, pre)
|
|
249
|
+
end
|
|
233
250
|
element[entity.lifecycle.field] = transition.target if transition
|
|
234
251
|
|
|
235
252
|
settled = Instance.new(aggregate: entity, id: view.id, state: element)
|
|
236
253
|
@rules.enforce_ensures(settled, target_command, target_args, old: old_element, domain: ctx.domain, parent: ctx.instance)
|
|
237
254
|
|
|
238
|
-
|
|
239
|
-
|
|
255
|
+
# NOT emitted here — C7.2 (docs/semantics/bluebook-semantics.md):
|
|
256
|
+
# a refused command records nothing, and the parent's own
|
|
257
|
+
# `ensures`/`enforce_invariants`/`save` steps still run after
|
|
258
|
+
# this one. The target's emission is parked and performed by
|
|
259
|
+
# `step_emit`, after the parent committed — where every other
|
|
260
|
+
# command's events are emitted too. (Before this, the entity
|
|
261
|
+
# leg's events were on the event log and in the adapter before
|
|
262
|
+
# the parent could refuse.)
|
|
263
|
+
ctx.pending_delegation = [target_command, target_args]
|
|
264
|
+
end
|
|
240
265
|
end
|
|
241
266
|
|
|
242
267
|
def step_enforce_ensures(ctx)
|
|
243
|
-
step(:enforce_ensures)
|
|
268
|
+
step(:enforce_ensures) do
|
|
244
269
|
@rules.enforce_ensures(ctx.instance, ctx.command, ctx.args, old: ctx.old_state,
|
|
245
270
|
domain: ctx.domain, parent: ctx.instance, correction: ctx.correction_bindings || {})
|
|
246
|
-
|
|
271
|
+
end
|
|
247
272
|
end
|
|
248
273
|
|
|
249
274
|
def step_enforce_invariants(ctx)
|
|
@@ -261,41 +286,49 @@ module Hecks
|
|
|
261
286
|
step(:save) do
|
|
262
287
|
@rules.resolve_state_references(ctx.domain, ctx.aggregate, ctx.instance.state)
|
|
263
288
|
seed_projected_fields(ctx)
|
|
264
|
-
ctx.persistence_outcome =
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
289
|
+
ctx.persistence_outcome = persist_instance(ctx)
|
|
290
|
+
raise_for_persistence_outcome!(ctx)
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def persist_instance(ctx)
|
|
295
|
+
if ctx.strategy == DependencyPlanning::ATOMIC_PUT
|
|
296
|
+
# A SECOND CREATION IS NOT A FRESH ONE — see
|
|
297
|
+
# hydrate_complete_state's own comment; the
|
|
298
|
+
# same refusal, on the same terms, for the
|
|
299
|
+
# complete-state path. `insert_only:` asks the
|
|
300
|
+
# ADAPTER to decide and refuse ATOMICALLY
|
|
301
|
+
# (never writing a `creates?` command over an
|
|
302
|
+
# identity that already names a record) rather
|
|
303
|
+
# than this interpreter reading the record
|
|
304
|
+
# first to check — a `repository.find` before
|
|
305
|
+
# every atomic_put would be exactly the read
|
|
306
|
+
# this strategy exists to skip.
|
|
307
|
+
ctx.repository.atomic_put(ctx.instance, insert_only: ctx.command.creates?)
|
|
308
|
+
else
|
|
309
|
+
# `expected_version:` is `ctx.instance.version` — nil for a
|
|
310
|
+
# brand-new record (never read from storage) or when the
|
|
311
|
+
# repository isn't CAS-capable, either of which falls straight
|
|
312
|
+
# through to a plain, unconditional save inside `AppendOnly#save`.
|
|
313
|
+
ctx.repository.save(ctx.instance, expected_version: ctx.instance.version)
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def raise_for_persistence_outcome!(ctx)
|
|
318
|
+
if ctx.persistence_outcome.status == :conflicted
|
|
319
|
+
raise(AlreadyExists, RefusalWording.render("AlreadyExists", "creating_duplicate",
|
|
320
|
+
command: ctx.command.hecks_name, aggregate: ctx.aggregate.hecks_name,
|
|
321
|
+
identity: identity_reading(ctx.aggregate),
|
|
322
|
+
offered: Rendering.describe(ctx.instance.id)))
|
|
323
|
+
elsif ctx.persistence_outcome.status == :stale
|
|
324
|
+
# NOT a `RefusalWording.render` call — this is not a declared
|
|
325
|
+
# vocabulary refusal, just a plain, informative message. See
|
|
326
|
+
# `Runtime::StaleWrite`'s own comment: caught by `#call`'s
|
|
327
|
+
# retry loop, re-raised only once retries are exhausted.
|
|
328
|
+
raise(StaleWrite,
|
|
329
|
+
"#{ctx.command.hecks_name} on #{ctx.aggregate.hecks_name} " \
|
|
330
|
+
"(#{identity_reading(ctx.aggregate)}: #{Rendering.describe(ctx.instance.id)}) lost a race — " \
|
|
331
|
+
"another write committed against this record after it was read")
|
|
299
332
|
end
|
|
300
333
|
end
|
|
301
334
|
|
|
@@ -334,9 +367,10 @@ module Hecks
|
|
|
334
367
|
|
|
335
368
|
# A DELEGATING COMMAND EMITS NOTHING OF ITS OWN (`CommandBuilder#build`'s
|
|
336
369
|
# own guard refuses declaring `emits` alongside `delegates_to`) — its
|
|
337
|
-
# result IS
|
|
338
|
-
#
|
|
339
|
-
# into `@rules.emit` for a command with no
|
|
370
|
+
# result IS the target entity command's own `emits`, parked by
|
|
371
|
+
# `step_delegate_to_entity` and emitted HERE, after save (C7.2), not
|
|
372
|
+
# a second, empty call into `@rules.emit` for a command with no
|
|
373
|
+
# announced events at all.
|
|
340
374
|
#
|
|
341
375
|
# `dry_run:` skips this too, same reasoning as `step_save` — nothing
|
|
342
376
|
# was committed, so `ctx.result` stays nil and `Dispatcher#dry_run?`
|
|
@@ -344,15 +378,20 @@ module Hecks
|
|
|
344
378
|
def step_emit(ctx)
|
|
345
379
|
return if ctx.dry_run
|
|
346
380
|
|
|
347
|
-
ctx.result = step(:emit)
|
|
348
|
-
# `ctx.
|
|
349
|
-
# and only when this command carries a
|
|
350
|
-
#
|
|
351
|
-
|
|
352
|
-
|
|
381
|
+
ctx.result = step(:emit) do
|
|
382
|
+
# `ctx.pending_delegation` is only ever set by
|
|
383
|
+
# `step_delegate_to_entity`, and only when this command carries a
|
|
384
|
+
# `:delegate` mutation.
|
|
385
|
+
if ctx.pending_delegation
|
|
386
|
+
target_command, target_args = ctx.pending_delegation
|
|
387
|
+
# The same dispatch, so the same correlation — a saga-driven
|
|
388
|
+
# door's events used to lose their stamp here.
|
|
389
|
+
next @rules.emit(target_command, ctx.domain, ctx.aggregate, ctx.instance, target_args, ctx.repository,
|
|
390
|
+
ctx.correlation)
|
|
391
|
+
end
|
|
353
392
|
|
|
354
393
|
@rules.emit(ctx.command, ctx.domain, ctx.aggregate, ctx.instance, ctx.args, ctx.repository, ctx.correlation)
|
|
355
|
-
|
|
394
|
+
end
|
|
356
395
|
end
|
|
357
396
|
|
|
358
397
|
def hydrate_existing(repository, aggregate, command, args, route = nil)
|
|
@@ -143,9 +143,12 @@ module Hecks
|
|
|
143
143
|
attrs = attrs.merge(parent: parent.state) if parent
|
|
144
144
|
attrs = attrs.merge(correction) unless correction.empty?
|
|
145
145
|
command.givens.each do |given|
|
|
146
|
-
next if Bluebook::Expression::Evaluator.
|
|
146
|
+
next if Bluebook::Expression::Evaluator.call_rule(given, state, attrs)
|
|
147
147
|
|
|
148
|
-
raise GivenNotMet
|
|
148
|
+
raise GivenNotMet.new(
|
|
149
|
+
"#{command.hecks_name} refused — #{given.description}",
|
|
150
|
+
detail: Bluebook::Expression::Evaluator.comparison_detail(given.canonical, state, attrs)
|
|
151
|
+
)
|
|
149
152
|
end
|
|
150
153
|
|
|
151
154
|
enforce_lifecycle_guard(declaring, command, subject) if declaring
|
|
@@ -177,6 +180,16 @@ module Hecks
|
|
|
177
180
|
# one, made deliberately: `as:` reads as "the instance being
|
|
178
181
|
# corrected," which is naturally the latest fact on record, not
|
|
179
182
|
# an arbitrary one.
|
|
183
|
+
# C9.2 (docs/semantics/bluebook-semantics.md) — a correction target
|
|
184
|
+
# is judged against the record's DURABLE history: the events the
|
|
185
|
+
# aggregate's own store recorded (`AppendOnly#events`), which
|
|
186
|
+
# survive a restart the way the Rust kernel's persisted
|
|
187
|
+
# `emitted_<event>` flag does. The in-process log is the fallback
|
|
188
|
+
# only for an adapter that records no readable history.
|
|
189
|
+
def correction_history(domain, aggregate)
|
|
190
|
+
@registry.repository(domain, aggregate).events || @registry.event_log
|
|
191
|
+
end
|
|
192
|
+
|
|
180
193
|
def enforce_correction_target(instance, aggregate, command, domain:)
|
|
181
194
|
bindings = {}
|
|
182
195
|
command.mutations.each do |mutation|
|
|
@@ -184,8 +197,8 @@ module Hecks
|
|
|
184
197
|
|
|
185
198
|
event_key = "#{domain}::#{aggregate.hecks_name}"
|
|
186
199
|
event_name = mutation.target.to_s
|
|
187
|
-
corrected =
|
|
188
|
-
event.name == event_name && event.aggregate == event_key && event.id == instance.id
|
|
200
|
+
corrected = correction_history(domain, aggregate).reverse.find do |event|
|
|
201
|
+
event.name == event_name && event.aggregate == event_key && event.id.to_s == instance.id.to_s
|
|
189
202
|
end
|
|
190
203
|
|
|
191
204
|
unless corrected
|
|
@@ -257,12 +270,17 @@ module Hecks
|
|
|
257
270
|
# one) wins right alongside it — a settled-record ensures can
|
|
258
271
|
# reference the correction target exactly as freely as a
|
|
259
272
|
# pre-mutation given already can.
|
|
260
|
-
|
|
273
|
+
# C2.3 (docs/semantics/bluebook-semantics.md) — an ensures reads
|
|
274
|
+
# the SETTLED STATE first: an argument that shares a field's
|
|
275
|
+
# name does not shadow the candidate here (it does in a given,
|
|
276
|
+
# C2.2), so `sets :note` + `ensures { note == ... }` judges what
|
|
277
|
+
# landed, and `old.<field>` remains the pre-state.
|
|
278
|
+
attrs = args.reject { |name, _| subject.key?(name) }.merge(dereference(domain, command, args))
|
|
261
279
|
attrs = attrs.merge(parent: parent.state) if parent
|
|
262
280
|
attrs = attrs.merge(correction) unless correction.empty?
|
|
263
281
|
attrs = attrs.merge(old: old)
|
|
264
282
|
command.ensures.each do |rule|
|
|
265
|
-
next if Bluebook::Expression::Evaluator.
|
|
283
|
+
next if Bluebook::Expression::Evaluator.call_rule(rule, state, attrs)
|
|
266
284
|
|
|
267
285
|
raise EnsuresNotMet, "#{command.hecks_name} refused — #{rule.description}"
|
|
268
286
|
end
|
|
@@ -291,7 +309,7 @@ module Hecks
|
|
|
291
309
|
state = GuardState.new(subject)
|
|
292
310
|
attrs = {}
|
|
293
311
|
aggregate.invariants.each do |invariant|
|
|
294
|
-
next if Bluebook::Expression::Evaluator.
|
|
312
|
+
next if Bluebook::Expression::Evaluator.call_rule(invariant, state, attrs)
|
|
295
313
|
|
|
296
314
|
raise InvariantViolation, "#{aggregate.hecks_name} refused — #{invariant.description}"
|
|
297
315
|
end
|
|
@@ -336,7 +354,7 @@ module Hecks
|
|
|
336
354
|
attrs = { parent: owner_instance.state }
|
|
337
355
|
|
|
338
356
|
entity.invariants.each do |invariant|
|
|
339
|
-
next if Bluebook::Expression::Evaluator.
|
|
357
|
+
next if Bluebook::Expression::Evaluator.call_rule(invariant, element_state, attrs)
|
|
340
358
|
|
|
341
359
|
raise InvariantViolation, "#{entity.hecks_name} refused — #{invariant.description}"
|
|
342
360
|
end
|
|
@@ -111,9 +111,7 @@ module Hecks
|
|
|
111
111
|
op = sign.positive? ? "increment" : "decrement"
|
|
112
112
|
current ||= 0
|
|
113
113
|
|
|
114
|
-
if current.is_a?(Value) && amount.is_a?(Value)
|
|
115
|
-
return arithmetic_value_object(current, amount, target, sign, op)
|
|
116
|
-
end
|
|
114
|
+
return arithmetic_value_object(current, amount, target, sign, op) if current.is_a?(Value) && amount.is_a?(Value)
|
|
117
115
|
|
|
118
116
|
# `current` genuinely absent (no declared default, never set) and
|
|
119
117
|
# `amount` arrives VO-wrapped — a real command argument typed the
|
|
@@ -145,10 +143,30 @@ module Hecks
|
|
|
145
143
|
op: op, target: target, offered: Rendering.describe(current))
|
|
146
144
|
end
|
|
147
145
|
|
|
148
|
-
current + (sign * amount)
|
|
146
|
+
bounded(current + (sign * amount), op, current, sign * amount, sign.positive? ? "+" : "-")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# C3.3/C3.4 — an effect's arithmetic is held to the same value
|
|
150
|
+
# model an expression's is: Integer is signed 64-bit, Float is
|
|
151
|
+
# finite. A result outside that is an evaluation FAULT (never a
|
|
152
|
+
# refusal, C8.3), worded as the Rust kernel's own generated
|
|
153
|
+
# `checked_add`/`checked_sub`/`checked_mul` word it.
|
|
154
|
+
INT64_RANGE = (-(2**63))..((2**63) - 1)
|
|
155
|
+
|
|
156
|
+
def bounded(result, oper, lhs, rhs, symbol)
|
|
157
|
+
if result.is_a?(Integer)
|
|
158
|
+
return result if INT64_RANGE.cover?(result)
|
|
159
|
+
|
|
160
|
+
raise Bluebook::Expression::EvaluationError,
|
|
161
|
+
"#{oper} overflowed: #{lhs} #{symbol} #{rhs.abs} does not fit in a 64-bit integer"
|
|
162
|
+
end
|
|
163
|
+
return result unless result.is_a?(Float) && !result.finite?
|
|
164
|
+
|
|
165
|
+
raise Bluebook::Expression::EvaluationError,
|
|
166
|
+
"#{oper} overflowed: #{lhs} #{symbol} #{rhs.abs} is not a finite number"
|
|
149
167
|
end
|
|
150
168
|
|
|
151
|
-
def arithmetic_value_object(current, amount, target, sign,
|
|
169
|
+
def arithmetic_value_object(current, amount, target, sign, oper)
|
|
152
170
|
current_fields = current.to_h
|
|
153
171
|
amount_fields = amount.to_h
|
|
154
172
|
# Widened from Integer to Numeric -- see #arithmetic's own
|
|
@@ -162,11 +180,12 @@ module Hecks
|
|
|
162
180
|
end
|
|
163
181
|
unless shared_numeric.size == 1
|
|
164
182
|
raise TypeMismatch,
|
|
165
|
-
RefusalWording.render("TypeMismatch", "arithmetic_shared_field", op:
|
|
183
|
+
RefusalWording.render("TypeMismatch", "arithmetic_shared_field", op: oper, target: target)
|
|
166
184
|
end
|
|
167
185
|
|
|
168
186
|
field = shared_numeric.first
|
|
169
|
-
current.with(field, current[field] + (sign * amount[field])
|
|
187
|
+
current.with(field, bounded(current[field] + (sign * amount[field]), oper,
|
|
188
|
+
current[field], amount[field], sign.positive? ? "+" : "-"))
|
|
170
189
|
end
|
|
171
190
|
|
|
172
191
|
# Not a bare `.find(...)&.sign || -1` — that silently answered
|
|
@@ -180,9 +199,9 @@ module Hecks
|
|
|
180
199
|
# raise is not a real runtime path yet — it is the same
|
|
181
200
|
# backstop one level down, in case a future caller reaches
|
|
182
201
|
# #sign_of directly for an op that was never meant to have one.
|
|
183
|
-
def sign_of(
|
|
184
|
-
MUTATION_OPS.find { |candidate| candidate.name ==
|
|
185
|
-
raise(WiringError, "no sign declared for mutation op #{
|
|
202
|
+
def sign_of(oper)
|
|
203
|
+
MUTATION_OPS.find { |candidate| candidate.name == oper.to_s }&.sign ||
|
|
204
|
+
raise(WiringError, "no sign declared for mutation op #{oper.inspect} — add one before calling #sign_of")
|
|
186
205
|
end
|
|
187
206
|
|
|
188
207
|
# Vendored addition, not (yet) upstream hecks (migration plan
|
|
@@ -196,7 +215,9 @@ module Hecks
|
|
|
196
215
|
current ||= 0
|
|
197
216
|
|
|
198
217
|
if current.is_a?(Value) && amount.is_a?(Value)
|
|
199
|
-
return combine_value_object(current, amount, target, "multiply")
|
|
218
|
+
return combine_value_object(current, amount, target, "multiply") do |c, a|
|
|
219
|
+
bounded(c * a, "multiply", c, a, "*")
|
|
220
|
+
end
|
|
200
221
|
end
|
|
201
222
|
|
|
202
223
|
# Same absent-`current`, VO-wrapped-`amount` gap as `#arithmetic`
|
|
@@ -209,7 +230,7 @@ module Hecks
|
|
|
209
230
|
offered: Rendering.describe(current.is_a?(Numeric) ? amount : current))
|
|
210
231
|
end
|
|
211
232
|
|
|
212
|
-
current * amount
|
|
233
|
+
bounded(current * amount, "multiply", current, amount, "*")
|
|
213
234
|
end
|
|
214
235
|
|
|
215
236
|
# Vendored addition, not (yet) upstream hecks (migration plan
|
|
@@ -270,7 +291,7 @@ module Hecks
|
|
|
270
291
|
fields[numeric_fields.first]
|
|
271
292
|
end
|
|
272
293
|
|
|
273
|
-
def combine_value_object(current, amount, target,
|
|
294
|
+
def combine_value_object(current, amount, target, oper)
|
|
274
295
|
current_fields = current.to_h
|
|
275
296
|
amount_fields = amount.to_h
|
|
276
297
|
shared_numeric = current_fields.keys.select do |field|
|
|
@@ -278,7 +299,7 @@ module Hecks
|
|
|
278
299
|
end
|
|
279
300
|
unless shared_numeric.size == 1
|
|
280
301
|
raise TypeMismatch,
|
|
281
|
-
RefusalWording.render("TypeMismatch", "arithmetic_shared_field", op:
|
|
302
|
+
RefusalWording.render("TypeMismatch", "arithmetic_shared_field", op: oper, target: target)
|
|
282
303
|
end
|
|
283
304
|
|
|
284
305
|
field = shared_numeric.first
|