hecks 1.3.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 (355) 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 +5 -4
  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/prism.rb +4 -4
  23. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  24. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  25. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  26. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  27. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  28. data/lib/hecks/adapters/driven.rb +4 -4
  29. data/lib/hecks/adapters/driving/github_webhook.rb +18 -18
  30. data/lib/hecks/behaviors/dsl.rb +2 -2
  31. data/lib/hecks/behaviors/expectations.rb +23 -23
  32. data/lib/hecks/behaviors/ir.rb +1 -1
  33. data/lib/hecks/behaviors/rspec.rb +1 -1
  34. data/lib/hecks/behaviors/runner.rb +2 -2
  35. data/lib/hecks/behaviors.rb +1 -1
  36. data/lib/hecks/bluebook/aggregate.rb +13 -13
  37. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
  38. data/lib/hecks/bluebook/assembly/build.rb +1 -1
  39. data/lib/hecks/bluebook/assembly/contract.rb +39 -16
  40. data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
  41. data/lib/hecks/bluebook/assembly/marks.rb +18 -18
  42. data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
  43. data/lib/hecks/bluebook/assembly.rb +9 -9
  44. data/lib/hecks/bluebook/attribute.rb +9 -9
  45. data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
  46. data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
  47. data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
  48. data/lib/hecks/bluebook/behaviour/command.rb +23 -23
  49. data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -7
  50. data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
  51. data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
  52. data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
  53. data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
  54. data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
  55. data/lib/hecks/bluebook/behaviour/query.rb +1 -1
  56. data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
  57. data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
  58. data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
  59. data/lib/hecks/bluebook/capabilities.rb +27 -0
  60. data/lib/hecks/bluebook/chapter.rb +28 -9
  61. data/lib/hecks/bluebook/command.rb +12 -12
  62. data/lib/hecks/bluebook/domain_port.rb +9 -9
  63. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  64. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  65. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
  66. data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
  67. data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
  68. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
  69. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
  70. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  71. data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
  72. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  73. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  74. data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
  75. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  76. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
  77. data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
  78. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  79. data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
  80. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  81. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  82. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
  83. data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
  84. data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
  85. data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
  86. data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
  87. data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
  88. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  89. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  90. data/lib/hecks/bluebook/entity.rb +11 -11
  91. data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
  92. data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
  93. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
  94. data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
  95. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
  96. data/lib/hecks/bluebook/expression/resolver.rb +60 -62
  97. data/lib/hecks/bluebook/hexagon.rb +1 -1
  98. data/lib/hecks/bluebook/lifecycle.rb +1 -1
  99. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
  100. data/lib/hecks/bluebook/meta_validator/judge.rb +101 -87
  101. data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
  102. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
  103. data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
  104. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
  105. data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
  106. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
  107. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
  108. data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
  109. data/lib/hecks/bluebook/meta_validator.rb +70 -70
  110. data/lib/hecks/bluebook/model_check.rb +170 -84
  111. data/lib/hecks/bluebook/pattern_subset.rb +9 -9
  112. data/lib/hecks/bluebook/policy.rb +15 -13
  113. data/lib/hecks/bluebook/process_manager.rb +14 -14
  114. data/lib/hecks/bluebook/project_register.rb +6 -6
  115. data/lib/hecks/bluebook/query.rb +4 -4
  116. data/lib/hecks/bluebook/read_model.rb +14 -14
  117. data/lib/hecks/bluebook/reference.rb +8 -8
  118. data/lib/hecks/bluebook/smoke_test.rb +19 -19
  119. data/lib/hecks/bluebook/synthesizer.rb +12 -12
  120. data/lib/hecks/bluebook/translation.rb +4 -4
  121. data/lib/hecks/bluebook/value_object.rb +6 -6
  122. data/lib/hecks/bluebook.rb +2 -2
  123. data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
  124. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
  125. data/lib/hecks/codemod.rb +36 -35
  126. data/lib/hecks/construct.rb +6 -6
  127. data/lib/hecks/corpus.rb +317 -0
  128. data/lib/hecks/deprecation.rb +95 -0
  129. data/lib/hecks/doc/reference.rb +19 -19
  130. data/lib/hecks/embryonaut_bluebook.rb +11 -11
  131. data/lib/hecks/facade/cli_door.rb +69 -10
  132. data/lib/hecks/facade/cli_runner.rb +105 -24
  133. data/lib/hecks/facade/command_request.rb +23 -0
  134. data/lib/hecks/facade/handle.rb +79 -32
  135. data/lib/hecks/facade/json_door.rb +106 -25
  136. data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
  137. data/lib/hecks/facade/surface/chapter.rb +26 -17
  138. data/lib/hecks/facade/surface.rb +16 -3
  139. data/lib/hecks/facade.rb +15 -4
  140. data/lib/hecks/forms/app.rb +46 -30
  141. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  142. data/lib/hecks/forms/field_renderer.rb +142 -6
  143. data/lib/hecks/forms/field_shape.rb +180 -19
  144. data/lib/hecks/forms/html.rb +51 -7
  145. data/lib/hecks/forms/index_renderer.rb +14 -2
  146. data/lib/hecks/forms/params.rb +120 -23
  147. data/lib/hecks/forms/port_argument.rb +2 -2
  148. data/lib/hecks/forms/query_form_renderer.rb +2 -2
  149. data/lib/hecks/forms/record_renderer.rb +2 -2
  150. data/lib/hecks/forms/record_table.rb +1 -1
  151. data/lib/hecks/forms/value_object_shape.rb +3 -3
  152. data/lib/hecks/forms.rb +24 -4
  153. data/lib/hecks/fqn.rb +1 -1
  154. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  155. data/lib/hecks/framework.rb +48 -17
  156. data/lib/hecks/freezer.rb +11 -11
  157. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
  158. data/lib/hecks/fuzzing/combination_miner.rb +119 -0
  159. data/lib/hecks/fuzzing/concurrent_dispatch.rb +140 -45
  160. data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
  161. data/lib/hecks/fuzzing/differential.rb +158 -0
  162. data/lib/hecks/fuzzing/domain_generator.rb +694 -0
  163. data/lib/hecks/fuzzing/era_boundary.rb +37 -18
  164. data/lib/hecks/fuzzing/form_census.rb +35 -20
  165. data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
  166. data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
  167. data/lib/hecks/fuzzing/isolated_boot.rb +62 -62
  168. data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
  169. data/lib/hecks/fuzzing/persistence_parity.rb +19 -21
  170. data/lib/hecks/fuzzing/properties/corrections.rb +9 -9
  171. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +154 -49
  172. data/lib/hecks/fuzzing/properties/guards.rb +42 -42
  173. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
  174. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +22 -35
  175. data/lib/hecks/fuzzing/properties/outbox.rb +24 -24
  176. data/lib/hecks/fuzzing/properties/querying.rb +17 -17
  177. data/lib/hecks/fuzzing/properties.rb +64 -40
  178. data/lib/hecks/fuzzing/qa_settings.rb +152 -0
  179. data/lib/hecks/fuzzing/replay.rb +114 -114
  180. data/lib/hecks/fuzzing/rotation_priority.rb +10 -10
  181. data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
  182. data/lib/hecks/fuzzing/self_consistency.rb +141 -112
  183. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +35 -35
  184. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  185. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  186. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  187. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +58 -31
  188. data/lib/hecks/fuzzing/sequence_generator.rb +111 -23
  189. data/lib/hecks/fuzzing/shrinker.rb +197 -0
  190. data/lib/hecks/fuzzing/structural_skips.rb +23 -130
  191. data/lib/hecks/fuzzing/sweep_depth.rb +4 -4
  192. data/lib/hecks/fuzzing/target_capabilities.rb +49 -18
  193. data/lib/hecks/fuzzing/value_generator.rb +14 -14
  194. data/lib/hecks/fuzzing.rb +5 -0
  195. data/lib/hecks/grammar/evolve.rb +10 -10
  196. data/lib/hecks/grammar.rb +7 -7
  197. data/lib/hecks/ir.rb +13 -13
  198. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  199. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  200. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  201. data/lib/hecks/language/oidc.json +5 -0
  202. data/lib/hecks/literal.rb +9 -9
  203. data/lib/hecks/naming.rb +25 -25
  204. data/lib/hecks/ports/access_control.rb +58 -2
  205. data/lib/hecks/ports/agent/answers.rb +83 -6
  206. data/lib/hecks/ports/agent.rb +119 -35
  207. data/lib/hecks/ports/authentication.rb +44 -4
  208. data/lib/hecks/ports/authorization.rb +53 -11
  209. data/lib/hecks/ports/clock.rb +42 -23
  210. data/lib/hecks/ports/extraction.rb +16 -0
  211. data/lib/hecks/ports/identity_assignment.rb +24 -2
  212. data/lib/hecks/ports/identity_generation.rb +17 -3
  213. data/lib/hecks/ports/identity_resolution.rb +18 -1
  214. data/lib/hecks/ports/loading.rb +4 -0
  215. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  216. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  217. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  218. data/lib/hecks/ports/persistence/execution.rb +4 -0
  219. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  220. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  221. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  222. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  223. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  224. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  225. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  226. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  227. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  228. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  229. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  230. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  231. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  241. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  242. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  243. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  244. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
  245. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  246. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  247. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  248. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
  249. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
  250. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
  251. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  252. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  253. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  254. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  255. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  256. data/lib/hecks/ports/persistence.rb +36 -1
  257. data/lib/hecks/ports/projection.rb +61 -7
  258. data/lib/hecks/ports/query/in_memory.rb +3 -3
  259. data/lib/hecks/ports/query/ordering.rb +6 -6
  260. data/lib/hecks/ports/query.rb +35 -0
  261. data/lib/hecks/projections/bootstrap_table.rb +112 -0
  262. data/lib/hecks/projections/diagrams.rb +75 -75
  263. data/lib/hecks/projections/glossary/html.rb +3 -3
  264. data/lib/hecks/projections/glossary/markdown.rb +3 -3
  265. data/lib/hecks/projections/glossary/mermaid.rb +1 -1
  266. data/lib/hecks/projections/glossary/sections.rb +1 -1
  267. data/lib/hecks/projections/glossary/sentences.rb +5 -5
  268. data/lib/hecks/projections/glossary.rb +15 -15
  269. data/lib/hecks/projections/ir.rb +1 -1
  270. data/lib/hecks/projections/model/deviations.rb +18 -17
  271. data/lib/hecks/projections/model.rb +25 -21
  272. data/lib/hecks/projections/oidc.rb +7 -7
  273. data/lib/hecks/projections/parser_table.rb +5 -5
  274. data/lib/hecks/projections/reference.rb +3 -3
  275. data/lib/hecks/projections/rust_vocabulary.rb +443 -0
  276. data/lib/hecks/projections/shape.rb +2 -2
  277. data/lib/hecks/projections/statements.rb +11 -11
  278. data/lib/hecks/projections/vocabulary.rb +9 -9
  279. data/lib/hecks/projections.rb +5 -3
  280. data/lib/hecks/projector/cli_projector.rb +29 -29
  281. data/lib/hecks/projector/docs_projector.rb +13 -13
  282. data/lib/hecks/projector/exporter.rb +42 -21
  283. data/lib/hecks/projector/ir_projector.rb +1 -1
  284. data/lib/hecks/projector/narrate_projector.rb +11 -11
  285. data/lib/hecks/projector/target.rb +13 -13
  286. data/lib/hecks/projector.rb +15 -15
  287. data/lib/hecks/query_ir.rb +47 -47
  288. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  289. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  290. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  291. data/lib/hecks/query_specification/common/null_policy.rb +57 -13
  292. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  293. data/lib/hecks/query_specification/common/options.rb +25 -0
  294. data/lib/hecks/query_specification/field_path.rb +69 -15
  295. data/lib/hecks/query_specification/hop_path.rb +57 -20
  296. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  297. data/lib/hecks/rendering.rb +3 -3
  298. data/lib/hecks/router/namespace_installer.rb +3 -3
  299. data/lib/hecks/router.rb +1 -1
  300. data/lib/hecks/runtime/aggregate_lock.rb +11 -11
  301. data/lib/hecks/runtime/boot_gates.rb +3 -3
  302. data/lib/hecks/runtime/caller.rb +8 -8
  303. data/lib/hecks/runtime/capability_graph.rb +2 -2
  304. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
  305. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +42 -63
  306. data/lib/hecks/runtime/command_interpreter.rb +159 -102
  307. data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
  308. data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
  309. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  310. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  311. data/lib/hecks/runtime/command_rules/references.rb +35 -35
  312. data/lib/hecks/runtime/command_rules.rb +3 -0
  313. data/lib/hecks/runtime/dependency_planning.rb +11 -11
  314. data/lib/hecks/runtime/dispatcher.rb +283 -90
  315. data/lib/hecks/runtime/entity_element.rb +151 -67
  316. data/lib/hecks/runtime/entity_interpreter.rb +108 -82
  317. data/lib/hecks/runtime/errors.rb +19 -19
  318. data/lib/hecks/runtime/event.rb +6 -6
  319. data/lib/hecks/runtime/identity.rb +22 -22
  320. data/lib/hecks/runtime/instance.rb +39 -14
  321. data/lib/hecks/runtime/interpreting.rb +12 -12
  322. data/lib/hecks/runtime/invocation.rb +276 -0
  323. data/lib/hecks/runtime/loader.rb +14 -14
  324. data/lib/hecks/runtime/outbox.rb +23 -23
  325. data/lib/hecks/runtime/policy_interpreter.rb +54 -54
  326. data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
  327. data/lib/hecks/runtime/query_interpreter.rb +55 -64
  328. data/lib/hecks/runtime/reaction_invocation.rb +20 -20
  329. data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
  330. data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
  331. data/lib/hecks/runtime/reference_hop.rb +6 -6
  332. data/lib/hecks/runtime/refusal_wording.rb +92 -115
  333. data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
  334. data/lib/hecks/runtime/registry/verification.rb +36 -26
  335. data/lib/hecks/runtime/registry.rb +45 -28
  336. data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
  337. data/lib/hecks/runtime/routing.rb +10 -153
  338. data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
  339. data/lib/hecks/runtime/saga_interpreter.rb +64 -64
  340. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  341. data/lib/hecks/runtime/tenant_check.rb +9 -9
  342. data/lib/hecks/runtime/tenant_scope.rb +5 -5
  343. data/lib/hecks/runtime/value/admission.rb +75 -30
  344. data/lib/hecks/runtime/value/coercion.rb +318 -127
  345. data/lib/hecks/runtime/value/entity_list_coercion.rb +34 -34
  346. data/lib/hecks/runtime/value.rb +21 -21
  347. data/lib/hecks/runtime.rb +7 -7
  348. data/lib/hecks/storehouse.rb +64 -64
  349. data/lib/hecks/version.rb +3 -3
  350. data/lib/hecks/vocabulary.rb +205 -4
  351. data/lib/hecks.rb +13 -11
  352. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
  353. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
  354. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
  355. metadata +22 -2
@@ -3,56 +3,57 @@ require "tmpdir"
3
3
  require "tempfile"
4
4
  require "json"
5
5
  require_relative "isolated_boot"
6
+ require_relative "../naming"
6
7
 
7
8
  module Hecks
8
9
  module Fuzzing
9
- # A GENERATED SEQUENCE, RACED FOR REAL — `spec/adapters/driven/
10
+ # **A generated sequence, raced for real** — `spec/adapters/driven/
10
11
  # postgres_era_concurrent_dispatch_spec.rb` proves ADR 0036's own fix
11
12
  # (a real `pg_advisory_xact_lock` serializes a PostgresEra-bound
12
- # dispatch across separate OS processes) against ONE hand-authored
13
- # fixture and ONE hand-picked conflicting pair (two `$6,000` Debits
14
- # against a `$10,000` account). This module asks the SAME question —
13
+ # dispatch across separate OS processes) against one hand-authored
14
+ # fixture and one hand-picked conflicting pair (two `$6,000` Debits
15
+ # against a `$10,000` account). This module asks the same question —
15
16
  # does the cross-process write lock actually serialize concurrent
16
- # writers? — of an ARBITRARY generated sequence against a REAL target
17
+ # writers? — of an arbitrary generated sequence against a real target
17
18
  # domain, so the practice's own adversarial generator gets to pick the
18
19
  # conflict instead of a human picking it once and never again.
19
20
  #
20
- # THE MECHANISM, GENERALIZED FROM THAT SPEC RATHER THAN REDERIVED: a
21
+ # The mechanism, generalized from that spec rather than REDERIVED: a
21
22
  # command step partway through a generated sequence is chosen as the
22
- # RACE STEP; every step before it is SETUP (replayed once, sequentially,
23
+ # race step; every step before it is setup (replayed once, sequentially,
23
24
  # to bring a fresh disposable schema to the state the race step expects
24
- # to act against); the race step is then dispatched TWICE — once from
25
+ # to act against); the race step is then dispatched twice — once from
25
26
  # each of two real, separate, forked OS processes racing against that
26
- # SAME schema, no artificial gating, whichever the scheduler favors.
27
+ # same schema, no artificial gating, whichever the scheduler favors.
27
28
  #
28
- # THE ORACLE IS THE SAME PAIR, DISPATCHED SEQUENTIALLY, NOT A
29
- # HARDCODED EXPECTATION — unlike the hand-authored spec (which can
29
+ # The oracle is the same pair, dispatched sequentially, not a
30
+ # hardcoded expectation — unlike the hand-authored spec (which can
30
31
  # assert `%w[refused succeeded]` because it knows its own fixture's
31
32
  # business rule), this module has no idea whether an arbitrary
32
33
  # generated race step conflicts with itself at all. So it asks the
33
- # DOMAIN: replay the identical setup on a SEPARATE fresh schema, then
34
- # dispatch the SAME race step twice, one after the other, in one
34
+ # domain: replay the identical setup on a separate fresh schema, then
35
+ # dispatch the same race step twice, one after the other, in one
35
36
  # process, with no contention at all — genuinely correct by
36
37
  # construction, since nothing else can touch that schema while it
37
38
  # runs. Two conflicting writes settle as {"succeeded", "refused"};
38
39
  # two independent ones settle as {"succeeded", "succeeded"}; either
39
- # way, THAT is the multiset the concurrent pair must also produce if
40
+ # way, that is the multiset the concurrent pair must also produce if
40
41
  # the write lock actually serializes them — order does not matter
41
- # (which real racer wins a genuine race is never controlled), the SET
42
+ # (which real racer wins a genuine race is never controlled), the set
42
43
  # of outcomes does.
43
44
  #
44
- # WHAT A BROKEN LOCK LOOKS LIKE HERE: the concurrent pair settling as
45
+ # What a broken lock looks like here: the concurrent pair settling as
45
46
  # {"succeeded", "succeeded"} where the sequential oracle says
46
47
  # {"succeeded", "refused"} — two processes each hydrated the
47
48
  # pre-write state, neither saw the other's write, and the second
48
49
  # commit landed as a silent lost update instead of failing its own
49
- # `given`. That is the EXACT corruption class ADR 0036 fixed for
50
+ # `given`. That is the exact corruption class ADR 0036 fixed for
50
51
  # PostgresEra and `postgres_concurrent_dispatch_spec.rb` still
51
52
  # documents, unfixed, for plain Postgres.
52
53
  #
53
- # NOT A REPLACEMENT for the hand-authored spec — that spec proves the
54
+ # Not a replacement for the hand-authored spec — that spec proves the
54
55
  # mechanism once, precisely, with controlled gating so the assertion
55
- # is deterministic; this module proves the SAME mechanism holds for
56
+ # is deterministic; this module proves the same mechanism holds for
56
57
  # whatever a real domain's own generated sequences throw at it, with
57
58
  # no gating (a genuine, ungated race), on every sweep this mode runs.
58
59
  module ConcurrentDispatch
@@ -60,8 +61,8 @@ module Hecks
60
61
 
61
62
  COMMAND_STEP = ->(step) { step["verb"] && !step["query"] && !step["dry_run"] }
62
63
 
63
- # ONE DIVERGENCE LIST, THE SAME SHAPE EVERY OTHER MODE IN THIS
64
- # PRACTICE PRODUCES — `[]` when nothing was found (including the
64
+ # One divergence list, the same shape every other mode in this
65
+ # practice produces — `[]` when nothing was found (including the
65
66
  # legitimate "this seed's generated sequence has no command step to
66
67
  # race at all" case: a sequence of pure queries/dry-runs has nothing
67
68
  # to concurrently dispatch, and that is not a finding).
@@ -70,14 +71,26 @@ module Hecks
70
71
  # (`bin/qa_sweep`'s own `persistence_parity_database`, reused here
71
72
  # for the identical reason: a container, not the thing that's
72
73
  # unique per run). `race_schema:`/`reference_schema:` are two
73
- # DIFFERENT disposable schema names this ONE call owns for its own
74
+ # different disposable schema names this one call owns for its own
74
75
  # duration — the caller creates neither ahead of time (both are
75
76
  # wiped fresh by the boots below) and drops both afterward, the
76
77
  # same lifecycle `persistence_parity_schema` already has.
77
78
  def check(domain_path, steps, database:, race_schema:, reference_schema:)
78
79
  normalized = steps.map { |step| step.transform_keys(&:to_s) }
79
- race_index = pick_race_index(normalized)
80
- return [] unless race_index
80
+ lockable, probe_errors = lockable_verbs(domain_path, normalized, database: database, schema: reference_schema)
81
+ race_index = pick_race_index(normalized, lockable)
82
+ unless race_index
83
+ # `[]` here means "nothing to race", and must only ever mean that.
84
+ # A sequence of pure queries/dry-runs is a legitimate clean
85
+ # result; a probe that raised for every verb is not — that one
86
+ # used to produce the identical `[]` and be logged as a clean
87
+ # concurrency Check (see `lockable_verbs`).
88
+ return [] if probe_errors.empty?
89
+
90
+ return [{ field: "concurrency_unraceable",
91
+ detail: "no command step could be raced because the cross-process-lock probe failed: " \
92
+ "#{probe_errors.uniq.join('; ')}" }]
93
+ end
81
94
 
82
95
  setup_steps = normalized[0...race_index]
83
96
  race_step = normalized[race_index]
@@ -90,7 +103,7 @@ module Hecks
90
103
  [{ field: "process", detail: "#{e.class}: #{e.message}" }]
91
104
  end
92
105
 
93
- # THE COMMAND STEP CLOSEST TO THE MIDDLE OF THE SEQUENCE — not the
106
+ # The command step CLOSEST to the middle of the sequence — not the
94
107
  # first (racing a bare identity-creation with no setup at all is a
95
108
  # legitimate, useful case, so index 0 is not excluded) and not
96
109
  # chosen for any domain-specific reason: a mid-sequence step has, on
@@ -98,13 +111,95 @@ module Hecks
98
111
  # actually conflicting with itself. `nil` when the generated
99
112
  # sequence has no command step at all (every step a query or a dry
100
113
  # run) — nothing here for this seed to race.
101
- def pick_race_index(steps)
114
+ #
115
+ # `lockable_verbs` — optional, but `check` always passes one: the
116
+ # set of verbs whose own aggregate is actually bound to an adapter
117
+ # that declares `:cross_process_lock` in this domain (see
118
+ # `lockable_verbs` below for why this can't be assumed just because
119
+ # the domain binds some of its own aggregates to PostgresEra).
120
+ # Racing anything outside that set is not a legitimate race at
121
+ # all — nil when nothing eligible is left, same as the "no command
122
+ # step" case, never a finding of its own.
123
+ def pick_race_index(steps, lockable_verbs = nil)
102
124
  command_indices = steps.each_index.select { |i| COMMAND_STEP.call(steps[i]) }
125
+ command_indices = command_indices.select { |i| lockable_verbs.include?(steps[i]["verb"]) } if lockable_verbs
103
126
  return nil if command_indices.empty?
104
127
 
105
128
  command_indices[command_indices.size / 2]
106
129
  end
107
130
 
131
+ # Which of this sequence's own command verbs are even candidates to
132
+ # race — BUG#142 (SW-quality_control-1789768606's own first real
133
+ # concurrency run, seed 3): a domain that `persisted_by("PostgresEra")`
134
+ # binds its own aggregates is not thereby binding a framework
135
+ # member it merely `uses_framework`s — `hecksagon_builder.rb`'s own
136
+ # `uses_framework` loads only that member's shape, never its
137
+ # persistence (see `examples/banking/bluebook/banking.hecksagon`'s
138
+ # own comment: a framework member's aggregates need a sibling
139
+ # hecksagon, registered under that member's own name, or
140
+ # `Ports::Persistence::BindingPolicy.default_binding` silently
141
+ # gives them "Memory" — no hecksagon registered under that name at
142
+ # all, so `resolve`'s own `missing_binding` refusal (which only
143
+ # fires when a hecksagon exists for that domain and simply omits
144
+ # this aggregate) never gets a chance to say so).
145
+ # `qa/bluebook/quality_control.hecksagon` attaches `Governance` via
146
+ # `uses_framework` with no such sibling — `Governance::
147
+ # RoleAssignment`/`RoleTransition` are Memory-backed, process-
148
+ # local, in the real ledger, `concurrency` mode included. Racing a
149
+ # Memory-backed aggregate across two real OS processes can never
150
+ # agree with the single-process sequential oracle — each racer's
151
+ # own boot gets its own independent, empty store — no matter how
152
+ # correct any write lock is; that is a guaranteed false positive,
153
+ # not evidence of a broken lock.
154
+ #
155
+ # Boots the same PostgresEra-rebound copy the race itself boots
156
+ # (`boot_preserving_schema` — structural inspection only, nothing
157
+ # dispatched, so the schema it's given is left exactly as it found
158
+ # it) and asks each distinct command verb's own resolved repository
159
+ # whether it actually declares `:cross_process_lock` — the same
160
+ # capability `Interpreting#run_dispatch_order_with_isolation`
161
+ # itself keys off of to decide whether a real advisory lock is
162
+ # even in play for that aggregate. A verb this boot can't resolve
163
+ # at all (a malformed adversarial verb, say) is conservatively
164
+ # excluded, not raced on a guess.
165
+ # Answers `[lockable, probe_errors]`. The errors half exists because
166
+ # the probe below rescues to `false`: a verb that cannot be resolved
167
+ # is indistinguishable, from the outside, from one that resolves fine
168
+ # and simply declares no `:cross_process_lock`. If resolution broke
169
+ # for every verb (a renamed capability symbol, a wiring change),
170
+ # `lockable` came back empty, `check` returned `[]`, and the sweep
171
+ # logged a clean concurrency Check for a race that never happened.
172
+ # `check` reports that case now instead of holding it.
173
+ def lockable_verbs(domain_path, steps, database:, schema:)
174
+ verbs = steps.select { |step| COMMAND_STEP.call(step) }.map { |step| step["verb"] }.uniq
175
+ lockable = []
176
+ probe_errors = []
177
+ boot_preserving_schema(domain_path, database: database, schema: schema) do |copy|
178
+ runtime = Hecks.boot(copy)
179
+ verbs.each do |verb|
180
+ lockable << verb if verb_cross_process_lockable?(runtime, verb, probe_errors)
181
+ end
182
+ end
183
+ [lockable, probe_errors]
184
+ end
185
+
186
+ def verb_cross_process_lockable?(runtime, verb, probe_errors = [])
187
+ domain, aggregate_name, = Naming.split_verb(verb)
188
+ return false unless domain && aggregate_name
189
+
190
+ aggregate = runtime.registry.bluebook(domain)&.aggregate(aggregate_name)
191
+ return false unless aggregate
192
+
193
+ repository = runtime.registry.repository(domain, aggregate)
194
+ repository.capabilities.include?(:cross_process_lock)
195
+ rescue StandardError => e
196
+ # Still `false` — a verb this boot cannot resolve is not raced on a
197
+ # guess — but no longer silent: `check` needs to tell "nothing here
198
+ # declares a cross-process lock" from "asking broke".
199
+ probe_errors << "#{verb}: #{e.class}: #{e.message}"
200
+ false
201
+ end
202
+
108
203
  def divergences_for(race_step, reference, concurrent)
109
204
  crashes = (reference + concurrent).select { |outcome| outcome.start_with?("crashed:") }.uniq
110
205
  return crashes.map { |c| { field: "concurrency_crash", verb: race_step["verb"], detail: c } } if crashes.any?
@@ -117,8 +212,8 @@ module Hecks
117
212
  "#{reference.sort} — the cross-process write lock did not correctly serialize this write" }]
118
213
  end
119
214
 
120
- # THE ORACLE — one boot, one process, the setup then the race step
121
- # TWICE in immediate succession. Nothing else ever touches this
215
+ # The oracle — one boot, one process, the setup then the race step
216
+ # twice in immediate succession. Nothing else ever touches this
122
217
  # schema while this runs, so whatever the domain itself settles on
123
218
  # is correct by construction, not asserted.
124
219
  def reference_outcomes(domain_path, setup_steps, race_step, database:, schema:)
@@ -132,32 +227,32 @@ module Hecks
132
227
  outcomes
133
228
  end
134
229
 
135
- # THE RACE ITSELF — setup runs ONCE, sequentially, in THIS process
230
+ # The race itself — setup runs once, sequentially, in this process
136
231
  # (the same `IsolatedBoot.call` wipe-then-boot every other mode
137
- # here already uses), and only THEN do the two real racers run.
138
- # Each racer boots its OWN fresh copy of the domain against the
139
- # SAME now-populated schema — `boot_preserving_schema`, below,
232
+ # here already uses), and only then do the two real racers run.
233
+ # Each racer boots its own fresh copy of the domain against the
234
+ # same now-populated schema — `boot_preserving_schema`, below,
140
235
  # deliberately skips the wipe `IsolatedBoot.call` always does, or
141
236
  # the setup this line just wrote would be gone before either racer
142
237
  # ever dispatched anything.
143
238
  #
144
- # REAL, SEPARATE OS PROCESSES, NOT `Thread.new` — `postgres_era_
239
+ # Real, separate OS processes, not `Thread.new` — `postgres_era_
145
240
  # concurrent_dispatch_spec.rb`'s own header explains why:
146
241
  # `Runtime::AggregateLock`'s in-process registry would fully (and
147
- # misleadingly) serialize two THREADS sharing one process even with
242
+ # misleadingly) serialize two threads sharing one process even with
148
243
  # the cross-process lock fix reverted. Only two genuinely separate
149
244
  # OS processes exercise the gap this check exists to catch.
150
245
  #
151
- # `Process.spawn`, NOT `Process.fork` — `bin/qa_sweep`'s own
246
+ # `Process.spawn`, not `Process.fork` — `bin/qa_sweep`'s own
152
247
  # top-of-file comment on `--all` names the identical hazard this
153
- # sidesteps: by the time a `concurrency` seed runs, THIS process
248
+ # sidesteps: by the time a `concurrency` seed runs, this process
154
249
  # already holds the QualityControl ledger's own live PostgresEra
155
250
  # connection (this module's own caller, `bin/qa_sweep`, booted it
156
251
  # long before any seed ran). `Process.fork` duplicates every open
157
252
  # file descriptor, SSL session state included — confirmed live
158
253
  # while wiring this mode up: forking directly from here corrupted
159
- # the LEDGER's own connection the moment either racer child exited,
160
- # surfacing on the NEXT unrelated ledger write, nowhere near this
254
+ # the ledger's own connection the moment either racer child exited,
255
+ # surfacing on the next unrelated ledger write, nowhere near this
161
256
  # method's own code. `bin/qa_concurrency_racer` is this method's own
162
257
  # worker, one real `ruby` process per racer — read that script's own
163
258
  # header for the rest of this reasoning.
@@ -186,14 +281,14 @@ module Hecks
186
281
  end
187
282
  end
188
283
 
189
- # ONE STEP, ONE OUTCOME — never raises: a declared domain refusal is
284
+ # One step, one outcome — never raises: a declared domain refusal is
190
285
  # "refused" (the expected, ordinary answer a `given`/invariant can
191
286
  # give), anything else escaping is "crashed:<class>: <message>", a
192
287
  # genuine finding this module's own caller surfaces rather than lets
193
288
  # kill a forked racer silently.
194
289
  def dispatch_one(runtime, step)
195
290
  args = (step["args"] || {}).transform_keys(&:to_sym)
196
- runtime.dispatch(step["verb"], **args)
291
+ runtime.dispatch_flat(step["verb"], args)
197
292
  "succeeded"
198
293
  rescue *Hecks::Runtime::DOMAIN_REFUSALS, Hecks::Bluebook::Expression::EvaluationError
199
294
  "refused"
@@ -201,7 +296,7 @@ module Hecks
201
296
  "crashed:#{e.class}: #{e.message}"
202
297
  end
203
298
 
204
- # SETUP TOLERATES AN ORDINARY REFUSAL (a generated sequence's own
299
+ # Setup tolerates an ordinary refusal (a generated sequence's own
205
300
  # earlier step can legitimately refuse — every other mode in this
206
301
  # practice already replays a prefix that way) but never a crash: an
207
302
  # unexpected exception during setup means the schema this race is
@@ -214,13 +309,13 @@ module Hecks
214
309
  end
215
310
  end
216
311
 
217
- # THE SAME COPY-AND-REBIND `IsolatedBoot.call(..., adapter:
218
- # :postgres_era, ...)` DOES, MINUS THE SCHEMA WIPE — deliberately
219
- # NOT reusing `IsolatedBoot.rebind_to_postgres_era!` itself, which
312
+ # The same copy-and-rebind `IsolatedBoot.call(..., adapter:
313
+ # :postgres_era, ...)` does, minus the schema wipe — deliberately
314
+ # not reusing `IsolatedBoot.rebind_to_postgres_era!` itself, which
220
315
  # bundles `ensure_postgres_era_schema!`'s own `DROP SCHEMA` into the
221
316
  # same call with no way to opt out (see that method's own header:
222
317
  # the wipe is the "zero-history guarantee every other adapter mode
223
- # already gives," exactly the guarantee THIS caller must NOT have —
318
+ # already gives," exactly the guarantee this caller must not have —
224
319
  # the whole point of a race is booting against what setup already
225
320
  # wrote). `copy_dereferencing`/`rewrite_bindings!` are the same two
226
321
  # public steps that method itself calls first; only the `.world`
@@ -0,0 +1,118 @@
1
+ module Hecks
2
+ module Fuzzing
3
+ # What one sweep has already reached, and where the next seed starts.
4
+ #
5
+ # `SequenceGenerator` steers within one sequence (an unexercised verb is
6
+ # weighted up — picker.rb's `steer`), but every seed of a sweep used to
7
+ # start from nothing and forget everything the seeds before it reached.
8
+ # Roughly half of the QA ledger's bugs are refusal-kind splits (BUG#2–4,
9
+ # 7, 8, 14–16, 19–21, 23, 27, 28, 36–38, 41, 54, 56): a particular verb,
10
+ # in a particular state, with a particular malformed argument, refused
11
+ # one way on Ruby and another on Rust. BUG#11 was found by hand because
12
+ # a 25-step random walk never reached `Annotate` at all. Uniform seeds
13
+ # reach those corners by luck; this makes the sweep remember.
14
+ #
15
+ # The coverage unit is a tuple, not a verb — `verb | step kind |
16
+ # lifecycle state before | adversarial mutation | outcome` (see
17
+ # `SequenceGenerator#coverage_tuple`). "Renew refused LifecycleRefused
18
+ # from free" and "Renew ok from held" are different places a runtime can
19
+ # be wrong, where a verb-level count calls them the same.
20
+ #
21
+ # Two levers, both expressed as plain generator arguments so a seed
22
+ # stays reproducible from one call:
23
+ #
24
+ # 1. Splicing (`prefix:`). A sequence that reached a new tuple joins
25
+ # the corpus, cut at its last new tuple. A later seed may start by
26
+ # re-generating a random-length prefix of a corpus entry — the same
27
+ # seed, favor and nested prefix, so the same steps, dispatched for
28
+ # real, reaching the same state — and then carries on with its own
29
+ # seed's randomness from there. Deep state gets reached once and
30
+ # explored many times, instead of re-rolled per seed.
31
+ # 2. Favor (`favor:`). The verbs this sweep has hit least often —
32
+ # including declared verbs it has never hit at all — are weighted
33
+ # up for the next seed.
34
+ #
35
+ # Nothing here is random at the campaign's own level beyond
36
+ # `Random.new(seed)` per plan, and a plan is printed with every finding
37
+ # (`bin/qa_sweep`'s `reproduce:` line), so no finding depends on
38
+ # re-running the whole campaign to get back to it.
39
+ class CoverageCampaign
40
+ Plan = Struct.new(:prefix, :favor, keyword_init: true) do
41
+ def generator_options = { prefix: prefix, favor: favor }
42
+ def spliced? = !prefix.nil?
43
+ end
44
+
45
+ attr_reader :corpus
46
+
47
+ def initialize(splice_probability:, favor_count:, corpus_limit: 64, max_prefix_depth: 6)
48
+ @splice_probability = splice_probability.to_f
49
+ @favor_count = favor_count.to_i
50
+ @corpus_limit = corpus_limit
51
+ @max_prefix_depth = max_prefix_depth
52
+ @corpus = []
53
+ @seen = Set.new
54
+ @verb_hits = Hash.new(0)
55
+ @declared_verbs = Set.new
56
+ @seeds = 0
57
+ @spliced = 0
58
+ @seeds_with_new = 0
59
+ end
60
+
61
+ def plan(seed)
62
+ random = Random.new(seed)
63
+ prefix = nil
64
+ if !@corpus.empty? && random.rand < @splice_probability
65
+ entry = @corpus[random.rand(@corpus.size)]
66
+ prefix = entry[:spec].merge("steps" => random.rand(1..entry[:attempts]))
67
+ end
68
+ Plan.new(prefix: prefix, favor: rare_verbs)
69
+ end
70
+
71
+ # `trace` is `SequenceGenerator.trace`'s answer: `coverage` is
72
+ # `[[attempt_index, tuple], ...]`, `verbs` every verb the booted
73
+ # catalog offered.
74
+ def record(seed, plan, trace)
75
+ @seeds += 1
76
+ @spliced += 1 if plan.spliced?
77
+ @declared_verbs.merge(trace.verbs)
78
+
79
+ new_at = trace.coverage.filter_map do |index, tuple|
80
+ @verb_hits[tuple.split(" | ").first] += 1
81
+ index if @seen.add?(tuple)
82
+ end
83
+ return if new_at.empty?
84
+
85
+ @seeds_with_new += 1
86
+ admit(seed, plan, new_at.max + 1)
87
+ end
88
+
89
+ def tuples_seen = @seen.size
90
+
91
+ def summary
92
+ "coverage: #{@seen.size} distinct (verb, kind, state, mutation, outcome) tuple(s) over #{@seeds} seed(s); " \
93
+ "#{@seeds_with_new} seed(s) reached something new; #{@spliced} spliced from a corpus of #{@corpus.size}"
94
+ end
95
+
96
+ private
97
+
98
+ def admit(seed, plan, attempts)
99
+ return if depth(plan.prefix) + 1 > @max_prefix_depth
100
+
101
+ spec = { "seed" => seed, "favor" => plan.favor }
102
+ spec["prefix"] = plan.prefix if plan.prefix
103
+ @corpus << { spec: spec, attempts: attempts }
104
+ @corpus.shift while @corpus.size > @corpus_limit
105
+ end
106
+
107
+ def depth(prefix) = prefix.nil? ? 0 : 1 + depth(prefix["prefix"])
108
+
109
+ # Never-hit declared verbs first (hits 0), then the least-hit, ties
110
+ # broken by name so a plan is a pure function of what was recorded.
111
+ def rare_verbs
112
+ return [] unless @favor_count.positive?
113
+
114
+ (@declared_verbs | @verb_hits.keys).min_by(@favor_count) { |verb| [@verb_hits[verb], verb] }
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,158 @@
1
+ require "json"
2
+ require "open3"
3
+ require_relative "replay"
4
+ require_relative "properties"
5
+ require_relative "self_consistency"
6
+ require_relative "rust_gap_manifest"
7
+ require_relative "nondeterministic"
8
+
9
+ module Hecks
10
+ module Fuzzing
11
+ # The Ruby-vs-Rust comparison of one generated sequence — moved here,
12
+ # unchanged, out of `bin/qa_sweep`'s own `diff_ruby_vs_rust` so a second
13
+ # caller (`bin/qa_generated_domains --rust`, which compares domains the
14
+ # generator wrote rather than ones on the rotation) runs the same
15
+ # comparison rather than a re-derived one. `bin/qa_sweep` delegates to
16
+ # it; its own comments on the known-gap filtering, the wire-precision
17
+ # reduction and the deep copy taken before `strip_emitted_flags!` apply
18
+ # here word for word.
19
+ #
20
+ # `differ` is anything answering `RustConformanceHelpers`' comparison
21
+ # helpers (spec/support/rust_conformance_helpers.rb) plus a
22
+ # `structural_skips` set — duck-typed, so lib never requires spec/.
23
+ # Which query verbs may diverge is not the differ's call: it is read
24
+ # off the binary's own manifest.json (`manifest_partition`, below).
25
+ #
26
+ # Returns one divergence list per active mode — `{ differential: [...],
27
+ # self_consistency: [...], properties_in_differential: [...],
28
+ # adapter_parity_sqlite: [...] }`, keys present only for the modes in
29
+ # `modes:` (`differential` always). `adapter_parity_sqlite:` is the
30
+ # caller's own callable, because which two adapters that mode pairs is
31
+ # the caller's dial, not this module's.
32
+ module Differential
33
+ module_function
34
+
35
+ # The one place a Ruby/Rust query divergence may be tolerated — and
36
+ # only for a verb `gaps` (a `RustGapManifest`) declares not generated.
37
+ # Rust refuses such a verb outright while Ruby answers it for real
38
+ # (or refuses it for its own business reason), so both sides' rows for
39
+ # that verb leave the comparison. Everything else stays in: a refusal
40
+ # the manifest doesn't account for — however its message is worded —
41
+ # is a real divergence.
42
+ #
43
+ # `skipped` is every tolerated verb this history actually reached (for
44
+ # the sweep's structural_skip_report). `stale` is a divergence per
45
+ # tolerated verb Rust nonetheless answered with a query row: the
46
+ # manifest says "not generated" but the binary disagrees, so the
47
+ # tolerance itself is wrong and must not silently hold.
48
+ def manifest_partition(gaps, ruby_refusals:, rust_refusals:, ruby_queries:, rust_queries:)
49
+ verb_of = ->(row) { row.key?("verb") ? row["verb"] : row["query"] }
50
+ declared = ->(row) { gaps.not_generated?(verb_of.call(row)) }
51
+ stale = rust_queries.select(&declared).map do |row|
52
+ { field: "manifest", verb: row["query"],
53
+ detail: "#{row['query']} is declared generated: false in manifest.json " \
54
+ "(#{gaps.not_generated(row['query']).values_at('gap_class', 'construct').join('/')}), " \
55
+ "but the Rust binary answered it — regenerate with bin/project_rust" }
56
+ end
57
+ reached = (ruby_refusals + rust_refusals + ruby_queries).select(&declared)
58
+ { ruby_refusals: ruby_refusals.reject(&declared), rust_refusals: rust_refusals.reject(&declared),
59
+ ruby_queries: ruby_queries.reject(&declared), rust_queries: rust_queries.reject(&declared),
60
+ skipped: reached.to_set(&verb_of), stale: stale }
61
+ end
62
+
63
+ def property_divergences(history)
64
+ Properties.check(history).reject { |_, result| result == true }
65
+ .map { |name, message| { field: name.to_s, detail: message } }
66
+ end
67
+
68
+ def self_consistency_divergences(history)
69
+ return [] unless history[:self_consistency]
70
+
71
+ history[:self_consistency].values.flatten(1)
72
+ end
73
+
74
+ # rubocop:disable-next Metrics/AbcSize
75
+ # rubocop:disable-next Metrics/CyclomaticComplexity
76
+ # rubocop:disable-next Metrics/PerceivedComplexity
77
+ # rubocop:disable-next Metrics/MethodLength
78
+ def diff(differ, domain_path, steps, binary, modes:, adapter_parity_sqlite: nil)
79
+ self_consistency = modes.include?(:self_consistency)
80
+ ruby_result = Replay.call(domain_path, steps, self_consistency: self_consistency)
81
+ ruby_instances = JSON.parse(JSON.generate(ruby_result[:instances]))
82
+ ruby_events = JSON.parse(JSON.generate(ruby_result[:events]))
83
+ ruby_refusals = ruby_result[:refusals].map do |r|
84
+ { "verb" => r[:verb].to_s, "kind" => r[:kind].to_s.split("::").last, "error" => r[:error] }
85
+ end
86
+ ruby_queries = JSON.parse(JSON.generate(ruby_result[:queries].map { |row| Nondeterministic.strip(row, :query_row) }))
87
+ ruby_sagas = JSON.parse(JSON.generate(ruby_result[:sagas]))
88
+ ruby_dry_runs = ruby_result[:dry_runs].map { |d| { "verb" => d[:verb].to_s, "ok" => d[:ok] } }
89
+
90
+ outcomes = {}
91
+ outcomes[:properties_in_differential] = property_divergences(ruby_result) if modes.include?(:properties_in_differential)
92
+ if modes.include?(:adapter_parity_sqlite) && adapter_parity_sqlite
93
+ outcomes[:adapter_parity_sqlite] = adapter_parity_sqlite.call
94
+ end
95
+
96
+ stdout, status = Open3.capture2(binary, stdin_data: JSON.generate({ "steps" => steps }))
97
+ unless status.success?
98
+ return outcomes.merge(differential: [{ field: "process",
99
+ detail: "rust binary exited #{status.exitstatus}: #{stdout}" }])
100
+ end
101
+
102
+ rust_output = JSON.parse(stdout)
103
+ rust_live_instances = JSON.parse(JSON.generate(rust_output["instances"])) if self_consistency
104
+ differ.strip_emitted_flags!(rust_output["instances"])
105
+ differ.strip_emitted_flags!(rust_output["queries"])
106
+ differ.strip_occurred_at!(rust_output["events"])
107
+
108
+ divergences = []
109
+ divergences << { field: "instances", ruby: ruby_instances, rust: rust_output["instances"] } \
110
+ unless rust_output["instances"] == ruby_instances
111
+ divergences << { field: "events", ruby: ruby_events, rust: rust_output["events"] } \
112
+ unless rust_output["events"] == ruby_events
113
+
114
+ kept = manifest_partition(RustGapManifest.for_binary(binary),
115
+ ruby_refusals: ruby_refusals, rust_refusals: rust_output["refusals"],
116
+ ruby_queries: ruby_queries, rust_queries: rust_output["queries"])
117
+ differ.structural_skips.merge(kept[:skipped])
118
+ divergences.concat(kept[:stale])
119
+
120
+ by_kind = ->(r) { r.slice("verb", "kind") }
121
+ rust_refusals = kept[:rust_refusals].map(&by_kind)
122
+ kept_ruby_refusals = kept[:ruby_refusals].map(&by_kind)
123
+ divergences << { field: "refusals", ruby: kept_ruby_refusals, rust: rust_refusals } \
124
+ unless rust_refusals == kept_ruby_refusals
125
+
126
+ wordless = ->(row) { differ.reduce_to_wire_precision(row.except("error", "reference_error")) }
127
+ rust_queries = kept[:rust_queries].map(&wordless)
128
+ kept_ruby_queries = kept[:ruby_queries].map(&wordless)
129
+ divergences << { field: "queries", ruby: kept_ruby_queries, rust: rust_queries } \
130
+ unless rust_queries == kept_ruby_queries
131
+
132
+ divergences << { field: "sagas", ruby: ruby_sagas, rust: rust_output["sagas"] } \
133
+ unless rust_output["sagas"] == ruby_sagas
134
+
135
+ cross_domain = differ.cross_domain_policy_names(rust_output)
136
+ kept_ruby_reactions = JSON.parse(JSON.generate(ruby_result[:reactions]))
137
+ .reject { |r| cross_domain.include?(r["policy"]) }
138
+ rust_reactions = rust_output.fetch("reactions")
139
+ divergences << { field: "reactions", ruby: kept_ruby_reactions, rust: rust_reactions } \
140
+ unless rust_reactions == kept_ruby_reactions
141
+
142
+ rust_dry_runs = Array(rust_output["dry_runs"]).map { |d| d.slice("verb", "ok") }
143
+ divergences << { field: "dry_runs", ruby: ruby_dry_runs, rust: rust_dry_runs } \
144
+ unless rust_dry_runs == ruby_dry_runs
145
+ outcomes[:differential] = divergences
146
+
147
+ if self_consistency
148
+ outcomes[:self_consistency] =
149
+ self_consistency_divergences(ruby_result) +
150
+ SelfConsistency.check_rust_rehydration(binary, differ, rust_live_instances) +
151
+ SelfConsistency.check_rust_idempotency(binary, differ, rust_live_instances)
152
+ end
153
+
154
+ outcomes
155
+ end
156
+ end
157
+ end
158
+ end