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
@@ -8,7 +8,7 @@ module Hecks
8
8
  # (attributes, value objects, entities, commands, queries, policies,
9
9
  # invariants, preconditions, projected fields, its lifecycle and
10
10
  # identity) and assembling the final `Aggregate` IR in `#build`.
11
- # `entity`/`command`/`query` only QUEUE a descriptor during
11
+ # `entity`/`command`/`query` only queue a descriptor during
12
12
  # `instance_eval` (`#drain_pending!` builds them, in order, once the
13
13
  # whole block has run) so a later-declared piece can still be
14
14
  # referenced by an earlier line — see `#drain_pending!`'s own header.
@@ -21,6 +21,15 @@ module Hecks
21
21
  include WordGate
22
22
  include Sealing
23
23
 
24
+ # @param name [String] the aggregate's name, as written after `aggregate`
25
+ # @param chapter_named_givens [Hash{String => Hash{String => Bluebook::Given}}] the
26
+ # chapter-wide given pool, shared and written through by `given_impl`
27
+ # @param chapter_pending_givens [Array<Hash>] unresolved chapter-wide bare given
28
+ # references, appended to when this aggregate's own reference cannot resolve yet
29
+ # @param chapter_entity_named_givens [Hash{String => Hash{String => Bluebook::Given}}]
30
+ # the chapter-wide, entity-scoped given pool, threaded unchanged to every entity
31
+ # @param chapter_entity_pending_givens [Array<Hash>] unresolved chapter-wide,
32
+ # entity-scoped bare given references, threaded unchanged to every entity
24
33
  def initialize(name, chapter_named_givens: {}, chapter_pending_givens: [],
25
34
  chapter_entity_named_givens: {}, chapter_entity_pending_givens: [])
26
35
  @name = name
@@ -34,23 +43,23 @@ module Hecks
34
43
  @queries = []
35
44
  @policies = []
36
45
  @reference_targets = []
37
- # THE ROOT of the cross-entity given pool — see `#entity`'s own
38
- # comment. ONE hash for the whole aggregate, threaded unchanged
46
+ # The root of the cross-entity given pool — see `#entity`'s own
47
+ # comment. One hash for the whole aggregate, threaded unchanged
39
48
  # into every piece nested under it, however deep.
40
49
  @entity_named_givens = {}
41
- # ONE LEVEL WIDER STILL — the CHAPTER's own pool, threaded in
42
- # from `BluebookBuilder#aggregate`, shared with every OTHER
50
+ # **One level wider still** — the chapter's own pool, threaded in
51
+ # from `BluebookBuilder#aggregate`, shared with every other
43
52
  # aggregate the same chapter builds. See `#given`'s own
44
53
  # comment for what this closes.
45
54
  @chapter_named_givens = chapter_named_givens
46
- # A CHAPTER MAY BE SPLIT ACROSS FILES — threaded in the SAME
55
+ # A chapter may be split across files — threaded in the same
47
56
  # way as `@chapter_named_givens`, one Array shared chapter-wide.
48
57
  # See `#pending_chapter_given`'s own comment for what queues
49
58
  # here and `BluebookBuilder#resolve_pending_chapter_givens!`
50
59
  # for where it drains.
51
60
  @chapter_pending_givens = chapter_pending_givens
52
- # ONE LEVEL WIDER STILL, PAST THE CHAPTER'S OWN AGGREGATE-LEVEL
53
- # POOL — the chapter's own entity-scoped pool, threaded from
61
+ # One level wider still, past the chapter's own aggregate-level
62
+ # pool — the chapter's own entity-scoped pool, threaded from
54
63
  # `BluebookBuilder#aggregate_impl` the same way
55
64
  # `@chapter_named_givens` is, and passed straight through
56
65
  # (unchanged) to every top-level piece this aggregate builds
@@ -58,7 +67,7 @@ module Hecks
58
67
  # comment for what this closes.
59
68
  @chapter_entity_named_givens = chapter_entity_named_givens
60
69
  @chapter_entity_pending_givens = chapter_entity_pending_givens
61
- # DEFERRED CONSTRUCTION — `entity`/`command`/`query` push a
70
+ # **Deferred construction** — `entity`/`command`/`query` push a
62
71
  # pending descriptor here instead of building immediately; see
63
72
  # `#drain_pending!`'s own comment for why.
64
73
  @pending_entities = []
@@ -66,44 +75,65 @@ module Hecks
66
75
  @pending_queries = []
67
76
  end
68
77
 
78
+ # Sets the human-readable description shown for this aggregate.
79
+ #
80
+ # @param value [String] the description text
81
+ # @return [String] the description as stored
69
82
  def description(value)
70
83
  # moved to the language: Description invariant, on Root.Declare
71
84
 
72
85
  @description = value
73
86
  end
74
87
 
75
- # ORIGIN, not runtime identity — a concept adopted from a canonical
88
+ # Names where a concept adopted from a canonical source came from.
89
+ #
90
+ # Origin, not runtime identity — a concept adopted from a canonical
76
91
  # source (§28) names where it came from without that fact ever
77
92
  # touching `hecks_fqn`/dispatch. Captured raw, the same way
78
93
  # `attribute ..., default: { value: "small" }` captures a literal
79
94
  # Hash untouched — no re-parsing, no structure imposed beyond
80
95
  # "whatever the author wrote."
81
- # RENAMED FROM `provenance`/`projects`/`lifecycle`/`entity`/
82
- # `query`/`policy`/`command` (all below) — item #13's full
96
+ #
97
+ # Answers the `provenance` word (and, via the same table rows,
98
+ # its siblings `projects`/`lifecycle`/`entity`/
99
+ # `query`/`policy`/`command` below) through the table's `calls:`
100
+ # column — item #13's full
83
101
  # metaprogrammed dispatch (slice 4c). All bootstrap-reachable
84
102
  # (used throughout the core/attached chapters), all in
85
- # GenericDispatch::BOOTSTRAP_CALLS_FALLBACK.
103
+ # `GenericDispatch::BOOTSTRAP_CALLS_FALLBACK`.
104
+ #
105
+ # @param from [Object] the canonical source, captured exactly as written
106
+ # @return [Object] `from` as stored
86
107
  def provenance_impl(from:)
87
108
  @provenance = from
88
109
  end
89
110
 
111
+ # Declares a reference from this aggregate's own head to another aggregate's identity.
112
+ #
90
113
  # `optional:` — matching `CommandBuilder#reference_to`'s own
91
- # signature, which already had it; this one used to never
92
- # forward it to `attribute_impl()`/`relationship_attribute`
93
- # even though those already accept it — closed in the same
94
- # commit that added this comment (`optional: optional`, below).
95
- # It was a real gap because an aggregate that can point at ONE
96
- # OF several targets (Item's own `personal_list_id`/
114
+ # signature, which already had it, and forwarded here to
115
+ # `attribute_impl()`/`relationship_attribute` (`optional: optional`, below). A real
116
+ # need: an aggregate that can point at one
117
+ # of several targets (Item's own `personal_list_id`/
97
118
  # `camping_list_id`, never both) needs each reference optional
98
119
  # on the aggregate's own persisted schema, not just as a
99
120
  # command's input — real corpus use:
100
121
  # `spec/fixtures/hop_chain.bluebook`'s own `Proposal` aggregate
101
122
  # declares `reference_to Engagement, optional: true` at the
102
123
  # aggregate head.
103
- # RENAMED FROM `reference_to` — item #13's full metaprogrammed
124
+ #
125
+ # Answers the `reference_to` word through the table's `calls:`
126
+ # column — item #13's full metaprogrammed
104
127
  # dispatch (slice 4b). Bootstrap-reachable (every core/attached
105
128
  # grammar chapter uses reference_to to describe itself), so also
106
- # named in GenericDispatch::BOOTSTRAP_CALLS_FALLBACK.
129
+ # named in `GenericDispatch::BOOTSTRAP_CALLS_FALLBACK`.
130
+ #
131
+ # @param type [Module, Symbol, String] the referenced aggregate, written as a bare
132
+ # constant
133
+ # @param as [Symbol, nil] the attribute's name; nil derives it from `type`
134
+ # @param optional [Boolean] whether the reference may be absent
135
+ # @return [void]
136
+ # @raise [Bluebook::DSL::Malformed] if `as` (or the derived name) is already declared
107
137
  def reference_to_impl(type, as: nil, optional: false)
108
138
  target = Naming.demodulise(type)
109
139
  @reference_targets << target
@@ -111,32 +141,42 @@ module Hecks
111
141
  as || default_reference_name(target), optional: optional)
112
142
  end
113
143
 
114
- # A RULE MAY ONLY READ WITHIN ITS OWN AGGREGATE BOUNDARY (S12,
115
- # ADR 0025 — "Consistency across aggregate boundaries"). A
116
- # `given`/`ensures`/`invariant` used to reach through a
117
- # `reference_to` at RULE-EVALUATION TIME (`References#
144
+ # Declares that this aggregate holds its own kept-fresh copy of a field reached through
145
+ # a reference, so a rule can read it locally instead of reaching across the boundary.
146
+ #
147
+ # A rule may only read within its own aggregate boundary (S12,
148
+ # ADR 0025 — "Consistency across aggregate boundaries"). Reaching through a
149
+ # `reference_to` at rule-evaluation time (`References#
118
150
  # dereference`, a live query against another aggregate's own
119
- # repository, unbounded and inconsistent with the "a rule reads
120
- # only this record" model everywhere else) — `projects` is what
151
+ # repository) would be unbounded and inconsistent with the "a rule reads
152
+ # only this record" model everywhere else — `projects` is what
121
153
  # replaces that: `projects :customer_status, from: :"customer.
122
- # status"` declares that THIS aggregate holds its own copy of
123
- # `Customer`'s own `:status`, kept fresh by a REBUILD SWEEP
154
+ # status"` declares that this aggregate holds its own copy of
155
+ # `Customer`'s own `:status`, kept fresh by a rebuild sweep
124
156
  # (`Runtime::ProjectionRebuild`) rather than read live. A rule
125
157
  # then reads `customer_status` the same way it reads any other
126
158
  # local field — no dot, no reference walk.
127
159
  #
128
- # `from:` NAMES THE LOCAL REFERENCE, not the target aggregate —
129
- # `customer`, the attribute THIS aggregate's own `reference_to
160
+ # `from:` names the local reference, not the target aggregate —
161
+ # `customer`, the attribute this aggregate's own `reference_to
130
162
  # Customer` already minted, not `Customer` the type — so two
131
163
  # references to the same aggregate (aliased differently) can
132
- # each carry their own projection without ambiguity. The TARGET
164
+ # each carry their own projection without ambiguity. The target
133
165
  # field's own existence cannot be checked here: the target
134
- # aggregate does not exist yet while THIS one is still being
166
+ # aggregate does not exist yet while this one is still being
135
167
  # declared (the same reason a query's own hop tail is checked
136
168
  # by `BluebookBuilder#validate_query_hops!`, once every
137
169
  # aggregate in the chapter is real, not by `AggregateBuilder`
138
170
  # itself) — `validate_projected_fields!` is where that half
139
171
  # happens.
172
+ #
173
+ # @param name [Symbol, String] the local field's name this aggregate projects the
174
+ # remote value into
175
+ # @param from [Symbol, String] the local reference and remote field, dotted, such as
176
+ # `:"customer.status"`
177
+ # @return [Array<Bluebook::ProjectedField>] every projected field declared so far, this
178
+ # one last
179
+ # @raise [Bluebook::DSL::Malformed] if `from` is not `reference.field` shaped
140
180
  def projects_impl(name, from:)
141
181
  reference, _, remote_field = from.to_s.rpartition(".")
142
182
 
@@ -151,9 +191,9 @@ module Hecks
151
191
  remote_field: remote_field.to_sym)
152
192
  end
153
193
 
154
- # `has_many`/`has_one`/`belongs_to` were LEGACY (ADR 0025,
194
+ # `has_many`/`has_one`/`belongs_to` were legacy (ADR 0025,
155
195
  # "References") — sugar over `reference_to` that collapsed to an
156
- # anonymous reference and, for `has_many`, LIED (singularised its
196
+ # anonymous reference and, for `has_many`, lied (singularised its
157
197
  # target and minted one scalar, so `film.backers` read `nil` and
158
198
  # never `[]`). Wave 6 (identity-and-relationships arc) un-deprecates
159
199
  # all three for real: a relationship word now retains the author's
@@ -161,16 +201,31 @@ module Hecks
161
201
  # identities, but no longer collapsed to a bare `reference_to`
162
202
  # during assembly. `MetaValidator.shadow_parsing?` still routes to
163
203
  # `legacy_has_many`/`legacy_has_one` so frozen era text written
164
- # under the OLD (lying/collapsing) meaning still parses the way it
204
+ # under the old (lying/collapsing) meaning still parses the way it
165
205
  # did when it was written — real, if rare corpus: "Combined corpus
166
206
  # uses: one."
167
207
  #
168
- # RENAMED FROM `has_many`/`has_one`/`belongs_to` — item #13's full
169
- # metaprogrammed dispatch (slice 4). Each Keyword row's own
170
- # `calls:` names the matching `_impl`; not bootstrap-reachable
171
- # (no core/attached chapter uses one of these to describe itself),
172
- # so no BOOTSTRAP_CALLS_FALLBACK entry is needed, unlike
173
- # `attribute`/`role`.
208
+ # Declares a list-typed relationship to another aggregate, referenced by its plural name.
209
+ #
210
+ # Answers the `has_many` word (and, via the same table rows, its
211
+ # siblings `has_one`/`belongs_to` below) through the table's
212
+ # `calls:` column — item #13's full metaprogrammed dispatch
213
+ # (slice 4). Each Keyword row's own `calls:` names the matching
214
+ # `_impl`, so all three are carried in
215
+ # `GenericDispatch::BOOTSTRAP_CALLS_FALLBACK` like every other
216
+ # `calls:`-routed word, but never actually exercised during real
217
+ # bootstrap (no core/attached chapter uses one of these to
218
+ # describe itself), unlike `attribute`/`role`.
219
+ #
220
+ # @param type [Module, Symbol, String] the related aggregate's plural name, a bare
221
+ # constant
222
+ # @param as [Symbol, nil] the attribute's name; nil derives it from `type`
223
+ # @param legacy_options [Hash] must be empty outside shadow-parsing; under
224
+ # shadow-parsing, `:optional` is read for the legacy single-reference form
225
+ # @return [void]
226
+ # @raise [Bluebook::DSL::Malformed] outside shadow-parsing, if `legacy_options` is
227
+ # non-empty; may also raise from `relationship_attribute` if the derived name is
228
+ # already declared
174
229
  def has_many_impl(type, as: nil, **legacy_options)
175
230
  return legacy_has_many(type, as: as, optional: legacy_options.fetch(:optional, false)) if MetaValidator.shadow_parsing?
176
231
 
@@ -186,6 +241,12 @@ module Hecks
186
241
  list: true)
187
242
  end
188
243
 
244
+ # Declares a single-valued relationship this aggregate holds toward another.
245
+ #
246
+ # @param type [Module, Symbol, String] the related aggregate, a bare constant
247
+ # @param as [Symbol, nil] the attribute's name; nil derives it from `type`
248
+ # @param optional [Boolean] whether the relationship may be absent
249
+ # @return [void]
189
250
  def has_one_impl(type, as: nil, optional: false)
190
251
  return legacy_has_one(type, as: as, optional: optional) if MetaValidator.shadow_parsing?
191
252
 
@@ -195,6 +256,12 @@ module Hecks
195
256
  optional: optional)
196
257
  end
197
258
 
259
+ # Declares a single-valued relationship toward the aggregate that owns this one.
260
+ #
261
+ # @param type [Module, Symbol, String] the owning aggregate, a bare constant
262
+ # @param as [Symbol, nil] the attribute's name; nil derives it from `type`
263
+ # @param optional [Boolean] whether the relationship may be absent
264
+ # @return [void]
198
265
  def belongs_to_impl(type, as: nil, optional: false)
199
266
  return legacy_has_one(type, as: as, optional: optional) if MetaValidator.shadow_parsing?
200
267
 
@@ -204,80 +271,116 @@ module Hecks
204
271
  optional: optional)
205
272
  end
206
273
 
274
+ # Declares this aggregate's own state machine.
275
+ #
276
+ # @param field [Symbol, String] the attribute the state machine lives on
277
+ # @param default [String, Symbol] the state a new record starts in
278
+ # @yield the lifecycle body of `transition` rows, evaluated against a `LifecycleBuilder`
279
+ # @return [Bluebook::Lifecycle] the built state machine
280
+ # @raise [Bluebook::DSL::Malformed] if two transitions for one command overlap
207
281
  def lifecycle_impl(field, default:, &)
208
282
  @lifecycle = LifecycleBuilder.build(field, default: default, &)
209
283
  end
210
284
 
211
- # A piece is declared IN this aggregate — its owner is stamped by
285
+ # Queues a piece nested in this aggregate, built later once every sibling has been seen.
286
+ #
287
+ # A piece is declared in this aggregate — its owner is stamped by
212
288
  # `Aggregate#initialize`, once the aggregate exists. Its own
213
289
  # commands were given the piece as their owner when it was declared,
214
290
  # so the chain closes as chapter -> aggregate -> entity -> command.
215
- # NOT built here — see `#drain_pending!`'s own comment for why
291
+ # Not built here — see `#drain_pending!`'s own comment for why
216
292
  # this only queues a descriptor.
217
293
  #
218
- # A PRECONDITION SHARED ACROSS SIBLING PIECES, DECLARED ONCE — one
294
+ # A precondition shared across sibling pieces, declared once — one
219
295
  # level wider than round 4's own `EntityBuilder#given` (shared
220
- # across ONE piece's own commands): `@entity_named_givens` is the
221
- # SAME hash threaded into EVERY piece this aggregate builds, so a
296
+ # across one piece's own commands): `@entity_named_givens` is the
297
+ # same hash threaded into every piece this aggregate builds, so a
222
298
  # piece's own entity-level `given(desc) { block }` write-throughs
223
- # into it, and any OTHER piece's own command can reference it back
224
- # bare, the identical description/canonical, evaluated in ITS OWN
299
+ # into it, and any other piece's own command can reference it back
300
+ # bare, the identical description/canonical, evaluated in its own
225
301
  # `parent`-relative context. Real, live corpus this closes:
226
- # `SafeDepositBox`'s `Visit`/`KeyIssuance` — two DIFFERENT pieces
302
+ # `SafeDepositBox`'s `Visit`/`KeyIssuance` — two different pieces
227
303
  # under one head, each independently typing `given("customer is
228
304
  # active") { parent.customer.status == "active" }` byte for byte,
229
- # which neither the aggregate's OWN "customer is active" (a
230
- # DIFFERENT canonical — bare `customer.status`, not
305
+ # which neither the aggregate's own "customer is active" (a
306
+ # different canonical — bare `customer.status`, not
231
307
  # `parent.customer.status`, wrong scope for a piece's own command
232
308
  # to evaluate) nor round 4's single-piece `given` could reach.
309
+ # @param name [String] the nested piece's name
310
+ # @yield the piece body, evaluated against an `EntityBuilder` once drained
311
+ # @return [Array<Array>] every pending piece queued so far, this one last
233
312
  def entity_impl(name, &block)
234
313
  @pending_entities << [name, block]
235
314
  end
236
315
 
316
+ # Queues a query declared on this aggregate, built later once every sibling has been seen.
317
+ #
318
+ # @param name [String] the query's name
319
+ # @yield the query body, evaluated against a `QueryBuilder` once drained
320
+ # @return [Array<Array>] every pending query queued so far, this one last
237
321
  def query_impl(name, &block)
238
322
  @pending_queries << [name, block]
239
323
  end
240
324
 
325
+ # Declares a policy scoped to this aggregate, stamping it with the aggregate's own name.
326
+ #
327
+ # @param name [String] the policy's name
328
+ # @yield the policy body, evaluated against a `PolicyBuilder`
329
+ # @return [Array<Bluebook::Policy>] every policy declared so far, this one last
330
+ # @raise [Bluebook::DSL::Malformed] if the body fails any check the policy builder raises
241
331
  def policy_impl(name, &)
242
332
  reaction = PolicyBuilder.build(name, &)
243
333
  reaction.aggregate = @name
244
334
  @policies << reaction
245
335
  end
246
336
 
247
- # `builder.closed_sets` TOO, not only `builder.build` — a REAL,
248
- # previously-unreachable gap this exact fix exposed: a
249
- # value_object's own INLINE `attribute :x, one_of(...)` (now legal
250
- # — S3, ADR 0025 removed the wrong-arity collision that used to
251
- # make this crash before it could ever matter) synthesises its own
252
- # anonymous value object via the SAME `AttributeCollector#closed_
337
+ # Declares a value object on this aggregate, either as a block of `attribute` lines or
338
+ # (the `type` shorthand) as a single `:value` attribute.
339
+ #
340
+ # `builder.closed_sets` too, not only `builder.build` — a real
341
+ # gap this exact fix closes: a
342
+ # value_object's own inline `attribute :x, one_of(...)` (legal
343
+ # since S3, ADR 0025 removed the wrong-arity collision that would
344
+ # otherwise crash it) synthesises its own
345
+ # anonymous value object via the same `AttributeCollector#closed_
253
346
  # sets` mechanism an aggregate's own attributes already use — and
254
347
  # nothing installed it anywhere. `Box.attributes` said `size:
255
348
  # "Size"` while no "Size" value object existed in the whole
256
349
  # domain: a dangling type name, not a working closed set. Flattened
257
- # into THIS aggregate's own `@value_objects`, the identical move
350
+ # into this aggregate's own `@value_objects`, the identical move
258
351
  # `@value_objects + closed_sets` already makes for the aggregate's
259
352
  # own direct attributes (see this file's other 5 call sites).
260
- # `type` — THE BARE SHORTHAND (single-attribute value objects):
353
+ # `type` — the bare shorthand (single-attribute value objects):
261
354
  # `value_object :Price, Integer` declares a value object with
262
- # exactly one attribute, NAMED `value`, of that type — pure sugar
355
+ # exactly one attribute, named `value`, of that type — pure sugar
263
356
  # for `value_object("Price") { attribute :value, Integer }`,
264
- # routed through the SAME `attribute_impl` the block form's own
357
+ # routed through the same `attribute_impl` the block form's own
265
358
  # `attribute` line reaches (so the quoted-text-type refusal,
266
359
  # `one_of(...)`/`list_of(...)` synthesis, everything an attribute
267
360
  # line already does, applies unchanged rather than being
268
361
  # re-derived here). The name `value` is not arbitrary: a
269
- # single-attribute value object is a NAME for a scalar, not a
362
+ # single-attribute value object is a name for a scalar, not a
270
363
  # genuine group ([[feedback_name_the_scalar_field]], `Behaviour::
271
364
  # ValueObject#sole_attribute`), and `value` is what the language
272
- # guarantees EVERY sole field answers to at runtime regardless of
365
+ # guarantees every sole field answers to at runtime regardless of
273
366
  # its declared name (`Runtime::Value#method_missing`'s alias) —
274
367
  # so the shorthand simply declares it under the canonical name
275
- # directly. Type AND block together are refused: the block exists
368
+ # directly. Type and block together are refused: the block exists
276
369
  # to say what the fields are, and the type just said it — two
277
370
  # answers to one question is an authoring error, never a merge.
278
- # NEITHER type NOR block keeps its historical behavior untouched
371
+ # Neither type nor block keeps its historical behavior untouched
279
372
  # (an empty attribute list — judged, or not, by the language
280
373
  # downstream, the same as before this parameter existed).
374
+ #
375
+ # @param name [String] the value object's name
376
+ # @param type [Module, nil] the bare shorthand's single attribute type; mutually
377
+ # exclusive with `block`
378
+ # @yield the value object body of `attribute`/`invariant`/`one_of` lines; mutually
379
+ # exclusive with `type`
380
+ # @return [Array<Bluebook::ValueObject>] this aggregate's own value objects, including
381
+ # this one and any closed sets its attributes synthesised
382
+ # @raise [Bluebook::DSL::Malformed] if both `type` and a block are given, or the body
383
+ # fails any check the value object language or its builder raises
281
384
  def value_object(name, type = nil, &block)
282
385
  if type && block
283
386
  raise Malformed,
@@ -293,37 +396,49 @@ module Hecks
293
396
  @value_objects.concat(builder.closed_sets)
294
397
  end
295
398
 
296
- # `from:` — LIFECYCLE STATE BECOMES A COMMAND GUARD (S10, ADR
399
+ # Queues a command declared on this aggregate, built later once every sibling has been
400
+ # seen.
401
+ #
402
+ # `from:` — lifecycle state becomes a command guard (S10, ADR
297
403
  # 0025) — `command "Debit", from: "open"` replaces `given
298
404
  # ("account is open") { status == "open" }`, written 35 times
299
- # in two wordings across the corpus. Checked against THIS
405
+ # in two wordings across the corpus. Checked against this
300
406
  # aggregate's own lifecycle field (`Admissibility#enforce_
301
407
  # lifecycle_guard`) — never a target state, never a transition:
302
408
  # the lifecycle already declares which states exist, so naming
303
409
  # the legal ones is checkable against it, where a free-text
304
410
  # given could drift out of sync with the state machine and did.
411
+ #
412
+ # @param name [String] the command's name
413
+ # @param from [String, Symbol, Array<String, Symbol>, nil] the lifecycle state(s) this
414
+ # command guards from; nil admits from any state
415
+ # @yield the command body, evaluated against a `CommandBuilder` once drained
416
+ # @return [Array<Array>] every pending command queued so far, this one last
305
417
  def command_impl(name, from: nil, &block)
306
- # The verb is declared ON this aggregate — the owner `acts_on` answers
418
+ # The verb is declared on this aggregate — the owner `acts_on` answers
307
419
  # with — stamped by `Aggregate#initialize` once the aggregate
308
- # exists. An ENTITY's commands take the entity as their owner instead,
309
- # at the entity's own declaration. NOT built here — see
420
+ # exists. An entity's commands take the entity as their owner instead,
421
+ # at the entity's own declaration. Not built here — see
310
422
  # `#drain_pending!`'s own comment for why this only queues a
311
423
  # descriptor.
312
424
  @pending_commands << [name, from, block]
313
425
  end
314
426
 
315
- # A PRECONDITION SHARED ACROSS COMMANDS, DECLARED ONCE (S10, ADR
427
+ # Declares a rule this aggregate's own commands must satisfy, or references one a
428
+ # sibling aggregate in the chapter already declared.
429
+ #
430
+ # A precondition shared across commands, declared once (S10, ADR
316
431
  # 0025) — an aggregate-level `given`, block required, stored by
317
432
  # its own description rather than appended anywhere: a command
318
433
  # names it back (`given("customer is active")`, no block of its
319
434
  # own) rather than re-typing the predicate, so there is one
320
435
  # description and therefore one refusal message no matter which
321
- # command a caller hits. DECLARE BEFORE THE COMMANDS THAT
322
- # REFERENCE IT — resolution happens at the referencing command's
323
- # OWN build time (`CommandBuilder#given`), against whatever this
324
- # aggregate has declared SO FAR, the one ordering constraint this
436
+ # command a caller hits. Declare before the commands that
437
+ # reference it — resolution happens at the referencing command's
438
+ # own build time (`CommandBuilder#given`), against whatever this
439
+ # aggregate has declared so far, the one ordering constraint this
325
440
  # word carries that `identified_by`/`attribute` do not.
326
- # BARE — NO BLOCK — REFERENCES a SIBLING AGGREGATE's own
441
+ # Bare — no block — references a sibling aggregate's own
327
442
  # already-declared precondition, one level wider than the
328
443
  # existing bare-command-references-its-own-aggregate shape
329
444
  # (`CommandBuilder#reference_named_given`): `SafeDepositBox`/
@@ -333,40 +448,54 @@ module Hecks
333
448
  # — see `BluebookBuilder#aggregate`'s own comment for how that
334
449
  # pool is threaded, and `docs/implemented/resolution-rules/chapter-given.md`
335
450
  # for the full algorithm and its known limitations (a bare
336
- # reference trusts its own author to have verified the SAME
451
+ # reference trusts its own author to have verified the same
337
452
  # canonical predicate applies — this mechanism does not, and
338
453
  # cannot, check that itself; see that doc for which real corpus
339
454
  # cases do and do not qualify).
340
455
  #
341
- # `declared_by:` DISAMBIGUATES the same description meaning TWO
456
+ # `declared_by:` disambiguates the same description meaning two
342
457
  # genuinely different predicates chapter-wide — real, live:
343
458
  # `Account`'s own "customer is active" reads bare
344
- # `customer.status` (a DIRECT `reference_to Customer`); `ATMCard`'s
459
+ # `customer.status` (a direct `reference_to Customer`); `ATMCard`'s
345
460
  # own (shared onward with `CardPayment`/`ExternalTransfer`/
346
461
  # `ScheduledPayment`/`Statement`) reads `account.customer.status`
347
- # (reached THROUGH `Account`) — the identical business fact, a
348
- # genuinely different runtime path, correctly kept as the SAME
462
+ # (reached through `Account`) — the identical business fact, a
463
+ # genuinely different runtime path, correctly kept as the same
349
464
  # domain wording rather than invented a second spelling for "the
350
465
  # same idea, one more hop away" (S10, ADR 0025's own "one idea,
351
466
  # one spelling"). Omit it when the description is unambiguous
352
- # chapter-wide (the common case, and the ONLY case this took
353
- # before this parameter existed) — required only once a SECOND,
467
+ # chapter-wide (the common case, and the only case this took
468
+ # before this parameter existed) — required only once a second,
354
469
  # textually-different canonical registers under the same
355
470
  # description; see `reference_named_chapter_given`'s own
356
471
  # ambiguity error for how that surfaces.
357
- # RENAMED FROM `given` — item #13's full metaprogrammed dispatch
358
- # (slice 4b), same reasoning as reference_to_impl above:
359
- # bootstrap-reachable, in BOOTSTRAP_CALLS_FALLBACK.
472
+ # Answers the `given` word through the table's `calls:` column —
473
+ # item #13's full metaprogrammed dispatch
474
+ # (slice 4b), same reasoning as `reference_to_impl` above:
475
+ # bootstrap-reachable, in `GenericDispatch::BOOTSTRAP_CALLS_FALLBACK`.
476
+ #
477
+ # @param description [String] the rule's description; also the name a sibling aggregate
478
+ # references it by when no block is given
479
+ # @param declared_by [Module, Symbol, String, nil] disambiguates which aggregate's own
480
+ # rule to reference, a bare constant, when more than one shares `description`; only
481
+ # meaningful with no block
482
+ # @yield the predicate body; evaluated for its extracted source, never called directly
483
+ # @return [void]
484
+ # @raise [Bluebook::DSL::Malformed] if given a block whose source cannot be extracted;
485
+ # given no block, the description is immediately ambiguous between more than one
486
+ # already-loaded aggregate with no `declared_by` to disambiguate; an unresolved
487
+ # reference defers instead, and may still raise once the whole chapter has loaded, if
488
+ # it then resolves to none or more than one candidate
360
489
  def given_impl(description, declared_by: nil, &predicate)
361
490
  return reference_named_chapter_given(description, declared_by: declared_by) unless predicate
362
491
 
363
492
  named = build_rule(Given, description, predicate, owner_name: @name, word: "given",
364
493
  extraction_failure: "its source could not be read, so no other runtime could ever evaluate it")
365
494
  @named_givens[description] = named
366
- # WRITE-THROUGH, first-declared-wins PER OWNER — keyed by
495
+ # Write-through, first-declared-wins per owner — keyed by
367
496
  # [description, this aggregate's own name], not description
368
- # alone: two DIFFERENT aggregates independently declaring the
369
- # SAME description are two DISTINCT candidates a later bare
497
+ # alone: two different aggregates independently declaring the
498
+ # same description are two distinct candidates a later bare
370
499
  # reference chooses between (via `declared_by:` once there is
371
500
  # more than one), never silently merged into one slot the way
372
501
  # a bare description-only key would.
@@ -376,16 +505,16 @@ module Hecks
376
505
 
377
506
  private
378
507
 
379
- # PRIMITIVE 2 (RuleReference#resolve_owner_keyed) — see that
508
+ # Primitive 2 (RuleReference#resolve_owner_keyed) — see that
380
509
  # method's own comment for the pool shape; the three branches
381
510
  # below (exact owner / unambiguous single candidate / ambiguous)
382
- # are this construct's OWN refusal wording, not shared, since
383
- # `declared_by:` only exists here so far. UNRESOLVED (no
511
+ # are this construct's own refusal wording, not shared, since
512
+ # `declared_by:` only exists here so far. Unresolved (no
384
513
  # candidate yet, or `declared_by:` naming an aggregate that
385
514
  # hasn't declared it yet) is no longer a fourth branch that
386
- # raises HERE — see `#pending_chapter_given`, below, for why:
515
+ # raises here — see `#pending_chapter_given`, below, for why:
387
516
  # a chapter split across files can genuinely reference a
388
- # precondition a LATER file declares, and "not found among
517
+ # precondition a later file declares, and "not found among
389
518
  # what's loaded so far" cannot tell that apart from "genuinely
390
519
  # never declared" until every file has.
391
520
  def reference_named_chapter_given(description, declared_by:)
@@ -411,7 +540,7 @@ module Hecks
411
540
  @named_givens[description] = named
412
541
  end
413
542
 
414
- # A CHAPTER MAY BE SPLIT ACROSS FILES — the SAME reason a query
543
+ # A chapter may be split across files — the same reason a query
415
544
  # hop's own cross-file target, a correlation key's own emitting
416
545
  # command, and an event's own declared shape are all resolved
417
546
  # once the whole chapter is assembled rather than refused the
@@ -419,18 +548,18 @@ module Hecks
419
548
  # far (`BluebookBuilder.validate_assembled!`'s own comment).
420
549
  #
421
550
  # Unlike those, though, a chapter-given's resolved value is not
422
- # a pass/fail check on an already-built IR — it IS part of the
551
+ # a pass/fail check on an already-built IR — it is part of the
423
552
  # referencing aggregate's own IR (`preconditions:` below), built
424
- # and handed off the moment THIS aggregate's own file finishes
553
+ # and handed off the moment this aggregate's own file finishes
425
554
  # loading, long before a later file might declare the real
426
- # thing. So this hands back a PLACEHOLDER `Given` — embedded
555
+ # thing. So this hands back a placeholder `Given` — embedded
427
556
  # exactly where the resolved one would be, by Ruby object
428
- # reference, in this aggregate's own `preconditions` AND in any
429
- # command in this SAME aggregate that separately bare-references
557
+ # reference, in this aggregate's own `preconditions` and in any
558
+ # command in this same aggregate that separately bare-references
430
559
  # the same description (`CommandBuilder#given`'s own hash-chain
431
560
  # read of this aggregate's `@named_givens`, the identical key) —
432
561
  # and queues the request in `@chapter_pending_givens`.
433
- # `BluebookBuilder#resolve_pending_chapter_givens!` MUTATES this
562
+ # `BluebookBuilder#resolve_pending_chapter_givens!` mutates this
434
563
  # exact object in place, once every file has loaded, so every
435
564
  # existing reference to it (there is only ever the one object,
436
565
  # never a copy) sees the resolved fields simultaneously. Safe
@@ -448,23 +577,39 @@ module Hecks
448
577
 
449
578
  public
450
579
 
451
- # THE AGGREGATE BOUNDARY IS WHAT AN INVARIANT DEFINES (S10, ADR
452
- # 0025 — "Rules") — checked after every command, before save,
453
- # the same way a value object's already is
580
+ # Declares a rule the whole aggregate must satisfy, checked after every command, before
581
+ # save.
582
+ #
583
+ # The aggregate boundary is what an invariant defines (S10, ADR
584
+ # 0025 — "Rules") — the same check a value object's already gets
454
585
  # (`ValueObjectBuilder#invariant`, whose own shape this mirrors
455
- # exactly). Today `invariant` lived only inside `value_object`;
456
- # an aggregate-level rule had nowhere to live, so "the balance
586
+ # exactly). Without an aggregate-level rule, "the balance
457
587
  # never goes negative" was three different `given`/`ensures`
458
588
  # texts across banking's six balance-moving commands, and the
459
589
  # four that only increase it said nothing at all — completeness
460
590
  # depended on someone noticing which commands could decrease it.
461
- # RENAMED FROM `invariant` — item #13's full metaprogrammed
462
- # dispatch (slice 4b), same reasoning as given_impl above.
591
+ #
592
+ # Answers the `invariant` word through the table's `calls:`
593
+ # column — item #13's full metaprogrammed
594
+ # dispatch (slice 4b), same reasoning as `given_impl` above.
595
+ #
596
+ # @param description [String] the rule's description
597
+ # @yield the predicate body; evaluated for its extracted source, never called directly
598
+ # @return [void]
599
+ # @raise [Bluebook::DSL::Malformed] if the block's source could not be extracted
463
600
  def invariant_impl(description, &predicate)
464
601
  @invariants << build_rule(Invariant, description, predicate, owner_name: @name, word: "invariant",
465
602
  extraction_failure: "it would be a rule the IR cannot carry")
466
603
  end
467
604
 
605
+ # Assembles every declared attribute, construct and rule into an `Aggregate`, after
606
+ # draining pending commands/queries/entities and running every `seal_*` check.
607
+ #
608
+ # @return [Bluebook::Aggregate] the built aggregate
609
+ # @raise [Bluebook::DSL::Malformed] if identity resolution or any `seal_*` check fails —
610
+ # a mutation or query naming an undeclared field, an inconsistent default, a lifecycle
611
+ # guard with no lifecycle, a lifecycle-field mutation outside a transition, a projected
612
+ # field naming an undeclared reference, or a correction targeting an unreferenced field
468
613
  def build
469
614
  drain_pending!
470
615
  resolve_pending_identity!
@@ -493,13 +638,27 @@ module Hecks
493
638
  provenance: @provenance
494
639
  )
495
640
 
496
- # After the IR exists, on purpose : a reference is declared IN the
641
+ # After the IR exists, on purpose : a reference is declared in the
497
642
  # aggregate, and the aggregate the IR graph knows is `ir`, not the
498
643
  # builder.
499
644
  stamp_references(ir)
500
645
  ir
501
646
  end
502
647
 
648
+ # Evaluates an `aggregate` block against a fresh builder and returns the built aggregate.
649
+ #
650
+ # @param name [String] the aggregate's name
651
+ # @param chapter_named_givens [Hash{String => Hash{String => Bluebook::Given}}] the
652
+ # chapter-wide given pool
653
+ # @param chapter_pending_givens [Array<Hash>] unresolved chapter-wide bare given
654
+ # references
655
+ # @param chapter_entity_named_givens [Hash{String => Hash{String => Bluebook::Given}}]
656
+ # the chapter-wide, entity-scoped given pool
657
+ # @param chapter_entity_pending_givens [Array<Hash>] unresolved chapter-wide,
658
+ # entity-scoped bare given references
659
+ # @yield the aggregate body, evaluated with the builder as `self`; may be omitted
660
+ # @return [Bluebook::Aggregate] the built aggregate
661
+ # @raise [Bluebook::DSL::Malformed] if the body fails any check `#build` raises
503
662
  def self.build(name, chapter_named_givens: {}, chapter_pending_givens: [],
504
663
  chapter_entity_named_givens: {}, chapter_entity_pending_givens: [], &block)
505
664
  builder = new(name, chapter_named_givens: chapter_named_givens, chapter_pending_givens: chapter_pending_givens,
@@ -511,14 +670,14 @@ module Hecks
511
670
 
512
671
  private
513
672
 
514
- # DEFERRED CONSTRUCTION — `entity`/`command`/`query` used to build
515
- # immediately, INLINE, the moment their own DSL line ran during
516
- # `instance_eval` — meaning a command's own resolution (`sets
673
+ # Deferred construction — `entity`/`command`/`query` queue a descriptor rather than
674
+ # building immediately, inline, the moment their own DSL line runs during
675
+ # `instance_eval`. Building immediately would mean a command's own resolution (`sets
517
676
  # :field` importing the owner's own attribute, `given("desc")`
518
677
  # referencing an aggregate-level precondition, a query's own
519
678
  # positional-param resolution) only ever saw whatever `@entities`/
520
- # `attributes`/`@named_givens`/`@value_objects` held AS OF THAT
521
- # EXACT TEXTUAL LINE — never what the aggregate's block would go
679
+ # `attributes`/`@named_givens`/`@value_objects` held as of that
680
+ # exact textual line — never what the aggregate's block would go
522
681
  # on to declare after it. Three real, confirmed cases in the
523
682
  # self-hosted meta-domain violate the "declare before you
524
683
  # reference" convention every other resolution rule relies on
@@ -526,26 +685,26 @@ module Hecks
526
685
  # Member/Dispatch — see docs/resolution-rules/
527
686
  # implicit-append-fields.md's own "Known limitations").
528
687
  #
529
- # This is the SAME move `BluebookBuilder` already makes one level
530
- # UP, at the CHAPTER level — build every aggregate first, THEN run
688
+ # This is the same move `BluebookBuilder` already makes one level
689
+ # up, at the chapter level — build every aggregate first, then run
531
690
  # cross-referential validation (`validate_query_hops!`,
532
691
  # `validate_projected_fields!`, `validate_no_bidirectional_
533
692
  # references!`) once `@aggregates` is fully populated — extended
534
- # one level down: `entity`/`command`/`query` now only QUEUE a
693
+ # one level down: `entity`/`command`/`query` now only queue a
535
694
  # descriptor (`@pending_entities`/`@pending_commands`/
536
695
  # `@pending_queries`, each preserving its own declared order),
537
- # and `#build` drains them here, in this exact order, BEFORE any
696
+ # and `#build` drains them here, in this exact order, before any
538
697
  # of the existing `seal_*` validations (which already assume
539
698
  # `@commands`/`@entities`/`@queries` are the real, final, built
540
- # objects) — entities FIRST and fully, since a command's own
699
+ # objects) — entities first and fully, since a command's own
541
700
  # `sets :list, append: {...}` needs a list's element entity
542
701
  # already built (`.attributes` populated) to resolve against, not
543
702
  # just named.
544
703
  #
545
704
  # `attribute`/`value_object`/`identified_by`/`given` (block form)
546
- # are NOT deferred — they still build eagerly during
705
+ # are not deferred — they still build eagerly during
547
706
  # `instance_eval`, unchanged. Nothing reads `@entities`/
548
- # `@commands`/`@queries` from anywhere OTHER than `#build` and its
707
+ # `@commands`/`@queries` from anywhere other than `#build` and its
549
708
  # own private helpers (checked directly), so nothing else in this
550
709
  # file needed to change for this to be safe.
551
710
  def drain_pending!
@@ -583,7 +742,7 @@ module Hecks
583
742
  @value_objects << value_object
584
743
  end
585
744
 
586
- # LEGACY — see `has_many`/`has_one`/`belongs_to`'s own comment;
745
+ # Legacy — see `has_many`/`has_one`/`belongs_to`'s own comment;
587
746
  # byte-identical to what those three did before this slice.
588
747
  def legacy_has_many(type, as:, optional: false)
589
748
  plural = Naming.demodulise(type)