hecks 0.2.0 → 1.0.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 (419) hide show
  1. checksums.yaml +5 -5
  2. data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
  3. data/lib/hecks/adapters/driven/claude_code.rb +127 -0
  4. data/lib/hecks/adapters/driven/d1.adapter +12 -0
  5. data/lib/hecks/adapters/driven/d1.rb +427 -0
  6. data/lib/hecks/adapters/driven/folder.adapter +3 -0
  7. data/lib/hecks/adapters/driven/folder.rb +199 -0
  8. data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
  9. data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
  10. data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
  11. data/lib/hecks/adapters/driven/governance_authorization.rb +101 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +56 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +99 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +65 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +179 -0
  17. data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
  18. data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
  19. data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
  20. data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
  21. data/lib/hecks/adapters/driven/lambda.adapter +4 -0
  22. data/lib/hecks/adapters/driven/lambda.rb +145 -0
  23. data/lib/hecks/adapters/driven/memory.adapter +3 -0
  24. data/lib/hecks/adapters/driven/memory.rb +103 -0
  25. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
  26. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
  27. data/lib/hecks/adapters/driven/postgres/codec.rb +88 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +207 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +438 -0
  31. data/lib/hecks/adapters/driven/postgres_era.adapter +17 -0
  32. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  33. data/lib/hecks/adapters/driven/prism.rb +80 -0
  34. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  35. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  36. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  37. data/lib/hecks/adapters/driven/sqlite/codec.rb +80 -0
  38. data/lib/hecks/adapters/driven/sqlite/projection.rb +172 -0
  39. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +192 -0
  40. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  41. data/lib/hecks/adapters/driven/sqlite.rb +306 -0
  42. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  43. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  44. data/lib/hecks/adapters/driven.rb +54 -0
  45. data/lib/hecks/adapters.rb +6 -0
  46. data/lib/hecks/behaviors/dsl.rb +125 -0
  47. data/lib/hecks/behaviors/expectations.rb +340 -0
  48. data/lib/hecks/behaviors/ir.rb +31 -0
  49. data/lib/hecks/behaviors/rspec.rb +42 -0
  50. data/lib/hecks/behaviors/runner.rb +96 -0
  51. data/lib/hecks/behaviors.rb +25 -0
  52. data/lib/hecks/bluebook/aggregate.rb +108 -0
  53. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  54. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  55. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  56. data/lib/hecks/bluebook/assembly/contracts.rb +438 -0
  57. data/lib/hecks/bluebook/assembly/marks.rb +228 -0
  58. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  59. data/lib/hecks/bluebook/assembly.rb +91 -0
  60. data/lib/hecks/bluebook/attribute.rb +96 -0
  61. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  62. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  63. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  64. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  65. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  66. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  67. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  68. data/lib/hecks/bluebook/behaviour/lifecycle.rb +68 -0
  69. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  70. data/lib/hecks/bluebook/behaviour/process_manager.rb +51 -0
  71. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  72. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  73. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  74. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  75. data/lib/hecks/bluebook/chapter.rb +78 -0
  76. data/lib/hecks/bluebook/command.rb +124 -0
  77. data/lib/hecks/bluebook/domain_port.rb +102 -0
  78. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  79. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +1018 -0
  80. data/lib/hecks/bluebook/dsl/attribute_collector.rb +348 -0
  81. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  82. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1087 -0
  83. data/lib/hecks/bluebook/dsl/command_builder.rb +714 -0
  84. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  85. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  86. data/lib/hecks/bluebook/dsl/entity_builder.rb +430 -0
  87. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  88. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  89. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  90. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  91. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  92. data/lib/hecks/bluebook/dsl/policy_builder.rb +135 -0
  93. data/lib/hecks/bluebook/dsl/port_builder.rb +39 -0
  94. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  95. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +306 -0
  96. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  97. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  98. data/lib/hecks/bluebook/dsl/rule_reference.rb +173 -0
  99. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  100. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  101. data/lib/hecks/bluebook/dsl/word_gate.rb +228 -0
  102. data/lib/hecks/bluebook/dsl/world_builder.rb +117 -0
  103. data/lib/hecks/bluebook/dsl.rb +45 -0
  104. data/lib/hecks/bluebook/entity.rb +103 -0
  105. data/lib/hecks/bluebook/expression/canonical_form.rb +123 -0
  106. data/lib/hecks/bluebook/expression/evaluator.rb +304 -0
  107. data/lib/hecks/bluebook/expression/projection.json +218 -0
  108. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  109. data/lib/hecks/bluebook/expression/resolver.rb +781 -0
  110. data/lib/hecks/bluebook/expression.rb +13 -0
  111. data/lib/hecks/bluebook/hexagon.rb +60 -0
  112. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  113. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  114. data/lib/hecks/bluebook/meta_validator/judge.rb +621 -0
  115. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  116. data/lib/hecks/bluebook/meta_validator/port_judge.rb +51 -0
  117. data/lib/hecks/bluebook/meta_validator/readings.rb +360 -0
  118. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +351 -0
  119. data/lib/hecks/bluebook/meta_validator/shapes.rb +266 -0
  120. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  121. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  122. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  123. data/lib/hecks/bluebook/meta_validator.rb +542 -0
  124. data/lib/hecks/bluebook/model_check.rb +445 -0
  125. data/lib/hecks/bluebook/pattern_subset.rb +184 -0
  126. data/lib/hecks/bluebook/policy.rb +41 -0
  127. data/lib/hecks/bluebook/process_manager.rb +128 -0
  128. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  129. data/lib/hecks/bluebook/project_loader.rb +40 -0
  130. data/lib/hecks/bluebook/project_register.rb +82 -0
  131. data/lib/hecks/bluebook/query.rb +61 -0
  132. data/lib/hecks/bluebook/read_model.rb +109 -0
  133. data/lib/hecks/bluebook/reference.rb +74 -0
  134. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  135. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  136. data/lib/hecks/bluebook/translation.rb +92 -0
  137. data/lib/hecks/bluebook/value_object.rb +66 -0
  138. data/lib/hecks/bluebook.rb +74 -0
  139. data/lib/hecks/codemod.rb +342 -0
  140. data/lib/hecks/construct.rb +71 -0
  141. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  142. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  143. data/lib/hecks/deploy/oidc.json +18 -0
  144. data/lib/hecks/doc/reference.rb +410 -0
  145. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  146. data/lib/hecks/facade/cli_door.rb +119 -0
  147. data/lib/hecks/facade/cli_runner.rb +190 -0
  148. data/lib/hecks/facade/command_request.rb +105 -0
  149. data/lib/hecks/facade/handle.rb +173 -0
  150. data/lib/hecks/facade/json_door.rb +166 -0
  151. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  152. data/lib/hecks/facade/surface/chapter.rb +107 -0
  153. data/lib/hecks/facade/surface.rb +48 -0
  154. data/lib/hecks/facade.rb +44 -0
  155. data/lib/hecks/forms/app.rb +341 -0
  156. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  157. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  158. data/lib/hecks/forms/field_renderer.rb +177 -0
  159. data/lib/hecks/forms/field_shape.rb +232 -0
  160. data/lib/hecks/forms/html.rb +84 -0
  161. data/lib/hecks/forms/index_renderer.rb +35 -0
  162. data/lib/hecks/forms/page.rb +157 -0
  163. data/lib/hecks/forms/params.rb +161 -0
  164. data/lib/hecks/forms/port_argument.rb +46 -0
  165. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  166. data/lib/hecks/forms/record_renderer.rb +119 -0
  167. data/lib/hecks/forms/record_table.rb +68 -0
  168. data/lib/hecks/forms/reference_options.rb +30 -0
  169. data/lib/hecks/forms/value_object_shape.rb +46 -0
  170. data/lib/hecks/forms.rb +54 -0
  171. data/lib/hecks/fqn.rb +94 -0
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  175. data/lib/hecks/framework/bluebook/governance.bluebook +145 -0
  176. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  177. data/lib/hecks/framework/oidc.json +39 -0
  178. data/lib/hecks/framework.rb +90 -0
  179. data/lib/hecks/freezer.rb +67 -0
  180. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
  181. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  182. data/lib/hecks/fuzzing/isolated_boot.rb +286 -0
  183. data/lib/hecks/fuzzing/properties.rb +1192 -0
  184. data/lib/hecks/fuzzing/replay.rb +668 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +80 -0
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +250 -0
  189. data/lib/hecks/fuzzing/sequence_generator.rb +131 -0
  190. data/lib/hecks/fuzzing/value_generator.rb +200 -0
  191. data/lib/hecks/fuzzing.rb +12 -0
  192. data/lib/hecks/grammar/evolve.rb +324 -0
  193. data/lib/hecks/grammar/expression.bluebook +420 -0
  194. data/lib/hecks/grammar/expression_operators.json +1648 -0
  195. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  196. data/lib/hecks/grammar/oidc.json +69 -0
  197. data/lib/hecks/grammar/translation.bluebook +203 -0
  198. data/lib/hecks/grammar.rb +178 -0
  199. data/lib/hecks/ir.rb +126 -0
  200. data/lib/hecks/language/adapter.bluebook +116 -0
  201. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  202. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  203. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  204. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  205. data/lib/hecks/language/bluebook/command.bluebook +471 -0
  206. data/lib/hecks/language/bluebook/entity.bluebook +392 -0
  207. data/lib/hecks/language/bluebook/policy.bluebook +189 -0
  208. data/lib/hecks/language/bluebook/process_manager.bluebook +381 -0
  209. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  210. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  211. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  212. data/lib/hecks/language/bluebook/syntax.bluebook +448 -0
  213. data/lib/hecks/language/bluebook/vocabulary.bluebook +379 -0
  214. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  215. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  216. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  217. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  218. data/lib/hecks/language/oidc.json +333 -0
  219. data/lib/hecks/language/port.bluebook +120 -0
  220. data/lib/hecks/language/translation/translation.bluebook +110 -0
  221. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  222. data/lib/hecks/language/world/wiring.bluebook +62 -0
  223. data/lib/hecks/language/world/world.bluebook +84 -0
  224. data/lib/hecks/literal.rb +125 -0
  225. data/lib/hecks/naming.rb +174 -0
  226. data/lib/hecks/ports/access_control.port +9 -0
  227. data/lib/hecks/ports/access_control.rb +62 -0
  228. data/lib/hecks/ports/agent/answers.rb +104 -0
  229. data/lib/hecks/ports/agent.port +8 -0
  230. data/lib/hecks/ports/agent.rb +167 -0
  231. data/lib/hecks/ports/authentication.port +6 -0
  232. data/lib/hecks/ports/authentication.rb +50 -0
  233. data/lib/hecks/ports/authorization.port +7 -0
  234. data/lib/hecks/ports/authorization.rb +62 -0
  235. data/lib/hecks/ports/clock.port +5 -0
  236. data/lib/hecks/ports/clock.rb +62 -0
  237. data/lib/hecks/ports/extraction.port +5 -0
  238. data/lib/hecks/ports/extraction.rb +37 -0
  239. data/lib/hecks/ports/identity_assignment.port +5 -0
  240. data/lib/hecks/ports/identity_assignment.rb +45 -0
  241. data/lib/hecks/ports/identity_generation.port +5 -0
  242. data/lib/hecks/ports/identity_generation.rb +49 -0
  243. data/lib/hecks/ports/identity_resolution.port +5 -0
  244. data/lib/hecks/ports/identity_resolution.rb +40 -0
  245. data/lib/hecks/ports/loading.port +4 -0
  246. data/lib/hecks/ports/loading.rb +13 -0
  247. data/lib/hecks/ports/persistence/append_only.rb +138 -0
  248. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  249. data/lib/hecks/ports/persistence/execution.rb +23 -0
  250. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  251. data/lib/hecks/ports/persistence/plugin.rb +54 -0
  252. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +169 -0
  253. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +128 -0
  254. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +161 -0
  255. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +61 -0
  256. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +304 -0
  257. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +171 -0
  258. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +190 -0
  259. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +478 -0
  260. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +166 -0
  261. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +314 -0
  262. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +168 -0
  263. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +170 -0
  264. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +134 -0
  265. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +137 -0
  266. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +89 -0
  267. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +85 -0
  268. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  269. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +132 -0
  270. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +74 -0
  271. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +758 -0
  272. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +120 -0
  273. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +31 -0
  274. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +46 -0
  275. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +102 -0
  276. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +46 -0
  277. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +70 -0
  278. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +72 -0
  279. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +120 -0
  280. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +183 -0
  281. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +41 -0
  282. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +28 -0
  283. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +30 -0
  284. data/lib/hecks/ports/persistence/plugins/era/translation.rb +11 -0
  285. data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
  286. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  287. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  288. data/lib/hecks/ports/persistence.port +4 -0
  289. data/lib/hecks/ports/persistence.rb +39 -0
  290. data/lib/hecks/ports/projection.port +4 -0
  291. data/lib/hecks/ports/projection.rb +96 -0
  292. data/lib/hecks/ports/query/in_memory.rb +60 -0
  293. data/lib/hecks/ports/query/ordering.rb +41 -0
  294. data/lib/hecks/ports/query.rb +36 -0
  295. data/lib/hecks/ports.rb +27 -0
  296. data/lib/hecks/projections/diagrams.rb +642 -0
  297. data/lib/hecks/projections/ir.rb +18 -0
  298. data/lib/hecks/projections/model/deviations.rb +98 -0
  299. data/lib/hecks/projections/model.rb +145 -0
  300. data/lib/hecks/projections/oidc.rb +110 -0
  301. data/lib/hecks/projections/parser_table.rb +159 -0
  302. data/lib/hecks/projections/reference.rb +38 -0
  303. data/lib/hecks/projections/shape.rb +44 -0
  304. data/lib/hecks/projections/statements.rb +110 -0
  305. data/lib/hecks/projections/vocabulary.rb +100 -0
  306. data/lib/hecks/projections.rb +32 -0
  307. data/lib/hecks/projector/cli_projector.rb +446 -0
  308. data/lib/hecks/projector/docs_projector.rb +321 -0
  309. data/lib/hecks/projector/exporter.rb +158 -0
  310. data/lib/hecks/projector/ir_projector.rb +18 -0
  311. data/lib/hecks/projector/narrate_projector.rb +243 -0
  312. data/lib/hecks/projector/target.rb +97 -0
  313. data/lib/hecks/projector.rb +186 -0
  314. data/lib/hecks/query_ir.rb +411 -0
  315. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  316. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  317. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  318. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  319. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  320. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  321. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  322. data/lib/hecks/query_specification/common/null_policy.rb +92 -0
  323. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  324. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  325. data/lib/hecks/query_specification/common/options.rb +32 -0
  326. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  327. data/lib/hecks/query_specification/common/specification.rb +10 -0
  328. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  329. data/lib/hecks/query_specification/field_path.rb +107 -0
  330. data/lib/hecks/query_specification/hop_path.rb +132 -0
  331. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  332. data/lib/hecks/query_specification.rb +14 -0
  333. data/lib/hecks/rendering.rb +48 -0
  334. data/lib/hecks/router/namespace_installer.rb +157 -0
  335. data/lib/hecks/router.rb +70 -0
  336. data/lib/hecks/runtime/aggregate_lock.rb +45 -0
  337. data/lib/hecks/runtime/boot_gates.rb +41 -0
  338. data/lib/hecks/runtime/caller.rb +66 -0
  339. data/lib/hecks/runtime/capability_graph.rb +44 -0
  340. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +135 -0
  341. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +260 -0
  342. data/lib/hecks/runtime/command_interpreter.rb +497 -0
  343. data/lib/hecks/runtime/command_rules/admissibility.rb +357 -0
  344. data/lib/hecks/runtime/command_rules/arithmetic.rb +260 -0
  345. data/lib/hecks/runtime/command_rules/authorization.rb +65 -0
  346. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  347. data/lib/hecks/runtime/command_rules/references.rb +189 -0
  348. data/lib/hecks/runtime/command_rules.rb +28 -0
  349. data/lib/hecks/runtime/dependency_planning.rb +245 -0
  350. data/lib/hecks/runtime/dispatcher.rb +301 -0
  351. data/lib/hecks/runtime/entity_element.rb +253 -0
  352. data/lib/hecks/runtime/entity_interpreter.rb +299 -0
  353. data/lib/hecks/runtime/errors.rb +122 -0
  354. data/lib/hecks/runtime/event.rb +51 -0
  355. data/lib/hecks/runtime/identity.rb +156 -0
  356. data/lib/hecks/runtime/instance.rb +175 -0
  357. data/lib/hecks/runtime/interpreting.rb +90 -0
  358. data/lib/hecks/runtime/loader.rb +184 -0
  359. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  360. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  361. data/lib/hecks/runtime/query_interpreter.rb +286 -0
  362. data/lib/hecks/runtime/reaction_invocation.rb +253 -0
  363. data/lib/hecks/runtime/read_model_interpreter.rb +341 -0
  364. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  365. data/lib/hecks/runtime/reference_hop.rb +99 -0
  366. data/lib/hecks/runtime/refusal_wording.rb +126 -0
  367. data/lib/hecks/runtime/registry/saga_persistence.rb +142 -0
  368. data/lib/hecks/runtime/registry/verification.rb +246 -0
  369. data/lib/hecks/runtime/registry.rb +279 -0
  370. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  371. data/lib/hecks/runtime/routing.rb +96 -0
  372. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  373. data/lib/hecks/runtime/saga_interpreter.rb +503 -0
  374. data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
  375. data/lib/hecks/runtime/tenant_check.rb +84 -0
  376. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  377. data/lib/hecks/runtime/value/admission.rb +130 -0
  378. data/lib/hecks/runtime/value/coercion.rb +565 -0
  379. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  380. data/lib/hecks/runtime/value.rb +125 -0
  381. data/lib/hecks/runtime.rb +107 -0
  382. data/lib/hecks/storehouse.rb +632 -0
  383. data/lib/hecks/version.rb +16 -0
  384. data/lib/hecks/vocabulary.rb +218 -0
  385. data/lib/hecks.rb +123 -6
  386. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
  387. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
  388. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
  389. metadata +412 -222
  390. data/bin/hecks +0 -7
  391. data/bin/hecks-package +0 -65
  392. data/bin/hecks_console +0 -12
  393. data/bin/hecks_serverless +0 -6
  394. data/lib/cli/build.rb +0 -14
  395. data/lib/cli/command_runner.rb +0 -28
  396. data/lib/cli/console.rb +0 -10
  397. data/lib/cli/generate.rb +0 -37
  398. data/lib/cli/hecks-cli.rb +0 -27
  399. data/lib/cli/test.rb +0 -57
  400. data/lib/console/commands.rb +0 -8
  401. data/lib/console/hecks-console.rb +0 -1
  402. data/lib/packager/README.md +0 -0
  403. data/lib/packager/app_runner.rb +0 -21
  404. data/lib/packager/args.rb +0 -26
  405. data/lib/packager/compatibility/fixnum.rb +0 -6
  406. data/lib/packager/hecks.rb +0 -39
  407. data/lib/packager/query_runner.rb +0 -21
  408. data/lib/packager/resources/Dockerfile +0 -11
  409. data/lib/packager/resources/app_binary +0 -7
  410. data/lib/packager/resources/bundle_config +0 -3
  411. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  412. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  413. data/lib/serverless/Domain +0 -32
  414. data/lib/serverless/cli.rb +0 -75
  415. data/lib/serverless/resources/command_name.js +0 -5
  416. data/lib/serverless/resources/environment.js +0 -7
  417. data/lib/serverless/resources/handler.js.tt +0 -28
  418. data/lib/serverless/resources/run_binary.js +0 -22
  419. data/lib/serverless/resources/serverless.yml +0 -20
@@ -0,0 +1,1018 @@
1
+ require_relative "word_gate"
2
+ module Hecks
3
+ module Bluebook
4
+ module DSL
5
+ class AggregateBuilder
6
+ GRAMMAR_CONTEXT = "Aggregate"
7
+
8
+ include AttributeCollector
9
+ include IdentityDeclaration
10
+ include RuleReference
11
+ include WordGate
12
+
13
+ def initialize(name, chapter_named_givens: {}, chapter_pending_givens: [],
14
+ chapter_entity_named_givens: {}, chapter_entity_pending_givens: [])
15
+ @name = name
16
+ @value_objects = []
17
+ @commands = []
18
+ @invariants = []
19
+ @named_givens = {}
20
+ @projected_fields = []
21
+ @identity_paths = []
22
+ @entities = []
23
+ @queries = []
24
+ @policies = []
25
+ @reference_targets = []
26
+ # THE ROOT of the cross-entity given pool — see `#entity`'s own
27
+ # comment. ONE hash for the whole aggregate, threaded unchanged
28
+ # into every piece nested under it, however deep.
29
+ @entity_named_givens = {}
30
+ # ONE LEVEL WIDER STILL — the CHAPTER's own pool, threaded in
31
+ # from `BluebookBuilder#aggregate`, shared with every OTHER
32
+ # aggregate the same chapter builds. See `#given`'s own
33
+ # comment for what this closes.
34
+ @chapter_named_givens = chapter_named_givens
35
+ # A CHAPTER MAY BE SPLIT ACROSS FILES — threaded in the SAME
36
+ # way as `@chapter_named_givens`, one Array shared chapter-wide.
37
+ # See `#pending_chapter_given`'s own comment for what queues
38
+ # here and `BluebookBuilder#resolve_pending_chapter_givens!`
39
+ # for where it drains.
40
+ @chapter_pending_givens = chapter_pending_givens
41
+ # ONE LEVEL WIDER STILL, PAST THE CHAPTER'S OWN AGGREGATE-LEVEL
42
+ # POOL — the chapter's own entity-scoped pool, threaded from
43
+ # `BluebookBuilder#aggregate_impl` the same way
44
+ # `@chapter_named_givens` is, and passed straight through
45
+ # (unchanged) to every top-level piece this aggregate builds
46
+ # (`#drain_pending!`). See `EntityBuilder#given_impl`'s own
47
+ # comment for what this closes.
48
+ @chapter_entity_named_givens = chapter_entity_named_givens
49
+ @chapter_entity_pending_givens = chapter_entity_pending_givens
50
+ # DEFERRED CONSTRUCTION — `entity`/`command`/`query` push a
51
+ # pending descriptor here instead of building immediately; see
52
+ # `#drain_pending!`'s own comment for why.
53
+ @pending_entities = []
54
+ @pending_commands = []
55
+ @pending_queries = []
56
+ end
57
+
58
+ def description(value)
59
+ # moved to the language: Description invariant, on Root.Declare
60
+
61
+ @description = value
62
+ end
63
+
64
+ # ORIGIN, not runtime identity — a concept adopted from a canonical
65
+ # source (§28) names where it came from without that fact ever
66
+ # touching `hecks_fqn`/dispatch. Captured raw, the same way
67
+ # `attribute ..., default: { value: "small" }` captures a literal
68
+ # Hash untouched — no re-parsing, no structure imposed beyond
69
+ # "whatever the author wrote."
70
+ # RENAMED FROM `provenance`/`projects`/`lifecycle`/`entity`/
71
+ # `query`/`policy`/`command` (all below) — item #13's full
72
+ # metaprogrammed dispatch (slice 4c). All bootstrap-reachable
73
+ # (used throughout the core/attached chapters), all in
74
+ # GenericDispatch::BOOTSTRAP_CALLS_FALLBACK.
75
+ def provenance_impl(from:)
76
+ @provenance = from
77
+ end
78
+
79
+ # `optional:` — matching `CommandBuilder#reference_to`'s own
80
+ # signature, which already had it; this one never forwarded it
81
+ # to `attribute_impl()` even though `attribute_impl()` itself
82
+ # already accepts it. A real gap: an aggregate that can point at
83
+ # ONE OF several targets (Item's own `personal_list_id`/
84
+ # `camping_list_id`, never both) needs each reference optional
85
+ # on the aggregate's own persisted schema, not just as a
86
+ # command's input.
87
+ # RENAMED FROM `reference_to` — item #13's full metaprogrammed
88
+ # dispatch (slice 4b). Bootstrap-reachable (every core/attached
89
+ # grammar chapter uses reference_to to describe itself), so also
90
+ # named in GenericDispatch::BOOTSTRAP_CALLS_FALLBACK.
91
+ def reference_to_impl(type, as: nil, optional: false)
92
+ target = Naming.demodulise(type)
93
+ @reference_targets << target
94
+ relationship_attribute(target, :reference_to,
95
+ as || default_reference_name(target), optional: optional)
96
+ end
97
+
98
+ # A RULE MAY ONLY READ WITHIN ITS OWN AGGREGATE BOUNDARY (S12,
99
+ # ADR 0025 — "Consistency across aggregate boundaries"). A
100
+ # `given`/`ensures`/`invariant` used to reach through a
101
+ # `reference_to` at RULE-EVALUATION TIME (`References#
102
+ # dereference`, a live query against another aggregate's own
103
+ # repository, unbounded and inconsistent with the "a rule reads
104
+ # only this record" model everywhere else) — `projects` is what
105
+ # replaces that: `projects :customer_status, from: :"customer.
106
+ # status"` declares that THIS aggregate holds its own copy of
107
+ # `Customer`'s own `:status`, kept fresh by a REBUILD SWEEP
108
+ # (`Runtime::ProjectionRebuild`) rather than read live. A rule
109
+ # then reads `customer_status` the same way it reads any other
110
+ # local field — no dot, no reference walk.
111
+ #
112
+ # `from:` NAMES THE LOCAL REFERENCE, not the target aggregate —
113
+ # `customer`, the attribute THIS aggregate's own `reference_to
114
+ # Customer` already minted, not `Customer` the type — so two
115
+ # references to the same aggregate (aliased differently) can
116
+ # each carry their own projection without ambiguity. The TARGET
117
+ # field's own existence cannot be checked here: the target
118
+ # aggregate does not exist yet while THIS one is still being
119
+ # declared (the same reason a query's own hop tail is checked
120
+ # by `BluebookBuilder#validate_query_hops!`, once every
121
+ # aggregate in the chapter is real, not by `AggregateBuilder`
122
+ # itself) — `validate_projected_fields!` is where that half
123
+ # happens.
124
+ def projects_impl(name, from:)
125
+ reference, _, remote_field = from.to_s.rpartition(".")
126
+
127
+ if reference.empty? || remote_field.empty?
128
+ raise Malformed,
129
+ "#{@name}.projects :#{name} names #{from.inspect}, which is not " \
130
+ "reference.field — say which reference and which field on it, e.g. " \
131
+ "from: :\"customer.status\""
132
+ end
133
+
134
+ @projected_fields << ProjectedField.new(name: name.to_sym, reference: reference.to_sym,
135
+ remote_field: remote_field.to_sym)
136
+ end
137
+
138
+ # `has_many`/`has_one`/`belongs_to` were LEGACY (ADR 0025,
139
+ # "References") — sugar over `reference_to` that collapsed to an
140
+ # anonymous reference and, for `has_many`, LIED (singularised its
141
+ # target and minted one scalar, so `film.backers` read `nil` and
142
+ # never `[]`). Wave 6 (identity-and-relationships arc) un-deprecates
143
+ # all three for real: a relationship word now retains the author's
144
+ # domain concept in IR — still stored as one or more target
145
+ # identities, but no longer collapsed to a bare `reference_to`
146
+ # during assembly. `MetaValidator.shadow_parsing?` still routes to
147
+ # `legacy_has_many`/`legacy_has_one` so frozen era text written
148
+ # under the OLD (lying/collapsing) meaning still parses the way it
149
+ # did when it was written — real, if rare corpus: "Combined corpus
150
+ # uses: one."
151
+ #
152
+ # RENAMED FROM `has_many`/`has_one`/`belongs_to` — item #13's full
153
+ # metaprogrammed dispatch (slice 4). Each Keyword row's own
154
+ # `calls:` names the matching `_impl`; not bootstrap-reachable
155
+ # (no core/attached chapter uses one of these to describe itself),
156
+ # so no BOOTSTRAP_CALLS_FALLBACK entry is needed, unlike
157
+ # `attribute`/`role`.
158
+ def has_many_impl(type, as: nil, **legacy_options)
159
+ if MetaValidator.shadow_parsing?
160
+ return legacy_has_many(type, as: as, optional: legacy_options.fetch(:optional, false))
161
+ end
162
+
163
+ unless legacy_options.empty?
164
+ raise Malformed, "#{@name}.has_many takes no #{legacy_options.keys.first}: — an empty list already means none"
165
+ end
166
+
167
+ plural = Naming.demodulise(type)
168
+ target = Naming.singularize(plural)
169
+ @reference_targets << target
170
+ relationship_attribute(target, :has_many, as || Naming.snake(plural).to_sym,
171
+ list: true)
172
+ end
173
+
174
+ def has_one_impl(type, as: nil, optional: false)
175
+ return legacy_has_one(type, as: as, optional: optional) if MetaValidator.shadow_parsing?
176
+
177
+ target = Naming.demodulise(type)
178
+ @reference_targets << target
179
+ relationship_attribute(target, :has_one, as || Naming.snake(target).to_sym,
180
+ optional: optional)
181
+ end
182
+
183
+ def belongs_to_impl(type, as: nil, optional: false)
184
+ return legacy_has_one(type, as: as, optional: optional) if MetaValidator.shadow_parsing?
185
+
186
+ target = Naming.demodulise(type)
187
+ @reference_targets << target
188
+ relationship_attribute(target, :belongs_to, as || Naming.snake(target).to_sym,
189
+ optional: optional)
190
+ end
191
+
192
+ def lifecycle_impl(field, default:, &block)
193
+ @lifecycle = LifecycleBuilder.build(field, default: default, &block)
194
+ end
195
+
196
+ # A piece is declared IN this aggregate — its owner is stamped by
197
+ # `Aggregate#initialize`, once the aggregate exists. Its own
198
+ # commands were given the piece as their owner when it was declared,
199
+ # so the chain closes as chapter -> aggregate -> entity -> command.
200
+ # NOT built here — see `#drain_pending!`'s own comment for why
201
+ # this only queues a descriptor.
202
+ #
203
+ # A PRECONDITION SHARED ACROSS SIBLING PIECES, DECLARED ONCE — one
204
+ # level wider than round 4's own `EntityBuilder#given` (shared
205
+ # across ONE piece's own commands): `@entity_named_givens` is the
206
+ # SAME hash threaded into EVERY piece this aggregate builds, so a
207
+ # piece's own entity-level `given(desc) { block }` write-throughs
208
+ # into it, and any OTHER piece's own command can reference it back
209
+ # bare, the identical description/canonical, evaluated in ITS OWN
210
+ # `parent`-relative context. Real, live corpus this closes:
211
+ # `SafeDepositBox`'s `Visit`/`KeyIssuance` — two DIFFERENT pieces
212
+ # under one head, each independently typing `given("customer is
213
+ # active") { parent.customer.status == "active" }` byte for byte,
214
+ # which neither the aggregate's OWN "customer is active" (a
215
+ # DIFFERENT canonical — bare `customer.status`, not
216
+ # `parent.customer.status`, wrong scope for a piece's own command
217
+ # to evaluate) nor round 4's single-piece `given` could reach.
218
+ def entity_impl(name, &block)
219
+ @pending_entities << [name, block]
220
+ end
221
+
222
+ def query_impl(name, &block)
223
+ @pending_queries << [name, block]
224
+ end
225
+
226
+ def policy_impl(name, &block)
227
+ reaction = PolicyBuilder.build(name, &block)
228
+ reaction.aggregate = @name
229
+ @policies << reaction
230
+ end
231
+
232
+ # `builder.closed_sets` TOO, not only `builder.build` — a REAL,
233
+ # previously-unreachable gap this exact fix exposed: a
234
+ # value_object's own INLINE `attribute :x, one_of(...)` (now legal
235
+ # — S3, ADR 0025 removed the wrong-arity collision that used to
236
+ # make this crash before it could ever matter) synthesises its own
237
+ # anonymous value object via the SAME `AttributeCollector#closed_
238
+ # sets` mechanism an aggregate's own attributes already use — and
239
+ # nothing installed it anywhere. `Box.attributes` said `size:
240
+ # "Size"` while no "Size" value object existed in the whole
241
+ # domain: a dangling type name, not a working closed set. Flattened
242
+ # into THIS aggregate's own `@value_objects`, the identical move
243
+ # `@value_objects + closed_sets` already makes for the aggregate's
244
+ # own direct attributes (see this file's other 5 call sites).
245
+ def value_object(name, &block)
246
+ builder = ValueObjectBuilder.new(name, owner_value_objects: @value_objects + closed_sets)
247
+ builder.instance_eval(&block) if block
248
+ @value_objects << builder.build
249
+ @value_objects.concat(builder.closed_sets)
250
+ end
251
+
252
+ # `from:` — LIFECYCLE STATE BECOMES A COMMAND GUARD (S10, ADR
253
+ # 0025) — `command "Debit", from: "open"` replaces `given
254
+ # ("account is open") { status == "open" }`, written 35 times
255
+ # in two wordings across the corpus. Checked against THIS
256
+ # aggregate's own lifecycle field (`Admissibility#enforce_
257
+ # lifecycle_guard`) — never a target state, never a transition:
258
+ # the lifecycle already declares which states exist, so naming
259
+ # the legal ones is checkable against it, where a free-text
260
+ # given could drift out of sync with the state machine and did.
261
+ def command_impl(name, from: nil, &block)
262
+ # The verb is declared ON this aggregate — the owner `acts_on` answers
263
+ # with — stamped by `Aggregate#initialize` once the aggregate
264
+ # exists. An ENTITY's commands take the entity as their owner instead,
265
+ # at the entity's own declaration. NOT built here — see
266
+ # `#drain_pending!`'s own comment for why this only queues a
267
+ # descriptor.
268
+ @pending_commands << [name, from, block]
269
+ end
270
+
271
+ # A PRECONDITION SHARED ACROSS COMMANDS, DECLARED ONCE (S10, ADR
272
+ # 0025) — an aggregate-level `given`, block required, stored by
273
+ # its own description rather than appended anywhere: a command
274
+ # names it back (`given("customer is active")`, no block of its
275
+ # own) rather than re-typing the predicate, so there is one
276
+ # description and therefore one refusal message no matter which
277
+ # command a caller hits. DECLARE BEFORE THE COMMANDS THAT
278
+ # REFERENCE IT — resolution happens at the referencing command's
279
+ # OWN build time (`CommandBuilder#given`), against whatever this
280
+ # aggregate has declared SO FAR, the one ordering constraint this
281
+ # word carries that `identified_by`/`attribute` do not.
282
+ # BARE — NO BLOCK — REFERENCES a SIBLING AGGREGATE's own
283
+ # already-declared precondition, one level wider than the
284
+ # existing bare-command-references-its-own-aggregate shape
285
+ # (`CommandBuilder#reference_named_given`): `SafeDepositBox`/
286
+ # `OnboardingCase` both name back `Account`'s own "customer is
287
+ # active" rather than retyping `customer.status == "active"` a
288
+ # third and fourth time. Resolved against `@chapter_named_givens`
289
+ # — see `BluebookBuilder#aggregate`'s own comment for how that
290
+ # pool is threaded, and `docs/implemented/resolution-rules/chapter-given.md`
291
+ # for the full algorithm and its known limitations (a bare
292
+ # reference trusts its own author to have verified the SAME
293
+ # canonical predicate applies — this mechanism does not, and
294
+ # cannot, check that itself; see that doc for which real corpus
295
+ # cases do and do not qualify).
296
+ #
297
+ # `declared_by:` DISAMBIGUATES the same description meaning TWO
298
+ # genuinely different predicates chapter-wide — real, live:
299
+ # `Account`'s own "customer is active" reads bare
300
+ # `customer.status` (a DIRECT `reference_to Customer`); `ATMCard`'s
301
+ # own (shared onward with `CardPayment`/`ExternalTransfer`/
302
+ # `ScheduledPayment`/`Statement`) reads `account.customer.status`
303
+ # (reached THROUGH `Account`) — the identical business fact, a
304
+ # genuinely different runtime path, correctly kept as the SAME
305
+ # domain wording rather than invented a second spelling for "the
306
+ # same idea, one more hop away" (S10, ADR 0025's own "one idea,
307
+ # one spelling"). Omit it when the description is unambiguous
308
+ # chapter-wide (the common case, and the ONLY case this took
309
+ # before this parameter existed) — required only once a SECOND,
310
+ # textually-different canonical registers under the same
311
+ # description; see `reference_named_chapter_given`'s own
312
+ # ambiguity error for how that surfaces.
313
+ # RENAMED FROM `given` — item #13's full metaprogrammed dispatch
314
+ # (slice 4b), same reasoning as reference_to_impl above:
315
+ # bootstrap-reachable, in BOOTSTRAP_CALLS_FALLBACK.
316
+ def given_impl(description, declared_by: nil, &predicate)
317
+ return reference_named_chapter_given(description, declared_by: declared_by) unless predicate
318
+
319
+ named = build_rule(Given, description, predicate, owner_name: @name, word: "given",
320
+ extraction_failure: "its source could not be read, so no other runtime could ever evaluate it")
321
+ @named_givens[description] = named
322
+ # WRITE-THROUGH, first-declared-wins PER OWNER — keyed by
323
+ # [description, this aggregate's own name], not description
324
+ # alone: two DIFFERENT aggregates independently declaring the
325
+ # SAME description are two DISTINCT candidates a later bare
326
+ # reference chooses between (via `declared_by:` once there is
327
+ # more than one), never silently merged into one slot the way
328
+ # a bare description-only key would.
329
+ @chapter_named_givens[description] ||= {}
330
+ @chapter_named_givens[description][@name] ||= named
331
+ end
332
+
333
+ private
334
+
335
+ # PRIMITIVE 2 (RuleReference#resolve_owner_keyed) — see that
336
+ # method's own comment for the pool shape; the three branches
337
+ # below (exact owner / unambiguous single candidate / ambiguous)
338
+ # are this construct's OWN refusal wording, not shared, since
339
+ # `declared_by:` only exists here so far. UNRESOLVED (no
340
+ # candidate yet, or `declared_by:` naming an aggregate that
341
+ # hasn't declared it yet) is no longer a fourth branch that
342
+ # raises HERE — see `#pending_chapter_given`, below, for why:
343
+ # a chapter split across files can genuinely reference a
344
+ # precondition a LATER file declares, and "not found among
345
+ # what's loaded so far" cannot tell that apart from "genuinely
346
+ # never declared" until every file has.
347
+ def reference_named_chapter_given(description, declared_by:)
348
+ verify_resolves_via!("given", "Aggregate", "owner_keyed")
349
+ candidates = resolve_owner_keyed(@chapter_named_givens, description)
350
+
351
+ named =
352
+ if declared_by
353
+ owner = Naming.demodulise(declared_by)
354
+ candidates[owner] || pending_chapter_given(description, declared_by: owner)
355
+ elsif candidates.size == 1
356
+ candidates.values.first
357
+ elsif candidates.empty?
358
+ pending_chapter_given(description, declared_by: nil)
359
+ else
360
+ raise(Malformed,
361
+ "#{@name}'s given #{description.inspect} is ambiguous in this chapter — " \
362
+ "#{candidates.keys.join(', ')} each declare a DIFFERENT predicate under " \
363
+ "this same description; name which one with declared_by: (e.g. " \
364
+ "given(#{description.inspect}, declared_by: #{candidates.keys.first}))")
365
+ end
366
+
367
+ @named_givens[description] = named
368
+ end
369
+
370
+ # A CHAPTER MAY BE SPLIT ACROSS FILES — the SAME reason a query
371
+ # hop's own cross-file target, a correlation key's own emitting
372
+ # command, and an event's own declared shape are all resolved
373
+ # once the whole chapter is assembled rather than refused the
374
+ # moment one file's own bare reference outruns what's loaded so
375
+ # far (`BluebookBuilder.validate_assembled!`'s own comment).
376
+ #
377
+ # Unlike those, though, a chapter-given's resolved value is not
378
+ # a pass/fail check on an already-built IR — it IS part of the
379
+ # referencing aggregate's own IR (`preconditions:` below), built
380
+ # and handed off the moment THIS aggregate's own file finishes
381
+ # loading, long before a later file might declare the real
382
+ # thing. So this hands back a PLACEHOLDER `Given` — embedded
383
+ # exactly where the resolved one would be, by Ruby object
384
+ # reference, in this aggregate's own `preconditions` AND in any
385
+ # command in this SAME aggregate that separately bare-references
386
+ # the same description (`CommandBuilder#given`'s own hash-chain
387
+ # read of this aggregate's `@named_givens`, the identical key) —
388
+ # and queues the request in `@chapter_pending_givens`.
389
+ # `BluebookBuilder#resolve_pending_chapter_givens!` MUTATES this
390
+ # exact object in place, once every file has loaded, so every
391
+ # existing reference to it (there is only ever the one object,
392
+ # never a copy) sees the resolved fields simultaneously. Safe
393
+ # because every real reader of a `Given` — refusal wording at
394
+ # dispatch, `Aggregate`'s own lazy `-> { preconditions.map { ... } }`
395
+ # IR accessor, docs — runs strictly after boot completes, never
396
+ # mid-load; `judge_deferred!` resolves every pending chapter-given
397
+ # before anything else touches this chapter's assembled IR.
398
+ def pending_chapter_given(description, declared_by:)
399
+ placeholder = Given.new(description: description, canonical: nil, predicate: nil)
400
+ @chapter_pending_givens << { aggregate: @name, description: description,
401
+ declared_by: declared_by, placeholder: placeholder }
402
+ placeholder
403
+ end
404
+
405
+ public
406
+
407
+ # THE AGGREGATE BOUNDARY IS WHAT AN INVARIANT DEFINES (S10, ADR
408
+ # 0025 — "Rules") — checked after every command, before save,
409
+ # the same way a value object's already is
410
+ # (`ValueObjectBuilder#invariant`, whose own shape this mirrors
411
+ # exactly). Today `invariant` lived only inside `value_object`;
412
+ # an aggregate-level rule had nowhere to live, so "the balance
413
+ # never goes negative" was three different `given`/`ensures`
414
+ # texts across banking's six balance-moving commands, and the
415
+ # four that only increase it said nothing at all — completeness
416
+ # depended on someone noticing which commands could decrease it.
417
+ # RENAMED FROM `invariant` — item #13's full metaprogrammed
418
+ # dispatch (slice 4b), same reasoning as given_impl above.
419
+ def invariant_impl(description, &predicate)
420
+ @invariants << build_rule(Invariant, description, predicate, owner_name: @name, word: "invariant",
421
+ extraction_failure: "it would be a rule the IR cannot carry")
422
+ end
423
+
424
+ def build
425
+ drain_pending!
426
+ resolve_pending_identity!
427
+ seal_mutation_targets
428
+ seal_query_targets
429
+ seal_defaults
430
+ seal_lifecycle_guards
431
+ seal_projected_fields
432
+ seal_correction_targets
433
+
434
+ ir = Aggregate.new(
435
+ name: @name,
436
+ description: @description,
437
+ attributes: attributes,
438
+ value_objects: @value_objects + closed_sets,
439
+ commands: @commands,
440
+ invariants: @invariants,
441
+ preconditions: @named_givens.values,
442
+ projected_fields: @projected_fields,
443
+ identified_by: @identity_paths,
444
+ lifecycle: @lifecycle,
445
+ entities: @entities,
446
+ queries: @queries,
447
+ policies: @policies,
448
+ reference_targets: @reference_targets + entity_reference_targets,
449
+ provenance: @provenance
450
+ )
451
+
452
+ # After the IR exists, on purpose : a reference is declared IN the
453
+ # aggregate, and the aggregate the IR graph knows is `ir`, not the
454
+ # builder.
455
+ stamp_references(ir)
456
+ ir
457
+ end
458
+
459
+ def self.build(name, chapter_named_givens: {}, chapter_pending_givens: [],
460
+ chapter_entity_named_givens: {}, chapter_entity_pending_givens: [], &block)
461
+ builder = new(name, chapter_named_givens: chapter_named_givens, chapter_pending_givens: chapter_pending_givens,
462
+ chapter_entity_named_givens: chapter_entity_named_givens,
463
+ chapter_entity_pending_givens: chapter_entity_pending_givens)
464
+ builder.instance_eval(&block) if block
465
+ builder.build
466
+ end
467
+
468
+ private
469
+
470
+ # DEFERRED CONSTRUCTION — `entity`/`command`/`query` used to build
471
+ # immediately, INLINE, the moment their own DSL line ran during
472
+ # `instance_eval` — meaning a command's own resolution (`sets
473
+ # :field` importing the owner's own attribute, `given("desc")`
474
+ # referencing an aggregate-level precondition, a query's own
475
+ # positional-param resolution) only ever saw whatever `@entities`/
476
+ # `attributes`/`@named_givens`/`@value_objects` held AS OF THAT
477
+ # EXACT TEXTUAL LINE — never what the aggregate's block would go
478
+ # on to declare after it. Three real, confirmed cases in the
479
+ # self-hosted meta-domain violate the "declare before you
480
+ # reference" convention every other resolution rule relies on
481
+ # (`command "Handler"` before `entity "Handler"`, same for
482
+ # Member/Dispatch — see docs/resolution-rules/
483
+ # implicit-append-fields.md's own "Known limitations").
484
+ #
485
+ # This is the SAME move `BluebookBuilder` already makes one level
486
+ # UP, at the CHAPTER level — build every aggregate first, THEN run
487
+ # cross-referential validation (`validate_query_hops!`,
488
+ # `validate_projected_fields!`, `validate_no_bidirectional_
489
+ # references!`) once `@aggregates` is fully populated — extended
490
+ # one level down: `entity`/`command`/`query` now only QUEUE a
491
+ # descriptor (`@pending_entities`/`@pending_commands`/
492
+ # `@pending_queries`, each preserving its own declared order),
493
+ # and `#build` drains them here, in this exact order, BEFORE any
494
+ # of the existing `seal_*` validations (which already assume
495
+ # `@commands`/`@entities`/`@queries` are the real, final, built
496
+ # objects) — entities FIRST and fully, since a command's own
497
+ # `sets :list, append: {...}` needs a list's element entity
498
+ # already built (`.attributes` populated) to resolve against, not
499
+ # just named.
500
+ #
501
+ # `attribute`/`value_object`/`identified_by`/`given` (block form)
502
+ # are NOT deferred — they still build eagerly during
503
+ # `instance_eval`, unchanged. Nothing reads `@entities`/
504
+ # `@commands`/`@queries` from anywhere OTHER than `#build` and its
505
+ # own private helpers (checked directly), so nothing else in this
506
+ # file needed to change for this to be safe.
507
+ def drain_pending!
508
+ @entities = @pending_entities.map do |name, block|
509
+ EntityBuilder.build(name, owner_value_objects: @value_objects + closed_sets,
510
+ owner_named_givens: @entity_named_givens,
511
+ identity_name_prefix: "#{Naming.demodulise(@name)}#{Naming.demodulise(name)}",
512
+ identity_value_object_installer: ->(value_object) { @value_objects << value_object },
513
+ aggregate_name: @name,
514
+ chapter_entity_named_givens: @chapter_entity_named_givens,
515
+ chapter_entity_pending_givens: @chapter_entity_pending_givens,
516
+ &block)
517
+ end
518
+
519
+ @commands = @pending_commands.map do |name, from, block|
520
+ CommandBuilder.build(name, owner: @name, from: from, named_givens: @named_givens,
521
+ owner_attributes: attributes,
522
+ owner_constructs: @value_objects + closed_sets + @entities, &block)
523
+ end
524
+
525
+ @queries = @pending_queries.map do |name, block|
526
+ QueryBuilder.build(name, owner_attributes: attributes, &block)
527
+ end
528
+ end
529
+
530
+ # `identified_by`'s own resolution pool (AttributeCollector#resolve_
531
+ # pending_identity!'s hook, S9) — an aggregate resolves a bare
532
+ # field's own value-object type against everything it declares
533
+ # itself, own inline closed sets included.
534
+ def identity_pool = @value_objects + closed_sets
535
+
536
+ def identity_value_object_name = "#{Naming.demodulise(@name)}Identity"
537
+
538
+ def install_identity_value_object!(value_object)
539
+ @value_objects << value_object
540
+ end
541
+
542
+ # LEGACY — see `has_many`/`has_one`/`belongs_to`'s own comment;
543
+ # byte-identical to what those three did before this slice.
544
+ def legacy_has_many(type, as:, optional: false)
545
+ plural = Naming.demodulise(type)
546
+ reference_to_impl(Naming.singularize(plural), as: as || Naming.snake(plural).to_sym, optional: optional)
547
+ end
548
+
549
+ def legacy_has_one(type, as:, optional: false)
550
+ reference_to_impl(type, as: as || Naming.snake(Naming.demodulise(type)).to_sym, optional: optional)
551
+ end
552
+
553
+ # Every reference is told which Aggregate declares it, so it can
554
+ # find the chapter and resolve its target.
555
+ #
556
+ # Stamped HERE, at build, rather than at `reference_to`, because a command
557
+ # builder does not hold the aggregate and should not learn to. And
558
+ # deliberately across every list that can carry one — a reference the walk
559
+ # missed would resolve to nil, and `resolve_references` SKIPS a nil target,
560
+ # so the guarantee would go quiet instead of going red. That is the exact
561
+ # shape of the bug that let an Account belong to an unregistered customer
562
+ # fourteen times over.
563
+ def stamp_references(ir)
564
+ reference_bearing_attributes.each { |attribute| attribute.type.declared_in = ir }
565
+ end
566
+
567
+ # AN OWNED PIECE'S OWN `reference_to` IS AN EDGE THIS AGGREGATE
568
+ # POINTS ACROSS TOO (S9, ADR 0025 — "entity/aggregate shared
569
+ # vocabulary") — a ring closing through a contained piece (Board
570
+ # -> Board::Card -> Product -> Board) is the same "no boundary
571
+ # anyone can reason about alone" `validate_no_bidirectional_
572
+ # references!` already refuses for a direct aggregate-to-
573
+ # aggregate ring; it was invisible before this because only
574
+ # `AggregateBuilder#reference_to` ever fed `@reference_targets`,
575
+ # never `EntityBuilder#reference_to`. Command/query reference
576
+ # ARGUMENTS are deliberately excluded — they are data flowing
577
+ # through a dispatch, not persisted state the graph a cycle
578
+ # means anything over.
579
+ def entity_reference_targets
580
+ @entities.flat_map { |entity| entity.attributes.select(&:reference?).map { |a| a.type.target_name.to_s } }
581
+ end
582
+
583
+ def reference_bearing_attributes
584
+ lists = [attributes, *@commands.map(&:attributes), *@queries.map(&:attributes)]
585
+ @entities.each do |entity|
586
+ lists << entity.attributes
587
+ lists.concat(entity.commands.map(&:attributes))
588
+ lists.concat(entity.queries.map(&:attributes))
589
+ end
590
+
591
+ lists.flatten.select(&:reference?)
592
+ end
593
+
594
+ # A mutation must name a field the aggregate actually HAS.
595
+ #
596
+ # NOT moved to the language, and deliberately so. The language says only
597
+ # `given("a mutation names a target") { !target.value.to_s.empty? }` —
598
+ # non-emptiness — because saying more means reaching a list that lives on
599
+ # a DIFFERENT root : a command's changes hang off Command, the fields they
600
+ # name hang off Aggregate, and a given is a closed predicate over its own
601
+ # state. Aggregate.Seal is the right shape and cannot see commands ; the
602
+ # reference trick that rescued "attributes use value-object types" needs a
603
+ # root to point at, and an aggregate's fields are a value-object list, not
604
+ # roots. This is the second rule that cannot port for that reason — the
605
+ # first is read-model uniqueness — and both wait on the same thing : a
606
+ # quantifier, or fields promoted to roots.
607
+ #
608
+ # So it lives here, at build, where every declaration is present. Found by
609
+ # writing `then_set :disputed_by` on CardPayment before the field existed :
610
+ # it wrote into nothing, refused nothing, and every check stayed green.
611
+ # A DEFAULT FILLS THE SHAPE IT IS DECLARED ON, or it fills nothing.
612
+ #
613
+ # `attribute :cover, one_of("covered", "open"), default: "open"` builds
614
+ # cleanly and then refuses EVERY create at dispatch — "cover is a Cover,
615
+ # pass its fields as an object" — because the value object wants its
616
+ # fields and got a bare string. The bluebook is wrong at the line where
617
+ # it is written and says so nowhere near it.
618
+ #
619
+ # It cost a corpus member 33 refusals out of 40 steps, with every gate
620
+ # green throughout: the refusals were perfectly consistent, which is
621
+ # consistency about nothing. `till.bluebook` has always had the right shape
622
+ # — `default: { cents: 0 }`.
623
+ #
624
+ # A PRIMITIVE takes a scalar and a VALUE OBJECT takes its fields, so the
625
+ # test is simply which one the type names. Nothing here guesses at the
626
+ # keys: a default that is a Hash is left to `Value.for_attribute`, which
627
+ # is where a wrong FIELD belongs.
628
+ def seal_defaults
629
+ # `closed_sets` TOO, not only `@value_objects` — the exact gap
630
+ # this method's own comment names: an inline `one_of(...)`
631
+ # synthesises its value object through `closed_sets`
632
+ # (AttributeCollector#synthesise_closed_set), never installed
633
+ # into `@value_objects` until `#build` merges them (see
634
+ # `#build`'s own `@value_objects + closed_sets`, and
635
+ # `declared_value_object`'s identical merge). Checking
636
+ # `@value_objects` alone made this exact attribute — a bare
637
+ # default on an inline closed set — invisible to the one
638
+ # check meant to catch it.
639
+ shapes = (@value_objects + closed_sets).map { |shape| shape.hecks_name.to_s }
640
+
641
+ attributes.each do |attribute|
642
+ next if attribute.default.nil? || attribute.default.is_a?(Hash)
643
+ next unless shapes.include?(attribute.type.to_s)
644
+
645
+ raise Malformed,
646
+ "#{@name}.#{attribute.name} defaults to #{attribute.default.inspect}, but " \
647
+ "#{attribute.type} is a value object — a default fills its FIELDS " \
648
+ "(default: { ... }), and a bare value refuses every create instead"
649
+ end
650
+ end
651
+
652
+ # A command's `from:` guard needs a lifecycle field to check
653
+ # against — declared at BUILD time (S10, ADR 0025), the same
654
+ # point every other "does this actually resolve" check in this
655
+ # file runs, rather than left to crash `enforce_lifecycle_
656
+ # guard` the first time such a command is ever dispatched.
657
+ def seal_lifecycle_guards
658
+ return if @lifecycle
659
+
660
+ @commands.each do |command|
661
+ next unless command.from
662
+
663
+ raise Malformed,
664
+ "#{@name}.#{command.hecks_name} guards from: #{Array(command.from).inspect}, but " \
665
+ "#{@name} declares no lifecycle — from: checks a lifecycle field, and there is " \
666
+ "none here to check"
667
+ end
668
+ end
669
+
670
+ # `projects`'s OWN half of "does this actually resolve" (S12,
671
+ # ADR 0025) — the LOCAL half only: `reference` must name a real
672
+ # reference-typed attribute this aggregate declares, and
673
+ # `name` must not collide with an attribute already declared
674
+ # (a projected field is its own kind of field, never a second
675
+ # spelling of one that already exists). The TARGET aggregate's
676
+ # own field is checked separately, once every aggregate in the
677
+ # chapter is real — see BluebookBuilder#validate_projected_
678
+ # fields!'s own comment for why that half cannot happen here.
679
+ def seal_projected_fields
680
+ declared = attributes.map { |attribute| attribute.name.to_sym }
681
+
682
+ @projected_fields.each do |field|
683
+ if declared.include?(field.name)
684
+ raise Malformed,
685
+ "#{@name}.projects :#{field.name} names a field #{@name} already declares — " \
686
+ "a projected field is never a second spelling of one that already exists"
687
+ end
688
+
689
+ reference_attribute = attributes.find { |attribute| attribute.name == field.reference }
690
+ unless reference_attribute&.reference?
691
+ raise Malformed,
692
+ "#{@name}.projects :#{field.name} reads through #{field.reference.inspect}, which " \
693
+ "#{@name} never declares as a reference_to — projects reads through a REFERENCE, " \
694
+ "never a value object or a scalar"
695
+ end
696
+ end
697
+ end
698
+
699
+ def seal_mutation_targets
700
+ known = attributes.map { |attribute| attribute.name.to_sym }
701
+ known << @lifecycle.field.to_sym if @lifecycle
702
+
703
+ @commands.each do |command|
704
+ command.mutations.each do |mutation|
705
+ # `:delegate` — CommandBuilder#delegates_to's own comment —
706
+ # targets no field of THIS aggregate at all; its `target`
707
+ # names an "Entity.Command" pair instead, checked when the
708
+ # command builds (`delegates_to`'s own `rpartition` guard)
709
+ # and again at dispatch time (`CommandInterpreter
710
+ # #step_delegate_to_entity`, which refuses a real one that
711
+ # names no such entity or command). Sealing THIS check
712
+ # against it would refuse every delegating command outright.
713
+ # `:corrects` — CommandBuilder#corrects_impl's own comment —
714
+ # targets an EVENT name, not a field either; checked instead
715
+ # by `seal_correction_targets`, below.
716
+ next if [:delegate, :corrects].include?(mutation.op)
717
+ next if known.include?(mutation.target.to_sym)
718
+
719
+ raise Malformed,
720
+ "#{@name}.#{command.hecks_name} sets #{mutation.target}, which #{@name} " \
721
+ "never declares — a mutation into a field that does not exist " \
722
+ "writes nothing and refuses nothing"
723
+ end
724
+ end
725
+ end
726
+
727
+ # `corrects` — CommandBuilder#corrects_impl's own comment. Runs
728
+ # once every command in the aggregate is known (the same reason
729
+ # this is a `seal_*` step rather than living in `corrects_impl`
730
+ # itself — a command cannot see its own siblings' `emits` while
731
+ # it is still being built). Two things are checked:
732
+ #
733
+ # 1. The named event must be something a SIBLING command here
734
+ # actually `emits` — naming an event nothing in this aggregate
735
+ # ever announces is a build-time authoring error. (Whether
736
+ # THIS record has actually emitted it YET is the dispatch-time
737
+ # half — CommandRules::Admissibility#enforce_correction_target.)
738
+ #
739
+ # 2. `reverses: true` derives the corrective `sets` from the
740
+ # ORIGINAL command's own mutations, rather than the author
741
+ # writing them — but only when every one of those mutations is
742
+ # STRUCTURALLY invertible with no runtime data: increment/
743
+ # decrement, same argument, opposite verb (`sign_for`'s own
744
+ # +1/-1 pair — CommandRules::Arithmetic applies `current +
745
+ # sign * amount`, so the SAME source with the OPPOSITE sign
746
+ # undoes it exactly). Nothing else qualifies today: `set` has
747
+ # no such rule at all — inverting it needs the SPECIFIC prior
748
+ # value at the moment the original fired, which is per-
749
+ # instance runtime data no build-time derivation can have;
750
+ # `multiply`/`clamp` are lossy by design (a clamped value's
751
+ # own pre-clamp magnitude is not recoverable from the mutation
752
+ # at all); `append`/`remove` LOOK symmetric but are not
753
+ # reliably so — `append`'s source is a per-field binding hash
754
+ # (`append: { name: :name, amount: :amount }`), `remove`'s is
755
+ # a single resolved value to match by equality
756
+ # (MutationApplier#removed), and collapsing one shape into the
757
+ # other correctly needs the target list's own value-object
758
+ # field names, not just the mutation's own recorded shape — a
759
+ # real gap, left for a follow-on round rather than guessed at
760
+ # here. Refuses rather than silently deriving something wrong
761
+ # — see docs/decisions/ for the ADR that draws this exact
762
+ # line.
763
+ def seal_correction_targets
764
+ inverse_op = { increment: :decrement, decrement: :increment }
765
+ emitted_by = Hash.new { |hash, key| hash[key] = [] }
766
+ @commands.each { |command| command.emits.each { |event_name| emitted_by[event_name] << command } }
767
+
768
+ @commands.each do |command|
769
+ correction = command.mutations.find { |mutation| mutation.op == :corrects }
770
+ next unless correction
771
+
772
+ event = correction.target
773
+ sources = emitted_by[event]
774
+ if sources.empty?
775
+ raise Malformed,
776
+ "#{@name}.#{command.hecks_name} corrects #{event.inspect}, but nothing " \
777
+ "declared on #{@name} ever emits it — corrects names a fact this " \
778
+ "aggregate actually announces, not an aspiration"
779
+ end
780
+
781
+ next unless correction.source[:reverses]
782
+
783
+ own_mutations = command.mutations.reject { |mutation| mutation.op == :corrects }
784
+ if own_mutations.any?
785
+ raise Malformed,
786
+ "#{@name}.#{command.hecks_name} declares both corrects #{event.inspect}, " \
787
+ "reverses: true AND its own sets — reverses: true means the correction " \
788
+ "is DERIVED; write one or the other, never both"
789
+ end
790
+
791
+ derived = sources.flat_map(&:mutations).reject { |mutation| mutation.op == :corrects }
792
+ unsupported = derived.reject { |mutation| inverse_op.key?(mutation.op) }
793
+ if unsupported.any?
794
+ raise Malformed,
795
+ "#{@name}.#{command.hecks_name} corrects #{event.inspect}, reverses: " \
796
+ "true, but the command(s) that emit it use " \
797
+ "#{unsupported.map(&:op).uniq.join(', ')} — not statically invertible " \
798
+ "(set needs the specific prior value, multiply/clamp are lossy) — " \
799
+ "declare the corrective sets by hand instead"
800
+ end
801
+
802
+ derived.each do |mutation|
803
+ command.mutations << Mutation.new(target: mutation.target, op: inverse_op.fetch(mutation.op),
804
+ source: mutation.source)
805
+ end
806
+ end
807
+ end
808
+
809
+ # A query must ask about a field the aggregate actually HAS — the same
810
+ # seal `then_set` gets, closing the same silence: a where over a field
811
+ # nothing declares matches nothing and refuses nothing, forever, on
812
+ # every adapter. Three more silences close with it. A dotted path may
813
+ # reach through the value-object graph but must LAND on a scalar
814
+ # member (QuerySpecification::FieldPath is the one walk every engine
815
+ # now shares) — landing on a value object hands SQL a JSON object
816
+ # where the reference interpreter unwraps a hash. An ordered
817
+ # comparator (lt/gt/gte/lte) must land on a numeric leaf — over text
818
+ # the reference interpreter quietly matches no rows while SQL
819
+ # compares lexicographically. And a :symbol value must name one of
820
+ # the query's own declared arguments, or it resolves to nil at
821
+ # dispatch and matches nothing.
822
+ ORDERED_COMPARATORS = %i[lt lte gt gte].freeze
823
+
824
+ def seal_query_targets
825
+ query_surfaces.each do |owner, fields, lifecycle, queries|
826
+ queries.each do |query|
827
+ query.wheres.each do |clause|
828
+ seal_query_field(owner, query, fields, lifecycle, clause.field)
829
+ seal_ordered_comparator(owner, query, fields, clause)
830
+ infer_local_query_argument(query, fields, lifecycle, clause)
831
+ seal_query_argument(owner, query, clause.value) unless clause.field.to_s.include?("/")
832
+ end
833
+ seal_query_field(owner, query, fields, lifecycle, query.order_by.field, ordering: true) if query.order_by
834
+ seal_query_argument(owner, query, query.limit&.value)
835
+ seal_query_argument(owner, query, query.offset&.value)
836
+ end
837
+ end
838
+ end
839
+
840
+ def query_surfaces
841
+ [[@name, attributes, @lifecycle, @queries]] +
842
+ @entities.map { |entity| ["#{@name}::#{entity.hecks_name}", entity.attributes, entity.lifecycle, entity.queries] }
843
+ end
844
+
845
+ # `/` CROSSES INTO ANOTHER RECORD, `.` WALKS FIELDS INSIDE THIS
846
+ # ONE (ADR 0025, "References") — the operator answers which
847
+ # kind of path this is now, not a name collision to arbitrate,
848
+ # so a hop is routed to its own method before any `.`-splitting
849
+ # runs at all; `seal_query_hop` below never sees a field this
850
+ # one would also have tried to resolve as a local dotted walk.
851
+ def seal_query_field(owner, query, fields, lifecycle, field, ordering: false)
852
+ return seal_query_hop(owner, query, fields, field, ordering: ordering) if field.to_s.include?("/")
853
+
854
+ name, *nested = field.to_s.split(".")
855
+ attribute = fields.find { |candidate| candidate.name.to_s == name }
856
+ if nested.empty? && attribute
857
+ refuse_ambiguous_comparison!(owner, query, field, attribute)
858
+ return
859
+ end
860
+ return if nested.empty? && lifecycle&.field.to_s == name
861
+ return if nested.any? && attribute && scalar_path?(attribute, nested)
862
+
863
+ if nested.any? && attribute && resolves?(attribute, nested)
864
+ raise Malformed,
865
+ "#{owner}.#{query.hecks_name} asks about #{field}, which lands on a " \
866
+ "value object, not a scalar — a dotted query path ends on a scalar " \
867
+ "member, or the engines answer it differently"
868
+ end
869
+
870
+ raise Malformed,
871
+ "#{owner}.#{query.hecks_name} asks about #{field}, which #{owner} " \
872
+ "never declares — a query over a field that does not exist " \
873
+ "matches nothing and refuses nothing"
874
+ end
875
+
876
+ # ORDER BY refuses a hop OUTRIGHT, right here — unlike a WHERE
877
+ # hop (deferred below), this doesn't need the target's shape to
878
+ # answer: an ask is ordered by what its own answering rows
879
+ # hold, and a hop answers with a candidate set, not a sort key
880
+ # (see Runtime::ReferenceHop).
881
+ #
882
+ # A WHERE hop is only RECOGNISED here, and CHECKED LATER. The
883
+ # head names one of this aggregate's own references, which is
884
+ # answerable now — a Reference knows its own target_name at
885
+ # declaration. What it points AT is not: stamp_references has
886
+ # already run by this point, but the chapter (Bluebook, and the
887
+ # owning aggregate's OWN place in it) does not exist yet, so
888
+ # Reference#resolve would answer nil for every target in the
889
+ # file, including ones declared above this one. The tail, and
890
+ # whether the target even exists, are BluebookBuilder's
891
+ # business — see validate_query_hops!, which runs once the
892
+ # chapter is real, for exactly the reason
893
+ # validate_no_bidirectional_references! already gives for
894
+ # living at that same later point.
895
+ def seal_query_hop(owner, query, fields, field, ordering:)
896
+ unless QuerySpecification::HopPath.hop_head?(field, fields)
897
+ raise Malformed,
898
+ "#{owner}.#{query.hecks_name} asks about #{field}, which #{owner} " \
899
+ "never declares — a query over a field that does not exist " \
900
+ "matches nothing and refuses nothing"
901
+ end
902
+
903
+ return unless ordering
904
+
905
+ raise Malformed,
906
+ "#{owner}.#{query.hecks_name} orders by #{field}, which hops through " \
907
+ "a reference — an ask is ordered by what its own answering rows " \
908
+ "hold, and a hop answers with a candidate set, not a sort key"
909
+ end
910
+
911
+ def seal_ordered_comparator(owner, query, fields, clause)
912
+ return unless ORDERED_COMPARATORS.include?(clause.op.to_s.to_sym)
913
+
914
+ # A WHERE clause hopping through a reference with an ordered
915
+ # comparator is legitimate ("client whose balance > 500") —
916
+ # unlike ORDER BY (refused outright in seal_query_field, see
917
+ # its own comment), a where-clause hop answers a real
918
+ # candidate set either way, ordered or not. Deferred for the
919
+ # same reason any other hop is: whether the tail is even
920
+ # numeric is BluebookBuilder#validate_query_hops!'s question
921
+ # to ask of the TARGET's shape, not this aggregate's own.
922
+ return if clause.field.to_s.include?("/") && QuerySpecification::HopPath.hop_head?(clause.field, fields)
923
+
924
+ name, *nested = clause.field.to_s.split(".")
925
+ attribute = fields.find { |candidate| candidate.name.to_s == name }
926
+ return if attribute &&
927
+ QuerySpecification::FieldPath.numeric?(attribute, nested) { |type| declared_value_object(type) }
928
+
929
+ held = attribute ? "holds no number" : "is the lifecycle field, which holds text"
930
+ raise Malformed,
931
+ "#{owner}.#{query.hecks_name} compares #{clause.field} with #{clause.op}, " \
932
+ "but #{clause.field} #{held} — an ordered comparison needs a numeric " \
933
+ "field, and over anything else the adapters answer differently or not at all"
934
+ end
935
+
936
+ def seal_query_argument(owner, query, value)
937
+ return unless value.is_a?(Symbol)
938
+ return if query.attribute(value)
939
+
940
+ raise Malformed,
941
+ "#{owner}.#{query.hecks_name} resolves :#{value} from its arguments, " \
942
+ "but declares no #{value} attribute — an argument that does not exist " \
943
+ "resolves to nil and matches nothing"
944
+ end
945
+
946
+ # A symbolic right-hand side is a query input. When the compared path
947
+ # lands on this owner's declared shape, its type is already known and
948
+ # repeating an `attribute` line inside the query adds no information.
949
+ # Reference hops are resolved only after the whole chapter has been
950
+ # owner-stamped; BluebookBuilder performs the identical inference for
951
+ # those deferred paths.
952
+ def infer_local_query_argument(query, fields, lifecycle, clause)
953
+ name = clause.value
954
+ return unless name.is_a?(Symbol)
955
+ return if query.attribute(name)
956
+ return if clause.field.to_s.include?("/")
957
+
958
+ head, *nested = clause.field.to_s.split(".")
959
+ leaf = if nested.empty? && lifecycle&.field.to_s == head
960
+ Attribute.new(name: name, type: String)
961
+ else
962
+ root = fields.find { |candidate| candidate.name.to_s == head }
963
+ found = root && QuerySpecification::FieldPath.leaf_attribute(root, nested) do |type|
964
+ declared_value_object(type)
965
+ end
966
+ found && Attribute.new(name: name, type: found.type, list: found.list?)
967
+ end
968
+ query.attributes << leaf if leaf
969
+ end
970
+
971
+ # A BARE FIELD NAMING A VALUE OBJECT HAS TO SAY WHICH MEMBER IT
972
+ # MEANS, when more than one could answer. The dotted case above
973
+ # already refuses a path that lands on a value object rather than
974
+ # a scalar; a bare name was returning unconditionally, so
975
+ # `where(frequency: ...)` against a StatementFrequency
976
+ # (cadence, retention_months, paper_fee_cents) compiled — and the
977
+ # engines then disagreed about which member it meant, one taking
978
+ # the FIRST numeric and another declining to unwrap at all.
979
+ #
980
+ # Unambiguous is: exactly one member, whatever its type, or
981
+ # exactly one NUMERIC member among several (Money's `cents`
982
+ # beside its `currency` — the reading every engine already
983
+ # shared, and what the corpus relies on). Anything else names
984
+ # its member with a dotted path, which already works.
985
+ #
986
+ # A list is exempt: `contains` over a `list_of` reads element
987
+ # membership, not a scalar comparison, and has its own agreed
988
+ # reading across the engines.
989
+ def refuse_ambiguous_comparison!(owner, query, field, attribute)
990
+ return if attribute.list?
991
+
992
+ value_object = declared_value_object(attribute.type.to_s)
993
+ return unless value_object
994
+
995
+ members = QuerySpecification::Common::Comparison.ambiguous_members(value_object)
996
+ return if members.empty?
997
+
998
+ raise Malformed,
999
+ "#{owner}.#{query.hecks_name} asks about #{field}, which names #{attribute.type} — " \
1000
+ "it has #{members.size} members (#{members.join(', ')}) and no single one a " \
1001
+ "comparison can mean; name the member (#{field}.#{members.first})"
1002
+ end
1003
+
1004
+ def scalar_path?(attribute, nested)
1005
+ QuerySpecification::FieldPath.scalar_leaf?(attribute, nested) { |type| declared_value_object(type) }
1006
+ end
1007
+
1008
+ def resolves?(attribute, nested)
1009
+ !QuerySpecification::FieldPath.leaf_attribute(attribute, nested) { |type| declared_value_object(type) }.nil?
1010
+ end
1011
+
1012
+ def declared_value_object(type_name)
1013
+ (@value_objects + closed_sets).find { |shape| shape.hecks_name.to_s == type_name }
1014
+ end
1015
+ end
1016
+ end
1017
+ end
1018
+ end