hecks 2.9.0 → 3.0.1

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 +13 -4
  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 +15 -8
  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 +15 -5
  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,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../tools"
4
+ require_relative "../corpus"
5
+
6
+ module Hecks
7
+ module Tools
8
+ # Prints the Rust-facing corpus from `Hecks::Corpus`, for CI steps and for asking what CI walks.
9
+ # Takes one flag (`hecks corpus_rust_coverage <flag>`):
10
+ #
11
+ # --rust-domains feature and path of every in-repo domain with a Cargo feature
12
+ # --rust-regen-order paths `hecks regenerate_corpus` regenerates, in order
13
+ # --rust-coverage run the coverage tool over every generated module; a
14
+ # Corpus::RUST_COVERAGE_PENDING module must still fail,
15
+ # every other one must pass
16
+ module CorpusReport
17
+ USAGE = "usage: hecks corpus_rust_coverage --rust-domains | --rust-regen-order | --rust-coverage"
18
+
19
+ module_function
20
+
21
+ # Prints the report the first argument asks for.
22
+ #
23
+ # @param argv [Array<String>] one of the three flags
24
+ # @param root [String] the checkout
25
+ # @return [Integer] 0, or 1 when a coverage check does not come out as it must
26
+ # @raise [SystemExit] with the usage line when the flag is unknown
27
+ def main(argv, root: Tools::ROOT)
28
+ case argv.first
29
+ when "--rust-domains"
30
+ Hecks::Corpus.rust_domains.each { |domain| puts "#{domain.feature}\t#{relative(domain.dir, root)}" }
31
+ 0
32
+ when "--rust-regen-order"
33
+ Hecks::Corpus.rust_regen_order.each { |domain| puts relative(domain.dir, root) }
34
+ 0
35
+ when "--rust-coverage" then rust_coverage
36
+ else abort USAGE
37
+ end
38
+ end
39
+
40
+ # @param path [String] an absolute path
41
+ # @param root [String] the checkout
42
+ # @return [String] the path relative to `root`
43
+ def relative(path, root) = path.delete_prefix("#{root}/")
44
+
45
+ # Runs the coverage tool for each module in this process.
46
+ #
47
+ # @param modules [Array<String>] the generated modules
48
+ # @return [Hash{String => Array}] each module's `[passed, output]`
49
+ def coverage_results(modules)
50
+ require_relative "../rust_build"
51
+ modules.to_h do |name|
52
+ result = Hecks::RustBuild.capture("rust_coverage", [name])
53
+ [name, [result.ok?, result.out + result.err]]
54
+ end
55
+ end
56
+
57
+ # @return [Integer] 0 when every module is as it must be, 1 otherwise
58
+ # @raise [SystemExit] when `RUST_COVERAGE_PENDING` names a module that does not exist
59
+ def rust_coverage
60
+ modules = Hecks::Corpus.generated_modules
61
+ pending = Hecks::Corpus::RUST_COVERAGE_PENDING
62
+ unknown = pending.keys - modules
63
+ if unknown.any?
64
+ abort "hecks corpus_rust_coverage: RUST_COVERAGE_PENDING names #{unknown.join(', ')}, " \
65
+ "which has no generated module"
66
+ end
67
+
68
+ results = coverage_results(modules)
69
+ problems = modules.filter_map do |name|
70
+ passed, output = results.fetch(name)
71
+ if pending.key?(name)
72
+ puts "#{name}: pending (#{passed ? 'NOW PASSES' : 'still fails'}) — #{pending[name]}"
73
+ "#{name} passes now — delete it from Hecks::Corpus::RUST_COVERAGE_PENDING" if passed
74
+ else
75
+ puts "#{name}: #{passed ? 'ok' : 'FAILED'}"
76
+ "#{name} failed:\n#{output}" unless passed
77
+ end
78
+ end
79
+
80
+ return puts("hecks corpus_rust_coverage: #{modules.size} generated modules checked").then { 0 } if problems.empty?
81
+
82
+ warn problems.join("\n\n")
83
+ 1
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hecks"
4
+ require "fileutils"
5
+ require "optparse"
6
+ require_relative "../tools"
7
+
8
+ module Hecks
9
+ module Tools
10
+ # The AWS deployment generator (ADR 0018): resolves a domain's declared `deployed_to(...)`
11
+ # target and dispatches to the matching `Projector` export, writing the recipe (template,
12
+ # scripts and Makefile) under `deploy/<stack>/` of the checkout.
13
+ #
14
+ # hecks deploy project <domain> [--tenant=<slug>] [--schema=<name>]
15
+ # [--out=<dir>] [--environment=<name>]
16
+ #
17
+ # `--environment` layers an overlay `.world` file over the base one; a missing overlay is an
18
+ # error. `--out` writes the recipe elsewhere, for a domain in a client's repo. `--tenant` and
19
+ # `--schema` override the stack for shared-database hosting: `stack_name` is appended to, so
20
+ # re-running with the same `--tenant` regenerates the same stack.
21
+ module DeployRecipe
22
+ USAGE = "usage: hecks deploy project <domain> [--tenant=<slug>] [--schema=<name>] " \
23
+ "[--out=<dir>] [--environment=<name>]"
24
+
25
+ # What the flags asked for.
26
+ Options = Struct.new(:tenant, :out, :environment)
27
+
28
+ module_function
29
+
30
+ # Generates the recipe and prints each file written.
31
+ #
32
+ # @param argv [Array<String>] the domain directory, then the flags
33
+ # @param root [String] the checkout whose `deploy/` receives the recipe unless `--out` says
34
+ # otherwise
35
+ # @return [Integer] 0
36
+ # @raise [SystemExit] with the reason on stderr when the domain or its `.world` cannot
37
+ # generate
38
+ def main(argv, root: Tools::ROOT)
39
+ argv = argv.dup
40
+ options = parse(argv)
41
+ domain = argv.shift or abort USAGE
42
+
43
+ world_file = world_file_for(domain)
44
+ bluebook_basename = File.basename(world_file, ".world")
45
+ world = load_world(world_file, domain, options.environment)
46
+ deploy_settings = tenant_settings(world.for_verb("deployed_to"), options, File.basename(domain))
47
+ infra_name = deploy_settings[:stack_name] || File.basename(domain)
48
+
49
+ registry = cross_domain_registry(domain, bluebook_basename)
50
+ chapter = registry.bluebook(world.domain) or
51
+ abort "#{world_file} declares no #{world.domain} chapter — #{bluebook_basename}.bluebook's own " \
52
+ "Hecks.bluebook name has to match world.domain."
53
+
54
+ out_dir = options.out ? File.expand_path(options.out) : File.join(root, "deploy", infra_name)
55
+ artifact = project(target_key(deploy_settings, world_file, domain), chapter, world, registry,
56
+ deploy_settings: deploy_settings, infra_name: infra_name, options: options,
57
+ domain: domain, root: root, world_file: world_file)
58
+ write(artifact, out_dir)
59
+ 0
60
+ end
61
+
62
+ # @param argv [Array<String>] the arguments; the flags are removed from it
63
+ # @return [Options] the flags, with `tenant` a hash of `tenant` and `schema`
64
+ # @raise [SystemExit] when `--schema` has no `--tenant`
65
+ def parse(argv)
66
+ options = Options.new({}, nil, nil)
67
+ OptionParser.new do |parser|
68
+ parser.on("--tenant=SLUG") { |v| options.tenant[:tenant] = v }
69
+ parser.on("--schema=NAME") { |v| options.tenant[:schema] = v }
70
+ parser.on("--out=DIR") { |v| options.out = v }
71
+ parser.on("--environment=NAME") { |v| options.environment = v }
72
+ end.parse!(argv)
73
+
74
+ if options.tenant[:schema] && !options.tenant[:tenant]
75
+ abort "--schema needs --tenant=<slug> — a schema override with no tenant to scope it to is meaningless"
76
+ end
77
+ options
78
+ end
79
+
80
+ # The `.world` file named after the directory; failing that, the sole `*.world` file under
81
+ # `<domain>/bluebook/`; failing that, the one named after the chapter the domain's hecksagon
82
+ # attaches by name (`qa/` attaches QualityControl and also holds a Governance `.world`).
83
+ # It picks by no other rule: guessing which is the domain is not this generator's call.
84
+ #
85
+ # @param domain [String] the domain directory
86
+ # @return [String] the `.world` file
87
+ # @raise [SystemExit] when there is none
88
+ def world_file_for(domain)
89
+ world_file = File.join(domain, "bluebook", "#{File.basename(domain)}.world")
90
+ unless File.exist?(world_file)
91
+ candidates = Dir.glob(File.join(domain, "bluebook", "*.world"))
92
+ named = candidates.select { |path| attached_stems(domain).include?(File.basename(path, ".world")) }
93
+ pick = candidates.size == 1 ? candidates : named
94
+ world_file = pick.first if pick.size == 1
95
+ end
96
+
97
+ File.exist?(world_file) or
98
+ abort "#{world_file} does not exist — a domain needs a .world file to declare " \
99
+ "deployed_to(\"AwsLambda\") or deployed_to(\"AwsFargate\")"
100
+ world_file
101
+ end
102
+
103
+ # The snake-cased names of the chapters a domain's hecksagons attach by name
104
+ # (`Hecks::Chapters.load!("QualityControl")`), which its `.world` and `.hecksagon` files
105
+ # are named after.
106
+ #
107
+ # @param domain [String] the domain directory
108
+ # @return [Array<String>] such as `["quality_control"]`
109
+ def attached_stems(domain)
110
+ Dir.glob(File.join(domain, "bluebook", "*.hecksagon"))
111
+ .flat_map { |path| File.read(path).scan(/Chapters\.load!\(\s*"([^"]+)"\s*\)/).flatten }
112
+ .map { |name| Hecks::Naming.snake(name) }.uniq
113
+ end
114
+
115
+ # @param world_file [String] the base `.world` file
116
+ # @param domain [String] the domain directory
117
+ # @param environment [String, nil] the overlay to layer over the base
118
+ # @return [Object] the world's own declaration, named as the bluebook is
119
+ # @raise [SystemExit] when an overlay is missing or the file declares no world
120
+ def load_world(world_file, domain, environment)
121
+ registry = Hecks::Runtime::Registry.new
122
+ Hecks.with_registry(registry) do
123
+ Kernel.load(world_file)
124
+ if environment
125
+ overlay_file = File.join(domain, "bluebook", "environments", "#{environment}.world")
126
+ File.exist?(overlay_file) or
127
+ abort "#{overlay_file} does not exist — --environment=#{environment} needs that overlay"
128
+ Kernel.load(overlay_file)
129
+ end
130
+ end
131
+
132
+ registry.worlds.values.first or abort "#{world_file} declares no Hecks.world block at all"
133
+ end
134
+
135
+ # Applied before the stack's name is computed, so the output directory agrees with the stack
136
+ # name `Lambda`/`Fargate` generate: they re-apply this same override against the `world` and
137
+ # `tenant:` they are handed.
138
+ #
139
+ # @param deploy_settings [Hash] the world's `deployed_to` settings
140
+ # @param options [Options] the flags
141
+ # @param domain_name [String] the domain directory's name
142
+ # @return [Hash] the settings, with the tenant appended to `stack_name` when there is one
143
+ def tenant_settings(deploy_settings, options, domain_name)
144
+ return deploy_settings unless options.tenant[:tenant]
145
+
146
+ base = deploy_settings[:stack_name] || domain_name
147
+ deploy_settings.merge(stack_name: "#{base}-#{options.tenant[:tenant]}")
148
+ end
149
+
150
+ # Loads the whole registry a domain boots with, not just its own `.bluebook`, because a
151
+ # cross-domain invoke grant can live in any attached chapter. A domain with no bluebook of
152
+ # its own (the QA ledger) gets its chapter from the hecksagon's `Chapters.load!`. `root:` is
153
+ # required or `uses_embryonaut_bluebook` refuses with "needs a registry with a root to
154
+ # vendor from".
155
+ #
156
+ # @param domain [String] the domain directory
157
+ # @param bluebook_basename [String] the file name the domain's bluebook and hecksagon share
158
+ # @return [Hecks::Runtime::Registry] the registry
159
+ def cross_domain_registry(domain, bluebook_basename)
160
+ registry = Hecks::Runtime::Registry.new(root: File.expand_path(domain))
161
+ lib_hecks = File.expand_path("..", __dir__)
162
+ Hecks.with_registry(registry) do
163
+ %w[ports/persistence.port ports/extraction.port adapters/driven/memory.adapter
164
+ adapters/driven/prism.adapter].each { |file| Kernel.load(File.join(lib_hecks, file)) }
165
+ bluebook_file = File.join(domain, "bluebook", "#{bluebook_basename}.bluebook")
166
+ Kernel.load(bluebook_file) if File.exist?(bluebook_file)
167
+ hecksagon_file = File.join(domain, "bluebook", "#{bluebook_basename}.hecksagon")
168
+ Kernel.load(hecksagon_file) if File.exist?(hecksagon_file)
169
+ end
170
+ registry
171
+ end
172
+
173
+ # Each adapter maps to a registered `Projector` export (`needs_world: true`); this only finds
174
+ # which one to call.
175
+ #
176
+ # @param deploy_settings [Hash] the world's `deployed_to` settings
177
+ # @param world_file [String] the `.world` file, named in the refusal
178
+ # @param domain [String] the domain directory, named in the refusal
179
+ # @return [Symbol] `:aws_lambda` or `:aws_fargate`
180
+ # @raise [SystemExit] with an example block when the world declares neither
181
+ def target_key(deploy_settings, world_file, domain)
182
+ case deploy_settings[:adapter]
183
+ when "AwsLambda" then :aws_lambda
184
+ when "AwsFargate" then :aws_fargate
185
+ else
186
+ abort <<~MSG
187
+ #{world_file} declares no deployed_to("AwsLambda") or deployed_to("AwsFargate") block. Add one, e.g.:
188
+
189
+ deployed_to("AwsLambda") do
190
+ region "us-east-1"
191
+ memory 512
192
+ timeout 10
193
+ end
194
+
195
+ or:
196
+
197
+ deployed_to("AwsFargate") do
198
+ region "us-east-1"
199
+ cpu 256
200
+ memory 512
201
+ port 8080
202
+ end
203
+
204
+ then re-run hecks deploy project #{domain}.
205
+ MSG
206
+ end
207
+ end
208
+
209
+ # `Lambda` and `Fargate` raise `ArgumentError` for a domain's own deploy conflicts; it is
210
+ # caught here so the message reaches stderr with a failing exit code instead of a backtrace.
211
+ #
212
+ # @param target [Symbol] the projector export
213
+ # @param chapter [Object] the domain's chapter
214
+ # @param world [Object] the domain's world
215
+ # @param registry [Hecks::Runtime::Registry] the registry the chapter came from
216
+ # @param context [Hash] `deploy_settings`, `infra_name`, `options`, `domain`, `root` and
217
+ # `world_file`
218
+ # @return [Hash{String => String}] the files to write
219
+ # @raise [SystemExit] when the domain's own settings conflict
220
+ def project(target, chapter, world, registry, **context)
221
+ artifact = Hecks::Projector.call(
222
+ target,
223
+ bluebook: chapter,
224
+ options: {
225
+ tenant: context[:options].tenant,
226
+ domain_dir: context[:domain],
227
+ root: context[:root],
228
+ world_file: context[:world_file],
229
+ cross_domain_registry: registry
230
+ },
231
+ world: world
232
+ )
233
+ # Only when the domain declared `smoke true`; otherwise nothing is added.
234
+ artifact.merge(Hecks::Projections::Deploy::Smoke.files(context[:deploy_settings],
235
+ stack_name: context[:infra_name]))
236
+ rescue ArgumentError => e
237
+ abort e.message
238
+ end
239
+
240
+ # Writes the files, and prints where the recipe deploys from.
241
+ #
242
+ # @param artifact [Hash{String => String}] the files to write
243
+ # @param out_dir [String] where they go
244
+ # @return [void]
245
+ def write(artifact, out_dir)
246
+ written = Hecks::Projector.write(artifact, out_dir, as: :files)
247
+
248
+ written.each { |path| puts "wrote #{path}" }
249
+ puts
250
+ puts "deploy from #{out_dir} — one command, nothing to type, nothing to remember:"
251
+ puts " make deploy"
252
+ end
253
+ end
254
+ end
255
+ end
@@ -0,0 +1,282 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tmpdir"
4
+ require "fileutils"
5
+ require "hecks"
6
+ require_relative "../tools"
7
+ require_relative "deploy_recipe"
8
+ require_relative "../hecks/adapters/console_capture"
9
+
10
+ module Hecks
11
+ module Tools
12
+ # Lints generated deploy Makefiles for prod-touching recipes that hide failure.
13
+ module DeployRecipeLint
14
+ # Commands that touch AWS or a real Postgres; `sam build` is excluded because it stays local.
15
+ RISKY_REGEX = /\baws\s+(?:cloudformation|ssm|secretsmanager|lambda|ec2)\b|\bpsql\b|DATABASE_URL=|\bsam\s+deploy\b/
16
+
17
+ # Deliberately loose: any real statement with "echo" counts, since only a missing step
18
+ # matters.
19
+ ECHO_REGEX = /\becho\b/
20
+
21
+ # An unconditional `exit 0` statement; `command && exit 0` is one statement and never matches.
22
+ BARE_EXIT_ZERO_REGEX = /\Aexit\s+0\s*\z/
23
+
24
+ # `exit $?` is only sound right after the command it reports on; an `@echo` in between
25
+ # resets it.
26
+ DOLLAR_QUESTION_EXIT_REGEX = /\Aexit\s+\$\$?\?\s*\z/
27
+
28
+ # Print-only or no-op statements, never the command a nearby `exit $?` reports on.
29
+ BENIGN_REGEX = /\A(?:@)?(?:echo\b|:\s*\z)/
30
+
31
+ Violation = Struct.new(:source, :target, :line, :rule, :message, keyword_init: true) do
32
+ def to_s
33
+ "#{source}:#{line}: [#{rule}] target #{target.inspect} — #{message}"
34
+ end
35
+ end
36
+
37
+ module_function
38
+
39
+ # Maps each target name to its `[line_no, raw_text]` recipe lines, tabs and continuations
40
+ # intact.
41
+ def parse_targets(text)
42
+ lines = text.lines
43
+ targets = {}
44
+ i = 0
45
+ while i < lines.length
46
+ line = lines[i]
47
+ if line.start_with?("\t", "#") || line.strip.empty? || line.include?(":=") || line.start_with?(".PHONY")
48
+ i += 1
49
+ next
50
+ end
51
+
52
+ if (m = line.match(%r{\A([A-Za-z0-9_./$(){}-]+):(?:\s.*)?\z}))
53
+ recipe = []
54
+ j = i + 1
55
+ while j < lines.length && (lines[j] == "\n" || lines[j].start_with?("\t") || lines[j].start_with?("#"))
56
+ recipe << [j, lines[j].chomp]
57
+ j += 1
58
+ end
59
+ targets[m[1]] = recipe
60
+ i = j
61
+ else
62
+ i += 1
63
+ end
64
+ end
65
+ targets
66
+ end
67
+
68
+ # Drops comment and blank lines; a comment can quote "sam deploy" or "echo" but never runs.
69
+ def real_statements(recipe_lines)
70
+ recipe_lines.reject { |_, text| text.sub(/\A\t/, "").start_with?("#") || text.strip.empty? }
71
+ end
72
+
73
+ # Splits real statements into shell chains; Make runs each backslash-terminated run as one
74
+ # shell.
75
+ def shell_chains(recipe_lines)
76
+ chains = []
77
+ current = []
78
+ real_statements(recipe_lines).each do |line_no, text|
79
+ stripped = text.sub(/\A\t/, "")
80
+ continues = stripped.end_with?("\\")
81
+ statement = continues ? stripped.sub(/\\\z/, "").rstrip : stripped
82
+ current << [line_no, statement]
83
+ unless continues
84
+ chains << current
85
+ current = []
86
+ end
87
+ end
88
+ chains << current unless current.empty?
89
+ chains
90
+ end
91
+
92
+ # Drops the leading Make `@` and trailing `;` so exit-code checks match the bare statement.
93
+ def normalize_statement(text)
94
+ text.sub(/\A@/, "").strip.sub(/;\s*\z/, "")
95
+ end
96
+
97
+ # Flags a chain that runs an AWS/DB command and later hits an unconditional `exit 0`.
98
+ def check_blind_exit_zero(target, chains)
99
+ chains.filter_map do |chain|
100
+ exit_zero = chain.find { |_, stmt| BARE_EXIT_ZERO_REGEX.match?(normalize_statement(stmt)) }
101
+ next unless exit_zero
102
+
103
+ risky_before = chain.take_while { |line_no, _| line_no != exit_zero.first }
104
+ .select { |_, stmt| RISKY_REGEX.match?(stmt) }
105
+ next if risky_before.empty?
106
+
107
+ Violation.new(
108
+ target: target, line: exit_zero.first + 1, rule: "UNVERIFIED_EXIT_ZERO",
109
+ message: "unconditional `exit 0` follows an AWS/DB-touching command (line #{risky_before.last.first + 1}: " \
110
+ "#{risky_before.last.last.strip.inspect}) whose own exit status this chain never checks — a real " \
111
+ "failure there would still report success."
112
+ )
113
+ end
114
+ end
115
+
116
+ # Flags `exit $?` whose preceding statement in the chain is a benign echo/no-op.
117
+ def check_stale_dollar_question(target, chains)
118
+ chains.filter_map do |chain|
119
+ idx = chain.index { |_, stmt| DOLLAR_QUESTION_EXIT_REGEX.match?(normalize_statement(stmt)) }
120
+ next unless idx
121
+
122
+ prev = idx.positive? ? chain[idx - 1] : nil
123
+ next unless prev
124
+ next unless BENIGN_REGEX.match?(normalize_statement(prev.last))
125
+
126
+ Violation.new(
127
+ target: target, line: chain[idx].first + 1, rule: "STALE_DOLLAR_QUESTION",
128
+ message: "`exit $?` follows a benign statement (line #{prev.first + 1}: #{prev.last.strip.inspect}), " \
129
+ "not the meaningful command it claims to report on — capture the real command's status into a " \
130
+ "named variable instead (this codebase's own convention, e.g. `BOOT_STATUS=$$?` ... " \
131
+ "`exit $$BOOT_STATUS`) and exit that, not a bare $?."
132
+ )
133
+ end
134
+ end
135
+
136
+ # Flags a target whose first AWS/DB command has no earlier echo step.
137
+ def check_prod_touch_without_echo(target, recipe_lines)
138
+ ordered = real_statements(recipe_lines)
139
+ first_risky = ordered.find { |_, text| RISKY_REGEX.match?(text) }
140
+ return [] unless first_risky
141
+
142
+ seen_echo = ordered.take_while { |line_no, _| line_no != first_risky.first }
143
+ .any? { |_, text| ECHO_REGEX.match?(text) }
144
+ return [] if seen_echo
145
+
146
+ [Violation.new(
147
+ target: target, line: first_risky.first + 1, rule: "PROD_TOUCH_WITHOUT_ECHO",
148
+ message: "touches AWS/DB (#{first_risky.last.strip.inspect}) with no earlier echo/validation step in this " \
149
+ "recipe naming what it's about to do to a human running it interactively."
150
+ )]
151
+ end
152
+
153
+ # Runs all three checks over every target; `source` labels each violation.
154
+ def lint(text, source:)
155
+ targets = parse_targets(text)
156
+ violations = targets.flat_map do |name, recipe_lines|
157
+ chains = shell_chains(recipe_lines)
158
+ check_blind_exit_zero(name, chains) +
159
+ check_stale_dollar_question(name, chains) +
160
+ check_prod_touch_without_echo(name, recipe_lines)
161
+ end
162
+ violations.each { |v| v.source = source }
163
+ violations
164
+ end
165
+
166
+ # Writes a minimal bluebook/world pair under `dir` for the deploy generator to generate from.
167
+ def write_fixture(dir, basename, world_body, env_local: nil)
168
+ domain_dir = File.join(dir, basename)
169
+ bluebook_dir = File.join(domain_dir, "bluebook")
170
+ FileUtils.mkdir_p(bluebook_dir)
171
+ bluebook_name = basename.split("_").map(&:capitalize).join
172
+
173
+ File.write(File.join(bluebook_dir, "#{basename}.bluebook"), <<~BLUEBOOK)
174
+ Hecks.bluebook "#{bluebook_name}" do
175
+ aggregate "Thing" do
176
+ identified_by :name
177
+ attribute :name, ThingName
178
+ value_object "ThingName" do
179
+ attribute :value, String
180
+ invariant("named") { !value.to_s.empty? }
181
+ end
182
+ command "Create" do
183
+ attribute :name, ThingName
184
+ sets :name
185
+ emits "ThingCreated"
186
+ end
187
+ end
188
+ end
189
+ BLUEBOOK
190
+
191
+ File.write(File.join(bluebook_dir, "#{basename}.world"), <<~WORLD)
192
+ Hecks.world "#{bluebook_name}" do
193
+ deployed_to("AwsLambda") do
194
+ #{world_body}
195
+ end
196
+ end
197
+ WORLD
198
+
199
+ File.write(File.join(domain_dir, ".env.local"), env_local) if env_local
200
+
201
+ domain_dir
202
+ end
203
+
204
+ # Runs the deploy generator on a tmpdir fixture; output always lands in
205
+ # <root>/deploy/<basename>.
206
+ def generate!(root, basename, world_body, env_local: nil)
207
+ Dir.mktmpdir do |dir|
208
+ domain_dir = write_fixture(dir, basename, world_body, env_local: env_local)
209
+ outcome = Hecks::Adapters::ConsoleCapture.capture { DeployRecipe.main([domain_dir], root: root) }
210
+ outcome.ok? or raise "hecks deploy project failed generating the #{basename} fixture: #{outcome.output}"
211
+ end
212
+ File.join(root, "deploy", basename)
213
+ end
214
+
215
+ # Own-RDS, Shared-mode and OAuth domains: together they hit every branch of the recipe
216
+ # builders.
217
+ def fixtures
218
+ {
219
+ "own fixture" => ["region \"us-east-1\"", nil],
220
+ "shared fixture" => ["region \"us-east-1\"\n database \"Shared\"\n owner \"SomeOwner\"", nil],
221
+ "oauth fixture" => [
222
+ "region \"us-east-1\"\n web \"Rust\"",
223
+ "GOOGLE_CLIENT_ID=test-client-id.apps.googleusercontent.com\nGOOGLE_CLIENT_SECRET=test-secret\n"
224
+ ]
225
+ }
226
+ end
227
+
228
+ # Lints the Makefiles named in `argv`, or three generated fixture domains when there are none.
229
+ #
230
+ # @param argv [Array<String>] Makefile paths, or `-h`/`--help`
231
+ # @param root [String] the checkout the fixtures are generated into
232
+ # @return [Integer] 0 when clean, 1 when a violation was found
233
+ def main(argv, root: Tools::ROOT)
234
+ return usage if %w[-h --help].include?(argv.first)
235
+
236
+ violations = []
237
+ generated_dirs = []
238
+ begin
239
+ if argv.empty?
240
+ fixtures.each do |label, (world_body, env_local)|
241
+ basename = "lint_deploy_recipes_fixture_#{label.tr(' ', '_')}"
242
+ dir = generate!(root, basename, world_body, env_local: env_local)
243
+ generated_dirs << dir
244
+ makefile_path = File.join(dir, "Makefile")
245
+ violations.concat(lint(File.read(makefile_path), source: "#{label} (#{makefile_path})"))
246
+ end
247
+ else
248
+ argv.each do |path|
249
+ File.exist?(path) or abort "hecks deploy lint: no such file #{path}"
250
+ violations.concat(lint(File.read(path), source: path))
251
+ end
252
+ end
253
+ report(violations)
254
+ ensure
255
+ generated_dirs.each { |dir| FileUtils.rm_rf(dir) }
256
+ end
257
+ end
258
+
259
+ # @return [Integer] 0, after printing the usage
260
+ def usage
261
+ puts "usage: hecks deploy lint [Makefile ...]"
262
+ puts " no args: generates 3 representative fixture domains (own/shared/oauth)"
263
+ puts " via the real deploy generator, lints each one's Makefile"
264
+ puts " with args: lints the given Makefile(s) directly (e.g. deploy/<domain>/Makefile)"
265
+ 0
266
+ end
267
+
268
+ # @param violations [Array<Violation>] what the lint found
269
+ # @return [Integer] 0 when there are none, 1 after printing them to stderr
270
+ def report(violations)
271
+ if violations.empty?
272
+ puts "hecks deploy lint: no violations found."
273
+ return 0
274
+ end
275
+
276
+ warn "hecks deploy lint: #{violations.size} violation(s) found:\n\n"
277
+ violations.each { |v| warn " #{v}\n" }
278
+ 1
279
+ end
280
+ end
281
+ end
282
+ end