hecks 2.8.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 (404) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +4 -6
  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 +17 -98
  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 +5 -48
  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 +31 -141
  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 +14 -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 +25 -31
  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 +16 -69
  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 -194
  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 -111
  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 +53 -267
  109. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +43 -305
  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 +6 -21
  114. data/lib/hecks/bluebook/model_check.rb +100 -597
  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 +4 -23
  128. data/lib/hecks/cli/document.rb +3 -21
  129. data/lib/hecks/cli/ir.rb +3 -5
  130. data/lib/hecks/cli/mcp.rb +2 -3
  131. data/lib/hecks/cli/mcp_door.rb +19 -114
  132. data/lib/hecks/cli/model_check.rb +18 -85
  133. data/lib/hecks/cli/project_cli.rb +7 -40
  134. data/lib/hecks/cli/project_diagrams.rb +0 -7
  135. data/lib/hecks/cli/run.rb +12 -58
  136. data/lib/hecks/cli/smoke_test.rb +2 -16
  137. data/lib/hecks/cli/stores.rb +1 -2
  138. data/lib/hecks/cli.rb +18 -24
  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 +82 -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 +9 -90
  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 +61 -64
  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 +6 -25
  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 +18 -124
  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 +150 -678
  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 +2 -15
  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 +42 -134
  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 +3 -6
  336. data/lib/hecks/release/runner/commands.rb +2 -4
  337. data/lib/hecks/release/runner/gem_publisher.rb +2 -3
  338. data/lib/hecks/release/runner/npm_publisher.rb +6 -20
  339. data/lib/hecks/release/runner/preflight.rb +4 -19
  340. data/lib/hecks/release/runner/tagger.rb +2 -9
  341. data/lib/hecks/release/runner.rb +3 -32
  342. data/lib/hecks/rendering.rb +4 -26
  343. data/lib/hecks/router/namespace_installer.rb +12 -36
  344. data/lib/hecks/router.rb +4 -8
  345. data/lib/hecks/runtime/aggregate_lock.rb +8 -33
  346. data/lib/hecks/runtime/boot_gates.rb +3 -18
  347. data/lib/hecks/runtime/caller.rb +19 -59
  348. data/lib/hecks/runtime/capability_graph.rb +5 -21
  349. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +16 -78
  350. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +23 -160
  351. data/lib/hecks/runtime/command_interpreter.rb +124 -306
  352. data/lib/hecks/runtime/command_rules/admissibility.rb +95 -366
  353. data/lib/hecks/runtime/command_rules/arithmetic.rb +66 -196
  354. data/lib/hecks/runtime/command_rules/authorization.rb +13 -45
  355. data/lib/hecks/runtime/command_rules/emission.rb +8 -21
  356. data/lib/hecks/runtime/command_rules/references.rb +23 -202
  357. data/lib/hecks/runtime/dependency_planning.rb +30 -102
  358. data/lib/hecks/runtime/dispatcher.rb +82 -241
  359. data/lib/hecks/runtime/entity_element.rb +40 -462
  360. data/lib/hecks/runtime/entity_interpreter.rb +64 -219
  361. data/lib/hecks/runtime/errors.rb +32 -115
  362. data/lib/hecks/runtime/event.rb +3 -25
  363. data/lib/hecks/runtime/identity.rb +18 -154
  364. data/lib/hecks/runtime/instance.rb +18 -150
  365. data/lib/hecks/runtime/interpreting.rb +11 -70
  366. data/lib/hecks/runtime/invocation.rb +28 -164
  367. data/lib/hecks/runtime/loader.rb +48 -186
  368. data/lib/hecks/runtime/outbox.rb +38 -261
  369. data/lib/hecks/runtime/policy_interpreter.rb +38 -200
  370. data/lib/hecks/runtime/port_operation_interpreter.rb +26 -87
  371. data/lib/hecks/runtime/query_interpreter.rb +45 -187
  372. data/lib/hecks/runtime/reaction_invocation.rb +36 -138
  373. data/lib/hecks/runtime/read_model_interpreter.rb +117 -208
  374. data/lib/hecks/runtime/rebuild_sweep.rb +8 -54
  375. data/lib/hecks/runtime/reference_hop.rb +8 -81
  376. data/lib/hecks/runtime/refusal_wording.rb +8 -89
  377. data/lib/hecks/runtime/registry/saga_persistence.rb +10 -102
  378. data/lib/hecks/runtime/registry/verification.rb +52 -225
  379. data/lib/hecks/runtime/registry/world_defaults.rb +4 -53
  380. data/lib/hecks/runtime/registry.rb +99 -244
  381. data/lib/hecks/runtime/remote_dispatcher.rb +37 -152
  382. data/lib/hecks/runtime/routing.rb +4 -15
  383. data/lib/hecks/runtime/saga_interpreter/correlation.rb +14 -61
  384. data/lib/hecks/runtime/saga_interpreter.rb +79 -263
  385. data/lib/hecks/runtime/saga_pending_dispatch.rb +2 -43
  386. data/lib/hecks/runtime/tenant_check.rb +6 -68
  387. data/lib/hecks/runtime/tenant_scope.rb +3 -28
  388. data/lib/hecks/runtime/value/admission.rb +16 -67
  389. data/lib/hecks/runtime/value/coercion.rb +155 -641
  390. data/lib/hecks/runtime/value/entity_list_coercion.rb +26 -219
  391. data/lib/hecks/runtime/value.rb +12 -84
  392. data/lib/hecks/runtime.rb +17 -65
  393. data/lib/hecks/storehouse.rb +141 -568
  394. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +8 -32
  395. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +6 -23
  396. data/lib/hecks/vendoring/git_environment.rb +2 -8
  397. data/lib/hecks/vendoring/git_source.rb +4 -10
  398. data/lib/hecks/vendoring.rb +7 -46
  399. data/lib/hecks/version.rb +3 -14
  400. data/lib/hecks.rb +26 -81
  401. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -46
  402. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +11 -80
  403. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +10 -99
  404. metadata +3 -2
@@ -7,31 +7,12 @@ require_relative "../../ports/query/in_memory"
7
7
  module Hecks
8
8
  module Runtime
9
9
  class CommandRules
10
- # A reference must point at something that exists.
11
- #
12
- # `reference_to Customer` is the one guarantee an aggregate reference is
13
- # for, and it was declared 14 times across banking and enforced nowhere :
14
- # an Account could belong to a customer who was never registered, and
15
- # every gate stayed green because
16
- # no corpus step ever passed a dangling reference.
10
+ # Refuses a `reference_to` whose target identity does not exist.
17
11
  module References
18
- # Resolved here rather than in coercion because coercion is pure — it
19
- # holds no repository. A reference into another domain is left alone : a
20
- # cross-domain target may legitimately not be loaded, which is the same
21
- # reading `across` policies already get.
12
+ # Checks reference-typed command arguments against their target repositories.
13
+ # Cross-domain targets are skipped: they may legitimately not be loaded.
22
14
  #
23
- # Shared by CommandInterpreter and EntityInterpreter — an entity command
24
- # can declare a reference-typed attribute the same way an aggregate
25
- # command can, even though nothing in the real corpus does yet.
26
- #
27
- # @param domain [String, Symbol] the domain the referenced aggregate is
28
- # resolved in
29
- # @param command [Class] the command or port-operation class (`Bluebook::Command`
30
- # or `PortOperation` subclass) whose reference-typed attributes are checked
31
- # @param args [Hash{Symbol => Object}] the offered, already-coerced argument values
32
- # @return [void]
33
- # @raise [Runtime::NotFound] if a reference-typed argument names an identity that
34
- # does not exist in its target aggregate's repository
15
+ # @raise [Runtime::NotFound] if an argument names an identity that does not exist
35
16
  def resolve_references(domain, command, args)
36
17
  command.attributes.each do |attribute|
37
18
  next unless attribute.reference?
@@ -47,23 +28,11 @@ module Hecks
47
28
  end
48
29
  end
49
30
 
50
- # Structural references are checked again against the settled state.
51
- # This is what makes a `has_many` declared on an aggregate honest even
52
- # when a command supplies its list through an ordinary typed argument.
31
+ # Re-checks references, cardinality and tenant agreement against settled state.
53
32
  #
54
- # @param domain [String, Symbol] the domain the referenced aggregate is
55
- # resolved in
56
- # @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct
57
- # whose reference-typed attributes are checked against `state`
58
- # @param state [Hash{Symbol => Object}] the settled, post-mutation state to
59
- # check every reference and relationship cardinality against
60
- # @return [void]
61
- # @raise [Runtime::TypeMismatch] if a required `has_one`/`belongs_to`
62
- # relationship holds nil
63
- # @raise [Runtime::NotFound] if a reference-typed field names an identity
64
- # that does not exist in its target aggregate's repository
65
- # @raise [Runtime::Unauthorized] if a referenced record belongs to a
66
- # different tenant than `construct`'s own record
33
+ # @raise [Runtime::TypeMismatch] if a required `has_one`/`belongs_to` holds nil
34
+ # @raise [Runtime::NotFound] if a reference names an identity that does not exist
35
+ # @raise [Runtime::Unauthorized] if a referenced record belongs to another tenant
67
36
  def resolve_state_references(domain, construct, state)
68
37
  own_tenant_field = tenant_field_for(construct)
69
38
 
@@ -90,20 +59,8 @@ module Hecks
90
59
  end
91
60
  end
92
61
 
93
- # `has_one` and `belongs_to` mean exactly one target unless the
94
- # declaration explicitly says optional. State validation owns this
95
- # boundary because a command may leave an aggregate field untouched;
96
- # checking only command arguments would let a required relationship be
97
- # persisted as nil. `has_many` admits zero members, so its empty list is
98
- # already a valid cardinality and needs no presence refusal.
99
- #
100
- # @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct
101
- # `attribute` is declared on, named in a refusal
102
- # @param attribute [Bluebook::Attribute] the reference-typed attribute to check
103
- # @param held [Object, nil] the field's settled value
104
- # @return [void]
105
- # @raise [Runtime::TypeMismatch] if `attribute` declares a required (non-optional,
106
- # non-list) `has_one`/`belongs_to` relationship and `held` is nil
62
+ # `has_one` and `belongs_to` require a target unless optional. Checked on state,
63
+ # since a command may leave the field untouched.
107
64
  def validate_relationship_cardinality(construct, attribute, held)
108
65
  return if attribute.relationship.nil? || attribute.list?
109
66
  return unless held.nil? && !attribute.optional?
@@ -114,32 +71,14 @@ module Hecks
114
71
  "#{attribute.type.target_name} identity, got nil"
115
72
  end
116
73
 
117
- # The reference resolves itself — through the chapter's own IR, so the
118
- # bluebook's declared heads are the index, rather than regexing the
119
- # target's name out of "Reference<Customer>" and searching
120
- # `registry.bluebook(domain).aggregates` for it, or reaching the
121
- # target through Ruby's constant tree, a class thrown away for its
122
- # `.ir` the moment it was found.
74
+ # Resolves the attribute's target through the chapter IR.
123
75
  #
124
- # @param attribute [Bluebook::Attribute] the reference-typed attribute to resolve
125
- # @return [Bluebook::Aggregate, nil] the target aggregate the reference names,
126
- # or nil if it cannot be resolved
127
- # @raise [Bluebook::DSL::Malformed] if the reference does not know which
128
- # aggregate declares it
76
+ # @raise [Bluebook::DSL::Malformed] if the reference does not know its declaring aggregate
129
77
  def referenced_aggregate(attribute)
130
78
  attribute.type.resolve
131
79
  end
132
80
 
133
81
  # Refuses a reference-typed value whose target identity does not exist.
134
- #
135
- # @param domain [String, Symbol] the domain `target` is resolved in
136
- # @param target [Bluebook::Aggregate] the referenced aggregate
137
- # @param held [Object, Array<Object>] the offered value: a single reference,
138
- # or, when `list:` is true, an Array of them
139
- # @param list [Boolean] whether `held` is list-shaped
140
- # @return [void]
141
- # @raise [Runtime::NotFound] if any referenced identity does not exist in
142
- # `target`'s repository
143
82
  def validate_reference_values(domain, target, held, list:)
144
83
  values = list ? Array(held) : [held]
145
84
  values.each do |value|
@@ -154,47 +93,8 @@ module Hecks
154
93
  end
155
94
  end
156
95
 
157
- # Angle-8's own write-side half of `TenantScope.apply` (runtime/
158
- # tenant_scope.rb) — the query-side mechanism this mirrors. That
159
- # module turns a declared `authorize policy, tenant: :field` into a
160
- # synthetic where-clause checked against the caller's own supplied
161
- # tenant argument; there is no caller-identity/session system this
162
- # runtime has to check a write's caller against (TenantScope's own
163
- # header names that as a separate, still-open gap), so this checks
164
- # the one thing that is available without one: whether the record
165
- # being written and the record it references agree about which
166
- # tenant they belong to. `lib/hecks/fuzzing/properties/guards.rb`'s
167
- # `commands_respect_tenant_scope` states the identical claim,
168
- # read off `history[:instances]` after the fact — this is what
169
- # makes that claim hold by construction (a refused write is never
170
- # stored) rather than merely checked for regression.
171
- #
172
- # Hooked into `resolve_state_references` rather than a new
173
- # DISPATCH_ORDER step deliberately: that method already walks
174
- # every `reference_to`-typed attribute against the settled,
175
- # post-mutation state (the same moment `commands_respect_tenant_
176
- # scope` itself inspects), already resolves the referenced record
177
- # through the repository right above, and already runs from both
178
- # `CommandInterpreter#step_save` and `EntityInterpreter#step_save`
179
- # — one change, both interpreters covered, no new vocabulary step
180
- # to keep in sync with `Vocabulary::AggregateDispatchOrder`/
181
- # `EntityDispatchOrder`.
182
- #
183
- # @param domain [String, Symbol] the domain `target` is resolved in
184
- # @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct
185
- # `attribute` is declared on, named in a refusal
186
- # @param attribute [Bluebook::Attribute] the reference-typed attribute being
187
- # checked
188
- # @param target [Bluebook::Aggregate] the referenced aggregate
189
- # @param held [Object, Array<Object>] the offered value: a single reference,
190
- # or, when `attribute.list?` is true, an Array of them
191
- # @param state [Hash{Symbol => Object}] `construct`'s own settled state, read
192
- # for its own tenant field's value
193
- # @param own_tenant_field [Symbol, nil] `construct`'s own declared tenant field;
194
- # a no-op if nil or absent from `state`
195
- # @return [void]
196
- # @raise [Runtime::Unauthorized] if a referenced record's own tenant field
197
- # disagrees with `construct`'s
96
+ # Refuses a write whose record and referenced record disagree about their tenant.
97
+ # Hooked into `resolve_state_references` so both interpreters are covered.
198
98
  def enforce_tenant_boundary(domain, construct, attribute, target, held, state, own_tenant_field)
199
99
  return unless own_tenant_field && state.key?(own_tenant_field)
200
100
 
@@ -224,54 +124,16 @@ module Hecks
224
124
  end
225
125
  end
226
126
 
227
- # The field an aggregate's own query names as tenant-scoping — the
228
- # exact same lookup `Fuzzing::Properties::Guards#tenant_field_for`
229
- # already established for the property that found this gap, reused
230
- # here rather than reinvented: an aggregate's own declared tenant
231
- # field is whichever field one of its own queries names in
232
- # `authorize policy, tenant: :field`. `nil` for a construct that
233
- # declares no such query — not every aggregate is tenant-scoped,
234
- # and an entity never declares a query of its own at all today
235
- # (`Entity.queries` is always empty in the real corpus), so this
236
- # answers `nil` for every entity without needing to special-case
237
- # one.
238
- #
239
- # @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct
240
- # whose declared queries are searched
241
- # @return [Symbol, nil] the tenant-scoping field name, or nil if `construct`
242
- # declares no query with `authorize policy, tenant: :field`
127
+ # The field a query names in `authorize policy, tenant: :field`, or nil.
243
128
  def tenant_field_for(construct)
244
129
  authorization = construct.queries.filter_map(&:authorization).find(&:tenant)
245
130
  authorization&.tenant&.to_sym
246
131
  end
247
132
 
248
- # Renders a reference-typed value as the plain string key its target
249
- # record is looked up by.
250
- #
251
- # `value` is the referenced record's own id, exactly as `Identity.of`
252
- # would build it for that record — a bare scalar for a single-field
253
- # identity (the overwhelming common case; Banking's own plain
254
- # `reference_to Customer` holds one already, so `Value.
255
- # materialize_unwrapped` is a no-op passthrough here), or a
256
- # `Naming.identity`-joined string for a compound one
257
- # (`belongs_to Translation, as: :translation_ref` — Translation's
258
- # own `identified_by :domain, :from, :to`, three fields). Without
259
- # `materialize_unwrapped`, plain `value.to_s` on that compound case's
260
- # own coerced Value would hit Ruby's default `Object#to_s` (a raw,
261
- # run-to-run-random memory address) instead of joining the record's
262
- # real id — found live via bin/fuzz on the self-hosted "translation"
263
- # domain (replay_is_deterministic), the same class of gap `Identity.from`
264
- # already had for a compound `identified_by`'s own bare (undotted)
265
- # attribute paths. `materialize_unwrapped` recurses a multi-
266
- # attribute value object to a plain Hash keyed by attribute name,
267
- # in declaration order — `Naming.identity` on `.values` reproduces
268
- # the identical join `Identity.of` itself would produce for the
269
- # same fields.
133
+ # Renders a reference value as the string key its target record is looked up by.
270
134
  #
271
- # @param value [Object] the offered reference value: a `Runtime::Value`, a
272
- # scalar, or anything `Value.materialize_unwrapped` can open
273
- # @return [String] the key to look the referenced record up by; empty when
274
- # `value` names no id
135
+ # A compound identity arrives as a Value; `materialize_unwrapped` is needed so it joins
136
+ # like `Identity.of` instead of rendering `Object#to_s`.
275
137
  def reference_key(value)
276
138
  unwrapped = Value.materialize_unwrapped(value)
277
139
  return Naming.identity(unwrapped.values).to_s if unwrapped.is_a?(Hash)
@@ -279,56 +141,15 @@ module Hecks
279
141
  unwrapped.to_s
280
142
  end
281
143
 
282
- # Hydrates a command argument's own related record, reachable by name
283
- # from `given`/`ensures` — `disputed_by.status`, say, `CardPayment
284
- # .Dispute`'s own fresh `Reference<Customer>` argument — without
285
- # teaching the pure expression evaluator anything about
286
- # repositories. The lookup happens here, once, before evaluation;
287
- # `Resolver#lookup` just digs into a plain Hash exactly as it
288
- # always has.
289
- #
290
- # `owner` narrowed to `command` only (S12, ADR 0025 — "rules
291
- # confined to their own aggregate boundary"): dereferencing the
292
- # declaring aggregate/entity's own stored `reference_to` is not part
293
- # of this method's job — a cross-aggregate fact a rule needs has to
294
- # be a `projects`-maintained local field (`AggregateBuilder#
295
- # projects_impl`'s own comment), already present in `subject`'s own
296
- # state, no hydration needed, rather than a live query against
297
- # another aggregate's own repository on every `given`/`ensures`/
298
- # `invariant` run. A reference-typed command argument stays in
299
- # bounds, though — the ADR's own boundary list names "its command
300
- # arguments" as readable, and nothing is stored yet for a fresh
301
- # argument to project from; resolving it once here, synchronous
302
- # with this command's own admission, is a different shape from a
303
- # live query against an already-persisted reference. `enforce_givens`/
304
- # `enforce_ensures` are this method's only two remaining callers,
305
- # both passing `command`/`args`, never a `subject`'s own
306
- # aggregate — verified by grep, not assumed.
307
- #
308
- # Recurses into what it finds, so a chain deeper than one hop
309
- # still resolves in one pass. Depth-bounded rather than cycle-
310
- # detected — nothing in this corpus dots more than two hops on a
311
- # fresh argument, and a bound is simpler than tracking visited
312
- # (type, id) pairs for a cycle nothing here declares.
144
+ # Recursion bound; simpler than cycle detection for a cycle no corpus rule declares.
313
145
  DEREFERENCE_DEPTH = 4
314
146
  private_constant :DEREFERENCE_DEPTH
315
147
 
316
- # Hydrates `owner`'s own reference-typed attributes into the records they name.
148
+ # Hydrates reference-typed attributes into the records they name, so `given`/`ensures`
149
+ # can dot into them. Only command arguments are dereferenced (ADR 0025); a stored
150
+ # reference must be a `projects` field.
317
151
  #
318
- # @param domain [String, Symbol] the domain a referenced aggregate is
319
- # resolved in
320
- # @param owner [Class, Bluebook::Aggregate, Bluebook::Entity, nil] the
321
- # construct whose reference-typed attributes are dereferenced; nil (or a
322
- # depth of zero) short-circuits to an empty Hash
323
- # @param source [Hash{Symbol => Object}] the offered payload to read each
324
- # reference-typed attribute's raw id from
325
- # @param depth [Integer] how many more hops to recurse into a resolved
326
- # reference's own reference-typed fields
327
- # @return [Hash{Symbol => Object}] one entry per resolved reference-typed
328
- # attribute, keyed by the attribute name with a trailing `_id` stripped,
329
- # valued at the referenced record's state merged with its own dereferenced
330
- # references; an attribute with no id, an unresolvable target, or no
331
- # matching record contributes nothing
152
+ # @return [Hash{Symbol => Object}] keyed by attribute name with `_id` stripped
332
153
  def dereference(domain, owner, source, depth: DEREFERENCE_DEPTH)
333
154
  return {} if depth <= 0 || owner.nil?
334
155
 
@@ -2,8 +2,7 @@ require_relative "../bluebook/expression"
2
2
 
3
3
  module Hecks
4
4
  module Runtime
5
- # Persistence-neutral facts derived from a command's existing semantic IR.
6
- # This module deliberately does not execute a plan or choose a repository.
5
+ # Persistence-neutral facts derived from a command's semantic IR; it plans, never executes.
7
6
  module DependencyPlanning
8
7
  ATOMIC_PUT = :atomic_put
9
8
  TRANSACTIONAL_FALLBACK = :load_apply_validate_store
@@ -17,23 +16,18 @@ module Hecks
17
16
  :unresolved_dependencies,
18
17
  keyword_init: true
19
18
  ) do
20
- # Reports whether every owner field this command could touch is a known,
21
- # deterministic write.
19
+ # Whether every owner field this command could touch is a known, deterministic write.
22
20
  #
23
21
  # @return [Boolean] `complete_state`
24
22
  def complete_state? = complete_state
25
23
 
26
- # Reports whether the command needs no prior state at all (implies `complete_state?`).
24
+ # Whether the command needs no prior state at all (implies `complete_state?`).
27
25
  #
28
26
  # @return [Boolean] `state_independent`
29
27
  def state_independent? = state_independent
30
28
 
31
- # Chooses the dispatch strategy this plan's proof and the adapter's own
32
- # capabilities together allow.
33
- #
34
- # Capability negotiation is correctness-first: an optimization is
35
- # selected only when both the semantic proof and adapter capability
36
- # are present. This is planning data only; no runtime path calls it yet.
29
+ # Chooses the dispatch strategy the plan's proof and the adapter's capabilities allow.
30
+ # An optimization is selected only when both are present.
37
31
  #
38
32
  # @param capabilities [Array<String, Symbol>] the repository's declared capabilities
39
33
  # @return [Symbol] `DependencyPlanning::ATOMIC_PUT` when the plan is complete,
@@ -47,34 +41,22 @@ module Hecks
47
41
  end
48
42
  end
49
43
 
50
- # Walks a canonical expression's parsed nodes (the same AST
51
- # Bluebook::Expression::Evaluator evaluates) and collects the
52
- # dotted paths it reads — used by Analyzer to classify a
53
- # given/ensures/invariant rule's dependencies without evaluating it.
44
+ # Collects the dotted paths a canonical expression reads, without evaluating it.
45
+ # Used by Analyzer to classify rule dependencies.
54
46
  module ExpressionReads
55
47
  module_function
56
48
 
57
- # Finds every dotted path a canonical expression reads, without evaluating it.
49
+ # Finds every dotted path a canonical expression reads.
58
50
  #
59
- # Read the same parsed canonical-expression nodes the evaluator uses.
60
- # A generic Struct walk keeps this additive when the expression grammar
61
- # gains a composed node; only Lookup nodes carry domain dependencies.
51
+ # Walks the parsed nodes the evaluator uses; only Lookup nodes carry dependencies.
62
52
  #
63
- # @param canonical [String] the canonical expression text, such as a rule's `canonical`
53
+ # @param canonical [String] the canonical expression text
64
54
  # @return [Array<String>] the dotted paths the expression reads
65
55
  def paths(canonical)
66
56
  collect(Bluebook::Expression::Evaluator.parse(canonical), Set.new)
67
57
  end
68
58
 
69
- # Walks one parsed expression node, collecting the dotted paths it reads.
70
- #
71
- # @param node [Object] a node from `Bluebook::Expression::Evaluator.parse` — a
72
- # `Bluebook::Expression::Resolver::Lookup`, a `Bluebook::Expression::Resolver::
73
- # BlockPredicate`, a `Struct` composed of further nodes, an `Array` of them, or any
74
- # other value (a literal), which contributes no paths
75
- # @param bound_names [Set<String>] names locally bound by an enclosing block
76
- # predicate's own parameter, excluded from the result rather than reported as reads
77
- # @return [Array<String>] the dotted paths read under `node`, excluding `bound_names`
59
+ # Walks one parsed node, skipping names bound by an enclosing block predicate.
78
60
  def collect(node, bound_names)
79
61
  case node
80
62
  when Bluebook::Expression::Resolver::Lookup
@@ -93,30 +75,13 @@ module Hecks
93
75
  end
94
76
  end
95
77
 
96
- # Static, correctness-first dependency analysis for one command:
97
- # walks its mutations, lifecycle transitions, and given/ensures/
98
- # invariant rules to derive a Plan (read_set/write_set/
99
- # complete_state?/state_independent?) describing what the command
100
- # touches without executing it. `Analyzer.call` is what
101
- # CommandInterpreter and EntityInterpreter both consult before
102
- # choosing a dispatch strategy.
78
+ # Static dependency analysis for one command: derives a Plan without executing it.
79
+ # CommandInterpreter and EntityInterpreter consult it before choosing a dispatch strategy.
103
80
  class Analyzer
104
81
  STATEFUL_MUTATIONS = %i[append increment decrement multiply clamp remove].freeze
105
82
 
106
- # `root_aggregate:` — Wave 8's own audit surfaced a real bug here,
107
- # not merely a missing feature: for an entity-owned command,
108
- # `EntityInterpreter` calls this with `aggregate:` set to the
109
- # entity itself (`element_interpreter.rb`'s own `Analyzer.call
110
- # (aggregate: entity, command:)`), so `owner_fields` was always
111
- # the entity's own attribute set. A `given`/`ensures` reading
112
- # `parent.X` legitimately means the root aggregate's own field —
113
- # a genuinely different owner — but `classify_path`'s `:parent`
114
- # branch checked that read against `owner_fields` (the entity's),
115
- # which can never contain a root-level field, so every entity
116
- # command with a real, legitimate `parent.*` read was refused as
117
- # unresolved regardless of correctness. Defaults to `aggregate`
118
- # (a no-op) for the plain-aggregate case — `CommandInterpreter`'s
119
- # own call site never needed to change.
83
+ # `root_aggregate:` is the owner a `parent.*` read resolves against. An entity-owned
84
+ # command passes `aggregate:` as the entity, whose fields never include root-level ones.
120
85
  #
121
86
  # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct the command
122
87
  # is dispatched against; an entity for an entity-owned command
@@ -138,22 +103,9 @@ module Hecks
138
103
  @owner_fields << aggregate.lifecycle.field.to_sym if aggregate.lifecycle
139
104
  @root_owner_fields = root_aggregate.attributes.to_set(&:name)
140
105
  @root_owner_fields << root_aggregate.lifecycle.field.to_sym if root_aggregate.lifecycle
141
- # `projects` fields (S12, ADR 0025) are owner state too — a
142
- # `given`/`ensures` reading one (e.g. `customer_status ==
143
- # "active"`) is reading this record's own stored field, same
144
- # as any attribute, even though nothing here writes it via a
145
- # declared mutation (`CommandInterpreter#seed_projected_fields`
146
- # populates it outside this analysis entirely). Left out of
147
- # `known_writes` deliberately: `add_preservation_reads` then
148
- # correctly treats it as a prior-state read that must survive
149
- # a partial mutation, which is exactly right — a projected
150
- # field's freshness comes from the interpreter reseeding it on
151
- # save, not from anything a caller-supplied write set carries.
152
- # Applies to both `owner_fields` and `root_owner_fields` — an
153
- # entity's own `parent.*` read can name the root aggregate's
154
- # projected field just as easily as one of its real attributes
155
- # (`Banking::Withdrawal.Dispute`'s own `parent.account_customer_
156
- # status`, ATMCard's projected field, is a real, live example).
106
+ # `projects` fields are owner state: a rule reading one reads this record's stored
107
+ # field. Left out of `known_writes` so `add_preservation_reads` keeps it as a read;
108
+ # the interpreter reseeds it on save. `parent.*` reads may name the root's too.
157
109
  aggregate.projected_fields.each { |field| @owner_fields << field.name } if aggregate.respond_to?(:projected_fields)
158
110
  if root_aggregate.respond_to?(:projected_fields)
159
111
  root_aggregate.projected_fields.each do |field|
@@ -198,10 +150,8 @@ module Hecks
198
150
  attr_reader :aggregate, :command, :owner_fields, :root_owner_fields, :payload_fields,
199
151
  :state_reads, :payload_reads, :writes, :known_writes, :unresolved
200
152
 
201
- # A fresh Instance supplies these values without reading a stored
202
- # record. Keep this aligned with Instance.defaults/default_for. They
203
- # establish completeness but are not command mutations, so they do not
204
- # appear in write_set.
153
+ # A fresh Instance supplies these values without a stored record; keep aligned with
154
+ # Instance.defaults/default_for. They are not command mutations, so not in write_set.
205
155
  def analyze_initial_state
206
156
  aggregate.attributes.each do |attribute|
207
157
  known_writes << attribute.name if deterministic_initial_value?(attribute)
@@ -239,8 +189,7 @@ module Hecks
239
189
  end
240
190
  end
241
191
 
242
- # Returns true only when the source is known without prior aggregate
243
- # state. Hash sources are the canonical append binding shape.
192
+ # True only when the source is known without prior state. Hash sources are append bindings.
244
193
  def analyze_source?(source)
245
194
  case source
246
195
  when Symbol
@@ -282,34 +231,16 @@ module Hecks
282
231
  end
283
232
  end
284
233
 
285
- # Known, harmless gap: `corrects ..., as: :name`'s bound name
286
- # (admissibility.rb's `enforce_correction_target`/`enforce_givens`/
287
- # `enforce_ensures`) isn't special-cased here the way `:old`/
288
- # `:parent` are — a given/ensures referencing it falls through to
289
- # `unresolved` below (its own field lookup finds no owner/payload
290
- # match), same net effect as any other not-yet-optimized command:
291
- # `complete_state?` comes back false, so dispatch takes the safe
292
- # `hydrate_existing` path instead of the `ATOMIC_PUT` fast path.
293
- # Not a correctness bug — `as:`'s runtime binding (a plain `attrs`
294
- # merge, exactly like `old:`'s) resolves and evaluates correctly
295
- # regardless of what this static analysis concludes — just a real,
296
- # deliberately-left optimization gap: closing it would mean
297
- # threading "which names this command declares as correction
298
- # bindings" into the Analyzer, which doesn't have that per-command
299
- # context today. Worth doing alongside `:old`/`:parent`'s own
300
- # handling someday, not attempted here.
234
+ # Gap: the `as:` name bound by `corrects` is not special-cased like `:old`/`:parent`, so a
235
+ # rule reading it lands in `unresolved` and dispatch takes the safe `hydrate_existing`
236
+ # path. Runtime binding is unaffected.
301
237
  def classify_path(path, phase)
302
238
  head, nested = path.split(".", 2)
303
239
  name = head.to_sym
304
240
 
305
241
  if name == :parent
306
- # `root_owner_fields` — not `owner_fields`. For an entity-owned
307
- # command `owner_fields` is the entity's own attribute set;
308
- # `parent.X` always means the root aggregate's own field, a
309
- # genuinely different owner (`root_aggregate:`'s own header,
310
- # above, has the full bug this fixes). Identical for a plain
311
- # aggregate command, where root_aggregate defaults to aggregate
312
- # itself and the two sets are the same set.
242
+ # `parent.X` names the root aggregate's field, not the entity's `owner_fields`;
243
+ # the two sets are identical for a plain aggregate command.
313
244
  resolve_nested_state_read!(path, nested, root_owner_fields, "does not name parent aggregate state")
314
245
  elsif name == :old
315
246
  resolve_nested_state_read!(path, nested, owner_fields, "does not name prior aggregate state")
@@ -322,10 +253,8 @@ module Hecks
322
253
  end
323
254
  end
324
255
 
325
- # Shared shape behind the `:parent`/`:old` branches above: read the
326
- # nested field name, check it against the given owner field set
327
- # (deliberately different sets for `parent`/`old` — see the caller),
328
- # and either record it as a state read or refuse with `message`.
256
+ # Shared by the `:parent`/`:old` branches: checks the nested field against `field_set`
257
+ # and records a state read, or refuses with `message`.
329
258
  def resolve_nested_state_read!(path, nested, field_set, message)
330
259
  field = nested.to_s.split(".", 2).first
331
260
  if field.empty? || !field_set.include?(field.to_sym)
@@ -335,9 +264,8 @@ module Hecks
335
264
  end
336
265
  end
337
266
 
338
- # A partial mutation must preserve every untouched field on the
339
- # correctness path. Those prior values are real reads even when no rule
340
- # names them. A deterministic write needs no preservation read.
267
+ # A partial mutation must preserve every untouched field, so those prior values are
268
+ # reads even when no rule names them.
341
269
  def add_preservation_reads
342
270
  state_reads.merge(owner_fields - known_writes)
343
271
  end