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
@@ -46,14 +46,32 @@ module Hecks
46
46
  module ExpressionReads
47
47
  module_function
48
48
 
49
+ # Paths by canonical text: parsing is a pure function of the text, and the analyzer asks
50
+ # for the same rule text on every dispatch of a command. A text that fails to parse
51
+ # raises before it is stored, so a failure is never cached. Written from dispatch
52
+ # threads, so every access holds PATHS_LOCK; the cache is cleared when it reaches
53
+ # PATHS_CACHE_LIMIT entries, so distinct texts cannot grow it without bound.
54
+ # rubocop:disable-next Style/MutableConstant
55
+ PATHS_CACHE = {}
56
+ PATHS_LOCK = Mutex.new
57
+ PATHS_CACHE_LIMIT = 4096
58
+ private_constant :PATHS_CACHE, :PATHS_LOCK
59
+
49
60
  # Finds every dotted path a canonical expression reads.
50
61
  #
51
62
  # Walks the parsed nodes the evaluator uses; only Lookup nodes carry dependencies.
52
63
  #
53
64
  # @param canonical [String] the canonical expression text
54
- # @return [Array<String>] the dotted paths the expression reads
65
+ # @return [Array<String>] the dotted paths the expression reads (frozen)
55
66
  def paths(canonical)
56
- collect(Bluebook::Expression::Evaluator.parse(canonical), Set.new)
67
+ cached = PATHS_LOCK.synchronize { PATHS_CACHE[canonical] }
68
+ return cached if cached
69
+
70
+ found = collect(Bluebook::Expression::Evaluator.parse(canonical), Set.new).freeze
71
+ PATHS_LOCK.synchronize do
72
+ PATHS_CACHE.clear if PATHS_CACHE.size >= PATHS_CACHE_LIMIT && !PATHS_CACHE.key?(canonical)
73
+ PATHS_CACHE[canonical] ||= found
74
+ end
57
75
  end
58
76
 
59
77
  # Walks one parsed node, skipping names bound by an enclosing block predicate.
@@ -1,5 +1,6 @@
1
1
  require_relative "errors"
2
2
  require_relative "refusal_wording"
3
+ require_relative "reaction_outcome"
3
4
  require_relative "caller"
4
5
  require_relative "invocation"
5
6
  require_relative "command_rules"
@@ -19,7 +20,36 @@ module Hecks
19
20
  class Dispatcher
20
21
  MAX_REACTION_DEPTH = 5
21
22
 
22
- Result = Struct.new(:verb, :instance, :events, :execution_plan, :persistence_outcome, keyword_init: true) do
23
+ Result = Struct.new(:verb, :instance, :events, :execution_plan, :persistence_outcome,
24
+ :refused_reactions, :blocking_reactions, :reaction_defects,
25
+ keyword_init: true) do
26
+ # Lists the policy reactions this dispatch caused that the domain refused.
27
+ #
28
+ # A policy's trigger that a `given` or invariant refuses does not undo the command that
29
+ # fired it, which has already persisted, so the outcome above stays a success. The
30
+ # refusal is recorded here instead of vanishing into the reaction log.
31
+ #
32
+ # @return [Array<Hash{Symbol => Object}>] one `{ policy:, trigger:, reason: }` per refused
33
+ # reaction, oldest first; empty when every reaction was delivered
34
+ def refused_reactions = self[:refused_reactions] || []
35
+
36
+ # Lists the refused reactions that block the run, as opposed to a benign non-match.
37
+ #
38
+ # See `ReactionOutcome`: a refusal is benign when a sibling reaction to the same event
39
+ # delivered another command on the same aggregate. `--wait` exits 1 on any that remain.
40
+ #
41
+ # @return [Array<Hash{Symbol => Object}>] the subset of `refused_reactions` that blocks
42
+ def blocking_reactions = self[:blocking_reactions] || []
43
+
44
+ # Lists the reactions that crashed, as opposed to being refused by the domain.
45
+ #
46
+ # A crash is warned and never re-raised, since the emitting command has persisted; it is
47
+ # recorded here so `--wait` can fail on a chain that stopped halfway.
48
+ #
49
+ # @return [Array<Hash{Symbol => Object}>] `{ policy:, trigger:, reason:, error_class: }`
50
+ # per crashed reaction; empty when none crashed
51
+ def reaction_defects = self[:reaction_defects] || []
52
+
23
53
  # Reads the identity of the record the dispatch settled on.
24
54
  #
25
55
  # @return [String, nil] the record's identity; nil for a port operation (no record)
@@ -126,6 +156,14 @@ module Hecks
126
156
  end
127
157
 
128
158
  def dispatch_invocation(verb, to:, with:, saga_correlation:, flat:)
159
+ @registry.collecting_reactions do |reactions|
160
+ dispatch_collecting(verb, reactions, to: to, with: with, saga_correlation: saga_correlation, flat: flat)
161
+ end
162
+ end
163
+ private :dispatch_invocation
164
+
165
+ # One dispatch, with `reactions` receiving every reaction its events cause.
166
+ def dispatch_collecting(verb, reactions, to:, with:, saga_correlation:, flat:)
129
167
  domain, aggregate_name, command_name = parse(verb)
130
168
  aggregate = resolve_aggregate(domain, aggregate_name, verb)
131
169
 
@@ -161,9 +199,21 @@ module Hecks
161
199
  react(announced, domain, aggregate, outbox_rows)
162
200
 
163
201
  Result.new(verb: verb, instance: instance, events: announced,
164
- execution_plan: execution_plan, persistence_outcome: persistence_outcome)
202
+ execution_plan: execution_plan, persistence_outcome: persistence_outcome,
203
+ refused_reactions: refused_from(reactions),
204
+ blocking_reactions: ReactionOutcome.blocking(reactions, event_of: @registry.method(:reaction_event)),
205
+ reaction_defects: ReactionOutcome.defects(reactions))
165
206
  end
166
- private :dispatch_invocation
207
+ private :dispatch_collecting
208
+
209
+ # The reactions among `logged` that the domain refused, as plain facts.
210
+ #
211
+ # A crash in a reaction is a defect, not a refusal, and is warned where it happens.
212
+ def refused_from(logged)
213
+ logged.select { |entry| entry[:delivered] == false && !entry[:defect] }
214
+ .map { |entry| entry.slice(:policy, :trigger, :reason) }
215
+ end
216
+ private :refused_from
167
217
 
168
218
  # Runs the policy and saga reactions owed because `announced` committed.
169
219
  #
@@ -62,6 +62,9 @@ module Hecks
62
62
  next unless normalized.key?(attribute.name)
63
63
 
64
64
  Value.refuse_object_reference(command, attribute, normalized[attribute.name])
65
+ # A has_many argument mirrors the aggregate's reference list, which words its own refusal.
66
+ reference_list = aggregate.attribute(attribute.name)&.reference?
67
+ Value.refuse_scalar_list(command, attribute, normalized[attribute.name]) unless reference_list
65
68
  normalized[attribute.name] = Value.for_attribute(aggregate, attribute, normalized[attribute.name], argument: true)
66
69
  end
67
70
  end
@@ -1,4 +1,4 @@
1
- require_relative "../facade/surface"
1
+ require_relative "../doors/ruby_door"
2
2
  require_relative "../ports/loading"
3
3
  require_relative "../ports/persistence"
4
4
  require_relative "dispatcher"
@@ -11,18 +11,23 @@ module Hecks
11
11
  # Boots a domain: loads its bluebook directory into a fresh Registry, runs
12
12
  # every boot gate, and hands back the bound Dispatcher (or RemoteDispatcher).
13
13
  class Loader
14
+ # Default for a boot's `environment:` keyword: read `HECKS_ENVIRONMENT`. An explicit
15
+ # `nil` means no overlay, whatever the variable holds.
16
+ FROM_ENV = :from_env
17
+
14
18
  # Boots `path`: loads its bluebook directory into a fresh Registry, runs
15
19
  # every registered boot gate, and returns the bound dispatcher. Pass
16
- # `install_facade: false` to skip the `Widget::Item.Add(...)` global
20
+ # `install_doors: false` to skip the `Widget::Item.Add(...)` global
17
21
  # facade sugar (only a caller dispatching by FQN string needs to).
18
22
  #
19
23
  # @param path [String] a domain directory to boot
20
24
  # @param shared [String, nil] shared ports/adapters root; nil walks up from `path`
21
- # @param environment [String, nil] environment overlay loaded after the domain; nil for none
25
+ # @param environment [String, nil] environment overlay loaded after the domain; defaults
26
+ # to `HECKS_ENVIRONMENT`, and an explicit nil loads none
22
27
  # @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted dispatcher
23
28
  # @raise [Errno::ENOENT] if `path` names no domain directory
24
29
  # @raise [Runtime::WiringError] if a boot gate finds a wiring problem
25
- def self.boot(path, shared: nil, install_facade: true, environment: nil)
30
+ def self.boot(path, shared: nil, install_doors: true, install_facade: nil, environment: FROM_ENV)
26
31
  loading = Ports::Loading.bootstrap
27
32
  directory = loading.bluebook_directory(path)
28
33
  root = loading.shared_root(shared, directory)
@@ -31,14 +36,55 @@ module Hecks
31
36
  Hecks.with_registry(registry) do
32
37
  loading.load_library
33
38
  loading.load_project(root)
34
- loading.load_domain(directory, environment: environment)
39
+ loading.load_domain(directory, environment: selected_environment(environment))
35
40
  end
36
41
 
37
42
  run_boot_gates!(registry, directory)
38
43
  dispatcher = dispatcher_for(registry)
39
44
  redrive_outbox!(dispatcher)
40
45
  seed_privacy_markings!(dispatcher, registry)
41
- install_facade ? bind_runtime(dispatcher) : dispatcher
46
+ Doors.install?(install_doors, install_facade) ? bind_runtime(dispatcher) : dispatcher
47
+ end
48
+
49
+ # What `describe` answers: the loaded declarations and nothing bound to run them.
50
+ Described = Struct.new(:registry)
51
+
52
+ # Loads `path`'s declarations into a fresh Registry and stops: no boot gate runs, no
53
+ # persistence adapter is resolved or bound, nothing connects to a database.
54
+ #
55
+ # This is what answers a question about the domain's own shape (its verbs, arguments and
56
+ # usage) in any environment, including one where the bound adapter cannot load.
57
+ #
58
+ # @param path [String] a domain directory to read
59
+ # @param shared [String, nil] shared ports/adapters root; nil walks up from `path`
60
+ # @param environment [String, nil] environment overlay loaded after the domain
61
+ # @return [Described] answers `registry` like a booted dispatcher does
62
+ # @raise [Errno::ENOENT] if `path` names no domain directory
63
+ def self.describe(path, shared: nil, environment: FROM_ENV)
64
+ loading = Ports::Loading.bootstrap
65
+ directory = loading.bluebook_directory(path)
66
+ root = loading.shared_root(shared, directory)
67
+ registry = Registry.new(root: File.dirname(directory))
68
+
69
+ Hecks.with_registry(registry) do
70
+ loading.load_library
71
+ loading.load_project(root)
72
+ loading.load_domain(directory, environment: selected_environment(environment))
73
+ end
74
+ Described.new(registry)
75
+ end
76
+
77
+ # The overlay a boot loads: the caller's own choice (nil meaning none), else the
78
+ # `HECKS_ENVIRONMENT` variable when the caller left the keyword at its default. A domain
79
+ # with no `environments/<name>.*` files ignores either.
80
+ #
81
+ # @param environment [String, nil, Symbol] the overlay a caller passed, or `FROM_ENV`
82
+ # @return [String, nil] the overlay name; nil for none
83
+ def self.selected_environment(environment)
84
+ return environment unless environment == FROM_ENV
85
+
86
+ named = ENV["HECKS_ENVIRONMENT"].to_s.strip
87
+ named.empty? ? nil : named
42
88
  end
43
89
 
44
90
  # Redrives pending outbox rows after the dispatcher and saga
@@ -77,10 +123,11 @@ module Hecks
77
123
  #
78
124
  # @param paths [Array<String>, String] the exact file paths to boot, in load order
79
125
  # @param shared [String, nil] shared ports/adapters root; nil walks up from the first path
80
- # @param environment [String, nil] environment overlay loaded after the named files
126
+ # @param environment [String, nil] environment overlay loaded after the named files;
127
+ # defaults to `HECKS_ENVIRONMENT`, and an explicit nil loads none
81
128
  # @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted dispatcher
82
129
  # @raise [Runtime::WiringError] if a boot gate finds a wiring problem
83
- def self.boot_files(paths, shared: nil, install_facade: true, environment: nil)
130
+ def self.boot_files(paths, shared: nil, install_doors: true, install_facade: nil, environment: FROM_ENV)
84
131
  loading = Ports::Loading.bootstrap
85
132
  files = Array(paths).map { |path| File.expand_path(path) }
86
133
  directory = File.dirname(files.first)
@@ -90,12 +137,12 @@ module Hecks
90
137
  Hecks.with_registry(registry) do
91
138
  loading.load_library
92
139
  loading.load_project(root)
93
- loading.load_selected(files, environment: environment)
140
+ loading.load_selected(files, environment: selected_environment(environment))
94
141
  end
95
142
 
96
143
  run_boot_gates!(registry, directory)
97
144
  dispatcher = dispatcher_for(registry)
98
- install_facade ? bind_runtime(dispatcher) : dispatcher
145
+ Doors.install?(install_doors, install_facade) ? bind_runtime(dispatcher) : dispatcher
99
146
  end
100
147
 
101
148
  # Runs every registered boot gate against `registry`, in order:
@@ -117,7 +164,8 @@ module Hecks
117
164
  gates
118
165
  end
119
166
 
120
- # Resolves the Ruby implementation of every adapter a hecksagon binds.
167
+ # Resolves the Ruby implementation of every adapter a hecksagon binds, and of every
168
+ # `default_adapter` a world names (a chapter that binds nothing takes its world's).
121
169
  # An adapter's implementation can register its own persistence plugin as
122
170
  # a side effect of loading (e.g. `PostgresEra`) — resolving here, before
123
171
  # gates are collected, is what registers those plugins' gates without
@@ -131,6 +179,11 @@ module Hecks
131
179
  next
132
180
  end
133
181
  end
182
+ registry.worlds.each_value do |world|
183
+ registry.adapter_class(world.default_adapter) if world.default_adapter
184
+ rescue WiringError
185
+ next
186
+ end
134
187
  end
135
188
 
136
189
  # A backstop only: fires when a translation declares a `computes`/
@@ -169,7 +222,7 @@ module Hecks
169
222
  # `dispatcher`. The binding lives in the facade's own modules, not a
170
223
  # class-level global, so two boots in one process don't share one name.
171
224
  def self.bind_runtime(dispatcher)
172
- Facade::Surface.install(dispatcher)
225
+ Doors::RubyDoor.install(dispatcher)
173
226
  dispatcher
174
227
  end
175
228
  end
@@ -240,7 +240,7 @@ module Hecks
240
240
  when "policy"
241
241
  policy = @registry.bluebook(home)&.policies&.find { |candidate| candidate.name == name } ||
242
242
  raise(WiringError, "outbox row #{row.delivery_id} names policy #{fqn}, which no bluebook declares")
243
- @policies.react(event, row.domain, only: [policy, home])
243
+ @policies.react(event, row.domain, only: [policy, home], event_uid: row.event_uid)
244
244
  when "saga"
245
245
  process_manager = @registry.bluebook(home)&.process_managers&.find { |candidate| candidate.name == name } ||
246
246
  raise(WiringError,
@@ -31,15 +31,18 @@ module Hecks
31
31
  # @param only [Array(Bluebook::Policy, String), nil] one `[policy, home_domain]`
32
32
  # pair to run exactly (the outbox relay's consumer), instead of scanning every
33
33
  # loaded bluebook for candidates
34
+ # @param event_uid [String, nil] the outbox uid every consumer of this event shares, when the
35
+ # event was rebuilt from an outbox row; otherwise the event object itself is the identity
34
36
  # @return [void]
35
- def react(event, domain, only: nil)
37
+ def react(event, domain, only: nil, event_uid: nil)
36
38
  selected = only ? [only] : policies_for(event, domain)
37
39
  selected.each do |policy, home_domain|
38
40
  result = deliver(policy, event, home_domain)
39
41
  next if result.nil?
40
42
 
41
43
  # A for_each policy answers an array; Array(...) would explode a record Hash.
42
- (result.is_a?(Array) ? result : [result]).each { |record| @registry.reaction_log << record }
44
+ identity = event_uid || event.object_id
45
+ (result.is_a?(Array) ? result : [result]).each { |record| @registry.log_reaction(record, event: identity) }
43
46
  end
44
47
  end
45
48
 
@@ -1,3 +1,4 @@
1
+ require_relative "adapter_lookup"
1
2
  require_relative "interpreting"
2
3
  require_relative "command_interpreter/argument_gate"
3
4
  require_relative "event"
@@ -125,16 +126,7 @@ module Hecks
125
126
  end
126
127
 
127
128
  def adapter_for(ctx)
128
- name = port_name_for(ctx)
129
- implementations = @registry.adapters.values.select { |adapter| adapter.port == name }
130
-
131
- case implementations.size
132
- when 1 then Adapters.const_get(implementations.first.name).new
133
- when 0 then raise WiringError, "no adapter implements the #{name} port — nothing can answer #{ctx.operation.hecks_name}"
134
- else raise WiringError,
135
- "#{implementations.size} adapters implement the #{name} port " \
136
- "(#{implementations.map(&:name).sort.join(', ')}) — the runtime will not choose for you"
137
- end
129
+ AdapterLookup.call(@registry, port_name_for(ctx), asked: ctx.operation.hecks_name)
138
130
  end
139
131
 
140
132
  # Adapters are handed plain data, never a Value.
@@ -1,4 +1,5 @@
1
1
  require_relative "../naming"
2
+ require_relative "adapter_lookup"
2
3
  require_relative "../ports/query"
3
4
  require_relative "../ports/query/ordering"
4
5
  require_relative "../query_specification/field_path"
@@ -33,6 +34,10 @@ module Hecks
33
34
 
34
35
  declared = declared_query(aggregate, query_name)
35
36
  args = normalize_args(aggregate, declared, args)
37
+ if (port = aggregate.query_binding(declared.name))
38
+ return answered_from_outside(aggregate, declared, args, port)
39
+ end
40
+
36
41
  declared = TenantScope.apply(declared, args)
37
42
  # Applied after TenantScope so its tenant clause rides through as an ordinary
38
43
  # local clause; it is deliberately not propagated into the hop's inner query.
@@ -66,6 +71,11 @@ module Hecks
66
71
 
67
72
  declared = declared_query(aggregate, query_name)
68
73
  args = normalize_args(aggregate, declared, args)
74
+ # No records to interpret: the adapter is the only answer there is, and the reference
75
+ # takes it through the same shape check, so a diff against #call sees only the adapter.
76
+ port = aggregate.query_binding(declared.name)
77
+ return answered_from_outside(aggregate, declared, args, port) if port
78
+
69
79
  declared = TenantScope.apply(declared, args)
70
80
  reference_interpret(@registry.repository(domain, aggregate).all, declared, args,
71
81
  domain: domain, shape: aggregate)
@@ -73,6 +83,73 @@ module Hecks
73
83
 
74
84
  private
75
85
 
86
+ # Asks the port's adapter the question the hecksagon bound to this query, by the query's
87
+ # snake-cased name and with its arguments as plain data. Each row of the answer is built as
88
+ # the value object the query `returns`, so an answer that is not that shape is refused
89
+ # before it enters the domain. The aggregate's records are never read.
90
+ #
91
+ # @return [Array<Hash>] the rows as the returned value object's fields, frozen
92
+ # @raise [Runtime::WiringError] if no adapter answers the port or the adapter lacks the method
93
+ # @raise [Runtime::TypeMismatch, Runtime::InvariantViolation, Runtime::UnknownArgument,
94
+ # Runtime::AbsentArgument] naming the query, if the answer is not its declared shape
95
+ def answered_from_outside(aggregate, declared, args, port)
96
+ asked = "#{aggregate.hecks_name}.#{declared.name}"
97
+ refuse_offered_arguments!(declared, args, asked)
98
+ adapter = AdapterLookup.call(@registry, port.name, asked: asked)
99
+ method = Naming.snake(declared.name)
100
+ # The real adapter's class is what must answer; a fuzz replay's stand-in refuses instead.
101
+ klass = AdapterLookup.adapter_class(@registry, port.name, asked: asked)
102
+ AdapterLookup.check_answers!(klass, port.name, method, declared, asked: asked)
103
+
104
+ answer = adapter.public_send(method, **Value.materialize(args))
105
+ Freezer.deep(shaped(aggregate, declared, answer, asked))
106
+ end
107
+
108
+ # Refuses arguments the adapter cannot be handed: a required one left out, or one the query
109
+ # does not declare. The adapter is asked by keyword, so either would otherwise reach it as a
110
+ # raw ArgumentError.
111
+ #
112
+ # @raise [Runtime::AbsentArgument] if a non-optional declared argument is missing
113
+ # @raise [Runtime::UnknownArgument] if `args` names an argument the query does not declare
114
+ def refuse_offered_arguments!(declared, args, asked)
115
+ names = declared.attributes.map { |attribute| attribute.name.to_sym }
116
+ offered = args.keys.map(&:to_sym)
117
+ unknown = (offered - names).sort
118
+ unless unknown.empty?
119
+ raise UnknownArgument, RefusalWording.render_site("UnknownArgument", "unknown_args",
120
+ command: asked, unknown: unknown, declared: names)
121
+ end
122
+
123
+ required = declared.attributes.reject(&:optional?).map { |attribute| attribute.name.to_sym }
124
+ absent = (required - offered).sort
125
+ return if absent.empty?
126
+
127
+ raise AbsentArgument, RefusalWording.render_site("AbsentArgument", "absent_args",
128
+ command: asked, absent: absent, declared: names)
129
+ end
130
+
131
+ # Builds the adapter's answer as the declared value object: one row, or a list of rows for
132
+ # `returns list_of(...)`. A refusal keeps its class and gains the query's name.
133
+ def shaped(aggregate, declared, answer, asked)
134
+ value_object = Value.value_object_for(aggregate, declared.returns_name) or
135
+ raise WiringError, "#{asked} returns #{declared.returns_name.inspect}, which the aggregate " \
136
+ "declares no value object for"
137
+ offered = declared.returns_list? ? answer : [answer]
138
+ unless offered.is_a?(Array) && offered.all?(Hash)
139
+ raise TypeMismatch, "#{asked} answered outside the domain, but #{answer.class} is not " \
140
+ "#{declared.returns_list? ? 'a list of' : 'a'} #{value_object.hecks_name} row"
141
+ end
142
+
143
+ offered.map { |row| answered_row(value_object, row, aggregate, declared, asked) }
144
+ end
145
+
146
+ # One row of an outside answer, built and validated as the returned value object.
147
+ def answered_row(value_object, row, aggregate, declared, asked)
148
+ Value.build(value_object, row, aggregate).to_h
149
+ rescue TypeMismatch, InvariantViolation, UnknownArgument, AbsentArgument => e
150
+ raise e.class, "#{asked} answered outside the domain, but not as its #{declared.returns} — #{e.message}"
151
+ end
152
+
76
153
  def declared_query(aggregate, query_name)
77
154
  aggregate.query(query_name) ||
78
155
  raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "no_query",
@@ -0,0 +1,89 @@
1
+ require_relative "refusal_wording"
2
+
3
+ module Hecks
4
+ module Runtime
5
+ # Tells a refused policy reaction that blocks a run from one that is a benign non-match.
6
+ #
7
+ # A policy pair such as `RecordTheMatch` / `RecordTheDrift` reacts to one event with two
8
+ # commands on one aggregate, each `given`-gated so exactly one applies; the other is refused
9
+ # by design and the run passed. A refusal is benign when another reaction to the same event
10
+ # instance delivered a different command on the same aggregate: an alternative took over. It
11
+ # is also benign when the creating command it triggered reports the record already exists (a
12
+ # re-run's idempotent registration: the record the run wanted is there). Every other refusal
13
+ # (a release's `Accept` refused by a dirty tree, with no alternative) blocks the outcome. A
14
+ # crash (`defect`) is never a refusal: it is warned where it happens and listed by
15
+ # `defects`.
16
+ module ReactionOutcome
17
+ # A reaction entry, with the identity of the event instance it answered, when known.
18
+ Row = Struct.new(:fields, :event) do
19
+ def delivered = fields[:delivered]
20
+ end
21
+
22
+ module_function
23
+
24
+ # Picks the refused reactions that block the run.
25
+ #
26
+ # An alternative counts only when it answered the same event instance, which `event_of`
27
+ # reads (an outbox uid, or the event object's id). Without it (a remote host's JSON carries
28
+ # no event identity) the event's name is all there is to match.
29
+ #
30
+ # @param entries [Array<Hash>] every reaction one dispatch caused, delivered or not; keys
31
+ # are Symbols (local) or Strings (a remote host's JSON)
32
+ # @param event_of [#call, nil] maps an entry, as given, to the event instance it answered
33
+ # @return [Array<Hash{Symbol => Object}>] `{ policy:, trigger:, reason: }` per blocker
34
+ def blocking(entries, event_of: nil)
35
+ rows = Array(entries).map { |entry| Row.new(entry.transform_keys(&:to_sym), event_of&.call(entry)) }
36
+ rows.select { |row| row.delivered == false && !row.fields[:defect] }
37
+ .reject { |row| already_there?(row) }
38
+ .reject { |row| rows.any? { |other| alternative?(row, other) } }
39
+ .map { |row| row.fields.slice(:policy, :trigger, :reason) }
40
+ end
41
+
42
+ # Whether `row` is its own creating command reporting the record already exists.
43
+ def already_there?(row)
44
+ RefusalWording.already_exists?(row.fields[:reason], **creating(row.fields[:trigger]))
45
+ end
46
+
47
+ # Whether `other` delivered a different command on the aggregate `refused` targeted, in
48
+ # answer to the same event.
49
+ def alternative?(refused, other)
50
+ other.delivered == true && same_event?(refused, other) &&
51
+ other.fields[:trigger] != refused.fields[:trigger] &&
52
+ aggregate_of(other.fields[:trigger]) == aggregate_of(refused.fields[:trigger])
53
+ end
54
+
55
+ # Whether both reactions answered one event: the same instance when both are known, else
56
+ # the same event name.
57
+ def same_event?(refused, other)
58
+ return false unless other.fields[:on] == refused.fields[:on]
59
+
60
+ refused.event.nil? || other.event.nil? || refused.event == other.event
61
+ end
62
+
63
+ # The `Domain::Aggregate` part of a `Domain::Aggregate.Command` trigger.
64
+ def aggregate_of(trigger) = trigger.to_s.split(".").first
65
+
66
+ # The command and aggregate a trigger names, spelled as the AlreadyExists sentence does.
67
+ def creating(trigger)
68
+ target, command = trigger.to_s.split(".", 2)
69
+ return { command: target&.split("::")&.last } unless command
70
+
71
+ { command: command.split(".").last, aggregate: target.split("::").last }
72
+ end
73
+
74
+ # Picks the reactions that crashed, which no refusal rule excuses.
75
+ #
76
+ # A chain that crashes halfway leaves its record short of the state the run was meant to
77
+ # reach, so a caller that waits on the run must treat every one as a failure.
78
+ #
79
+ # @param entries [Array<Hash>] every reaction one dispatch caused, keys as for `blocking`
80
+ # @return [Array<Hash{Symbol => Object}>] `{ policy:, trigger:, reason:, error_class: }`
81
+ # per crashed reaction
82
+ def defects(entries)
83
+ Array(entries).map { |entry| entry.transform_keys(&:to_sym) }
84
+ .select { |row| row[:defect] }
85
+ .map { |row| row.slice(:policy, :trigger, :reason, :error_class) }
86
+ end
87
+ end
88
+ end
89
+ end
@@ -19,6 +19,28 @@ module Hecks
19
19
  substitute(template(refusal, site), values)
20
20
  end
21
21
 
22
+ # Whether `reason` is the AlreadyExists refusal for a duplicate creation, matched by the
23
+ # template's own wording so a remote host's reason reads the same as a local one.
24
+ #
25
+ # With `command` (and `aggregate`), the sentence must be that command's own: text that only
26
+ # resembles the template but names another command is not a duplicate creation.
27
+ #
28
+ # @param reason [String, nil] the refusal message
29
+ # @param command [String, nil] the bare name of the creating command that was refused
30
+ # @param aggregate [String, nil] the bare name of the aggregate it creates
31
+ # @return [Boolean]
32
+ def already_exists?(reason, command: nil, aggregate: nil)
33
+ given = { "command" => command, "aggregate" => aggregate }
34
+ pieces = TEMPLATES.fetch(%w[AlreadyExists creating_duplicate]).split(/(\{\w+\})/, -1)
35
+ pattern = pieces.map do |piece|
36
+ name = piece[/\A\{(\w+)\}\z/, 1]
37
+ next Regexp.escape(piece) unless name
38
+
39
+ given[name] ? Regexp.escape(given[name]) : ".*"
40
+ end.join
41
+ Regexp.new("\\A#{pattern}\\z", Regexp::MULTILINE).match?(reason.to_s)
42
+ end
43
+
22
44
  # Renders a site's template from raw `arguments`, formatting each per its row.
23
45
  # Raises ArgumentError on a missing or undeclared argument, KeyError on an unknown site.
24
46
  def render_site(refusal, site, **arguments)
@@ -65,7 +87,7 @@ module Hecks
65
87
  end
66
88
  end
67
89
 
68
- # Read lazily: bin/project_vocabulary boots this file before it writes these rows.
90
+ # Read lazily: hecks project_vocabulary boots this file before it writes these rows.
69
91
  def argument_rows(refusal, site)
70
92
  @argument_rows ||= Hecks::Vocabulary.rows("RefusalSiteArgument")
71
93
  .group_by { |row| [row["refusal"], row["site"]] }