hecks 1.3.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 (373) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +72 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +83 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
  7. data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
  9. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  10. data/lib/hecks/adapters/driven/heki.rb +106 -9
  11. data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
  12. data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
  13. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  15. data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
  16. data/lib/hecks/adapters/driven/lambda.rb +82 -35
  17. data/lib/hecks/adapters/driven/local_storage.rb +83 -10
  18. data/lib/hecks/adapters/driven/memory.rb +205 -9
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  24. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  25. data/lib/hecks/adapters/driven/prism.rb +50 -11
  26. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  31. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  32. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  33. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  34. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  35. data/lib/hecks/adapters/driven.rb +6 -4
  36. data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
  37. data/lib/hecks/behaviors/dsl.rb +60 -2
  38. data/lib/hecks/behaviors/expectations.rb +190 -29
  39. data/lib/hecks/behaviors/ir.rb +12 -1
  40. data/lib/hecks/behaviors/rspec.rb +9 -1
  41. data/lib/hecks/behaviors/runner.rb +21 -2
  42. data/lib/hecks/behaviors.rb +9 -1
  43. data/lib/hecks/bluebook/aggregate.rb +43 -13
  44. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
  45. data/lib/hecks/bluebook/assembly/build.rb +26 -5
  46. data/lib/hecks/bluebook/assembly/contract.rb +98 -23
  47. data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
  48. data/lib/hecks/bluebook/assembly/marks.rb +159 -30
  49. data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
  50. data/lib/hecks/bluebook/assembly.rb +32 -14
  51. data/lib/hecks/bluebook/attribute.rb +26 -12
  52. data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
  53. data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
  54. data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
  55. data/lib/hecks/bluebook/behaviour/command.rb +55 -25
  56. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
  57. data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
  58. data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
  59. data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
  60. data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
  61. data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
  62. data/lib/hecks/bluebook/behaviour/query.rb +6 -1
  63. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
  64. data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
  65. data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
  66. data/lib/hecks/bluebook/capabilities.rb +27 -0
  67. data/lib/hecks/bluebook/chapter.rb +51 -9
  68. data/lib/hecks/bluebook/command.rb +62 -17
  69. data/lib/hecks/bluebook/domain_port.rb +34 -9
  70. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  71. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  72. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
  73. data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
  74. data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
  75. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
  76. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
  77. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  78. data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
  79. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  80. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  81. data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
  82. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  83. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
  84. data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
  85. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  86. data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
  87. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  88. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  89. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
  90. data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
  91. data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
  92. data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
  93. data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
  94. data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
  95. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  96. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  97. data/lib/hecks/bluebook/entity.rb +40 -11
  98. data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
  99. data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
  100. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
  101. data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
  102. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
  103. data/lib/hecks/bluebook/expression/resolver.rb +369 -128
  104. data/lib/hecks/bluebook/hexagon.rb +35 -1
  105. data/lib/hecks/bluebook/lifecycle.rb +12 -1
  106. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
  107. data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
  108. data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
  109. data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
  110. data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
  111. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
  112. data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
  113. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
  114. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
  115. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
  116. data/lib/hecks/bluebook/meta_validator.rb +235 -139
  117. data/lib/hecks/bluebook/model_check.rb +434 -104
  118. data/lib/hecks/bluebook/pattern_subset.rb +32 -10
  119. data/lib/hecks/bluebook/policy.rb +15 -13
  120. data/lib/hecks/bluebook/process_manager.rb +27 -14
  121. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  122. data/lib/hecks/bluebook/project_loader.rb +40 -0
  123. data/lib/hecks/bluebook/project_register.rb +50 -6
  124. data/lib/hecks/bluebook/query.rb +31 -4
  125. data/lib/hecks/bluebook/read_model.rb +35 -15
  126. data/lib/hecks/bluebook/reference.rb +26 -13
  127. data/lib/hecks/bluebook/smoke_test.rb +46 -23
  128. data/lib/hecks/bluebook/synthesizer.rb +46 -12
  129. data/lib/hecks/bluebook/translation.rb +34 -5
  130. data/lib/hecks/bluebook/value_object.rb +29 -11
  131. data/lib/hecks/bluebook.rb +5 -6
  132. data/lib/hecks/codemod.rb +138 -50
  133. data/lib/hecks/construct.rb +21 -7
  134. data/lib/hecks/corpus.rb +438 -0
  135. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  136. data/lib/hecks/doc/reference.rb +200 -31
  137. data/lib/hecks/embryonaut_bluebook.rb +38 -15
  138. data/lib/hecks/facade/cli_door.rb +69 -10
  139. data/lib/hecks/facade/cli_runner.rb +105 -24
  140. data/lib/hecks/facade/command_request.rb +23 -0
  141. data/lib/hecks/facade/handle.rb +155 -35
  142. data/lib/hecks/facade/json_door.rb +106 -25
  143. data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
  144. data/lib/hecks/facade/surface/chapter.rb +26 -17
  145. data/lib/hecks/facade/surface.rb +16 -3
  146. data/lib/hecks/facade.rb +15 -4
  147. data/lib/hecks/forms/app.rb +46 -30
  148. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  149. data/lib/hecks/forms/field_renderer.rb +142 -6
  150. data/lib/hecks/forms/field_shape.rb +183 -19
  151. data/lib/hecks/forms/html.rb +51 -7
  152. data/lib/hecks/forms/index_renderer.rb +14 -2
  153. data/lib/hecks/forms/page.rb +14 -0
  154. data/lib/hecks/forms/params.rb +120 -23
  155. data/lib/hecks/forms/port_argument.rb +14 -2
  156. data/lib/hecks/forms/query_form_renderer.rb +65 -2
  157. data/lib/hecks/forms/record_renderer.rb +60 -2
  158. data/lib/hecks/forms/record_table.rb +28 -1
  159. data/lib/hecks/forms/reference_options.rb +24 -0
  160. data/lib/hecks/forms/value_object_shape.rb +13 -3
  161. data/lib/hecks/forms.rb +24 -4
  162. data/lib/hecks/fqn.rb +59 -1
  163. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  164. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  165. data/lib/hecks/framework/oidc.json +15 -0
  166. data/lib/hecks/framework.rb +79 -25
  167. data/lib/hecks/freezer.rb +27 -11
  168. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
  169. data/lib/hecks/fuzzing/combination_miner.rb +178 -0
  170. data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
  171. data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
  172. data/lib/hecks/fuzzing/differential.rb +192 -0
  173. data/lib/hecks/fuzzing/domain_generator.rb +871 -0
  174. data/lib/hecks/fuzzing/era_boundary.rb +68 -19
  175. data/lib/hecks/fuzzing/form_census.rb +121 -20
  176. data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
  177. data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
  178. data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
  179. data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
  180. data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
  181. data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
  182. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
  183. data/lib/hecks/fuzzing/properties/guards.rb +86 -42
  184. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
  185. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
  186. data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
  187. data/lib/hecks/fuzzing/properties/querying.rb +82 -28
  188. data/lib/hecks/fuzzing/properties.rb +84 -51
  189. data/lib/hecks/fuzzing/qa_settings.rb +164 -0
  190. data/lib/hecks/fuzzing/replay.rb +241 -133
  191. data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
  192. data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
  193. data/lib/hecks/fuzzing/self_consistency.rb +315 -137
  194. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
  195. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  196. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  197. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  198. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
  199. data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
  200. data/lib/hecks/fuzzing/shrinker.rb +309 -0
  201. data/lib/hecks/fuzzing/structural_skips.rb +37 -130
  202. data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
  203. data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
  204. data/lib/hecks/fuzzing/value_generator.rb +110 -22
  205. data/lib/hecks/fuzzing.rb +5 -0
  206. data/lib/hecks/grammar/evolve.rb +188 -12
  207. data/lib/hecks/grammar.rb +53 -7
  208. data/lib/hecks/ir.rb +51 -20
  209. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  210. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  211. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  212. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  213. data/lib/hecks/language/oidc.json +5 -0
  214. data/lib/hecks/literal.rb +41 -9
  215. data/lib/hecks/naming.rb +112 -31
  216. data/lib/hecks/ports/access_control.rb +53 -2
  217. data/lib/hecks/ports/agent/answers.rb +83 -6
  218. data/lib/hecks/ports/agent.rb +119 -35
  219. data/lib/hecks/ports/authentication.rb +44 -4
  220. data/lib/hecks/ports/authorization.rb +50 -11
  221. data/lib/hecks/ports/clock.rb +42 -23
  222. data/lib/hecks/ports/extraction.rb +16 -0
  223. data/lib/hecks/ports/identity_assignment.rb +23 -2
  224. data/lib/hecks/ports/identity_generation.rb +17 -3
  225. data/lib/hecks/ports/identity_resolution.rb +17 -1
  226. data/lib/hecks/ports/key_vault.port +6 -0
  227. data/lib/hecks/ports/key_vault.rb +58 -0
  228. data/lib/hecks/ports/loading.rb +4 -0
  229. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  230. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  231. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  232. data/lib/hecks/ports/persistence/execution.rb +4 -0
  233. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  234. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  235. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  236. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  237. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  238. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  239. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  250. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  251. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  252. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
  253. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  254. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  255. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  256. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  257. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  258. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
  259. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  260. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  261. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  262. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
  263. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
  264. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  265. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
  266. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  267. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  268. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  269. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  270. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  271. data/lib/hecks/ports/persistence.rb +36 -1
  272. data/lib/hecks/ports/projection.rb +61 -7
  273. data/lib/hecks/ports/query/in_memory.rb +41 -8
  274. data/lib/hecks/ports/query/ordering.rb +21 -6
  275. data/lib/hecks/ports/query.rb +35 -0
  276. data/lib/hecks/ports.rb +1 -0
  277. data/lib/hecks/projections/bootstrap_table.rb +147 -0
  278. data/lib/hecks/projections/diagrams.rb +315 -79
  279. data/lib/hecks/projections/glossary/html.rb +91 -3
  280. data/lib/hecks/projections/glossary/markdown.rb +50 -3
  281. data/lib/hecks/projections/glossary/mermaid.rb +49 -1
  282. data/lib/hecks/projections/glossary/sections.rb +1 -1
  283. data/lib/hecks/projections/glossary/sentences.rb +110 -5
  284. data/lib/hecks/projections/glossary.rb +173 -31
  285. data/lib/hecks/projections/ir.rb +1 -1
  286. data/lib/hecks/projections/model/deviations.rb +62 -17
  287. data/lib/hecks/projections/model.rb +75 -21
  288. data/lib/hecks/projections/oidc.rb +45 -8
  289. data/lib/hecks/projections/parser_table.rb +53 -12
  290. data/lib/hecks/projections/reference.rb +15 -3
  291. data/lib/hecks/projections/rust_vocabulary.rb +646 -0
  292. data/lib/hecks/projections/shape.rb +10 -2
  293. data/lib/hecks/projections/statements.rb +69 -22
  294. data/lib/hecks/projections/vocabulary.rb +26 -9
  295. data/lib/hecks/projections.rb +5 -3
  296. data/lib/hecks/projector/cli_projector.rb +245 -37
  297. data/lib/hecks/projector/docs_projector.rb +154 -28
  298. data/lib/hecks/projector/exporter.rb +104 -29
  299. data/lib/hecks/projector/ir_projector.rb +7 -1
  300. data/lib/hecks/projector/narrate_projector.rb +144 -23
  301. data/lib/hecks/projector/target.rb +42 -18
  302. data/lib/hecks/projector.rb +86 -18
  303. data/lib/hecks/query_ir.rb +94 -47
  304. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  305. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  306. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  307. data/lib/hecks/query_specification/common/null_policy.rb +61 -15
  308. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  309. data/lib/hecks/query_specification/common/options.rb +25 -0
  310. data/lib/hecks/query_specification/field_path.rb +69 -15
  311. data/lib/hecks/query_specification/hop_path.rb +57 -20
  312. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  313. data/lib/hecks/rendering.rb +9 -3
  314. data/lib/hecks/router/namespace_installer.rb +16 -3
  315. data/lib/hecks/router.rb +56 -1
  316. data/lib/hecks/runtime/aggregate_lock.rb +20 -11
  317. data/lib/hecks/runtime/boot_gates.rb +21 -3
  318. data/lib/hecks/runtime/caller.rb +40 -8
  319. data/lib/hecks/runtime/capability_graph.rb +13 -2
  320. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
  321. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
  322. data/lib/hecks/runtime/command_interpreter.rb +195 -113
  323. data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
  324. data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
  325. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  326. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  327. data/lib/hecks/runtime/command_rules/references.rb +145 -55
  328. data/lib/hecks/runtime/command_rules.rb +3 -0
  329. data/lib/hecks/runtime/dependency_planning.rb +56 -11
  330. data/lib/hecks/runtime/dispatcher.rb +263 -92
  331. data/lib/hecks/runtime/entity_element.rb +304 -67
  332. data/lib/hecks/runtime/entity_interpreter.rb +149 -88
  333. data/lib/hecks/runtime/errors.rb +37 -23
  334. data/lib/hecks/runtime/event.rb +15 -10
  335. data/lib/hecks/runtime/identity.rb +91 -23
  336. data/lib/hecks/runtime/instance.rb +103 -18
  337. data/lib/hecks/runtime/interpreting.rb +24 -16
  338. data/lib/hecks/runtime/invocation.rb +358 -0
  339. data/lib/hecks/runtime/loader.rb +105 -19
  340. data/lib/hecks/runtime/outbox.rb +164 -26
  341. data/lib/hecks/runtime/policy_interpreter.rb +73 -60
  342. data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
  343. data/lib/hecks/runtime/query_interpreter.rb +93 -74
  344. data/lib/hecks/runtime/reaction_invocation.rb +73 -28
  345. data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
  346. data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
  347. data/lib/hecks/runtime/reference_hop.rb +48 -6
  348. data/lib/hecks/runtime/refusal_wording.rb +142 -115
  349. data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
  350. data/lib/hecks/runtime/registry/verification.rb +153 -28
  351. data/lib/hecks/runtime/registry.rb +202 -32
  352. data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
  353. data/lib/hecks/runtime/routing.rb +36 -154
  354. data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
  355. data/lib/hecks/runtime/saga_interpreter.rb +90 -76
  356. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  357. data/lib/hecks/runtime/tenant_check.rb +33 -13
  358. data/lib/hecks/runtime/tenant_scope.rb +23 -5
  359. data/lib/hecks/runtime/value/admission.rb +75 -30
  360. data/lib/hecks/runtime/value/coercion.rb +555 -142
  361. data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
  362. data/lib/hecks/runtime/value.rb +71 -21
  363. data/lib/hecks/runtime.rb +39 -7
  364. data/lib/hecks/storehouse.rb +368 -72
  365. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  366. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  367. data/lib/hecks/version.rb +3 -3
  368. data/lib/hecks/vocabulary.rb +205 -4
  369. data/lib/hecks.rb +91 -11
  370. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
  371. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
  372. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
  373. metadata +28 -2
@@ -20,17 +20,17 @@ module Hecks
20
20
  { "query" => entry[:verb], "args" => args }
21
21
  end
22
22
 
23
- # A REPORT ASK — the bare domain form `entry[:verb]` already carries
23
+ # A report ask — the bare domain form `entry[:verb]` already carries
24
24
  # ("Domain.report_name", no "::"), so `Dispatcher#query` routes it
25
25
  # to the read model rather than an aggregate query. A `ReadModel`
26
26
  # has no declared `.attributes` the way a `Query` does — its own
27
- # argument surface is exactly ONE key, `reference_name`, and ONLY
27
+ # argument surface is exactly one key, `reference_name`, and only
28
28
  # for a rooted model (`read_model_actionable?` already gated a
29
29
  # rootless one straight into eligibility with nothing to supply).
30
- # A BARE scalar, not `identity_shaped` — `ReadModelInterpreter#
31
- # refuse_object_reference` explicitly REJECTS a Hash/Value offered
30
+ # A bare scalar, not `identity_shaped` — `ReadModelInterpreter#
31
+ # refuse_object_reference` explicitly rejects a Hash/Value offered
32
32
  # here (this is the one place in the whole generator where the
33
- # subject's own identity must NOT be wrapped the way a command
33
+ # subject's own identity must not be wrapped the way a command
34
34
  # argument's would be).
35
35
  def build_read_model_step(runtime, entry)
36
36
  model = entry[:model]
@@ -40,20 +40,20 @@ module Hecks
40
40
  { "query" => entry[:verb], "args" => args }
41
41
  end
42
42
 
43
- # THE ADVERSARIAL LAYER SITS HERE, AND ONLY HERE — after the step's
44
- # arguments and identity are fully built, BEFORE the one real
43
+ # The adversarial layer sits here, and only here — after the step's
44
+ # arguments and identity are fully built, before the one real
45
45
  # dispatch this generator makes to learn what the step did, and
46
46
  # before the step is returned as corpus data. That ordering is
47
47
  # the whole contract: the mutated `args` are what this generator's
48
48
  # own inline dispatch sees (so `known_ids` tracking reflects what
49
- # actually happened), AND they are the bytes `Fuzzing::Replay`
49
+ # actually happened), and they are the bytes `Fuzzing::Replay`
50
50
  # later hands Ruby's runtime and the bytes `JSON.generate({steps:
51
51
  # ...})` hands the compiled Rust binary — one step, one payload,
52
52
  # both engines. Nothing downstream of this method can tell a
53
53
  # mutated step from an ordinary one except by reading the
54
54
  # `"adversarial"` metadata it carries (see adversary.rb).
55
55
  #
56
- # TWO MORE DRAWS SIT RIGHT AFTER THE MUTATION, IN A FIXED ORDER —
56
+ # Two more draws sit right after the mutation, in a fixed order —
57
57
  # a caller (`caller_draw!`, adversary.rb: `role:`/`actor_id:` on
58
58
  # the step, bound around this one dispatch exactly the way
59
59
  # `Fuzzing::Replay` and `kernel/cli.rs` will later bind it) and
@@ -70,13 +70,14 @@ module Hecks
70
70
  mutations = adversarial_mutations!(args, entry, catalog)
71
71
  caller, caller_note = caller_draw!(entry, catalog)
72
72
  mutations << caller_note if caller_note
73
+ @state_before = state_before(runtime, entry, args)
73
74
 
74
75
  step =
75
76
  if dry_run_draw?
76
77
  safe_call { as_caller(caller) { runtime.dry_run?(entry[:verb], **symbolize(args)) } }
77
78
  { "dry_run" => entry[:verb], "args" => args }
78
79
  else
79
- outcome = safe_call { as_caller(caller) { runtime.dispatch(entry[:verb], **symbolize(args)) } }
80
+ outcome = safe_call { as_caller(caller) { runtime.dispatch_flat(entry[:verb], symbolize(args)) } }
80
81
  if outcome
81
82
  record_outcome(catalog, entry, args)
82
83
  @event_count += outcome.events.length
@@ -93,7 +94,7 @@ module Hecks
93
94
  def dry_run_draw? = @dry_run.positive? && @random.rand < @dry_run
94
95
 
95
96
  # `Hecks.as_caller` for exactly this block, or a bare yield — the
96
- # SAME binding `Fuzzing::Replay` makes from the step's own keys
97
+ # same binding `Fuzzing::Replay` makes from the step's own keys
97
98
  # later, so the generator's inline dispatch and both replays see
98
99
  # one caller.
99
100
  def as_caller(caller, &)
@@ -104,7 +105,7 @@ module Hecks
104
105
 
105
106
  def args_for(attributes, aggregate)
106
107
  args = attributes.each_with_object({}) do |attribute, built|
107
- # AN OPTIONAL ARGUMENT IS SOMETIMES NOT GIVEN, and that is an
108
+ # An optional argument is sometimes not given, and that is an
108
109
  # ordinary payload rather than a damaged one — see
109
110
  # OPTIONAL_OMITTED_PROBABILITY for why this cannot live in
110
111
  # `malform` below and what it was costing while it did not
@@ -113,15 +114,15 @@ module Hecks
113
114
 
114
115
  if attribute.list?
115
116
  value = list_value_for(attribute, aggregate)
116
- # A list-of-ENTITY command attribute has no real example
117
+ # A list-of-entity command attribute has no real example
117
118
  # anywhere in this repo's domains — every entity-owned list is
118
119
  # populated via a per-element append command instead, never a
119
120
  # whole-list command argument — so `list_value_for` (below)
120
121
  # answers `nil` for one rather than guessing at an entity's own
121
122
  # shape, and this step still skips it exactly as it always
122
- # has. A list-of-VALUE-OBJECT attribute (`ConsoleSettings::
123
+ # has. A list-of-value-object attribute (`ConsoleSettings::
123
124
  # Collection.ReplaceColumns`' own `columns`, `list_of(Column)`)
124
- # is the real, previously-unfuzzable case this now covers —
125
+ # is the real case this covers —
125
126
  # `sets :columns` imports the owner aggregate's own declared
126
127
  # `list_of` attribute onto the command verbatim (Command
127
128
  # Builder#resolve_bare_set!), so it is a required, ordinary
@@ -137,9 +138,9 @@ module Hecks
137
138
  malform(args, attributes, aggregate)
138
139
  end
139
140
 
140
- # A `list_of` ATTRIBUTE'S OWN VALUE — an array of independently
141
+ # A `list_of` attribute's own value — an array of independently
141
142
  # generated elements, each shaped exactly the way a bare (non-list)
142
- # attribute of the SAME declared element type already is
143
+ # attribute of the same declared element type already is
143
144
  # (`ValueGenerator.value_for`), since `list_of(X)`'s own element
144
145
  # coercion is `X`'s ordinary shape repeated, not a different one
145
146
  # (`Attribute#type` is already unwrapped from `list_of(...)` at
@@ -157,12 +158,12 @@ module Hecks
157
158
  Array.new(@random.rand(0..3)) { ValueGenerator.value_for(attribute, aggregate, random: @random, known_ids: @known_ids) }
158
159
  end
159
160
 
160
- # ONE MALFORMATION AT A TIME, and usually none. A step whose payload is
161
+ # One malformation at a time, and usually none. A step whose payload is
161
162
  # wrong in three ways only ever proves which check runs first ; wrong in
162
163
  # exactly one way names the check that fired. And the rate stays low on
163
164
  # purpose — a corrupted step is almost always refused, a sequence of
164
165
  # refusals reaches no state at all, and bin/fuzz already counts those as
165
- # SILENT rather than scoring them.
166
+ # silent rather than scoring them.
166
167
  def malform(args, attributes, aggregate)
167
168
  return args if args.empty? || @random.rand >= MALFORMED_ARGUMENT_PROBABILITY
168
169
 
@@ -173,12 +174,12 @@ module Hecks
173
174
  end
174
175
  end
175
176
 
176
- # NEVER THE IDENTITY. A creating command with no id auto-mints one, and
177
+ # Never the identity. A creating command with no id auto-mints one, and
177
178
  # a minted id is deliberately unreproducible — a random hex, never a
178
179
  # guessable counter — so dropping it manufactures a step whose outcome
179
180
  # cannot be replayed and says nothing about the runtime's behaviour.
180
181
  # Every step in the hand-written corpus
181
- # supplies an id for the same reason. Whether an auto-minted id OUGHT to
182
+ # supplies an id for the same reason. Whether an auto-minted id ought to
182
183
  # be reproducible is a real question, but it is not one a payload fuzzer
183
184
  # can ask.
184
185
  def drop_one(args, aggregate)
@@ -204,11 +205,11 @@ module Hecks
204
205
  if entry[:entity]
205
206
  parent_scalar = pick_known(aggregate.hecks_name)
206
207
  args[parent_key] = identity_shaped(aggregate, aggregate.identified_by, parent_scalar, aggregate)
207
- # ONE IDENTITY PER HOP, each drawn from the pool its OWN
208
+ # One identity per hop, each drawn from the pool its own
208
209
  # parent-plus-hops landed elements in (`entity_pool_key`) —
209
210
  # a depth-1 chain draws exactly what it always did; a
210
211
  # `Board.Card` chain draws a Board under this Workspace, then
211
- # a Card under THAT Board. Flat args, one head per hop, is
212
+ # a Card under that Board. Flat args, one head per hop, is
212
213
  # the legacy addressing `EntityElement#locate_chain` reads
213
214
  # (`args[head]` per identity path); the routed `to: {
214
215
  # aggregate:, entities: [...] }` spelling is an adversarial
@@ -222,19 +223,19 @@ module Hecks
222
223
  scalars << scalar
223
224
  end
224
225
  elsif entry[:command].creates?
225
- # A COMPOSITE IDENTITY (`identified_by` answering nil with MORE
226
- # THAN ONE declared path — Behaviour::Identified's own "a
226
+ # A composite identity (`identified_by` answering nil with more
227
+ # than one declared path — Behaviour::Identified's own "a
227
228
  # composite has no single head" comment) supplies every one of
228
229
  # its parts as its own ordinary, individually-declared command
229
230
  # attribute already — `RoleAssignment::Assign` takes actor_id/
230
231
  # role_name/starts_at directly, `args_for` (above) already
231
232
  # generated all three. Forcing a synthetic top-level `id` here
232
- # too — this codebase's own fallback for the SINGLE-key and the
233
+ # too — this codebase's own fallback for the single-key and the
233
234
  # genuinely untyped (`identity_paths.empty?`, no `identified_by`
234
235
  # declared at all) cases — hands a composite creating command
235
236
  # an argument it never declared at all, refused every time as
236
237
  # unknown before this check existed (a creating `Assign`/`Grant`
237
- # step was never anything BUT refused). `identity_paths.empty?`
238
+ # step was never anything but refused). `identity_paths.empty?`
238
239
  # is the untyped default (falls all the way back to a minted
239
240
  # `:id` the runtime itself never declared as an attribute
240
241
  # either), which still needs exactly the old minting behavior.
@@ -248,9 +249,9 @@ module Hecks
248
249
  end
249
250
  end
250
251
 
251
- # TRUE ONLY FOR A GENUINE MULTI-FIELD IDENTITY — `identified_by`
252
+ # True only for a genuine multi-field identity — `identified_by`
252
253
  # returns nil both for a real composite (`identity_paths.size > 1`)
253
- # and for the untyped default with NO identity declared at all
254
+ # and for the untyped default with no identity declared at all
254
255
  # (`identity_paths.size == 0`, Behaviour::Identified's own
255
256
  # `Array(@identified_by)` fallback) ; only the first of those two
256
257
  # has its own parts already sitting in `args` as real, individually-
@@ -294,7 +295,7 @@ module Hecks
294
295
 
295
296
  # A step the runtime declines is not a generator failure — it simply did
296
297
  # not take effect, so nothing is recorded and the sequence carries on. The
297
- # step still goes into the corpus, because a REFUSAL IS AN ANSWER, and
298
+ # step still goes into the corpus, because a refusal is an answer, and
298
299
  # its wording is pinned by the corpus.
299
300
  #
300
301
  # EvaluationError sits alongside the declared refusals deliberately : a
@@ -305,10 +306,36 @@ module Hecks
305
306
  # generator built a step that breaks the interpreter for reasons that have
306
307
  # nothing to do with the domain declining a payload.
307
308
  def safe_call
308
- yield
309
- rescue *Hecks::Runtime::DOMAIN_REFUSALS, Hecks::Bluebook::Expression::EvaluationError
309
+ result = yield
310
+ @last_outcome = "ok"
311
+ result
312
+ rescue *Hecks::Runtime::DOMAIN_REFUSALS, Hecks::Bluebook::Expression::EvaluationError => e
313
+ @last_outcome = e.class.name.split("::").last
310
314
  nil
311
315
  end
316
+
317
+ # Where the addressed aggregate stood just before this dispatch — its
318
+ # lifecycle value, or `exists`/`absent` when it declares none. Read
319
+ # straight off the repository, never through dispatch, and draws
320
+ # nothing from the RNG, so it changes no generated byte. An identity
321
+ # an adversarial mutation mangled past resolving is `?`.
322
+ def state_before(runtime, entry, args)
323
+ aggregate = entry[:aggregate]
324
+ symbolic = symbolize(args)
325
+ id = Runtime::Identity.of(aggregate, symbolic) || Runtime::Identity.from(aggregate, symbolic, :id)
326
+ return "absent" unless id
327
+
328
+ record = runtime.registry.repository(entry[:verb].split("::").first, aggregate).find(id)
329
+ return "absent" unless record
330
+
331
+ lifecycle = aggregate.lifecycle
332
+ return "exists" unless lifecycle
333
+
334
+ key = record.state.key?(lifecycle.field) ? lifecycle.field : lifecycle.field.to_s
335
+ record.state[key].to_s
336
+ rescue StandardError
337
+ "?"
338
+ end
312
339
  end
313
340
  end
314
341
  end
@@ -15,13 +15,13 @@ module Hecks
15
15
  # `{name, note, steps}` shape `spec/corpus/*.json` already uses — so a
16
16
  # generated sequence replays as a corpus member, completely unchanged.
17
17
  # Boots a throwaway copy of
18
- # the domain and DISPATCHES each candidate step for real as it builds the
18
+ # the domain and dispatches each candidate step for real as it builds the
19
19
  # sequence (not just synthesizing plausible-looking JSON) : the only way to
20
20
  # know whether a step actually reached a new state, or which id an
21
21
  # auto-minted entity landed on, is to run it and watch what happened.
22
22
  #
23
23
  # Single-call fuzzing mostly misses the bugs this project has actually
24
- # found — they needed STATE first (a saga leg acting on a transfer that
24
+ # found — they needed state first (a saga leg acting on a transfer that
25
25
  # already exists, a reference pointing at a customer already registered).
26
26
  # So this tracks what it has created as it goes, the same way
27
27
  # spec/banking_state_machine_spec.rb's hand-written generator does, and
@@ -47,18 +47,18 @@ module Hecks
47
47
  CREATING_WEIGHT = 2
48
48
 
49
49
  # How often a payload is deliberately the wrong shape. Low, because a
50
- # refused step reaches no new state and a sequence of them is SILENT.
50
+ # refused step reaches no new state and a sequence of them is silent.
51
51
  MALFORMED_ARGUMENT_PROBABILITY = 0.12
52
52
 
53
- # HOW OFTEN AN OPTIONAL ARGUMENT IS SIMPLY NOT GIVEN — a fair coin,
53
+ # How often an optional argument is simply not given — a fair coin,
54
54
  # because that is exactly what `optional:` means: present or absent,
55
55
  # both legal, neither the interesting case.
56
56
  #
57
- # This is NOT a malformation and must not be filed as one. `malform`
58
- # drops an argument too, but a dropped REQUIRED argument is refused,
59
- # a refusal writes no record, and bin/fuzz counts it SILENT — so the
57
+ # This is not a malformation and must not be filed as one. `malform`
58
+ # drops an argument too, but a dropped required argument is refused,
59
+ # a refusal writes no record, and bin/fuzz counts it silent — so the
60
60
  # one outcome worth reaching (a stored record carrying a null, then
61
- # QUERIED) was unreachable from that path by construction. Until
61
+ # queried) was unreachable from that path by construction. Until
62
62
  # this existed no generated history contained a null at all, which
63
63
  # meant `query_answers_match_reference` — the differential that
64
64
  # diffs every native adapter against the reference interpreter — had
@@ -72,11 +72,11 @@ module Hecks
72
72
  # How strongly an unexercised verb is preferred over one this sequence has
73
73
  # already dispatched. Random picking revisits the same handful of verbs and
74
74
  # leaves whole commands untouched for a whole run — which is the same
75
- # "reached no interesting state" problem the SILENT count reports, seen from
75
+ # "reached no interesting state" problem the silent count reports, seen from
76
76
  # the generating end rather than the scoring end.
77
77
  UNEXERCISED_WEIGHT = 4
78
78
 
79
- # `adversarial:` — the fraction of generated COMMAND steps (0.0..1.0)
79
+ # `adversarial:` — the fraction of generated command steps (0.0..1.0)
80
80
  # that get one deliberately adversarial argument mutation
81
81
  # (adversary.rb — the shapes BUG#7–#16 were found through). `0.0`,
82
82
  # the default, draws nothing extra from the seeded RNG, so a seed's
@@ -89,22 +89,91 @@ module Hecks
89
89
  # `dry_run_draw?`): `0.0` draws nothing, so every pinned seed is
90
90
  # byte-for-byte what it was; `bin/qa_sweep` reads them from
91
91
  # `QualityControlDials::ROLE_DRAW_PROBABILITY`/`DRY_RUN_FRACTION`.
92
- def self.generate(domain_path, seed:, steps:, adapter: :memory, adversarial: 0.0, role_draw: 0.0, dry_run: 0.0)
93
- new(domain_path, seed: seed, steps: steps, adapter: adapter, adversarial: adversarial,
94
- role_draw: role_draw, dry_run: dry_run).call
92
+ #
93
+ # `prefix:` / `favor:` — the two levers `CoverageCampaign` pulls
94
+ # (coverage_campaign.rb has the why). `prefix:` is `{ "seed", "steps",
95
+ # "favor", "prefix" }`: re-generate that seed's first `steps` attempts
96
+ # (itself recursively prefixed) before this seed's own randomness
97
+ # starts, so a seed can begin from state an earlier seed reached.
98
+ # `favor:` names verbs the picker weights up. `nil`/`[]`, the
99
+ # defaults, draw nothing extra and change nothing: every pinned seed
100
+ # is byte-for-byte what it was.
101
+ #
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
121
+ def self.generate(domain_path, seed:, steps:, **)
122
+ new(domain_path, seed: seed, steps: steps, **).call
123
+ end
124
+
125
+ # The same generation, with what it reached — `coverage` is
126
+ # `[[attempt_index, tuple], ...]` (`coverage_tuple`), `verbs` every
127
+ # verb the booted catalog offered, so a campaign can tell a verb it
128
+ # never hit from one that does not exist.
129
+ Trace = Struct.new(:steps, :coverage, :verbs, keyword_init: true)
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
140
+ def self.trace(domain_path, seed:, steps:, **)
141
+ generator = new(domain_path, seed: seed, steps: steps, **)
142
+ Trace.new(steps: generator.call, coverage: generator.coverage, verbs: generator.verbs)
95
143
  end
96
144
 
97
- # How many EVENTS the generated sequence actually produced — not
145
+ # How strongly a `favor:` verb is preferred when it is eligible —
146
+ # the same order of magnitude as an unexercised verb, so favor
147
+ # steers without drowning out what this sequence has not touched.
148
+ FAVOR_WEIGHT = 4
149
+
150
+ attr_reader :coverage, :verbs
151
+
152
+ # How many events the generated sequence actually produced — not
98
153
  # steps, not successful dispatches, but the sum of every Result#events
99
154
  # length across the run. This is the count bin/fuzz declares as the
100
155
  # script's own `expectations.events` claim: whatever was achieved
101
- # DURING generation becomes the claim a fresh replay of the same
156
+ # during generation becomes the claim a fresh replay of the same
102
157
  # script is held to. Zero means the sequence never
103
158
  # reached an interesting state — a fuzzer-effectiveness fact, not a
104
159
  # replay one.
105
160
  attr_reader :event_count
106
161
 
107
- def initialize(domain_path, seed:, steps:, adapter: :memory, adversarial: 0.0, role_draw: 0.0, dry_run: 0.0)
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
175
+ def initialize(domain_path, seed:, steps:, adapter: :memory, adversarial: 0.0, role_draw: 0.0, dry_run: 0.0,
176
+ prefix: nil, favor: [])
108
177
  { adversarial: adversarial, role_draw: role_draw, dry_run: dry_run }.each do |name, fraction|
109
178
  next if fraction.is_a?(Numeric) && fraction.between?(0, 1)
110
179
 
@@ -122,41 +191,107 @@ module Hecks
122
191
  @known_ids = Hash.new { |h, k| h[k] = [] }
123
192
  @entity_known_ids = Hash.new { |h, k| h[k] = [] }
124
193
  @appended_identities = Hash.new { |h, k| h[k] = [] }
125
- # ROLE => [actor ids] this sequence's own successful
194
+ # Role => [actor ids] this sequence's own successful
126
195
  # `Governance::RoleAssignment.Assign` steps granted — what the
127
196
  # `actor_known` caller shape draws from (adversary.rb).
128
197
  @granted = Hash.new { |h, k| h[k] = [] }
129
198
  @precedence_caller = nil
130
199
  @exercised = Set.new
131
200
  @event_count = 0
201
+ @prefix = prefix
202
+ @favor = Array(favor)
203
+ @own_favor = @favor
204
+ @coverage = []
205
+ @verbs = []
206
+ @attempt = 0
132
207
  end
133
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
134
214
  def call
135
215
  # Real leftover data from ordinary use (bin/console, whatever) lives
136
216
  # under the example's data/ — a generator that boots against it
137
217
  # starts from state its own known_ids tracking doesn't know about.
138
- # IsolatedBoot resets that AND rebinds persistence to Memory, since
218
+ # IsolatedBoot resets that and rebinds persistence to Memory, since
139
219
  # a Postgres-bound domain's real store lives outside the copied
140
220
  # directory entirely and `rm_rf`ing data/ alone cannot reach it —
141
221
  # see isolated_boot.rb's own header.
142
222
  IsolatedBoot.call(@domain_path, adapter: @adapter) do |copy|
143
223
  runtime = Hecks.boot(copy)
144
224
  catalog = build_catalog(runtime)
145
- Array.new(@step_count) { attempt_step(runtime, catalog) }.compact
225
+ @verbs = catalog.values_at(:creating, :instance, :entity_commands, :queries, :entity_queries, :read_models)
226
+ .flatten.map { |entry| entry[:verb] }.uniq
227
+
228
+ steps = []
229
+ if @prefix
230
+ realize_prefix(runtime, catalog, @prefix, prefix_limit(@prefix), steps)
231
+ @random = Random.new(@seed)
232
+ @favor = @own_favor
233
+ end
234
+ @step_count.times { steps << attempt_step(runtime, catalog) }
235
+ steps.compact
146
236
  end
147
237
  end
148
238
 
149
239
  private
150
240
 
241
+ # A prefix is the first `limit` attempts of another seed's generation,
242
+ # re-run for real: its own nested prefix first (capped the same way it
243
+ # was capped when that seed was generated), then that seed's own
244
+ # `Random.new(seed)` and favor for the rest. Same inputs, same
245
+ # catalog, same draws — the same steps, and the same known ids and
246
+ # exercised verbs carried forward into this seed.
247
+ #
248
+ # The prefix is on top of this seed's own budget, not out of it. A
249
+ # spliced seed still makes all `steps` attempts of its own after the
250
+ # prefix; a prefix is capped at `steps` attempts, so a spliced
251
+ # sequence is at most twice as long as an unspliced one. Taking the
252
+ # prefix out of the budget (the first version of this) left a spliced
253
+ # seed replaying state already seen with almost nothing left to
254
+ # explore from it — measured: fewer distinct tuples than unguided.
255
+ def realize_prefix(runtime, catalog, spec, limit, steps)
256
+ return 0 unless limit.positive?
257
+
258
+ inner = spec["prefix"]
259
+ used = inner ? realize_prefix(runtime, catalog, inner, [prefix_limit(inner), limit].min, steps) : 0
260
+ @random = Random.new(Integer(spec.fetch("seed")))
261
+ @favor = Array(spec["favor"])
262
+ (limit - used).times { steps << attempt_step(runtime, catalog) }
263
+ limit
264
+ end
265
+
266
+ def prefix_limit(spec) = Integer(spec.fetch("steps")).clamp(0, @step_count)
267
+
151
268
  def attempt_step(runtime, catalog)
269
+ index = @attempt
270
+ @attempt += 1
152
271
  entry = pick(catalog)
153
272
  return nil unless entry
154
273
 
155
274
  @exercised << entry[:verb]
156
- if entry[:query] then build_query_step(runtime, entry)
157
- elsif entry[:model] then build_read_model_step(runtime, entry)
158
- else build_command_step(runtime, catalog, entry)
159
- end
275
+ @state_before = "-"
276
+ step =
277
+ if entry[:query] then build_query_step(runtime, entry)
278
+ elsif entry[:model] then build_read_model_step(runtime, entry)
279
+ else build_command_step(runtime, catalog, entry)
280
+ end
281
+ @coverage << [index, coverage_tuple(entry, step)]
282
+ step
283
+ end
284
+
285
+ # `verb | kind | state before | mutation | outcome` — see
286
+ # CoverageCampaign's header for why the unit is this and not the
287
+ # verb. `state` is the addressed aggregate's lifecycle value (or
288
+ # `exists`/`absent` for one without a lifecycle) read just before
289
+ # dispatch; `mutation` names every adversarial mutation and its shape;
290
+ # `outcome` is `ok` or the refusal class `safe_call` rescued.
291
+ def coverage_tuple(entry, step)
292
+ kind = %w[verb query dry_run].find { |key| step.key?(key) }
293
+ mutation = Array(step["adversarial"]).map { |m| [m["mutation"], m["shape"]].compact.join(":") }.join("+")
294
+ [entry[:verb], kind, @state_before, mutation.empty? ? "-" : mutation, @last_outcome].join(" | ")
160
295
  end
161
296
  end
162
297
  end