hecks 1.4.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +65 -0
  3. data/lib/hecks/adapters/driven/folder.rb +73 -0
  4. data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
  5. data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
  6. data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
  7. data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
  8. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  9. data/lib/hecks/adapters/driven/heki.rb +93 -2
  10. data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
  12. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  13. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  14. data/lib/hecks/adapters/driven/lambda/client.rb +35 -7
  15. data/lib/hecks/adapters/driven/lambda.rb +43 -2
  16. data/lib/hecks/adapters/driven/local_storage.rb +67 -1
  17. data/lib/hecks/adapters/driven/memory.rb +13 -13
  18. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
  19. data/lib/hecks/adapters/driven/prism.rb +46 -7
  20. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  21. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  22. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  23. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  24. data/lib/hecks/adapters/driven.rb +2 -0
  25. data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
  26. data/lib/hecks/behaviors/dsl.rb +58 -0
  27. data/lib/hecks/behaviors/expectations.rb +168 -7
  28. data/lib/hecks/behaviors/ir.rb +11 -0
  29. data/lib/hecks/behaviors/rspec.rb +8 -0
  30. data/lib/hecks/behaviors/runner.rb +19 -0
  31. data/lib/hecks/behaviors.rb +8 -0
  32. data/lib/hecks/bluebook/aggregate.rb +30 -0
  33. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
  34. data/lib/hecks/bluebook/assembly/build.rb +25 -4
  35. data/lib/hecks/bluebook/assembly/contract.rb +66 -14
  36. data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
  37. data/lib/hecks/bluebook/assembly/marks.rb +143 -14
  38. data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
  39. data/lib/hecks/bluebook/assembly.rb +25 -7
  40. data/lib/hecks/bluebook/attribute.rb +17 -3
  41. data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
  42. data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
  43. data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
  44. data/lib/hecks/bluebook/behaviour/command.rb +33 -3
  45. data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
  46. data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
  47. data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
  48. data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
  49. data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
  50. data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
  51. data/lib/hecks/bluebook/behaviour/query.rb +5 -0
  52. data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
  53. data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
  54. data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
  55. data/lib/hecks/bluebook/chapter.rb +23 -0
  56. data/lib/hecks/bluebook/command.rb +53 -8
  57. data/lib/hecks/bluebook/domain_port.rb +25 -0
  58. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
  59. data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
  60. data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
  61. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
  62. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
  63. data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
  64. data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
  65. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
  66. data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
  67. data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
  68. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
  69. data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
  70. data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
  71. data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
  72. data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
  73. data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
  74. data/lib/hecks/bluebook/entity.rb +29 -0
  75. data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
  76. data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
  77. data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
  78. data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
  79. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
  80. data/lib/hecks/bluebook/expression/resolver.rb +320 -77
  81. data/lib/hecks/bluebook/hexagon.rb +34 -0
  82. data/lib/hecks/bluebook/lifecycle.rb +11 -0
  83. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
  84. data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
  85. data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
  86. data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
  87. data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
  88. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
  89. data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
  90. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
  91. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
  92. data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
  93. data/lib/hecks/bluebook/meta_validator.rb +180 -84
  94. data/lib/hecks/bluebook/model_check.rb +268 -24
  95. data/lib/hecks/bluebook/pattern_subset.rb +23 -1
  96. data/lib/hecks/bluebook/process_manager.rb +13 -0
  97. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  98. data/lib/hecks/bluebook/project_loader.rb +40 -0
  99. data/lib/hecks/bluebook/project_register.rb +44 -0
  100. data/lib/hecks/bluebook/query.rb +27 -0
  101. data/lib/hecks/bluebook/read_model.rb +21 -1
  102. data/lib/hecks/bluebook/reference.rb +21 -8
  103. data/lib/hecks/bluebook/smoke_test.rb +29 -6
  104. data/lib/hecks/bluebook/synthesizer.rb +34 -0
  105. data/lib/hecks/bluebook/translation.rb +30 -1
  106. data/lib/hecks/bluebook/value_object.rb +23 -5
  107. data/lib/hecks/bluebook.rb +3 -4
  108. data/lib/hecks/codemod.rb +107 -20
  109. data/lib/hecks/construct.rb +15 -1
  110. data/lib/hecks/corpus.rb +146 -25
  111. data/lib/hecks/deploy/bluebook/deploy.bluebook +105 -0
  112. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  113. data/lib/hecks/deploy/oidc.json +5 -0
  114. data/lib/hecks/doc/reference.rb +185 -16
  115. data/lib/hecks/embryonaut_bluebook.rb +32 -9
  116. data/lib/hecks/facade/handle.rb +76 -3
  117. data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
  118. data/lib/hecks/forms/field_shape.rb +3 -0
  119. data/lib/hecks/forms/page.rb +14 -0
  120. data/lib/hecks/forms/port_argument.rb +12 -0
  121. data/lib/hecks/forms/query_form_renderer.rb +63 -0
  122. data/lib/hecks/forms/record_renderer.rb +58 -0
  123. data/lib/hecks/forms/record_table.rb +27 -0
  124. data/lib/hecks/forms/reference_options.rb +24 -0
  125. data/lib/hecks/forms/value_object_shape.rb +10 -0
  126. data/lib/hecks/fqn.rb +58 -0
  127. data/lib/hecks/framework/bluebook/compliance.bluebook +221 -0
  128. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  129. data/lib/hecks/framework/oidc.json +15 -0
  130. data/lib/hecks/framework.rb +43 -20
  131. data/lib/hecks/freezer.rb +17 -1
  132. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
  133. data/lib/hecks/fuzzing/combination_miner.rb +59 -0
  134. data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
  135. data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
  136. data/lib/hecks/fuzzing/differential.rb +34 -0
  137. data/lib/hecks/fuzzing/domain_generator.rb +188 -11
  138. data/lib/hecks/fuzzing/era_boundary.rb +45 -15
  139. data/lib/hecks/fuzzing/form_census.rb +86 -0
  140. data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
  141. data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
  142. data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
  143. data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
  144. data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
  145. data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
  146. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
  147. data/lib/hecks/fuzzing/properties/guards.rb +44 -0
  148. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
  149. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
  150. data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
  151. data/lib/hecks/fuzzing/properties/querying.rb +68 -14
  152. data/lib/hecks/fuzzing/properties.rb +24 -15
  153. data/lib/hecks/fuzzing/qa_settings.rb +12 -0
  154. data/lib/hecks/fuzzing/replay.rb +137 -29
  155. data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
  156. data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
  157. data/lib/hecks/fuzzing/self_consistency.rb +189 -40
  158. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
  159. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
  160. data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
  161. data/lib/hecks/fuzzing/shrinker.rb +114 -2
  162. data/lib/hecks/fuzzing/structural_skips.rb +18 -4
  163. data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
  164. data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
  165. data/lib/hecks/fuzzing/value_generator.rb +98 -10
  166. data/lib/hecks/grammar/evolve.rb +178 -2
  167. data/lib/hecks/grammar.rb +46 -0
  168. data/lib/hecks/ir.rb +38 -7
  169. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  170. data/lib/hecks/literal.rb +32 -0
  171. data/lib/hecks/naming.rb +88 -7
  172. data/lib/hecks/ports/access_control.rb +5 -10
  173. data/lib/hecks/ports/authorization.rb +3 -6
  174. data/lib/hecks/ports/identity_assignment.rb +1 -2
  175. data/lib/hecks/ports/identity_resolution.rb +1 -2
  176. data/lib/hecks/ports/key_vault.port +6 -0
  177. data/lib/hecks/ports/key_vault.rb +85 -0
  178. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
  179. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
  180. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
  181. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
  182. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
  183. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  184. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
  185. data/lib/hecks/ports/query/in_memory.rb +39 -6
  186. data/lib/hecks/ports/query/ordering.rb +15 -0
  187. data/lib/hecks/ports.rb +1 -0
  188. data/lib/hecks/projections/bootstrap_table.rb +43 -8
  189. data/lib/hecks/projections/deploy/fargate.rb +666 -0
  190. data/lib/hecks/projections/deploy/lambda.rb +2423 -0
  191. data/lib/hecks/projections/deploy/shared.rb +624 -0
  192. data/lib/hecks/projections/diagrams.rb +243 -7
  193. data/lib/hecks/projections/glossary/html.rb +88 -0
  194. data/lib/hecks/projections/glossary/markdown.rb +47 -0
  195. data/lib/hecks/projections/glossary/mermaid.rb +48 -0
  196. data/lib/hecks/projections/glossary/sentences.rb +105 -0
  197. data/lib/hecks/projections/glossary.rb +161 -19
  198. data/lib/hecks/projections/model/deviations.rb +44 -0
  199. data/lib/hecks/projections/model.rb +51 -1
  200. data/lib/hecks/projections/oidc.rb +40 -3
  201. data/lib/hecks/projections/parser_table.rb +49 -8
  202. data/lib/hecks/projections/reference.rb +12 -0
  203. data/lib/hecks/projections/rust_vocabulary.rb +219 -16
  204. data/lib/hecks/projections/shape.rb +8 -0
  205. data/lib/hecks/projections/statements.rb +63 -16
  206. data/lib/hecks/projections/vocabulary.rb +17 -0
  207. data/lib/hecks/projections.rb +3 -0
  208. data/lib/hecks/projector/cli_projector.rb +218 -10
  209. data/lib/hecks/projector/docs_projector.rb +145 -19
  210. data/lib/hecks/projector/exporter.rb +65 -11
  211. data/lib/hecks/projector/ir_projector.rb +6 -0
  212. data/lib/hecks/projector/narrate_projector.rb +136 -15
  213. data/lib/hecks/projector/target.rb +47 -10
  214. data/lib/hecks/projector.rb +101 -19
  215. data/lib/hecks/query_ir.rb +47 -0
  216. data/lib/hecks/query_specification/common/null_policy.rb +5 -3
  217. data/lib/hecks/rendering.rb +6 -0
  218. data/lib/hecks/router/namespace_installer.rb +13 -0
  219. data/lib/hecks/router.rb +55 -0
  220. data/lib/hecks/runtime/aggregate_lock.rb +9 -0
  221. data/lib/hecks/runtime/boot_gates.rb +18 -0
  222. data/lib/hecks/runtime/caller.rb +32 -0
  223. data/lib/hecks/runtime/capability_graph.rb +11 -0
  224. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
  225. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
  226. data/lib/hecks/runtime/command_interpreter.rb +42 -17
  227. data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
  228. data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
  229. data/lib/hecks/runtime/command_rules/references.rb +118 -28
  230. data/lib/hecks/runtime/dependency_planning.rb +45 -0
  231. data/lib/hecks/runtime/dispatcher.rb +28 -50
  232. data/lib/hecks/runtime/entity_element.rb +161 -8
  233. data/lib/hecks/runtime/entity_interpreter.rb +44 -9
  234. data/lib/hecks/runtime/errors.rb +18 -4
  235. data/lib/hecks/runtime/event.rb +10 -5
  236. data/lib/hecks/runtime/identity.rb +71 -3
  237. data/lib/hecks/runtime/instance.rb +67 -7
  238. data/lib/hecks/runtime/interpreting.rb +13 -5
  239. data/lib/hecks/runtime/invocation.rb +118 -36
  240. data/lib/hecks/runtime/loader.rb +94 -8
  241. data/lib/hecks/runtime/outbox.rb +145 -7
  242. data/lib/hecks/runtime/policy_interpreter.rb +22 -9
  243. data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
  244. data/lib/hecks/runtime/query_interpreter.rb +40 -12
  245. data/lib/hecks/runtime/reaction_invocation.rb +53 -8
  246. data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
  247. data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
  248. data/lib/hecks/runtime/reference_hop.rb +42 -0
  249. data/lib/hecks/runtime/refusal_wording.rb +50 -0
  250. data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
  251. data/lib/hecks/runtime/registry/verification.rb +119 -4
  252. data/lib/hecks/runtime/registry.rb +157 -4
  253. data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
  254. data/lib/hecks/runtime/routing.rb +27 -2
  255. data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
  256. data/lib/hecks/runtime/saga_interpreter.rb +27 -13
  257. data/lib/hecks/runtime/tenant_check.rb +26 -6
  258. data/lib/hecks/runtime/tenant_scope.rb +18 -0
  259. data/lib/hecks/runtime/value/coercion.rb +255 -33
  260. data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
  261. data/lib/hecks/runtime/value.rb +50 -0
  262. data/lib/hecks/runtime.rb +32 -0
  263. data/lib/hecks/storehouse.rb +305 -9
  264. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  265. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  266. data/lib/hecks/version.rb +1 -1
  267. data/lib/hecks.rb +79 -1
  268. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
  269. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
  270. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
  271. metadata +14 -5
  272. data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
  273. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
  274. data/lib/hecks/deprecation.rb +0 -95
  275. data/lib/hecks/framework/bluebook/compliance.bluebook +0 -1
@@ -18,7 +18,9 @@ module Hecks
18
18
  # domain, so the practice's own adversarial generator gets to pick the
19
19
  # conflict instead of a human picking it once and never again.
20
20
  #
21
- # The mechanism, generalized from that spec rather than REDERIVED: a
21
+ # ## The mechanism
22
+ #
23
+ # Generalized from that spec rather than rederived: a
22
24
  # command step partway through a generated sequence is chosen as the
23
25
  # race step; every step before it is setup (replayed once, sequentially,
24
26
  # to bring a fresh disposable schema to the state the race step expects
@@ -26,7 +28,9 @@ module Hecks
26
28
  # each of two real, separate, forked OS processes racing against that
27
29
  # same schema, no artificial gating, whichever the scheduler favors.
28
30
  #
29
- # The oracle is the same pair, dispatched sequentially, not a
31
+ # ## The oracle
32
+ #
33
+ # The same pair, dispatched sequentially, not a
30
34
  # hardcoded expectation — unlike the hand-authored spec (which can
31
35
  # assert `%w[refused succeeded]` because it knows its own fixture's
32
36
  # business rule), this module has no idea whether an arbitrary
@@ -42,7 +46,9 @@ module Hecks
42
46
  # (which real racer wins a genuine race is never controlled), the set
43
47
  # of outcomes does.
44
48
  #
45
- # What a broken lock looks like here: the concurrent pair settling as
49
+ # ## What a broken lock looks like here
50
+ #
51
+ # The concurrent pair settling as
46
52
  # {"succeeded", "succeeded"} where the sequential oracle says
47
53
  # {"succeeded", "refused"} — two processes each hydrated the
48
54
  # pre-write state, neither saw the other's write, and the second
@@ -51,7 +57,9 @@ module Hecks
51
57
  # PostgresEra and `postgres_concurrent_dispatch_spec.rb` still
52
58
  # documents, unfixed, for plain Postgres.
53
59
  #
54
- # Not a replacement for the hand-authored spec — that spec proves the
60
+ # ## Not a replacement for the hand-authored spec
61
+ #
62
+ # That spec proves the
55
63
  # mechanism once, precisely, with controlled gating so the assertion
56
64
  # is deterministic; this module proves the same mechanism holds for
57
65
  # whatever a real domain's own generated sequences throw at it, with
@@ -75,6 +83,15 @@ module Hecks
75
83
  # duration — the caller creates neither ahead of time (both are
76
84
  # wiped fresh by the boots below) and drops both afterward, the
77
85
  # same lifecycle `persistence_parity_schema` already has.
86
+ #
87
+ # @param domain_path [String] filesystem path to the target domain's directory
88
+ # @param steps [Array<Hash>] a generated sequence's own steps, any-keyed (normalized
89
+ # to String keys internally)
90
+ # @param database [String] the shared, never-dropped scratch database to connect to
91
+ # @param race_schema [String] a disposable schema name for the two racing processes
92
+ # @param reference_schema [String] a disposable schema name for the sequential oracle
93
+ # @return [Array<Hash>] `[]` if nothing raceable was found or no divergence occurred;
94
+ # otherwise one or more `field:`-keyed divergence findings
78
95
  def check(domain_path, steps, database:, race_schema:, reference_schema:)
79
96
  normalized = steps.map { |step| step.transform_keys(&:to_s) }
80
97
  lockable, probe_errors = lockable_verbs(domain_path, normalized, database: database, schema: reference_schema)
@@ -82,9 +99,9 @@ module Hecks
82
99
  unless race_index
83
100
  # `[]` here means "nothing to race", and must only ever mean that.
84
101
  # 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`).
102
+ # result; a probe that raised for every verb is not — see
103
+ # `lockable_verbs`, whose own `probe_errors` half exists so this
104
+ # branch can tell the two apart rather than answering `[]` for both.
88
105
  return [] if probe_errors.empty?
89
106
 
90
107
  return [{ field: "concurrency_unraceable",
@@ -103,7 +120,7 @@ module Hecks
103
120
  [{ field: "process", detail: "#{e.class}: #{e.message}" }]
104
121
  end
105
122
 
106
- # The command step CLOSEST to the middle of the sequence — not the
123
+ # The command step closest to the middle of the sequence — not the
107
124
  # first (racing a bare identity-creation with no setup at all is a
108
125
  # legitimate, useful case, so index 0 is not excluded) and not
109
126
  # chosen for any domain-specific reason: a mid-sequence step has, on
@@ -120,6 +137,12 @@ module Hecks
120
137
  # Racing anything outside that set is not a legitimate race at
121
138
  # all — nil when nothing eligible is left, same as the "no command
122
139
  # step" case, never a finding of its own.
140
+ #
141
+ # @param steps [Array<Hash>] a normalized (String-keyed) generated sequence
142
+ # @param lockable_verbs [Array<String>, nil] the verbs eligible to race, as
143
+ # `lockable_verbs` returns; nil to consider every command step eligible
144
+ # @return [Integer, nil] the index of the chosen race step, or nil if no eligible
145
+ # command step exists
123
146
  def pick_race_index(steps, lockable_verbs = nil)
124
147
  command_indices = steps.each_index.select { |i| COMMAND_STEP.call(steps[i]) }
125
148
  command_indices = command_indices.select { |i| lockable_verbs.include?(steps[i]["verb"]) } if lockable_verbs
@@ -170,6 +193,14 @@ module Hecks
170
193
  # `lockable` came back empty, `check` returned `[]`, and the sweep
171
194
  # logged a clean concurrency Check for a race that never happened.
172
195
  # `check` reports that case now instead of holding it.
196
+ #
197
+ # @param domain_path [String] filesystem path to the target domain's directory
198
+ # @param steps [Array<Hash>] a normalized (String-keyed) generated sequence
199
+ # @param database [String] the shared, never-dropped scratch database to connect to
200
+ # @param schema [String] a disposable schema name for the structural-inspection boot
201
+ # @return [Array(Array<String>, Array<String>)] `[lockable, probe_errors]` — the
202
+ # command verbs bound to a `:cross_process_lock`-declaring adapter, and one
203
+ # `"verb: Class: message"` entry per verb whose resolution raised
173
204
  def lockable_verbs(domain_path, steps, database:, schema:)
174
205
  verbs = steps.select { |step| COMMAND_STEP.call(step) }.map { |step| step["verb"] }.uniq
175
206
  lockable = []
@@ -183,6 +214,15 @@ module Hecks
183
214
  [lockable, probe_errors]
184
215
  end
185
216
 
217
+ # Answers whether `verb`'s own resolved repository declares a cross-process lock.
218
+ #
219
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted,
220
+ # structural-inspection-only runtime to resolve `verb` against
221
+ # @param verb [String] the command verb to check
222
+ # @param probe_errors [Array<String>] mutated in place: appended with
223
+ # `"verb: Class: message"` if resolving `verb` raises
224
+ # @return [Boolean] whether `verb`'s aggregate resolves and declares
225
+ # `:cross_process_lock`; false (with an appended probe error) if resolution raises
186
226
  def verb_cross_process_lockable?(runtime, verb, probe_errors = [])
187
227
  domain, aggregate_name, = Naming.split_verb(verb)
188
228
  return false unless domain && aggregate_name
@@ -200,6 +240,17 @@ module Hecks
200
240
  false
201
241
  end
202
242
 
243
+ # Compares the sequential oracle's outcomes against the two racing
244
+ # processes' own outcomes, and reports any crash or disagreement.
245
+ #
246
+ # @param race_step [Hash] the race step, String-keyed
247
+ # @param reference [Array<String>] the sequential oracle's two outcomes,
248
+ # `"succeeded"`, `"refused"`, or `"crashed:..."`
249
+ # @param concurrent [Array<String>] the two racing processes' own outcomes,
250
+ # the same shape as `reference`
251
+ # @return [Array<Hash>] `[]` if the two multisets agree and neither side crashed;
252
+ # otherwise one or more `field:`-keyed divergence findings (`"concurrency_crash"`
253
+ # or `"concurrency_race"`)
203
254
  def divergences_for(race_step, reference, concurrent)
204
255
  crashes = (reference + concurrent).select { |outcome| outcome.start_with?("crashed:") }.uniq
205
256
  return crashes.map { |c| { field: "concurrency_crash", verb: race_step["verb"], detail: c } } if crashes.any?
@@ -216,6 +267,14 @@ module Hecks
216
267
  # twice in immediate succession. Nothing else ever touches this
217
268
  # schema while this runs, so whatever the domain itself settles on
218
269
  # is correct by construction, not asserted.
270
+ #
271
+ # @param domain_path [String] filesystem path to the target domain's directory
272
+ # @param setup_steps [Array<Hash>] the normalized steps replayed once, sequentially,
273
+ # before the race step
274
+ # @param race_step [Hash] the normalized step dispatched twice in immediate succession
275
+ # @param database [String] the shared, never-dropped scratch database to connect to
276
+ # @param schema [String] this oracle's own disposable schema name
277
+ # @return [Array<String>] the two outcomes `dispatch_one` recorded, in dispatch order
219
278
  def reference_outcomes(domain_path, setup_steps, race_step, database:, schema:)
220
279
  outcomes = []
221
280
  IsolatedBoot.call(domain_path, adapter: :postgres_era, database: database, schema: schema) do |copy|
@@ -256,6 +315,15 @@ module Hecks
256
315
  # method's own code. `bin/qa_concurrency_racer` is this method's own
257
316
  # worker, one real `ruby` process per racer — read that script's own
258
317
  # header for the rest of this reasoning.
318
+ #
319
+ # @param domain_path [String] filesystem path to the target domain's directory
320
+ # @param setup_steps [Array<Hash>] the normalized steps replayed once, sequentially,
321
+ # before the two racers spawn
322
+ # @param race_step [Hash] the normalized step both racer processes dispatch
323
+ # @param database [String] the shared, never-dropped scratch database to connect to
324
+ # @param schema [String] this race's own disposable schema name, shared by both racers
325
+ # @return [Array<String>] the two racers' own outcomes, in `Process.wait` order;
326
+ # `"crashed:no output from bin/qa_concurrency_racer"` for a racer that printed nothing
259
327
  def concurrent_outcomes(domain_path, setup_steps, race_step, database:, schema:)
260
328
  IsolatedBoot.call(domain_path, adapter: :postgres_era, database: database, schema: schema) do |copy|
261
329
  dispatch_all!(Hecks.boot(copy), setup_steps)
@@ -286,6 +354,11 @@ module Hecks
286
354
  # give), anything else escaping is "crashed:<class>: <message>", a
287
355
  # genuine finding this module's own caller surfaces rather than lets
288
356
  # kill a forked racer silently.
357
+ #
358
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
359
+ # to dispatch against
360
+ # @param step [Hash] the normalized (String-keyed) step to dispatch
361
+ # @return [String] `"succeeded"`, `"refused"`, or `"crashed:<class>: <message>"`
289
362
  def dispatch_one(runtime, step)
290
363
  args = (step["args"] || {}).transform_keys(&:to_sym)
291
364
  runtime.dispatch_flat(step["verb"], args)
@@ -296,12 +369,21 @@ module Hecks
296
369
  "crashed:#{e.class}: #{e.message}"
297
370
  end
298
371
 
372
+ # Dispatches every step in order, raising the first time one crashes.
373
+ #
299
374
  # Setup tolerates an ordinary refusal (a generated sequence's own
300
375
  # earlier step can legitimately refuse — every other mode in this
301
376
  # practice already replays a prefix that way) but never a crash: an
302
377
  # unexpected exception during setup means the schema this race is
303
378
  # about to run against is in an unknown state, which is itself
304
379
  # worth surfacing, not silently racing anyway.
380
+ #
381
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
382
+ # to dispatch against
383
+ # @param steps [Array<Hash>] the normalized steps to dispatch, in order
384
+ # @return [void]
385
+ # @raise [RuntimeError] if any step's own `dispatch_one` outcome is a crash, naming
386
+ # the step's verb and outcome
305
387
  def dispatch_all!(runtime, steps)
306
388
  steps.each do |step|
307
389
  outcome = dispatch_one(runtime, step)
@@ -309,6 +391,9 @@ module Hecks
309
391
  end
310
392
  end
311
393
 
394
+ # Boots a `PostgresEra`-rebound copy of the domain against an
395
+ # existing schema, without wiping it first.
396
+ #
312
397
  # The same copy-and-rebind `IsolatedBoot.call(..., adapter:
313
398
  # :postgres_era, ...)` does, minus the schema wipe — deliberately
314
399
  # not reusing `IsolatedBoot.rebind_to_postgres_era!` itself, which
@@ -322,6 +407,14 @@ module Hecks
322
407
  # this writes is duplicated from it, not re-derived, because the
323
408
  # shape a `PostgresEra`-bound copy's `.world` needs is exactly that
324
409
  # method's own, one step short.
410
+ #
411
+ # @param domain_path [String] filesystem path to the target domain's directory
412
+ # @param database [String] the PostgresEra database name to bind the copy to
413
+ # @param schema [String] the existing PostgresEra schema name to bind the copy to,
414
+ # left exactly as found
415
+ # @yield [copy] runs against the rebound copy
416
+ # @yieldparam copy [String] filesystem path to the rebound domain copy
417
+ # @return [Object] the block's own result
325
418
  def boot_preserving_schema(domain_path, database:, schema:)
326
419
  Dir.mktmpdir("hecks-concurrency") do |tmp|
327
420
  copy = File.join(tmp, File.basename(domain_path))
@@ -333,6 +426,14 @@ module Hecks
333
426
  end
334
427
  end
335
428
 
429
+ # Writes a `.world` file binding every hecksagon-named member in
430
+ # `copy` to `database`/`schema`, replacing any other `.world` file
431
+ # the copy already carries.
432
+ #
433
+ # @param copy [String] filesystem path to the domain copy to rewrite
434
+ # @param database [String] the PostgresEra database name to write into the `.world` file
435
+ # @param schema [String] the PostgresEra schema name to write into the `.world` file
436
+ # @return [void]
336
437
  def write_postgres_era_world!(copy, database:, schema:)
337
438
  Dir.glob(File.join(copy, "**", "*.hecksagon")).each do |hecksagon_path|
338
439
  names = File.read(hecksagon_path).scan(/Hecks\.hecksagon\s+"([^"]+)"/).flatten.uniq
@@ -3,23 +3,27 @@ module Hecks
3
3
  # What one sweep has already reached, and where the next seed starts.
4
4
  #
5
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,
6
+ # weighted up — picker.rb's `steer`), but without this, every seed of a
7
+ # sweep would start from nothing and forget everything the seeds before
8
+ # it reached. Roughly half of the QA ledger's bugs are refusal-kind splits (BUG#2–4,
9
9
  # 7, 8, 14–16, 19–21, 23, 27, 28, 36–38, 41, 54, 56): a particular verb,
10
10
  # in a particular state, with a particular malformed argument, refused
11
11
  # one way on Ruby and another on Rust. BUG#11 was found by hand because
12
12
  # a 25-step random walk never reached `Annotate` at all. Uniform seeds
13
13
  # reach those corners by luck; this makes the sweep remember.
14
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.
15
+ # ## The coverage unit
20
16
  #
21
- # Two levers, both expressed as plain generator arguments so a seed
22
- # stays reproducible from one call:
17
+ # A tuple, not a verb — `verb | step kind | lifecycle state before |
18
+ # adversarial mutation | outcome` (see `SequenceGenerator#coverage_tuple`).
19
+ # "Renew refused LifecycleRefused from free" and "Renew ok from held" are
20
+ # different places a runtime can be wrong, where a verb-level count
21
+ # calls them the same.
22
+ #
23
+ # ## Two levers
24
+ #
25
+ # Both expressed as plain generator arguments so a seed stays
26
+ # reproducible from one call:
23
27
  #
24
28
  # 1. Splicing (`prefix:`). A sequence that reached a new tuple joins
25
29
  # the corpus, cut at its last new tuple. A later seed may start by
@@ -32,18 +36,37 @@ module Hecks
32
36
  # including declared verbs it has never hit at all — are weighted
33
37
  # up for the next seed.
34
38
  #
39
+ # ## Reproducibility
40
+ #
35
41
  # Nothing here is random at the campaign's own level beyond
36
42
  # `Random.new(seed)` per plan, and a plan is printed with every finding
37
43
  # (`bin/qa_sweep`'s `reproduce:` line), so no finding depends on
38
44
  # re-running the whole campaign to get back to it.
39
45
  class CoverageCampaign
46
+ # One seed's generation instructions, printable and reproducible.
40
47
  Plan = Struct.new(:prefix, :favor, keyword_init: true) do
48
+ # Renders this plan as the keyword arguments a generator call accepts.
49
+ #
50
+ # @return [Hash{Symbol => Object}] `{ prefix:, favor: }`, the keyword arguments
51
+ # `SequenceGenerator.generate` and `.trace` accept for this plan
41
52
  def generator_options = { prefix: prefix, favor: favor }
53
+
54
+ # Answers whether this plan starts from a spliced corpus prefix.
55
+ #
56
+ # @return [Boolean] whether this plan starts from a spliced corpus prefix
42
57
  def spliced? = !prefix.nil?
43
58
  end
44
59
 
45
60
  attr_reader :corpus
46
61
 
62
+ # @param splice_probability [Float, Integer] chance, per seed with a non-empty
63
+ # corpus, of splicing a corpus entry's prefix instead of generating fresh
64
+ # @param favor_count [Integer, Float] how many rare verbs to favor per plan; 0
65
+ # or non-positive disables favoring entirely
66
+ # @param corpus_limit [Integer] maximum corpus entries kept; the oldest is
67
+ # dropped once this is exceeded
68
+ # @param max_prefix_depth [Integer] maximum nested-prefix depth a new corpus
69
+ # entry may reach before splicing stops extending it further
47
70
  def initialize(splice_probability:, favor_count:, corpus_limit: 64, max_prefix_depth: 6)
48
71
  @splice_probability = splice_probability.to_f
49
72
  @favor_count = favor_count.to_i
@@ -58,6 +81,13 @@ module Hecks
58
81
  @seeds_with_new = 0
59
82
  end
60
83
 
84
+ # Builds this seed's generation plan — a possible splice, plus which
85
+ # verbs to favor.
86
+ #
87
+ # @param seed [Integer] the seed this plan is for; drives the splice-or-not
88
+ # coin flip and, when splicing, which corpus entry and prefix length are drawn
89
+ # @return [Fuzzing::CoverageCampaign::Plan] the plan `SequenceGenerator.generate`
90
+ # or `.trace` should be run with for this seed
61
91
  def plan(seed)
62
92
  random = Random.new(seed)
63
93
  prefix = nil
@@ -68,9 +98,15 @@ module Hecks
68
98
  Plan.new(prefix: prefix, favor: rare_verbs)
69
99
  end
70
100
 
71
- # `trace` is `SequenceGenerator.trace`'s answer: `coverage` is
72
- # `[[attempt_index, tuple], ...]`, `verbs` every verb the booted
73
- # catalog offered.
101
+ # Folds one seed's generation results into the campaign's running
102
+ # coverage, and admits its corpus entry if it reached anything new.
103
+ #
104
+ # @param seed [Integer] the seed this trace was generated from
105
+ # @param plan [Fuzzing::CoverageCampaign::Plan] the plan that seed was generated with
106
+ # @param trace [Fuzzing::SequenceGenerator::Trace] `SequenceGenerator.trace`'s answer:
107
+ # `coverage` is `[[attempt_index, tuple], ...]`, `verbs` every verb the booted
108
+ # catalog offered
109
+ # @return [void]
74
110
  def record(seed, plan, trace)
75
111
  @seeds += 1
76
112
  @spliced += 1 if plan.spliced?
@@ -86,8 +122,15 @@ module Hecks
86
122
  admit(seed, plan, new_at.max + 1)
87
123
  end
88
124
 
125
+ # Counts the distinct coverage tuples this campaign has reached so far.
126
+ #
127
+ # @return [Integer] the number of distinct coverage tuples seen
89
128
  def tuples_seen = @seen.size
90
129
 
130
+ # Renders the campaign's running totals as one human-readable line.
131
+ #
132
+ # @return [String] a one-line summary of tuples seen, seeds run, seeds that
133
+ # reached something new, and how many were spliced from the corpus
91
134
  def summary
92
135
  "coverage: #{@seen.size} distinct (verb, kind, state, mutation, outcome) tuple(s) over #{@seeds} seed(s); " \
93
136
  "#{@seeds_with_new} seed(s) reached something new; #{@spliced} spliced from a corpus of #{@corpus.size}"
@@ -45,6 +45,16 @@ module Hecks
45
45
  # tolerated verb Rust nonetheless answered with a query row: the
46
46
  # manifest says "not generated" but the binary disagrees, so the
47
47
  # tolerance itself is wrong and must not silently hold.
48
+ # @param gaps [Hecks::Fuzzing::RustGapManifest] the compiled binary's manifest
49
+ # @param ruby_refusals [Array<Hash>] Ruby's own refusal rows for this history
50
+ # @param rust_refusals [Array<Hash>] the Rust binary's own refusal rows
51
+ # @param ruby_queries [Array<Hash>] Ruby's own query rows for this history
52
+ # @param rust_queries [Array<Hash>] the Rust binary's own query rows
53
+ # @return [Hash] `ruby_refusals:`/`rust_refusals:`/`ruby_queries:`/
54
+ # `rust_queries:` (each with tolerated verbs removed), `skipped:` (a
55
+ # `Set<String>` of every tolerated verb this history actually reached),
56
+ # and `stale:` (an `Array<Hash>` divergence per tolerated verb the Rust
57
+ # binary nonetheless answered)
48
58
  def manifest_partition(gaps, ruby_refusals:, rust_refusals:, ruby_queries:, rust_queries:)
49
59
  verb_of = ->(row) { row.key?("verb") ? row["verb"] : row["query"] }
50
60
  declared = ->(row) { gaps.not_generated?(verb_of.call(row)) }
@@ -60,11 +70,22 @@ module Hecks
60
70
  skipped: reached.to_set(&verb_of), stale: stale }
61
71
  end
62
72
 
73
+ # Runs the standard property battery and reports every one that failed.
74
+ #
75
+ # @param history [Hash] a replayed history as returned by `Replay.call`
76
+ # @return [Array<Hash>] one `{field:, detail:}` entry per failed property
63
77
  def property_divergences(history)
64
78
  Properties.check(history).reject { |_, result| result == true }
65
79
  .map { |name, message| { field: name.to_s, detail: message } }
66
80
  end
67
81
 
82
+ # Flattens `history[:self_consistency]`'s own per-check divergence lists.
83
+ #
84
+ # @param history [Hash] a replayed history as returned by `Replay.call`,
85
+ # optionally carrying `:self_consistency` (`Replay.call`'s own
86
+ # `self_consistency: true`)
87
+ # @return [Array<Hash>] every self-consistency divergence recorded; empty if
88
+ # `history` carries no `:self_consistency`
68
89
  def self_consistency_divergences(history)
69
90
  return [] unless history[:self_consistency]
70
91
 
@@ -75,6 +96,19 @@ module Hecks
75
96
  # rubocop:disable-next Metrics/CyclomaticComplexity
76
97
  # rubocop:disable-next Metrics/PerceivedComplexity
77
98
  # rubocop:disable-next Metrics/MethodLength
99
+ # @param differ [Object] duck-typed comparison helper answering
100
+ # `RustConformanceHelpers`' interface (adapter-defined)
101
+ # @param domain_path [String] path to the domain directory to replay
102
+ # @param steps [Array<Hash>] the step sequence to replay on both engines
103
+ # @param binary [String] path to the compiled Rust conformance binary to run
104
+ # @param modes [Array<Symbol>] which comparisons to run — any of
105
+ # `:differential` (always run), `:self_consistency`,
106
+ # `:properties_in_differential`, `:adapter_parity_sqlite`
107
+ # @param adapter_parity_sqlite [Proc, nil] callable answering the
108
+ # `:adapter_parity_sqlite` mode's own comparison, or `nil` to skip it
109
+ # even when named in `modes`
110
+ # @return [Hash{Symbol => Array<Hash>}] one divergence list per active mode,
111
+ # keyed by the mode name; `:differential` is always present
78
112
  def diff(differ, domain_path, steps, binary, modes:, adapter_parity_sqlite: nil)
79
113
  self_consistency = modes.include?(:self_consistency)
80
114
  ruby_result = Replay.call(domain_path, steps, self_consistency: self_consistency)