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,51 @@
1
+ module Hecks
2
+ module Fuzzing
3
+ # Fields dropped before two histories are compared, declared once per shape they ride on.
4
+ # A literal except-list naming any of them under lib/hecks/fuzzing fails its spec.
5
+ module Nondeterministic
6
+ FIELDS = {
7
+ query_row: {
8
+ instances_at: "a full state snapshot taken for the query oracle's own use " \
9
+ "(Properties::Querying); the same state is already compared as `instances`, " \
10
+ "so keeping it would re-report one instances divergence under every query step"
11
+ }.freeze,
12
+ outbox_row: {
13
+ event_uid: "`Runtime::Outbox::Fanout#rows_for`'s own SecureRandom.uuid, minted fresh per " \
14
+ "enqueue and kept off `Event#to_h` — relay bookkeeping, never something the " \
15
+ "domain produced, so two otherwise-identical replays carry two different uuids",
16
+ delivery_id: "`\"\#{event_uid}/\#{consumer}\"` — inherits event_uid's per-enqueue uuid"
17
+ }.freeze,
18
+ event: {
19
+ occurred_at: "a wall-clock read; never reproducible byte-for-byte between two independent " \
20
+ "runs (Replay's own projected `:events` leave it off for the same reason)"
21
+ }.freeze,
22
+ history: {
23
+ bluebook: "a live IR object (the first-loaded bluebook) handed to properties — an object " \
24
+ "identity, not a value, so two boots never compare equal on it",
25
+ bluebooks: "the full live IR map, keyed by domain — object identities, same as `bluebook`"
26
+ }.freeze
27
+ }.freeze
28
+
29
+ module_function
30
+
31
+ # The field names declared for one comparison group.
32
+ #
33
+ # @raise [KeyError] if `group` names no declared group
34
+ def names(group)
35
+ FIELDS.fetch(group).keys
36
+ end
37
+
38
+ # Every declared name, across every group.
39
+ def all_names
40
+ FIELDS.values.flat_map(&:keys).uniq
41
+ end
42
+
43
+ # `hash` without `group`'s fields; expects symbol keys.
44
+ #
45
+ # @raise [KeyError] if `group` names no declared group
46
+ def strip(hash, group)
47
+ hash.except(*names(group))
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../runtime/errors"
4
+
5
+ module Hecks
6
+ module Fuzzing
7
+ # What a replay asks in place of a port's adapter: an object that refuses every question.
8
+ #
9
+ # A domain's adapters reach outside the process, and the chapters of this repository that
10
+ # drive tools (the shell, git, child processes, the file system) would run them for real from
11
+ # a generated sequence. A replay checks the domain's own rules, so the adapter never runs:
12
+ # the operation is refused, which a port operation records as its `refuses` event and a query
13
+ # answered by a port raises like any domain refusal.
14
+ #
15
+ # The refusal is a `GivenNotMet` for the runtime's sake but not one a `given` declares, and no
16
+ # property that reads guard descriptions quotes it.
17
+ class OutsideWorld
18
+ # The refusal every question to a stand-in ends in.
19
+ class Refused < Runtime::GivenNotMet; end
20
+
21
+ # @param port_name [String] the port whose adapter this stands in for
22
+ # @param asked [String] what was being asked, worded into the refusal
23
+ # @return [OutsideWorld] the stand-in
24
+ def self.call(port_name, asked)
25
+ new(port_name, asked)
26
+ end
27
+
28
+ # @param port_name [String] the port whose adapter this stands in for
29
+ # @param asked [String] what was being asked, worded into the refusal
30
+ def initialize(port_name, asked)
31
+ @port_name = port_name
32
+ @asked = asked
33
+ end
34
+
35
+ # Every method a port could declare exists here, and refuses.
36
+ def respond_to_missing?(_name, _include_private = false) = true
37
+
38
+ # @raise [Refused] always
39
+ def method_missing(name, *, **)
40
+ raise Refused, "#{@asked} was not asked of the #{@port_name} port's adapter: a replay runs " \
41
+ "no adapter (#{name})"
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,102 @@
1
+ require "json"
2
+ require_relative "replay"
3
+ require_relative "nondeterministic"
4
+
5
+ module Hecks
6
+ module Fuzzing
7
+ # Differential check of one Ruby engine against two persistences, `left:` and `right:`.
8
+ # Defaults to Memory vs a disposable `PostgresEra` database, which needs `database:`/`schema:`.
9
+ module PersistenceParity
10
+ module_function
11
+
12
+ # Replays `steps` on both adapters and lists where the results differ.
13
+ #
14
+ # Compares the same six fields as `hecks quality_control ask run`'s Ruby-vs-Rust mode. Both
15
+ # results are JSON-round-tripped so plain data compares equal on both sides.
16
+ #
17
+ # @param left [Symbol] an `IsolatedBoot` adapter: `:memory`, `:sqlite`, `:postgres`,
18
+ # `:postgres_era`
19
+ # @param right [Symbol] the second adapter, same set as `left`
20
+ # @param database [String, nil] required when either side is `:postgres_era`
21
+ # @param schema [String, nil] required when either side is `:postgres_era`
22
+ # @return [Array<Hash>] entries `{field: String, left => Object, right => Object}`,
23
+ # keyed by the adapter symbols; empty if both sides agree
24
+ def diff(domain_path, steps, left: :memory, right: :postgres_era, database: nil, schema: nil)
25
+ left_result = Replay.call(domain_path, steps, adapter: left, database: database, schema: schema)
26
+ right_result = Replay.call(domain_path, steps, adapter: right, database: database, schema: schema)
27
+
28
+ divergences = []
29
+ divergences.concat(diff_instances(left_result, right_result, left, right))
30
+ divergences.concat(diff_events(left_result, right_result, left, right))
31
+ divergences.concat(diff_refusals(left_result, right_result, left, right))
32
+ divergences.concat(diff_queries(left_result, right_result, left, right))
33
+ divergences.concat(diff_sagas(left_result, right_result, left, right))
34
+ divergences.concat(diff_reactions(left_result, right_result, left, right))
35
+ divergences
36
+ end
37
+
38
+ # Round-trips `value` through JSON so both sides compare as plain data.
39
+ def as_json(value) = JSON.parse(JSON.generate(value))
40
+
41
+ # Compares both sides' stored instances.
42
+ def diff_instances(left_result, right_result, left, right)
43
+ l = as_json(left_result[:instances])
44
+ r = as_json(right_result[:instances])
45
+ return [] if l == r
46
+
47
+ [{ field: "instances", left => l, right => r }]
48
+ end
49
+
50
+ # Compares both sides' emitted events.
51
+ def diff_events(left_result, right_result, left, right)
52
+ l = as_json(left_result[:events])
53
+ r = as_json(right_result[:events])
54
+ return [] if l == r
55
+
56
+ [{ field: "events", left => l, right => r }]
57
+ end
58
+
59
+ # Compares both sides' refusals, with `verb`/`kind` as strings.
60
+ def diff_refusals(left_result, right_result, left, right)
61
+ normalize = lambda do |refusals|
62
+ refusals.map { |r| { "verb" => r[:verb].to_s, "kind" => r[:kind].to_s, "error" => r[:error] } }
63
+ end
64
+ l = normalize.call(left_result[:refusals])
65
+ r = normalize.call(right_result[:refusals])
66
+ return [] if l == r
67
+
68
+ [{ field: "refusals", left => l, right => r }]
69
+ end
70
+
71
+ # Compares both sides' query answers, minus the `query_row` fields.
72
+ #
73
+ # The `Nondeterministic` `query_row` group is dropped; instances are already compared.
74
+ def diff_queries(left_result, right_result, left, right)
75
+ strip = ->(rows) { rows.map { |row| Nondeterministic.strip(row, :query_row) } }
76
+ l = as_json(strip.call(left_result[:queries]))
77
+ r = as_json(strip.call(right_result[:queries]))
78
+ return [] if l == r
79
+
80
+ [{ field: "queries", left => l, right => r }]
81
+ end
82
+
83
+ # Compares both sides' saga logs.
84
+ def diff_sagas(left_result, right_result, left, right)
85
+ l = as_json(left_result[:sagas])
86
+ r = as_json(right_result[:sagas])
87
+ return [] if l == r
88
+
89
+ [{ field: "sagas", left => l, right => r }]
90
+ end
91
+
92
+ # Compares both sides' reaction logs.
93
+ def diff_reactions(left_result, right_result, left, right)
94
+ l = as_json(left_result[:reactions])
95
+ r = as_json(right_result[:reactions])
96
+ return [] if l == r
97
+
98
+ [{ field: "reactions", left => l, right => r }]
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,88 @@
1
+ module Hecks
2
+ module Fuzzing
3
+ module Properties
4
+ # Property: every `corrects` mutation points at an earlier event in the same history.
5
+ module Corrections
6
+ # Every event emitted by a command with a `corrects` mutation, aggregate-level or
7
+ # nested in entities, needs an earlier event with the corrected name and same id.
8
+ #
9
+ # `history[:events]` is in occurrence order, so "earlier" is array position.
10
+ # Entity-level `corrects` is checked by neither dispatch nor build, so only this
11
+ # property catches it going wrong.
12
+ #
13
+ # @param history [Hash] a replayed history as returned by `Replay.call`
14
+ # @return [true, String] true if every event emitted by a `corrects`-bearing
15
+ # command has a matching, strictly earlier corrected event in the same
16
+ # history; otherwise a message listing every unmatched correction
17
+ def corrections_reference_an_emitted_event(history)
18
+ violations = []
19
+
20
+ (history[:bluebooks] || {}).each do |domain, bluebook|
21
+ bluebook.aggregates.each do |aggregate|
22
+ aggregate_key = "#{domain}::#{aggregate.hecks_name}"
23
+
24
+ each_command_including_entities(aggregate) do |command|
25
+ corrects_mutations = command.mutations.select { |mutation| mutation.op == :corrects }
26
+ next if corrects_mutations.empty?
27
+
28
+ corrects_mutations.each do |mutation|
29
+ corrected_event = mutation.target.to_s
30
+
31
+ command.emits.each do |produced_event_name|
32
+ violations.concat(unmatched_corrections(history[:events], aggregate_key,
33
+ produced_event_name.to_s, corrected_event,
34
+ command.hecks_name))
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ violations.empty? || violations.uniq.join("; ")
42
+ end
43
+
44
+ # Finds every occurrence of `produced_event_name`, on `aggregate_key`, with no
45
+ # matching `corrected_event` for the same id appearing earlier in `events`.
46
+ #
47
+ # @param events [Array<Hash>] `history[:events]`, in occurrence order
48
+ # @param aggregate_key [String] `"domain::AggregateName"` the events belong to
49
+ # @param produced_event_name [String] name of the event a `corrects` mutation's
50
+ # command emits
51
+ # @param corrected_event [String] name of the event the mutation claims to correct
52
+ # @param command_name [String] the command's own `hecks_name`, for the message
53
+ # @return [Array<String>] one message per occurrence with no matching earlier
54
+ # corrected event; empty when every occurrence is matched
55
+ def unmatched_corrections(events, aggregate_key, produced_event_name, corrected_event, command_name)
56
+ own_events = events.each_with_index.select do |event, _index|
57
+ event[:name] == produced_event_name && event[:aggregate] == aggregate_key
58
+ end
59
+
60
+ own_events.filter_map do |event, index|
61
+ preceding = events.first(index)
62
+ next if preceding.any? do |earlier|
63
+ earlier[:name] == corrected_event && earlier[:aggregate] == aggregate_key &&
64
+ earlier[:id].to_s == event[:id].to_s
65
+ end
66
+
67
+ "#{command_name} (#{aggregate_key}##{event[:id]}) emitted #{produced_event_name}, claiming to " \
68
+ "correct #{corrected_event}, but no #{corrected_event} for the same aggregate/id appears " \
69
+ "earlier in this history"
70
+ end
71
+ end
72
+
73
+ # Yields every command on `owner` and, recursively, on its entities (ADR 0026).
74
+ #
75
+ # @param owner [Bluebook::Aggregate, Bluebook::Entity] the aggregate or entity
76
+ # whose own commands, and whose entities' commands, to walk
77
+ # @yield [command] once per declared command, aggregate-level or nested
78
+ # @yieldparam command [Bluebook::Command] a command declared on `owner` or one
79
+ # of its entities
80
+ # @return [void]
81
+ def each_command_including_entities(owner, &block)
82
+ owner.commands.each(&block)
83
+ owner.entities.each { |entity| each_command_including_entities(entity, &block) }
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,314 @@
1
+ module Hecks
2
+ module Fuzzing
3
+ module Properties
4
+ # Property: a dry-run step leaves the event count and every instance unchanged.
5
+ # Entries without before/after snapshots (hand-built histories) are skipped.
6
+ module DryRuns
7
+ # Checks that every dry-run step left neither the event count nor any
8
+ # instance's state changed.
9
+ #
10
+ # @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
11
+ # @return [true, String] true if every dry-run trace shows no change; otherwise
12
+ # a semicolon-joined message naming each offending dry run
13
+ def dry_runs_leave_no_trace(history)
14
+ offenders = Array(history[:dry_run_traces]).filter_map do |entry|
15
+ before = entry[:before]
16
+ after = entry[:after]
17
+ next unless before && after
18
+
19
+ traces = []
20
+ traces << "events #{before[:events]} -> #{after[:events]}" unless before[:events] == after[:events]
21
+ traces << "instances changed" unless before[:instances] == after[:instances]
22
+ next if traces.empty?
23
+
24
+ "dry run of #{entry[:verb]} (ok: #{entry[:ok]}) left a trace: #{traces.join(', ')}"
25
+ end
26
+
27
+ offenders.empty? || offenders.join("; ")
28
+ end
29
+ end
30
+
31
+ # Properties: saga/policy dispatch args match their `with_spec`, and command
32
+ # mutations match an independent recomputation.
33
+ module DispatchAndMutations
34
+ # Checks saga and policy dispatch args against an independent re-derivation of
35
+ # `with_spec`. Re-deriving, not calling the interpreter again, keeps the check
36
+ # from agreeing with itself. Args are compared as captured live, because a
37
+ # saga's memory keeps changing across a run.
38
+ #
39
+ # @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
40
+ # @return [true, String] true if every saga and policy dispatch's own bound args
41
+ # match an independent re-derivation of their `with_spec`; otherwise a
42
+ # semicolon-joined message naming each offending dispatch
43
+ def dispatch_binding_fidelity(history)
44
+ saga_offenders = history.fetch(:saga_dispatches, []).filter_map do |entry|
45
+ expected = resolve_dispatch_binding(entry)
46
+ next if expected == entry[:args]
47
+
48
+ "#{entry[:process_manager]}##{entry[:instance]} dispatching #{entry[:dispatch]} on #{entry[:on]} — " \
49
+ "bound #{entry[:args].inspect}, but independently re-deriving with_spec's own resolution gives " \
50
+ "#{expected.inspect}"
51
+ end
52
+
53
+ policy_offenders = history.fetch(:policy_dispatches, []).filter_map do |entry|
54
+ expected = resolve_trigger_binding(entry)
55
+ next if expected == entry[:args]
56
+
57
+ "#{entry[:policy]} on #{entry[:on]} — bound #{entry[:args].inspect}, but independently re-deriving " \
58
+ "with_spec's own resolution gives #{expected.inspect}"
59
+ end
60
+
61
+ offenders = saga_offenders + policy_offenders
62
+ offenders.empty? || offenders.join("; ")
63
+ end
64
+
65
+ # Re-derives SagaInterpreter#dispatch_args: a literal, the correlation key,
66
+ # the triggering event's payload, or else the saga's carried memory.
67
+ def resolve_dispatch_binding(entry)
68
+ entry[:with_spec].to_h do |key, value|
69
+ resolved = if !value.is_a?(Symbol) then value
70
+ elsif value == entry[:correlation_head] then entry[:instance]
71
+ elsif entry[:event_payload].key?(value) then entry[:event_payload][value]
72
+ else entry[:memory][value]
73
+ end
74
+ [key.to_sym, Runtime::Value.materialize(resolved)]
75
+ end
76
+ end
77
+
78
+ # Re-derives PolicyInterpreter#trigger_args: a policy has no correlation or
79
+ # memory, so the trigger payload is the only source.
80
+ def resolve_trigger_binding(entry)
81
+ entry[:with_spec].to_h do |key, value|
82
+ resolved = value.is_a?(Symbol) ? entry[:payload][value] : value
83
+ [key.to_sym, Runtime::Value.materialize(resolved)]
84
+ end
85
+ end
86
+
87
+ # Ops with an independent recomputation here. `:increment` and `:decrement` are
88
+ # left out: their value-object and overflow handling is a much larger reproduction.
89
+ #
90
+ # Only the entity-scoped applier (`EntityElement#apply_to_element`) is
91
+ # reproduced, since replay captures mutation traces for entity-owned commands only.
92
+ # `:set` needs its own recomputation because the self-consistency checks share the
93
+ # same `step_apply_mutations` call and cannot see a wrong `:set` result.
94
+ #
95
+ # `:unrecomputable` (never a finding) marks a step whose fuzzer-malformed args do
96
+ # not fit the op's contract.
97
+ RECOMPUTABLE_MUTATION_OPS = %i[append remove multiply clamp set].freeze
98
+
99
+ # Checks that every recomputable mutation an entity-owned command
100
+ # applied landed on the same after-state an independent
101
+ # recomputation of the same rule produces.
102
+ #
103
+ # @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
104
+ # @return [true, String] true if every recomputable mutation's after-state
105
+ # matches an independent recomputation; otherwise a semicolon-joined message
106
+ # naming each offending mutation
107
+ def mutations_match_recompute(history)
108
+ bluebooks = history.fetch(:bluebooks)
109
+
110
+ offenders = history.fetch(:mutation_traces, []).flat_map do |entry|
111
+ next [] unless entry[:after]
112
+
113
+ command = command_for_verb(bluebooks, entry[:verb])
114
+ next [] unless command
115
+
116
+ aggregate = aggregate_for_verb(bluebooks, entry[:verb])
117
+ next [] unless aggregate
118
+
119
+ owner = owner_for_verb(bluebooks, entry[:verb]) || aggregate
120
+
121
+ command.mutations.select { |m| RECOMPUTABLE_MUTATION_OPS.include?(m.op) }.filter_map do |mutation|
122
+ expected = recompute_mutation(mutation, entry[:before][mutation.target], entry[:args], entry[:before],
123
+ aggregate, command, owner)
124
+ next if expected == :unrecomputable
125
+
126
+ actual = entry[:after][mutation.target]
127
+ next if symbolize_deep(expected) == symbolize_deep(actual)
128
+
129
+ "#{entry[:verb]} — #{mutation.op} on #{mutation.target} — recomputing independently gives " \
130
+ "#{expected.inspect}, but the real dispatch left #{actual.inspect}"
131
+ end
132
+ end
133
+
134
+ offenders.empty? || offenders.join("; ")
135
+ end
136
+
137
+ # Root aggregate for `verb`, derived from the verb alone because hand-built
138
+ # entries may lack `:domain`/`:aggregate`. Value-object types resolve against
139
+ # the root's namespace only.
140
+ def aggregate_for_verb(bluebooks, verb)
141
+ domain_name, aggregate_name, = Naming.split_verb(verb)
142
+ return nil unless domain_name
143
+
144
+ bluebooks[domain_name]&.aggregate(aggregate_name)
145
+ end
146
+
147
+ # The construct declaring `mutation.target`: the root aggregate, or the owning
148
+ # entity for a dot-shaped command. Attribute lookups must ask that construct,
149
+ # as `EntityElement#locate_chain` does.
150
+ def owner_for_verb(bluebooks, verb)
151
+ domain_name, aggregate_name, command_path = Naming.split_verb(verb)
152
+ return nil unless command_path
153
+
154
+ aggregate = bluebooks[domain_name]&.aggregate(aggregate_name)
155
+ return nil unless aggregate
156
+ return aggregate unless command_path.include?(".")
157
+
158
+ entity_name, = command_path.split(".", 2)
159
+ aggregate.entities.find { |candidate| candidate.hecks_name == entity_name }
160
+ end
161
+
162
+ # Dispatches to the recompute rule for `mutation.op`.
163
+ def recompute_mutation(mutation, current, args, before_scope, aggregate, command, owner = aggregate)
164
+ case mutation.op
165
+ when :append
166
+ recompute_append(current, mutation.source, before_scope, args, aggregate, command, owner, mutation.target)
167
+ when :remove then recompute_remove(current, mutation.source, args)
168
+ when :multiply then recompute_multiply(current, resolve_mutation_source(mutation.source, args))
169
+ when :clamp then recompute_clamp(current, mutation.source)
170
+ when :set then recompute_set(mutation.source, args, aggregate, owner, mutation.target)
171
+ end
172
+ end
173
+
174
+ # Re-derives the `:set` branch of `EntityElement#apply_to_element`. The
175
+ # attribute comes from `owner`, not the root aggregate, which would leave a
176
+ # nested entity's value uncoerced and disagree with the real after-state.
177
+ # Any raise means the raw material was wrong, so the result is `:unrecomputable`.
178
+ def recompute_set(source, args, aggregate, owner, target)
179
+ raw = resolve_mutation_source(source, args)
180
+ attribute = owner&.attribute(target)
181
+ coerced = attribute ? Runtime::Value.for_attribute(aggregate, attribute, raw) : raw
182
+ Runtime::Value.materialize(coerced)
183
+ rescue StandardError
184
+ :unrecomputable
185
+ end
186
+
187
+ # Re-derives `EntityElement#appended_to_element`: each field resolves from a
188
+ # caller arg (coerced as `Interpreting#coerce_declared_arguments` does) or from
189
+ # the entity's current field (already materialized), then the element is appended.
190
+ # A nested-entity element also gets its declared defaults.
191
+ def recompute_append(current, source_map, before_scope, args, aggregate, command, owner = aggregate, target = nil)
192
+ fields = source_map.transform_values do |source|
193
+ resolve_mutation_append_field(source, before_scope, args, aggregate, command)
194
+ end
195
+ fill_recompute_declared_defaults(aggregate, owner, target, fields)
196
+ Array(current) + [symbolize_deep(fields)]
197
+ end
198
+
199
+ # Fills defaults for a nested-entity element via `Instance.default_for`, which is
200
+ # independently tested. No-op unless `target` names an entity under `owner`
201
+ # (`owner.entities`, not `aggregate.entities`).
202
+ #
203
+ # A value-object element type (the more common shape: `list_of` a plain composite,
204
+ # not a nested entity) defaults the other way real dispatch does — through
205
+ # `Runtime::Value.apply_defaults`, the exact primitive `EntityElement#appended_to_
206
+ # element`'s own `Value.build` call uses. Without this branch, a caller-omitted,
207
+ # VO-declared `default:` field is left out of the recomputed element entirely,
208
+ # disagreeing with real dispatch's fully-defaulted one.
209
+ def fill_recompute_declared_defaults(aggregate, owner, target, fields)
210
+ return fields unless target
211
+
212
+ element_type = owner&.attribute(target)&.type
213
+ return fields unless element_type
214
+
215
+ entity = owner.entities.find { |piece| piece.hecks_name == element_type.to_s }
216
+ if entity
217
+ entity.attributes.each do |attribute|
218
+ next if fields.key?(attribute.name)
219
+
220
+ fields[attribute.name] = attribute.list? ? [] : Runtime::Instance.default_for(aggregate, attribute)
221
+ end
222
+ return fields
223
+ end
224
+
225
+ value_object = aggregate.value_object(element_type)
226
+ return fields unless value_object
227
+
228
+ Runtime::Value.apply_defaults(value_object, fields)
229
+ end
230
+
231
+ # Resolves one appended field's value from its declared source.
232
+ def resolve_mutation_append_field(source, before_scope, args, aggregate, command)
233
+ return source unless source.is_a?(Symbol)
234
+ return before_scope[source] unless args.key?(source)
235
+
236
+ coerce_recompute_append_arg(aggregate, command, source, args[source])
237
+ end
238
+
239
+ # Coerces a raw arg only when `source` is a declared attribute of the command,
240
+ # as a real dispatch does. Materialized so the comparison is plain data on both sides.
241
+ def coerce_recompute_append_arg(aggregate, command, source, raw)
242
+ attribute = command.attribute(source)
243
+ return raw unless attribute
244
+
245
+ Runtime::Value.materialize(Runtime::Value.for_attribute(aggregate, attribute, raw, argument: true))
246
+ end
247
+
248
+ # Re-derives `MutationApplier#removed`: removes every value-equal element.
249
+ # `Value.materialize` first: once `args` arrives normalized (replay.rb's
250
+ # `build_mutation_trace`), a composite `remove:` source is already a built
251
+ # `Runtime::Value` with its own declared defaults filled — comparing it
252
+ # unmaterialized against `current`'s plain Hashes would never match, wrongly
253
+ # keeping an element real dispatch correctly removed.
254
+ def recompute_remove(current, source, args)
255
+ target = symbolize_deep(Runtime::Value.materialize(resolve_mutation_source(source, args)))
256
+ Array(current).reject { |element| symbolize_deep(element) == target }
257
+ end
258
+
259
+ # Re-derives `CommandRules::Arithmetic#multiply` on plain data: a Hash with one
260
+ # numeric field scales that field, a bare Numeric scales itself. A nil
261
+ # `current` is treated as 0.
262
+ def recompute_multiply(current, amount)
263
+ return :unrecomputable unless amount.is_a?(Numeric)
264
+
265
+ current ||= 0
266
+ if current.is_a?(Hash)
267
+ field = current.keys.find { |f| current[f].is_a?(Numeric) }
268
+ return :unrecomputable unless field
269
+
270
+ current.merge(field => current[field] * amount)
271
+ elsif current.is_a?(Numeric)
272
+ current * amount
273
+ else
274
+ :unrecomputable
275
+ end
276
+ end
277
+
278
+ # Re-derives `CommandRules::Arithmetic#clamp` like #recompute_multiply. The
279
+ # source is always a literal `[min, max]`, never an argument reference.
280
+ def recompute_clamp(current, bounds)
281
+ return :unrecomputable unless bounds.is_a?(Array) && bounds.size == 2
282
+
283
+ min, max = bounds
284
+ current ||= 0
285
+ if current.is_a?(Hash)
286
+ field = current.keys.find { |f| current[f].is_a?(Numeric) }
287
+ return :unrecomputable unless field
288
+
289
+ current.merge(field => current[field].clamp(min, max))
290
+ elsif current.is_a?(Numeric)
291
+ current.clamp(min, max)
292
+ else
293
+ :unrecomputable
294
+ end
295
+ end
296
+
297
+ # A mutation source is an argument name (Symbol) or a literal.
298
+ def resolve_mutation_source(source, args)
299
+ source.is_a?(Symbol) ? args[source] : source
300
+ end
301
+
302
+ # Symbolizes hash keys recursively. Generated args carry string keys while
303
+ # materialized trace state carries symbols, and `Hash#==` would otherwise differ.
304
+ def symbolize_deep(value)
305
+ case value
306
+ when Hash then value.to_h { |key, val| [key.to_sym, symbolize_deep(val)] }
307
+ when Array then value.map { |val| symbolize_deep(val) }
308
+ else value
309
+ end
310
+ end
311
+ end
312
+ end
313
+ end
314
+ end