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
@@ -0,0 +1,309 @@
1
+ module Hecks
2
+ module Fuzzing
3
+ # A failing step list, made small enough to read.
4
+ #
5
+ # `bin/fuzz` always shrank its findings; `bin/qa_sweep` — the loop that
6
+ # actually finds the bugs — never did, so the ledger filled with
7
+ # demonstrations like "seed 25, 25 steps, step 16" and a human cut
8
+ # BUG#40 down to its four real steps by hand. This is `bin/fuzz`'s
9
+ # shrinker lifted out of that script so both callers share it, and
10
+ # made cheaper at the same time: a sweep's candidate check can cost a
11
+ # Rust subprocess (or a real Postgres round trip), where `bin/fuzz`'s
12
+ # only ever cost an in-process replay.
13
+ #
14
+ # ## Ownership of "same finding"
15
+ #
16
+ # The caller owns it. `call` is handed a block that
17
+ # answers true when a candidate step list still reproduces what the
18
+ # original did — `bin/fuzz` compares its own verdict signature,
19
+ # `bin/qa_sweep` compares `Shrinker.signature` of one mode's
20
+ # divergences. This module never replays anything itself, so it has
21
+ # no idea which engine, adapter or comparison it is minimizing for.
22
+ #
23
+ # ## Two passes, in order
24
+ #
25
+ # 1. Steps, chunks first. Removing one step at a time (what
26
+ # `bin/fuzz` did) costs O(n²) candidate checks on a sequence
27
+ # where most steps are irrelevant. Delta-debugging style: try
28
+ # removing halves, then quarters, … then single steps, keeping
29
+ # any removal that still reproduces; a single-step pass repeats
30
+ # until it changes nothing, so the result is 1-minimal (no one
31
+ # remaining step can be dropped).
32
+ # 2. Arguments, inside each surviving step. Unchanged from `bin/fuzz`
33
+ # (spec/bin_fuzz_spec.rb pins its accumulation contract): drop
34
+ # one key at a time from the step's current args, keep it dropped
35
+ # only while the finding still reproduces.
36
+ #
37
+ # ## Budget
38
+ #
39
+ # A budget, because a sweep has other targets waiting. `budget:` caps
40
+ # how many candidate checks one call may spend (nil = unbounded, the
41
+ # `bin/fuzz` behaviour). When it runs out the best candidate found so
42
+ # far is returned — every accepted candidate reproduced, so a partial
43
+ # shrink is still a correct, just less small, demonstration.
44
+ module Shrinker
45
+ Result = Struct.new(:steps, :attempts, :exhausted, keyword_init: true)
46
+
47
+ module_function
48
+
49
+ # Shrinks `steps` to the smallest step list the block still accepts as
50
+ # reproducing the original finding — pass 1 (steps), then pass 2
51
+ # (arguments), as this module's own header describes.
52
+ #
53
+ # @param steps [Array<Hash>] the step list to shrink, each step a command,
54
+ # query, or read-model hash as `SequenceGenerator` produces
55
+ # @param budget [Integer, nil] maximum candidate checks to spend; nil for
56
+ # unbounded
57
+ # @yield [candidate] tests whether a candidate step list still reproduces
58
+ # the finding being shrunk
59
+ # @yieldparam candidate [Array<Hash>] a step-dropped or argument-trimmed
60
+ # copy of `steps`
61
+ # @yieldreturn [Boolean] true if `candidate` still reproduces the finding
62
+ # @return [Fuzzing::Shrinker::Result] `steps:` the shrunk step list,
63
+ # `attempts:` how many candidate checks were spent, `exhausted:` whether
64
+ # the budget ran out before shrinking finished
65
+ # @raise [ArgumentError] if no block is given
66
+ def call(steps, budget: nil, &reproduces)
67
+ raise ArgumentError, "Shrinker.call needs a block answering whether a candidate reproduces" unless reproduces
68
+
69
+ meter = Meter.new(budget)
70
+ current = drop_steps(steps.dup, meter, &reproduces)
71
+ current = drop_arguments(current, meter, &reproduces)
72
+ Result.new(steps: current, attempts: meter.used, exhausted: meter.exhausted?)
73
+ end
74
+
75
+ # Runs pass 1 of the shrink: delta-debugging removal of step chunks,
76
+ # halving the chunk size each full pass until single steps are tried
77
+ # and nothing more can be dropped.
78
+ #
79
+ # @param steps [Array<Hash>] the step list to shrink
80
+ # @param meter [Fuzzing::Shrinker::Meter] the shared budget meter
81
+ # @yield [candidate] tests whether a candidate step list still reproduces
82
+ # the finding being shrunk
83
+ # @yieldparam candidate [Array<Hash>] `steps` with one chunk removed
84
+ # @yieldreturn [Boolean] true if `candidate` still reproduces the finding
85
+ # @return [Array<Hash>] the smallest step list pass 1 could reach, or the
86
+ # best found so far if the budget ran out first
87
+ def drop_steps(steps, meter, &reproduces)
88
+ current = steps
89
+ chunk = [current.length / 2, 1].max
90
+ loop do
91
+ changed = false
92
+ index = 0
93
+ while index < current.length
94
+ return current if meter.exhausted?
95
+
96
+ candidate = current[0...index] + (current[(index + chunk)..] || [])
97
+ if candidate.empty?
98
+ index += chunk
99
+ next
100
+ end
101
+
102
+ if meter.try { reproduces.call(candidate) }
103
+ current = candidate
104
+ changed = true
105
+ else
106
+ index += chunk
107
+ end
108
+ end
109
+
110
+ if chunk > 1
111
+ chunk = [chunk / 2, 1].max
112
+ elsif !changed
113
+ break
114
+ end
115
+ end
116
+ current
117
+ end
118
+
119
+ # Runs pass 2 of the shrink: drops one argument key at a time from
120
+ # each surviving step's args, keeping the drop only while the
121
+ # finding still reproduces.
122
+ #
123
+ # See the module header's pass 2 — `args` is read by whichever
124
+ # spelling the step actually carries (`key?` first, never `||`,
125
+ # which cannot tell a stored `false` from an absent key).
126
+ #
127
+ # @param steps [Array<Hash>] the step list, already step-shrunk by `drop_steps`
128
+ # @param meter [Fuzzing::Shrinker::Meter] the shared budget meter
129
+ # @yield [candidate] tests whether a candidate step list still reproduces
130
+ # the finding being shrunk
131
+ # @yieldparam candidate [Array<Hash>] `steps` with one step's one argument
132
+ # key dropped
133
+ # @yieldreturn [Boolean] true if `candidate` still reproduces the finding
134
+ # @return [Array<Hash>] `steps` with every argument key `drop_arguments`
135
+ # accepted dropped
136
+ def drop_arguments(steps, meter, &reproduces)
137
+ steps.each_index do |position|
138
+ original = args_of(steps[position])
139
+ next unless original.is_a?(Hash)
140
+
141
+ original.each_key do |key|
142
+ return steps if meter.exhausted?
143
+
144
+ step = steps[position]
145
+ trimmed = args_of(step).reject { |name, _| name == key }
146
+ candidate = steps.map(&:dup)
147
+ candidate[position] = step.merge("args" => trimmed)
148
+ steps = candidate if meter.try { reproduces.call(candidate) }
149
+ end
150
+ end
151
+ steps
152
+ end
153
+
154
+ # Reads a step's argument hash, whichever key spelling it uses.
155
+ #
156
+ # @param step [Hash] one step, string- or symbol-keyed
157
+ # @return [Hash, nil] the step's `"args"` value if the step is string-keyed,
158
+ # else its `:args` value (nil if neither key is present)
159
+ def args_of(step)
160
+ step.key?("args") ? step["args"] : step[:args]
161
+ end
162
+
163
+ # Which finding this is, as a set of stable strings — the identity a
164
+ # shrink candidate has to keep. The `field` alone is too loose for
165
+ # the comparisons that carry lists: "refusals differ" on a 3-step
166
+ # candidate could be a different refusal split than the one the
167
+ # original seed found, and a shrinker that accepted it would hand
168
+ # back a demonstration of the wrong bug. So:
169
+ #
170
+ # refusals/queries/dry_runs/reactions — the field plus every verb
171
+ # (or query) named in the symmetric difference of the two sides;
172
+ # instances — the field plus the aggregate of every top-level key
173
+ # whose two sides disagree (the `#id` suffix dropped);
174
+ # a crash/process finding — the field plus the exception class
175
+ # leading its detail;
176
+ # anything else (a property name, a self-consistency axis) — the
177
+ # field, which already names the finding.
178
+ #
179
+ # `reproduces?` holds when a candidate's signature contains the
180
+ # original's: removing steps may add a second divergence, but it
181
+ # must never lose the one being demonstrated.
182
+ LIST_FIELDS = %w[refusals queries dry_runs reactions].freeze
183
+
184
+ CRASH_FIELDS = %w[crash process generator_crash].freeze
185
+
186
+ # Computes which finding a set of divergences is — the stable identity
187
+ # a shrink candidate has to keep. See this method's own preceding
188
+ # comment for what each field kind contributes.
189
+ #
190
+ # @param divergences [Array<Hash>] divergence entries, each at least
191
+ # `{field:, detail:}` plus whichever mode-specific keys carry the two
192
+ # compared sides
193
+ # @return [Set<String>] stable identity strings: each divergence's `field`,
194
+ # plus `detail_keys`' own per-field detail strings
195
+ def signature(divergences)
196
+ divergences.each_with_object(Set.new) do |divergence, keys|
197
+ field = divergence[:field].to_s
198
+ keys << field
199
+ keys.merge(detail_keys(field, divergence))
200
+ end
201
+ end
202
+
203
+ # Extracts the finer-grained identity strings one divergence's field
204
+ # contributes beyond the field name itself.
205
+ #
206
+ # @param field [String] the divergence's `field`, stringified
207
+ # @param divergence [Hash] the divergence entry `field` came from
208
+ # @return [Array<String>] extra identity strings for `field`'s own kind
209
+ # (list, `instances`, or crash/process); empty for any other field
210
+ def detail_keys(field, divergence)
211
+ left, right = divergence.except(:field, :detail).values.select { |v| v.is_a?(Array) || v.is_a?(Hash) }
212
+ if LIST_FIELDS.include?(field) then list_keys(field, left, right)
213
+ elsif field == "instances" then instance_keys(left, right)
214
+ elsif CRASH_FIELDS.include?(field) && divergence[:detail]
215
+ ["#{field}:#{divergence[:detail].to_s[/\A\w+(?:::\w+)*/]}"]
216
+ else []
217
+ end
218
+ end
219
+
220
+ # Names the rows present on one side of a list-field divergence and not
221
+ # the other.
222
+ #
223
+ # @param field [String] the divergence's field name, prefixed onto each key
224
+ # @param left [Array, nil] one side's list; a non-Array yields no keys
225
+ # @param right [Array, nil] the other side's list; a non-Array yields no keys
226
+ # @return [Array<String>] `"field:name"` for every row in the symmetric
227
+ # difference of `left` and `right`; empty unless both are arrays
228
+ def list_keys(field, left, right)
229
+ return [] unless left.is_a?(Array) && right.is_a?(Array)
230
+
231
+ ((left - right) + (right - left)).map { |row| "#{field}:#{named(row)}" }
232
+ end
233
+
234
+ # Names the top-level keys the two sides of an `instances` divergence
235
+ # disagree on.
236
+ #
237
+ # Instance keys are `Aggregate#id` on the wire — the id is whatever
238
+ # the generator minted, so it names the record, not the finding;
239
+ # keeping it would pin every creating step.
240
+ #
241
+ # @param left [Hash, nil] one side's instance snapshot; a non-Hash yields no keys
242
+ # @param right [Hash, nil] the other side's instance snapshot; a non-Hash
243
+ # yields no keys
244
+ # @return [Array<String>] `"instances:Aggregate"` for every top-level key
245
+ # the two sides disagree on, its `#id` suffix dropped; empty unless both
246
+ # are hashes
247
+ def instance_keys(left, right)
248
+ return [] unless left.is_a?(Hash) && right.is_a?(Hash)
249
+
250
+ (left.keys | right.keys).reject { |key| left[key] == right[key] }
251
+ .map { |key| "instances:#{key.to_s.split('#').first}" }
252
+ end
253
+
254
+ # Checks that a candidate's own divergences still demonstrate the
255
+ # finding being shrunk.
256
+ #
257
+ # @param original_signature [Set<String>] the finding's own signature, as
258
+ # returned by `signature`
259
+ # @param divergences [Array<Hash>] the candidate's own divergence entries,
260
+ # in `signature`'s own shape
261
+ # @return [Boolean] true if `divergences` is non-empty and its signature is
262
+ # a superset of `original_signature`
263
+ def reproduces?(original_signature, divergences)
264
+ !divergences.empty? && original_signature.subset?(signature(divergences))
265
+ end
266
+
267
+ # Names one row from a list-field divergence, for `list_keys`.
268
+ #
269
+ # @param row [Object] one item from a list-field divergence's array;
270
+ # typically a Hash carrying a `verb`, `query`, or `policy` key (string
271
+ # or symbol)
272
+ # @return [String] `row`'s `verb`/`query`/`policy` value, stringified, if
273
+ # `row` is a Hash carrying one of those keys; otherwise `row.to_s`
274
+ def named(row)
275
+ return row.to_s unless row.is_a?(Hash)
276
+
277
+ key = [%w[verb query policy], %i[verb query policy]].flatten.find { |name| row.key?(name) }
278
+ (key ? row[key] : row).to_s
279
+ end
280
+
281
+ # Counts candidate checks against the budget.
282
+ class Meter
283
+ attr_reader :used
284
+
285
+ # @param budget [Integer, nil] maximum candidate checks to allow; nil for unbounded
286
+ def initialize(budget)
287
+ @budget = budget
288
+ @used = 0
289
+ end
290
+
291
+ # Reports whether this meter's budget has been fully spent.
292
+ #
293
+ # @return [Boolean] true if a budget is set and every allotted check has
294
+ # been used
295
+ def exhausted? = !@budget.nil? && @used >= @budget
296
+
297
+ # Spends one candidate check against the budget and runs the block.
298
+ #
299
+ # @yield runs the candidate check being counted
300
+ # @yieldreturn [Object] the block's own result
301
+ # @return [Object] whatever the block returns
302
+ def try
303
+ @used += 1
304
+ yield
305
+ end
306
+ end
307
+ end
308
+ end
309
+ end
@@ -1,146 +1,53 @@
1
1
  module Hecks
2
2
  module Fuzzing
3
- # WHAT A "NOT GENERATED FOR THIS DOMAIN" REFUSAL IS HIDING, PER VERB.
3
+ # What each query verb the differential comparison skipped was hiding.
4
4
  #
5
- # `RustConformanceHelpers#structurally_refused_verbs` drops every named
6
- # query/read model the compiled Rust kernel answers "is not generated
7
- # for this domain" from BOTH sides of the differential comparison —
8
- # correct (the refusal is codegen's own honest "I cannot execute this
9
- # construct at all", `rust/project/queries.rb`'s and `read_models.rb`'s
10
- # documented boundary), but SILENT: a sweep over a domain whose every
11
- # `offset`/`cursor`/`group_by`/`count`/`median` ask was dropped reads
12
- # as "agreed across all steps" with nothing on the record saying how
13
- # much of the domain was never compared at all. Worse, the drop keys
14
- # on Rust's wording alone, so a codegen regression that started
15
- # refusing a plain wheres-only query the same way would ALSO vanish
16
- # into "agreed" — the exact quiet divergence the practice hunts.
5
+ # `Differential.manifest_partition` drops a named query or read model
6
+ # from both sides of the Ruby/Rust comparison only when the binary's own
7
+ # manifest.json declares it `generated: false`. The drop is correct, but
8
+ # a sweep that dropped every `offset`/`cursor` ask would still read as
9
+ # "agreed across all steps". `bin/qa_sweep` therefore logs one Check per
10
+ # sweep naming every skipped verb and the construct family that caused
11
+ # it, and marks it Surprised when a family is outside
12
+ # `QualityControlDials::STRUCTURAL_REFUSAL_BOUNDARY`.
17
13
  #
18
- # This module attributes each dropped verb to the constructs its OWN
19
- # Ruby declaration carries, in the vocabulary `queries.rb`/
20
- # `read_models.rb`'s skip reasons already use, so `bin/qa_sweep` can
21
- # log one Check per sweep saying exactly which verbs were skipped and
22
- # WHY — and mark it Surprised whenever a skipped verb declares nothing
23
- # the dial `QualityControlDials::STRUCTURAL_REFUSAL_BOUNDARY` admits as
24
- # a reason to skip (a wheres-only query on plain fields, a rooted
25
- # read model with nothing but heads, a verb Ruby doesn't even declare).
26
- #
27
- # COARSER THAN CODEGEN'S OWN PREDICATE, ON PURPOSE. `query_where_skip_
28
- # reason` decides per where clause from the field's resolved KIND
29
- # (number/string/multi-member value object) — reproducing that here
30
- # would be a second copy of codegen's typing rules, drifting on its
31
- # own. Instead a literal-valued where is reported as `where_literal`,
32
- # a family the dial admits, and only shapes with NO admitted family at
33
- # all surprise. That trades some sensitivity for zero false surprises
34
- # on today's corpus; the Check's observation still names every skipped
35
- # verb with its constructs, so a human reading the sweep sees what was
36
- # accepted and can narrow the dial when codegen grows.
14
+ # The construct comes straight from the manifest entry, written by the
15
+ # generator branch that made the skip (`Projector::SkipReason`), rather
16
+ # than guessed from the Ruby declaration's `to_h` keys, which is coarser
17
+ # than codegen's own decision and can drift from it.
37
18
  module StructuralSkips
38
19
  module_function
39
20
 
40
- # `Query#to_h`/`ReadModel#to_h` keys that, when present and
41
- # non-empty, are constructs codegen documents as (conditionally or
42
- # wholly) ungenerated — the same names as the dial's own vocabulary.
43
- QUERY_OPTION_KEYS = %i[order_by limit offset cursor consistency freshness authorization inspection
44
- index_hints scope_to].freeze
45
- READ_MODEL_OPTION_KEYS = %i[wheres order_by limit offset cursor consistency freshness authorization inspection
46
- index_hints group_by count median_field].freeze
47
-
48
- # One entry per skipped verb: `{ verb:, constructs: [...] }`, the
49
- # constructs sorted so the printed observation is stable across
50
- # seeds and runs.
51
- def attribute(bluebooks, verbs)
52
- verbs.sort.map { |verb| { verb: verb, constructs: constructs_of(bluebooks, verb) } }
21
+ # Attributes each skipped verb to the construct family its manifest
22
+ # entry names.
23
+ #
24
+ # One entry per skipped verb: `{ verb:, constructs: [...] }`, sorted by
25
+ # verb so the printed observation is stable across seeds and runs. A
26
+ # verb the manifest doesn't declare answers `unknown`, which no
27
+ # boundary admits.
28
+ #
29
+ # @param gaps [Fuzzing::RustGapManifest] the binary's not-generated manifest entries
30
+ # @param verbs [Array<String>] the skipped query/read-model verbs to attribute
31
+ # @return [Array<Hash{Symbol => Object}>] one `{ verb: String, constructs: Array<String> }`
32
+ # per verb, in sorted-verb order
33
+ def attribute(gaps, verbs)
34
+ verbs.sort.map do |verb|
35
+ entry = gaps.not_generated(verb)
36
+ { verb: verb, constructs: entry ? [entry.fetch("construct")] : %w[unknown] }
37
+ end
53
38
  end
54
39
 
55
- # Every skipped verb whose constructs are NOT all inside `boundary`
56
- # — including a verb with no constructs at all (nothing explains the
57
- # skip) and a verb Ruby never declared (`unknown`).
40
+ # Selects the attributed entries whose constructs are not all inside `boundary`.
41
+ #
42
+ # @param attributed [Array<Hash{Symbol => Object}>] entries as returned by `attribute`
43
+ # @param boundary [Array<String, Symbol>] the construct families the documented codegen
44
+ # boundary admits
45
+ # @return [Array<Hash{Symbol => Object}>] the subset of `attributed` naming an
46
+ # unadmitted construct, or no construct at all
58
47
  def outside_boundary(attributed, boundary)
59
48
  admitted = boundary.map(&:to_s)
60
49
  attributed.select { |entry| entry[:constructs].empty? || (entry[:constructs] - admitted).any? }
61
50
  end
62
-
63
- def constructs_of(bluebooks, verb)
64
- if verb.include?("::")
65
- query = find_query(bluebooks, verb)
66
- return %w[unknown] unless query
67
-
68
- query_constructs(query)
69
- else
70
- model = find_read_model(bluebooks, verb)
71
- return %w[unknown] unless model
72
-
73
- read_model_constructs(model)
74
- end
75
- end
76
-
77
- # "Domain::Aggregate.Query" or "Domain::Aggregate.Entity.Query" — the
78
- # two named-query spellings `Fuzzing::SequenceGenerator`'s catalog
79
- # generates (entity queries stay one hop deep there).
80
- def find_query(bluebooks, verb)
81
- domain, rest = verb.split("::", 2)
82
- aggregate_name, *path = rest.to_s.split(".")
83
- aggregate = bluebooks[domain]&.aggregate(aggregate_name)
84
- return nil unless aggregate && path.any?
85
-
86
- owner = aggregate
87
- path[0...-1].each do |entity_name|
88
- owner = owner.entities.find { |entity| entity.hecks_name == entity_name }
89
- return nil unless owner
90
- end
91
- owner.query(path.last)
92
- end
93
-
94
- # "Domain.report_name" — the bare read-model form `Dispatcher#query`
95
- # routes by the absence of "::".
96
- def find_read_model(bluebooks, verb)
97
- domain, name = verb.split(".", 2)
98
- bluebooks[domain]&.read_models&.find { |model| model.query_name.to_s == name.to_s }
99
- end
100
-
101
- def query_constructs(query)
102
- spec = query.to_h
103
- constructs = QUERY_OPTION_KEYS.select { |key| present?(spec[key]) }.map(&:to_s)
104
- constructs << "null_semantics" if spec[:null_semantics] && spec[:null_semantics] != { mode: "native" }
105
- constructs << "no_wheres" if Array(spec[:wheres]).empty?
106
- constructs.concat(where_constructs(Array(spec[:wheres])))
107
- constructs.uniq.sort
108
- end
109
-
110
- def read_model_constructs(model)
111
- spec = model.to_h
112
- constructs = READ_MODEL_OPTION_KEYS.select { |key| present?(spec[key]) }.map(&:to_s)
113
- constructs << "null_semantics" if spec[:null_semantics] && spec[:null_semantics] != { mode: "native" }
114
- constructs << "rootless" if model.reference_target.nil?
115
- constructs.concat(where_constructs(Array(spec[:wheres])))
116
- constructs.uniq.sort
117
- end
118
-
119
- # The where-clause families `query_where_skip_reason` refuses by:
120
- # a `/` hop through a reference, a `.` walk into a nested field
121
- # (an entity-scoped or value-object path codegen may not resolve),
122
- # `none_in_state` (admitted by the vocabulary, deliberately not
123
- # generated), and any literal (non-Symbol) value, whose true wire
124
- # type codegen may not recover from the IR.
125
- def where_constructs(wheres)
126
- wheres.flat_map do |where|
127
- field = where[:field].to_s
128
- value = where[:value].to_s
129
- families = []
130
- families << "reference_hop_where" if field.include?("/")
131
- families << "where_nested_field" if field.include?(".")
132
- families << "where_none_in_state" if where[:op].to_s == "none_in_state"
133
- families << "where_literal" unless value.start_with?(":")
134
- families
135
- end
136
- end
137
-
138
- def present?(value)
139
- return false if value.nil? || value == false
140
- return value.any? if value.respond_to?(:empty?)
141
-
142
- true
143
- end
144
51
  end
145
52
  end
146
53
  end
@@ -1,13 +1,13 @@
1
1
  module Hecks
2
2
  module Fuzzing
3
- # HOW HARD ONE SWEEP FUZZES, AS A FUNCTION OF `Target.clean_streak`
3
+ # How hard one sweep fuzzes, as a function of `Target.clean_streak`
4
4
  # (qa/bluebook/quality_control.bluebook — that attribute's own comment
5
5
  # says what the streak means). The sibling of `RotationPriority`: the
6
6
  # ledger holds the number, `QualityControlDials::WIDENING_TIERS` holds
7
7
  # the policy, and this module is where the two meet — a pure function
8
8
  # of both, nothing else read.
9
9
  #
10
- # WHY THIS IS NOT IN `bin/qa_sweep` ANY MORE. It was — `WIDENING_TIERS`
10
+ # Why this is not in `bin/qa_sweep` any more. It was — `WIDENING_TIERS`
11
11
  # and `widen_for_streak` lived at the top of that script, which meant
12
12
  # policy data lived in a script (unlike every other dial, which lives
13
13
  # in the bluebook a human edits and reviews) and duplicated itself as
@@ -15,7 +15,7 @@ module Hecks
15
15
  # and `bin/qa_sweep` calls it the way it already calls
16
16
  # `RotationPriority.pick`.
17
17
  #
18
- # PURE, DELIBERATELY — the same discipline `RotationPriority` keeps:
18
+ # **Pure, deliberately** — the same discipline `RotationPriority` keeps:
19
19
  # same streak in, same `[seeds, steps]` out, every time, which is what
20
20
  # lets a human predict what a given sweep is about to do before it
21
21
  # runs one. `tiers:` defaults to the dial but is a plain argument, so
@@ -26,7 +26,7 @@ module Hecks
26
26
  module SweepDepth
27
27
  module_function
28
28
 
29
- # THE FALLBACK TABLE, for a boot with no `QualityControlDials` at all
29
+ # The fallback table, for a boot with no `QualityControlDials` at all
30
30
  # — the same shape and the same three rows the dial ships with, so a
31
31
  # dial-less ledger fuzzes exactly as a dialled one does by default.
32
32
  # Never read when the dial exists; `bin/qa_sweep` passes the dial in.
@@ -40,6 +40,14 @@ module Hecks
40
40
  # streak does not exceed wins — rows are read in order, so the table
41
41
  # must be ascending, and the last row's `Float::INFINITY` is what
42
42
  # makes it the ceiling rather than a gap.
43
+ #
44
+ # @param streak [Integer] `Target.clean_streak`, the number of consecutive clean sweeps
45
+ # @param tiers [Array<Hash{Symbol => Numeric}>] ascending `{ upto:, seeds:, steps: }` rows;
46
+ # defaults to `DEFAULT_TIERS`
47
+ # @return [Array(Integer, Integer)] `[seeds, steps]` for the first row `streak` fits under
48
+ # @raise [ArgumentError] if `streak` is negative
49
+ # @raise [ArgumentError] if no row in `tiers` covers `streak` (the last row must declare
50
+ # `upto: Float::INFINITY`)
43
51
  def for_streak(streak, tiers: DEFAULT_TIERS)
44
52
  raise ArgumentError, "streak must not be negative" if streak.negative?
45
53