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
|
@@ -4,7 +4,9 @@ require_relative "nondeterministic"
|
|
|
4
4
|
|
|
5
5
|
module Hecks
|
|
6
6
|
module Fuzzing
|
|
7
|
-
#
|
|
7
|
+
# ## The second differential axis
|
|
8
|
+
#
|
|
9
|
+
# bin/qa_sweep's own `diff_ruby_vs_rust`
|
|
8
10
|
# compares two different engines (the Ruby interpreter vs the compiled
|
|
9
11
|
# Rust kernel) against the same persistence (Memory, always — see
|
|
10
12
|
# `SequenceGenerator`'s own header: sequence generation stays
|
|
@@ -27,6 +29,8 @@ module Hecks
|
|
|
27
29
|
# Memory, unconditionally, no matter what `directory.world` itself
|
|
28
30
|
# declares.
|
|
29
31
|
#
|
|
32
|
+
# ## A separate axis, not a replacement
|
|
33
|
+
#
|
|
30
34
|
# Not a replacement for `bin/qa_sweep`'s own Ruby-vs-Rust differential
|
|
31
35
|
# mode — a genuinely separate axis, opt-in (`--persistence-parity`),
|
|
32
36
|
# because this one pays for a real `PG.connect` and real SQL per
|
|
@@ -34,7 +38,9 @@ module Hecks
|
|
|
34
38
|
# own `--persistence-parity` handling for the seed-count dial that
|
|
35
39
|
# keeps that cost bounded.
|
|
36
40
|
#
|
|
37
|
-
#
|
|
41
|
+
# ## `left:`/`right:`
|
|
42
|
+
#
|
|
43
|
+
# Generalized — originally hardcoded to Memory vs
|
|
38
44
|
# PostgresEra (the only pairing that existed), now any two of
|
|
39
45
|
# `IsolatedBoot`'s own adapter symbols (`:memory`, `:sqlite`,
|
|
40
46
|
# `:postgres`, `:postgres_era`). Defaults preserve the original
|
|
@@ -49,7 +55,9 @@ module Hecks
|
|
|
49
55
|
# loop instead of needing a deferred wave of its own the way
|
|
50
56
|
# PostgresEra does.
|
|
51
57
|
#
|
|
52
|
-
# `database:`/`schema:`
|
|
58
|
+
# ## `database:`/`schema:`
|
|
59
|
+
#
|
|
60
|
+
# Required only when `:postgres_era` is one of
|
|
53
61
|
# the two adapters (the caller — today, only `bin/qa_sweep` — owns the
|
|
54
62
|
# disposable database's whole lifecycle: created before the sweep,
|
|
55
63
|
# dropped after — see that script's own comment, and the discipline
|
|
@@ -81,6 +89,21 @@ module Hecks
|
|
|
81
89
|
# sides identically (a `Runtime::Value`, a `Symbol` key, a `Time`
|
|
82
90
|
# nobody asked for — none of that survives an accidental leak into
|
|
83
91
|
# this comparison unnoticed).
|
|
92
|
+
#
|
|
93
|
+
# @param domain_path [String] path to the domain directory to boot
|
|
94
|
+
# @param steps [Array<Hash>] the step list to replay against both adapters
|
|
95
|
+
# @param left [Symbol] the first adapter to replay against, one of
|
|
96
|
+
# `IsolatedBoot`'s adapter symbols (`:memory`, `:sqlite`, `:postgres`,
|
|
97
|
+
# `:postgres_era`)
|
|
98
|
+
# @param right [Symbol] the second adapter to replay against, same set as `left`
|
|
99
|
+
# @param database [String, nil] connection identity for `:postgres_era`; required
|
|
100
|
+
# when `left` or `right` is `:postgres_era`, ignored otherwise
|
|
101
|
+
# @param schema [String, nil] disposable schema name for `:postgres_era`; required
|
|
102
|
+
# when `left` or `right` is `:postgres_era`, ignored otherwise
|
|
103
|
+
# @return [Array<Hash>] divergence entries, each `{field: String, left => Object,
|
|
104
|
+
# right => Object}` — `left`'s and `right`'s own adapter symbols become the
|
|
105
|
+
# entry's own keys, holding each side's JSON-shaped value for that field; empty
|
|
106
|
+
# if both sides agree on every field
|
|
84
107
|
def diff(domain_path, steps, left: :memory, right: :postgres_era, database: nil, schema: nil)
|
|
85
108
|
left_result = Replay.call(domain_path, steps, adapter: left, database: database, schema: schema)
|
|
86
109
|
right_result = Replay.call(domain_path, steps, adapter: right, database: database, schema: schema)
|
|
@@ -95,8 +118,25 @@ module Hecks
|
|
|
95
118
|
divergences
|
|
96
119
|
end
|
|
97
120
|
|
|
121
|
+
# Round-trips `value` through JSON, the same wire-precision reduction
|
|
122
|
+
# `diff_ruby_vs_rust` uses so `Hash#==`/`Array#==` compares plain,
|
|
123
|
+
# JSON-shaped data on both sides.
|
|
124
|
+
#
|
|
125
|
+
# @param value [Object] any JSON-serializable value from a replay result
|
|
126
|
+
# @return [Object] `value`, JSON-round-tripped: Symbol keys become Strings,
|
|
127
|
+
# and any non-JSON-native value surfaces as its own JSON form
|
|
98
128
|
def as_json(value) = JSON.parse(JSON.generate(value))
|
|
99
129
|
|
|
130
|
+
# Compares both sides' stored instances.
|
|
131
|
+
#
|
|
132
|
+
# @param left_result [Hash] `left`'s replay result, as returned by `Fuzzing::Replay.call`
|
|
133
|
+
# @param right_result [Hash] `right`'s replay result, as returned by `Fuzzing::Replay.call`
|
|
134
|
+
# @param left [Symbol] the adapter `left_result` was replayed against; becomes
|
|
135
|
+
# this entry's own key
|
|
136
|
+
# @param right [Symbol] the adapter `right_result` was replayed against; becomes
|
|
137
|
+
# this entry's own key
|
|
138
|
+
# @return [Array<Hash>] one `{field: "instances", left => Object, right => Object}`
|
|
139
|
+
# entry if the two sides' JSON-shaped instances differ; empty otherwise
|
|
100
140
|
def diff_instances(left_result, right_result, left, right)
|
|
101
141
|
l = as_json(left_result[:instances])
|
|
102
142
|
r = as_json(right_result[:instances])
|
|
@@ -105,6 +145,16 @@ module Hecks
|
|
|
105
145
|
[{ field: "instances", left => l, right => r }]
|
|
106
146
|
end
|
|
107
147
|
|
|
148
|
+
# Compares both sides' emitted events.
|
|
149
|
+
#
|
|
150
|
+
# @param left_result [Hash] `left`'s replay result, as returned by `Fuzzing::Replay.call`
|
|
151
|
+
# @param right_result [Hash] `right`'s replay result, as returned by `Fuzzing::Replay.call`
|
|
152
|
+
# @param left [Symbol] the adapter `left_result` was replayed against; becomes
|
|
153
|
+
# this entry's own key
|
|
154
|
+
# @param right [Symbol] the adapter `right_result` was replayed against; becomes
|
|
155
|
+
# this entry's own key
|
|
156
|
+
# @return [Array<Hash>] one `{field: "events", left => Object, right => Object}`
|
|
157
|
+
# entry if the two sides' JSON-shaped events differ; empty otherwise
|
|
108
158
|
def diff_events(left_result, right_result, left, right)
|
|
109
159
|
l = as_json(left_result[:events])
|
|
110
160
|
r = as_json(right_result[:events])
|
|
@@ -113,11 +163,22 @@ module Hecks
|
|
|
113
163
|
[{ field: "events", left => l, right => r }]
|
|
114
164
|
end
|
|
115
165
|
|
|
166
|
+
# Compares both sides' refusals.
|
|
167
|
+
#
|
|
116
168
|
# `verb:`/`kind:` normalized to plain strings the same way
|
|
117
169
|
# `diff_ruby_vs_rust`'s own `ruby_refusals` mapping does — both
|
|
118
170
|
# sides here already answer strings (`Replay#refusal_kind` always
|
|
119
171
|
# returns one), so this is belt-and-suspenders consistency with the
|
|
120
172
|
# sibling mode's own shape, not a real coercion.
|
|
173
|
+
#
|
|
174
|
+
# @param left_result [Hash] `left`'s replay result, as returned by `Fuzzing::Replay.call`
|
|
175
|
+
# @param right_result [Hash] `right`'s replay result, as returned by `Fuzzing::Replay.call`
|
|
176
|
+
# @param left [Symbol] the adapter `left_result` was replayed against; becomes
|
|
177
|
+
# this entry's own key
|
|
178
|
+
# @param right [Symbol] the adapter `right_result` was replayed against; becomes
|
|
179
|
+
# this entry's own key
|
|
180
|
+
# @return [Array<Hash>] one `{field: "refusals", left => Object, right => Object}`
|
|
181
|
+
# entry if the two sides' normalized refusals differ; empty otherwise
|
|
121
182
|
def diff_refusals(left_result, right_result, left, right)
|
|
122
183
|
normalize = lambda do |refusals|
|
|
123
184
|
refusals.map { |r| { "verb" => r[:verb].to_s, "kind" => r[:kind].to_s, "error" => r[:error] } }
|
|
@@ -129,9 +190,20 @@ module Hecks
|
|
|
129
190
|
[{ field: "refusals", left => l, right => r }]
|
|
130
191
|
end
|
|
131
192
|
|
|
193
|
+
# Compares both sides' query answers.
|
|
194
|
+
#
|
|
132
195
|
# `Nondeterministic`'s `query_row` group dropped from every entry —
|
|
133
196
|
# the same group `Differential.diff` drops, for the reason declared
|
|
134
197
|
# there (already covered by `diff_instances` above).
|
|
198
|
+
#
|
|
199
|
+
# @param left_result [Hash] `left`'s replay result, as returned by `Fuzzing::Replay.call`
|
|
200
|
+
# @param right_result [Hash] `right`'s replay result, as returned by `Fuzzing::Replay.call`
|
|
201
|
+
# @param left [Symbol] the adapter `left_result` was replayed against; becomes
|
|
202
|
+
# this entry's own key
|
|
203
|
+
# @param right [Symbol] the adapter `right_result` was replayed against; becomes
|
|
204
|
+
# this entry's own key
|
|
205
|
+
# @return [Array<Hash>] one `{field: "queries", left => Object, right => Object}`
|
|
206
|
+
# entry if the two sides' stripped, JSON-shaped queries differ; empty otherwise
|
|
135
207
|
def diff_queries(left_result, right_result, left, right)
|
|
136
208
|
strip = ->(rows) { rows.map { |row| Nondeterministic.strip(row, :query_row) } }
|
|
137
209
|
l = as_json(strip.call(left_result[:queries]))
|
|
@@ -141,6 +213,16 @@ module Hecks
|
|
|
141
213
|
[{ field: "queries", left => l, right => r }]
|
|
142
214
|
end
|
|
143
215
|
|
|
216
|
+
# Compares both sides' saga logs.
|
|
217
|
+
#
|
|
218
|
+
# @param left_result [Hash] `left`'s replay result, as returned by `Fuzzing::Replay.call`
|
|
219
|
+
# @param right_result [Hash] `right`'s replay result, as returned by `Fuzzing::Replay.call`
|
|
220
|
+
# @param left [Symbol] the adapter `left_result` was replayed against; becomes
|
|
221
|
+
# this entry's own key
|
|
222
|
+
# @param right [Symbol] the adapter `right_result` was replayed against; becomes
|
|
223
|
+
# this entry's own key
|
|
224
|
+
# @return [Array<Hash>] one `{field: "sagas", left => Object, right => Object}`
|
|
225
|
+
# entry if the two sides' JSON-shaped sagas differ; empty otherwise
|
|
144
226
|
def diff_sagas(left_result, right_result, left, right)
|
|
145
227
|
l = as_json(left_result[:sagas])
|
|
146
228
|
r = as_json(right_result[:sagas])
|
|
@@ -149,6 +231,16 @@ module Hecks
|
|
|
149
231
|
[{ field: "sagas", left => l, right => r }]
|
|
150
232
|
end
|
|
151
233
|
|
|
234
|
+
# Compares both sides' reaction logs.
|
|
235
|
+
#
|
|
236
|
+
# @param left_result [Hash] `left`'s replay result, as returned by `Fuzzing::Replay.call`
|
|
237
|
+
# @param right_result [Hash] `right`'s replay result, as returned by `Fuzzing::Replay.call`
|
|
238
|
+
# @param left [Symbol] the adapter `left_result` was replayed against; becomes
|
|
239
|
+
# this entry's own key
|
|
240
|
+
# @param right [Symbol] the adapter `right_result` was replayed against; becomes
|
|
241
|
+
# this entry's own key
|
|
242
|
+
# @return [Array<Hash>] one `{field: "reactions", left => Object, right => Object}`
|
|
243
|
+
# entry if the two sides' JSON-shaped reactions differ; empty otherwise
|
|
152
244
|
def diff_reactions(left_result, right_result, left, right)
|
|
153
245
|
l = as_json(left_result[:reactions])
|
|
154
246
|
r = as_json(right_result[:reactions])
|
|
@@ -45,6 +45,11 @@ module Hecks
|
|
|
45
45
|
# mutation is invisible to both doors today — this property is the
|
|
46
46
|
# only thing anywhere, on either engine, that would ever catch one
|
|
47
47
|
# going wrong.
|
|
48
|
+
#
|
|
49
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
50
|
+
# @return [true, String] true if every event emitted by a `corrects`-bearing
|
|
51
|
+
# command has a matching, strictly earlier corrected event in the same
|
|
52
|
+
# history; otherwise a message listing every unmatched correction
|
|
48
53
|
def corrections_reference_an_emitted_event(history)
|
|
49
54
|
violations = []
|
|
50
55
|
|
|
@@ -72,6 +77,17 @@ module Hecks
|
|
|
72
77
|
violations.empty? || violations.uniq.join("; ")
|
|
73
78
|
end
|
|
74
79
|
|
|
80
|
+
# Finds every occurrence of `produced_event_name`, on `aggregate_key`, with no
|
|
81
|
+
# matching `corrected_event` for the same id appearing earlier in `events`.
|
|
82
|
+
#
|
|
83
|
+
# @param events [Array<Hash>] `history[:events]`, in occurrence order
|
|
84
|
+
# @param aggregate_key [String] `"domain::AggregateName"` the events belong to
|
|
85
|
+
# @param produced_event_name [String] name of the event a `corrects` mutation's
|
|
86
|
+
# command emits
|
|
87
|
+
# @param corrected_event [String] name of the event the mutation claims to correct
|
|
88
|
+
# @param command_name [String] the command's own `hecks_name`, for the message
|
|
89
|
+
# @return [Array<String>] one message per occurrence with no matching earlier
|
|
90
|
+
# corrected event; empty when every occurrence is matched
|
|
75
91
|
def unmatched_corrections(events, aggregate_key, produced_event_name, corrected_event, command_name)
|
|
76
92
|
own_events = events.each_with_index.select do |event, _index|
|
|
77
93
|
event[:name] == produced_event_name && event[:aggregate] == aggregate_key
|
|
@@ -90,6 +106,15 @@ module Hecks
|
|
|
90
106
|
end
|
|
91
107
|
end
|
|
92
108
|
|
|
109
|
+
# Yields every command declared on `owner`, then recurses into each of its
|
|
110
|
+
# entities to yield theirs too, at any nesting depth (ADR 0026, S17).
|
|
111
|
+
#
|
|
112
|
+
# @param owner [Bluebook::Aggregate, Bluebook::Entity] the aggregate or entity
|
|
113
|
+
# whose own commands, and whose entities' commands, to walk
|
|
114
|
+
# @yield [command] once per declared command, aggregate-level or nested
|
|
115
|
+
# @yieldparam command [Bluebook::Command] a command declared on `owner` or one
|
|
116
|
+
# of its entities
|
|
117
|
+
# @return [void]
|
|
93
118
|
def each_command_including_entities(owner, &block)
|
|
94
119
|
owner.commands.each(&block)
|
|
95
120
|
owner.entities.each { |entity| each_command_including_entities(entity, &block) }
|
|
@@ -16,6 +16,12 @@ module Hecks
|
|
|
16
16
|
# hand-built history, an older corpus) are skipped, not failed: no
|
|
17
17
|
# claim, no finding.
|
|
18
18
|
module DryRuns
|
|
19
|
+
# Checks that every dry-run step left neither the event count nor any
|
|
20
|
+
# instance's state changed.
|
|
21
|
+
#
|
|
22
|
+
# @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
|
|
23
|
+
# @return [true, String] true if every dry-run trace shows no change; otherwise
|
|
24
|
+
# a semicolon-joined message naming each offending dry run
|
|
19
25
|
def dry_runs_leave_no_trace(history)
|
|
20
26
|
offenders = Array(history[:dry_run_traces]).filter_map do |entry|
|
|
21
27
|
before = entry[:before]
|
|
@@ -67,7 +73,8 @@ module Hecks
|
|
|
67
73
|
# argument binding on a fan-out dispatch that produces a perfectly
|
|
68
74
|
# normal-looking log entry (`delivered: true`) and would only ever
|
|
69
75
|
# surface as a downstream assertion failure, if it surfaces at
|
|
70
|
-
# all" —
|
|
76
|
+
# all" — a defect class one level over from what
|
|
77
|
+
# `fanout_dispatches_once_per_matching_row` already catches.
|
|
71
78
|
#
|
|
72
79
|
# Real targets: Settlement (mixed literal/correlation-head/event-
|
|
73
80
|
# payload/memory-fallback bindings across three legs, plus a
|
|
@@ -75,6 +82,11 @@ module Hecks
|
|
|
75
82
|
# the forward Credit leg carries), ExternalSettlement, Onboarding
|
|
76
83
|
# (no compensation leg, by design — nothing to check there beyond
|
|
77
84
|
# the forward leg's own event-payload binding).
|
|
85
|
+
#
|
|
86
|
+
# @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
|
|
87
|
+
# @return [true, String] true if every saga and policy dispatch's own bound args
|
|
88
|
+
# match an independent re-derivation of their `with_spec`; otherwise a
|
|
89
|
+
# semicolon-joined message naming each offending dispatch
|
|
78
90
|
def dispatch_binding_fidelity(history)
|
|
79
91
|
saga_offenders = history.fetch(:saga_dispatches, []).filter_map do |entry|
|
|
80
92
|
expected = resolve_dispatch_binding(entry)
|
|
@@ -102,6 +114,11 @@ module Hecks
|
|
|
102
114
|
# the current triggering event's own payload, or — the fallback —
|
|
103
115
|
# the saga's own carried memory (seeded from the starting event's
|
|
104
116
|
# payload, at begin_saga).
|
|
117
|
+
#
|
|
118
|
+
# @param entry [Hash] one `history[:saga_dispatches]` entry: at least
|
|
119
|
+
# `:with_spec`, `:correlation_head`, `:instance`, `:event_payload`, and `:memory`
|
|
120
|
+
# @return [Hash{Symbol => Object}] the resolved, materialized args `with_spec`
|
|
121
|
+
# should produce, keyed by symbol
|
|
105
122
|
def resolve_dispatch_binding(entry)
|
|
106
123
|
entry[:with_spec].to_h do |key, value|
|
|
107
124
|
resolved = if !value.is_a?(Symbol) then value
|
|
@@ -117,6 +134,11 @@ module Hecks
|
|
|
117
134
|
# policy holds no correlation and no memory, so `payload` (the
|
|
118
135
|
# triggering event's own payload, already merged with a fan-out
|
|
119
136
|
# row's id when there is one) is the whole source.
|
|
137
|
+
#
|
|
138
|
+
# @param entry [Hash] one `history[:policy_dispatches]` entry: at least
|
|
139
|
+
# `:with_spec` and `:payload`
|
|
140
|
+
# @return [Hash{Symbol => Object}] the resolved, materialized args `with_spec`
|
|
141
|
+
# should produce, keyed by symbol
|
|
120
142
|
def resolve_trigger_binding(entry)
|
|
121
143
|
entry[:with_spec].to_h do |key, value|
|
|
122
144
|
resolved = value.is_a?(Symbol) ? entry[:payload][value] : value
|
|
@@ -209,6 +231,14 @@ module Hecks
|
|
|
209
231
|
# corpus shape).
|
|
210
232
|
RECOMPUTABLE_MUTATION_OPS = %i[append remove multiply clamp set].freeze
|
|
211
233
|
|
|
234
|
+
# Checks that every recomputable mutation an entity-owned command
|
|
235
|
+
# applied landed on the same after-state an independent
|
|
236
|
+
# recomputation of the same rule produces.
|
|
237
|
+
#
|
|
238
|
+
# @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
|
|
239
|
+
# @return [true, String] true if every recomputable mutation's after-state
|
|
240
|
+
# matches an independent recomputation; otherwise a semicolon-joined message
|
|
241
|
+
# naming each offending mutation
|
|
212
242
|
def mutations_match_recompute(history)
|
|
213
243
|
bluebooks = history.fetch(:bluebooks)
|
|
214
244
|
|
|
@@ -249,6 +279,12 @@ module Hecks
|
|
|
249
279
|
# value-object type against the root's own namespace only, the
|
|
250
280
|
# same reason `EntityElement#locate_chain` threads `root_aggregate`
|
|
251
281
|
# through every hop separately from each hop's own `owner`.
|
|
282
|
+
#
|
|
283
|
+
# @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
|
|
284
|
+
# keyed by domain name
|
|
285
|
+
# @param verb [String] the dispatched verb to resolve
|
|
286
|
+
# @return [Bluebook::Aggregate, nil] the root aggregate `verb`'s domain and
|
|
287
|
+
# aggregate name, or nil if `verb` names no domain
|
|
252
288
|
def aggregate_for_verb(bluebooks, verb)
|
|
253
289
|
domain_name, aggregate_name, = Naming.split_verb(verb)
|
|
254
290
|
return nil unless domain_name
|
|
@@ -268,6 +304,15 @@ module Hecks
|
|
|
268
304
|
# each hop's own `owner`. Re-derived independently from
|
|
269
305
|
# `entry[:verb]` alone, the same reasoning `aggregate_for_verb`'s
|
|
270
306
|
# own comment gives.
|
|
307
|
+
#
|
|
308
|
+
# @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
|
|
309
|
+
# keyed by domain name
|
|
310
|
+
# @param verb [String] the dispatched verb to resolve
|
|
311
|
+
# @return [Bluebook::Aggregate, Bluebook::Entity, nil] the construct that
|
|
312
|
+
# declares the mutation's own target: the root aggregate for an
|
|
313
|
+
# aggregate-owned command, or the owning entity for a dot-shaped
|
|
314
|
+
# (entity-owned) command; nil if `verb` names no command path or no
|
|
315
|
+
# resolvable aggregate
|
|
271
316
|
def owner_for_verb(bluebooks, verb)
|
|
272
317
|
domain_name, aggregate_name, command_path = Naming.split_verb(verb)
|
|
273
318
|
return nil unless command_path
|
|
@@ -280,6 +325,21 @@ module Hecks
|
|
|
280
325
|
aggregate.entities.find { |candidate| candidate.hecks_name == entity_name }
|
|
281
326
|
end
|
|
282
327
|
|
|
328
|
+
# Dispatches to the recompute rule for `mutation.op`.
|
|
329
|
+
#
|
|
330
|
+
# @param mutation [Bluebook::Mutation] the mutation to recompute
|
|
331
|
+
# @param current [Object] the target field's before-value, as captured in
|
|
332
|
+
# `entry[:before]`
|
|
333
|
+
# @param args [Hash] the step's own raw args, as captured in `entry[:args]`
|
|
334
|
+
# @param before_scope [Hash] the entity element's own before-state
|
|
335
|
+
# (`entry[:before]`), the source an append's caller-omitted field resolves against
|
|
336
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
|
|
337
|
+
# namespace resolution
|
|
338
|
+
# @param command [Bluebook::Command] the command that declared `mutation`
|
|
339
|
+
# @param owner [Bluebook::Aggregate, Bluebook::Entity] the construct that
|
|
340
|
+
# declares `mutation.target`; defaults to `aggregate`
|
|
341
|
+
# @return [Object, Symbol] the recomputed after-value for `mutation.op`, or
|
|
342
|
+
# `:unrecomputable` if the raw material doesn't fit the op's own contract
|
|
283
343
|
def recompute_mutation(mutation, current, args, before_scope, aggregate, command, owner = aggregate)
|
|
284
344
|
case mutation.op
|
|
285
345
|
when :append
|
|
@@ -312,19 +372,16 @@ module Hecks
|
|
|
312
372
|
# (the root aggregate, for value-object namespace resolution
|
|
313
373
|
# only — `value_object_for(aggregate, attribute.type)` — the
|
|
314
374
|
# same aggregate/owner split `owner_for_verb`'s own comment
|
|
315
|
-
# explains for `recompute_append`).
|
|
316
|
-
#
|
|
317
|
-
#
|
|
318
|
-
#
|
|
319
|
-
#
|
|
320
|
-
#
|
|
321
|
-
# `
|
|
322
|
-
#
|
|
323
|
-
#
|
|
324
|
-
#
|
|
325
|
-
# every single run — caught immediately by running this against
|
|
326
|
-
# `qa/stress_domains/nested_pieces` before this comment existed,
|
|
327
|
-
# not by inspection alone.
|
|
375
|
+
# explains for `recompute_append`). Coercing against
|
|
376
|
+
# `owner.attribute` rather than `aggregate.attribute` matters
|
|
377
|
+
# concretely: `NestedPieces::Workspace.Board.Label` (`sets
|
|
378
|
+
# :label`) has no `:label` attribute on `Workspace` at all, so
|
|
379
|
+
# coercing against the aggregate would silently pass the raw,
|
|
380
|
+
# uncoerced String through instead of wrapping it as
|
|
381
|
+
# `BoardLabel`, and the comparison below would then disagree with
|
|
382
|
+
# the real, correctly-coerced `after` state on every single run —
|
|
383
|
+
# a real false positive, confirmed by running this against
|
|
384
|
+
# `qa/stress_domains/nested_pieces`, not by inspection alone.
|
|
328
385
|
#
|
|
329
386
|
# Reusing `Value.for_attribute` for the coercion step, rather
|
|
330
387
|
# than re-deriving it, is the same "coercion is its own already-
|
|
@@ -341,6 +398,17 @@ module Hecks
|
|
|
341
398
|
# real dispatch was itself malformed — `:unrecomputable`, not a
|
|
342
399
|
# crash, the same discipline every other branch in this method
|
|
343
400
|
# already follows for the generator's own deliberate malforming.
|
|
401
|
+
#
|
|
402
|
+
# @param source [Symbol, Object] the mutation's own declared source: an argument
|
|
403
|
+
# name to resolve from `args`, or a literal value already
|
|
404
|
+
# @param args [Hash] the step's own raw args
|
|
405
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
|
|
406
|
+
# namespace resolution
|
|
407
|
+
# @param owner [Bluebook::Aggregate, Bluebook::Entity, nil] the construct that
|
|
408
|
+
# declares `target`
|
|
409
|
+
# @param target [Symbol, nil] the mutation's own target field name
|
|
410
|
+
# @return [Object, Symbol] the recomputed, materialized value, or
|
|
411
|
+
# `:unrecomputable` if resolving or coercing the source raises
|
|
344
412
|
def recompute_set(source, args, aggregate, owner, target)
|
|
345
413
|
raw = resolve_mutation_source(source, args)
|
|
346
414
|
attribute = owner&.attribute(target)
|
|
@@ -393,6 +461,22 @@ module Hecks
|
|
|
393
461
|
# through it via `Instance.defaults`), not the new glue
|
|
394
462
|
# (`fill_declared_defaults` itself) this property exists to
|
|
395
463
|
# catch a drift in.
|
|
464
|
+
#
|
|
465
|
+
# @param current [Object] the target field's before-value (an Array of elements)
|
|
466
|
+
# @param source_map [Hash{Symbol => Object}] the append mutation's own field map:
|
|
467
|
+
# each appended field's name to its source (an argument name Symbol, or a literal)
|
|
468
|
+
# @param before_scope [Hash] the entity element's own before-state, the source a
|
|
469
|
+
# caller-omitted field resolves against
|
|
470
|
+
# @param args [Hash] the step's own raw args
|
|
471
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
|
|
472
|
+
# namespace resolution
|
|
473
|
+
# @param command [Bluebook::Command] the command that declared the mutation, for
|
|
474
|
+
# coercing a caller-supplied arg
|
|
475
|
+
# @param owner [Bluebook::Aggregate, Bluebook::Entity] the construct whose nested
|
|
476
|
+
# entity `target` may declare; defaults to `aggregate`
|
|
477
|
+
# @param target [Symbol, nil] the append mutation's own target field name
|
|
478
|
+
# @return [Array] `current` (coerced to an Array) with the newly built,
|
|
479
|
+
# symbol-keyed element appended
|
|
396
480
|
def recompute_append(current, source_map, before_scope, args, aggregate, command, owner = aggregate, target = nil)
|
|
397
481
|
fields = source_map.transform_values do |source|
|
|
398
482
|
resolve_mutation_append_field(source, before_scope, args, aggregate, command)
|
|
@@ -415,6 +499,17 @@ module Hecks
|
|
|
415
499
|
# (`Card` is `Board.entities`, never `Workspace.entities`), the
|
|
416
500
|
# same distinction `EntityElement#appended_to_element`'s own fix
|
|
417
501
|
# draws.
|
|
502
|
+
#
|
|
503
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate, forwarded to
|
|
504
|
+
# `Instance.default_for`
|
|
505
|
+
# @param owner [Bluebook::Aggregate, Bluebook::Entity, nil] the construct whose
|
|
506
|
+
# nested entity `target` may declare
|
|
507
|
+
# @param target [Symbol, nil] the append mutation's own target field name
|
|
508
|
+
# @param fields [Hash{Symbol => Object}] the field map already resolved by
|
|
509
|
+
# `recompute_append`
|
|
510
|
+
# @return [Hash{Symbol => Object}] `fields` unchanged if `target` is nil or does
|
|
511
|
+
# not name a nested entity; otherwise `fields` with every declared attribute it
|
|
512
|
+
# doesn't already hold filled in with its own default
|
|
418
513
|
def fill_recompute_declared_defaults(aggregate, owner, target, fields)
|
|
419
514
|
return fields unless target
|
|
420
515
|
|
|
@@ -430,6 +525,18 @@ module Hecks
|
|
|
430
525
|
fields
|
|
431
526
|
end
|
|
432
527
|
|
|
528
|
+
# Resolves one appended field's own value from its declared source.
|
|
529
|
+
#
|
|
530
|
+
# @param source [Symbol, Object] an append field's own source: an argument name,
|
|
531
|
+
# or a literal value
|
|
532
|
+
# @param before_scope [Hash] the entity element's own before-state
|
|
533
|
+
# @param args [Hash] the step's own raw args
|
|
534
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
|
|
535
|
+
# namespace resolution
|
|
536
|
+
# @param command [Bluebook::Command] the command that declared the mutation
|
|
537
|
+
# @return [Object] `before_scope[source]` if `source` names no caller-supplied
|
|
538
|
+
# arg; `source` unchanged if it is not a Symbol; otherwise the coerced,
|
|
539
|
+
# materialized arg value
|
|
433
540
|
def resolve_mutation_append_field(source, before_scope, args, aggregate, command)
|
|
434
541
|
return source unless source.is_a?(Symbol)
|
|
435
542
|
return before_scope[source] unless args.key?(source)
|
|
@@ -455,6 +562,15 @@ module Hecks
|
|
|
455
562
|
# throughout this whole property, so the eventual `symbolize_deep`
|
|
456
563
|
# comparison is always materialized-against-materialized, never a
|
|
457
564
|
# live `Value` against a Hash.
|
|
565
|
+
#
|
|
566
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
|
|
567
|
+
# namespace resolution
|
|
568
|
+
# @param command [Bluebook::Command] the command whose own declared attribute
|
|
569
|
+
# `source` may name
|
|
570
|
+
# @param source [Symbol] the argument name to coerce
|
|
571
|
+
# @param raw [Object] the raw arg value to coerce
|
|
572
|
+
# @return [Object] `raw`, coerced and materialized, if `source` names a declared
|
|
573
|
+
# attribute of `command`; `raw` unchanged otherwise
|
|
458
574
|
def coerce_recompute_append_arg(aggregate, command, source, raw)
|
|
459
575
|
attribute = command.attribute(source)
|
|
460
576
|
return raw unless attribute
|
|
@@ -463,6 +579,13 @@ module Hecks
|
|
|
463
579
|
end
|
|
464
580
|
|
|
465
581
|
# `MutationApplier#removed`'s own value-equality match, reproduced.
|
|
582
|
+
#
|
|
583
|
+
# @param current [Object] the target field's before-value (an Array of elements)
|
|
584
|
+
# @param source [Symbol, Object] the remove mutation's own source: an argument
|
|
585
|
+
# name, or a literal value already
|
|
586
|
+
# @param args [Hash] the step's own raw args
|
|
587
|
+
# @return [Array] `current` (coerced to an Array) with every element matching the
|
|
588
|
+
# resolved target value removed
|
|
466
589
|
def recompute_remove(current, source, args)
|
|
467
590
|
target = symbolize_deep(resolve_mutation_source(source, args))
|
|
468
591
|
Array(current).reject { |element| symbolize_deep(element) == target }
|
|
@@ -475,6 +598,12 @@ module Hecks
|
|
|
475
598
|
# `current ||= 0` — the same phantom-field fallback #multiply
|
|
476
599
|
# itself already gives (unaffected by this session's #clamp fix,
|
|
477
600
|
# since #multiply never needed one).
|
|
601
|
+
#
|
|
602
|
+
# @param current [Hash, Numeric, nil] the target field's before-value
|
|
603
|
+
# @param amount [Object] the resolved multiplier
|
|
604
|
+
# @return [Hash, Numeric, Symbol] the scaled field/value, or `:unrecomputable` if
|
|
605
|
+
# `amount` is not Numeric or `current` is neither a Hash with a numeric field
|
|
606
|
+
# nor a Numeric
|
|
478
607
|
def recompute_multiply(current, amount)
|
|
479
608
|
return :unrecomputable unless amount.is_a?(Numeric)
|
|
480
609
|
|
|
@@ -498,6 +627,12 @@ module Hecks
|
|
|
498
627
|
# is always a literal `[min, max]`, never an argument reference
|
|
499
628
|
# (MutationApplier's own comment on why `resolve_source` is
|
|
500
629
|
# skipped for clamp) — so nothing here reads `args` for it at all.
|
|
630
|
+
#
|
|
631
|
+
# @param current [Hash, Numeric, nil] the target field's before-value
|
|
632
|
+
# @param bounds [Object] the mutation's own declared `[min, max]` literal
|
|
633
|
+
# @return [Hash, Numeric, Symbol] the clamped field/value, or `:unrecomputable`
|
|
634
|
+
# if `bounds` is not a 2-element Array or `current` is neither a Hash with a
|
|
635
|
+
# numeric field nor a Numeric
|
|
501
636
|
def recompute_clamp(current, bounds)
|
|
502
637
|
return :unrecomputable unless bounds.is_a?(Array) && bounds.size == 2
|
|
503
638
|
|
|
@@ -518,6 +653,11 @@ module Hecks
|
|
|
518
653
|
# `CommandRules::Arithmetic#resolve_source`, reproduced: a
|
|
519
654
|
# mutation's source is either the name of an argument or a
|
|
520
655
|
# literal, told apart by type.
|
|
656
|
+
#
|
|
657
|
+
# @param source [Symbol, Object] an argument name to resolve from `args`, or a
|
|
658
|
+
# literal value already
|
|
659
|
+
# @param args [Hash] the step's own raw args
|
|
660
|
+
# @return [Object] `args[source]` if `source` is a Symbol; `source` unchanged otherwise
|
|
521
661
|
def resolve_mutation_source(source, args)
|
|
522
662
|
source.is_a?(Symbol) ? args[source] : source
|
|
523
663
|
end
|
|
@@ -531,6 +671,10 @@ module Hecks
|
|
|
531
671
|
# both sides are normalized the same way first. Recursive, since
|
|
532
672
|
# an appended/removed element can itself nest a value object
|
|
533
673
|
# (RemoveTag's own `Tag` argument, `{"key"=>..., "value"=>...}`).
|
|
674
|
+
#
|
|
675
|
+
# @param value [Object] a Hash, Array, or scalar to normalize
|
|
676
|
+
# @return [Object] `value` with every Hash key symbolized, recursively through
|
|
677
|
+
# nested Hashes and Arrays; any other value unchanged
|
|
534
678
|
def symbolize_deep(value)
|
|
535
679
|
case value
|
|
536
680
|
when Hash then value.to_h { |key, val| [key.to_sym, symbolize_deep(val)] }
|
|
@@ -54,6 +54,11 @@ module Hecks
|
|
|
54
54
|
# branches shares logic with the others beyond that shared setup.
|
|
55
55
|
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
56
56
|
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
57
|
+
#
|
|
58
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
59
|
+
# @return [true, String] true if every tenant-scoped query answer, and every
|
|
60
|
+
# refusal of one, agrees with `TenantScope.apply`'s own contract; otherwise a
|
|
61
|
+
# message naming the offending query, its args, and what it should have done
|
|
57
62
|
def authorize_scopes_or_refuses(history)
|
|
58
63
|
bluebooks = history.fetch(:bluebooks)
|
|
59
64
|
|
|
@@ -116,6 +121,14 @@ module Hecks
|
|
|
116
121
|
# those; the raised class does not.
|
|
117
122
|
GUARD_REFUSAL_KINDS = %w[Hecks::Runtime::GivenNotMet Hecks::Runtime::EnsuresNotMet].freeze
|
|
118
123
|
|
|
124
|
+
# Checks that every given/ensures refusal a run actually raised names a rule
|
|
125
|
+
# the refusing command actually declares.
|
|
126
|
+
#
|
|
127
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
128
|
+
# @return [true, String] true if every guard refusal's quoted description matches
|
|
129
|
+
# a `given`/`ensures` the refusing command (or its delegation target) declares;
|
|
130
|
+
# otherwise a message naming the refusal and the declared descriptions it does
|
|
131
|
+
# not match
|
|
119
132
|
def guard_refusals_are_declared(history)
|
|
120
133
|
bluebooks = history.fetch(:bluebooks)
|
|
121
134
|
|
|
@@ -190,6 +203,12 @@ module Hecks
|
|
|
190
203
|
# false positive.
|
|
191
204
|
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
192
205
|
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
206
|
+
#
|
|
207
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
208
|
+
# @return [true, String] true if every stored record's `reference_to` attribute
|
|
209
|
+
# points at a record sharing the same tenant value (or has no resolvable
|
|
210
|
+
# target, or neither side declares a tenant field); otherwise a message naming
|
|
211
|
+
# the referencing record, the reference, and the tenant mismatch
|
|
193
212
|
def commands_respect_tenant_scope(history)
|
|
194
213
|
bluebooks = history.fetch(:bluebooks)
|
|
195
214
|
instances = history.fetch(:instances)
|
|
@@ -236,6 +255,10 @@ module Hecks
|
|
|
236
255
|
# ..., tenant:` on any of its own queries — not every aggregate
|
|
237
256
|
# is tenant-scoped, and one that isn't has nothing for this
|
|
238
257
|
# property to check either side of.
|
|
258
|
+
#
|
|
259
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate to look up
|
|
260
|
+
# @return [Symbol, nil] the tenant field name, or nil if no query on the
|
|
261
|
+
# aggregate declares `authorize ..., tenant:`
|
|
239
262
|
def tenant_field_for(aggregate)
|
|
240
263
|
authorization = aggregate.queries.filter_map(&:authorization).find(&:tenant)
|
|
241
264
|
authorization&.tenant&.to_sym
|
|
@@ -267,6 +290,14 @@ module Hecks
|
|
|
267
290
|
# own guards first, then every delegation target's; an offence is
|
|
268
291
|
# only a description neither declares. Found live mining chess's
|
|
269
292
|
# history: every refused move through a door read as undeclared.
|
|
293
|
+
#
|
|
294
|
+
# @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
|
|
295
|
+
# keyed by domain name
|
|
296
|
+
# @param verb [String] the dispatched verb, `"Domain::Aggregate.Command"` or
|
|
297
|
+
# `"Domain::Aggregate.Entity.Command"`
|
|
298
|
+
# @param command [Bluebook::Command] the command `verb` resolves to
|
|
299
|
+
# @return [Array<String>] `command`'s own declared given/ensures descriptions,
|
|
300
|
+
# plus every `delegate`d command's, in that order
|
|
270
301
|
def effective_guard_descriptions(bluebooks, verb, command)
|
|
271
302
|
own = command.guard_descriptions
|
|
272
303
|
delegated = command.mutations.select { |m| m.op == :delegate }.flat_map do |delegation|
|
|
@@ -277,6 +308,14 @@ module Hecks
|
|
|
277
308
|
own + delegated
|
|
278
309
|
end
|
|
279
310
|
|
|
311
|
+
# Resolves a dispatched verb back to the declared command it names.
|
|
312
|
+
#
|
|
313
|
+
# @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
|
|
314
|
+
# keyed by domain name
|
|
315
|
+
# @param verb [String] the dispatched verb, `"Domain::Aggregate.Command"` or
|
|
316
|
+
# `"Domain::Aggregate.Entity.Command"`
|
|
317
|
+
# @return [Bluebook::Command, nil] the declared command, or nil if `verb`'s
|
|
318
|
+
# domain, aggregate, entity, or command is not found among `bluebooks`
|
|
280
319
|
def command_for_verb(bluebooks, verb)
|
|
281
320
|
domain, aggregate_name, command_path = Naming.split_verb(verb)
|
|
282
321
|
return nil unless command_path
|
|
@@ -337,6 +376,11 @@ module Hecks
|
|
|
337
376
|
# different named precondition `"customer is not closed"` instead
|
|
338
377
|
# (a suspended customer must still be freezable), so it is not a
|
|
339
378
|
# `"customer is active"` example, just the same mechanism.
|
|
379
|
+
#
|
|
380
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
381
|
+
# @return [true, String] true if every recomputed `enforce_givens`/
|
|
382
|
+
# `enforce_lifecycle_guard` check agrees with what the real dispatch did;
|
|
383
|
+
# otherwise a message naming the verb and the disagreement
|
|
340
384
|
def lifecycle_guard_and_given_violations_are_refused(history)
|
|
341
385
|
offenders = history.fetch(:guard_checks).filter_map do |check|
|
|
342
386
|
next if check[:recomputed_refused] == check[:actual_refused]
|