hecks 2.7.0 → 2.9.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 (463) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +11 -0
  3. data/lib/hecks/adapters/driven/claude_code.rb +4 -104
  4. data/lib/hecks/adapters/driven/d1.rb +44 -237
  5. data/lib/hecks/adapters/driven/folder.rb +17 -76
  6. data/lib/hecks/adapters/driven/google_authentication.rb +15 -49
  7. data/lib/hecks/adapters/driven/governance_authorization.rb +23 -70
  8. data/lib/hecks/adapters/driven/heki/journal.rb +8 -41
  9. data/lib/hecks/adapters/driven/heki/saga_store.rb +12 -54
  10. data/lib/hecks/adapters/driven/heki/snapshot.rb +4 -12
  11. data/lib/hecks/adapters/driven/heki.rb +13 -42
  12. data/lib/hecks/adapters/driven/identity_registry.rb +3 -14
  13. data/lib/hecks/adapters/driven/in_memory_ordering.rb +9 -26
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +2 -14
  15. data/lib/hecks/adapters/driven/lambda/client.rb +10 -51
  16. data/lib/hecks/adapters/driven/lambda.rb +10 -84
  17. data/lib/hecks/adapters/driven/local_storage.rb +5 -40
  18. data/lib/hecks/adapters/driven/memory.rb +18 -99
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +5 -21
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +6 -28
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +3 -12
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +4 -30
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +34 -108
  24. data/lib/hecks/adapters/driven/postgres.rb +145 -134
  25. data/lib/hecks/adapters/driven/postgres_era.adapter +4 -15
  26. data/lib/hecks/adapters/driven/prism.rb +7 -30
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +21 -121
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +8 -40
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +26 -82
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +40 -84
  31. data/lib/hecks/adapters/driven/sqlite.rb +128 -65
  32. data/lib/hecks/adapters/driven/tenant_provisioner.rb +15 -37
  33. data/lib/hecks/adapters/driven.rb +5 -36
  34. data/lib/hecks/adapters/driving/github_webhook.rb +8 -76
  35. data/lib/hecks/behaviors/dsl.rb +7 -25
  36. data/lib/hecks/behaviors/expectations.rb +32 -142
  37. data/lib/hecks/behaviors/ir.rb +7 -20
  38. data/lib/hecks/behaviors/rspec.rb +6 -17
  39. data/lib/hecks/behaviors/runner.rb +10 -34
  40. data/lib/hecks/behaviors.rb +4 -8
  41. data/lib/hecks/bluebook/aggregate.rb +9 -75
  42. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +9 -38
  43. data/lib/hecks/bluebook/assembly/build.rb +4 -15
  44. data/lib/hecks/bluebook/assembly/contract.rb +15 -136
  45. data/lib/hecks/bluebook/assembly/contracts.rb +54 -174
  46. data/lib/hecks/bluebook/assembly/marks.rb +31 -223
  47. data/lib/hecks/bluebook/assembly/specializer.rb +6 -52
  48. data/lib/hecks/bluebook/assembly.rb +6 -45
  49. data/lib/hecks/bluebook/attribute.rb +12 -61
  50. data/lib/hecks/bluebook/behaviour/aggregate.rb +10 -39
  51. data/lib/hecks/bluebook/behaviour/attribute.rb +3 -14
  52. data/lib/hecks/bluebook/behaviour/chapter.rb +17 -42
  53. data/lib/hecks/bluebook/behaviour/command.rb +14 -66
  54. data/lib/hecks/bluebook/behaviour/domain_port.rb +4 -20
  55. data/lib/hecks/bluebook/behaviour/entity.rb +7 -27
  56. data/lib/hecks/bluebook/behaviour/hexagon.rb +23 -41
  57. data/lib/hecks/bluebook/behaviour/lifecycle.rb +15 -40
  58. data/lib/hecks/bluebook/behaviour/policy.rb +23 -52
  59. data/lib/hecks/bluebook/behaviour/process_manager.rb +30 -57
  60. data/lib/hecks/bluebook/behaviour/read_model.rb +38 -26
  61. data/lib/hecks/bluebook/behaviour/traits.rb +27 -64
  62. data/lib/hecks/bluebook/behaviour/value_object.rb +5 -24
  63. data/lib/hecks/bluebook/capabilities.rb +17 -49
  64. data/lib/hecks/bluebook/chapter.rb +10 -48
  65. data/lib/hecks/bluebook/command.rb +23 -81
  66. data/lib/hecks/bluebook/domain_port.rb +14 -63
  67. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +50 -229
  68. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +50 -377
  69. data/lib/hecks/bluebook/dsl/attribute_collector.rb +33 -204
  70. data/lib/hecks/bluebook/dsl/binding_proxy.rb +17 -65
  71. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +86 -708
  72. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +56 -170
  73. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
  74. data/lib/hecks/bluebook/dsl/command_builder.rb +74 -546
  75. data/lib/hecks/bluebook/dsl/const_shim.rb +8 -41
  76. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +12 -87
  77. data/lib/hecks/bluebook/dsl/entity_builder.rb +44 -330
  78. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +20 -269
  79. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +26 -189
  80. data/lib/hecks/bluebook/dsl/identity_declaration.rb +13 -88
  81. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +6 -20
  82. data/lib/hecks/bluebook/dsl/policy_builder.rb +16 -88
  83. data/lib/hecks/bluebook/dsl/port_builder.rb +2 -6
  84. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +22 -96
  85. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +31 -184
  86. data/lib/hecks/bluebook/dsl/query_builder.rb +11 -42
  87. data/lib/hecks/bluebook/dsl/read_model_builder.rb +75 -201
  88. data/lib/hecks/bluebook/dsl/rule_reference.rb +22 -163
  89. data/lib/hecks/bluebook/dsl/translation_builder.rb +28 -114
  90. data/lib/hecks/bluebook/dsl/value_object_builder.rb +10 -124
  91. data/lib/hecks/bluebook/dsl/word_gate.rb +16 -162
  92. data/lib/hecks/bluebook/dsl/world_builder.rb +19 -106
  93. data/lib/hecks/bluebook/entity.rb +13 -71
  94. data/lib/hecks/bluebook/expression/ast_json.rb +24 -190
  95. data/lib/hecks/bluebook/expression/ast_reader.rb +12 -54
  96. data/lib/hecks/bluebook/expression/canonical_form.rb +11 -88
  97. data/lib/hecks/bluebook/expression/evaluator.rb +47 -295
  98. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +30 -147
  99. data/lib/hecks/bluebook/expression/resolver.rb +37 -660
  100. data/lib/hecks/bluebook/hexagon.rb +5 -28
  101. data/lib/hecks/bluebook/lifecycle.rb +5 -16
  102. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -7
  103. data/lib/hecks/bluebook/meta_validator/judge.rb +111 -361
  104. data/lib/hecks/bluebook/meta_validator/plan.rb +33 -162
  105. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -7
  106. data/lib/hecks/bluebook/meta_validator/readings.rb +56 -342
  107. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -176
  108. data/lib/hecks/bluebook/meta_validator/shapes.rb +54 -268
  109. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +47 -308
  110. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +7 -43
  111. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -18
  112. data/lib/hecks/bluebook/meta_validator.rb +79 -349
  113. data/lib/hecks/bluebook/model_check/client_profile.rb +10 -91
  114. data/lib/hecks/bluebook/model_check.rb +102 -600
  115. data/lib/hecks/bluebook/pattern_subset.rb +11 -69
  116. data/lib/hecks/bluebook/process_manager.rb +13 -83
  117. data/lib/hecks/bluebook/project_loader.rb +3 -39
  118. data/lib/hecks/bluebook/project_register.rb +12 -60
  119. data/lib/hecks/bluebook/query.rb +9 -48
  120. data/lib/hecks/bluebook/read_model.rb +57 -81
  121. data/lib/hecks/bluebook/reference.rb +7 -41
  122. data/lib/hecks/bluebook/smoke_test.rb +8 -98
  123. data/lib/hecks/bluebook/synthesizer.rb +13 -62
  124. data/lib/hecks/bluebook/translation.rb +26 -62
  125. data/lib/hecks/bluebook/value_object.rb +11 -36
  126. data/lib/hecks/bluebook.rb +5 -22
  127. data/lib/hecks/cache_dir.rb +84 -0
  128. data/lib/hecks/cli/document.rb +86 -0
  129. data/lib/hecks/cli/ir.rb +45 -0
  130. data/lib/hecks/cli/mcp.rb +31 -0
  131. data/lib/hecks/cli/mcp_door.rb +366 -0
  132. data/lib/hecks/cli/model_check.rb +203 -0
  133. data/lib/hecks/cli/project_cli.rb +90 -0
  134. data/lib/hecks/cli/project_diagrams.rb +36 -0
  135. data/lib/hecks/cli/run.rb +173 -0
  136. data/lib/hecks/cli/smoke_test.rb +45 -0
  137. data/lib/hecks/cli/stores.rb +53 -0
  138. data/lib/hecks/cli.rb +160 -0
  139. data/lib/hecks/construct.rb +8 -34
  140. data/lib/hecks/deploy/bluebook/deploy.bluebook +33 -142
  141. data/lib/hecks/deploy/bluebook/deploy.hecksagon +2 -9
  142. data/lib/hecks/embryonaut_bluebook/lock.rb +3 -35
  143. data/lib/hecks/embryonaut_bluebook/shape.rb +1 -17
  144. data/lib/hecks/embryonaut_bluebook/vendor.rb +8 -53
  145. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +4 -19
  146. data/lib/hecks/embryonaut_bluebook.rb +4 -71
  147. data/lib/hecks/facade/cli_door.rb +24 -106
  148. data/lib/hecks/facade/cli_runner.rb +28 -172
  149. data/lib/hecks/facade/command_request.rb +13 -33
  150. data/lib/hecks/facade/handle.rb +34 -121
  151. data/lib/hecks/facade/json_door.rb +31 -146
  152. data/lib/hecks/facade/surface/aggregate_door.rb +39 -104
  153. data/lib/hecks/facade/surface/chapter.rb +10 -54
  154. data/lib/hecks/facade/surface.rb +8 -23
  155. data/lib/hecks/facade.rb +10 -25
  156. data/lib/hecks/forms/app.rb +15 -70
  157. data/lib/hecks/forms/command_form_renderer.rb +10 -29
  158. data/lib/hecks/forms/field_renderer.rb +17 -156
  159. data/lib/hecks/forms/field_shape.rb +30 -222
  160. data/lib/hecks/forms/html.rb +12 -47
  161. data/lib/hecks/forms/index_renderer.rb +1 -4
  162. data/lib/hecks/forms/page.rb +3 -9
  163. data/lib/hecks/forms/params.rb +14 -60
  164. data/lib/hecks/forms/port_argument.rb +2 -19
  165. data/lib/hecks/forms/query_form_renderer.rb +4 -79
  166. data/lib/hecks/forms/record_renderer.rb +8 -75
  167. data/lib/hecks/forms/record_table.rb +7 -50
  168. data/lib/hecks/forms/reference_options.rb +3 -30
  169. data/lib/hecks/forms/value_object_shape.rb +8 -35
  170. data/lib/hecks/forms.rb +5 -33
  171. data/lib/hecks/fqn.rb +5 -12
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +16 -65
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +47 -134
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +5 -14
  175. data/lib/hecks/framework/bluebook/governance.bluebook +13 -34
  176. data/lib/hecks/framework/bluebook/identity.bluebook +11 -26
  177. data/lib/hecks/framework/bluebook/privacy.bluebook +8 -33
  178. data/lib/hecks/framework.rb +11 -80
  179. data/lib/hecks/freezer.rb +5 -39
  180. data/lib/hecks/grammar/expression.bluebook +15 -40
  181. data/lib/hecks/grammar/translation.bluebook +3 -6
  182. data/lib/hecks/grammar.rb +16 -41
  183. data/lib/hecks/ir.rb +14 -83
  184. data/lib/hecks/language/adapter.bluebook +2 -7
  185. data/lib/hecks/language/bluebook/aggregate.bluebook +56 -221
  186. data/lib/hecks/language/bluebook/attaches/paging.bluebook +6 -15
  187. data/lib/hecks/language/bluebook/bluebook.bluebook +15 -83
  188. data/lib/hecks/language/bluebook/bluebook.hecksagon +3 -15
  189. data/lib/hecks/language/bluebook/command.bluebook +27 -144
  190. data/lib/hecks/language/bluebook/entity.bluebook +24 -97
  191. data/lib/hecks/language/bluebook/policy.bluebook +16 -67
  192. data/lib/hecks/language/bluebook/process_manager.bluebook +28 -151
  193. data/lib/hecks/language/bluebook/projection.bluebook +126 -60
  194. data/lib/hecks/language/bluebook/query.bluebook +12 -43
  195. data/lib/hecks/language/bluebook/shape.bluebook +23 -73
  196. data/lib/hecks/language/bluebook/syntax.bluebook +51 -262
  197. data/lib/hecks/language/bluebook/vocabulary.bluebook +98 -197
  198. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +3 -10
  199. data/lib/hecks/language/hecksagon/domain_port.bluebook +4 -19
  200. data/lib/hecks/language/hecksagon/hecksagon.bluebook +6 -25
  201. data/lib/hecks/language/hecksagon/port_operation.bluebook +3 -14
  202. data/lib/hecks/language/oidc.json +35 -0
  203. data/lib/hecks/language/port.bluebook +4 -17
  204. data/lib/hecks/language/translation/translation.bluebook +3 -9
  205. data/lib/hecks/language/translation/translation_aggregate.bluebook +5 -24
  206. data/lib/hecks/language/world/wiring.bluebook +1 -3
  207. data/lib/hecks/language/world/world.bluebook +5 -7
  208. data/lib/hecks/literal.rb +8 -50
  209. data/lib/hecks/mcp_door_scope.rb +95 -0
  210. data/lib/hecks/mcp_stdio_guard.rb +4 -41
  211. data/lib/hecks/naming.rb +29 -137
  212. data/lib/hecks/ports/access_control.rb +7 -23
  213. data/lib/hecks/ports/agent/answers.rb +5 -23
  214. data/lib/hecks/ports/agent.rb +47 -172
  215. data/lib/hecks/ports/authentication.rb +12 -39
  216. data/lib/hecks/ports/authorization.rb +16 -43
  217. data/lib/hecks/ports/clock.rb +5 -45
  218. data/lib/hecks/ports/extraction.rb +7 -12
  219. data/lib/hecks/ports/identity_assignment.rb +8 -29
  220. data/lib/hecks/ports/identity_generation.rb +6 -29
  221. data/lib/hecks/ports/identity_resolution.rb +2 -10
  222. data/lib/hecks/ports/key_vault.rb +9 -45
  223. data/lib/hecks/ports/persistence/append_only.rb +62 -65
  224. data/lib/hecks/ports/persistence/binding_policy.rb +4 -14
  225. data/lib/hecks/ports/persistence/codec_boundary.rb +20 -80
  226. data/lib/hecks/ports/persistence/null_saga_store.rb +4 -20
  227. data/lib/hecks/ports/persistence/plugin.rb +13 -42
  228. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +38 -141
  229. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +15 -115
  230. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +16 -84
  231. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +7 -26
  232. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -22
  233. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +55 -181
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/events.rb +77 -0
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +12 -122
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +28 -128
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +73 -474
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +31 -124
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +45 -219
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +13 -108
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +18 -75
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +9 -49
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +6 -123
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +23 -55
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +14 -72
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +7 -16
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +14 -115
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +13 -45
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +127 -554
  250. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +13 -63
  251. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +2 -17
  252. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +3 -14
  253. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +13 -58
  254. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -17
  255. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +9 -26
  256. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +9 -25
  257. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +22 -122
  258. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +34 -123
  259. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +5 -12
  260. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +3 -9
  261. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -14
  262. data/lib/hecks/ports/persistence/plugins/era.rb +4 -23
  263. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +7 -23
  264. data/lib/hecks/ports/persistence/postgres_dump.rb +4 -35
  265. data/lib/hecks/ports/persistence/remote_runtime.rb +2 -24
  266. data/lib/hecks/ports/persistence/repository_factory.rb +8 -27
  267. data/lib/hecks/ports/persistence/state_codec.rb +23 -103
  268. data/lib/hecks/ports/persistence.rb +7 -14
  269. data/lib/hecks/ports/projection.rb +42 -84
  270. data/lib/hecks/ports/query/in_memory.rb +10 -58
  271. data/lib/hecks/ports/query/ordering.rb +11 -32
  272. data/lib/hecks/ports/query.rb +14 -37
  273. data/lib/hecks/ports.rb +2 -8
  274. data/lib/hecks/projections/bootstrap_table.rb +11 -43
  275. data/lib/hecks/projections/deploy/fargate/assembly.rb +2 -15
  276. data/lib/hecks/projections/deploy/fargate/cdn.rb +1 -48
  277. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +2 -11
  278. data/lib/hecks/projections/deploy/fargate/check.rb +5 -14
  279. data/lib/hecks/projections/deploy/fargate/containers.rb +3 -78
  280. data/lib/hecks/projections/deploy/fargate/extras.rb +3 -44
  281. data/lib/hecks/projections/deploy/fargate/monitoring.rb +0 -44
  282. data/lib/hecks/projections/deploy/fargate/settings.rb +2 -54
  283. data/lib/hecks/projections/deploy/fargate/yaml.rb +13 -58
  284. data/lib/hecks/projections/deploy/fargate.rb +35 -202
  285. data/lib/hecks/projections/deploy/lambda.rb +151 -679
  286. data/lib/hecks/projections/deploy/preview/access_yaml.rb +1 -33
  287. data/lib/hecks/projections/deploy/preview/containers.rb +6 -111
  288. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +4 -56
  289. data/lib/hecks/projections/deploy/preview/script.rb +2 -14
  290. data/lib/hecks/projections/deploy/preview/settings.rb +3 -33
  291. data/lib/hecks/projections/deploy/preview/task_yaml.rb +2 -38
  292. data/lib/hecks/projections/deploy/preview/template.rb +2 -58
  293. data/lib/hecks/projections/deploy/preview.rb +2 -67
  294. data/lib/hecks/projections/deploy/scripts/settings.rb +3 -25
  295. data/lib/hecks/projections/deploy/scripts.rb +4 -57
  296. data/lib/hecks/projections/deploy/shared.rb +47 -146
  297. data/lib/hecks/projections/deploy/smoke.rb +4 -61
  298. data/lib/hecks/projections/deploy/template_diff/comparer.rb +3 -25
  299. data/lib/hecks/projections/deploy/template_diff/loader.rb +2 -11
  300. data/lib/hecks/projections/deploy/template_diff.rb +5 -57
  301. data/lib/hecks/projections/diagrams.rb +20 -582
  302. data/lib/hecks/projections/glossary/html.rb +14 -130
  303. data/lib/hecks/projections/glossary/markdown.rb +8 -69
  304. data/lib/hecks/projections/glossary/mermaid.rb +9 -66
  305. data/lib/hecks/projections/glossary/sensitivity.rb +5 -34
  306. data/lib/hecks/projections/glossary/sentences.rb +14 -159
  307. data/lib/hecks/projections/glossary.rb +30 -222
  308. data/lib/hecks/projections/ir.rb +3 -10
  309. data/lib/hecks/projections/model/deviations.rb +15 -42
  310. data/lib/hecks/projections/model.rb +12 -51
  311. data/lib/hecks/projections/oidc.rb +12 -56
  312. data/lib/hecks/projections/parser_table.rb +9 -28
  313. data/lib/hecks/projections/reference.rb +9 -16
  314. data/lib/hecks/projections/rust_vocabulary.rb +12 -250
  315. data/lib/hecks/projections/shape.rb +3 -29
  316. data/lib/hecks/projections/statements.rb +7 -86
  317. data/lib/hecks/projections/vocabulary.rb +6 -22
  318. data/lib/hecks/projections.rb +2 -16
  319. data/lib/hecks/projector/cli_projector.rb +60 -375
  320. data/lib/hecks/projector/docs_projector.rb +10 -201
  321. data/lib/hecks/projector/exporter.rb +46 -138
  322. data/lib/hecks/projector/ir_projector.rb +2 -9
  323. data/lib/hecks/projector/narrate_projector.rb +15 -189
  324. data/lib/hecks/projector/target.rb +13 -76
  325. data/lib/hecks/projector.rb +30 -121
  326. data/lib/hecks/query_specification/common/comparators.rb +5 -26
  327. data/lib/hecks/query_specification/common/comparison.rb +35 -143
  328. data/lib/hecks/query_specification/common/dsl.rb +8 -24
  329. data/lib/hecks/query_specification/common/null_policy.rb +20 -70
  330. data/lib/hecks/query_specification/common/options.rb +2 -6
  331. data/lib/hecks/query_specification/common/where_clause.rb +2 -10
  332. data/lib/hecks/query_specification/field_path.rb +37 -77
  333. data/lib/hecks/query_specification/hop_path.rb +12 -76
  334. data/lib/hecks/release/gem_pin.rb +2 -26
  335. data/lib/hecks/release/runner/ci_publisher.rb +93 -0
  336. data/lib/hecks/release/runner/commands.rb +66 -0
  337. data/lib/hecks/release/runner/console.rb +52 -0
  338. data/lib/hecks/release/runner/gem_publisher.rb +43 -0
  339. data/lib/hecks/release/runner/git.rb +48 -0
  340. data/lib/hecks/release/runner/npm_publisher.rb +75 -0
  341. data/lib/hecks/release/runner/preflight.rb +104 -0
  342. data/lib/hecks/release/runner/published.rb +55 -0
  343. data/lib/hecks/release/runner/tagger.rb +80 -0
  344. data/lib/hecks/release/runner.rb +215 -0
  345. data/lib/hecks/rendering.rb +4 -26
  346. data/lib/hecks/router/namespace_installer.rb +12 -36
  347. data/lib/hecks/router.rb +4 -8
  348. data/lib/hecks/runtime/aggregate_lock.rb +8 -33
  349. data/lib/hecks/runtime/boot_gates.rb +3 -18
  350. data/lib/hecks/runtime/caller.rb +19 -59
  351. data/lib/hecks/runtime/capability_graph.rb +5 -21
  352. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +16 -78
  353. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +23 -160
  354. data/lib/hecks/runtime/command_interpreter.rb +125 -307
  355. data/lib/hecks/runtime/command_rules/admissibility.rb +95 -366
  356. data/lib/hecks/runtime/command_rules/arithmetic.rb +66 -196
  357. data/lib/hecks/runtime/command_rules/authorization.rb +13 -45
  358. data/lib/hecks/runtime/command_rules/emission.rb +8 -21
  359. data/lib/hecks/runtime/command_rules/references.rb +23 -202
  360. data/lib/hecks/runtime/dependency_planning.rb +30 -102
  361. data/lib/hecks/runtime/dispatcher.rb +82 -241
  362. data/lib/hecks/runtime/entity_element.rb +40 -462
  363. data/lib/hecks/runtime/entity_interpreter.rb +65 -220
  364. data/lib/hecks/runtime/errors.rb +32 -115
  365. data/lib/hecks/runtime/event.rb +3 -25
  366. data/lib/hecks/runtime/identity.rb +18 -154
  367. data/lib/hecks/runtime/instance.rb +18 -150
  368. data/lib/hecks/runtime/interpreting.rb +11 -70
  369. data/lib/hecks/runtime/invocation.rb +28 -164
  370. data/lib/hecks/runtime/loader.rb +48 -186
  371. data/lib/hecks/runtime/outbox.rb +38 -261
  372. data/lib/hecks/runtime/policy_interpreter.rb +38 -200
  373. data/lib/hecks/runtime/port_operation_interpreter.rb +26 -87
  374. data/lib/hecks/runtime/query_interpreter.rb +45 -187
  375. data/lib/hecks/runtime/reaction_invocation.rb +36 -138
  376. data/lib/hecks/runtime/read_model_interpreter.rb +139 -208
  377. data/lib/hecks/runtime/rebuild_sweep.rb +8 -54
  378. data/lib/hecks/runtime/reference_hop.rb +8 -81
  379. data/lib/hecks/runtime/refusal_wording.rb +8 -89
  380. data/lib/hecks/runtime/registry/saga_persistence.rb +10 -102
  381. data/lib/hecks/runtime/registry/verification.rb +52 -225
  382. data/lib/hecks/runtime/registry/world_defaults.rb +4 -53
  383. data/lib/hecks/runtime/registry.rb +103 -244
  384. data/lib/hecks/runtime/remote_dispatcher.rb +37 -152
  385. data/lib/hecks/runtime/routing.rb +4 -15
  386. data/lib/hecks/runtime/saga_interpreter/correlation.rb +14 -61
  387. data/lib/hecks/runtime/saga_interpreter.rb +79 -263
  388. data/lib/hecks/runtime/saga_pending_dispatch.rb +2 -43
  389. data/lib/hecks/runtime/tenant_check.rb +6 -68
  390. data/lib/hecks/runtime/tenant_scope.rb +3 -28
  391. data/lib/hecks/runtime/value/admission.rb +16 -67
  392. data/lib/hecks/runtime/value/coercion.rb +155 -641
  393. data/lib/hecks/runtime/value/entity_list_coercion.rb +26 -219
  394. data/lib/hecks/runtime/value.rb +12 -84
  395. data/lib/hecks/runtime.rb +17 -65
  396. data/lib/hecks/storehouse.rb +146 -567
  397. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +8 -32
  398. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +6 -23
  399. data/lib/hecks/vendoring/git_environment.rb +2 -8
  400. data/lib/hecks/vendoring/git_source.rb +4 -10
  401. data/lib/hecks/vendoring.rb +7 -46
  402. data/lib/hecks/version.rb +3 -14
  403. data/lib/hecks/vocabulary.rb +5 -0
  404. data/lib/hecks.rb +32 -82
  405. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -46
  406. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +11 -80
  407. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +10 -99
  408. metadata +30 -59
  409. data/lib/hecks/bench/cli.rb +0 -74
  410. data/lib/hecks/bench/environment.rb +0 -100
  411. data/lib/hecks/bench/postgres_probe.rb +0 -41
  412. data/lib/hecks/bench/report.rb +0 -116
  413. data/lib/hecks/bench/ruby_runner.rb +0 -151
  414. data/lib/hecks/bench/run.rb +0 -55
  415. data/lib/hecks/bench/rust_runner.rb +0 -165
  416. data/lib/hecks/bench/stats.rb +0 -75
  417. data/lib/hecks/bench/suite.rb +0 -172
  418. data/lib/hecks/bench/workload.rb +0 -137
  419. data/lib/hecks/bench.rb +0 -28
  420. data/lib/hecks/codemod.rb +0 -443
  421. data/lib/hecks/corpus.rb +0 -479
  422. data/lib/hecks/doc/reference.rb +0 -585
  423. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
  424. data/lib/hecks/fuzzing/combination_miner.rb +0 -178
  425. data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
  426. data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
  427. data/lib/hecks/fuzzing/differential.rb +0 -192
  428. data/lib/hecks/fuzzing/domain_generator.rb +0 -871
  429. data/lib/hecks/fuzzing/era_boundary.rb +0 -154
  430. data/lib/hecks/fuzzing/form_census.rb +0 -285
  431. data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
  432. data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
  433. data/lib/hecks/fuzzing/isolated_boot.rb +0 -658
  434. data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
  435. data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
  436. data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
  437. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
  438. data/lib/hecks/fuzzing/properties/guards.rb +0 -398
  439. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
  440. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
  441. data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
  442. data/lib/hecks/fuzzing/properties/querying.rb +0 -260
  443. data/lib/hecks/fuzzing/properties.rb +0 -267
  444. data/lib/hecks/fuzzing/qa_settings.rb +0 -164
  445. data/lib/hecks/fuzzing/replay.rb +0 -930
  446. data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
  447. data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
  448. data/lib/hecks/fuzzing/self_consistency.rb +0 -825
  449. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
  450. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
  451. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
  452. data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
  453. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
  454. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
  455. data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
  456. data/lib/hecks/fuzzing/shrinker.rb +0 -309
  457. data/lib/hecks/fuzzing/structural_skips.rb +0 -53
  458. data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
  459. data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
  460. data/lib/hecks/fuzzing/value_generator.rb +0 -340
  461. data/lib/hecks/fuzzing.rb +0 -23
  462. data/lib/hecks/grammar/evolve.rb +0 -524
  463. data/lib/hecks/query_ir.rb +0 -482
@@ -1,688 +0,0 @@
1
- module Hecks
2
- module Fuzzing
3
- module Properties
4
- # **A dry run leaves no trace** — `Dispatcher#dry_run?`'s whole contract
5
- # ("the command evaluated hypothetically, nothing saved or emitted,
6
- # no reaction"), held to the store rather than trusted: `Replay`
7
- # snapshots every instance and the event count on either side of
8
- # each `{"dry_run": …}` step, in a separate `dry_run_traces` array
9
- # (same order as `history[:dry_runs]`, never merged into it — that
10
- # array stays the exact `{verb:, ok:, error?:}` shape the compiled
11
- # Rust binary's own `dry_run` answers, so `spec/rust_conformance_
12
- # spec.rb`'s direct comparison against it never sees a key Rust
13
- # doesn't have), and this is the comparison. Refused or accepted
14
- # makes no difference — a hypothetical that was refused had even
15
- # less business writing anything. Entries without the snapshots (a
16
- # hand-built history, an older corpus) are skipped, not failed: no
17
- # claim, no finding.
18
- module DryRuns
19
- # Checks that every dry-run step left neither the event count nor any
20
- # instance's state changed.
21
- #
22
- # @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
23
- # @return [true, String] true if every dry-run trace shows no change; otherwise
24
- # a semicolon-joined message naming each offending dry run
25
- def dry_runs_leave_no_trace(history)
26
- offenders = Array(history[:dry_run_traces]).filter_map do |entry|
27
- before = entry[:before]
28
- after = entry[:after]
29
- next unless before && after
30
-
31
- traces = []
32
- traces << "events #{before[:events]} -> #{after[:events]}" unless before[:events] == after[:events]
33
- traces << "instances changed" unless before[:instances] == after[:instances]
34
- next if traces.empty?
35
-
36
- "dry run of #{entry[:verb]} (ok: #{entry[:ok]}) left a trace: #{traces.join(', ')}"
37
- end
38
-
39
- offenders.empty? || offenders.join("; ")
40
- end
41
- end
42
-
43
- # Dispatch-binding and mutation-recomputation properties: a saga/
44
- # policy dispatch is bound to the value its own with_spec names, and a
45
- # command's append/remove/multiply/clamp mutations land on the same
46
- # after-state an independent recomputation of the same rule produces.
47
-
48
- # Holds dispatch_binding_fidelity and mutations_match_recompute, plus
49
- # the independently-written re-derivations (#resolve_dispatch_binding,
50
- # #resolve_trigger_binding, #recompute_append/#remove/#multiply/#clamp)
51
- # each checks the real dispatch/mutation against.
52
- module DispatchAndMutations
53
- # Closes Handler#dispatches and — the same shape, one construct
54
- # over — Policy#with_spec (Dispatch#command_name/Dispatch#with_spec
55
- # are not claimable feature strings at all — see FEATURE_COVERAGE's
56
- # own comment on this entry). history[:saga_dispatches]/[:policy_dispatches]
57
- # (Registry#saga_dispatch_log/#policy_dispatch_log — additive,
58
- # Ruby-only, never touching saga_log/reaction_log, the byte-for-
59
- # byte shape spec/rust_conformance_spec.rb holds Rust to) each
60
- # carry the raw inputs a dispatch's own args were resolved from,
61
- # captured live at the moment the resolution actually ran — a
62
- # saga's own memory keeps changing across a run, so re-deriving
63
- # from history[:saga_instances]'s final memory (the only other
64
- # place it would be visible) would grade the wrong moment,
65
- # lifecycle_guard_and_given_violations_are_refused's own false
66
- # positive one item earlier, in a different shape.
67
- #
68
- # #resolve_dispatch_binding/#resolve_trigger_binding are separate,
69
- # independently-written re-derivations of SagaInterpreter#
70
- # dispatch_args/PolicyInterpreter#trigger_args's own resolution —
71
- # never calling either method again, which would only ever agree
72
- # with itself. Exactly the class of bug this closes: "a wrong
73
- # argument binding on a fan-out dispatch that produces a perfectly
74
- # normal-looking log entry (`delivered: true`) and would only ever
75
- # surface as a downstream assertion failure, if it surfaces at
76
- # all" — a defect class one level over from what
77
- # `fanout_dispatches_once_per_matching_row` already catches.
78
- #
79
- # Real targets: Settlement (mixed literal/correlation-head/event-
80
- # payload/memory-fallback bindings across three legs, plus a
81
- # compensation leg that deliberately omits `reference:`, a field
82
- # the forward Credit leg carries), ExternalSettlement, Onboarding
83
- # (no compensation leg, by design — nothing to check there beyond
84
- # the forward leg's own event-payload binding).
85
- #
86
- # @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
87
- # @return [true, String] true if every saga and policy dispatch's own bound args
88
- # match an independent re-derivation of their `with_spec`; otherwise a
89
- # semicolon-joined message naming each offending dispatch
90
- def dispatch_binding_fidelity(history)
91
- saga_offenders = history.fetch(:saga_dispatches, []).filter_map do |entry|
92
- expected = resolve_dispatch_binding(entry)
93
- next if expected == entry[:args]
94
-
95
- "#{entry[:process_manager]}##{entry[:instance]} dispatching #{entry[:dispatch]} on #{entry[:on]} — " \
96
- "bound #{entry[:args].inspect}, but independently re-deriving with_spec's own resolution gives " \
97
- "#{expected.inspect}"
98
- end
99
-
100
- policy_offenders = history.fetch(:policy_dispatches, []).filter_map do |entry|
101
- expected = resolve_trigger_binding(entry)
102
- next if expected == entry[:args]
103
-
104
- "#{entry[:policy]} on #{entry[:on]} — bound #{entry[:args].inspect}, but independently re-deriving " \
105
- "with_spec's own resolution gives #{expected.inspect}"
106
- end
107
-
108
- offenders = saga_offenders + policy_offenders
109
- offenders.empty? || offenders.join("; ")
110
- end
111
-
112
- # SagaInterpreter#dispatch_args's own 4-branch resolution,
113
- # reproduced independently: a literal, the correlation key itself,
114
- # the current triggering event's own payload, or — the fallback —
115
- # the saga's own carried memory (seeded from the starting event's
116
- # payload, at begin_saga).
117
- #
118
- # @param entry [Hash] one `history[:saga_dispatches]` entry: at least
119
- # `:with_spec`, `:correlation_head`, `:instance`, `:event_payload`, and `:memory`
120
- # @return [Hash{Symbol => Object}] the resolved, materialized args `with_spec`
121
- # should produce, keyed by symbol
122
- def resolve_dispatch_binding(entry)
123
- entry[:with_spec].to_h do |key, value|
124
- resolved = if !value.is_a?(Symbol) then value
125
- elsif value == entry[:correlation_head] then entry[:instance]
126
- elsif entry[:event_payload].key?(value) then entry[:event_payload][value]
127
- else entry[:memory][value]
128
- end
129
- [key.to_sym, Runtime::Value.materialize(resolved)]
130
- end
131
- end
132
-
133
- # PolicyInterpreter#trigger_args's own 2-branch resolution — a
134
- # policy holds no correlation and no memory, so `payload` (the
135
- # triggering event's own payload, already merged with a fan-out
136
- # row's id when there is one) is the whole source.
137
- #
138
- # @param entry [Hash] one `history[:policy_dispatches]` entry: at least
139
- # `:with_spec` and `:payload`
140
- # @return [Hash{Symbol => Object}] the resolved, materialized args `with_spec`
141
- # should produce, keyed by symbol
142
- def resolve_trigger_binding(entry)
143
- entry[:with_spec].to_h do |key, value|
144
- resolved = value.is_a?(Symbol) ? entry[:payload][value] : value
145
- [key.to_sym, Runtime::Value.materialize(resolved)]
146
- end
147
- end
148
-
149
- # Closes Command#mutations — the last of the five, and the
150
- # largest: no real corpus entity anywhere uses `append`/`remove`/
151
- # `multiply`/`clamp` (every real aggregate-owned mutation is
152
- # aggregate-scoped — Account.Credit's :ledger, LogVisit's
153
- # :visits); the only entity-owned use of these four ops in the
154
- # whole repository is spec/fixtures/entity_list_mutations, now a
155
- # real, bootable, Memory-default domain (no .hecksagon needed at
156
- # all — a domain with none boots every aggregate against Memory
157
- # by construction, confirmed live) rather than the raw-Kernel.
158
- # load-only fixture it was.
159
- #
160
- # `history[:mutation_traces]` (Replay's own bounded, additive
161
- # extension — see #build_mutation_trace's own comment) carries a
162
- # per-step before/after snapshot of the entity element an
163
- # entity-dispatched command's own mutations acted on, materialized
164
- # to plain data, plus the step's own raw args — the delta
165
- # `aggregation_matches_recompute` never had to ask for, because
166
- # count/median are pure functions of final state and a mutation
167
- # is not (the same "captured live, not re-derived from final
168
- # state" lesson item 8's own saga_dispatch_log already learned).
169
- #
170
- # #recompute_append/#recompute_remove/#recompute_multiply/
171
- # #recompute_clamp/#recompute_set are separate, independently-
172
- # written reproductions of EntityElement#appended_to_element/
173
- # #removed_from_element/#apply_to_element's own `:set` branch and
174
- # CommandRules::Arithmetic#multiply/#clamp — never calling either
175
- # again, which would only ever agree with itself. Every one of
176
- # these five reproduces the entity-scoped applier specifically
177
- # (`EntityElement#apply_to_element`, entity_element.rb), never
178
- # the aggregate-level `MutationApplier#apply` (mutation_applier.rb)
179
- # — `build_mutation_trace` (replay.rb) only ever captures an
180
- # entity-owned command's own mutation in the first place (that
181
- # method's own `command_name&.include?(".")` guard), so there is
182
- # no aggregate-level trace for this property to ever compare
183
- # against `owner_for_verb`'s own comment draws the identical
184
- # aggregate/owner distinction for `:append`. `:increment`/
185
- # `:decrement` remain out of scope (CommandRules::Arithmetic#
186
- # arithmetic's own VO-vs-scalar branching and INT64_RANGE overflow
187
- # checks are a materially larger reproduction than the other five
188
- # ops combined — scoped out of this pass, not forgotten; see
189
- # docs/decisions/0056); a command mixing them with a recomputable
190
- # op still gets the recomputable one checked.
191
- #
192
- # `:set` closes a real, separate gap from the other four — this is
193
- # not "the fifth op of a symmetrical set." `self_consistency.rb`'s
194
- # own rehydration/idempotency checks (lib/hecks/fuzzing/
195
- # self_consistency.rb) can never catch a bug in `EntityElement#
196
- # apply_to_element`'s `:set` branch (entity_element.rb) no matter
197
- # how much they run: both the "live" state they snapshot and the
198
- # "rehydrated" state they fold from `Ports::Persistence::
199
- # AppendOnly`'s own journal trace back to the same single
200
- # `step_apply_mutations` call (entity_interpreter.rb) — the
201
- # journal holds the full POST-mutation state, not a delta (that
202
- # file's own header), so a wrong `:set` result is already baked
203
- # into both sides of that comparison before either one runs. A
204
- # Ruby/Rust differential check is subject to the identical
205
- # structural blind spot whenever Rust's own generated `:set`
206
- # handling was derived from — and so shares — the same
207
- # misunderstanding Ruby's implementation has. `#recompute_set`
208
- # is a genuinely third computation, independent of both: it
209
- # re-derives the expected value from the mutation's own declared
210
- # `source` (an argument or a literal — matching `apply_to_
211
- # element`'s own `resolve_source`, which unlike the aggregate-
212
- # level `apply` never special-cases a `StateRef` source at all)
213
- # and the step's own captured `args`, then compares against what
214
- # the real dispatch actually stored — closing exactly the class
215
- # of bug the self-consistency and (whenever Rust's codegen shares
216
- # a ruby-derived misunderstanding) differential checks cannot see.
217
- # `spec/fuzzing/mutation_set_self_correctness_spec.rb` proves this
218
- # concretely: a defect planted directly in `EntityElement#apply_
219
- # to_element`'s `:set` branch leaves `SelfConsistency.check`
220
- # completely clean while `mutations_match_recompute` names it.
221
- #
222
- # `:unrecomputable` (never compared, never a finding) covers the
223
- # generator's own deliberate arg-malforming (`StepBuilder#malform`)
224
- # landing a non-Numeric amount/non-2-element bounds where
225
- # multiply/clamp need one — the same shape `guard_check`'s own
226
- # AbsentArgument false positive taught: a step whose raw material
227
- # doesn't fit the op's own contract is inconclusive, not a claimed
228
- # mismatch. `#recompute_set` returns it for the same reason,
229
- # whenever `Value.for_attribute`'s own coercion door raises on the
230
- # resolved raw source (malformed fuzzer input, never a real
231
- # corpus shape).
232
- RECOMPUTABLE_MUTATION_OPS = %i[append remove multiply clamp set].freeze
233
-
234
- # Checks that every recomputable mutation an entity-owned command
235
- # applied landed on the same after-state an independent
236
- # recomputation of the same rule produces.
237
- #
238
- # @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
239
- # @return [true, String] true if every recomputable mutation's after-state
240
- # matches an independent recomputation; otherwise a semicolon-joined message
241
- # naming each offending mutation
242
- def mutations_match_recompute(history)
243
- bluebooks = history.fetch(:bluebooks)
244
-
245
- offenders = history.fetch(:mutation_traces, []).flat_map do |entry|
246
- next [] unless entry[:after]
247
-
248
- command = command_for_verb(bluebooks, entry[:verb])
249
- next [] unless command
250
-
251
- aggregate = aggregate_for_verb(bluebooks, entry[:verb])
252
- next [] unless aggregate
253
-
254
- owner = owner_for_verb(bluebooks, entry[:verb]) || aggregate
255
-
256
- command.mutations.select { |m| RECOMPUTABLE_MUTATION_OPS.include?(m.op) }.filter_map do |mutation|
257
- expected = recompute_mutation(mutation, entry[:before][mutation.target], entry[:args], entry[:before],
258
- aggregate, command, owner)
259
- next if expected == :unrecomputable
260
-
261
- actual = entry[:after][mutation.target]
262
- next if symbolize_deep(expected) == symbolize_deep(actual)
263
-
264
- "#{entry[:verb]} — #{mutation.op} on #{mutation.target} — recomputing independently gives " \
265
- "#{expected.inspect}, but the real dispatch left #{actual.inspect}"
266
- end
267
- end
268
-
269
- offenders.empty? || offenders.join("; ")
270
- end
271
-
272
- # `command_for_verb`'s own root-aggregate half (Guards) —
273
- # re-derived independently rather than read off `entry[:domain]`/
274
- # `entry[:aggregate]` (present on a real `build_mutation_trace`
275
- # entry, but not on every hand-built fixture this property is
276
- # tested against) so this works from `entry[:verb]` alone, the
277
- # one field every entry always carries. BUG#5's fix needs the
278
- # root aggregate specifically — `Value.for_attribute` resolves a
279
- # value-object type against the root's own namespace only, the
280
- # same reason `EntityElement#locate_chain` threads `root_aggregate`
281
- # through every hop separately from each hop's own `owner`.
282
- #
283
- # @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
284
- # keyed by domain name
285
- # @param verb [String] the dispatched verb to resolve
286
- # @return [Bluebook::Aggregate, nil] the root aggregate `verb`'s domain and
287
- # aggregate name, or nil if `verb` names no domain
288
- def aggregate_for_verb(bluebooks, verb)
289
- domain_name, aggregate_name, = Naming.split_verb(verb)
290
- return nil unless domain_name
291
-
292
- bluebooks[domain_name]&.aggregate(aggregate_name)
293
- end
294
-
295
- # `mutation.target`'s own declaring construct — the root
296
- # aggregate for an aggregate-owned command (`AddSlot`'s own
297
- # `:slots`), or the entity a dot-shaped command belongs to
298
- # (`Board.AddCard`'s own `:cards`, declared on `Board`, not on
299
- # `Workspace`) — needed only by BUG#12's own recompute-side fix
300
- # (`recompute_append`, below): `entity.attribute(mutation.target)`
301
- # has to be asked of whichever construct actually declares it,
302
- # never the root aggregate unconditionally, the same distinction
303
- # `EntityElement#locate_chain` draws between `root_aggregate` and
304
- # each hop's own `owner`. Re-derived independently from
305
- # `entry[:verb]` alone, the same reasoning `aggregate_for_verb`'s
306
- # own comment gives.
307
- #
308
- # @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
309
- # keyed by domain name
310
- # @param verb [String] the dispatched verb to resolve
311
- # @return [Bluebook::Aggregate, Bluebook::Entity, nil] the construct that
312
- # declares the mutation's own target: the root aggregate for an
313
- # aggregate-owned command, or the owning entity for a dot-shaped
314
- # (entity-owned) command; nil if `verb` names no command path or no
315
- # resolvable aggregate
316
- def owner_for_verb(bluebooks, verb)
317
- domain_name, aggregate_name, command_path = Naming.split_verb(verb)
318
- return nil unless command_path
319
-
320
- aggregate = bluebooks[domain_name]&.aggregate(aggregate_name)
321
- return nil unless aggregate
322
- return aggregate unless command_path.include?(".")
323
-
324
- entity_name, = command_path.split(".", 2)
325
- aggregate.entities.find { |candidate| candidate.hecks_name == entity_name }
326
- end
327
-
328
- # Dispatches to the recompute rule for `mutation.op`.
329
- #
330
- # @param mutation [Bluebook::Mutation] the mutation to recompute
331
- # @param current [Object] the target field's before-value, as captured in
332
- # `entry[:before]`
333
- # @param args [Hash] the step's own raw args, as captured in `entry[:args]`
334
- # @param before_scope [Hash] the entity element's own before-state
335
- # (`entry[:before]`), the source an append's caller-omitted field resolves against
336
- # @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
337
- # namespace resolution
338
- # @param command [Bluebook::Command] the command that declared `mutation`
339
- # @param owner [Bluebook::Aggregate, Bluebook::Entity] the construct that
340
- # declares `mutation.target`; defaults to `aggregate`
341
- # @return [Object, Symbol] the recomputed after-value for `mutation.op`, or
342
- # `:unrecomputable` if the raw material doesn't fit the op's own contract
343
- def recompute_mutation(mutation, current, args, before_scope, aggregate, command, owner = aggregate)
344
- case mutation.op
345
- when :append
346
- recompute_append(current, mutation.source, before_scope, args, aggregate, command, owner, mutation.target)
347
- when :remove then recompute_remove(current, mutation.source, args)
348
- when :multiply then recompute_multiply(current, resolve_mutation_source(mutation.source, args))
349
- when :clamp then recompute_clamp(current, mutation.source)
350
- when :set then recompute_set(mutation.source, args, aggregate, owner, mutation.target)
351
- end
352
- end
353
-
354
- # `EntityElement#apply_to_element`'s own `:set` branch (entity_
355
- # element.rb), reproduced independently — not `MutationApplier#
356
- # apply`'s aggregate-level twin (mutation_applier.rb), a
357
- # different method with a different shape: `build_mutation_trace`
358
- # (replay.rb) only ever captures an entity-owned command's own
359
- # mutation (`command_name&.include?(".")`, that method's own
360
- # header), so `mutations_match_recompute` can only ever be
361
- # checking `apply_to_element`'s branch, never `apply`'s — the
362
- # same distinction `owner_for_verb`'s own comment already draws
363
- # for `:append`. Confirmed by reading `apply_to_element` directly:
364
- # its own `:set` branch resolves the source through `rules.
365
- # resolve_source` unconditionally (no `StateRef` branch at all —
366
- # unlike the aggregate-level `apply`, an entity-owned `sets` has
367
- # no declared corpus site using `state(:x)` today, so this
368
- # reproduces what ships, not a hypothetical), reads the
369
- # attribute off `entity.attribute(mutation.target)` (the owning
370
- # entity, `owner` here — `Board`, never `Workspace`), and coerces
371
- # through `Value.for_attribute(aggregate, attribute, value)`
372
- # (the root aggregate, for value-object namespace resolution
373
- # only — `value_object_for(aggregate, attribute.type)` — the
374
- # same aggregate/owner split `owner_for_verb`'s own comment
375
- # explains for `recompute_append`). Coercing against
376
- # `owner.attribute` rather than `aggregate.attribute` matters
377
- # concretely: `NestedPieces::Workspace.Board.Label` (`sets
378
- # :label`) has no `:label` attribute on `Workspace` at all, so
379
- # coercing against the aggregate would silently pass the raw,
380
- # uncoerced String through instead of wrapping it as
381
- # `BoardLabel`, and the comparison below would then disagree with
382
- # the real, correctly-coerced `after` state on every single run —
383
- # a real false positive, confirmed by running this against
384
- # `qa/stress_domains/nested_pieces`, not by inspection alone.
385
- #
386
- # Reusing `Value.for_attribute` for the coercion step, rather
387
- # than re-deriving it, is the same "coercion is its own already-
388
- # guaranteed door" reasoning `GUARANTEED_BY_CONSTRUCTION` states
389
- # and `#coerce_recompute_append_arg` already leans on for BUG#5 —
390
- # what this property exists to check is the source resolution and
391
- # routing (did the right raw value, from the right source, land
392
- # on the right target?), not whether `Value.for_attribute` itself
393
- # coerces correctly (a separate, already-enforced concern).
394
- # Rescued broadly: a `:set` mutation trace is only ever captured
395
- # after a real, already-admitted dispatch (`Replay#build_
396
- # mutation_trace`'s own header), so a raise here means this
397
- # recomputation resolved the wrong raw material, not that the
398
- # real dispatch was itself malformed — `:unrecomputable`, not a
399
- # crash, the same discipline every other branch in this method
400
- # already follows for the generator's own deliberate malforming.
401
- #
402
- # @param source [Symbol, Object] the mutation's own declared source: an argument
403
- # name to resolve from `args`, or a literal value already
404
- # @param args [Hash] the step's own raw args
405
- # @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
406
- # namespace resolution
407
- # @param owner [Bluebook::Aggregate, Bluebook::Entity, nil] the construct that
408
- # declares `target`
409
- # @param target [Symbol, nil] the mutation's own target field name
410
- # @return [Object, Symbol] the recomputed, materialized value, or
411
- # `:unrecomputable` if resolving or coercing the source raises
412
- def recompute_set(source, args, aggregate, owner, target)
413
- raw = resolve_mutation_source(source, args)
414
- attribute = owner&.attribute(target)
415
- coerced = attribute ? Runtime::Value.for_attribute(aggregate, attribute, raw) : raw
416
- Runtime::Value.materialize(coerced)
417
- rescue StandardError
418
- :unrecomputable
419
- end
420
-
421
- # `EntityElement#appended_to_element`'s own field-mapping half,
422
- # reproduced: the field map resolved the same two-tier way
423
- # (`MutationApplier#resolve_append_source` — a caller-supplied
424
- # arg, or the entity's own current field), then appended. (An
425
- # entity-dispatched command's own mutations do reach here —
426
- # `#build_mutation_trace`'s own comment describing them as never
427
- # reaching "the entity_element branch" means `MutationApplier#
428
- # appended`'s own aggregate-level entity_element fallback
429
- # specifically, which really is unreached from here; entity-owned
430
- # append dispatches go through `EntityElement#appended_to_element`
431
- # instead, and do reach this method.)
432
- #
433
- # BUG#5 — an entity-owned `:append` whose target field is itself
434
- # value-object-typed (`Board.AddCard`'s own `sets :cards, append:
435
- # { sequence: :sequence }`, `CardSequence`-typed). A caller-
436
- # supplied arg reaches the real applier (`EntityElement#
437
- # appended_to_element`) already coerced: `Interpreting#
438
- # coerce_declared_arguments` runs `Value.for_attribute` over
439
- # every arg the acting command itself declares, before dispatch
440
- # ever reaches a mutation applier at all — independent of, and
441
- # earlier than, anything `appended_to_element`'s own value_object
442
- # check does. `before_scope[source]` (the entity's own current
443
- # field) needs no such re-coercion here: it's already the
444
- # materialized shape `build_mutation_trace` snapshotted it in
445
- # (`Value.materialize`, same as `entry[:after]`), not a raw value
446
- # sitting behind a live `Value`.
447
- #
448
- # BUG#12 — `owner`/`target` (new here) let this also reproduce
449
- # `EntityElement#fill_declared_defaults`'s own entity-nested-in-
450
- # entity fallback (`appended_to_element`'s `else` branch, when
451
- # the appended element is itself an entity — `Card`, nested
452
- # inside `Board` — not a value object): `owner.attribute(target)
453
- # &.type` names the appended element's own type; when that names
454
- # an entity of `aggregate` rather than a value object, every one
455
- # of its own declared attributes `fields` doesn't already hold
456
- # gets `Instance.default_for`'s own default — reused, not
457
- # reimplemented, for the identical "never agree with itself"
458
- # reason BUG#5's own coercion re-derivation above already gives:
459
- # `Instance.default_for` is pre-existing, independently-tested
460
- # machinery (an ordinary aggregate's own creation already runs
461
- # through it via `Instance.defaults`), not the new glue
462
- # (`fill_declared_defaults` itself) this property exists to
463
- # catch a drift in.
464
- #
465
- # @param current [Object] the target field's before-value (an Array of elements)
466
- # @param source_map [Hash{Symbol => Object}] the append mutation's own field map:
467
- # each appended field's name to its source (an argument name Symbol, or a literal)
468
- # @param before_scope [Hash] the entity element's own before-state, the source a
469
- # caller-omitted field resolves against
470
- # @param args [Hash] the step's own raw args
471
- # @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
472
- # namespace resolution
473
- # @param command [Bluebook::Command] the command that declared the mutation, for
474
- # coercing a caller-supplied arg
475
- # @param owner [Bluebook::Aggregate, Bluebook::Entity] the construct whose nested
476
- # entity `target` may declare; defaults to `aggregate`
477
- # @param target [Symbol, nil] the append mutation's own target field name
478
- # @return [Array] `current` (coerced to an Array) with the newly built,
479
- # symbol-keyed element appended
480
- def recompute_append(current, source_map, before_scope, args, aggregate, command, owner = aggregate, target = nil)
481
- fields = source_map.transform_values do |source|
482
- resolve_mutation_append_field(source, before_scope, args, aggregate, command)
483
- end
484
- fill_recompute_declared_defaults(aggregate, owner, target, fields)
485
- Array(current) + [symbolize_deep(fields)]
486
- end
487
-
488
- # BUG#12's own recompute-side half — see `#recompute_append`'s
489
- # own comment above for why this exists and why it reuses
490
- # `Instance.default_for` rather than calling `EntityElement#
491
- # fill_declared_defaults` again. A no-op whenever `target` names
492
- # no attribute at all (every RECOMPUTABLE_MUTATION_OPS caller but
493
- # `:append` passes no `target`) or `target`'s own declared type
494
- # isn't an entity nested directly under `owner` (a value object,
495
- # or nothing declared at all — `owner.attribute` answering `nil`
496
- # for a target the DSL itself would already have refused at
497
- # build time). `owner.entities`, not `aggregate.entities` — a
498
- # piece nested inside a piece is a child of the owning entity
499
- # (`Card` is `Board.entities`, never `Workspace.entities`), the
500
- # same distinction `EntityElement#appended_to_element`'s own fix
501
- # draws.
502
- #
503
- # @param aggregate [Bluebook::Aggregate] the root aggregate, forwarded to
504
- # `Instance.default_for`
505
- # @param owner [Bluebook::Aggregate, Bluebook::Entity, nil] the construct whose
506
- # nested entity `target` may declare
507
- # @param target [Symbol, nil] the append mutation's own target field name
508
- # @param fields [Hash{Symbol => Object}] the field map already resolved by
509
- # `recompute_append`
510
- # @return [Hash{Symbol => Object}] `fields` unchanged if `target` is nil or does
511
- # not name a nested entity; otherwise `fields` with every declared attribute it
512
- # doesn't already hold filled in with its own default
513
- def fill_recompute_declared_defaults(aggregate, owner, target, fields)
514
- return fields unless target
515
-
516
- element_type = owner&.attribute(target)&.type
517
- entity = element_type && owner.entities.find { |piece| piece.hecks_name == element_type.to_s }
518
- return fields unless entity
519
-
520
- entity.attributes.each do |attribute|
521
- next if fields.key?(attribute.name)
522
-
523
- fields[attribute.name] = attribute.list? ? [] : Runtime::Instance.default_for(aggregate, attribute)
524
- end
525
- fields
526
- end
527
-
528
- # Resolves one appended field's own value from its declared source.
529
- #
530
- # @param source [Symbol, Object] an append field's own source: an argument name,
531
- # or a literal value
532
- # @param before_scope [Hash] the entity element's own before-state
533
- # @param args [Hash] the step's own raw args
534
- # @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
535
- # namespace resolution
536
- # @param command [Bluebook::Command] the command that declared the mutation
537
- # @return [Object] `before_scope[source]` if `source` names no caller-supplied
538
- # arg; `source` unchanged if it is not a Symbol; otherwise the coerced,
539
- # materialized arg value
540
- def resolve_mutation_append_field(source, before_scope, args, aggregate, command)
541
- return source unless source.is_a?(Symbol)
542
- return before_scope[source] unless args.key?(source)
543
-
544
- coerce_recompute_append_arg(aggregate, command, source, args[source])
545
- end
546
-
547
- # `Interpreting#coerce_declared_arguments`'s own coercion,
548
- # reproduced independently (never calling it again, the same
549
- # "never agree with itself" rule this whole module's header
550
- # comment gives) — a raw arg is coerced only when its own name
551
- # (`source`) is one of the acting command's own declared
552
- # attributes, exactly the condition that method checks before a
553
- # real dispatch ever coerces it either. `command.attribute(source)`
554
- # answering `nil` (a source that names no declared attribute —
555
- # never possible for `coerce_declared_arguments` to have touched
556
- # it in the real dispatch either) leaves `raw` exactly as it
557
- # arrived, the same as every bare-scalar append this already
558
- # handled correctly before BUG#5's fix.
559
- #
560
- # `Value.materialize`d immediately after coercing — matching the
561
- # plain-data shape `entry[:before]`/`entry[:after]` already carry
562
- # throughout this whole property, so the eventual `symbolize_deep`
563
- # comparison is always materialized-against-materialized, never a
564
- # live `Value` against a Hash.
565
- #
566
- # @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
567
- # namespace resolution
568
- # @param command [Bluebook::Command] the command whose own declared attribute
569
- # `source` may name
570
- # @param source [Symbol] the argument name to coerce
571
- # @param raw [Object] the raw arg value to coerce
572
- # @return [Object] `raw`, coerced and materialized, if `source` names a declared
573
- # attribute of `command`; `raw` unchanged otherwise
574
- def coerce_recompute_append_arg(aggregate, command, source, raw)
575
- attribute = command.attribute(source)
576
- return raw unless attribute
577
-
578
- Runtime::Value.materialize(Runtime::Value.for_attribute(aggregate, attribute, raw, argument: true))
579
- end
580
-
581
- # `MutationApplier#removed`'s own value-equality match, reproduced.
582
- #
583
- # @param current [Object] the target field's before-value (an Array of elements)
584
- # @param source [Symbol, Object] the remove mutation's own source: an argument
585
- # name, or a literal value already
586
- # @param args [Hash] the step's own raw args
587
- # @return [Array] `current` (coerced to an Array) with every element matching the
588
- # resolved target value removed
589
- def recompute_remove(current, source, args)
590
- target = symbolize_deep(resolve_mutation_source(source, args))
591
- Array(current).reject { |element| symbolize_deep(element) == target }
592
- end
593
-
594
- # `CommandRules::Arithmetic#multiply`'s own two branches,
595
- # reproduced on plain materialized data instead of a real Value:
596
- # a single-numeric-field Hash (the VO-typed case — ListCount, one
597
- # Integer field) scales that field ; a bare Numeric scales itself.
598
- # `current ||= 0` — the same phantom-field fallback #multiply
599
- # itself already gives (unaffected by this session's #clamp fix,
600
- # since #multiply never needed one).
601
- #
602
- # @param current [Hash, Numeric, nil] the target field's before-value
603
- # @param amount [Object] the resolved multiplier
604
- # @return [Hash, Numeric, Symbol] the scaled field/value, or `:unrecomputable` if
605
- # `amount` is not Numeric or `current` is neither a Hash with a numeric field
606
- # nor a Numeric
607
- def recompute_multiply(current, amount)
608
- return :unrecomputable unless amount.is_a?(Numeric)
609
-
610
- current ||= 0
611
- if current.is_a?(Hash)
612
- field = current.keys.find { |f| current[f].is_a?(Numeric) }
613
- return :unrecomputable unless field
614
-
615
- current.merge(field => current[field] * amount)
616
- elsif current.is_a?(Numeric)
617
- current * amount
618
- else
619
- :unrecomputable
620
- end
621
- end
622
-
623
- # `CommandRules::Arithmetic#clamp`'s own two branches, reproduced
624
- # the same way #recompute_multiply is — including this session's
625
- # own `current ||= 0` fix (command_rules/arithmetic.rb), the one
626
- # arithmetic op that didn't have it until now. `mutation.source`
627
- # is always a literal `[min, max]`, never an argument reference
628
- # (MutationApplier's own comment on why `resolve_source` is
629
- # skipped for clamp) — so nothing here reads `args` for it at all.
630
- #
631
- # @param current [Hash, Numeric, nil] the target field's before-value
632
- # @param bounds [Object] the mutation's own declared `[min, max]` literal
633
- # @return [Hash, Numeric, Symbol] the clamped field/value, or `:unrecomputable`
634
- # if `bounds` is not a 2-element Array or `current` is neither a Hash with a
635
- # numeric field nor a Numeric
636
- def recompute_clamp(current, bounds)
637
- return :unrecomputable unless bounds.is_a?(Array) && bounds.size == 2
638
-
639
- min, max = bounds
640
- current ||= 0
641
- if current.is_a?(Hash)
642
- field = current.keys.find { |f| current[f].is_a?(Numeric) }
643
- return :unrecomputable unless field
644
-
645
- current.merge(field => current[field].clamp(min, max))
646
- elsif current.is_a?(Numeric)
647
- current.clamp(min, max)
648
- else
649
- :unrecomputable
650
- end
651
- end
652
-
653
- # `CommandRules::Arithmetic#resolve_source`, reproduced: a
654
- # mutation's source is either the name of an argument or a
655
- # literal, told apart by type.
656
- #
657
- # @param source [Symbol, Object] an argument name to resolve from `args`, or a
658
- # literal value already
659
- # @param args [Hash] the step's own raw args
660
- # @return [Object] `args[source]` if `source` is a Symbol; `source` unchanged otherwise
661
- def resolve_mutation_source(source, args)
662
- source.is_a?(Symbol) ? args[source] : source
663
- end
664
-
665
- # A generated step's own `args` arrive with string keys on every
666
- # nested Hash (the wire/JSON shape `spec/corpus/*.json` already
667
- # uses) while `history[:mutation_traces]`' own materialized
668
- # before/after state carries symbol keys throughout (Runtime::
669
- # Value.materialize's own convention) — two hashes holding the
670
- # identical fact compare unequal by Ruby's own `Hash#==` unless
671
- # both sides are normalized the same way first. Recursive, since
672
- # an appended/removed element can itself nest a value object
673
- # (RemoveTag's own `Tag` argument, `{"key"=>..., "value"=>...}`).
674
- #
675
- # @param value [Object] a Hash, Array, or scalar to normalize
676
- # @return [Object] `value` with every Hash key symbolized, recursively through
677
- # nested Hashes and Arrays; any other value unchanged
678
- def symbolize_deep(value)
679
- case value
680
- when Hash then value.to_h { |key, val| [key.to_sym, symbolize_deep(val)] }
681
- when Array then value.map { |val| symbolize_deep(val) }
682
- else value
683
- end
684
- end
685
- end
686
- end
687
- end
688
- end