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,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