hecks 1.2.0 → 1.4.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 (358) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +7 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +10 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
  7. data/lib/hecks/adapters/driven/heki/journal.rb +60 -2
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
  9. data/lib/hecks/adapters/driven/heki.rb +13 -7
  10. data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
  12. data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
  13. data/lib/hecks/adapters/driven/lambda.rb +39 -33
  14. data/lib/hecks/adapters/driven/local_storage.rb +17 -10
  15. data/lib/hecks/adapters/driven/memory.rb +205 -9
  16. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
  17. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  18. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  19. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  20. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  21. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  22. data/lib/hecks/adapters/driven/postgres_era.adapter +5 -0
  23. data/lib/hecks/adapters/driven/prism.rb +4 -4
  24. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  25. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  26. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  27. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  28. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  29. data/lib/hecks/adapters/driven.rb +4 -4
  30. data/lib/hecks/adapters/driving/github_webhook.rb +145 -0
  31. data/lib/hecks/behaviors/dsl.rb +2 -2
  32. data/lib/hecks/behaviors/expectations.rb +51 -23
  33. data/lib/hecks/behaviors/ir.rb +1 -1
  34. data/lib/hecks/behaviors/rspec.rb +1 -1
  35. data/lib/hecks/behaviors/runner.rb +2 -2
  36. data/lib/hecks/behaviors.rb +1 -1
  37. data/lib/hecks/bluebook/aggregate.rb +13 -13
  38. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
  39. data/lib/hecks/bluebook/assembly/build.rb +1 -1
  40. data/lib/hecks/bluebook/assembly/contract.rb +39 -16
  41. data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
  42. data/lib/hecks/bluebook/assembly/marks.rb +18 -18
  43. data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
  44. data/lib/hecks/bluebook/assembly.rb +9 -9
  45. data/lib/hecks/bluebook/attribute.rb +9 -9
  46. data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
  47. data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
  48. data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
  49. data/lib/hecks/bluebook/behaviour/command.rb +23 -23
  50. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -3
  51. data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
  52. data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
  53. data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
  54. data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
  55. data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
  56. data/lib/hecks/bluebook/behaviour/query.rb +1 -1
  57. data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
  58. data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
  59. data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
  60. data/lib/hecks/bluebook/capabilities.rb +27 -0
  61. data/lib/hecks/bluebook/chapter.rb +28 -9
  62. data/lib/hecks/bluebook/command.rb +12 -12
  63. data/lib/hecks/bluebook/domain_port.rb +9 -9
  64. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  65. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  66. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
  67. data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
  68. data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
  69. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
  70. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
  71. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  72. data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
  73. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  74. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  75. data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
  76. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  77. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
  78. data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
  79. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  80. data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
  81. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  82. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  83. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
  84. data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
  85. data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
  86. data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
  87. data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
  88. data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
  89. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  90. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  91. data/lib/hecks/bluebook/entity.rb +11 -11
  92. data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
  93. data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
  94. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
  95. data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
  96. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
  97. data/lib/hecks/bluebook/expression/resolver.rb +60 -62
  98. data/lib/hecks/bluebook/hexagon.rb +1 -1
  99. data/lib/hecks/bluebook/lifecycle.rb +1 -1
  100. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
  101. data/lib/hecks/bluebook/meta_validator/judge.rb +122 -86
  102. data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
  103. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
  104. data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
  105. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
  106. data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
  107. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
  108. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
  109. data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
  110. data/lib/hecks/bluebook/meta_validator.rb +70 -70
  111. data/lib/hecks/bluebook/model_check.rb +301 -84
  112. data/lib/hecks/bluebook/pattern_subset.rb +9 -9
  113. data/lib/hecks/bluebook/policy.rb +15 -13
  114. data/lib/hecks/bluebook/process_manager.rb +14 -14
  115. data/lib/hecks/bluebook/project_register.rb +6 -6
  116. data/lib/hecks/bluebook/query.rb +4 -4
  117. data/lib/hecks/bluebook/read_model.rb +14 -14
  118. data/lib/hecks/bluebook/reference.rb +8 -8
  119. data/lib/hecks/bluebook/smoke_test.rb +19 -19
  120. data/lib/hecks/bluebook/synthesizer.rb +12 -12
  121. data/lib/hecks/bluebook/translation.rb +4 -4
  122. data/lib/hecks/bluebook/value_object.rb +6 -6
  123. data/lib/hecks/bluebook.rb +2 -2
  124. data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
  125. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
  126. data/lib/hecks/codemod.rb +36 -35
  127. data/lib/hecks/construct.rb +6 -6
  128. data/lib/hecks/corpus.rb +317 -0
  129. data/lib/hecks/deprecation.rb +95 -0
  130. data/lib/hecks/doc/reference.rb +19 -19
  131. data/lib/hecks/embryonaut_bluebook.rb +11 -11
  132. data/lib/hecks/facade/cli_door.rb +69 -10
  133. data/lib/hecks/facade/cli_runner.rb +105 -24
  134. data/lib/hecks/facade/command_request.rb +23 -0
  135. data/lib/hecks/facade/handle.rb +79 -32
  136. data/lib/hecks/facade/json_door.rb +106 -25
  137. data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
  138. data/lib/hecks/facade/surface/chapter.rb +26 -17
  139. data/lib/hecks/facade/surface.rb +16 -3
  140. data/lib/hecks/facade.rb +15 -4
  141. data/lib/hecks/forms/app.rb +46 -30
  142. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  143. data/lib/hecks/forms/field_renderer.rb +142 -6
  144. data/lib/hecks/forms/field_shape.rb +183 -20
  145. data/lib/hecks/forms/html.rb +51 -7
  146. data/lib/hecks/forms/index_renderer.rb +14 -2
  147. data/lib/hecks/forms/params.rb +120 -23
  148. data/lib/hecks/forms/port_argument.rb +2 -2
  149. data/lib/hecks/forms/query_form_renderer.rb +2 -2
  150. data/lib/hecks/forms/record_renderer.rb +2 -2
  151. data/lib/hecks/forms/record_table.rb +1 -1
  152. data/lib/hecks/forms/value_object_shape.rb +3 -3
  153. data/lib/hecks/forms.rb +24 -4
  154. data/lib/hecks/fqn.rb +1 -1
  155. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  156. data/lib/hecks/framework.rb +48 -17
  157. data/lib/hecks/freezer.rb +11 -11
  158. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
  159. data/lib/hecks/fuzzing/combination_miner.rb +119 -0
  160. data/lib/hecks/fuzzing/concurrent_dispatch.rb +361 -0
  161. data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
  162. data/lib/hecks/fuzzing/differential.rb +158 -0
  163. data/lib/hecks/fuzzing/domain_generator.rb +694 -0
  164. data/lib/hecks/fuzzing/era_boundary.rb +124 -0
  165. data/lib/hecks/fuzzing/form_census.rb +199 -0
  166. data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
  167. data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
  168. data/lib/hecks/fuzzing/isolated_boot.rb +226 -38
  169. data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
  170. data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
  171. data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
  172. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
  173. data/lib/hecks/fuzzing/properties/guards.rb +129 -26
  174. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
  175. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
  176. data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
  177. data/lib/hecks/fuzzing/properties/querying.rb +17 -17
  178. data/lib/hecks/fuzzing/properties.rb +84 -39
  179. data/lib/hecks/fuzzing/qa_settings.rb +152 -0
  180. data/lib/hecks/fuzzing/replay.rb +224 -114
  181. data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
  182. data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
  183. data/lib/hecks/fuzzing/self_consistency.rb +676 -0
  184. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
  189. data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
  190. data/lib/hecks/fuzzing/shrinker.rb +197 -0
  191. data/lib/hecks/fuzzing/structural_skips.rb +39 -0
  192. data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
  193. data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
  194. data/lib/hecks/fuzzing/value_generator.rb +66 -14
  195. data/lib/hecks/fuzzing.rb +11 -0
  196. data/lib/hecks/grammar/evolve.rb +10 -10
  197. data/lib/hecks/grammar.rb +7 -7
  198. data/lib/hecks/ir.rb +13 -13
  199. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  200. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  201. data/lib/hecks/language/bluebook/vocabulary.bluebook +365 -15
  202. data/lib/hecks/language/oidc.json +5 -0
  203. data/lib/hecks/literal.rb +9 -9
  204. data/lib/hecks/naming.rb +89 -21
  205. data/lib/hecks/ports/access_control.rb +58 -2
  206. data/lib/hecks/ports/agent/answers.rb +83 -6
  207. data/lib/hecks/ports/agent.rb +119 -35
  208. data/lib/hecks/ports/authentication.rb +44 -4
  209. data/lib/hecks/ports/authorization.rb +53 -11
  210. data/lib/hecks/ports/clock.rb +42 -23
  211. data/lib/hecks/ports/extraction.rb +16 -0
  212. data/lib/hecks/ports/identity_assignment.rb +24 -2
  213. data/lib/hecks/ports/identity_generation.rb +17 -3
  214. data/lib/hecks/ports/identity_resolution.rb +18 -1
  215. data/lib/hecks/ports/loading.rb +4 -0
  216. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  217. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  218. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  219. data/lib/hecks/ports/persistence/execution.rb +4 -0
  220. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  221. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  222. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  223. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  224. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  225. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  226. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  227. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  228. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  229. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  230. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  231. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +160 -58
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +44 -6
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +338 -83
  242. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  243. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  244. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  245. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
  246. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  247. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  248. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  249. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
  250. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
  251. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
  252. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  253. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  254. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  255. data/lib/hecks/ports/persistence/repository_factory.rb +29 -6
  256. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  257. data/lib/hecks/ports/persistence.rb +36 -1
  258. data/lib/hecks/ports/projection.rb +61 -7
  259. data/lib/hecks/ports/query/in_memory.rb +3 -3
  260. data/lib/hecks/ports/query/ordering.rb +6 -6
  261. data/lib/hecks/ports/query.rb +35 -0
  262. data/lib/hecks/projections/bootstrap_table.rb +112 -0
  263. data/lib/hecks/projections/diagrams.rb +75 -75
  264. data/lib/hecks/projections/glossary/html.rb +250 -0
  265. data/lib/hecks/projections/glossary/markdown.rb +105 -0
  266. data/lib/hecks/projections/glossary/mermaid.rb +110 -0
  267. data/lib/hecks/projections/glossary/page.css +271 -0
  268. data/lib/hecks/projections/glossary/page.js +72 -0
  269. data/lib/hecks/projections/glossary/sections.rb +17 -0
  270. data/lib/hecks/projections/glossary/sentences.rb +205 -0
  271. data/lib/hecks/projections/glossary.rb +214 -286
  272. data/lib/hecks/projections/ir.rb +1 -1
  273. data/lib/hecks/projections/model/deviations.rb +18 -17
  274. data/lib/hecks/projections/model.rb +25 -21
  275. data/lib/hecks/projections/oidc.rb +7 -7
  276. data/lib/hecks/projections/parser_table.rb +5 -5
  277. data/lib/hecks/projections/reference.rb +3 -3
  278. data/lib/hecks/projections/rust_vocabulary.rb +443 -0
  279. data/lib/hecks/projections/shape.rb +2 -2
  280. data/lib/hecks/projections/statements.rb +11 -11
  281. data/lib/hecks/projections/vocabulary.rb +9 -9
  282. data/lib/hecks/projections.rb +5 -3
  283. data/lib/hecks/projector/cli_projector.rb +29 -29
  284. data/lib/hecks/projector/docs_projector.rb +13 -13
  285. data/lib/hecks/projector/exporter.rb +42 -21
  286. data/lib/hecks/projector/ir_projector.rb +1 -1
  287. data/lib/hecks/projector/narrate_projector.rb +15 -22
  288. data/lib/hecks/projector/target.rb +13 -13
  289. data/lib/hecks/projector.rb +15 -15
  290. data/lib/hecks/query_ir.rb +47 -47
  291. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  292. data/lib/hecks/query_specification/common/comparison.rb +132 -24
  293. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  294. data/lib/hecks/query_specification/common/null_policy.rb +57 -13
  295. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  296. data/lib/hecks/query_specification/common/options.rb +25 -0
  297. data/lib/hecks/query_specification/field_path.rb +69 -15
  298. data/lib/hecks/query_specification/hop_path.rb +57 -20
  299. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  300. data/lib/hecks/rendering.rb +3 -3
  301. data/lib/hecks/router/namespace_installer.rb +3 -3
  302. data/lib/hecks/router.rb +1 -1
  303. data/lib/hecks/runtime/aggregate_lock.rb +11 -11
  304. data/lib/hecks/runtime/boot_gates.rb +3 -3
  305. data/lib/hecks/runtime/caller.rb +8 -8
  306. data/lib/hecks/runtime/capability_graph.rb +2 -2
  307. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
  308. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +64 -67
  309. data/lib/hecks/runtime/command_interpreter.rb +159 -102
  310. data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
  311. data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
  312. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  313. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  314. data/lib/hecks/runtime/command_rules/references.rb +94 -19
  315. data/lib/hecks/runtime/command_rules.rb +3 -0
  316. data/lib/hecks/runtime/dependency_planning.rb +11 -11
  317. data/lib/hecks/runtime/dispatcher.rb +283 -90
  318. data/lib/hecks/runtime/entity_element.rb +282 -46
  319. data/lib/hecks/runtime/entity_interpreter.rb +160 -69
  320. data/lib/hecks/runtime/errors.rb +19 -19
  321. data/lib/hecks/runtime/event.rb +6 -6
  322. data/lib/hecks/runtime/identity.rb +22 -22
  323. data/lib/hecks/runtime/instance.rb +39 -14
  324. data/lib/hecks/runtime/interpreting.rb +12 -12
  325. data/lib/hecks/runtime/invocation.rb +276 -0
  326. data/lib/hecks/runtime/loader.rb +14 -14
  327. data/lib/hecks/runtime/outbox.rb +23 -23
  328. data/lib/hecks/runtime/policy_interpreter.rb +54 -54
  329. data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
  330. data/lib/hecks/runtime/query_interpreter.rb +111 -56
  331. data/lib/hecks/runtime/reaction_invocation.rb +76 -9
  332. data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
  333. data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
  334. data/lib/hecks/runtime/reference_hop.rb +6 -6
  335. data/lib/hecks/runtime/refusal_wording.rb +92 -112
  336. data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
  337. data/lib/hecks/runtime/registry/verification.rb +36 -26
  338. data/lib/hecks/runtime/registry.rb +56 -27
  339. data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
  340. data/lib/hecks/runtime/routing.rb +10 -88
  341. data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
  342. data/lib/hecks/runtime/saga_interpreter.rb +93 -56
  343. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  344. data/lib/hecks/runtime/tenant_check.rb +9 -9
  345. data/lib/hecks/runtime/tenant_scope.rb +5 -5
  346. data/lib/hecks/runtime/value/admission.rb +75 -30
  347. data/lib/hecks/runtime/value/coercion.rb +379 -226
  348. data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
  349. data/lib/hecks/runtime/value.rb +28 -23
  350. data/lib/hecks/runtime.rb +7 -7
  351. data/lib/hecks/storehouse.rb +64 -64
  352. data/lib/hecks/version.rb +3 -3
  353. data/lib/hecks/vocabulary.rb +207 -5
  354. data/lib/hecks.rb +13 -11
  355. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
  356. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
  357. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
  358. metadata +43 -2
@@ -7,6 +7,7 @@ require_relative "sequence_generator/catalog"
7
7
  require_relative "sequence_generator/picker"
8
8
  require_relative "sequence_generator/step_builder"
9
9
  require_relative "sequence_generator/outcome_tracker"
10
+ require_relative "sequence_generator/adversary"
10
11
 
11
12
  module Hecks
12
13
  module Fuzzing
@@ -14,13 +15,13 @@ module Hecks
14
15
  # `{name, note, steps}` shape `spec/corpus/*.json` already uses — so a
15
16
  # generated sequence replays as a corpus member, completely unchanged.
16
17
  # Boots a throwaway copy of
17
- # the domain and DISPATCHES each candidate step for real as it builds the
18
+ # the domain and dispatches each candidate step for real as it builds the
18
19
  # sequence (not just synthesizing plausible-looking JSON) : the only way to
19
20
  # know whether a step actually reached a new state, or which id an
20
21
  # auto-minted entity landed on, is to run it and watch what happened.
21
22
  #
22
23
  # Single-call fuzzing mostly misses the bugs this project has actually
23
- # found — they needed STATE first (a saga leg acting on a transfer that
24
+ # found — they needed state first (a saga leg acting on a transfer that
24
25
  # already exists, a reference pointing at a customer already registered).
25
26
  # So this tracks what it has created as it goes, the same way
26
27
  # spec/banking_state_machine_spec.rb's hand-written generator does, and
@@ -28,13 +29,16 @@ module Hecks
28
29
  #
29
30
  # One concern per file beside this one: what the domain offers
30
31
  # (sequence_generator/catalog.rb), which step to try next (picker.rb),
31
- # how a step is built and dispatched (step_builder.rb), and what a
32
- # success taught us (outcome_tracker.rb).
32
+ # how a step is built and dispatched (step_builder.rb), what a
33
+ # success taught us (outcome_tracker.rb), and — opt-in, `adversarial:`
34
+ # — the argument shapes real Ruby/Rust divergences were found through
35
+ # (adversary.rb).
33
36
  class SequenceGenerator
34
37
  include Catalog
35
38
  include Picker
36
39
  include StepBuilder
37
40
  include OutcomeTracker
41
+ include Adversary
38
42
 
39
43
  # Creating commands are always eligible ; weighting them heavier (not
40
44
  # exclusively — a domain with only one or two aggregates would starve
@@ -43,18 +47,18 @@ module Hecks
43
47
  CREATING_WEIGHT = 2
44
48
 
45
49
  # How often a payload is deliberately the wrong shape. Low, because a
46
- # refused step reaches no new state and a sequence of them is SILENT.
50
+ # refused step reaches no new state and a sequence of them is silent.
47
51
  MALFORMED_ARGUMENT_PROBABILITY = 0.12
48
52
 
49
- # HOW OFTEN AN OPTIONAL ARGUMENT IS SIMPLY NOT GIVEN — a fair coin,
53
+ # How often an optional argument is simply not given — a fair coin,
50
54
  # because that is exactly what `optional:` means: present or absent,
51
55
  # both legal, neither the interesting case.
52
56
  #
53
- # This is NOT a malformation and must not be filed as one. `malform`
54
- # drops an argument too, but a dropped REQUIRED argument is refused,
55
- # a refusal writes no record, and bin/fuzz counts it SILENT — so the
57
+ # This is not a malformation and must not be filed as one. `malform`
58
+ # drops an argument too, but a dropped required argument is refused,
59
+ # a refusal writes no record, and bin/fuzz counts it silent — so the
56
60
  # one outcome worth reaching (a stored record carrying a null, then
57
- # QUERIED) was unreachable from that path by construction. Until
61
+ # queried) was unreachable from that path by construction. Until
58
62
  # this existed no generated history contained a null at all, which
59
63
  # meant `query_answers_match_reference` — the differential that
60
64
  # diffs every native adapter against the reference interpreter — had
@@ -68,62 +72,179 @@ module Hecks
68
72
  # How strongly an unexercised verb is preferred over one this sequence has
69
73
  # already dispatched. Random picking revisits the same handful of verbs and
70
74
  # leaves whole commands untouched for a whole run — which is the same
71
- # "reached no interesting state" problem the SILENT count reports, seen from
75
+ # "reached no interesting state" problem the silent count reports, seen from
72
76
  # the generating end rather than the scoring end.
73
77
  UNEXERCISED_WEIGHT = 4
74
78
 
75
- def self.generate(domain_path, seed:, steps:, adapter: :memory)
76
- new(domain_path, seed: seed, steps: steps, adapter: adapter).call
79
+ # `adversarial:` the fraction of generated command steps (0.0..1.0)
80
+ # that get one deliberately adversarial argument mutation
81
+ # (adversary.rb — the shapes BUG#7–#16 were found through). `0.0`,
82
+ # the default, draws nothing extra from the seeded RNG, so a seed's
83
+ # output is byte-for-byte what it was before the option existed;
84
+ # any positive value is just as deterministic per seed, since every
85
+ # choice the mutation makes comes from the same `Random.new(seed)`.
86
+ #
87
+ # `role_draw:` / `dry_run:` — two more opt-in fractions with the
88
+ # identical contract (adversary.rb's `caller_draw!`, step_builder.rb's
89
+ # `dry_run_draw?`): `0.0` draws nothing, so every pinned seed is
90
+ # byte-for-byte what it was; `bin/qa_sweep` reads them from
91
+ # `QualityControlDials::ROLE_DRAW_PROBABILITY`/`DRY_RUN_FRACTION`.
92
+ #
93
+ # `prefix:` / `favor:` — the two levers `CoverageCampaign` pulls
94
+ # (coverage_campaign.rb has the why). `prefix:` is `{ "seed", "steps",
95
+ # "favor", "prefix" }`: re-generate that seed's first `steps` attempts
96
+ # (itself recursively prefixed) before this seed's own randomness
97
+ # starts, so a seed can begin from state an earlier seed reached.
98
+ # `favor:` names verbs the picker weights up. `nil`/`[]`, the
99
+ # defaults, draw nothing extra and change nothing: every pinned seed
100
+ # is byte-for-byte what it was.
101
+ def self.generate(domain_path, seed:, steps:, **)
102
+ new(domain_path, seed: seed, steps: steps, **).call
103
+ end
104
+
105
+ # The same generation, with what it reached — `coverage` is
106
+ # `[[attempt_index, tuple], ...]` (`coverage_tuple`), `verbs` every
107
+ # verb the booted catalog offered, so a campaign can tell a verb it
108
+ # never hit from one that does not exist.
109
+ Trace = Struct.new(:steps, :coverage, :verbs, keyword_init: true)
110
+
111
+ def self.trace(domain_path, seed:, steps:, **)
112
+ generator = new(domain_path, seed: seed, steps: steps, **)
113
+ Trace.new(steps: generator.call, coverage: generator.coverage, verbs: generator.verbs)
77
114
  end
78
115
 
79
- # How many EVENTS the generated sequence actually produced not
116
+ # How strongly a `favor:` verb is preferred when it is eligible
117
+ # the same order of magnitude as an unexercised verb, so favor
118
+ # steers without drowning out what this sequence has not touched.
119
+ FAVOR_WEIGHT = 4
120
+
121
+ attr_reader :coverage, :verbs
122
+
123
+ # How many events the generated sequence actually produced — not
80
124
  # steps, not successful dispatches, but the sum of every Result#events
81
125
  # length across the run. This is the count bin/fuzz declares as the
82
126
  # script's own `expectations.events` claim: whatever was achieved
83
- # DURING generation becomes the claim a fresh replay of the same
127
+ # during generation becomes the claim a fresh replay of the same
84
128
  # script is held to. Zero means the sequence never
85
129
  # reached an interesting state — a fuzzer-effectiveness fact, not a
86
130
  # replay one.
87
131
  attr_reader :event_count
88
132
 
89
- def initialize(domain_path, seed:, steps:, adapter: :memory)
90
- @domain_path = domain_path
91
- @seed = seed
92
- @step_count = steps
93
- @adapter = adapter
94
- @random = Random.new(seed)
95
- @known_ids = Hash.new { |h, k| h[k] = [] }
96
- @entity_known_ids = Hash.new { |h, k| h[k] = [] }
97
- @exercised = Set.new
98
- @event_count = 0
133
+ def initialize(domain_path, seed:, steps:, adapter: :memory, adversarial: 0.0, role_draw: 0.0, dry_run: 0.0,
134
+ prefix: nil, favor: [])
135
+ { adversarial: adversarial, role_draw: role_draw, dry_run: dry_run }.each do |name, fraction|
136
+ next if fraction.is_a?(Numeric) && fraction.between?(0, 1)
137
+
138
+ raise ArgumentError, "#{name}: must be a fraction between 0.0 and 1.0, got #{fraction.inspect}"
139
+ end
140
+
141
+ @domain_path = domain_path
142
+ @seed = seed
143
+ @step_count = steps
144
+ @adapter = adapter
145
+ @adversarial = adversarial.to_f
146
+ @role_draw = role_draw.to_f
147
+ @dry_run = dry_run.to_f
148
+ @random = Random.new(seed)
149
+ @known_ids = Hash.new { |h, k| h[k] = [] }
150
+ @entity_known_ids = Hash.new { |h, k| h[k] = [] }
151
+ @appended_identities = Hash.new { |h, k| h[k] = [] }
152
+ # Role => [actor ids] this sequence's own successful
153
+ # `Governance::RoleAssignment.Assign` steps granted — what the
154
+ # `actor_known` caller shape draws from (adversary.rb).
155
+ @granted = Hash.new { |h, k| h[k] = [] }
156
+ @precedence_caller = nil
157
+ @exercised = Set.new
158
+ @event_count = 0
159
+ @prefix = prefix
160
+ @favor = Array(favor)
161
+ @own_favor = @favor
162
+ @coverage = []
163
+ @verbs = []
164
+ @attempt = 0
99
165
  end
100
166
 
101
167
  def call
102
168
  # Real leftover data from ordinary use (bin/console, whatever) lives
103
169
  # under the example's data/ — a generator that boots against it
104
170
  # starts from state its own known_ids tracking doesn't know about.
105
- # IsolatedBoot resets that AND rebinds persistence to Memory, since
171
+ # IsolatedBoot resets that and rebinds persistence to Memory, since
106
172
  # a Postgres-bound domain's real store lives outside the copied
107
173
  # directory entirely and `rm_rf`ing data/ alone cannot reach it —
108
174
  # see isolated_boot.rb's own header.
109
175
  IsolatedBoot.call(@domain_path, adapter: @adapter) do |copy|
110
176
  runtime = Hecks.boot(copy)
111
177
  catalog = build_catalog(runtime)
112
- Array.new(@step_count) { attempt_step(runtime, catalog) }.compact
178
+ @verbs = catalog.values_at(:creating, :instance, :entity_commands, :queries, :entity_queries, :read_models)
179
+ .flatten.map { |entry| entry[:verb] }.uniq
180
+
181
+ steps = []
182
+ if @prefix
183
+ realize_prefix(runtime, catalog, @prefix, prefix_limit(@prefix), steps)
184
+ @random = Random.new(@seed)
185
+ @favor = @own_favor
186
+ end
187
+ @step_count.times { steps << attempt_step(runtime, catalog) }
188
+ steps.compact
113
189
  end
114
190
  end
115
191
 
116
192
  private
117
193
 
194
+ # A prefix is the first `limit` attempts of another seed's generation,
195
+ # re-run for real: its own nested prefix first (capped the same way it
196
+ # was capped when that seed was generated), then that seed's own
197
+ # `Random.new(seed)` and favor for the rest. Same inputs, same
198
+ # catalog, same draws — the same steps, and the same known ids and
199
+ # exercised verbs carried forward into this seed.
200
+ #
201
+ # The prefix is on top of this seed's own budget, not out of it. A
202
+ # spliced seed still makes all `steps` attempts of its own after the
203
+ # prefix; a prefix is capped at `steps` attempts, so a spliced
204
+ # sequence is at most twice as long as an unspliced one. Taking the
205
+ # prefix out of the budget (the first version of this) left a spliced
206
+ # seed replaying state already seen with almost nothing left to
207
+ # explore from it — measured: fewer distinct tuples than unguided.
208
+ def realize_prefix(runtime, catalog, spec, limit, steps)
209
+ return 0 unless limit.positive?
210
+
211
+ inner = spec["prefix"]
212
+ used = inner ? realize_prefix(runtime, catalog, inner, [prefix_limit(inner), limit].min, steps) : 0
213
+ @random = Random.new(Integer(spec.fetch("seed")))
214
+ @favor = Array(spec["favor"])
215
+ (limit - used).times { steps << attempt_step(runtime, catalog) }
216
+ limit
217
+ end
218
+
219
+ def prefix_limit(spec) = Integer(spec.fetch("steps")).clamp(0, @step_count)
220
+
118
221
  def attempt_step(runtime, catalog)
222
+ index = @attempt
223
+ @attempt += 1
119
224
  entry = pick(catalog)
120
225
  return nil unless entry
121
226
 
122
227
  @exercised << entry[:verb]
123
- if entry[:query] then build_query_step(runtime, entry)
124
- elsif entry[:model] then build_read_model_step(runtime, entry)
125
- else build_command_step(runtime, catalog, entry)
126
- end
228
+ @state_before = "-"
229
+ step =
230
+ if entry[:query] then build_query_step(runtime, entry)
231
+ elsif entry[:model] then build_read_model_step(runtime, entry)
232
+ else build_command_step(runtime, catalog, entry)
233
+ end
234
+ @coverage << [index, coverage_tuple(entry, step)]
235
+ step
236
+ end
237
+
238
+ # `verb | kind | state before | mutation | outcome` — see
239
+ # CoverageCampaign's header for why the unit is this and not the
240
+ # verb. `state` is the addressed aggregate's lifecycle value (or
241
+ # `exists`/`absent` for one without a lifecycle) read just before
242
+ # dispatch; `mutation` names every adversarial mutation and its shape;
243
+ # `outcome` is `ok` or the refusal class `safe_call` rescued.
244
+ def coverage_tuple(entry, step)
245
+ kind = %w[verb query dry_run].find { |key| step.key?(key) }
246
+ mutation = Array(step["adversarial"]).map { |m| [m["mutation"], m["shape"]].compact.join(":") }.join("+")
247
+ [entry[:verb], kind, @state_before, mutation.empty? ? "-" : mutation, @last_outcome].join(" | ")
127
248
  end
128
249
  end
129
250
  end
@@ -0,0 +1,197 @@
1
+ module Hecks
2
+ module Fuzzing
3
+ # A failing step list, made small enough to read.
4
+ #
5
+ # `bin/fuzz` always shrank its findings; `bin/qa_sweep` — the loop that
6
+ # actually finds the bugs — never did, so the ledger filled with
7
+ # demonstrations like "seed 25, 25 steps, step 16" and a human cut
8
+ # BUG#40 down to its four real steps by hand. This is `bin/fuzz`'s
9
+ # shrinker lifted out of that script so both callers share it, and
10
+ # made cheaper at the same time: a sweep's candidate check can cost a
11
+ # Rust subprocess (or a real Postgres round trip), where `bin/fuzz`'s
12
+ # only ever cost an in-process replay.
13
+ #
14
+ # The caller owns "same finding". `call` is handed a block that
15
+ # answers true when a candidate step list still reproduces what the
16
+ # original did — `bin/fuzz` compares its own verdict signature,
17
+ # `bin/qa_sweep` compares `Shrinker.signature` of one mode's
18
+ # divergences. This module never replays anything itself, so it has
19
+ # no idea which engine, adapter or comparison it is minimizing for.
20
+ #
21
+ # **Two passes, in order**:
22
+ #
23
+ # 1. Steps, chunks first. Removing one step at a time (what
24
+ # `bin/fuzz` did) costs O(n²) candidate checks on a sequence
25
+ # where most steps are irrelevant. Delta-debugging style: try
26
+ # removing halves, then quarters, … then single steps, keeping
27
+ # any removal that still reproduces; a single-step pass repeats
28
+ # until it changes nothing, so the result is 1-minimal (no one
29
+ # remaining step can be dropped).
30
+ # 2. Arguments, inside each surviving step. Unchanged from `bin/fuzz`
31
+ # (spec/bin_fuzz_spec.rb pins its accumulation contract): drop
32
+ # one key at a time from the step's current args, keep it dropped
33
+ # only while the finding still reproduces.
34
+ #
35
+ # A budget, because a sweep has other targets waiting. `budget:` caps
36
+ # how many candidate checks one call may spend (nil = unbounded, the
37
+ # `bin/fuzz` behaviour). When it runs out the best candidate found so
38
+ # far is returned — every accepted candidate reproduced, so a partial
39
+ # shrink is still a correct, just less small, demonstration.
40
+ module Shrinker
41
+ Result = Struct.new(:steps, :attempts, :exhausted, keyword_init: true)
42
+
43
+ module_function
44
+
45
+ def call(steps, budget: nil, &reproduces)
46
+ raise ArgumentError, "Shrinker.call needs a block answering whether a candidate reproduces" unless reproduces
47
+
48
+ meter = Meter.new(budget)
49
+ current = drop_steps(steps.dup, meter, &reproduces)
50
+ current = drop_arguments(current, meter, &reproduces)
51
+ Result.new(steps: current, attempts: meter.used, exhausted: meter.exhausted?)
52
+ end
53
+
54
+ def drop_steps(steps, meter, &reproduces)
55
+ current = steps
56
+ chunk = [current.length / 2, 1].max
57
+ loop do
58
+ changed = false
59
+ index = 0
60
+ while index < current.length
61
+ return current if meter.exhausted?
62
+
63
+ candidate = current[0...index] + (current[(index + chunk)..] || [])
64
+ if candidate.empty?
65
+ index += chunk
66
+ next
67
+ end
68
+
69
+ if meter.try { reproduces.call(candidate) }
70
+ current = candidate
71
+ changed = true
72
+ else
73
+ index += chunk
74
+ end
75
+ end
76
+
77
+ if chunk > 1
78
+ chunk = [chunk / 2, 1].max
79
+ elsif !changed
80
+ break
81
+ end
82
+ end
83
+ current
84
+ end
85
+
86
+ # See the module header's pass 2 — `args` is read by whichever
87
+ # spelling the step actually carries (`key?` first, never `||`,
88
+ # which cannot tell a stored `false` from an absent key).
89
+ def drop_arguments(steps, meter, &reproduces)
90
+ steps.each_index do |position|
91
+ original = args_of(steps[position])
92
+ next unless original.is_a?(Hash)
93
+
94
+ original.each_key do |key|
95
+ return steps if meter.exhausted?
96
+
97
+ step = steps[position]
98
+ trimmed = args_of(step).reject { |name, _| name == key }
99
+ candidate = steps.map(&:dup)
100
+ candidate[position] = step.merge("args" => trimmed)
101
+ steps = candidate if meter.try { reproduces.call(candidate) }
102
+ end
103
+ end
104
+ steps
105
+ end
106
+
107
+ def args_of(step)
108
+ step.key?("args") ? step["args"] : step[:args]
109
+ end
110
+
111
+ # Which finding this is, as a set of stable strings — the identity a
112
+ # shrink candidate has to keep. The `field` alone is too loose for
113
+ # the comparisons that carry lists: "refusals differ" on a 3-step
114
+ # candidate could be a different refusal split than the one the
115
+ # original seed found, and a shrinker that accepted it would hand
116
+ # back a demonstration of the wrong bug. So:
117
+ #
118
+ # refusals/queries/dry_runs/reactions — the field plus every verb
119
+ # (or query) named in the symmetric difference of the two sides;
120
+ # instances — the field plus the aggregate of every top-level key
121
+ # whose two sides disagree (the `#id` suffix dropped);
122
+ # a crash/process finding — the field plus the exception class
123
+ # leading its detail;
124
+ # anything else (a property name, a self-consistency axis) — the
125
+ # field, which already names the finding.
126
+ #
127
+ # `reproduces?` holds when a candidate's signature contains the
128
+ # original's: removing steps may add a second divergence, but it
129
+ # must never lose the one being demonstrated.
130
+ LIST_FIELDS = %w[refusals queries dry_runs reactions].freeze
131
+
132
+ CRASH_FIELDS = %w[crash process generator_crash].freeze
133
+
134
+ def signature(divergences)
135
+ divergences.each_with_object(Set.new) do |divergence, keys|
136
+ field = divergence[:field].to_s
137
+ keys << field
138
+ keys.merge(detail_keys(field, divergence))
139
+ end
140
+ end
141
+
142
+ def detail_keys(field, divergence)
143
+ left, right = divergence.except(:field, :detail).values.select { |v| v.is_a?(Array) || v.is_a?(Hash) }
144
+ if LIST_FIELDS.include?(field) then list_keys(field, left, right)
145
+ elsif field == "instances" then instance_keys(left, right)
146
+ elsif CRASH_FIELDS.include?(field) && divergence[:detail]
147
+ ["#{field}:#{divergence[:detail].to_s[/\A\w+(?:::\w+)*/]}"]
148
+ else []
149
+ end
150
+ end
151
+
152
+ def list_keys(field, left, right)
153
+ return [] unless left.is_a?(Array) && right.is_a?(Array)
154
+
155
+ ((left - right) + (right - left)).map { |row| "#{field}:#{named(row)}" }
156
+ end
157
+
158
+ # Instance keys are `Aggregate#id` on the wire — the id is whatever
159
+ # the generator minted, so it names the record, not the finding;
160
+ # keeping it would pin every creating step.
161
+ def instance_keys(left, right)
162
+ return [] unless left.is_a?(Hash) && right.is_a?(Hash)
163
+
164
+ (left.keys | right.keys).reject { |key| left[key] == right[key] }
165
+ .map { |key| "instances:#{key.to_s.split('#').first}" }
166
+ end
167
+
168
+ def reproduces?(original_signature, divergences)
169
+ !divergences.empty? && original_signature.subset?(signature(divergences))
170
+ end
171
+
172
+ def named(row)
173
+ return row.to_s unless row.is_a?(Hash)
174
+
175
+ key = [%w[verb query policy], %i[verb query policy]].flatten.find { |name| row.key?(name) }
176
+ (key ? row[key] : row).to_s
177
+ end
178
+
179
+ # Counts candidate checks against the budget.
180
+ class Meter
181
+ attr_reader :used
182
+
183
+ def initialize(budget)
184
+ @budget = budget
185
+ @used = 0
186
+ end
187
+
188
+ def exhausted? = !@budget.nil? && @used >= @budget
189
+
190
+ def try
191
+ @used += 1
192
+ yield
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,39 @@
1
+ module Hecks
2
+ module Fuzzing
3
+ # What each query verb the differential comparison skipped was hiding.
4
+ #
5
+ # `Differential.manifest_partition` drops a named query or read model
6
+ # from both sides of the Ruby/Rust comparison only when the binary's own
7
+ # manifest.json declares it `generated: false`. The drop is correct, but
8
+ # a sweep that dropped every `offset`/`cursor` ask would still read as
9
+ # "agreed across all steps". `bin/qa_sweep` therefore logs one Check per
10
+ # sweep naming every skipped verb and the construct family that caused
11
+ # it, and marks it Surprised when a family is outside
12
+ # `QualityControlDials::STRUCTURAL_REFUSAL_BOUNDARY`.
13
+ #
14
+ # The construct comes straight from the manifest entry, written by the
15
+ # generator branch that made the skip (`Projector::SkipReason`). This
16
+ # module used to guess it from the Ruby declaration's `to_h` keys, which
17
+ # was coarser than codegen's own decision and could drift from it.
18
+ module StructuralSkips
19
+ module_function
20
+
21
+ # One entry per skipped verb: `{ verb:, constructs: [...] }`, sorted by
22
+ # verb so the printed observation is stable across seeds and runs. A
23
+ # verb the manifest doesn't declare answers `unknown`, which no
24
+ # boundary admits.
25
+ def attribute(gaps, verbs)
26
+ verbs.sort.map do |verb|
27
+ entry = gaps.not_generated(verb)
28
+ { verb: verb, constructs: entry ? [entry.fetch("construct")] : %w[unknown] }
29
+ end
30
+ end
31
+
32
+ # Every skipped verb whose constructs are not all inside `boundary`.
33
+ def outside_boundary(attributed, boundary)
34
+ admitted = boundary.map(&:to_s)
35
+ attributed.select { |entry| entry[:constructs].empty? || (entry[:constructs] - admitted).any? }
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,53 @@
1
+ module Hecks
2
+ module Fuzzing
3
+ # How hard one sweep fuzzes, as a function of `Target.clean_streak`
4
+ # (qa/bluebook/quality_control.bluebook — that attribute's own comment
5
+ # says what the streak means). The sibling of `RotationPriority`: the
6
+ # ledger holds the number, `QualityControlDials::WIDENING_TIERS` holds
7
+ # the policy, and this module is where the two meet — a pure function
8
+ # of both, nothing else read.
9
+ #
10
+ # Why this is not in `bin/qa_sweep` any more. It was — `WIDENING_TIERS`
11
+ # and `widen_for_streak` lived at the top of that script, which meant
12
+ # policy data lived in a script (unlike every other dial, which lives
13
+ # in the bluebook a human edits and reviews) and duplicated itself as
14
+ # prose in SKILL.md. Now the table is a dial, this is the one reader,
15
+ # and `bin/qa_sweep` calls it the way it already calls
16
+ # `RotationPriority.pick`.
17
+ #
18
+ # **Pure, deliberately** — the same discipline `RotationPriority` keeps:
19
+ # same streak in, same `[seeds, steps]` out, every time, which is what
20
+ # lets a human predict what a given sweep is about to do before it
21
+ # runs one. `tiers:` defaults to the dial but is a plain argument, so
22
+ # a unit spec can pass its own table with no ledger boot at all and a
23
+ # `bin/qa_sweep` run against a ledger that declares no dials (an
24
+ # isolated spec's own fixture) can fall back the same way it already
25
+ # does for `ADVERSARIAL_FRACTION`.
26
+ module SweepDepth
27
+ module_function
28
+
29
+ # The fallback table, for a boot with no `QualityControlDials` at all
30
+ # — the same shape and the same three rows the dial ships with, so a
31
+ # dial-less ledger fuzzes exactly as a dialled one does by default.
32
+ # Never read when the dial exists; `bin/qa_sweep` passes the dial in.
33
+ DEFAULT_TIERS = [
34
+ { upto: 4, seeds: 10, steps: 25 },
35
+ { upto: 19, seeds: 25, steps: 50 },
36
+ { upto: Float::INFINITY, seeds: 50, steps: 100 }
37
+ ].freeze
38
+
39
+ # `[seeds, steps]` for one streak. The first row whose `upto` the
40
+ # streak does not exceed wins — rows are read in order, so the table
41
+ # must be ascending, and the last row's `Float::INFINITY` is what
42
+ # makes it the ceiling rather than a gap.
43
+ def for_streak(streak, tiers: DEFAULT_TIERS)
44
+ raise ArgumentError, "streak must not be negative" if streak.negative?
45
+
46
+ tier = tiers.find { |row| streak <= row[:upto] }
47
+ raise ArgumentError, "no tier covers a streak of #{streak} — the last row must be upto: Float::INFINITY" unless tier
48
+
49
+ [tier[:seeds], tier[:steps]]
50
+ end
51
+ end
52
+ end
53
+ end