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,180 +0,0 @@
|
|
|
1
|
-
require_relative "../../bluebook/expression/evaluator"
|
|
2
|
-
|
|
3
|
-
module Hecks
|
|
4
|
-
module Fuzzing
|
|
5
|
-
module Properties
|
|
6
|
-
# The transactional outbox's own contract (`Runtime::Outbox`, that
|
|
7
|
-
# file's own header), held to the history a replay actually produced
|
|
8
|
-
# rather than trusted. Two facts, each independently checkable from
|
|
9
|
-
# `history[:outbox_traces]` (`Replay#call`'s own before/after
|
|
10
|
-
# capture, one entry per step whose dispatch enqueued at least one
|
|
11
|
-
# row):
|
|
12
|
-
#
|
|
13
|
-
# ## The two checks
|
|
14
|
-
#
|
|
15
|
-
# 1. "delivery is inline by default" — a row this replay's own
|
|
16
|
-
# dispatch enqueued must not still be `pending`/`claimed` once
|
|
17
|
-
# that same call returns (nothing here ever simulates a
|
|
18
|
-
# crash), and must not be `failed` either — `deliver_row`'s
|
|
19
|
-
# own rescue only reaches `failed` for a genuine defect in the
|
|
20
|
-
# relay's own consumer resolution (a row naming a policy/
|
|
21
|
-
# process_manager `run_consumer`'s own independent registry
|
|
22
|
-
# lookup cannot find — `WiringError`), never an ordinary
|
|
23
|
-
# domain refusal (`PolicyInterpreter#deliver`/`SagaInterpreter#
|
|
24
|
-
# advance` both rescue those themselves, recording `delivered:
|
|
25
|
-
# false` on the reaction/saga log and letting `run_consumer`
|
|
26
|
-
# return normally). Both checked for every row, `saga:` and
|
|
27
|
-
# `policy:` alike.
|
|
28
|
-
#
|
|
29
|
-
# 2. A `policy:` row specifically — `PolicyInterpreter#deliver`
|
|
30
|
-
# returns `nil` (no `reaction_log` entry appended at all)
|
|
31
|
-
# exactly when its own `where` (or, for a fan-out policy, the
|
|
32
|
-
# same `where`, gating the whole `for_each`) does not hold;
|
|
33
|
-
# every other outcome (delivered, refused, a defect,
|
|
34
|
-
# reaction-depth-reached) is still a non-nil record `#react`
|
|
35
|
-
# appends. So a `delivered` policy row with no matching
|
|
36
|
-
# `reaction_log` entry is legitimate only when that policy's
|
|
37
|
-
# own `where`, independently re-evaluated here against the
|
|
38
|
-
# row's own recorded event, genuinely does not hold. A
|
|
39
|
-
# `for_each` policy's own fan-out correctness (how many rows
|
|
40
|
-
# it should have dispatched to) is `fanout_dispatches_once_
|
|
41
|
-
# per_matching_row`'s job, not this one's.
|
|
42
|
-
#
|
|
43
|
-
# ## Why a `saga:` row is exempt from check 2
|
|
44
|
-
#
|
|
45
|
-
# Deliberately: `Fanout.sagas`' own `listens?` (starts_on/ends_on/
|
|
46
|
-
# handler_for matching the event name alone) says nothing about
|
|
47
|
-
# whether a correlation resolves or a live instance exists, and
|
|
48
|
-
# `begin_saga`/`end_saga` (saga_interpreter.rb) both have silent,
|
|
49
|
-
# perfectly ordinary no-op paths that append nothing to `saga_log`
|
|
50
|
-
# — `begin_saga` when an instance under that correlation already
|
|
51
|
-
# exists, `end_saga` when no live instance exists to end (an
|
|
52
|
-
# `AccountOpened` fired by opening an account directly, bypassing
|
|
53
|
-
# the onboarding flow whose `ends_on` names that same event,
|
|
54
|
-
# reproduces this exactly: `Fanout.listens?` enqueues the row
|
|
55
|
-
# because the event name matches `ends_on`, `end_saga` finds
|
|
56
|
-
# nothing under that correlation to delete, and neither logs a
|
|
57
|
-
# word). A `saga:` row draining to `delivered` with zero matching
|
|
58
|
-
# `saga_log` entries is therefore not a finding — only check 1
|
|
59
|
-
# applies to it.
|
|
60
|
-
#
|
|
61
|
-
# ## Not a grammar construct
|
|
62
|
-
#
|
|
63
|
-
# `FEATURE_COVERAGE`'s own `dry_runs_leave_no_trace` precedent: the
|
|
64
|
-
# outbox is a runtime door (`Runtime::Outbox`), not a word a
|
|
65
|
-
# bluebook declares, so there is no feature string here to claim.
|
|
66
|
-
module Outbox
|
|
67
|
-
# Checks every outbox row a replay's history recorded against the
|
|
68
|
-
# outbox's own contract (see this file's own header).
|
|
69
|
-
#
|
|
70
|
-
# @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
|
|
71
|
-
# @return [true, String] true if every outbox row satisfies the contract; otherwise
|
|
72
|
-
# a semicolon-joined message naming each offending row
|
|
73
|
-
def outbox_rows_match_reactions(history)
|
|
74
|
-
bluebooks = history.fetch(:bluebooks, {})
|
|
75
|
-
|
|
76
|
-
offenders = Array(history[:outbox_traces]).flat_map do |trace|
|
|
77
|
-
trace[:rows].flat_map { |row| outbox_row_offenders(row, trace, bluebooks) }
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
offenders.empty? || offenders.join("; ")
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Checks one outbox row against the outbox's own contract.
|
|
84
|
-
#
|
|
85
|
-
# @param row [Hash] one `Runtime::Outbox::Row#to_h` entry, `trace[:rows]`'s shape:
|
|
86
|
-
# at least `:status`, `:delivery_id`, `:consumer`, `:event`, and (when failed)
|
|
87
|
-
# `:error`
|
|
88
|
-
# @param trace [Hash] this row's outbox trace entry, one of `history[:outbox_traces]`,
|
|
89
|
-
# carrying `:rows` and `:reactions`
|
|
90
|
-
# @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
|
|
91
|
-
# keyed by domain name
|
|
92
|
-
# @return [Array<String>] zero or one offending message naming what this row got
|
|
93
|
-
# wrong; empty when the row is fine or its status names no check
|
|
94
|
-
def outbox_row_offenders(row, trace, bluebooks)
|
|
95
|
-
on = row.dig(:event, :name)
|
|
96
|
-
|
|
97
|
-
case row[:status]
|
|
98
|
-
when "pending", "claimed"
|
|
99
|
-
["outbox row #{row[:delivery_id]} (#{row[:consumer]} on #{on}) never drained inline — status stayed " \
|
|
100
|
-
"#{row[:status].inspect} though delivery is inline by contract (Runtime::Outbox's own header)"]
|
|
101
|
-
when "failed"
|
|
102
|
-
["outbox row #{row[:delivery_id]} (#{row[:consumer]} on #{on}) failed to deliver: #{row[:error]} — " \
|
|
103
|
-
"a domain refusal never reaches this far; a failed row names a defect in the relay's own consumer " \
|
|
104
|
-
"resolution"]
|
|
105
|
-
when "delivered"
|
|
106
|
-
outbox_delivered_policy_offenders(row, on, trace, bluebooks)
|
|
107
|
-
else
|
|
108
|
-
[]
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
# Checks one delivered `policy:` outbox row for a missing `reaction_log` entry.
|
|
113
|
-
#
|
|
114
|
-
# See this file's own header for why a `saga:` row is exempt: its
|
|
115
|
-
# own `listens?` gives no such guarantee, unlike a policy's single,
|
|
116
|
-
# deterministic `where` gate.
|
|
117
|
-
#
|
|
118
|
-
# @param row [Hash] the delivered outbox row being checked
|
|
119
|
-
# @param on [String] the row's event name, `row.dig(:event, :name)`
|
|
120
|
-
# @param trace [Hash] this row's outbox trace entry, carrying `:reactions`
|
|
121
|
-
# @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
|
|
122
|
-
# keyed by domain name
|
|
123
|
-
# @return [Array<String>] zero or one offending message; empty when the row is
|
|
124
|
-
# not a policy row, the policy is undeclared or fans out, or the policy's
|
|
125
|
-
# `where` does not independently re-evaluate true
|
|
126
|
-
def outbox_delivered_policy_offenders(row, on, trace, bluebooks)
|
|
127
|
-
kind, fqn = row[:consumer].to_s.split(":", 2)
|
|
128
|
-
return [] unless kind == "policy"
|
|
129
|
-
|
|
130
|
-
home, name = fqn.to_s.split("::", 2)
|
|
131
|
-
return [] if trace[:reactions].any? { |entry| entry[:policy] == name && entry[:on] == on }
|
|
132
|
-
|
|
133
|
-
policy = bluebooks[home]&.policies&.find { |candidate| candidate.name == name }
|
|
134
|
-
# Nothing declared under this name — inconclusive, not a claimed mismatch.
|
|
135
|
-
return [] unless policy
|
|
136
|
-
# Fan-out row count is fanout_dispatches_once_per_matching_row's job.
|
|
137
|
-
return [] if policy.fans_out?
|
|
138
|
-
|
|
139
|
-
held = independently_re_evaluate_policy_where(policy, row[:event])
|
|
140
|
-
# False, or inconclusive (the where itself raised) — never a claimed mismatch.
|
|
141
|
-
return [] if held != true
|
|
142
|
-
|
|
143
|
-
["outbox row #{row[:delivery_id]} (#{row[:consumer]} on #{on}) drained as delivered, but no matching " \
|
|
144
|
-
"reaction_log entry exists and the policy's own where clause independently re-evaluates true — " \
|
|
145
|
-
"PolicyInterpreter#deliver only ever returns nil (no reaction_log entry) when where does not hold"]
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
# `PolicyInterpreter#where_holds?`'s own two branches, reproduced —
|
|
149
|
-
# never calling that method again, which would only ever agree
|
|
150
|
-
# with itself (the same rule `resolve_dispatch_binding`'s own
|
|
151
|
-
# comment states). `Evaluator.call` (the raw-string entry, parsed
|
|
152
|
-
# and cached — never `call_rule`, which needs the policy's own
|
|
153
|
-
# build-time `where_rule` AST, an object this history has no
|
|
154
|
-
# reason to carry) is the exact same call `Replay#fan_out_finding`
|
|
155
|
-
# already makes for the identical fact one property over
|
|
156
|
-
# (`policy.where.to_s.empty? || Evaluator.call(policy.where, {},
|
|
157
|
-
# payload)`), reused rather than re-derived a second, slightly
|
|
158
|
-
# different way. `rescue`d to `nil`, not `false`: a where clause
|
|
159
|
-
# that cannot be re-evaluated from the row's own recorded payload
|
|
160
|
-
# alone is inconclusive, not proof either way — the same "never a
|
|
161
|
-
# claimed pass or a claimed mismatch from a resolution this replay
|
|
162
|
-
# cannot actually reproduce" discipline `build_guard_check`'s own
|
|
163
|
-
# rescue clause already follows.
|
|
164
|
-
#
|
|
165
|
-
# @param policy [Bluebook::Policy] the policy whose `where` clause is re-evaluated
|
|
166
|
-
# @param event [Hash] the outbox row's own recorded event, read for `:payload`
|
|
167
|
-
# @return [Boolean, nil] whether `policy`'s `where` holds against `event`'s payload,
|
|
168
|
-
# or nil if the where clause cannot be re-evaluated from it
|
|
169
|
-
def independently_re_evaluate_policy_where(policy, event)
|
|
170
|
-
return true if policy.where.to_s.empty?
|
|
171
|
-
|
|
172
|
-
payload = (event[:payload] || {}).transform_keys(&:to_sym)
|
|
173
|
-
Bluebook::Expression::Evaluator.call(policy.where, {}, payload)
|
|
174
|
-
rescue StandardError
|
|
175
|
-
nil
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
end
|
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
module Hecks
|
|
2
|
-
module Fuzzing
|
|
3
|
-
module Properties
|
|
4
|
-
# Query-answering properties: a query's own answer matches a from-
|
|
5
|
-
# scratch reference recomputation, paging partitions its rows the same
|
|
6
|
-
# way, and the shared query/paging helpers both lean on.
|
|
7
|
-
|
|
8
|
-
# Holds query_answers_match_reference and paging_offset_partitions_correctly,
|
|
9
|
-
# plus the shared verb-resolution and eligible-rows/hop-chain helpers
|
|
10
|
-
# (#query_for_verb, #query_eligible_rows, #resolve_hop_clause) other
|
|
11
|
-
# property modules in this directory also call.
|
|
12
|
-
module Querying
|
|
13
|
-
# The query oracle — differential testing within the one runtime,
|
|
14
|
-
# the shape the retired cross-runtime harness should always have
|
|
15
|
-
# been. Every generated ask was answered twice at the same instant
|
|
16
|
-
# (Replay records both): once through whatever the aggregate is
|
|
17
|
-
# actually bound to (Memory's native hook is Ports::Query::InMemory;
|
|
18
|
-
# a SQL binding would compile it), once through the reference
|
|
19
|
-
# interpreter's own evaluation. The two are separate, live
|
|
20
|
-
# implementations of the same comparator vocabulary, and they have
|
|
21
|
-
# drifted before — an adapter that accepts what the reference says
|
|
22
|
-
# matches nothing, or orders what it refuses to order, shows up
|
|
23
|
-
# here as a finding no self-referential adapter spec could see.
|
|
24
|
-
# M23 — `Replay` runs the native and reference engines independently
|
|
25
|
-
# (each in its own begin/rescue — see that file's own comment at the
|
|
26
|
-
# capture site), so this property can tell apart "both engines
|
|
27
|
-
# refused" (fine — the ask was genuinely bad, nothing to compare)
|
|
28
|
-
# from "one refused and the other did not" (a real divergence — the
|
|
29
|
-
# two engines disagree about whether the ask was even valid, never
|
|
30
|
-
# mind what it answers). `native_refused`/`reference_refused` are
|
|
31
|
-
# read by key presence, not truthiness — `Replay` only ever adds
|
|
32
|
-
# `:error`/`:reference_error` to an entry when that side actually
|
|
33
|
-
# raised, so an absent key is an unambiguous "this side answered." A
|
|
34
|
-
# read-model ask (no reference twin attempted at all, `asked[:query]`
|
|
35
|
-
# without "::") is skipped entirely, same as always — there is no
|
|
36
|
-
# second engine to disagree with.
|
|
37
|
-
#
|
|
38
|
-
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
39
|
-
# @return [true, String] true if every native/reference query answer, and every
|
|
40
|
-
# refusal of one, agrees; otherwise a message naming the offending query, its
|
|
41
|
-
# args, and how the two engines disagreed
|
|
42
|
-
def query_answers_match_reference(history)
|
|
43
|
-
offenders = history.fetch(:queries).filter_map do |asked|
|
|
44
|
-
next unless asked[:query].is_a?(String) && asked[:query].include?("::")
|
|
45
|
-
|
|
46
|
-
native_refused = asked.key?(:error)
|
|
47
|
-
reference_refused = asked.key?(:reference_error)
|
|
48
|
-
|
|
49
|
-
if native_refused != reference_refused
|
|
50
|
-
next "#{asked[:query]} #{asked[:args].inspect} — native " \
|
|
51
|
-
"#{native_refused ? "refused (#{asked[:error]})" : 'answered'}, " \
|
|
52
|
-
"but the reference interpreter #{reference_refused ? "refused (#{asked[:reference_error]})" : 'answered'} — " \
|
|
53
|
-
"a refusal-shaped divergence, not just a differing row set"
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
next if native_refused
|
|
57
|
-
next if asked[:rows] == asked[:reference_rows]
|
|
58
|
-
|
|
59
|
-
"#{asked[:query]} #{asked[:args].inspect} answered #{asked[:rows].inspect} " \
|
|
60
|
-
"natively but #{asked[:reference_rows].inspect} through the reference interpreter"
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
offenders.empty? || offenders.join("; ")
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# The same "two engines, compared" shape query_answers_match_reference
|
|
67
|
-
# already uses, aimed squarely at Query#options' offset/limit pair —
|
|
68
|
-
# but recomputed from history[:instances] directly, a third,
|
|
69
|
-
# independent computation, rather than comparing QueryInterpreter's
|
|
70
|
-
# own native and reference paths against each other (which could
|
|
71
|
-
# share the identical bug neither implementation happened to hit —
|
|
72
|
-
# see #4's own fix, which touched both #interpret and
|
|
73
|
-
# #reference_interpret at once). `order_by` declared alongside
|
|
74
|
-
# `offset` or `limit` names a genuinely paged query. Ports::Query::
|
|
75
|
-
# Ordering.apply is the same engine QueryInterpreter#ordered calls,
|
|
76
|
-
# reused here rather than re-derived, so this oracle cannot drift
|
|
77
|
-
# from what "in order" means without the interpreter drifting the
|
|
78
|
-
# identical way — only the offset-then-limit .drop/.first slice
|
|
79
|
-
# (#4's own fix) is independently reproduced, in plain Ruby.
|
|
80
|
-
#
|
|
81
|
-
# Real target: ATMCard.ByFee (`limit 3; offset 1`).
|
|
82
|
-
#
|
|
83
|
-
# One independent recomputation, order-dependent by construction
|
|
84
|
-
# (declared query -> eligible rows -> ordered -> offset-sliced ->
|
|
85
|
-
# limit-sliced -> compared against the real answer, per the doc
|
|
86
|
-
# comment above): splitting it would scatter `rows`/`ordered`/
|
|
87
|
-
# `skipped`/`expected` across method boundaries as params/returns
|
|
88
|
-
# for a sequence that's only ever computed once, in this order.
|
|
89
|
-
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
90
|
-
#
|
|
91
|
-
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
92
|
-
# @return [true, String] true if every paged query's real answer matches an
|
|
93
|
-
# independent order/offset/limit recomputation over its eligible rows;
|
|
94
|
-
# otherwise a message naming the query, its args, and the two answers
|
|
95
|
-
def paging_offset_partitions_correctly(history)
|
|
96
|
-
bluebooks = history.fetch(:bluebooks)
|
|
97
|
-
|
|
98
|
-
offenders = history.fetch(:queries).filter_map do |asked|
|
|
99
|
-
next if asked[:error] || !asked[:query].is_a?(String) || !asked[:query].include?("::")
|
|
100
|
-
|
|
101
|
-
declared = query_for_verb(bluebooks, asked[:query])
|
|
102
|
-
next unless declared&.order_by && (declared.offset || declared.limit)
|
|
103
|
-
|
|
104
|
-
domain, aggregate_name, = Naming.split_verb(asked[:query])
|
|
105
|
-
args = asked[:args] || {}
|
|
106
|
-
rows = query_eligible_rows(asked.fetch(:instances_at), domain, aggregate_name, declared.wheres, args,
|
|
107
|
-
bluebooks: bluebooks)
|
|
108
|
-
ordered = Ports::Query::Ordering.apply(
|
|
109
|
-
rows, declared.order_by, declared.null_semantics, identity: ->(row) { row[:id].to_s }
|
|
110
|
-
) { |row| Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(row, declared.order_by.field)) }
|
|
111
|
-
|
|
112
|
-
skipped = declared.offset ? ordered.drop(resolve_paging_value(declared.offset.value, args).to_i) : ordered
|
|
113
|
-
expected = declared.limit ? skipped.first(resolve_paging_value(declared.limit.value, args).to_i) : skipped
|
|
114
|
-
actual = asked[:rows]
|
|
115
|
-
next if actual == expected
|
|
116
|
-
|
|
117
|
-
"#{asked[:query]} #{args.inspect} answered #{actual.inspect}, but independently recomputing " \
|
|
118
|
-
"order/offset/limit from #{rows.length} eligible row(s) gives #{expected.inspect}"
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
offenders.empty? || offenders.join("; ")
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
# The declared Query itself, resolved from a replayed verb — the
|
|
125
|
-
# same shape #command_for_verb resolves a command by, one
|
|
126
|
-
# construct over. Entity-level queries (a dotted query_path) are
|
|
127
|
-
# out of scope here — paging on an entity's own list has no real
|
|
128
|
-
# corpus site yet, and the "one many-side head, one aggregate,
|
|
129
|
-
# no FK-join" shape #query_eligible_rows assumes doesn't hold for
|
|
130
|
-
# one.
|
|
131
|
-
#
|
|
132
|
-
# @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
|
|
133
|
-
# keyed by domain name
|
|
134
|
-
# @param verb [String] the asked verb, `"Domain::Aggregate.Query"`
|
|
135
|
-
# @return [Bluebook::Query, nil] the declared query, or nil if `verb` names an
|
|
136
|
-
# entity-level query path, or a domain, aggregate, or query not found among
|
|
137
|
-
# `bluebooks`
|
|
138
|
-
def query_for_verb(bluebooks, verb)
|
|
139
|
-
domain, aggregate_name, query_path = Naming.split_verb(verb)
|
|
140
|
-
return nil unless query_path && !query_path.include?(".")
|
|
141
|
-
|
|
142
|
-
bluebook = bluebooks[domain]
|
|
143
|
-
aggregate = bluebook&.aggregate(aggregate_name)
|
|
144
|
-
aggregate&.query(query_path)
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
# Independently recomputes which of an aggregate's own stored rows a
|
|
148
|
-
# query's `wheres` admit.
|
|
149
|
-
#
|
|
150
|
-
# A query's own rows — unlike #eligible_rows (a ReadModel's
|
|
151
|
-
# reduced/grouped many-side head, possibly FK-joined against a
|
|
152
|
-
# root), a Query always asks about its own owning aggregate
|
|
153
|
-
# directly ; no join, no reference_target. `id:` merged in the
|
|
154
|
-
# same way #eligible_rows' own rows are, since a stable sort
|
|
155
|
-
# (Ordering.apply's own `identity:`) and the real answer's own
|
|
156
|
-
# `record.state.merge(id: record.id)` both need it.
|
|
157
|
-
# `bluebooks:` — needed only to recognise and resolve a `/` hop
|
|
158
|
-
# clause (`engagement/client/status`, hop_chain.bluebook's own
|
|
159
|
-
# PricedAboveViaEngagement): a hop's head names one of the owning
|
|
160
|
-
# aggregate's declared references, and only the declaration graph
|
|
161
|
-
# can say which attribute that is and which aggregate it targets.
|
|
162
|
-
# A local clause never consults it. Latent gap this closed, found
|
|
163
|
-
# by the fuzzer itself the first time a generated sequence ever
|
|
164
|
-
# built a full hop chain and had its paged query answer a row
|
|
165
|
-
# (seed 1, the moment scalar_value_objects.bluebook joined the
|
|
166
|
-
# fixtures corpus and shifted every seeded draw): the recompute
|
|
167
|
-
# dug `engagement/client/status` as a local dotted path, found
|
|
168
|
-
# nil, and declared every genuinely-eligible row ineligible — a
|
|
169
|
-
# false property violation against a correct runtime answer,
|
|
170
|
-
# reproducible on an untouched main with this same 4-step script.
|
|
171
|
-
#
|
|
172
|
-
# @param instances [Hash{String => Hash}] `history[:instances]`, or an
|
|
173
|
-
# `:instances_at` snapshot from a replayed query entry
|
|
174
|
-
# @param domain [String] the domain name the target aggregate belongs to
|
|
175
|
-
# @param aggregate_name [String] the target aggregate's own declared name
|
|
176
|
-
# @param wheres [Array<QuerySpecification::Common::WhereClause>] the clauses
|
|
177
|
-
# every returned row must satisfy
|
|
178
|
-
# @param args [Hash] the query's own call args, for a clause whose value is a
|
|
179
|
-
# Symbol naming one
|
|
180
|
-
# @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain, keyed
|
|
181
|
-
# by domain name; needed only to resolve a `/` hop clause
|
|
182
|
-
# @return [Array<Hash>] each admitted row's own state, merged with its `id:`
|
|
183
|
-
def query_eligible_rows(instances, domain, aggregate_name, wheres, args, bluebooks: {})
|
|
184
|
-
aggregate = bluebooks[domain]&.aggregate(aggregate_name)
|
|
185
|
-
prefix = "#{domain}::#{aggregate_name}#"
|
|
186
|
-
instances.filter_map do |key, state|
|
|
187
|
-
next unless key.start_with?(prefix)
|
|
188
|
-
|
|
189
|
-
row = state.merge(id: key.split("#").last)
|
|
190
|
-
next unless wheres.all? do |clause|
|
|
191
|
-
resolved = resolve_hop_clause(instances, domain, aggregate, clause, args, bluebooks)
|
|
192
|
-
held = Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(row, resolved.field))
|
|
193
|
-
Ports::Query::InMemory.holds?(resolved, held, args)
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
row
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
# Resolves one hop of a `/`-chained clause into a local `in` clause against
|
|
201
|
-
# the target aggregate's own ids.
|
|
202
|
-
#
|
|
203
|
-
# `Runtime::ReferenceHop#fold`, independently restated over the
|
|
204
|
-
# replay's own `:instances_at` snapshot instead of live
|
|
205
|
-
# repositories — the same shape every other recompute in this
|
|
206
|
-
# file takes (never the runtime's own code path, or the property
|
|
207
|
-
# would be checking the runtime against itself). One hop peels
|
|
208
|
-
# off the head (`HopPath.next_hop`, the identical one-step
|
|
209
|
-
# primitive the live fold uses), the inner clause recurses
|
|
210
|
-
# through `query_eligible_rows` against the target's own
|
|
211
|
-
# snapshot rows (so a multi-hop tail resolves hop by hop, exactly
|
|
212
|
-
# as the live path's own recursion does), and the ids that
|
|
213
|
-
# answered fold back as the same local `in` membership clause the
|
|
214
|
-
# live fold builds. A clause with no `/`, or one whose head this
|
|
215
|
-
# aggregate's declarations cannot resolve, passes through
|
|
216
|
-
# untouched and evaluates locally as it always did.
|
|
217
|
-
#
|
|
218
|
-
# @param instances [Hash{String => Hash}] `history[:instances]`, or an
|
|
219
|
-
# `:instances_at` snapshot from a replayed query entry
|
|
220
|
-
# @param domain [String] the domain name `aggregate` belongs to
|
|
221
|
-
# @param aggregate [Bluebook::Aggregate, nil] the aggregate `clause` is
|
|
222
|
-
# evaluated against; nil skips hop resolution entirely
|
|
223
|
-
# @param clause [QuerySpecification::Common::WhereClause] the clause to resolve
|
|
224
|
-
# @param args [Hash] the query's own call args, passed through to the inner
|
|
225
|
-
# hop's own `query_eligible_rows` recursion
|
|
226
|
-
# @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain, keyed
|
|
227
|
-
# by domain name
|
|
228
|
-
# @return [QuerySpecification::Common::WhereClause] `clause` unchanged if it has
|
|
229
|
-
# no resolvable hop head; otherwise a local `in` clause over the matching
|
|
230
|
-
# target ids
|
|
231
|
-
def resolve_hop_clause(instances, domain, aggregate, clause, args, bluebooks)
|
|
232
|
-
return clause unless aggregate && QuerySpecification::HopPath.hop_head?(clause.field, aggregate.attributes)
|
|
233
|
-
|
|
234
|
-
hop, rest = QuerySpecification::HopPath.next_hop(clause.field, aggregate.attributes)
|
|
235
|
-
target = hop.target
|
|
236
|
-
return clause unless target
|
|
237
|
-
|
|
238
|
-
inner = QuerySpecification::Common::WhereClause.new(field: rest, op: clause.op, value: clause.value)
|
|
239
|
-
ids = query_eligible_rows(instances, domain, target.hecks_name, [inner], args, bluebooks: bluebooks)
|
|
240
|
-
.map { |row| row[:id].to_s }.uniq
|
|
241
|
-
|
|
242
|
-
QuerySpecification::Common::WhereClause.new(field: hop.attribute.name, op: "in", value: ids)
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
# Resolves a declared limit/offset value against the query's own call args.
|
|
246
|
-
#
|
|
247
|
-
# `QueryInterpreter#resolve_query_value`, reproduced: a declared
|
|
248
|
-
# limit/offset is either a literal or a Symbol naming an argument
|
|
249
|
-
# the caller supplied.
|
|
250
|
-
#
|
|
251
|
-
# @param value [Integer, Symbol] the declared limit/offset value
|
|
252
|
-
# @param args [Hash] the query's own call args
|
|
253
|
-
# @return [Object] `value` unchanged if not a Symbol; otherwise `args[value]`
|
|
254
|
-
def resolve_paging_value(value, args)
|
|
255
|
-
value.is_a?(Symbol) ? args[value] : value
|
|
256
|
-
end
|
|
257
|
-
end
|
|
258
|
-
end
|
|
259
|
-
end
|
|
260
|
-
end
|