hecks 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (373) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +72 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +83 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
  7. data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
  9. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  10. data/lib/hecks/adapters/driven/heki.rb +106 -9
  11. data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
  12. data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
  13. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  15. data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
  16. data/lib/hecks/adapters/driven/lambda.rb +82 -35
  17. data/lib/hecks/adapters/driven/local_storage.rb +83 -10
  18. data/lib/hecks/adapters/driven/memory.rb +205 -9
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  24. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  25. data/lib/hecks/adapters/driven/prism.rb +50 -11
  26. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  31. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  32. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  33. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  34. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  35. data/lib/hecks/adapters/driven.rb +6 -4
  36. data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
  37. data/lib/hecks/behaviors/dsl.rb +60 -2
  38. data/lib/hecks/behaviors/expectations.rb +190 -29
  39. data/lib/hecks/behaviors/ir.rb +12 -1
  40. data/lib/hecks/behaviors/rspec.rb +9 -1
  41. data/lib/hecks/behaviors/runner.rb +21 -2
  42. data/lib/hecks/behaviors.rb +9 -1
  43. data/lib/hecks/bluebook/aggregate.rb +43 -13
  44. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
  45. data/lib/hecks/bluebook/assembly/build.rb +26 -5
  46. data/lib/hecks/bluebook/assembly/contract.rb +98 -23
  47. data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
  48. data/lib/hecks/bluebook/assembly/marks.rb +159 -30
  49. data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
  50. data/lib/hecks/bluebook/assembly.rb +32 -14
  51. data/lib/hecks/bluebook/attribute.rb +26 -12
  52. data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
  53. data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
  54. data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
  55. data/lib/hecks/bluebook/behaviour/command.rb +55 -25
  56. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
  57. data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
  58. data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
  59. data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
  60. data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
  61. data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
  62. data/lib/hecks/bluebook/behaviour/query.rb +6 -1
  63. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
  64. data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
  65. data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
  66. data/lib/hecks/bluebook/capabilities.rb +27 -0
  67. data/lib/hecks/bluebook/chapter.rb +51 -9
  68. data/lib/hecks/bluebook/command.rb +62 -17
  69. data/lib/hecks/bluebook/domain_port.rb +34 -9
  70. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  71. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  72. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
  73. data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
  74. data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
  75. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
  76. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
  77. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  78. data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
  79. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  80. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  81. data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
  82. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  83. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
  84. data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
  85. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  86. data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
  87. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  88. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  89. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
  90. data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
  91. data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
  92. data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
  93. data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
  94. data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
  95. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  96. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  97. data/lib/hecks/bluebook/entity.rb +40 -11
  98. data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
  99. data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
  100. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
  101. data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
  102. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
  103. data/lib/hecks/bluebook/expression/resolver.rb +369 -128
  104. data/lib/hecks/bluebook/hexagon.rb +35 -1
  105. data/lib/hecks/bluebook/lifecycle.rb +12 -1
  106. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
  107. data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
  108. data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
  109. data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
  110. data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
  111. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
  112. data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
  113. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
  114. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
  115. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
  116. data/lib/hecks/bluebook/meta_validator.rb +235 -139
  117. data/lib/hecks/bluebook/model_check.rb +434 -104
  118. data/lib/hecks/bluebook/pattern_subset.rb +32 -10
  119. data/lib/hecks/bluebook/policy.rb +15 -13
  120. data/lib/hecks/bluebook/process_manager.rb +27 -14
  121. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  122. data/lib/hecks/bluebook/project_loader.rb +40 -0
  123. data/lib/hecks/bluebook/project_register.rb +50 -6
  124. data/lib/hecks/bluebook/query.rb +31 -4
  125. data/lib/hecks/bluebook/read_model.rb +35 -15
  126. data/lib/hecks/bluebook/reference.rb +26 -13
  127. data/lib/hecks/bluebook/smoke_test.rb +46 -23
  128. data/lib/hecks/bluebook/synthesizer.rb +46 -12
  129. data/lib/hecks/bluebook/translation.rb +34 -5
  130. data/lib/hecks/bluebook/value_object.rb +29 -11
  131. data/lib/hecks/bluebook.rb +5 -6
  132. data/lib/hecks/codemod.rb +138 -50
  133. data/lib/hecks/construct.rb +21 -7
  134. data/lib/hecks/corpus.rb +438 -0
  135. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  136. data/lib/hecks/doc/reference.rb +200 -31
  137. data/lib/hecks/embryonaut_bluebook.rb +38 -15
  138. data/lib/hecks/facade/cli_door.rb +69 -10
  139. data/lib/hecks/facade/cli_runner.rb +105 -24
  140. data/lib/hecks/facade/command_request.rb +23 -0
  141. data/lib/hecks/facade/handle.rb +155 -35
  142. data/lib/hecks/facade/json_door.rb +106 -25
  143. data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
  144. data/lib/hecks/facade/surface/chapter.rb +26 -17
  145. data/lib/hecks/facade/surface.rb +16 -3
  146. data/lib/hecks/facade.rb +15 -4
  147. data/lib/hecks/forms/app.rb +46 -30
  148. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  149. data/lib/hecks/forms/field_renderer.rb +142 -6
  150. data/lib/hecks/forms/field_shape.rb +183 -19
  151. data/lib/hecks/forms/html.rb +51 -7
  152. data/lib/hecks/forms/index_renderer.rb +14 -2
  153. data/lib/hecks/forms/page.rb +14 -0
  154. data/lib/hecks/forms/params.rb +120 -23
  155. data/lib/hecks/forms/port_argument.rb +14 -2
  156. data/lib/hecks/forms/query_form_renderer.rb +65 -2
  157. data/lib/hecks/forms/record_renderer.rb +60 -2
  158. data/lib/hecks/forms/record_table.rb +28 -1
  159. data/lib/hecks/forms/reference_options.rb +24 -0
  160. data/lib/hecks/forms/value_object_shape.rb +13 -3
  161. data/lib/hecks/forms.rb +24 -4
  162. data/lib/hecks/fqn.rb +59 -1
  163. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  164. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  165. data/lib/hecks/framework/oidc.json +15 -0
  166. data/lib/hecks/framework.rb +79 -25
  167. data/lib/hecks/freezer.rb +27 -11
  168. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
  169. data/lib/hecks/fuzzing/combination_miner.rb +178 -0
  170. data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
  171. data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
  172. data/lib/hecks/fuzzing/differential.rb +192 -0
  173. data/lib/hecks/fuzzing/domain_generator.rb +871 -0
  174. data/lib/hecks/fuzzing/era_boundary.rb +68 -19
  175. data/lib/hecks/fuzzing/form_census.rb +121 -20
  176. data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
  177. data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
  178. data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
  179. data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
  180. data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
  181. data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
  182. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
  183. data/lib/hecks/fuzzing/properties/guards.rb +86 -42
  184. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
  185. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
  186. data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
  187. data/lib/hecks/fuzzing/properties/querying.rb +82 -28
  188. data/lib/hecks/fuzzing/properties.rb +84 -51
  189. data/lib/hecks/fuzzing/qa_settings.rb +164 -0
  190. data/lib/hecks/fuzzing/replay.rb +241 -133
  191. data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
  192. data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
  193. data/lib/hecks/fuzzing/self_consistency.rb +315 -137
  194. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
  195. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  196. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  197. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  198. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
  199. data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
  200. data/lib/hecks/fuzzing/shrinker.rb +309 -0
  201. data/lib/hecks/fuzzing/structural_skips.rb +37 -130
  202. data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
  203. data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
  204. data/lib/hecks/fuzzing/value_generator.rb +110 -22
  205. data/lib/hecks/fuzzing.rb +5 -0
  206. data/lib/hecks/grammar/evolve.rb +188 -12
  207. data/lib/hecks/grammar.rb +53 -7
  208. data/lib/hecks/ir.rb +51 -20
  209. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  210. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  211. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  212. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  213. data/lib/hecks/language/oidc.json +5 -0
  214. data/lib/hecks/literal.rb +41 -9
  215. data/lib/hecks/naming.rb +112 -31
  216. data/lib/hecks/ports/access_control.rb +53 -2
  217. data/lib/hecks/ports/agent/answers.rb +83 -6
  218. data/lib/hecks/ports/agent.rb +119 -35
  219. data/lib/hecks/ports/authentication.rb +44 -4
  220. data/lib/hecks/ports/authorization.rb +50 -11
  221. data/lib/hecks/ports/clock.rb +42 -23
  222. data/lib/hecks/ports/extraction.rb +16 -0
  223. data/lib/hecks/ports/identity_assignment.rb +23 -2
  224. data/lib/hecks/ports/identity_generation.rb +17 -3
  225. data/lib/hecks/ports/identity_resolution.rb +17 -1
  226. data/lib/hecks/ports/key_vault.port +6 -0
  227. data/lib/hecks/ports/key_vault.rb +58 -0
  228. data/lib/hecks/ports/loading.rb +4 -0
  229. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  230. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  231. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  232. data/lib/hecks/ports/persistence/execution.rb +4 -0
  233. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  234. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  235. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  236. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  237. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  238. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  239. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  250. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  251. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  252. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
  253. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  254. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  255. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  256. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  257. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  258. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
  259. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  260. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  261. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  262. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
  263. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
  264. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  265. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
  266. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  267. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  268. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  269. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  270. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  271. data/lib/hecks/ports/persistence.rb +36 -1
  272. data/lib/hecks/ports/projection.rb +61 -7
  273. data/lib/hecks/ports/query/in_memory.rb +41 -8
  274. data/lib/hecks/ports/query/ordering.rb +21 -6
  275. data/lib/hecks/ports/query.rb +35 -0
  276. data/lib/hecks/ports.rb +1 -0
  277. data/lib/hecks/projections/bootstrap_table.rb +147 -0
  278. data/lib/hecks/projections/diagrams.rb +315 -79
  279. data/lib/hecks/projections/glossary/html.rb +91 -3
  280. data/lib/hecks/projections/glossary/markdown.rb +50 -3
  281. data/lib/hecks/projections/glossary/mermaid.rb +49 -1
  282. data/lib/hecks/projections/glossary/sections.rb +1 -1
  283. data/lib/hecks/projections/glossary/sentences.rb +110 -5
  284. data/lib/hecks/projections/glossary.rb +173 -31
  285. data/lib/hecks/projections/ir.rb +1 -1
  286. data/lib/hecks/projections/model/deviations.rb +62 -17
  287. data/lib/hecks/projections/model.rb +75 -21
  288. data/lib/hecks/projections/oidc.rb +45 -8
  289. data/lib/hecks/projections/parser_table.rb +53 -12
  290. data/lib/hecks/projections/reference.rb +15 -3
  291. data/lib/hecks/projections/rust_vocabulary.rb +646 -0
  292. data/lib/hecks/projections/shape.rb +10 -2
  293. data/lib/hecks/projections/statements.rb +69 -22
  294. data/lib/hecks/projections/vocabulary.rb +26 -9
  295. data/lib/hecks/projections.rb +5 -3
  296. data/lib/hecks/projector/cli_projector.rb +245 -37
  297. data/lib/hecks/projector/docs_projector.rb +154 -28
  298. data/lib/hecks/projector/exporter.rb +104 -29
  299. data/lib/hecks/projector/ir_projector.rb +7 -1
  300. data/lib/hecks/projector/narrate_projector.rb +144 -23
  301. data/lib/hecks/projector/target.rb +42 -18
  302. data/lib/hecks/projector.rb +86 -18
  303. data/lib/hecks/query_ir.rb +94 -47
  304. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  305. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  306. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  307. data/lib/hecks/query_specification/common/null_policy.rb +61 -15
  308. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  309. data/lib/hecks/query_specification/common/options.rb +25 -0
  310. data/lib/hecks/query_specification/field_path.rb +69 -15
  311. data/lib/hecks/query_specification/hop_path.rb +57 -20
  312. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  313. data/lib/hecks/rendering.rb +9 -3
  314. data/lib/hecks/router/namespace_installer.rb +16 -3
  315. data/lib/hecks/router.rb +56 -1
  316. data/lib/hecks/runtime/aggregate_lock.rb +20 -11
  317. data/lib/hecks/runtime/boot_gates.rb +21 -3
  318. data/lib/hecks/runtime/caller.rb +40 -8
  319. data/lib/hecks/runtime/capability_graph.rb +13 -2
  320. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
  321. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
  322. data/lib/hecks/runtime/command_interpreter.rb +195 -113
  323. data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
  324. data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
  325. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  326. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  327. data/lib/hecks/runtime/command_rules/references.rb +145 -55
  328. data/lib/hecks/runtime/command_rules.rb +3 -0
  329. data/lib/hecks/runtime/dependency_planning.rb +56 -11
  330. data/lib/hecks/runtime/dispatcher.rb +263 -92
  331. data/lib/hecks/runtime/entity_element.rb +304 -67
  332. data/lib/hecks/runtime/entity_interpreter.rb +149 -88
  333. data/lib/hecks/runtime/errors.rb +37 -23
  334. data/lib/hecks/runtime/event.rb +15 -10
  335. data/lib/hecks/runtime/identity.rb +91 -23
  336. data/lib/hecks/runtime/instance.rb +103 -18
  337. data/lib/hecks/runtime/interpreting.rb +24 -16
  338. data/lib/hecks/runtime/invocation.rb +358 -0
  339. data/lib/hecks/runtime/loader.rb +105 -19
  340. data/lib/hecks/runtime/outbox.rb +164 -26
  341. data/lib/hecks/runtime/policy_interpreter.rb +73 -60
  342. data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
  343. data/lib/hecks/runtime/query_interpreter.rb +93 -74
  344. data/lib/hecks/runtime/reaction_invocation.rb +73 -28
  345. data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
  346. data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
  347. data/lib/hecks/runtime/reference_hop.rb +48 -6
  348. data/lib/hecks/runtime/refusal_wording.rb +142 -115
  349. data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
  350. data/lib/hecks/runtime/registry/verification.rb +153 -28
  351. data/lib/hecks/runtime/registry.rb +202 -32
  352. data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
  353. data/lib/hecks/runtime/routing.rb +36 -154
  354. data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
  355. data/lib/hecks/runtime/saga_interpreter.rb +90 -76
  356. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  357. data/lib/hecks/runtime/tenant_check.rb +33 -13
  358. data/lib/hecks/runtime/tenant_scope.rb +23 -5
  359. data/lib/hecks/runtime/value/admission.rb +75 -30
  360. data/lib/hecks/runtime/value/coercion.rb +555 -142
  361. data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
  362. data/lib/hecks/runtime/value.rb +71 -21
  363. data/lib/hecks/runtime.rb +39 -7
  364. data/lib/hecks/storehouse.rb +368 -72
  365. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  366. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  367. data/lib/hecks/version.rb +3 -3
  368. data/lib/hecks/vocabulary.rb +205 -4
  369. data/lib/hecks.rb +91 -11
  370. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
  371. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
  372. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
  373. metadata +28 -2
@@ -3,11 +3,11 @@ module Hecks
3
3
  module Bluebook
4
4
  module DSL
5
5
  # Parses a `.hecksagon` file's top-level DSL block into a `Hecksagon`
6
- # — a domain's own WIRING: which adapter binds to which verb
6
+ # — a domain's own wiring: which adapter binds to which verb
7
7
  # (`persisted_by`, `projected_by`, ...), which framework/vendored
8
8
  # bluebooks it attaches, which external events it subscribes to, and
9
9
  # its bare chapter-root port. Kept separate from the bluebook itself
10
- # (the domain's own declared MODEL) because wiring is an operational
10
+ # (the domain's own declared model) because wiring is an operational
11
11
  # decision, not a fact the domain states about itself.
12
12
  class HecksagonBuilder
13
13
  GRAMMAR_CONTEXT = "Hecksagon".freeze
@@ -20,6 +20,7 @@ module Hecks
20
20
 
21
21
  attr_reader :binds, :subscriptions, :framework_members, :vendored_bluebooks
22
22
 
23
+ # @param domain [String] name of the domain whose wiring this hecksagon declares
23
24
  def initialize(domain)
24
25
  @domain = domain
25
26
  @binds = []
@@ -28,44 +29,65 @@ module Hecks
28
29
  @vendored_bluebooks = []
29
30
  end
30
31
 
31
- # An event this hecksagon takes from OUTSIDE the domain's own
32
- # bluebook.
32
+ # Subscribes this hecksagon to an event it takes from outside the domain's own bluebook.
33
+ #
34
+ # @param event [String, Symbol] the external event's name
35
+ # @return [Array<String>] every subscription declared so far, this one last
33
36
  def subscribe(event) = @subscriptions << event.to_s
34
37
 
38
+ # Attaches a framework member to this domain and loads it into the current registry.
39
+ #
35
40
  # A framework/ member this domain wants attached —
36
41
  # Governance, Identity, whatever else lands beside them.
37
- # Attaching one is a WIRING decision, the same kind `persisted_by`/
42
+ # Attaching one is a wiring decision, the same kind `persisted_by`/
38
43
  # `projected_by` already are, so it lives here rather than as a
39
- # fact stated in the domain's own bluebook. Recorded onto THIS
44
+ # fact stated in the domain's own bluebook. Recorded onto this
40
45
  # hecksagon, the same way `subscribe` records onto its own
41
46
  # `subscriptions` — and loads the member's bluebook then its own
42
47
  # hecksagon into whatever registry this one is loading into, see
43
48
  # `Framework.load!`.
49
+ #
50
+ # @param name [String, Symbol] the member's name, such as `"Governance"`
51
+ # @return [Boolean, nil] true when this call loaded the member's bluebook, nil when the
52
+ # current registry already held it
53
+ # @raise [Runtime::WiringError] if no framework member has that name
44
54
  def uses_framework(name)
45
55
  @framework_members << name.to_s
46
56
  Hecks::Framework.load!(name)
47
57
  end
48
58
 
49
- # A VENDORED, EXTERNAL bluebook this domain wants attached — same
59
+ # Attaches a vendored embryonaut bluebook to this domain and loads its files into the
60
+ # current registry.
61
+ #
62
+ # A vendored, external bluebook this domain wants attached — same
50
63
  # wiring-decision shape `uses_framework` already is, one level
51
64
  # further out: not a member shipped inside hecks's own lib/,
52
- # but a separate package (embryonaut_bluebooks) vendored into THIS
65
+ # but a separate package (embryonaut_bluebooks) vendored into this
53
66
  # project's own checkout. See EmbryonautBluebook's own header for
54
- # the full reasoning on why its ROOT can't be a fixed constant the
67
+ # the full reasoning on why its root can't be a fixed constant the
55
68
  # way Framework::ROOT is, and for the recovery provenance.
56
69
  #
57
- # RECORDED ONTO @vendored_bluebooks, same shape `uses_framework`
58
- # already gives @framework_members — a SEPARATE list on purpose:
70
+ # Recorded onto @vendored_bluebooks, same shape `uses_framework`
71
+ # already gives @framework_members — a separate list on purpose:
59
72
  # `framework_members` is load-bearing for `refuse_ungoverned_roles!`
60
- # (below) and for Governance's own attachment check; conflating
61
- # the two would make a vendored bluebook attachment satisfy a
62
- # Governance check it has nothing to do with.
73
+ # (`Registry::Verification`) and for Governance's own attachment
74
+ # check; conflating the two would make a vendored bluebook attachment
75
+ # satisfy a Governance check it has nothing to do with.
76
+ #
77
+ # @param name [String, Symbol] the vendored package's directory name under
78
+ # `vendor/embryonaut_bluebooks/`
79
+ # @return [Array<String>, nil] paths of the `.bluebook` files this call loaded, or nil
80
+ # when the current registry already held the bluebook
81
+ # @raise [Runtime::WiringError] if the current registry has no root to vendor from, or
82
+ # no vendored bluebook of that name exists under it
63
83
  def uses_embryonaut_bluebook(name)
64
84
  @vendored_bluebooks << name.to_s
65
85
  Hecks::EmbryonautBluebook.load!(name)
66
86
  end
67
87
 
68
- # THE PRIMARY PORT, BARE AT THE ROOT — belongs to the CHAPTER as a
88
+ # Declares a port at the hecksagon's root and attaches it to the registered bluebook.
89
+ #
90
+ # The primary port, bare at the root — belongs to the chapter as a
69
91
  # whole, not one aggregate. `BindingProxy#port` is the aggregate-
70
92
  # scoped sibling (`Payments::Payment.port("Gateway") do ... end`);
71
93
  # this is what's left when a port isn't about any one record. The
@@ -73,14 +95,24 @@ module Hecks
73
95
  # loads after its bluebook, and this attaches to that real, final
74
96
  # object directly rather than building a second copy MetaValidator
75
97
  # would have to know how to reconstruct.
76
- # RENAMED FROM `port` — item #13's full metaprogrammed dispatch
77
- # (slice 5). Not bootstrap-reachable (checked directly — no
78
- # core/attached chapter declares a Hecksagon of its own). Reached
79
- # through `WordGate#method_missing`'s new `word_gate_dispatch`,
80
- # called explicitly below since `HecksagonBuilder`'s own
81
- # class-level `method_missing` (the open-verb catch-all beneath
82
- # this) always wins over the module's — see `word_gate.rb`'s own
83
- # header for the full mechanism.
98
+ #
99
+ # Answers the `port` word through the table's `calls:` column —
100
+ # item #13's full metaprogrammed dispatch (slice 5). Not
101
+ # bootstrap-reachable (checked directly — no core/attached chapter
102
+ # declares a Hecksagon of its own). Reached through `WordGate`'s
103
+ # `word_gate_dispatch`, called explicitly below since
104
+ # `HecksagonBuilder`'s own class-level `method_missing` (the
105
+ # open-verb catch-all beneath this) always wins over the module's —
106
+ # see `word_gate.rb`'s own header for the full mechanism.
107
+ #
108
+ # @param name [String] the port's name
109
+ # @yield the port body, evaluated against a `DomainPortBuilder`: either `verb`/`signal`
110
+ # or `operation`/`tells`/`asks` blocks
111
+ # @return [Bluebook::Port, Bluebook::DomainPort] the built port: a verb-shaped `Port`
112
+ # registered on the current registry, or a `DomainPort` attached to the bluebook
113
+ # @raise [Bluebook::DSL::Malformed] if the current registry holds no bluebook for this
114
+ # domain, or the body declares both a verb and operations, neither, or an operation
115
+ # the port grammar refuses
84
116
  def port_impl(name, &block)
85
117
  bluebook_ir = Hecks.current_registry.bluebook(@domain) or
86
118
  raise Malformed, "#{@domain} declares no such bluebook — a port needs one to belong to"
@@ -103,26 +135,74 @@ module Hecks
103
135
  bluebook_ir.add_port(built)
104
136
  end
105
137
 
106
- # NO ungoverned-role check here anymore — see
107
- # Registry::Verification#refuse_ungoverned_roles!. Moved out of
108
- # per-block `build`, recovered alongside `environment:`
138
+ # A TRANSLATION BOUNDARY, NOT A BUSINESS RULE — `translates "Name"
139
+ # do on Foreign::Domain::SomeEvent; trigger Local.Command; end`
140
+ # builds the EXACT SAME `Policy` a `policy` block inside this
141
+ # domain's own `.bluebook` would (same `on_impl`/`trigger_impl`,
142
+ # same `PolicyInterpreter` runtime — reusing `PolicyBuilder`
143
+ # directly, zero new runtime semantics). What's new is only WHERE
144
+ # it can be written: a cross-domain reaction is a wiring/context-
145
+ # mapping decision (this chapter conforming to a foreign chapter's
146
+ # published event), the same kind of decision `port`/
147
+ # `uses_framework` already are — not a fact this domain's own
148
+ # model states about itself, which `policy` (inside the bluebook)
149
+ # remains the right word for.
150
+ #
151
+ # MUST BE A BLOCK, not `translates Event, into: Command` — a flat
152
+ # call's arguments are evaluated eagerly, under THIS builder's own
153
+ # `ConstShim` resolver (`BindingProxy.namespace`, set up by
154
+ # `self.build` below), which mints a `BindingProxy` INSTANCE per
155
+ # segment and cannot answer a further `::` (it is not a Module) —
156
+ # a multi-segment reference like `Deploy::Tenant::TenantProvisioned`
157
+ # would raise before this method ever ran. `port_impl`'s own
158
+ # `ConstShim.with(->(const) { const })` swap only works because
159
+ # its own callers never pass a multi-segment bare constant as an
160
+ # eagerly-evaluated argument — everything of that shape lives
161
+ # inside ITS block, evaluated later, under the swapped resolver.
162
+ # A block defers evaluation the same way; a flat call cannot.
163
+ def translates(name, &block)
164
+ bluebook_ir = Hecks.current_registry.bluebook(@domain) or
165
+ raise Malformed, "#{@domain} declares no such bluebook — translates needs one to attach its reaction to"
166
+
167
+ # SAME RESOLVER BluebookBuilder ITSELF uses for a `policy`
168
+ # block's own `on`/`trigger` (bluebook_builder.rb) — a
169
+ # `ScopedConstant`, not the bare passthrough `port_impl` swaps
170
+ # to, because `on`/`trigger` here take genuinely multi-segment
171
+ # references (`Deploy::Tenant::TenantProvisioned`), not a
172
+ # single-segment type name.
173
+ resolver = ->(const) { ConstShim::ScopedConstant.for(const) }
174
+ built = ConstShim.with(resolver) { PolicyBuilder.build(name, &block) }
175
+
176
+ bluebook_ir.add_policy(built)
177
+ end
178
+
179
+ # Assembles the collected binds, subscriptions and attachments into a `Hecksagon`.
180
+ #
181
+ # No ungoverned-role check here — see
182
+ # Registry::Verification#refuse_ungoverned_roles!. It lives outside
183
+ # per-block `build`, alongside `environment:`
109
184
  # (Runtime::Loader.boot's comment has the provenance): a domain
110
185
  # split across multiple hecksagon blocks (base + an
111
186
  # `environments/<name>.hecksagon` overlay) would have every
112
187
  # block but the one declaring `uses_framework "Governance"`
113
- # refused HERE, even though `Registry#add_hecksagon` merges them
188
+ # refused here, even though `Registry#add_hecksagon` merges them
114
189
  # into one Hecksagon before anything ever dispatches against it.
115
- # Checking the MERGED result once, at verify! time — after every
190
+ # Checking the merged result once, at verify! time — after every
116
191
  # file for this domain has loaded — is both more permissive (no
117
192
  # need to repeat `uses_framework` in every file) and strictly
118
193
  # more correct (a check against an incomplete, not-yet-merged
119
194
  # hecksagon can never see the real final shape).
195
+ #
196
+ # @return [Bluebook::Hecksagon] this block's wiring, which `Registry#add_hecksagon` merges
197
+ # with any other block declared for the same domain
120
198
  def build
121
199
  Hecksagon.new(domain: @domain, binds: @binds, subscriptions: @subscriptions,
122
200
  framework_members: @framework_members, vendored_bluebooks: @vendored_bluebooks)
123
201
  end
124
202
 
125
- # DOMAIN-LEVEL DEFAULT BINDS — `persisted_by "Heki"` bare, at the top
203
+ # Records any verb the grammar does not own as a domain-wide bind to the named adapter.
204
+ #
205
+ # **Domain-level default binds** — `persisted_by "Heki"` bare, at the top
126
206
  # of a hecksagon block, applies to every aggregate in this domain
127
207
  # that doesn't declare its own override. Mirrors `BindingProxy`'s own
128
208
  # `method_missing` one level down (`aggregate:` filled in there,
@@ -130,12 +210,22 @@ module Hecks
130
210
  # `persisted_by`/`projected_by` specifically, so any future verb
131
211
  # gets a domain-level default for free too. See `Hecksagon#bind_for`
132
212
  # for the fallback lookup this feeds.
213
+ #
214
+ # @param verb [Symbol] the word called, such as `:persisted_by`
215
+ # @param args [Array<Object>] positional arguments; the first names the adapter
216
+ # @param kwargs [Hash{Symbol => Object}] keyword arguments; `:role` is kept on the bind
217
+ # @yield an optional block, called once after the bind is recorded
218
+ # @return [Object] this builder after recording a bind, or the grammar word's own result
219
+ # when `verb` is one the `Hecksagon` grammar admits
220
+ # @raise [NoMethodError] if `verb` is no grammar word and names no adapter
221
+ # @raise [Bluebook::DSL::Malformed] if the grammar admits `verb` only in another context,
222
+ # or admits it here and its implementation refuses the declaration
133
223
  def method_missing(verb, *args, **kwargs, &block)
134
224
  # A closed-set grammar word (`port`, today) gets first refusal
135
225
  # — item #13's full metaprogrammed dispatch (slice 5); see
136
226
  # `WordGate#word_gate_dispatch`'s own header for why this class
137
227
  # needs to call it explicitly rather than including it the
138
- # ordinary way. Only once THAT says "not admitted" does the
228
+ # ordinary way. Only once that says "not admitted" does the
139
229
  # genuinely open-ended `persisted_by "Heki"`-style bind
140
230
  # vocabulary below get a turn.
141
231
  result = word_gate_dispatch(verb, args, kwargs, block)
@@ -150,6 +240,16 @@ module Hecks
150
240
 
151
241
  def respond_to_missing?(_name, _include_private = false) = true
152
242
 
243
+ # Evaluates a `Hecks.hecksagon` block with bare `Domain::Aggregate` constants resolving to
244
+ # bind-collecting proxies, and returns the wiring it declared.
245
+ #
246
+ # @param domain [String] name of the domain being wired
247
+ # @yield the hecksagon body, evaluated with the builder as `self`; may be omitted
248
+ # @return [Bluebook::Hecksagon] the declared wiring
249
+ # @raise [Bluebook::DSL::Malformed] if a `port` names no registered bluebook or aggregate,
250
+ # or declares a shape the port grammar refuses
251
+ # @raise [Runtime::WiringError] if `uses_framework` or `uses_embryonaut_bluebook` names
252
+ # something that cannot be found
153
253
  def self.build(domain, &block)
154
254
  builder = new(domain)
155
255
  resolver = ->(name) { BindingProxy.namespace(name, builder.binds) }
@@ -1,28 +1,31 @@
1
1
  module Hecks
2
2
  module Bluebook
3
3
  module DSL
4
- # `identified_by` — SHARED by AggregateBuilder and EntityBuilder
4
+ # `identified_by` — shared by AggregateBuilder and EntityBuilder
5
5
  # only (S9, ADR 0025 — "EntityBuilder's duplicate identified_by and
6
6
  # resolve_pending_identity! go"): a piece's own identity cannot
7
7
  # spell differently from its aggregate's, and until this slice it
8
8
  # was hand-duplicated onto both rather than shared.
9
9
  #
10
- # A SEPARATE MODULE from AttributeCollector on purpose — every
10
+ # A separate module from AttributeCollector on purpose — every
11
11
  # `attribute()`-taking builder (Command, Query, PortOperation,
12
- # ValueObject, ...) `include`s that one too, and none of THEM
12
+ # ValueObject, ...) `include`s that one too, and none of them
13
13
  # declares an identity of its own ; folding `identified_by` in
14
14
  # there made it answer for six builders that never earned the
15
15
  # word (`syntax_conformance_spec.rb` catches exactly this — a
16
16
  # builder answering a method the grammar never grants it).
17
17
  #
18
- # Requires its includer to ALSO `include AttributeCollector`
18
+ # Requires its includer to also `include AttributeCollector`
19
19
  # (for `attributes`/`resolve_identity_field!`/`resolve_identity_
20
- # type!`, still declared there since every includer of THAT
20
+ # type!`, still declared there since every includer of that
21
21
  # module needs them) and to supply `identity_pool` (private) —
22
22
  # the value-object list a bare field's own type resolves
23
23
  # against: AggregateBuilder's own `@value_objects + closed_sets`,
24
- # or EntityBuilder's OWNER's, since a piece mints none of its own.
24
+ # or EntityBuilder's owner's, since a piece mints none of its own.
25
25
  module IdentityDeclaration
26
+ # Records which of the three live `identified_by` forms a builder declared, deferring
27
+ # resolution until build time.
28
+ #
26
29
  # There are three live forms, deliberately distinguishable at the
27
30
  # declaration site:
28
31
  #
@@ -33,12 +36,12 @@ module Hecks
33
36
  # One symbol is retired: it cannot say whether the author means a
34
37
  # value concept or a field-shaped database key. Frozen source still
35
38
  # reaches the old interpretation through `legacy_identified_by`.
36
- # RENAMED FROM `identified_by` — item #13's full metaprogrammed
37
- # dispatch (slice 4c), same shared-mixin shape `attribute_impl`
38
- # already proved in slice 3: ONE renamed method, both Aggregate
39
- # and Entity Keyword rows name it in `calls:`. Bootstrap-
40
- # reachable (every self-hosted aggregate/entity declares an
41
- # identity), so in BOOTSTRAP_CALLS_FALLBACK for both contexts.
39
+ # Both `AggregateBuilder` and `EntityBuilder` name this method in their Keyword rows'
40
+ # `calls:` column, so `GenericDispatch` forwards `identified_by` here unchanged, both
41
+ # after boot (reading the live grammar table) and during it
42
+ # (`GenericDispatch::BOOTSTRAP_CALLS_FALLBACK`, which carries every `calls:`-routed row
43
+ # unconditionally, not a hand-picked bootstrap-reachable subset — though this one is
44
+ # exercised during boot too, since every self-hosted aggregate/entity declares an identity).
42
45
  # Dispatches across the three live forms documented above (value-
43
46
  # object + block, single type target, single/compound field
44
47
  # target), each an early return that sets exactly one pending
@@ -46,6 +49,20 @@ module Hecks
46
49
  # with-nil semantics and the shared `@name`/`identity_pool`
47
50
  # threaded back out as parameters, for no gain beyond what the
48
51
  # three-forms comment above already documents.
52
+ #
53
+ # @param targets [Array<Symbol, Module>] zero or more identity targets: a value-object
54
+ # type (a bareword, resolved by `ConstShim` to a `Module`), one or more attribute-name
55
+ # symbols for a field or compound key, or empty when `definition` is given
56
+ # @param as [Symbol, nil] the field name to mint for a value-object-type target; not
57
+ # accepted with a field or compound-key target
58
+ # @yield a bespoke value-object body, `instance_eval`'d by `ValueObjectBuilder.build` to
59
+ # mint the identity's own anonymous value object
60
+ # @return [Array<Symbol>, Array<String>, nil] `targets` when a compound key is declared,
61
+ # the resolved identity paths from `legacy_identified_by` in shadow-parsing mode, or
62
+ # `nil` otherwise; callers reach this through `GenericDispatch`, which discards it
63
+ # @raise [Bluebook::DSL::Malformed] if `identified_by` was already called, combines a
64
+ # block with targets, names no identity, mixes a value-object type with field names,
65
+ # passes `as:` with a field or compound key, or the block declares no attributes
49
66
  # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
50
67
  def identified_by_impl(*targets, as: nil, &definition)
51
68
  return legacy_identified_by(*targets, as: as, &definition) if MetaValidator.shadow_parsing?
@@ -67,15 +84,15 @@ module Hecks
67
84
  &definition
68
85
  )
69
86
  rescue NameError => e
70
- # A REMOVED SPELLING MUST REFUSE LOUDLY, not degrade into a raw
87
+ # A removed spelling must refuse loudly, not degrade into a raw
71
88
  # Ruby error — the one contract `EraGuard.shadow_parse` leans
72
89
  # on to know a normal parse genuinely could not read this text
73
90
  # (only `Malformed` triggers its shadow-mode retry, era_guard.rb's
74
- # own comment). The OLD `identified_by { name.value }` — a block
75
- # whose text was NEVER CALLED, only extracted (legacy_
91
+ # own comment). The old `identified_by { name.value }` — a block
92
+ # whose text was never called, only extracted (legacy_
76
93
  # identified_by, below) — is exactly this shape: read under the
77
- # CURRENT grammar it is instead instance_eval'd as a value-object
78
- # DEFINITION, and a bare identifier like `name` inside it resolves
94
+ # current grammar it is instead instance_eval'd as a value-object
95
+ # definition, and a bare identifier like `name` inside it resolves
79
96
  # to nothing WordGate#method_missing recognizes, so Ruby itself
80
97
  # raises NameError. Left uncaught, that NameError skipped
81
98
  # shadow_parse's rescue entirely and reached callers as a raw
@@ -124,9 +141,9 @@ module Hecks
124
141
 
125
142
  private
126
143
 
127
- # RESOLVES whichever of the three `identified_by` shapes is
144
+ # Resolves whichever of the three `identified_by` shapes is
128
145
  # pending, against `identity_pool` — the includer's own private
129
- # hook. Called at BUILD time, not at `identified_by`'s own call
146
+ # hook. Called at build time, not at `identified_by`'s own call
130
147
  # time — see `AttributeCollector#resolve_identity_field!`'s own
131
148
  # comment on why.
132
149
  def resolve_pending_identity!
@@ -161,8 +178,8 @@ module Hecks
161
178
  raise Malformed, "#{@name} declares identified_by more than once"
162
179
  end
163
180
 
164
- # LEGACY — the two removed spellings (a value object + as:, and the
165
- # multi-line block), kept alive ONLY for `EraGuard.shadow_parse`
181
+ # Legacy — the two removed spellings (a value object + as:, and the
182
+ # multi-line block), kept alive only for `EraGuard.shadow_parse`
166
183
  # (S0a, ADR 0025) to still make sense of frozen era text that used
167
184
  # them; unreachable outside `MetaValidator.shadow_parsing?`.
168
185
  def legacy_identified_by(*targets, as:, &path)
@@ -12,16 +12,26 @@ module Hecks
12
12
 
13
13
  include WordGate
14
14
 
15
+ # @param field [Symbol, String] the attribute the state machine lives on, such as `:status`
16
+ # @param default [String, Symbol] the state a new record starts in
15
17
  def initialize(field, default:)
16
18
  @field = field
17
19
  @default = default
18
20
  @transitions = []
19
21
  end
20
22
 
21
- # RENAMED FROM `transition` — item #13's full metaprogrammed
23
+ # Records one transition row per command in `mapping`, all sharing its `from:` guard.
24
+ #
25
+ # Answers the `transition` word, which the grammar table routes here
26
+ # through its `calls:` column — item #13's full metaprogrammed
22
27
  # dispatch (slice 4c). Bootstrap-reachable (syntax.bluebook's
23
28
  # own Keyword/Argument entities describe their `status`
24
- # lifecycle with it), so in BOOTSTRAP_CALLS_FALLBACK.
29
+ # lifecycle with it), so in `BOOTSTRAP_CALLS_FALLBACK`.
30
+ #
31
+ # @param mapping [Hash{String, Symbol => String, Symbol, Array<String>}] command name to
32
+ # target state, as in `"Purchase" => "sold"`; the optional `:from` key holds the
33
+ # state, or Array of states, the transition applies from, and nil or absent means any
34
+ # @return [Hash] the command-to-target pairs just recorded, `:from` removed
25
35
  def transition_impl(mapping)
26
36
  mapping = mapping.dup
27
37
  from = mapping.delete(:from)
@@ -34,11 +44,24 @@ module Hecks
34
44
  end
35
45
  end
36
46
 
47
+ # Assembles the declared transitions into a `Lifecycle`, refusing an ambiguous table.
48
+ #
49
+ # @return [Bluebook::Lifecycle] the state machine: its field, default and transitions
50
+ # @raise [Bluebook::DSL::Malformed] if two transitions for one command could both apply
51
+ # from the same state (C5.3); skipped while shadow-parsing frozen era text
37
52
  def build
38
53
  refuse_ambiguity!
39
54
  Lifecycle.new(field: @field, default: @default, transitions: @transitions)
40
55
  end
41
56
 
57
+ # Evaluates a `lifecycle` block against a fresh builder and returns what it built.
58
+ #
59
+ # @param field [Symbol, String] the attribute the state machine lives on
60
+ # @param default [String, Symbol] the state a new record starts in
61
+ # @yield the lifecycle body of `transition` rows, evaluated with the builder as `self`
62
+ # @return [Bluebook::Lifecycle] the built state machine
63
+ # @raise [Bluebook::DSL::Malformed] if two transitions for one command overlap, or the
64
+ # block uses a word the `Lifecycle` grammar does not admit
42
65
  def self.build(field, default:, &block)
43
66
  builder = new(field, default: default)
44
67
  builder.instance_eval(&block) if block
@@ -51,9 +74,9 @@ module Hecks
51
74
  # one command whose `from:` sets overlap (or where either has no
52
75
  # `from:` at all) were silently first-wins; refused where the state
53
76
  # machine can be read whole. Two transitions for one command from
54
- # DISJOINT states are the legitimate shape (`match_transition`
77
+ # disjoint states are the legitimate shape (`match_transition`
55
78
  # picks by the current state) and stay. A `from:` naming a state
56
- # nothing declares is NOT refused here: it is a reachability
79
+ # nothing declares is not refused here: it is a reachability
57
80
  # finding `bin/model_check` already reports (unreachable state,
58
81
  # dead transition), and a bluebook may declare it on purpose.
59
82
  def refuse_ambiguity!
@@ -13,14 +13,17 @@ module Hecks
13
13
 
14
14
  include WordGate
15
15
 
16
+ # @param name [String] the policy's own name, as given to `policy "Name" do ... end`
16
17
  def initialize(name)
17
18
  @name = name
18
19
  end
19
20
 
20
- # `on Account::AccountFrozen` — A BARE CONSTANT ACCEPTED (ADR
21
+ # Records the event this policy reacts to.
22
+ #
23
+ # `on Account::AccountFrozen` — a bare constant accepted (ADR
21
24
  # 0025, S6 — "events first-class"), resolved through `ConstShim`
22
25
  # the same way `trigger`/`dispatch` already resolve a command
23
- # reference (`Naming.event_ref`, that method's own header). NOT
26
+ # reference (`Naming.event_ref`, that method's own header). Not
24
27
  # a required spelling yet, unlike `trigger`'s own quoted-text
25
28
  # refusal — see `policy.bluebook`'s own KeywordSeed comment for
26
29
  # why: event names aren't 100% migrated across the live corpus
@@ -28,17 +31,22 @@ module Hecks
28
31
  # AccountFrozen"` (quoted) and `on Account::AccountFrozen`
29
32
  # (bare) stay admitted until a full migration lands.
30
33
  #
31
- # RENAMED FROM the generic single-fill coercion — item #13's
32
- # full metaprogrammed dispatch, slice 1 (whole-project
33
- # table-unification survey), now overridden here the same way
34
- # `trigger_impl` overrides its own generic default.
34
+ # Reached through `calls: "on_impl"` rather than `GenericDispatch`'s generic single-fill
35
+ # coercion, the same way `trigger_impl` below overrides its own generic default — `on`
36
+ # admits both a bare constant and quoted text, not one single argument kind.
37
+ #
38
+ # @param event_ref [Symbol, String, Module] the event, as a bare constant (a
39
+ # `ScopedConstant` module `ConstShim` resolves) or quoted text
40
+ # @return [void]
35
41
  def on_impl(event_ref)
36
42
  @on_event = Naming.event_ref(event_ref)
37
43
  end
38
44
 
39
- # `with:` — WHAT THE TRIGGER IS GIVEN, when the event's own shape
40
- # is not it. Omitted, the whole event payload forwards verbatim,
41
- # which is what every policy did before this existed.
45
+ # Records the command this policy dispatches and, optionally, how the event's payload
46
+ # projects onto it.
47
+ #
48
+ # `with:` — what the trigger is given, when the event's own shape
49
+ # is not it. Omitted, the whole event payload forwards verbatim.
42
50
  #
43
51
  # Same `key => value` shape a saga's own `dispatch ..., with:`
44
52
  # takes, and read the same way at runtime: a Symbol names a field
@@ -46,28 +54,35 @@ module Hecks
46
54
  # supplies itself. The reason it exists is the reason a saga's
47
55
  # does — a reaction crosses an aggregate boundary, and the event
48
56
  # on one side is under no obligation to be shaped like the
49
- # command on the other. Without it the target has to DECLARE
57
+ # command on the other. Without it the target has to declare
50
58
  # every field the event happens to carry, whether it reads them
51
59
  # or not.
52
- # THE COMMAND ITSELF, NOT ITS NAME (ADR 0025, "events and
60
+ # The command itself, not its name (ADR 0025, "events and
53
61
  # reactions" — command references become first-class): `trigger
54
62
  # Account::Debit`, a bare constant `ConstShim` resolves the same
55
63
  # way `reference_to Account` always has, not a quoted verb string.
56
- # Collapses the qualified/unqualified split this word and a
57
- # saga's own `dispatch` used to disagree about — see
58
- # `Naming.command_ref`'s own header for how the `::`/`.` rewrite
59
- # works, and `SagaInterpreter#qualified` for why an unqualified
60
- # form has always been enough (same-domain is the fallback, so
61
- # `Account::Debit` and `Banking::Account::Debit` mean the same
62
- # thing here).
64
+ # Matches the qualified-or-not shape a saga's own `dispatch`
65
+ # command name takes — see `Naming.command_ref`'s own header for
66
+ # how the `::`/`.` rewrite works, and `SagaInterpreter#qualified`
67
+ # for why an unqualified form is always enough (same-domain is
68
+ # the fallback, so `Account::Debit` and `Banking::Account::Debit`
69
+ # mean the same thing here).
63
70
  #
64
- # LEGACY UNDER SHADOW-PARSING (S0a's own bridge) — frozen era
71
+ # Legacy under shadow-parsing (S0a's own bridge) — frozen era
65
72
  # text still writes the quoted form.
66
73
  #
67
- # RENAMED FROM `trigger` — item #13's full metaprogrammed
68
- # dispatch (slice 4), same reasoning as has_many_impl above: not
69
- # bootstrap-reachable, reached through calls: with no fallback
70
- # needed.
74
+ # `["Policy", "trigger"] => :trigger_impl` is in
75
+ # `GenericDispatch::BOOTSTRAP_CALLS_FALLBACK`, which carries every `calls:`-routed row
76
+ # unconditionally — this one is also exercised during boot itself, since every
77
+ # self-hosted policy declares a trigger.
78
+ #
79
+ # @param command_ref [Symbol, String, Module] the command, as a bare constant (a
80
+ # `ScopedConstant` module `ConstShim` resolves) or, under shadow-parsing, quoted text
81
+ # @param with [Hash{Symbol => Object}, nil] a projection from the triggering event's payload
82
+ # onto the command's own arguments; a Symbol value names an event field, anything else is
83
+ # a literal; `nil` forwards the whole event payload verbatim
84
+ # @return [void]
85
+ # @raise [Bluebook::DSL::Malformed] if `command_ref` is quoted text outside shadow-parsing
71
86
  def trigger_impl(command_ref, with: nil)
72
87
  if command_ref.is_a?(::String) && !MetaValidator.shadow_parsing?
73
88
  raise Malformed,
@@ -80,11 +95,30 @@ module Hecks
80
95
  @projection_declared = !with.nil?
81
96
  end
82
97
 
83
- # `across` — item #13's full metaprogrammed dispatch, slice 1:
84
- # same shape as `on`, above.
98
+ # Records the domain this policy's trigger reaches into.
99
+ #
100
+ # `across "Notifications"` names the domain a trigger reaches into.
101
+ # `expect_undelivered: true` declares that this domain expects that
102
+ # target never to be reached (no such domain, on purpose), which
103
+ # `ModelCheck` holds it to in both directions. Reached through
104
+ # `calls: "across_impl"` rather than the generic single-fill
105
+ # coercion, because `expect_undelivered:` is a keyword argument the
106
+ # generic coercion cannot take.
107
+ #
108
+ # @param domain [String, Symbol] the target domain's name
109
+ # @param expect_undelivered [Boolean] whether this domain expects `across`'s target to
110
+ # name no real domain; `ModelCheck` holds it to that in both directions
111
+ # @return [void]
112
+ def across_impl(domain, expect_undelivered: false)
113
+ @target_domain = domain.to_s
114
+ @expect_undelivered = expect_undelivered == true
115
+ end
85
116
 
86
- # THE GUARD — same extraction CommandBuilder#given/#ensures already
87
- # use (Ports::Extraction reads the block's SOURCE ; the block itself
117
+ # Records the guard predicate that decides whether this policy applies to a triggering
118
+ # event.
119
+ #
120
+ # **The guard** — same extraction CommandBuilder#given/#ensures already
121
+ # use (Ports::Extraction reads the block's source ; the block itself
88
122
  # is never called, here or at runtime — Runtime::PolicyInterpreter
89
123
  # evaluates the extracted text through the same
90
124
  # Bluebook::Expression::Evaluator a command's own given/ensures run
@@ -94,9 +128,15 @@ module Hecks
94
128
  # this policy does not apply to this event, exactly like an
95
129
  # `event_qualifier` miss, which carries no message either.
96
130
  #
97
- # Evaluated against the triggering EVENT's OWN PAYLOAD, not a
131
+ # Evaluated against the triggering event's own payload, not a
98
132
  # stored record — a policy reacts to what just happened, and has no
99
133
  # aggregate instance of its own to read state from.
134
+ #
135
+ # @yield the guard predicate, extracted as source text and evaluated later by
136
+ # `Runtime::PolicyInterpreter` against the triggering event's payload; never called here
137
+ # @return [void]
138
+ # @raise [Bluebook::DSL::Malformed] if the block's source could not be extracted, or if it
139
+ # references a pattern `Expression::AstJson.refuse_unshared_patterns!` does not share
100
140
  def where(&predicate)
101
141
  canonical = Ports::Extraction.canonical(predicate)
102
142
 
@@ -112,7 +152,7 @@ module Hecks
112
152
  @where = canonical
113
153
  end
114
154
 
115
- # THE FAN-OUT SOURCE — a query verb, "Aggregate.query_name" or
155
+ # **The fan-out source** — a query verb, "Aggregate.query_name" or
116
156
  # "Domain::Aggregate.query_name", the same qualified-or-not shape a
117
157
  # saga's own `dispatch` command name already takes
118
158
  # (SagaInterpreter#qualified). Runtime::PolicyInterpreter runs the
@@ -121,18 +161,28 @@ module Hecks
121
161
  # `for_each` — item #13's full metaprogrammed dispatch, slice 1:
122
162
  # same shape as `on`, above.
123
163
 
164
+ # Builds the `Policy` this builder has accumulated.
165
+ #
166
+ # @return [Bluebook::Policy] the built policy, carrying every field set by `on`,
167
+ # `trigger`, `across`, `where`, `for_each`, and `with:`
124
168
  def build
125
169
  Policy.new(
126
- name: @name,
127
- on_event: @on_event,
128
- trigger_command: @trigger_command,
129
- target_domain: @target_domain,
130
- where: @where,
131
- for_each: @for_each,
132
- with_spec: @with_spec || []
170
+ name: @name,
171
+ on_event: @on_event,
172
+ trigger_command: @trigger_command,
173
+ target_domain: @target_domain,
174
+ expect_undelivered: @expect_undelivered || false,
175
+ where: @where,
176
+ for_each: @for_each,
177
+ with_spec: @with_spec || []
133
178
  ).tap { |policy| policy.instance_variable_set(:@projection_declared, !!@projection_declared) }
134
179
  end
135
180
 
181
+ # Builds a `Policy` from a `policy "Name" do ... end` block.
182
+ #
183
+ # @param name [String] the policy's own name
184
+ # @yield the policy's body, `instance_eval`'d against a new builder
185
+ # @return [Bluebook::Policy] the built policy
136
186
  def self.build(name, &block)
137
187
  builder = new(name)
138
188
  builder.instance_eval(&block) if block