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
@@ -2,20 +2,22 @@ require "hecks/ports/persistence/plugins/era"
2
2
 
3
3
  module Hecks
4
4
  module Fuzzing
5
- # THE FORK-LOSS CLASS, AS A CHECK — not a fuzz. Every other mode in
6
- # this file generates a sequence and compares two answers to the SAME
5
+ # **The fork-loss class, as a check** — not a fuzz. Every other mode in
6
+ # this file generates a sequence and compares two answers to the same
7
7
  # question; this one asks a single, unconditional question of a
8
- # target's own REAL, already-configured `PostgresEra` ledger:
8
+ # target's own real, already-configured `PostgresEra` ledger:
9
9
  # does any ancestor era still hold writes nobody has merged forward?
10
10
  #
11
- # THE BUG THIS TARGETS, NAMED EXACTLY. Minting a new era (an attribute
11
+ # ## The bug this targets, named exactly
12
+ #
13
+ # Minting a new era (an attribute
12
14
  # or aggregate addition — `StorageShape.project`, lib/hecks/ports/
13
15
  # persistence/plugins/era/storage_shape.rb) advances the readable head
14
- # to a NEW partition; an OLD checkout, or a process that boots slower
16
+ # to a new partition; an old checkout, or a process that boots slower
15
17
  # than the mint, can keep writing into the era it still believes is
16
18
  # current. Those writes are not lost — `Lineage#diverged_count`
17
19
  # (postgres_era/lineage/tail_merge.rb) can always find them — but
18
- # nothing EVER asked it automatically. The practice's own ledger found
20
+ # nothing ever asked it automatically. The practice's own ledger found
19
21
  # exactly this live, twice: once as an operational gap this session's
20
22
  # Step 0 recovered by hand (`bin/merge_tail`, three conflicting
21
23
  # records, three eras deep), and once as BUG#24 (a superuser
@@ -26,15 +28,17 @@ module Hecks
26
28
  # line already answers by hand, run as an ordinary sweep Check instead
27
29
  # of only when a human remembers to ask.
28
30
  #
29
- # THE TARGET'S REAL DATABASE, READ-ONLY, NEVER A DISPOSABLE ONE — every
31
+ # ## Connection and safety
32
+ #
33
+ # The target's real database, read-only, never a disposable one — every
30
34
  # other Postgres-touching mode here (`persistence_parity`,
31
35
  # `adapter_parity_sqlite`) owns a throwaway schema for the exact
32
36
  # reason it must never look at what a real deployment actually holds;
33
- # this check exists FOR what a real deployment actually holds, so it
37
+ # this check exists for what a real deployment actually holds, so it
34
38
  # connects the same way `bin/merge_tail` itself does — the target's
35
39
  # own `.world` binding — and never writes anything: `eras`/
36
40
  # `diverged_count` are both plain `SELECT`s, and `ensure_base!` only
37
- # ever provisions (`CREATE TABLE IF NOT EXISTS`) when THIS connection
41
+ # ever provisions (`CREATE TABLE IF NOT EXISTS`) when this connection
38
42
  # is the database's own provisioning owner, the identical idempotent
39
43
  # call every ordinary boot already makes. A target with no real,
40
44
  # reachable database configured (nothing provisioned locally, say) is
@@ -44,21 +48,39 @@ module Hecks
44
48
  module EraBoundary
45
49
  module_function
46
50
 
51
+ # Reports whether any ancestor `PostgresEra` era still holds writes the
52
+ # head has never merged forward, for one target's real database.
53
+ #
47
54
  # `{ checked: true, diverged_total:, breakdown: [{ordinal:, diverged:}] }`
48
- # or `{ checked: false, reason: "..." }` — the caller decides what a
49
- # `false` means (an operational note, not a finding); a `true` with
55
+ # or `{ checked: false, kind:, reason: "..." }`. A `true` with
50
56
  # `diverged_total.positive?` is the finding this module exists to
51
57
  # surface: real post-cut writes an ancestor era is still holding.
58
+ #
59
+ # `kind:` distinguishes the two different reasons `checked: false` can
60
+ # occur — without it, `bin/qa_sweep` would log a refused connection
61
+ # and a `Lineage` defect exactly like an audit that ran and found
62
+ # nothing, counting either toward `sweep.made` and the target's clean
63
+ # streak. `:not_applicable` is a target with no lineage to audit at
64
+ # all; `:error` is the audit failing to run, which the caller
65
+ # surfaces as a finding rather than holding.
66
+ #
67
+ # @param domain_path [String] filesystem path to the target domain's bluebook
68
+ # directory, or a directory containing one
69
+ # @return [Hash{Symbol => Object}] on success, `{checked: true, era_count: Integer,
70
+ # breakdown: Array<Hash{ordinal: Integer, diverged: Integer}>, diverged_total: Integer}`;
71
+ # otherwise `{checked: false, kind: Symbol, reason: String}`
52
72
  def diverged_ancestor_writes(domain_path)
53
73
  registry, directory = load_registry(domain_path)
54
74
  bluebook = registry.bluebooks.values.first
55
- return unchecked("no bluebook in #{directory}") unless bluebook
75
+ return failed("no bluebook in #{directory}") unless bluebook
56
76
 
57
77
  first = bluebook.aggregates.first
58
- return unchecked("#{bluebook.name} declares no aggregates") unless first
78
+ return not_applicable("#{bluebook.name} declares no aggregates") unless first
59
79
 
60
80
  adapter_name = Hecks::Ports::Persistence::BindingPolicy.resolve(registry, bluebook.name, first).adapter
61
- return unchecked("#{bluebook.name} is bound to #{adapter_name}, not PostgresEra") unless adapter_name == "PostgresEra"
81
+ unless adapter_name == "PostgresEra"
82
+ return not_applicable("#{bluebook.name} is bound to #{adapter_name}, not PostgresEra")
83
+ end
62
84
 
63
85
  settings = registry.world(bluebook.name)&.for_binding(Hecks::Ports::Persistence::VERB, adapter_name) || {}
64
86
  db = Hecks::Adapters::PostgresEra.connect_for(bluebook.name, settings)
@@ -81,14 +103,41 @@ module Hecks
81
103
  db.close
82
104
  end
83
105
  rescue StandardError => e
84
- unchecked("#{e.class}: #{e.message}")
106
+ # Could not audit is not the same as nothing to audit — a refused
107
+ # connection, a `Lineage` defect, malformed `.world` settings. Each
108
+ # of these answers `:error`, not the benign `:not_applicable` shape,
109
+ # so `bin/qa_sweep` surfaces it as a finding rather than holding it
110
+ # as a clean audit.
111
+ failed("#{e.class}: #{e.message}")
85
112
  end
86
113
 
87
- def unchecked(reason) = { checked: false, reason: reason }
114
+ # Builds the `checked: false` result for a target with no PostgresEra
115
+ # lineage to audit — no ancestor era can carry post-cut writes when
116
+ # there is no lineage at all. Not a finding, and not evidence of
117
+ # anything either: the caller logs no Check at all.
118
+ #
119
+ # @param reason [String] human-readable explanation of why no lineage applies
120
+ # @return [Hash{Symbol => Object}] `{checked: false, kind: :not_applicable, reason: String}`
121
+ def not_applicable(reason) = { checked: false, kind: :not_applicable, reason: reason }
122
+
123
+ # Builds the `checked: false` result for an audit that could not run
124
+ # — reported as a finding, never held.
125
+ #
126
+ # @param reason [String] human-readable explanation of what failed, typically
127
+ # `"#{exception.class}: #{exception.message}"`
128
+ # @return [Hash{Symbol => Object}] `{checked: false, kind: :error, reason: String}`
129
+ def failed(reason) = { checked: false, kind: :error, reason: reason }
88
130
 
89
- # THE SAME LOAD `bin/merge_tail` ITSELF PERFORMS (that script's own
90
- # top half) — a fresh `Registry`, never the ledger's own (this asks
91
- # about the SWEPT TARGET's lineage, not QualityControl's own).
131
+ # Boots a fresh registry for the domain at `domain_path` — the same
132
+ # load `bin/merge_tail` itself performs (that script's own top half),
133
+ # never the ledger's own registry (this asks about the swept target's
134
+ # lineage, not QualityControl's own).
135
+ #
136
+ # @param domain_path [String] filesystem path to the target domain's bluebook
137
+ # directory, or a directory containing one
138
+ # @return [Array(Runtime::Registry, String)] the booted registry and the resolved
139
+ # bluebook directory path
140
+ # @raise [Errno::ENOENT] if `domain_path` names no existing domain directory
92
141
  def load_registry(domain_path)
93
142
  loading = Hecks::Ports::Loading.bootstrap
94
143
  directory = loading.bluebook_directory(domain_path)
@@ -1,27 +1,28 @@
1
1
  require "json"
2
+ require_relative "../corpus"
2
3
 
3
4
  module Hecks
4
5
  module Fuzzing
5
- # WHAT AN AGGREGATE CAN EXHIBIT, AND WHICH PAIRS IT PUTS TOGETHER.
6
+ # What an aggregate can exhibit, and which pairs it puts together.
6
7
  #
7
8
  # Extracted from `spec/combination_coverage_spec.rb`'s own pairwise
8
9
  # table so it has exactly two consumers that can never drift: that
9
10
  # spec (the golden corpus, held to every pair) and `bin/qa_domain_
10
- # novelty` (a CANDIDATE stress domain, measured against every path
11
+ # novelty` (a candidate stress domain, measured against every path
11
12
  # the QA ledger already sweeps — see that script's own header for why
12
13
  # a new domain has to name the pair no existing target meets before
13
14
  # it earns a place in the rotation).
14
15
  #
15
- # THE UNIT IS ONE AGGREGATE. Two forms in the same chapter but
16
+ # **The unit is one aggregate**. Two forms in the same chapter but
16
17
  # different heads never meet at dispatch; two forms on one head do
17
18
  # — that spec's own header has the four defects that argument came
18
19
  # from. Each entry below is a form the language declares and a
19
20
  # runtime has to handle, chosen because it has produced a defect or
20
21
  # sits one step from one; adding one here is how a new form joins
21
- # BOTH gates at once, and it will name its own uncovered pairs on the
22
+ # both gates at once, and it will name its own uncovered pairs on the
22
23
  # first run of each.
23
24
  #
24
- # ONE FLAT TABLE, ON PURPOSE — each entry is an independent boolean
25
+ # **One flat table, on purpose** — each entry is an independent boolean
25
26
  # check against the same string-keyed aggregate IR hash (the shape
26
27
  # `spec/golden/ir/*.json` carries and `Projector::Exporter.call`
27
28
  # round-trips to through JSON), laid out so every declared form can
@@ -48,7 +49,7 @@ module Hecks
48
49
  "closed_set" => ->(a) { (a["value_objects"] || []).any? { |shape| shape["closed_set"] } },
49
50
  "has_default" => ->(a) { attributes(a).any? { |held| !held["default"].nil? } },
50
51
  "has_optional" => ->(a) { commands(a).any? { |verb| (verb["attributes"] || []).any? { |held| held["optional"] } } },
51
- # THE REFERENCE-HOP FAMILY (ANGLE-2, qa/bluebook ledger) — the
52
+ # The reference-hop family (angle-2, qa/bluebook ledger) — the
52
53
  # forms `qa/stress_domains/referral_chain` exists for, absent
53
54
  # from the census until that domain named them. Each is one
54
55
  # step from a catalogued gap: `two_hop_given` is `dereference`'s
@@ -59,6 +60,19 @@ module Hecks
59
60
  # command redeclaring the aggregate's own reference field under
60
61
  # a non-reference type, so only `resolve_state_references` (never
61
62
  # ported) can catch a dangling id.
63
+ # A retroactive correction, and a role-gated command — both are
64
+ # declared forms this census could not see, on aggregates it was
65
+ # already measuring: `examples/banking` has carried `corrects`
66
+ # mutations and `role`-bearing commands the whole time, and
67
+ # `qa/stress_domains/corrections` exists for the first. Three
68
+ # stress domains' own NOTES.md record `bin/qa_domain_novelty`
69
+ # answering "no new pair" for a domain whose whole point was a
70
+ # shape this table did not name (case_escalation's is the
71
+ # bluntest: "read that as a gap in the census, not in this
72
+ # domain"). Entity commands count for both — BUG#30-33 and
73
+ # BUG#31 were all entity-level `corrects`.
74
+ "corrects" => ->(a) { every_command(a).any? { |verb| corrects?(verb) } },
75
+ "role_gated" => ->(a) { every_command(a).any? { |verb| !verb["role"].to_s.empty? } },
62
76
  "two_hop_given" => ->(a) { two_hop_given?(a) },
63
77
  "multi_hop_where" => ->(a) { multi_hop_where?(a) },
64
78
  "revalued_reference" => ->(a) { revalued_reference?(a) }
@@ -66,25 +80,80 @@ module Hecks
66
80
 
67
81
  module_function
68
82
 
83
+ # Reads an aggregate IR hash's own declared entities.
84
+ #
85
+ # @param aggregate [Hash] a string-keyed aggregate IR hash
86
+ # @return [Array<Hash>] the aggregate's own declared entities ("pieces"); `[]`
87
+ # if it declares none
69
88
  def entities(aggregate) = aggregate["entities"] || []
89
+
90
+ # Reads an aggregate IR hash's own declared commands.
91
+ #
92
+ # @param aggregate [Hash] a string-keyed aggregate IR hash
93
+ # @return [Array<Hash>] the aggregate's own declared commands; `[]` if it
94
+ # declares none
70
95
  def commands(aggregate) = aggregate["commands"] || []
96
+
97
+ # One aggregate's commands, its pieces' included — a form carried by
98
+ # an entity command is carried by the aggregate that owns it, the
99
+ # same way `composite_piece`/`piece_lifecycle` already read pieces.
100
+ #
101
+ # @param aggregate [Hash] a string-keyed aggregate IR hash
102
+ # @return [Array<Hash>] `aggregate`'s own commands, plus every entity's own
103
+ def every_command(aggregate) = commands(aggregate) + entities(aggregate).flat_map { |piece| commands(piece) }
104
+
105
+ # Answers whether a command IR hash declares a `corrects` mutation.
106
+ #
107
+ # @param verb [Hash] a string-keyed command IR hash
108
+ # @return [Boolean] true if `verb` declares a `corrects` mutation
109
+ def corrects?(verb) = (verb["mutations"] || []).any? { |change| change["op"].to_s == "corrects" }
110
+
111
+ # Reads an aggregate IR hash's own declared attributes.
112
+ #
113
+ # @param aggregate [Hash] a string-keyed aggregate IR hash
114
+ # @return [Array<Hash>] the aggregate's own declared attributes; `[]` if it
115
+ # declares none
71
116
  def attributes(aggregate) = aggregate["attributes"] || []
117
+
118
+ # Reads an aggregate IR hash's own declared queries.
119
+ #
120
+ # @param aggregate [Hash] a string-keyed aggregate IR hash
121
+ # @return [Array<Hash>] the aggregate's own declared queries; `[]` if it
122
+ # declares none
72
123
  def queries(aggregate) = aggregate["queries"] || []
124
+
125
+ # Answers whether an attribute IR hash's own declared type is a reference.
126
+ #
127
+ # @param attribute [Hash] a string-keyed attribute IR hash
128
+ # @return [Boolean] true if `attribute`'s own declared type is a reference
73
129
  def reference?(attribute) = attribute["type"].to_s.start_with?("Reference<")
74
130
 
131
+ # Answers whether any of an aggregate's own commands crosses a two-hop given.
132
+ #
133
+ # @param aggregate [Hash] a string-keyed aggregate IR hash
134
+ # @return [Boolean] true if any command's own given crosses at least two
135
+ # references (`TWO_HOP_GIVEN_PATH_LENGTH`)
75
136
  def two_hop_given?(aggregate)
76
137
  commands(aggregate).any? { |verb| (verb["givens"] || []).any? { |given| deep_lookup?(given["ast"]) } }
77
138
  end
78
139
 
79
140
  # A `where` whose field crosses two `/` — `member/sponsor/standing`.
141
+ #
142
+ # @param aggregate [Hash] a string-keyed aggregate IR hash
143
+ # @return [Boolean] true if any query's own where clause crosses at least
144
+ # two `/` hops
80
145
  def multi_hop_where?(aggregate)
81
146
  queries(aggregate).any? { |query| (query["wheres"] || []).any? { |where| where["field"].to_s.count("/") >= 2 } }
82
147
  end
83
148
 
84
149
  # A command attribute sharing a name with one of the aggregate's
85
- # own reference-typed attributes while carrying a DIFFERENT, non-
150
+ # own reference-typed attributes while carrying a different, non-
86
151
  # reference type — `attribute :member, Handle` against
87
152
  # `reference_to Member`.
153
+ #
154
+ # @param aggregate [Hash] a string-keyed aggregate IR hash
155
+ # @return [Boolean] true if any command redeclares one of the aggregate's
156
+ # own reference-typed attribute names under a non-reference type
88
157
  def revalued_reference?(aggregate)
89
158
  references = attributes(aggregate).select { |held| reference?(held) }.to_set { |held| held["name"].to_s }
90
159
  commands(aggregate).any? do |verb|
@@ -94,6 +163,11 @@ module Hecks
94
163
 
95
164
  # Walks a given's own exported AST for any `lookup` whose path is
96
165
  # long enough to have crossed two references.
166
+ #
167
+ # @param node [Hash, Array, Object] an AST node (or subtree) from an
168
+ # exported given's `"ast"`
169
+ # @return [Boolean] true if any `lookup` node's own path is at least
170
+ # `TWO_HOP_GIVEN_PATH_LENGTH` long
97
171
  def deep_lookup?(node)
98
172
  case node
99
173
  when Hash
@@ -108,21 +182,39 @@ module Hecks
108
182
  end
109
183
 
110
184
  # Every form, answered for one aggregate — the table above, applied.
185
+ #
186
+ # @param aggregate [Hash] a string-keyed aggregate IR hash
187
+ # @return [Hash{String => Boolean}] every `FORMS` name mapped to whether
188
+ # `aggregate` exhibits it
111
189
  def properties(aggregate)
112
190
  FORMS.transform_values { |form| form.call(aggregate) }
113
191
  end
114
192
 
115
193
  # Every unordered pair of forms, each rendered "left + right" in
116
194
  # alphabetical order — the key both gates' excuse tables use.
195
+ #
196
+ # @return [Array<String>] every unordered pair of `FORMS` names, as
197
+ # `"left + right"`
117
198
  def pairs
118
199
  FORMS.keys.combination(2).map { |pair| pair_key(*pair) }
119
200
  end
120
201
 
202
+ # Renders two form names as one sorted pair key.
203
+ #
204
+ # @param left [String] a form name
205
+ # @param right [String] a form name
206
+ # @return [String] `left`/`right`, alphabetically ordered, joined as
207
+ # `"left + right"`
121
208
  def pair_key(left, right) = [left, right].sort.join(" + ")
122
209
 
123
210
  # `held` is `[[aggregate_name, properties], ...]`. Answers which
124
- # pairs are met on ONE aggregate, and by which — a Hash from pair
211
+ # pairs are met on one aggregate, and by which — a Hash from pair
125
212
  # key to the names carrying it, so a caller can say who.
213
+ #
214
+ # @param held [Array<Array(String, Hash)>] `[aggregate_name, properties]`
215
+ # pairs, `properties` as returned by `#properties`
216
+ # @return [Hash{String => Array<String>}] every met pair key mapped to the
217
+ # aggregate names that carry it
126
218
  def covered_pairs(held)
127
219
  held.each_with_object(Hash.new { |h, k| h[k] = [] }) do |(name, shows), covered|
128
220
  shows.select { |_, present| present }.keys.combination(2).each do |left, right|
@@ -134,33 +226,42 @@ module Hecks
134
226
  # `[[\"Chapter::Aggregate\", properties], ...]` for every aggregate
135
227
  # a string-keyed chapter IR declares — the same walk the golden
136
228
  # spec makes over `spec/golden/ir/*.json`.
229
+ #
230
+ # @param chapter_ir [Hash] a string-keyed chapter IR hash, with `"name"` and
231
+ # `"aggregates"`
232
+ # @return [Array<Array(String, Hash)>] `["Chapter::Aggregate", properties]`
233
+ # pairs, one per declared aggregate, `properties` as returned by
234
+ # `#properties`
137
235
  def aggregates_in(chapter_ir)
138
236
  (chapter_ir["aggregates"] || []).map do |aggregate|
139
237
  ["#{chapter_ir['name']}::#{aggregate['name']}", properties(aggregate)]
140
238
  end
141
239
  end
142
240
 
143
- # WHERE A DOMAIN PATH KEEPS ITS BLUEBOOKS — `<domain>/bluebook/*.
144
- # bluebook` (every example and stress domain), or the directory
145
- # itself (`qa/bluebook`, the ledger's own `Target.path`): the same
146
- # two shapes `bin/model_check`'s `bluebook_in` reads. `nil` when
147
- # neither holds a bluebook.
241
+ # Where a domain path keeps its bluebooks — see
242
+ # `Hecks::Corpus.bluebook_files`, the one definition every corpus
243
+ # walk shares. `nil` when neither shape holds a bluebook.
244
+ #
245
+ # @param domain_path [String] path to a domain directory
246
+ # @return [Array<String>, nil] the domain's own `.bluebook` file paths, or
247
+ # `nil` if it holds none
148
248
  def bluebook_files(domain_path)
149
- [File.join(domain_path, "bluebook"), domain_path].each do |dir|
150
- files = Dir[File.join(dir, "*.bluebook")]
151
- return files unless files.empty?
152
- end
153
- nil
249
+ Hecks::Corpus.bluebook_files(domain_path)
154
250
  end
155
251
 
156
- # THE SAME CENSUS OVER A DOMAIN ON DISK, booted the lightweight
252
+ # The same census over a domain on disk, booted the lightweight
157
253
  # way `bin/model_check` and `Hecks::Codemod.load_bluebook` already
158
254
  # do (ports and the two in-process adapters, no `Hecks.boot`, no
159
- # live database, no `.hecksagon`: the census reads declared SHAPE,
255
+ # live database, no `.hecksagon`: the census reads declared shape,
160
256
  # and a framework chapter a `.hecksagon` would attach is not this
161
257
  # domain's own). Only the domain's own chapter is measured — the
162
258
  # first bluebook loaded, the same "target chapter is always first"
163
259
  # fact `bin/project_rust` relies on.
260
+ # @param domain_path [String] path to a domain directory
261
+ # @return [Array<Array(String, Hash)>] `["Chapter::Aggregate", properties]`
262
+ # pairs, one per aggregate declared on `domain_path`'s own first-loaded
263
+ # chapter (see `Hecks::Projector::Exporter.call`)
264
+ # @raise [ArgumentError] if `domain_path` has no bluebook files to measure
164
265
  def census(domain_path)
165
266
  root = File.expand_path("../../..", __dir__)
166
267
  files = bluebook_files(domain_path)
@@ -0,0 +1,171 @@
1
+ require "json"
2
+ require_relative "isolated_boot"
3
+ require_relative "sequence_generator"
4
+ require_relative "replay"
5
+ require_relative "shrinker"
6
+ require_relative "differential"
7
+
8
+ module Hecks
9
+ module Fuzzing
10
+ # **One generated domain, checked** — the child-process half of
11
+ # `bin/qa_generated_domains` (one domain per process: every generated
12
+ # domain is named `QaGenerated`, see DomainGenerator's header).
13
+ #
14
+ # The same comparisons `bin/qa_sweep` runs on a rotation target, over
15
+ # sequences generated against a domain nobody wrote: with a compiled
16
+ # binary, `Differential.diff` (Ruby vs Rust, self-consistency, declared
17
+ # properties); without one, a Ruby-only replay (properties,
18
+ # self-consistency, an interpreter crash). A domain that does not even
19
+ # boot is `invalid` — a generator defect, not a finding, reported apart.
20
+ #
21
+ # `match:` is how domain-level shrinking asks "does this smaller domain
22
+ # still show the same finding?": only a divergence in the same mode
23
+ # whose `Shrinker.signature` contains the original's counts. Verb names
24
+ # survive a removal that did not touch them, so the signature does too.
25
+ module GeneratedDomainCheck
26
+ DIFFERENTIAL_MODES = %i[differential self_consistency properties_in_differential].freeze
27
+
28
+ module_function
29
+
30
+ # Checks one generated domain over up to `seeds` generated sequences, stopping
31
+ # at the first divergence found.
32
+ #
33
+ # @param domain_path [String] path to the generated domain directory
34
+ # @param seeds [Integer] maximum number of seeds to try before declaring clean
35
+ # @param steps [Integer] steps to generate per seed
36
+ # @param adversarial [Float] fraction of command steps to mutate adversarially
37
+ # @param binary [String, nil] path to a compiled Rust conformance binary to
38
+ # diff against, or `nil` for a Ruby-only check
39
+ # @param differ [Object, nil] duck-typed comparison helper answering
40
+ # `RustConformanceHelpers`' interface (adapter-defined; constructed ad hoc
41
+ # by callers such as bin/qa_generated_domains), or `nil`
42
+ # @param match [Hash, nil] `{"mode" =>, "signature" =>}` from a parent
43
+ # domain's own finding — only a divergence matching it counts, for
44
+ # domain-level shrinking; `nil` accepts the first divergence found
45
+ # @param shrink_budget [Integer] step-level shrink attempts to spend on a
46
+ # found finding; `0` skips shrinking
47
+ # @return [Hash] `{"status" => "invalid", "error" =>}` if the domain does not
48
+ # boot; `{"status" => "clean", "seeds_run" =>}` if no seed diverged;
49
+ # otherwise a finding merged with `{"status" => "found", "seeds_run" =>}`
50
+ # (see `#finding`), plus `"shrunk_steps"` when `shrink_budget` is positive
51
+ def run(domain_path, seeds:, steps:, adversarial:, binary: nil, differ: nil, match: nil, shrink_budget: 0)
52
+ error = boot_error(domain_path)
53
+ return { "status" => "invalid", "error" => error } if error
54
+
55
+ (1..seeds).each do |seed|
56
+ finding = check_seed(domain_path, seed, steps, adversarial, binary, differ, match)
57
+ next unless finding
58
+
59
+ finding["shrunk_steps"] = shrink(domain_path, finding, binary, differ, shrink_budget) if shrink_budget.positive?
60
+ return finding.merge("status" => "found", "seeds_run" => seed)
61
+ end
62
+ { "status" => "clean", "seeds_run" => seeds }
63
+ end
64
+
65
+ # Boots `domain_path` in an isolated copy, to find out whether it boots at all.
66
+ #
67
+ # @param domain_path [String] path to the domain directory to boot
68
+ # @return [String, nil] `nil` if the boot succeeds; otherwise the raising
69
+ # exception's class and first message line
70
+ def boot_error(domain_path)
71
+ IsolatedBoot.call(domain_path) { |copy| Hecks.boot(copy) }
72
+ nil
73
+ rescue StandardError, ScriptError => e
74
+ "#{e.class}: #{e.message.lines.first&.strip}"
75
+ end
76
+
77
+ # Generates one sequence for `seed` and checks it for a divergence.
78
+ #
79
+ # @param domain_path [String] path to the domain directory
80
+ # @param seed [Integer] RNG seed for `SequenceGenerator.generate`
81
+ # @param steps [Integer] steps to generate
82
+ # @param adversarial [Float] fraction of command steps to mutate adversarially
83
+ # @param binary [String, nil] path to a compiled Rust conformance binary, or
84
+ # `nil` for a Ruby-only check
85
+ # @param differ [Object, nil] duck-typed comparison helper answering
86
+ # `RustConformanceHelpers`' interface (adapter-defined), or `nil`
87
+ # @param match [Hash, nil] `{"mode" =>, "signature" =>}` a divergence must
88
+ # match to count, or `nil` to accept the first one found
89
+ # @return [Hash, nil] a finding (see `#finding`) if a matching divergence was
90
+ # found, or if generation itself crashed; `nil` if the sequence was clean
91
+ def check_seed(domain_path, seed, steps, adversarial, binary, differ, match)
92
+ sequence = SequenceGenerator.generate(domain_path, seed: seed, steps: steps, adversarial: adversarial)
93
+ rescue StandardError => e
94
+ finding(seed, :generator, [{ field: "generator_crash", detail: "#{e.class}: #{e.message}" }], [], match)
95
+ else
96
+ outcomes(domain_path, sequence, binary, differ).each do |mode, divergences|
97
+ found = finding(seed, mode, divergences, sequence, match)
98
+ return found if found
99
+ end
100
+ nil
101
+ end
102
+
103
+ # Runs the comparison appropriate to whether a compiled binary is available:
104
+ # `Differential.diff` against Rust when `binary` is given, otherwise a
105
+ # Ruby-only replay checked for property and self-consistency divergences.
106
+ #
107
+ # @param domain_path [String] path to the domain directory
108
+ # @param sequence [Array<Hash>] the generated step sequence to replay
109
+ # @param binary [String, nil] path to a compiled Rust conformance binary, or
110
+ # `nil` for a Ruby-only check
111
+ # @param differ [Object, nil] duck-typed comparison helper answering
112
+ # `RustConformanceHelpers`' interface (adapter-defined), or `nil`
113
+ # @return [Hash{Symbol => Array<Hash>}] one divergence list per mode checked;
114
+ # `{ruby_only: [{field: "crash", detail:}]}` if the Ruby-only replay itself
115
+ # raised
116
+ def outcomes(domain_path, sequence, binary, differ)
117
+ return Differential.diff(differ, domain_path, sequence, binary, modes: DIFFERENTIAL_MODES) if binary
118
+
119
+ history = Replay.call(domain_path, sequence, self_consistency: true)
120
+ { ruby_only: Differential.property_divergences(history),
121
+ self_consistency: Differential.self_consistency_divergences(history) }
122
+ rescue StandardError => e
123
+ { ruby_only: [{ field: "crash", detail: "#{e.class}: #{e.message}" }] }
124
+ end
125
+
126
+ # Builds a reported finding from a mode's divergences, unless `divergences`
127
+ # is empty or `match` names a different signature.
128
+ #
129
+ # @param seed [Integer] the seed that produced `sequence`
130
+ # @param mode [Symbol] the comparison mode the divergences came from
131
+ # @param divergences [Array<Hash>] divergence entries for `mode`
132
+ # @param sequence [Array<Hash>] the generated step sequence
133
+ # @param match [Hash, nil] `{"mode" =>, "signature" =>}` the finding must
134
+ # match, or `nil` to accept any
135
+ # @return [Hash, nil] `{"seed" =>, "mode" =>, "signature" =>, "steps" =>,
136
+ # "divergences" =>}` if `divergences` is non-empty and matches `match`;
137
+ # `nil` otherwise
138
+ def finding(seed, mode, divergences, sequence, match)
139
+ return nil if divergences.empty?
140
+
141
+ signature = Shrinker.signature(divergences)
142
+ return nil if match && !(match["mode"] == mode.to_s && Set.new(match["signature"]).subset?(signature))
143
+
144
+ { "seed" => seed, "mode" => mode.to_s, "signature" => signature.to_a.sort, "steps" => sequence,
145
+ "divergences" => JSON.parse(JSON.generate(divergences)) }
146
+ end
147
+
148
+ # The step-level half of shrinking, in this same process: the domain
149
+ # is already as small as the parent could make it.
150
+ #
151
+ # @param domain_path [String] path to the (already domain-shrunk) domain
152
+ # directory
153
+ # @param found [Hash] the finding to shrink, as returned by `#finding`
154
+ # @param binary [String, nil] path to a compiled Rust conformance binary, or
155
+ # `nil` for a Ruby-only check
156
+ # @param differ [Object, nil] duck-typed comparison helper answering
157
+ # `RustConformanceHelpers`' interface (adapter-defined), or `nil`
158
+ # @param budget [Integer] step-level shrink attempts to spend
159
+ # @return [Array<Hash>] the smallest step list found that still reproduces
160
+ # `found`'s divergence signature
161
+ def shrink(domain_path, found, binary, differ, budget)
162
+ original = Set.new(found["signature"])
163
+ mode = found["mode"].to_sym
164
+ Shrinker.call(found["steps"], budget: budget) do |candidate|
165
+ divergences = outcomes(domain_path, candidate, binary, differ)[mode] || []
166
+ Shrinker.reproduces?(original, divergences)
167
+ end.steps
168
+ end
169
+ end
170
+ end
171
+ end