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,309 +0,0 @@
1
- module Hecks
2
- module Fuzzing
3
- # A failing step list, made small enough to read.
4
- #
5
- # `bin/fuzz` always shrank its findings; `bin/qa_sweep` — the loop that
6
- # actually finds the bugs — never did, so the ledger filled with
7
- # demonstrations like "seed 25, 25 steps, step 16" and a human cut
8
- # BUG#40 down to its four real steps by hand. This is `bin/fuzz`'s
9
- # shrinker lifted out of that script so both callers share it, and
10
- # made cheaper at the same time: a sweep's candidate check can cost a
11
- # Rust subprocess (or a real Postgres round trip), where `bin/fuzz`'s
12
- # only ever cost an in-process replay.
13
- #
14
- # ## Ownership of "same finding"
15
- #
16
- # The caller owns it. `call` is handed a block that
17
- # answers true when a candidate step list still reproduces what the
18
- # original did — `bin/fuzz` compares its own verdict signature,
19
- # `bin/qa_sweep` compares `Shrinker.signature` of one mode's
20
- # divergences. This module never replays anything itself, so it has
21
- # no idea which engine, adapter or comparison it is minimizing for.
22
- #
23
- # ## Two passes, in order
24
- #
25
- # 1. Steps, chunks first. Removing one step at a time (what
26
- # `bin/fuzz` did) costs O(n²) candidate checks on a sequence
27
- # where most steps are irrelevant. Delta-debugging style: try
28
- # removing halves, then quarters, … then single steps, keeping
29
- # any removal that still reproduces; a single-step pass repeats
30
- # until it changes nothing, so the result is 1-minimal (no one
31
- # remaining step can be dropped).
32
- # 2. Arguments, inside each surviving step. Unchanged from `bin/fuzz`
33
- # (spec/bin_fuzz_spec.rb pins its accumulation contract): drop
34
- # one key at a time from the step's current args, keep it dropped
35
- # only while the finding still reproduces.
36
- #
37
- # ## Budget
38
- #
39
- # A budget, because a sweep has other targets waiting. `budget:` caps
40
- # how many candidate checks one call may spend (nil = unbounded, the
41
- # `bin/fuzz` behaviour). When it runs out the best candidate found so
42
- # far is returned — every accepted candidate reproduced, so a partial
43
- # shrink is still a correct, just less small, demonstration.
44
- module Shrinker
45
- Result = Struct.new(:steps, :attempts, :exhausted, keyword_init: true)
46
-
47
- module_function
48
-
49
- # Shrinks `steps` to the smallest step list the block still accepts as
50
- # reproducing the original finding — pass 1 (steps), then pass 2
51
- # (arguments), as this module's own header describes.
52
- #
53
- # @param steps [Array<Hash>] the step list to shrink, each step a command,
54
- # query, or read-model hash as `SequenceGenerator` produces
55
- # @param budget [Integer, nil] maximum candidate checks to spend; nil for
56
- # unbounded
57
- # @yield [candidate] tests whether a candidate step list still reproduces
58
- # the finding being shrunk
59
- # @yieldparam candidate [Array<Hash>] a step-dropped or argument-trimmed
60
- # copy of `steps`
61
- # @yieldreturn [Boolean] true if `candidate` still reproduces the finding
62
- # @return [Fuzzing::Shrinker::Result] `steps:` the shrunk step list,
63
- # `attempts:` how many candidate checks were spent, `exhausted:` whether
64
- # the budget ran out before shrinking finished
65
- # @raise [ArgumentError] if no block is given
66
- def call(steps, budget: nil, &reproduces)
67
- raise ArgumentError, "Shrinker.call needs a block answering whether a candidate reproduces" unless reproduces
68
-
69
- meter = Meter.new(budget)
70
- current = drop_steps(steps.dup, meter, &reproduces)
71
- current = drop_arguments(current, meter, &reproduces)
72
- Result.new(steps: current, attempts: meter.used, exhausted: meter.exhausted?)
73
- end
74
-
75
- # Runs pass 1 of the shrink: delta-debugging removal of step chunks,
76
- # halving the chunk size each full pass until single steps are tried
77
- # and nothing more can be dropped.
78
- #
79
- # @param steps [Array<Hash>] the step list to shrink
80
- # @param meter [Fuzzing::Shrinker::Meter] the shared budget meter
81
- # @yield [candidate] tests whether a candidate step list still reproduces
82
- # the finding being shrunk
83
- # @yieldparam candidate [Array<Hash>] `steps` with one chunk removed
84
- # @yieldreturn [Boolean] true if `candidate` still reproduces the finding
85
- # @return [Array<Hash>] the smallest step list pass 1 could reach, or the
86
- # best found so far if the budget ran out first
87
- def drop_steps(steps, meter, &reproduces)
88
- current = steps
89
- chunk = [current.length / 2, 1].max
90
- loop do
91
- changed = false
92
- index = 0
93
- while index < current.length
94
- return current if meter.exhausted?
95
-
96
- candidate = current[0...index] + (current[(index + chunk)..] || [])
97
- if candidate.empty?
98
- index += chunk
99
- next
100
- end
101
-
102
- if meter.try { reproduces.call(candidate) }
103
- current = candidate
104
- changed = true
105
- else
106
- index += chunk
107
- end
108
- end
109
-
110
- if chunk > 1
111
- chunk = [chunk / 2, 1].max
112
- elsif !changed
113
- break
114
- end
115
- end
116
- current
117
- end
118
-
119
- # Runs pass 2 of the shrink: drops one argument key at a time from
120
- # each surviving step's args, keeping the drop only while the
121
- # finding still reproduces.
122
- #
123
- # See the module header's pass 2 — `args` is read by whichever
124
- # spelling the step actually carries (`key?` first, never `||`,
125
- # which cannot tell a stored `false` from an absent key).
126
- #
127
- # @param steps [Array<Hash>] the step list, already step-shrunk by `drop_steps`
128
- # @param meter [Fuzzing::Shrinker::Meter] the shared budget meter
129
- # @yield [candidate] tests whether a candidate step list still reproduces
130
- # the finding being shrunk
131
- # @yieldparam candidate [Array<Hash>] `steps` with one step's one argument
132
- # key dropped
133
- # @yieldreturn [Boolean] true if `candidate` still reproduces the finding
134
- # @return [Array<Hash>] `steps` with every argument key `drop_arguments`
135
- # accepted dropped
136
- def drop_arguments(steps, meter, &reproduces)
137
- steps.each_index do |position|
138
- original = args_of(steps[position])
139
- next unless original.is_a?(Hash)
140
-
141
- original.each_key do |key|
142
- return steps if meter.exhausted?
143
-
144
- step = steps[position]
145
- trimmed = args_of(step).reject { |name, _| name == key }
146
- candidate = steps.map(&:dup)
147
- candidate[position] = step.merge("args" => trimmed)
148
- steps = candidate if meter.try { reproduces.call(candidate) }
149
- end
150
- end
151
- steps
152
- end
153
-
154
- # Reads a step's argument hash, whichever key spelling it uses.
155
- #
156
- # @param step [Hash] one step, string- or symbol-keyed
157
- # @return [Hash, nil] the step's `"args"` value if the step is string-keyed,
158
- # else its `:args` value (nil if neither key is present)
159
- def args_of(step)
160
- step.key?("args") ? step["args"] : step[:args]
161
- end
162
-
163
- # Which finding this is, as a set of stable strings — the identity a
164
- # shrink candidate has to keep. The `field` alone is too loose for
165
- # the comparisons that carry lists: "refusals differ" on a 3-step
166
- # candidate could be a different refusal split than the one the
167
- # original seed found, and a shrinker that accepted it would hand
168
- # back a demonstration of the wrong bug. So:
169
- #
170
- # refusals/queries/dry_runs/reactions — the field plus every verb
171
- # (or query) named in the symmetric difference of the two sides;
172
- # instances — the field plus the aggregate of every top-level key
173
- # whose two sides disagree (the `#id` suffix dropped);
174
- # a crash/process finding — the field plus the exception class
175
- # leading its detail;
176
- # anything else (a property name, a self-consistency axis) — the
177
- # field, which already names the finding.
178
- #
179
- # `reproduces?` holds when a candidate's signature contains the
180
- # original's: removing steps may add a second divergence, but it
181
- # must never lose the one being demonstrated.
182
- LIST_FIELDS = %w[refusals queries dry_runs reactions].freeze
183
-
184
- CRASH_FIELDS = %w[crash process generator_crash].freeze
185
-
186
- # Computes which finding a set of divergences is — the stable identity
187
- # a shrink candidate has to keep. See this method's own preceding
188
- # comment for what each field kind contributes.
189
- #
190
- # @param divergences [Array<Hash>] divergence entries, each at least
191
- # `{field:, detail:}` plus whichever mode-specific keys carry the two
192
- # compared sides
193
- # @return [Set<String>] stable identity strings: each divergence's `field`,
194
- # plus `detail_keys`' own per-field detail strings
195
- def signature(divergences)
196
- divergences.each_with_object(Set.new) do |divergence, keys|
197
- field = divergence[:field].to_s
198
- keys << field
199
- keys.merge(detail_keys(field, divergence))
200
- end
201
- end
202
-
203
- # Extracts the finer-grained identity strings one divergence's field
204
- # contributes beyond the field name itself.
205
- #
206
- # @param field [String] the divergence's `field`, stringified
207
- # @param divergence [Hash] the divergence entry `field` came from
208
- # @return [Array<String>] extra identity strings for `field`'s own kind
209
- # (list, `instances`, or crash/process); empty for any other field
210
- def detail_keys(field, divergence)
211
- left, right = divergence.except(:field, :detail).values.select { |v| v.is_a?(Array) || v.is_a?(Hash) }
212
- if LIST_FIELDS.include?(field) then list_keys(field, left, right)
213
- elsif field == "instances" then instance_keys(left, right)
214
- elsif CRASH_FIELDS.include?(field) && divergence[:detail]
215
- ["#{field}:#{divergence[:detail].to_s[/\A\w+(?:::\w+)*/]}"]
216
- else []
217
- end
218
- end
219
-
220
- # Names the rows present on one side of a list-field divergence and not
221
- # the other.
222
- #
223
- # @param field [String] the divergence's field name, prefixed onto each key
224
- # @param left [Array, nil] one side's list; a non-Array yields no keys
225
- # @param right [Array, nil] the other side's list; a non-Array yields no keys
226
- # @return [Array<String>] `"field:name"` for every row in the symmetric
227
- # difference of `left` and `right`; empty unless both are arrays
228
- def list_keys(field, left, right)
229
- return [] unless left.is_a?(Array) && right.is_a?(Array)
230
-
231
- ((left - right) + (right - left)).map { |row| "#{field}:#{named(row)}" }
232
- end
233
-
234
- # Names the top-level keys the two sides of an `instances` divergence
235
- # disagree on.
236
- #
237
- # Instance keys are `Aggregate#id` on the wire — the id is whatever
238
- # the generator minted, so it names the record, not the finding;
239
- # keeping it would pin every creating step.
240
- #
241
- # @param left [Hash, nil] one side's instance snapshot; a non-Hash yields no keys
242
- # @param right [Hash, nil] the other side's instance snapshot; a non-Hash
243
- # yields no keys
244
- # @return [Array<String>] `"instances:Aggregate"` for every top-level key
245
- # the two sides disagree on, its `#id` suffix dropped; empty unless both
246
- # are hashes
247
- def instance_keys(left, right)
248
- return [] unless left.is_a?(Hash) && right.is_a?(Hash)
249
-
250
- (left.keys | right.keys).reject { |key| left[key] == right[key] }
251
- .map { |key| "instances:#{key.to_s.split('#').first}" }
252
- end
253
-
254
- # Checks that a candidate's own divergences still demonstrate the
255
- # finding being shrunk.
256
- #
257
- # @param original_signature [Set<String>] the finding's own signature, as
258
- # returned by `signature`
259
- # @param divergences [Array<Hash>] the candidate's own divergence entries,
260
- # in `signature`'s own shape
261
- # @return [Boolean] true if `divergences` is non-empty and its signature is
262
- # a superset of `original_signature`
263
- def reproduces?(original_signature, divergences)
264
- !divergences.empty? && original_signature.subset?(signature(divergences))
265
- end
266
-
267
- # Names one row from a list-field divergence, for `list_keys`.
268
- #
269
- # @param row [Object] one item from a list-field divergence's array;
270
- # typically a Hash carrying a `verb`, `query`, or `policy` key (string
271
- # or symbol)
272
- # @return [String] `row`'s `verb`/`query`/`policy` value, stringified, if
273
- # `row` is a Hash carrying one of those keys; otherwise `row.to_s`
274
- def named(row)
275
- return row.to_s unless row.is_a?(Hash)
276
-
277
- key = [%w[verb query policy], %i[verb query policy]].flatten.find { |name| row.key?(name) }
278
- (key ? row[key] : row).to_s
279
- end
280
-
281
- # Counts candidate checks against the budget.
282
- class Meter
283
- attr_reader :used
284
-
285
- # @param budget [Integer, nil] maximum candidate checks to allow; nil for unbounded
286
- def initialize(budget)
287
- @budget = budget
288
- @used = 0
289
- end
290
-
291
- # Reports whether this meter's budget has been fully spent.
292
- #
293
- # @return [Boolean] true if a budget is set and every allotted check has
294
- # been used
295
- def exhausted? = !@budget.nil? && @used >= @budget
296
-
297
- # Spends one candidate check against the budget and runs the block.
298
- #
299
- # @yield runs the candidate check being counted
300
- # @yieldreturn [Object] the block's own result
301
- # @return [Object] whatever the block returns
302
- def try
303
- @used += 1
304
- yield
305
- end
306
- end
307
- end
308
- end
309
- end
@@ -1,53 +0,0 @@
1
- module Hecks
2
- module Fuzzing
3
- # What each query verb the differential comparison skipped was hiding.
4
- #
5
- # `Differential.manifest_partition` drops a named query or read model
6
- # from both sides of the Ruby/Rust comparison only when the binary's own
7
- # manifest.json declares it `generated: false`. The drop is correct, but
8
- # a sweep that dropped every `offset`/`cursor` ask would still read as
9
- # "agreed across all steps". `bin/qa_sweep` therefore logs one Check per
10
- # sweep naming every skipped verb and the construct family that caused
11
- # it, and marks it Surprised when a family is outside
12
- # `QualityControlDials::STRUCTURAL_REFUSAL_BOUNDARY`.
13
- #
14
- # The construct comes straight from the manifest entry, written by the
15
- # generator branch that made the skip (`Projector::SkipReason`), rather
16
- # than guessed from the Ruby declaration's `to_h` keys, which is coarser
17
- # than codegen's own decision and can drift from it.
18
- module StructuralSkips
19
- module_function
20
-
21
- # Attributes each skipped verb to the construct family its manifest
22
- # entry names.
23
- #
24
- # One entry per skipped verb: `{ verb:, constructs: [...] }`, sorted by
25
- # verb so the printed observation is stable across seeds and runs. A
26
- # verb the manifest doesn't declare answers `unknown`, which no
27
- # boundary admits.
28
- #
29
- # @param gaps [Fuzzing::RustGapManifest] the binary's not-generated manifest entries
30
- # @param verbs [Array<String>] the skipped query/read-model verbs to attribute
31
- # @return [Array<Hash{Symbol => Object}>] one `{ verb: String, constructs: Array<String> }`
32
- # per verb, in sorted-verb order
33
- def attribute(gaps, verbs)
34
- verbs.sort.map do |verb|
35
- entry = gaps.not_generated(verb)
36
- { verb: verb, constructs: entry ? [entry.fetch("construct")] : %w[unknown] }
37
- end
38
- end
39
-
40
- # Selects the attributed entries whose constructs are not all inside `boundary`.
41
- #
42
- # @param attributed [Array<Hash{Symbol => Object}>] entries as returned by `attribute`
43
- # @param boundary [Array<String, Symbol>] the construct families the documented codegen
44
- # boundary admits
45
- # @return [Array<Hash{Symbol => Object}>] the subset of `attributed` naming an
46
- # unadmitted construct, or no construct at all
47
- def outside_boundary(attributed, boundary)
48
- admitted = boundary.map(&:to_s)
49
- attributed.select { |entry| entry[:constructs].empty? || (entry[:constructs] - admitted).any? }
50
- end
51
- end
52
- end
53
- end
@@ -1,61 +0,0 @@
1
- module Hecks
2
- module Fuzzing
3
- # How hard one sweep fuzzes, as a function of `Target.clean_streak`
4
- # (qa/bluebook/quality_control.bluebook — that attribute's own comment
5
- # says what the streak means). The sibling of `RotationPriority`: the
6
- # ledger holds the number, `QualityControlDials::WIDENING_TIERS` holds
7
- # the policy, and this module is where the two meet — a pure function
8
- # of both, nothing else read.
9
- #
10
- # Why this is not in `bin/qa_sweep` any more. It was — `WIDENING_TIERS`
11
- # and `widen_for_streak` lived at the top of that script, which meant
12
- # policy data lived in a script (unlike every other dial, which lives
13
- # in the bluebook a human edits and reviews) and duplicated itself as
14
- # prose in SKILL.md. Now the table is a dial, this is the one reader,
15
- # and `bin/qa_sweep` calls it the way it already calls
16
- # `RotationPriority.pick`.
17
- #
18
- # **Pure, deliberately** — the same discipline `RotationPriority` keeps:
19
- # same streak in, same `[seeds, steps]` out, every time, which is what
20
- # lets a human predict what a given sweep is about to do before it
21
- # runs one. `tiers:` defaults to the dial but is a plain argument, so
22
- # a unit spec can pass its own table with no ledger boot at all and a
23
- # `bin/qa_sweep` run against a ledger that declares no dials (an
24
- # isolated spec's own fixture) can fall back the same way it already
25
- # does for `ADVERSARIAL_FRACTION`.
26
- module SweepDepth
27
- module_function
28
-
29
- # The fallback table, for a boot with no `QualityControlDials` at all
30
- # — the same shape and the same three rows the dial ships with, so a
31
- # dial-less ledger fuzzes exactly as a dialled one does by default.
32
- # Never read when the dial exists; `bin/qa_sweep` passes the dial in.
33
- DEFAULT_TIERS = [
34
- { upto: 4, seeds: 10, steps: 25 },
35
- { upto: 19, seeds: 25, steps: 50 },
36
- { upto: Float::INFINITY, seeds: 50, steps: 100 }
37
- ].freeze
38
-
39
- # `[seeds, steps]` for one streak. The first row whose `upto` the
40
- # streak does not exceed wins — rows are read in order, so the table
41
- # must be ascending, and the last row's `Float::INFINITY` is what
42
- # makes it the ceiling rather than a gap.
43
- #
44
- # @param streak [Integer] `Target.clean_streak`, the number of consecutive clean sweeps
45
- # @param tiers [Array<Hash{Symbol => Numeric}>] ascending `{ upto:, seeds:, steps: }` rows;
46
- # defaults to `DEFAULT_TIERS`
47
- # @return [Array(Integer, Integer)] `[seeds, steps]` for the first row `streak` fits under
48
- # @raise [ArgumentError] if `streak` is negative
49
- # @raise [ArgumentError] if no row in `tiers` covers `streak` (the last row must declare
50
- # `upto: Float::INFINITY`)
51
- def for_streak(streak, tiers: DEFAULT_TIERS)
52
- raise ArgumentError, "streak must not be negative" if streak.negative?
53
-
54
- tier = tiers.find { |row| streak <= row[:upto] }
55
- raise ArgumentError, "no tier covers a streak of #{streak} — the last row must be upto: Float::INFINITY" unless tier
56
-
57
- [tier[:seeds], tier[:steps]]
58
- end
59
- end
60
- end
61
- end
@@ -1,255 +0,0 @@
1
- module Hecks
2
- module Fuzzing
3
- # What a sweep target can actually be checked for, read off the
4
- # filesystem — never off a stored list.
5
- #
6
- # `bin/qa_sweep` does not decide its comparison modes with inline
7
- # conditionals — "is there a Cargo feature named after this directory?
8
- # then `:differential`, else `:ruby_only`", a separate hand-typed abort
9
- # for `--persistence-parity` ("does any .hecksagon bind PostgresEra?"),
10
- # and so on for every further mode the practice adds (era boundary,
11
- # concurrency, a WASM front) — because each such inline `if` is a
12
- # policy decision hiding in a script. This module is those decisions
13
- # as data: `MODE_REQUIREMENTS` says which capabilities each mode
14
- # needs, `infer` says which capabilities a target's own directory
15
- # actually has, and `resolve` is the one rule that joins them —
16
- # `modes_to_run = enabled ∩ eligible`.
17
- #
18
- # ## Inference decides; a stored list only records
19
- #
20
- # `Target.capabilities` (qa/bluebook/quality_control.bluebook, once
21
- # PR-1's era lands) is written by the runner from exactly this
22
- # inference at release time so `Target.EligibleFor(mode)` can audit
23
- # the rotation from the ledger alone — but the runner re-infers every
24
- # sweep, because a stored list that lags yesterday's Cargo feature is
25
- # precisely the "quiet divergence" (the chapter's own opening comment)
26
- # this whole practice exists to hunt. Nothing here ever reads the
27
- # ledger.
28
- #
29
- # ## Regex provenance
30
- #
31
- # Every regex is one the harness already owned, moved here rather than
32
- # re-derived, and each one's provenance is named beside it so a
33
- # future edit to the original site is a visible drift, not a silent
34
- # one: the Cargo feature line (`RustConformanceHelpers#build_rust_for`),
35
- # the PostgresEra binding (`bin/qa_sweep`'s old `POSTGRES_ERA_BINDING`),
36
- # the translations glob (`IsolatedBoot#strip_translations!`).
37
- module TargetCapabilities
38
- module_function
39
-
40
- # `RustConformanceHelpers#build_rust_for`'s own test, scoped to the
41
- # `[features]` table the way `bin/project_rust`'s Cargo sync scopes
42
- # its own lookup (a `[package] name = "rust"` line must never read
43
- # as a feature named `rust`).
44
- FEATURES_TABLE = /^\[features\](?:\n(?!\[).*)*$/
45
-
46
- # `bin/qa_sweep`'s former `POSTGRES_ERA_BINDING` — both spellings
47
- # `IsolatedBoot#rewrite_bindings!` has to catch: aggregate-scoped
48
- # (`Directory::Member.persisted_by("PostgresEra")`) and the bare
49
- # domain-level default (`persisted_by "PostgresEra"`).
50
- POSTGRES_ERA_BINDING = /persisted_by\s*\(?\s*"PostgresEra"/
51
-
52
- # A local variable assigned the bare literal `"PostgresEra"` on a line of
53
- # its own (`adapter = "PostgresEra"`). Only the whole-line literal counts:
54
- # a ternary or any other right-hand side is not a PostgresEra binding.
55
- POSTGRES_ERA_VARIABLE = /^\s*([a-z_]\w*)\s*=\s*"PostgresEra"\s*(?:#.*)?$/
56
-
57
- # `HecksagonBuilder#uses_framework "X"` — captures which member a
58
- # hecksagon attaches, whatever its name. Whether that member answers
59
- # a role check is then read off its own declaration (`provides
60
- # "authorization"`, via `Framework.providers_of`), the same rule
61
- # `Registry#authorization_provider_for` applies at runtime — never a
62
- # match on the literal "Governance".
63
- FRAMEWORK_ATTACHED = /uses_framework\s*\(?\s*"([^"]+)"/
64
-
65
- # A command-level `role "..."` — the only construct
66
- # `refuse_role_mismatch` ever has anything to check a caller against.
67
- ROLE_GATED = /^\s*role\s+"/
68
-
69
- # `authorize :vault_access, tenant: :branch_code`
70
- # (examples/banking/bluebook/safe_deposit_boxes.bluebook) — the one
71
- # `tenant:` spelling the corpus has.
72
- TENANT_SCOPED = /\btenant:/
73
-
74
- PROCESS_MANAGER = /^\s*process_manager\s+"/
75
-
76
- # Which capabilities each mode needs before it can say anything true
77
- # about a target. An empty list means "any target at all" — every
78
- # domain boots under Memory, so Ruby-only properties and the
79
- # self-consistency pass are always answerable. `ruby_only` is listed
80
- # requirement-free on purpose and then excluded by `resolve` whenever
81
- # `differential` resolved too: they are the same seat, and a compiled
82
- # Rust binary is strictly the better occupant (item 1 of the
83
- # detection plan folded the Ruby-only property battery into the
84
- # differential seat, so nothing is lost by the exclusion).
85
- #
86
- # The four `false`-by-default modes in `QualityControlDials::MODES`
87
- # (`adapter_parity_postgres`, `era_boundary`, `concurrency`,
88
- # `wasm_front`) are named here with their requirements even though
89
- # nothing runs them yet — so `resolved modes:` can already say, per
90
- # target, which of them would be eligible the day a human flips the
91
- # dial, and so flipping it is a one-line data change rather than a
92
- # code change plus a data change.
93
- MODE_REQUIREMENTS = {
94
- differential: %w[rust],
95
- ruby_only: [],
96
- self_consistency: [],
97
- properties_in_differential: %w[rust],
98
- structural_skip_report: %w[rust],
99
- adapter_parity_sqlite: %w[sqlite],
100
- persistence_parity: %w[postgres_era],
101
- adapter_parity_postgres: %w[postgres_era],
102
- era_boundary: %w[translations postgres_era],
103
- concurrency: %w[postgres_era],
104
- wasm_front: %w[rust]
105
- }.freeze
106
-
107
- # Which of those modes `bin/qa_sweep` can actually run today.
108
- # `MODE_REQUIREMENTS` above says what a mode needs; this says what
109
- # exists to do it, and the two are not the same. Conflating them is
110
- # how `qa/settings.yml` came to enable `wasm_front` and
111
- # `adapter_parity_postgres` with nothing behind either — no seat, no
112
- # check folded into the seed loop, no `MODE_EXPECTATIONS` entry — while
113
- # `resolved modes:` still printed them per target, so the sweep
114
- # advertised coverage it never performed. A mode named in
115
- # `MODE_REQUIREMENTS` but absent here is one this practice wants and has
116
- # not built: `bin/qa_sweep` refuses to start when a dial or `--modes`
117
- # enables it, rather than resolving it into a line nobody can act on.
118
- # `spec/qa_sweep_runnable_modes_spec.rb` keeps this list honest from
119
- # both sides by grepping the runner itself.
120
- RUNNABLE_MODES = %i[differential ruby_only self_consistency properties_in_differential
121
- structural_skip_report adapter_parity_sqlite persistence_parity
122
- era_boundary concurrency].freeze
123
-
124
- # Modes that name a separate, expensive pass of their own rather than
125
- # an extra check folded into the ordinary per-seed loop —
126
- # `bin/qa_sweep` runs these only when asked by name (`--modes
127
- # persistence_parity`, or its older alias `--persistence-parity`) or
128
- # as `--all`'s own second wave, never silently inside a plain
129
- # single-target sweep (the seed cap `PERSISTENCE_PARITY_SEED_CAP`
130
- # exists because that pass pays for real Postgres I/O per dispatch).
131
- DEFERRED_MODES = %i[persistence_parity adapter_parity_postgres era_boundary concurrency].freeze
132
-
133
- # Reads a target directory to find out which capabilities it actually has.
134
- #
135
- # Sorted, plain strings — comma-joined by the runner into the
136
- # `Target.Release(capabilities:)` value object and printed verbatim
137
- # on the `resolved modes:` line, so the same spelling is what a
138
- # human reads, what `--all` parses back, and what the ledger stores.
139
- #
140
- # @param domain_path [String] filesystem path to the target domain's directory
141
- # @param rust_dir [String] path to the Rust project root, checked for a matching
142
- # Cargo feature; defaults to this repo's own `rust/` directory
143
- # @return [Array<String>] the target's capabilities, sorted; a subset of
144
- # `sqlite`, `rust`, `postgres_era`, `translations`, `governance`, `role_gated`,
145
- # `tenant`, `sagas`
146
- def infer(domain_path, rust_dir: File.expand_path("../../../rust", __dir__))
147
- capabilities = %w[sqlite]
148
- capabilities << "rust" if rust_feature?(domain_path, rust_dir)
149
- capabilities << "postgres_era" if postgres_era_bound?(domain_path)
150
- capabilities << "translations" if Dir.glob(File.join(domain_path, "**", "translations", "*.bluebook")).any?
151
- capabilities << "governance" if authorization_attached?(domain_path)
152
- capabilities << "role_gated" if any_file?(domain_path, "*.bluebook", ROLE_GATED)
153
- capabilities << "tenant" if any_file?(domain_path, "*.bluebook", TENANT_SCOPED)
154
- capabilities << "sagas" if any_file?(domain_path, "*.bluebook", PROCESS_MANAGER)
155
- capabilities.sort
156
- end
157
-
158
- # Answers whether `mode` can run against a target with `capabilities`.
159
- #
160
- # @param mode [Symbol, String] a key of `MODE_REQUIREMENTS`, such as `:differential`
161
- # @param capabilities [Array<String>] the target's capabilities, as `infer` returns
162
- # @return [Boolean] whether `capabilities` covers every capability `mode` requires
163
- # @raise [ArgumentError] if `mode` names no entry in `MODE_REQUIREMENTS`
164
- def eligible?(mode, capabilities)
165
- required = MODE_REQUIREMENTS.fetch(mode.to_sym) { raise ArgumentError, "unknown sweep mode #{mode.inspect}" }
166
- (required - capabilities).empty?
167
- end
168
-
169
- # The one rule. `enabled` is whatever the dial (or `--modes`) turned
170
- # on, in the dial's own declaration order — that order is preserved
171
- # so the printed line reads the same way the dial does. Then the
172
- # single exclusion named on `MODE_REQUIREMENTS`.
173
- #
174
- # @param enabled [Array<Symbol, String>] modes turned on, in the dial's own
175
- # declaration order
176
- # @param capabilities [Array<String>] the target's capabilities, as `infer` returns
177
- # @return [Array<Symbol>] `enabled`'s eligible modes, in `enabled`'s order, with
178
- # `:ruby_only` dropped whenever `:differential` is also eligible
179
- def resolve(enabled, capabilities)
180
- resolved = enabled.map(&:to_sym).select { |mode| eligible?(mode, capabilities) }
181
- resolved.delete(:ruby_only) if resolved.include?(:differential)
182
- resolved
183
- end
184
-
185
- # Answers whether `rust_dir`'s Cargo.toml declares a feature named after
186
- # `domain_path`'s own directory.
187
- #
188
- # @param domain_path [String] filesystem path to the target domain's directory;
189
- # its basename, lowercased, is the feature name looked up
190
- # @param rust_dir [String] path to the Rust project root (holds `Cargo.toml`)
191
- # @return [Boolean] whether a matching Cargo feature is declared; false if
192
- # `rust_dir` has no `Cargo.toml`
193
- def rust_feature?(domain_path, rust_dir)
194
- cargo_toml = File.join(rust_dir, "Cargo.toml")
195
- return false unless File.file?(cargo_toml)
196
-
197
- feature = File.basename(domain_path).downcase
198
- features = File.read(cargo_toml)[FEATURES_TABLE] || ""
199
- features.match?(/^#{Regexp.escape(feature)}\s*=\s*\[\]/)
200
- end
201
-
202
- # Answers whether any `.hecksagon` under `domain_path` binds PostgresEra,
203
- # either with the literal name or through a local variable that the same
204
- # file assigned `"PostgresEra"` and then passed to `persisted_by`.
205
- #
206
- # @param domain_path [String] filesystem path to the target domain's directory
207
- # @return [Boolean] whether a PostgresEra `persisted_by` binding is found
208
- def postgres_era_bound?(domain_path)
209
- Dir.glob(File.join(domain_path, "**", "*.hecksagon")).any? do |path|
210
- text = File.read(path)
211
- text.match?(POSTGRES_ERA_BINDING) || postgres_era_variable_bound?(text)
212
- end
213
- end
214
-
215
- # Answers whether `text` passes a variable assigned `"PostgresEra"` to `persisted_by`.
216
- #
217
- # @param text [String] the contents of one `.hecksagon` file
218
- # @return [Boolean] whether some `persisted_by` call names such a variable
219
- def postgres_era_variable_bound?(text)
220
- text.scan(POSTGRES_ERA_VARIABLE).flatten.any? do |name|
221
- text.match?(/persisted_by\s*\(?\s*#{Regexp.escape(name)}(?!\w)/)
222
- end
223
- end
224
-
225
- # Answers whether any `.hecksagon` under `domain_path` attaches a member
226
- # that provides authorization.
227
- #
228
- # The capability label stays "governance" — it is the value
229
- # `bin/qa_sweep` writes into the QualityControl ledger's
230
- # `Target.capabilities`, and renaming it is a ledger change, not
231
- # part of dropping the name check.
232
- #
233
- # @param domain_path [String] filesystem path to the target domain's directory
234
- # @return [Boolean] whether the domain attaches a member that provides the
235
- # `Bluebook::Capabilities::AUTHORIZATION` capability
236
- def authorization_attached?(domain_path)
237
- attached = Dir.glob(File.join(domain_path, "**", "*.hecksagon"))
238
- .flat_map { |path| File.read(path).scan(FRAMEWORK_ATTACHED).flatten }.uniq
239
- providers = Framework.providers_of(Bluebook::Capabilities::AUTHORIZATION)
240
- attached.intersect?(providers)
241
- end
242
-
243
- # Answers whether any file under `domain_path` matching `glob` contains `pattern`.
244
- #
245
- # @param domain_path [String] filesystem path to the target domain's directory
246
- # @param glob [String] a `Dir.glob` pattern relative to `domain_path`, such as
247
- # `"*.bluebook"`
248
- # @param pattern [Regexp] the pattern each matching file's contents is tested against
249
- # @return [Boolean] whether any matching file's contents match `pattern`
250
- def any_file?(domain_path, glob, pattern)
251
- Dir.glob(File.join(domain_path, "**", glob)).any? { |path| File.read(path).match?(pattern) }
252
- end
253
- end
254
- end
255
- end