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,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "time"
5
+ require_relative "console_capture"
6
+ require "hecks/cli/run"
7
+ require "hecks/cli/model_check"
8
+ require "hecks/cli/smoke_test"
9
+ require "hecks/cli/behaviors"
10
+ require "hecks/cli/refresh_projections"
11
+
12
+ module Hecks
13
+ module Adapters
14
+ # What the `DomainRuntime` port's adapter does when a journaled Custodian command asks it to:
15
+ # the asks of `ModelCheckRun` and `Operation`, plus the `Follow` query.
16
+ #
17
+ # An ask is handed the whole record it was made on (its `held_state`), so each method reads the
18
+ # record's fields, runs the existing `Hecks::CLI::*` entry point captured, and answers the text
19
+ # it printed. An answer records a success; a raise records a refusal, so a check that finds
20
+ # something, or a run that fails, is refused with the report as its reason.
21
+ module InProcessOperations
22
+ # The chapters `hecks model_check` sweep when no domain is named need a
23
+ # checkout; an installed gem has none.
24
+ CHECKOUT_MARKER = "hecks.gemspec"
25
+
26
+ # Longest a `Follow` query may wait for new entries, so one call cannot hold a launcher open.
27
+ MAX_FOLLOW_WAIT = 60
28
+
29
+ # Runs the static analysis over the named domains, or over the whole corpus in a checkout.
30
+ #
31
+ # @param held [Hash] the `ModelCheckRun` record: `domains` (comma separated paths), `strict`
32
+ # and `profile`
33
+ # @return [Hash{Symbol => Hash}] `report:` the analysis as printed
34
+ # @raise [ConsoleCapture::Failure] when a finding was left, or the analysis could not run
35
+ def check(**held)
36
+ argv = plain(held[:domains]).to_s.split(",").map(&:strip).reject(&:empty?)
37
+ missing = argv.reject { |domain| File.exist?(domain) }
38
+ raise Runtime::NotFound, "no such domain #{missing.first.inspect}" if missing.any?
39
+
40
+ argv.unshift("--strict") if plain(held[:strict])
41
+ argv.unshift("--profile", plain(held[:profile]).to_s) if present?(held[:profile])
42
+
43
+ text = ConsoleCapture.answer do
44
+ CLI::ModelCheck.call(argv, program: "hecks model_check", root: checkout_root)
45
+ end
46
+ { report: { value: text } }
47
+ end
48
+
49
+ # Dispatches one verb, or executes a step list, against a domain.
50
+ #
51
+ # @param held [Hash] the `Operation` record: `subject` (the domain), then `script`, or
52
+ # `verb` with `arguments` (its `name=value` words)
53
+ # @return [Hash{Symbol => Hash}] `output:` what the run reported
54
+ # @raise [ConsoleCapture::Failure] when the verb was refused or an expectation was unmet
55
+ def execute(**held)
56
+ argv = [plain(held[:subject])].compact
57
+ script = plain(held[:script])
58
+ argv += script ? [script] : [plain(held[:verb]), *words(held[:arguments])].compact
59
+
60
+ output(ConsoleCapture.answer { CLI::Run.call(argv, program: "hecks run") })
61
+ end
62
+
63
+ # Forces every read-model projection the domain declares to catch up.
64
+ #
65
+ # @param held [Hash] the `Operation` record: `subject` (the domain)
66
+ # @return [Hash{Symbol => Hash}] `output:` how many projections were refreshed
67
+ # @raise [Runtime::NotFound] if the domain cannot be found
68
+ def refresh(**held)
69
+ count = CLI::RefreshProjections.call(boot(held[:subject]))
70
+ output("refreshed #{count} projection(s)")
71
+ end
72
+
73
+ # Runs a `.behaviors` file, or every one under a directory.
74
+ #
75
+ # @param held [Hash] the `Operation` record: `subject` (the file or directory)
76
+ # @return [Hash{Symbol => Hash}] `output:` the per-test report
77
+ # @raise [ConsoleCapture::Failure] when any test failed, errored or did not parse
78
+ def verify(**held)
79
+ argv = [plain(held[:subject])].compact
80
+ output(ConsoleCapture.answer { CLI::Behaviors.call(argv, program: "hecks run_behaviors") })
81
+ end
82
+
83
+ # Boots a wired domain and dispatches one synthesized call per command and report.
84
+ #
85
+ # @param held [Hash] the `Operation` record: `subject` (the domain; the enclosing one when
86
+ # absent and this is not a checkout)
87
+ # @return [Hash{Symbol => Hash}] `output:` the per-domain report
88
+ # @raise [ConsoleCapture::Failure] when any dispatch failed
89
+ def smoke(**held)
90
+ domain = plain(held[:subject]) || (checkout_root ? nil : Folder.new.domain_root)
91
+ argv = [domain].compact
92
+
93
+ output(ConsoleCapture.answer { CLI::SmokeTest.call(argv, root: checkout_root || Dir.pwd) })
94
+ end
95
+
96
+ # A domain's event log past a cursor, answered once: what the stream yielded before it
97
+ # ended. Pass the answered `cursor` back as `since` to keep tailing. `wait` bounds the stream
98
+ # and it ends at the first entries, so a call returns as soon as there is something to say.
99
+ #
100
+ # @param domain [Hash, String] a domain directory
101
+ # @param aggregate [Hash, String, nil] only entries of this aggregate, by bare name
102
+ # @param since [Hash, Integer, nil] how many entries were already seen; all when absent
103
+ # @param from_now [Hash, Boolean, nil] skip what exists; answer only a cursor at its end
104
+ # @param wait [Hash, Integer, nil] seconds to wait for a new entry, at most 60
105
+ # @param interval [Hash, Float, nil] seconds between checks of the log
106
+ # @return [Hash] the `Tail` row: `cursor:`, `events:` (payloads as JSON text) and `taken_at:`
107
+ # @raise [Runtime::NotFound] if the domain cannot be found or keeps no event log
108
+ def follow(domain:, aggregate: nil, since: nil, from_now: nil, wait: nil, interval: nil)
109
+ registry = boot(domain).registry
110
+ entries = []
111
+ cursor = stream(domain: domain, aggregate: aggregate, since: since, from_now: from_now,
112
+ timeout: [plain(wait).to_f, MAX_FOLLOW_WAIT].min, interval: interval,
113
+ registry: registry) do |entry|
114
+ entries << entry.merge("payload" => JSON.generate(entry["payload"]))
115
+ :batch
116
+ end
117
+ { cursor: cursor, events: entries, taken_at: Time.at(Ports::Clock.now(registry)).utc.iso8601 }
118
+ end
119
+
120
+ # Tails a domain's event log, handing each entry to the block as the log shows it, until
121
+ # the block returns `:stop` (or `:batch`: after this check's entries), `limit` entries were
122
+ # handed over, `timeout` seconds pass, or the reader goes away; unbounded, until interrupted.
123
+ #
124
+ # @param domain [Hash, String] a domain directory
125
+ # @param aggregate [Hash, String, nil] only entries of this aggregate, by bare name
126
+ # @param since [Hash, Integer, nil] entries already seen; `from_now` skips what exists
127
+ # @param limit [Hash, Integer, nil] entries to hand over at most, or `timeout` seconds
128
+ # @param interval [Hash, Float, nil] seconds between checks (0.5 when absent)
129
+ # @param registry [Runtime::Registry, nil] the domain's booted registry; booted here if absent
130
+ # @return [Integer] the count of log entries seen, to pass back as `since`
131
+ # @raise [Runtime::NotFound] if the domain cannot be found or keeps no event log
132
+ def stream(domain:, aggregate: nil, since: nil, from_now: nil, limit: nil, timeout: nil, interval: nil,
133
+ registry: nil, &block)
134
+ repository = event_repository(domain, registry)
135
+ tail = Tail.new(plain(since)&.to_i || (plain(from_now) ? repository.events.size : 0), 0, false)
136
+ cap = plain(limit)&.to_i
137
+ deadline = plain(timeout) && (monotonic + plain(timeout).to_f)
138
+
139
+ loop do
140
+ verdict = drain(repository, tail, plain(aggregate), cap, &block)
141
+ return tail.seen if verdict == :stop || tail.batch_done || (deadline && monotonic >= deadline)
142
+
143
+ sleep((plain(interval) || 0.5).to_f)
144
+ end
145
+ rescue Errno::EPIPE, Interrupt
146
+ tail.seen
147
+ end
148
+
149
+ private
150
+
151
+ # Where a stream stands: log entries seen, entries handed over, whether a batch ended it.
152
+ Tail = Struct.new(:seen, :handed, :batch_done)
153
+
154
+ # Hands over what the log holds past the tail, one entry at a time; answers `:stop` when the
155
+ # block or the limit ends the stream, so the tail's `seen` stays just past the last entry
156
+ # handed over.
157
+ def drain(repository, tail, filter, cap)
158
+ events = repository.events
159
+ while tail.seen < events.size
160
+ event = events[tail.seen]
161
+ tail.seen += 1
162
+ next unless followed?(event, filter)
163
+
164
+ verdict = yield JSON.parse(JSON.generate(event.to_h))
165
+ tail.handed += 1
166
+ tail.batch_done ||= verdict == :batch
167
+ return :stop if verdict == :stop || (cap && tail.handed >= cap)
168
+ end
169
+ nil
170
+ end
171
+
172
+ def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
173
+
174
+ def output(text) = { output: { value: text } }
175
+
176
+ def present?(argument) = !plain(argument).to_s.empty?
177
+
178
+ # A list of value objects or bare strings, as plain strings.
179
+ def words(list) = Array(list).map { |word| plain(word) }
180
+
181
+ # The repository root when this is a hecks checkout: `hecks.gemspec` sits beside `lib/`.
182
+ def checkout_root
183
+ root = File.expand_path("../../../..", __dir__)
184
+ root if File.exist?(File.join(root, CHECKOUT_MARKER))
185
+ end
186
+
187
+ def event_repository(domain, registry = nil)
188
+ registry ||= boot(domain).registry
189
+ repositories = registry.bluebooks.flat_map do |name, bluebook|
190
+ bluebook.aggregates.map { |aggregate| registry.repository(name, aggregate) }
191
+ end
192
+ repositories.find { |repository| repository.respond_to?(:events) } or
193
+ raise Runtime::NotFound, "no adapter in #{plain(domain)} persists an event log"
194
+ end
195
+
196
+ # Says whether `event` passes the aggregate filter: a bare name, case-insensitive, or exact.
197
+ def followed?(event, filter)
198
+ filter.nil? || event.aggregate.to_s.split("::").last.casecmp?(filter) || event.aggregate == filter
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "time"
5
+ require_relative "held_domain"
6
+ require_relative "edge_audit"
7
+ require_relative "compaction"
8
+ require_relative "../git"
9
+
10
+ module Hecks
11
+ module Adapters
12
+ class JournalStore
13
+ # The changes the journal store makes once `Era.Permit` has let one through.
14
+ #
15
+ # The rules that need no database are `given`s of the Era commands, including that a
16
+ # re-attested text differs from its digest and keeps the era's shape. What stays here is what
17
+ # only the database can enforce: the connection's write fence, the per-domain advisory lock
18
+ # and its timeout, the append-only re-attestation, the audit that rolls a merge back, the
19
+ # monotonic `compacted_through` floor and the row-level-security delete check.
20
+ module Changes
21
+ # Makes the change the record asks for.
22
+ #
23
+ # A change is made only for a record `Era.Permit` admitted, whichever way `apply` is asked.
24
+ #
25
+ # @param held [Hash] the `Era` record
26
+ # @return [Hash{Symbol => Hash}] `report:` what was done, as the script said it
27
+ # @raise [Runtime::WiringError] if the record was not admitted, or the database or an
28
+ # era's own guard refuses the change
29
+ # @raise [Runtime::NotFound] if the domain cannot be loaded
30
+ def apply(**held)
31
+ unless plain(held[:status]) == "admitted"
32
+ raise Runtime::WiringError,
33
+ "the change was not admitted (status #{plain(held[:status]).inspect}): " \
34
+ "only an admitted change is made"
35
+ end
36
+
37
+ { report: { value: send(:"apply_#{plain(held[:operation])}", held) } }
38
+ end
39
+
40
+ private
41
+
42
+ def apply_hold_first(held)
43
+ domain = HeldDomain.open(plain(held[:domain]), mode: :bare)
44
+ domain.writing do |lineage|
45
+ raise Runtime::WiringError, "#{domain.bluebook.name} already holds an era" unless lineage.eras.empty?
46
+
47
+ lineage.hold_first!(domain.source_text, projection: domain.shape)
48
+ end
49
+ "#{domain.bluebook.name} holds era 1 now."
50
+ end
51
+
52
+ def apply_merge_tail(held)
53
+ domain = HeldDomain.open(plain(held[:domain]))
54
+ named = winners(held)
55
+ diverged = domain.writing { |lineage| diverged_writes(domain, lineage) }
56
+ PostgresEra::LineageManager.merge!(registry: domain.registry, bluebook: domain.bluebook,
57
+ settings: domain.settings, winners: named)
58
+ ["#{domain.bluebook.name}: #{diverged} post-cut write#{'s' unless diverged == 1} in ancestor " \
59
+ "eras before the merge",
60
+ "merged — the head now interleaves both worlds by their recorded ordinals",
61
+ *named.map { |id, side| " winner #{id}=#{side} appended as the newest row" }].join("\n")
62
+ end
63
+
64
+ def diverged_writes(domain, lineage)
65
+ eras = domain.held_eras(lineage)
66
+ eras.size > 1 ? (1...eras.last[:ordinal]).sum { |era| lineage.diverged_count(era) } : 0
67
+ end
68
+
69
+ def apply_reattest(held)
70
+ domain = HeldDomain.open(plain(held[:domain]), mode: :bare)
71
+ ordinal = plain(held[:era])
72
+ domain.writing { |lineage| reattest(domain, lineage, ordinal) }
73
+ end
74
+
75
+ def reattest(domain, lineage, ordinal)
76
+ lineage.raw_era(ordinal) or raise Runtime::NotFound, "#{domain.bluebook.name} holds no era #{ordinal}"
77
+ fresh = lineage.reattest!(ordinal)
78
+ "ATTESTED: era #{ordinal} re-frozen as #{fresh[0, 12]}… — the old and new digests are recorded."
79
+ end
80
+
81
+ def apply_backfill_projections(held)
82
+ domain = HeldDomain.open(plain(held[:domain]), mode: :bare)
83
+ domain.writing { |lineage| backfill(domain, lineage) }
84
+ end
85
+
86
+ def backfill(domain, lineage)
87
+ missing = -> { missing_projections(lineage) }
88
+ before = missing.call
89
+ return "#{domain.bluebook.name}: every held era already carries a projection — nothing to do." if before.zero?
90
+
91
+ begin
92
+ # Raises on the first row whose own digest check fails: a tampered row, not a merely
93
+ # legacy one, which backfilling must not paper over.
94
+ lineage.eras
95
+ rescue Runtime::WiringError => e
96
+ raise Runtime::WiringError, "#{domain.bluebook.name}: stopped at a row that isn't merely legacy — " \
97
+ "it fails its own integrity check: #{e.message} — resolve that first " \
98
+ "(`hecks reattest`), then run this again for the remaining rows."
99
+ end
100
+ done = before - missing.call
101
+ "#{domain.bluebook.name}: backfilled #{done} era#{'s' unless done == 1} — every row now carries a projection."
102
+ end
103
+
104
+ def missing_projections(lineage)
105
+ lineage.db.exec_params(
106
+ "SELECT count(*)::int AS n FROM hecks_eras WHERE domain = $1 AND held_projection IS NULL",
107
+ [lineage.domain]
108
+ )[0]["n"].to_i
109
+ end
110
+
111
+ def apply_compact(held) = apply_compaction(held, :entries)
112
+
113
+ def apply_compact_heki(held) = apply_compaction(held, :heki)
114
+
115
+ def apply_compaction(held, kind)
116
+ Compaction.new(plain(held[:domain]), aggregates: names(held[:aggregates]), kind: kind).apply!.join("\n")
117
+ end
118
+
119
+ # Writes `translations/<edge>.approval`; the host reads it at its next boot.
120
+ def apply_approve_translation(held)
121
+ domain = HeldDomain.open(plain(held[:domain]))
122
+ finding = domain.reading { |lineage| audited_edge(domain, lineage) }
123
+ document = Translation::ApprovalFile.build(
124
+ edge: finding.edge, approved_by: Git.new.identity(chdir: domain.directory),
125
+ approved_at: Time.now.utc.iso8601, rehearsal: rehearsal_block(held)
126
+ )
127
+ path = Translation::ApprovalFile.write!(domain.directory, finding.edge, document)
128
+ "approval of edge #{document['edge']} written to #{path} (digest #{document['edge_digest'][0, 12]}…, " \
129
+ "by #{document['approved_by']}). Commit it with the edge; the host applies it at its next boot."
130
+ end
131
+
132
+ def audited_edge(domain, lineage)
133
+ eras = domain.held_eras(lineage)
134
+ finding = eras.empty? ? nil : EdgeAudit.call(domain, lineage, eras)
135
+ raise Runtime::WiringError, "#{domain.bluebook.name} has no edge to approve" unless finding
136
+ raise Runtime::WiringError, "#{finding.report}\nAUDIT REFUSED." unless finding.ok
137
+
138
+ finding
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hecks"
4
+
5
+ module Hecks
6
+ module Adapters
7
+ class JournalStore
8
+ # The journals of one booted domain that can be compacted, and what compacting them does.
9
+ #
10
+ # `:entries` are Postgres and Sqlite journals, compacted through the aggregate's own
11
+ # checkpoint (`compact_entries!`); `:heki` journals are compacted to empty (`compact!`),
12
+ # the snapshot keeping the current state. The database-level guard, that the
13
+ # `compacted_through` floor only moves forward, stays in the adapter that owns the journal.
14
+ class Compaction
15
+ Candidate = Struct.new(:domain_name, :aggregate, :adapter, keyword_init: true)
16
+
17
+ KINDS = { entries: %i[compact_entries! checkpoint], heki: %i[compact!] }.freeze
18
+
19
+ # @param path [String] the domain directory
20
+ # @param aggregates [Array<String>] aggregate names or storage names; every one when empty
21
+ # @param kind [Symbol] `:entries` or `:heki`
22
+ # @raise [Runtime::NotFound] if the directory does not exist
23
+ def initialize(path, aggregates:, kind:)
24
+ raise Runtime::NotFound, "no such domain #{path.inspect}" unless Dir.exist?(path.to_s)
25
+
26
+ @registry = Hecks.boot(File.expand_path(path), install_doors: false).registry
27
+ @wanted = aggregates
28
+ @kind = kind
29
+ end
30
+
31
+ # @return [Array<Candidate>] the aggregates whose journals this kind can compact
32
+ def candidates
33
+ @candidates ||= @registry.bluebooks.flat_map do |domain_name, bluebook|
34
+ bluebook.aggregates.filter_map { |aggregate| candidate(domain_name, aggregate) }
35
+ end
36
+ end
37
+
38
+ # The candidates a `projected_by` binding reads: their projections replay the full journal.
39
+ #
40
+ # @return [Array<Candidate>] the bound candidates
41
+ def bound
42
+ candidates.select do |candidate|
43
+ Ports::Projection.binds_for(@registry, candidate.domain_name, candidate.aggregate).any?
44
+ end
45
+ end
46
+
47
+ # What compacting would do, without doing it.
48
+ #
49
+ # @return [Array<String>] one line per candidate
50
+ def preview
51
+ candidates.map { |candidate| bound.include?(candidate) ? refusal_line(candidate) : dry_line(candidate) }
52
+ end
53
+
54
+ # Compacts every candidate.
55
+ #
56
+ # @return [Array<String>] one line per candidate
57
+ # @raise [Runtime::WiringError] if a projection reads a journal that would be emptied, or
58
+ # the journal's own floor refuses the compaction
59
+ def apply!
60
+ raise Runtime::WiringError, preview.grep(/^REFUSED/).join("\n") if bound.any?
61
+
62
+ candidates.map { |candidate| kind_apply(candidate) }
63
+ end
64
+
65
+ private
66
+
67
+ def candidate(domain_name, aggregate)
68
+ return unless @wanted.empty? || @wanted.include?(aggregate.hecks_name) || @wanted.include?(aggregate.storage_name)
69
+
70
+ repository = @registry.repository(domain_name, aggregate)
71
+ return unless repository.is_a?(Ports::Persistence::AppendOnly)
72
+
73
+ adapter = repository.adapter
74
+ return unless KINDS.fetch(@kind).all? { |operation| adapter.respond_to?(operation) }
75
+
76
+ Candidate.new(domain_name: domain_name, aggregate: aggregate, adapter: adapter)
77
+ end
78
+
79
+ def kind_apply(candidate)
80
+ @kind == :heki ? compact_heki(candidate) : compact_entries(candidate)
81
+ end
82
+
83
+ def dry_line(candidate)
84
+ @kind == :heki ? heki_dry(candidate) : entries_dry(candidate)
85
+ end
86
+
87
+ def refusal_line(candidate)
88
+ "REFUSED #{candidate.aggregate.storage_name}: a projected_by binding reads its full journal — " \
89
+ "compacting would break its catch-up. Not compacted."
90
+ end
91
+
92
+ def entries_dry(candidate)
93
+ name = candidate.aggregate.storage_name
94
+ adapter = candidate.adapter
95
+ through = adapter.checkpoint
96
+ if through <= adapter.compacted_through
97
+ return "SKIP #{name}: nothing new to compact (already compacted through #{through})"
98
+ end
99
+
100
+ "DRY RUN #{name}: would delete journal entries through sequence #{through} (compacted_through is " \
101
+ "currently #{adapter.compacted_through}); current state (#{adapter.count} records) is unaffected."
102
+ end
103
+
104
+ def compact_entries(candidate)
105
+ name = candidate.aggregate.storage_name
106
+ adapter = candidate.adapter
107
+ through = adapter.checkpoint
108
+ if through <= adapter.compacted_through
109
+ return "SKIP #{name}: nothing new to compact (already compacted through #{through})"
110
+ end
111
+
112
+ removed = adapter.compact_entries!(through: through)
113
+ "COMPACTED #{name}: deleted #{removed} journal entries through sequence #{through}; " \
114
+ "#{adapter.count} records preserved"
115
+ end
116
+
117
+ def heki_dry(candidate)
118
+ name = candidate.aggregate.storage_name
119
+ adapter = candidate.adapter
120
+ return "SKIP #{name}: journal already empty" if adapter.entries.empty?
121
+
122
+ "DRY RUN #{name}: would discard #{adapter.entries.length} journal entries (#{journal_bytes(adapter)} " \
123
+ "bytes) at #{adapter.path}.journal; current state (#{adapter.count} records) is unaffected."
124
+ end
125
+
126
+ def compact_heki(candidate)
127
+ name = candidate.aggregate.storage_name
128
+ adapter = candidate.adapter
129
+ entries = adapter.entries.length
130
+ return "SKIP #{name}: journal already empty" if entries.zero?
131
+
132
+ bytes = journal_bytes(adapter)
133
+ adapter.compact!
134
+ "COMPACTED #{name}: discarded #{entries} journal entries (#{bytes} bytes freed); " \
135
+ "#{adapter.count} records preserved at #{adapter.path}"
136
+ end
137
+
138
+ def journal_bytes(adapter)
139
+ path = "#{adapter.path}.journal"
140
+ File.exist?(path) ? File.size(path) : 0
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "held_domain"
5
+
6
+ module Hecks
7
+ module Adapters
8
+ class JournalStore
9
+ # The translation audit of the edge that leads a domain's latest held era to its current
10
+ # shape, without holding or naming anything: an era that has no name yet is named in memory.
11
+ #
12
+ # It answers what the audit's three layers found, as the report `hecks audit_translation`
13
+ # prints, and the edge itself, so approving it audits the very edge that was read.
14
+ class EdgeAudit
15
+ # What one audit found.
16
+ #
17
+ # @!attribute [r] edge
18
+ # @return [Bluebook::Translation] the audited edge
19
+ # @!attribute [r] leaving
20
+ # @return [Integer] how many edges leave the latest held era
21
+ # @!attribute [r] ok
22
+ # @return [Boolean] whether no layer refused
23
+ # @!attribute [r] report
24
+ # @return [String] the report, one block per aggregate
25
+ Finding = Struct.new(:edge, :leaving, :ok, :report, keyword_init: true)
26
+
27
+ # Audits the edge that leaves the latest held era.
28
+ #
29
+ # @param held [HeldDomain] the loaded domain, translation edges included
30
+ # @param lineage [Lineage] the domain's lineage
31
+ # @param eras [Array<Hash>] the eras the lineage holds, at least one
32
+ # @return [Finding, nil] the finding; nil when the domain stands at era 1, with no edge
33
+ # @raise [Runtime::NotFound] if no edge leads the latest era to the current shape, or a
34
+ # preview query is refused by Postgres
35
+ def self.call(held, lineage, eras)
36
+ new(held, lineage, eras).call
37
+ end
38
+
39
+ # @param held [HeldDomain] the loaded domain
40
+ # @param lineage [Lineage] the domain's lineage
41
+ # @param eras [Array<Hash>] the held eras
42
+ def initialize(held, lineage, eras)
43
+ @held = held
44
+ @lineage = lineage
45
+ @eras = eras
46
+ @manager = PostgresEra::LineageManager
47
+ end
48
+
49
+ # @return [Finding, nil] see `EdgeAudit.call`
50
+ def call
51
+ latest = @held.named(@eras.last)
52
+ if @held.shape == Runtime::StorageShape.project(@manager.shadow(latest[:held_text]))
53
+ return nil if latest[:ordinal] == 1
54
+
55
+ settled(latest)
56
+ else
57
+ pending(latest)
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ # The shape already minted: the edge that arrives at the latest era.
64
+ def settled(latest)
65
+ chain = @manager.edge_chain(@held.registry, @held.bluebook, @eras[0..-2], latest[:label])
66
+ finding(chain, latest[:ordinal], 1)
67
+ end
68
+
69
+ # The shape not yet minted: the edge that leaves the latest era for the current shape.
70
+ def pending(latest)
71
+ label = Runtime::StorageShape.mint_hash(@held.bluebook)[0, Runtime::StorageShape::LABEL_LENGTH]
72
+ domain = @held.bluebook.name
73
+ leaving = @held.registry.translations.select { |t| t.domain == domain && t.from == latest[:label] }
74
+ edge = leaving.find { |t| t.to == label } or
75
+ raise Runtime::NotFound, "no translation edge leads #{latest[:label]} to #{label} — " \
76
+ "run `hecks scaffold_translation` first"
77
+ ancestors = @eras[0..-2] + [latest]
78
+ chain = @manager.edge_chain(@held.registry, @held.bluebook, ancestors, label)
79
+ finding(chain, latest[:ordinal] + 1, leaving.size, edge)
80
+ end
81
+
82
+ def finding(chain, era, leaving, edge = chain.last[:translation])
83
+ lines = []
84
+ ok = true
85
+ @held.bluebook.aggregates.each do |aggregate|
86
+ verdict, records = verdict_for(aggregate, edge, era, chain)
87
+ lines.concat(describe(aggregate, edge, verdict, records))
88
+ ok &&= verdict.ok?
89
+ end
90
+ Finding.new(edge: edge, leaving: leaving, ok: ok, report: lines.join("\n"))
91
+ end
92
+
93
+ def verdict_for(aggregate, edge, era, chain)
94
+ after = translated(aggregate, era, chain)
95
+ before = if chain.size > 1
96
+ translated(aggregate, era, chain[0..-2])
97
+ else
98
+ @lineage.ancestor_latest(aggregate, era, chain)
99
+ end
100
+ verdict = Translation::Audit.check(aggregate: aggregate, declared: edge.for_aggregate(aggregate.name),
101
+ before: before, after: after)
102
+ [verdict, after.size]
103
+ end
104
+
105
+ def translated(aggregate, era, chain)
106
+ @lineage.translated_latest(aggregate, era, chain)
107
+ rescue PG::Error => e
108
+ raise Runtime::NotFound, "REFUSED: #{e.message.strip}"
109
+ end
110
+
111
+ def describe(aggregate, edge, verdict, records)
112
+ lines = ["── #{@held.bluebook.name}::#{aggregate.name} (edge #{edge.from} → #{edge.to}, " \
113
+ "#{records} record#{'s' unless records == 1})"]
114
+ verdict.violations.each { |violation| lines << " REFUSED: #{violation}" }
115
+ lines << " dropped (declared data loss): #{verdict.dropped.join(', ')}" unless verdict.dropped.empty?
116
+ unless verdict.unfed.empty?
117
+ lines << " unfed (no rule, no default — add default: if required): #{verdict.unfed.join(', ')}"
118
+ end
119
+ verdict.samples.each { |sample| lines.concat(sample_lines(sample)) }
120
+ lines
121
+ end
122
+
123
+ def sample_lines(sample)
124
+ lines = [" ##{sample[:id]}"]
125
+ lines << " before: #{JSON.generate(sample[:before])}" if sample[:before]
126
+ lines << " after: #{JSON.generate(sample[:after])}" if sample[:after]
127
+ lines
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end