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,604 +0,0 @@
1
- require "fileutils"
2
- require "tmpdir"
3
- require "securerandom"
4
-
5
- module Hecks
6
- module Fuzzing
7
- # A fresh, in-process adapter for every ephemeral boot.
8
- #
9
- # ## Why rebind at all
10
- #
11
- # Fuzzing/replay copies a domain to a tmpdir and boots from there
12
- # specifically to get zero-history state — `rm_rf`ing the copy's own
13
- # `data/` achieves that for a file-based adapter (Memory,
14
- # SqlitePersistence, Heki), because copying the directory copies the
15
- # store. It achieves nothing for an adapter that lives outside the
16
- # copied directory entirely — Postgres, named by a fixed connection
17
- # string in `.world` (examples/pizzas/bluebook/pizzas.world, for
18
- # instance). Copying the directory does not copy or isolate the
19
- # database, so a "fresh" boot against a Postgres-bound domain would
20
- # still see every record any other run, ever, wrote to it.
21
- #
22
- # So every `.hecksagon` in the copy gets its persistence binding
23
- # rewritten to Memory before booting, and any `projected_by` bind
24
- # dropped outright (optional — `Registry#read_repository` already
25
- # falls back to the authoritative repository when none exists). The
26
- # domain's own rules and shape are untouched ; only which adapter this
27
- # one ephemeral copy answers through changes. What the domain is
28
- # bound to for real deployment is never touched — only this tmp copy.
29
- #
30
- # ## `adapter:` modes
31
- #
32
- # `adapter:` (PRD 02) picks which real persistence one ephemeral boot
33
- # answers through. `:memory` is the default and the only mode every
34
- # caller that passes no `adapter:` still gets. `:sqlite` rebinds to
35
- # the real SQLite adapter instead of the in-memory one: 15 declared
36
- # properties (properties.rb) and every fuzz/replay run only ever
37
- # exercised Memory's own hand-written repository until this mode
38
- # existed, and `spec/adapters/query_agreement_spec.rb` already found 4
39
- # shipped query bugs from exactly that comparison, on a fixed corpus
40
- # far smaller than what the fuzzer generates. `:sqlite` rewrites to
41
- # `"SqlitePersistence"` — the name
42
- # `lib/hecks/adapters/driven/sqlite.adapter` actually registers under
43
- # (`Sqlite` is the class; `SqlitePersistence` is a thin subclass
44
- # that's the one real port binding names) — and needs no `.world`
45
- # settings at all, the same reason Memory needs none:
46
- # `Sqlite#resolve_path` already defaults an
47
- # unbound `database` setting to `"data/<table>.db"` relative to
48
- # `root:`, which `Hecks.boot(copy)` passes as this ephemeral copy's
49
- # own directory — a fresh, empty `data/` per run, exactly like
50
- # Memory's own zero-history guarantee, just backed by a real SQLite
51
- # file instead of a Hash. `:postgres` (PRD 02, docs/prds/02-fuzzer-
52
- # real-adapters.md) and `:postgres_era` (`rebind_to_postgres_era!`'s
53
- # own header, below) are the two real-Postgres modes. Each writes its
54
- # own fresh `.world` per `.hecksagon` rather than relying on the
55
- # zero-config default Sqlite/Memory get, and each needs a real,
56
- # reachable Postgres server: `:postgres` sources one shared,
57
- # permanent scratch database/schema this module itself owns (see
58
- # `FUZZ_POSTGRES_DATABASE`'s own header); `:postgres_era` instead
59
- # requires the caller to supply (and own the lifecycle of) its own
60
- # throwaway `database:`/`schema:`, since its only caller
61
- # (`bin/qa_sweep --persistence-parity`) already has to manage a
62
- # disposable database of its own, never a shared one this module could
63
- # safely default to.
64
- module IsolatedBoot
65
- module_function
66
-
67
- # PRD 02 (docs/future-features.md) — `adapter:` picks which real
68
- # persistence this one ephemeral boot answers through, not just
69
- # Memory. `:memory` is the original, zero-config behavior (every
70
- # other caller in this codebase that doesn't pass `adapter:` gets
71
- # exactly what it always got). `:sqlite` is nearly as cheap —
72
- # `Adapters::Sqlite#resolve_path` already defaults to
73
- # `data/<table>.db` under the copy's own root with no settings at
74
- # all, the same "needs nothing declared" property Memory has, so a
75
- # real on-disk B-tree gets exercised for free. `:postgres` is the
76
- # expensive one — a real server, a real round trip per dispatch —
77
- # so `bin/fuzz --adapter postgres` is meant to run with smaller
78
- # seed/step counts than the Memory default, not as a like-for-like
79
- # swap; see that flag's own comment.
80
- # `database:`/`schema:` are only meaningful for `adapter: :postgres_era`
81
- # — see `rebind_to_postgres_era!`'s own header for why that mode takes
82
- # caller-supplied connection identity instead of a hardcoded shared
83
- # constant the way `:postgres` does. Every other adapter ignores both;
84
- # accepting them unconditionally here (rather than a separate method
85
- # signature per adapter) keeps `SequenceGenerator`/`Replay`'s own
86
- # single passthrough (`adapter:`, now joined by these two) uniform
87
- # across all four modes.
88
- #
89
- # @param domain_path [String] path to the real domain directory to copy and isolate
90
- # @param adapter [Symbol] which persistence the copy answers through: `:memory`
91
- # (default), `:sqlite`, `:postgres`, or `:postgres_era`
92
- # @param database [String, nil] the database name, required only for
93
- # `adapter: :postgres_era`; ignored by every other adapter
94
- # @param schema [String, nil] the schema name, required only for
95
- # `adapter: :postgres_era`; ignored by every other adapter
96
- # @param scratch [Hash{Symbol => String}] `:database` and/or `:schema` for
97
- # `adapter: :postgres` to use instead of the shared fuzz scratch schema, so a caller
98
- # such as `bin/bench` never drops the schema a concurrent `bin/fuzz` is using;
99
- # ignored by every other adapter
100
- # @yield [String] the freshly rebound copy's root directory, ready to boot
101
- # @yieldreturn [Object] anything; becomes this method's own return value
102
- # @return [Object] whatever the given block returns
103
- # @raise [ArgumentError] if `adapter` is not `:memory`, `:sqlite`, `:postgres`, or
104
- # `:postgres_era`, or if `adapter: :postgres_era` is given without both
105
- # `database:` and `schema:`
106
- def call(domain_path, adapter: :memory, database: nil, schema: nil, scratch: {})
107
- Dir.mktmpdir("hecks-fuzz") do |tmp|
108
- copy = File.join(tmp, File.basename(domain_path))
109
- copy_dereferencing(domain_path, copy)
110
- FileUtils.rm_rf(File.join(copy, "data"))
111
- case adapter
112
- when :memory then rebind_to_memory!(copy)
113
- when :sqlite then rebind_to_sqlite!(copy)
114
- when :postgres then rebind_to_postgres!(copy, **scratch)
115
- when :postgres_era then rebind_to_postgres_era!(copy, database: database, schema: schema)
116
- else raise ArgumentError,
117
- "unknown fuzz adapter #{adapter.inspect} — :memory, :sqlite, :postgres, or :postgres_era"
118
- end
119
- yield copy
120
- end
121
- end
122
-
123
- # Symlinks are followed, not copied. `FileUtils.cp_r` reproduces a
124
- # symlink as a symlink, and a relative one then points at nothing
125
- # from a tmpdir — `lib/hecks/framework/bluebook/compliance
126
- # .bluebook` is exactly that, a link to
127
- # `examples/compliance/bluebook/compliance.bluebook`, so the whole
128
- # framework domain failed to boot here with a LoadError naming a
129
- # path under /var/folders that had never existed. Nothing about the
130
- # domain was wrong; the copy was.
131
- #
132
- # `FileUtils.cp` follows a symlink and copies its content, which is
133
- # what an isolated boot wants: the copy has to stand alone, since
134
- # rebind! rewrites files in it and must not reach back through a
135
- # link into the real tree.
136
- #
137
- # The vendored bluebook packages the domain's hecksagons name with
138
- # `uses_embryonaut_bluebook` are carried too — see `carry_vendored_bluebooks!`.
139
- #
140
- # @param source [String] the real domain directory to copy
141
- # @param destination [String] the tmpdir path to copy it into; created if missing
142
- # @return [void]
143
- def copy_dereferencing(source, destination)
144
- copy_files(source, destination)
145
- carry_vendored_bluebooks!(source, destination)
146
- end
147
-
148
- # Copies every file under `source` into `destination`, following symlinks.
149
- #
150
- # @param source [String] the directory to copy
151
- # @param destination [String] the path to copy it into; created if missing
152
- # @return [void]
153
- def copy_files(source, destination)
154
- FileUtils.mkdir_p(destination)
155
- Dir.glob(File.join(source, "**", "*"), File::FNM_DOTMATCH).each do |path|
156
- next if [".", ".."].include?(File.basename(path))
157
-
158
- target = File.join(destination, path.delete_prefix("#{source}/"))
159
- if File.directory?(path)
160
- FileUtils.mkdir_p(target)
161
- else
162
- FileUtils.mkdir_p(File.dirname(target))
163
- begin
164
- FileUtils.cp(path, target)
165
- rescue Errno::ENOENT
166
- # A transient sibling (an adapter's atomic-write `.tmp.<pid>`
167
- # file, caught mid-rename by the glob above) can vanish
168
- # between listing and copy — under parallel_r-spec two
169
- # workers share the real example tree. A file that no longer
170
- # exists was never part of the state this copy needs.
171
- end
172
- end
173
- end
174
- end
175
-
176
- # Copies the vendored bluebook packages the copy's hecksagons name into
177
- # the place a boot of the copy looks for them.
178
- #
179
- # `Hecks.boot` roots a registry at the parent of the directory it boots,
180
- # and `uses_embryonaut_bluebook "<name>"` loads from
181
- # `<root>/vendor/embryonaut_bluebooks/<name>/bluebook` (see
182
- # `EmbryonautBluebook.load!`). The copy holds only the domain directory,
183
- # so without this a target that vendors a package crashed the boot with
184
- # "no vendored embryonaut bluebook named ...". Only the packages a
185
- # hecksagon names are copied, never the rest of `vendor/`. A package the
186
- # source root does not have is skipped, so the boot's own error stands.
187
- #
188
- # @param source [String] the real domain directory
189
- # @param destination [String] the copy's directory; its parent is the copy's root
190
- # @return [void]
191
- def carry_vendored_bluebooks!(source, destination)
192
- names = Dir.glob(File.join(destination, "**", "*.hecksagon")).flat_map do |path|
193
- File.read(path).scan(/uses_embryonaut_bluebook\s*\(?\s*"([\w-]+)"/).flatten
194
- end
195
- names.uniq.each do |name|
196
- package = File.join(File.dirname(source), "vendor", "embryonaut_bluebooks", name)
197
- next unless File.directory?(package)
198
-
199
- copy_files(package, File.join(File.dirname(destination), "vendor", "embryonaut_bluebooks", name))
200
- end
201
- end
202
-
203
- # Rewrites every `.hecksagon` in the copy to bind through Memory and drops its
204
- # `.world` files, so the boot needs no settings at all.
205
- #
206
- # @param copy [String] the isolated copy's root directory
207
- # @return [void]
208
- def rebind_to_memory!(copy)
209
- rewrite_bindings!(copy, "Memory")
210
- strip_translations!(copy)
211
-
212
- # **The settings, not just the bind** — `WorldBuilder#method_missing`
213
- # stores a settings block under both "verb:adapter" and the bare
214
- # "verb" (world_builder.rb:32-33), so a bind rewritten to Memory
215
- # still falls back to whatever adapter's settings were declared
216
- # bare — Postgres's `database:`, which Memory does not take and
217
- # WiringError refuses on sight. Both Memory and Sqlite need no
218
- # settings at all (`Sqlite#resolve_path` defaults `database` to
219
- # `data/<table>.db` under `root:` when unset — the same
220
- # zero-config default Memory gets from having no `.world` at
221
- # all), so the simplest correct fix for either target is
222
- # dropping `.world` from the copy entirely.
223
- Dir.glob(File.join(copy, "**", "*.world")).each { |path| File.delete(path) }
224
- end
225
-
226
- # Same dance as memory, one adapter over — `Adapters::Sqlite#
227
- # resolve_path` (adapters/driven/sqlite.rb) defaults to
228
- # `data/<table>.db` under the boot's own root when no `database`
229
- # setting is declared, which `data/` already being cleared makes a
230
- # fresh on-disk store with zero configuration, the same "no .world
231
- # needed" property Memory has. `data/` itself is left for Sqlite to
232
- # recreate on first write, same as it always was for Memory/Heki —
233
- # nothing here creates it up front.
234
- #
235
- # @param copy [String] the isolated copy's root directory
236
- # @return [void]
237
- def rebind_to_sqlite!(copy)
238
- rewrite_bindings!(copy, "SqlitePersistence")
239
- strip_translations!(copy)
240
- Dir.glob(File.join(copy, "**", "*.world")).each { |path| File.delete(path) }
241
- end
242
-
243
- # **The expensive one** — Postgres has no zero-config default the way
244
- # Sqlite/Memory do (`Adapters::Postgres.connect_for` refuses outright
245
- # with no `database` setting), so dropping `.world` the way the
246
- # other two do would just move the WiringError from "wrong adapter"
247
- # to "no adapter." Every domain name this copy declares gets a
248
- # fresh `.world` written for it instead — not a rewrite of whatever
249
- # was there, a replacement, same reasoning `rewrite_bindings!`
250
- # already applies to `.hecksagon`: this ephemeral boot owns every
251
- # binding decision, nothing about a real deployment's own settings
252
- # is relevant or safe to half-preserve here.
253
- #
254
- # One shared schema, dropped and recreated before every boot — not a
255
- # fresh randomly-named one per call. `bin/fuzz` drives every
256
- # ephemeral boot sequentially (one `IsolatedBoot.call` fully exits
257
- # before the next begins — see that file's own single-threaded
258
- # `while` loop), so nothing is ever concurrent here ; a fresh name
259
- # every time would just leak schemas in `FUZZ_POSTGRES_DATABASE`
260
- # forever with nothing to ever drop them. If a caller ever does
261
- # start running fuzz adapters concurrently, this needs to move to a
262
- # process-unique schema name (`SecureRandom.hex` is already
263
- # `require`d here for exactly that day) — flagged, not solved,
264
- # since nothing today calls this from more than one thread.
265
- FUZZ_POSTGRES_DATABASE = "hecks_fuzz".freeze
266
- FUZZ_POSTGRES_SCHEMA = "hecks_fuzz".freeze
267
-
268
- # Rewrites every `.hecksagon` in the copy to bind through Postgres, against the
269
- # shared `FUZZ_POSTGRES_DATABASE`/`FUZZ_POSTGRES_SCHEMA` scratch schema unless the
270
- # caller names its own, which lets `bin/bench` run without touching the fuzzer's.
271
- #
272
- # @param copy [String] the isolated copy's root directory
273
- # @param database [String, nil] the scratch database, `FUZZ_POSTGRES_DATABASE` when nil
274
- # @param schema [String, nil] the scratch schema, `FUZZ_POSTGRES_SCHEMA` when nil
275
- # @return [void]
276
- def rebind_to_postgres!(copy, database: nil, schema: nil)
277
- require "pg"
278
- database ||= FUZZ_POSTGRES_DATABASE
279
- schema ||= FUZZ_POSTGRES_SCHEMA
280
- rewrite_bindings!(copy, "Postgres")
281
- strip_translations!(copy)
282
- ensure_fuzz_schema!(database, schema)
283
-
284
- # One `.world` per directory a `.hecksagon` actually lives in, not
285
- # one at `copy`'s own root — `Folder#load_domain` resolves a
286
- # single `bluebook_directory` and globs `*.world` there, non-
287
- # recursively (`Folder#load_each`); a domain can hold several
288
- # `Hecks.hecksagon "<Name>" do ... end` sibling blocks in that one
289
- # file (banking.hecksagon declares "Banking", "Governance", and
290
- # "Identity" together), so every name found in one directory
291
- # is bundled into one `.world` written there, not scattered by name.
292
- write_worlds!(copy, "hecks_fuzz_postgres.world") do |name|
293
- <<~WORLD
294
- Hecks.world "#{name}" do
295
- persisted_by("Postgres") do
296
- database "#{database}"
297
- schema "#{schema}"
298
- end
299
- end
300
- WORLD
301
- end
302
- end
303
-
304
- # Admin connection lives outside the tmp copy entirely — same as
305
- # every other real-Postgres spec in this repo (`support/
306
- # postgres_probe.rb`'s own header). Database created once per
307
- # process and remembered (`@fuzz_database_ready` on this module's
308
- # own singleton, the same memoization shape `PostgresProbe
309
- # .available?` already uses) ; the schema inside it is dropped and
310
- # recreated on every call, which is what actually isolates one
311
- # ephemeral boot's data from the next.
312
- #
313
- # @param database [String] the scratch database to create if missing
314
- # @param schema [String] the schema to drop and recreate inside it
315
- # @return [void]
316
- def ensure_fuzz_schema!(database = FUZZ_POSTGRES_DATABASE, schema = FUZZ_POSTGRES_SCHEMA)
317
- # `Adapters::Postgres#initialize` opens one real `PG::Connection`
318
- # per aggregate and never explicitly closes it — fine for a
319
- # process that boots once and runs, exactly what every other
320
- # caller of this adapter is. A fuzz run boots dozens to hundreds
321
- # of ephemeral times in one process (every seed does at least a
322
- # generate + a replay, `replay_is_deterministic` doubles that,
323
- # shrinking multiplies it further), and `PG::Connection` only
324
- # actually closes its socket when Ruby's GC finalizes the
325
- # (by-then-unreferenced) object — which, under a tight loop like
326
- # this, does not reliably keep pace with how fast new
327
- # connections open. Discovered live: `bin/fuzz --adapter
328
- # postgres` against `examples/banking` hit a real local
329
- # Postgres's own `max_connections` after roughly a dozen
330
- # ephemeral boots, `PG::ConnectionBad: ... "too many clients
331
- # already"`. A `GC.start` here — right before the next ephemeral
332
- # boot's connections open, not on some timer — reclaims every
333
- # connection the previous boot's now-unreferenced adapters held,
334
- # keeping the live count bounded regardless of run length. This
335
- # is a real constraint on running Postgres in a loop, not
336
- # something to route around by connecting less carefully.
337
- GC.start
338
-
339
- @fuzz_databases_ready ||= {}
340
- unless @fuzz_databases_ready[database]
341
- admin = PG.connect(dbname: "postgres")
342
- exists = admin.exec_params(
343
- "SELECT 1 FROM pg_database WHERE datname = $1", [database]
344
- ).ntuples.positive?
345
- admin.exec(%(CREATE DATABASE "#{database}")) unless exists
346
- admin.close
347
- @fuzz_databases_ready[database] = true
348
- end
349
-
350
- db = PG.connect(dbname: database)
351
- # **Quiet on purpose** — same as `Adapters::Postgres.connect_for`'s
352
- # own `SET client_min_messages`: a `DROP SCHEMA ... CASCADE` that
353
- # actually has something to drop (every boot after the first)
354
- # NOTICEs once per dropped object, which is the ordinary case
355
- # here, not news, and would otherwise bury `bin/fuzz`'s own
356
- # output under a wall of "drop cascades to table ..." on every
357
- # single ephemeral boot.
358
- db.exec("SET client_min_messages = warning")
359
- quoted = db.quote_ident(schema)
360
- db.exec("DROP SCHEMA IF EXISTS #{quoted} CASCADE")
361
- db.exec("CREATE SCHEMA #{quoted}")
362
- db.close
363
- end
364
-
365
- # The adapter `:postgres` never touches — `Postgres` and `PostgresEra`
366
- # are sibling, not interchangeable, adapters (see postgres_era.rb's
367
- # own header: "the only one that declares the LINEAGE capability").
368
- # PRD 02 (docs/prds/02-fuzzer-real-adapters.md) shipped `:postgres`
369
- # and explicitly scoped `PostgresEra` out: "nothing here touches
370
- # era/lineage machinery." That gap is real, not cosmetic —
371
- # `examples/directory` (a `compute`/`rekey` translation edge, the
372
- # one domain in this corpus that actually exercises PostgresEra-
373
- # bound SQL compilation) had to be shelved out of `hecks_qa`'s own
374
- # rotation for exactly this reason: every existing fuzz/replay path,
375
- # `:memory` included, structurally cannot reach it. This mode closes
376
- # that — `bin/qa_sweep --persistence-parity` is its first caller.
377
- #
378
- # No shared constant database, unlike `:postgres` above — deliberate.
379
- # `rebind_to_postgres!`'s own `FUZZ_POSTGRES_DATABASE`/`_SCHEMA` are
380
- # module-level constants because `bin/fuzz --adapter postgres` is a
381
- # general-purpose, run-it-anytime tool with no caller-tracked
382
- # lifecycle of its own. This mode's only caller
383
- # (`bin/qa_sweep --persistence-parity`) is different: it dispatches
384
- # through `QualityControl::Target.claim!`'s own cross-process lock
385
- # first (see that script's own header), so at most one sweep is ever
386
- # touching a given target's own disposable database at a time — but
387
- # the caller, not this module, is what owns that database's name and
388
- # lifecycle (created, and genuinely dropped, by the caller itself),
389
- # exactly the discipline `spec/support/qa_sweep_all_fixture.rb`'s own header
390
- # describes and this repository's persistence-parity work is
391
- # required to follow. Accepting `database:`/`schema:` as required
392
- # keyword arguments (never a fallback constant) is what keeps that
393
- # ownership from silently drifting back onto this module the way
394
- # `:postgres`'s own `FUZZ_POSTGRES_DATABASE` already has.
395
- #
396
- # @param copy [String] the isolated copy's root directory
397
- # @param database [String] the caller-owned throwaway database name
398
- # @param schema [String] the caller-owned throwaway schema name
399
- # @return [void]
400
- # @raise [ArgumentError] if `database` or `schema` is empty
401
- def rebind_to_postgres_era!(copy, database:, schema:)
402
- require "pg"
403
- if database.to_s.empty? || schema.to_s.empty?
404
- raise ArgumentError,
405
- "adapter: :postgres_era requires both database: and schema: — a throwaway database/schema " \
406
- "THIS CALLER creates and drops itself (see rebind_to_postgres_era!'s own header). " \
407
- "There is no shared default, unlike :postgres, so the caller cannot forget to own the lifecycle."
408
- end
409
-
410
- rewrite_bindings!(copy, "PostgresEra")
411
- ensure_postgres_era_schema!(database: database, schema: schema)
412
-
413
- # Same one-`.world`-per-`.hecksagon`-directory shape `rebind_to_
414
- # postgres!` uses, for the identical reason (`Folder#
415
- # load_domain` globs `*.world` non-recursively) — see
416
- # `write_worlds!`. `PostgresEra`
417
- # additionally takes `schema:` (postgres_era.rb's own "shared-
418
- # instance isolation" comment): the caller-supplied throwaway
419
- # schema is what actually isolates this one ephemeral boot from
420
- # the next, the same job `FUZZ_POSTGRES_SCHEMA` does for `:postgres`
421
- # — `connect_for` itself idempotently `CREATE SCHEMA IF NOT
422
- # EXISTS`s it, so this method only ever needs to drop it first
423
- # (in `ensure_postgres_era_schema!`, below) for the zero-history
424
- # guarantee every other adapter mode already gives.
425
- #
426
- # `allow_superuser true` — on the record, on purpose. A bare
427
- # `database` connects as the ambient Postgres user, and
428
- # PostgresEra refuses to boot at all when that user is a
429
- # superuser (its era write-fence is row-level security, which a
430
- # superuser walks through — `Lineage#check_fence_applies!`,
431
- # BUG#24). That refusal protects a real ledger from an old
432
- # checkout's stale writes; nothing here is one. This is an
433
- # ephemeral boot into a throwaway schema the caller itself
434
- # creates and drops, whose data no second checkout ever shares,
435
- # and what it compares is Memory's answers against PostgresEra's
436
- # own SQL — the era fence is not under test and cannot be
437
- # crossed. So opt in explicitly rather than make every
438
- # persistence-parity run first provision a fenced role for a
439
- # database it is about to throw away; the one-line warning
440
- # PostgresEra prints per boot under the opt-in is the honest
441
- # price. Inert on a machine whose ambient user is ordinary.
442
- write_worlds!(copy, "hecks_fuzz_postgres_era.world") do |name|
443
- <<~WORLD
444
- Hecks.world "#{name}" do
445
- persisted_by("PostgresEra") do
446
- database "#{database}"
447
- schema "#{schema}"
448
- allow_superuser true
449
- end
450
- end
451
- WORLD
452
- end
453
- end
454
-
455
- # Writes one `.world` file per directory that holds a `.hecksagon`, naming every
456
- # `Hecks.hecksagon` block found in that directory, and deletes every other `.world`
457
- # in the copy.
458
- #
459
- # One world file per directory, every hecksagon name in that directory — never one
460
- # write per `*.hecksagon` file. A domain's `context_map.hecksagon` sits beside its
461
- # main hecksagon (the `qa` domain declares Governance in one and QualityControl in
462
- # the other), and a second `File.write` to the same world path would drop the first
463
- # file's names, leaving that domain bound to Postgres with no `database` to open.
464
- #
465
- # Any `.world` the copy shipped with (a real deployment's own connection string) is
466
- # redundant with, and would conflict with (`Registry#add_world`'s own header, on
467
- # loading the same domain name twice), the fresh one written here, so it goes the same
468
- # way `rebind_to_memory!` sends every `.world` in a Memory-mode boot.
469
- #
470
- # @param copy [String] the isolated copy's root directory
471
- # @param world_file [String] the basename of the world file written in each directory
472
- # @yield [name] builds the world text for one hecksagon name
473
- # @yieldparam name [String] a domain name a `Hecks.hecksagon` block in the directory declares
474
- # @yieldreturn [String] the `Hecks.world` block for that name
475
- # @return [void]
476
- def write_worlds!(copy, world_file, &world_for)
477
- names_by_dir = Hash.new { |hash, dir| hash[dir] = [] }
478
- Dir.glob(File.join(copy, "**", "*.hecksagon")).each do |hecksagon_path|
479
- names = File.read(hecksagon_path).scan(/Hecks\.hecksagon\s+"([^"]+)"/).flatten
480
- names_by_dir[File.dirname(hecksagon_path)].concat(names)
481
- end
482
- names_by_dir.each do |dir, names|
483
- next if names.empty?
484
-
485
- File.write(File.join(dir, world_file), names.uniq.map(&world_for).join("\n"))
486
- end
487
-
488
- Dir.glob(File.join(copy, "**", "*.world")).each do |path|
489
- File.delete(path) unless File.basename(path) == world_file
490
- end
491
- end
492
-
493
- # The zero-history guarantee for this mode — `DROP SCHEMA ... CASCADE`
494
- # before every ephemeral boot, mirroring `ensure_fuzz_schema!` above
495
- # (same `GC.start`-before-connecting fix for the identical
496
- # `max_connections` exhaustion that method's own comment documents —
497
- # `PostgresEra` opens real `PG::Connection`s exactly like `Postgres`
498
- # does, same unclosed-until-GC'd lifetime). The database itself is
499
- # created here too, idempotently (`CREATE DATABASE IF NOT EXISTS`
500
- # has no Postgres spelling, hence the existence check) — but never
501
- # dropped here: this module creates it once per process because
502
- # `Hecks.boot` needs it to exist before `PostgresEra.connect_for`'s
503
- # own `PG.connect(dbname: ...)` can succeed at all, but dropping it
504
- # again is the caller's own job (its name and lifecycle belong to
505
- # the caller — see `rebind_to_postgres_era!`'s own header), not
506
- # something this per-ephemeral-boot helper should ever do mid-sweep.
507
- #
508
- # @param database [String] the caller-owned database name, created if it does not
509
- # already exist
510
- # @param schema [String] the caller-owned schema name, dropped and recreated
511
- # @return [void]
512
- def ensure_postgres_era_schema!(database:, schema:)
513
- GC.start
514
-
515
- admin = PG.connect(dbname: "postgres")
516
- exists = admin.exec_params(
517
- "SELECT 1 FROM pg_database WHERE datname = $1", [database]
518
- ).ntuples.positive?
519
- admin.exec(%(CREATE DATABASE "#{database}")) unless exists
520
- admin.close
521
-
522
- db = PG.connect(dbname: database)
523
- db.exec("SET client_min_messages = warning")
524
- quoted = db.quote_ident(schema)
525
- db.exec("DROP SCHEMA IF EXISTS #{quoted} CASCADE")
526
- db.close
527
- end
528
-
529
- # The shared rewrite — factored out of `rebind_to_memory!` when
530
- # Sqlite/Postgres modes needed the identical `.hecksagon` surgery
531
- # with only the target adapter name differing. `persisted_by`/
532
- # `projected_by` can be spelled two ways: aggregate-scoped
533
- # (`Banking::Customer.persisted_by("Heki")`, always parenthesised)
534
- # and, since §0's domain-level default binds, a bare call at the
535
- # hecksagon's own root (`persisted_by "Heki"`, no parens, no
536
- # receiver). Both must be caught here or a domain that only
537
- # declares the bare form keeps its real adapter under an
538
- # "isolated" fuzz boot. `projected_by` is dropped outright rather
539
- # than rebound, for every target adapter — `Registry
540
- # #read_repository` already falls back to the authoritative
541
- # repository when none exists, so a read model this ephemeral copy
542
- # never wires is simply unread, not broken.
543
- # A `compute`/`rekey` translation edge refuses to boot at all under
544
- # any non-lineage-capable adapter — found live, wiring this very
545
- # mode up against `examples/directory`: `Runtime::EraCheck
546
- # .check_compute_rules!` (era_check.rb) runs unconditionally for
547
- # every loaded bluebook once the era plugin is loaded at all
548
- # (`bin/qa_sweep`'s own top-of-file `require "hecks/ports/
549
- # persistence/plugins/era"`, needed for the ledger's own
550
- # PostgresEra-bound aggregates), and refuses outright — "compute
551
- # rules require the Postgres adapter" — for any aggregate whose
552
- # lineage carries a `compute` rule and whose bound adapter is not
553
- # lineage-capable. `PostgresEra` is the only adapter that answers
554
- # `lineage_capable? == true` (postgres_era.rb's own `self.
555
- # lineage_capable? = true`) — plain `Postgres` does not, so this
556
- # refusal was already real for `:postgres`/`:sqlite`/`:memory`
557
- # alike, for any domain with a translation edge, before this
558
- # mode's own `:postgres_era` ever existed. This is very likely the
559
- # mechanical reason `examples/directory` had to be shelved out of
560
- # `hecks_qa`'s own rotation in the first place — not merely "less
561
- # interesting to fuzz on Memory," but "cannot boot on Memory at
562
- # all" once the era plugin is loaded, which every real
563
- # `bin/qa_sweep` invocation already does.
564
- #
565
- # The fix is to drop the edge, not to chase the refusal — an
566
- # ephemeral, zero-history replay boot (every mode `IsolatedBoot`
567
- # offers) never has a pre-existing era-1 row to translate in the
568
- # first place, so the translation edge is irrelevant to anything a
569
- # fuzz/replay run actually exercises (ordinary command dispatch
570
- # against a fresh boot) — it only ever matters at mint time,
571
- # against a real, pre-existing database
572
- # (`PostgresEra::LineageManager.check!`, a wholly separate,
573
- # human-approved path this harness was never meant to reach).
574
- # Dropping it here is exactly the same move `rebind_to_memory!`
575
- # already makes for `.world` (irrelevant/conflicting settings for
576
- # an ephemeral boot, deleted outright) — never called for
577
- # `:postgres_era` itself, where the bound adapter genuinely is
578
- # lineage-capable and the edge causes no refusal to begin with.
579
- #
580
- # @param copy [String] the isolated copy's root directory
581
- # @return [void]
582
- def strip_translations!(copy)
583
- Dir.glob(File.join(copy, "**", "translations", "*.bluebook")).each { |path| File.delete(path) }
584
- end
585
-
586
- # Rewrites every `persisted_by` bind — aggregate-scoped or bare at the
587
- # hecksagon's own root, naming its adapter as a literal or through a local
588
- # variable — in the copy's `.hecksagon` files to name `adapter_name`,
589
- # and drops every `projected_by` bind outright.
590
- #
591
- # @param copy [String] the isolated copy's root directory
592
- # @param adapter_name [String] the port binding name to rewrite every
593
- # `persisted_by` to, such as `"Memory"` or `"SqlitePersistence"`
594
- # @return [void]
595
- def rewrite_bindings!(copy, adapter_name)
596
- Dir.glob(File.join(copy, "**", "*.hecksagon")).each do |path|
597
- lines = File.readlines(path).grep_v(/\bprojected_by\s*\(?\s*"/)
598
- bind = /persisted_by\s*\(?\s*(?:"[^"]+"|[a-z_]\w*)\s*\)?/
599
- File.write(path, lines.join.gsub(bind, "persisted_by(\"#{adapter_name}\")"))
600
- end
601
- end
602
- end
603
- end
604
- end