hecks 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +7 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +10 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
  7. data/lib/hecks/adapters/driven/heki/journal.rb +60 -2
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
  9. data/lib/hecks/adapters/driven/heki.rb +13 -7
  10. data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
  12. data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
  13. data/lib/hecks/adapters/driven/lambda.rb +39 -33
  14. data/lib/hecks/adapters/driven/local_storage.rb +17 -10
  15. data/lib/hecks/adapters/driven/memory.rb +205 -9
  16. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
  17. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  18. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  19. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  20. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  21. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  22. data/lib/hecks/adapters/driven/postgres_era.adapter +5 -0
  23. data/lib/hecks/adapters/driven/prism.rb +4 -4
  24. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  25. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  26. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  27. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  28. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  29. data/lib/hecks/adapters/driven.rb +4 -4
  30. data/lib/hecks/adapters/driving/github_webhook.rb +145 -0
  31. data/lib/hecks/behaviors/dsl.rb +2 -2
  32. data/lib/hecks/behaviors/expectations.rb +51 -23
  33. data/lib/hecks/behaviors/ir.rb +1 -1
  34. data/lib/hecks/behaviors/rspec.rb +1 -1
  35. data/lib/hecks/behaviors/runner.rb +2 -2
  36. data/lib/hecks/behaviors.rb +1 -1
  37. data/lib/hecks/bluebook/aggregate.rb +13 -13
  38. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
  39. data/lib/hecks/bluebook/assembly/build.rb +1 -1
  40. data/lib/hecks/bluebook/assembly/contract.rb +39 -16
  41. data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
  42. data/lib/hecks/bluebook/assembly/marks.rb +18 -18
  43. data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
  44. data/lib/hecks/bluebook/assembly.rb +9 -9
  45. data/lib/hecks/bluebook/attribute.rb +9 -9
  46. data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
  47. data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
  48. data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
  49. data/lib/hecks/bluebook/behaviour/command.rb +23 -23
  50. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -3
  51. data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
  52. data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
  53. data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
  54. data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
  55. data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
  56. data/lib/hecks/bluebook/behaviour/query.rb +1 -1
  57. data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
  58. data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
  59. data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
  60. data/lib/hecks/bluebook/capabilities.rb +27 -0
  61. data/lib/hecks/bluebook/chapter.rb +28 -9
  62. data/lib/hecks/bluebook/command.rb +12 -12
  63. data/lib/hecks/bluebook/domain_port.rb +9 -9
  64. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  65. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  66. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
  67. data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
  68. data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
  69. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
  70. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
  71. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  72. data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
  73. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  74. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  75. data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
  76. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  77. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
  78. data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
  79. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  80. data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
  81. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  82. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  83. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
  84. data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
  85. data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
  86. data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
  87. data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
  88. data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
  89. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  90. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  91. data/lib/hecks/bluebook/entity.rb +11 -11
  92. data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
  93. data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
  94. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
  95. data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
  96. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
  97. data/lib/hecks/bluebook/expression/resolver.rb +60 -62
  98. data/lib/hecks/bluebook/hexagon.rb +1 -1
  99. data/lib/hecks/bluebook/lifecycle.rb +1 -1
  100. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
  101. data/lib/hecks/bluebook/meta_validator/judge.rb +122 -86
  102. data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
  103. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
  104. data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
  105. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
  106. data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
  107. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
  108. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
  109. data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
  110. data/lib/hecks/bluebook/meta_validator.rb +70 -70
  111. data/lib/hecks/bluebook/model_check.rb +301 -84
  112. data/lib/hecks/bluebook/pattern_subset.rb +9 -9
  113. data/lib/hecks/bluebook/policy.rb +15 -13
  114. data/lib/hecks/bluebook/process_manager.rb +14 -14
  115. data/lib/hecks/bluebook/project_register.rb +6 -6
  116. data/lib/hecks/bluebook/query.rb +4 -4
  117. data/lib/hecks/bluebook/read_model.rb +14 -14
  118. data/lib/hecks/bluebook/reference.rb +8 -8
  119. data/lib/hecks/bluebook/smoke_test.rb +19 -19
  120. data/lib/hecks/bluebook/synthesizer.rb +12 -12
  121. data/lib/hecks/bluebook/translation.rb +4 -4
  122. data/lib/hecks/bluebook/value_object.rb +6 -6
  123. data/lib/hecks/bluebook.rb +2 -2
  124. data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
  125. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
  126. data/lib/hecks/codemod.rb +36 -35
  127. data/lib/hecks/construct.rb +6 -6
  128. data/lib/hecks/corpus.rb +317 -0
  129. data/lib/hecks/deprecation.rb +95 -0
  130. data/lib/hecks/doc/reference.rb +19 -19
  131. data/lib/hecks/embryonaut_bluebook.rb +11 -11
  132. data/lib/hecks/facade/cli_door.rb +69 -10
  133. data/lib/hecks/facade/cli_runner.rb +105 -24
  134. data/lib/hecks/facade/command_request.rb +23 -0
  135. data/lib/hecks/facade/handle.rb +79 -32
  136. data/lib/hecks/facade/json_door.rb +106 -25
  137. data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
  138. data/lib/hecks/facade/surface/chapter.rb +26 -17
  139. data/lib/hecks/facade/surface.rb +16 -3
  140. data/lib/hecks/facade.rb +15 -4
  141. data/lib/hecks/forms/app.rb +46 -30
  142. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  143. data/lib/hecks/forms/field_renderer.rb +142 -6
  144. data/lib/hecks/forms/field_shape.rb +183 -20
  145. data/lib/hecks/forms/html.rb +51 -7
  146. data/lib/hecks/forms/index_renderer.rb +14 -2
  147. data/lib/hecks/forms/params.rb +120 -23
  148. data/lib/hecks/forms/port_argument.rb +2 -2
  149. data/lib/hecks/forms/query_form_renderer.rb +2 -2
  150. data/lib/hecks/forms/record_renderer.rb +2 -2
  151. data/lib/hecks/forms/record_table.rb +1 -1
  152. data/lib/hecks/forms/value_object_shape.rb +3 -3
  153. data/lib/hecks/forms.rb +24 -4
  154. data/lib/hecks/fqn.rb +1 -1
  155. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  156. data/lib/hecks/framework.rb +48 -17
  157. data/lib/hecks/freezer.rb +11 -11
  158. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
  159. data/lib/hecks/fuzzing/combination_miner.rb +119 -0
  160. data/lib/hecks/fuzzing/concurrent_dispatch.rb +361 -0
  161. data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
  162. data/lib/hecks/fuzzing/differential.rb +158 -0
  163. data/lib/hecks/fuzzing/domain_generator.rb +694 -0
  164. data/lib/hecks/fuzzing/era_boundary.rb +124 -0
  165. data/lib/hecks/fuzzing/form_census.rb +199 -0
  166. data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
  167. data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
  168. data/lib/hecks/fuzzing/isolated_boot.rb +226 -38
  169. data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
  170. data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
  171. data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
  172. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
  173. data/lib/hecks/fuzzing/properties/guards.rb +129 -26
  174. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
  175. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
  176. data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
  177. data/lib/hecks/fuzzing/properties/querying.rb +17 -17
  178. data/lib/hecks/fuzzing/properties.rb +84 -39
  179. data/lib/hecks/fuzzing/qa_settings.rb +152 -0
  180. data/lib/hecks/fuzzing/replay.rb +224 -114
  181. data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
  182. data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
  183. data/lib/hecks/fuzzing/self_consistency.rb +676 -0
  184. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
  189. data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
  190. data/lib/hecks/fuzzing/shrinker.rb +197 -0
  191. data/lib/hecks/fuzzing/structural_skips.rb +39 -0
  192. data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
  193. data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
  194. data/lib/hecks/fuzzing/value_generator.rb +66 -14
  195. data/lib/hecks/fuzzing.rb +11 -0
  196. data/lib/hecks/grammar/evolve.rb +10 -10
  197. data/lib/hecks/grammar.rb +7 -7
  198. data/lib/hecks/ir.rb +13 -13
  199. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  200. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  201. data/lib/hecks/language/bluebook/vocabulary.bluebook +365 -15
  202. data/lib/hecks/language/oidc.json +5 -0
  203. data/lib/hecks/literal.rb +9 -9
  204. data/lib/hecks/naming.rb +89 -21
  205. data/lib/hecks/ports/access_control.rb +58 -2
  206. data/lib/hecks/ports/agent/answers.rb +83 -6
  207. data/lib/hecks/ports/agent.rb +119 -35
  208. data/lib/hecks/ports/authentication.rb +44 -4
  209. data/lib/hecks/ports/authorization.rb +53 -11
  210. data/lib/hecks/ports/clock.rb +42 -23
  211. data/lib/hecks/ports/extraction.rb +16 -0
  212. data/lib/hecks/ports/identity_assignment.rb +24 -2
  213. data/lib/hecks/ports/identity_generation.rb +17 -3
  214. data/lib/hecks/ports/identity_resolution.rb +18 -1
  215. data/lib/hecks/ports/loading.rb +4 -0
  216. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  217. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  218. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  219. data/lib/hecks/ports/persistence/execution.rb +4 -0
  220. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  221. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  222. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  223. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  224. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  225. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  226. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  227. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  228. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  229. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  230. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  231. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +160 -58
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +44 -6
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +338 -83
  242. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  243. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  244. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  245. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
  246. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  247. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  248. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  249. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
  250. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
  251. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
  252. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  253. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  254. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  255. data/lib/hecks/ports/persistence/repository_factory.rb +29 -6
  256. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  257. data/lib/hecks/ports/persistence.rb +36 -1
  258. data/lib/hecks/ports/projection.rb +61 -7
  259. data/lib/hecks/ports/query/in_memory.rb +3 -3
  260. data/lib/hecks/ports/query/ordering.rb +6 -6
  261. data/lib/hecks/ports/query.rb +35 -0
  262. data/lib/hecks/projections/bootstrap_table.rb +112 -0
  263. data/lib/hecks/projections/diagrams.rb +75 -75
  264. data/lib/hecks/projections/glossary/html.rb +250 -0
  265. data/lib/hecks/projections/glossary/markdown.rb +105 -0
  266. data/lib/hecks/projections/glossary/mermaid.rb +110 -0
  267. data/lib/hecks/projections/glossary/page.css +271 -0
  268. data/lib/hecks/projections/glossary/page.js +72 -0
  269. data/lib/hecks/projections/glossary/sections.rb +17 -0
  270. data/lib/hecks/projections/glossary/sentences.rb +205 -0
  271. data/lib/hecks/projections/glossary.rb +214 -286
  272. data/lib/hecks/projections/ir.rb +1 -1
  273. data/lib/hecks/projections/model/deviations.rb +18 -17
  274. data/lib/hecks/projections/model.rb +25 -21
  275. data/lib/hecks/projections/oidc.rb +7 -7
  276. data/lib/hecks/projections/parser_table.rb +5 -5
  277. data/lib/hecks/projections/reference.rb +3 -3
  278. data/lib/hecks/projections/rust_vocabulary.rb +443 -0
  279. data/lib/hecks/projections/shape.rb +2 -2
  280. data/lib/hecks/projections/statements.rb +11 -11
  281. data/lib/hecks/projections/vocabulary.rb +9 -9
  282. data/lib/hecks/projections.rb +5 -3
  283. data/lib/hecks/projector/cli_projector.rb +29 -29
  284. data/lib/hecks/projector/docs_projector.rb +13 -13
  285. data/lib/hecks/projector/exporter.rb +42 -21
  286. data/lib/hecks/projector/ir_projector.rb +1 -1
  287. data/lib/hecks/projector/narrate_projector.rb +15 -22
  288. data/lib/hecks/projector/target.rb +13 -13
  289. data/lib/hecks/projector.rb +15 -15
  290. data/lib/hecks/query_ir.rb +47 -47
  291. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  292. data/lib/hecks/query_specification/common/comparison.rb +132 -24
  293. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  294. data/lib/hecks/query_specification/common/null_policy.rb +57 -13
  295. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  296. data/lib/hecks/query_specification/common/options.rb +25 -0
  297. data/lib/hecks/query_specification/field_path.rb +69 -15
  298. data/lib/hecks/query_specification/hop_path.rb +57 -20
  299. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  300. data/lib/hecks/rendering.rb +3 -3
  301. data/lib/hecks/router/namespace_installer.rb +3 -3
  302. data/lib/hecks/router.rb +1 -1
  303. data/lib/hecks/runtime/aggregate_lock.rb +11 -11
  304. data/lib/hecks/runtime/boot_gates.rb +3 -3
  305. data/lib/hecks/runtime/caller.rb +8 -8
  306. data/lib/hecks/runtime/capability_graph.rb +2 -2
  307. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
  308. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +64 -67
  309. data/lib/hecks/runtime/command_interpreter.rb +159 -102
  310. data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
  311. data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
  312. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  313. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  314. data/lib/hecks/runtime/command_rules/references.rb +94 -19
  315. data/lib/hecks/runtime/command_rules.rb +3 -0
  316. data/lib/hecks/runtime/dependency_planning.rb +11 -11
  317. data/lib/hecks/runtime/dispatcher.rb +283 -90
  318. data/lib/hecks/runtime/entity_element.rb +282 -46
  319. data/lib/hecks/runtime/entity_interpreter.rb +160 -69
  320. data/lib/hecks/runtime/errors.rb +19 -19
  321. data/lib/hecks/runtime/event.rb +6 -6
  322. data/lib/hecks/runtime/identity.rb +22 -22
  323. data/lib/hecks/runtime/instance.rb +39 -14
  324. data/lib/hecks/runtime/interpreting.rb +12 -12
  325. data/lib/hecks/runtime/invocation.rb +276 -0
  326. data/lib/hecks/runtime/loader.rb +14 -14
  327. data/lib/hecks/runtime/outbox.rb +23 -23
  328. data/lib/hecks/runtime/policy_interpreter.rb +54 -54
  329. data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
  330. data/lib/hecks/runtime/query_interpreter.rb +111 -56
  331. data/lib/hecks/runtime/reaction_invocation.rb +76 -9
  332. data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
  333. data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
  334. data/lib/hecks/runtime/reference_hop.rb +6 -6
  335. data/lib/hecks/runtime/refusal_wording.rb +92 -112
  336. data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
  337. data/lib/hecks/runtime/registry/verification.rb +36 -26
  338. data/lib/hecks/runtime/registry.rb +56 -27
  339. data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
  340. data/lib/hecks/runtime/routing.rb +10 -88
  341. data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
  342. data/lib/hecks/runtime/saga_interpreter.rb +93 -56
  343. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  344. data/lib/hecks/runtime/tenant_check.rb +9 -9
  345. data/lib/hecks/runtime/tenant_scope.rb +5 -5
  346. data/lib/hecks/runtime/value/admission.rb +75 -30
  347. data/lib/hecks/runtime/value/coercion.rb +379 -226
  348. data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
  349. data/lib/hecks/runtime/value.rb +28 -23
  350. data/lib/hecks/runtime.rb +7 -7
  351. data/lib/hecks/storehouse.rb +64 -64
  352. data/lib/hecks/version.rb +3 -3
  353. data/lib/hecks/vocabulary.rb +207 -5
  354. data/lib/hecks.rb +13 -11
  355. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
  356. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
  357. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
  358. metadata +43 -2
@@ -26,20 +26,11 @@ module Hecks
26
26
  queries << { verb: "#{domain_name}::#{aggregate.hecks_name}.#{query.name}",
27
27
  query: query, aggregate: aggregate }
28
28
  end
29
- aggregate.entities.each do |entity|
30
- entity.commands.each do |command|
31
- entity_commands << { verb: "#{domain_name}::#{aggregate.hecks_name}.#{entity.hecks_name}.#{command.hecks_name}",
32
- command: command, aggregate: aggregate, entity: entity }
33
- end
34
- entity.queries.each do |query|
35
- entity_queries << { verb: "#{domain_name}::#{aggregate.hecks_name}.#{entity.hecks_name}.#{query.name}",
36
- query: query, aggregate: aggregate, entity: entity }
37
- end
38
- end
29
+ catalog_entities(domain_name, aggregate, entity_commands, entity_queries)
39
30
  end
40
31
 
41
- # THE BARE DOMAIN FORM — "Domain.report_name", no "::" — the
42
- # SAME shape `Dispatcher#query` itself branches on to route a
32
+ # **The bare domain form** — "Domain.report_name", no "::" — the
33
+ # same shape `Dispatcher#query` itself branches on to route a
43
34
  # read-model ask apart from an aggregate query. A report was
44
35
  # never in this catalog at all before: `aggregation_matches_
45
36
  # recompute` (count/median) has only ever been exercised by
@@ -56,41 +47,121 @@ module Hecks
56
47
  { creating: creating, instance: instance, entity_commands: entity_commands,
57
48
  queries: queries, entity_queries: entity_queries, read_models: read_models,
58
49
  populators: populators(runtime),
50
+ # Every `role "..."` any command in the boot declares — the
51
+ # "wrong hat" pool the caller draw's `mismatched` shape picks
52
+ # from (adversary.rb `other_role`).
53
+ roles: (creating + instance + entity_commands).filter_map { |e| e[:command].role }
54
+ .map(&:to_s).reject(&:empty?).uniq.sort,
55
+ # The grant verb every loaded authorization provider declares
56
+ # (`provides "authorization", grant: ...`) — what the caller
57
+ # draw steers at a declared role and records as a real grant.
58
+ # Read off the declaration, never the literal Governance name.
59
+ grant_verbs: runtime.registry.authorization_providers
60
+ .filter_map { |chapter| chapter.provided_verb(Bluebook::Capabilities::AUTHORIZATION, :grant) }
61
+ .sort,
59
62
  # Which aggregates this corpus can actually make one of — the ones
60
63
  # `satisfiable?` is entitled to wait for.
61
64
  creatable: creating.to_set { |entry| entry[:aggregate].hecks_name } }
62
65
  end
63
66
 
64
- # Which command, on which aggregate, appends to which entity list — so a
65
- # successful dispatch can predict the identity the element it just added
66
- # landed on. Entity#identified_by is filled by `Array(current).size + 1`
67
+ # Every entity, at every depth `Card` nested inside `Board`
68
+ # inside `Workspace` (qa/stress_domains/nested_pieces) walks in
69
+ # as `chain: [Board, Card]`, the exact hop list
70
+ # `EntityInterpreter::Resolution.of` resolves the dotted verb
71
+ # back into. Before this walk existed the catalog only ever read
72
+ # `aggregate.entities` one level down, so a two-hop entity
73
+ # command (BUG#11's whole class) could never be generated at all
74
+ # — the one shape the differential harness most needed to reach
75
+ # was structurally absent from every sequence it ever produced.
76
+ # `entity:` stays the last hop (what every existing reader means
77
+ # by "the entity"); `chain:` is the whole path.
78
+ #
79
+ # Entity queries stay one hop deep, exactly as before — a nested
80
+ # entity's query has no established wire spelling this generator
81
+ # can vouch for, and nothing in the corpus declares one.
82
+ def catalog_entities(domain_name, aggregate, entity_commands, entity_queries)
83
+ each_entity_chain(aggregate) do |chain|
84
+ entity = chain.last
85
+ path = chain.map(&:hecks_name).join(".")
86
+ entity.commands.each do |command|
87
+ entity_commands << { verb: "#{domain_name}::#{aggregate.hecks_name}.#{path}.#{command.hecks_name}",
88
+ command: command, aggregate: aggregate, entity: entity, chain: chain }
89
+ end
90
+ next unless chain.size == 1
91
+
92
+ entity.queries.each do |query|
93
+ entity_queries << { verb: "#{domain_name}::#{aggregate.hecks_name}.#{path}.#{query.name}",
94
+ query: query, aggregate: aggregate, entity: entity }
95
+ end
96
+ end
97
+ end
98
+
99
+ # Depth-first, parents before children, in declaration order — so
100
+ # the depth-1 entries land in `entity_commands` in exactly the
101
+ # order they always did (a pinned seed's picker pool is the same
102
+ # pool it was), and a nested entity's own entries follow its
103
+ # parent's.
104
+ def each_entity_chain(owner, chain = [], &block)
105
+ owner.entities.each do |entity|
106
+ path = chain + [entity]
107
+ yield path
108
+ each_entity_chain(entity, path, &block)
109
+ end
110
+ end
111
+
112
+ # Which command, on which owner, appends to which entity list — so a
113
+ # successful dispatch can predict the identity the element it just
114
+ # added landed on. Entity#identified_by is filled by `Array(current).size + 1`
67
115
  # (CommandInterpreter#entity_element) when the append's own field
68
116
  # mapping doesn't already assign it — the common case, predicted here.
69
117
  # A domain whose append explicitly assigns identity through a mapped
70
- # argument is covered too, without guessing: whatever value THIS
71
- # generator supplied for that argument at dispatch time IS the
118
+ # argument is covered too, without guessing: whatever value this
119
+ # generator supplied for that argument at dispatch time is the
72
120
  # identity, and gets recorded directly (see `record_outcome`).
121
+ #
122
+ # `owner_chain:` — `[]` for an aggregate-level append (`Board.AddList`,
123
+ # `Folder.AddSlip`), the entity path for an entity-level one
124
+ # (`Board.AddCard` appending into `Board.cards`, owner_chain
125
+ # `[Board]`) — so `record_outcome` can key the appended element
126
+ # under the exact parent-plus-hops it landed beneath.
127
+ # `identity_arguments:` — every identity head the mapping sources
128
+ # from a command argument (a composite entity identity has several),
129
+ # what the adversarial duplicate-identity mutation replays;
130
+ # `identity_argument:` stays the single-head reading the existing
131
+ # auto-mint prediction already keys on.
73
132
  def populators(runtime)
74
133
  runtime.registry.bluebooks.each_value.flat_map do |bluebook|
75
134
  bluebook.aggregates.flat_map do |aggregate|
76
- aggregate.commands.filter_map do |command|
77
- append = command.mutations.find { |mutation| mutation.op == :append }
78
- next unless append
79
-
80
- list_attribute = aggregate.attribute(append.target)
81
- next unless list_attribute&.list?
82
-
83
- entity = aggregate.entities.find { |candidate| candidate.hecks_name == list_attribute.type.to_s }
84
- next unless entity
135
+ owners = [[aggregate, []]]
136
+ each_entity_chain(aggregate) { |chain| owners << [chain.last, chain] }
85
137
 
86
- identity_field = entity.identified_by
87
- mapped = identity_field && append.source[identity_field]
88
- { command: command, aggregate: aggregate, entity: entity,
89
- identity_field: identity_field, identity_argument: mapped.is_a?(Symbol) ? mapped : nil }
138
+ owners.flat_map do |owner, chain|
139
+ owner.commands.filter_map { |command| populator_for(aggregate, owner, chain, command) }
90
140
  end
91
141
  end
92
142
  end
93
143
  end
144
+
145
+ def populator_for(aggregate, owner, chain, command)
146
+ append = command.mutations.find { |mutation| mutation.op == :append }
147
+ return unless append
148
+
149
+ list_attribute = owner.attribute(append.target)
150
+ return unless list_attribute&.list?
151
+
152
+ entity = owner.entities.find { |candidate| candidate.hecks_name == list_attribute.type.to_s }
153
+ return unless entity
154
+
155
+ identity_field = entity.identified_by
156
+ mapped = identity_field && append.source[identity_field]
157
+ identity_arguments = entity.identity_heads.filter_map do |head|
158
+ source = append.source[head]
159
+ source if source.is_a?(Symbol)
160
+ end
161
+ { command: command, aggregate: aggregate, owner: owner, owner_chain: chain, entity: entity,
162
+ identity_field: identity_field, identity_argument: mapped.is_a?(Symbol) ? mapped : nil,
163
+ identity_arguments: identity_arguments }
164
+ end
94
165
  end
95
166
  end
96
167
  end
@@ -15,11 +15,12 @@ module Hecks
15
15
  parent_scalar = identity_scalar_of(aggregate, args)
16
16
 
17
17
  @known_ids[aggregate.hecks_name] << parent_scalar if entry[:entity].nil? && entry[:command].creates?
18
+ record_grant(args) if catalog[:grant_verbs].include?(entry[:verb])
18
19
 
19
- populator = catalog[:populators].find { |p| p[:command].equal?(entry[:command]) && p[:aggregate].equal?(aggregate) }
20
+ populator = populator_for_entry(catalog, entry)
20
21
  return unless populator
21
22
 
22
- key = "#{aggregate.hecks_name}.#{populator[:entity].hecks_name}##{parent_scalar}"
23
+ key = append_pool_key(populator, args)
23
24
 
24
25
  # The auto-minted case (CommandInterpreter#entity_element): the
25
26
  # element just landed at count-so-far + 1. The explicit case: this
@@ -33,20 +34,66 @@ module Hecks
33
34
  (@entity_known_ids[key].size + 1).to_s
34
35
  end
35
36
  @entity_known_ids[key] << new_id
37
+
38
+ # The caller-supplied identity tuple, kept whole — every mapped
39
+ # identity argument and the exact value this step offered for it
40
+ # — so the adversarial duplicate-identity mutation (BUG#13's own
41
+ # shape, `Folder.AddSlip` twice under the same `reference`) can
42
+ # offer it again later against the same parent, composite
43
+ # identities included (BUG#13's fix explicitly did not cover
44
+ # those; this is how a sequence gets to ask).
45
+ return if populator[:identity_arguments].empty?
46
+
47
+ @appended_identities[key] << populator[:identity_arguments].to_h { |name| [name.to_s, args[name.to_s]] }
48
+ end
49
+
50
+ # A grant this sequence made for real — the authorization provider's
51
+ # declared `grant:` verb (`Governance::RoleAssignment.Assign` in
52
+ # every boot today) succeeded with these exact args, so `actor_id` now holds
53
+ # `role_name` in this boot's own store, on both replay sides. The
54
+ # `actor_known` caller shape (adversary.rb) replays that identity
55
+ # against a command gated on the same role: the one way a
56
+ # generated step reaches `holds_role?`'s authorized branch rather
57
+ # than only its refusing one.
58
+ def record_grant(args)
59
+ role = ValueGenerator.scalar_of(args["role_name"]).to_s
60
+ actor = ValueGenerator.scalar_of(args["actor_id"]).to_s
61
+ @granted[role] << actor unless role.empty? || actor.empty?
62
+ end
63
+
64
+ # The pool an appended element lands in — the aggregate's own
65
+ # identity, then one scalar per owning hop (an aggregate-level
66
+ # append has none; `Board.AddCard` has `Board`'s own `number`,
67
+ # read straight back off the args this step addressed it by).
68
+ def append_pool_key(populator, args)
69
+ parent_scalar = identity_scalar_of(populator[:aggregate], args)
70
+ owner_scalars = populator[:owner_chain].map do |piece|
71
+ ValueGenerator.scalar_of(args[(piece.identified_by || :id).to_s])
72
+ end
73
+ entity_pool_key(populator[:aggregate].hecks_name,
74
+ populator[:owner_chain].map(&:hecks_name) + [populator[:entity].hecks_name],
75
+ [parent_scalar] + owner_scalars)
76
+ end
77
+
78
+ # `"Agg.Board#w1"` for a depth-1 pool — byte-identical to the key
79
+ # this always used, so nothing a pinned seed draws from moves —
80
+ # and `"Agg.Board.Card#w1/1"` one hop deeper.
81
+ def entity_pool_key(aggregate_name, chain_names, scalars)
82
+ "#{aggregate_name}.#{chain_names.join('.')}##{scalars.join('/')}"
36
83
  end
37
84
 
38
- # THE SCALAR THIS STEP'S OWN AGGREGATE IDENTITY RESOLVES TO, from the
85
+ # The scalar this step's own aggregate identity resolves to, from the
39
86
  # args a creating (or entity-populating) command actually
40
- # dispatched — a SINGLE declared head (or the untyped default
87
+ # dispatched — a single declared head (or the untyped default
41
88
  # `:id`) reads straight off its own top-level arg the way this
42
89
  # always has (`ValueGenerator.scalar_of`, opening the identity value
43
- # object). A genuinely COMPOSITE identity (`composite_identity?`,
90
+ # object). A genuinely composite identity (`composite_identity?`,
44
91
  # shared with StepBuilder#add_identity! — see its own comment) has
45
92
  # no such top-level arg at all: `add_identity!` deliberately leaves
46
93
  # a composite creating command's own individually-declared fields
47
94
  # alone rather than forcing a synthetic `id` neither the bluebook
48
95
  # nor the command ever declared, so this joins those fields itself,
49
- # in the SAME declaration order `Runtime::Identity.of` joins them
96
+ # in the same declaration order `Runtime::Identity.of` joins them
50
97
  # at dispatch (`Naming.identity`, `Naming::IDENTITY_JOIN`) — which
51
98
  # is exactly what makes the result usable later as the bare `id:`
52
99
  # a composite Revoke/act-again command expects (`Identity.from`'s
@@ -68,8 +115,8 @@ module Hecks
68
115
  pool.sample(random: @random)
69
116
  end
70
117
 
71
- def pick_entity_known(aggregate_name, entity_name, parent_id)
72
- pool = @entity_known_ids["#{aggregate_name}.#{entity_name}##{parent_id}"]
118
+ def pick_entity_known(key)
119
+ pool = @entity_known_ids[key]
73
120
  return ValueGenerator.random_id(@random) if pool.empty? || @random.rand < ValueGenerator::INVALID_REFERENCE_PROBABILITY
74
121
 
75
122
  pool.sample(random: @random)
@@ -1,8 +1,8 @@
1
1
  module Hecks
2
2
  module Fuzzing
3
3
  class SequenceGenerator
4
- # Which step to try next: eligibility (what is POSSIBLE from the
5
- # state so far) and weighting (what is LIKELY to reach somewhere
4
+ # Which step to try next: eligibility (what is possible from the
5
+ # state so far) and weighting (what is likely to reach somewhere
6
6
  # new).
7
7
  module Picker
8
8
  private
@@ -16,15 +16,26 @@ module Hecks
16
16
 
17
17
  makers = catalog[:creating].select { |entry| satisfiable?(catalog, entry) }
18
18
  pool = rest + makers.flat_map { |entry| [entry] * creating_weight(rest.size) }
19
+ pool += deep_entity_bias(rest) if adversarial?
19
20
 
20
21
  steer(pool).sample(random: @random)
21
22
  end
22
23
 
23
- # WHILE THERE IS NOTHING TO FIND, MAKING SOMETHING IS THE ONLY USEFUL MOVE.
24
+ # BUG#11's own preference (adversary.rb) an entity command two
25
+ # or more hops deep is weighted up the same way an unexercised
26
+ # verb is, only in adversarial mode: the eligibility rules above
27
+ # still decide what is possible, and a default-mode pool is
28
+ # exactly the pool it always was.
29
+ def deep_entity_bias(rest)
30
+ deep = rest.select { |entry| (entry[:chain] || []).size >= Adversary::DEEP_ENTITY_DEPTH }
31
+ deep * Adversary::DEEP_ENTITY_WEIGHT
32
+ end
33
+
34
+ # While there is nothing to find, making something is the only useful move.
24
35
  #
25
36
  # A flat weight is right once the domain has records in it, and badly
26
37
  # wrong before: banking declares ten queries, and from an empty store
27
- # exactly ONE creating command is satisfiable — Customer.Register, the
38
+ # exactly one creating command is satisfiable — Customer.Register, the
28
39
  # verb the whole cascade waits on. Two entries against ten left runs
29
40
  # spending their entire budget querying a store nothing had been written
30
41
  # to ; one seed answered ten empty queries in a row and emitted nothing
@@ -43,9 +54,9 @@ module Hecks
43
54
  @known_ids[entry[:aggregate].hecks_name].any? && satisfiable?(catalog, entry)
44
55
  end
45
56
 
46
- # A ROOTLESS report (no `reference_to` at all) has nothing to wait
57
+ # A rootless report (no `reference_to` at all) has nothing to wait
47
58
  # for — it reads whole tables, the same "always eligible" position
48
- # `catalog[:queries]` itself takes. A ROOTED one needs a real
59
+ # `catalog[:queries]` itself takes. A rooted one needs a real
49
60
  # instance of its own `reference_target` to ask about first, same
50
61
  # rule `actionable?` already gives an instance command — asking
51
62
  # `Banking.disputed_payment_count` before any Account exists would
@@ -55,7 +66,7 @@ module Hecks
55
66
  entry[:model].reference_target.nil? || @known_ids[entry[:model].reference_target].any?
56
67
  end
57
68
 
58
- # A COMMAND THAT REFERENCES NOTHING THAT EXISTS CANNOT SUCCEED, so it is
69
+ # A command that references nothing that exists cannot succeed, so it is
59
70
  # not offered until something does.
60
71
  #
61
72
  # `ValueGenerator.reference_value` has no real id to hand over when its
@@ -67,7 +78,7 @@ module Hecks
67
78
  # spent its whole budget being refused.
68
79
  #
69
80
  # This is the rule instance commands already follow — `known_ids.any?` —
70
- # applied to what a command REFERENCES rather than to what it acts on.
81
+ # applied to what a command references rather than to what it acts on.
71
82
  # A target no creating command in this corpus can make (a cross-domain
72
83
  # reference, which `CommandRules#resolve_references` skips anyway) is
73
84
  # exempt, or the whole domain would starve waiting for it.
@@ -81,14 +92,23 @@ module Hecks
81
92
  # Coverage-guided rather than uniformly random : a verb this sequence has
82
93
  # not dispatched yet is weighted up, so a run spends its budget on the
83
94
  # commands it has not reached instead of re-rolling the ones it has. The
84
- # eligibility rules above still decide what is POSSIBLE — this only decides
95
+ # eligibility rules above still decide what is possible — this only decides
85
96
  # what is likely, so a verb gated behind state it does not have yet stays
86
97
  # out of the pool entirely rather than being preferred forever.
87
98
  def steer(pool)
88
99
  fresh = pool.reject { |entry| @exercised.include?(entry[:verb]) }
89
- return pool if fresh.empty?
100
+ steered = fresh.empty? ? pool : pool + (fresh * UNEXERCISED_WEIGHT)
101
+ favor(steered)
102
+ end
103
+
104
+ # `favor:` (CoverageCampaign's rare verbs) — weighted up when
105
+ # eligible, never made eligible. An empty favor list returns the
106
+ # pool untouched, so an unguided seed draws exactly what it did.
107
+ def favor(pool)
108
+ return pool if @favor.empty?
90
109
 
91
- pool + (fresh * UNEXERCISED_WEIGHT)
110
+ favored = pool.uniq.select { |entry| @favor.include?(entry[:verb]) }
111
+ pool + (favored * SequenceGenerator::FAVOR_WEIGHT)
92
112
  end
93
113
  end
94
114
  end
@@ -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,21 +40,72 @@ 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
45
+ # dispatch this generator makes to learn what the step did, and
46
+ # before the step is returned as corpus data. That ordering is
47
+ # the whole contract: the mutated `args` are what this generator's
48
+ # own inline dispatch sees (so `known_ids` tracking reflects what
49
+ # actually happened), and they are the bytes `Fuzzing::Replay`
50
+ # later hands Ruby's runtime and the bytes `JSON.generate({steps:
51
+ # ...})` hands the compiled Rust binary — one step, one payload,
52
+ # both engines. Nothing downstream of this method can tell a
53
+ # mutated step from an ordinary one except by reading the
54
+ # `"adversarial"` metadata it carries (see adversary.rb).
55
+ #
56
+ # Two more draws sit right after the mutation, in a fixed order —
57
+ # a caller (`caller_draw!`, adversary.rb: `role:`/`actor_id:` on
58
+ # the step, bound around this one dispatch exactly the way
59
+ # `Fuzzing::Replay` and `kernel/cli.rs` will later bind it) and
60
+ # then the dry-run coin (`{"dry_run": verb}` instead of
61
+ # `{"verb": verb}` — `Dispatcher#dry_run?` here, the same door on
62
+ # both replay sides). Order matters for the seed contract: every
63
+ # draw comes from the one `Random.new(seed)`, so the sequence of
64
+ # draws per step is what makes a seed reproducible. Both are
65
+ # off by default and draw nothing when off.
43
66
  def build_command_step(runtime, catalog, entry)
44
67
  args = args_for(entry[:command].attributes, entry[:aggregate])
45
68
  add_identity!(args, entry)
69
+ steer_grant!(args, entry, catalog)
70
+ mutations = adversarial_mutations!(args, entry, catalog)
71
+ caller, caller_note = caller_draw!(entry, catalog)
72
+ mutations << caller_note if caller_note
73
+ @state_before = state_before(runtime, entry, args)
46
74
 
47
- outcome = safe_call { runtime.dispatch(entry[:verb], **symbolize(args)) }
48
- if outcome
49
- record_outcome(catalog, entry, args)
50
- @event_count += outcome.events.length
51
- end
52
- { "verb" => entry[:verb], "args" => args }
75
+ step =
76
+ if dry_run_draw?
77
+ safe_call { as_caller(caller) { runtime.dry_run?(entry[:verb], **symbolize(args)) } }
78
+ { "dry_run" => entry[:verb], "args" => args }
79
+ else
80
+ outcome = safe_call { as_caller(caller) { runtime.dispatch_flat(entry[:verb], symbolize(args)) } }
81
+ if outcome
82
+ record_outcome(catalog, entry, args)
83
+ @event_count += outcome.events.length
84
+ end
85
+ { "verb" => entry[:verb], "args" => args }
86
+ end
87
+ step.merge!(caller) if caller
88
+ step["adversarial"] = mutations unless mutations.empty?
89
+ step
90
+ end
91
+
92
+ # No RNG draw at all when the fraction is zero — the same "off is
93
+ # byte-identical" contract `adversarial?` keeps.
94
+ def dry_run_draw? = @dry_run.positive? && @random.rand < @dry_run
95
+
96
+ # `Hecks.as_caller` for exactly this block, or a bare yield — the
97
+ # same binding `Fuzzing::Replay` makes from the step's own keys
98
+ # later, so the generator's inline dispatch and both replays see
99
+ # one caller.
100
+ def as_caller(caller, &)
101
+ return yield unless caller
102
+
103
+ Hecks.as_caller(role: caller["role"], actor_id: caller["actor_id"], &)
53
104
  end
54
105
 
55
106
  def args_for(attributes, aggregate)
56
107
  args = attributes.each_with_object({}) do |attribute, built|
57
- # AN OPTIONAL ARGUMENT IS SOMETIMES NOT GIVEN, and that is an
108
+ # An optional argument is sometimes not given, and that is an
58
109
  # ordinary payload rather than a damaged one — see
59
110
  # OPTIONAL_OMITTED_PROBABILITY for why this cannot live in
60
111
  # `malform` below and what it was costing while it did not
@@ -63,13 +114,13 @@ module Hecks
63
114
 
64
115
  if attribute.list?
65
116
  value = list_value_for(attribute, aggregate)
66
- # A list-of-ENTITY command attribute has no real example
117
+ # A list-of-entity command attribute has no real example
67
118
  # anywhere in this repo's domains — every entity-owned list is
68
119
  # populated via a per-element append command instead, never a
69
120
  # whole-list command argument — so `list_value_for` (below)
70
121
  # answers `nil` for one rather than guessing at an entity's own
71
122
  # shape, and this step still skips it exactly as it always
72
- # has. A list-of-VALUE-OBJECT attribute (`ConsoleSettings::
123
+ # has. A list-of-value-object attribute (`ConsoleSettings::
73
124
  # Collection.ReplaceColumns`' own `columns`, `list_of(Column)`)
74
125
  # is the real, previously-unfuzzable case this now covers —
75
126
  # `sets :columns` imports the owner aggregate's own declared
@@ -87,9 +138,9 @@ module Hecks
87
138
  malform(args, attributes, aggregate)
88
139
  end
89
140
 
90
- # A `list_of` ATTRIBUTE'S OWN VALUE — an array of independently
141
+ # A `list_of` attribute's own value — an array of independently
91
142
  # generated elements, each shaped exactly the way a bare (non-list)
92
- # attribute of the SAME declared element type already is
143
+ # attribute of the same declared element type already is
93
144
  # (`ValueGenerator.value_for`), since `list_of(X)`'s own element
94
145
  # coercion is `X`'s ordinary shape repeated, not a different one
95
146
  # (`Attribute#type` is already unwrapped from `list_of(...)` at
@@ -107,12 +158,12 @@ module Hecks
107
158
  Array.new(@random.rand(0..3)) { ValueGenerator.value_for(attribute, aggregate, random: @random, known_ids: @known_ids) }
108
159
  end
109
160
 
110
- # 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
111
162
  # wrong in three ways only ever proves which check runs first ; wrong in
112
163
  # exactly one way names the check that fired. And the rate stays low on
113
164
  # purpose — a corrupted step is almost always refused, a sequence of
114
165
  # refusals reaches no state at all, and bin/fuzz already counts those as
115
- # SILENT rather than scoring them.
166
+ # silent rather than scoring them.
116
167
  def malform(args, attributes, aggregate)
117
168
  return args if args.empty? || @random.rand >= MALFORMED_ARGUMENT_PROBABILITY
118
169
 
@@ -123,12 +174,12 @@ module Hecks
123
174
  end
124
175
  end
125
176
 
126
- # 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
127
178
  # a minted id is deliberately unreproducible — a random hex, never a
128
179
  # guessable counter — so dropping it manufactures a step whose outcome
129
180
  # cannot be replayed and says nothing about the runtime's behaviour.
130
181
  # Every step in the hand-written corpus
131
- # 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
132
183
  # be reproducible is a real question, but it is not one a payload fuzzer
133
184
  # can ask.
134
185
  def drop_one(args, aggregate)
@@ -154,23 +205,37 @@ module Hecks
154
205
  if entry[:entity]
155
206
  parent_scalar = pick_known(aggregate.hecks_name)
156
207
  args[parent_key] = identity_shaped(aggregate, aggregate.identified_by, parent_scalar, aggregate)
157
- entity_key = (entry[:entity].identified_by || :id).to_s
158
- entity_scalar = pick_entity_known(aggregate.hecks_name, entry[:entity].hecks_name, parent_scalar)
159
- args[entity_key] = identity_shaped(entry[:entity], entry[:entity].identified_by, entity_scalar, aggregate)
208
+ # One identity per hop, each drawn from the pool its own
209
+ # parent-plus-hops landed elements in (`entity_pool_key`)
210
+ # a depth-1 chain draws exactly what it always did; a
211
+ # `Board.Card` chain draws a Board under this Workspace, then
212
+ # a Card under that Board. Flat args, one head per hop, is
213
+ # the legacy addressing `EntityElement#locate_chain` reads
214
+ # (`args[head]` per identity path); the routed `to: {
215
+ # aggregate:, entities: [...] }` spelling is an adversarial
216
+ # shape layered on top (adversary.rb), never the default.
217
+ scalars = [parent_scalar]
218
+ names = []
219
+ entry[:chain].each do |piece|
220
+ names << piece.hecks_name
221
+ scalar = pick_entity_known(entity_pool_key(aggregate.hecks_name, names, scalars))
222
+ args[(piece.identified_by || :id).to_s] = identity_shaped(piece, piece.identified_by, scalar, aggregate)
223
+ scalars << scalar
224
+ end
160
225
  elsif entry[:command].creates?
161
- # A COMPOSITE IDENTITY (`identified_by` answering nil with MORE
162
- # 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
163
228
  # composite has no single head" comment) supplies every one of
164
229
  # its parts as its own ordinary, individually-declared command
165
230
  # attribute already — `RoleAssignment::Assign` takes actor_id/
166
231
  # role_name/starts_at directly, `args_for` (above) already
167
232
  # generated all three. Forcing a synthetic top-level `id` here
168
- # 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
169
234
  # genuinely untyped (`identity_paths.empty?`, no `identified_by`
170
235
  # declared at all) cases — hands a composite creating command
171
236
  # an argument it never declared at all, refused every time as
172
237
  # unknown before this check existed (a creating `Assign`/`Grant`
173
- # step was never anything BUT refused). `identity_paths.empty?`
238
+ # step was never anything but refused). `identity_paths.empty?`
174
239
  # is the untyped default (falls all the way back to a minted
175
240
  # `:id` the runtime itself never declared as an attribute
176
241
  # either), which still needs exactly the old minting behavior.
@@ -184,9 +249,9 @@ module Hecks
184
249
  end
185
250
  end
186
251
 
187
- # TRUE ONLY FOR A GENUINE MULTI-FIELD IDENTITY — `identified_by`
252
+ # True only for a genuine multi-field identity — `identified_by`
188
253
  # returns nil both for a real composite (`identity_paths.size > 1`)
189
- # and for the untyped default with NO identity declared at all
254
+ # and for the untyped default with no identity declared at all
190
255
  # (`identity_paths.size == 0`, Behaviour::Identified's own
191
256
  # `Array(@identified_by)` fallback) ; only the first of those two
192
257
  # has its own parts already sitting in `args` as real, individually-
@@ -230,7 +295,7 @@ module Hecks
230
295
 
231
296
  # A step the runtime declines is not a generator failure — it simply did
232
297
  # not take effect, so nothing is recorded and the sequence carries on. The
233
- # 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
234
299
  # its wording is pinned by the corpus.
235
300
  #
236
301
  # EvaluationError sits alongside the declared refusals deliberately : a
@@ -241,10 +306,36 @@ module Hecks
241
306
  # generator built a step that breaks the interpreter for reasons that have
242
307
  # nothing to do with the domain declining a payload.
243
308
  def safe_call
244
- yield
245
- 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
246
314
  nil
247
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
248
339
  end
249
340
  end
250
341
  end