hecks 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (373) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +72 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +83 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
  7. data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
  9. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  10. data/lib/hecks/adapters/driven/heki.rb +106 -9
  11. data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
  12. data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
  13. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  15. data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
  16. data/lib/hecks/adapters/driven/lambda.rb +82 -35
  17. data/lib/hecks/adapters/driven/local_storage.rb +83 -10
  18. data/lib/hecks/adapters/driven/memory.rb +205 -9
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  24. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  25. data/lib/hecks/adapters/driven/prism.rb +50 -11
  26. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  31. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  32. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  33. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  34. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  35. data/lib/hecks/adapters/driven.rb +6 -4
  36. data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
  37. data/lib/hecks/behaviors/dsl.rb +60 -2
  38. data/lib/hecks/behaviors/expectations.rb +190 -29
  39. data/lib/hecks/behaviors/ir.rb +12 -1
  40. data/lib/hecks/behaviors/rspec.rb +9 -1
  41. data/lib/hecks/behaviors/runner.rb +21 -2
  42. data/lib/hecks/behaviors.rb +9 -1
  43. data/lib/hecks/bluebook/aggregate.rb +43 -13
  44. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
  45. data/lib/hecks/bluebook/assembly/build.rb +26 -5
  46. data/lib/hecks/bluebook/assembly/contract.rb +98 -23
  47. data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
  48. data/lib/hecks/bluebook/assembly/marks.rb +159 -30
  49. data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
  50. data/lib/hecks/bluebook/assembly.rb +32 -14
  51. data/lib/hecks/bluebook/attribute.rb +26 -12
  52. data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
  53. data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
  54. data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
  55. data/lib/hecks/bluebook/behaviour/command.rb +55 -25
  56. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
  57. data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
  58. data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
  59. data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
  60. data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
  61. data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
  62. data/lib/hecks/bluebook/behaviour/query.rb +6 -1
  63. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
  64. data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
  65. data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
  66. data/lib/hecks/bluebook/capabilities.rb +27 -0
  67. data/lib/hecks/bluebook/chapter.rb +51 -9
  68. data/lib/hecks/bluebook/command.rb +62 -17
  69. data/lib/hecks/bluebook/domain_port.rb +34 -9
  70. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  71. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  72. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
  73. data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
  74. data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
  75. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
  76. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
  77. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  78. data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
  79. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  80. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  81. data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
  82. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  83. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
  84. data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
  85. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  86. data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
  87. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  88. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  89. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
  90. data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
  91. data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
  92. data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
  93. data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
  94. data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
  95. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  96. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  97. data/lib/hecks/bluebook/entity.rb +40 -11
  98. data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
  99. data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
  100. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
  101. data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
  102. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
  103. data/lib/hecks/bluebook/expression/resolver.rb +369 -128
  104. data/lib/hecks/bluebook/hexagon.rb +35 -1
  105. data/lib/hecks/bluebook/lifecycle.rb +12 -1
  106. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
  107. data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
  108. data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
  109. data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
  110. data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
  111. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
  112. data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
  113. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
  114. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
  115. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
  116. data/lib/hecks/bluebook/meta_validator.rb +235 -139
  117. data/lib/hecks/bluebook/model_check.rb +434 -104
  118. data/lib/hecks/bluebook/pattern_subset.rb +32 -10
  119. data/lib/hecks/bluebook/policy.rb +15 -13
  120. data/lib/hecks/bluebook/process_manager.rb +27 -14
  121. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  122. data/lib/hecks/bluebook/project_loader.rb +40 -0
  123. data/lib/hecks/bluebook/project_register.rb +50 -6
  124. data/lib/hecks/bluebook/query.rb +31 -4
  125. data/lib/hecks/bluebook/read_model.rb +35 -15
  126. data/lib/hecks/bluebook/reference.rb +26 -13
  127. data/lib/hecks/bluebook/smoke_test.rb +46 -23
  128. data/lib/hecks/bluebook/synthesizer.rb +46 -12
  129. data/lib/hecks/bluebook/translation.rb +34 -5
  130. data/lib/hecks/bluebook/value_object.rb +29 -11
  131. data/lib/hecks/bluebook.rb +5 -6
  132. data/lib/hecks/codemod.rb +138 -50
  133. data/lib/hecks/construct.rb +21 -7
  134. data/lib/hecks/corpus.rb +438 -0
  135. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  136. data/lib/hecks/doc/reference.rb +200 -31
  137. data/lib/hecks/embryonaut_bluebook.rb +38 -15
  138. data/lib/hecks/facade/cli_door.rb +69 -10
  139. data/lib/hecks/facade/cli_runner.rb +105 -24
  140. data/lib/hecks/facade/command_request.rb +23 -0
  141. data/lib/hecks/facade/handle.rb +155 -35
  142. data/lib/hecks/facade/json_door.rb +106 -25
  143. data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
  144. data/lib/hecks/facade/surface/chapter.rb +26 -17
  145. data/lib/hecks/facade/surface.rb +16 -3
  146. data/lib/hecks/facade.rb +15 -4
  147. data/lib/hecks/forms/app.rb +46 -30
  148. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  149. data/lib/hecks/forms/field_renderer.rb +142 -6
  150. data/lib/hecks/forms/field_shape.rb +183 -19
  151. data/lib/hecks/forms/html.rb +51 -7
  152. data/lib/hecks/forms/index_renderer.rb +14 -2
  153. data/lib/hecks/forms/page.rb +14 -0
  154. data/lib/hecks/forms/params.rb +120 -23
  155. data/lib/hecks/forms/port_argument.rb +14 -2
  156. data/lib/hecks/forms/query_form_renderer.rb +65 -2
  157. data/lib/hecks/forms/record_renderer.rb +60 -2
  158. data/lib/hecks/forms/record_table.rb +28 -1
  159. data/lib/hecks/forms/reference_options.rb +24 -0
  160. data/lib/hecks/forms/value_object_shape.rb +13 -3
  161. data/lib/hecks/forms.rb +24 -4
  162. data/lib/hecks/fqn.rb +59 -1
  163. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  164. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  165. data/lib/hecks/framework/oidc.json +15 -0
  166. data/lib/hecks/framework.rb +79 -25
  167. data/lib/hecks/freezer.rb +27 -11
  168. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
  169. data/lib/hecks/fuzzing/combination_miner.rb +178 -0
  170. data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
  171. data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
  172. data/lib/hecks/fuzzing/differential.rb +192 -0
  173. data/lib/hecks/fuzzing/domain_generator.rb +871 -0
  174. data/lib/hecks/fuzzing/era_boundary.rb +68 -19
  175. data/lib/hecks/fuzzing/form_census.rb +121 -20
  176. data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
  177. data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
  178. data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
  179. data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
  180. data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
  181. data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
  182. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
  183. data/lib/hecks/fuzzing/properties/guards.rb +86 -42
  184. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
  185. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
  186. data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
  187. data/lib/hecks/fuzzing/properties/querying.rb +82 -28
  188. data/lib/hecks/fuzzing/properties.rb +84 -51
  189. data/lib/hecks/fuzzing/qa_settings.rb +164 -0
  190. data/lib/hecks/fuzzing/replay.rb +241 -133
  191. data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
  192. data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
  193. data/lib/hecks/fuzzing/self_consistency.rb +315 -137
  194. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
  195. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  196. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  197. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  198. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
  199. data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
  200. data/lib/hecks/fuzzing/shrinker.rb +309 -0
  201. data/lib/hecks/fuzzing/structural_skips.rb +37 -130
  202. data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
  203. data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
  204. data/lib/hecks/fuzzing/value_generator.rb +110 -22
  205. data/lib/hecks/fuzzing.rb +5 -0
  206. data/lib/hecks/grammar/evolve.rb +188 -12
  207. data/lib/hecks/grammar.rb +53 -7
  208. data/lib/hecks/ir.rb +51 -20
  209. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  210. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  211. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  212. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  213. data/lib/hecks/language/oidc.json +5 -0
  214. data/lib/hecks/literal.rb +41 -9
  215. data/lib/hecks/naming.rb +112 -31
  216. data/lib/hecks/ports/access_control.rb +53 -2
  217. data/lib/hecks/ports/agent/answers.rb +83 -6
  218. data/lib/hecks/ports/agent.rb +119 -35
  219. data/lib/hecks/ports/authentication.rb +44 -4
  220. data/lib/hecks/ports/authorization.rb +50 -11
  221. data/lib/hecks/ports/clock.rb +42 -23
  222. data/lib/hecks/ports/extraction.rb +16 -0
  223. data/lib/hecks/ports/identity_assignment.rb +23 -2
  224. data/lib/hecks/ports/identity_generation.rb +17 -3
  225. data/lib/hecks/ports/identity_resolution.rb +17 -1
  226. data/lib/hecks/ports/key_vault.port +6 -0
  227. data/lib/hecks/ports/key_vault.rb +58 -0
  228. data/lib/hecks/ports/loading.rb +4 -0
  229. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  230. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  231. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  232. data/lib/hecks/ports/persistence/execution.rb +4 -0
  233. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  234. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  235. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  236. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  237. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  238. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  239. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  250. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  251. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  252. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
  253. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  254. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  255. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  256. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  257. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  258. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
  259. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  260. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  261. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  262. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
  263. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
  264. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  265. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
  266. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  267. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  268. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  269. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  270. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  271. data/lib/hecks/ports/persistence.rb +36 -1
  272. data/lib/hecks/ports/projection.rb +61 -7
  273. data/lib/hecks/ports/query/in_memory.rb +41 -8
  274. data/lib/hecks/ports/query/ordering.rb +21 -6
  275. data/lib/hecks/ports/query.rb +35 -0
  276. data/lib/hecks/ports.rb +1 -0
  277. data/lib/hecks/projections/bootstrap_table.rb +147 -0
  278. data/lib/hecks/projections/diagrams.rb +315 -79
  279. data/lib/hecks/projections/glossary/html.rb +91 -3
  280. data/lib/hecks/projections/glossary/markdown.rb +50 -3
  281. data/lib/hecks/projections/glossary/mermaid.rb +49 -1
  282. data/lib/hecks/projections/glossary/sections.rb +1 -1
  283. data/lib/hecks/projections/glossary/sentences.rb +110 -5
  284. data/lib/hecks/projections/glossary.rb +173 -31
  285. data/lib/hecks/projections/ir.rb +1 -1
  286. data/lib/hecks/projections/model/deviations.rb +62 -17
  287. data/lib/hecks/projections/model.rb +75 -21
  288. data/lib/hecks/projections/oidc.rb +45 -8
  289. data/lib/hecks/projections/parser_table.rb +53 -12
  290. data/lib/hecks/projections/reference.rb +15 -3
  291. data/lib/hecks/projections/rust_vocabulary.rb +646 -0
  292. data/lib/hecks/projections/shape.rb +10 -2
  293. data/lib/hecks/projections/statements.rb +69 -22
  294. data/lib/hecks/projections/vocabulary.rb +26 -9
  295. data/lib/hecks/projections.rb +5 -3
  296. data/lib/hecks/projector/cli_projector.rb +245 -37
  297. data/lib/hecks/projector/docs_projector.rb +154 -28
  298. data/lib/hecks/projector/exporter.rb +104 -29
  299. data/lib/hecks/projector/ir_projector.rb +7 -1
  300. data/lib/hecks/projector/narrate_projector.rb +144 -23
  301. data/lib/hecks/projector/target.rb +42 -18
  302. data/lib/hecks/projector.rb +86 -18
  303. data/lib/hecks/query_ir.rb +94 -47
  304. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  305. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  306. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  307. data/lib/hecks/query_specification/common/null_policy.rb +61 -15
  308. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  309. data/lib/hecks/query_specification/common/options.rb +25 -0
  310. data/lib/hecks/query_specification/field_path.rb +69 -15
  311. data/lib/hecks/query_specification/hop_path.rb +57 -20
  312. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  313. data/lib/hecks/rendering.rb +9 -3
  314. data/lib/hecks/router/namespace_installer.rb +16 -3
  315. data/lib/hecks/router.rb +56 -1
  316. data/lib/hecks/runtime/aggregate_lock.rb +20 -11
  317. data/lib/hecks/runtime/boot_gates.rb +21 -3
  318. data/lib/hecks/runtime/caller.rb +40 -8
  319. data/lib/hecks/runtime/capability_graph.rb +13 -2
  320. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
  321. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
  322. data/lib/hecks/runtime/command_interpreter.rb +195 -113
  323. data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
  324. data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
  325. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  326. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  327. data/lib/hecks/runtime/command_rules/references.rb +145 -55
  328. data/lib/hecks/runtime/command_rules.rb +3 -0
  329. data/lib/hecks/runtime/dependency_planning.rb +56 -11
  330. data/lib/hecks/runtime/dispatcher.rb +263 -92
  331. data/lib/hecks/runtime/entity_element.rb +304 -67
  332. data/lib/hecks/runtime/entity_interpreter.rb +149 -88
  333. data/lib/hecks/runtime/errors.rb +37 -23
  334. data/lib/hecks/runtime/event.rb +15 -10
  335. data/lib/hecks/runtime/identity.rb +91 -23
  336. data/lib/hecks/runtime/instance.rb +103 -18
  337. data/lib/hecks/runtime/interpreting.rb +24 -16
  338. data/lib/hecks/runtime/invocation.rb +358 -0
  339. data/lib/hecks/runtime/loader.rb +105 -19
  340. data/lib/hecks/runtime/outbox.rb +164 -26
  341. data/lib/hecks/runtime/policy_interpreter.rb +73 -60
  342. data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
  343. data/lib/hecks/runtime/query_interpreter.rb +93 -74
  344. data/lib/hecks/runtime/reaction_invocation.rb +73 -28
  345. data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
  346. data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
  347. data/lib/hecks/runtime/reference_hop.rb +48 -6
  348. data/lib/hecks/runtime/refusal_wording.rb +142 -115
  349. data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
  350. data/lib/hecks/runtime/registry/verification.rb +153 -28
  351. data/lib/hecks/runtime/registry.rb +202 -32
  352. data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
  353. data/lib/hecks/runtime/routing.rb +36 -154
  354. data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
  355. data/lib/hecks/runtime/saga_interpreter.rb +90 -76
  356. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  357. data/lib/hecks/runtime/tenant_check.rb +33 -13
  358. data/lib/hecks/runtime/tenant_scope.rb +23 -5
  359. data/lib/hecks/runtime/value/admission.rb +75 -30
  360. data/lib/hecks/runtime/value/coercion.rb +555 -142
  361. data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
  362. data/lib/hecks/runtime/value.rb +71 -21
  363. data/lib/hecks/runtime.rb +39 -7
  364. data/lib/hecks/storehouse.rb +368 -72
  365. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  366. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  367. data/lib/hecks/version.rb +3 -3
  368. data/lib/hecks/vocabulary.rb +205 -4
  369. data/lib/hecks.rb +91 -11
  370. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
  371. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
  372. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
  373. metadata +28 -2
@@ -14,32 +14,36 @@ require_relative "properties/outbox"
14
14
 
15
15
  module Hecks
16
16
  module Fuzzing
17
- # Declared properties, checked over a REPLAYED history — the other
17
+ # Declared properties, checked over a replayed history — the other
18
18
  # half of property-based testing the fuzzer was missing: it already
19
19
  # generates and (with bin/fuzz's shrinker) minimizes, but checked
20
20
  # nothing beyond "did the interpreter crash" and "did the replay
21
21
  # match the claim." A property here is a fact that should hold of
22
- # ANY history a valid domain produces, independent of which seed
22
+ # any history a valid domain produces, independent of which seed
23
23
  # produced it.
24
24
  #
25
+ # ## Property shape
26
+ #
25
27
  # Each property is `name => ->(history) { true/false, or a message
26
28
  # string naming what broke }` — a truthy return (including `true`)
27
- # is a pass; a String return is a failure, and the string IS the
29
+ # is a pass; a String return is a failure, and the string is the
28
30
  # finding. `history` is Replay's return shape.
29
31
  #
30
- # EVERY PROPERTY DECLARES THE LANGUAGE FEATURE IT COVERS, in
32
+ # ## Feature coverage
33
+ #
34
+ # Every property declares the language feature it covers, in
31
35
  # `FEATURE_COVERAGE` below — a "Construct#attribute" pair spelled
32
36
  # exactly as `Bluebook::MetaValidator.grammar_registry` names it,
33
- # the SAME meta-domain that judges every real bluebook (see that
37
+ # the same meta-domain that judges every real bluebook (see that
34
38
  # module's own header: "the language IS the source"). That is the
35
39
  # link this file exists to make real: a construct the language
36
40
  # declares is a fact `spec/meta_domain_coverage_spec.rb` can
37
41
  # enumerate on its own, without anyone re-typing the list here —
38
42
  # so a new attribute added to `language/bluebook/*.bluebook` shows
39
- # up in that spec as UNCLAIMED the moment it lands, not whenever
43
+ # up in that spec as unclaimed the moment it lands, not whenever
40
44
  # someone remembers to go looking. Claiming a feature here is a
41
45
  # deliberate act (a real property, checked at least once failing
42
- # AND once passing — `spec/fuzzing/properties_spec.rb`'s own
46
+ # and once passing — `spec/fuzzing/properties_spec.rb`'s own
43
47
  # discipline) or an explicit, reasoned exemption in that same
44
48
  # spec — never silence.
45
49
  module Properties
@@ -64,11 +68,11 @@ module Hecks
64
68
 
65
69
  module_function
66
70
 
67
- # WHICH LANGUAGE FEATURE EACH PROPERTY IS ANSWERABLE FOR. Not
71
+ # Which language feature each property is answerable for. Not
68
72
  # exhaustive of everything a property's body happens to touch —
69
73
  # `Command#attributes`, say, is exercised by nearly every property
70
- # here without being what any of them was WRITTEN to guard — but
71
- # exhaustive of the feature that would go UNCHECKED if this
74
+ # here without being what any of them was written to guard — but
75
+ # exhaustive of the feature that would go unchecked if this
72
76
  # property did not exist. That is the question the coverage gate
73
77
  # actually asks.
74
78
  FEATURE_COVERAGE = {
@@ -80,17 +84,17 @@ module Hecks
80
84
  authorize_scopes_or_refuses: %w[Query#options],
81
85
  guard_refusals_are_declared: %w[Command#givens Command#ensures],
82
86
  lifecycle_guard_and_given_violations_are_refused: %w[Command#from Aggregate#preconditions Entity#preconditions],
83
- # Dispatch#command_name/Dispatch#with_spec are NOT claimable
84
- # feature names — META_DOMAIN_ALL_FEATURES only walks ONE level
87
+ # Dispatch#command_name/Dispatch#with_spec are not claimable
88
+ # feature names — META_DOMAIN_ALL_FEATURES only walks one level
85
89
  # of entity nesting (`agg.entities.flat_map`, meta_domain_
86
- # coverage_spec.rb), and Dispatch sits TWO deep (ProcessManager
90
+ # coverage_spec.rb), and Dispatch sits two deep (ProcessManager
87
91
  # -> Handler -> Dispatch), so those strings never exist there
88
92
  # to claim — a pre-existing meta-domain coverage-generation gap,
89
93
  # found here (their old META_DOMAIN_KNOWN_GAPS entries were
90
94
  # themselves already-orphaned strings no completeness check ever
91
95
  # verified, since KNOWN_GAPS has no "never lets a gap rot" check
92
96
  # the way FEATURE_COVERAGE/GUARANTEED_BY_CONSTRUCTION both do).
93
- # This property still closes the REAL behavior both would have
97
+ # This property still closes the real behavior both would have
94
98
  # named — a Dispatch's own command_name/with_spec are exactly
95
99
  # what dispatch_args resolves and this property checks — the
96
100
  # grammar just has no feature string for either one.
@@ -102,58 +106,78 @@ module Hecks
102
106
  aggregation_matches_recompute: %w[ReadModel#count ReadModel#median_field],
103
107
  stored_records_satisfy_declared_invariants: %w[Aggregate#invariants Entity#invariants],
104
108
  group_by_matches_recompute: %w[ReadModel#group_by],
105
- # A RUNTIME DOOR, NOT A GRAMMAR CONSTRUCT — `Dispatcher#dry_run?`
109
+ # A runtime door, not a grammar construct — `Dispatcher#dry_run?`
106
110
  # is something an application asks of a booted domain, not a
107
111
  # word a bluebook can declare, so there is no feature string
108
112
  # for it to claim. Listed (empty) rather than omitted so the
109
113
  # discipline this table states — every property names what it
110
114
  # is answerable for — has no silent exception.
111
115
  dry_runs_leave_no_trace: [],
112
- # ANOTHER RUNTIME DOOR, NOT A GRAMMAR CONSTRUCT — same reasoning
116
+ # Another runtime door, not a grammar construct — same reasoning
113
117
  # as dry_runs_leave_no_trace right above: `Runtime::Outbox` is
114
118
  # something a persistence adapter provides underneath a booted
115
119
  # domain, never a word a bluebook declares.
116
- outbox_rows_match_reactions: []
120
+ outbox_rows_match_reactions: [],
121
+ # **The `corrects` mutation's own target** — this property reads
122
+ # `command.mutations.select { op == :corrects }` and asks whether
123
+ # the event each one names was ever actually emitted, so the
124
+ # feature it answers for is the mutation list, the same one
125
+ # `mutations_match_recompute` reads for a different question.
126
+ # (Not `Command#references`: that field is the dangling-reference
127
+ # question no property asks yet, and it stays a named gap.)
128
+ corrections_reference_an_emitted_event: %w[Command#mutations],
129
+ # No feature string exists for what this one reads. It depends on
130
+ # an argument's own `relationship` (which reference-typed argument
131
+ # points at which aggregate) — but `Argument` is a value object,
132
+ # and the meta-domain walk enumerates aggregate and entity fields
133
+ # only, so no `Argument#…` name is claimable. The declaration side
134
+ # it shares with queries, `authorize …, tenant:`, is
135
+ # `Query#options`, already claimed by `authorize_scopes_or_refuses`;
136
+ # claiming it twice would say this property covers a query
137
+ # question it never asks. Listed (empty) rather than omitted, the
138
+ # same discipline the two runtime doors above keep.
139
+ commands_respect_tenant_scope: []
117
140
  }.freeze
118
141
 
119
- # FEATURES A REPLAY PROPERTY COULD NEVER CATCH VIOLATED, because the
120
- # RUNTIME'S OWN CONSTRUCTION makes the violation impossible to
142
+ # Features a replay property could never catch violated, because the
143
+ # runtime's own construction makes the violation impossible to
121
144
  # produce in the first place — not "untested," but unfalsifiable by
122
145
  # a history, the same class of guarantee this codebase already
123
- # states for identity ("NOTHING IS MINTED" — command_interpreter.rb's
124
- # own header) and now generalises. Each entry names the ONE place in
146
+ # states for identity ("nothing is minted" — command_interpreter.rb's
147
+ # own header) and now generalises. Each entry names the one place in
125
148
  # the runtime that makes it true, universally, for every domain and
126
149
  # every adapter — never per-domain logic a future domain could
127
150
  # accidentally route around.
128
151
  #
129
- # THE BLUEBOOK/HECKSAGON BOUNDARY IS WHY THIS WORKS: a bluebook
130
- # declares SHAPE (attribute types, patterns, closed sets, VO
152
+ # The bluebook/hecksagon boundary is why this works: a bluebook
153
+ # declares shape (attribute types, patterns, closed sets, VO
131
154
  # invariants — see docs/decisions/0009), and shape is enforced by
132
- # ONE coercion door every domain's every attribute passes through
155
+ # one coercion door every domain's every attribute passes through
133
156
  # (`Runtime::Value.build`, via value/coercion.rb + value/admission.rb)
134
157
  # regardless of which hecksagon later binds the aggregate to Memory,
135
158
  # Postgres, or anything else. A value that violated its own declared
136
- # pattern, invariant, or closed set could never be COERCED, so it
137
- # could never be STORED, so it could never appear in a replay's own
159
+ # pattern, invariant, or closed set could never be coerced, so it
160
+ # could never be stored, so it could never appear in a replay's own
138
161
  # `:instances` to be caught violating it. Checking for it after the
139
162
  # fact would be watching for something the construction path already
140
163
  # made impossible.
141
164
  #
142
- # NOT a place to hide a real gap — a feature belongs here only once
143
- # the SPECIFIC enforcing code path has been read and confirmed, the
165
+ # Not a place to hide a real gap — a feature belongs here only once
166
+ # the specific enforcing code path has been read and confirmed, the
144
167
  # same discipline `spec/fuzzing/meta_domain_coverage_spec.rb` demands
145
- # of `KNOWN_GAPS` in the other direction. `Entity#identified_by` was
146
- # checked FOR this category once before and found NOT to qualify —
147
- # `command_interpreter.rb`'s `AlreadyExists` refusal was given to
148
- # every CREATING AGGREGATE command uniformly, and MutationApplier
149
- # (command_interpreter/mutation_applier.rb) had no matching check on
150
- # an entity's own append. It does now: #check_entity_collision runs
151
- # unconditionally on both branches an entity identity can arrive by
152
- # (caller-supplied, or composite — the two the auto-mint branch
153
- # doesn't cover), the same way command_interpreter#hydrate's own
154
- # check is unconditional for every creating aggregate command. Real,
155
- # confirmed live before the fix (SafeDepositBox's Visit/KeyIssuance —
156
- # see spec/runtime/safe_deposit_box_spec.rb).
168
+ # of `KNOWN_GAPS` in the other direction. `Entity#identified_by`
169
+ # illustrates the discipline: `command_interpreter.rb`'s
170
+ # `AlreadyExists` refusal covers every creating aggregate command
171
+ # uniformly, but nothing else on its own covers an entity's own
172
+ # append — so this entry names the path that actually does,
173
+ # `MutationApplier#check_entity_collision` (command_interpreter/
174
+ # mutation_applier.rb), which runs unconditionally on both branches
175
+ # an entity identity can arrive by (caller-supplied, or composite —
176
+ # the two the auto-mint branch doesn't cover), the same way
177
+ # `command_interpreter#hydrate`'s own check is unconditional for
178
+ # every creating aggregate command. The collision this closes is
179
+ # real, not hypothetical: SafeDepositBox's Visit/KeyIssuance entities
180
+ # reproduce it, confirmed by spec/runtime/safe_deposit_box_spec.rb.
157
181
  GUARANTEED_BY_CONSTRUCTION = {
158
182
  "Aggregate#attributes" => "every field's pattern/closed-set/type passes through Value.build's one coercion " \
159
183
  "door (value/coercion.rb#check_patterns, value/admission.rb) before it can exist " \
@@ -171,13 +195,18 @@ module Hecks
171
195
  "(command_interpreter.rb, command.creates?) for every creating command uniformly, " \
172
196
  "before a duplicate id can ever be stored — collision is refused at the door, not " \
173
197
  "produced and later caught",
174
- "Entity#identified_by" => "MutationApplier#check_entity_collision (command_interpreter/mutation_applier.rb) " \
175
- "checks Array(current) against every part of the entity's own identity before an " \
176
- "append can land, on both branches identity arrives by (caller-supplied, or " \
177
- "composite) — the same AlreadyExists refusal Aggregate#identified_by gets above, " \
178
- "one level down. Auto-minted entities never reach the check (current.size + 1 " \
179
- "can't repeat unless something remove:s from the list between mints, which no " \
180
- "real domain does today — see the comment on #entity_element itself)",
198
+ "Entity#identified_by" => "EntityElement.check_entity_collision (runtime/entity_element.rb, moved there " \
199
+ "BUG#145 so both call sites share it) checks Array(current) against every part " \
200
+ "of the entity's own identity before an append can land — MutationApplier#" \
201
+ "entity_element's aggregate-owned call (Workspace.boards, on both branches " \
202
+ "identity arrives by: caller-supplied, or composite) AND EntityElement#" \
203
+ "appended_to_element's entity-owned, nested-one-hop-further call (Board.cards — " \
204
+ "unconditional, no auto-mint branch exists at that depth) — the same " \
205
+ "AlreadyExists refusal Aggregate#identified_by gets above, one or two levels " \
206
+ "down. Auto-minted (aggregate-owned) entities never reach the check " \
207
+ "(current.size + 1 can't repeat unless something remove:s from the list between " \
208
+ "mints, which no real domain does today — see the comment on #entity_element " \
209
+ "itself)",
181
210
  "Command#attributes" => "command arguments are coerced through the SAME Value.build door as any other " \
182
211
  "attribute — an accepted dispatch's own args already passed pattern/admits/invariant checks",
183
212
  "Command#emits" => "CommandRules::Emission#emit iterates command.emits ITSELF to construct every " \
@@ -198,7 +227,7 @@ module Hecks
198
227
  # ValueObject), so this reads "Member#pairs", not "Member#shape" —
199
228
  # the free-text, un-parsed spelling a standalone root once needed
200
229
  # no longer exists at all, an entity's own element is never
201
- # serialized as text. "ValueObject#members" is the SAME fact
230
+ # serialized as text. "ValueObject#members" is the same fact
202
231
  # "ValueObject#rows" already counts, seen from the other side — a
203
232
  # value object cannot declare admitted rows without a members list
204
233
  # to hold them, and vice versa.
@@ -206,9 +235,13 @@ module Hecks
206
235
  "Member#pairs" => "one level into ValueObject#rows — same door"
207
236
  }.freeze
208
237
 
209
- # THE STANDARD BATTERY, run over one replayed history — everything
210
- # except determinism, which needs to replay TWICE itself and so
211
- # takes the steps directly rather than a single history.
238
+ # Runs the standard property battery over one replayed history —
239
+ # everything except determinism, which needs to replay twice itself
240
+ # and so takes the steps directly rather than a single history.
241
+ #
242
+ # @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
243
+ # @return [Hash{Symbol => true, String}] each property name mapped to `true`
244
+ # (passed) or a message string naming what broke
212
245
  def check(history)
213
246
  { lifecycle_values_are_declared: lifecycle_values_are_declared(history),
214
247
  saga_advances_follow_declared_handlers: saga_advances_follow_declared_handlers(history),
@@ -0,0 +1,164 @@
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
+ # Loads and validates `qa/settings.yml` (or `path`), returning a frozen instance.
87
+ #
88
+ # @param path [String] path to the YAML settings file; defaults to `DEFAULT_PATH`
89
+ # @return [Hecks::Fuzzing::QaSettings] the validated, frozen settings
90
+ # @raise [ArgumentError] if `path` does not exist, is not valid YAML, is not a
91
+ # YAML mapping at the top level, is missing a required key, declares an
92
+ # unknown key, or gives a value the wrong type for its dial
93
+ def load(path = DEFAULT_PATH)
94
+ raise ArgumentError, "qa settings file not found: #{path}" unless File.file?(path)
95
+
96
+ raw = begin
97
+ YAML.safe_load_file(path, symbolize_names: true)
98
+ rescue Psych::SyntaxError => e
99
+ raise ArgumentError, "#{path} is not valid YAML: #{e.message}"
100
+ end
101
+ raise ArgumentError, "#{path} must be a YAML mapping at the top level, got #{raw.class}" unless raw.is_a?(Hash)
102
+
103
+ new(raw, path)
104
+ end
105
+ end
106
+
107
+ # @param raw [Hash] parsed YAML settings keyed by symbol, one entry per dial in
108
+ # `EXPECTED_TYPES`
109
+ # @param path [String] path to the settings file, used only in error messages
110
+ # @raise [ArgumentError] if `raw` is missing a required key, declares an unknown
111
+ # key, or gives a value the wrong type for its dial
112
+ def initialize(raw, path)
113
+ missing = EXPECTED_TYPES.keys - raw.keys
114
+ raise ArgumentError, "#{path} is missing #{missing.sort.join(', ')}" if missing.any?
115
+
116
+ extra = raw.keys - EXPECTED_TYPES.keys
117
+ if extra.any?
118
+ raise ArgumentError,
119
+ "#{path} declares unknown key(s) #{extra.sort.join(', ')} — " \
120
+ "Hecks::Fuzzing::QaSettings::EXPECTED_TYPES doesn't recognise them"
121
+ end
122
+
123
+ EXPECTED_TYPES.each do |key, expected|
124
+ value = raw.fetch(key)
125
+ expected_classes = Array(expected)
126
+ unless expected_classes.any? { |klass| value.is_a?(klass) }
127
+ raise ArgumentError,
128
+ "#{path}: #{key} must be a #{expected_classes.map(&:name).join(' or ')}, " \
129
+ "got #{value.class} (#{value.inspect})"
130
+ end
131
+
132
+ instance_variable_set(:"@#{key}", value)
133
+ end
134
+
135
+ symbolize_adapter_parity_pairs!(path)
136
+ freeze_values!
137
+ end
138
+
139
+ private
140
+
141
+ # `left:`/`right:` name adapters `IsolatedBoot` case-matches by
142
+ # symbol (`case adapter when :memory ...`), and YAML has no way to
143
+ # spell a bare Ruby Symbol as a mapping value — only
144
+ # `symbolize_names:` turns a key into one. So `adapter_parity_
145
+ # pairs` is the one dial that needs a coercion step after the
146
+ # type check above, rather than every dial growing one.
147
+ def symbolize_adapter_parity_pairs!(path)
148
+ @adapter_parity_pairs = @adapter_parity_pairs.to_h do |mode, pair|
149
+ unless pair.is_a?(Hash) && pair.key?(:left) && pair.key?(:right)
150
+ raise ArgumentError,
151
+ "#{path}: adapter_parity_pairs.#{mode} must have both left and right, got #{pair.inspect}"
152
+ end
153
+
154
+ [mode, { left: pair[:left].to_sym, right: pair[:right].to_sym }]
155
+ end
156
+ end
157
+
158
+ def freeze_values!
159
+ EXPECTED_TYPES.each_key { |key| instance_variable_get(:"@#{key}").freeze }
160
+ freeze
161
+ end
162
+ end
163
+ end
164
+ end