hecks 0.3.0 → 1.0.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/d1.rb +38 -14
- data/lib/hecks/adapters/driven/governance_authorization.rb +41 -2
- data/lib/hecks/adapters/driven/heki/journal.rb +7 -2
- data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
- data/lib/hecks/adapters/driven/heki/snapshot.rb +31 -4
- data/lib/hecks/adapters/driven/heki.rb +40 -9
- data/lib/hecks/adapters/driven/lambda.rb +16 -2
- data/lib/hecks/adapters/driven/postgres/codec.rb +8 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +45 -6
- data/lib/hecks/adapters/driven/postgres.rb +76 -29
- data/lib/hecks/adapters/driven/postgres_era.adapter +11 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +1 -1
- data/lib/hecks/adapters/driven/sqlite/codec.rb +17 -0
- data/lib/hecks/adapters/driven/sqlite/projection.rb +76 -9
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +17 -5
- data/lib/hecks/adapters/driven/sqlite.rb +30 -9
- data/lib/hecks/adapters/driven.rb +19 -1
- data/lib/hecks/behaviors/dsl.rb +29 -0
- data/lib/hecks/behaviors/expectations.rb +62 -2
- data/lib/hecks/bluebook/assembly/contracts.rb +36 -7
- data/lib/hecks/bluebook/assembly/marks.rb +4 -3
- data/lib/hecks/bluebook/assembly.rb +14 -1
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +18 -1
- data/lib/hecks/bluebook/behaviour/process_manager.rb +14 -1
- data/lib/hecks/bluebook/behaviour/value_object.rb +13 -7
- data/lib/hecks/bluebook/chapter.rb +21 -11
- data/lib/hecks/bluebook/command.rb +1 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +117 -5
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +21 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +71 -2
- data/lib/hecks/bluebook/dsl/command_builder.rb +144 -3
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +49 -7
- data/lib/hecks/bluebook/dsl/entity_builder.rb +129 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +18 -3
- data/lib/hecks/bluebook/dsl/port_builder.rb +12 -3
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +109 -10
- data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -0
- data/lib/hecks/bluebook/dsl/word_gate.rb +9 -2
- data/lib/hecks/bluebook/dsl/world_builder.rb +44 -4
- data/lib/hecks/bluebook/expression/ast_json.rb +149 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +71 -3
- data/lib/hecks/bluebook/expression/evaluator.rb +50 -7
- data/lib/hecks/bluebook/expression/projection.json +48 -0
- data/lib/hecks/bluebook/expression/resolver.rb +161 -10
- data/lib/hecks/bluebook/expression.rb +1 -0
- data/lib/hecks/bluebook/hexagon.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +78 -16
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +4 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +14 -4
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +42 -3
- data/lib/hecks/bluebook/meta_validator/shapes.rb +30 -10
- data/lib/hecks/bluebook/meta_validator.rb +103 -13
- data/lib/hecks/bluebook/model_check.rb +147 -6
- data/lib/hecks/bluebook/pattern_subset.rb +66 -2
- data/lib/hecks/bluebook/process_manager.rb +53 -11
- data/lib/hecks/bluebook/project_register.rb +25 -0
- data/lib/hecks/bluebook/value_object.rb +19 -2
- data/lib/hecks/doc/reference.rb +22 -1
- data/lib/hecks/facade/cli_door.rb +6 -3
- data/lib/hecks/facade/json_door.rb +16 -4
- data/lib/hecks/forms/app.rb +47 -6
- data/lib/hecks/forms/command_form_renderer.rb +1 -1
- data/lib/hecks/forms/field_renderer.rb +11 -4
- data/lib/hecks/forms/html.rb +31 -0
- data/lib/hecks/forms/params.rb +30 -1
- data/lib/hecks/forms/port_argument.rb +46 -0
- data/lib/hecks/forms/record_renderer.rb +6 -2
- data/lib/hecks/forms/record_table.rb +6 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +19 -19
- data/lib/hecks/framework/bluebook/governance.bluebook +26 -11
- data/lib/hecks/framework/bluebook/identity.bluebook +2 -2
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +3 -2
- data/lib/hecks/fuzzing/isolated_boot.rb +212 -18
- data/lib/hecks/fuzzing/properties.rb +52 -6
- data/lib/hecks/fuzzing/replay.rb +51 -18
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +28 -2
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +71 -6
- data/lib/hecks/fuzzing/sequence_generator.rb +5 -4
- data/lib/hecks/fuzzing/value_generator.rb +25 -2
- data/lib/hecks/grammar/evolve.rb +33 -0
- data/lib/hecks/grammar/expression.bluebook +8 -5
- data/lib/hecks/grammar/expression_operators.json +497 -1
- data/lib/hecks/language/bluebook/aggregate.bluebook +12 -12
- data/lib/hecks/language/bluebook/bluebook.bluebook +3 -3
- data/lib/hecks/language/bluebook/command.bluebook +42 -8
- data/lib/hecks/language/bluebook/entity.bluebook +86 -10
- data/lib/hecks/language/bluebook/policy.bluebook +21 -4
- data/lib/hecks/language/bluebook/process_manager.bluebook +135 -18
- data/lib/hecks/language/bluebook/projection.bluebook +6 -6
- data/lib/hecks/language/bluebook/query.bluebook +4 -4
- data/lib/hecks/language/bluebook/shape.bluebook +6 -6
- data/lib/hecks/language/bluebook/syntax.bluebook +12 -11
- data/lib/hecks/language/bluebook/vocabulary.bluebook +22 -7
- data/lib/hecks/language/hecksagon/domain_port.bluebook +25 -1
- data/lib/hecks/language/oidc.json +20 -0
- data/lib/hecks/language/port.bluebook +30 -2
- data/lib/hecks/naming.rb +54 -1
- data/lib/hecks/ports/access_control.port +7 -2
- data/lib/hecks/ports/access_control.rb +1 -1
- data/lib/hecks/ports/agent.port +6 -2
- data/lib/hecks/ports/agent.rb +1 -1
- data/lib/hecks/ports/authentication.port +4 -2
- data/lib/hecks/ports/authentication.rb +1 -1
- data/lib/hecks/ports/authorization.port +5 -2
- data/lib/hecks/ports/authorization.rb +14 -11
- data/lib/hecks/ports/clock.port +3 -2
- data/lib/hecks/ports/clock.rb +1 -1
- data/lib/hecks/ports/extraction.port +3 -2
- data/lib/hecks/ports/extraction.rb +1 -1
- data/lib/hecks/ports/identity_assignment.port +3 -2
- data/lib/hecks/ports/identity_assignment.rb +1 -1
- data/lib/hecks/ports/identity_generation.port +3 -2
- data/lib/hecks/ports/identity_generation.rb +1 -1
- data/lib/hecks/ports/identity_resolution.port +3 -2
- data/lib/hecks/ports/identity_resolution.rb +1 -1
- data/lib/hecks/ports/persistence/append_only.rb +40 -4
- data/lib/hecks/ports/persistence/execution.rb +6 -1
- data/lib/hecks/ports/persistence/plugin.rb +54 -0
- data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_check.rb +41 -8
- data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_guard.rb +24 -56
- data/lib/hecks/ports/persistence/{lineage.rb → plugins/era/lineage.rb} +31 -4
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/era_store.rb +3 -3
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/head_compiler.rb +59 -10
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/mint_transaction.rb +2 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/provisioning.rb +29 -1
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/tail_merge.rb +11 -4
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/transform_installer.rb +20 -0
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage.rb +1 -1
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/coverage_check.rb +5 -5
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/era_resolver.rb +5 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/merge_coordinator.rb +2 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/minter.rb +4 -4
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager.rb +2 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era.rb +116 -29
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/approval_digest.rb +1 -1
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/layer_one.rb +14 -5
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/layer_two.rb +31 -6
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/unfed_report.rb +5 -1
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/reattest.rb +3 -3
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/differ.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
- data/lib/hecks/ports/persistence.rb +1 -1
- data/lib/hecks/ports/projection.rb +20 -0
- data/lib/hecks/projections/diagrams.rb +230 -1
- data/lib/hecks/projections/shape.rb +15 -2
- data/lib/hecks/projector/cli_projector.rb +10 -1
- data/lib/hecks/projector/exporter.rb +19 -2
- data/lib/hecks/query_ir.rb +19 -0
- data/lib/hecks/query_specification/common/null_policy.rb +13 -1
- data/lib/hecks/query_specification/field_path.rb +20 -2
- data/lib/hecks/query_specification/hop_path.rb +7 -5
- data/lib/hecks/runtime/aggregate_lock.rb +45 -0
- data/lib/hecks/runtime/boot_gates.rb +41 -0
- data/lib/hecks/runtime/caller.rb +19 -3
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +13 -2
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -0
- data/lib/hecks/runtime/command_interpreter.rb +165 -34
- data/lib/hecks/runtime/command_rules/admissibility.rb +101 -16
- data/lib/hecks/runtime/command_rules/arithmetic.rb +40 -4
- data/lib/hecks/runtime/command_rules/authorization.rb +2 -1
- data/lib/hecks/runtime/command_rules/references.rb +27 -19
- data/lib/hecks/runtime/dependency_planning.rb +66 -5
- data/lib/hecks/runtime/dispatcher.rb +19 -4
- data/lib/hecks/runtime/entity_interpreter.rb +92 -14
- data/lib/hecks/runtime/errors.rb +22 -0
- data/lib/hecks/runtime/identity.rb +30 -2
- data/lib/hecks/runtime/instance.rb +59 -4
- data/lib/hecks/runtime/interpreting.rb +21 -0
- data/lib/hecks/runtime/loader.rb +59 -18
- data/lib/hecks/runtime/query_interpreter.rb +36 -4
- data/lib/hecks/runtime/reaction_invocation.rb +9 -1
- data/lib/hecks/runtime/read_model_interpreter.rb +89 -5
- data/lib/hecks/runtime/refusal_wording.rb +2 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +75 -3
- data/lib/hecks/runtime/registry/verification.rb +88 -0
- data/lib/hecks/runtime/registry.rb +69 -8
- data/lib/hecks/runtime/saga_interpreter.rb +215 -13
- data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
- data/lib/hecks/runtime/value/admission.rb +19 -1
- data/lib/hecks/runtime/value/coercion.rb +75 -10
- data/lib/hecks/runtime.rb +17 -5
- data/lib/hecks/storehouse.rb +632 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +6 -1
- data/lib/hecks.rb +28 -3
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
- metadata +49 -37
- /data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_guard/shape_diff.rb +0 -0
- /data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_tamper.rb +0 -0
- /data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/field_cache.rb +0 -0
- /data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/resumable_backfill.rb +0 -0
- /data/lib/hecks/{runtime → ports/persistence/plugins/era}/storage_shape.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/rule_compiler.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/renderer.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/writer.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold.rb +0 -0
- /data/lib/hecks/{translation.rb → ports/persistence/plugins/era/translation.rb} +0 -0
|
@@ -9,6 +9,7 @@ require_relative "../ports/persistence/execution"
|
|
|
9
9
|
require_relative "instance"
|
|
10
10
|
require_relative "refusal_wording"
|
|
11
11
|
require_relative "entity_element"
|
|
12
|
+
require_relative "rebuild_sweep"
|
|
12
13
|
|
|
13
14
|
module Hecks
|
|
14
15
|
module Runtime
|
|
@@ -31,13 +32,23 @@ module Hecks
|
|
|
31
32
|
# Runtime::RefusalWording's own doc comment gives the same reason.
|
|
32
33
|
DISPATCH_ORDER = Hecks::Vocabulary.symbols("AggregateDispatchOrder")
|
|
33
34
|
|
|
35
|
+
# A LAST-RESORT SAFETY VALVE, NOT THE NORMAL OUTCOME PATH — see
|
|
36
|
+
# `Runtime::StaleWrite`'s own comment. Two concurrent writers
|
|
37
|
+
# against one aggregate resolve through exactly one retry in the
|
|
38
|
+
# ordinary case (the loser's retried hydrate reads the winner's now-
|
|
39
|
+
# committed state and its own `given` refuses for real, raising
|
|
40
|
+
# `GivenNotMet`, not `StaleWrite`) — this cap exists for pathological
|
|
41
|
+
# contention (many concurrent writers on one hot aggregate), not the
|
|
42
|
+
# two-writer case.
|
|
43
|
+
MAX_STALE_WRITE_RETRIES = 5
|
|
44
|
+
|
|
34
45
|
# EVERY CROSS-STEP LOCAL `call` used to thread through its own literal
|
|
35
46
|
# sequence, held in one place now that the sequence is data-driven —
|
|
36
47
|
# `result` and `transition`/`old_state` default to nil until the step
|
|
37
48
|
# that sets them runs, same as they were unset locals before that point.
|
|
38
49
|
Context = Struct.new(:domain, :aggregate, :command, :args, :repository, :instance, :transition, :old_state,
|
|
39
50
|
:result, :correlation, :route, :plan, :strategy, :persistence_outcome, :delegated_events,
|
|
40
|
-
:dry_run)
|
|
51
|
+
:dry_run, :correction_bindings)
|
|
41
52
|
|
|
42
53
|
def initialize(registry, rules:)
|
|
43
54
|
@registry = registry
|
|
@@ -49,14 +60,34 @@ module Hecks
|
|
|
49
60
|
# dispatch would (givens checked, mutations applied to `ctx.instance`
|
|
50
61
|
# in memory); `step_save`/`step_emit` are the only two that read this
|
|
51
62
|
# flag, each skipping its own real work — see their own comments.
|
|
63
|
+
# RETRIES THE WHOLE METHOD BODY on `StaleWrite` — a fresh `ctx`, a
|
|
64
|
+
# fresh `step_hydrate` re-reading current state, so `enforce_givens`
|
|
65
|
+
# re-evaluates against reality rather than the snapshot that just
|
|
66
|
+
# went stale. See `MAX_STALE_WRITE_RETRIES`/`Runtime::StaleWrite`
|
|
67
|
+
# for why exhaustion is a pathological-contention signal, not the
|
|
68
|
+
# expected shape of a two-writer race.
|
|
52
69
|
def call(domain, aggregate, command, args, correlation = nil, route: nil, dry_run: false)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
70
|
+
attempt = 0
|
|
71
|
+
begin
|
|
72
|
+
ctx = Context.new(domain, aggregate, command, args)
|
|
73
|
+
ctx.correlation = correlation
|
|
74
|
+
ctx.route = route
|
|
75
|
+
ctx.dry_run = dry_run
|
|
76
|
+
ctx.plan = DependencyPlanning::Analyzer.call(aggregate: aggregate, command: command)
|
|
77
|
+
# RESOLVED HERE, ONCE, BEFORE HYDRATION — `Registry#repository`
|
|
78
|
+
# memoizes, so this and `step_hydrate`'s own read of `ctx.repository`
|
|
79
|
+
# (no second fetch there any more) always name the same instance;
|
|
80
|
+
# the isolation decision below (lock vs. CAS+retry) needs the
|
|
81
|
+
# repository's capabilities before a single step runs.
|
|
82
|
+
ctx.repository = @registry.repository(domain, aggregate)
|
|
83
|
+
lock_id = Identity.best_effort(aggregate, args, route, reference_key: reference_key(command))
|
|
84
|
+
run_dispatch_order_with_isolation(DISPATCH_ORDER, ctx, lock_key_id: lock_id)
|
|
85
|
+
[ctx.instance, ctx.result, ctx.plan, ctx.persistence_outcome]
|
|
86
|
+
rescue StaleWrite
|
|
87
|
+
attempt += 1
|
|
88
|
+
retry if attempt < MAX_STALE_WRITE_RETRIES
|
|
89
|
+
raise
|
|
90
|
+
end
|
|
60
91
|
end
|
|
61
92
|
|
|
62
93
|
private
|
|
@@ -82,7 +113,8 @@ module Hecks
|
|
|
82
113
|
end
|
|
83
114
|
|
|
84
115
|
def step_hydrate(ctx)
|
|
85
|
-
ctx.repository
|
|
116
|
+
# `ctx.repository` is resolved once, in `#call`, before the
|
|
117
|
+
# isolation decision (lock vs. CAS+retry) — not here any more.
|
|
86
118
|
ctx.strategy = ctx.plan.strategy_for(capabilities: ctx.repository.capabilities)
|
|
87
119
|
ctx.instance = step(:hydrate) {
|
|
88
120
|
if ctx.plan.complete_state? && ctx.plan.state_independent?
|
|
@@ -99,8 +131,17 @@ module Hecks
|
|
|
99
131
|
|
|
100
132
|
def step_enforce_givens(ctx)
|
|
101
133
|
step(:enforce_givens) {
|
|
134
|
+
# STRUCTURAL, before the declared givens — the same ordering
|
|
135
|
+
# NotFound/AlreadyExists already get at hydration: "does the
|
|
136
|
+
# fact this command's corrects names even exist" is not a
|
|
137
|
+
# domain rule an author wrote, it is a precondition for the
|
|
138
|
+
# domain rules to mean anything at all. Also locates the
|
|
139
|
+
# correction target itself, if `as:` named one — carried on
|
|
140
|
+
# `ctx` so `step_enforce_ensures` (the settled-record half)
|
|
141
|
+
# can bind the SAME name too, not just this pre-mutation half.
|
|
142
|
+
ctx.correction_bindings = @rules.enforce_correction_target(ctx.instance, ctx.aggregate, ctx.command, domain: ctx.domain)
|
|
102
143
|
@rules.enforce_givens(ctx.instance, ctx.command, ctx.args, domain: ctx.domain,
|
|
103
|
-
declaring: ctx.aggregate, parent: ctx.instance)
|
|
144
|
+
declaring: ctx.aggregate, parent: ctx.instance, correction: ctx.correction_bindings)
|
|
104
145
|
}
|
|
105
146
|
end
|
|
106
147
|
|
|
@@ -201,7 +242,7 @@ module Hecks
|
|
|
201
242
|
def step_enforce_ensures(ctx)
|
|
202
243
|
step(:enforce_ensures) {
|
|
203
244
|
@rules.enforce_ensures(ctx.instance, ctx.command, ctx.args, old: ctx.old_state,
|
|
204
|
-
domain: ctx.domain, parent: ctx.instance)
|
|
245
|
+
domain: ctx.domain, parent: ctx.instance, correction: ctx.correction_bindings || {})
|
|
205
246
|
}
|
|
206
247
|
end
|
|
207
248
|
|
|
@@ -219,9 +260,10 @@ module Hecks
|
|
|
219
260
|
|
|
220
261
|
step(:save) do
|
|
221
262
|
@rules.resolve_state_references(ctx.domain, ctx.aggregate, ctx.instance.state)
|
|
263
|
+
seed_projected_fields(ctx)
|
|
222
264
|
ctx.persistence_outcome = if ctx.strategy == DependencyPlanning::ATOMIC_PUT
|
|
223
265
|
# A SECOND CREATION IS NOT A FRESH ONE — see
|
|
224
|
-
#
|
|
266
|
+
# hydrate_complete_state's own comment; the
|
|
225
267
|
# same refusal, on the same terms, for the
|
|
226
268
|
# complete-state path. `insert_only:` asks the
|
|
227
269
|
# ADAPTER to decide and refuse ATOMICALLY
|
|
@@ -233,18 +275,63 @@ module Hecks
|
|
|
233
275
|
# this strategy exists to skip.
|
|
234
276
|
ctx.repository.atomic_put(ctx.instance, insert_only: ctx.command.creates?)
|
|
235
277
|
else
|
|
236
|
-
ctx.
|
|
237
|
-
|
|
278
|
+
# `expected_version:` is `ctx.instance.version` — nil for a
|
|
279
|
+
# brand-new record (never read from storage) or when the
|
|
280
|
+
# repository isn't CAS-capable, either of which falls straight
|
|
281
|
+
# through to a plain, unconditional save inside `AppendOnly#save`.
|
|
282
|
+
ctx.repository.save(ctx.instance, expected_version: ctx.instance.version)
|
|
238
283
|
end
|
|
239
284
|
if ctx.persistence_outcome.status == :conflicted
|
|
240
285
|
raise(AlreadyExists, RefusalWording.render("AlreadyExists", "creating_duplicate",
|
|
241
286
|
command: ctx.command.hecks_name, aggregate: ctx.aggregate.hecks_name,
|
|
242
287
|
identity: identity_reading(ctx.aggregate),
|
|
243
288
|
offered: Rendering.describe(ctx.instance.id)))
|
|
289
|
+
elsif ctx.persistence_outcome.status == :stale
|
|
290
|
+
# NOT a `RefusalWording.render` call — this is not a declared
|
|
291
|
+
# vocabulary refusal, just a plain, informative message. See
|
|
292
|
+
# `Runtime::StaleWrite`'s own comment: caught by `#call`'s
|
|
293
|
+
# retry loop, re-raised only once retries are exhausted.
|
|
294
|
+
raise(StaleWrite,
|
|
295
|
+
"#{ctx.command.hecks_name} on #{ctx.aggregate.hecks_name} " \
|
|
296
|
+
"(#{identity_reading(ctx.aggregate)}: #{Rendering.describe(ctx.instance.id)}) lost a race — " \
|
|
297
|
+
"another write committed against this record after it was read")
|
|
244
298
|
end
|
|
245
299
|
end
|
|
246
300
|
end
|
|
247
301
|
|
|
302
|
+
# THE ONE-TIME, SYNCHRONOUS HALF OF `projects` (S12, ADR 0025) —
|
|
303
|
+
# `RebuildSweep` (`runtime/rebuild_sweep.rb`) is deliberately the
|
|
304
|
+
# ONLY thing that keeps a projected field current against a
|
|
305
|
+
# target that changes AFTER this record was written — no reactive
|
|
306
|
+
# `Policy#for_each` keeping it live in real time, that stays
|
|
307
|
+
# deferred, same as that file's own header explains. But without
|
|
308
|
+
# SOME synchronous population, a projected field never gets an
|
|
309
|
+
# INITIAL value at all until an operator remembers to run a
|
|
310
|
+
# sweep by hand — every acting command reading it (`given
|
|
311
|
+
# ("customer is active") { customer_status == "active" }`, say)
|
|
312
|
+
# would refuse a freshly created, genuinely active record for no
|
|
313
|
+
# real reason, which is not the eventual-consistency tradeoff the
|
|
314
|
+
# ADR accepts, just a bug. So: every time a record with `projects`
|
|
315
|
+
# fields is about to save — creating or acting, either can be the
|
|
316
|
+
# first time a referenced record resolves — read each one ONCE,
|
|
317
|
+
# here, using the exact same `RebuildSweep.remote_value` a sweep
|
|
318
|
+
# itself would compute. This is still eventually consistent in
|
|
319
|
+
# the sense the ADR means: a change on the TARGET side after this
|
|
320
|
+
# save still needs a sweep to reach here. It is only ever
|
|
321
|
+
# SYNCHRONOUS with THIS record's own write, never a live read
|
|
322
|
+
# triggered by a `given`/`ensures`/`invariant` mid-dispatch — the
|
|
323
|
+
# boundary rule those enforce holds exactly as before.
|
|
324
|
+
def seed_projected_fields(ctx)
|
|
325
|
+
return if ctx.aggregate.projected_fields.empty?
|
|
326
|
+
|
|
327
|
+
ctx.aggregate.projected_fields.each do |field|
|
|
328
|
+
value = RebuildSweep.remote_value(@registry, ctx.domain, ctx.aggregate, ctx.instance.state, field)
|
|
329
|
+
next if value.nil?
|
|
330
|
+
|
|
331
|
+
ctx.instance.state[field.name] = value
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
248
335
|
# A DELEGATING COMMAND EMITS NOTHING OF ITS OWN (`CommandBuilder#build`'s
|
|
249
336
|
# own guard refuses declaring `emits` alongside `delegates_to`) — its
|
|
250
337
|
# result IS whatever `step_delegate_to_entity` already collected from
|
|
@@ -292,19 +379,61 @@ module Hecks
|
|
|
292
379
|
found.dup
|
|
293
380
|
end
|
|
294
381
|
|
|
295
|
-
#
|
|
296
|
-
#
|
|
297
|
-
#
|
|
298
|
-
#
|
|
299
|
-
#
|
|
382
|
+
# WAVE 8 (equivalence-gap plan, item 1.6) — NOT done, and this
|
|
383
|
+
# comment now says precisely how far it got, corrected from an
|
|
384
|
+
# EARLIER version of itself that briefly (same PR, never released)
|
|
385
|
+
# claimed the inventory was empty and deleted this method outright.
|
|
386
|
+
# A full corpus audit (`DependencyPlanning::Analyzer.call` against
|
|
387
|
+
# every `creates?`-true command in all 5 EXAMPLE domains — banking,
|
|
388
|
+
# pizzas, chess, compliance, roster) found and fixed 4 real
|
|
389
|
+
# authoring bugs (`Statement.Generate`, `CreatePizza`, `Chess::
|
|
390
|
+
# Game.Start`, `Roster.Open` — each declared an attribute and
|
|
391
|
+
# never `sets` it, so the field silently stayed nil on every
|
|
392
|
+
# created record regardless of what a caller sent), plus a real
|
|
393
|
+
# `DependencyPlanning::Analyzer` bug for ENTITY-owned commands
|
|
394
|
+
# (`root_aggregate:`, that class's own header) that surfaced one
|
|
395
|
+
# more live corpus bug of its own (`payment_cards.bluebook`'s own
|
|
396
|
+
# `Withdrawal.Dispute`, which never actually checked whether the
|
|
397
|
+
# card was retired) — all real, all kept.
|
|
398
|
+
#
|
|
399
|
+
# DELETING THIS METHOD ON THAT BASIS TURNED OUT TO BE WRONG,
|
|
400
|
+
# caught by running the full suite rather than trusting the
|
|
401
|
+
# audit's own scope: the 5 example domains are nowhere near the
|
|
402
|
+
# WHOLE inventory of `creates?`-true commands this fallback
|
|
403
|
+
# actually carries. Dozens of separate, purpose-built spec
|
|
404
|
+
# fixtures across the suite (`spec/fixtures/*.bluebook`, and
|
|
405
|
+
# inline `Hecks.bluebook` blocks declared directly inside
|
|
406
|
+
# individual spec files — governance, mutation ops, ports,
|
|
407
|
+
# routing, tenant isolation, sagas, and more) declare their OWN
|
|
408
|
+
# small `creates?`-true commands the same incomplete way, and
|
|
409
|
+
# rely on this exact fallback to create anything at all. Deleting
|
|
410
|
+
# it produced 218 failures across specs with nothing to do with
|
|
411
|
+
# Wave 8's own subject — confirmed, not guessed, by actually
|
|
412
|
+
# running `bundle exec rspec` after the deletion, not merely by
|
|
413
|
+
# extrapolating from the one audited inventory. `spec/fixtures/
|
|
414
|
+
# till.bluebook`'s own `Till.OpenTill` (missing `sets :number`,
|
|
415
|
+
# the identical bug shape as the 4 fixed above) was fixed
|
|
416
|
+
# alongside the four real corpus ones as one instance of this — but
|
|
417
|
+
# it was one of many, not the last one, and finding the rest is
|
|
418
|
+
# real, separate, much larger follow-up work this pass does not
|
|
419
|
+
# attempt: a suite-wide audit of every declared bluebook fixture,
|
|
420
|
+
# not just the 5 example domains the plan's own text named.
|
|
421
|
+
#
|
|
422
|
+
# Transitional compatibility for live source that has not yet
|
|
423
|
+
# acquired explicit effects, same as it always was — deliberately
|
|
424
|
+
# isolated from the normal routing and planning path so
|
|
425
|
+
# `reference_to` no longer chooses how a migrated command hydrates
|
|
426
|
+
# or persists. A real, future Wave 8 removes this once THAT wider
|
|
427
|
+
# inventory is empty, not before.
|
|
300
428
|
#
|
|
301
|
-
# `ctx.plan.complete_state?` already claimed every command whose
|
|
302
|
-
# fully resolved in the two branches above `step_hydrate`
|
|
303
|
-
# reaching this check already means the plan is
|
|
304
|
-
# un-migrated command still routes here on
|
|
305
|
-
# the old `hydrate` did, regardless
|
|
306
|
-
# mutations (`write_set`).
|
|
307
|
-
#
|
|
429
|
+
# `ctx.plan.complete_state?` already claimed every command whose
|
|
430
|
+
# plan is fully resolved in the two branches above `step_hydrate`
|
|
431
|
+
# tries first, so reaching this check already means the plan is
|
|
432
|
+
# incomplete — an un-migrated command still routes here on
|
|
433
|
+
# `creates?` alone, the same as the old `hydrate` did, regardless
|
|
434
|
+
# of whether it happens to have any mutations (`write_set`).
|
|
435
|
+
# Requiring an EMPTY write_set here refused every un-migrated
|
|
436
|
+
# creating command that sets even one field.
|
|
308
437
|
def legacy_implicit_creation?(ctx)
|
|
309
438
|
ctx.route.nil? && ctx.command.creates?
|
|
310
439
|
end
|
|
@@ -321,7 +450,7 @@ module Hecks
|
|
|
321
450
|
offered: Rendering.describe(id)))
|
|
322
451
|
end
|
|
323
452
|
|
|
324
|
-
Instance.new(aggregate: aggregate, id: id)
|
|
453
|
+
Instance.new(aggregate: aggregate, id: id, args: args)
|
|
325
454
|
end
|
|
326
455
|
|
|
327
456
|
def hydrate_complete_state(repository, aggregate, command, args, route, strategy)
|
|
@@ -337,9 +466,11 @@ module Hecks
|
|
|
337
466
|
aggregate: aggregate.hecks_name,
|
|
338
467
|
identity: identity_reading(aggregate)))
|
|
339
468
|
|
|
340
|
-
# A SECOND CREATION IS NOT A FRESH ONE —
|
|
341
|
-
#
|
|
342
|
-
#
|
|
469
|
+
# A SECOND CREATION IS NOT A FRESH ONE — `creates?` on an identity
|
|
470
|
+
# a record already exists under refuses (`AlreadyExists`) rather
|
|
471
|
+
# than silently overwriting it, the same refusal `hydrate_prior_
|
|
472
|
+
# or_initial`'s own body gives for its own complete-but-state-
|
|
473
|
+
# dependent case, below. ONLY when `strategy` will NOT be ATOMIC_PUT:
|
|
343
474
|
# an atomic-put-capable adapter enforces this itself, atomically,
|
|
344
475
|
# via `insert_only:` in step_save (no read here, no race with the
|
|
345
476
|
# write) — but `strategy_for` already fell back to a plain `save`
|
|
@@ -355,7 +486,7 @@ module Hecks
|
|
|
355
486
|
offered: Rendering.describe(id)))
|
|
356
487
|
end
|
|
357
488
|
|
|
358
|
-
Instance.new(aggregate: aggregate, id: id)
|
|
489
|
+
Instance.new(aggregate: aggregate, id: id, args: args)
|
|
359
490
|
end
|
|
360
491
|
|
|
361
492
|
# A complete command may still depend on prior state: lifecycle guards
|
|
@@ -377,8 +508,8 @@ module Hecks
|
|
|
377
508
|
identity: identity_reading(aggregate)))
|
|
378
509
|
found = repository.find(id)
|
|
379
510
|
|
|
380
|
-
# A SECOND CREATION IS NOT A FRESH ONE — see
|
|
381
|
-
#
|
|
511
|
+
# A SECOND CREATION IS NOT A FRESH ONE — see hydrate_complete_
|
|
512
|
+
# state's own comment; the same refusal, on the same terms, for
|
|
382
513
|
# a complete-but-state-dependent command (one with a `given`
|
|
383
514
|
# reading its own prior state, which is what routes here instead
|
|
384
515
|
# of hydrate_complete_state). Gated on `command.creates?`: a
|
|
@@ -398,7 +529,7 @@ module Hecks
|
|
|
398
529
|
offered: Rendering.describe(id)))
|
|
399
530
|
end
|
|
400
531
|
|
|
401
|
-
found ? found.dup : Instance.new(aggregate: aggregate, id: id)
|
|
532
|
+
found ? found.dup : Instance.new(aggregate: aggregate, id: id, args: args)
|
|
402
533
|
end
|
|
403
534
|
|
|
404
535
|
# THE JOIN, THE DIG, AND THE READING — all shared with `EntityInterpreter`
|
|
@@ -116,11 +116,32 @@ module Hecks
|
|
|
116
116
|
# parent aggregate reaching ITS OWN customer) resolves the same
|
|
117
117
|
# way `account.customer.status` does for a command-level reference.
|
|
118
118
|
# nil for an aggregate command — CommandInterpreter never passes it.
|
|
119
|
-
|
|
119
|
+
# `correction:` — the `{as_name => payload}` bindings
|
|
120
|
+
# `enforce_correction_target` (above) already located, merged in
|
|
121
|
+
# LAST so an `as:` name wins the same way `old:` always wins in
|
|
122
|
+
# `enforce_ensures`, below — it is a fresh local binding a
|
|
123
|
+
# `corrects` command introduces, not a real argument/state field
|
|
124
|
+
# a caller could collide with by accident.
|
|
125
|
+
def enforce_givens(subject, command, args, domain:, declaring: nil, parent: nil, correction: {})
|
|
120
126
|
state = GuardState.new(subject)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
127
|
+
# A RULE MAY ONLY READ WITHIN ITS OWN AGGREGATE BOUNDARY (S12,
|
|
128
|
+
# ADR 0025) — `subject`'s own STORED references are no longer
|
|
129
|
+
# dereferenced here at all. What used to be a live query against
|
|
130
|
+
# another aggregate's own repository is now just `subject`'s own
|
|
131
|
+
# state: a `projects :customer_status, from: :"customer.status"`
|
|
132
|
+
# field is a REGULAR stored attribute, already present in
|
|
133
|
+
# `subject`/`state` with no hydration step needed. `dereference`
|
|
134
|
+
# is still called on `command`/`args`, below — that is a
|
|
135
|
+
# DIFFERENT case the ADR explicitly keeps in bounds ("its command
|
|
136
|
+
# arguments"): a reference-typed ARGUMENT this dispatch was just
|
|
137
|
+
# handed (`Dispute`'s own `disputed_by`, say) has nothing stored
|
|
138
|
+
# to project yet, so resolving it here, once, synchronously with
|
|
139
|
+
# THIS command's own admission, is not the live-query-against-
|
|
140
|
+
# another-aggregate's-stored-state pattern the boundary rule
|
|
141
|
+
# forbids.
|
|
142
|
+
attrs = args.merge(dereference(domain, command, args))
|
|
143
|
+
attrs = attrs.merge(parent: parent.state) if parent
|
|
144
|
+
attrs = attrs.merge(correction) unless correction.empty?
|
|
124
145
|
command.givens.each do |given|
|
|
125
146
|
next if Bluebook::Expression::Evaluator.call(given.canonical, state, attrs)
|
|
126
147
|
|
|
@@ -130,6 +151,55 @@ module Hecks
|
|
|
130
151
|
enforce_lifecycle_guard(declaring, command, subject) if declaring
|
|
131
152
|
end
|
|
132
153
|
|
|
154
|
+
# `corrects` — CommandBuilder#corrects_impl's own comment. NOT
|
|
155
|
+
# expressible as an ordinary `given`: "has this exact record
|
|
156
|
+
# already emitted this exact event" is not a predicate over the
|
|
157
|
+
# record's OWN fields, it is a fact about the event log, so it is
|
|
158
|
+
# raised structurally here, the same way NotFound/AlreadyExists
|
|
159
|
+
# are, rather than through the expression evaluator. The build-
|
|
160
|
+
# time half — does ANYTHING in this aggregate ever emit the named
|
|
161
|
+
# event at all — is `AggregateBuilder#seal_correction_targets`;
|
|
162
|
+
# this is the dispatch-time half — has THIS record actually done
|
|
163
|
+
# so yet.
|
|
164
|
+
#
|
|
165
|
+
# ALSO LOCATES the matched event now, not just its existence, and
|
|
166
|
+
# returns a `{as_name => payload}` bindings hash — one entry per
|
|
167
|
+
# `:corrects` mutation that named an `as:` — so `given`/`ensures`
|
|
168
|
+
# on a corrects-bearing command can reference the located OLD
|
|
169
|
+
# event by that name, the same shape `enforce_ensures`'s own
|
|
170
|
+
# `old:` binding already has (CommandBuilder#corrects_impl's own
|
|
171
|
+
# comment: `as:` was stored, from the start, specifically to be
|
|
172
|
+
# wired into the evaluator once a real runtime consumer existed —
|
|
173
|
+
# this is that consumer). `.reverse.find` — the MOST RECENT
|
|
174
|
+
# matching event, if this record has somehow emitted the same
|
|
175
|
+
# correction target more than once; the prior existence-only
|
|
176
|
+
# check never had to make this choice, so it's a genuinely new
|
|
177
|
+
# one, made deliberately: `as:` reads as "the instance being
|
|
178
|
+
# corrected," which is naturally the latest fact on record, not
|
|
179
|
+
# an arbitrary one.
|
|
180
|
+
def enforce_correction_target(instance, aggregate, command, domain:)
|
|
181
|
+
bindings = {}
|
|
182
|
+
command.mutations.each do |mutation|
|
|
183
|
+
next unless mutation.op == :corrects
|
|
184
|
+
|
|
185
|
+
event_key = "#{domain}::#{aggregate.hecks_name}"
|
|
186
|
+
event_name = mutation.target.to_s
|
|
187
|
+
corrected = @registry.event_log.reverse.find do |event|
|
|
188
|
+
event.name == event_name && event.aggregate == event_key && event.id == instance.id
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
unless corrected
|
|
192
|
+
raise NothingToCorrect,
|
|
193
|
+
"#{command.hecks_name} refused — corrects #{event_name}, but " \
|
|
194
|
+
"#{event_key} ##{instance.id} has never emitted it"
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
as = mutation.source[:as]
|
|
198
|
+
bindings[as.to_sym] = corrected.payload if as && !as.to_s.empty?
|
|
199
|
+
end
|
|
200
|
+
bindings
|
|
201
|
+
end
|
|
202
|
+
|
|
133
203
|
# LIFECYCLE STATE AS A COMMAND GUARD (S10, ADR 0025) — `command
|
|
134
204
|
# "Debit", from: "open"` checked here, folded into the SAME
|
|
135
205
|
# dispatch step `given` already runs at (both are preconditions,
|
|
@@ -175,15 +245,21 @@ module Hecks
|
|
|
175
245
|
# Not new to ensures — `given` lives under the same rule — but an
|
|
176
246
|
# ensures is more likely to collide, since it typically re-reads a
|
|
177
247
|
# field the command just took in to mutate it.
|
|
178
|
-
def enforce_ensures(subject, command, args, old:, domain:, parent: nil)
|
|
248
|
+
def enforce_ensures(subject, command, args, old:, domain:, parent: nil, correction: {})
|
|
179
249
|
state = GuardState.new(subject)
|
|
180
|
-
|
|
181
|
-
#
|
|
182
|
-
#
|
|
183
|
-
#
|
|
184
|
-
#
|
|
185
|
-
|
|
186
|
-
|
|
250
|
+
# S12, ADR 0025 — same boundary reasoning as enforce_givens
|
|
251
|
+
# above: `subject`'s own stored references are no longer
|
|
252
|
+
# dereferenced here; a `projects`-maintained field is already
|
|
253
|
+
# part of `state`. `command`/`args` still dereferences — a
|
|
254
|
+
# fresh reference-typed ARGUMENT stays in bounds.
|
|
255
|
+
# `old` still wins over everything, unchanged. `correction`
|
|
256
|
+
# (an `as:`-bound corrected event, if this command declares
|
|
257
|
+
# one) wins right alongside it — a settled-record ensures can
|
|
258
|
+
# reference the correction target exactly as freely as a
|
|
259
|
+
# pre-mutation given already can.
|
|
260
|
+
attrs = args.merge(dereference(domain, command, args))
|
|
261
|
+
attrs = attrs.merge(parent: parent.state) if parent
|
|
262
|
+
attrs = attrs.merge(correction) unless correction.empty?
|
|
187
263
|
attrs = attrs.merge(old: old)
|
|
188
264
|
command.ensures.each do |rule|
|
|
189
265
|
next if Bluebook::Expression::Evaluator.call(rule.canonical, state, attrs)
|
|
@@ -197,16 +273,23 @@ module Hecks
|
|
|
197
273
|
# ensures` already checks at, and for the same reason: an
|
|
198
274
|
# invariant is a claim about the SETTLED record, not the
|
|
199
275
|
# command that produced it, so it reads no `args`/`old` at all,
|
|
200
|
-
# only the record's own
|
|
276
|
+
# only the record's own state. `subject` here is
|
|
201
277
|
# always the AGGREGATE's own instance — `CommandInterpreter`
|
|
202
278
|
# passes its own `ctx.instance`, and `EntityInterpreter` passes
|
|
203
279
|
# the PARENT record (`ctx.instance`, not the element), since an
|
|
204
280
|
# entity mutation changes data inside the SAME aggregate
|
|
205
281
|
# boundary the invariant guards; there is no separate "entity
|
|
206
282
|
# invariant" to check the piece's own view against.
|
|
283
|
+
#
|
|
284
|
+
# NO `dereference` (S12, ADR 0025) — an invariant may only read
|
|
285
|
+
# `subject`'s own boundary, same rule `enforce_givens`/
|
|
286
|
+
# `enforce_ensures` now hold to. No invariant in the corpus has
|
|
287
|
+
# ever read across a `reference_to` (verified before this
|
|
288
|
+
# change), so this is not a migration, just closing the same
|
|
289
|
+
# capability off here that was already unused.
|
|
207
290
|
def enforce_invariants(subject, aggregate, domain:)
|
|
208
291
|
state = GuardState.new(subject)
|
|
209
|
-
attrs =
|
|
292
|
+
attrs = {}
|
|
210
293
|
aggregate.invariants.each do |invariant|
|
|
211
294
|
next if Bluebook::Expression::Evaluator.call(invariant.canonical, state, attrs)
|
|
212
295
|
|
|
@@ -247,8 +330,10 @@ module Hecks
|
|
|
247
330
|
Array(owner_instance[list_attr.name]).each do |element|
|
|
248
331
|
wrapped = Instance.new(aggregate: entity, id: nil, state: element)
|
|
249
332
|
element_state = GuardState.new(wrapped)
|
|
250
|
-
|
|
251
|
-
|
|
333
|
+
# NO `dereference` (S12, ADR 0025) — same boundary rule as
|
|
334
|
+
# enforce_invariants above; `parent` (the owner's own
|
|
335
|
+
# state, projected fields included) stays readable.
|
|
336
|
+
attrs = { parent: owner_instance.state }
|
|
252
337
|
|
|
253
338
|
entity.invariants.each do |invariant|
|
|
254
339
|
next if Bluebook::Expression::Evaluator.call(invariant.canonical, element_state, attrs)
|
|
@@ -45,7 +45,13 @@ module Hecks
|
|
|
45
45
|
# what Vocabulary::MutationOp declares — MutationApplier's own
|
|
46
46
|
# `when :delegate` branch (mutation_applier.rb) never calls
|
|
47
47
|
# #sign_of either, same as `remove`'s own note above.
|
|
48
|
-
MutationOp.new(name: "delegate", sign: nil)
|
|
48
|
+
MutationOp.new(name: "delegate", sign: nil),
|
|
49
|
+
# CommandBuilder#corrects_impl's own comment gives the full
|
|
50
|
+
# reasoning — a command amending a past event rather than
|
|
51
|
+
# acting fresh. Carries no sign, like delegate: it does no
|
|
52
|
+
# arithmetic of its own; the record's actual change, if any,
|
|
53
|
+
# is an ordinary `sets` declared alongside it.
|
|
54
|
+
MutationOp.new(name: "corrects", sign: nil)
|
|
49
55
|
].freeze
|
|
50
56
|
|
|
51
57
|
# A mutation's source is either the NAME OF AN ARGUMENT or a LITERAL, and
|
|
@@ -62,9 +68,39 @@ module Hecks
|
|
|
62
68
|
# made. The real mistake, an absent argument, was never the one refused,
|
|
63
69
|
# which is what fuzz surfaced.
|
|
64
70
|
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
71
|
+
# STALE (as of the equivalence-gap plan's own audit): this used to
|
|
72
|
+
# say "the language cannot yet say which arguments are optional" —
|
|
73
|
+
# it already can, and always could once `attribute ..., optional:
|
|
74
|
+
# true` existed (`CommandBuilder#attribute_impl`,
|
|
75
|
+
# `attribute_collector.rb`): `sets` already sources correctly from
|
|
76
|
+
# an optional attribute, resolving absent to nil exactly as this
|
|
77
|
+
# method does, and REFUSING it here would be wrong, not merely
|
|
78
|
+
# undone work — `TillRoom::Till.TakeIn`'s own `note` (spec/
|
|
79
|
+
# fixtures/till.bluebook) and Banking's `CardPayment.Authorize`'s
|
|
80
|
+
# `tags` (payment_cards.bluebook) are real, live commands whose
|
|
81
|
+
# `sets` mutation is deliberately sourced from an optional
|
|
82
|
+
# attribute the caller may omit — `spec/runtime/command_rules_spec
|
|
83
|
+
# .rb`'s own "says an absent OPTIONAL argument is nil, not the
|
|
84
|
+
# name of the argument" pins exactly this as correct, not pending.
|
|
85
|
+
# The meta-domain's own self-hosted commands (Command.Declare's
|
|
86
|
+
# `role`/`goal`/`provenance`/`from`/`position`, and ~35 more sites
|
|
87
|
+
# across the language) all lean on the identical pattern — nil is
|
|
88
|
+
# the RIGHT answer for a `sets` sourced from a declared-optional
|
|
89
|
+
# attribute the caller left out, every time.
|
|
90
|
+
#
|
|
91
|
+
# The one thing that WAS still a real, narrow gap — a `sets`
|
|
92
|
+
# source Symbol naming NOTHING the command declares at all (a
|
|
93
|
+
# typo, not an optional argument) — silently resolved to nil
|
|
94
|
+
# forever the same way, indistinguishable at either build or run
|
|
95
|
+
# time from a legitimate optional absence. Closed at BUILD time
|
|
96
|
+
# instead of here: `CommandBuilder#refuse_unknown_argument_sources!`
|
|
97
|
+
# refuses it the moment the `.bluebook` file loads, mirroring
|
|
98
|
+
# `AggregateBuilder#seal_query_argument`'s identical check for a
|
|
99
|
+
# query's own where-clause argument. This function stays exactly
|
|
100
|
+
# what it always was — a pure, unconditional lookup — because by
|
|
101
|
+
# the time ANY mutation reaches it, the source has already been
|
|
102
|
+
# proven to name either a real, possibly-optional argument, or a
|
|
103
|
+
# StateRef/literal; there is nothing left here to refuse.
|
|
68
104
|
def resolve_source(source, args)
|
|
69
105
|
return args[source] if source.is_a?(Symbol)
|
|
70
106
|
|
|
@@ -37,7 +37,8 @@ module Hecks
|
|
|
37
37
|
|
|
38
38
|
authorized =
|
|
39
39
|
if caller.actor_id && governance_attached?(domain)
|
|
40
|
-
Ports::Authorization.holds_role?(registry, actor_id: caller.actor_id, role: command.role
|
|
40
|
+
Ports::Authorization.holds_role?(registry, actor_id: caller.actor_id, role: command.role,
|
|
41
|
+
as_of: caller.as_of, scope: caller.scope)
|
|
41
42
|
else
|
|
42
43
|
caller.role == command.role
|
|
43
44
|
end
|
|
@@ -128,31 +128,39 @@ module Hecks
|
|
|
128
128
|
unwrapped.to_s
|
|
129
129
|
end
|
|
130
130
|
|
|
131
|
-
# A
|
|
132
|
-
# `ensures` — `
|
|
131
|
+
# A COMMAND ARGUMENT's own related record, reachable by name from
|
|
132
|
+
# `given`/`ensures` — `disputed_by.status`, say, `CardPayment
|
|
133
|
+
# .Dispute`'s own fresh `Reference<Customer>` argument — without
|
|
133
134
|
# teaching the pure expression evaluator anything about
|
|
134
135
|
# repositories. The lookup happens HERE, once, before evaluation;
|
|
135
136
|
# `Resolver#lookup` just digs into a plain Hash exactly as it
|
|
136
137
|
# always has.
|
|
137
138
|
#
|
|
138
|
-
# `owner`
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
#
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
#
|
|
146
|
-
#
|
|
147
|
-
#
|
|
139
|
+
# `owner` NARROWED TO `command` ONLY (S12, ADR 0025 — "rules
|
|
140
|
+
# confined to their own aggregate boundary"): dereferencing the
|
|
141
|
+
# DECLARING aggregate/entity's own STORED `reference_to` used to
|
|
142
|
+
# be the other half of this method's job — a live query against
|
|
143
|
+
# another aggregate's own repository, every time a `given`/
|
|
144
|
+
# `ensures`/`invariant` ran. That half is gone; a cross-aggregate
|
|
145
|
+
# fact a rule needs now has to be a `projects`-maintained LOCAL
|
|
146
|
+
# field (`AggregateBuilder#projects_impl`'s own comment), already
|
|
147
|
+
# present in `subject`'s own state, no hydration needed. A
|
|
148
|
+
# reference-typed COMMAND ARGUMENT stays in bounds, though — the
|
|
149
|
+
# ADR's own boundary list names "its command arguments" as
|
|
150
|
+
# readable, and nothing is stored yet for a fresh argument to
|
|
151
|
+
# project from; resolving it once here, synchronous with THIS
|
|
152
|
+
# command's own admission, is a different shape from a live query
|
|
153
|
+
# against an ALREADY-PERSISTED reference. `enforce_givens`/
|
|
154
|
+
# `enforce_ensures` are this method's only two remaining callers,
|
|
155
|
+
# both passing `command`/`args`, never a `subject`'s own
|
|
156
|
+
# aggregate — verified before this comment was written, not
|
|
157
|
+
# assumed.
|
|
148
158
|
#
|
|
149
|
-
# RECURSES into what it finds, so a chain
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
#
|
|
153
|
-
#
|
|
154
|
-
# than tracking visited (type, id) pairs for a cycle nothing here
|
|
155
|
-
# declares.
|
|
159
|
+
# RECURSES into what it finds, so a chain deeper than one hop
|
|
160
|
+
# still resolves in one pass. Depth-bounded rather than cycle-
|
|
161
|
+
# detected — nothing in this corpus dots more than two hops on a
|
|
162
|
+
# fresh argument, and a bound is simpler than tracking visited
|
|
163
|
+
# (type, id) pairs for a cycle nothing here declares.
|
|
156
164
|
DEREFERENCE_DEPTH = 4
|
|
157
165
|
private_constant :DEREFERENCE_DEPTH
|
|
158
166
|
|