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,142 @@
1
+ require_relative "../../bluebook/expression/evaluator"
2
+
3
+ module Hecks
4
+ module Fuzzing
5
+ module Properties
6
+ # The transactional outbox's own contract (`Runtime::Outbox`, that
7
+ # file's own header), held to the history a replay actually produced
8
+ # rather than trusted. Two facts, each independently checkable from
9
+ # `history[:outbox_traces]` (`Replay#call`'s own before/after
10
+ # capture, one entry per step whose dispatch enqueued at least one
11
+ # row):
12
+ #
13
+ # 1. "delivery is inline by default" — a row this replay's own
14
+ # dispatch enqueued must not still be `pending`/`claimed` once
15
+ # that same call returns (nothing here ever simulates a
16
+ # crash), and must not be `failed` either — `deliver_row`'s
17
+ # own rescue only reaches `failed` for a genuine defect in the
18
+ # relay's own consumer resolution (a row naming a policy/
19
+ # process_manager `run_consumer`'s own independent registry
20
+ # lookup cannot find — `WiringError`), never an ordinary
21
+ # domain refusal (`PolicyInterpreter#deliver`/`SagaInterpreter#
22
+ # advance` both rescue those themselves, recording `delivered:
23
+ # false` on the reaction/saga log and letting `run_consumer`
24
+ # return normally). Both checked for every row, `saga:` and
25
+ # `policy:` alike.
26
+ #
27
+ # 2. A `policy:` row specifically — `PolicyInterpreter#deliver`
28
+ # returns `nil` (no `reaction_log` entry appended at all)
29
+ # exactly when its own `where` (or, for a fan-out policy, the
30
+ # same `where`, gating the whole `for_each`) does not hold;
31
+ # every other outcome (delivered, refused, a defect,
32
+ # reaction-depth-reached) is still a non-nil record `#react`
33
+ # appends. So a `delivered` policy row with no matching
34
+ # `reaction_log` entry is legitimate only when that policy's
35
+ # own `where`, independently re-evaluated here against the
36
+ # row's own recorded event, genuinely does not hold. A
37
+ # `for_each` policy's own fan-out correctness (how many rows
38
+ # it should have dispatched to) is `fanout_dispatches_once_
39
+ # per_matching_row`'s job, not this one's.
40
+ #
41
+ # A `saga:` row has no equivalent second check, deliberately — this
42
+ # was the first shape this property shipped with, and it was wrong,
43
+ # caught live against `examples/banking` before this comment
44
+ # existed: `Fanout.sagas`' own `listens?` (starts_on/ends_on/
45
+ # handler_for matching the event name alone) says nothing about
46
+ # whether a correlation resolves or a live instance exists, and
47
+ # `begin_saga`/`end_saga` (saga_interpreter.rb) both have silent,
48
+ # perfectly ordinary no-op paths that append nothing to `saga_log`
49
+ # — `begin_saga` when an instance under that correlation already
50
+ # exists, `end_saga` when no live instance exists to end (an
51
+ # `AccountOpened` fired by opening an account directly, bypassing
52
+ # the onboarding flow whose `ends_on` names that same event,
53
+ # reproduces this exactly: `Fanout.listens?` enqueues the row
54
+ # because the event name matches `ends_on`, `end_saga` finds
55
+ # nothing under that correlation to delete, and neither logs a
56
+ # word). A `saga:` row draining to `delivered` with zero matching
57
+ # `saga_log` entries is therefore not a finding — only check 1
58
+ # applies to it.
59
+ #
60
+ # **Not a grammar construct** — `FEATURE_COVERAGE`'s own `dry_runs_
61
+ # leave_no_trace` precedent: the outbox is a runtime door
62
+ # (`Runtime::Outbox`), not a word a bluebook declares, so there is
63
+ # no feature string here to claim.
64
+ module Outbox
65
+ def outbox_rows_match_reactions(history)
66
+ bluebooks = history.fetch(:bluebooks, {})
67
+
68
+ offenders = Array(history[:outbox_traces]).flat_map do |trace|
69
+ trace[:rows].flat_map { |row| outbox_row_offenders(row, trace, bluebooks) }
70
+ end
71
+
72
+ offenders.empty? || offenders.join("; ")
73
+ end
74
+
75
+ def outbox_row_offenders(row, trace, bluebooks)
76
+ on = row.dig(:event, :name)
77
+
78
+ case row[:status]
79
+ when "pending", "claimed"
80
+ ["outbox row #{row[:delivery_id]} (#{row[:consumer]} on #{on}) never drained inline — status stayed " \
81
+ "#{row[:status].inspect} though delivery is inline by contract (Runtime::Outbox's own header)"]
82
+ when "failed"
83
+ ["outbox row #{row[:delivery_id]} (#{row[:consumer]} on #{on}) failed to deliver: #{row[:error]} — " \
84
+ "a domain refusal never reaches this far; a failed row names a defect in the relay's own consumer " \
85
+ "resolution"]
86
+ when "delivered"
87
+ outbox_delivered_policy_offenders(row, on, trace, bluebooks)
88
+ else
89
+ []
90
+ end
91
+ end
92
+
93
+ # See this file's own header for why a `saga:` row is exempt: its
94
+ # own `listens?` gives no such guarantee, unlike a policy's single,
95
+ # deterministic `where` gate.
96
+ def outbox_delivered_policy_offenders(row, on, trace, bluebooks)
97
+ kind, fqn = row[:consumer].to_s.split(":", 2)
98
+ return [] unless kind == "policy"
99
+
100
+ home, name = fqn.to_s.split("::", 2)
101
+ return [] if trace[:reactions].any? { |entry| entry[:policy] == name && entry[:on] == on }
102
+
103
+ policy = bluebooks[home]&.policies&.find { |candidate| candidate.name == name }
104
+ return [] unless policy # nothing declared under this name — inconclusive, not a claimed mismatch
105
+ return [] if policy.fans_out? # fan-out row count is fanout_dispatches_once_per_matching_row's job
106
+
107
+ held = independently_re_evaluate_policy_where(policy, row[:event])
108
+ return [] if held != true # false, or inconclusive (the where itself raised) — never a claimed mismatch
109
+
110
+ ["outbox row #{row[:delivery_id]} (#{row[:consumer]} on #{on}) drained as delivered, but no matching " \
111
+ "reaction_log entry exists and the policy's own where clause independently re-evaluates true — " \
112
+ "PolicyInterpreter#deliver only ever returns nil (no reaction_log entry) when where does not hold"]
113
+ end
114
+
115
+ # `PolicyInterpreter#where_holds?`'s own two branches, reproduced —
116
+ # never calling that method again, which would only ever agree
117
+ # with itself (the same rule `resolve_dispatch_binding`'s own
118
+ # comment states). `Evaluator.call` (the raw-string entry, parsed
119
+ # and cached — never `call_rule`, which needs the policy's own
120
+ # build-time `where_rule` AST, an object this history has no
121
+ # reason to carry) is the exact same call `Replay#fan_out_finding`
122
+ # already makes for the identical fact one property over
123
+ # (`policy.where.to_s.empty? || Evaluator.call(policy.where, {},
124
+ # payload)`), reused rather than re-derived a second, slightly
125
+ # different way. `rescue`d to `nil`, not `false`: a where clause
126
+ # that cannot be re-evaluated from the row's own recorded payload
127
+ # alone is inconclusive, not proof either way — the same "never a
128
+ # claimed pass or a claimed mismatch from a resolution this replay
129
+ # cannot actually reproduce" discipline `build_guard_check`'s own
130
+ # rescue clause already follows.
131
+ def independently_re_evaluate_policy_where(policy, event)
132
+ return true if policy.where.to_s.empty?
133
+
134
+ payload = (event[:payload] || {}).transform_keys(&:to_sym)
135
+ Bluebook::Expression::Evaluator.call(policy.where, {}, payload)
136
+ rescue StandardError
137
+ nil
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -10,7 +10,7 @@ module Hecks
10
10
  # (#query_for_verb, #query_eligible_rows, #resolve_hop_clause) other
11
11
  # property modules in this directory also call.
12
12
  module Querying
13
- # THE QUERY ORACLE — differential testing within the one runtime,
13
+ # The query oracle — differential testing within the one runtime,
14
14
  # the shape the retired cross-runtime harness should always have
15
15
  # been. Every generated ask was answered twice at the same instant
16
16
  # (Replay records both): once through whatever the aggregate is
@@ -18,17 +18,17 @@ module Hecks
18
18
  # a SQL binding would compile it), once through the reference
19
19
  # interpreter's own evaluation. The two are separate, live
20
20
  # implementations of the same comparator vocabulary, and they have
21
- # drifted before — an adapter that ACCEPTS what the reference says
21
+ # drifted before — an adapter that accepts what the reference says
22
22
  # matches nothing, or orders what it refuses to order, shows up
23
23
  # here as a finding no self-referential adapter spec could see.
24
24
  # M23 — `Replay` now runs the native and reference engines
25
- # INDEPENDENTLY (each in its own begin/rescue — see that file's own
25
+ # independently (each in its own begin/rescue — see that file's own
26
26
  # comment at the capture site), so this property can tell apart what
27
27
  # used to be indistinguishable: "both engines refused" (fine — the
28
28
  # ask was genuinely bad, nothing to compare) from "one refused and
29
29
  # the other did not" (a real divergence — the two engines disagree
30
- # about whether the ask was even VALID, never mind what it answers).
31
- # `native_refused`/`reference_refused` are read by KEY PRESENCE, not
30
+ # about whether the ask was even valid, never mind what it answers).
31
+ # `native_refused`/`reference_refused` are read by key presence, not
32
32
  # truthiness — `Replay` only ever adds `:error`/`:reference_error`
33
33
  # to an entry when that side actually raised, so an absent key is an
34
34
  # unambiguous "this side answered." A read-model ask (no reference
@@ -59,16 +59,16 @@ module Hecks
59
59
  offenders.empty? || offenders.join("; ")
60
60
  end
61
61
 
62
- # THE SAME "TWO ENGINES, COMPARED" SHAPE query_answers_match_reference
62
+ # The same "two engines, compared" shape query_answers_match_reference
63
63
  # already uses, aimed squarely at Query#options' offset/limit pair —
64
- # but recomputed from history[:instances] directly, a THIRD,
64
+ # but recomputed from history[:instances] directly, a third,
65
65
  # independent computation, rather than comparing QueryInterpreter's
66
66
  # own native and reference paths against each other (which could
67
67
  # share the identical bug neither implementation happened to hit —
68
- # see #4's own fix, which touched BOTH #interpret and
68
+ # see #4's own fix, which touched both #interpret and
69
69
  # #reference_interpret at once). `order_by` declared alongside
70
70
  # `offset` or `limit` names a genuinely paged query. Ports::Query::
71
- # Ordering.apply is the SAME engine QueryInterpreter#ordered calls,
71
+ # Ordering.apply is the same engine QueryInterpreter#ordered calls,
72
72
  # reused here rather than re-derived, so this oracle cannot drift
73
73
  # from what "in order" means without the interpreter drifting the
74
74
  # identical way — only the offset-then-limit .drop/.first slice
@@ -112,7 +112,7 @@ module Hecks
112
112
  offenders.empty? || offenders.join("; ")
113
113
  end
114
114
 
115
- # THE DECLARED Query ITSELF, resolved from a replayed verb — the
115
+ # The declared Query itself, resolved from a replayed verb — the
116
116
  # same shape #command_for_verb resolves a command by, one
117
117
  # construct over. Entity-level queries (a dotted query_path) are
118
118
  # out of scope here — paging on an entity's own list has no real
@@ -128,24 +128,24 @@ module Hecks
128
128
  aggregate&.query(query_path)
129
129
  end
130
130
 
131
- # A QUERY'S OWN ROWS — unlike #eligible_rows (a ReadModel's
131
+ # A query's own rows — unlike #eligible_rows (a ReadModel's
132
132
  # reduced/grouped many-side head, possibly FK-joined against a
133
- # root), a Query always asks about its OWN owning aggregate
133
+ # root), a Query always asks about its own owning aggregate
134
134
  # directly ; no join, no reference_target. `id:` merged in the
135
135
  # same way #eligible_rows' own rows are, since a stable sort
136
136
  # (Ordering.apply's own `identity:`) and the real answer's own
137
137
  # `record.state.merge(id: record.id)` both need it.
138
- # `bluebooks:` — needed ONLY to recognise and resolve a `/` HOP
138
+ # `bluebooks:` — needed only to recognise and resolve a `/` hop
139
139
  # clause (`engagement/client/status`, hop_chain.bluebook's own
140
- # PricedAboveViaEngagement): a hop's head names one of the OWNING
140
+ # PricedAboveViaEngagement): a hop's head names one of the owning
141
141
  # aggregate's declared references, and only the declaration graph
142
142
  # can say which attribute that is and which aggregate it targets.
143
143
  # A local clause never consults it. Latent gap this closed, found
144
144
  # by the fuzzer itself the first time a generated sequence ever
145
- # built a full hop chain AND had its paged query answer a row
145
+ # built a full hop chain and had its paged query answer a row
146
146
  # (seed 1, the moment scalar_value_objects.bluebook joined the
147
147
  # fixtures corpus and shifted every seeded draw): the recompute
148
- # dug `engagement/client/status` as a LOCAL dotted path, found
148
+ # dug `engagement/client/status` as a local dotted path, found
149
149
  # nil, and declared every genuinely-eligible row ineligible — a
150
150
  # false property violation against a correct runtime answer,
151
151
  # reproducible on an untouched main with this same 4-step script.
@@ -173,7 +173,7 @@ module Hecks
173
173
  # would be checking the runtime against itself). One hop peels
174
174
  # off the head (`HopPath.next_hop`, the identical one-step
175
175
  # primitive the live fold uses), the inner clause recurses
176
- # through `query_eligible_rows` against the TARGET's own
176
+ # through `query_eligible_rows` against the target's own
177
177
  # snapshot rows (so a multi-hop tail resolves hop by hop, exactly
178
178
  # as the live path's own recursion does), and the ids that
179
179
  # answered fold back as the same local `in` membership clause the
@@ -9,35 +9,37 @@ require_relative "properties/querying"
9
9
  require_relative "properties/guards"
10
10
  require_relative "properties/dispatch_and_mutations"
11
11
  require_relative "properties/invariants_and_aggregation"
12
+ require_relative "properties/corrections"
13
+ require_relative "properties/outbox"
12
14
 
13
15
  module Hecks
14
16
  module Fuzzing
15
- # Declared properties, checked over a REPLAYED history — the other
17
+ # Declared properties, checked over a replayed history — the other
16
18
  # half of property-based testing the fuzzer was missing: it already
17
19
  # generates and (with bin/fuzz's shrinker) minimizes, but checked
18
20
  # nothing beyond "did the interpreter crash" and "did the replay
19
21
  # match the claim." A property here is a fact that should hold of
20
- # ANY history a valid domain produces, independent of which seed
22
+ # any history a valid domain produces, independent of which seed
21
23
  # produced it.
22
24
  #
23
25
  # Each property is `name => ->(history) { true/false, or a message
24
26
  # string naming what broke }` — a truthy return (including `true`)
25
- # is a pass; a String return is a failure, and the string IS the
27
+ # is a pass; a String return is a failure, and the string is the
26
28
  # finding. `history` is Replay's return shape.
27
29
  #
28
- # EVERY PROPERTY DECLARES THE LANGUAGE FEATURE IT COVERS, in
30
+ # Every property declares the language feature it covers, in
29
31
  # `FEATURE_COVERAGE` below — a "Construct#attribute" pair spelled
30
32
  # exactly as `Bluebook::MetaValidator.grammar_registry` names it,
31
- # the SAME meta-domain that judges every real bluebook (see that
33
+ # the same meta-domain that judges every real bluebook (see that
32
34
  # module's own header: "the language IS the source"). That is the
33
35
  # link this file exists to make real: a construct the language
34
36
  # declares is a fact `spec/meta_domain_coverage_spec.rb` can
35
37
  # enumerate on its own, without anyone re-typing the list here —
36
38
  # so a new attribute added to `language/bluebook/*.bluebook` shows
37
- # up in that spec as UNCLAIMED the moment it lands, not whenever
39
+ # up in that spec as unclaimed the moment it lands, not whenever
38
40
  # someone remembers to go looking. Claiming a feature here is a
39
41
  # deliberate act (a real property, checked at least once failing
40
- # AND once passing — `spec/fuzzing/properties_spec.rb`'s own
42
+ # and once passing — `spec/fuzzing/properties_spec.rb`'s own
41
43
  # discipline) or an explicit, reasoned exemption in that same
42
44
  # spec — never silence.
43
45
  module Properties
@@ -55,15 +57,18 @@ module Hecks
55
57
  extend Querying
56
58
  extend Guards
57
59
  extend DispatchAndMutations
60
+ extend DryRuns
58
61
  extend InvariantsAndAggregation
62
+ extend Corrections
63
+ extend Outbox
59
64
 
60
65
  module_function
61
66
 
62
- # WHICH LANGUAGE FEATURE EACH PROPERTY IS ANSWERABLE FOR. Not
67
+ # Which language feature each property is answerable for. Not
63
68
  # exhaustive of everything a property's body happens to touch —
64
69
  # `Command#attributes`, say, is exercised by nearly every property
65
- # here without being what any of them was WRITTEN to guard — but
66
- # exhaustive of the feature that would go UNCHECKED if this
70
+ # here without being what any of them was written to guard — but
71
+ # exhaustive of the feature that would go unchecked if this
67
72
  # property did not exist. That is the question the coverage gate
68
73
  # actually asks.
69
74
  FEATURE_COVERAGE = {
@@ -75,17 +80,17 @@ module Hecks
75
80
  authorize_scopes_or_refuses: %w[Query#options],
76
81
  guard_refusals_are_declared: %w[Command#givens Command#ensures],
77
82
  lifecycle_guard_and_given_violations_are_refused: %w[Command#from Aggregate#preconditions Entity#preconditions],
78
- # Dispatch#command_name/Dispatch#with_spec are NOT claimable
79
- # feature names — META_DOMAIN_ALL_FEATURES only walks ONE level
83
+ # Dispatch#command_name/Dispatch#with_spec are not claimable
84
+ # feature names — META_DOMAIN_ALL_FEATURES only walks one level
80
85
  # of entity nesting (`agg.entities.flat_map`, meta_domain_
81
- # coverage_spec.rb), and Dispatch sits TWO deep (ProcessManager
86
+ # coverage_spec.rb), and Dispatch sits two deep (ProcessManager
82
87
  # -> Handler -> Dispatch), so those strings never exist there
83
88
  # to claim — a pre-existing meta-domain coverage-generation gap,
84
89
  # found here (their old META_DOMAIN_KNOWN_GAPS entries were
85
90
  # themselves already-orphaned strings no completeness check ever
86
91
  # verified, since KNOWN_GAPS has no "never lets a gap rot" check
87
92
  # the way FEATURE_COVERAGE/GUARANTEED_BY_CONSTRUCTION both do).
88
- # This property still closes the REAL behavior both would have
93
+ # This property still closes the real behavior both would have
89
94
  # named — a Dispatch's own command_name/with_spec are exactly
90
95
  # what dispatch_args resolves and this property checks — the
91
96
  # grammar just has no feature string for either one.
@@ -96,39 +101,70 @@ module Hecks
96
101
  fanout_dispatches_once_per_matching_row: %w[Policy#for_each Policy#where],
97
102
  aggregation_matches_recompute: %w[ReadModel#count ReadModel#median_field],
98
103
  stored_records_satisfy_declared_invariants: %w[Aggregate#invariants Entity#invariants],
99
- group_by_matches_recompute: %w[ReadModel#group_by]
104
+ group_by_matches_recompute: %w[ReadModel#group_by],
105
+ # A runtime door, not a grammar construct — `Dispatcher#dry_run?`
106
+ # is something an application asks of a booted domain, not a
107
+ # word a bluebook can declare, so there is no feature string
108
+ # for it to claim. Listed (empty) rather than omitted so the
109
+ # discipline this table states — every property names what it
110
+ # is answerable for — has no silent exception.
111
+ dry_runs_leave_no_trace: [],
112
+ # Another runtime door, not a grammar construct — same reasoning
113
+ # as dry_runs_leave_no_trace right above: `Runtime::Outbox` is
114
+ # something a persistence adapter provides underneath a booted
115
+ # domain, never a word a bluebook declares.
116
+ outbox_rows_match_reactions: [],
117
+ # **The `corrects` mutation's own target** — this property reads
118
+ # `command.mutations.select { op == :corrects }` and asks whether
119
+ # the event each one names was ever actually emitted, so the
120
+ # feature it answers for is the mutation list, the same one
121
+ # `mutations_match_recompute` reads for a different question.
122
+ # (Not `Command#references`: that field is the dangling-reference
123
+ # question no property asks yet, and it stays a named gap.)
124
+ corrections_reference_an_emitted_event: %w[Command#mutations],
125
+ # No feature string exists for what this one reads. It depends on
126
+ # an argument's own `relationship` (which reference-typed argument
127
+ # points at which aggregate) — but `Argument` is a value object,
128
+ # and the meta-domain walk enumerates aggregate and entity fields
129
+ # only, so no `Argument#…` name is claimable. The declaration side
130
+ # it shares with queries, `authorize …, tenant:`, is
131
+ # `Query#options`, already claimed by `authorize_scopes_or_refuses`;
132
+ # claiming it twice would say this property covers a query
133
+ # question it never asks. Listed (empty) rather than omitted, the
134
+ # same discipline the two runtime doors above keep.
135
+ commands_respect_tenant_scope: []
100
136
  }.freeze
101
137
 
102
- # FEATURES A REPLAY PROPERTY COULD NEVER CATCH VIOLATED, because the
103
- # RUNTIME'S OWN CONSTRUCTION makes the violation impossible to
138
+ # Features a replay property could never catch violated, because the
139
+ # runtime's own construction makes the violation impossible to
104
140
  # produce in the first place — not "untested," but unfalsifiable by
105
141
  # a history, the same class of guarantee this codebase already
106
- # states for identity ("NOTHING IS MINTED" — command_interpreter.rb's
107
- # own header) and now generalises. Each entry names the ONE place in
142
+ # states for identity ("nothing is minted" — command_interpreter.rb's
143
+ # own header) and now generalises. Each entry names the one place in
108
144
  # the runtime that makes it true, universally, for every domain and
109
145
  # every adapter — never per-domain logic a future domain could
110
146
  # accidentally route around.
111
147
  #
112
- # THE BLUEBOOK/HECKSAGON BOUNDARY IS WHY THIS WORKS: a bluebook
113
- # declares SHAPE (attribute types, patterns, closed sets, VO
148
+ # The bluebook/hecksagon boundary is why this works: a bluebook
149
+ # declares shape (attribute types, patterns, closed sets, VO
114
150
  # invariants — see docs/decisions/0009), and shape is enforced by
115
- # ONE coercion door every domain's every attribute passes through
151
+ # one coercion door every domain's every attribute passes through
116
152
  # (`Runtime::Value.build`, via value/coercion.rb + value/admission.rb)
117
153
  # regardless of which hecksagon later binds the aggregate to Memory,
118
154
  # Postgres, or anything else. A value that violated its own declared
119
- # pattern, invariant, or closed set could never be COERCED, so it
120
- # could never be STORED, so it could never appear in a replay's own
155
+ # pattern, invariant, or closed set could never be coerced, so it
156
+ # could never be stored, so it could never appear in a replay's own
121
157
  # `:instances` to be caught violating it. Checking for it after the
122
158
  # fact would be watching for something the construction path already
123
159
  # made impossible.
124
160
  #
125
- # NOT a place to hide a real gap — a feature belongs here only once
126
- # the SPECIFIC enforcing code path has been read and confirmed, the
161
+ # Not a place to hide a real gap — a feature belongs here only once
162
+ # the specific enforcing code path has been read and confirmed, the
127
163
  # same discipline `spec/fuzzing/meta_domain_coverage_spec.rb` demands
128
164
  # of `KNOWN_GAPS` in the other direction. `Entity#identified_by` was
129
- # checked FOR this category once before and found NOT to qualify —
165
+ # checked for this category once before and found not to qualify —
130
166
  # `command_interpreter.rb`'s `AlreadyExists` refusal was given to
131
- # every CREATING AGGREGATE command uniformly, and MutationApplier
167
+ # every creating aggregate command uniformly, and MutationApplier
132
168
  # (command_interpreter/mutation_applier.rb) had no matching check on
133
169
  # an entity's own append. It does now: #check_entity_collision runs
134
170
  # unconditionally on both branches an entity identity can arrive by
@@ -154,13 +190,18 @@ module Hecks
154
190
  "(command_interpreter.rb, command.creates?) for every creating command uniformly, " \
155
191
  "before a duplicate id can ever be stored — collision is refused at the door, not " \
156
192
  "produced and later caught",
157
- "Entity#identified_by" => "MutationApplier#check_entity_collision (command_interpreter/mutation_applier.rb) " \
158
- "checks Array(current) against every part of the entity's own identity before an " \
159
- "append can land, on both branches identity arrives by (caller-supplied, or " \
160
- "composite) the same AlreadyExists refusal Aggregate#identified_by gets above, " \
161
- "one level down. Auto-minted entities never reach the check (current.size + 1 " \
162
- "can't repeat unless something remove:s from the list between mints, which no " \
163
- "real domain does today see the comment on #entity_element itself)",
193
+ "Entity#identified_by" => "EntityElement.check_entity_collision (runtime/entity_element.rb, moved there " \
194
+ "BUG#145 so both call sites share it) checks Array(current) against every part " \
195
+ "of the entity's own identity before an append can land MutationApplier#" \
196
+ "entity_element's aggregate-owned call (Workspace.boards, on both branches " \
197
+ "identity arrives by: caller-supplied, or composite) AND EntityElement#" \
198
+ "appended_to_element's entity-owned, nested-one-hop-further call (Board.cards " \
199
+ "unconditional, no auto-mint branch exists at that depth) the same " \
200
+ "AlreadyExists refusal Aggregate#identified_by gets above, one or two levels " \
201
+ "down. Auto-minted (aggregate-owned) entities never reach the check " \
202
+ "(current.size + 1 can't repeat unless something remove:s from the list between " \
203
+ "mints, which no real domain does today — see the comment on #entity_element " \
204
+ "itself)",
164
205
  "Command#attributes" => "command arguments are coerced through the SAME Value.build door as any other " \
165
206
  "attribute — an accepted dispatch's own args already passed pattern/admits/invariant checks",
166
207
  "Command#emits" => "CommandRules::Emission#emit iterates command.emits ITSELF to construct every " \
@@ -181,7 +222,7 @@ module Hecks
181
222
  # ValueObject), so this reads "Member#pairs", not "Member#shape" —
182
223
  # the free-text, un-parsed spelling a standalone root once needed
183
224
  # no longer exists at all, an entity's own element is never
184
- # serialized as text. "ValueObject#members" is the SAME fact
225
+ # serialized as text. "ValueObject#members" is the same fact
185
226
  # "ValueObject#rows" already counts, seen from the other side — a
186
227
  # value object cannot declare admitted rows without a members list
187
228
  # to hold them, and vice versa.
@@ -189,8 +230,8 @@ module Hecks
189
230
  "Member#pairs" => "one level into ValueObject#rows — same door"
190
231
  }.freeze
191
232
 
192
- # THE STANDARD BATTERY, run over one replayed history — everything
193
- # except determinism, which needs to replay TWICE itself and so
233
+ # The standard battery, run over one replayed history — everything
234
+ # except determinism, which needs to replay twice itself and so
194
235
  # takes the steps directly rather than a single history.
195
236
  def check(history)
196
237
  { lifecycle_values_are_declared: lifecycle_values_are_declared(history),
@@ -205,8 +246,12 @@ module Hecks
205
246
  paging_offset_partitions_correctly: paging_offset_partitions_correctly(history),
206
247
  lifecycle_guard_and_given_violations_are_refused: lifecycle_guard_and_given_violations_are_refused(history),
207
248
  authorize_scopes_or_refuses: authorize_scopes_or_refuses(history),
249
+ commands_respect_tenant_scope: commands_respect_tenant_scope(history),
208
250
  dispatch_binding_fidelity: dispatch_binding_fidelity(history),
209
- mutations_match_recompute: mutations_match_recompute(history) }
251
+ mutations_match_recompute: mutations_match_recompute(history),
252
+ dry_runs_leave_no_trace: dry_runs_leave_no_trace(history),
253
+ corrections_reference_an_emitted_event: corrections_reference_an_emitted_event(history),
254
+ outbox_rows_match_reactions: outbox_rows_match_reactions(history) }
210
255
  end
211
256
  end
212
257
  end
@@ -0,0 +1,152 @@
1
+ require "yaml"
2
+
3
+ module Hecks
4
+ module Fuzzing
5
+ # Reads `qa/settings.yml` — the hecks_qa practice's own dials, now
6
+ # data in a file rather than Ruby constant literals. The why of each
7
+ # dial (what it does, who reads it) stays exactly one place: the
8
+ # comments on `QualityControlDials` in `qa/bluebook/quality_control.
9
+ # bluebook`, which now sources every value from an instance of this
10
+ # class instead of writing it inline. This class is only the loading
11
+ # and the validation — no dial policy lives here.
12
+ #
13
+ # **Fails loud, not quiet** — the whole practice's own opening line
14
+ # ("the enemy is the quiet divergence") applies to its own settings
15
+ # file too: a missing key, an extra key nothing recognises, or a
16
+ # value of the wrong shape all raise immediately, at load time
17
+ # (which is bluebook-load time, i.e. `Hecks.boot`), naming exactly
18
+ # what's wrong — never a `nil` dial silently reaching a script that
19
+ # assumes a number.
20
+ #
21
+ # **Plain data in, frozen data out**. `.load` parses the YAML with
22
+ # `Psych.safe_load_file` (no custom tags, no arbitrary Ruby objects)
23
+ # and hands back an instance whose accessors are the exact values a
24
+ # human wrote in the file — a `Hash`/`Array` for the nested dials,
25
+ # never a second, richer wrapper type nothing else in this practice
26
+ # expects.
27
+ class QaSettings
28
+ # One entry per dial this class knows about: the accessor name
29
+ # (matching `qa/settings.yml`'s own key, and `QualityControlDials`'
30
+ # constant name snake_cased) mapped to the class (or classes) a
31
+ # valid value must be an instance of. `TrueClass`/`FalseClass`
32
+ # both name a boolean dial — Ruby has no single class both `true`
33
+ # and `false` share. `Numeric` admits both an Integer and a Float
34
+ # for a fraction dial (`0` and `0.0` are both a human plausibly
35
+ # types for "off").
36
+ EXPECTED_TYPES = {
37
+ cadence_seconds: Integer,
38
+ pr_cap_per_day: Integer,
39
+ widening_tiers: Array,
40
+ sweep_max_parallel: Integer,
41
+ liveness_fallback_seconds: Integer,
42
+ draft_only: [TrueClass, FalseClass],
43
+ auto_merge: [TrueClass, FalseClass],
44
+ branch_prefix: String,
45
+ adversarial_fraction: Numeric,
46
+ guided_generation: [TrueClass, FalseClass],
47
+ corpus_splice_probability: Numeric,
48
+ favor_rare_verbs: Integer,
49
+ self_consistency_checks: [TrueClass, FalseClass],
50
+ shrink_budget: Integer,
51
+ yield_weight_seconds: Integer,
52
+ yield_decay_percent: Integer,
53
+ rotation_stale_floor_seconds: Integer,
54
+ persistence_parity_seed_cap: Integer,
55
+ concurrency_seed_cap: Integer,
56
+ adapter_parity_pairs: Hash,
57
+ modes: Hash,
58
+ role_draw_probability: Numeric,
59
+ dry_run_fraction: Numeric,
60
+ generated_domains_per_tick: Integer,
61
+ generated_domains_rust: [TrueClass, FalseClass],
62
+ generated_domain_seeds: Integer,
63
+ structural_refusal_boundary: Array
64
+ }.freeze
65
+
66
+ attr_reader(*EXPECTED_TYPES.keys)
67
+
68
+ # **The real file, always** — resolved off this file's own `__dir__`
69
+ # (lib/hecks/fuzzing/), never off the caller's. `QualityControlDials`
70
+ # is defined inside `qa/bluebook/quality_control.bluebook`, and that
71
+ # exact directory gets copied to a tmpdir for every isolated/replayed
72
+ # boot (`Hecks::Fuzzing::IsolatedBoot#copy_dereferencing` copies only
73
+ # `qa/bluebook`'s own contents, never its parent `qa/`) — a path
74
+ # resolved from the bluebook's own `__dir__` would silently point at
75
+ # a copy with no `settings.yml` beside it at all. `qa/settings.yml`
76
+ # is read-only, human-edited data with no lifecycle (see this class's
77
+ # own header) — there is no isolation reason to ever read a copy of
78
+ # it, real boot or fuzzed one, so every caller gets the one real file
79
+ # by default. `qa_settings_spec.rb` passes its own fixture paths
80
+ # explicitly instead, the same way every other test in this practice
81
+ # that needs a non-default dial passes one in rather than mutating
82
+ # global state.
83
+ DEFAULT_PATH = File.expand_path("../../../qa/settings.yml", __dir__)
84
+
85
+ class << self
86
+ def load(path = DEFAULT_PATH)
87
+ raise ArgumentError, "qa settings file not found: #{path}" unless File.file?(path)
88
+
89
+ raw = begin
90
+ YAML.safe_load_file(path, symbolize_names: true)
91
+ rescue Psych::SyntaxError => e
92
+ raise ArgumentError, "#{path} is not valid YAML: #{e.message}"
93
+ end
94
+ raise ArgumentError, "#{path} must be a YAML mapping at the top level, got #{raw.class}" unless raw.is_a?(Hash)
95
+
96
+ new(raw, path)
97
+ end
98
+ end
99
+
100
+ def initialize(raw, path)
101
+ missing = EXPECTED_TYPES.keys - raw.keys
102
+ raise ArgumentError, "#{path} is missing #{missing.sort.join(', ')}" if missing.any?
103
+
104
+ extra = raw.keys - EXPECTED_TYPES.keys
105
+ if extra.any?
106
+ raise ArgumentError,
107
+ "#{path} declares unknown key(s) #{extra.sort.join(', ')} — " \
108
+ "Hecks::Fuzzing::QaSettings::EXPECTED_TYPES doesn't recognise them"
109
+ end
110
+
111
+ EXPECTED_TYPES.each do |key, expected|
112
+ value = raw.fetch(key)
113
+ expected_classes = Array(expected)
114
+ unless expected_classes.any? { |klass| value.is_a?(klass) }
115
+ raise ArgumentError,
116
+ "#{path}: #{key} must be a #{expected_classes.map(&:name).join(' or ')}, " \
117
+ "got #{value.class} (#{value.inspect})"
118
+ end
119
+
120
+ instance_variable_set(:"@#{key}", value)
121
+ end
122
+
123
+ symbolize_adapter_parity_pairs!(path)
124
+ freeze_values!
125
+ end
126
+
127
+ private
128
+
129
+ # `left:`/`right:` name adapters `IsolatedBoot` case-matches by
130
+ # symbol (`case adapter when :memory ...`), and YAML has no way to
131
+ # spell a bare Ruby Symbol as a mapping value — only
132
+ # `symbolize_names:` turns a key into one. So `adapter_parity_
133
+ # pairs` is the one dial that needs a coercion step after the
134
+ # type check above, rather than every dial growing one.
135
+ def symbolize_adapter_parity_pairs!(path)
136
+ @adapter_parity_pairs = @adapter_parity_pairs.to_h do |mode, pair|
137
+ unless pair.is_a?(Hash) && pair.key?(:left) && pair.key?(:right)
138
+ raise ArgumentError,
139
+ "#{path}: adapter_parity_pairs.#{mode} must have both left and right, got #{pair.inspect}"
140
+ end
141
+
142
+ [mode, { left: pair[:left].to_sym, right: pair[:right].to_sym }]
143
+ end
144
+ end
145
+
146
+ def freeze_values!
147
+ EXPECTED_TYPES.each_key { |key| instance_variable_get(:"@#{key}").freeze }
148
+ freeze
149
+ end
150
+ end
151
+ end
152
+ end