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,46 @@
1
+ //! Rust side of the expression differential: interprets each `{ast, instance}` case on stdin
2
+ //! through the same `expr_json` the mint-time invariant checker uses, and prints one result each.
3
+ //! `spec/rust_host_expr_json_conformance_spec.rb` diffs those against Ruby's own interpreters.
4
+
5
+ #[allow(dead_code)]
6
+ #[path = "../expr_json.rs"]
7
+ mod expr_json;
8
+
9
+ use expr_json::Value;
10
+ use serde_json::{json, Value as Json};
11
+ use std::io::Read;
12
+
13
+ // Usage: `expr_harness < {"cases": [{"ast": <ast_json>, "instance": <json>}, ...]}`.
14
+ // Prints `{"results": [{"ok": <value>} | {"error": <message>}, ...]}`, one per case, in order.
15
+ fn main() -> anyhow::Result<()> {
16
+ let mut input = String::new();
17
+ std::io::stdin().read_to_string(&mut input)?;
18
+ let request: Json = serde_json::from_str(&input)?;
19
+ let cases = request.get("cases").and_then(Json::as_array).ok_or_else(|| anyhow::anyhow!("no \"cases\" array on stdin"))?;
20
+
21
+ let results: Vec<Json> = cases
22
+ .iter()
23
+ .map(|case| {
24
+ let outcome = expr_json::parse(&case["ast"]).and_then(|expr| expr_json::interpret(&expr, &case["instance"]));
25
+ match outcome {
26
+ Ok(value) => json!({ "ok": to_json(&value) }),
27
+ Err(error) => json!({ "error": error }),
28
+ }
29
+ })
30
+ .collect();
31
+
32
+ println!("{}", json!({ "results": results }));
33
+ Ok(())
34
+ }
35
+
36
+ fn to_json(value: &Value) -> Json {
37
+ match value {
38
+ Value::Int(i) => json!(i),
39
+ Value::Float(f) => serde_json::Number::from_f64(*f).map_or(Json::Null, Json::Number),
40
+ Value::Str(s) => json!(s),
41
+ Value::Bool(b) => json!(b),
42
+ Value::Nil => Json::Null,
43
+ Value::Array(items) => Json::Array(items.iter().map(to_json).collect()),
44
+ Value::Object(object) => object.clone(),
45
+ }
46
+ }
@@ -0,0 +1,119 @@
1
+ //! Rust side of the differential-parity harness: generic lineage reads and writes on Postgres.
2
+ //! Usage: `lineage_harness <db_name> <app_role> <domain> <era>`, JSON on stdin and stdout.
3
+ //!
4
+ //! stdin is `{"operations": [...]}`, each one of:
5
+ //! {"op": "read_all", "storage_name": "order"}
6
+ //! {"op": "read_by_id", "storage_name": "order", "id": "order-1"}
7
+ //! {"op": "write", "aggregate": "Pizzas::Order", "id": "order-9", "state": {...}}
8
+ //! stdout is `{"results": [...]}`, one entry per operation in order. A per-operation Postgres
9
+ //! error (such as an RLS refusal) lands in that entry; only a failed connection exits nonzero.
10
+
11
+ // journal.rs has no `crate::` references, so it is shared with `bootstrap` by path, not a lib
12
+ // split.
13
+ // The flat-journal functions it also carries are unused here.
14
+ #[allow(dead_code)]
15
+ #[path = "../journal.rs"]
16
+ mod journal;
17
+
18
+ // journal.rs's Postgres tests connect through `crate::test_pg`, so the harness's own test build
19
+ // carries the same connection-string helpers.
20
+ #[cfg(test)]
21
+ #[path = "../test_pg.rs"]
22
+ mod test_pg;
23
+
24
+ use serde_json::{json, Value};
25
+ use std::io::Read;
26
+ use tokio_postgres::NoTls;
27
+
28
+ #[tokio::main]
29
+ async fn main() -> anyhow::Result<()> {
30
+ let args: Vec<String> = std::env::args().collect();
31
+ let [_, db_name, app_role, domain, era] = args.as_slice() else {
32
+ anyhow::bail!("usage: lineage_harness <db_name> <app_role> <domain> <era>");
33
+ };
34
+ let era: i32 = era.parse().map_err(|_| anyhow::anyhow!("era must be an integer, got {era:?}"))?;
35
+
36
+ let mut input = String::new();
37
+ std::io::stdin().read_to_string(&mut input)?;
38
+ let request: Value = serde_json::from_str(&input)?;
39
+ let operations = request
40
+ .get("operations")
41
+ .and_then(|v| v.as_array())
42
+ .ok_or_else(|| anyhow::anyhow!("stdin JSON missing \"operations\" array"))?;
43
+
44
+ // tokio_postgres ignores libpq's `PGPASSWORD`, so pass it through when the server asks for one.
45
+ let password = std::env::var("PGPASSWORD").ok().filter(|p| !p.is_empty()).map(|p| format!(" password={p}")).unwrap_or_default();
46
+ let conn_string = format!("host=localhost dbname={db_name} user={app_role}{password}");
47
+ let (client, connection) = tokio_postgres::connect(&conn_string, NoTls).await?;
48
+ tokio::spawn(async move {
49
+ if let Err(err) = connection.await {
50
+ eprintln!("postgres connection error: {err:#}");
51
+ }
52
+ });
53
+
54
+ let config = journal::LineageConfig { domain: domain.clone(), era: Some(era), mirrored: None };
55
+ let mut results = Vec::with_capacity(operations.len());
56
+
57
+ for operation in operations {
58
+ results.push(run_one(&client, &config, operation).await);
59
+ }
60
+
61
+ println!("{}", json!({ "results": results }));
62
+ Ok(())
63
+ }
64
+
65
+ async fn run_one(client: &tokio_postgres::Client, config: &journal::LineageConfig, operation: &Value) -> Value {
66
+ let op = operation.get("op").and_then(|v| v.as_str()).unwrap_or("");
67
+
68
+ let outcome = match op {
69
+ "read_all" => read_all(client, config, operation).await,
70
+ "read_by_id" => read_by_id(client, config, operation).await,
71
+ "write" => write(client, config, operation).await,
72
+ other => Err(anyhow::anyhow!("unknown op {other:?}")),
73
+ };
74
+
75
+ match outcome {
76
+ Ok(mut value) => {
77
+ value["op"] = json!(op);
78
+ value["ok"] = json!(true);
79
+ value
80
+ }
81
+ Err(err) => json!({ "op": op, "ok": false, "error": format!("{err:#}") }),
82
+ }
83
+ }
84
+
85
+ async fn read_all(client: &tokio_postgres::Client, config: &journal::LineageConfig, operation: &Value) -> anyhow::Result<Value> {
86
+ let storage_name = require_str(operation, "storage_name")?;
87
+ let rows = journal::read_lineage_head_all(client, &config.domain, storage_name).await?;
88
+ Ok(json!({ "rows": rows.into_iter().map(|(id, state)| json!([id, state])).collect::<Vec<_>>() }))
89
+ }
90
+
91
+ async fn read_by_id(client: &tokio_postgres::Client, config: &journal::LineageConfig, operation: &Value) -> anyhow::Result<Value> {
92
+ let storage_name = require_str(operation, "storage_name")?;
93
+ let id = require_str(operation, "id")?;
94
+ let state = journal::read_lineage_head_by_id(client, &config.domain, storage_name, id).await?;
95
+ Ok(json!({ "state": state }))
96
+ }
97
+
98
+ async fn write(client: &tokio_postgres::Client, config: &journal::LineageConfig, operation: &Value) -> anyhow::Result<Value> {
99
+ let aggregate = require_str(operation, "aggregate")?;
100
+ let id = require_str(operation, "id")?;
101
+ let state = operation
102
+ .get("state")
103
+ .ok_or_else(|| anyhow::anyhow!("write operation missing \"state\": {operation}"))?;
104
+
105
+ journal::append_lineage_mutation(
106
+ client,
107
+ config,
108
+ &journal::Mutation { aggregate, id, operation: "save", state },
109
+ )
110
+ .await?;
111
+ Ok(json!({}))
112
+ }
113
+
114
+ fn require_str<'a>(operation: &'a Value, field: &str) -> anyhow::Result<&'a str> {
115
+ operation
116
+ .get(field)
117
+ .and_then(|v| v.as_str())
118
+ .ok_or_else(|| anyhow::anyhow!("operation missing {field:?}: {operation}"))
119
+ }
@@ -0,0 +1,135 @@
1
+ //! Rust-mints-it-too side of the differential-parity harness: one boot decision per invocation.
2
+ //! Lets the spec mint an edge with no Ruby process so the two `_head` views can be diffed.
3
+
4
+ // Modules are shared with `bootstrap` by path, not a lib split, so `crate::` in mint.rs resolves
5
+ // here.
6
+ #[allow(dead_code)]
7
+ #[path = "../log.rs"]
8
+ mod log;
9
+ #[allow(dead_code)]
10
+ #[path = "../journal.rs"]
11
+ mod journal;
12
+ #[allow(dead_code)]
13
+ #[path = "../storage_shape.rs"]
14
+ mod storage_shape;
15
+ #[allow(dead_code)]
16
+ #[path = "../reference_transform.rs"]
17
+ mod reference_transform;
18
+ #[allow(dead_code)]
19
+ #[path = "../expr_json.rs"]
20
+ mod expr_json;
21
+ #[allow(dead_code)]
22
+ #[path = "../reference_validate.rs"]
23
+ mod reference_validate;
24
+ #[allow(dead_code)]
25
+ #[path = "../approval.rs"]
26
+ mod approval;
27
+ #[allow(dead_code)]
28
+ #[path = "../mint.rs"]
29
+ mod mint;
30
+
31
+ // The shared modules' Postgres tests connect through `crate::test_pg`, so the harness's own test
32
+ // build carries the same connection-string helpers.
33
+ #[cfg(test)]
34
+ #[path = "../test_pg.rs"]
35
+ mod test_pg;
36
+
37
+ use serde_json::{json, Value};
38
+ use std::collections::HashMap;
39
+ use tokio_postgres::NoTls;
40
+
41
+ // Usage: `mint_harness <db_name> <owner_role> <domain> <ir_json_path>`, as the schema owner.
42
+ // `ir_json_path` is read like `ir::ir()` reads `HECKS_IR_PATH`, so two runs can boot two shapes.
43
+ // Prints `{"era": <ordinal>, "label": <label>}`; a refusal exits nonzero with the error on stderr.
44
+ #[tokio::main]
45
+ async fn main() -> anyhow::Result<()> {
46
+ let args: Vec<String> = std::env::args().collect();
47
+ let [_, db_name, owner_role, domain, ir_path] = args.as_slice() else {
48
+ anyhow::bail!("usage: mint_harness <db_name> <owner_role> <domain> <ir_json_path>");
49
+ };
50
+
51
+ let ir_text = std::fs::read_to_string(ir_path).map_err(|e| anyhow::anyhow!("reading {ir_path}: {e}"))?;
52
+ let ir: Value = serde_json::from_str(&ir_text).map_err(|e| anyhow::anyhow!("parsing {ir_path}: {e}"))?;
53
+
54
+ // A server that wants a password over TCP gets it from PGPASSWORD, as libpq would read it.
55
+ // Built field by field, not spliced into a connection string, so a password with a space,
56
+ // quote or backslash reaches the server as typed.
57
+ let mut config = tokio_postgres::Config::new();
58
+ config.host("localhost").dbname(db_name).user(owner_role);
59
+ if let Ok(password) = std::env::var("PGPASSWORD") {
60
+ config.password(password);
61
+ }
62
+ let (client, connection) = config.connect(NoTls).await?;
63
+ tokio::spawn(async move {
64
+ if let Err(err) = connection.await {
65
+ eprintln!("postgres connection error: {err:#}");
66
+ }
67
+ });
68
+
69
+ journal::ensure_schema(&client).await?;
70
+ mint::ensure_base(&client, domain).await?;
71
+
72
+ let my_hash = storage_shape::mint_hash(&ir);
73
+ let my_label = storage_shape::mint_label(&ir);
74
+
75
+ // Every fixture is fully lineage-capable, so the `lineage_capable_aggregates` filter is
76
+ // skipped.
77
+ let aggregates: Vec<mint::Aggregate> = ir
78
+ .get("aggregates")
79
+ .and_then(Value::as_array)
80
+ .cloned()
81
+ .unwrap_or_default()
82
+ .iter()
83
+ .map(|agg| {
84
+ let name = agg.get("name").and_then(Value::as_str).unwrap_or("").to_string();
85
+ let storage_name = journal::snake(&name);
86
+ mint::Aggregate { name, storage_name }
87
+ })
88
+ .collect();
89
+
90
+ let held = journal::held_eras(&client, domain).await?;
91
+
92
+ let era = match mint::decide_boot_action(&held, &my_label) {
93
+ mint::BootDecision::UseExisting { ordinal } => ordinal,
94
+ mint::BootDecision::HoldFirst => {
95
+ let source_text = ir.get("source_text").and_then(Value::as_str).unwrap_or("mint_harness fixture source");
96
+ mint::hold_first(&client, domain, source_text, &ir, &aggregates, None).await?;
97
+ 1
98
+ }
99
+ mint::BootDecision::LatestUnnamed { ordinal } => {
100
+ anyhow::bail!("latest held era {ordinal} has no label -- mint_harness has no bluebook parser to name it with");
101
+ }
102
+ mint::BootDecision::Mint { ordinal, from_ordinal, from_label } => {
103
+ let mut labels: Vec<String> = held.iter().map(|h| h.label.clone().unwrap_or_default()).collect();
104
+ labels.push(my_label.clone());
105
+ let edges = mint::parse_edges(&ir, domain);
106
+ let chain = mint::edge_chain(&edges, &labels)
107
+ .map_err(|e| anyhow::anyhow!("drifted from era {from_ordinal} ({from_label}) to {my_label} with no covering edge -- {e}"))?;
108
+
109
+ // The same gate the host runs: a compute/rekey edge mints only on an approval, from the
110
+ // journal or committed in ir.json's `approvals`.
111
+ let raw_edges = ir.get("translations").and_then(Value::as_array).cloned().unwrap_or_default();
112
+ let committed_approvals = approval::committed(&ir);
113
+ for edge in &chain {
114
+ // An edge with no raw entry fails closed, as the host does, and never skips the gate.
115
+ let Some(raw_edge) = raw_edges.iter().find(|candidate| {
116
+ candidate.get("domain").and_then(Value::as_str) == Some(domain.as_str())
117
+ && candidate.get("from").and_then(Value::as_str) == Some(edge.from.as_str())
118
+ && candidate.get("to").and_then(Value::as_str) == Some(edge.to.as_str())
119
+ }) else {
120
+ anyhow::bail!("cannot boot: {domain}'s edge {} -> {} vanished between parsing and approval-checking it", edge.from, edge.to);
121
+ };
122
+ approval::check(&client, domain, raw_edge, ordinal, &committed_approvals).await?;
123
+ }
124
+ let watermarks: HashMap<i32, Option<i64>> = held.iter().map(|h| (h.ordinal, h.watermark)).collect();
125
+ mint::audit_before_mint(&client, domain, &ir, &aggregates, ordinal, &chain, &raw_edges, &watermarks).await?;
126
+
127
+ let held_text = ir.get("source_text").and_then(Value::as_str).unwrap_or("mint_harness fixture source");
128
+ mint::mint_era(&client, domain, ordinal, &my_hash, &my_label, held_text, &aggregates, &chain, None, &mint::lifecycle_defaults(&ir)).await?;
129
+ ordinal
130
+ }
131
+ };
132
+
133
+ println!("{}", json!({ "era": era, "label": my_label }));
134
+ Ok(())
135
+ }
@@ -0,0 +1,265 @@
1
+ //! Stripe checkout and webhook mechanics for the registration routes: HMAC
2
+ //! signature verification, an embedded Checkout Session, and a mock session.
3
+
4
+ use hmac::{Hmac, Mac};
5
+ use serde_json::Value;
6
+ use sha2::Sha256;
7
+
8
+ type HmacSha256 = Hmac<Sha256>;
9
+
10
+ // docs.stripe.com/webhooks#verify-events — 5 minutes, guards against a
11
+ // replayed old payload even against a leaked (not yet rotated) secret.
12
+ const TOLERANCE_SECONDS: i64 = 300;
13
+
14
+ pub struct SignatureError(pub String);
15
+
16
+ impl std::fmt::Display for SignatureError {
17
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18
+ write!(f, "{}", self.0)
19
+ }
20
+ }
21
+
22
+ // Signs `<timestamp>.<raw body>`, the exact bytes Stripe sent -- re-serializing
23
+ // the JSON would disagree on whitespace or key order and break every match.
24
+ // `now` is a parameter, not read internally, so a test can hold time fixed.
25
+ pub fn verify_signature(payload: &str, header: &str, secret: &str, now: i64) -> Result<(), SignatureError> {
26
+ let mut timestamp: Option<i64> = None;
27
+ let mut signatures: Vec<&str> = Vec::new();
28
+ for part in header.split(',') {
29
+ let Some((key, value)) = part.split_once('=') else { continue };
30
+ match key {
31
+ "t" => timestamp = value.parse().ok(),
32
+ "v1" => signatures.push(value),
33
+ _ => {}
34
+ }
35
+ }
36
+ let Some(timestamp) = timestamp else {
37
+ return Err(SignatureError("Stripe-Signature header has no t= timestamp".to_string()));
38
+ };
39
+ if signatures.is_empty() {
40
+ return Err(SignatureError("Stripe-Signature header has no v1= signature".to_string()));
41
+ }
42
+ if (now - timestamp).abs() > TOLERANCE_SECONDS {
43
+ return Err(SignatureError(format!(
44
+ "timestamp {timestamp} is outside the {TOLERANCE_SECONDS}s tolerance (now: {now})"
45
+ )));
46
+ }
47
+
48
+ let signed_payload = format!("{timestamp}.{payload}");
49
+ let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
50
+ .map_err(|e| SignatureError(format!("webhook secret unusable as an HMAC key: {e}")))?;
51
+ mac.update(signed_payload.as_bytes());
52
+ let expected_hex = hex_encode(&mac.finalize().into_bytes());
53
+
54
+ if signatures.iter().any(|sig| constant_time_eq(sig.as_bytes(), expected_hex.as_bytes())) {
55
+ Ok(())
56
+ } else {
57
+ Err(SignatureError("signature does not match — wrong secret, or the payload was tampered with in transit".to_string()))
58
+ }
59
+ }
60
+
61
+ fn hex_encode(bytes: &[u8]) -> String {
62
+ bytes.iter().map(|b| format!("{b:02x}")).collect()
63
+ }
64
+
65
+ // A bare `==` short-circuits at the first differing byte, opening a timing
66
+ // side-channel on the signature comparison, so this compares every byte.
67
+ fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
68
+ if a.len() != b.len() {
69
+ return false;
70
+ }
71
+ a.iter().zip(b.iter()).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
72
+ }
73
+
74
+ // Encodes success_url/cancel_url as query params, matching the Ruby local
75
+ // checkout adapter's own `URI.encode_www_form` output exactly.
76
+ pub fn mock_checkout_session(registration_id: &str, success_url: &str, cancel_url: &str, site_url: &str) -> String {
77
+ let mut url = reqwest::Url::parse(&format!("{site_url}/pay/{registration_id}.html"))
78
+ .unwrap_or_else(|_| reqwest::Url::parse("http://invalid.invalid/").unwrap());
79
+ url.query_pairs_mut().append_pair("success_url", success_url).append_pair("cancel_url", cancel_url);
80
+ url.to_string()
81
+ }
82
+
83
+ // Embedded, not Stripe-hosted, so Stripe returns a `client_secret` instead of
84
+ // a `url`. `ui_mode` must be `embedded_page`; Stripe rejects the older
85
+ // `embedded` value. No success_url/cancel_url: the browser stays on the site
86
+ // and learns of completion from Stripe.js, with the webhook settling payment.
87
+
88
+ // Pinned: `embedded_page` needs this version line, not whatever the account defaults to.
89
+ pub const STRIPE_API_VERSION: &str = "2026-04-22.dahlia";
90
+
91
+ pub struct StripeAuth<'a> {
92
+ pub api_key: &'a str,
93
+ pub base_url: &'a str,
94
+ }
95
+
96
+ pub struct EmbeddedSession {
97
+ pub session_id: String,
98
+ pub client_secret: String,
99
+ }
100
+
101
+ // 31, not 30: Stripe requires an expiry at least 30 minutes out.
102
+ pub const SESSION_HOLD_SECONDS: i64 = 31 * 60;
103
+
104
+ pub fn session_expires_at(now: i64) -> i64 {
105
+ now + SESSION_HOLD_SECONDS
106
+ }
107
+
108
+ // Errors carry Stripe's message when it refuses, never a secret.
109
+ pub async fn create_checkout_session(
110
+ auth: &StripeAuth<'_>,
111
+ price_cents: i64,
112
+ product_name: &str,
113
+ registration_id: &str,
114
+ expires_at: i64,
115
+ ) -> anyhow::Result<EmbeddedSession> {
116
+ let unit_amount = price_cents.to_string();
117
+ let expires_at = expires_at.to_string();
118
+ let params = [
119
+ ("mode", "payment"),
120
+ ("ui_mode", "embedded_page"),
121
+ ("redirect_on_completion", "never"),
122
+ ("expires_at", expires_at.as_str()),
123
+ ("line_items[0][price_data][currency]", "usd"),
124
+ ("line_items[0][price_data][unit_amount]", unit_amount.as_str()),
125
+ ("line_items[0][price_data][product_data][name]", product_name),
126
+ ("line_items[0][quantity]", "1"),
127
+ ("metadata[registration_id]", registration_id),
128
+ ];
129
+
130
+ let response = reqwest::Client::new()
131
+ .post(format!("{}/v1/checkout/sessions", auth.base_url))
132
+ .bearer_auth(auth.api_key)
133
+ .header("Stripe-Version", STRIPE_API_VERSION)
134
+ .form(&params)
135
+ .send()
136
+ .await?;
137
+
138
+ let status = response.status();
139
+ let body: Value = response.json().await?;
140
+ if !status.is_success() {
141
+ let message = body
142
+ .get("error")
143
+ .and_then(|e| e.get("message"))
144
+ .and_then(|v| v.as_str())
145
+ .unwrap_or("unknown Stripe error");
146
+ anyhow::bail!("Stripe checkout session creation failed ({status}): {message}");
147
+ }
148
+
149
+ let text = |field: &str| body.get(field).and_then(|v| v.as_str()).map(String::from);
150
+ match (text("id"), text("client_secret")) {
151
+ (Some(session_id), Some(client_secret)) => Ok(EmbeddedSession { session_id, client_secret }),
152
+ // The body is left out of the message: it may carry the client secret.
153
+ _ => anyhow::bail!("Stripe's response carried no \"id\" and \"client_secret\" for the embedded session"),
154
+ }
155
+ }
156
+
157
+ #[cfg(test)]
158
+ mod tests {
159
+ use super::*;
160
+
161
+ fn sign(secret: &str, timestamp: i64, payload: &str) -> String {
162
+ let signed_payload = format!("{timestamp}.{payload}");
163
+ let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
164
+ mac.update(signed_payload.as_bytes());
165
+ hex_encode(&mac.finalize().into_bytes())
166
+ }
167
+
168
+ #[test]
169
+ fn mock_checkout_session_matches_local_checkouts_own_pay_page_shape() {
170
+ // The Ruby local checkout adapter's own real output, same three parts: this
171
+ // site's own /pay/<registration_id>.html page, with success_url
172
+ // and cancel_url riding along as encoded query params.
173
+ let url = mock_checkout_session(
174
+ "REG-1",
175
+ "https://example.com/yoga.html?registered=1",
176
+ "https://example.com/yoga.html?registered=0",
177
+ "https://example.com",
178
+ );
179
+ assert_eq!(
180
+ url,
181
+ "https://example.com/pay/REG-1.html?success_url=https%3A%2F%2Fexample.com%2Fyoga.html%3Fregistered%3D1&cancel_url=https%3A%2F%2Fexample.com%2Fyoga.html%3Fregistered%3D0"
182
+ );
183
+ }
184
+
185
+ #[test]
186
+ fn mock_checkout_session_carries_the_registration_id_in_its_own_path_not_a_query_param() {
187
+ let url = mock_checkout_session("REG-2", "https://example.com/yoga.html", "https://example.com/yoga.html", "https://example.com");
188
+ assert!(url.starts_with("https://example.com/pay/REG-2.html?"));
189
+ }
190
+
191
+ #[test]
192
+ fn a_correctly_signed_payload_verifies() {
193
+ let payload = r#"{"type":"checkout.session.completed"}"#;
194
+ let secret = "whsec_test";
195
+ let now = 1_700_000_000;
196
+ let header = format!("t={now},v1={}", sign(secret, now, payload));
197
+
198
+ assert!(verify_signature(payload, &header, secret, now).is_ok());
199
+ }
200
+
201
+ #[test]
202
+ fn a_payload_tampered_with_after_signing_is_rejected() {
203
+ let secret = "whsec_test";
204
+ let now = 1_700_000_000;
205
+ let header = format!("t={now},v1={}", sign(secret, now, r#"{"type":"checkout.session.completed"}"#));
206
+
207
+ // Same header, different body — exactly what an attacker
208
+ // intercepting and rewriting the request in transit would send.
209
+ let tampered = r#"{"type":"checkout.session.expired"}"#;
210
+ assert!(verify_signature(tampered, &header, secret, now).is_err());
211
+ }
212
+
213
+ #[test]
214
+ fn the_wrong_secret_is_rejected() {
215
+ let payload = r#"{"type":"checkout.session.completed"}"#;
216
+ let now = 1_700_000_000;
217
+ let header = format!("t={now},v1={}", sign("whsec_real", now, payload));
218
+
219
+ assert!(verify_signature(payload, &header, "whsec_wrong", now).is_err());
220
+ }
221
+
222
+ #[test]
223
+ fn a_timestamp_outside_the_tolerance_window_is_rejected_even_with_a_correct_signature() {
224
+ let payload = r#"{"type":"checkout.session.completed"}"#;
225
+ let secret = "whsec_test";
226
+ let signed_at = 1_700_000_000;
227
+ let header = format!("t={signed_at},v1={}", sign(secret, signed_at, payload));
228
+
229
+ // A replayed webhook -- the signature is genuinely correct for
230
+ // its own timestamp, which is exactly why the tolerance check has
231
+ // to be a separate, independent gate rather than folded into
232
+ // "does the signature verify at all."
233
+ let much_later = signed_at + TOLERANCE_SECONDS + 1;
234
+ assert!(verify_signature(payload, &header, secret, much_later).is_err());
235
+ }
236
+
237
+ #[test]
238
+ fn a_second_v1_during_a_secret_rotation_window_verifies_against_either() {
239
+ let payload = r#"{"type":"checkout.session.completed"}"#;
240
+ let old_secret = "whsec_old";
241
+ let new_secret = "whsec_new";
242
+ let now = 1_700_000_000;
243
+ let header = format!(
244
+ "t={now},v1={},v1={}",
245
+ sign(old_secret, now, payload),
246
+ sign(new_secret, now, payload)
247
+ );
248
+
249
+ assert!(verify_signature(payload, &header, old_secret, now).is_ok());
250
+ assert!(verify_signature(payload, &header, new_secret, now).is_ok());
251
+ }
252
+
253
+ #[test]
254
+ fn a_header_with_no_v1_at_all_is_rejected() {
255
+ let now = 1_700_000_000;
256
+ let header = format!("t={now}");
257
+ assert!(verify_signature("{}", &header, "whsec_test", now).is_err());
258
+ }
259
+
260
+ #[test]
261
+ fn a_header_with_no_timestamp_at_all_is_rejected() {
262
+ let header = "v1=deadbeef".to_string();
263
+ assert!(verify_signature("{}", &header, "whsec_test", 1_700_000_000).is_err());
264
+ }
265
+ }