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
@@ -0,0 +1,124 @@
1
+ require "hecks/ports/persistence/plugins/era"
2
+
3
+ module Hecks
4
+ module Fuzzing
5
+ # **The fork-loss class, as a check** — not a fuzz. Every other mode in
6
+ # this file generates a sequence and compares two answers to the same
7
+ # question; this one asks a single, unconditional question of a
8
+ # target's own real, already-configured `PostgresEra` ledger:
9
+ # does any ancestor era still hold writes nobody has merged forward?
10
+ #
11
+ # **The bug this targets, named exactly**. Minting a new era (an attribute
12
+ # or aggregate addition — `StorageShape.project`, lib/hecks/ports/
13
+ # persistence/plugins/era/storage_shape.rb) advances the readable head
14
+ # to a new partition; an old checkout, or a process that boots slower
15
+ # than the mint, can keep writing into the era it still believes is
16
+ # current. Those writes are not lost — `Lineage#diverged_count`
17
+ # (postgres_era/lineage/tail_merge.rb) can always find them — but
18
+ # nothing ever asked it automatically. The practice's own ledger found
19
+ # exactly this live, twice: once as an operational gap this session's
20
+ # Step 0 recovered by hand (`bin/merge_tail`, three conflicting
21
+ # records, three eras deep), and once as BUG#24 (a superuser
22
+ # connection walking straight through the era write-fence — fixed by
23
+ # refusing that connection outright, `Lineage#check_fence_applies!`).
24
+ # Both are "a fork happened and nothing said so" — this module is the
25
+ # automatic version of the question `bin/merge_tail`'s own diagnostic
26
+ # line already answers by hand, run as an ordinary sweep Check instead
27
+ # of only when a human remembers to ask.
28
+ #
29
+ # The target's real database, read-only, never a disposable one — every
30
+ # other Postgres-touching mode here (`persistence_parity`,
31
+ # `adapter_parity_sqlite`) owns a throwaway schema for the exact
32
+ # reason it must never look at what a real deployment actually holds;
33
+ # this check exists for what a real deployment actually holds, so it
34
+ # connects the same way `bin/merge_tail` itself does — the target's
35
+ # own `.world` binding — and never writes anything: `eras`/
36
+ # `diverged_count` are both plain `SELECT`s, and `ensure_base!` only
37
+ # ever provisions (`CREATE TABLE IF NOT EXISTS`) when this connection
38
+ # is the database's own provisioning owner, the identical idempotent
39
+ # call every ordinary boot already makes. A target with no real,
40
+ # reachable database configured (nothing provisioned locally, say) is
41
+ # reported `checked: false` with the reason — an operational note this
42
+ # mode's own caller surfaces once, never a crash and never silently
43
+ # "clean".
44
+ module EraBoundary
45
+ module_function
46
+
47
+ # `{ checked: true, diverged_total:, breakdown: [{ordinal:, diverged:}] }`
48
+ # or `{ checked: false, kind:, reason: "..." }`. A `true` with
49
+ # `diverged_total.positive?` is the finding this module exists to
50
+ # surface: real post-cut writes an ancestor era is still holding.
51
+ #
52
+ # `kind:` splits the two things `checked: false` used to mean. Both
53
+ # answered the same shape, and `bin/qa_sweep` logged that shape as a
54
+ # held Check either way — so a refused connection or a `Lineage`
55
+ # defect read, in the ledger, exactly like an audit that ran and
56
+ # found nothing: counted toward `sweep.made` and the target's clean
57
+ # streak. `:not_applicable` is a target with no lineage to audit at
58
+ # all; `:error` is the audit failing to run, which the caller
59
+ # surfaces as a finding rather than holding.
60
+ def diverged_ancestor_writes(domain_path)
61
+ registry, directory = load_registry(domain_path)
62
+ bluebook = registry.bluebooks.values.first
63
+ return failed("no bluebook in #{directory}") unless bluebook
64
+
65
+ first = bluebook.aggregates.first
66
+ return not_applicable("#{bluebook.name} declares no aggregates") unless first
67
+
68
+ adapter_name = Hecks::Ports::Persistence::BindingPolicy.resolve(registry, bluebook.name, first).adapter
69
+ unless adapter_name == "PostgresEra"
70
+ return not_applicable("#{bluebook.name} is bound to #{adapter_name}, not PostgresEra")
71
+ end
72
+
73
+ settings = registry.world(bluebook.name)&.for_binding(Hecks::Ports::Persistence::VERB, adapter_name) || {}
74
+ db = Hecks::Adapters::PostgresEra.connect_for(bluebook.name, settings)
75
+ begin
76
+ lineage = Hecks::Adapters::PostgresEra::Lineage.new(db, bluebook.name)
77
+ lineage.ensure_base!
78
+ eras = lineage.eras
79
+ # `bin/merge_tail`'s own arithmetic, restated read-only: every
80
+ # ancestor era (every ordinal strictly before the head's own) may
81
+ # still hold post-cut writes the head has never interleaved.
82
+ breakdown = if eras.size > 1
83
+ (1...eras.last[:ordinal]).map do |ordinal|
84
+ { ordinal: ordinal, diverged: lineage.diverged_count(ordinal) }
85
+ end
86
+ else
87
+ []
88
+ end
89
+ { checked: true, era_count: eras.size, breakdown: breakdown, diverged_total: breakdown.sum { |b| b[:diverged] } }
90
+ ensure
91
+ db.close
92
+ end
93
+ rescue StandardError => e
94
+ # Could not audit is not the same as nothing to audit — a refused
95
+ # connection, a `Lineage` defect, malformed `.world` settings. Every
96
+ # one of these used to answer the benign shape and be held.
97
+ failed("#{e.class}: #{e.message}")
98
+ end
99
+
100
+ # Nothing to audit — this target holds no PostgresEra lineage, so no
101
+ # ancestor era can carry post-cut writes. Not a finding, and not
102
+ # evidence of anything either: the caller logs no Check at all.
103
+ def not_applicable(reason) = { checked: false, kind: :not_applicable, reason: reason }
104
+
105
+ # The audit itself could not run — reported as a finding, never held.
106
+ def failed(reason) = { checked: false, kind: :error, reason: reason }
107
+
108
+ # The same load `bin/merge_tail` itself performs (that script's own
109
+ # top half) — a fresh `Registry`, never the ledger's own (this asks
110
+ # about the swept target's lineage, not QualityControl's own).
111
+ def load_registry(domain_path)
112
+ loading = Hecks::Ports::Loading.bootstrap
113
+ directory = loading.bluebook_directory(domain_path)
114
+ registry = Hecks::Runtime::Registry.new(root: File.dirname(directory))
115
+ Hecks.with_registry(registry) do
116
+ loading.load_library
117
+ loading.load_project(loading.shared_root(nil, directory))
118
+ loading.load_domain(directory)
119
+ end
120
+ [registry, directory]
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,199 @@
1
+ require "json"
2
+ require_relative "../corpus"
3
+
4
+ module Hecks
5
+ module Fuzzing
6
+ # What an aggregate can exhibit, and which pairs it puts together.
7
+ #
8
+ # Extracted from `spec/combination_coverage_spec.rb`'s own pairwise
9
+ # table so it has exactly two consumers that can never drift: that
10
+ # spec (the golden corpus, held to every pair) and `bin/qa_domain_
11
+ # novelty` (a candidate stress domain, measured against every path
12
+ # the QA ledger already sweeps — see that script's own header for why
13
+ # a new domain has to name the pair no existing target meets before
14
+ # it earns a place in the rotation).
15
+ #
16
+ # **The unit is one aggregate**. Two forms in the same chapter but
17
+ # different heads never meet at dispatch; two forms on one head do
18
+ # — that spec's own header has the four defects that argument came
19
+ # from. Each entry below is a form the language declares and a
20
+ # runtime has to handle, chosen because it has produced a defect or
21
+ # sits one step from one; adding one here is how a new form joins
22
+ # both gates at once, and it will name its own uncovered pairs on the
23
+ # first run of each.
24
+ #
25
+ # **One flat table, on purpose** — each entry is an independent boolean
26
+ # check against the same string-keyed aggregate IR hash (the shape
27
+ # `spec/golden/ir/*.json` carries and `Projector::Exporter.call`
28
+ # round-trips to through JSON), laid out so every declared form can
29
+ # be read, and added to, at a glance.
30
+ module FormCensus
31
+ # A `given`'s own lookup path crossing at least two references —
32
+ # `member.sponsor.standing`, `source.customer.status`: the
33
+ # `CommandRules::References#dereference` recursion, walked only
34
+ # on a fresh command argument (S12, ADR 0025) and hydrated one
35
+ # repository lookup per hop.
36
+ TWO_HOP_GIVEN_PATH_LENGTH = 3
37
+
38
+ FORMS = {
39
+ "composite_id" => ->(a) { (a["identified_by"] || []).size >= 2 },
40
+ "has_entity" => ->(a) { entities(a).any? },
41
+ "two_entities" => ->(a) { entities(a).size >= 2 },
42
+ "composite_piece" => ->(a) { entities(a).any? { |piece| (piece["identified_by"] || []).size >= 2 } },
43
+ "multi_emit" => ->(a) { commands(a).any? { |verb| (verb["emits"] || []).size >= 2 } },
44
+ "lifecycle" => ->(a) { !a["lifecycle"].nil? },
45
+ "piece_lifecycle" => ->(a) { entities(a).any? { |piece| !piece["lifecycle"].nil? } },
46
+ "has_query" => ->(a) { queries(a).any? },
47
+ "list_attr" => ->(a) { attributes(a).any? { |held| held["list"] } },
48
+ "reference_attr" => ->(a) { attributes(a).any? { |held| reference?(held) } },
49
+ "closed_set" => ->(a) { (a["value_objects"] || []).any? { |shape| shape["closed_set"] } },
50
+ "has_default" => ->(a) { attributes(a).any? { |held| !held["default"].nil? } },
51
+ "has_optional" => ->(a) { commands(a).any? { |verb| (verb["attributes"] || []).any? { |held| held["optional"] } } },
52
+ # The reference-hop family (angle-2, qa/bluebook ledger) — the
53
+ # forms `qa/stress_domains/referral_chain` exists for, absent
54
+ # from the census until that domain named them. Each is one
55
+ # step from a catalogued gap: `two_hop_given` is `dereference`'s
56
+ # own recursion (`DEREFERENCE_DEPTH`); `multi_hop_where` is a
57
+ # `/`-chain `HopPath` walks for real and `rust/project/queries.
58
+ # rb` structurally refuses (D2 of the equivalence-gap plan);
59
+ # `revalued_reference` is ADR 0037 Finding 5's exact trigger — a
60
+ # command redeclaring the aggregate's own reference field under
61
+ # a non-reference type, so only `resolve_state_references` (never
62
+ # ported) can catch a dangling id.
63
+ # A retroactive correction, and a role-gated command — both are
64
+ # declared forms this census could not see, on aggregates it was
65
+ # already measuring: `examples/banking` has carried `corrects`
66
+ # mutations and `role`-bearing commands the whole time, and
67
+ # `qa/stress_domains/corrections` exists for the first. Three
68
+ # stress domains' own NOTES.md record `bin/qa_domain_novelty`
69
+ # answering "no new pair" for a domain whose whole point was a
70
+ # shape this table did not name (case_escalation's is the
71
+ # bluntest: "read that as a gap in the census, not in this
72
+ # domain"). Entity commands count for both — BUG#30-33 and
73
+ # BUG#31 were all entity-level `corrects`.
74
+ "corrects" => ->(a) { every_command(a).any? { |verb| corrects?(verb) } },
75
+ "role_gated" => ->(a) { every_command(a).any? { |verb| !verb["role"].to_s.empty? } },
76
+ "two_hop_given" => ->(a) { two_hop_given?(a) },
77
+ "multi_hop_where" => ->(a) { multi_hop_where?(a) },
78
+ "revalued_reference" => ->(a) { revalued_reference?(a) }
79
+ }.freeze
80
+
81
+ module_function
82
+
83
+ def entities(aggregate) = aggregate["entities"] || []
84
+ def commands(aggregate) = aggregate["commands"] || []
85
+
86
+ # One aggregate's commands, its pieces' included — a form carried by
87
+ # an entity command is carried by the aggregate that owns it, the
88
+ # same way `composite_piece`/`piece_lifecycle` already read pieces.
89
+ def every_command(aggregate) = commands(aggregate) + entities(aggregate).flat_map { |piece| commands(piece) }
90
+
91
+ def corrects?(verb) = (verb["mutations"] || []).any? { |change| change["op"].to_s == "corrects" }
92
+ def attributes(aggregate) = aggregate["attributes"] || []
93
+ def queries(aggregate) = aggregate["queries"] || []
94
+ def reference?(attribute) = attribute["type"].to_s.start_with?("Reference<")
95
+
96
+ def two_hop_given?(aggregate)
97
+ commands(aggregate).any? { |verb| (verb["givens"] || []).any? { |given| deep_lookup?(given["ast"]) } }
98
+ end
99
+
100
+ # A `where` whose field crosses two `/` — `member/sponsor/standing`.
101
+ def multi_hop_where?(aggregate)
102
+ queries(aggregate).any? { |query| (query["wheres"] || []).any? { |where| where["field"].to_s.count("/") >= 2 } }
103
+ end
104
+
105
+ # A command attribute sharing a name with one of the aggregate's
106
+ # own reference-typed attributes while carrying a different, non-
107
+ # reference type — `attribute :member, Handle` against
108
+ # `reference_to Member`.
109
+ def revalued_reference?(aggregate)
110
+ references = attributes(aggregate).select { |held| reference?(held) }.to_set { |held| held["name"].to_s }
111
+ commands(aggregate).any? do |verb|
112
+ (verb["attributes"] || []).any? { |held| references.include?(held["name"].to_s) && !reference?(held) }
113
+ end
114
+ end
115
+
116
+ # Walks a given's own exported AST for any `lookup` whose path is
117
+ # long enough to have crossed two references.
118
+ def deep_lookup?(node)
119
+ case node
120
+ when Hash
121
+ return true if node["op"] == "lookup" && Array(node["path"]).size >= TWO_HOP_GIVEN_PATH_LENGTH
122
+
123
+ node.each_value.any? { |child| deep_lookup?(child) }
124
+ when Array
125
+ node.any? { |child| deep_lookup?(child) }
126
+ else
127
+ false
128
+ end
129
+ end
130
+
131
+ # Every form, answered for one aggregate — the table above, applied.
132
+ def properties(aggregate)
133
+ FORMS.transform_values { |form| form.call(aggregate) }
134
+ end
135
+
136
+ # Every unordered pair of forms, each rendered "left + right" in
137
+ # alphabetical order — the key both gates' excuse tables use.
138
+ def pairs
139
+ FORMS.keys.combination(2).map { |pair| pair_key(*pair) }
140
+ end
141
+
142
+ def pair_key(left, right) = [left, right].sort.join(" + ")
143
+
144
+ # `held` is `[[aggregate_name, properties], ...]`. Answers which
145
+ # pairs are met on one aggregate, and by which — a Hash from pair
146
+ # key to the names carrying it, so a caller can say who.
147
+ def covered_pairs(held)
148
+ held.each_with_object(Hash.new { |h, k| h[k] = [] }) do |(name, shows), covered|
149
+ shows.select { |_, present| present }.keys.combination(2).each do |left, right|
150
+ covered[pair_key(left, right)] << name
151
+ end
152
+ end
153
+ end
154
+
155
+ # `[[\"Chapter::Aggregate\", properties], ...]` for every aggregate
156
+ # a string-keyed chapter IR declares — the same walk the golden
157
+ # spec makes over `spec/golden/ir/*.json`.
158
+ def aggregates_in(chapter_ir)
159
+ (chapter_ir["aggregates"] || []).map do |aggregate|
160
+ ["#{chapter_ir['name']}::#{aggregate['name']}", properties(aggregate)]
161
+ end
162
+ end
163
+
164
+ # Where a domain path keeps its bluebooks — see
165
+ # `Hecks::Corpus.bluebook_files`, the one definition every corpus
166
+ # walk shares. `nil` when neither shape holds a bluebook.
167
+ def bluebook_files(domain_path)
168
+ Hecks::Corpus.bluebook_files(domain_path)
169
+ end
170
+
171
+ # The same census over a domain on disk, booted the lightweight
172
+ # way `bin/model_check` and `Hecks::Codemod.load_bluebook` already
173
+ # do (ports and the two in-process adapters, no `Hecks.boot`, no
174
+ # live database, no `.hecksagon`: the census reads declared shape,
175
+ # and a framework chapter a `.hecksagon` would attach is not this
176
+ # domain's own). Only the domain's own chapter is measured — the
177
+ # first bluebook loaded, the same "target chapter is always first"
178
+ # fact `bin/project_rust` relies on.
179
+ def census(domain_path)
180
+ root = File.expand_path("../../..", __dir__)
181
+ files = bluebook_files(domain_path)
182
+ raise ArgumentError, "#{domain_path} has no bluebook/*.bluebook (or *.bluebook) to measure" if files.nil?
183
+
184
+ registry = Hecks::Runtime::Registry.new(root: File.expand_path(domain_path))
185
+ Hecks.with_registry(registry) do
186
+ Kernel.load(File.join(root, "lib/hecks/ports/persistence.port"))
187
+ Kernel.load(File.join(root, "lib/hecks/ports/extraction.port"))
188
+ Kernel.load(File.join(root, "lib/hecks/adapters/driven/memory.adapter"))
189
+ Kernel.load(File.join(root, "lib/hecks/adapters/driven/prism.adapter"))
190
+ files.each { |file| Kernel.load(file) }
191
+ end
192
+
193
+ chapter_name = registry.bluebooks.keys.first
194
+ exported = JSON.parse(JSON.generate(Hecks::Projector::Exporter.call(registry).fetch(chapter_name)))
195
+ aggregates_in(exported)
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,95 @@
1
+ require "json"
2
+ require_relative "isolated_boot"
3
+ require_relative "sequence_generator"
4
+ require_relative "replay"
5
+ require_relative "shrinker"
6
+ require_relative "differential"
7
+
8
+ module Hecks
9
+ module Fuzzing
10
+ # **One generated domain, checked** — the child-process half of
11
+ # `bin/qa_generated_domains` (one domain per process: every generated
12
+ # domain is named `QaGenerated`, see DomainGenerator's header).
13
+ #
14
+ # The same comparisons `bin/qa_sweep` runs on a rotation target, over
15
+ # sequences generated against a domain nobody wrote: with a compiled
16
+ # binary, `Differential.diff` (Ruby vs Rust, self-consistency, declared
17
+ # properties); without one, a Ruby-only replay (properties,
18
+ # self-consistency, an interpreter crash). A domain that does not even
19
+ # boot is `invalid` — a generator defect, not a finding, reported apart.
20
+ #
21
+ # `match:` is how domain-level shrinking asks "does this smaller domain
22
+ # still show the same finding?": only a divergence in the same mode
23
+ # whose `Shrinker.signature` contains the original's counts. Verb names
24
+ # survive a removal that did not touch them, so the signature does too.
25
+ module GeneratedDomainCheck
26
+ DIFFERENTIAL_MODES = %i[differential self_consistency properties_in_differential].freeze
27
+
28
+ module_function
29
+
30
+ def run(domain_path, seeds:, steps:, adversarial:, binary: nil, differ: nil, match: nil, shrink_budget: 0)
31
+ error = boot_error(domain_path)
32
+ return { "status" => "invalid", "error" => error } if error
33
+
34
+ (1..seeds).each do |seed|
35
+ finding = check_seed(domain_path, seed, steps, adversarial, binary, differ, match)
36
+ next unless finding
37
+
38
+ finding["shrunk_steps"] = shrink(domain_path, finding, binary, differ, shrink_budget) if shrink_budget.positive?
39
+ return finding.merge("status" => "found", "seeds_run" => seed)
40
+ end
41
+ { "status" => "clean", "seeds_run" => seeds }
42
+ end
43
+
44
+ def boot_error(domain_path)
45
+ IsolatedBoot.call(domain_path) { |copy| Hecks.boot(copy) }
46
+ nil
47
+ rescue StandardError, ScriptError => e
48
+ "#{e.class}: #{e.message.lines.first&.strip}"
49
+ end
50
+
51
+ def check_seed(domain_path, seed, steps, adversarial, binary, differ, match)
52
+ sequence = SequenceGenerator.generate(domain_path, seed: seed, steps: steps, adversarial: adversarial)
53
+ rescue StandardError => e
54
+ finding(seed, :generator, [{ field: "generator_crash", detail: "#{e.class}: #{e.message}" }], [], match)
55
+ else
56
+ outcomes(domain_path, sequence, binary, differ).each do |mode, divergences|
57
+ found = finding(seed, mode, divergences, sequence, match)
58
+ return found if found
59
+ end
60
+ nil
61
+ end
62
+
63
+ def outcomes(domain_path, sequence, binary, differ)
64
+ return Differential.diff(differ, domain_path, sequence, binary, modes: DIFFERENTIAL_MODES) if binary
65
+
66
+ history = Replay.call(domain_path, sequence, self_consistency: true)
67
+ { ruby_only: Differential.property_divergences(history),
68
+ self_consistency: Differential.self_consistency_divergences(history) }
69
+ rescue StandardError => e
70
+ { ruby_only: [{ field: "crash", detail: "#{e.class}: #{e.message}" }] }
71
+ end
72
+
73
+ def finding(seed, mode, divergences, sequence, match)
74
+ return nil if divergences.empty?
75
+
76
+ signature = Shrinker.signature(divergences)
77
+ return nil if match && !(match["mode"] == mode.to_s && Set.new(match["signature"]).subset?(signature))
78
+
79
+ { "seed" => seed, "mode" => mode.to_s, "signature" => signature.to_a.sort, "steps" => sequence,
80
+ "divergences" => JSON.parse(JSON.generate(divergences)) }
81
+ end
82
+
83
+ # The step-level half of shrinking, in this same process: the domain
84
+ # is already as small as the parent could make it.
85
+ def shrink(domain_path, found, binary, differ, budget)
86
+ original = Set.new(found["signature"])
87
+ mode = found["mode"].to_sym
88
+ Shrinker.call(found["steps"], budget: budget) do |candidate|
89
+ divergences = outcomes(domain_path, candidate, binary, differ)[mode] || []
90
+ Shrinker.reproduces?(original, divergences)
91
+ end.steps
92
+ end
93
+ end
94
+ end
95
+ end
@@ -1,6 +1,6 @@
1
1
  module Hecks
2
2
  module Fuzzing
3
- # A value that is DELIBERATELY THE WRONG SHAPE for the attribute it is handed
3
+ # A value that is deliberately the wrong shape for the attribute it is handed
4
4
  # to — the sibling of ValueGenerator, which only ever builds values that satisfy
5
5
  # a declared type.
6
6
  #
@@ -14,7 +14,7 @@ module Hecks
14
14
  # Rendering's whole story), and no generated value
15
15
  # could ever have produced it.
16
16
  #
17
- # These are expected to be REFUSED, and that is the point : a refusal is an
17
+ # These are expected to be refused, and that is the point : a refusal is an
18
18
  # answer, and its wording is pinned byte-for-byte. The bugs live in
19
19
  # the sentence, not in the happy path.
20
20
  module InvalidValueGenerator
@@ -37,7 +37,7 @@ module Hecks
37
37
  build(kind, attribute, aggregate, random: random)
38
38
  end
39
39
 
40
- # Only the confusions that MEAN anything for this attribute. Offering
40
+ # Only the confusions that mean anything for this attribute. Offering
41
41
  # `scalar_for_object` for a plain Integer would just be a second spelling of
42
42
  # `numeral_string`, and a kind that cannot be wrong for the attribute it is
43
43
  # handed teaches the corpus nothing.
@@ -60,16 +60,16 @@ module Hecks
60
60
  end
61
61
  end
62
62
 
63
- # A bare scalar where a value object is declared. A SINGLE-FIELD value object
63
+ # A bare scalar where a value object is declared. A single-field value object
64
64
  # legitimately accepts one (that is the standing-in rule every domain relies
65
- # on), so the interesting case is a value object with SEVERAL fields, where a
65
+ # on), so the interesting case is a value object with several fields, where a
66
66
  # scalar cannot stand for anything and the refusal has to say so.
67
67
  def scalar_for(attribute, aggregate, random:)
68
68
  value_object = aggregate.value_object(attribute.type.to_s)
69
69
  sole = value_object&.sole_attribute
70
70
  return "a bare scalar" unless sole
71
71
 
72
- # One field, so a scalar is legal — corrupt the FIELD's own type instead,
72
+ # One field, so a scalar is legal — corrupt the field's own type instead,
73
73
  # which is still a shape the attribute cannot accept.
74
74
  { sole.name.to_s => ["nested", "array"] }
75
75
  end