hecks 2.9.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (600) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +51 -6
  3. data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
  4. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  5. data/lib/hecks/adapters/driven/lambda.rb +1 -1
  6. data/lib/hecks/adapters/driven/memory.rb +16 -3
  7. data/lib/hecks/adapters/driven/postgres/reconnect.rb +6 -2
  8. data/lib/hecks/adapters/driven/postgres/shared_connection.rb +180 -0
  9. data/lib/hecks/adapters/driven/postgres.rb +13 -4
  10. data/lib/hecks/adapters/driven/prism.rb +41 -8
  11. data/lib/hecks/adapters/driven/tenant_provisioner.rb +73 -16
  12. data/lib/hecks/behaviors/expectations.rb +1 -1
  13. data/lib/hecks/behaviors.rb +1 -1
  14. data/lib/hecks/bench/cli.rb +62 -0
  15. data/lib/hecks/bench/environment.rb +71 -0
  16. data/lib/hecks/bench/postgres_probe.rb +30 -0
  17. data/lib/hecks/bench/report.rb +76 -0
  18. data/lib/hecks/bench/ruby_runner.rb +93 -0
  19. data/lib/hecks/bench/run.rb +55 -0
  20. data/lib/hecks/bench/rust_runner.rb +101 -0
  21. data/lib/hecks/bench/stats.rb +46 -0
  22. data/lib/hecks/bench/suite.rb +98 -0
  23. data/lib/hecks/bench/workload.rb +89 -0
  24. data/lib/hecks/bench.rb +17 -0
  25. data/lib/hecks/bluebook/assembly/contracts.rb +2 -0
  26. data/lib/hecks/bluebook/attribute.rb +1 -1
  27. data/lib/hecks/bluebook/behaviour/aggregate.rb +30 -1
  28. data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -0
  29. data/lib/hecks/bluebook/chapter.rb +6 -2
  30. data/lib/hecks/bluebook/domain_port.rb +36 -5
  31. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +10 -0
  32. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +8 -0
  33. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -1
  34. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +31 -3
  35. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +122 -3
  36. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +1 -1
  37. data/lib/hecks/bluebook/dsl/query_builder.rb +27 -1
  38. data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -1
  39. data/lib/hecks/bluebook/dsl/world_builder.rb +29 -2
  40. data/lib/hecks/bluebook/expression/canonical_form.rb +1 -1
  41. data/lib/hecks/bluebook/expression/resolver.rb +3 -2
  42. data/lib/hecks/bluebook/hexagon.rb +13 -5
  43. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -0
  44. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +4 -1
  45. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +246 -0
  46. data/lib/hecks/bluebook/meta_validator.rb +9 -4
  47. data/lib/hecks/bluebook/model_check.rb +23 -0
  48. data/lib/hecks/bluebook/pattern_subset.rb +38 -0
  49. data/lib/hecks/bluebook/query.rb +20 -5
  50. data/lib/hecks/bluebook/smoke_test.rb +1 -1
  51. data/lib/hecks/canonical_json.rb +30 -0
  52. data/lib/hecks/chapters.rb +102 -0
  53. data/lib/hecks/cli/behaviors.rb +61 -0
  54. data/lib/hecks/cli/canonicalise.rb +25 -0
  55. data/lib/hecks/cli/check_engine_agreement.rb +32 -0
  56. data/lib/hecks/cli/check_era.rb +88 -0
  57. data/lib/hecks/cli/console.rb +66 -0
  58. data/lib/hecks/cli/deploy_template_diff.rb +40 -0
  59. data/lib/hecks/cli/doc_coverage.rb +32 -0
  60. data/lib/hecks/cli/document.rb +2 -2
  61. data/lib/hecks/cli/expression_projection.rb +41 -0
  62. data/lib/hecks/cli/follow.rb +88 -0
  63. data/lib/hecks/cli/history.rb +44 -0
  64. data/lib/hecks/cli/ir.rb +1 -1
  65. data/lib/hecks/cli/mcp.rb +3 -3
  66. data/lib/hecks/cli/model_check.rb +15 -8
  67. data/lib/hecks/cli/pattern_cases.rb +47 -0
  68. data/lib/hecks/cli/present.rb +39 -0
  69. data/lib/hecks/cli/project_cli.rb +224 -32
  70. data/lib/hecks/cli/project_diagrams.rb +4 -3
  71. data/lib/hecks/cli/project_glossary.rb +44 -0
  72. data/lib/hecks/cli/reference.rb +46 -0
  73. data/lib/hecks/cli/refresh_projections.rb +44 -0
  74. data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +150 -0
  75. data/lib/hecks/cli/release.rb +70 -0
  76. data/lib/hecks/cli/release_gem.rb +59 -0
  77. data/lib/hecks/cli/rspec_io_parallel_files.rb +137 -0
  78. data/lib/hecks/cli/rspec_shard_files.rb +66 -0
  79. data/lib/hecks/cli/run.rb +3 -3
  80. data/lib/hecks/cli/seed_semantics_corpus.rb +71 -0
  81. data/lib/hecks/cli/serve_query_ir_mcp.rb +30 -0
  82. data/lib/hecks/cli/shape.rb +70 -0
  83. data/lib/hecks/cli/smoke_http.rb +230 -0
  84. data/lib/hecks/cli/smoke_test.rb +5 -3
  85. data/lib/hecks/cli/spec_example.rb +27 -0
  86. data/lib/hecks/cli/statements.rb +32 -0
  87. data/lib/hecks/cli/stores.rb +4 -5
  88. data/lib/hecks/cli/stress_concurrency_specs.rb +177 -0
  89. data/lib/hecks/cli.rb +201 -52
  90. data/lib/hecks/codemod.rb +324 -0
  91. data/lib/hecks/corpus.rb +423 -0
  92. data/lib/hecks/deploy/bluebook/deploy.bluebook +591 -23
  93. data/lib/hecks/deploy/bluebook/deploy.hecksagon +41 -4
  94. data/lib/hecks/deploy/oidc.json +82 -2
  95. data/lib/hecks/doc/reference.rb +351 -0
  96. data/lib/hecks/doors/cli_door.rb +196 -0
  97. data/lib/hecks/doors/cli_runner.rb +297 -0
  98. data/lib/hecks/{facade → doors}/command_request.rb +1 -1
  99. data/lib/hecks/{facade → doors}/handle.rb +2 -2
  100. data/lib/hecks/{facade → doors}/json_door.rb +4 -4
  101. data/lib/hecks/doors/launcher_options.rb +129 -0
  102. data/lib/hecks/{cli → doors}/mcp_door.rb +7 -7
  103. data/lib/hecks/doors/mcp_door_scope.rb +97 -0
  104. data/lib/hecks/{facade/surface → doors/ruby_door}/aggregate_door.rb +8 -3
  105. data/lib/hecks/{facade/surface → doors/ruby_door}/chapter.rb +3 -3
  106. data/lib/hecks/{facade/surface.rb → doors/ruby_door.rb} +36 -5
  107. data/lib/hecks/doors.rb +61 -0
  108. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +1 -1
  109. data/lib/hecks/embryonaut_bluebook.rb +1 -1
  110. data/lib/hecks/engine_agreement.rb +156 -0
  111. data/lib/hecks/facade.rb +24 -37
  112. data/lib/hecks/forms/app.rb +6 -6
  113. data/lib/hecks/forms/banking_presentation.rb +50 -0
  114. data/lib/hecks/forms/field_shape.rb +2 -1
  115. data/lib/hecks/forms/port_argument.rb +1 -1
  116. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +393 -0
  117. data/lib/hecks/fuzzing/combination_miner.rb +86 -0
  118. data/lib/hecks/fuzzing/concurrent_dispatch.rb +214 -0
  119. data/lib/hecks/fuzzing/coverage_campaign.rb +143 -0
  120. data/lib/hecks/fuzzing/differential.rb +156 -0
  121. data/lib/hecks/fuzzing/domain_generator.rb +615 -0
  122. data/lib/hecks/fuzzing/era_boundary.rb +71 -0
  123. data/lib/hecks/fuzzing/form_census.rb +136 -0
  124. data/lib/hecks/fuzzing/generated_domain_check.rb +93 -0
  125. data/lib/hecks/fuzzing/invalid_value_generator.rb +64 -0
  126. data/lib/hecks/fuzzing/isolated_boot.rb +301 -0
  127. data/lib/hecks/fuzzing/nondeterministic.rb +51 -0
  128. data/lib/hecks/fuzzing/outside_world.rb +45 -0
  129. data/lib/hecks/fuzzing/persistence_parity.rb +102 -0
  130. data/lib/hecks/fuzzing/properties/corrections.rb +88 -0
  131. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +314 -0
  132. data/lib/hecks/fuzzing/properties/guards.rb +190 -0
  133. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +300 -0
  134. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +93 -0
  135. data/lib/hecks/fuzzing/properties/outbox.rb +89 -0
  136. data/lib/hecks/fuzzing/properties/querying.rb +175 -0
  137. data/lib/hecks/fuzzing/properties.rb +162 -0
  138. data/lib/hecks/fuzzing/qa_settings.rb +123 -0
  139. data/lib/hecks/fuzzing/replay.rb +505 -0
  140. data/lib/hecks/fuzzing/rotation_priority.rb +48 -0
  141. data/lib/hecks/fuzzing/rust_gap_manifest.rb +108 -0
  142. data/lib/hecks/fuzzing/self_consistency.rb +339 -0
  143. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +385 -0
  144. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +132 -0
  145. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +95 -0
  146. data/lib/hecks/fuzzing/sequence_generator/picker.rb +77 -0
  147. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +109 -0
  148. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +237 -0
  149. data/lib/hecks/fuzzing/sequence_generator.rb +189 -0
  150. data/lib/hecks/fuzzing/shrinker.rb +166 -0
  151. data/lib/hecks/fuzzing/structural_skips.rb +33 -0
  152. data/lib/hecks/fuzzing/sweep_depth.rb +43 -0
  153. data/lib/hecks/fuzzing/target_capabilities.rb +163 -0
  154. data/lib/hecks/fuzzing/value_generator.rb +216 -0
  155. data/lib/hecks/fuzzing.rb +23 -0
  156. data/lib/hecks/grammar/evolve.rb +336 -0
  157. data/lib/hecks/hecks/adapters/codebase/codemods.rb +45 -0
  158. data/lib/hecks/hecks/adapters/codebase/conformance.rb +71 -0
  159. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +202 -0
  160. data/lib/hecks/hecks/adapters/codebase/gem_registry.rb +80 -0
  161. data/lib/hecks/hecks/adapters/codebase/kernel_tables.rb +54 -0
  162. data/lib/hecks/hecks/adapters/codebase/language.rb +240 -0
  163. data/lib/hecks/hecks/adapters/codebase/npm_registry.rb +102 -0
  164. data/lib/hecks/hecks/adapters/codebase/publishing.rb +142 -0
  165. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +67 -0
  166. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +90 -0
  167. data/lib/hecks/hecks/adapters/codebase/ruby_child.rb +83 -0
  168. data/lib/hecks/hecks/adapters/codebase/secret_vault.rb +36 -0
  169. data/lib/hecks/hecks/adapters/codebase/source_tree.adapter +7 -0
  170. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +189 -0
  171. data/lib/hecks/hecks/adapters/codebase/sqlite_fixture.rb +71 -0
  172. data/lib/hecks/hecks/adapters/codebase/style.rb +171 -0
  173. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +55 -0
  174. data/lib/hecks/hecks/adapters/codebase/test_suite.rb +165 -0
  175. data/lib/hecks/hecks/adapters/codebase/tree.rb +111 -0
  176. data/lib/hecks/hecks/adapters/console_capture.rb +79 -0
  177. data/lib/hecks/hecks/adapters/deploy_toolchain.adapter +7 -0
  178. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +108 -0
  179. data/lib/hecks/hecks/adapters/git.adapter +7 -0
  180. data/lib/hecks/hecks/adapters/git.rb +102 -0
  181. data/lib/hecks/hecks/adapters/host_http.adapter +7 -0
  182. data/lib/hecks/hecks/adapters/host_http.rb +65 -0
  183. data/lib/hecks/hecks/adapters/in_process_boot.adapter +7 -0
  184. data/lib/hecks/hecks/adapters/in_process_boot.rb +197 -0
  185. data/lib/hecks/hecks/adapters/in_process_operations.rb +202 -0
  186. data/lib/hecks/hecks/adapters/journal_store/changes.rb +143 -0
  187. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +145 -0
  188. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +132 -0
  189. data/lib/hecks/hecks/adapters/journal_store/examination.rb +135 -0
  190. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +163 -0
  191. data/lib/hecks/hecks/adapters/journal_store/readings.rb +187 -0
  192. data/lib/hecks/hecks/adapters/journal_store.adapter +7 -0
  193. data/lib/hecks/hecks/adapters/journal_store.rb +64 -0
  194. data/lib/hecks/hecks/adapters/local_files.adapter +7 -0
  195. data/lib/hecks/hecks/adapters/local_files.rb +46 -0
  196. data/lib/hecks/hecks/adapters/pg_admin.adapter +7 -0
  197. data/lib/hecks/hecks/adapters/pg_admin.rb +245 -0
  198. data/lib/hecks/hecks/adapters/process_pool.adapter +7 -0
  199. data/lib/hecks/hecks/adapters/process_pool.rb +119 -0
  200. data/lib/hecks/hecks/adapters/rust_toolchain.adapter +7 -0
  201. data/lib/hecks/hecks/adapters/rust_toolchain.rb +179 -0
  202. data/lib/hecks/hecks/adapters/rust_workspace.rb +150 -0
  203. data/lib/hecks/hecks/adapters/shell.adapter +7 -0
  204. data/lib/hecks/hecks/adapters/shell.rb +49 -0
  205. data/lib/hecks/hecks/adapters/terminal.adapter +7 -0
  206. data/lib/hecks/hecks/adapters/terminal.rb +74 -0
  207. data/lib/hecks/hecks/codebase.behaviors +172 -0
  208. data/lib/hecks/hecks/codebase.bluebook +2453 -0
  209. data/lib/hecks/hecks/context_map.hecksagon +13 -0
  210. data/lib/hecks/hecks/custodian.behaviors +179 -0
  211. data/lib/hecks/hecks/custodian.bluebook +1847 -0
  212. data/lib/hecks/hecks/deploy.behaviors +74 -0
  213. data/lib/hecks/hecks/deploy.world +12 -0
  214. data/lib/hecks/hecks/environments/memory.world +10 -0
  215. data/lib/hecks/hecks/governance.world +7 -0
  216. data/lib/hecks/hecks/hecks.behaviors +62 -0
  217. data/lib/hecks/hecks/hecks.bluebook +124 -0
  218. data/lib/hecks/hecks/hecks.hecksagon +390 -0
  219. data/lib/hecks/hecks/hecks.world +42 -0
  220. data/lib/hecks/kernel_capabilities.rb +194 -0
  221. data/lib/hecks/language/bluebook/bluebook.bluebook +11 -0
  222. data/lib/hecks/language/bluebook/query.bluebook +7 -0
  223. data/lib/hecks/language/bluebook/vocabulary.bluebook +4 -4
  224. data/lib/hecks/language/hecksagon/domain_port.bluebook +10 -0
  225. data/lib/hecks/language/hecksagon/hecksagon.bluebook +20 -0
  226. data/lib/hecks/mcp_stdio_guard.rb +2 -2
  227. data/lib/hecks/persistence_legacy_fixture/regenerate.rb +161 -0
  228. data/lib/hecks/persistence_legacy_fixture.rb +113 -0
  229. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +8 -5
  230. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +4 -1
  231. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +15 -2
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +1 -1
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +1 -1
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +1 -1
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +32 -15
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +15 -5
  237. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +227 -0
  238. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +1 -1
  239. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +43 -24
  240. data/lib/hecks/ports/persistence/plugins/era/translation.rb +1 -0
  241. data/lib/hecks/ports/persistence/state_codec.rb +24 -5
  242. data/lib/hecks/projection_files.rb +285 -0
  243. data/lib/hecks/projections/bootstrap_table.rb +1 -1
  244. data/lib/hecks/projections/deploy/fargate.rb +5 -5
  245. data/lib/hecks/projections/deploy/lambda.rb +43 -32
  246. data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
  247. data/lib/hecks/projections/deploy/preview/template.rb +1 -1
  248. data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +1 -1
  249. data/lib/hecks/projections/deploy/scripts/expected-era.header +1 -1
  250. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +3 -3
  251. data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +1 -1
  252. data/lib/hecks/projections/deploy/shared.rb +1 -1
  253. data/lib/hecks/projections/diagrams.rb +3 -3
  254. data/lib/hecks/projections/model/deviations.rb +1 -1
  255. data/lib/hecks/projections/parser_table.rb +4 -4
  256. data/lib/hecks/projections/reference.rb +2 -3
  257. data/lib/hecks/projections/rust_vocabulary.rb +1 -1
  258. data/lib/hecks/projector/cli_projector.rb +106 -37
  259. data/lib/hecks/projector.rb +1 -1
  260. data/lib/hecks/quality_control/adapters/agent.adapter +7 -0
  261. data/lib/hecks/quality_control/adapters/agent.rb +58 -0
  262. data/lib/hecks/quality_control/adapters/angle_tools.adapter +7 -0
  263. data/lib/hecks/quality_control/adapters/angle_tools.rb +15 -0
  264. data/lib/hecks/quality_control/adapters/clearance_recorder.rb +36 -0
  265. data/lib/hecks/quality_control/adapters/clearance_tools.adapter +7 -0
  266. data/lib/hecks/quality_control/adapters/clearance_tools.rb +17 -0
  267. data/lib/hecks/quality_control/adapters/git_pr.adapter +7 -0
  268. data/lib/hecks/quality_control/adapters/git_pr.rb +226 -0
  269. data/lib/hecks/quality_control/adapters/github_checks.adapter +9 -0
  270. data/lib/hecks/quality_control/adapters/github_checks.rb +83 -0
  271. data/lib/hecks/quality_control/adapters/github_ci_webhook.rb +56 -0
  272. data/lib/hecks/quality_control/adapters/github_issues.adapter +7 -0
  273. data/lib/hecks/quality_control/adapters/github_issues.rb +49 -0
  274. data/lib/hecks/quality_control/adapters/qa_tool.rb +59 -0
  275. data/lib/hecks/quality_control/adapters/sweep_tools.adapter +7 -0
  276. data/lib/hecks/quality_control/adapters/sweep_tools.rb +51 -0
  277. data/lib/hecks/quality_control/adapters/target_tools.adapter +7 -0
  278. data/lib/hecks/quality_control/adapters/target_tools.rb +43 -0
  279. data/lib/hecks/quality_control/cli/child.rb +45 -0
  280. data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +45 -0
  281. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +248 -0
  282. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +163 -0
  283. data/lib/hecks/quality_control/cli/qa_generated_domains.rb +450 -0
  284. data/lib/hecks/quality_control/cli/qa_log_bug.rb +195 -0
  285. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +220 -0
  286. data/lib/hecks/quality_control/cli/qa_open_pr.rb +305 -0
  287. data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +154 -0
  288. data/lib/hecks/quality_control/cli/qa_postgres_role.rb +159 -0
  289. data/lib/hecks/quality_control/cli/qa_pr_check.rb +232 -0
  290. data/lib/hecks/quality_control/cli/qa_seed_angles.rb +142 -0
  291. data/lib/hecks/quality_control/cli/qa_seed_targets.rb +69 -0
  292. data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +280 -0
  293. data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +329 -0
  294. data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +92 -0
  295. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +117 -0
  296. data/lib/hecks/quality_control/cli/qa_sweep/parsing.rb +175 -0
  297. data/lib/hecks/quality_control/cli/qa_sweep/release_mode.rb +60 -0
  298. data/lib/hecks/quality_control/cli/qa_sweep.rb +611 -0
  299. data/lib/hecks/quality_control/cli/qa_tick.rb +274 -0
  300. data/lib/hecks/quality_control/quality_control.bluebook +2224 -0
  301. data/lib/hecks/quality_control/quality_control.hecksagon +13 -0
  302. data/lib/hecks/quality_control/quality_control.ports.hecksagon +54 -0
  303. data/lib/hecks/query_ir.rb +346 -0
  304. data/lib/hecks/query_ir_mcp.rb +198 -0
  305. data/lib/hecks/release/runner/ci_publisher.rb +2 -2
  306. data/lib/hecks/release/runner/clean_tree.rb +42 -0
  307. data/lib/hecks/release/runner/console.rb +6 -0
  308. data/lib/hecks/release/runner/gem_publisher.rb +14 -7
  309. data/lib/hecks/release/runner/npm_publisher.rb +6 -36
  310. data/lib/hecks/release/runner/preflight.rb +26 -2
  311. data/lib/hecks/release/runner/published.rb +8 -26
  312. data/lib/hecks/release/runner/tagger.rb +30 -15
  313. data/lib/hecks/release/runner.rb +57 -6
  314. data/lib/hecks/router/namespace_installer.rb +3 -3
  315. data/lib/hecks/runtime/adapter_lookup.rb +124 -0
  316. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +4 -0
  317. data/lib/hecks/runtime/dependency_planning.rb +20 -2
  318. data/lib/hecks/runtime/dispatcher.rb +53 -3
  319. data/lib/hecks/runtime/interpreting.rb +3 -0
  320. data/lib/hecks/runtime/loader.rb +65 -12
  321. data/lib/hecks/runtime/outbox.rb +1 -1
  322. data/lib/hecks/runtime/policy_interpreter.rb +5 -2
  323. data/lib/hecks/runtime/port_operation_interpreter.rb +2 -10
  324. data/lib/hecks/runtime/query_interpreter.rb +77 -0
  325. data/lib/hecks/runtime/reaction_outcome.rb +89 -0
  326. data/lib/hecks/runtime/refusal_wording.rb +23 -1
  327. data/lib/hecks/runtime/registry/verification.rb +163 -4
  328. data/lib/hecks/runtime/registry.rb +57 -3
  329. data/lib/hecks/runtime/remote_dispatcher.rb +31 -2
  330. data/lib/hecks/runtime/value/coercion.rb +23 -135
  331. data/lib/hecks/runtime/value/field_checks.rb +172 -0
  332. data/lib/hecks/runtime/value.rb +4 -1
  333. data/lib/hecks/runtime.rb +20 -6
  334. data/lib/hecks/rust_build/conformance.rb +158 -0
  335. data/lib/hecks/rust_build/conformance_fuzz.rb +71 -0
  336. data/lib/hecks/rust_build/coverage.rb +218 -0
  337. data/lib/hecks/rust_build/native_build.rb +124 -0
  338. data/lib/hecks/rust_build/project_rust/root_mod.rb +120 -0
  339. data/lib/hecks/rust_build/project_rust.rb +332 -0
  340. data/lib/hecks/rust_build/wasm.rb +105 -0
  341. data/lib/hecks/rust_build/wasm_browser.rb +113 -0
  342. data/lib/hecks/rust_build.rb +144 -0
  343. data/lib/hecks/storehouse.rb +16 -16
  344. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +2 -2
  345. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +1 -1
  346. data/lib/hecks/three_zero/forms.yml +127 -0
  347. data/lib/hecks/three_zero.rb +12 -0
  348. data/lib/hecks/tools/argument_gate_matrix.rb +316 -0
  349. data/lib/hecks/tools/comment_style.rb +1070 -0
  350. data/lib/hecks/tools/corpus_report.rb +87 -0
  351. data/lib/hecks/tools/deploy_recipe.rb +255 -0
  352. data/lib/hecks/tools/deploy_recipe_lint.rb +282 -0
  353. data/lib/hecks/tools/drop_implicit_append_fields.rb +95 -0
  354. data/lib/hecks/tools/era_projection_backfill.rb +94 -0
  355. data/lib/hecks/tools/era_reattest.rb +146 -0
  356. data/lib/hecks/tools/evolve_run.rb +189 -0
  357. data/lib/hecks/tools/fuzz_sweep.rb +373 -0
  358. data/lib/hecks/tools/heki_compaction.rb +100 -0
  359. data/lib/hecks/tools/hoist_local_givens.rb +246 -0
  360. data/lib/hecks/tools/journal_compaction.rb +89 -0
  361. data/lib/hecks/tools/oidc_manifests.rb +70 -0
  362. data/lib/hecks/tools/query_ir_run.rb +81 -0
  363. data/lib/hecks/tools/regeneration_run.rb +166 -0
  364. data/lib/hecks/tools/rust_comment_style.rb +642 -0
  365. data/lib/hecks/tools/sequence_script.rb +62 -0
  366. data/lib/hecks/tools/tail_merge.rb +117 -0
  367. data/lib/hecks/tools/tenant_provisioning.rb +142 -0
  368. data/lib/hecks/tools/translation_audit.rb +208 -0
  369. data/lib/hecks/tools/translation_scaffold.rb +131 -0
  370. data/lib/hecks/tools.rb +86 -0
  371. data/lib/hecks/version.rb +1 -1
  372. data/lib/hecks.rb +32 -15
  373. data/qa/settings.yml +66 -0
  374. data/rust/.cargo/config.toml +6 -0
  375. data/rust/.gitignore +2 -0
  376. data/rust/Cargo.lock +54 -0
  377. data/rust/Cargo.toml +61 -0
  378. data/rust/build/.cargo/config.toml +3 -0
  379. data/rust/build/.gitignore +1 -0
  380. data/rust/build/Cargo.lock +7 -0
  381. data/rust/build/Cargo.toml +19 -0
  382. data/rust/build/src/build_artifact.rs +47 -0
  383. data/rust/build/src/cargo_sync.rs +248 -0
  384. data/rust/build/src/json.rs +421 -0
  385. data/rust/build/src/lineage_pass.rs +285 -0
  386. data/rust/build/src/main.rs +51 -0
  387. data/rust/build/src/optional_pass.rs +150 -0
  388. data/rust/build/src/pipeline.rs +264 -0
  389. data/rust/build/src/reserved_names.rs +76 -0
  390. data/rust/build/src/resolve.rs +124 -0
  391. data/rust/build/src/root.rs +25 -0
  392. data/rust/build/src/sidecars.rs +73 -0
  393. data/rust/build/src/subprocess.rs +51 -0
  394. data/rust/build/src/tmp.rs +31 -0
  395. data/rust/codegen/.cargo/config.toml +3 -0
  396. data/rust/codegen/.gitignore +1 -0
  397. data/rust/codegen/Cargo.lock +7 -0
  398. data/rust/codegen/Cargo.toml +20 -0
  399. data/rust/codegen/src/attr.rs +32 -0
  400. data/rust/codegen/src/bridging.rs +481 -0
  401. data/rust/codegen/src/commands.rs +1226 -0
  402. data/rust/codegen/src/constraints.rs +109 -0
  403. data/rust/codegen/src/dependency_planning.rs +209 -0
  404. data/rust/codegen/src/domain_generator.rs +1288 -0
  405. data/rust/codegen/src/exemplar.rs +230 -0
  406. data/rust/codegen/src/expr_emitter.rs +87 -0
  407. data/rust/codegen/src/fielded.rs +192 -0
  408. data/rust/codegen/src/hecks_naming.rs +33 -0
  409. data/rust/codegen/src/json.rs +335 -0
  410. data/rust/codegen/src/json_codec.rs +697 -0
  411. data/rust/codegen/src/literal.rs +207 -0
  412. data/rust/codegen/src/main.rs +290 -0
  413. data/rust/codegen/src/manifest.rs +227 -0
  414. data/rust/codegen/src/mutations.rs +784 -0
  415. data/rust/codegen/src/naming.rs +286 -0
  416. data/rust/codegen/src/ports.rs +208 -0
  417. data/rust/codegen/src/prelude.rs +116 -0
  418. data/rust/codegen/src/queries.rs +786 -0
  419. data/rust/codegen/src/reactions.rs +532 -0
  420. data/rust/codegen/src/read_models.rs +697 -0
  421. data/rust/codegen/src/reference_specs.rs +40 -0
  422. data/rust/codegen/src/registry.rs +966 -0
  423. data/rust/codegen/src/reserved_names.rs +76 -0
  424. data/rust/codegen/src/shared.rs +93 -0
  425. data/rust/codegen/src/skip_reason.rs +25 -0
  426. data/rust/codegen/src/types.rs +362 -0
  427. data/rust/host/.cargo/config.toml +3 -0
  428. data/rust/host/.gitignore +1 -0
  429. data/rust/host/Cargo.lock +4297 -0
  430. data/rust/host/Cargo.toml +61 -0
  431. data/rust/host/HECKS_RELEASE +1 -0
  432. data/rust/host/rds-ca-bundle.pem +2736 -0
  433. data/rust/host/src/api.rs +1596 -0
  434. data/rust/host/src/approval.rs +652 -0
  435. data/rust/host/src/auth.rs +1287 -0
  436. data/rust/host/src/bin/expr_harness.rs +46 -0
  437. data/rust/host/src/bin/lineage_harness.rs +119 -0
  438. data/rust/host/src/bin/mint_harness.rs +135 -0
  439. data/rust/host/src/checkout.rs +265 -0
  440. data/rust/host/src/dispatch.rs +1443 -0
  441. data/rust/host/src/expr_json.rs +1117 -0
  442. data/rust/host/src/field_hints.rs +52 -0
  443. data/rust/host/src/ir.rs +733 -0
  444. data/rust/host/src/journal.rs +874 -0
  445. data/rust/host/src/lambda_client.rs +378 -0
  446. data/rust/host/src/log.rs +147 -0
  447. data/rust/host/src/main.rs +474 -0
  448. data/rust/host/src/mint.rs +1865 -0
  449. data/rust/host/src/payments/keystore.rs +330 -0
  450. data/rust/host/src/payments/tests.rs +1400 -0
  451. data/rust/host/src/payments.rs +767 -0
  452. data/rust/host/src/presentation.rs +698 -0
  453. data/rust/host/src/presentation_write.rs +1431 -0
  454. data/rust/host/src/query_step.rs +149 -0
  455. data/rust/host/src/rate_limit.rs +904 -0
  456. data/rust/host/src/reference_transform.rs +241 -0
  457. data/rust/host/src/reference_validate.rs +338 -0
  458. data/rust/host/src/resend.rs +296 -0
  459. data/rust/host/src/secrets.rs +106 -0
  460. data/rust/host/src/server.rs +663 -0
  461. data/rust/host/src/storage_shape.rs +175 -0
  462. data/rust/host/src/test_pg.rs +40 -0
  463. data/rust/host/src/ui_schema.rs +1351 -0
  464. data/rust/host/src/wasm_runner.rs +191 -0
  465. data/rust/host/src/web/newsletter/cooldown.rs +126 -0
  466. data/rust/host/src/web/newsletter/tests.rs +104 -0
  467. data/rust/host/src/web/newsletter.rs +310 -0
  468. data/rust/host/src/web/newsletter_send.rs +357 -0
  469. data/rust/host/src/web/registrations.rs +1505 -0
  470. data/rust/host/src/web.rs +2937 -0
  471. data/rust/lsp/.gitignore +1 -0
  472. data/rust/lsp/Cargo.lock +7 -0
  473. data/rust/lsp/Cargo.toml +16 -0
  474. data/rust/lsp/README.md +133 -0
  475. data/rust/lsp/src/diagnostics.rs +165 -0
  476. data/rust/lsp/src/json.rs +385 -0
  477. data/rust/lsp/src/main.rs +399 -0
  478. data/rust/lsp/src/outline.rs +177 -0
  479. data/rust/lsp/src/rpc.rs +67 -0
  480. data/rust/parser/.cargo/config.toml +5 -0
  481. data/rust/parser/.gitignore +1 -0
  482. data/rust/parser/Cargo.lock +7 -0
  483. data/rust/parser/Cargo.toml +21 -0
  484. data/rust/parser/src/build/closed_sets.rs +27 -0
  485. data/rust/parser/src/build/identity.rs +276 -0
  486. data/rust/parser/src/build/mod.rs +12 -0
  487. data/rust/parser/src/build/naming.rs +141 -0
  488. data/rust/parser/src/build/pattern_subset.rs +161 -0
  489. data/rust/parser/src/build/query_derive.rs +72 -0
  490. data/rust/parser/src/build/query_inference.rs +269 -0
  491. data/rust/parser/src/build/query_options.rs +100 -0
  492. data/rust/parser/src/build/read_model.rs +94 -0
  493. data/rust/parser/src/build/references.rs +32 -0
  494. data/rust/parser/src/canonical.rs +114 -0
  495. data/rust/parser/src/diag.rs +55 -0
  496. data/rust/parser/src/emit.rs +953 -0
  497. data/rust/parser/src/expr/ast_json.rs +147 -0
  498. data/rust/parser/src/expr/evaluator.rs +81 -0
  499. data/rust/parser/src/expr/mod.rs +84 -0
  500. data/rust/parser/src/expr/resolver.rs +497 -0
  501. data/rust/parser/src/ir.rs +387 -0
  502. data/rust/parser/src/keywords.rs +585 -0
  503. data/rust/parser/src/lex.rs +662 -0
  504. data/rust/parser/src/main.rs +252 -0
  505. data/rust/parser/src/parse/aggregate.rs +612 -0
  506. data/rust/parser/src/parse/chapter.rs +517 -0
  507. data/rust/parser/src/parse/command.rs +637 -0
  508. data/rust/parser/src/parse/domain_port.rs +127 -0
  509. data/rust/parser/src/parse/entity.rs +527 -0
  510. data/rust/parser/src/parse/file.rs +67 -0
  511. data/rust/parser/src/parse/hecksagon.rs +231 -0
  512. data/rust/parser/src/parse/lifecycle.rs +162 -0
  513. data/rust/parser/src/parse/mod.rs +1262 -0
  514. data/rust/parser/src/parse/policy.rs +87 -0
  515. data/rust/parser/src/parse/process_manager.rs +305 -0
  516. data/rust/parser/src/parse/query.rs +100 -0
  517. data/rust/parser/src/parse/read_model.rs +214 -0
  518. data/rust/parser/src/parse/value_object.rs +250 -0
  519. data/rust/parser/src/ruby_value.rs +422 -0
  520. data/rust/project/bridging.rb +257 -0
  521. data/rust/project/commands.rb +877 -0
  522. data/rust/project/constraints.rb +86 -0
  523. data/rust/project/dependency_planning.rb +178 -0
  524. data/rust/project/domain_generator.rb +980 -0
  525. data/rust/project/exemplar.rb +148 -0
  526. data/rust/project/expr_emitter.rb +72 -0
  527. data/rust/project/fielded.rb +141 -0
  528. data/rust/project/json_codec.rb +641 -0
  529. data/rust/project/mutations.rb +721 -0
  530. data/rust/project/naming.rb +205 -0
  531. data/rust/project/ports.rb +65 -0
  532. data/rust/project/queries.rb +493 -0
  533. data/rust/project/reactions.rb +317 -0
  534. data/rust/project/read_models.rb +690 -0
  535. data/rust/project/reference_specs.rb +27 -0
  536. data/rust/project/registry.rb +396 -0
  537. data/rust/project/skip_reason.rb +29 -0
  538. data/rust/project/types.rb +225 -0
  539. data/rust/project/write_if_changed.rb +83 -0
  540. data/rust/project.rb +30 -0
  541. data/rust/project_rust_pipeline.rb +383 -0
  542. data/rust/src/exemplar/commands.rs +279 -0
  543. data/rust/src/exemplar/constraints.rs +18 -0
  544. data/rust/src/exemplar/fielded.rs +280 -0
  545. data/rust/src/exemplar/json.rs +277 -0
  546. data/rust/src/exemplar/mod.rs +17 -0
  547. data/rust/src/exemplar/mutations.rs +80 -0
  548. data/rust/src/exemplar/queries.rs +33 -0
  549. data/rust/src/exemplar/reactions.rs +88 -0
  550. data/rust/src/exemplar/read_models.rs +48 -0
  551. data/rust/src/exemplar/registry.rs +166 -0
  552. data/rust/src/exemplar/types.rs +44 -0
  553. data/rust/src/kernel/attribute_shapes/composite.rs +88 -0
  554. data/rust/src/kernel/attribute_shapes/list.rs +18 -0
  555. data/rust/src/kernel/attribute_shapes/mod.rs +44 -0
  556. data/rust/src/kernel/attribute_shapes/optional.rs +9 -0
  557. data/rust/src/kernel/attribute_shapes/scalar.rs +39 -0
  558. data/rust/src/kernel/cli.rs +585 -0
  559. data/rust/src/kernel/dispatch.rs +1090 -0
  560. data/rust/src/kernel/expr.rs +458 -0
  561. data/rust/src/kernel/expression_operators/arithmetic.rs +128 -0
  562. data/rust/src/kernel/expression_operators/comparison.rs +48 -0
  563. data/rust/src/kernel/expression_operators/enumeration.rs +232 -0
  564. data/rust/src/kernel/expression_operators/logical.rs +13 -0
  565. data/rust/src/kernel/expression_operators/membership.rs +18 -0
  566. data/rust/src/kernel/expression_operators/mod.rs +68 -0
  567. data/rust/src/kernel/expression_operators/pattern_match.rs +73 -0
  568. data/rust/src/kernel/expression_operators/positional.rs +126 -0
  569. data/rust/src/kernel/expression_operators/presence.rs +101 -0
  570. data/rust/src/kernel/expression_operators/sign_test.rs +16 -0
  571. data/rust/src/kernel/expression_operators/sized.rs +35 -0
  572. data/rust/src/kernel/expression_operators/text.rs +79 -0
  573. data/rust/src/kernel/expression_operators/to_string.rs +18 -0
  574. data/rust/src/kernel/json.rs +779 -0
  575. data/rust/src/kernel/mod.rs +133 -0
  576. data/rust/src/kernel/named_query.rs +219 -0
  577. data/rust/src/kernel/orchestrate.rs +1810 -0
  578. data/rust/src/kernel/pattern.rs +307 -0
  579. data/rust/src/kernel/query_comparators.rs +206 -0
  580. data/rust/src/kernel/query_ordering.rs +103 -0
  581. data/rust/src/kernel/read_model.rs +914 -0
  582. data/rust/src/kernel/reference_lookup.rs +149 -0
  583. data/rust/src/kernel/repository.rs +468 -0
  584. data/rust/src/kernel/routing.rs +612 -0
  585. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +118 -0
  586. data/rust/src/kernel/vocab/entity_dispatch_order.rs +114 -0
  587. data/rust/src/kernel/vocab/field_hint.rs +70 -0
  588. data/rust/src/kernel/vocab/mod.rs +20 -0
  589. data/rust/src/kernel/vocab/query_comparator.rs +65 -0
  590. data/rust/src/kernel/vocab/refusal_template.rs +1592 -0
  591. data/rust/src/lib.rs +8 -0
  592. data/rust/src/main.rs +16 -0
  593. data/rust/web/.gitignore +1 -0
  594. data/rust/web/Cargo.lock +119 -0
  595. data/rust/web/Cargo.toml +18 -0
  596. data/rust/web/src/lib.rs +8 -0
  597. metadata +477 -12
  598. data/lib/hecks/facade/cli_door.rb +0 -96
  599. data/lib/hecks/facade/cli_runner.rb +0 -127
  600. data/lib/hecks/mcp_door_scope.rb +0 -95
@@ -0,0 +1,77 @@
1
+ module Hecks
2
+ module Fuzzing
3
+ class SequenceGenerator
4
+ # Which step to try next: eligibility (what is possible from the
5
+ # state so far) and weighting (what is likely to reach somewhere
6
+ # new).
7
+ module Picker
8
+ private
9
+
10
+ def pick(catalog)
11
+ rest = catalog[:queries].dup
12
+ catalog[:instance].each { |entry| rest << entry if actionable?(catalog, entry) }
13
+ catalog[:entity_commands].each { |entry| rest << entry if actionable?(catalog, entry) }
14
+ catalog[:entity_queries].each { |entry| rest << entry if @known_ids[entry[:aggregate].hecks_name].any? }
15
+ catalog[:read_models].each { |entry| rest << entry if read_model_actionable?(entry) }
16
+
17
+ makers = catalog[:creating].select { |entry| satisfiable?(catalog, entry) }
18
+ pool = rest + makers.flat_map { |entry| [entry] * creating_weight(rest.size) }
19
+ pool += deep_entity_bias(rest) if adversarial?
20
+
21
+ steer(pool).sample(random: @random)
22
+ end
23
+
24
+ # Weights up entity commands two or more hops deep; adversarial mode only,
25
+ # and eligibility still decides what is possible.
26
+ def deep_entity_bias(rest)
27
+ deep = rest.select { |entry| (entry[:chain] || []).size >= Adversary::DEEP_ENTITY_DEPTH }
28
+ deep * Adversary::DEEP_ENTITY_WEIGHT
29
+ end
30
+
31
+ # While the store is empty, creation matches everything else combined, so a
32
+ # run does not spend its budget querying nothing.
33
+ def creating_weight(rest_size)
34
+ return CREATING_WEIGHT if @known_ids.each_value.any?(&:any?)
35
+
36
+ [rest_size, CREATING_WEIGHT].max
37
+ end
38
+
39
+ def actionable?(catalog, entry)
40
+ @known_ids[entry[:aggregate].hecks_name].any? && satisfiable?(catalog, entry)
41
+ end
42
+
43
+ # A rootless report is always eligible; a rooted one needs an instance of its
44
+ # `reference_target`, or the ask is refused.
45
+ def read_model_actionable?(entry)
46
+ entry[:model].reference_target.nil? || @known_ids[entry[:model].reference_target].any?
47
+ end
48
+
49
+ # A command whose references point at nothing that exists is refused, so it
50
+ # is withheld until they do. A target no creating command can make (a
51
+ # cross-domain reference) is exempt, or the domain would starve.
52
+ def satisfiable?(catalog, entry)
53
+ entry[:command].attributes.select(&:reference?).all? do |attribute|
54
+ target = attribute.type.target_name.to_s
55
+ !catalog[:creatable].include?(target) || @known_ids[target].any?
56
+ end
57
+ end
58
+
59
+ # Weights up verbs this sequence has not dispatched yet. Only likelihood
60
+ # changes; a verb gated behind missing state stays out of the pool.
61
+ def steer(pool)
62
+ fresh = pool.reject { |entry| @exercised.include?(entry[:verb]) }
63
+ steered = fresh.empty? ? pool : pool + (fresh * UNEXERCISED_WEIGHT)
64
+ favor(steered)
65
+ end
66
+
67
+ # Weights up the `favor:` verbs when eligible, never making one eligible.
68
+ def favor(pool)
69
+ return pool if @favor.empty?
70
+
71
+ favored = pool.uniq.select { |entry| @favor.include?(entry[:verb]) }
72
+ pool + (favored * SequenceGenerator::FAVOR_WEIGHT)
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,109 @@
1
+ require_relative "../../query_specification/field_path"
2
+ require_relative "../../runtime/value"
3
+
4
+ module Hecks
5
+ module Fuzzing
6
+ class SequenceGenerator
7
+ # Query arguments drawn from rows the sequence has already written, so a
8
+ # query can match a stored row instead of a fresh word that matches nothing.
9
+ #
10
+ # After each effective command, the compared fields of every aggregate a
11
+ # query filters on are read back from the repository as one row per record.
12
+ # The choice uses its own `Random` (`restart_random`); drawing from `@random`
13
+ # would shift every later step of every seed.
14
+ module QueryBinding
15
+ # How often a query step takes its arguments from a written row; the
16
+ # rest of the time the generated miss is kept.
17
+ BOUND_QUERY_PROBABILITY = 0.75
18
+
19
+ # Comparators taking one scalar operand; `in` and `none_in_state` do not.
20
+ BINDABLE_OPERATORS = %i[eq ne lt lte gt gte].freeze
21
+
22
+ # Keeps the binding stream apart from `@random`, which is seeded with the bare seed.
23
+ BINDING_SEED_OFFSET = 0x9e3779b1
24
+
25
+ private
26
+
27
+ # `{"Domain::Aggregate" => {domain:, aggregate:, fields: [String]}}`
28
+ # for every aggregate some query filters on, where `fields` is the
29
+ # union of the stored fields those queries compare.
30
+ def build_query_bindings(runtime)
31
+ runtime.registry.bluebooks.each_with_object({}) do |(domain_name, bluebook), bindings|
32
+ bluebook.aggregates.each do |aggregate|
33
+ fields = aggregate.queries.flat_map { |query| bound_fields(query).values }.uniq
34
+ next if fields.empty?
35
+
36
+ bindings["#{domain_name}::#{aggregate.hecks_name}"] =
37
+ { domain: domain_name, aggregate: aggregate, fields: fields }
38
+ end
39
+ end
40
+ end
41
+
42
+ # `{parameter name => stored field}` for each `where` clause whose
43
+ # operand is one of the query's own parameters. A clause on a literal,
44
+ # or aimed at another aggregate's field (`target`), binds nothing.
45
+ def bound_fields(query)
46
+ query.wheres.each_with_object({}) do |clause, fields|
47
+ next unless clause.value.is_a?(Symbol) && clause.target.nil?
48
+ next unless BINDABLE_OPERATORS.include?(clause.op.to_sym)
49
+
50
+ fields[clause.value.to_s] ||= clause.field.to_s
51
+ end
52
+ end
53
+
54
+ # Remembers the compared fields of every stored record of every
55
+ # aggregate a query filters on, as one row per distinct record shape.
56
+ def harvest_written_rows(runtime, catalog)
57
+ catalog[:query_bindings].each do |key, binding|
58
+ repository = runtime.registry.repository(binding[:domain], binding[:aggregate])
59
+ repository.all.each do |record|
60
+ row = binding[:fields].to_h { |field| [field, written_value(field_of(record, field))] }.compact
61
+ @written_rows[key] << row unless row.empty? || @written_rows[key].include?(row)
62
+ end
63
+ end
64
+ end
65
+
66
+ def field_of(record, field) = QuerySpecification::FieldPath.dig(record.state, field)
67
+
68
+ # A stored value as the JSON-shaped argument a step carries: string
69
+ # keys, nested values opened. `nil` for anything a query argument
70
+ # cannot be (a missing value, a list, a timestamp object).
71
+ def written_value(value)
72
+ case value
73
+ when Runtime::Value then written_value(value.to_h)
74
+ when Hash then value.to_h { |key, inner| [key.to_s, written_value(inner)] }
75
+ when String, Integer, Float, true, false then value
76
+ end
77
+ end
78
+
79
+ # Replaces the generated arguments of a query step with a remembered
80
+ # row's, most of the time. Only a parameter the step already carries
81
+ # is replaced, so an optional argument left out, or a required one a
82
+ # malformation dropped, stays that way.
83
+ def bind_to_written_row!(args, entry)
84
+ return if entry[:entity]
85
+
86
+ rows = @written_rows[entry[:verb].split(".").first]
87
+ params = bound_fields(entry[:query]).select { |param, _| args.key?(param) }
88
+ return if rows.empty? || params.empty?
89
+ return unless @binding_random.rand < BOUND_QUERY_PROBABILITY
90
+
91
+ row = rows.select { |candidate| params.values.any? { |field| candidate.key?(field) } }
92
+ .sample(random: @binding_random)
93
+ params.each do |param, field|
94
+ args[param] = shaped_like(args[param], row[field]) if row&.key?(field)
95
+ end
96
+ end
97
+
98
+ # A single-field value object is written `{"value" => x}` but may be
99
+ # asked with the bare `x`; keeps whichever spelling the generator
100
+ # already chose for this argument.
101
+ def shaped_like(current, written)
102
+ return written unless written.is_a?(Hash) && written.size == 1 && !current.is_a?(Hash)
103
+
104
+ written.values.first
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,237 @@
1
+ require_relative "../../bluebook/expression/resolver"
2
+ require_relative "../invalid_value_generator"
3
+ require_relative "../value_generator"
4
+ require_relative "../../runtime/errors"
5
+ require_relative "../../runtime/value"
6
+ require_relative "../../naming"
7
+
8
+ module Hecks
9
+ module Fuzzing
10
+ class SequenceGenerator
11
+ # Turn a picked entry into a corpus step: generate its arguments,
12
+ # occasionally malform exactly one of them, shape its identity, and
13
+ # dispatch it for real.
14
+ module StepBuilder
15
+ private
16
+
17
+ def build_query_step(runtime, entry)
18
+ args = args_for(entry[:query].attributes, entry[:aggregate])
19
+ bind_to_written_row!(args, entry)
20
+ safe_call { runtime.query(entry[:verb], **symbolize(args)) }
21
+ { "query" => entry[:verb], "args" => args }
22
+ end
23
+
24
+ # A report ask. Its only argument is `reference_name`, on a rooted model.
25
+ # The id stays a bare scalar: `ReadModelInterpreter#refuse_object_reference`
26
+ # rejects a wrapped identity.
27
+ def build_read_model_step(runtime, entry)
28
+ model = entry[:model]
29
+ args = model.reference_target.nil? ? {} : { model.reference_name.to_s => pick_known(model.reference_target) }
30
+
31
+ safe_call { runtime.query(entry[:verb], **symbolize(args)) }
32
+ { "query" => entry[:verb], "args" => args }
33
+ end
34
+
35
+ # The adversarial mutation happens here, after the args and identity are
36
+ # built and before the one inline dispatch, so the dispatch, the corpus
37
+ # step and both replay engines all see the same mutated payload.
38
+ #
39
+ # The caller draw and then the dry-run coin follow the mutation. The order
40
+ # is part of the seed contract; both draw nothing when off.
41
+ def build_command_step(runtime, catalog, entry)
42
+ args = args_for(entry[:command].attributes, entry[:aggregate])
43
+ add_identity!(args, entry)
44
+ steer_grant!(args, entry, catalog)
45
+ mutations = adversarial_mutations!(args, entry, catalog)
46
+ caller, caller_note = caller_draw!(entry, catalog)
47
+ mutations << caller_note if caller_note
48
+ @state_before = state_before(runtime, entry, args)
49
+
50
+ step =
51
+ if dry_run_draw?
52
+ safe_call { as_caller(caller) { runtime.dry_run?(entry[:verb], **symbolize(args)) } }
53
+ { "dry_run" => entry[:verb], "args" => args }
54
+ else
55
+ outcome = safe_call { as_caller(caller) { runtime.dispatch_flat(entry[:verb], symbolize(args)) } }
56
+ if outcome
57
+ record_outcome(catalog, entry, args)
58
+ harvest_written_rows(runtime, catalog)
59
+ @event_count += outcome.events.length
60
+ end
61
+ { "verb" => entry[:verb], "args" => args }
62
+ end
63
+ step.merge!(caller) if caller
64
+ step["adversarial"] = mutations unless mutations.empty?
65
+ step
66
+ end
67
+
68
+ # Draws nothing when the fraction is zero, like `adversarial?`.
69
+ def dry_run_draw? = @dry_run.positive? && @random.rand < @dry_run
70
+
71
+ # Runs the block under `Hecks.as_caller`, the binding `Fuzzing::Replay` makes
72
+ # from the step's keys, or yields bare when there is no caller.
73
+ def as_caller(caller, &)
74
+ return yield unless caller
75
+
76
+ Hecks.as_caller(role: caller["role"], actor_id: caller["actor_id"], &)
77
+ end
78
+
79
+ def args_for(attributes, aggregate)
80
+ args = attributes.each_with_object({}) do |attribute, built|
81
+ # Omitting an optional argument is an ordinary payload, not a malformation
82
+ # (see OPTIONAL_OMITTED_PROBABILITY).
83
+ next if attribute.optional? && @random.rand < SequenceGenerator::OPTIONAL_OMITTED_PROBABILITY
84
+
85
+ if attribute.list?
86
+ value = list_value_for(attribute, aggregate)
87
+ # `list_value_for` answers nil for a list of entities (those are
88
+ # populated by per-element append commands), so the step skips it.
89
+ next if value.nil?
90
+
91
+ built[attribute.name.to_s] = value
92
+ else
93
+ built[attribute.name.to_s] = ValueGenerator.value_for(attribute, aggregate, random: @random, known_ids: @known_ids)
94
+ end
95
+ end
96
+
97
+ malform(args, attributes, aggregate)
98
+ end
99
+
100
+ # An array of 0-3 independently generated elements shaped like the bare
101
+ # element type. `nil`, not `[]`, when the element type is not a value object.
102
+ def list_value_for(attribute, aggregate)
103
+ value_object = Runtime::Value.value_object_for(aggregate, attribute.type.to_s)
104
+ return nil unless value_object
105
+
106
+ Array.new(@random.rand(0..3)) { ValueGenerator.value_for(attribute, aggregate, random: @random, known_ids: @known_ids) }
107
+ end
108
+
109
+ # At most one malformation per step, so the check that fired is identifiable.
110
+ # The rate stays low because refused steps reach no state.
111
+ def malform(args, attributes, aggregate)
112
+ return args if args.empty? || @random.rand >= MALFORMED_ARGUMENT_PROBABILITY
113
+
114
+ case @random.rand(3)
115
+ when 0 then corrupt_one(args, attributes, aggregate)
116
+ when 1 then drop_one(args, aggregate)
117
+ else args.merge([InvalidValueGenerator.undeclared_argument(random: @random)].to_h)
118
+ end
119
+ end
120
+
121
+ # Never drops the identity: an auto-minted id is unreproducible, so the
122
+ # step's outcome could not be replayed.
123
+ def drop_one(args, aggregate)
124
+ identity = (aggregate.identified_by || :id).to_s
125
+ droppable = args.keys - [identity, "id"]
126
+ return args if droppable.empty?
127
+
128
+ args.reject { |name, _| name == droppable.sample(random: @random) }
129
+ end
130
+
131
+ def corrupt_one(args, attributes, aggregate)
132
+ named = attributes.reject(&:list?).select { |attribute| args.key?(attribute.name.to_s) }
133
+ return args if named.empty?
134
+
135
+ attribute = named.sample(random: @random)
136
+ args.merge(attribute.name.to_s => InvalidValueGenerator.corrupt(attribute, aggregate, random: @random))
137
+ end
138
+
139
+ def add_identity!(args, entry)
140
+ aggregate = entry[:aggregate]
141
+ parent_key = (aggregate.identified_by || :id).to_s
142
+
143
+ if entry[:entity]
144
+ parent_scalar = pick_known(aggregate.hecks_name)
145
+ args[parent_key] = identity_shaped(aggregate, aggregate.identified_by, parent_scalar, aggregate)
146
+ # One identity per hop, drawn from that hop's pool (`entity_pool_key`), as
147
+ # flat args, which is what `EntityElement#locate_chain` reads.
148
+ scalars = [parent_scalar]
149
+ names = []
150
+ entry[:chain].each do |piece|
151
+ names << piece.hecks_name
152
+ scalar = pick_entity_known(entity_pool_key(aggregate.hecks_name, names, scalars))
153
+ args[(piece.identified_by || :id).to_s] = identity_shaped(piece, piece.identified_by, scalar, aggregate)
154
+ scalars << scalar
155
+ end
156
+ elsif entry[:command].creates?
157
+ # A composite identity's parts are already generated as the command's own
158
+ # attributes; a synthetic `id` would be refused as an undeclared argument.
159
+ unless composite_identity?(aggregate)
160
+ args[parent_key] ||= identity_shaped(aggregate, aggregate.identified_by, ValueGenerator.random_id(@random),
161
+ aggregate)
162
+ end
163
+ else
164
+ scalar = pick_known(aggregate.hecks_name)
165
+ args[parent_key] = identity_shaped(aggregate, aggregate.identified_by, scalar, aggregate)
166
+ end
167
+ end
168
+
169
+ # True only for a multi-field identity: `identified_by` is also nil for the
170
+ # untyped default, whose parts are not in `args`.
171
+ def composite_identity?(aggregate) = aggregate.identified_by.nil? && aggregate.identity_paths.size > 1
172
+
173
+ # Shapes a bare scalar id like the construct's identity field. A
174
+ # value-object-typed identity given a bare scalar is a TypeMismatch, so it
175
+ # must be wrapped; the untyped default `:id` stays bare.
176
+ def identity_shaped(construct, key, scalar, aggregate)
177
+ return scalar unless key
178
+
179
+ attribute = construct.attribute(key)
180
+ return scalar unless attribute
181
+
182
+ value_object = aggregate.value_object(attribute.type.to_s)
183
+ return scalar unless value_object
184
+
185
+ field = value_object.attributes.first
186
+ return scalar unless field
187
+
188
+ { field.name.to_s => coerce_scalar(field.type.to_s, scalar) }
189
+ end
190
+
191
+ def coerce_scalar(type_name, scalar)
192
+ case type_name
193
+ when "Integer" then scalar.to_i
194
+ when "Float" then scalar.to_f
195
+ else scalar.to_s
196
+ end
197
+ end
198
+
199
+ def symbolize(args) = args.transform_keys(&:to_sym)
200
+
201
+ # A declined step is not a generator failure: nothing is recorded, the
202
+ # sequence carries on, and the step still enters the corpus as a refusal.
203
+ #
204
+ # EvaluationError counts as a refusal (an unreadable payload); any other
205
+ # error propagates and fails spec/fuzzing.
206
+ def safe_call
207
+ result = yield
208
+ @last_outcome = "ok"
209
+ result
210
+ rescue *Hecks::Runtime::DOMAIN_REFUSALS, Hecks::Bluebook::Expression::EvaluationError => e
211
+ @last_outcome = e.class.name.split("::").last
212
+ nil
213
+ end
214
+
215
+ # The addressed aggregate's lifecycle value before this dispatch, or
216
+ # `exists`/`absent`; `?` when a mutation mangled the identity. Draws nothing from the RNG.
217
+ def state_before(runtime, entry, args)
218
+ aggregate = entry[:aggregate]
219
+ symbolic = symbolize(args)
220
+ id = Runtime::Identity.of(aggregate, symbolic) || Runtime::Identity.from(aggregate, symbolic, :id)
221
+ return "absent" unless id
222
+
223
+ record = runtime.registry.repository(entry[:verb].split("::").first, aggregate).find(id)
224
+ return "absent" unless record
225
+
226
+ lifecycle = aggregate.lifecycle
227
+ return "exists" unless lifecycle
228
+
229
+ key = record.state.key?(lifecycle.field) ? lifecycle.field : lifecycle.field.to_s
230
+ record.state[key].to_s
231
+ rescue StandardError
232
+ "?"
233
+ end
234
+ end
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,189 @@
1
+ require "fileutils"
2
+ require "tmpdir"
3
+ require_relative "isolated_boot"
4
+ require_relative "invalid_value_generator"
5
+ require_relative "value_generator"
6
+ require_relative "sequence_generator/catalog"
7
+ require_relative "sequence_generator/picker"
8
+ require_relative "sequence_generator/step_builder"
9
+ require_relative "sequence_generator/outcome_tracker"
10
+ require_relative "sequence_generator/query_binding"
11
+ require_relative "sequence_generator/adversary"
12
+
13
+ module Hecks
14
+ module Fuzzing
15
+ # A random-but-valid sequence of dispatches and queries, in the `{name, note, steps}`
16
+ # shape of `spec/corpus/*.json`, so a generated sequence replays as a corpus member.
17
+ # Each step is dispatched for real against a throwaway boot while the sequence is built.
18
+ class SequenceGenerator
19
+ include Catalog
20
+ include Picker
21
+ include StepBuilder
22
+ include OutcomeTracker
23
+ include QueryBinding
24
+ include Adversary
25
+
26
+ # Creating commands are always eligible; weighting them up reaches an actionable
27
+ # state sooner without starving domains that have few aggregates.
28
+ CREATING_WEIGHT = 2
29
+
30
+ # Low, because a refused step reaches no new state and a run of them is silent.
31
+ MALFORMED_ARGUMENT_PROBABILITY = 0.12
32
+
33
+ # A fair coin: an optional argument is legal present or absent. Separate from
34
+ # `malform`, whose dropped required argument is refused and so never stores a null.
35
+ OPTIONAL_OMITTED_PROBABILITY = 0.5
36
+
37
+ # Preference for a verb this sequence has not yet dispatched, so whole commands
38
+ # are not left untouched for a run.
39
+ UNEXERCISED_WEIGHT = 4
40
+
41
+ # Generates a random-but-valid step sequence for `domain_path`, dispatching each
42
+ # step against a throwaway boot. Fractions at `0.0` draw nothing from the RNG.
43
+ #
44
+ # @param seed [Integer] RNG seed; every draw is reproducible from it
45
+ # @param steps [Integer] number of generation attempts
46
+ # @param adversarial [Float] fraction of command steps mutated adversarially
47
+ # @param role_draw [Float] fraction of gated commands given a drawn caller
48
+ # @param dry_run [Float] fraction of command steps dispatched as dry runs
49
+ # @param prefix [Hash, nil] `{"seed", "steps", "favor", "prefix"}` replayed first
50
+ # @param favor [Array<String>] verbs the picker weights up
51
+ # @return [Array<Hash>] the generated steps
52
+ def self.generate(domain_path, seed:, steps:, **)
53
+ new(domain_path, seed: seed, steps: steps, **).call
54
+ end
55
+
56
+ # Coverage tuples reached (`[[attempt_index, tuple], ...]`) and every verb the
57
+ # booted catalog offered, so a campaign can tell an unhit verb from a missing one.
58
+ Trace = Struct.new(:steps, :coverage, :verbs, keyword_init: true)
59
+
60
+ # Generates one sequence like `.generate`, also returning the coverage tuples and
61
+ # verbs a campaign needs.
62
+ #
63
+ # @return [Trace] the generated steps, coverage tuples reached, and catalog verbs
64
+ def self.trace(domain_path, seed:, steps:, **)
65
+ generator = new(domain_path, seed: seed, steps: steps, **)
66
+ Trace.new(steps: generator.call, coverage: generator.coverage, verbs: generator.verbs)
67
+ end
68
+
69
+ # Weight of a `favor:` verb; comparable to UNEXERCISED_WEIGHT so favor steers
70
+ # without drowning out untouched verbs.
71
+ FAVOR_WEIGHT = 4
72
+
73
+ attr_reader :coverage, :verbs
74
+
75
+ # Sum of every `Result#events` length across the run; hecks fuzz declares it as the
76
+ # script's `expectations.events` claim. Zero means no interesting state was reached.
77
+ attr_reader :event_count
78
+
79
+ # Takes the same keywords as `.generate`, plus `adapter:` (default `:memory`).
80
+ #
81
+ # @raise [ArgumentError] if `adversarial`, `role_draw`, or `dry_run` is not a
82
+ # Numeric between 0.0 and 1.0
83
+ def initialize(domain_path, seed:, steps:, adapter: :memory, adversarial: 0.0, role_draw: 0.0, dry_run: 0.0,
84
+ prefix: nil, favor: [])
85
+ { adversarial: adversarial, role_draw: role_draw, dry_run: dry_run }.each do |name, fraction|
86
+ next if fraction.is_a?(Numeric) && fraction.between?(0, 1)
87
+
88
+ raise ArgumentError, "#{name}: must be a fraction between 0.0 and 1.0, got #{fraction.inspect}"
89
+ end
90
+
91
+ @domain_path = domain_path
92
+ @seed = seed
93
+ @step_count = steps
94
+ @adapter = adapter
95
+ @adversarial = adversarial.to_f
96
+ @role_draw = role_draw.to_f
97
+ @dry_run = dry_run.to_f
98
+ restart_random(seed)
99
+ @known_ids = Hash.new { |h, k| h[k] = [] }
100
+ @entity_known_ids = Hash.new { |h, k| h[k] = [] }
101
+ @appended_identities = Hash.new { |h, k| h[k] = [] }
102
+ # "Domain::Aggregate" => stored rows a query filters on (query_binding.rb).
103
+ @written_rows = Hash.new { |h, k| h[k] = [] }
104
+ # Role => actor ids granted by this sequence's own `RoleAssignment.Assign` steps.
105
+ @granted = Hash.new { |h, k| h[k] = [] }
106
+ @precedence_caller = nil
107
+ @exercised = Set.new
108
+ @event_count = 0
109
+ @prefix = prefix
110
+ @favor = Array(favor)
111
+ @own_favor = @favor
112
+ @coverage = []
113
+ @verbs = []
114
+ @attempt = 0
115
+ end
116
+
117
+ # Runs the generation against a fresh, isolated boot of the domain.
118
+ #
119
+ # @return [Array<Hash>] the generated steps; a picker miss is dropped
120
+ def call
121
+ # Leftover data under the example's data/ would start the run from state
122
+ # known_ids does not track; IsolatedBoot resets it and rebinds persistence to Memory.
123
+ IsolatedBoot.call(@domain_path, adapter: @adapter) do |copy|
124
+ runtime = Hecks.boot(copy, environment: nil)
125
+ catalog = build_catalog(runtime)
126
+ @verbs = catalog.values_at(:creating, :instance, :entity_commands, :queries, :entity_queries, :read_models)
127
+ .flatten.map { |entry| entry[:verb] }.uniq
128
+
129
+ steps = []
130
+ if @prefix
131
+ realize_prefix(runtime, catalog, @prefix, prefix_limit(@prefix), steps)
132
+ restart_random(@seed)
133
+ @favor = @own_favor
134
+ end
135
+ @step_count.times { steps << attempt_step(runtime, catalog) }
136
+ steps.compact
137
+ end
138
+ end
139
+
140
+ private
141
+
142
+ # Re-runs the first `limit` attempts of another seed's generation (its own prefix
143
+ # first), carrying its known ids and exercised verbs into this seed. The prefix is
144
+ # on top of this seed's budget: taking it out of the budget left too little to explore.
145
+ def realize_prefix(runtime, catalog, spec, limit, steps)
146
+ return 0 unless limit.positive?
147
+
148
+ inner = spec["prefix"]
149
+ used = inner ? realize_prefix(runtime, catalog, inner, [prefix_limit(inner), limit].min, steps) : 0
150
+ restart_random(Integer(spec.fetch("seed")))
151
+ @favor = Array(spec["favor"])
152
+ (limit - used).times { steps << attempt_step(runtime, catalog) }
153
+ limit
154
+ end
155
+
156
+ # Both streams restart together so a prefix replay draws the same query bindings.
157
+ def restart_random(seed)
158
+ @random = Random.new(seed)
159
+ @binding_random = Random.new(seed + QueryBinding::BINDING_SEED_OFFSET)
160
+ end
161
+
162
+ def prefix_limit(spec) = Integer(spec.fetch("steps")).clamp(0, @step_count)
163
+
164
+ def attempt_step(runtime, catalog)
165
+ index = @attempt
166
+ @attempt += 1
167
+ entry = pick(catalog)
168
+ return nil unless entry
169
+
170
+ @exercised << entry[:verb]
171
+ @state_before = "-"
172
+ step =
173
+ if entry[:query] then build_query_step(runtime, entry)
174
+ elsif entry[:model] then build_read_model_step(runtime, entry)
175
+ else build_command_step(runtime, catalog, entry)
176
+ end
177
+ @coverage << [index, coverage_tuple(entry, step)]
178
+ step
179
+ end
180
+
181
+ # `verb | kind | state before | mutation | outcome`; see CoverageCampaign.
182
+ def coverage_tuple(entry, step)
183
+ kind = %w[verb query dry_run].find { |key| step.key?(key) }
184
+ mutation = Array(step["adversarial"]).map { |m| [m["mutation"], m["shape"]].compact.join(":") }.join("+")
185
+ [entry[:verb], kind, @state_before, mutation.empty? ? "-" : mutation, @last_outcome].join(" | ")
186
+ end
187
+ end
188
+ end
189
+ end