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,178 @@
1
+ require "json"
2
+ require "open3"
3
+ require_relative "form_census"
4
+
5
+ module Hecks
6
+ module Fuzzing
7
+ # An agent reads the adversarial corpus and writes the next meeting.
8
+ #
9
+ # `DomainGenerator` forces two `FormCensus::FORMS` onto one aggregate
10
+ # from a seed — cheap, mechanical, and blind to why a combination
11
+ # should break. The bugs the ledger actually logged came from someone
12
+ # reading a stress domain, a bug title, and a runtime file side by
13
+ # side and guessing where the next divergence lives (`corrections`
14
+ # from BUG#30-33, `case_escalation` from ADR 0037 Finding 5 meeting
15
+ # `corrects`). This is that guess, delegated: the mechanical half
16
+ # (what the corpus already puts together, which pairs nothing meets,
17
+ # what the recent bugs were) is computed here; the judgment half is a
18
+ # prompt (`qa/combination_miner/prompt.md`) an agent answers by
19
+ # writing candidate bluebooks, each with a hypothesis. Checking them
20
+ # is not this module's job — `bin/qa_mine_combinations` hands every
21
+ # valid candidate to `bin/qa_generated_domains --source`, the same
22
+ # differential, self-consistency, Rust build and shrinking path a
23
+ # generated domain takes.
24
+ #
25
+ # **Opt-in, never the rotation**. An agent call costs money and minutes
26
+ # and answers differently every time; `bin/qa_tick` never runs it and
27
+ # no `QualityControlDials` entry turns it on. A person runs
28
+ # `bin/qa_mine_combinations` when they want new shapes.
29
+ module CombinationMiner
30
+ PROMPT_TEMPLATE = "qa/combination_miner/prompt.md".freeze
31
+ HYPOTHESIS_FILE = "HYPOTHESIS.md".freeze
32
+
33
+ module_function
34
+
35
+ # Every adversarial domain plus every example — the corpus the
36
+ # census is measured over. Promoted generated domains are included:
37
+ # their shapes are already swept too.
38
+ #
39
+ # @param root [String] repository root to search under
40
+ # @return [Array<String>] paths of every stress/example corpus member that
41
+ # declares at least one bluebook file
42
+ def corpus_paths(root)
43
+ Hecks::Corpus.members(:stress, :example, root: root).map(&:path)
44
+ .select { |path| Hecks::Corpus.bluebook_files(path) }
45
+ .sort
46
+ end
47
+
48
+ # The last `limit` bug commit subjects on this branch — cheap, needs
49
+ # no ledger or Postgres, and names the mechanism in the title by
50
+ # convention (`BUG#n: …`).
51
+ #
52
+ # @param root [String] repository root to run `git log` in
53
+ # @param limit [Integer] maximum number of commit subjects to return
54
+ # @return [Array<String>] matching commit subjects, newest first; empty if
55
+ # `git log` fails
56
+ def recent_bug_titles(root, limit: 60)
57
+ out, status = Open3.capture2("git", "log", "--format=%s", "--grep=BUG#", "-n", limit.to_s, chdir: root)
58
+ status.success? ? out.lines.map(&:strip).reject(&:empty?) : []
59
+ end
60
+
61
+ # The mechanical half: which form pairs the corpus meets (and by
62
+ # whom), which it never meets, and what could not be measured.
63
+ #
64
+ # @param paths [Array<String>] corpus member paths to census, as returned by
65
+ # `#corpus_paths`
66
+ # @param root [String] repository root, to render `paths` relative to it
67
+ # @param bug_titles [Array<String>] recent bug commit subjects, as returned
68
+ # by `#recent_bug_titles`
69
+ # @return [Hash] `"forms"` (`Array<String>`, every declared form name),
70
+ # `"corpus"` (`Array<String>`, `paths` relative to `root`),
71
+ # `"unmet_pairs"` (`Array<String>`, form pairs no corpus member meets),
72
+ # `"single_carrier_pairs"` (`Array<String>`, pairs only one form name
73
+ # meets), `"skipped"` (`Array<String>`, paths that could not be
74
+ # censused, with the error), `"recent_bugs"` (`bug_titles`), and
75
+ # `"covered"` (`Hash{String => Array<String>}`, pair to every form name
76
+ # that meets it)
77
+ def brief(paths, root:, bug_titles:)
78
+ covered = Hash.new { |hash, key| hash[key] = [] }
79
+ skipped = []
80
+ paths.each do |path|
81
+ FormCensus.covered_pairs(FormCensus.census(path)).each { |pair, names| covered[pair].concat(names) }
82
+ rescue StandardError, ScriptError => e
83
+ skipped << "#{relative(path, root)} (#{e.class}: #{e.message.lines.first&.strip})"
84
+ end
85
+ { "forms" => FormCensus::FORMS.keys, "corpus" => paths.map { |path| relative(path, root) },
86
+ "unmet_pairs" => (FormCensus.pairs - covered.keys).sort,
87
+ "single_carrier_pairs" => covered.select { |_, names| names.uniq.size == 1 }
88
+ .map { |pair, names| "#{pair} (only #{names.first})" }.sort,
89
+ "skipped" => skipped, "recent_bugs" => bug_titles, "covered" => covered }
90
+ end
91
+
92
+ # Fills `PROMPT_TEMPLATE`'s `{{placeholders}}` with `brief`'s own data, for
93
+ # the agent to answer by writing candidate bluebooks.
94
+ #
95
+ # @param root [String] repository root the prompt template is read from
96
+ # @param brief [Hash] the mechanical brief, as returned by `#brief`
97
+ # @param count [Integer] how many candidates the prompt asks the agent for
98
+ # @param out_dir [String] where the prompt asks the agent to write candidates
99
+ # @return [String] the filled-in prompt text
100
+ # @raise [KeyError] if the template names a placeholder `substitutions` does
101
+ # not provide
102
+ def prompt(root, brief, count:, out_dir:)
103
+ substitutions = {
104
+ "count" => count.to_s, "out_dir" => out_dir, "forms" => brief["forms"].join(", "),
105
+ "corpus" => bulleted(brief["corpus"]), "unmet_pairs" => bulleted(brief["unmet_pairs"]),
106
+ "single_carrier_pairs" => bulleted(brief["single_carrier_pairs"]),
107
+ "recent_bugs" => bulleted(brief["recent_bugs"]), "skipped" => bulleted(brief["skipped"])
108
+ }
109
+ File.read(File.join(root, PROMPT_TEMPLATE)).gsub(/\{\{(\w+)\}\}/) { substitutions.fetch(Regexp.last_match(1)) }
110
+ end
111
+
112
+ # The second, narrower ask: these candidates did not boot, here is
113
+ # why, fix them in place. Nothing else in the directory changes.
114
+ #
115
+ # @param failures [Array<Array(Hash, String)>] `[candidate, error]` pairs,
116
+ # `candidate` as returned by `#candidates`, `error` the boot failure message
117
+ # @param out_dir [String] the directory the candidates were written under
118
+ # @return [String] the repair round's own prompt text
119
+ def repair_prompt(failures, out_dir:)
120
+ listed = failures.map { |candidate, error| "- #{candidate[:bluebook]}\n boot error: #{error}" }.join("\n")
121
+ <<~PROMPT
122
+ REPAIR ROUND. You wrote candidate Hecks bluebooks under #{out_dir} for the QA combination miner.
123
+ These did not boot. Edit each file IN PLACE so it boots, keeping the construct combination its
124
+ #{HYPOTHESIS_FILE} names (update the hypothesis if the fix changes the shape). Do not create new
125
+ candidates and do not touch anything outside #{out_dir}.
126
+
127
+ #{listed}
128
+ PROMPT
129
+ end
130
+
131
+ # `<out_dir>/<slug>/<anything>.bluebook` plus an optional
132
+ # HYPOTHESIS.md beside it. One bluebook per candidate: the first,
133
+ # alphabetically, if an agent wrote more.
134
+ #
135
+ # @param out_dir [String] the directory candidates were written under
136
+ # @return [Array<Hash>] one `{slug:, dir:, bluebook:, hypothesis:}` entry per
137
+ # candidate subdirectory that declares a `.bluebook` file; `hypothesis` is
138
+ # `nil` when no `HYPOTHESIS.md` sits beside it
139
+ def candidates(out_dir)
140
+ Dir[File.join(out_dir, "*")].select { |dir| File.directory?(dir) }.sort.filter_map do |dir|
141
+ bluebook = Dir[File.join(dir, "*.bluebook")].min
142
+ next unless bluebook
143
+
144
+ hypothesis = File.join(dir, HYPOTHESIS_FILE)
145
+ { slug: File.basename(dir), dir: dir, bluebook: bluebook,
146
+ hypothesis: File.exist?(hypothesis) ? File.read(hypothesis).strip : nil }
147
+ end
148
+ end
149
+
150
+ # Pairs the candidate meets on one aggregate that no corpus domain
151
+ # does — informational, never a gate: an agent may be aiming at a
152
+ # shape the census does not name yet.
153
+ #
154
+ # @param candidate_dir [String] path to the candidate's own directory
155
+ # @param covered [Hash{String => Array<String>}] the corpus's own covered
156
+ # pairs, as returned in `#brief`'s `"covered"` entry
157
+ # @return [Array<String>] form pairs the candidate meets that `covered` does
158
+ # not already name, sorted
159
+ def new_pairs(candidate_dir, covered)
160
+ FormCensus.covered_pairs(FormCensus.census(candidate_dir)).keys.reject { |pair| covered.key?(pair) }.sort
161
+ end
162
+
163
+ # Renders a path relative to the repository root.
164
+ #
165
+ # @param path [String] an absolute (or `root`-prefixed) path
166
+ # @param root [String] the prefix to strip
167
+ # @return [String] `path` with `root`'s own prefix removed
168
+ def relative(path, root) = path.delete_prefix("#{root}/")
169
+
170
+ # Renders a list of strings as a Markdown bullet list.
171
+ #
172
+ # @param items [Array<String>] lines to render as a Markdown bullet list
173
+ # @return [String] one `"- item"` line per entry, newline-joined; `"(none)"`
174
+ # when `items` is empty
175
+ def bulleted(items) = items.empty? ? "(none)" : items.map { |item| "- #{item}" }.join("\n")
176
+ end
177
+ end
178
+ end
@@ -3,56 +3,65 @@ require "tmpdir"
3
3
  require "tempfile"
4
4
  require "json"
5
5
  require_relative "isolated_boot"
6
+ require_relative "../naming"
6
7
 
7
8
  module Hecks
8
9
  module Fuzzing
9
- # A GENERATED SEQUENCE, RACED FOR REAL — `spec/adapters/driven/
10
+ # **A generated sequence, raced for real** — `spec/adapters/driven/
10
11
  # postgres_era_concurrent_dispatch_spec.rb` proves ADR 0036's own fix
11
12
  # (a real `pg_advisory_xact_lock` serializes a PostgresEra-bound
12
- # dispatch across separate OS processes) against ONE hand-authored
13
- # fixture and ONE hand-picked conflicting pair (two `$6,000` Debits
14
- # against a `$10,000` account). This module asks the SAME question —
13
+ # dispatch across separate OS processes) against one hand-authored
14
+ # fixture and one hand-picked conflicting pair (two `$6,000` Debits
15
+ # against a `$10,000` account). This module asks the same question —
15
16
  # does the cross-process write lock actually serialize concurrent
16
- # writers? — of an ARBITRARY generated sequence against a REAL target
17
+ # writers? — of an arbitrary generated sequence against a real target
17
18
  # domain, so the practice's own adversarial generator gets to pick the
18
19
  # conflict instead of a human picking it once and never again.
19
20
  #
20
- # THE MECHANISM, GENERALIZED FROM THAT SPEC RATHER THAN REDERIVED: a
21
+ # ## The mechanism
22
+ #
23
+ # Generalized from that spec rather than rederived: a
21
24
  # command step partway through a generated sequence is chosen as the
22
- # RACE STEP; every step before it is SETUP (replayed once, sequentially,
25
+ # race step; every step before it is setup (replayed once, sequentially,
23
26
  # to bring a fresh disposable schema to the state the race step expects
24
- # to act against); the race step is then dispatched TWICE — once from
27
+ # to act against); the race step is then dispatched twice — once from
25
28
  # each of two real, separate, forked OS processes racing against that
26
- # SAME schema, no artificial gating, whichever the scheduler favors.
29
+ # same schema, no artificial gating, whichever the scheduler favors.
30
+ #
31
+ # ## The oracle
27
32
  #
28
- # THE ORACLE IS THE SAME PAIR, DISPATCHED SEQUENTIALLY, NOT A
29
- # HARDCODED EXPECTATION — unlike the hand-authored spec (which can
33
+ # The same pair, dispatched sequentially, not a
34
+ # hardcoded expectation — unlike the hand-authored spec (which can
30
35
  # assert `%w[refused succeeded]` because it knows its own fixture's
31
36
  # business rule), this module has no idea whether an arbitrary
32
37
  # generated race step conflicts with itself at all. So it asks the
33
- # DOMAIN: replay the identical setup on a SEPARATE fresh schema, then
34
- # dispatch the SAME race step twice, one after the other, in one
38
+ # domain: replay the identical setup on a separate fresh schema, then
39
+ # dispatch the same race step twice, one after the other, in one
35
40
  # process, with no contention at all — genuinely correct by
36
41
  # construction, since nothing else can touch that schema while it
37
42
  # runs. Two conflicting writes settle as {"succeeded", "refused"};
38
43
  # two independent ones settle as {"succeeded", "succeeded"}; either
39
- # way, THAT is the multiset the concurrent pair must also produce if
44
+ # way, that is the multiset the concurrent pair must also produce if
40
45
  # the write lock actually serializes them — order does not matter
41
- # (which real racer wins a genuine race is never controlled), the SET
46
+ # (which real racer wins a genuine race is never controlled), the set
42
47
  # of outcomes does.
43
48
  #
44
- # WHAT A BROKEN LOCK LOOKS LIKE HERE: the concurrent pair settling as
49
+ # ## What a broken lock looks like here
50
+ #
51
+ # The concurrent pair settling as
45
52
  # {"succeeded", "succeeded"} where the sequential oracle says
46
53
  # {"succeeded", "refused"} — two processes each hydrated the
47
54
  # pre-write state, neither saw the other's write, and the second
48
55
  # commit landed as a silent lost update instead of failing its own
49
- # `given`. That is the EXACT corruption class ADR 0036 fixed for
56
+ # `given`. That is the exact corruption class ADR 0036 fixed for
50
57
  # PostgresEra and `postgres_concurrent_dispatch_spec.rb` still
51
58
  # documents, unfixed, for plain Postgres.
52
59
  #
53
- # NOT A REPLACEMENT for the hand-authored spec — that spec proves the
60
+ # ## Not a replacement for the hand-authored spec
61
+ #
62
+ # That spec proves the
54
63
  # mechanism once, precisely, with controlled gating so the assertion
55
- # is deterministic; this module proves the SAME mechanism holds for
64
+ # is deterministic; this module proves the same mechanism holds for
56
65
  # whatever a real domain's own generated sequences throw at it, with
57
66
  # no gating (a genuine, ungated race), on every sweep this mode runs.
58
67
  module ConcurrentDispatch
@@ -60,8 +69,8 @@ module Hecks
60
69
 
61
70
  COMMAND_STEP = ->(step) { step["verb"] && !step["query"] && !step["dry_run"] }
62
71
 
63
- # ONE DIVERGENCE LIST, THE SAME SHAPE EVERY OTHER MODE IN THIS
64
- # PRACTICE PRODUCES — `[]` when nothing was found (including the
72
+ # One divergence list, the same shape every other mode in this
73
+ # practice produces — `[]` when nothing was found (including the
65
74
  # legitimate "this seed's generated sequence has no command step to
66
75
  # race at all" case: a sequence of pure queries/dry-runs has nothing
67
76
  # to concurrently dispatch, and that is not a finding).
@@ -70,14 +79,35 @@ module Hecks
70
79
  # (`bin/qa_sweep`'s own `persistence_parity_database`, reused here
71
80
  # for the identical reason: a container, not the thing that's
72
81
  # unique per run). `race_schema:`/`reference_schema:` are two
73
- # DIFFERENT disposable schema names this ONE call owns for its own
82
+ # different disposable schema names this one call owns for its own
74
83
  # duration — the caller creates neither ahead of time (both are
75
84
  # wiped fresh by the boots below) and drops both afterward, the
76
85
  # same lifecycle `persistence_parity_schema` already has.
86
+ #
87
+ # @param domain_path [String] filesystem path to the target domain's directory
88
+ # @param steps [Array<Hash>] a generated sequence's own steps, any-keyed (normalized
89
+ # to String keys internally)
90
+ # @param database [String] the shared, never-dropped scratch database to connect to
91
+ # @param race_schema [String] a disposable schema name for the two racing processes
92
+ # @param reference_schema [String] a disposable schema name for the sequential oracle
93
+ # @return [Array<Hash>] `[]` if nothing raceable was found or no divergence occurred;
94
+ # otherwise one or more `field:`-keyed divergence findings
77
95
  def check(domain_path, steps, database:, race_schema:, reference_schema:)
78
96
  normalized = steps.map { |step| step.transform_keys(&:to_s) }
79
- race_index = pick_race_index(normalized)
80
- return [] unless race_index
97
+ lockable, probe_errors = lockable_verbs(domain_path, normalized, database: database, schema: reference_schema)
98
+ race_index = pick_race_index(normalized, lockable)
99
+ unless race_index
100
+ # `[]` here means "nothing to race", and must only ever mean that.
101
+ # A sequence of pure queries/dry-runs is a legitimate clean
102
+ # result; a probe that raised for every verb is not — see
103
+ # `lockable_verbs`, whose own `probe_errors` half exists so this
104
+ # branch can tell the two apart rather than answering `[]` for both.
105
+ return [] if probe_errors.empty?
106
+
107
+ return [{ field: "concurrency_unraceable",
108
+ detail: "no command step could be raced because the cross-process-lock probe failed: " \
109
+ "#{probe_errors.uniq.join('; ')}" }]
110
+ end
81
111
 
82
112
  setup_steps = normalized[0...race_index]
83
113
  race_step = normalized[race_index]
@@ -90,7 +120,7 @@ module Hecks
90
120
  [{ field: "process", detail: "#{e.class}: #{e.message}" }]
91
121
  end
92
122
 
93
- # THE COMMAND STEP CLOSEST TO THE MIDDLE OF THE SEQUENCE — not the
123
+ # The command step closest to the middle of the sequence — not the
94
124
  # first (racing a bare identity-creation with no setup at all is a
95
125
  # legitimate, useful case, so index 0 is not excluded) and not
96
126
  # chosen for any domain-specific reason: a mid-sequence step has, on
@@ -98,13 +128,129 @@ module Hecks
98
128
  # actually conflicting with itself. `nil` when the generated
99
129
  # sequence has no command step at all (every step a query or a dry
100
130
  # run) — nothing here for this seed to race.
101
- def pick_race_index(steps)
131
+ #
132
+ # `lockable_verbs` — optional, but `check` always passes one: the
133
+ # set of verbs whose own aggregate is actually bound to an adapter
134
+ # that declares `:cross_process_lock` in this domain (see
135
+ # `lockable_verbs` below for why this can't be assumed just because
136
+ # the domain binds some of its own aggregates to PostgresEra).
137
+ # Racing anything outside that set is not a legitimate race at
138
+ # all — nil when nothing eligible is left, same as the "no command
139
+ # step" case, never a finding of its own.
140
+ #
141
+ # @param steps [Array<Hash>] a normalized (String-keyed) generated sequence
142
+ # @param lockable_verbs [Array<String>, nil] the verbs eligible to race, as
143
+ # `lockable_verbs` returns; nil to consider every command step eligible
144
+ # @return [Integer, nil] the index of the chosen race step, or nil if no eligible
145
+ # command step exists
146
+ def pick_race_index(steps, lockable_verbs = nil)
102
147
  command_indices = steps.each_index.select { |i| COMMAND_STEP.call(steps[i]) }
148
+ command_indices = command_indices.select { |i| lockable_verbs.include?(steps[i]["verb"]) } if lockable_verbs
103
149
  return nil if command_indices.empty?
104
150
 
105
151
  command_indices[command_indices.size / 2]
106
152
  end
107
153
 
154
+ # Which of this sequence's own command verbs are even candidates to
155
+ # race — BUG#142 (SW-quality_control-1789768606's own first real
156
+ # concurrency run, seed 3): a domain that `persisted_by("PostgresEra")`
157
+ # binds its own aggregates is not thereby binding a framework
158
+ # member it merely `uses_framework`s — `hecksagon_builder.rb`'s own
159
+ # `uses_framework` loads only that member's shape, never its
160
+ # persistence (see `examples/banking/bluebook/banking.hecksagon`'s
161
+ # own comment: a framework member's aggregates need a sibling
162
+ # hecksagon, registered under that member's own name, or
163
+ # `Ports::Persistence::BindingPolicy.default_binding` silently
164
+ # gives them "Memory" — no hecksagon registered under that name at
165
+ # all, so `resolve`'s own `missing_binding` refusal (which only
166
+ # fires when a hecksagon exists for that domain and simply omits
167
+ # this aggregate) never gets a chance to say so).
168
+ # `qa/bluebook/quality_control.hecksagon` attaches `Governance` via
169
+ # `uses_framework` with no such sibling — `Governance::
170
+ # RoleAssignment`/`RoleTransition` are Memory-backed, process-
171
+ # local, in the real ledger, `concurrency` mode included. Racing a
172
+ # Memory-backed aggregate across two real OS processes can never
173
+ # agree with the single-process sequential oracle — each racer's
174
+ # own boot gets its own independent, empty store — no matter how
175
+ # correct any write lock is; that is a guaranteed false positive,
176
+ # not evidence of a broken lock.
177
+ #
178
+ # Boots the same PostgresEra-rebound copy the race itself boots
179
+ # (`boot_preserving_schema` — structural inspection only, nothing
180
+ # dispatched, so the schema it's given is left exactly as it found
181
+ # it) and asks each distinct command verb's own resolved repository
182
+ # whether it actually declares `:cross_process_lock` — the same
183
+ # capability `Interpreting#run_dispatch_order_with_isolation`
184
+ # itself keys off of to decide whether a real advisory lock is
185
+ # even in play for that aggregate. A verb this boot can't resolve
186
+ # at all (a malformed adversarial verb, say) is conservatively
187
+ # excluded, not raced on a guess.
188
+ # Answers `[lockable, probe_errors]`. The errors half exists because
189
+ # the probe below rescues to `false`: a verb that cannot be resolved
190
+ # is indistinguishable, from the outside, from one that resolves fine
191
+ # and simply declares no `:cross_process_lock`. If resolution broke
192
+ # for every verb (a renamed capability symbol, a wiring change),
193
+ # `lockable` came back empty, `check` returned `[]`, and the sweep
194
+ # logged a clean concurrency Check for a race that never happened.
195
+ # `check` reports that case now instead of holding it.
196
+ #
197
+ # @param domain_path [String] filesystem path to the target domain's directory
198
+ # @param steps [Array<Hash>] a normalized (String-keyed) generated sequence
199
+ # @param database [String] the shared, never-dropped scratch database to connect to
200
+ # @param schema [String] a disposable schema name for the structural-inspection boot
201
+ # @return [Array(Array<String>, Array<String>)] `[lockable, probe_errors]` — the
202
+ # command verbs bound to a `:cross_process_lock`-declaring adapter, and one
203
+ # `"verb: Class: message"` entry per verb whose resolution raised
204
+ def lockable_verbs(domain_path, steps, database:, schema:)
205
+ verbs = steps.select { |step| COMMAND_STEP.call(step) }.map { |step| step["verb"] }.uniq
206
+ lockable = []
207
+ probe_errors = []
208
+ boot_preserving_schema(domain_path, database: database, schema: schema) do |copy|
209
+ runtime = Hecks.boot(copy)
210
+ verbs.each do |verb|
211
+ lockable << verb if verb_cross_process_lockable?(runtime, verb, probe_errors)
212
+ end
213
+ end
214
+ [lockable, probe_errors]
215
+ end
216
+
217
+ # Answers whether `verb`'s own resolved repository declares a cross-process lock.
218
+ #
219
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted,
220
+ # structural-inspection-only runtime to resolve `verb` against
221
+ # @param verb [String] the command verb to check
222
+ # @param probe_errors [Array<String>] mutated in place: appended with
223
+ # `"verb: Class: message"` if resolving `verb` raises
224
+ # @return [Boolean] whether `verb`'s aggregate resolves and declares
225
+ # `:cross_process_lock`; false (with an appended probe error) if resolution raises
226
+ def verb_cross_process_lockable?(runtime, verb, probe_errors = [])
227
+ domain, aggregate_name, = Naming.split_verb(verb)
228
+ return false unless domain && aggregate_name
229
+
230
+ aggregate = runtime.registry.bluebook(domain)&.aggregate(aggregate_name)
231
+ return false unless aggregate
232
+
233
+ repository = runtime.registry.repository(domain, aggregate)
234
+ repository.capabilities.include?(:cross_process_lock)
235
+ rescue StandardError => e
236
+ # Still `false` — a verb this boot cannot resolve is not raced on a
237
+ # guess — but no longer silent: `check` needs to tell "nothing here
238
+ # declares a cross-process lock" from "asking broke".
239
+ probe_errors << "#{verb}: #{e.class}: #{e.message}"
240
+ false
241
+ end
242
+
243
+ # Compares the sequential oracle's outcomes against the two racing
244
+ # processes' own outcomes, and reports any crash or disagreement.
245
+ #
246
+ # @param race_step [Hash] the race step, String-keyed
247
+ # @param reference [Array<String>] the sequential oracle's two outcomes,
248
+ # `"succeeded"`, `"refused"`, or `"crashed:..."`
249
+ # @param concurrent [Array<String>] the two racing processes' own outcomes,
250
+ # the same shape as `reference`
251
+ # @return [Array<Hash>] `[]` if the two multisets agree and neither side crashed;
252
+ # otherwise one or more `field:`-keyed divergence findings (`"concurrency_crash"`
253
+ # or `"concurrency_race"`)
108
254
  def divergences_for(race_step, reference, concurrent)
109
255
  crashes = (reference + concurrent).select { |outcome| outcome.start_with?("crashed:") }.uniq
110
256
  return crashes.map { |c| { field: "concurrency_crash", verb: race_step["verb"], detail: c } } if crashes.any?
@@ -117,10 +263,18 @@ module Hecks
117
263
  "#{reference.sort} — the cross-process write lock did not correctly serialize this write" }]
118
264
  end
119
265
 
120
- # THE ORACLE — one boot, one process, the setup then the race step
121
- # TWICE in immediate succession. Nothing else ever touches this
266
+ # The oracle — one boot, one process, the setup then the race step
267
+ # twice in immediate succession. Nothing else ever touches this
122
268
  # schema while this runs, so whatever the domain itself settles on
123
269
  # is correct by construction, not asserted.
270
+ #
271
+ # @param domain_path [String] filesystem path to the target domain's directory
272
+ # @param setup_steps [Array<Hash>] the normalized steps replayed once, sequentially,
273
+ # before the race step
274
+ # @param race_step [Hash] the normalized step dispatched twice in immediate succession
275
+ # @param database [String] the shared, never-dropped scratch database to connect to
276
+ # @param schema [String] this oracle's own disposable schema name
277
+ # @return [Array<String>] the two outcomes `dispatch_one` recorded, in dispatch order
124
278
  def reference_outcomes(domain_path, setup_steps, race_step, database:, schema:)
125
279
  outcomes = []
126
280
  IsolatedBoot.call(domain_path, adapter: :postgres_era, database: database, schema: schema) do |copy|
@@ -132,35 +286,44 @@ module Hecks
132
286
  outcomes
133
287
  end
134
288
 
135
- # THE RACE ITSELF — setup runs ONCE, sequentially, in THIS process
289
+ # The race itself — setup runs once, sequentially, in this process
136
290
  # (the same `IsolatedBoot.call` wipe-then-boot every other mode
137
- # here already uses), and only THEN do the two real racers run.
138
- # Each racer boots its OWN fresh copy of the domain against the
139
- # SAME now-populated schema — `boot_preserving_schema`, below,
291
+ # here already uses), and only then do the two real racers run.
292
+ # Each racer boots its own fresh copy of the domain against the
293
+ # same now-populated schema — `boot_preserving_schema`, below,
140
294
  # deliberately skips the wipe `IsolatedBoot.call` always does, or
141
295
  # the setup this line just wrote would be gone before either racer
142
296
  # ever dispatched anything.
143
297
  #
144
- # REAL, SEPARATE OS PROCESSES, NOT `Thread.new` — `postgres_era_
298
+ # Real, separate OS processes, not `Thread.new` — `postgres_era_
145
299
  # concurrent_dispatch_spec.rb`'s own header explains why:
146
300
  # `Runtime::AggregateLock`'s in-process registry would fully (and
147
- # misleadingly) serialize two THREADS sharing one process even with
301
+ # misleadingly) serialize two threads sharing one process even with
148
302
  # the cross-process lock fix reverted. Only two genuinely separate
149
303
  # OS processes exercise the gap this check exists to catch.
150
304
  #
151
- # `Process.spawn`, NOT `Process.fork` — `bin/qa_sweep`'s own
305
+ # `Process.spawn`, not `Process.fork` — `bin/qa_sweep`'s own
152
306
  # top-of-file comment on `--all` names the identical hazard this
153
- # sidesteps: by the time a `concurrency` seed runs, THIS process
307
+ # sidesteps: by the time a `concurrency` seed runs, this process
154
308
  # already holds the QualityControl ledger's own live PostgresEra
155
309
  # connection (this module's own caller, `bin/qa_sweep`, booted it
156
310
  # long before any seed ran). `Process.fork` duplicates every open
157
311
  # file descriptor, SSL session state included — confirmed live
158
312
  # while wiring this mode up: forking directly from here corrupted
159
- # the LEDGER's own connection the moment either racer child exited,
160
- # surfacing on the NEXT unrelated ledger write, nowhere near this
313
+ # the ledger's own connection the moment either racer child exited,
314
+ # surfacing on the next unrelated ledger write, nowhere near this
161
315
  # method's own code. `bin/qa_concurrency_racer` is this method's own
162
316
  # worker, one real `ruby` process per racer — read that script's own
163
317
  # header for the rest of this reasoning.
318
+ #
319
+ # @param domain_path [String] filesystem path to the target domain's directory
320
+ # @param setup_steps [Array<Hash>] the normalized steps replayed once, sequentially,
321
+ # before the two racers spawn
322
+ # @param race_step [Hash] the normalized step both racer processes dispatch
323
+ # @param database [String] the shared, never-dropped scratch database to connect to
324
+ # @param schema [String] this race's own disposable schema name, shared by both racers
325
+ # @return [Array<String>] the two racers' own outcomes, in `Process.wait` order;
326
+ # `"crashed:no output from bin/qa_concurrency_racer"` for a racer that printed nothing
164
327
  def concurrent_outcomes(domain_path, setup_steps, race_step, database:, schema:)
165
328
  IsolatedBoot.call(domain_path, adapter: :postgres_era, database: database, schema: schema) do |copy|
166
329
  dispatch_all!(Hecks.boot(copy), setup_steps)
@@ -186,14 +349,19 @@ module Hecks
186
349
  end
187
350
  end
188
351
 
189
- # ONE STEP, ONE OUTCOME — never raises: a declared domain refusal is
352
+ # One step, one outcome — never raises: a declared domain refusal is
190
353
  # "refused" (the expected, ordinary answer a `given`/invariant can
191
354
  # give), anything else escaping is "crashed:<class>: <message>", a
192
355
  # genuine finding this module's own caller surfaces rather than lets
193
356
  # kill a forked racer silently.
357
+ #
358
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
359
+ # to dispatch against
360
+ # @param step [Hash] the normalized (String-keyed) step to dispatch
361
+ # @return [String] `"succeeded"`, `"refused"`, or `"crashed:<class>: <message>"`
194
362
  def dispatch_one(runtime, step)
195
363
  args = (step["args"] || {}).transform_keys(&:to_sym)
196
- runtime.dispatch(step["verb"], **args)
364
+ runtime.dispatch_flat(step["verb"], args)
197
365
  "succeeded"
198
366
  rescue *Hecks::Runtime::DOMAIN_REFUSALS, Hecks::Bluebook::Expression::EvaluationError
199
367
  "refused"
@@ -201,12 +369,21 @@ module Hecks
201
369
  "crashed:#{e.class}: #{e.message}"
202
370
  end
203
371
 
204
- # SETUP TOLERATES AN ORDINARY REFUSAL (a generated sequence's own
372
+ # Dispatches every step in order, raising the first time one crashes.
373
+ #
374
+ # Setup tolerates an ordinary refusal (a generated sequence's own
205
375
  # earlier step can legitimately refuse — every other mode in this
206
376
  # practice already replays a prefix that way) but never a crash: an
207
377
  # unexpected exception during setup means the schema this race is
208
378
  # about to run against is in an unknown state, which is itself
209
379
  # worth surfacing, not silently racing anyway.
380
+ #
381
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
382
+ # to dispatch against
383
+ # @param steps [Array<Hash>] the normalized steps to dispatch, in order
384
+ # @return [void]
385
+ # @raise [RuntimeError] if any step's own `dispatch_one` outcome is a crash, naming
386
+ # the step's verb and outcome
210
387
  def dispatch_all!(runtime, steps)
211
388
  steps.each do |step|
212
389
  outcome = dispatch_one(runtime, step)
@@ -214,19 +391,30 @@ module Hecks
214
391
  end
215
392
  end
216
393
 
217
- # THE SAME COPY-AND-REBIND `IsolatedBoot.call(..., adapter:
218
- # :postgres_era, ...)` DOES, MINUS THE SCHEMA WIPE — deliberately
219
- # NOT reusing `IsolatedBoot.rebind_to_postgres_era!` itself, which
394
+ # Boots a `PostgresEra`-rebound copy of the domain against an
395
+ # existing schema, without wiping it first.
396
+ #
397
+ # The same copy-and-rebind `IsolatedBoot.call(..., adapter:
398
+ # :postgres_era, ...)` does, minus the schema wipe — deliberately
399
+ # not reusing `IsolatedBoot.rebind_to_postgres_era!` itself, which
220
400
  # bundles `ensure_postgres_era_schema!`'s own `DROP SCHEMA` into the
221
401
  # same call with no way to opt out (see that method's own header:
222
402
  # the wipe is the "zero-history guarantee every other adapter mode
223
- # already gives," exactly the guarantee THIS caller must NOT have —
403
+ # already gives," exactly the guarantee this caller must not have —
224
404
  # the whole point of a race is booting against what setup already
225
405
  # wrote). `copy_dereferencing`/`rewrite_bindings!` are the same two
226
406
  # public steps that method itself calls first; only the `.world`
227
407
  # this writes is duplicated from it, not re-derived, because the
228
408
  # shape a `PostgresEra`-bound copy's `.world` needs is exactly that
229
409
  # method's own, one step short.
410
+ #
411
+ # @param domain_path [String] filesystem path to the target domain's directory
412
+ # @param database [String] the PostgresEra database name to bind the copy to
413
+ # @param schema [String] the existing PostgresEra schema name to bind the copy to,
414
+ # left exactly as found
415
+ # @yield [copy] runs against the rebound copy
416
+ # @yieldparam copy [String] filesystem path to the rebound domain copy
417
+ # @return [Object] the block's own result
230
418
  def boot_preserving_schema(domain_path, database:, schema:)
231
419
  Dir.mktmpdir("hecks-concurrency") do |tmp|
232
420
  copy = File.join(tmp, File.basename(domain_path))
@@ -238,6 +426,14 @@ module Hecks
238
426
  end
239
427
  end
240
428
 
429
+ # Writes a `.world` file binding every hecksagon-named member in
430
+ # `copy` to `database`/`schema`, replacing any other `.world` file
431
+ # the copy already carries.
432
+ #
433
+ # @param copy [String] filesystem path to the domain copy to rewrite
434
+ # @param database [String] the PostgresEra database name to write into the `.world` file
435
+ # @param schema [String] the PostgresEra schema name to write into the `.world` file
436
+ # @return [void]
241
437
  def write_postgres_era_world!(copy, database:, schema:)
242
438
  Dir.glob(File.join(copy, "**", "*.hecksagon")).each do |hecksagon_path|
243
439
  names = File.read(hecksagon_path).scan(/Hecks\.hecksagon\s+"([^"]+)"/).flatten.uniq