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.
Files changed (216) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +13 -0
  3. data/lib/hecks/adapters/driven/google_authentication.rb +1 -1
  4. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  5. data/lib/hecks/adapters/driven/memory.rb +1 -1
  6. data/lib/hecks/adapters/driven/postgres.rb +1 -1
  7. data/lib/hecks/adapters/driven/prism.rb +2 -2
  8. data/lib/hecks/adapters/driven/sqlite/projection.rb +1 -1
  9. data/lib/hecks/adapters/driven/tenant_provisioner.rb +31 -10
  10. data/lib/hecks/behaviors/expectations.rb +2 -2
  11. data/lib/hecks/bluebook/assembly/contract.rb +1 -1
  12. data/lib/hecks/bluebook/assembly/contracts.rb +5 -5
  13. data/lib/hecks/bluebook/assembly/specializer.rb +2 -2
  14. data/lib/hecks/bluebook/assembly.rb +1 -1
  15. data/lib/hecks/bluebook/behaviour/chapter.rb +5 -2
  16. data/lib/hecks/bluebook/behaviour/read_model.rb +18 -0
  17. data/lib/hecks/bluebook/capabilities.rb +2 -2
  18. data/lib/hecks/bluebook/chapter.rb +1 -1
  19. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +2 -2
  20. data/lib/hecks/bluebook/dsl/binding_proxy.rb +6 -1
  21. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +2 -2
  22. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
  23. data/lib/hecks/bluebook/dsl/command_builder.rb +3 -3
  24. data/lib/hecks/bluebook/dsl/entity_builder.rb +1 -1
  25. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +1 -1
  26. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +14 -9
  27. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +1 -1
  28. data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -3
  29. data/lib/hecks/bluebook/dsl/translation_builder.rb +1 -1
  30. data/lib/hecks/bluebook/dsl/world_builder.rb +47 -3
  31. data/lib/hecks/bluebook/hexagon.rb +15 -7
  32. data/lib/hecks/bluebook/meta_validator/judge.rb +3 -3
  33. data/lib/hecks/bluebook/meta_validator/plan.rb +4 -4
  34. data/lib/hecks/bluebook/meta_validator/readings.rb +1 -1
  35. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +2 -2
  36. data/lib/hecks/bluebook/meta_validator/shapes.rb +1 -1
  37. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +11 -10
  38. data/lib/hecks/bluebook/meta_validator/world_judge.rb +3 -1
  39. data/lib/hecks/bluebook/meta_validator.rb +1 -1
  40. data/lib/hecks/bluebook/model_check/client_profile.rb +4 -70
  41. data/lib/hecks/bluebook/model_check.rb +13 -14
  42. data/lib/hecks/cache_dir.rb +103 -0
  43. data/lib/hecks/cli/document.rb +104 -0
  44. data/lib/hecks/cli/ir.rb +47 -0
  45. data/lib/hecks/cli/mcp.rb +32 -0
  46. data/lib/hecks/cli/mcp_door.rb +461 -0
  47. data/lib/hecks/cli/model_check.rb +270 -0
  48. data/lib/hecks/cli/project_cli.rb +123 -0
  49. data/lib/hecks/cli/project_diagrams.rb +43 -0
  50. data/lib/hecks/cli/run.rb +219 -0
  51. data/lib/hecks/cli/smoke_test.rb +59 -0
  52. data/lib/hecks/cli/stores.rb +54 -0
  53. data/lib/hecks/cli.rb +166 -0
  54. data/lib/hecks/embryonaut_bluebook/lock.rb +72 -0
  55. data/lib/hecks/embryonaut_bluebook/shape.rb +47 -0
  56. data/lib/hecks/embryonaut_bluebook/vendor.rb +194 -0
  57. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +88 -0
  58. data/lib/hecks/embryonaut_bluebook.rb +29 -3
  59. data/lib/hecks/facade/handle.rb +2 -2
  60. data/lib/hecks/facade/surface/aggregate_door.rb +1 -1
  61. data/lib/hecks/forms/value_object_shape.rb +1 -1
  62. data/lib/hecks/framework/bluebook/console_settings.bluebook +3 -3
  63. data/lib/hecks/language/bluebook/vocabulary.bluebook +16 -0
  64. data/lib/hecks/language/world/world.bluebook +32 -2
  65. data/lib/hecks/mcp_door_scope.rb +176 -0
  66. data/lib/hecks/ports/key_vault.rb +1 -1
  67. data/lib/hecks/ports/persistence/append_only.rb +1 -1
  68. data/lib/hecks/ports/persistence/binding_policy.rb +16 -5
  69. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -2
  70. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +2 -2
  71. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +2 -2
  72. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +132 -0
  73. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +1 -1
  74. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -2
  75. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +1 -1
  76. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +1 -1
  77. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +13 -7
  78. data/lib/hecks/ports/persistence/plugins/era.rb +7 -4
  79. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +73 -0
  80. data/lib/hecks/ports/persistence/postgres_dump.rb +125 -0
  81. data/lib/hecks/ports/persistence/repository_factory.rb +2 -2
  82. data/lib/hecks/ports/persistence/state_codec.rb +1 -1
  83. data/lib/hecks/ports/persistence.rb +4 -0
  84. data/lib/hecks/projections/deploy/fargate/assembly.rb +193 -0
  85. data/lib/hecks/projections/deploy/fargate/cdn.rb +318 -0
  86. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +87 -0
  87. data/lib/hecks/projections/deploy/fargate/check.rb +172 -0
  88. data/lib/hecks/projections/deploy/fargate/containers.rb +412 -0
  89. data/lib/hecks/projections/deploy/fargate/extras.rb +268 -0
  90. data/lib/hecks/projections/deploy/fargate/monitoring.rb +448 -0
  91. data/lib/hecks/projections/deploy/fargate/settings.rb +235 -0
  92. data/lib/hecks/projections/deploy/fargate/yaml.rb +116 -0
  93. data/lib/hecks/projections/deploy/fargate.rb +188 -139
  94. data/lib/hecks/projections/deploy/lambda.rb +8 -8
  95. data/lib/hecks/projections/deploy/preview/access_yaml.rb +145 -0
  96. data/lib/hecks/projections/deploy/preview/containers.rb +218 -0
  97. data/lib/hecks/projections/deploy/preview/preview.sh.erb +446 -0
  98. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +216 -0
  99. data/lib/hecks/projections/deploy/preview/script.rb +40 -0
  100. data/lib/hecks/projections/deploy/preview/settings.rb +140 -0
  101. data/lib/hecks/projections/deploy/preview/task_yaml.rb +215 -0
  102. data/lib/hecks/projections/deploy/preview/template.rb +247 -0
  103. data/lib/hecks/projections/deploy/preview/yaml_text.rb +48 -0
  104. data/lib/hecks/projections/deploy/preview.rb +103 -0
  105. data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +182 -0
  106. data/lib/hecks/projections/deploy/scripts/expected-era.header +17 -0
  107. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +54 -0
  108. data/lib/hecks/projections/deploy/scripts/settings.rb +114 -0
  109. data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +232 -0
  110. data/lib/hecks/projections/deploy/scripts.rb +171 -0
  111. data/lib/hecks/projections/deploy/smoke/harness.js +366 -0
  112. data/lib/hecks/projections/deploy/smoke/workflow.yml.tmpl +56 -0
  113. data/lib/hecks/projections/deploy/smoke.rb +159 -0
  114. data/lib/hecks/projections/deploy/template_diff/comparer.rb +231 -0
  115. data/lib/hecks/projections/deploy/template_diff/loader.rb +105 -0
  116. data/lib/hecks/projections/deploy/template_diff.rb +173 -0
  117. data/lib/hecks/projections/glossary/sensitivity.rb +1 -1
  118. data/lib/hecks/projections/reference.rb +7 -1
  119. data/lib/hecks/projections.rb +2 -0
  120. data/lib/hecks/projector/exporter.rb +4 -4
  121. data/lib/hecks/projector/target.rb +1 -1
  122. data/lib/hecks/release/gem_pin/published_versions.rb +58 -0
  123. data/lib/hecks/release/gem_pin.rb +112 -0
  124. data/lib/hecks/release/runner/ci_publisher.rb +96 -0
  125. data/lib/hecks/release/runner/commands.rb +68 -0
  126. data/lib/hecks/release/runner/console.rb +52 -0
  127. data/lib/hecks/release/runner/gem_publisher.rb +44 -0
  128. data/lib/hecks/release/runner/git.rb +48 -0
  129. data/lib/hecks/release/runner/npm_publisher.rb +89 -0
  130. data/lib/hecks/release/runner/preflight.rb +119 -0
  131. data/lib/hecks/release/runner/published.rb +55 -0
  132. data/lib/hecks/release/runner/tagger.rb +87 -0
  133. data/lib/hecks/release/runner.rb +244 -0
  134. data/lib/hecks/router/namespace_installer.rb +1 -1
  135. data/lib/hecks/runtime/command_interpreter.rb +3 -3
  136. data/lib/hecks/runtime/command_rules/admissibility.rb +2 -2
  137. data/lib/hecks/runtime/command_rules/arithmetic.rb +2 -2
  138. data/lib/hecks/runtime/entity_element.rb +1 -1
  139. data/lib/hecks/runtime/entity_interpreter.rb +1 -1
  140. data/lib/hecks/runtime/invocation.rb +1 -1
  141. data/lib/hecks/runtime/loader.rb +27 -3
  142. data/lib/hecks/runtime/outbox.rb +1 -1
  143. data/lib/hecks/runtime/reaction_invocation.rb +3 -3
  144. data/lib/hecks/runtime/read_model_interpreter.rb +37 -15
  145. data/lib/hecks/runtime/reference_hop.rb +4 -4
  146. data/lib/hecks/runtime/registry/saga_persistence.rb +8 -2
  147. data/lib/hecks/runtime/registry/verification.rb +15 -13
  148. data/lib/hecks/runtime/registry/world_defaults.rb +106 -0
  149. data/lib/hecks/runtime/registry.rb +22 -13
  150. data/lib/hecks/runtime/saga_interpreter.rb +1 -1
  151. data/lib/hecks/runtime/tenant_check.rb +1 -1
  152. data/lib/hecks/runtime/value/coercion.rb +3 -3
  153. data/lib/hecks/runtime/value/entity_list_coercion.rb +1 -1
  154. data/lib/hecks/storehouse.rb +18 -12
  155. data/lib/hecks/vendoring/git_environment.rb +35 -0
  156. data/lib/hecks/vendoring/git_source.rb +102 -0
  157. data/lib/hecks/vendoring.rb +102 -0
  158. data/lib/hecks/version.rb +1 -1
  159. data/lib/hecks/vocabulary.rb +5 -0
  160. data/lib/hecks.rb +8 -2
  161. metadata +72 -58
  162. data/lib/hecks/bench/cli.rb +0 -74
  163. data/lib/hecks/bench/environment.rb +0 -100
  164. data/lib/hecks/bench/postgres_probe.rb +0 -41
  165. data/lib/hecks/bench/report.rb +0 -116
  166. data/lib/hecks/bench/ruby_runner.rb +0 -151
  167. data/lib/hecks/bench/run.rb +0 -55
  168. data/lib/hecks/bench/rust_runner.rb +0 -165
  169. data/lib/hecks/bench/stats.rb +0 -75
  170. data/lib/hecks/bench/suite.rb +0 -172
  171. data/lib/hecks/bench/workload.rb +0 -137
  172. data/lib/hecks/bench.rb +0 -28
  173. data/lib/hecks/codemod.rb +0 -443
  174. data/lib/hecks/corpus.rb +0 -479
  175. data/lib/hecks/doc/reference.rb +0 -585
  176. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
  177. data/lib/hecks/fuzzing/combination_miner.rb +0 -178
  178. data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
  179. data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
  180. data/lib/hecks/fuzzing/differential.rb +0 -192
  181. data/lib/hecks/fuzzing/domain_generator.rb +0 -871
  182. data/lib/hecks/fuzzing/era_boundary.rb +0 -154
  183. data/lib/hecks/fuzzing/form_census.rb +0 -285
  184. data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
  185. data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
  186. data/lib/hecks/fuzzing/isolated_boot.rb +0 -604
  187. data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
  188. data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
  189. data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
  190. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
  191. data/lib/hecks/fuzzing/properties/guards.rb +0 -398
  192. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
  193. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
  194. data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
  195. data/lib/hecks/fuzzing/properties/querying.rb +0 -260
  196. data/lib/hecks/fuzzing/properties.rb +0 -267
  197. data/lib/hecks/fuzzing/qa_settings.rb +0 -164
  198. data/lib/hecks/fuzzing/replay.rb +0 -930
  199. data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
  200. data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
  201. data/lib/hecks/fuzzing/self_consistency.rb +0 -825
  202. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
  203. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
  204. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
  205. data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
  206. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
  207. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
  208. data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
  209. data/lib/hecks/fuzzing/shrinker.rb +0 -309
  210. data/lib/hecks/fuzzing/structural_skips.rb +0 -53
  211. data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
  212. data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
  213. data/lib/hecks/fuzzing/value_generator.rb +0 -340
  214. data/lib/hecks/fuzzing.rb +0 -23
  215. data/lib/hecks/grammar/evolve.rb +0 -524
  216. 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