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
@@ -13,8 +13,10 @@ module Hecks
13
13
  verify_world_defaults!
14
14
  verify_singleton_port_answers!
15
15
  refuse_cross_package_bluebook_merge!
16
+ refuse_reserved_chapter_names!
16
17
  refuse_membership_without_identity!
17
18
  refuse_unresolved_port_operations!
19
+ refuse_unanswerable_queries!
18
20
 
19
21
  @hecksagons.each_value do |hexagon|
20
22
  refuse_ungoverned_roles!(hexagon)
@@ -95,6 +97,13 @@ module Hecks
95
97
  # each bind above; a singleton port is never bound to an aggregate at all.
96
98
  PER_AGGREGATE_PORTS = %w[persistence projection loading].freeze
97
99
 
100
+ # The directory holding the gem's own Hecks chapter, the only place a chapter named
101
+ # `Hecks` may be declared from.
102
+ GEM_CHAPTER_DIR = File.expand_path("../../hecks", __dir__).freeze
103
+
104
+ # The chapter names only the gem itself may declare (ADR 0080, section 9).
105
+ RESERVED_CHAPTER_NAMES = %w[Hecks].freeze
106
+
98
107
  # Checks every singleton port with exactly one wired adapter against its own
99
108
  # declared `answers` methods.
100
109
  def verify_singleton_port_answers!
@@ -180,14 +189,14 @@ module Hecks
180
189
  end
181
190
  end
182
191
 
183
- # `uses_framework` loads a bounded context; the consumer must declare the sibling
184
- # hecksagon that is its ACL (Governance/Identity/Privacy already do).
192
+ # `uses_framework` and `attaches` load a bounded context; the consumer must declare the
193
+ # sibling hecksagon that is its ACL (Governance/Identity/Privacy already do).
185
194
  def refuse_unwired_framework_members!(hexagon)
186
- Array(hexagon.framework_members).each do |member|
195
+ hexagon.member_chapters.each do |member|
187
196
  next if hecksagon(member)
188
197
 
189
198
  raise WiringError,
190
- "#{hexagon.domain} attaches framework member #{member.inspect} " \
199
+ "#{hexagon.domain} attaches #{member.inspect} " \
191
200
  "(bounded context) but never declared Hecks.hecksagon " \
192
201
  "#{member.inspect} — put that sibling (and any `translates` " \
193
202
  "ACL) in context_map.hecksagon; same-name blocks merge, " \
@@ -229,6 +238,130 @@ module Hecks
229
238
  end
230
239
  end
231
240
 
241
+ # A query has exactly one answer path. It is derivable when the bluebook says which of the
242
+ # aggregate's own records it wants (a where, order_by, limit or offset, or no arguments at
243
+ # all: the plain list) and returns no value object of its own; it is bound when it
244
+ # `returns` a value object and the hecksagon names the port whose adapter answers it. None
245
+ # of the two is "no answer path"; both is "two answer paths". A bound query also needs an
246
+ # adapter behind its port that implements it, found here so it fails at boot rather than
247
+ # on the first ask.
248
+ def refuse_unanswerable_queries!
249
+ @bluebooks.each_value do |chapter|
250
+ chapter.aggregates.each do |aggregate|
251
+ refuse_misbound_queries!(chapter, aggregate)
252
+
253
+ aggregate.queries.each { |query| refuse_answer_paths!(chapter, aggregate, query) }
254
+ aggregate.entities.each do |entity|
255
+ entity.queries.each { |query| refuse_entity_answer_path!(chapter, aggregate, entity, query) }
256
+ end
257
+ end
258
+ end
259
+ end
260
+
261
+ # Counts the query's answer paths (the bindings, plus the derived answer when it exists)
262
+ # and refuses any count but one, then checks a bound query's shape and adapter.
263
+ def refuse_answer_paths!(chapter, aggregate, query)
264
+ where = "#{chapter.name}::#{aggregate.hecks_name}.#{query.hecks_name}"
265
+ ports = aggregate.query_bindings(query.hecks_name)
266
+ paths = ports.size + (derivable?(query) ? 1 : 0)
267
+
268
+ if paths > 1
269
+ raise WiringError, "#{where} has two answer paths: #{answer_path_names(query, ports)} — " \
270
+ "a query is answered from records or from outside, not both"
271
+ end
272
+ return check_bound_query!(chapter, aggregate, query, ports.first) if ports.any?
273
+ return if paths == 1
274
+
275
+ raise WiringError, no_answer_path_message(chapter, aggregate, query, where)
276
+ end
277
+
278
+ # A query that returns nothing and says something about which records it wants, or wants
279
+ # all of them. One that takes arguments but filters, orders and bounds nothing reads none
280
+ # of them, so it derives no answer.
281
+ def derivable?(query)
282
+ return false if query.returns
283
+
284
+ filters_records?(query) || query.attributes.empty?
285
+ end
286
+
287
+ def answer_path_names(query, ports)
288
+ names = ports.map { |port| "the #{port.name} port" }
289
+ names.unshift("its records") if derivable?(query)
290
+ names.join(" and ")
291
+ end
292
+
293
+ def no_answer_path_message(chapter, aggregate, query, where)
294
+ if query.returns
295
+ "#{where} has no answer path: it returns #{query.returns}, which only an adapter can " \
296
+ "answer, and no hecksagon port binds it. Bind it: `#{chapter.name}::#{aggregate.hecks_name}" \
297
+ ".port \"Port\" do answers_query \"#{query.hecks_name}\" end`."
298
+ else
299
+ "#{where} has no answer path: it declares no where and returns nothing, and no hecksagon " \
300
+ "binds it — its arguments (#{query.attributes.map(&:name).join(', ')}) select nothing. " \
301
+ "Add a where, or declare what it returns and bind it in the hecksagon."
302
+ end
303
+ end
304
+
305
+ # Checks what a binding needs to be answerable: a returned value object the aggregate
306
+ # declares, and one adapter behind the port that implements the query's method.
307
+ def check_bound_query!(chapter, aggregate, query, port)
308
+ where = "#{chapter.name}::#{aggregate.hecks_name}.#{query.hecks_name}"
309
+ unless query.returns
310
+ raise WiringError, "#{where} is bound to the #{port.name} port but returns nothing — " \
311
+ "declare the value object its answer takes with `returns`"
312
+ end
313
+ if filters_records?(query)
314
+ raise WiringError, "#{where} has two answer paths: it is bound to the #{port.name} port but " \
315
+ "also declares where, order_by or limit over stored records"
316
+ end
317
+ if query.authorization
318
+ raise WiringError, "#{where} is bound to the #{port.name} port but declares authorize — " \
319
+ "an outside answer is never tenant-scoped or authorized, so drop the " \
320
+ "authorize or answer the query from records"
321
+ end
322
+ unless Value.value_object_for(aggregate, query.returns_name)
323
+ raise WiringError, "#{where} returns #{query.returns_name}, but #{aggregate.hecks_name} " \
324
+ "declares no such value object"
325
+ end
326
+
327
+ klass = AdapterLookup.adapter_class(self, port.name, asked: where)
328
+ AdapterLookup.check_answers!(klass, port.name, Naming.snake(query.hecks_name), query, asked: where)
329
+ end
330
+
331
+ # An entity's query reads the elements of its aggregate's list, and nothing outside can
332
+ # be bound to one, so declaring a value object to return leaves it no answer.
333
+ def refuse_entity_answer_path!(chapter, aggregate, entity, query)
334
+ where = "#{chapter.name}::#{aggregate.hecks_name}.#{entity.hecks_name}.#{query.hecks_name}"
335
+ if query.returns
336
+ raise WiringError, "#{where} has no answer path: it returns #{query.returns}, but " \
337
+ "only an aggregate's queries can be bound to a port"
338
+ end
339
+ return if derivable?(query)
340
+
341
+ raise WiringError, "#{where} has no answer path: it declares no where and returns nothing — its " \
342
+ "arguments (#{query.attributes.map(&:name).join(', ')}) select nothing"
343
+ end
344
+
345
+ # Refuses a binding that names a query its aggregate does not declare, or names one twice
346
+ # on a single port (the DSL already refuses that, so this guards a hand-built port).
347
+ def refuse_misbound_queries!(chapter, aggregate)
348
+ aggregate.ports.each do |port|
349
+ names = port.answered_queries.map(&:name)
350
+ names.each do |name|
351
+ where = "#{chapter.name}::#{aggregate.hecks_name}.#{name}"
352
+ unless aggregate.query(name)
353
+ raise WiringError, "the #{port.name} port binds #{where}, which the aggregate does not declare"
354
+ end
355
+ raise WiringError, "#{where} has two answer paths: the #{port.name} port twice" if names.count(name) > 1
356
+ end
357
+ end
358
+ end
359
+
360
+ # A query that says anything about which stored records it wants.
361
+ def filters_records?(query)
362
+ !(query.wheres.empty? && query.order_by.nil? && query.limit.nil? && query.offset.nil?)
363
+ end
364
+
232
365
  def port_operation_declared?(chapter, verb)
233
366
  aggregate_name, port_name, operation_name = verb.split(".", 3)
234
367
  ports = chapter.aggregate(aggregate_name)&.ports || []
@@ -263,6 +396,32 @@ module Hecks
263
396
  bluebook_ir.aggregates.flat_map { |aggregate| aggregate.commands + aggregate.entities.flat_map(&:commands) }
264
397
  end
265
398
 
399
+ # Refuses a chapter whose name is reserved unless every file that declared it sits under
400
+ # the gem's own chapter directory. A rename is the only fix, so the message says so.
401
+ #
402
+ # @raise [WiringError] naming the reserved word and the offending file(s)
403
+ def refuse_reserved_chapter_names!
404
+ RESERVED_CHAPTER_NAMES.each do |reserved|
405
+ next unless @bluebooks.key?(reserved)
406
+
407
+ sources = Array(@bluebook_sources[reserved]).compact
408
+ foreign = sources.reject { |path| gem_chapter_source?(path) }
409
+ next if foreign.empty? && !sources.empty?
410
+
411
+ where = foreign.empty? ? "" : " (declared in #{foreign.join(', ')})"
412
+ raise WiringError,
413
+ "a chapter named #{reserved.inspect} is refused: #{reserved.inspect} is a reserved " \
414
+ "word, the name of the gem's own chapter, and only the gem may declare it#{where}. " \
415
+ "Rename the chapter (for example to your app's name) in its " \
416
+ "`Hecks.bluebook #{reserved.inspect}` line and in its hecksagon."
417
+ end
418
+ end
419
+
420
+ # Whether `path` is a file of the gem's own chapter directory.
421
+ def gem_chapter_source?(path)
422
+ File.expand_path(path.to_s).start_with?("#{GEM_CHAPTER_DIR}/")
423
+ end
424
+
266
425
  # Two packages can share a chapter name by coincidence (found live: a stale
267
426
  # vendor/hecksagain fork of Governance/Identity/Deploy, still on 4 apps' load
268
427
  # paths). Intentional accumulation (several files declaring the same chapter on
@@ -17,6 +17,9 @@ module Hecks
17
17
  include SagaPersistence
18
18
  include WorldDefaults
19
19
 
20
+ # The thread-local key under which open reaction collections are kept.
21
+ REACTION_SINKS = :hecks_reaction_sinks
22
+
20
23
  attr_reader :root, :bluebooks, :hecksagons, :ports, :adapters, :worlds, :event_log,
21
24
  :reaction_log, :saga_log, :saga_instances, :translations, :saga_mutex,
22
25
  :saga_dispatch_log, :policy_dispatch_log, :bluebook_sources,
@@ -37,6 +40,7 @@ module Hecks
37
40
  @translations = []
38
41
  @event_log = []
39
42
  @reaction_log = []
43
+ @reaction_events = {}.compare_by_identity
40
44
  @saga_log = []
41
45
  # Recorded at hecksagon-build time (AggregateDoor#mark_sensitive); Loader.boot's
42
46
  # post-dispatch step turns each entry into a real Privacy::Marking.Mark, idempotently.
@@ -102,6 +106,17 @@ module Hecks
102
106
  # @return [Bluebook::Chapter] `item`, unchanged
103
107
  def add_bluebook(item) = @bluebooks[item.name] = item
104
108
 
109
+ # Drops a chapter and the open builder and source record that accumulated it, so the
110
+ # chapter can be loaded again from nothing.
111
+ #
112
+ # @param name [String, Symbol] the chapter name to forget
113
+ # @return [void]
114
+ def forget_chapter(name)
115
+ @bluebooks.delete(name.to_s)
116
+ @bluebook_builders.delete(name.to_s)
117
+ @bluebook_sources.delete(name.to_s)
118
+ end
119
+
105
120
  # Tracks which .bluebook file(s) contributed to a chapter name (a boot-time loading
106
121
  # fact, never Rust-mirrored) so refuse_cross_package_bluebook_merge! can catch two
107
122
  # unrelated packages accumulating into the same name by coincidence.
@@ -233,7 +248,7 @@ module Hecks
233
248
  # @return [Bluebook::Chapter, nil] the chapter that answers `domain`'s role
234
249
  # checks, or nil if none does
235
250
  def authorization_provider_for(domain)
236
- names = [domain.to_s, *Array(hecksagon(domain)&.framework_members)]
251
+ names = [domain.to_s, *Array(hecksagon(domain)&.member_chapters)]
237
252
  names.filter_map { |name| bluebook(name) }
238
253
  .find { |chapter| chapter.provides?(Bluebook::Capabilities::AUTHORIZATION) }
239
254
  end
@@ -254,7 +269,7 @@ module Hecks
254
269
  # @return [Bluebook::Chapter, nil] the chapter that answers `domain`'s identity
255
270
  # questions, or nil if none does
256
271
  def identity_provider_for(domain)
257
- names = [domain.to_s, *Array(hecksagon(domain)&.framework_members)]
272
+ names = [domain.to_s, *Array(hecksagon(domain)&.member_chapters)]
258
273
  attached = names.filter_map { |name| bluebook(name) }
259
274
  .find { |chapter| chapter.provides?(Bluebook::Capabilities::IDENTITY) }
260
275
  return attached if attached
@@ -350,7 +365,7 @@ module Hecks
350
365
  def vendored_provider_for(domain, capability)
351
366
  hexagon = hecksagon(domain)
352
367
  vendored = Array(hexagon&.vendored_bluebooks).map { |name| Naming.pascal(name) }
353
- names = [domain.to_s, *Array(hexagon&.framework_members), *vendored]
368
+ names = [domain.to_s, *Array(hexagon&.member_chapters), *vendored]
354
369
  attached = names.filter_map { |name| bluebook(name) }
355
370
  .find { |chapter| chapter.provides?(capability) }
356
371
  return attached if attached
@@ -373,6 +388,43 @@ module Hecks
373
388
  @repositories[[domain.to_s, aggregate.hecks_name]] ||= Ports::Persistence.repository(self, domain, aggregate)
374
389
  end
375
390
 
391
+ # Records one policy reaction on the shared log, and on every collection open on this
392
+ # thread (see `collecting_reactions`).
393
+ #
394
+ # The event that triggered a reaction is kept beside the log, not in the record: the record
395
+ # is the byte-for-byte shape the Rust kernel also writes.
396
+ #
397
+ # @param record [Hash{Symbol => Object}] the reaction's outcome
398
+ # @param event [String, Integer, nil] identity of the event instance the reaction answered
399
+ # @return [void]
400
+ # rubocop:disable-next Hecks/ThreadSharedIvarMutation
401
+ def log_reaction(record, event: nil)
402
+ @reaction_log << record
403
+ @reaction_events[record] = event if event
404
+ Array(Thread.current[REACTION_SINKS]).each { |sink| sink << record }
405
+ end
406
+
407
+ # The identity of the event instance a logged reaction answered.
408
+ #
409
+ # @param record [Hash{Symbol => Object}] a record `log_reaction` was given
410
+ # @return [String, Integer, nil] nil for a record logged without its event
411
+ def reaction_event(record) = @reaction_events[record]
412
+
413
+ # Collects the reactions logged on this thread while the block runs, so one dispatch
414
+ # reads exactly its own even when other threads dispatch on the same registry. Nested
415
+ # dispatches (a reaction that dispatches) each collect, and the outer one sees theirs too.
416
+ #
417
+ # @yield [Array<Hash>] the collection, filled as reactions are logged
418
+ # @return [Object] what the block returns
419
+ def collecting_reactions
420
+ sinks = (Thread.current[REACTION_SINKS] ||= [])
421
+ sink = []
422
+ sinks << sink
423
+ yield sink
424
+ ensure
425
+ sinks&.delete_if { |candidate| candidate.equal?(sink) }
426
+ end
427
+
376
428
  # Clears everything a dispatch produced (logs, saga instances, repositories); leaves
377
429
  # what booting the files produced (bluebooks, hecksagons, ports, adapters, worlds,
378
430
  # resolved eras) untouched.
@@ -386,6 +438,7 @@ module Hecks
386
438
  def reset_runtime_state!
387
439
  @event_log.clear
388
440
  @reaction_log.clear
441
+ @reaction_events.clear
389
442
  @saga_log.clear
390
443
  @saga_dispatch_log.clear
391
444
  @policy_dispatch_log.clear
@@ -469,6 +522,7 @@ module Hecks
469
522
  subscriptions: (base.subscriptions + overlay.subscriptions).uniq,
470
523
  framework_members: (base.framework_members + overlay.framework_members).uniq,
471
524
  vendored_bluebooks: (base.vendored_bluebooks + overlay.vendored_bluebooks).uniq,
525
+ attached_chapters: (base.attached_chapters + overlay.attached_chapters).uniq,
472
526
  bounded: base.bounded? || overlay.bounded?,
473
527
  translates: (base.translates + overlay.translates).uniq
474
528
  )
@@ -16,7 +16,22 @@ module Hecks
16
16
  # already Lambda-backed. Writes cannot: givens and constraints must run in Rust,
17
17
  # not be pre-checked here against incomplete local state.
18
18
  class RemoteDispatcher
19
- Result = Struct.new(:verb, :instance, :events, keyword_init: true) do
19
+ Result = Struct.new(:verb, :instance, :events, :refused_reactions, :blocking_reactions,
20
+ :reaction_defects, keyword_init: true) do
21
+ # Lists the policy reactions this dispatch caused that the routed runtime refused.
22
+ #
23
+ # @return [Array<Hash{Symbol => Object}>] one `{ policy:, trigger:, reason: }` per refused
24
+ # reaction, oldest first; empty when every reaction was delivered
25
+ def refused_reactions = self[:refused_reactions] || []
26
+
27
+ # Lists the refused reactions that block the run, as opposed to a benign non-match.
28
+ #
29
+ # @return [Array<Hash{Symbol => Object}>] the subset of `refused_reactions` that blocks
30
+ def blocking_reactions = self[:blocking_reactions] || []
31
+
32
+ # @return [Array<Hash{Symbol => Object}>] the reactions the host reported as crashed
33
+ def reaction_defects = self[:reaction_defects] || []
34
+
20
35
  # The settled record's identity.
21
36
  #
22
37
  # @return [String]
@@ -96,7 +111,10 @@ module Hecks
96
111
  instance = Instance.new(aggregate: aggregate, id: mutation["id"],
97
112
  state: JSON.parse(JSON.generate(mutation["state"]), symbolize_names: true))
98
113
 
99
- Result.new(verb: verb, instance: instance, events: step_events(response))
114
+ Result.new(verb: verb, instance: instance, events: step_events(response),
115
+ refused_reactions: refused_reactions_of(response),
116
+ blocking_reactions: ReactionOutcome.blocking(step_reactions(response)),
117
+ reaction_defects: ReactionOutcome.defects(step_reactions(response)))
100
118
  end
101
119
 
102
120
  # Delegates to the local `Dispatcher`; see `Dispatcher#query`.
@@ -116,6 +134,17 @@ module Hecks
116
134
 
117
135
  private
118
136
 
137
+ # The newest step's reactions the remote runtime refused, from its `reactions_per_step`
138
+ # log (the whole-run `reactions` would also carry the replayed history's).
139
+ def refused_reactions_of(response)
140
+ step_reactions(response)
141
+ .select { |entry| entry["delivered"] == false }
142
+ .map { |entry| { policy: entry["policy"], trigger: entry["trigger"], reason: entry["reason"] } }
143
+ end
144
+
145
+ # Every reaction the newest step caused, delivered or not.
146
+ def step_reactions(response) = Array(response.fetch("reactions_per_step", []).last)
147
+
119
148
  def step_events(response)
120
149
  response.fetch("events", []).map { |e| build_event(e) }
121
150
  end
@@ -12,7 +12,7 @@ module Hecks
12
12
  # as `Value.for`, `Value.build`, and so on.
13
13
  module Coercion
14
14
  # The complete set of attribute value shapes. Mirrored by hand into a
15
- # generated Rust enum (bin/project_kernel_capabilities) — adding a shape
15
+ # generated Rust enum (hecks project_kernel_capabilities) — adding a shape
16
16
  # here without a matching Rust file leaves the kernel unaware of it.
17
17
  SHAPES = %i[scalar list optional composite].freeze
18
18
 
@@ -309,6 +309,7 @@ module Hecks
309
309
  check_required_fields(value_object, fields)
310
310
  admit_member(value_object, fields)
311
311
  check_admitted(value_object, fields)
312
+ check_list_shapes(value_object, fields)
312
313
  check_numeric_fields(value_object, fields)
313
314
  check_scalar_shapes(value_object, fields)
314
315
  check_patterns(value_object, fields)
@@ -453,6 +454,26 @@ module Hecks
453
454
  known_by: known_by(attribute))
454
455
  end
455
456
 
457
+ # A list argument is an Array whatever its element type, so a lone scalar offered for
458
+ # it is refused; nil stays legitimate, as for any optional argument. The single-element
459
+ # form belongs to the `append`/`remove` effects, which take one element and are
460
+ # coerced against the aggregate's own list, never through this gate.
461
+ #
462
+ # @param command [#hecks_name] what `attribute` is declared on, named in a refusal
463
+ # @param attribute [Bluebook::Attribute] the attribute to check; a no-op unless list-typed
464
+ # (a `has_many` reference list has its own refusal)
465
+ # @param value [Object] the offered value
466
+ # @return [void]
467
+ # @raise [Runtime::TypeMismatch] if `value` is neither nil nor an Array
468
+ def refuse_scalar_list(command, attribute, value)
469
+ return unless attribute.list? && !attribute.reference? && !value.nil? && !value.is_a?(Array)
470
+
471
+ raise TypeMismatch, RefusalWording.render_site(
472
+ "TypeMismatch", "numeric_field", type: command.hecks_name, field: attribute.name,
473
+ expected: "list_of(#{attribute.type})", offered: Rendering.describe(value)
474
+ )
475
+ end
476
+
456
477
  # "an object" for the Hash/Value shape; `Rendering.describe` otherwise —
457
478
  # the same rendering every other TypeMismatch in this file uses.
458
479
  #
@@ -518,7 +539,7 @@ module Hecks
518
539
 
519
540
  # Converts a derived numeric identity string back before `check_numeric_fields` runs.
520
541
  private def coerce_identifier(field, identifier)
521
- return identifier unless identifier.is_a?(String) && NUMERIC.key?(field.type.to_s)
542
+ return identifier unless identifier.is_a?(String) && FieldChecks::NUMERIC.key?(field.type.to_s)
522
543
 
523
544
  case field.type.to_s
524
545
  when "Integer" then Integer(identifier)
@@ -537,139 +558,6 @@ module Hecks
537
558
  def canonical_fields(fields)
538
559
  JSON.generate(fields.sort_by { |name, _| name.to_s }.to_h)
539
560
  end
540
-
541
- # C3.8 boundary check — refuses a mistyped argument before it breaks a predicate.
542
- private def check_bare_primitive(owner, attribute, value)
543
- type = attribute.type.to_s
544
- expected = NUMERIC[type]
545
- mistyped = if expected
546
- !value.is_a?(expected)
547
- elsif NON_NUMERIC_SCALARS.include?(type)
548
- COMPOSITE_SHAPES.any? { |shape| value.is_a?(shape) }
549
- else
550
- false
551
- end
552
- if mistyped
553
- raise TypeMismatch,
554
- RefusalWording.render_site("TypeMismatch", "numeric_field",
555
- type: owner.hecks_name, field: attribute.name,
556
- expected: type, offered: Rendering.describe(value))
557
- end
558
-
559
- check_numeric_bounds(owner.hecks_name, attribute.name, value)
560
- end
561
-
562
- # C3.3/C3.4 — value bounds enforced at every boundary: an Integer must fit
563
- # signed 64 bits, a Float must be finite.
564
- INT64_RANGE = (-(2**63))..((2**63) - 1)
565
-
566
- private def check_numeric_bounds(type_name, field_name, given)
567
- if given.is_a?(Integer) && !INT64_RANGE.cover?(given)
568
- raise TypeMismatch,
569
- RefusalWording.render_site("TypeMismatch", "integer_range",
570
- type: type_name, field: field_name, offered: Rendering.describe(given))
571
- end
572
- return unless given.is_a?(Float) && !given.finite?
573
-
574
- raise TypeMismatch,
575
- RefusalWording.render_site("TypeMismatch", "non_finite_field",
576
- type: type_name, field: field_name, offered: Rendering.describe(given))
577
- end
578
-
579
- # A value object refuses an undeclared key the same way a command's own
580
- # payload does, checked before any other field-content check.
581
- private def check_unknown_fields(value_object, fields)
582
- known = value_object.attributes.map { |attribute| attribute.name.to_sym }
583
- unknown = (fields.keys.map(&:to_sym) - known).sort
584
- return if unknown.empty?
585
-
586
- declared = value_object.attributes.map(&:name)
587
- raise UnknownArgument,
588
- RefusalWording.render_site("UnknownArgument", "unknown_args",
589
- command: value_object.hecks_name, unknown: unknown,
590
- declared: declared)
591
- end
592
-
593
- # C3.7 — every non-optional, non-list field must arrive (or construction
594
- # refuses); a `default:` has already been filled in by `apply_defaults`.
595
- private def check_required_fields(value_object, fields)
596
- value_object.attributes.each do |attribute|
597
- next if attribute.optional? || attribute.list?
598
- next unless fields[attribute.name].nil?
599
-
600
- raise TypeMismatch,
601
- RefusalWording.render_site("TypeMismatch", "numeric_field",
602
- type: value_object.hecks_name, field: attribute.name,
603
- expected: attribute.type, offered: "nil")
604
- end
605
- end
606
-
607
- # Checked before invariants, because an invariant reading a mistyped field
608
- # is exactly the thing that would otherwise explode.
609
- NUMERIC = { "Integer" => Integer, "Float" => Numeric }.freeze
610
- private def check_numeric_fields(value_object, fields)
611
- value_object.attributes.each do |attribute|
612
- expected = NUMERIC[attribute.type.to_s]
613
- next unless expected
614
-
615
- given = fields[attribute.name]
616
- next if given.nil?
617
-
618
- unless given.is_a?(expected)
619
- raise TypeMismatch,
620
- RefusalWording.render_site("TypeMismatch", "numeric_field",
621
- type: value_object.hecks_name, field: attribute.name,
622
- expected: attribute.type, offered: Rendering.describe(given))
623
- end
624
-
625
- # `is_a?(expected)` alone waves NaN/Infinity through — both are real
626
- # Floats. `-0.0` is deliberately left unchecked: finite and legitimate.
627
- check_numeric_bounds(value_object.hecks_name, attribute.name, given)
628
- end
629
- end
630
-
631
- # A scalar field (String, or a boolean) must not arrive as a composite
632
- # (Array/Hash) standing in for a leaf value. A String field additionally
633
- # must not arrive as any other scalar, except inside `judge_bootstrapping?`.
634
- COMPOSITE_SHAPES = [Array, ::Hash].freeze
635
- NON_NUMERIC_SCALARS = %w[String TrueClass FalseClass].freeze
636
- private def check_scalar_shapes(value_object, fields)
637
- value_object.attributes.each do |attribute|
638
- type = attribute.type.to_s
639
- next unless NON_NUMERIC_SCALARS.include?(type)
640
-
641
- given = fields[attribute.name]
642
- next if given.nil?
643
-
644
- composite = COMPOSITE_SHAPES.any? { |shape| given.is_a?(shape) }
645
- non_string_scalar = type == "String" && !composite && !given.is_a?(String) && !judge_bootstrapping?
646
- next unless composite || non_string_scalar
647
-
648
- raise TypeMismatch,
649
- RefusalWording.render_site("TypeMismatch", "numeric_field",
650
- type: value_object.hecks_name, field: attribute.name,
651
- expected: attribute.type, offered: Rendering.describe(given))
652
- end
653
- end
654
-
655
- # A field declared with a pattern must match it, refused as a TypeMismatch
656
- # rather than surfacing later as a broken predicate. The pattern itself is
657
- # already vetted by PatternSubset when the bluebook is declared.
658
- private def check_patterns(value_object, fields)
659
- value_object.attributes.each do |attribute|
660
- pattern = attribute.pattern
661
- next unless pattern
662
-
663
- given = fields[attribute.name]
664
- next if given.nil?
665
- next if given.is_a?(String) && Regexp.new(pattern).match?(given)
666
-
667
- raise TypeMismatch,
668
- RefusalWording.render_site("TypeMismatch", "pattern_mismatch",
669
- type: value_object.hecks_name, field: attribute.name,
670
- pattern: pattern, offered: Rendering.describe(given))
671
- end
672
- end
673
561
  end
674
562
  end
675
563
  end