hecks 2.9.0 → 3.0.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 (600) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +51 -6
  3. data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
  4. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  5. data/lib/hecks/adapters/driven/lambda.rb +1 -1
  6. data/lib/hecks/adapters/driven/memory.rb +16 -3
  7. data/lib/hecks/adapters/driven/postgres/reconnect.rb +6 -2
  8. data/lib/hecks/adapters/driven/postgres/shared_connection.rb +180 -0
  9. data/lib/hecks/adapters/driven/postgres.rb +4 -2
  10. data/lib/hecks/adapters/driven/prism.rb +41 -8
  11. data/lib/hecks/adapters/driven/tenant_provisioner.rb +73 -16
  12. data/lib/hecks/behaviors/expectations.rb +1 -1
  13. data/lib/hecks/behaviors.rb +1 -1
  14. data/lib/hecks/bench/cli.rb +62 -0
  15. data/lib/hecks/bench/environment.rb +71 -0
  16. data/lib/hecks/bench/postgres_probe.rb +30 -0
  17. data/lib/hecks/bench/report.rb +76 -0
  18. data/lib/hecks/bench/ruby_runner.rb +93 -0
  19. data/lib/hecks/bench/run.rb +55 -0
  20. data/lib/hecks/bench/rust_runner.rb +101 -0
  21. data/lib/hecks/bench/stats.rb +46 -0
  22. data/lib/hecks/bench/suite.rb +98 -0
  23. data/lib/hecks/bench/workload.rb +89 -0
  24. data/lib/hecks/bench.rb +17 -0
  25. data/lib/hecks/bluebook/assembly/contracts.rb +2 -0
  26. data/lib/hecks/bluebook/attribute.rb +1 -1
  27. data/lib/hecks/bluebook/behaviour/aggregate.rb +30 -1
  28. data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -0
  29. data/lib/hecks/bluebook/chapter.rb +6 -2
  30. data/lib/hecks/bluebook/domain_port.rb +36 -5
  31. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +10 -0
  32. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +8 -0
  33. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -1
  34. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +31 -3
  35. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +122 -3
  36. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +1 -1
  37. data/lib/hecks/bluebook/dsl/query_builder.rb +27 -1
  38. data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -1
  39. data/lib/hecks/bluebook/dsl/world_builder.rb +29 -2
  40. data/lib/hecks/bluebook/expression/canonical_form.rb +1 -1
  41. data/lib/hecks/bluebook/expression/resolver.rb +3 -2
  42. data/lib/hecks/bluebook/hexagon.rb +13 -5
  43. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -0
  44. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +4 -1
  45. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +246 -0
  46. data/lib/hecks/bluebook/meta_validator.rb +9 -4
  47. data/lib/hecks/bluebook/model_check.rb +23 -0
  48. data/lib/hecks/bluebook/pattern_subset.rb +38 -0
  49. data/lib/hecks/bluebook/query.rb +20 -5
  50. data/lib/hecks/bluebook/smoke_test.rb +1 -1
  51. data/lib/hecks/canonical_json.rb +30 -0
  52. data/lib/hecks/chapters.rb +102 -0
  53. data/lib/hecks/cli/behaviors.rb +61 -0
  54. data/lib/hecks/cli/canonicalise.rb +25 -0
  55. data/lib/hecks/cli/check_engine_agreement.rb +32 -0
  56. data/lib/hecks/cli/check_era.rb +88 -0
  57. data/lib/hecks/cli/console.rb +66 -0
  58. data/lib/hecks/cli/deploy_template_diff.rb +40 -0
  59. data/lib/hecks/cli/doc_coverage.rb +32 -0
  60. data/lib/hecks/cli/document.rb +2 -2
  61. data/lib/hecks/cli/expression_projection.rb +41 -0
  62. data/lib/hecks/cli/follow.rb +88 -0
  63. data/lib/hecks/cli/history.rb +44 -0
  64. data/lib/hecks/cli/ir.rb +1 -1
  65. data/lib/hecks/cli/mcp.rb +3 -3
  66. data/lib/hecks/cli/model_check.rb +7 -4
  67. data/lib/hecks/cli/pattern_cases.rb +47 -0
  68. data/lib/hecks/cli/present.rb +39 -0
  69. data/lib/hecks/cli/project_cli.rb +224 -32
  70. data/lib/hecks/cli/project_diagrams.rb +4 -3
  71. data/lib/hecks/cli/project_glossary.rb +44 -0
  72. data/lib/hecks/cli/reference.rb +46 -0
  73. data/lib/hecks/cli/refresh_projections.rb +44 -0
  74. data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +150 -0
  75. data/lib/hecks/cli/release.rb +70 -0
  76. data/lib/hecks/cli/release_gem.rb +59 -0
  77. data/lib/hecks/cli/rspec_io_parallel_files.rb +137 -0
  78. data/lib/hecks/cli/rspec_shard_files.rb +66 -0
  79. data/lib/hecks/cli/run.rb +3 -3
  80. data/lib/hecks/cli/seed_semantics_corpus.rb +71 -0
  81. data/lib/hecks/cli/serve_query_ir_mcp.rb +30 -0
  82. data/lib/hecks/cli/shape.rb +70 -0
  83. data/lib/hecks/cli/smoke_http.rb +230 -0
  84. data/lib/hecks/cli/smoke_test.rb +5 -3
  85. data/lib/hecks/cli/spec_example.rb +27 -0
  86. data/lib/hecks/cli/statements.rb +32 -0
  87. data/lib/hecks/cli/stores.rb +4 -5
  88. data/lib/hecks/cli/stress_concurrency_specs.rb +177 -0
  89. data/lib/hecks/cli.rb +201 -52
  90. data/lib/hecks/codemod.rb +324 -0
  91. data/lib/hecks/corpus.rb +423 -0
  92. data/lib/hecks/deploy/bluebook/deploy.bluebook +591 -23
  93. data/lib/hecks/deploy/bluebook/deploy.hecksagon +41 -4
  94. data/lib/hecks/deploy/oidc.json +82 -2
  95. data/lib/hecks/doc/reference.rb +351 -0
  96. data/lib/hecks/doors/cli_door.rb +196 -0
  97. data/lib/hecks/doors/cli_runner.rb +297 -0
  98. data/lib/hecks/{facade → doors}/command_request.rb +1 -1
  99. data/lib/hecks/{facade → doors}/handle.rb +2 -2
  100. data/lib/hecks/{facade → doors}/json_door.rb +4 -4
  101. data/lib/hecks/doors/launcher_options.rb +129 -0
  102. data/lib/hecks/{cli → doors}/mcp_door.rb +7 -7
  103. data/lib/hecks/doors/mcp_door_scope.rb +97 -0
  104. data/lib/hecks/{facade/surface → doors/ruby_door}/aggregate_door.rb +8 -3
  105. data/lib/hecks/{facade/surface → doors/ruby_door}/chapter.rb +3 -3
  106. data/lib/hecks/{facade/surface.rb → doors/ruby_door.rb} +36 -5
  107. data/lib/hecks/doors.rb +61 -0
  108. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +1 -1
  109. data/lib/hecks/embryonaut_bluebook.rb +1 -1
  110. data/lib/hecks/engine_agreement.rb +156 -0
  111. data/lib/hecks/facade.rb +24 -37
  112. data/lib/hecks/forms/app.rb +6 -6
  113. data/lib/hecks/forms/banking_presentation.rb +50 -0
  114. data/lib/hecks/forms/field_shape.rb +2 -1
  115. data/lib/hecks/forms/port_argument.rb +1 -1
  116. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +393 -0
  117. data/lib/hecks/fuzzing/combination_miner.rb +86 -0
  118. data/lib/hecks/fuzzing/concurrent_dispatch.rb +214 -0
  119. data/lib/hecks/fuzzing/coverage_campaign.rb +143 -0
  120. data/lib/hecks/fuzzing/differential.rb +156 -0
  121. data/lib/hecks/fuzzing/domain_generator.rb +615 -0
  122. data/lib/hecks/fuzzing/era_boundary.rb +71 -0
  123. data/lib/hecks/fuzzing/form_census.rb +136 -0
  124. data/lib/hecks/fuzzing/generated_domain_check.rb +93 -0
  125. data/lib/hecks/fuzzing/invalid_value_generator.rb +64 -0
  126. data/lib/hecks/fuzzing/isolated_boot.rb +301 -0
  127. data/lib/hecks/fuzzing/nondeterministic.rb +51 -0
  128. data/lib/hecks/fuzzing/outside_world.rb +45 -0
  129. data/lib/hecks/fuzzing/persistence_parity.rb +102 -0
  130. data/lib/hecks/fuzzing/properties/corrections.rb +88 -0
  131. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +314 -0
  132. data/lib/hecks/fuzzing/properties/guards.rb +190 -0
  133. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +300 -0
  134. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +93 -0
  135. data/lib/hecks/fuzzing/properties/outbox.rb +89 -0
  136. data/lib/hecks/fuzzing/properties/querying.rb +175 -0
  137. data/lib/hecks/fuzzing/properties.rb +162 -0
  138. data/lib/hecks/fuzzing/qa_settings.rb +123 -0
  139. data/lib/hecks/fuzzing/replay.rb +505 -0
  140. data/lib/hecks/fuzzing/rotation_priority.rb +48 -0
  141. data/lib/hecks/fuzzing/rust_gap_manifest.rb +108 -0
  142. data/lib/hecks/fuzzing/self_consistency.rb +339 -0
  143. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +385 -0
  144. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +132 -0
  145. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +95 -0
  146. data/lib/hecks/fuzzing/sequence_generator/picker.rb +77 -0
  147. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +109 -0
  148. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +237 -0
  149. data/lib/hecks/fuzzing/sequence_generator.rb +189 -0
  150. data/lib/hecks/fuzzing/shrinker.rb +166 -0
  151. data/lib/hecks/fuzzing/structural_skips.rb +33 -0
  152. data/lib/hecks/fuzzing/sweep_depth.rb +43 -0
  153. data/lib/hecks/fuzzing/target_capabilities.rb +163 -0
  154. data/lib/hecks/fuzzing/value_generator.rb +216 -0
  155. data/lib/hecks/fuzzing.rb +23 -0
  156. data/lib/hecks/grammar/evolve.rb +336 -0
  157. data/lib/hecks/hecks/adapters/codebase/codemods.rb +45 -0
  158. data/lib/hecks/hecks/adapters/codebase/conformance.rb +71 -0
  159. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +202 -0
  160. data/lib/hecks/hecks/adapters/codebase/gem_registry.rb +80 -0
  161. data/lib/hecks/hecks/adapters/codebase/kernel_tables.rb +54 -0
  162. data/lib/hecks/hecks/adapters/codebase/language.rb +240 -0
  163. data/lib/hecks/hecks/adapters/codebase/npm_registry.rb +102 -0
  164. data/lib/hecks/hecks/adapters/codebase/publishing.rb +142 -0
  165. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +67 -0
  166. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +90 -0
  167. data/lib/hecks/hecks/adapters/codebase/ruby_child.rb +83 -0
  168. data/lib/hecks/hecks/adapters/codebase/secret_vault.rb +36 -0
  169. data/lib/hecks/hecks/adapters/codebase/source_tree.adapter +7 -0
  170. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +189 -0
  171. data/lib/hecks/hecks/adapters/codebase/sqlite_fixture.rb +71 -0
  172. data/lib/hecks/hecks/adapters/codebase/style.rb +171 -0
  173. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +55 -0
  174. data/lib/hecks/hecks/adapters/codebase/test_suite.rb +165 -0
  175. data/lib/hecks/hecks/adapters/codebase/tree.rb +111 -0
  176. data/lib/hecks/hecks/adapters/console_capture.rb +79 -0
  177. data/lib/hecks/hecks/adapters/deploy_toolchain.adapter +7 -0
  178. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +108 -0
  179. data/lib/hecks/hecks/adapters/git.adapter +7 -0
  180. data/lib/hecks/hecks/adapters/git.rb +102 -0
  181. data/lib/hecks/hecks/adapters/host_http.adapter +7 -0
  182. data/lib/hecks/hecks/adapters/host_http.rb +65 -0
  183. data/lib/hecks/hecks/adapters/in_process_boot.adapter +7 -0
  184. data/lib/hecks/hecks/adapters/in_process_boot.rb +197 -0
  185. data/lib/hecks/hecks/adapters/in_process_operations.rb +202 -0
  186. data/lib/hecks/hecks/adapters/journal_store/changes.rb +143 -0
  187. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +145 -0
  188. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +132 -0
  189. data/lib/hecks/hecks/adapters/journal_store/examination.rb +135 -0
  190. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +163 -0
  191. data/lib/hecks/hecks/adapters/journal_store/readings.rb +187 -0
  192. data/lib/hecks/hecks/adapters/journal_store.adapter +7 -0
  193. data/lib/hecks/hecks/adapters/journal_store.rb +64 -0
  194. data/lib/hecks/hecks/adapters/local_files.adapter +7 -0
  195. data/lib/hecks/hecks/adapters/local_files.rb +46 -0
  196. data/lib/hecks/hecks/adapters/pg_admin.adapter +7 -0
  197. data/lib/hecks/hecks/adapters/pg_admin.rb +245 -0
  198. data/lib/hecks/hecks/adapters/process_pool.adapter +7 -0
  199. data/lib/hecks/hecks/adapters/process_pool.rb +119 -0
  200. data/lib/hecks/hecks/adapters/rust_toolchain.adapter +7 -0
  201. data/lib/hecks/hecks/adapters/rust_toolchain.rb +179 -0
  202. data/lib/hecks/hecks/adapters/rust_workspace.rb +150 -0
  203. data/lib/hecks/hecks/adapters/shell.adapter +7 -0
  204. data/lib/hecks/hecks/adapters/shell.rb +49 -0
  205. data/lib/hecks/hecks/adapters/terminal.adapter +7 -0
  206. data/lib/hecks/hecks/adapters/terminal.rb +74 -0
  207. data/lib/hecks/hecks/codebase.behaviors +172 -0
  208. data/lib/hecks/hecks/codebase.bluebook +2453 -0
  209. data/lib/hecks/hecks/context_map.hecksagon +13 -0
  210. data/lib/hecks/hecks/custodian.behaviors +179 -0
  211. data/lib/hecks/hecks/custodian.bluebook +1847 -0
  212. data/lib/hecks/hecks/deploy.behaviors +74 -0
  213. data/lib/hecks/hecks/deploy.world +12 -0
  214. data/lib/hecks/hecks/environments/memory.world +10 -0
  215. data/lib/hecks/hecks/governance.world +7 -0
  216. data/lib/hecks/hecks/hecks.behaviors +62 -0
  217. data/lib/hecks/hecks/hecks.bluebook +124 -0
  218. data/lib/hecks/hecks/hecks.hecksagon +390 -0
  219. data/lib/hecks/hecks/hecks.world +42 -0
  220. data/lib/hecks/kernel_capabilities.rb +194 -0
  221. data/lib/hecks/language/bluebook/bluebook.bluebook +11 -0
  222. data/lib/hecks/language/bluebook/query.bluebook +7 -0
  223. data/lib/hecks/language/bluebook/vocabulary.bluebook +4 -4
  224. data/lib/hecks/language/hecksagon/domain_port.bluebook +10 -0
  225. data/lib/hecks/language/hecksagon/hecksagon.bluebook +20 -0
  226. data/lib/hecks/mcp_stdio_guard.rb +2 -2
  227. data/lib/hecks/persistence_legacy_fixture/regenerate.rb +161 -0
  228. data/lib/hecks/persistence_legacy_fixture.rb +113 -0
  229. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +8 -5
  230. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +4 -1
  231. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +15 -2
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +1 -1
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +1 -1
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +1 -1
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +32 -15
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +6 -3
  237. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +227 -0
  238. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +1 -1
  239. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +43 -24
  240. data/lib/hecks/ports/persistence/plugins/era/translation.rb +1 -0
  241. data/lib/hecks/ports/persistence/state_codec.rb +24 -5
  242. data/lib/hecks/projection_files.rb +285 -0
  243. data/lib/hecks/projections/bootstrap_table.rb +1 -1
  244. data/lib/hecks/projections/deploy/fargate.rb +5 -5
  245. data/lib/hecks/projections/deploy/lambda.rb +43 -32
  246. data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
  247. data/lib/hecks/projections/deploy/preview/template.rb +1 -1
  248. data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +1 -1
  249. data/lib/hecks/projections/deploy/scripts/expected-era.header +1 -1
  250. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +3 -3
  251. data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +1 -1
  252. data/lib/hecks/projections/deploy/shared.rb +1 -1
  253. data/lib/hecks/projections/diagrams.rb +3 -3
  254. data/lib/hecks/projections/model/deviations.rb +1 -1
  255. data/lib/hecks/projections/parser_table.rb +4 -4
  256. data/lib/hecks/projections/reference.rb +2 -3
  257. data/lib/hecks/projections/rust_vocabulary.rb +1 -1
  258. data/lib/hecks/projector/cli_projector.rb +106 -37
  259. data/lib/hecks/projector.rb +1 -1
  260. data/lib/hecks/quality_control/adapters/agent.adapter +7 -0
  261. data/lib/hecks/quality_control/adapters/agent.rb +58 -0
  262. data/lib/hecks/quality_control/adapters/angle_tools.adapter +7 -0
  263. data/lib/hecks/quality_control/adapters/angle_tools.rb +15 -0
  264. data/lib/hecks/quality_control/adapters/clearance_recorder.rb +36 -0
  265. data/lib/hecks/quality_control/adapters/clearance_tools.adapter +7 -0
  266. data/lib/hecks/quality_control/adapters/clearance_tools.rb +17 -0
  267. data/lib/hecks/quality_control/adapters/git_pr.adapter +7 -0
  268. data/lib/hecks/quality_control/adapters/git_pr.rb +226 -0
  269. data/lib/hecks/quality_control/adapters/github_checks.adapter +9 -0
  270. data/lib/hecks/quality_control/adapters/github_checks.rb +83 -0
  271. data/lib/hecks/quality_control/adapters/github_ci_webhook.rb +56 -0
  272. data/lib/hecks/quality_control/adapters/github_issues.adapter +7 -0
  273. data/lib/hecks/quality_control/adapters/github_issues.rb +49 -0
  274. data/lib/hecks/quality_control/adapters/qa_tool.rb +59 -0
  275. data/lib/hecks/quality_control/adapters/sweep_tools.adapter +7 -0
  276. data/lib/hecks/quality_control/adapters/sweep_tools.rb +51 -0
  277. data/lib/hecks/quality_control/adapters/target_tools.adapter +7 -0
  278. data/lib/hecks/quality_control/adapters/target_tools.rb +43 -0
  279. data/lib/hecks/quality_control/cli/child.rb +45 -0
  280. data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +45 -0
  281. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +248 -0
  282. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +163 -0
  283. data/lib/hecks/quality_control/cli/qa_generated_domains.rb +450 -0
  284. data/lib/hecks/quality_control/cli/qa_log_bug.rb +195 -0
  285. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +220 -0
  286. data/lib/hecks/quality_control/cli/qa_open_pr.rb +305 -0
  287. data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +154 -0
  288. data/lib/hecks/quality_control/cli/qa_postgres_role.rb +159 -0
  289. data/lib/hecks/quality_control/cli/qa_pr_check.rb +232 -0
  290. data/lib/hecks/quality_control/cli/qa_seed_angles.rb +142 -0
  291. data/lib/hecks/quality_control/cli/qa_seed_targets.rb +69 -0
  292. data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +280 -0
  293. data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +329 -0
  294. data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +92 -0
  295. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +117 -0
  296. data/lib/hecks/quality_control/cli/qa_sweep/parsing.rb +175 -0
  297. data/lib/hecks/quality_control/cli/qa_sweep/release_mode.rb +60 -0
  298. data/lib/hecks/quality_control/cli/qa_sweep.rb +611 -0
  299. data/lib/hecks/quality_control/cli/qa_tick.rb +274 -0
  300. data/lib/hecks/quality_control/quality_control.bluebook +2224 -0
  301. data/lib/hecks/quality_control/quality_control.hecksagon +13 -0
  302. data/lib/hecks/quality_control/quality_control.ports.hecksagon +54 -0
  303. data/lib/hecks/query_ir.rb +346 -0
  304. data/lib/hecks/query_ir_mcp.rb +198 -0
  305. data/lib/hecks/release/runner/ci_publisher.rb +2 -2
  306. data/lib/hecks/release/runner/clean_tree.rb +42 -0
  307. data/lib/hecks/release/runner/console.rb +6 -0
  308. data/lib/hecks/release/runner/gem_publisher.rb +14 -7
  309. data/lib/hecks/release/runner/npm_publisher.rb +6 -36
  310. data/lib/hecks/release/runner/preflight.rb +26 -2
  311. data/lib/hecks/release/runner/published.rb +8 -26
  312. data/lib/hecks/release/runner/tagger.rb +30 -15
  313. data/lib/hecks/release/runner.rb +57 -6
  314. data/lib/hecks/router/namespace_installer.rb +3 -3
  315. data/lib/hecks/runtime/adapter_lookup.rb +124 -0
  316. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +4 -0
  317. data/lib/hecks/runtime/dependency_planning.rb +20 -2
  318. data/lib/hecks/runtime/dispatcher.rb +53 -3
  319. data/lib/hecks/runtime/interpreting.rb +3 -0
  320. data/lib/hecks/runtime/loader.rb +65 -12
  321. data/lib/hecks/runtime/outbox.rb +1 -1
  322. data/lib/hecks/runtime/policy_interpreter.rb +5 -2
  323. data/lib/hecks/runtime/port_operation_interpreter.rb +2 -10
  324. data/lib/hecks/runtime/query_interpreter.rb +77 -0
  325. data/lib/hecks/runtime/reaction_outcome.rb +89 -0
  326. data/lib/hecks/runtime/refusal_wording.rb +23 -1
  327. data/lib/hecks/runtime/registry/verification.rb +163 -4
  328. data/lib/hecks/runtime/registry.rb +57 -3
  329. data/lib/hecks/runtime/remote_dispatcher.rb +31 -2
  330. data/lib/hecks/runtime/value/coercion.rb +23 -135
  331. data/lib/hecks/runtime/value/field_checks.rb +172 -0
  332. data/lib/hecks/runtime/value.rb +4 -1
  333. data/lib/hecks/runtime.rb +20 -6
  334. data/lib/hecks/rust_build/conformance.rb +158 -0
  335. data/lib/hecks/rust_build/conformance_fuzz.rb +71 -0
  336. data/lib/hecks/rust_build/coverage.rb +218 -0
  337. data/lib/hecks/rust_build/native_build.rb +124 -0
  338. data/lib/hecks/rust_build/project_rust/root_mod.rb +120 -0
  339. data/lib/hecks/rust_build/project_rust.rb +332 -0
  340. data/lib/hecks/rust_build/wasm.rb +105 -0
  341. data/lib/hecks/rust_build/wasm_browser.rb +113 -0
  342. data/lib/hecks/rust_build.rb +144 -0
  343. data/lib/hecks/storehouse.rb +16 -16
  344. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +2 -2
  345. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +1 -1
  346. data/lib/hecks/three_zero/forms.yml +127 -0
  347. data/lib/hecks/three_zero.rb +12 -0
  348. data/lib/hecks/tools/argument_gate_matrix.rb +316 -0
  349. data/lib/hecks/tools/comment_style.rb +1070 -0
  350. data/lib/hecks/tools/corpus_report.rb +87 -0
  351. data/lib/hecks/tools/deploy_recipe.rb +255 -0
  352. data/lib/hecks/tools/deploy_recipe_lint.rb +282 -0
  353. data/lib/hecks/tools/drop_implicit_append_fields.rb +95 -0
  354. data/lib/hecks/tools/era_projection_backfill.rb +94 -0
  355. data/lib/hecks/tools/era_reattest.rb +146 -0
  356. data/lib/hecks/tools/evolve_run.rb +189 -0
  357. data/lib/hecks/tools/fuzz_sweep.rb +373 -0
  358. data/lib/hecks/tools/heki_compaction.rb +100 -0
  359. data/lib/hecks/tools/hoist_local_givens.rb +246 -0
  360. data/lib/hecks/tools/journal_compaction.rb +89 -0
  361. data/lib/hecks/tools/oidc_manifests.rb +70 -0
  362. data/lib/hecks/tools/query_ir_run.rb +81 -0
  363. data/lib/hecks/tools/regeneration_run.rb +166 -0
  364. data/lib/hecks/tools/rust_comment_style.rb +642 -0
  365. data/lib/hecks/tools/sequence_script.rb +62 -0
  366. data/lib/hecks/tools/tail_merge.rb +117 -0
  367. data/lib/hecks/tools/tenant_provisioning.rb +142 -0
  368. data/lib/hecks/tools/translation_audit.rb +208 -0
  369. data/lib/hecks/tools/translation_scaffold.rb +131 -0
  370. data/lib/hecks/tools.rb +86 -0
  371. data/lib/hecks/version.rb +1 -1
  372. data/lib/hecks.rb +32 -15
  373. data/qa/settings.yml +66 -0
  374. data/rust/.cargo/config.toml +6 -0
  375. data/rust/.gitignore +2 -0
  376. data/rust/Cargo.lock +54 -0
  377. data/rust/Cargo.toml +61 -0
  378. data/rust/build/.cargo/config.toml +3 -0
  379. data/rust/build/.gitignore +1 -0
  380. data/rust/build/Cargo.lock +7 -0
  381. data/rust/build/Cargo.toml +19 -0
  382. data/rust/build/src/build_artifact.rs +47 -0
  383. data/rust/build/src/cargo_sync.rs +248 -0
  384. data/rust/build/src/json.rs +421 -0
  385. data/rust/build/src/lineage_pass.rs +285 -0
  386. data/rust/build/src/main.rs +51 -0
  387. data/rust/build/src/optional_pass.rs +150 -0
  388. data/rust/build/src/pipeline.rs +264 -0
  389. data/rust/build/src/reserved_names.rs +76 -0
  390. data/rust/build/src/resolve.rs +124 -0
  391. data/rust/build/src/root.rs +25 -0
  392. data/rust/build/src/sidecars.rs +73 -0
  393. data/rust/build/src/subprocess.rs +51 -0
  394. data/rust/build/src/tmp.rs +31 -0
  395. data/rust/codegen/.cargo/config.toml +3 -0
  396. data/rust/codegen/.gitignore +1 -0
  397. data/rust/codegen/Cargo.lock +7 -0
  398. data/rust/codegen/Cargo.toml +20 -0
  399. data/rust/codegen/src/attr.rs +32 -0
  400. data/rust/codegen/src/bridging.rs +481 -0
  401. data/rust/codegen/src/commands.rs +1226 -0
  402. data/rust/codegen/src/constraints.rs +109 -0
  403. data/rust/codegen/src/dependency_planning.rs +209 -0
  404. data/rust/codegen/src/domain_generator.rs +1288 -0
  405. data/rust/codegen/src/exemplar.rs +230 -0
  406. data/rust/codegen/src/expr_emitter.rs +87 -0
  407. data/rust/codegen/src/fielded.rs +192 -0
  408. data/rust/codegen/src/hecks_naming.rs +33 -0
  409. data/rust/codegen/src/json.rs +335 -0
  410. data/rust/codegen/src/json_codec.rs +697 -0
  411. data/rust/codegen/src/literal.rs +207 -0
  412. data/rust/codegen/src/main.rs +290 -0
  413. data/rust/codegen/src/manifest.rs +227 -0
  414. data/rust/codegen/src/mutations.rs +784 -0
  415. data/rust/codegen/src/naming.rs +286 -0
  416. data/rust/codegen/src/ports.rs +208 -0
  417. data/rust/codegen/src/prelude.rs +116 -0
  418. data/rust/codegen/src/queries.rs +786 -0
  419. data/rust/codegen/src/reactions.rs +532 -0
  420. data/rust/codegen/src/read_models.rs +697 -0
  421. data/rust/codegen/src/reference_specs.rs +40 -0
  422. data/rust/codegen/src/registry.rs +966 -0
  423. data/rust/codegen/src/reserved_names.rs +76 -0
  424. data/rust/codegen/src/shared.rs +93 -0
  425. data/rust/codegen/src/skip_reason.rs +25 -0
  426. data/rust/codegen/src/types.rs +362 -0
  427. data/rust/host/.cargo/config.toml +3 -0
  428. data/rust/host/.gitignore +1 -0
  429. data/rust/host/Cargo.lock +4297 -0
  430. data/rust/host/Cargo.toml +61 -0
  431. data/rust/host/HECKS_RELEASE +1 -0
  432. data/rust/host/rds-ca-bundle.pem +2736 -0
  433. data/rust/host/src/api.rs +1596 -0
  434. data/rust/host/src/approval.rs +652 -0
  435. data/rust/host/src/auth.rs +1287 -0
  436. data/rust/host/src/bin/expr_harness.rs +46 -0
  437. data/rust/host/src/bin/lineage_harness.rs +119 -0
  438. data/rust/host/src/bin/mint_harness.rs +135 -0
  439. data/rust/host/src/checkout.rs +265 -0
  440. data/rust/host/src/dispatch.rs +1443 -0
  441. data/rust/host/src/expr_json.rs +1117 -0
  442. data/rust/host/src/field_hints.rs +52 -0
  443. data/rust/host/src/ir.rs +733 -0
  444. data/rust/host/src/journal.rs +874 -0
  445. data/rust/host/src/lambda_client.rs +378 -0
  446. data/rust/host/src/log.rs +147 -0
  447. data/rust/host/src/main.rs +474 -0
  448. data/rust/host/src/mint.rs +1865 -0
  449. data/rust/host/src/payments/keystore.rs +330 -0
  450. data/rust/host/src/payments/tests.rs +1400 -0
  451. data/rust/host/src/payments.rs +767 -0
  452. data/rust/host/src/presentation.rs +698 -0
  453. data/rust/host/src/presentation_write.rs +1431 -0
  454. data/rust/host/src/query_step.rs +149 -0
  455. data/rust/host/src/rate_limit.rs +904 -0
  456. data/rust/host/src/reference_transform.rs +241 -0
  457. data/rust/host/src/reference_validate.rs +338 -0
  458. data/rust/host/src/resend.rs +296 -0
  459. data/rust/host/src/secrets.rs +106 -0
  460. data/rust/host/src/server.rs +663 -0
  461. data/rust/host/src/storage_shape.rs +175 -0
  462. data/rust/host/src/test_pg.rs +40 -0
  463. data/rust/host/src/ui_schema.rs +1351 -0
  464. data/rust/host/src/wasm_runner.rs +191 -0
  465. data/rust/host/src/web/newsletter/cooldown.rs +126 -0
  466. data/rust/host/src/web/newsletter/tests.rs +104 -0
  467. data/rust/host/src/web/newsletter.rs +310 -0
  468. data/rust/host/src/web/newsletter_send.rs +357 -0
  469. data/rust/host/src/web/registrations.rs +1505 -0
  470. data/rust/host/src/web.rs +2937 -0
  471. data/rust/lsp/.gitignore +1 -0
  472. data/rust/lsp/Cargo.lock +7 -0
  473. data/rust/lsp/Cargo.toml +16 -0
  474. data/rust/lsp/README.md +133 -0
  475. data/rust/lsp/src/diagnostics.rs +165 -0
  476. data/rust/lsp/src/json.rs +385 -0
  477. data/rust/lsp/src/main.rs +399 -0
  478. data/rust/lsp/src/outline.rs +177 -0
  479. data/rust/lsp/src/rpc.rs +67 -0
  480. data/rust/parser/.cargo/config.toml +5 -0
  481. data/rust/parser/.gitignore +1 -0
  482. data/rust/parser/Cargo.lock +7 -0
  483. data/rust/parser/Cargo.toml +21 -0
  484. data/rust/parser/src/build/closed_sets.rs +27 -0
  485. data/rust/parser/src/build/identity.rs +276 -0
  486. data/rust/parser/src/build/mod.rs +12 -0
  487. data/rust/parser/src/build/naming.rs +141 -0
  488. data/rust/parser/src/build/pattern_subset.rs +161 -0
  489. data/rust/parser/src/build/query_derive.rs +72 -0
  490. data/rust/parser/src/build/query_inference.rs +269 -0
  491. data/rust/parser/src/build/query_options.rs +100 -0
  492. data/rust/parser/src/build/read_model.rs +94 -0
  493. data/rust/parser/src/build/references.rs +32 -0
  494. data/rust/parser/src/canonical.rs +114 -0
  495. data/rust/parser/src/diag.rs +55 -0
  496. data/rust/parser/src/emit.rs +953 -0
  497. data/rust/parser/src/expr/ast_json.rs +147 -0
  498. data/rust/parser/src/expr/evaluator.rs +81 -0
  499. data/rust/parser/src/expr/mod.rs +84 -0
  500. data/rust/parser/src/expr/resolver.rs +497 -0
  501. data/rust/parser/src/ir.rs +387 -0
  502. data/rust/parser/src/keywords.rs +585 -0
  503. data/rust/parser/src/lex.rs +662 -0
  504. data/rust/parser/src/main.rs +252 -0
  505. data/rust/parser/src/parse/aggregate.rs +612 -0
  506. data/rust/parser/src/parse/chapter.rs +517 -0
  507. data/rust/parser/src/parse/command.rs +637 -0
  508. data/rust/parser/src/parse/domain_port.rs +127 -0
  509. data/rust/parser/src/parse/entity.rs +527 -0
  510. data/rust/parser/src/parse/file.rs +67 -0
  511. data/rust/parser/src/parse/hecksagon.rs +231 -0
  512. data/rust/parser/src/parse/lifecycle.rs +162 -0
  513. data/rust/parser/src/parse/mod.rs +1262 -0
  514. data/rust/parser/src/parse/policy.rs +87 -0
  515. data/rust/parser/src/parse/process_manager.rs +305 -0
  516. data/rust/parser/src/parse/query.rs +100 -0
  517. data/rust/parser/src/parse/read_model.rs +214 -0
  518. data/rust/parser/src/parse/value_object.rs +250 -0
  519. data/rust/parser/src/ruby_value.rs +422 -0
  520. data/rust/project/bridging.rb +257 -0
  521. data/rust/project/commands.rb +877 -0
  522. data/rust/project/constraints.rb +86 -0
  523. data/rust/project/dependency_planning.rb +178 -0
  524. data/rust/project/domain_generator.rb +980 -0
  525. data/rust/project/exemplar.rb +148 -0
  526. data/rust/project/expr_emitter.rb +72 -0
  527. data/rust/project/fielded.rb +141 -0
  528. data/rust/project/json_codec.rb +641 -0
  529. data/rust/project/mutations.rb +721 -0
  530. data/rust/project/naming.rb +205 -0
  531. data/rust/project/ports.rb +65 -0
  532. data/rust/project/queries.rb +493 -0
  533. data/rust/project/reactions.rb +317 -0
  534. data/rust/project/read_models.rb +690 -0
  535. data/rust/project/reference_specs.rb +27 -0
  536. data/rust/project/registry.rb +396 -0
  537. data/rust/project/skip_reason.rb +29 -0
  538. data/rust/project/types.rb +225 -0
  539. data/rust/project/write_if_changed.rb +83 -0
  540. data/rust/project.rb +30 -0
  541. data/rust/project_rust_pipeline.rb +383 -0
  542. data/rust/src/exemplar/commands.rs +279 -0
  543. data/rust/src/exemplar/constraints.rs +18 -0
  544. data/rust/src/exemplar/fielded.rs +280 -0
  545. data/rust/src/exemplar/json.rs +277 -0
  546. data/rust/src/exemplar/mod.rs +17 -0
  547. data/rust/src/exemplar/mutations.rs +80 -0
  548. data/rust/src/exemplar/queries.rs +33 -0
  549. data/rust/src/exemplar/reactions.rs +88 -0
  550. data/rust/src/exemplar/read_models.rs +48 -0
  551. data/rust/src/exemplar/registry.rs +166 -0
  552. data/rust/src/exemplar/types.rs +44 -0
  553. data/rust/src/kernel/attribute_shapes/composite.rs +88 -0
  554. data/rust/src/kernel/attribute_shapes/list.rs +18 -0
  555. data/rust/src/kernel/attribute_shapes/mod.rs +44 -0
  556. data/rust/src/kernel/attribute_shapes/optional.rs +9 -0
  557. data/rust/src/kernel/attribute_shapes/scalar.rs +39 -0
  558. data/rust/src/kernel/cli.rs +585 -0
  559. data/rust/src/kernel/dispatch.rs +1090 -0
  560. data/rust/src/kernel/expr.rs +458 -0
  561. data/rust/src/kernel/expression_operators/arithmetic.rs +128 -0
  562. data/rust/src/kernel/expression_operators/comparison.rs +48 -0
  563. data/rust/src/kernel/expression_operators/enumeration.rs +232 -0
  564. data/rust/src/kernel/expression_operators/logical.rs +13 -0
  565. data/rust/src/kernel/expression_operators/membership.rs +18 -0
  566. data/rust/src/kernel/expression_operators/mod.rs +68 -0
  567. data/rust/src/kernel/expression_operators/pattern_match.rs +73 -0
  568. data/rust/src/kernel/expression_operators/positional.rs +126 -0
  569. data/rust/src/kernel/expression_operators/presence.rs +101 -0
  570. data/rust/src/kernel/expression_operators/sign_test.rs +16 -0
  571. data/rust/src/kernel/expression_operators/sized.rs +35 -0
  572. data/rust/src/kernel/expression_operators/text.rs +79 -0
  573. data/rust/src/kernel/expression_operators/to_string.rs +18 -0
  574. data/rust/src/kernel/json.rs +779 -0
  575. data/rust/src/kernel/mod.rs +133 -0
  576. data/rust/src/kernel/named_query.rs +219 -0
  577. data/rust/src/kernel/orchestrate.rs +1810 -0
  578. data/rust/src/kernel/pattern.rs +307 -0
  579. data/rust/src/kernel/query_comparators.rs +206 -0
  580. data/rust/src/kernel/query_ordering.rs +103 -0
  581. data/rust/src/kernel/read_model.rs +914 -0
  582. data/rust/src/kernel/reference_lookup.rs +149 -0
  583. data/rust/src/kernel/repository.rs +468 -0
  584. data/rust/src/kernel/routing.rs +612 -0
  585. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +118 -0
  586. data/rust/src/kernel/vocab/entity_dispatch_order.rs +114 -0
  587. data/rust/src/kernel/vocab/field_hint.rs +70 -0
  588. data/rust/src/kernel/vocab/mod.rs +20 -0
  589. data/rust/src/kernel/vocab/query_comparator.rs +65 -0
  590. data/rust/src/kernel/vocab/refusal_template.rs +1592 -0
  591. data/rust/src/lib.rs +8 -0
  592. data/rust/src/main.rs +16 -0
  593. data/rust/web/.gitignore +1 -0
  594. data/rust/web/Cargo.lock +119 -0
  595. data/rust/web/Cargo.toml +18 -0
  596. data/rust/web/src/lib.rs +8 -0
  597. metadata +477 -12
  598. data/lib/hecks/facade/cli_door.rb +0 -96
  599. data/lib/hecks/facade/cli_runner.rb +0 -127
  600. data/lib/hecks/mcp_door_scope.rb +0 -95
@@ -0,0 +1,71 @@
1
+ require "hecks/ports/persistence/plugins/era"
2
+
3
+ module Hecks
4
+ module Fuzzing
5
+ # Checks a target's real, already-configured `PostgresEra` ledger for
6
+ # ancestor eras still holding writes nobody has merged forward.
7
+ module EraBoundary
8
+ module_function
9
+
10
+ # `kind:` on a `checked: false` result distinguishes a target with no
11
+ # lineage to audit (`:not_applicable`) from an audit that failed to
12
+ # run (`:error`) — `hecks quality_control ask run` holds the first and surfaces the
13
+ # second as a finding, instead of treating both as a clean audit.
14
+ def diverged_ancestor_writes(domain_path)
15
+ registry, directory = load_registry(domain_path)
16
+ bluebook = registry.bluebooks.values.first
17
+ return failed("no bluebook in #{directory}") unless bluebook
18
+
19
+ first = bluebook.aggregates.first
20
+ return not_applicable("#{bluebook.name} declares no aggregates") unless first
21
+
22
+ adapter_name = Hecks::Ports::Persistence::BindingPolicy.resolve(registry, bluebook.name, first).adapter
23
+ unless adapter_name == "PostgresEra"
24
+ return not_applicable("#{bluebook.name} is bound to #{adapter_name}, not PostgresEra")
25
+ end
26
+
27
+ settings = registry.binding_settings(bluebook.name, Hecks::Ports::Persistence::VERB, adapter_name)
28
+ db = Hecks::Adapters::PostgresEra.connect_for(bluebook.name, settings)
29
+ begin
30
+ lineage = Hecks::Adapters::PostgresEra::Lineage.new(db, bluebook.name)
31
+ lineage.ensure_base!
32
+ eras = lineage.eras
33
+ breakdown = if eras.size > 1
34
+ (1...eras.last[:ordinal]).map do |ordinal|
35
+ { ordinal: ordinal, diverged: lineage.diverged_count(ordinal) }
36
+ end
37
+ else
38
+ []
39
+ end
40
+ { checked: true, era_count: eras.size, breakdown: breakdown, diverged_total: breakdown.sum { |b| b[:diverged] } }
41
+ ensure
42
+ db.close
43
+ end
44
+ rescue StandardError => e
45
+ # A refused connection or malformed `.world` settings is not the
46
+ # same as nothing to audit, so this reports `:error`, never the
47
+ # benign `:not_applicable` shape.
48
+ failed("#{e.class}: #{e.message}")
49
+ end
50
+
51
+ def not_applicable(reason) = { checked: false, kind: :not_applicable, reason: reason }
52
+
53
+ def failed(reason) = { checked: false, kind: :error, reason: reason }
54
+
55
+ # Boots a fresh registry for the target domain, never the ledger's own
56
+ # registry (this asks about the swept target's lineage, not
57
+ # QualityControl's own).
58
+ def load_registry(domain_path)
59
+ loading = Hecks::Ports::Loading.bootstrap
60
+ directory = loading.bluebook_directory(domain_path)
61
+ registry = Hecks::Runtime::Registry.new(root: File.dirname(directory))
62
+ Hecks.with_registry(registry) do
63
+ loading.load_library
64
+ loading.load_project(loading.shared_root(nil, directory))
65
+ loading.load_domain(directory)
66
+ end
67
+ [registry, directory]
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,136 @@
1
+ require "json"
2
+ require_relative "../corpus"
3
+
4
+ module Hecks
5
+ module Fuzzing
6
+ # The declared forms an aggregate can exhibit, and the pairs it puts together.
7
+ #
8
+ # Shared by the combination-coverage spec and `hecks quality_control judge_novelty` so they
9
+ # cannot drift. The unit is one aggregate: forms on one head meet at dispatch, forms in one
10
+ # chapter do not.
11
+ module FormCensus
12
+ # A given path crossing two references has at least this many segments.
13
+ TWO_HOP_GIVEN_PATH_LENGTH = 3
14
+
15
+ FORMS = {
16
+ "composite_id" => ->(a) { (a["identified_by"] || []).size >= 2 },
17
+ "has_entity" => ->(a) { entities(a).any? },
18
+ "two_entities" => ->(a) { entities(a).size >= 2 },
19
+ "composite_piece" => ->(a) { entities(a).any? { |piece| (piece["identified_by"] || []).size >= 2 } },
20
+ "multi_emit" => ->(a) { commands(a).any? { |verb| (verb["emits"] || []).size >= 2 } },
21
+ "lifecycle" => ->(a) { !a["lifecycle"].nil? },
22
+ "piece_lifecycle" => ->(a) { entities(a).any? { |piece| !piece["lifecycle"].nil? } },
23
+ "has_query" => ->(a) { queries(a).any? },
24
+ "list_attr" => ->(a) { attributes(a).any? { |held| held["list"] } },
25
+ "reference_attr" => ->(a) { attributes(a).any? { |held| reference?(held) } },
26
+ "closed_set" => ->(a) { (a["value_objects"] || []).any? { |shape| shape["closed_set"] } },
27
+ "has_default" => ->(a) { attributes(a).any? { |held| !held["default"].nil? } },
28
+ "has_optional" => ->(a) { commands(a).any? { |verb| (verb["attributes"] || []).any? { |held| held["optional"] } } },
29
+ # Reference-hop forms, then `corrects` and `role`; entity commands count too.
30
+ "corrects" => ->(a) { every_command(a).any? { |verb| corrects?(verb) } },
31
+ "role_gated" => ->(a) { every_command(a).any? { |verb| !verb["role"].to_s.empty? } },
32
+ "two_hop_given" => ->(a) { two_hop_given?(a) },
33
+ "multi_hop_where" => ->(a) { multi_hop_where?(a) },
34
+ "revalued_reference" => ->(a) { revalued_reference?(a) }
35
+ }.freeze
36
+
37
+ module_function
38
+
39
+ def entities(aggregate) = aggregate["entities"] || []
40
+
41
+ def commands(aggregate) = aggregate["commands"] || []
42
+
43
+ # An aggregate's commands, its entities' included.
44
+ def every_command(aggregate) = commands(aggregate) + entities(aggregate).flat_map { |piece| commands(piece) }
45
+
46
+ def corrects?(verb) = (verb["mutations"] || []).any? { |change| change["op"].to_s == "corrects" }
47
+
48
+ def attributes(aggregate) = aggregate["attributes"] || []
49
+
50
+ def queries(aggregate) = aggregate["queries"] || []
51
+
52
+ def reference?(attribute) = attribute["type"].to_s.start_with?("Reference<")
53
+
54
+ def two_hop_given?(aggregate)
55
+ commands(aggregate).any? { |verb| (verb["givens"] || []).any? { |given| deep_lookup?(given["ast"]) } }
56
+ end
57
+
58
+ # A `where` whose field crosses two `/` hops, as in `member/sponsor/standing`.
59
+ def multi_hop_where?(aggregate)
60
+ queries(aggregate).any? { |query| (query["wheres"] || []).any? { |where| where["field"].to_s.count("/") >= 2 } }
61
+ end
62
+
63
+ # A command attribute reusing the name of a reference attribute under a non-reference type.
64
+ def revalued_reference?(aggregate)
65
+ references = attributes(aggregate).select { |held| reference?(held) }.to_set { |held| held["name"].to_s }
66
+ commands(aggregate).any? do |verb|
67
+ (verb["attributes"] || []).any? { |held| references.include?(held["name"].to_s) && !reference?(held) }
68
+ end
69
+ end
70
+
71
+ def deep_lookup?(node)
72
+ case node
73
+ when Hash
74
+ return true if node["op"] == "lookup" && Array(node["path"]).size >= TWO_HOP_GIVEN_PATH_LENGTH
75
+
76
+ node.each_value.any? { |child| deep_lookup?(child) }
77
+ when Array
78
+ node.any? { |child| deep_lookup?(child) }
79
+ else
80
+ false
81
+ end
82
+ end
83
+
84
+ def properties(aggregate)
85
+ FORMS.transform_values { |form| form.call(aggregate) }
86
+ end
87
+
88
+ def pairs
89
+ FORMS.keys.combination(2).map { |pair| pair_key(*pair) }
90
+ end
91
+
92
+ def pair_key(left, right) = [left, right].sort.join(" + ")
93
+
94
+ def covered_pairs(held)
95
+ held.each_with_object(Hash.new { |h, k| h[k] = [] }) do |(name, shows), covered|
96
+ shows.select { |_, present| present }.keys.combination(2).each do |left, right|
97
+ covered[pair_key(left, right)] << name
98
+ end
99
+ end
100
+ end
101
+
102
+ def aggregates_in(chapter_ir)
103
+ (chapter_ir["aggregates"] || []).map do |aggregate|
104
+ ["#{chapter_ir['name']}::#{aggregate['name']}", properties(aggregate)]
105
+ end
106
+ end
107
+
108
+ def bluebook_files(domain_path)
109
+ Hecks::Corpus.bluebook_files(domain_path)
110
+ end
111
+
112
+ # The census over a domain on disk, booted lightweight (no `Hecks.boot`, no database).
113
+ # Only the first-loaded chapter is measured.
114
+ #
115
+ # @raise [ArgumentError] if `domain_path` has no bluebook files
116
+ def census(domain_path)
117
+ root = File.expand_path("../../..", __dir__)
118
+ files = bluebook_files(domain_path)
119
+ raise ArgumentError, "#{domain_path} has no bluebook/*.bluebook (or *.bluebook) to measure" if files.nil?
120
+
121
+ registry = Hecks::Runtime::Registry.new(root: File.expand_path(domain_path))
122
+ Hecks.with_registry(registry) do
123
+ Kernel.load(File.join(root, "lib/hecks/ports/persistence.port"))
124
+ Kernel.load(File.join(root, "lib/hecks/ports/extraction.port"))
125
+ Kernel.load(File.join(root, "lib/hecks/adapters/driven/memory.adapter"))
126
+ Kernel.load(File.join(root, "lib/hecks/adapters/driven/prism.adapter"))
127
+ files.each { |file| Kernel.load(file) }
128
+ end
129
+
130
+ chapter_name = registry.bluebooks.keys.first
131
+ exported = JSON.parse(JSON.generate(Hecks::Projector::Exporter.call(registry).fetch(chapter_name)))
132
+ aggregates_in(exported)
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,93 @@
1
+ require "json"
2
+ require_relative "isolated_boot"
3
+ require_relative "sequence_generator"
4
+ require_relative "replay"
5
+ require_relative "shrinker"
6
+ require_relative "differential"
7
+
8
+ module Hecks
9
+ module Fuzzing
10
+ # The child-process half of `hecks quality_control check_generated_domains`: checks one
11
+ # generated domain.
12
+ #
13
+ # One domain per process, since every generated domain is named `QaGenerated`.
14
+ # A domain that fails to boot is `invalid` (a generator defect), not a finding.
15
+ module GeneratedDomainCheck
16
+ DIFFERENTIAL_MODES = %i[differential self_consistency properties_in_differential].freeze
17
+
18
+ module_function
19
+
20
+ # Checks one generated domain over up to `seeds` sequences, stopping at the first finding.
21
+ #
22
+ # With a compiled `binary` it diffs Ruby against Rust; without one it replays Ruby only.
23
+ # `match` (`{"mode" =>, "signature" =>}`) keeps only a finding that still matches, so
24
+ # domain-level shrinking can ask whether a smaller domain shows the same finding.
25
+ #
26
+ # @return [Hash] `"status"` is `"invalid"`, `"clean"` or `"found"`; a found finding is
27
+ # `#finding` merged with `"seeds_run"`, plus `"shrunk_steps"` if `shrink_budget` > 0
28
+ def run(domain_path, seeds:, steps:, adversarial:, binary: nil, differ: nil, match: nil, shrink_budget: 0)
29
+ error = boot_error(domain_path)
30
+ return { "status" => "invalid", "error" => error } if error
31
+
32
+ (1..seeds).each do |seed|
33
+ finding = check_seed(domain_path, seed, steps, adversarial, binary, differ, match)
34
+ next unless finding
35
+
36
+ finding["shrunk_steps"] = shrink(domain_path, finding, binary, differ, shrink_budget) if shrink_budget.positive?
37
+ return finding.merge("status" => "found", "seeds_run" => seed)
38
+ end
39
+ { "status" => "clean", "seeds_run" => seeds }
40
+ end
41
+
42
+ def boot_error(domain_path)
43
+ IsolatedBoot.call(domain_path) { |copy| Hecks.boot(copy, environment: nil) }
44
+ nil
45
+ rescue StandardError, ScriptError => e
46
+ "#{e.class}: #{e.message.lines.first&.strip}"
47
+ end
48
+
49
+ def check_seed(domain_path, seed, steps, adversarial, binary, differ, match)
50
+ sequence = SequenceGenerator.generate(domain_path, seed: seed, steps: steps, adversarial: adversarial)
51
+ rescue StandardError => e
52
+ finding(seed, :generator, [{ field: "generator_crash", detail: "#{e.class}: #{e.message}" }], [], match)
53
+ else
54
+ outcomes(domain_path, sequence, binary, differ).each do |mode, divergences|
55
+ found = finding(seed, mode, divergences, sequence, match)
56
+ return found if found
57
+ end
58
+ nil
59
+ end
60
+
61
+ # Diffs against Rust when `binary` is given, otherwise replays Ruby only.
62
+ def outcomes(domain_path, sequence, binary, differ)
63
+ return Differential.diff(differ, domain_path, sequence, binary, modes: DIFFERENTIAL_MODES) if binary
64
+
65
+ history = Replay.call(domain_path, sequence, self_consistency: true)
66
+ { ruby_only: Differential.property_divergences(history),
67
+ self_consistency: Differential.self_consistency_divergences(history) }
68
+ rescue StandardError => e
69
+ { ruby_only: [{ field: "crash", detail: "#{e.class}: #{e.message}" }] }
70
+ end
71
+
72
+ def finding(seed, mode, divergences, sequence, match)
73
+ return nil if divergences.empty?
74
+
75
+ signature = Shrinker.signature(divergences)
76
+ return nil if match && !(match["mode"] == mode.to_s && Set.new(match["signature"]).subset?(signature))
77
+
78
+ { "seed" => seed, "mode" => mode.to_s, "signature" => signature.to_a.sort, "steps" => sequence,
79
+ "divergences" => JSON.parse(JSON.generate(divergences)) }
80
+ end
81
+
82
+ # The step-level half of shrinking; the parent already made the domain as small as it could.
83
+ def shrink(domain_path, found, binary, differ, budget)
84
+ original = Set.new(found["signature"])
85
+ mode = found["mode"].to_sym
86
+ Shrinker.call(found["steps"], budget: budget) do |candidate|
87
+ divergences = outcomes(domain_path, candidate, binary, differ)[mode] || []
88
+ Shrinker.reproduces?(original, divergences)
89
+ end.steps
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,64 @@
1
+ module Hecks
2
+ module Fuzzing
3
+ # Values deliberately the wrong shape for an attribute; the sibling of ValueGenerator.
4
+ #
5
+ # Refusals are answers whose wording is pinned byte-for-byte, so these probe the refusal
6
+ # sentence rather than the happy path.
7
+ module InvalidValueGenerator
8
+ module_function
9
+
10
+ # Each kind names a real confusion, not random noise.
11
+ KINDS = %i[
12
+ object_for_scalar
13
+ scalar_for_object
14
+ float_for_integer
15
+ numeral_string
16
+ array_for_scalar
17
+ boolean_for_string
18
+ null
19
+ ].freeze
20
+
21
+ def corrupt(attribute, aggregate, random:)
22
+ kind = kinds_for(attribute, aggregate).sample(random: random)
23
+ build(kind, attribute, aggregate, random: random)
24
+ end
25
+
26
+ # Only the confusions that mean something for this attribute's declared type.
27
+ def kinds_for(attribute, aggregate)
28
+ value_object = aggregate.value_object(attribute.type.to_s)
29
+ return %i[scalar_for_object array_for_scalar null] if value_object
30
+ return %i[object_for_scalar float_for_integer numeral_string array_for_scalar null] if attribute.type.to_s == "Integer"
31
+
32
+ %i[object_for_scalar array_for_scalar boolean_for_string null]
33
+ end
34
+
35
+ def build(kind, attribute, aggregate, random:)
36
+ case kind
37
+ when :object_for_scalar then { "cents" => random.rand(1..1000) }
38
+ when :scalar_for_object then scalar_for(attribute, aggregate, random: random)
39
+ when :float_for_integer then random.rand(1.0..100.0).round(2)
40
+ when :numeral_string then random.rand(1..1000).to_s
41
+ when :array_for_scalar then [random.rand(1..10), random.rand(1..10)]
42
+ when :boolean_for_string then random.rand(2).zero?
43
+ end
44
+ end
45
+
46
+ # A bare scalar where a value object is declared.
47
+ # A single-field value object accepts one, so only a multi-field one makes it a refusal.
48
+ def scalar_for(attribute, aggregate, random:)
49
+ value_object = aggregate.value_object(attribute.type.to_s)
50
+ sole = value_object&.sole_attribute
51
+ return "a bare scalar" unless sole
52
+
53
+ # One field, so a scalar is legal; corrupt the field's own type instead.
54
+ { sole.name.to_s => ["nested", "array"] }
55
+ end
56
+
57
+ # An argument the command never declares, which exercises `refuse_unknown_arguments`.
58
+ def undeclared_argument(random:)
59
+ name = %w[colour flavour rank note].sample(random: random)
60
+ [name, %w[red loud third scribbled].sample(random: random)]
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,301 @@
1
+ require "fileutils"
2
+ require "tmpdir"
3
+ require "securerandom"
4
+ require_relative "outside_world"
5
+ require_relative "../runtime/adapter_lookup"
6
+
7
+ module Hecks
8
+ module Fuzzing
9
+ # Rewrites a copied domain's persistence bindings before an ephemeral
10
+ # fuzz/replay boot; a directory copy alone does not isolate Postgres state.
11
+ module IsolatedBoot
12
+ module_function
13
+
14
+ # Copies `domain_path` to a tmpdir, rebinds its persistence to `adapter`, and
15
+ # yields the copy's root. `:postgres` is expensive: use smaller seed/step counts.
16
+ #
17
+ # The block runs with `OutsideWorld` standing in for every adapter a port or a port-answered
18
+ # query reaches: a fuzz or replay checks the domain's own rules, and the adapters of the
19
+ # chapters that drive tools would run shells and write files from a generated sequence.
20
+ def call(domain_path, adapter: :memory, database: nil, schema: nil, scratch: {})
21
+ Dir.mktmpdir("hecks-fuzz") do |tmp|
22
+ copy = File.join(tmp, File.basename(domain_path))
23
+ copy_dereferencing(domain_path, copy)
24
+ FileUtils.rm_rf(File.join(copy, "data"))
25
+ case adapter
26
+ when :memory then rebind_to_memory!(copy)
27
+ when :sqlite then rebind_to_sqlite!(copy)
28
+ when :postgres then rebind_to_postgres!(copy, **scratch)
29
+ when :postgres_era then rebind_to_postgres_era!(copy, database: database, schema: schema)
30
+ else raise ArgumentError,
31
+ "unknown fuzz adapter #{adapter.inspect} — :memory, :sqlite, :postgres, or :postgres_era"
32
+ end
33
+ Runtime::AdapterLookup.standing_in(OutsideWorld) { yield copy }
34
+ end
35
+ end
36
+
37
+ # Copies `source` into `destination`, following symlinks rather than reproducing
38
+ # them (cp_r would copy a symlink as a symlink, dangling once the source tree is
39
+ # gone), and carries any vendored bluebooks the copy's hecksagons name.
40
+ def copy_dereferencing(source, destination)
41
+ copy_files(source, destination)
42
+ carry_vendored_bluebooks!(source, destination)
43
+ end
44
+
45
+ # Copies every file under `source` into `destination`, following symlinks.
46
+ def copy_files(source, destination)
47
+ FileUtils.mkdir_p(destination)
48
+ Dir.glob(File.join(source, "**", "*"), File::FNM_DOTMATCH).each do |path|
49
+ next if [".", ".."].include?(File.basename(path))
50
+
51
+ target = File.join(destination, path.delete_prefix("#{source}/"))
52
+ if File.directory?(path)
53
+ FileUtils.mkdir_p(target)
54
+ else
55
+ FileUtils.mkdir_p(File.dirname(target))
56
+ begin
57
+ FileUtils.cp(path, target)
58
+ rescue Errno::ENOENT
59
+ # A sibling can vanish between listing and copy under parallel workers;
60
+ # skip a file that's already gone.
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ # Copies vendored bluebook packages a copy's hecksagons name into the copy, since
67
+ # `Hecks.boot` only sees the domain directory itself, not `vendor/`.
68
+ def carry_vendored_bluebooks!(source, destination)
69
+ names = Dir.glob(File.join(destination, "**", "*.hecksagon")).flat_map do |path|
70
+ File.read(path).scan(/uses_embryonaut_bluebook\s*\(?\s*"([\w-]+)"/).flatten
71
+ end
72
+ names.uniq.each do |name|
73
+ package = File.join(File.dirname(source), "vendor", "embryonaut_bluebooks", name)
74
+ next unless File.directory?(package)
75
+
76
+ copy_files(package, File.join(File.dirname(destination), "vendor", "embryonaut_bluebooks", name))
77
+ end
78
+ end
79
+
80
+ # Rewrites every `.hecksagon` in the copy to bind through Memory and replaces its
81
+ # `.world` files with a `default_adapter "Memory"` one, so the boot needs no settings.
82
+ def rebind_to_memory!(copy)
83
+ rewrite_bindings!(copy, "Memory")
84
+ strip_translations!(copy)
85
+
86
+ # Also replaces the settings, not just the bind: a bare fallback setting (like
87
+ # Postgres's `database:`) would otherwise still apply and WiringError refuses it.
88
+ write_default_worlds!(copy, "Memory")
89
+ end
90
+
91
+ # Same as `rebind_to_memory!`, one adapter over — Sqlite also needs no `.world`
92
+ # settings, since `Sqlite#resolve_path` defaults `database` to `data/<table>.db`.
93
+ def rebind_to_sqlite!(copy)
94
+ rewrite_bindings!(copy, "SqlitePersistence")
95
+ strip_translations!(copy)
96
+ write_default_worlds!(copy, "SqlitePersistence")
97
+ end
98
+
99
+ # Postgres has no zero-config default (Postgres.connect_for refuses without a
100
+ # `database` setting), so every domain name gets a fresh `.world` written for it.
101
+ # One shared schema, dropped and recreated per boot, not a fresh name per call —
102
+ # safe only because callers run one ephemeral boot at a time.
103
+ FUZZ_POSTGRES_DATABASE = "hecks_fuzz".freeze
104
+ FUZZ_POSTGRES_SCHEMA = "hecks_fuzz".freeze
105
+
106
+ # Rewrites every `.hecksagon` in the copy to bind through Postgres, against the
107
+ # shared scratch schema unless the caller names its own via `scratch:`.
108
+ def rebind_to_postgres!(copy, database: nil, schema: nil)
109
+ require "pg"
110
+ database ||= FUZZ_POSTGRES_DATABASE
111
+ schema ||= FUZZ_POSTGRES_SCHEMA
112
+ rewrite_bindings!(copy, "Postgres")
113
+ strip_translations!(copy)
114
+ ensure_fuzz_schema!(database, schema)
115
+
116
+ # One `.world` per directory a `.hecksagon` lives in, not one at the copy's
117
+ # root — `Folder#load_domain` globs `*.world` non-recursively.
118
+ write_worlds!(copy, "hecks_fuzz_postgres.world") do |name|
119
+ <<~WORLD
120
+ Hecks.world "#{name}" do
121
+ default_adapter "Postgres"
122
+ persisted_by("Postgres") do
123
+ database "#{database}"
124
+ schema "#{schema}"
125
+ end
126
+ end
127
+ WORLD
128
+ end
129
+ write_unbound_chapter_worlds!(copy)
130
+ end
131
+
132
+ # Creates `database` once per process (memoized) and drops/recreates `schema`
133
+ # inside it on every call — that's what isolates one ephemeral boot from the next.
134
+ def ensure_fuzz_schema!(database = FUZZ_POSTGRES_DATABASE, schema = FUZZ_POSTGRES_SCHEMA)
135
+ # PG::Connection only closes its socket when GC finalizes it, and a tight fuzz
136
+ # loop opens connections faster than GC reclaims them — without this, a run
137
+ # exhausts Postgres's max_connections after a few dozen ephemeral boots.
138
+ GC.start
139
+
140
+ @fuzz_databases_ready ||= {}
141
+ unless @fuzz_databases_ready[database]
142
+ admin = PG.connect(dbname: "postgres")
143
+ exists = admin.exec_params(
144
+ "SELECT 1 FROM pg_database WHERE datname = $1", [database]
145
+ ).ntuples.positive?
146
+ admin.exec(%(CREATE DATABASE "#{database}")) unless exists
147
+ admin.close
148
+ @fuzz_databases_ready[database] = true
149
+ end
150
+
151
+ db = PG.connect(dbname: database)
152
+ # Quiet on purpose: an ordinary DROP CASCADE NOTICEs per dropped object, which
153
+ # would bury hecks fuzz's own output on every boot after the first.
154
+ db.exec("SET client_min_messages = warning")
155
+ quoted = db.quote_ident(schema)
156
+ db.exec("DROP SCHEMA IF EXISTS #{quoted} CASCADE")
157
+ db.exec("CREATE SCHEMA #{quoted}")
158
+ db.close
159
+ end
160
+
161
+ # `:postgres_era` is the only mode that exercises era/lineage-bound SQL; plain
162
+ # `:postgres` never touches that machinery. Unlike `:postgres`, there's no shared
163
+ # scratch constant here — `database:`/`schema:` are required keyword args because
164
+ # the caller (`hecks quality_control ask run --persistence-parity`) owns that database's
165
+ # lifecycle.
166
+ def rebind_to_postgres_era!(copy, database:, schema:)
167
+ require "pg"
168
+ if database.to_s.empty? || schema.to_s.empty?
169
+ raise ArgumentError,
170
+ "adapter: :postgres_era requires both database: and schema: — a throwaway database/schema " \
171
+ "THIS CALLER creates and drops itself (see rebind_to_postgres_era!'s own header). " \
172
+ "There is no shared default, unlike :postgres, so the caller cannot forget to own the lifecycle."
173
+ end
174
+
175
+ rewrite_bindings!(copy, "PostgresEra")
176
+ ensure_postgres_era_schema!(database: database, schema: schema)
177
+
178
+ # `schema:` isolates one ephemeral boot from the next, the same job
179
+ # `FUZZ_POSTGRES_SCHEMA` does for `:postgres` (`connect_for` idempotently
180
+ # creates it; this method only needs to drop it first, in
181
+ # `ensure_postgres_era_schema!`).
182
+ #
183
+ # `allow_superuser true` is deliberate: PostgresEra normally refuses to boot as
184
+ # a superuser (its era write-fence is row-level security, which a superuser
185
+ # bypasses), to protect a real ledger from stale writes. This is a throwaway
186
+ # schema the caller creates and drops, comparing Memory against PostgresEra's
187
+ # SQL — the era fence isn't under test, so opting in here is safe.
188
+ write_worlds!(copy, "hecks_fuzz_postgres_era.world") do |name|
189
+ <<~WORLD
190
+ Hecks.world "#{name}" do
191
+ default_adapter "PostgresEra"
192
+ persisted_by("PostgresEra") do
193
+ database "#{database}"
194
+ schema "#{schema}"
195
+ allow_superuser true
196
+ end
197
+ end
198
+ WORLD
199
+ end
200
+ write_unbound_chapter_worlds!(copy)
201
+ end
202
+
203
+ # Replaces every `.world` in the copy with one that declares only `default_adapter`,
204
+ # so an aggregate left unbound by its hecksagon still falls back to `adapter_name`.
205
+ def write_default_worlds!(copy, adapter_name)
206
+ write_worlds!(copy, "hecks_fuzz_default.world") do |name|
207
+ <<~WORLD
208
+ Hecks.world "#{name}" do
209
+ default_adapter "#{adapter_name}"
210
+ end
211
+ WORLD
212
+ end
213
+ end
214
+
215
+ # Gives every chapter with no hecksagon block (so no bind) a world of its own that
216
+ # falls back to Memory, since the worlds `write_worlds!` wrote require a database
217
+ # a chapter with no bind can't provide.
218
+ def write_unbound_chapter_worlds!(copy)
219
+ named = Dir.glob(File.join(copy, "**", "*.hecksagon")).flat_map do |path|
220
+ File.read(path).scan(/Hecks\.hecksagon\s+"([^"]+)"/).flatten
221
+ end
222
+ bluebooks = Dir.glob(File.join(copy, "**", "*.bluebook")).group_by { |path| File.dirname(path) }
223
+ bluebooks.each do |dir, files|
224
+ unbound = files.flat_map { |path| File.read(path).scan(/Hecks\.bluebook[\s(]+"([^"]+)"/).flatten }
225
+ .uniq - named
226
+ named.concat(unbound)
227
+ next if unbound.empty?
228
+
229
+ worlds = unbound.map { |name| %(Hecks.world "#{name}" do\n default_adapter "Memory"\nend\n) }
230
+ File.write(File.join(dir, "hecks_fuzz_unbound.world"), worlds.join("\n"))
231
+ end
232
+ end
233
+
234
+ # Writes one `.world` per directory holding a `.hecksagon`, naming every
235
+ # `Hecks.hecksagon` block found there, and deletes every other `.world` in the
236
+ # copy. One write per directory, not per file — a domain can split hecksagon
237
+ # blocks (e.g. `context_map.hecksagon` beside the main one) across sibling files
238
+ # in the same directory, and a second `File.write` to the same path would drop
239
+ # the first file's names.
240
+ def write_worlds!(copy, world_file, &world_for)
241
+ names_by_dir = Hash.new { |hash, dir| hash[dir] = [] }
242
+ Dir.glob(File.join(copy, "**", "*.hecksagon")).each do |hecksagon_path|
243
+ names = File.read(hecksagon_path).scan(/Hecks\.hecksagon\s+"([^"]+)"/).flatten
244
+ names_by_dir[File.dirname(hecksagon_path)].concat(names)
245
+ end
246
+ names_by_dir.each do |dir, names|
247
+ next if names.empty?
248
+
249
+ File.write(File.join(dir, world_file), names.uniq.map(&world_for).join("\n"))
250
+ end
251
+
252
+ Dir.glob(File.join(copy, "**", "*.world")).each do |path|
253
+ File.delete(path) unless File.basename(path) == world_file
254
+ end
255
+ end
256
+
257
+ # Mirrors `ensure_fuzz_schema!`'s GC.start-before-connecting fix for the same
258
+ # max_connections exhaustion. Creates `database` if missing, but never drops
259
+ # it — that's the caller's job (see `rebind_to_postgres_era!`).
260
+ def ensure_postgres_era_schema!(database:, schema:)
261
+ GC.start
262
+
263
+ admin = PG.connect(dbname: "postgres")
264
+ exists = admin.exec_params(
265
+ "SELECT 1 FROM pg_database WHERE datname = $1", [database]
266
+ ).ntuples.positive?
267
+ admin.exec(%(CREATE DATABASE "#{database}")) unless exists
268
+ admin.close
269
+
270
+ db = PG.connect(dbname: database)
271
+ db.exec("SET client_min_messages = warning")
272
+ quoted = db.quote_ident(schema)
273
+ db.exec("DROP SCHEMA IF EXISTS #{quoted} CASCADE")
274
+ db.close
275
+ end
276
+
277
+ # Deletes every `translations/*.bluebook` in the copy. A compute/rekey translation
278
+ # edge refuses to boot under any non-lineage-capable adapter (only `PostgresEra`
279
+ # answers `lineage_capable? == true`), so every other mode strips it here rather
280
+ # than tripping that refusal. Safe to drop: an ephemeral, zero-history boot never
281
+ # has a pre-existing era-1 row to translate — the edge only matters at mint time,
282
+ # against a real database, never here.
283
+ def strip_translations!(copy)
284
+ Dir.glob(File.join(copy, "**", "translations", "*.bluebook")).each { |path| File.delete(path) }
285
+ end
286
+
287
+ # Rewrites every `persisted_by` bind — aggregate-scoped or bare at the hecksagon's
288
+ # own root, naming its adapter as a literal or through a local variable — to name
289
+ # `adapter_name`, and drops every `projected_by` bind outright (unread rather than
290
+ # broken, since `Registry#read_repository` falls back to the authoritative repo).
291
+ def rewrite_bindings!(copy, adapter_name)
292
+ Dir.glob(File.join(copy, "**", "*.hecksagon")).each do |path|
293
+ lines = File.readlines(path).grep_v(/\bprojected_by\s*\(?\s*"/)
294
+ # Horizontal space only: a newline ends the statement, and the next one must stay.
295
+ bind = /persisted_by[ \t]*\(?[ \t]*(?:"[^"]+"|[a-z_]\w*)[ \t]*\)?/
296
+ File.write(path, lines.join.gsub(bind, "persisted_by(\"#{adapter_name}\")"))
297
+ end
298
+ end
299
+ end
300
+ end
301
+ end