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,196 @@
1
+ require_relative "../runtime/errors"
2
+
3
+ module Hecks
4
+ module Doors
5
+ # Turns `path=value` command-line words into a nested, typed argument Hash.
6
+ # Types come from the projection, never from guessing at the value ("99" may be a String).
7
+ module CliDoor
8
+ # The words a Boolean is spelled with, and what each means.
9
+ BOOLEAN_WORDS = { "true" => true, "yes" => true, "1" => true, "on" => true,
10
+ "false" => false, "no" => false, "0" => false, "off" => false }.freeze
11
+
12
+ module_function
13
+
14
+ # Builds a verb's nested, typed argument Hash from its `name=value` words.
15
+ #
16
+ # arguments(spec, ["reference.value=A-1", "sequence.value=99"])
17
+ # # => { reference: { value: "A-1" }, sequence: { value: 99 } }
18
+ #
19
+ # @param spec [Hash{Symbol => Object}] one verb's entry from `Projector::CliProjector`
20
+ # @param pairs [Array<String>] the words after the verb: `name=value` pairs, where a path may
21
+ # be the short form of a single-field value object (`reference` for `reference.value`);
22
+ # `--name` for a Boolean; and at most one bare word, which fills the verb's first argument
23
+ # @return [Hash{Symbol => Object}] the arguments nested by path, leaves cast to type
24
+ # @raise [Runtime::NotFound] if a path, flag or bare word does not fit the verb
25
+ # @raise [Runtime::TypeMismatch] if a value does not parse as its Integer or Float
26
+ def arguments(spec, pairs)
27
+ # Extra accepted arguments stay out of help, which teaches only to=....
28
+ options = (spec[:arguments] + Array(spec[:legacy_arguments])).to_h do |argument|
29
+ [argument[:path], argument]
30
+ end
31
+
32
+ normalize(spec, pairs, options).each_with_object({}) do |pair, args|
33
+ path, value = split(pair)
34
+ # key? rather than `||`, so the lookup below honors the same spelling `full` chose.
35
+ full = options.key?(path) ? path : expand(path, options)
36
+ argument = options.key?(full) ? options[full] : raise(Runtime::NotFound, unknown(path, options.keys))
37
+
38
+ next words(args, full.split("."), value, argument[:type]) if argument[:words]
39
+ next append(args, full.split("."), cast(value, argument[:type])) if argument[:list]
40
+
41
+ bury(args, full.split("."), cast(value, argument[:type]))
42
+ end
43
+ end
44
+
45
+ # Rewrites the short forms into `name=value`: `--name` for a Boolean (a following `yes`,
46
+ # `no`, `true`, `false`, `on`, `off`, `1` or `0` is that flag's value, not an argument),
47
+ # `--name=value` as `name=value`, and one bare word as the verb's first argument (`to` for
48
+ # a command on an existing aggregate, the first attribute for one that creates).
49
+ def normalize(spec, words, options)
50
+ queue = words.dup
51
+ pairs = []
52
+ bare = []
53
+ until queue.empty?
54
+ word = queue.shift
55
+ if word.start_with?("--") && word.include?("=")
56
+ pairs << underscored(word.delete_prefix("--"), options)
57
+ elsif word.start_with?("--")
58
+ path = flag(word.delete_prefix("--"), options)
59
+ value = BOOLEAN_WORDS.key?(queue.first.to_s.downcase) ? queue.shift : "true"
60
+ pairs << "#{path}=#{value}"
61
+ elsif word.include?("=")
62
+ pairs << word
63
+ else
64
+ bare << word
65
+ pairs << word
66
+ end
67
+ end
68
+ raise Runtime::NotFound, too_many_bare(bare) if bare.length > 1
69
+
70
+ pairs.map { |pair| bare.include?(pair) && !pair.include?("=") ? positional(pair, spec) : pair }
71
+ end
72
+
73
+ # A `name=value` pair whose name is spelled with dashes (`seed-start=3`), as the argument
74
+ # spelled with underscores, unless an argument is named with the dashes.
75
+ def underscored(pair, options)
76
+ name, value = pair.split("=", 2)
77
+ options.key?(name) ? pair : "#{name.tr('-', '_')}=#{value}"
78
+ end
79
+
80
+ # A `--name` flag, as the path of the Boolean argument it stands for. A dashed name
81
+ # (`--gem-only`) is the argument spelled with underscores (`gem_only`).
82
+ def flag(name, options)
83
+ name = name.tr("-", "_") unless options.key?(name) || options.keys.any? { |key| key.start_with?("#{name}.") }
84
+ path = options.key?(name) ? name : expand(name, options)
85
+ return path if options.dig(path, :type) == "Boolean"
86
+
87
+ raise Runtime::NotFound, "--#{name} is a flag, but this verb has no Boolean argument #{name.inspect}"
88
+ end
89
+
90
+ # The one bare word, as a pair for the verb's first argument the launcher does not mint.
91
+ def positional(word, spec)
92
+ first = spec[:arguments].find { |argument| !argument[:minted] }
93
+ return "#{first[:path]}=#{word}" if first
94
+
95
+ if spec[:arguments].any?
96
+ raise Runtime::NotFound, "#{word.inspect} is not name=value; this verb's only argument is its run key, " \
97
+ "which is minted when omitted (run=<key> names one)"
98
+ end
99
+
100
+ raise Runtime::NotFound, "#{word.inspect} is not name=value, and this verb takes no arguments"
101
+ end
102
+
103
+ # Words the refusal for more than one unnamed argument.
104
+ def too_many_bare(bare)
105
+ "only one argument may go unnamed, not #{bare.map(&:inspect).join(', ')}; name the rest as name=value"
106
+ end
107
+
108
+ # Cuts one word at its first `=`, so a value may itself contain `=`.
109
+ def split(pair)
110
+ name, value = pair.split("=", 2)
111
+ raise Runtime::NotFound, "#{pair.inspect} is not name=value" if value.nil?
112
+
113
+ [name, value]
114
+ end
115
+
116
+ # Expands a bare value-object name into the path of its only field.
117
+ # Left unchanged when several fields match, since guessing which was meant is worse.
118
+ def expand(path, options)
119
+ candidates = options.keys.select { |key| key.start_with?("#{path}.") }
120
+ candidates.length == 1 ? candidates.first : path
121
+ end
122
+
123
+ # Converts one value to its declared type; only Integer, Float and Boolean convert.
124
+ def cast(value, type)
125
+ case type
126
+ when "Integer" then Integer(value)
127
+ when "Float" then Float(value)
128
+ when "Boolean" then boolean(value)
129
+ else value
130
+ end
131
+ rescue ArgumentError
132
+ raise Runtime::TypeMismatch, "#{value.inspect} is not #{type} — the chapter declares this field as #{type}"
133
+ end
134
+
135
+ # Reads a Boolean word: `true`, `yes`, `1` or `on` is true; `false`, `no`, `0` or `off` is
136
+ # false; case does not matter. Anything else is refused, since guessing would turn a typo
137
+ # (`dry_run=ture`) into a silent false.
138
+ #
139
+ # @param value [String] the word
140
+ # @return [Boolean]
141
+ # @raise [Runtime::TypeMismatch] if the word is not one of those eight
142
+ def boolean(value)
143
+ BOOLEAN_WORDS.fetch(value.to_s.downcase) do
144
+ raise Runtime::TypeMismatch,
145
+ "#{value.inspect} is not Boolean — use true, false, yes, no, 1, 0, on or off"
146
+ end
147
+ end
148
+
149
+ # Appends one element to a list argument, creating the list on first use.
150
+ #
151
+ # Repeats must grow the list (`bury` would silently keep only the last), and a single
152
+ # element is still a one-item list, since a `list_of` attribute must never see a bare
153
+ # object. Multi-field elements are left to `JsonDoor`: a flat command line cannot say
154
+ # which `a.x=` pairs with which `a.y=`.
155
+ def append(hash, path, value)
156
+ *branches, leaf = path.map(&:to_sym)
157
+ holder = branches[0..-2].reduce(hash) { |node, key| node[key] ||= {} }
158
+ list = holder[branches.last] ||= []
159
+
160
+ list << { leaf => value }
161
+ hash
162
+ end
163
+
164
+ # Adds the words of a `list_of` scalar argument to its Array: a comma-separated value is
165
+ # several elements and a repeated name adds more, each cast to the element type. The
166
+ # runtime refuses a lone scalar for a list, so the door is where every spelling becomes an
167
+ # Array.
168
+ #
169
+ # @param hash [Hash{Symbol => Object}] the arguments built so far
170
+ # @param path [Array<String>] the argument's dotted path
171
+ # @param value [String] the word after `=`
172
+ # @param type [String] the element type
173
+ # @return [Hash{Symbol => Object}] `hash`
174
+ def words(hash, path, value, type)
175
+ *branches, leaf = path.map(&:to_sym)
176
+ holder = branches.reduce(hash) { |node, key| node[key] ||= {} }
177
+ list = holder[leaf] ||= []
178
+ list.concat(value.split(",").map { |word| cast(word.strip, type) })
179
+ hash
180
+ end
181
+
182
+ # Sets one value at a nested path, creating intermediate Hashes and overwriting the leaf.
183
+ def bury(hash, path, value)
184
+ *branches, leaf = path.map(&:to_sym)
185
+ target = branches.reduce(hash) { |node, key| node[key] ||= {} }
186
+ target[leaf] = value
187
+ hash
188
+ end
189
+
190
+ # Words the refusal for an argument the verb does not take, listing what it does.
191
+ def unknown(path, known)
192
+ "no argument #{path.inspect} — this verb takes #{known.sort.join(', ')}"
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,297 @@
1
+ require "json"
2
+ require_relative "cli_door"
3
+ require_relative "command_request"
4
+ require_relative "json_door"
5
+ require_relative "launcher_options"
6
+ require_relative "../projector"
7
+ require_relative "../ports/clock"
8
+
9
+ module Hecks
10
+ module Doors
11
+ # Parses and dispatches a command line against a `Projector::CliProjector` projection.
12
+ # Does no IO: it answers `[text, status]` and leaves printing and exiting to its callers.
13
+ module CliRunner
14
+ # The class an adapter raises when the tool it wraps refuses.
15
+ TOOL_REFUSAL = "Hecks::Adapters::ConsoleCapture::Failure".freeze
16
+
17
+ module_function
18
+
19
+ # Runs one command line against a booted domain and answers the text to print.
20
+ # The booted domain's own chapter is projected, or an attached one its first word names.
21
+ #
22
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted domain
23
+ # @param argv [Array<String>] an optional `ask`, the verb or question name, then
24
+ # `path=value` pairs; `--help`, `-h`, `help` or nothing answers usage
25
+ # @param program [String] how the caller was invoked, echoed in usage and hints
26
+ # @return [Array(String, Integer)] the text and the status: 0 answered, 1 refused or misused
27
+ # @raise [Runtime::WiringError] if a repository or the clock adapter cannot be resolved
28
+ # @raise [Runtime::StaleWrite] if concurrent writers beat the command through every retry
29
+ def call(runtime:, argv:, program: "hecks run")
30
+ plan = resolve(runtime, argv, program)
31
+ return plan[:answer] if plan[:answer]
32
+
33
+ dispatch(runtime, plan[:spec], plan[:name], plan[:rest], plan[:program], plan[:asking],
34
+ bluebook: plan[:bluebook], launcher: plan[:launcher])
35
+ end
36
+
37
+ # Answers a command line that asks only for usage, from the projection alone.
38
+ #
39
+ # Needs a registry and no bound adapter, so a `Runtime::Loader::Described` serves: the
40
+ # help text, the no-argument usage, a verb's `--help` and an unknown verb's hint all come
41
+ # out byte-identical to `call`'s.
42
+ #
43
+ # @param runtime [#registry] a booted domain, or what `Hecks.describe` answers
44
+ # @param argv [Array<String>] as for `call`
45
+ # @param program [String] how the caller was invoked
46
+ # @return [Array(String, Integer), nil] the text and status, or nil when the line would
47
+ # run a verb or question and so needs a booted domain
48
+ def usage(runtime:, argv:, program: "hecks run")
49
+ resolve(runtime, argv, program)[:answer]
50
+ end
51
+
52
+ # Parses a command line against the projection: either the answer it gives without
53
+ # running anything, or the verb it would run.
54
+ #
55
+ # @return [Hash] `{answer: [text, status]}`, or `{spec:, name:, rest:, asking:, program:,
56
+ # bluebook:, launcher:}` for a verb to dispatch
57
+ def resolve(runtime, argv, program)
58
+ bluebook, argv, program = chapter_for(runtime, argv, program)
59
+ launcher = LauncherOptions.settings(runtime, bluebook.name)
60
+ options = LauncherOptions.projection(launcher, program)
61
+ cli = Projector.call(:cli, bluebook: bluebook, options: options)
62
+
63
+ name = argv.first
64
+ return { answer: [cli[:usage], 0] } if name.nil? || %w[--help -h help].include?(name)
65
+
66
+ # `ask` gives questions their own namespace; a chapter may declare a command and query
67
+ # of one name.
68
+ asking = name == "ask"
69
+ argv = argv[1..] if asking
70
+ name = argv.first
71
+ return { answer: [cli[:usage], 1] } if name.nil?
72
+
73
+ # A question answers to its bare name too; `ask` is needed only when a command shares it.
74
+ asking ||= !cli[:names][:command].key?(name) && cli[:names][:question].key?(name)
75
+
76
+ # The alias map lets `create_pizza` and `order.create_pizza` reach the same verb.
77
+ pool = asking ? cli[:questions] : cli[:verbs]
78
+ key = cli[:names][asking ? :question : :command][name]
79
+ spec = pool[key]
80
+ return { answer: [unknown(cli, name, asking, program), 1] } unless spec
81
+
82
+ rest = argv[1..]
83
+ if rest.include?("--help")
84
+ help = Projector.call(:cli, bluebook: bluebook,
85
+ options: options.merge(verb: name, ask: asking))[:usage]
86
+ return { answer: [help, 0] }
87
+ end
88
+
89
+ { spec: spec, name: name, rest: rest, asking: asking, program: program, bluebook: bluebook,
90
+ launcher: launcher }
91
+ end
92
+
93
+ # The chapter a command line speaks to: the booted domain's own, or, when the first word
94
+ # names a chapter its hecksagon attaches or uses as a framework member (`deploy`,
95
+ # `governance`), that chapter, with the word dropped and added to the program name.
96
+ def chapter_for(runtime, argv, program)
97
+ own = runtime.registry.bluebooks.values.first
98
+ attached = Array(runtime.registry.hecksagon(own.name)&.member_chapters)
99
+ target = attached.find { |name| Naming.snake(name) == argv.first }
100
+ return [own, argv, program] unless target
101
+
102
+ [runtime.registry.bluebook(target), argv[1..], "#{program} #{argv.first}"]
103
+ end
104
+
105
+ # Parses one resolved verb's arguments, runs it as a query or command, and turns the
106
+ # outcome, or the domain's refusal, into `[json_or_message, status]`. With `--wait`, a
107
+ # question whose report names a gap (`LauncherOptions.gap_reported?`) answers status 1.
108
+ #
109
+ # @param bluebook [Bluebook::Chapter] the chapter the verb belongs to
110
+ # @param launcher [Hash, nil] the chapter's `launcher` world setting; nil when not opted in
111
+ def dispatch(runtime, spec, name, rest, program, asking, bluebook: nil, launcher: nil)
112
+ rest, wait = LauncherOptions.take_wait(spec, rest) if launcher
113
+ args = stamp_time(runtime, spec, CliDoor.arguments(spec, rest))
114
+
115
+ return answer_query(runtime, spec, args, wait) if spec[:kind] == :query
116
+
117
+ args, minted = LauncherOptions.run_key(runtime, spec, args, launcher)
118
+ # Answers with only this verb's outcome; a full store dump is every record there is.
119
+ request = CommandRequest.normalize(args, receiver: spec[:receiver],
120
+ legacy_receiver: spec[:legacy_receiver])
121
+ handle = runtime.dispatch_flat(spec[:verb], request)
122
+ extra = refused_answer(handle)
123
+ extra = { run: minted }.merge(extra) if minted
124
+ return settled(runtime, spec, handle, bluebook, launcher, extra) if wait
125
+ return [JSON.pretty_generate(answered(handle).merge(extra)), 0] if handle.state.nil?
126
+
127
+ [JSON.pretty_generate({ id: handle.id,
128
+ state: JsonDoor.materialize(handle.state),
129
+ events: handle.events.map(&:name) }.merge(extra)), 0]
130
+ rescue Runtime::NotFound, Runtime::TypeMismatch => e
131
+ # A bad argument and a missing record both need the same next step: read the help.
132
+ ["#{e.message}\n\n #{program} #{'ask ' if asking}#{name} --help", 1]
133
+ rescue *Runtime::DOMAIN_REFUSALS => e
134
+ # The refusal is the chapter's own sentence, verbatim.
135
+ [e.message, 1]
136
+ rescue StandardError => e
137
+ # So is a wrapped tool's, when a question's adapter refuses to answer. Matched by name: the
138
+ # adapters belong to the Hecks chapter, which a client's runtime never loads.
139
+ raise unless e.class.ancestors.map(&:name).include?(TOOL_REFUSAL)
140
+
141
+ [e.message, 1]
142
+ end
143
+
144
+ # Runs a question and answers its rows, or its text when an adapter answered in text.
145
+ #
146
+ # @param wait [Boolean, nil] whether `--wait` was given: a report naming a gap then fails
147
+ # @return [Array(String, Integer)] the answer and the status
148
+ def answer_query(runtime, spec, args, wait)
149
+ rows = runtime.query(spec[:verb], **args)
150
+ text = text_answer(spec, rows)
151
+ [text || JSON.pretty_generate(rows.map { |row| JsonDoor.materialize(row) }),
152
+ wait && LauncherOptions.gap_reported?(text) ? 1 : 0]
153
+ end
154
+
155
+ # The answer `--wait` gives: the record re-read after every reaction has run, with all its
156
+ # events, and a status of 1 when its lifecycle ended in a failure state, a refused reaction
157
+ # blocks the run or a reaction crashed.
158
+ #
159
+ # A refusal blocks unless a sibling reaction delivered another command on the same
160
+ # aggregate (`Runtime::ReactionOutcome`). The settled record is always the text, so a failed
161
+ # run pipes like a passing one; the reason a human reads is a third element.
162
+ #
163
+ # @return [Array(String, Integer)] the JSON and the status, plus (Array(String, Integer,
164
+ # String)) why the run failed when it did
165
+ def settled(runtime, spec, handle, bluebook, launcher, extra)
166
+ why = reactions_failed(handle)
167
+ return finish(JSON.pretty_generate(answered(handle).merge(extra)), why) if handle.state.nil?
168
+
169
+ aggregate = aggregate_of(bluebook, spec)
170
+ state = reread(runtime, bluebook, aggregate, handle) || handle.state
171
+ fqn = "#{bluebook.name}::#{aggregate&.hecks_name}"
172
+ events = runtime.events.select { |event| event.aggregate == fqn && event.id == handle.id }
173
+ answer = { id: handle.id, state: JsonDoor.materialize(state),
174
+ events: (events.empty? ? handle.events : events).map(&:name) }.merge(extra)
175
+ if LauncherOptions.failed?(aggregate, state, launcher)
176
+ field = aggregate.lifecycle.field
177
+ why += ["#{aggregate.hecks_name} ended in the failure state #{state[field.to_sym].to_s.inspect}"]
178
+ end
179
+ finish(JSON.pretty_generate(answer), why)
180
+ end
181
+
182
+ # The answer and its status: 0 when nothing failed, else 1 with the reasons joined.
183
+ def finish(text, reasons)
184
+ reasons.empty? ? [text, 0] : [text, 1, reasons.join("\n")]
185
+ end
186
+
187
+ # What the reactions of the run the handle reports did wrong: each refusal that blocks it
188
+ # and each crash, as one sentence apiece.
189
+ def reactions_failed(handle)
190
+ blocking = handle.respond_to?(:blocking_reactions) ? handle.blocking_reactions : []
191
+ crashed = handle.respond_to?(:reaction_defects) ? handle.reaction_defects : []
192
+ blocking.map { |row| "reaction #{row[:policy]} was refused (#{row[:trigger]}): #{row[:reason]}" } +
193
+ crashed.map { |row| "reaction #{row[:policy]} crashed (#{row[:error_class]}): #{row[:reason]}" }
194
+ end
195
+
196
+ # Whether a reaction the domain refused blocks the run the handle reports.
197
+ def blocked?(handle)
198
+ handle.respond_to?(:blocking_reactions) && !handle.blocking_reactions.empty?
199
+ end
200
+
201
+ # The aggregate a top-level command belongs to; nil for an entity command or a port.
202
+ def aggregate_of(bluebook, spec)
203
+ head = spec[:verb].split("::", 2).last
204
+ return if head.count(".") != 1
205
+
206
+ bluebook.aggregates.find { |aggregate| aggregate.hecks_name == head.split(".").first }
207
+ end
208
+
209
+ # The record as its repository holds it now, or nil when it cannot be read.
210
+ def reread(runtime, bluebook, aggregate, handle)
211
+ return unless aggregate && runtime.respond_to?(:registry)
212
+
213
+ runtime.registry.repository(bluebook.name, aggregate)&.find(handle.id)&.to_h
214
+ end
215
+
216
+ # The text a query answered by a port gave, when that is the whole answer.
217
+ #
218
+ # A query declared `returns Document` (one value object of the single String attribute
219
+ # `text`) answers one document; printing it raw keeps the document (JSON, Markdown,
220
+ # sentences) readable and pipeable instead of quoted inside another JSON document. What a
221
+ # query declares decides it, not how many rows came back: any other query prints JSON,
222
+ # so a script reading it sees an array whether it held one row or two.
223
+ #
224
+ # @param spec [Hash] the question's projected spec
225
+ # @param rows [Array<Hash>] the query's rows
226
+ # @return [String, nil] the text, or nil when the query does not return a Document
227
+ def text_answer(spec, rows)
228
+ return unless spec[:returns] == "Document" && rows.length == 1 && rows.first.keys == [:text]
229
+ return unless rows.first[:text].is_a?(String)
230
+
231
+ rows.first[:text]
232
+ end
233
+
234
+ # Fills in a `now` argument the caller left out, from the clock port; an explicit one wins.
235
+ #
236
+ # Done at the door, not in the runtime: a clock read inside the interpreter would give a
237
+ # replayed corpus step the replay day's time. Matched by the argument's name alone.
238
+ def stamp_time(runtime, spec, args)
239
+ return args unless spec[:arguments].any? { |argument| argument[:path].start_with?("now.") }
240
+ return args if args.key?(:now)
241
+
242
+ args.merge(now: { value: Ports::Clock.now(runtime.registry) })
243
+ end
244
+
245
+ # The reactions one dispatch caused that the domain refused, as `refused_reactions:`.
246
+ #
247
+ # A policy's trigger that a `given` refuses is not the command's own refusal: the command
248
+ # has already persisted. The dispatch result carries them (`Result#refused_reactions`);
249
+ # without this the answer would say nothing of it. A defect (a crash) is shown apart, as
250
+ # `reaction_defects:`, and fails `--wait`.
251
+ #
252
+ # @param handle [Runtime::Dispatcher::Result, Runtime::RemoteDispatcher::Result] the outcome
253
+ # @return [Hash] `refused_reactions:` each with the `policy`, its `trigger` and the `reason`;
254
+ # empty when every reaction was delivered (or a remote host sent no per-step log), plus
255
+ # `reaction_defects:` when one crashed
256
+ def refused_answer(handle)
257
+ refused = handle.respond_to?(:refused_reactions) ? handle.refused_reactions : []
258
+ defects = handle.respond_to?(:reaction_defects) ? handle.reaction_defects : []
259
+ answer = refused.empty? ? {} : { refused_reactions: refused }
260
+ defects.empty? ? answer : answer.merge(reaction_defects: defects)
261
+ end
262
+
263
+ # Shapes a port operation's outcome, which has no state: each event with its full payload.
264
+ #
265
+ # The answer lives only in the payloads, so names alone would drop it. Exit status stays 0
266
+ # for refusal events too, since a healthy no is an answer, not a misuse.
267
+ def answered(handle)
268
+ # A port operation hydrates no instance, so the handle's `id` is nil by design.
269
+ { id: handle.id || handle.events.first&.id,
270
+ events: handle.events.map do |event|
271
+ { name: event.name, payload: JsonDoor.materialize(event.payload) }
272
+ end }
273
+ end
274
+
275
+ # Words the answer for an unknown verb or question, suggesting up to five near names.
276
+ # Ranked by shared prefix, which survives a dropped letter where a substring match would not.
277
+ def unknown(cli, name, asking, program)
278
+ # Both spellings are candidates: `order.create_piza` shares no prefix with `create_pizza`.
279
+ pool = cli[:names][asking ? :question : :command].keys
280
+ near = pool.map { |candidate| [shared_prefix(candidate, name), candidate] }
281
+ .select { |shared, _| shared >= [name.length / 2, 3].max }
282
+ .sort_by { |shared, candidate| [-shared, candidate] }
283
+ .map(&:last)
284
+
285
+ lines = ["no such #{asking ? 'question' : 'verb'}: #{name}"]
286
+ lines += ["", "did you mean:", *near.first(5).map { |candidate| " #{candidate}" }] unless near.empty?
287
+ lines += ["", " #{program}#{' ask' if asking} for the full list"]
288
+ lines.join("\n")
289
+ end
290
+
291
+ def shared_prefix(one, other)
292
+ length = [one.length, other.length].min
293
+ (0...length).find { |index| one[index] != other[index] } || length
294
+ end
295
+ end
296
+ end
297
+ end
@@ -1,7 +1,7 @@
1
1
  require_relative "../runtime/errors"
2
2
 
3
3
  module Hecks
4
- module Facade
4
+ module Doors
5
5
  # Turns an external command request into the dispatcher's `to:`/`with:` envelope.
6
6
  #
7
7
  # aggregate command: { to: "record-id", with: { declared: "facts" } }
@@ -3,7 +3,7 @@ require_relative "../runtime/caller"
3
3
  require_relative "../ports/authorization"
4
4
 
5
5
  module Hecks
6
- module Facade
6
+ module Doors
7
7
  # A record in hand: what `Pizza.create_pizza!(...)` and `Pizza.find(id)`
8
8
  # give back. One shared class per aggregate; verbs are per-handle singleton methods.
9
9
  #
@@ -59,7 +59,7 @@ module Hecks
59
59
  # Refreshes this handle's state from the repository, picking up writes made through
60
60
  # another handle or door. Keeps the current state when the record is not found.
61
61
  #
62
- # @return [Facade::Handle] this handle, so the call chains
62
+ # @return [Doors::Handle] this handle, so the call chains
63
63
  # @raise [Runtime::WiringError] if the aggregate's persistence bind cannot be
64
64
  # resolved into a repository
65
65
  def reload
@@ -6,7 +6,7 @@ require_relative "../runtime/errors"
6
6
  require_relative "../runtime/value"
7
7
 
8
8
  module Hecks
9
- module Facade
9
+ module Doors
10
10
  # The JSON door: translates URL segments and parsed JSON bodies to and from the facade.
11
11
  # No HTTP lives here; every miss raises `Runtime::NotFound`, distinguished by message.
12
12
  module JsonDoor
@@ -23,7 +23,7 @@ module Hecks
23
23
  # @param name [String, Symbol] the aggregate's declared name, such as `"Customer"`
24
24
  # @return [Module] the aggregate door installed at `domain::name`
25
25
  # @raise [Runtime::NotFound] if the chapter or its aggregate is not declared
26
- # @raise [NameError] if the aggregate has no facade constant (`install_facade: false`)
26
+ # @raise [NameError] if the aggregate has no facade constant (`install_doors: false`)
27
27
  def aggregate(dispatcher, domain, name)
28
28
  ir = dispatcher.registry.bluebook(domain)&.aggregate(name)
29
29
  raise Runtime::NotFound, "#{domain} declares no aggregate named #{name.inspect}" unless ir
@@ -67,7 +67,7 @@ module Hecks
67
67
  #
68
68
  # @param klass [Module] an aggregate door, as `aggregate` returns
69
69
  # @param id [String] the record's identity, as the URL carried it
70
- # @return [Facade::Handle] the record in hand
70
+ # @return [Doors::Handle] the record in hand
71
71
  # @raise [Runtime::NotFound] if the repository holds no record with that id
72
72
  def find!(klass, id)
73
73
  klass.find(id) or raise Runtime::NotFound, "no #{klass.ir.hecks_name} found for id #{id.inspect}"
@@ -93,7 +93,7 @@ module Hecks
93
93
  # Delegates to `Runtime::Value.materialize`; the only addition is `#to_h` on a
94
94
  # `Handle`, which `materialize` does not recognize.
95
95
  #
96
- # @param value [Facade::Handle, Runtime::Value, Hash, Array, Object] what to unwrap;
96
+ # @param value [Doors::Handle, Runtime::Value, Hash, Array, Object] what to unwrap;
97
97
  # a `Handle` is read through its `to_h`, so its `:id` comes along
98
98
  # @return [Hash, Array, Object] the same data with every `Runtime::Value` replaced
99
99
  # by a Hash of its fields; any other value is returned as it came