hecks 2.6.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/exe/hecks +13 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +1 -1
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- data/lib/hecks/adapters/driven/memory.rb +1 -1
- data/lib/hecks/adapters/driven/postgres.rb +1 -1
- data/lib/hecks/adapters/driven/prism.rb +2 -2
- data/lib/hecks/adapters/driven/sqlite/projection.rb +1 -1
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +31 -10
- data/lib/hecks/behaviors/expectations.rb +2 -2
- data/lib/hecks/bluebook/assembly/contract.rb +1 -1
- data/lib/hecks/bluebook/assembly/contracts.rb +5 -5
- data/lib/hecks/bluebook/assembly/specializer.rb +2 -2
- data/lib/hecks/bluebook/assembly.rb +1 -1
- data/lib/hecks/bluebook/behaviour/chapter.rb +5 -2
- data/lib/hecks/bluebook/behaviour/read_model.rb +18 -0
- data/lib/hecks/bluebook/capabilities.rb +2 -2
- data/lib/hecks/bluebook/chapter.rb +1 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +6 -1
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +3 -3
- data/lib/hecks/bluebook/dsl/entity_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +1 -1
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +14 -9
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -3
- data/lib/hecks/bluebook/dsl/translation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/world_builder.rb +47 -3
- data/lib/hecks/bluebook/hexagon.rb +15 -7
- data/lib/hecks/bluebook/meta_validator/judge.rb +3 -3
- data/lib/hecks/bluebook/meta_validator/plan.rb +4 -4
- data/lib/hecks/bluebook/meta_validator/readings.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/shapes.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +11 -10
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +3 -1
- data/lib/hecks/bluebook/meta_validator.rb +1 -1
- data/lib/hecks/bluebook/model_check/client_profile.rb +4 -70
- data/lib/hecks/bluebook/model_check.rb +13 -14
- data/lib/hecks/cache_dir.rb +103 -0
- data/lib/hecks/cli/document.rb +104 -0
- data/lib/hecks/cli/ir.rb +47 -0
- data/lib/hecks/cli/mcp.rb +32 -0
- data/lib/hecks/cli/mcp_door.rb +461 -0
- data/lib/hecks/cli/model_check.rb +270 -0
- data/lib/hecks/cli/project_cli.rb +123 -0
- data/lib/hecks/cli/project_diagrams.rb +43 -0
- data/lib/hecks/cli/run.rb +219 -0
- data/lib/hecks/cli/smoke_test.rb +59 -0
- data/lib/hecks/cli/stores.rb +54 -0
- data/lib/hecks/cli.rb +166 -0
- data/lib/hecks/embryonaut_bluebook/lock.rb +72 -0
- data/lib/hecks/embryonaut_bluebook/shape.rb +47 -0
- data/lib/hecks/embryonaut_bluebook/vendor.rb +194 -0
- data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +88 -0
- data/lib/hecks/embryonaut_bluebook.rb +29 -3
- data/lib/hecks/facade/handle.rb +2 -2
- data/lib/hecks/facade/surface/aggregate_door.rb +1 -1
- data/lib/hecks/forms/value_object_shape.rb +1 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +3 -3
- data/lib/hecks/language/bluebook/vocabulary.bluebook +16 -0
- data/lib/hecks/language/world/world.bluebook +32 -2
- data/lib/hecks/mcp_door_scope.rb +176 -0
- data/lib/hecks/ports/key_vault.rb +1 -1
- data/lib/hecks/ports/persistence/append_only.rb +1 -1
- data/lib/hecks/ports/persistence/binding_policy.rb +16 -5
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -2
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +132 -0
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +13 -7
- data/lib/hecks/ports/persistence/plugins/era.rb +7 -4
- data/lib/hecks/ports/persistence/postgres_dump/connection.rb +73 -0
- data/lib/hecks/ports/persistence/postgres_dump.rb +125 -0
- data/lib/hecks/ports/persistence/repository_factory.rb +2 -2
- data/lib/hecks/ports/persistence/state_codec.rb +1 -1
- data/lib/hecks/ports/persistence.rb +4 -0
- data/lib/hecks/projections/deploy/fargate/assembly.rb +193 -0
- data/lib/hecks/projections/deploy/fargate/cdn.rb +318 -0
- data/lib/hecks/projections/deploy/fargate/cdn_default.rb +87 -0
- data/lib/hecks/projections/deploy/fargate/check.rb +172 -0
- data/lib/hecks/projections/deploy/fargate/containers.rb +412 -0
- data/lib/hecks/projections/deploy/fargate/extras.rb +268 -0
- data/lib/hecks/projections/deploy/fargate/monitoring.rb +448 -0
- data/lib/hecks/projections/deploy/fargate/settings.rb +235 -0
- data/lib/hecks/projections/deploy/fargate/yaml.rb +116 -0
- data/lib/hecks/projections/deploy/fargate.rb +188 -139
- data/lib/hecks/projections/deploy/lambda.rb +8 -8
- data/lib/hecks/projections/deploy/preview/access_yaml.rb +145 -0
- data/lib/hecks/projections/deploy/preview/containers.rb +218 -0
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +446 -0
- data/lib/hecks/projections/deploy/preview/routing_yaml.rb +216 -0
- data/lib/hecks/projections/deploy/preview/script.rb +40 -0
- data/lib/hecks/projections/deploy/preview/settings.rb +140 -0
- data/lib/hecks/projections/deploy/preview/task_yaml.rb +215 -0
- data/lib/hecks/projections/deploy/preview/template.rb +247 -0
- data/lib/hecks/projections/deploy/preview/yaml_text.rb +48 -0
- data/lib/hecks/projections/deploy/preview.rb +103 -0
- data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +182 -0
- data/lib/hecks/projections/deploy/scripts/expected-era.header +17 -0
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +54 -0
- data/lib/hecks/projections/deploy/scripts/settings.rb +114 -0
- data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +232 -0
- data/lib/hecks/projections/deploy/scripts.rb +171 -0
- data/lib/hecks/projections/deploy/smoke/harness.js +366 -0
- data/lib/hecks/projections/deploy/smoke/workflow.yml.tmpl +56 -0
- data/lib/hecks/projections/deploy/smoke.rb +159 -0
- data/lib/hecks/projections/deploy/template_diff/comparer.rb +231 -0
- data/lib/hecks/projections/deploy/template_diff/loader.rb +105 -0
- data/lib/hecks/projections/deploy/template_diff.rb +173 -0
- data/lib/hecks/projections/glossary/sensitivity.rb +1 -1
- data/lib/hecks/projections/reference.rb +7 -1
- data/lib/hecks/projections.rb +2 -0
- data/lib/hecks/projector/exporter.rb +4 -4
- data/lib/hecks/projector/target.rb +1 -1
- data/lib/hecks/release/gem_pin/published_versions.rb +58 -0
- data/lib/hecks/release/gem_pin.rb +112 -0
- data/lib/hecks/release/runner/ci_publisher.rb +96 -0
- data/lib/hecks/release/runner/commands.rb +68 -0
- data/lib/hecks/release/runner/console.rb +52 -0
- data/lib/hecks/release/runner/gem_publisher.rb +44 -0
- data/lib/hecks/release/runner/git.rb +48 -0
- data/lib/hecks/release/runner/npm_publisher.rb +89 -0
- data/lib/hecks/release/runner/preflight.rb +119 -0
- data/lib/hecks/release/runner/published.rb +55 -0
- data/lib/hecks/release/runner/tagger.rb +87 -0
- data/lib/hecks/release/runner.rb +244 -0
- data/lib/hecks/router/namespace_installer.rb +1 -1
- data/lib/hecks/runtime/command_interpreter.rb +3 -3
- data/lib/hecks/runtime/command_rules/admissibility.rb +2 -2
- data/lib/hecks/runtime/command_rules/arithmetic.rb +2 -2
- data/lib/hecks/runtime/entity_element.rb +1 -1
- data/lib/hecks/runtime/entity_interpreter.rb +1 -1
- data/lib/hecks/runtime/invocation.rb +1 -1
- data/lib/hecks/runtime/loader.rb +27 -3
- data/lib/hecks/runtime/outbox.rb +1 -1
- data/lib/hecks/runtime/reaction_invocation.rb +3 -3
- data/lib/hecks/runtime/read_model_interpreter.rb +37 -15
- data/lib/hecks/runtime/reference_hop.rb +4 -4
- data/lib/hecks/runtime/registry/saga_persistence.rb +8 -2
- data/lib/hecks/runtime/registry/verification.rb +15 -13
- data/lib/hecks/runtime/registry/world_defaults.rb +106 -0
- data/lib/hecks/runtime/registry.rb +22 -13
- data/lib/hecks/runtime/saga_interpreter.rb +1 -1
- data/lib/hecks/runtime/tenant_check.rb +1 -1
- data/lib/hecks/runtime/value/coercion.rb +3 -3
- data/lib/hecks/runtime/value/entity_list_coercion.rb +1 -1
- data/lib/hecks/storehouse.rb +18 -12
- data/lib/hecks/vendoring/git_environment.rb +35 -0
- data/lib/hecks/vendoring/git_source.rb +102 -0
- data/lib/hecks/vendoring.rb +102 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +5 -0
- data/lib/hecks.rb +8 -2
- metadata +72 -58
- data/lib/hecks/bench/cli.rb +0 -74
- data/lib/hecks/bench/environment.rb +0 -100
- data/lib/hecks/bench/postgres_probe.rb +0 -41
- data/lib/hecks/bench/report.rb +0 -116
- data/lib/hecks/bench/ruby_runner.rb +0 -151
- data/lib/hecks/bench/run.rb +0 -55
- data/lib/hecks/bench/rust_runner.rb +0 -165
- data/lib/hecks/bench/stats.rb +0 -75
- data/lib/hecks/bench/suite.rb +0 -172
- data/lib/hecks/bench/workload.rb +0 -137
- data/lib/hecks/bench.rb +0 -28
- data/lib/hecks/codemod.rb +0 -443
- data/lib/hecks/corpus.rb +0 -479
- data/lib/hecks/doc/reference.rb +0 -585
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
- data/lib/hecks/fuzzing/combination_miner.rb +0 -178
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
- data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
- data/lib/hecks/fuzzing/differential.rb +0 -192
- data/lib/hecks/fuzzing/domain_generator.rb +0 -871
- data/lib/hecks/fuzzing/era_boundary.rb +0 -154
- data/lib/hecks/fuzzing/form_census.rb +0 -285
- data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
- data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
- data/lib/hecks/fuzzing/isolated_boot.rb +0 -604
- data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
- data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
- data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
- data/lib/hecks/fuzzing/properties/guards.rb +0 -398
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
- data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
- data/lib/hecks/fuzzing/properties/querying.rb +0 -260
- data/lib/hecks/fuzzing/properties.rb +0 -267
- data/lib/hecks/fuzzing/qa_settings.rb +0 -164
- data/lib/hecks/fuzzing/replay.rb +0 -930
- data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
- data/lib/hecks/fuzzing/self_consistency.rb +0 -825
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
- data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
- data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
- data/lib/hecks/fuzzing/shrinker.rb +0 -309
- data/lib/hecks/fuzzing/structural_skips.rb +0 -53
- data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
- data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
- data/lib/hecks/fuzzing/value_generator.rb +0 -340
- data/lib/hecks/fuzzing.rb +0 -23
- data/lib/hecks/grammar/evolve.rb +0 -524
- data/lib/hecks/query_ir.rb +0 -482
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
module Hecks
|
|
2
|
-
module Fuzzing
|
|
3
|
-
# The one declared set of `FIELDS` that leave a comparison — partition,
|
|
4
|
-
# not filter. Every comparison in lib/hecks/fuzzing that drops a field
|
|
5
|
-
# before comparing two histories names a group here instead of writing
|
|
6
|
-
# its own literal `except(...)` list, so the reason for dropping a field
|
|
7
|
-
# is written exactly once, beside the field.
|
|
8
|
-
#
|
|
9
|
-
# Two specs hold this set honest (spec/fuzzing/nondeterministic_spec.rb):
|
|
10
|
-
# a literal except-list naming any of these fields anywhere under
|
|
11
|
-
# lib/hecks/fuzzing fails the build (the tolerance must be declared
|
|
12
|
-
# here, not re-derived at a call site), and every declared field must
|
|
13
|
-
# actually be produced where its group says — a field nothing emits any
|
|
14
|
-
# more is a stale tolerance and fails too.
|
|
15
|
-
#
|
|
16
|
-
# Groups are keyed by the shape the field rides on, since the same name
|
|
17
|
-
# can be compared on one surface and dropped on another:
|
|
18
|
-
#
|
|
19
|
-
# - `query_row` — one entry of `Replay.call`'s `:queries`.
|
|
20
|
-
# - `outbox_row` — one row of an `:outbox_traces` entry's `:rows`.
|
|
21
|
-
# - `event` — an event hash in full `Event#to_h` form (an outbox
|
|
22
|
-
# row's `:event`, or a Rust binary's `events` entries,
|
|
23
|
-
# string-keyed there).
|
|
24
|
-
# - `history` — the top-level `Replay.call` history hash itself.
|
|
25
|
-
module Nondeterministic
|
|
26
|
-
FIELDS = {
|
|
27
|
-
query_row: {
|
|
28
|
-
instances_at: "a full state snapshot taken for the query oracle's own use " \
|
|
29
|
-
"(Properties::Querying); the same state is already compared as `instances`, " \
|
|
30
|
-
"so keeping it would re-report one instances divergence under every query step"
|
|
31
|
-
}.freeze,
|
|
32
|
-
outbox_row: {
|
|
33
|
-
event_uid: "`Runtime::Outbox::Fanout#rows_for`'s own SecureRandom.uuid, minted fresh per " \
|
|
34
|
-
"enqueue and kept off `Event#to_h` — relay bookkeeping, never something the " \
|
|
35
|
-
"domain produced, so two otherwise-identical replays carry two different uuids",
|
|
36
|
-
delivery_id: "`\"\#{event_uid}/\#{consumer}\"` — inherits event_uid's per-enqueue uuid"
|
|
37
|
-
}.freeze,
|
|
38
|
-
event: {
|
|
39
|
-
occurred_at: "a wall-clock read; never reproducible byte-for-byte between two independent " \
|
|
40
|
-
"runs (Replay's own projected `:events` leave it off for the same reason)"
|
|
41
|
-
}.freeze,
|
|
42
|
-
history: {
|
|
43
|
-
bluebook: "a live IR object (the first-loaded bluebook) handed to properties — an object " \
|
|
44
|
-
"identity, not a value, so two boots never compare equal on it",
|
|
45
|
-
bluebooks: "the full live IR map, keyed by domain — object identities, same as `bluebook`"
|
|
46
|
-
}.freeze
|
|
47
|
-
}.freeze
|
|
48
|
-
|
|
49
|
-
module_function
|
|
50
|
-
|
|
51
|
-
# Lists the nondeterministic field names declared for one comparison group.
|
|
52
|
-
#
|
|
53
|
-
# @param group [Symbol] a key of `FIELDS`, such as `:query_row` or `:event`
|
|
54
|
-
# @return [Array<Symbol>] the field names declared for `group`
|
|
55
|
-
# @raise [KeyError] if `group` names no declared group
|
|
56
|
-
def names(group)
|
|
57
|
-
FIELDS.fetch(group).keys
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# Every declared name, across every group.
|
|
61
|
-
#
|
|
62
|
-
# @return [Array<Symbol>] the union of every group's field names, deduplicated
|
|
63
|
-
def all_names
|
|
64
|
-
FIELDS.values.flat_map(&:keys).uniq
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# `hash` without `group`'s fields — symbol keys, the shape every
|
|
68
|
-
# Ruby-side history carries.
|
|
69
|
-
#
|
|
70
|
-
# @param hash [Hash] a symbol-keyed row, event, or history hash to compare
|
|
71
|
-
# @param group [Symbol] a key of `FIELDS` naming which fields to drop
|
|
72
|
-
# @return [Hash] `hash` with `group`'s declared fields removed
|
|
73
|
-
# @raise [KeyError] if `group` names no declared group
|
|
74
|
-
def strip(hash, group)
|
|
75
|
-
hash.except(*names(group))
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
require "json"
|
|
2
|
-
require_relative "replay"
|
|
3
|
-
require_relative "nondeterministic"
|
|
4
|
-
|
|
5
|
-
module Hecks
|
|
6
|
-
module Fuzzing
|
|
7
|
-
# ## The second differential axis
|
|
8
|
-
#
|
|
9
|
-
# bin/qa_sweep's own `diff_ruby_vs_rust`
|
|
10
|
-
# compares two different engines (the Ruby interpreter vs the compiled
|
|
11
|
-
# Rust kernel) against the same persistence (Memory, always — see
|
|
12
|
-
# `SequenceGenerator`'s own header: sequence generation stays
|
|
13
|
-
# Memory-only, and `IsolatedBoot`'s own header explains why every
|
|
14
|
-
# existing fuzz/replay path structurally cannot reach a real Postgres-
|
|
15
|
-
# bound domain's own SQL compilation). This module compares the
|
|
16
|
-
# opposite pairing: the same one Ruby engine, against two different
|
|
17
|
-
# persistences — Memory (the reference, exactly as fast and as
|
|
18
|
-
# deterministic as every other fuzz check) and a real, disposable
|
|
19
|
-
# `PostgresEra` database (the actual SQL compute/rekey/query-pushdown
|
|
20
|
-
# path a Postgres-bound domain's users really hit).
|
|
21
|
-
#
|
|
22
|
-
# `examples/directory` is why this exists at all: the one domain in
|
|
23
|
-
# this corpus with a real `compute`/`rekey` translation edge
|
|
24
|
-
# (docs/prds/02-fuzzer-real-adapters.md's own "What shipped" section
|
|
25
|
-
# names it directly), which no existing fuzz/replay/property check has
|
|
26
|
-
# ever been able to exercise against real PostgresEra SQL — every one
|
|
27
|
-
# of them boots through `IsolatedBoot`, and until `adapter:
|
|
28
|
-
# :postgres_era` existed (isolated_boot.rb's own header), that meant
|
|
29
|
-
# Memory, unconditionally, no matter what `directory.world` itself
|
|
30
|
-
# declares.
|
|
31
|
-
#
|
|
32
|
-
# ## A separate axis, not a replacement
|
|
33
|
-
#
|
|
34
|
-
# Not a replacement for `bin/qa_sweep`'s own Ruby-vs-Rust differential
|
|
35
|
-
# mode — a genuinely separate axis, opt-in (`--persistence-parity`),
|
|
36
|
-
# because this one pays for a real `PG.connect` and real SQL per
|
|
37
|
-
# dispatch where Memory-vs-Rust pays for neither. See `bin/qa_sweep`'s
|
|
38
|
-
# own `--persistence-parity` handling for the seed-count dial that
|
|
39
|
-
# keeps that cost bounded.
|
|
40
|
-
#
|
|
41
|
-
# ## `left:`/`right:`
|
|
42
|
-
#
|
|
43
|
-
# Generalized — originally hardcoded to Memory vs
|
|
44
|
-
# PostgresEra (the only pairing that existed), now any two of
|
|
45
|
-
# `IsolatedBoot`'s own adapter symbols (`:memory`, `:sqlite`,
|
|
46
|
-
# `:postgres`, `:postgres_era`). Defaults preserve the original
|
|
47
|
-
# pairing exactly, so every existing caller (this file's own spec,
|
|
48
|
-
# `bin/qa_sweep`'s `--persistence-parity`) is unchanged. The second
|
|
49
|
-
# pairing this generalization exists for is Memory vs SQLite
|
|
50
|
-
# (`QualityControlDials::ADAPTER_PARITY_PAIRS`, `bin/qa_sweep`'s own
|
|
51
|
-
# `adapter_parity_sqlite` mode) — `:sqlite` is nearly as cheap as
|
|
52
|
-
# Memory itself (`IsolatedBoot#rebind_to_sqlite!`'s own header: an
|
|
53
|
-
# on-disk file, no server, no disposable database/schema lifecycle to
|
|
54
|
-
# own), so that pairing folds straight into the ordinary per-seed
|
|
55
|
-
# loop instead of needing a deferred wave of its own the way
|
|
56
|
-
# PostgresEra does.
|
|
57
|
-
#
|
|
58
|
-
# ## `database:`/`schema:`
|
|
59
|
-
#
|
|
60
|
-
# Required only when `:postgres_era` is one of
|
|
61
|
-
# the two adapters (the caller — today, only `bin/qa_sweep` — owns the
|
|
62
|
-
# disposable database's whole lifecycle: created before the sweep,
|
|
63
|
-
# dropped after — see that script's own comment, and the discipline
|
|
64
|
-
# `IsolatedBoot#rebind_to_postgres_era!` itself refuses to let a
|
|
65
|
-
# caller skip); every other adapter ignores both, same as `Replay.
|
|
66
|
-
# call`/`IsolatedBoot.call` already do for their own `adapter:`.
|
|
67
|
-
module PersistenceParity
|
|
68
|
-
module_function
|
|
69
|
-
|
|
70
|
-
# The same six fields `bin/qa_sweep`'s own `diff_ruby_vs_rust`
|
|
71
|
-
# compares (its own comment: "instances, events, refusals, queries,
|
|
72
|
-
# sagas, reactions") — deliberately the identical set, so a report
|
|
73
|
-
# this mode produces reads exactly like the sibling mode's own,
|
|
74
|
-
# differing only in which two things were compared, not in what
|
|
75
|
-
# "found something" means.
|
|
76
|
-
#
|
|
77
|
-
# Simpler normalization than `diff_ruby_vs_rust`, on purpose — that
|
|
78
|
-
# method reduces Rust's own JSON-over-stdout output to "wire
|
|
79
|
-
# precision" and filters known Ruby/Rust structural gaps, because
|
|
80
|
-
# it is comparing two genuinely different engines that are allowed
|
|
81
|
-
# to differ in already-catalogued, understood ways. Both sides here
|
|
82
|
-
# are the same Ruby engine (`Replay.call`, called twice, adapter
|
|
83
|
-
# only) — there is no second engine's own known-gap catalogue to
|
|
84
|
-
# filter against, so any real difference is the finding. Both
|
|
85
|
-
# results still round-trip through `JSON.generate`/`JSON.parse`
|
|
86
|
-
# before comparing, matching `diff_ruby_vs_rust`'s own discipline —
|
|
87
|
-
# not because either side needs a wire-format reduction, but so
|
|
88
|
-
# `Hash#==`/`Array#==` compares plain, JSON-shaped data on both
|
|
89
|
-
# sides identically (a `Runtime::Value`, a `Symbol` key, a `Time`
|
|
90
|
-
# nobody asked for — none of that survives an accidental leak into
|
|
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
|
|
107
|
-
def diff(domain_path, steps, left: :memory, right: :postgres_era, database: nil, schema: nil)
|
|
108
|
-
left_result = Replay.call(domain_path, steps, adapter: left, database: database, schema: schema)
|
|
109
|
-
right_result = Replay.call(domain_path, steps, adapter: right, database: database, schema: schema)
|
|
110
|
-
|
|
111
|
-
divergences = []
|
|
112
|
-
divergences.concat(diff_instances(left_result, right_result, left, right))
|
|
113
|
-
divergences.concat(diff_events(left_result, right_result, left, right))
|
|
114
|
-
divergences.concat(diff_refusals(left_result, right_result, left, right))
|
|
115
|
-
divergences.concat(diff_queries(left_result, right_result, left, right))
|
|
116
|
-
divergences.concat(diff_sagas(left_result, right_result, left, right))
|
|
117
|
-
divergences.concat(diff_reactions(left_result, right_result, left, right))
|
|
118
|
-
divergences
|
|
119
|
-
end
|
|
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
|
|
128
|
-
def as_json(value) = JSON.parse(JSON.generate(value))
|
|
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
|
|
140
|
-
def diff_instances(left_result, right_result, left, right)
|
|
141
|
-
l = as_json(left_result[:instances])
|
|
142
|
-
r = as_json(right_result[:instances])
|
|
143
|
-
return [] if l == r
|
|
144
|
-
|
|
145
|
-
[{ field: "instances", left => l, right => r }]
|
|
146
|
-
end
|
|
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
|
|
158
|
-
def diff_events(left_result, right_result, left, right)
|
|
159
|
-
l = as_json(left_result[:events])
|
|
160
|
-
r = as_json(right_result[:events])
|
|
161
|
-
return [] if l == r
|
|
162
|
-
|
|
163
|
-
[{ field: "events", left => l, right => r }]
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
# Compares both sides' refusals.
|
|
167
|
-
#
|
|
168
|
-
# `verb:`/`kind:` normalized to plain strings the same way
|
|
169
|
-
# `diff_ruby_vs_rust`'s own `ruby_refusals` mapping does — both
|
|
170
|
-
# sides here already answer strings (`Replay#refusal_kind` always
|
|
171
|
-
# returns one), so this is belt-and-suspenders consistency with the
|
|
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
|
|
182
|
-
def diff_refusals(left_result, right_result, left, right)
|
|
183
|
-
normalize = lambda do |refusals|
|
|
184
|
-
refusals.map { |r| { "verb" => r[:verb].to_s, "kind" => r[:kind].to_s, "error" => r[:error] } }
|
|
185
|
-
end
|
|
186
|
-
l = normalize.call(left_result[:refusals])
|
|
187
|
-
r = normalize.call(right_result[:refusals])
|
|
188
|
-
return [] if l == r
|
|
189
|
-
|
|
190
|
-
[{ field: "refusals", left => l, right => r }]
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
# Compares both sides' query answers.
|
|
194
|
-
#
|
|
195
|
-
# `Nondeterministic`'s `query_row` group dropped from every entry —
|
|
196
|
-
# the same group `Differential.diff` drops, for the reason declared
|
|
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
|
|
207
|
-
def diff_queries(left_result, right_result, left, right)
|
|
208
|
-
strip = ->(rows) { rows.map { |row| Nondeterministic.strip(row, :query_row) } }
|
|
209
|
-
l = as_json(strip.call(left_result[:queries]))
|
|
210
|
-
r = as_json(strip.call(right_result[:queries]))
|
|
211
|
-
return [] if l == r
|
|
212
|
-
|
|
213
|
-
[{ field: "queries", left => l, right => r }]
|
|
214
|
-
end
|
|
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
|
|
226
|
-
def diff_sagas(left_result, right_result, left, right)
|
|
227
|
-
l = as_json(left_result[:sagas])
|
|
228
|
-
r = as_json(right_result[:sagas])
|
|
229
|
-
return [] if l == r
|
|
230
|
-
|
|
231
|
-
[{ field: "sagas", left => l, right => r }]
|
|
232
|
-
end
|
|
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
|
|
244
|
-
def diff_reactions(left_result, right_result, left, right)
|
|
245
|
-
l = as_json(left_result[:reactions])
|
|
246
|
-
r = as_json(right_result[:reactions])
|
|
247
|
-
return [] if l == r
|
|
248
|
-
|
|
249
|
-
[{ field: "reactions", left => l, right => r }]
|
|
250
|
-
end
|
|
251
|
-
end
|
|
252
|
-
end
|
|
253
|
-
end
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
module Hecks
|
|
2
|
-
module Fuzzing
|
|
3
|
-
module Properties
|
|
4
|
-
# Angle-9 — `corrects` (retroactive correction) had exactly one
|
|
5
|
-
# declaration anywhere in the corpus (`examples/banking/bluebook/
|
|
6
|
-
# deposit_accounts.bluebook:353`, aggregate-level) and no property
|
|
7
|
-
# anywhere in this file ever checked it, and no `FEATURE_COVERAGE`
|
|
8
|
-
# claim either — confirmed absent before this file existed. `qa/
|
|
9
|
-
# stress_domains/corrections` gives it its first real coverage; see
|
|
10
|
-
# that domain's own NOTES.md for what it found (an entity-level
|
|
11
|
-
# `corrects` crashes Ruby at dispatch outright, and Rust's own
|
|
12
|
-
# generated code has no admissibility check for it at all — neither
|
|
13
|
-
# engine can be compared on the untested combination this property
|
|
14
|
-
# was written to watch, which is itself the headline finding).
|
|
15
|
-
module Corrections
|
|
16
|
-
# Every event named by a `corrects` mutation, on any command a
|
|
17
|
-
# booted history's own bluebooks declare — aggregate-level, or
|
|
18
|
-
# nested inside an entity at any depth (`each_command_including_
|
|
19
|
-
# entities`, below, is the same recursive walk `SequenceGenerator::
|
|
20
|
-
# Catalog#each_entity_chain` already uses, for the identical reason:
|
|
21
|
-
# `Aggregate#entities`/`Entity#entities` nest, ADR 0026, S17). For
|
|
22
|
-
# every such command, every event this history actually recorded
|
|
23
|
-
# under one of the command's own `emits` names must have an event
|
|
24
|
-
# named by the command's own `corrects` target — same aggregate-
|
|
25
|
-
# qualified name, same id — appearing strictly earlier in the same
|
|
26
|
-
# history.
|
|
27
|
-
#
|
|
28
|
-
# `history[:events]` is already in occurrence order (`Replay.call`'s
|
|
29
|
-
# own `runtime.events`, appended as each step dispatches) — "earlier"
|
|
30
|
-
# is therefore "earlier in this array," no timestamp comparison
|
|
31
|
-
# needed, and no per-step attribution back to which command produced
|
|
32
|
-
# which event is needed either: an event's own declared name already
|
|
33
|
-
# identifies the one command in its aggregate that can produce it
|
|
34
|
-
# (`AggregateBuilder::Sealing#seal_correction_targets`'s own
|
|
35
|
-
# `emitted_by` hash reads the identical fact, one level shallower).
|
|
36
|
-
#
|
|
37
|
-
# Why this cannot be `GUARANTEED_BY_CONSTRUCTION` the way the
|
|
38
|
-
# aggregate-level case almost is: `CommandRules::Admissibility#
|
|
39
|
-
# enforce_correction_target` (the dispatch-time check) and
|
|
40
|
-
# `AggregateBuilder::Sealing#seal_correction_targets` (the build-time
|
|
41
|
-
# check) both exist only for an aggregate-level `corrects` —
|
|
42
|
-
# `EntityInterpreter#step_enforce_givens` never calls the former at
|
|
43
|
-
# all, and the latter walks only `@commands` (the aggregate's own
|
|
44
|
-
# top-level list), never `@entities`. An entity-level `corrects`
|
|
45
|
-
# mutation is invisible to both doors today — this property is the
|
|
46
|
-
# only thing anywhere, on either engine, that would ever catch one
|
|
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
|
|
53
|
-
def corrections_reference_an_emitted_event(history)
|
|
54
|
-
violations = []
|
|
55
|
-
|
|
56
|
-
(history[:bluebooks] || {}).each do |domain, bluebook|
|
|
57
|
-
bluebook.aggregates.each do |aggregate|
|
|
58
|
-
aggregate_key = "#{domain}::#{aggregate.hecks_name}"
|
|
59
|
-
|
|
60
|
-
each_command_including_entities(aggregate) do |command|
|
|
61
|
-
corrects_mutations = command.mutations.select { |mutation| mutation.op == :corrects }
|
|
62
|
-
next if corrects_mutations.empty?
|
|
63
|
-
|
|
64
|
-
corrects_mutations.each do |mutation|
|
|
65
|
-
corrected_event = mutation.target.to_s
|
|
66
|
-
|
|
67
|
-
command.emits.each do |produced_event_name|
|
|
68
|
-
violations.concat(unmatched_corrections(history[:events], aggregate_key,
|
|
69
|
-
produced_event_name.to_s, corrected_event,
|
|
70
|
-
command.hecks_name))
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
violations.empty? || violations.uniq.join("; ")
|
|
78
|
-
end
|
|
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
|
|
91
|
-
def unmatched_corrections(events, aggregate_key, produced_event_name, corrected_event, command_name)
|
|
92
|
-
own_events = events.each_with_index.select do |event, _index|
|
|
93
|
-
event[:name] == produced_event_name && event[:aggregate] == aggregate_key
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
own_events.filter_map do |event, index|
|
|
97
|
-
preceding = events.first(index)
|
|
98
|
-
next if preceding.any? do |earlier|
|
|
99
|
-
earlier[:name] == corrected_event && earlier[:aggregate] == aggregate_key &&
|
|
100
|
-
earlier[:id].to_s == event[:id].to_s
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
"#{command_name} (#{aggregate_key}##{event[:id]}) emitted #{produced_event_name}, claiming to " \
|
|
104
|
-
"correct #{corrected_event}, but no #{corrected_event} for the same aggregate/id appears " \
|
|
105
|
-
"earlier in this history"
|
|
106
|
-
end
|
|
107
|
-
end
|
|
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]
|
|
118
|
-
def each_command_including_entities(owner, &block)
|
|
119
|
-
owner.commands.each(&block)
|
|
120
|
-
owner.entities.each { |entity| each_command_including_entities(entity, &block) }
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
end
|