hecks 2.9.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (600) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +51 -6
  3. data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
  4. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  5. data/lib/hecks/adapters/driven/lambda.rb +1 -1
  6. data/lib/hecks/adapters/driven/memory.rb +16 -3
  7. data/lib/hecks/adapters/driven/postgres/reconnect.rb +6 -2
  8. data/lib/hecks/adapters/driven/postgres/shared_connection.rb +180 -0
  9. data/lib/hecks/adapters/driven/postgres.rb +4 -2
  10. data/lib/hecks/adapters/driven/prism.rb +41 -8
  11. data/lib/hecks/adapters/driven/tenant_provisioner.rb +73 -16
  12. data/lib/hecks/behaviors/expectations.rb +1 -1
  13. data/lib/hecks/behaviors.rb +1 -1
  14. data/lib/hecks/bench/cli.rb +62 -0
  15. data/lib/hecks/bench/environment.rb +71 -0
  16. data/lib/hecks/bench/postgres_probe.rb +30 -0
  17. data/lib/hecks/bench/report.rb +76 -0
  18. data/lib/hecks/bench/ruby_runner.rb +93 -0
  19. data/lib/hecks/bench/run.rb +55 -0
  20. data/lib/hecks/bench/rust_runner.rb +101 -0
  21. data/lib/hecks/bench/stats.rb +46 -0
  22. data/lib/hecks/bench/suite.rb +98 -0
  23. data/lib/hecks/bench/workload.rb +89 -0
  24. data/lib/hecks/bench.rb +17 -0
  25. data/lib/hecks/bluebook/assembly/contracts.rb +2 -0
  26. data/lib/hecks/bluebook/attribute.rb +1 -1
  27. data/lib/hecks/bluebook/behaviour/aggregate.rb +30 -1
  28. data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -0
  29. data/lib/hecks/bluebook/chapter.rb +6 -2
  30. data/lib/hecks/bluebook/domain_port.rb +36 -5
  31. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +10 -0
  32. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +8 -0
  33. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -1
  34. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +31 -3
  35. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +122 -3
  36. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +1 -1
  37. data/lib/hecks/bluebook/dsl/query_builder.rb +27 -1
  38. data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -1
  39. data/lib/hecks/bluebook/dsl/world_builder.rb +29 -2
  40. data/lib/hecks/bluebook/expression/canonical_form.rb +1 -1
  41. data/lib/hecks/bluebook/expression/resolver.rb +3 -2
  42. data/lib/hecks/bluebook/hexagon.rb +13 -5
  43. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -0
  44. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +4 -1
  45. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +246 -0
  46. data/lib/hecks/bluebook/meta_validator.rb +9 -4
  47. data/lib/hecks/bluebook/model_check.rb +23 -0
  48. data/lib/hecks/bluebook/pattern_subset.rb +38 -0
  49. data/lib/hecks/bluebook/query.rb +20 -5
  50. data/lib/hecks/bluebook/smoke_test.rb +1 -1
  51. data/lib/hecks/canonical_json.rb +30 -0
  52. data/lib/hecks/chapters.rb +102 -0
  53. data/lib/hecks/cli/behaviors.rb +61 -0
  54. data/lib/hecks/cli/canonicalise.rb +25 -0
  55. data/lib/hecks/cli/check_engine_agreement.rb +32 -0
  56. data/lib/hecks/cli/check_era.rb +88 -0
  57. data/lib/hecks/cli/console.rb +66 -0
  58. data/lib/hecks/cli/deploy_template_diff.rb +40 -0
  59. data/lib/hecks/cli/doc_coverage.rb +32 -0
  60. data/lib/hecks/cli/document.rb +2 -2
  61. data/lib/hecks/cli/expression_projection.rb +41 -0
  62. data/lib/hecks/cli/follow.rb +88 -0
  63. data/lib/hecks/cli/history.rb +44 -0
  64. data/lib/hecks/cli/ir.rb +1 -1
  65. data/lib/hecks/cli/mcp.rb +3 -3
  66. data/lib/hecks/cli/model_check.rb +7 -4
  67. data/lib/hecks/cli/pattern_cases.rb +47 -0
  68. data/lib/hecks/cli/present.rb +39 -0
  69. data/lib/hecks/cli/project_cli.rb +224 -32
  70. data/lib/hecks/cli/project_diagrams.rb +4 -3
  71. data/lib/hecks/cli/project_glossary.rb +44 -0
  72. data/lib/hecks/cli/reference.rb +46 -0
  73. data/lib/hecks/cli/refresh_projections.rb +44 -0
  74. data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +150 -0
  75. data/lib/hecks/cli/release.rb +70 -0
  76. data/lib/hecks/cli/release_gem.rb +59 -0
  77. data/lib/hecks/cli/rspec_io_parallel_files.rb +137 -0
  78. data/lib/hecks/cli/rspec_shard_files.rb +66 -0
  79. data/lib/hecks/cli/run.rb +3 -3
  80. data/lib/hecks/cli/seed_semantics_corpus.rb +71 -0
  81. data/lib/hecks/cli/serve_query_ir_mcp.rb +30 -0
  82. data/lib/hecks/cli/shape.rb +70 -0
  83. data/lib/hecks/cli/smoke_http.rb +230 -0
  84. data/lib/hecks/cli/smoke_test.rb +5 -3
  85. data/lib/hecks/cli/spec_example.rb +27 -0
  86. data/lib/hecks/cli/statements.rb +32 -0
  87. data/lib/hecks/cli/stores.rb +4 -5
  88. data/lib/hecks/cli/stress_concurrency_specs.rb +177 -0
  89. data/lib/hecks/cli.rb +201 -52
  90. data/lib/hecks/codemod.rb +324 -0
  91. data/lib/hecks/corpus.rb +423 -0
  92. data/lib/hecks/deploy/bluebook/deploy.bluebook +591 -23
  93. data/lib/hecks/deploy/bluebook/deploy.hecksagon +41 -4
  94. data/lib/hecks/deploy/oidc.json +82 -2
  95. data/lib/hecks/doc/reference.rb +351 -0
  96. data/lib/hecks/doors/cli_door.rb +196 -0
  97. data/lib/hecks/doors/cli_runner.rb +297 -0
  98. data/lib/hecks/{facade → doors}/command_request.rb +1 -1
  99. data/lib/hecks/{facade → doors}/handle.rb +2 -2
  100. data/lib/hecks/{facade → doors}/json_door.rb +4 -4
  101. data/lib/hecks/doors/launcher_options.rb +129 -0
  102. data/lib/hecks/{cli → doors}/mcp_door.rb +7 -7
  103. data/lib/hecks/doors/mcp_door_scope.rb +97 -0
  104. data/lib/hecks/{facade/surface → doors/ruby_door}/aggregate_door.rb +8 -3
  105. data/lib/hecks/{facade/surface → doors/ruby_door}/chapter.rb +3 -3
  106. data/lib/hecks/{facade/surface.rb → doors/ruby_door.rb} +36 -5
  107. data/lib/hecks/doors.rb +61 -0
  108. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +1 -1
  109. data/lib/hecks/embryonaut_bluebook.rb +1 -1
  110. data/lib/hecks/engine_agreement.rb +156 -0
  111. data/lib/hecks/facade.rb +24 -37
  112. data/lib/hecks/forms/app.rb +6 -6
  113. data/lib/hecks/forms/banking_presentation.rb +50 -0
  114. data/lib/hecks/forms/field_shape.rb +2 -1
  115. data/lib/hecks/forms/port_argument.rb +1 -1
  116. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +393 -0
  117. data/lib/hecks/fuzzing/combination_miner.rb +86 -0
  118. data/lib/hecks/fuzzing/concurrent_dispatch.rb +214 -0
  119. data/lib/hecks/fuzzing/coverage_campaign.rb +143 -0
  120. data/lib/hecks/fuzzing/differential.rb +156 -0
  121. data/lib/hecks/fuzzing/domain_generator.rb +615 -0
  122. data/lib/hecks/fuzzing/era_boundary.rb +71 -0
  123. data/lib/hecks/fuzzing/form_census.rb +136 -0
  124. data/lib/hecks/fuzzing/generated_domain_check.rb +93 -0
  125. data/lib/hecks/fuzzing/invalid_value_generator.rb +64 -0
  126. data/lib/hecks/fuzzing/isolated_boot.rb +301 -0
  127. data/lib/hecks/fuzzing/nondeterministic.rb +51 -0
  128. data/lib/hecks/fuzzing/outside_world.rb +45 -0
  129. data/lib/hecks/fuzzing/persistence_parity.rb +102 -0
  130. data/lib/hecks/fuzzing/properties/corrections.rb +88 -0
  131. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +314 -0
  132. data/lib/hecks/fuzzing/properties/guards.rb +190 -0
  133. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +300 -0
  134. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +93 -0
  135. data/lib/hecks/fuzzing/properties/outbox.rb +89 -0
  136. data/lib/hecks/fuzzing/properties/querying.rb +175 -0
  137. data/lib/hecks/fuzzing/properties.rb +162 -0
  138. data/lib/hecks/fuzzing/qa_settings.rb +123 -0
  139. data/lib/hecks/fuzzing/replay.rb +505 -0
  140. data/lib/hecks/fuzzing/rotation_priority.rb +48 -0
  141. data/lib/hecks/fuzzing/rust_gap_manifest.rb +108 -0
  142. data/lib/hecks/fuzzing/self_consistency.rb +339 -0
  143. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +385 -0
  144. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +132 -0
  145. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +95 -0
  146. data/lib/hecks/fuzzing/sequence_generator/picker.rb +77 -0
  147. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +109 -0
  148. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +237 -0
  149. data/lib/hecks/fuzzing/sequence_generator.rb +189 -0
  150. data/lib/hecks/fuzzing/shrinker.rb +166 -0
  151. data/lib/hecks/fuzzing/structural_skips.rb +33 -0
  152. data/lib/hecks/fuzzing/sweep_depth.rb +43 -0
  153. data/lib/hecks/fuzzing/target_capabilities.rb +163 -0
  154. data/lib/hecks/fuzzing/value_generator.rb +216 -0
  155. data/lib/hecks/fuzzing.rb +23 -0
  156. data/lib/hecks/grammar/evolve.rb +336 -0
  157. data/lib/hecks/hecks/adapters/codebase/codemods.rb +45 -0
  158. data/lib/hecks/hecks/adapters/codebase/conformance.rb +71 -0
  159. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +202 -0
  160. data/lib/hecks/hecks/adapters/codebase/gem_registry.rb +80 -0
  161. data/lib/hecks/hecks/adapters/codebase/kernel_tables.rb +54 -0
  162. data/lib/hecks/hecks/adapters/codebase/language.rb +240 -0
  163. data/lib/hecks/hecks/adapters/codebase/npm_registry.rb +102 -0
  164. data/lib/hecks/hecks/adapters/codebase/publishing.rb +142 -0
  165. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +67 -0
  166. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +90 -0
  167. data/lib/hecks/hecks/adapters/codebase/ruby_child.rb +83 -0
  168. data/lib/hecks/hecks/adapters/codebase/secret_vault.rb +36 -0
  169. data/lib/hecks/hecks/adapters/codebase/source_tree.adapter +7 -0
  170. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +189 -0
  171. data/lib/hecks/hecks/adapters/codebase/sqlite_fixture.rb +71 -0
  172. data/lib/hecks/hecks/adapters/codebase/style.rb +171 -0
  173. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +55 -0
  174. data/lib/hecks/hecks/adapters/codebase/test_suite.rb +165 -0
  175. data/lib/hecks/hecks/adapters/codebase/tree.rb +111 -0
  176. data/lib/hecks/hecks/adapters/console_capture.rb +79 -0
  177. data/lib/hecks/hecks/adapters/deploy_toolchain.adapter +7 -0
  178. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +108 -0
  179. data/lib/hecks/hecks/adapters/git.adapter +7 -0
  180. data/lib/hecks/hecks/adapters/git.rb +102 -0
  181. data/lib/hecks/hecks/adapters/host_http.adapter +7 -0
  182. data/lib/hecks/hecks/adapters/host_http.rb +65 -0
  183. data/lib/hecks/hecks/adapters/in_process_boot.adapter +7 -0
  184. data/lib/hecks/hecks/adapters/in_process_boot.rb +197 -0
  185. data/lib/hecks/hecks/adapters/in_process_operations.rb +202 -0
  186. data/lib/hecks/hecks/adapters/journal_store/changes.rb +143 -0
  187. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +145 -0
  188. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +132 -0
  189. data/lib/hecks/hecks/adapters/journal_store/examination.rb +135 -0
  190. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +163 -0
  191. data/lib/hecks/hecks/adapters/journal_store/readings.rb +187 -0
  192. data/lib/hecks/hecks/adapters/journal_store.adapter +7 -0
  193. data/lib/hecks/hecks/adapters/journal_store.rb +64 -0
  194. data/lib/hecks/hecks/adapters/local_files.adapter +7 -0
  195. data/lib/hecks/hecks/adapters/local_files.rb +46 -0
  196. data/lib/hecks/hecks/adapters/pg_admin.adapter +7 -0
  197. data/lib/hecks/hecks/adapters/pg_admin.rb +245 -0
  198. data/lib/hecks/hecks/adapters/process_pool.adapter +7 -0
  199. data/lib/hecks/hecks/adapters/process_pool.rb +119 -0
  200. data/lib/hecks/hecks/adapters/rust_toolchain.adapter +7 -0
  201. data/lib/hecks/hecks/adapters/rust_toolchain.rb +179 -0
  202. data/lib/hecks/hecks/adapters/rust_workspace.rb +150 -0
  203. data/lib/hecks/hecks/adapters/shell.adapter +7 -0
  204. data/lib/hecks/hecks/adapters/shell.rb +49 -0
  205. data/lib/hecks/hecks/adapters/terminal.adapter +7 -0
  206. data/lib/hecks/hecks/adapters/terminal.rb +74 -0
  207. data/lib/hecks/hecks/codebase.behaviors +172 -0
  208. data/lib/hecks/hecks/codebase.bluebook +2453 -0
  209. data/lib/hecks/hecks/context_map.hecksagon +13 -0
  210. data/lib/hecks/hecks/custodian.behaviors +179 -0
  211. data/lib/hecks/hecks/custodian.bluebook +1847 -0
  212. data/lib/hecks/hecks/deploy.behaviors +74 -0
  213. data/lib/hecks/hecks/deploy.world +12 -0
  214. data/lib/hecks/hecks/environments/memory.world +10 -0
  215. data/lib/hecks/hecks/governance.world +7 -0
  216. data/lib/hecks/hecks/hecks.behaviors +62 -0
  217. data/lib/hecks/hecks/hecks.bluebook +124 -0
  218. data/lib/hecks/hecks/hecks.hecksagon +390 -0
  219. data/lib/hecks/hecks/hecks.world +42 -0
  220. data/lib/hecks/kernel_capabilities.rb +194 -0
  221. data/lib/hecks/language/bluebook/bluebook.bluebook +11 -0
  222. data/lib/hecks/language/bluebook/query.bluebook +7 -0
  223. data/lib/hecks/language/bluebook/vocabulary.bluebook +4 -4
  224. data/lib/hecks/language/hecksagon/domain_port.bluebook +10 -0
  225. data/lib/hecks/language/hecksagon/hecksagon.bluebook +20 -0
  226. data/lib/hecks/mcp_stdio_guard.rb +2 -2
  227. data/lib/hecks/persistence_legacy_fixture/regenerate.rb +161 -0
  228. data/lib/hecks/persistence_legacy_fixture.rb +113 -0
  229. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +8 -5
  230. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +4 -1
  231. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +15 -2
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +1 -1
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +1 -1
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +1 -1
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +32 -15
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +6 -3
  237. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +227 -0
  238. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +1 -1
  239. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +43 -24
  240. data/lib/hecks/ports/persistence/plugins/era/translation.rb +1 -0
  241. data/lib/hecks/ports/persistence/state_codec.rb +24 -5
  242. data/lib/hecks/projection_files.rb +285 -0
  243. data/lib/hecks/projections/bootstrap_table.rb +1 -1
  244. data/lib/hecks/projections/deploy/fargate.rb +5 -5
  245. data/lib/hecks/projections/deploy/lambda.rb +43 -32
  246. data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
  247. data/lib/hecks/projections/deploy/preview/template.rb +1 -1
  248. data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +1 -1
  249. data/lib/hecks/projections/deploy/scripts/expected-era.header +1 -1
  250. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +3 -3
  251. data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +1 -1
  252. data/lib/hecks/projections/deploy/shared.rb +1 -1
  253. data/lib/hecks/projections/diagrams.rb +3 -3
  254. data/lib/hecks/projections/model/deviations.rb +1 -1
  255. data/lib/hecks/projections/parser_table.rb +4 -4
  256. data/lib/hecks/projections/reference.rb +2 -3
  257. data/lib/hecks/projections/rust_vocabulary.rb +1 -1
  258. data/lib/hecks/projector/cli_projector.rb +106 -37
  259. data/lib/hecks/projector.rb +1 -1
  260. data/lib/hecks/quality_control/adapters/agent.adapter +7 -0
  261. data/lib/hecks/quality_control/adapters/agent.rb +58 -0
  262. data/lib/hecks/quality_control/adapters/angle_tools.adapter +7 -0
  263. data/lib/hecks/quality_control/adapters/angle_tools.rb +15 -0
  264. data/lib/hecks/quality_control/adapters/clearance_recorder.rb +36 -0
  265. data/lib/hecks/quality_control/adapters/clearance_tools.adapter +7 -0
  266. data/lib/hecks/quality_control/adapters/clearance_tools.rb +17 -0
  267. data/lib/hecks/quality_control/adapters/git_pr.adapter +7 -0
  268. data/lib/hecks/quality_control/adapters/git_pr.rb +226 -0
  269. data/lib/hecks/quality_control/adapters/github_checks.adapter +9 -0
  270. data/lib/hecks/quality_control/adapters/github_checks.rb +83 -0
  271. data/lib/hecks/quality_control/adapters/github_ci_webhook.rb +56 -0
  272. data/lib/hecks/quality_control/adapters/github_issues.adapter +7 -0
  273. data/lib/hecks/quality_control/adapters/github_issues.rb +49 -0
  274. data/lib/hecks/quality_control/adapters/qa_tool.rb +59 -0
  275. data/lib/hecks/quality_control/adapters/sweep_tools.adapter +7 -0
  276. data/lib/hecks/quality_control/adapters/sweep_tools.rb +51 -0
  277. data/lib/hecks/quality_control/adapters/target_tools.adapter +7 -0
  278. data/lib/hecks/quality_control/adapters/target_tools.rb +43 -0
  279. data/lib/hecks/quality_control/cli/child.rb +45 -0
  280. data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +45 -0
  281. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +248 -0
  282. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +163 -0
  283. data/lib/hecks/quality_control/cli/qa_generated_domains.rb +450 -0
  284. data/lib/hecks/quality_control/cli/qa_log_bug.rb +195 -0
  285. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +220 -0
  286. data/lib/hecks/quality_control/cli/qa_open_pr.rb +305 -0
  287. data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +154 -0
  288. data/lib/hecks/quality_control/cli/qa_postgres_role.rb +159 -0
  289. data/lib/hecks/quality_control/cli/qa_pr_check.rb +232 -0
  290. data/lib/hecks/quality_control/cli/qa_seed_angles.rb +142 -0
  291. data/lib/hecks/quality_control/cli/qa_seed_targets.rb +69 -0
  292. data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +280 -0
  293. data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +329 -0
  294. data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +92 -0
  295. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +117 -0
  296. data/lib/hecks/quality_control/cli/qa_sweep/parsing.rb +175 -0
  297. data/lib/hecks/quality_control/cli/qa_sweep/release_mode.rb +60 -0
  298. data/lib/hecks/quality_control/cli/qa_sweep.rb +611 -0
  299. data/lib/hecks/quality_control/cli/qa_tick.rb +274 -0
  300. data/lib/hecks/quality_control/quality_control.bluebook +2224 -0
  301. data/lib/hecks/quality_control/quality_control.hecksagon +13 -0
  302. data/lib/hecks/quality_control/quality_control.ports.hecksagon +54 -0
  303. data/lib/hecks/query_ir.rb +346 -0
  304. data/lib/hecks/query_ir_mcp.rb +198 -0
  305. data/lib/hecks/release/runner/ci_publisher.rb +2 -2
  306. data/lib/hecks/release/runner/clean_tree.rb +42 -0
  307. data/lib/hecks/release/runner/console.rb +6 -0
  308. data/lib/hecks/release/runner/gem_publisher.rb +14 -7
  309. data/lib/hecks/release/runner/npm_publisher.rb +6 -36
  310. data/lib/hecks/release/runner/preflight.rb +26 -2
  311. data/lib/hecks/release/runner/published.rb +8 -26
  312. data/lib/hecks/release/runner/tagger.rb +30 -15
  313. data/lib/hecks/release/runner.rb +57 -6
  314. data/lib/hecks/router/namespace_installer.rb +3 -3
  315. data/lib/hecks/runtime/adapter_lookup.rb +124 -0
  316. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +4 -0
  317. data/lib/hecks/runtime/dependency_planning.rb +20 -2
  318. data/lib/hecks/runtime/dispatcher.rb +53 -3
  319. data/lib/hecks/runtime/interpreting.rb +3 -0
  320. data/lib/hecks/runtime/loader.rb +65 -12
  321. data/lib/hecks/runtime/outbox.rb +1 -1
  322. data/lib/hecks/runtime/policy_interpreter.rb +5 -2
  323. data/lib/hecks/runtime/port_operation_interpreter.rb +2 -10
  324. data/lib/hecks/runtime/query_interpreter.rb +77 -0
  325. data/lib/hecks/runtime/reaction_outcome.rb +89 -0
  326. data/lib/hecks/runtime/refusal_wording.rb +23 -1
  327. data/lib/hecks/runtime/registry/verification.rb +163 -4
  328. data/lib/hecks/runtime/registry.rb +57 -3
  329. data/lib/hecks/runtime/remote_dispatcher.rb +31 -2
  330. data/lib/hecks/runtime/value/coercion.rb +23 -135
  331. data/lib/hecks/runtime/value/field_checks.rb +172 -0
  332. data/lib/hecks/runtime/value.rb +4 -1
  333. data/lib/hecks/runtime.rb +20 -6
  334. data/lib/hecks/rust_build/conformance.rb +158 -0
  335. data/lib/hecks/rust_build/conformance_fuzz.rb +71 -0
  336. data/lib/hecks/rust_build/coverage.rb +218 -0
  337. data/lib/hecks/rust_build/native_build.rb +124 -0
  338. data/lib/hecks/rust_build/project_rust/root_mod.rb +120 -0
  339. data/lib/hecks/rust_build/project_rust.rb +332 -0
  340. data/lib/hecks/rust_build/wasm.rb +105 -0
  341. data/lib/hecks/rust_build/wasm_browser.rb +113 -0
  342. data/lib/hecks/rust_build.rb +144 -0
  343. data/lib/hecks/storehouse.rb +16 -16
  344. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +2 -2
  345. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +1 -1
  346. data/lib/hecks/three_zero/forms.yml +127 -0
  347. data/lib/hecks/three_zero.rb +12 -0
  348. data/lib/hecks/tools/argument_gate_matrix.rb +316 -0
  349. data/lib/hecks/tools/comment_style.rb +1070 -0
  350. data/lib/hecks/tools/corpus_report.rb +87 -0
  351. data/lib/hecks/tools/deploy_recipe.rb +255 -0
  352. data/lib/hecks/tools/deploy_recipe_lint.rb +282 -0
  353. data/lib/hecks/tools/drop_implicit_append_fields.rb +95 -0
  354. data/lib/hecks/tools/era_projection_backfill.rb +94 -0
  355. data/lib/hecks/tools/era_reattest.rb +146 -0
  356. data/lib/hecks/tools/evolve_run.rb +189 -0
  357. data/lib/hecks/tools/fuzz_sweep.rb +373 -0
  358. data/lib/hecks/tools/heki_compaction.rb +100 -0
  359. data/lib/hecks/tools/hoist_local_givens.rb +246 -0
  360. data/lib/hecks/tools/journal_compaction.rb +89 -0
  361. data/lib/hecks/tools/oidc_manifests.rb +70 -0
  362. data/lib/hecks/tools/query_ir_run.rb +81 -0
  363. data/lib/hecks/tools/regeneration_run.rb +166 -0
  364. data/lib/hecks/tools/rust_comment_style.rb +642 -0
  365. data/lib/hecks/tools/sequence_script.rb +62 -0
  366. data/lib/hecks/tools/tail_merge.rb +117 -0
  367. data/lib/hecks/tools/tenant_provisioning.rb +142 -0
  368. data/lib/hecks/tools/translation_audit.rb +208 -0
  369. data/lib/hecks/tools/translation_scaffold.rb +131 -0
  370. data/lib/hecks/tools.rb +86 -0
  371. data/lib/hecks/version.rb +1 -1
  372. data/lib/hecks.rb +32 -15
  373. data/qa/settings.yml +66 -0
  374. data/rust/.cargo/config.toml +6 -0
  375. data/rust/.gitignore +2 -0
  376. data/rust/Cargo.lock +54 -0
  377. data/rust/Cargo.toml +61 -0
  378. data/rust/build/.cargo/config.toml +3 -0
  379. data/rust/build/.gitignore +1 -0
  380. data/rust/build/Cargo.lock +7 -0
  381. data/rust/build/Cargo.toml +19 -0
  382. data/rust/build/src/build_artifact.rs +47 -0
  383. data/rust/build/src/cargo_sync.rs +248 -0
  384. data/rust/build/src/json.rs +421 -0
  385. data/rust/build/src/lineage_pass.rs +285 -0
  386. data/rust/build/src/main.rs +51 -0
  387. data/rust/build/src/optional_pass.rs +150 -0
  388. data/rust/build/src/pipeline.rs +264 -0
  389. data/rust/build/src/reserved_names.rs +76 -0
  390. data/rust/build/src/resolve.rs +124 -0
  391. data/rust/build/src/root.rs +25 -0
  392. data/rust/build/src/sidecars.rs +73 -0
  393. data/rust/build/src/subprocess.rs +51 -0
  394. data/rust/build/src/tmp.rs +31 -0
  395. data/rust/codegen/.cargo/config.toml +3 -0
  396. data/rust/codegen/.gitignore +1 -0
  397. data/rust/codegen/Cargo.lock +7 -0
  398. data/rust/codegen/Cargo.toml +20 -0
  399. data/rust/codegen/src/attr.rs +32 -0
  400. data/rust/codegen/src/bridging.rs +481 -0
  401. data/rust/codegen/src/commands.rs +1226 -0
  402. data/rust/codegen/src/constraints.rs +109 -0
  403. data/rust/codegen/src/dependency_planning.rs +209 -0
  404. data/rust/codegen/src/domain_generator.rs +1288 -0
  405. data/rust/codegen/src/exemplar.rs +230 -0
  406. data/rust/codegen/src/expr_emitter.rs +87 -0
  407. data/rust/codegen/src/fielded.rs +192 -0
  408. data/rust/codegen/src/hecks_naming.rs +33 -0
  409. data/rust/codegen/src/json.rs +335 -0
  410. data/rust/codegen/src/json_codec.rs +697 -0
  411. data/rust/codegen/src/literal.rs +207 -0
  412. data/rust/codegen/src/main.rs +290 -0
  413. data/rust/codegen/src/manifest.rs +227 -0
  414. data/rust/codegen/src/mutations.rs +784 -0
  415. data/rust/codegen/src/naming.rs +286 -0
  416. data/rust/codegen/src/ports.rs +208 -0
  417. data/rust/codegen/src/prelude.rs +116 -0
  418. data/rust/codegen/src/queries.rs +786 -0
  419. data/rust/codegen/src/reactions.rs +532 -0
  420. data/rust/codegen/src/read_models.rs +697 -0
  421. data/rust/codegen/src/reference_specs.rs +40 -0
  422. data/rust/codegen/src/registry.rs +966 -0
  423. data/rust/codegen/src/reserved_names.rs +76 -0
  424. data/rust/codegen/src/shared.rs +93 -0
  425. data/rust/codegen/src/skip_reason.rs +25 -0
  426. data/rust/codegen/src/types.rs +362 -0
  427. data/rust/host/.cargo/config.toml +3 -0
  428. data/rust/host/.gitignore +1 -0
  429. data/rust/host/Cargo.lock +4297 -0
  430. data/rust/host/Cargo.toml +61 -0
  431. data/rust/host/HECKS_RELEASE +1 -0
  432. data/rust/host/rds-ca-bundle.pem +2736 -0
  433. data/rust/host/src/api.rs +1596 -0
  434. data/rust/host/src/approval.rs +652 -0
  435. data/rust/host/src/auth.rs +1287 -0
  436. data/rust/host/src/bin/expr_harness.rs +46 -0
  437. data/rust/host/src/bin/lineage_harness.rs +119 -0
  438. data/rust/host/src/bin/mint_harness.rs +135 -0
  439. data/rust/host/src/checkout.rs +265 -0
  440. data/rust/host/src/dispatch.rs +1443 -0
  441. data/rust/host/src/expr_json.rs +1117 -0
  442. data/rust/host/src/field_hints.rs +52 -0
  443. data/rust/host/src/ir.rs +733 -0
  444. data/rust/host/src/journal.rs +874 -0
  445. data/rust/host/src/lambda_client.rs +378 -0
  446. data/rust/host/src/log.rs +147 -0
  447. data/rust/host/src/main.rs +474 -0
  448. data/rust/host/src/mint.rs +1865 -0
  449. data/rust/host/src/payments/keystore.rs +330 -0
  450. data/rust/host/src/payments/tests.rs +1400 -0
  451. data/rust/host/src/payments.rs +767 -0
  452. data/rust/host/src/presentation.rs +698 -0
  453. data/rust/host/src/presentation_write.rs +1431 -0
  454. data/rust/host/src/query_step.rs +149 -0
  455. data/rust/host/src/rate_limit.rs +904 -0
  456. data/rust/host/src/reference_transform.rs +241 -0
  457. data/rust/host/src/reference_validate.rs +338 -0
  458. data/rust/host/src/resend.rs +296 -0
  459. data/rust/host/src/secrets.rs +106 -0
  460. data/rust/host/src/server.rs +663 -0
  461. data/rust/host/src/storage_shape.rs +175 -0
  462. data/rust/host/src/test_pg.rs +40 -0
  463. data/rust/host/src/ui_schema.rs +1351 -0
  464. data/rust/host/src/wasm_runner.rs +191 -0
  465. data/rust/host/src/web/newsletter/cooldown.rs +126 -0
  466. data/rust/host/src/web/newsletter/tests.rs +104 -0
  467. data/rust/host/src/web/newsletter.rs +310 -0
  468. data/rust/host/src/web/newsletter_send.rs +357 -0
  469. data/rust/host/src/web/registrations.rs +1505 -0
  470. data/rust/host/src/web.rs +2937 -0
  471. data/rust/lsp/.gitignore +1 -0
  472. data/rust/lsp/Cargo.lock +7 -0
  473. data/rust/lsp/Cargo.toml +16 -0
  474. data/rust/lsp/README.md +133 -0
  475. data/rust/lsp/src/diagnostics.rs +165 -0
  476. data/rust/lsp/src/json.rs +385 -0
  477. data/rust/lsp/src/main.rs +399 -0
  478. data/rust/lsp/src/outline.rs +177 -0
  479. data/rust/lsp/src/rpc.rs +67 -0
  480. data/rust/parser/.cargo/config.toml +5 -0
  481. data/rust/parser/.gitignore +1 -0
  482. data/rust/parser/Cargo.lock +7 -0
  483. data/rust/parser/Cargo.toml +21 -0
  484. data/rust/parser/src/build/closed_sets.rs +27 -0
  485. data/rust/parser/src/build/identity.rs +276 -0
  486. data/rust/parser/src/build/mod.rs +12 -0
  487. data/rust/parser/src/build/naming.rs +141 -0
  488. data/rust/parser/src/build/pattern_subset.rs +161 -0
  489. data/rust/parser/src/build/query_derive.rs +72 -0
  490. data/rust/parser/src/build/query_inference.rs +269 -0
  491. data/rust/parser/src/build/query_options.rs +100 -0
  492. data/rust/parser/src/build/read_model.rs +94 -0
  493. data/rust/parser/src/build/references.rs +32 -0
  494. data/rust/parser/src/canonical.rs +114 -0
  495. data/rust/parser/src/diag.rs +55 -0
  496. data/rust/parser/src/emit.rs +953 -0
  497. data/rust/parser/src/expr/ast_json.rs +147 -0
  498. data/rust/parser/src/expr/evaluator.rs +81 -0
  499. data/rust/parser/src/expr/mod.rs +84 -0
  500. data/rust/parser/src/expr/resolver.rs +497 -0
  501. data/rust/parser/src/ir.rs +387 -0
  502. data/rust/parser/src/keywords.rs +585 -0
  503. data/rust/parser/src/lex.rs +662 -0
  504. data/rust/parser/src/main.rs +252 -0
  505. data/rust/parser/src/parse/aggregate.rs +612 -0
  506. data/rust/parser/src/parse/chapter.rs +517 -0
  507. data/rust/parser/src/parse/command.rs +637 -0
  508. data/rust/parser/src/parse/domain_port.rs +127 -0
  509. data/rust/parser/src/parse/entity.rs +527 -0
  510. data/rust/parser/src/parse/file.rs +67 -0
  511. data/rust/parser/src/parse/hecksagon.rs +231 -0
  512. data/rust/parser/src/parse/lifecycle.rs +162 -0
  513. data/rust/parser/src/parse/mod.rs +1262 -0
  514. data/rust/parser/src/parse/policy.rs +87 -0
  515. data/rust/parser/src/parse/process_manager.rs +305 -0
  516. data/rust/parser/src/parse/query.rs +100 -0
  517. data/rust/parser/src/parse/read_model.rs +214 -0
  518. data/rust/parser/src/parse/value_object.rs +250 -0
  519. data/rust/parser/src/ruby_value.rs +422 -0
  520. data/rust/project/bridging.rb +257 -0
  521. data/rust/project/commands.rb +877 -0
  522. data/rust/project/constraints.rb +86 -0
  523. data/rust/project/dependency_planning.rb +178 -0
  524. data/rust/project/domain_generator.rb +980 -0
  525. data/rust/project/exemplar.rb +148 -0
  526. data/rust/project/expr_emitter.rb +72 -0
  527. data/rust/project/fielded.rb +141 -0
  528. data/rust/project/json_codec.rb +641 -0
  529. data/rust/project/mutations.rb +721 -0
  530. data/rust/project/naming.rb +205 -0
  531. data/rust/project/ports.rb +65 -0
  532. data/rust/project/queries.rb +493 -0
  533. data/rust/project/reactions.rb +317 -0
  534. data/rust/project/read_models.rb +690 -0
  535. data/rust/project/reference_specs.rb +27 -0
  536. data/rust/project/registry.rb +396 -0
  537. data/rust/project/skip_reason.rb +29 -0
  538. data/rust/project/types.rb +225 -0
  539. data/rust/project/write_if_changed.rb +83 -0
  540. data/rust/project.rb +30 -0
  541. data/rust/project_rust_pipeline.rb +383 -0
  542. data/rust/src/exemplar/commands.rs +279 -0
  543. data/rust/src/exemplar/constraints.rs +18 -0
  544. data/rust/src/exemplar/fielded.rs +280 -0
  545. data/rust/src/exemplar/json.rs +277 -0
  546. data/rust/src/exemplar/mod.rs +17 -0
  547. data/rust/src/exemplar/mutations.rs +80 -0
  548. data/rust/src/exemplar/queries.rs +33 -0
  549. data/rust/src/exemplar/reactions.rs +88 -0
  550. data/rust/src/exemplar/read_models.rs +48 -0
  551. data/rust/src/exemplar/registry.rs +166 -0
  552. data/rust/src/exemplar/types.rs +44 -0
  553. data/rust/src/kernel/attribute_shapes/composite.rs +88 -0
  554. data/rust/src/kernel/attribute_shapes/list.rs +18 -0
  555. data/rust/src/kernel/attribute_shapes/mod.rs +44 -0
  556. data/rust/src/kernel/attribute_shapes/optional.rs +9 -0
  557. data/rust/src/kernel/attribute_shapes/scalar.rs +39 -0
  558. data/rust/src/kernel/cli.rs +585 -0
  559. data/rust/src/kernel/dispatch.rs +1090 -0
  560. data/rust/src/kernel/expr.rs +458 -0
  561. data/rust/src/kernel/expression_operators/arithmetic.rs +128 -0
  562. data/rust/src/kernel/expression_operators/comparison.rs +48 -0
  563. data/rust/src/kernel/expression_operators/enumeration.rs +232 -0
  564. data/rust/src/kernel/expression_operators/logical.rs +13 -0
  565. data/rust/src/kernel/expression_operators/membership.rs +18 -0
  566. data/rust/src/kernel/expression_operators/mod.rs +68 -0
  567. data/rust/src/kernel/expression_operators/pattern_match.rs +73 -0
  568. data/rust/src/kernel/expression_operators/positional.rs +126 -0
  569. data/rust/src/kernel/expression_operators/presence.rs +101 -0
  570. data/rust/src/kernel/expression_operators/sign_test.rs +16 -0
  571. data/rust/src/kernel/expression_operators/sized.rs +35 -0
  572. data/rust/src/kernel/expression_operators/text.rs +79 -0
  573. data/rust/src/kernel/expression_operators/to_string.rs +18 -0
  574. data/rust/src/kernel/json.rs +779 -0
  575. data/rust/src/kernel/mod.rs +133 -0
  576. data/rust/src/kernel/named_query.rs +219 -0
  577. data/rust/src/kernel/orchestrate.rs +1810 -0
  578. data/rust/src/kernel/pattern.rs +307 -0
  579. data/rust/src/kernel/query_comparators.rs +206 -0
  580. data/rust/src/kernel/query_ordering.rs +103 -0
  581. data/rust/src/kernel/read_model.rs +914 -0
  582. data/rust/src/kernel/reference_lookup.rs +149 -0
  583. data/rust/src/kernel/repository.rs +468 -0
  584. data/rust/src/kernel/routing.rs +612 -0
  585. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +118 -0
  586. data/rust/src/kernel/vocab/entity_dispatch_order.rs +114 -0
  587. data/rust/src/kernel/vocab/field_hint.rs +70 -0
  588. data/rust/src/kernel/vocab/mod.rs +20 -0
  589. data/rust/src/kernel/vocab/query_comparator.rs +65 -0
  590. data/rust/src/kernel/vocab/refusal_template.rs +1592 -0
  591. data/rust/src/lib.rs +8 -0
  592. data/rust/src/main.rs +16 -0
  593. data/rust/web/.gitignore +1 -0
  594. data/rust/web/Cargo.lock +119 -0
  595. data/rust/web/Cargo.toml +18 -0
  596. data/rust/web/src/lib.rs +8 -0
  597. metadata +477 -12
  598. data/lib/hecks/facade/cli_door.rb +0 -96
  599. data/lib/hecks/facade/cli_runner.rb +0 -127
  600. data/lib/hecks/mcp_door_scope.rb +0 -95
@@ -0,0 +1,505 @@
1
+ require "fileutils"
2
+ require "tmpdir"
3
+ require_relative "isolated_boot"
4
+ require_relative "self_consistency"
5
+ require_relative "../query_specification/common/comparators"
6
+ require_relative "../query_specification/common/where_clause"
7
+ require_relative "../query_specification/field_path"
8
+ require_relative "../ports/query/in_memory"
9
+
10
+ module Hecks
11
+ module Fuzzing
12
+ # Replays a step list in-process against a fresh boot, returning the
13
+ # observable history as data instead of JSON on stdout.
14
+ module Replay
15
+ module_function
16
+
17
+ # Mirrors QuerySpecification::Common::COMPARATORS, not the Rust
18
+ # kernel's own comparator enum, which is missing `none_in_state` and has drifted.
19
+ FILTER_COMPARATORS = Hecks::QuerySpecification::Common::COMPARATORS.map(&:to_s).freeze
20
+
21
+ # The two classes Admissibility#enforce_givens/#enforce_lifecycle_guard raise;
22
+ # any other DOMAIN_REFUSAL proves the guard itself did not fire.
23
+ GUARD_REFUSAL_CLASSES = [Runtime::GivenNotMet, Runtime::LifecycleRefused].freeze
24
+
25
+ # Replays +steps+ against a fresh boot of +domain_path+. The oracle snapshots taken
26
+ # inside the loop are timed relative to dispatch; do not reorder them.
27
+ #
28
+ # @param domain_path [String] filesystem path to the domain directory to replay
29
+ # @param adapter [Symbol] persistence adapter to boot the copy with
30
+ # @param database [String, nil] PostgresEra database name (required for :postgres_era)
31
+ # @param self_consistency [Boolean] also run SelfConsistency.check before returning
32
+ # @return [Hash] instances, events, refusals, reactions, sagas, queries, oracle traces
33
+ # rubocop:disable-next Metrics/AbcSize
34
+ # rubocop:disable-next Metrics/CyclomaticComplexity
35
+ # rubocop:disable-next Metrics/MethodLength
36
+ # rubocop:disable-next Metrics/PerceivedComplexity
37
+ def call(domain_path, steps, adapter: :memory, database: nil, schema: nil, self_consistency: false)
38
+ # See isolated_boot.rb's own header: resets data/ and rebinds persistence to the
39
+ # chosen adapter, since a Postgres-bound domain's real store lives outside the
40
+ # copied directory and can't be reached by resetting data/ alone.
41
+ IsolatedBoot.call(domain_path, adapter: adapter, database: database, schema: schema) do |copy|
42
+ runtime = Hecks.boot(copy, environment: nil)
43
+
44
+ refusals = []
45
+ queries = []
46
+ dry_runs = []
47
+ dry_run_traces = []
48
+ fan_outs = []
49
+ guard_checks = []
50
+ mutation_traces = []
51
+ outbox_traces = []
52
+
53
+ # Every `[domain, aggregate_name]` a `for_each` policy could query, resolved
54
+ # once from every loaded bluebook's own fanning-out policies. Empty when no
55
+ # domain declares `for_each`, so the snapshot below costs nothing until one does.
56
+ fan_out_targets = runtime.registry.bluebooks.each_with_object({}) do |(domain, bluebook), targets|
57
+ bluebook.policies.select(&:fans_out?).each do |policy|
58
+ query_domain, aggregate_name, = policy.for_each_route(domain)
59
+ targets[[query_domain, aggregate_name]] ||= runtime.registry.bluebook(query_domain)&.aggregate(aggregate_name)
60
+ end
61
+ end
62
+
63
+ steps.each do |step|
64
+ step = step.transform_keys(&:to_s)
65
+ args = (step["args"] || {}).transform_keys(&:to_sym)
66
+
67
+ if (question = step["query"])
68
+ # A `{aggregate:, field:, op:, value:}` Hash "query" step — the ad hoc
69
+ # filter kernel/cli.rs's own object-form step also reads, bypassing the
70
+ # declared bluebook query DSL. Answered via `Ports::Query::InMemory` directly.
71
+ if question.is_a?(Hash)
72
+ begin
73
+ rows = run_filter(runtime, question)
74
+ queries << { query: question, rows: rows, instances_at: snapshot_instances(runtime) }
75
+ rescue StandardError => e
76
+ refusals << { verb: filter_label(question), error: e.message, kind: refusal_kind(e) }
77
+ end
78
+ next
79
+ end
80
+
81
+ # Each engine runs in its own begin/rescue, never a shared one — a shared
82
+ # rescue would hide "one engine refused, the other didn't" (the real
83
+ # divergence this differential oracle exists to catch) behind a plain refusal.
84
+ native_rows = native_error = nil
85
+ begin
86
+ native_rows = runtime.query(question, **args)
87
+ rescue *Runtime::DOMAIN_REFUSALS, Bluebook::Expression::EvaluationError => e
88
+ native_error = e
89
+ end
90
+
91
+ # Read-model asks (bare domain form, no "::") have no
92
+ # reference twin at all — never attempted, not "attempted and
93
+ # agreed."
94
+ has_reference = question.include?("::")
95
+ reference_rows = reference_error = nil
96
+ if has_reference
97
+ begin
98
+ reference_rows = runtime.reference_query(question, **args)
99
+ rescue *Runtime::DOMAIN_REFUSALS, Bluebook::Expression::EvaluationError => e
100
+ reference_error = e
101
+ end
102
+ end
103
+
104
+ entry = { query: question, args: args, rows: native_rows, instances_at: snapshot_instances(runtime) }
105
+ entry[:error] = native_error.message if native_error
106
+ if has_reference
107
+ entry[:reference_rows] = reference_rows
108
+ entry[:reference_error] = reference_error.message if reference_error
109
+ end
110
+ queries << entry
111
+
112
+ refusals << { verb: question, error: native_error.message, kind: refusal_kind(native_error) } if native_error
113
+ next
114
+ end
115
+
116
+ # `dry_run` is evaluated hypothetically and recorded, never a refusal.
117
+ # `dry_runs`' shape (`{verb:, ok:, error?:}`) is compared against
118
+ # `kernel/cli.rs`'s own answer; `dry_run_traces` is Ruby-only oracle data.
119
+ if (hypothetical = step["dry_run"])
120
+ before = { instances: snapshot_instances(runtime), events: runtime.events.size }
121
+ entry = { verb: hypothetical }
122
+ begin
123
+ as_step_caller(step) { runtime.dry_run?(hypothetical, **args) }
124
+ entry[:ok] = true
125
+ rescue *Runtime::DOMAIN_REFUSALS, Bluebook::Expression::EvaluationError => e
126
+ entry.merge!(ok: false, error: e.message)
127
+ end
128
+ after = { instances: snapshot_instances(runtime), events: runtime.events.size }
129
+ dry_runs << entry
130
+ dry_run_traces << entry.merge(before: before, after: after)
131
+ next
132
+ end
133
+
134
+ begin
135
+ # Taken before dispatch, so this step's own reactions can be sliced out
136
+ # after and matched against an independent recomputation of what a
137
+ # `for_each` policy should have fanned out over.
138
+ reaction_mark = runtime.reactions.size
139
+
140
+ # `saga_log_mark` slices `runtime.sagas` the same way `reaction_mark`
141
+ # slices `runtime.reactions`. `outbox_before_ids` is a set of delivery_ids,
142
+ # not a size, because `runtime.outbox.rows` concatenates several stores'
143
+ # own arrays, so a plain "grew from N to M" tail slice could miss or
144
+ # misattribute rows once more than one repository has an outbox.
145
+ saga_log_mark = runtime.sagas.size
146
+ outbox_before_ids = runtime.outbox.rows.map(&:delivery_id)
147
+
148
+ # The snapshot a `for_each` query would have seen, taken before dispatch:
149
+ # `deliver_for_each` runs its query synchronously inside this same
150
+ # dispatch, so reading the live repository after would see what the
151
+ # fan-out's own dispatched commands already mutated, not what it matched.
152
+ fan_out_snapshot = fan_out_targets.each_with_object({}) do |((fdomain, faggregate_name), aggregate), snap|
153
+ next unless aggregate
154
+
155
+ snap[[fdomain, faggregate_name]] =
156
+ runtime.registry.repository(fdomain, aggregate).all.to_h { |record| [record.id, record.state.dup] }
157
+ end
158
+
159
+ # Same idiom as fan_out_snapshot above: the guard is replayed read-only
160
+ # before this step's real dispatch can mutate anything a cross-aggregate
161
+ # given dereferences.
162
+ guard_check = build_guard_check(runtime, step["verb"], args)
163
+
164
+ # Same idiom again: the entity element a mutation step's args address is
165
+ # snapshotted before dispatch, so it can be diffed against its post-dispatch state.
166
+ mutation_trace = build_mutation_trace(runtime, step["verb"], args)
167
+
168
+ # `role:`/`actor_id:` are optional per-step keys; a step with neither
169
+ # dispatches bare, as every existing corpus step always has. Binds the
170
+ # ambient caller for exactly this one dispatch, then unbinds, so
171
+ # back-to-back steps with different (or no) `role:` never leak into each other.
172
+ result = as_step_caller(step) { runtime.dispatch_flat(step["verb"], args) }
173
+
174
+ fan_outs.concat(fan_out_findings(runtime, fan_out_snapshot, result.events, runtime.reactions[reaction_mark..]))
175
+
176
+ # Every outbox row this step's own dispatch newly wrote, across every
177
+ # bound repository including any a reaction cascade touched, paired with
178
+ # the reaction/saga rows that same dispatch produced. Skipped when empty.
179
+ outbox_new_rows = runtime.outbox.rows.reject { |row| outbox_before_ids.include?(row.delivery_id) }
180
+ if outbox_new_rows.any?
181
+ outbox_traces << { verb: step["verb"], rows: outbox_new_rows.map(&:to_h),
182
+ reactions: runtime.reactions[reaction_mark..].dup,
183
+ sagas: runtime.sagas[saga_log_mark..].dup }
184
+ end
185
+
186
+ guard_checks << guard_check.merge(actual_refused: false, actual_kind: nil) if guard_check
187
+ # After — only on success ; a refused step mutated nothing,
188
+ # so there is no "after" to compare (and #build_mutation_
189
+ # trace already skipped anything with no mutations to
190
+ # trace in the first place).
191
+ mutation_traces << mutation_trace.merge(after: read_mutation_after(runtime, mutation_trace)) if mutation_trace
192
+ rescue *Runtime::DOMAIN_REFUSALS, Bluebook::Expression::EvaluationError => e
193
+ # `kind:` is the raised class, not derived from the message: several
194
+ # refusal templates share the same wording, so only the class tells a
195
+ # guard refusal apart from the rest.
196
+ refusals << { verb: step["verb"], error: e.message, kind: refusal_kind(e) }
197
+ # Only a refusal raised by the guard itself counts here. A refusal from a
198
+ # stage before or after enforce_givens can share TypeMismatch's class, so
199
+ # anything outside the two guard classes is left out — inconclusive, not a pass.
200
+ if guard_check && GUARD_REFUSAL_CLASSES.include?(e.class)
201
+ guard_checks << guard_check.merge(actual_refused: true,
202
+ actual_kind: e.class.name)
203
+ end
204
+ end
205
+ end
206
+
207
+ instances = snapshot_instances(runtime)
208
+
209
+ events = runtime.events.map do |event|
210
+ { name: event.name, aggregate: event.aggregate, id: event.id, payload: event.payload }
211
+ end
212
+
213
+ # The live process-manager store, materialised to inert data (`{ pm_name =>
214
+ # { correlation => { state:, memory: } } }`), captured here because the
215
+ # runtime goes out of scope with the boot and the Memory rebind leaves no
216
+ # real saga store for Properties.sagas_rehydrate_cleanly to read otherwise.
217
+ saga_instances = runtime.registry.saga_instances.each_with_object({}) do |(pm_name, conversations), out|
218
+ out[pm_name] = conversations.each_with_object({}) do |(correlation, instance), rows|
219
+ rows[correlation] = { state: instance[:state], memory: Runtime::Value.materialize(instance[:memory]) }
220
+ end
221
+ end
222
+
223
+ # The booted chapter rides along so properties.rb's lifecycle/saga checks
224
+ # have the declared IR beside the history it produced. `bluebook:` (singular)
225
+ # is the first-loaded chapter, kept for properties scoped to it deliberately;
226
+ # `bluebooks:` (plural) is the full domain-name-keyed map, for a property that
227
+ # needs to resolve a verb back to its own declaring bluebook.
228
+ history = { instances: instances, events: events, refusals: refusals,
229
+ reactions: runtime.reactions, sagas: runtime.sagas, saga_instances: saga_instances,
230
+ queries: queries, dry_runs: dry_runs, dry_run_traces: dry_run_traces,
231
+ fan_outs: fan_outs, guard_checks: guard_checks,
232
+ mutation_traces: mutation_traces, outbox_traces: outbox_traces,
233
+ saga_dispatches: runtime.saga_dispatches, policy_dispatches: runtime.policy_dispatches,
234
+ bluebook: runtime.registry.bluebooks.values.first,
235
+ bluebooks: runtime.registry.bluebooks.dup }
236
+
237
+ # `runtime` is still live here — the one and only place it is — so
238
+ # SelfConsistency runs now, against the same registry a second boot couldn't reuse.
239
+ history[:self_consistency] = SelfConsistency.check(runtime, history) if self_consistency
240
+
241
+ history
242
+ end
243
+ end
244
+
245
+ # Runs the block with this step's `role:`/`actor_id:` bound as the ambient
246
+ # caller, if it declares one; a step with no `role:` dispatches bare.
247
+ def as_step_caller(step, &)
248
+ return yield unless step["role"]
249
+
250
+ Hecks.as_caller(role: step["role"], actor_id: step["actor_id"], &)
251
+ end
252
+
253
+ # Every persisted record's state, keyed by `"Domain::Aggregate#id"`. Called once
254
+ # per query step, not only at the end, so a property recomputing "the eligible
255
+ # rows" reads the state as that query actually saw it, not a shared final one.
256
+ def snapshot_instances(runtime)
257
+ instances = {}
258
+ runtime.registry.bluebooks.each do |domain_name, bluebook|
259
+ bluebook.aggregates.each do |aggregate|
260
+ runtime.registry.repository(domain_name, aggregate).all.each do |record|
261
+ instances["#{domain_name}::#{aggregate.name}##{record.id}"] = record.state
262
+ end
263
+ end
264
+ end
265
+ instances
266
+ end
267
+
268
+ # Resolves the record (if any) this step's guard should be checked against, and
269
+ # replays enforce_givens then admissible_transition, in that order, to mirror
270
+ # the real DISPATCH_ORDER. `nil` for anything out of scope.
271
+ # rubocop:disable-next Metrics/AbcSize
272
+ # rubocop:disable-next Metrics/CyclomaticComplexity
273
+ # rubocop:disable-next Metrics/PerceivedComplexity
274
+ def build_guard_check(runtime, verb, args)
275
+ domain_name, aggregate_name, command_name = Naming.split_verb(verb)
276
+ return nil unless command_name && !command_name.include?(".")
277
+
278
+ aggregate = runtime.registry.bluebook(domain_name)&.aggregate(aggregate_name)
279
+ command = aggregate&.command(command_name)
280
+ return nil unless aggregate && command && !command.creates?
281
+
282
+ # A transition-only guard (no per-command `from:`/`given`, only an aggregate
283
+ # `lifecycle` block naming this command) still counts as something to check,
284
+ # now that `admissible_transition` is reproduced below.
285
+ has_transition = aggregate.lifecycle&.transitions_for(command.hecks_name)&.any?
286
+ return nil if command.givens.empty? && !command.from && !has_transition
287
+
288
+ reference_key = command.references.to_s.empty? ? nil : Naming.reference_key(command.references)
289
+ id = Runtime::Identity.of(aggregate, args) ||
290
+ Runtime::Identity.from(aggregate, args, :id) ||
291
+ (reference_key && Runtime::Identity.from(aggregate, args, reference_key))
292
+ return nil unless id
293
+
294
+ record = runtime.registry.repository(domain_name, aggregate).find(id)
295
+ return nil unless record
296
+
297
+ rules = Runtime::CommandRules.new(runtime.registry)
298
+ # Real dispatch normalizes args (step_normalize_args) before it ever reaches
299
+ # enforce_givens, coercing a bare scalar like "white" into its command's declared
300
+ # value-object shape. Skipping that here would hand enforce_givens the raw fuzzed
301
+ # shape instead, and a `given` comparing a normalized field against it would walk a
302
+ # path a String happens to answer (a substring lookup) rather than the refusal real
303
+ # dispatch raises. `send` reaches the interpreter's own private coercion (the same
304
+ # idiom SelfConsistency#last_advancing_event uses for `saga_correlation`) rather than
305
+ # reimplementing it here.
306
+ interpreter = Runtime::CommandInterpreter.new(runtime.registry, rules: rules)
307
+ normalized_args = interpreter.send(:normalize_args, aggregate, command, args)
308
+ recomputed_kind = begin
309
+ rules.enforce_givens(record.dup, command, normalized_args, domain: domain_name, declaring: aggregate)
310
+
311
+ # A second, separate DISPATCH_ORDER step: the aggregate's own `lifecycle`
312
+ # transition guard is a wholly different method from enforce_givens' own
313
+ # per-command `from:` check, called only when enforce_givens didn't already refuse.
314
+ # It reads only the record's own lifecycle field and the command's declared
315
+ # transitions, never `args`, so it needs no normalized input of its own.
316
+ rules.admissible_transition(aggregate, command, record.dup)
317
+ nil
318
+ rescue *GUARD_REFUSAL_CLASSES => e
319
+ e.class.name
320
+ end
321
+
322
+ { verb: verb, domain: domain_name, aggregate: aggregate_name, command: command_name, id: id,
323
+ recomputed_refused: !recomputed_kind.nil?, recomputed_kind: recomputed_kind }
324
+ rescue StandardError
325
+ nil
326
+ end
327
+
328
+ # Scoped to entity-dispatched commands only, the one place `append`/`remove`/
329
+ # `multiply`/`clamp` act on an entity's own attributes. `nil` for anything out of
330
+ # scope: an aggregate-level command, no mutations, or unresolvable identity args.
331
+ # rubocop:disable-next Metrics/AbcSize
332
+ # rubocop:disable-next Metrics/CyclomaticComplexity
333
+ # rubocop:disable-next Metrics/PerceivedComplexity
334
+ def build_mutation_trace(runtime, verb, args)
335
+ domain_name, aggregate_name, command_name = Naming.split_verb(verb)
336
+ return nil unless command_name&.include?(".")
337
+
338
+ aggregate = runtime.registry.bluebook(domain_name)&.aggregate(aggregate_name)
339
+ return nil unless aggregate
340
+
341
+ entity_name, entity_command_name = command_name.split(".", 2)
342
+ entity = aggregate.entities.find { |candidate| candidate.hecks_name == entity_name }
343
+ command = entity&.command(entity_command_name)
344
+ return nil unless command&.mutations&.any?
345
+
346
+ # Same idiom as build_guard_check above: real dispatch normalizes args
347
+ # (step_normalize_args) before apply_mutations ever runs, coercing a declared
348
+ # payload attribute into its command's shape — a composite argument's own
349
+ # `Value.build` fills that shape's declared defaults along the way. Recomputing
350
+ # a mutation against the raw fuzzed args instead would disagree with real
351
+ # dispatch whenever a caller omitted a defaulted field, the same false-divergence
352
+ # shape build_guard_check guards against for the guard check.
353
+ args = Runtime::CommandInterpreter.new(runtime.registry, rules: Runtime::CommandRules.new(runtime.registry))
354
+ .send(:normalize_args, aggregate, command, args)
355
+
356
+ reference_key = command.references.to_s.empty? ? nil : Naming.reference_key(command.references)
357
+ parent_id = Runtime::Identity.of(aggregate, args) ||
358
+ Runtime::Identity.from(aggregate, args, :id) ||
359
+ (reference_key && Runtime::Identity.from(aggregate, args, reference_key))
360
+ return nil unless parent_id
361
+
362
+ record = runtime.registry.repository(domain_name, aggregate).find(parent_id)
363
+ return nil unless record
364
+
365
+ list_attr = aggregate.attributes.find { |a| a.list? && a.type.to_s == entity.hecks_name }
366
+ return nil unless list_attr
367
+
368
+ wants = entity.identity_paths.map do |path|
369
+ head = path.to_s.split(".").first.to_sym
370
+ raw = args[head]
371
+ return nil if raw.nil?
372
+
373
+ [head, Runtime::Value.for_attribute(aggregate, entity.attribute(head), raw)]
374
+ end
375
+
376
+ element = Array(record.state[list_attr.name]).find { |el| wants.all? { |head, want| el[head] == want } }
377
+ return nil unless element
378
+
379
+ { verb: verb, domain: domain_name, aggregate: aggregate_name, command: command_name,
380
+ parent_id: parent_id, list_attr: list_attr.name, element_wants: wants,
381
+ before: Runtime::Value.materialize(element), args: args }
382
+ rescue StandardError
383
+ nil
384
+ end
385
+
386
+ # Re-locates the same element by identity, not position, since a mutation could
387
+ # have changed the array's own length or order. `nil` if it vanished.
388
+ def read_mutation_after(runtime, trace)
389
+ aggregate = runtime.registry.bluebook(trace[:domain])&.aggregate(trace[:aggregate])
390
+ return nil unless aggregate
391
+
392
+ record = runtime.registry.repository(trace[:domain], aggregate).find(trace[:parent_id])
393
+ return nil unless record
394
+
395
+ element = Array(record.state[trace[:list_attr]]).find do |el|
396
+ trace[:element_wants].all? { |head, want| el[head] == want }
397
+ end
398
+ element && Runtime::Value.materialize(element)
399
+ rescue StandardError
400
+ nil
401
+ end
402
+
403
+ # One finding per (event, for_each policy) pair, independent of
404
+ # `PolicyInterpreter#deliver_for_each`, answered via `Ports::Query::InMemory`
405
+ # directly rather than `QueryInterpreter`, so this stays blind to nothing the
406
+ # fan-out feature adds. `expected_row_ids` is `nil` when `where` did not hold.
407
+ def fan_out_findings(runtime, snapshot, announced, reactions_since)
408
+ announced.each_with_object([]) do |event, findings|
409
+ # `event.aggregate` is domain-qualified ("Banking::Account"); split the same
410
+ # two ways `PolicyInterpreter#policies_for` does.
411
+ domain = event.aggregate.to_s.split("::").first
412
+ bluebook = runtime.registry.bluebook(domain)
413
+ next unless bluebook
414
+
415
+ emitting = Naming.demodulise(event.aggregate)
416
+
417
+ bluebook.policies.each do |policy|
418
+ next unless policy.fans_out? && policy.event_name == event.name
419
+ next unless policy.event_qualifier.nil? || policy.event_qualifier == emitting
420
+
421
+ findings << fan_out_finding(runtime, snapshot, policy, event, domain, reactions_since)
422
+ end
423
+ end
424
+ end
425
+
426
+ # Builds one fan-out finding, comparing one `for_each` policy's independently
427
+ # recomputed expected rows against what actually reacted for one event.
428
+ def fan_out_finding(runtime, snapshot, policy, event, domain, reactions_since)
429
+ payload = event.payload.transform_keys(&:to_sym)
430
+ held = policy.where.to_s.empty? ||
431
+ Bluebook::Expression::Evaluator.call(policy.where, {}, payload)
432
+
433
+ expected = held ? expected_fan_out_rows(runtime, snapshot, policy, domain, payload) : nil
434
+
435
+ actual = reactions_since.select { |r| r[:policy] == policy.name && r[:on] == event.name }
436
+ .filter_map { |r| r[:for_row] }
437
+
438
+ { policy: policy.name, on: event.name, expected_row_ids: expected, actual_row_ids: actual }
439
+ end
440
+
441
+ # `policy.for_each`'s declared query, answered against the pre-dispatch snapshot
442
+ # (the real fan-out's query runs before its own dispatched commands can mutate
443
+ # anything it would match, so this must read the same "before" state). A `Symbol`
444
+ # where-value binds to the triggering event's own payload; a literal is compared as declared.
445
+ def expected_fan_out_rows(runtime, snapshot, policy, domain, payload)
446
+ query_domain, aggregate_name, query_name = policy.for_each_route(domain)
447
+ aggregate = runtime.registry.bluebook(query_domain)&.aggregate(aggregate_name)
448
+ query = aggregate&.query(query_name)
449
+ return [] unless query
450
+
451
+ rows = snapshot[[query_domain, aggregate_name]] || {}
452
+ matched = rows.select do |_id, state|
453
+ query.wheres.all? do |clause|
454
+ held = Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(state, clause.field))
455
+ Ports::Query::InMemory.holds?(clause, held, payload)
456
+ end
457
+ end
458
+
459
+ matched.keys.map(&:to_s).sort
460
+ end
461
+
462
+ # Names the outcome class a recorded refusal row should carry: an evaluation
463
+ # fault is `"Fault"` (matching the Rust kernel's `Refusal::Fault`), never a raw
464
+ # Ruby exception name.
465
+ def refusal_kind(error)
466
+ error.is_a?(Bluebook::Expression::EvaluationError) ? "Fault" : error.class.name
467
+ end
468
+
469
+ # Answers one ad hoc filter step, the mirror image of kernel/cli.rs's own
470
+ # `run_filter`, calling the same production `Ports::Query::InMemory` rather than
471
+ # re-deriving comparator behavior by hand. Sorted by id ascending regardless,
472
+ # since an ad hoc filter declares no order of its own.
473
+ #
474
+ # @raise [Bluebook::Expression::EvaluationError] if `op` or `"aggregate"` is unknown
475
+ def run_filter(runtime, filter)
476
+ aggregate_ref = filter["aggregate"].to_s
477
+ field = filter["field"].to_s
478
+ op = filter["op"].to_s
479
+ value = filter["value"]
480
+
481
+ # A malformed ad-hoc ask is a fault (C8.3), not a bare RuntimeError.
482
+ unless FILTER_COMPARATORS.include?(op)
483
+ raise Bluebook::Expression::EvaluationError, "unknown query comparator #{op.inspect}"
484
+ end
485
+
486
+ domain_name, aggregate_name = aggregate_ref.split("::", 2)
487
+ aggregate = runtime.registry.bluebook(domain_name)&.aggregate(aggregate_name)
488
+ raise Bluebook::Expression::EvaluationError, "unknown aggregate #{aggregate_ref.inspect}" unless aggregate
489
+
490
+ clause = QuerySpecification::Common::WhereClause.new(field: field, op: op, value: value)
491
+ records = runtime.registry.repository(domain_name, aggregate).all
492
+ matched = records.select do |record|
493
+ held = Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(record, field))
494
+ Ports::Query::InMemory.holds?(clause, held, {})
495
+ end
496
+
497
+ matched.sort_by { |record| record.id.to_s }.map { |record| { id: record.id }.merge(record.state) }
498
+ end
499
+
500
+ # Builds the `refusals` entry's own "verb" column for a refused ad hoc filter,
501
+ # which carries no real verb to report.
502
+ def filter_label(filter) = "filter #{filter['aggregate']}.#{filter['field']} #{filter['op']}"
503
+ end
504
+ end
505
+ end
@@ -0,0 +1,48 @@
1
+ module Hecks
2
+ module Fuzzing
3
+ # Weighted pick over `QualityControl::Target.Rotation` rows: staleness blended with yield.
4
+ # Pure: no clock, no queries; `now` is an argument.
5
+ module RotationPriority
6
+ module_function
7
+
8
+ # The next stored `Target.yield_score`: decayed old score plus this period's surprises.
9
+ # Integer division, like every other count in the ledger.
10
+ #
11
+ # @param old_score [Integer] the score before this period
12
+ # @param surprises_this_period [Integer] Surprised checks found this period
13
+ # @param decay_percent [Integer] percent of `old_score` that survives
14
+ # @return [Integer] the next score to store
15
+ # @raise [ArgumentError] if either count is negative
16
+ def next_yield_score(old_score:, surprises_this_period:,
17
+ decay_percent: QualityControlDials::YIELD_DECAY_PERCENT)
18
+ raise ArgumentError, "old_score must not be negative" if old_score.negative?
19
+ raise ArgumentError, "surprises_this_period must not be negative" if surprises_this_period.negative?
20
+
21
+ ((old_score * decay_percent) / 100) + surprises_this_period
22
+ end
23
+
24
+ # Picks the next target from `Target.Rotation` rows, or nil when there are none.
25
+ #
26
+ # A row stale past `floor_seconds` wins outright (oldest first), so no target starves.
27
+ # Otherwise the highest `staleness + yield_score * weight_seconds` wins.
28
+ #
29
+ # @param rows [Array<Hash>] rows with `:last_swept`, `:yield_score` as `{ value: Integer }`
30
+ # @param now [Integer] current time, Unix epoch seconds
31
+ # @param weight_seconds [Integer] seconds of staleness one yield point is worth
32
+ # @param floor_seconds [Integer] staleness past which a row jumps the queue
33
+ # @return [Hash, nil] the picked row
34
+ def pick(rows, now:, weight_seconds: QualityControlDials::YIELD_WEIGHT_SECONDS,
35
+ floor_seconds: QualityControlDials::ROTATION_STALE_FLOOR_SECONDS)
36
+ return nil if rows.empty?
37
+
38
+ floored = rows.select { |row| staleness(row, now) >= floor_seconds }
39
+ return floored.min_by { |row| row[:last_swept][:value] } unless floored.empty?
40
+
41
+ rows.max_by { |row| staleness(row, now) + (row[:yield_score][:value] * weight_seconds) }
42
+ end
43
+
44
+ def staleness(row, now) = now - row[:last_swept][:value]
45
+ private_class_method :staleness
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,108 @@
1
+ require "json"
2
+ require "hecks/naming"
3
+
4
+ module Hecks
5
+ module Fuzzing
6
+ # What a compiled Rust binary declares it did not generate, read from the generated
7
+ # `manifest.json` files rather than inferred from Rust's refusal wording.
8
+ #
9
+ # The differential fuzzer drops a query or read-model verb only when a manifest says
10
+ # `generated: false`; any other refusal stays in the comparison and fails it.
11
+ #
12
+ # A binary's manifests are its own `src/generated/<feature>/` plus every shared chapter
13
+ # (a generated directory with no `merged.rs`). A missing manifest tolerates nothing.
14
+ class RustGapManifest
15
+ # Commands change state, so dropping their refusal would make the comparison dishonest.
16
+ TOLERABLE_KINDS = %w[query read_model].freeze
17
+ PINNED_BINARY = %r{\A(?<rust_dir>.+)/target/debug/rust-(?<feature>[a-z0-9_]+)\z}
18
+
19
+ attr_reader :rust_dir, :feature, :entries
20
+
21
+ # Builds the manifest reader for the pinned conformance binary at `binary`.
22
+ #
23
+ # @param binary [String] path to a pinned binary, `<rust_dir>/target/debug/rust-<feature>`
24
+ # @return [Fuzzing::RustGapManifest] the manifest reader for that binary's feature
25
+ # @raise [ArgumentError] if `binary` does not match the pinned-binary path shape
26
+ def self.for_binary(binary)
27
+ match = PINNED_BINARY.match(File.expand_path(binary.to_s))
28
+ unless match
29
+ raise ArgumentError, "#{binary.inspect} is not a pinned conformance binary " \
30
+ "(<rust_dir>/target/debug/rust-<feature>) — cannot tell which manifest describes it"
31
+ end
32
+
33
+ new(rust_dir: match[:rust_dir], feature: match[:feature])
34
+ end
35
+
36
+ # Every committed manifest entry under `rust_dir`, each tagged with its generated module.
37
+ #
38
+ # @param rust_dir [String] path to the Rust project root (holds `src/generated/*/`)
39
+ # @return [Array<Hash>] String-keyed entries, each plus `"module" => String`
40
+ def self.all_entries(rust_dir)
41
+ Dir.glob(File.join(rust_dir, "src/generated/*/manifest.json")).flat_map do |path|
42
+ module_name = File.basename(File.dirname(path))
43
+ JSON.parse(File.read(path)).map { |entry| entry.merge("module" => module_name) }
44
+ end
45
+ end
46
+
47
+ # @param rust_dir [String] path to the Rust project root (holds `src/generated/*/`)
48
+ # @param feature [String] the domain feature this manifest reader describes
49
+ def initialize(rust_dir:, feature:)
50
+ @rust_dir = rust_dir
51
+ @feature = feature
52
+ @entries = manifest_paths.flat_map { |path| JSON.parse(File.read(path)) }.freeze
53
+ @not_generated = index_not_generated
54
+ end
55
+
56
+ # The manifest entry declaring `verb` not generated, or nil. An ad hoc filter (a Hash)
57
+ # is never declared.
58
+ #
59
+ # @param verb [String, Object] the wire-spelled query or read-model verb
60
+ # @return [Hash, nil] the String-keyed manifest entry
61
+ def not_generated(verb)
62
+ return nil unless verb.is_a?(String)
63
+
64
+ @not_generated[verb]
65
+ end
66
+
67
+ # Answers whether `verb` is declared not generated by this manifest.
68
+ #
69
+ # @param verb [String, Object] the wire-spelled query or read-model verb to look up
70
+ # @return [Boolean] whether the manifest declares `verb` not generated
71
+ def not_generated?(verb) = !not_generated(verb).nil?
72
+
73
+ # Lists every verb this manifest declares not generated.
74
+ #
75
+ # @return [Set<String>] every verb the manifest declares not generated
76
+ def not_generated_verbs = @not_generated.keys.to_set
77
+
78
+ private
79
+
80
+ def manifest_paths
81
+ generated = File.join(rust_dir, "src/generated")
82
+ own = File.join(generated, feature, "manifest.json")
83
+ chapters = Dir.glob(File.join(generated, "*/manifest.json")).reject do |path|
84
+ dir = File.dirname(path)
85
+ File.basename(dir) == feature || File.exist?(File.join(dir, "merged.rs"))
86
+ end
87
+ ([own] + chapters.sort).select { |path| File.exist?(path) }
88
+ end
89
+
90
+ def index_not_generated
91
+ @entries.each_with_object({}) do |entry, index|
92
+ next unless TOLERABLE_KINDS.include?(entry["kind"]) && entry["generated"] == false
93
+
94
+ wire_verbs(entry).each { |verb| index[verb] = entry }
95
+ end.freeze
96
+ end
97
+
98
+ # A read model's id is "Domain::Name" but it is asked as "Domain.Name" or
99
+ # "Domain.snake_name" (the spellings `kernel::read_model::find` accepts).
100
+ def wire_verbs(entry)
101
+ return [entry["id"]] if entry["kind"] == "query"
102
+
103
+ domain, name = entry["id"].split("::", 2)
104
+ ["#{domain}.#{name}", "#{domain}.#{Hecks::Naming.snake(name)}"].uniq
105
+ end
106
+ end
107
+ end
108
+ end