hecks 1.4.0 → 1.5.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 (267) 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.hecksagon +19 -0
  112. data/lib/hecks/doc/reference.rb +185 -16
  113. data/lib/hecks/embryonaut_bluebook.rb +32 -9
  114. data/lib/hecks/facade/handle.rb +76 -3
  115. data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
  116. data/lib/hecks/forms/field_shape.rb +3 -0
  117. data/lib/hecks/forms/page.rb +14 -0
  118. data/lib/hecks/forms/port_argument.rb +12 -0
  119. data/lib/hecks/forms/query_form_renderer.rb +63 -0
  120. data/lib/hecks/forms/record_renderer.rb +58 -0
  121. data/lib/hecks/forms/record_table.rb +27 -0
  122. data/lib/hecks/forms/reference_options.rb +24 -0
  123. data/lib/hecks/forms/value_object_shape.rb +10 -0
  124. data/lib/hecks/fqn.rb +58 -0
  125. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  126. data/lib/hecks/framework/oidc.json +15 -0
  127. data/lib/hecks/framework.rb +43 -20
  128. data/lib/hecks/freezer.rb +17 -1
  129. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
  130. data/lib/hecks/fuzzing/combination_miner.rb +59 -0
  131. data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
  132. data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
  133. data/lib/hecks/fuzzing/differential.rb +34 -0
  134. data/lib/hecks/fuzzing/domain_generator.rb +188 -11
  135. data/lib/hecks/fuzzing/era_boundary.rb +45 -15
  136. data/lib/hecks/fuzzing/form_census.rb +86 -0
  137. data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
  138. data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
  139. data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
  140. data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
  141. data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
  142. data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
  143. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
  144. data/lib/hecks/fuzzing/properties/guards.rb +44 -0
  145. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
  146. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
  147. data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
  148. data/lib/hecks/fuzzing/properties/querying.rb +68 -14
  149. data/lib/hecks/fuzzing/properties.rb +24 -15
  150. data/lib/hecks/fuzzing/qa_settings.rb +12 -0
  151. data/lib/hecks/fuzzing/replay.rb +137 -29
  152. data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
  153. data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
  154. data/lib/hecks/fuzzing/self_consistency.rb +189 -40
  155. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
  156. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
  157. data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
  158. data/lib/hecks/fuzzing/shrinker.rb +114 -2
  159. data/lib/hecks/fuzzing/structural_skips.rb +18 -4
  160. data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
  161. data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
  162. data/lib/hecks/fuzzing/value_generator.rb +98 -10
  163. data/lib/hecks/grammar/evolve.rb +178 -2
  164. data/lib/hecks/grammar.rb +46 -0
  165. data/lib/hecks/ir.rb +38 -7
  166. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  167. data/lib/hecks/literal.rb +32 -0
  168. data/lib/hecks/naming.rb +88 -7
  169. data/lib/hecks/ports/access_control.rb +5 -10
  170. data/lib/hecks/ports/authorization.rb +3 -6
  171. data/lib/hecks/ports/identity_assignment.rb +1 -2
  172. data/lib/hecks/ports/identity_resolution.rb +1 -2
  173. data/lib/hecks/ports/key_vault.port +6 -0
  174. data/lib/hecks/ports/key_vault.rb +58 -0
  175. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
  176. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
  177. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
  178. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
  179. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
  180. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  181. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
  182. data/lib/hecks/ports/query/in_memory.rb +39 -6
  183. data/lib/hecks/ports/query/ordering.rb +15 -0
  184. data/lib/hecks/ports.rb +1 -0
  185. data/lib/hecks/projections/bootstrap_table.rb +43 -8
  186. data/lib/hecks/projections/diagrams.rb +243 -7
  187. data/lib/hecks/projections/glossary/html.rb +88 -0
  188. data/lib/hecks/projections/glossary/markdown.rb +47 -0
  189. data/lib/hecks/projections/glossary/mermaid.rb +48 -0
  190. data/lib/hecks/projections/glossary/sentences.rb +105 -0
  191. data/lib/hecks/projections/glossary.rb +161 -19
  192. data/lib/hecks/projections/model/deviations.rb +44 -0
  193. data/lib/hecks/projections/model.rb +51 -1
  194. data/lib/hecks/projections/oidc.rb +40 -3
  195. data/lib/hecks/projections/parser_table.rb +49 -8
  196. data/lib/hecks/projections/reference.rb +12 -0
  197. data/lib/hecks/projections/rust_vocabulary.rb +219 -16
  198. data/lib/hecks/projections/shape.rb +8 -0
  199. data/lib/hecks/projections/statements.rb +63 -16
  200. data/lib/hecks/projections/vocabulary.rb +17 -0
  201. data/lib/hecks/projector/cli_projector.rb +218 -10
  202. data/lib/hecks/projector/docs_projector.rb +145 -19
  203. data/lib/hecks/projector/exporter.rb +65 -11
  204. data/lib/hecks/projector/ir_projector.rb +6 -0
  205. data/lib/hecks/projector/narrate_projector.rb +136 -15
  206. data/lib/hecks/projector/target.rb +29 -5
  207. data/lib/hecks/projector.rb +74 -6
  208. data/lib/hecks/query_ir.rb +47 -0
  209. data/lib/hecks/query_specification/common/null_policy.rb +5 -3
  210. data/lib/hecks/rendering.rb +6 -0
  211. data/lib/hecks/router/namespace_installer.rb +13 -0
  212. data/lib/hecks/router.rb +55 -0
  213. data/lib/hecks/runtime/aggregate_lock.rb +9 -0
  214. data/lib/hecks/runtime/boot_gates.rb +18 -0
  215. data/lib/hecks/runtime/caller.rb +32 -0
  216. data/lib/hecks/runtime/capability_graph.rb +11 -0
  217. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
  218. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
  219. data/lib/hecks/runtime/command_interpreter.rb +42 -17
  220. data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
  221. data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
  222. data/lib/hecks/runtime/command_rules/references.rb +118 -28
  223. data/lib/hecks/runtime/dependency_planning.rb +45 -0
  224. data/lib/hecks/runtime/dispatcher.rb +28 -50
  225. data/lib/hecks/runtime/entity_element.rb +161 -8
  226. data/lib/hecks/runtime/entity_interpreter.rb +44 -9
  227. data/lib/hecks/runtime/errors.rb +18 -4
  228. data/lib/hecks/runtime/event.rb +10 -5
  229. data/lib/hecks/runtime/identity.rb +71 -3
  230. data/lib/hecks/runtime/instance.rb +67 -7
  231. data/lib/hecks/runtime/interpreting.rb +13 -5
  232. data/lib/hecks/runtime/invocation.rb +118 -36
  233. data/lib/hecks/runtime/loader.rb +94 -8
  234. data/lib/hecks/runtime/outbox.rb +145 -7
  235. data/lib/hecks/runtime/policy_interpreter.rb +22 -9
  236. data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
  237. data/lib/hecks/runtime/query_interpreter.rb +40 -12
  238. data/lib/hecks/runtime/reaction_invocation.rb +53 -8
  239. data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
  240. data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
  241. data/lib/hecks/runtime/reference_hop.rb +42 -0
  242. data/lib/hecks/runtime/refusal_wording.rb +50 -0
  243. data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
  244. data/lib/hecks/runtime/registry/verification.rb +119 -4
  245. data/lib/hecks/runtime/registry.rb +157 -4
  246. data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
  247. data/lib/hecks/runtime/routing.rb +27 -2
  248. data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
  249. data/lib/hecks/runtime/saga_interpreter.rb +27 -13
  250. data/lib/hecks/runtime/tenant_check.rb +26 -6
  251. data/lib/hecks/runtime/tenant_scope.rb +18 -0
  252. data/lib/hecks/runtime/value/coercion.rb +255 -33
  253. data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
  254. data/lib/hecks/runtime/value.rb +50 -0
  255. data/lib/hecks/runtime.rb +32 -0
  256. data/lib/hecks/storehouse.rb +305 -9
  257. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  258. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  259. data/lib/hecks/version.rb +1 -1
  260. data/lib/hecks.rb +79 -1
  261. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
  262. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
  263. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
  264. metadata +11 -5
  265. data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
  266. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
  267. data/lib/hecks/deprecation.rb +0 -95
@@ -15,9 +15,11 @@ module Hecks
15
15
  # compare it against. This module asks a different question of one
16
16
  # engine at a time: does it agree with itself?
17
17
  #
18
- # Three checks, run on the same already-generated sequence and its
19
- # resulting live state — no second fuzzing pass, no re-dispatch
20
- # through the command layer:
18
+ # ## Three checks
19
+ #
20
+ # Run on the same already-generated sequence and its resulting live
21
+ # state — no second fuzzing pass, no re-dispatch through the command
22
+ # layer:
21
23
  #
22
24
  # 1. `check_rehydration` — does reloading an aggregate from its
23
25
  # own durable journal reproduce exactly the state a live dispatch
@@ -29,30 +31,31 @@ module Hecks
29
31
  # 3. `check_value_object_round_trip` — does every value object the
30
32
  # sequence actually built survive `to_json` then rebuilt back?
31
33
  #
32
- # **The rehydration path, found, not guessed**. hecks is not event-sourced
33
- # at the aggregate level — there is no `AccountOpened`-shaped log a
34
- # `CommandInterpreter` folds to rebuild state. What there is, real and
35
- # already shipping, is `Ports::Persistence::AppendOnly` (lib/hecks/
36
- # ports/persistence/append_only.rb): every adapter accepts the same
37
- # `Entry` stream (`operation`, `id`, the full state after that
38
- # command — not a delta) and answers `#entries`; `#recover!` — "an
39
- # append is durable before a projection is attempted; replaying the
40
- # log restores a snapshot/table after a crash in that small window" —
41
- # is called on every repository this runtime ever builds
42
- # (`RepositoryFactory.build`'s own `recover: true` default). That is
43
- # the production cold-rehydration path. Reusing `#recover!` directly
44
- # against the live adapter would prove nothing, though: `Fuzzing::
45
- # Replay` runs against `Adapters::Memory` (`IsolatedBoot`'s own
46
- # default), and Memory's own `Entry#state` is a shallow `instance.
47
- # state.dup` — the exact same `Runtime::Value` objects a command
48
- # produced ride along unchanged, so folding them straight back through
49
- # Memory's own `#project` is a tautology that can never fail (`Value#
50
- # for_attribute`'s own `value.is_a?(self) && value.type_name == ...`
51
- # branch passes an already-typed value straight through, no
52
- # rebuilding at all).
34
+ # ## The rehydration path, found, not guessed
35
+ #
36
+ # hecks is not event-sourced at the aggregate level — there is no
37
+ # `AccountOpened`-shaped log a `CommandInterpreter` folds to rebuild
38
+ # state. What there is, real and already shipping, is
39
+ # `Ports::Persistence::AppendOnly` (lib/hecks/ports/persistence/
40
+ # append_only.rb): every adapter accepts the same `Entry` stream
41
+ # (`operation`, `id`, the full state after that command — not a
42
+ # delta) and answers `#entries`; `#recover!` — "an append is durable
43
+ # before a projection is attempted; replaying the log restores a
44
+ # snapshot/table after a crash in that small window" — is called on
45
+ # every repository this runtime ever builds (`RepositoryFactory
46
+ # .build`'s own `recover: true` default). That is the production
47
+ # cold-rehydration path. Reusing `#recover!` directly against the
48
+ # live adapter would prove nothing, though: `Fuzzing::Replay` runs
49
+ # against `Adapters::Memory` (`IsolatedBoot`'s own default), and
50
+ # Memory's own `Entry#state` is a shallow `instance.state.dup` — the
51
+ # exact same `Runtime::Value` objects a command produced ride along
52
+ # unchanged, so folding them straight back through Memory's own
53
+ # `#project` is a tautology that can never fail (`Value#for_attribute`
54
+ # 's own `value.is_a?(self) && value.type_name == ...` branch passes
55
+ # an already-typed value straight through, no rebuilding at all).
53
56
  #
54
- # So `cold_read`, below, feeds the same entries through `Adapters::
55
- # Heki` instead — a real, already-shipped, disk-backed adapter
57
+ # So `fold!`, below, feeds the same entries through `Adapters::Heki`
58
+ # instead — a real, already-shipped, disk-backed adapter
56
59
  # (examples/banking's own `persisted_by("Heki")`), in a throwaway
57
60
  # directory. Writing forces every value through `JSON.generate`
58
61
  # (Heki's own journal line, its own compressed snapshot); reading
@@ -76,6 +79,11 @@ module Hecks
76
79
  # could hand `Runtime::Instance` undecoded state and the codec's
77
80
  # guarantee would have a bypass in exactly the check that exists to
78
81
  # compare stored state against live state.
82
+ #
83
+ # @param aggregate [Bluebook::Aggregate] the aggregate this store holds
84
+ # @param settings [Hash] adapter settings, forwarded to `Adapters::Heki.new`
85
+ # @param root [String, nil] the directory this store reads/writes under
86
+ # @return [Adapters::Heki] a fresh, codec-boundary-guarded Heki instance
79
87
  def guarded_heki(**) = Ports::Persistence::CodecBoundary.guard!(Adapters::Heki.new(**))
80
88
 
81
89
  # The whole pass — called once, with the runtime still live (inside
@@ -87,6 +95,14 @@ module Hecks
87
95
  # below, run and collected — kept as three independently callable
88
96
  # methods (not fused into one shared fold) so a spec proving one
89
97
  # check can fire never has to reason about the other two.
98
+ #
99
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the still-live
100
+ # booted runtime this replay just dispatched through
101
+ # @param history [Hash] the replayed history `Replay.call` is about to return
102
+ # @return [Hash{Symbol => Array<Hash>}] the five checks' own findings, keyed
103
+ # `:rehydration`, `:idempotency`, `:value_object_round_trip`,
104
+ # `:saga_rehydration`, `:saga_redelivery_idempotency` — each value an empty
105
+ # Array when that check found nothing
90
106
  def check(runtime, history)
91
107
  { rehydration: check_rehydration(runtime), idempotency: check_idempotency(runtime),
92
108
  value_object_round_trip: check_value_object_round_trip(history),
@@ -95,6 +111,12 @@ module Hecks
95
111
  end
96
112
 
97
113
  # Check 1 — rehydrate-from-journal == live state.
114
+ #
115
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the still-live
116
+ # booted runtime this replay just dispatched through
117
+ # @return [Array<Hash>] one divergence Hash (`:field`, `:domain`, `:aggregate`,
118
+ # `:live`, `:rehydrated`) per aggregate whose cold-read state disagrees with its
119
+ # live state; empty when every touched aggregate rehydrates cleanly
98
120
  def check_rehydration(runtime)
99
121
  each_touched_repository(runtime).filter_map do |domain_name, aggregate, repository, entries|
100
122
  live = snapshot(repository)
@@ -115,6 +137,12 @@ module Hecks
115
137
  # replay-specific bug (leaked state between applications, a
116
138
  # double-applied effect) that a single, one-shot cold read could
117
139
  # never see, even one that already agrees with live state.
140
+ #
141
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the still-live
142
+ # booted runtime this replay just dispatched through
143
+ # @return [Array<Hash>] one divergence Hash (`:field`, `:domain`, `:aggregate`,
144
+ # `:once`, `:twice`) per aggregate whose second fold disagrees with its first;
145
+ # empty when every touched aggregate folds idempotently
118
146
  def check_idempotency(runtime)
119
147
  each_touched_repository(runtime).filter_map do |domain_name, aggregate, repository, entries|
120
148
  Dir.mktmpdir("hecks-self-consistency") do |tmp|
@@ -162,6 +190,12 @@ module Hecks
162
190
  # positive `nil` already produced once — `instances` and `events`
163
191
  # alone already reach every value object a generated sequence
164
192
  # actually persisted or announced.
193
+ #
194
+ # @param history [Hash] a replayed history as returned by `Replay.call`
195
+ # @return [Array<Hash>] one divergence Hash (`:field`, `:type`, `:original`, and
196
+ # either `:rehydrated` or `:error`) per value object whose `to_json`-then-`build`
197
+ # round trip disagrees with (or raises on) the original; empty when every value
198
+ # object found round-trips cleanly
165
199
  def check_value_object_round_trip(history)
166
200
  bluebooks = history[:bluebooks] || {}
167
201
  seen = {}.compare_by_identity
@@ -235,6 +269,14 @@ module Hecks
235
269
  # not the settled snapshot this history exists to describe), so
236
270
  # there is no ground truth to compare it against here. Written as an
237
271
  # empty array on the way in and never read back on the way out.
272
+ #
273
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the still-live
274
+ # booted runtime this replay just dispatched through
275
+ # @param history [Hash] a replayed history as returned by `Replay.call`
276
+ # @return [Array<Hash>] one divergence Hash (`:field`, `:domain`,
277
+ # `:process_manager`, `:live`, `:rehydrated`) per process manager whose cold-read
278
+ # saga instances disagree with the ones this replay persisted; empty when every
279
+ # process manager with persisted instances rehydrates cleanly
238
280
  def check_saga_rehydration(runtime, history)
239
281
  saga_instances = history[:saga_instances] || {}
240
282
  each_domain_process_manager(runtime).filter_map do |domain_name, process_manager|
@@ -325,6 +367,14 @@ module Hecks
325
367
  # the redelivered event name (the ordinary, expected case once a
326
368
  # saga has moved past the leg that produced its own current
327
369
  # checkpoint) is exactly what this proves stays put.
370
+ #
371
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the still-live
372
+ # booted runtime this replay just dispatched through
373
+ # @param history [Hash] a replayed history as returned by `Replay.call`
374
+ # @return [Array<Hash>] one divergence Hash (`:field`, `:domain`,
375
+ # `:process_manager`, `:correlation`, `:on`, `:before`, `:after`) per correlation
376
+ # whose redelivered event visibly changed state/memory on a rehydrated
377
+ # checkpoint; empty when every redelivered correlation stayed put
328
378
  def check_saga_idempotency(runtime, history)
329
379
  saga_instances = history[:saga_instances] || {}
330
380
  interpreter = Runtime::SagaInterpreter.new(runtime.registry, door: runtime)
@@ -375,19 +425,25 @@ module Hecks
375
425
  # comment). This check has no Ruby side to spare — it is asking the
376
426
  # Rust binary whether it agrees with itself, so `emitted_*` fields
377
427
  # are exactly as real a fact to compare as any other. Stripping them
378
- # here (an earlier version of this method did) silently deleted
379
- # them from the returned seed-round-trip result while leaving them
380
- # present on the original `seed_instances` a caller passes in — an
381
- # asymmetric comparison that reported every record carrying one as
382
- # a rehydration divergence, unconditionally, on every domain that
383
- # has one at all. Found live against `examples/banking`
384
- # (`Banking::Account`'s own `corrects` reaction) while this
385
- # integration was being written, by comparing this method's own
386
- # answer against the compiled binary's raw stdout for the identical
387
- # seed call: the raw round trip preserved `emitted_fee_applied`
388
- # correctly; only this method's own stripping dropped it. `spec/
389
- # self_consistency_rust_spec.rb`'s own "banking" example pins the
390
- # regression against a real domain going forward.
428
+ # here would silently delete them from the returned seed-round-trip
429
+ # result while leaving them present on the original `seed_instances`
430
+ # a caller passes in — an asymmetric comparison that would report
431
+ # every record carrying one as a rehydration divergence,
432
+ # unconditionally, on every domain that has one at all. Confirmed
433
+ # live against `examples/banking` (`Banking::Account`'s own
434
+ # `corrects` reaction) by comparing this method's own answer against
435
+ # the compiled binary's raw stdout for the identical seed call: the
436
+ # raw round trip preserves `emitted_fee_applied` correctly. `spec/
437
+ # self_consistency_rust_spec.rb`'s own "banking" example pins this
438
+ # against a real domain going forward.
439
+ #
440
+ # @param binary [String] path to the compiled Rust conformance binary
441
+ # @param _differ [Object] a `RustConformanceHelpers`-including instance, unused here
442
+ # @param seed_instances [Hash] the `"instances"` shape to seed the fresh invocation
443
+ # with, in place of replaying `steps`
444
+ # @return [Hash] the fresh invocation's own `"instances"`, or a
445
+ # `{"__self_consistency_error__" => String}` Hash if the binary exited nonzero or
446
+ # the binary itself reported an `"error"`
391
447
  def rust_seed_round_trip(binary, _differ, seed_instances)
392
448
  stdout, status = Open3.capture2(binary, stdin_data: JSON.generate({ "steps" => [], "seed" => seed_instances }))
393
449
  return { "__self_consistency_error__" => "rust binary exited #{status.exitstatus}: #{stdout}" } \
@@ -404,6 +460,13 @@ module Hecks
404
460
  # state, unchanged. `live_instances` is `rust_output["instances"]`
405
461
  # — the exact same value `bin/qa_sweep`'s own differential compare
406
462
  # already diffed against Ruby, reused here rather than re-derived.
463
+ #
464
+ # @param binary [String] path to the compiled Rust conformance binary
465
+ # @param differ [Object] a `RustConformanceHelpers`-including instance, forwarded to
466
+ # `rust_seed_round_trip` unused
467
+ # @param live_instances [Hash] the prior invocation's own `"instances"` output
468
+ # @return [Array<Hash>] a single divergence Hash (`:field`, `:live`, `:rehydrated`)
469
+ # if seeding with `live_instances` fails to reproduce it; empty otherwise
407
470
  def check_rust_rehydration(binary, differ, live_instances)
408
471
  rehydrated = rust_seed_round_trip(binary, differ, live_instances)
409
472
  return [] if rehydrated == live_instances
@@ -416,6 +479,14 @@ module Hecks
416
479
  # same "replay it again, byte for byte" claim `check_idempotency`
417
480
  # proves for Ruby, aimed at the one rehydration door this compiled
418
481
  # binary actually has.
482
+ #
483
+ # @param binary [String] path to the compiled Rust conformance binary
484
+ # @param differ [Object] a `RustConformanceHelpers`-including instance, forwarded to
485
+ # `rust_seed_round_trip` unused
486
+ # @param live_instances [Hash] the prior invocation's own `"instances"` output to
487
+ # seed the first round trip with
488
+ # @return [Array<Hash>] a single divergence Hash (`:field`, `:once`, `:twice`) if a
489
+ # second seed round trip disagrees with the first; empty otherwise
419
490
  def check_rust_idempotency(binary, differ, live_instances)
420
491
  once = rust_seed_round_trip(binary, differ, live_instances)
421
492
  twice = rust_seed_round_trip(binary, differ, once)
@@ -432,6 +503,12 @@ module Hecks
432
503
  # finding a "clean, nothing touched" report would mean anything
433
504
  # for. Mirrors `Replay#snapshot_instances`' own
434
505
  # `bluebooks.each { aggregates.each { repository(...) } }` walk.
506
+ #
507
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the still-live
508
+ # booted runtime this replay just dispatched through
509
+ # @return [Array<Array(String, Bluebook::Aggregate, Ports::Persistence::AppendOnly,
510
+ # Array<Ports::Persistence::Entry>)>] one `[domain_name, aggregate, repository,
511
+ # entries]` tuple per aggregate with at least one journal entry
435
512
  def each_touched_repository(runtime)
436
513
  found = []
437
514
  runtime.registry.bluebooks.each do |domain_name, bluebook|
@@ -446,6 +523,12 @@ module Hecks
446
523
  found
447
524
  end
448
525
 
526
+ # Materializes a repository's own stored records, keyed by id, for comparing
527
+ # against a cold read of the same aggregate.
528
+ #
529
+ # @param repository [Ports::Persistence::AppendOnly] the repository to read
530
+ # @return [Hash{String => Object}] every stored record's id (stringified) mapped to
531
+ # its materialized state
449
532
  def snapshot(repository)
450
533
  repository.all.to_h { |record| [record.id.to_s, Runtime::Value.materialize(record.state)] }
451
534
  end
@@ -461,6 +544,11 @@ module Hecks
461
544
  # identical to "ran and found nothing," which is deliberate: neither
462
545
  # check has a positive "passed" artifact to report either way (see
463
546
  # this file's own header on why silence is never a claimed pass).
547
+ #
548
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the still-live
549
+ # booted runtime this replay just dispatched through
550
+ # @return [Array<Array(String, Bluebook::ProcessManager)>] one `[domain_name,
551
+ # process_manager]` tuple per process manager any loaded bluebook declares
464
552
  def each_domain_process_manager(runtime)
465
553
  found = []
466
554
  runtime.registry.bluebooks.each do |domain_name, bluebook|
@@ -472,6 +560,11 @@ module Hecks
472
560
  # The live-side ground truth, key-shape-normalized (see `deep_
473
561
  # stringify_keys`'s own comment) so it compares fairly against a
474
562
  # real Heki round trip's own shallow-symbolize convention.
563
+ #
564
+ # @param persisted [Hash{String, Symbol => Hash}] `history[:saga_instances]`'s own
565
+ # per-process-manager entry, keyed by correlation, each holding `:state`/`:memory`
566
+ # @return [Hash{String => Hash}] the same rows, keyed by stringified correlation,
567
+ # with `:memory` recursively string-keyed
475
568
  def normalize_saga_rows(persisted)
476
569
  persisted.each_with_object({}) do |(correlation, saga), rows|
477
570
  rows[correlation.to_s] = { state: saga[:state], memory: deep_stringify_keys(saga[:memory]) }
@@ -482,8 +575,16 @@ module Hecks
482
575
  # `@store`/`@saga_store`, so `#each_saga` is forced back through
483
576
  # `read_snapshot`/`replay_journal`, real bytes off real disk, not
484
577
  # whatever the writer that just wrote them still holds in its own
485
- # process memory (the same reason `fold!`, above, opens a second
578
+ # process memory (the same reason `fold!`, below, opens a second
486
579
  # `Adapters::Heki` instance rather than reading its own writer back).
580
+ #
581
+ # @param anchor [Bluebook::Aggregate] any aggregate of the owning domain, used only
582
+ # to open a `Heki` store at the right domain/root
583
+ # @param tmp [String] the throwaway directory the saga store was written into
584
+ # @param domain_name [String] the owning domain's name, threaded into the store's
585
+ # own `settings`
586
+ # @return [Hash{String => Hash}] every persisted correlation's `:state`/`:memory`,
587
+ # with `:memory` recursively string-keyed
487
588
  def cold_read_saga_rows(anchor, tmp, domain_name)
488
589
  reader = guarded_heki(aggregate: anchor, root: tmp, settings: { domain: domain_name })
489
590
  reader.each_saga.with_object({}) do |(_pm, correlation, state, memory, _completed), rows|
@@ -513,6 +614,10 @@ module Hecks
513
614
  # key, a changed value, a missing field) — exactly the kind (b)'s
514
615
  # own seeded fixture in `spec/fuzzing/self_consistency_saga_spec.rb`
515
616
  # proves this still catches.
617
+ #
618
+ # @param value [Object] any value; only Hash and Array are recursed into
619
+ # @return [Object] `value` with every Hash key (at every nesting level) replaced by
620
+ # its own `to_s`; anything else is returned unchanged
516
621
  def deep_stringify_keys(value)
517
622
  case value
518
623
  when Hash then value.each_with_object({}) { |(k, v), h| h[k.to_s] = deep_stringify_keys(v) }
@@ -540,6 +645,17 @@ module Hecks
540
645
  # "hand-rolled approximation" this check exists to avoid; `send` on
541
646
  # an interpreter sharing this same `runtime`'s own registry is the
542
647
  # real thing, not a copy of it.
648
+ #
649
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the still-live
650
+ # booted runtime this replay just dispatched through
651
+ # @param interpreter [Runtime::SagaInterpreter] the interpreter whose (private)
652
+ # `saga_correlation` re-derives this conversation's own correlation
653
+ # @param process_manager [Bluebook::ProcessManager] the process manager whose saga
654
+ # log is walked
655
+ # @param correlation [Object] the correlation identifier to match, as
656
+ # `saga_correlation` produces it — typically a String
657
+ # @return [Runtime::Event, nil] the real domain event this correlation's current
658
+ # checkpoint last advanced on, or nil if it was only ever `begin_saga`'d
543
659
  def last_advancing_event(runtime, interpreter, process_manager, correlation)
544
660
  entry = runtime.registry.saga_log.reverse_each.find do |row|
545
661
  row[:process_manager] == process_manager.name && row[:instance] == correlation &&
@@ -560,6 +676,21 @@ module Hecks
560
676
  # fix) belongs right where it guards `interpreter.advance`, not in
561
677
  # a helper a reader would have to jump to just to see what is and
562
678
  # isn't being restored around that one call.
679
+ #
680
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the still-live
681
+ # booted runtime this replay just dispatched through
682
+ # @param interpreter [Runtime::SagaInterpreter] the interpreter to redeliver through
683
+ # @param domain_name [String] the owning domain's name
684
+ # @param process_manager [Bluebook::ProcessManager] the process manager under test
685
+ # @param anchor [Bluebook::Aggregate] any aggregate of the owning domain, used only
686
+ # to open a `Heki` store at the right domain/root
687
+ # @param correlation [Object] the conversation's own correlation identifier
688
+ # @param saga [Hash] this correlation's persisted `:state`/`:memory`
689
+ # @param redelivery [Runtime::Event] the real domain event to redeliver
690
+ # @return [Hash, nil] a divergence Hash (`:field`, `:domain`, `:process_manager`,
691
+ # `:correlation`, `:on`, `:before`, `:after`) if the redelivery visibly changed
692
+ # state/memory; nil if it left the checkpoint unchanged, or if the cold-read
693
+ # checkpoint could not be found at all
563
694
  def check_one_saga_redelivery(runtime, interpreter, domain_name, process_manager, anchor,
564
695
  correlation, saga, redelivery)
565
696
  # rubocop:disable-next Metrics/BlockLength
@@ -621,6 +752,13 @@ module Hecks
621
752
  # second time" — `writer` already holds everything the first fold
622
753
  # wrote, so a second fold re-applies the identical operations on
623
754
  # top, and the two cold reads either agree (idempotent) or don't.
755
+ #
756
+ # @param writer [Adapters::Heki] the open store to append `entries` into
757
+ # @param tmp [String] the throwaway directory `writer` was opened against
758
+ # @param aggregate [Bluebook::Aggregate] the aggregate `entries` belongs to
759
+ # @param entries [Array<Ports::Persistence::Entry>] the journal entries to fold in
760
+ # @return [Hash{String => Object}] every stored record's id (stringified) mapped to
761
+ # its materialized state, read back cold
624
762
  def fold!(writer, tmp, aggregate, entries)
625
763
  entries.each do |entry|
626
764
  writer.append(entry)
@@ -655,6 +793,17 @@ module Hecks
655
793
  # answer — identity, not `Value#==`, is the right notion of
656
794
  # "already found" here (two different value objects that happen to
657
795
  # hold equal fields are still two separate round trips to prove).
796
+ #
797
+ # @param node [Object] the value to walk; a `Runtime::Value` is recorded and
798
+ # recursed into by field, a Hash or Array is recursed into by value/element,
799
+ # anything else is ignored
800
+ # @param found [Array<Array(Runtime::Value, Bluebook::Aggregate, nil)>] accumulator
801
+ # this method appends `[value, aggregate]` pairs to, in place
802
+ # @param seen [Hash] a `compare_by_identity` Hash used as a found-by-identity set,
803
+ # mutated in place
804
+ # @param aggregate [Bluebook::Aggregate, nil] the aggregate `node` belongs to, carried
805
+ # along unchanged for every value found beneath it
806
+ # @return [void]
658
807
  def walk_value_objects(node, found, seen, aggregate)
659
808
  case node
660
809
  when Runtime::Value
@@ -8,6 +8,8 @@ module Hecks
8
8
  # The argument shapes the Ruby/Rust divergences were actually found
9
9
  # through, injected on purpose, on every domain.
10
10
  #
11
+ # ## Why these shapes
12
+ #
11
13
  # Ten bugs in one QA session (BUG#7–#16, `QualityControl`'s own
12
14
  # ledger) clustered in a handful of mechanisms — and several were
13
15
  # only ever reachable because one domain happened to declare the
@@ -16,7 +18,7 @@ module Hecks
16
18
  # but-null `to:` was misread as a routing envelope (BUG#16), a blank
17
19
  # creating identity minted a phantom aggregate in Rust (BUG#15), a
18
20
  # single-field closed-set argument offered as bare `null` refused
19
- # with different KINDS on the two engines (BUG#14). No other domain
21
+ # with different kinds on the two engines (BUG#14). No other domain
20
22
  # in the rotation could ever have found any of those, because
21
23
  # nothing generated ever produced the shape. This module produces
22
24
  # them — for a configurable fraction of command steps, chosen and
@@ -24,15 +26,19 @@ module Hecks
24
26
  # already draws from, so `generate(domain, seed:, steps:,
25
27
  # adversarial:)` stays exactly as reproducible per seed as it was.
26
28
  #
27
- # **Opt-in, by construction**. `adversarial: 0.0` (the default) returns
28
- # before drawing a single random number, so every pinned seed in
29
- # spec/fuzzing, spec/rust_conformance_fuzz_spec.rb, bin/fuzz and
30
- # bin/generate produces byte-for-byte what it produced before this
31
- # module existed. `bin/qa_sweep` turns it on (reading
29
+ # ## Opt-in, by construction
30
+ #
31
+ # `adversarial: 0.0` (the default) returns before drawing a single
32
+ # random number, so every pinned seed in spec/fuzzing,
33
+ # spec/rust_conformance_fuzz_spec.rb, bin/fuzz and bin/generate
34
+ # produces byte-for-byte what it produced before this module
35
+ # existed. `bin/qa_sweep` turns it on (reading
32
36
  # `QualityControlDials::ADVERSARIAL_FRACTION`) — that script is the
33
37
  # one place a mutated step's own divergence is a finding rather than
34
38
  # a red CI gate.
35
39
  #
40
+ # ## When it applies
41
+ #
36
42
  # One mutation per step, applied in `StepBuilder#build_command_step`
37
43
  # after the arguments and identity are built and before the step's
38
44
  # own inline dispatch — so the generator's own `known_ids` tracking
@@ -122,7 +122,7 @@ module Hecks
122
122
  # shape, and this step still skips it exactly as it always
123
123
  # has. A list-of-value-object attribute (`ConsoleSettings::
124
124
  # Collection.ReplaceColumns`' own `columns`, `list_of(Column)`)
125
- # is the real, previously-unfuzzable case this now covers —
125
+ # is the real case this covers —
126
126
  # `sets :columns` imports the owner aggregate's own declared
127
127
  # `list_of` attribute onto the command verbatim (Command
128
128
  # Builder#resolve_bare_set!), so it is a required, ordinary
@@ -98,6 +98,26 @@ module Hecks
98
98
  # `favor:` names verbs the picker weights up. `nil`/`[]`, the
99
99
  # defaults, draw nothing extra and change nothing: every pinned seed
100
100
  # is byte-for-byte what it was.
101
+ #
102
+ # Generates one random-but-valid step sequence for `domain_path`, dispatching
103
+ # each step for real against a throwaway boot as it builds it.
104
+ #
105
+ # @param domain_path [String] path to the domain directory to boot
106
+ # @param seed [Integer] RNG seed; every draw this run makes is reproducible
107
+ # from it
108
+ # @param steps [Integer] number of generation attempts to make
109
+ # @param adapter [Symbol] persistence adapter to boot with (default `:memory`)
110
+ # @param adversarial [Float] fraction of command steps to mutate adversarially
111
+ # (default `0.0`, drawing nothing extra)
112
+ # @param role_draw [Float] fraction of gated commands to draw a caller for
113
+ # (default `0.0`, drawing nothing extra)
114
+ # @param dry_run [Float] fraction of command steps dispatched as dry runs
115
+ # (default `0.0`, drawing nothing extra)
116
+ # @param prefix [Hash, nil] another seed's own generation spec to replay
117
+ # first (`{"seed" =>, "steps" =>, "favor" =>, "prefix" =>}`), or `nil`
118
+ # @param favor [Array<String>, Array<Symbol>] verbs the picker weights up
119
+ # @return [Array<Hash>] the generated step list, each a command, query, or
120
+ # read-model step
101
121
  def self.generate(domain_path, seed:, steps:, **)
102
122
  new(domain_path, seed: seed, steps: steps, **).call
103
123
  end
@@ -108,6 +128,15 @@ module Hecks
108
128
  # never hit from one that does not exist.
109
129
  Trace = Struct.new(:steps, :coverage, :verbs, keyword_init: true)
110
130
 
131
+ # Generates one sequence exactly like `.generate`, but also returns the
132
+ # coverage and verb data a campaign needs, which `.generate` discards.
133
+ #
134
+ # @param domain_path [String] path to the domain directory to boot
135
+ # @param seed [Integer] RNG seed; every draw this run makes is reproducible
136
+ # from it
137
+ # @param steps [Integer] number of generation attempts to make
138
+ # @return [Hecks::Fuzzing::SequenceGenerator::Trace] the generated steps,
139
+ # the coverage tuples reached, and every verb the booted catalog offered
111
140
  def self.trace(domain_path, seed:, steps:, **)
112
141
  generator = new(domain_path, seed: seed, steps: steps, **)
113
142
  Trace.new(steps: generator.call, coverage: generator.coverage, verbs: generator.verbs)
@@ -130,6 +159,19 @@ module Hecks
130
159
  # replay one.
131
160
  attr_reader :event_count
132
161
 
162
+ # @param domain_path [String] path to the domain directory to boot
163
+ # @param seed [Integer] RNG seed; every draw this run makes is reproducible
164
+ # from it
165
+ # @param steps [Integer] number of generation attempts `#call` will make
166
+ # @param adapter [Symbol] persistence adapter to boot with
167
+ # @param adversarial [Float] fraction of command steps to mutate adversarially
168
+ # @param role_draw [Float] fraction of gated commands to draw a caller for
169
+ # @param dry_run [Float] fraction of command steps dispatched as dry runs
170
+ # @param prefix [Hash, nil] another seed's own generation spec to replay
171
+ # first, or `nil`
172
+ # @param favor [Array<String>, Array<Symbol>] verbs the picker weights up
173
+ # @raise [ArgumentError] if `adversarial`, `role_draw`, or `dry_run` is not a
174
+ # Numeric between 0.0 and 1.0
133
175
  def initialize(domain_path, seed:, steps:, adapter: :memory, adversarial: 0.0, role_draw: 0.0, dry_run: 0.0,
134
176
  prefix: nil, favor: [])
135
177
  { adversarial: adversarial, role_draw: role_draw, dry_run: dry_run }.each do |name, fraction|
@@ -164,6 +206,11 @@ module Hecks
164
206
  @attempt = 0
165
207
  end
166
208
 
209
+ # Runs the generation this instance was configured for, against a fresh,
210
+ # isolated boot of `@domain_path`.
211
+ #
212
+ # @return [Array<Hash>] the generated step list, each a command, query, or
213
+ # read-model step; a picker miss that produced no step is dropped
167
214
  def call
168
215
  # Real leftover data from ordinary use (bin/console, whatever) lives
169
216
  # under the example's data/ — a generator that boots against it