hecks 0.2.0 → 0.3.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 (408) 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 +403 -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 +62 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +51 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +38 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +148 -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 +131 -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 +80 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +168 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +391 -0
  31. data/lib/hecks/adapters/driven/postgres_era/lineage/era_store.rb +171 -0
  32. data/lib/hecks/adapters/driven/postgres_era/lineage/field_cache.rb +190 -0
  33. data/lib/hecks/adapters/driven/postgres_era/lineage/head_compiler.rb +429 -0
  34. data/lib/hecks/adapters/driven/postgres_era/lineage/mint_transaction.rb +166 -0
  35. data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +286 -0
  36. data/lib/hecks/adapters/driven/postgres_era/lineage/resumable_backfill.rb +168 -0
  37. data/lib/hecks/adapters/driven/postgres_era/lineage/tail_merge.rb +163 -0
  38. data/lib/hecks/adapters/driven/postgres_era/lineage/transform_installer.rb +114 -0
  39. data/lib/hecks/adapters/driven/postgres_era/lineage.rb +137 -0
  40. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/coverage_check.rb +89 -0
  41. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +82 -0
  42. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  43. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/minter.rb +132 -0
  44. data/lib/hecks/adapters/driven/postgres_era/lineage_manager.rb +74 -0
  45. data/lib/hecks/adapters/driven/postgres_era.adapter +6 -0
  46. data/lib/hecks/adapters/driven/postgres_era.rb +671 -0
  47. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  48. data/lib/hecks/adapters/driven/prism.rb +80 -0
  49. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  50. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  51. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  52. data/lib/hecks/adapters/driven/sqlite/codec.rb +63 -0
  53. data/lib/hecks/adapters/driven/sqlite/projection.rb +105 -0
  54. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +180 -0
  55. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  56. data/lib/hecks/adapters/driven/sqlite.rb +285 -0
  57. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  58. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  59. data/lib/hecks/adapters/driven.rb +36 -0
  60. data/lib/hecks/adapters.rb +6 -0
  61. data/lib/hecks/behaviors/dsl.rb +96 -0
  62. data/lib/hecks/behaviors/expectations.rb +280 -0
  63. data/lib/hecks/behaviors/ir.rb +31 -0
  64. data/lib/hecks/behaviors/rspec.rb +42 -0
  65. data/lib/hecks/behaviors/runner.rb +96 -0
  66. data/lib/hecks/behaviors.rb +25 -0
  67. data/lib/hecks/bluebook/aggregate.rb +108 -0
  68. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  69. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  70. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  71. data/lib/hecks/bluebook/assembly/contracts.rb +409 -0
  72. data/lib/hecks/bluebook/assembly/marks.rb +227 -0
  73. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  74. data/lib/hecks/bluebook/assembly.rb +78 -0
  75. data/lib/hecks/bluebook/attribute.rb +96 -0
  76. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  77. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  78. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  79. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  80. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  81. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  82. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  83. data/lib/hecks/bluebook/behaviour/lifecycle.rb +51 -0
  84. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  85. data/lib/hecks/bluebook/behaviour/process_manager.rb +38 -0
  86. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  87. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  88. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  89. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  90. data/lib/hecks/bluebook/chapter.rb +68 -0
  91. data/lib/hecks/bluebook/command.rb +124 -0
  92. data/lib/hecks/bluebook/domain_port.rb +102 -0
  93. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  94. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +906 -0
  95. data/lib/hecks/bluebook/dsl/attribute_collector.rb +327 -0
  96. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  97. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1018 -0
  98. data/lib/hecks/bluebook/dsl/command_builder.rb +626 -0
  99. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  100. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  101. data/lib/hecks/bluebook/dsl/entity_builder.rb +305 -0
  102. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  103. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  104. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  105. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  106. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  107. data/lib/hecks/bluebook/dsl/policy_builder.rb +120 -0
  108. data/lib/hecks/bluebook/dsl/port_builder.rb +30 -0
  109. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  110. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +207 -0
  111. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  112. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  113. data/lib/hecks/bluebook/dsl/rule_reference.rb +172 -0
  114. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  115. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  116. data/lib/hecks/bluebook/dsl/word_gate.rb +221 -0
  117. data/lib/hecks/bluebook/dsl/world_builder.rb +77 -0
  118. data/lib/hecks/bluebook/dsl.rb +45 -0
  119. data/lib/hecks/bluebook/entity.rb +103 -0
  120. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -0
  121. data/lib/hecks/bluebook/expression/evaluator.rb +261 -0
  122. data/lib/hecks/bluebook/expression/projection.json +170 -0
  123. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  124. data/lib/hecks/bluebook/expression/resolver.rb +630 -0
  125. data/lib/hecks/bluebook/expression.rb +13 -0
  126. data/lib/hecks/bluebook/hexagon.rb +60 -0
  127. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  128. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  129. data/lib/hecks/bluebook/meta_validator/judge.rb +559 -0
  130. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  131. data/lib/hecks/bluebook/meta_validator/port_judge.rb +47 -0
  132. data/lib/hecks/bluebook/meta_validator/readings.rb +350 -0
  133. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +312 -0
  134. data/lib/hecks/bluebook/meta_validator/shapes.rb +246 -0
  135. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  136. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  137. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  138. data/lib/hecks/bluebook/meta_validator.rb +452 -0
  139. data/lib/hecks/bluebook/model_check.rb +318 -0
  140. data/lib/hecks/bluebook/pattern_subset.rb +120 -0
  141. data/lib/hecks/bluebook/policy.rb +41 -0
  142. data/lib/hecks/bluebook/process_manager.rb +86 -0
  143. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  144. data/lib/hecks/bluebook/project_loader.rb +40 -0
  145. data/lib/hecks/bluebook/project_register.rb +82 -0
  146. data/lib/hecks/bluebook/query.rb +61 -0
  147. data/lib/hecks/bluebook/read_model.rb +109 -0
  148. data/lib/hecks/bluebook/reference.rb +74 -0
  149. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  150. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  151. data/lib/hecks/bluebook/translation.rb +92 -0
  152. data/lib/hecks/bluebook/value_object.rb +58 -0
  153. data/lib/hecks/bluebook.rb +74 -0
  154. data/lib/hecks/codemod.rb +342 -0
  155. data/lib/hecks/construct.rb +71 -0
  156. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  157. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  158. data/lib/hecks/deploy/oidc.json +18 -0
  159. data/lib/hecks/doc/reference.rb +389 -0
  160. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  161. data/lib/hecks/facade/cli_door.rb +116 -0
  162. data/lib/hecks/facade/cli_runner.rb +190 -0
  163. data/lib/hecks/facade/command_request.rb +105 -0
  164. data/lib/hecks/facade/handle.rb +173 -0
  165. data/lib/hecks/facade/json_door.rb +154 -0
  166. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  167. data/lib/hecks/facade/surface/chapter.rb +107 -0
  168. data/lib/hecks/facade/surface.rb +48 -0
  169. data/lib/hecks/facade.rb +44 -0
  170. data/lib/hecks/forms/app.rb +300 -0
  171. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  172. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  173. data/lib/hecks/forms/field_renderer.rb +170 -0
  174. data/lib/hecks/forms/field_shape.rb +232 -0
  175. data/lib/hecks/forms/html.rb +53 -0
  176. data/lib/hecks/forms/index_renderer.rb +35 -0
  177. data/lib/hecks/forms/page.rb +157 -0
  178. data/lib/hecks/forms/params.rb +132 -0
  179. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  180. data/lib/hecks/forms/record_renderer.rb +115 -0
  181. data/lib/hecks/forms/record_table.rb +63 -0
  182. data/lib/hecks/forms/reference_options.rb +30 -0
  183. data/lib/hecks/forms/value_object_shape.rb +46 -0
  184. data/lib/hecks/forms.rb +54 -0
  185. data/lib/hecks/fqn.rb +94 -0
  186. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  187. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  188. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  189. data/lib/hecks/framework/bluebook/governance.bluebook +130 -0
  190. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  191. data/lib/hecks/framework/oidc.json +39 -0
  192. data/lib/hecks/framework.rb +90 -0
  193. data/lib/hecks/freezer.rb +67 -0
  194. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  195. data/lib/hecks/fuzzing/isolated_boot.rb +92 -0
  196. data/lib/hecks/fuzzing/properties.rb +1146 -0
  197. data/lib/hecks/fuzzing/replay.rb +635 -0
  198. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  199. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +54 -0
  200. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  201. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +185 -0
  202. data/lib/hecks/fuzzing/sequence_generator.rb +130 -0
  203. data/lib/hecks/fuzzing/value_generator.rb +177 -0
  204. data/lib/hecks/fuzzing.rb +12 -0
  205. data/lib/hecks/grammar/evolve.rb +291 -0
  206. data/lib/hecks/grammar/expression.bluebook +417 -0
  207. data/lib/hecks/grammar/expression_operators.json +1152 -0
  208. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  209. data/lib/hecks/grammar/oidc.json +69 -0
  210. data/lib/hecks/grammar/translation.bluebook +203 -0
  211. data/lib/hecks/grammar.rb +178 -0
  212. data/lib/hecks/ir.rb +126 -0
  213. data/lib/hecks/language/adapter.bluebook +116 -0
  214. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  215. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  216. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  217. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  218. data/lib/hecks/language/bluebook/command.bluebook +437 -0
  219. data/lib/hecks/language/bluebook/entity.bluebook +316 -0
  220. data/lib/hecks/language/bluebook/policy.bluebook +172 -0
  221. data/lib/hecks/language/bluebook/process_manager.bluebook +264 -0
  222. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  223. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  224. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  225. data/lib/hecks/language/bluebook/syntax.bluebook +447 -0
  226. data/lib/hecks/language/bluebook/vocabulary.bluebook +364 -0
  227. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  228. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  229. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  230. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  231. data/lib/hecks/language/oidc.json +313 -0
  232. data/lib/hecks/language/port.bluebook +92 -0
  233. data/lib/hecks/language/translation/translation.bluebook +110 -0
  234. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  235. data/lib/hecks/language/world/wiring.bluebook +62 -0
  236. data/lib/hecks/language/world/world.bluebook +84 -0
  237. data/lib/hecks/literal.rb +125 -0
  238. data/lib/hecks/naming.rb +121 -0
  239. data/lib/hecks/ports/access_control.port +4 -0
  240. data/lib/hecks/ports/access_control.rb +62 -0
  241. data/lib/hecks/ports/agent/answers.rb +104 -0
  242. data/lib/hecks/ports/agent.port +4 -0
  243. data/lib/hecks/ports/agent.rb +167 -0
  244. data/lib/hecks/ports/authentication.port +4 -0
  245. data/lib/hecks/ports/authentication.rb +50 -0
  246. data/lib/hecks/ports/authorization.port +4 -0
  247. data/lib/hecks/ports/authorization.rb +59 -0
  248. data/lib/hecks/ports/clock.port +4 -0
  249. data/lib/hecks/ports/clock.rb +62 -0
  250. data/lib/hecks/ports/extraction.port +4 -0
  251. data/lib/hecks/ports/extraction.rb +37 -0
  252. data/lib/hecks/ports/identity_assignment.port +4 -0
  253. data/lib/hecks/ports/identity_assignment.rb +45 -0
  254. data/lib/hecks/ports/identity_generation.port +4 -0
  255. data/lib/hecks/ports/identity_generation.rb +49 -0
  256. data/lib/hecks/ports/identity_resolution.port +4 -0
  257. data/lib/hecks/ports/identity_resolution.rb +40 -0
  258. data/lib/hecks/ports/loading.port +4 -0
  259. data/lib/hecks/ports/loading.rb +13 -0
  260. data/lib/hecks/ports/persistence/append_only.rb +102 -0
  261. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  262. data/lib/hecks/ports/persistence/execution.rb +18 -0
  263. data/lib/hecks/ports/persistence/lineage.rb +277 -0
  264. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  265. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  266. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  267. data/lib/hecks/ports/persistence.port +4 -0
  268. data/lib/hecks/ports/persistence.rb +39 -0
  269. data/lib/hecks/ports/projection.port +4 -0
  270. data/lib/hecks/ports/projection.rb +76 -0
  271. data/lib/hecks/ports/query/in_memory.rb +60 -0
  272. data/lib/hecks/ports/query/ordering.rb +41 -0
  273. data/lib/hecks/ports/query.rb +36 -0
  274. data/lib/hecks/ports.rb +27 -0
  275. data/lib/hecks/projections/diagrams.rb +413 -0
  276. data/lib/hecks/projections/ir.rb +18 -0
  277. data/lib/hecks/projections/model/deviations.rb +98 -0
  278. data/lib/hecks/projections/model.rb +145 -0
  279. data/lib/hecks/projections/oidc.rb +110 -0
  280. data/lib/hecks/projections/parser_table.rb +159 -0
  281. data/lib/hecks/projections/reference.rb +38 -0
  282. data/lib/hecks/projections/shape.rb +31 -0
  283. data/lib/hecks/projections/statements.rb +110 -0
  284. data/lib/hecks/projections/vocabulary.rb +100 -0
  285. data/lib/hecks/projections.rb +32 -0
  286. data/lib/hecks/projector/cli_projector.rb +437 -0
  287. data/lib/hecks/projector/docs_projector.rb +321 -0
  288. data/lib/hecks/projector/exporter.rb +141 -0
  289. data/lib/hecks/projector/ir_projector.rb +18 -0
  290. data/lib/hecks/projector/narrate_projector.rb +243 -0
  291. data/lib/hecks/projector/target.rb +97 -0
  292. data/lib/hecks/projector.rb +186 -0
  293. data/lib/hecks/query_ir.rb +392 -0
  294. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  295. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  296. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  297. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  298. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  299. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  300. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  301. data/lib/hecks/query_specification/common/null_policy.rb +80 -0
  302. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  303. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  304. data/lib/hecks/query_specification/common/options.rb +32 -0
  305. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  306. data/lib/hecks/query_specification/common/specification.rb +10 -0
  307. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  308. data/lib/hecks/query_specification/field_path.rb +89 -0
  309. data/lib/hecks/query_specification/hop_path.rb +130 -0
  310. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  311. data/lib/hecks/query_specification.rb +14 -0
  312. data/lib/hecks/rendering.rb +48 -0
  313. data/lib/hecks/router/namespace_installer.rb +157 -0
  314. data/lib/hecks/router.rb +70 -0
  315. data/lib/hecks/runtime/caller.rb +50 -0
  316. data/lib/hecks/runtime/capability_graph.rb +44 -0
  317. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +124 -0
  318. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +248 -0
  319. data/lib/hecks/runtime/command_interpreter.rb +413 -0
  320. data/lib/hecks/runtime/command_rules/admissibility.rb +307 -0
  321. data/lib/hecks/runtime/command_rules/arithmetic.rb +254 -0
  322. data/lib/hecks/runtime/command_rules/authorization.rb +64 -0
  323. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  324. data/lib/hecks/runtime/command_rules/references.rb +181 -0
  325. data/lib/hecks/runtime/command_rules.rb +28 -0
  326. data/lib/hecks/runtime/dependency_planning.rb +231 -0
  327. data/lib/hecks/runtime/dispatcher.rb +286 -0
  328. data/lib/hecks/runtime/entity_element.rb +253 -0
  329. data/lib/hecks/runtime/entity_interpreter.rb +228 -0
  330. data/lib/hecks/runtime/era_check.rb +136 -0
  331. data/lib/hecks/runtime/era_guard/shape_diff.rb +128 -0
  332. data/lib/hecks/runtime/era_guard.rb +193 -0
  333. data/lib/hecks/runtime/era_tamper.rb +61 -0
  334. data/lib/hecks/runtime/errors.rb +100 -0
  335. data/lib/hecks/runtime/event.rb +51 -0
  336. data/lib/hecks/runtime/identity.rb +128 -0
  337. data/lib/hecks/runtime/instance.rb +120 -0
  338. data/lib/hecks/runtime/interpreting.rb +69 -0
  339. data/lib/hecks/runtime/loader.rb +143 -0
  340. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  341. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  342. data/lib/hecks/runtime/query_interpreter.rb +254 -0
  343. data/lib/hecks/runtime/reaction_invocation.rb +245 -0
  344. data/lib/hecks/runtime/read_model_interpreter.rb +266 -0
  345. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  346. data/lib/hecks/runtime/reference_hop.rb +99 -0
  347. data/lib/hecks/runtime/refusal_wording.rb +124 -0
  348. data/lib/hecks/runtime/registry/saga_persistence.rb +70 -0
  349. data/lib/hecks/runtime/registry/verification.rb +158 -0
  350. data/lib/hecks/runtime/registry.rb +218 -0
  351. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  352. data/lib/hecks/runtime/routing.rb +96 -0
  353. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  354. data/lib/hecks/runtime/saga_interpreter.rb +301 -0
  355. data/lib/hecks/runtime/storage_shape.rb +120 -0
  356. data/lib/hecks/runtime/tenant_check.rb +84 -0
  357. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  358. data/lib/hecks/runtime/value/admission.rb +112 -0
  359. data/lib/hecks/runtime/value/coercion.rb +500 -0
  360. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  361. data/lib/hecks/runtime/value.rb +125 -0
  362. data/lib/hecks/runtime.rb +95 -0
  363. data/lib/hecks/translation/audit/approval_digest.rb +31 -0
  364. data/lib/hecks/translation/audit/layer_one.rb +37 -0
  365. data/lib/hecks/translation/audit/layer_two.rb +77 -0
  366. data/lib/hecks/translation/audit/unfed_report.rb +42 -0
  367. data/lib/hecks/translation/audit.rb +70 -0
  368. data/lib/hecks/translation/reattest.rb +72 -0
  369. data/lib/hecks/translation/rule_compiler.rb +120 -0
  370. data/lib/hecks/translation/scaffold/differ.rb +183 -0
  371. data/lib/hecks/translation/scaffold/renderer.rb +41 -0
  372. data/lib/hecks/translation/scaffold/writer.rb +28 -0
  373. data/lib/hecks/translation/scaffold.rb +30 -0
  374. data/lib/hecks/translation.rb +11 -0
  375. data/lib/hecks/version.rb +16 -0
  376. data/lib/hecks/vocabulary.rb +213 -0
  377. data/lib/hecks.rb +118 -6
  378. metadata +401 -222
  379. data/bin/hecks +0 -7
  380. data/bin/hecks-package +0 -65
  381. data/bin/hecks_console +0 -12
  382. data/bin/hecks_serverless +0 -6
  383. data/lib/cli/build.rb +0 -14
  384. data/lib/cli/command_runner.rb +0 -28
  385. data/lib/cli/console.rb +0 -10
  386. data/lib/cli/generate.rb +0 -37
  387. data/lib/cli/hecks-cli.rb +0 -27
  388. data/lib/cli/test.rb +0 -57
  389. data/lib/console/commands.rb +0 -8
  390. data/lib/console/hecks-console.rb +0 -1
  391. data/lib/packager/README.md +0 -0
  392. data/lib/packager/app_runner.rb +0 -21
  393. data/lib/packager/args.rb +0 -26
  394. data/lib/packager/compatibility/fixnum.rb +0 -6
  395. data/lib/packager/hecks.rb +0 -39
  396. data/lib/packager/query_runner.rb +0 -21
  397. data/lib/packager/resources/Dockerfile +0 -11
  398. data/lib/packager/resources/app_binary +0 -7
  399. data/lib/packager/resources/bundle_config +0 -3
  400. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  401. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  402. data/lib/serverless/Domain +0 -32
  403. data/lib/serverless/cli.rb +0 -75
  404. data/lib/serverless/resources/command_name.js +0 -5
  405. data/lib/serverless/resources/environment.js +0 -7
  406. data/lib/serverless/resources/handler.js.tt +0 -28
  407. data/lib/serverless/resources/run_binary.js +0 -22
  408. data/lib/serverless/resources/serverless.yml +0 -20
@@ -0,0 +1,207 @@
1
+ require_relative "word_gate"
2
+ module Hecks
3
+ module Bluebook
4
+ module DSL
5
+ class ProcessManagerBuilder
6
+ GRAMMAR_CONTEXT = "ProcessManager"
7
+
8
+ class InvalidProcessManager < StandardError; end
9
+
10
+ include WordGate
11
+
12
+ def initialize(name)
13
+ @name = name
14
+ @handlers = []
15
+ end
16
+
17
+ # `correlates_by`/`starts_on`/`ends_on` — item #13's full
18
+ # metaprogrammed dispatch, slice 1 (whole-project table-
19
+ # unification survey): each a bare, kind-driven coerce-and-
20
+ # assign with nothing else, now executed by `GenericDispatch`.
21
+
22
+ # ONE STATE-MACHINE VOCABULARY (S7, ADR 0025 — "events and
23
+ # reactions"): the SAME word `Lifecycle#transition` already
24
+ # carries, one level over — `transition "AccountDebited" =>
25
+ # "awaiting_credit", from: "requested" do ... end` replaces `on
26
+ # "AccountDebited", transition: { "requested" => "awaiting_
27
+ # credit" } do ... end`. Same bare rocket-pair argument shape
28
+ # (not a NAMED `transition:` kwarg wrapping a second Hash), same
29
+ # `from:` — including the array form Lifecycle's own commands
30
+ # could already take and a process manager's own events could
31
+ # not — and the states a procedure runs on are DERIVED from the
32
+ # transitions that name them, the same way `Behaviour::Lifecycle
33
+ # #states` already derives an aggregate's ; `state "x"` lines
34
+ # duplicated exactly what the transition list already said,
35
+ # and could drift from it (`validate!`'s own "undeclared state"
36
+ # check existed only because they could).
37
+ #
38
+ # `starts_on`/`ends_on` are NOT unified into this — verified
39
+ # against the real corpus rather than assumed: Settlement's own
40
+ # `ends_on "TransferSettled"` names an event NONE of its own
41
+ # transitions ever handle (`Transfer.Settle`'s own emission, a
42
+ # full step downstream of the transition that dispatches it),
43
+ # so "the terminal state's own event" is not a fact the
44
+ # transition graph carries — deriving it would either be wrong
45
+ # for this exact corpus member or need a second new word to
46
+ # cover the case, which is not less vocabulary than keeping the
47
+ # one that already says it correctly.
48
+ #
49
+ # EXPANDS IMMEDIATELY, unlike `Lifecycle#transition` (which
50
+ # defers to `Behaviour::Lifecycle#expand`, called at emission
51
+ # time) — `ProcessManager`'s own IR constructor takes `states:`/
52
+ # `handlers:` exactly as it always has, so the runtime
53
+ # (`Behaviour::ProcessManager`, `SagaInterpreter`, saga
54
+ # persistence/rehydration) needs no change at all: what changed
55
+ # is how the DECLARATION reaches that same shape, not the shape
56
+ # a real run ever sees or persists.
57
+ # RENAMED FROM `transition` — item #13's full metaprogrammed
58
+ # dispatch (slice 4c). Not bootstrap-reachable (checked
59
+ # directly — no core/attached chapter declares a ProcessManager
60
+ # of its own).
61
+ def transition_impl(mapping, &block)
62
+ mapping = mapping.dup
63
+ from = mapping.delete(:from)
64
+
65
+ # ALWAYS REQUIRED, unlike `Lifecycle#transition`'s own `from:`
66
+ # — an aggregate's unconstrained transition is admitted from
67
+ # ANY current state (`Behaviour::Lifecycle#applies_from?`
68
+ # returns true for a nil `from`), a reading `SagaInterpreter#
69
+ # advance_saga`'s own admission check does not share: it tests
70
+ # `instance[:state] == handler.from_state` by plain equality,
71
+ # nothing softer. Leaving that check unchanged (this slice's
72
+ # own scope decision — see the class-level comment on why the
73
+ # runtime stays untouched) means an unconstrained PM
74
+ # transition would build cleanly and then match no instance
75
+ # ever, silently — refused here instead, at the one point that
76
+ # can still see the mistake.
77
+ if from.nil?
78
+ raise InvalidProcessManager,
79
+ "#{@name}'s transition #{mapping.inspect} names no from: — a process manager's own " \
80
+ "admission checks a saga instance's CURRENT state exactly, so a transition with no " \
81
+ "from: would match no instance ever, silently"
82
+ end
83
+
84
+ handler = HandlerBuilder.new
85
+ handler.instance_eval(&block) if block
86
+
87
+ mapping.each do |event_type, target|
88
+ state_transition = StateTransition.new(target: target, from: from)
89
+ expand(event_type.to_s, state_transition, handler.dispatches).each { |row| @handlers << row }
90
+ end
91
+ end
92
+
93
+ def build
94
+ validate!
95
+
96
+ ProcessManager.new(
97
+ name: @name,
98
+ correlates_by: @correlates_by,
99
+ starts_on: @starts_on,
100
+ ends_on: @ends_on,
101
+ states: derived_states,
102
+ handlers: @handlers
103
+ )
104
+ end
105
+
106
+ def self.build(name, &block)
107
+ builder = new(name)
108
+ builder.instance_eval(&block) if block
109
+ builder.build
110
+ end
111
+
112
+ private
113
+
114
+ # ONE DECLARED TRANSITION IS SEVERAL ROWS when `from` names more
115
+ # than one source state — `Behaviour::Lifecycle#expand`'s own
116
+ # comment, the identical fan-out, one level over: a
117
+ # `ProcessManagerHandler` only ever carries a single `from_state`,
118
+ # so a `from: [...]` transition mints one row per source, each
119
+ # carrying the SAME dispatches.
120
+ def expand(event_type, transition, dispatches)
121
+ sources = transition.from.nil? ? [nil] : Array(transition.from)
122
+
123
+ sources.map do |source|
124
+ ProcessManagerHandler.new(
125
+ event_type: event_type,
126
+ from_state: source.to_s,
127
+ to_state: transition.target,
128
+ dispatches: dispatches
129
+ )
130
+ end
131
+ end
132
+
133
+ # DERIVED, not declared (S7) — every state this procedure ever
134
+ # runs on is already named by some transition's own `from_state`
135
+ # or `to_state`; a state nothing transitions into or out of is
136
+ # not a state this procedure has, the same reading
137
+ # `Behaviour::Lifecycle#states` already gives an aggregate's own
138
+ # field. FIRST-SEEN ORDER, walking declaration order — `begin_
139
+ # saga`'s own `pm.states.first` is what a fresh instance starts
140
+ # in, so the order has to survive the derivation, not just the
141
+ # membership.
142
+ def derived_states
143
+ @handlers.flat_map { |h| [h.from_state, h.to_state] }.reject(&:empty?).uniq
144
+ end
145
+
146
+ def validate!
147
+ raise InvalidProcessManager, "#{@name} declares no correlates_by — " \
148
+ "nothing would tie its events to one instance" unless @correlates_by
149
+
150
+ # THE FIELD, NAMED — never the value object that carries it. A bare
151
+ # `correlates_by :end_to_end` reads whatever the payload holds under
152
+ # that key AS the correlation key, and what a non-scalar key even
153
+ # is stays open (the object itself? its serialised text?).
154
+ # Requiring the dotted spelling —
155
+ # `:"end_to_end.value"` — makes every correlates_by name a scalar
156
+ # by construction, the same discipline `identified_by` already
157
+ # holds a head to. This is a syntactic check, not a type check: it
158
+ # does not know or care whether the field IS a value object, only
159
+ # that the declaration cannot leave that question open.
160
+ raise InvalidProcessManager, "#{@name} correlates_by #{@correlates_by.inspect}, which names a whole " \
161
+ "field rather than one of its scalars — say which one, e.g. " \
162
+ "#{@correlates_by}.value" unless @correlates_by.to_s.include?(".")
163
+
164
+ raise InvalidProcessManager, "#{@name} declares no starts_on — " \
165
+ "nothing would ever begin it" if @starts_on.to_s.empty?
166
+
167
+ raise InvalidProcessManager, "#{@name} declares no transitions — " \
168
+ "it would start and then ignore every event" if @handlers.empty?
169
+ end
170
+
171
+ class HandlerBuilder
172
+ GRAMMAR_CONTEXT = "Handler"
173
+
174
+ attr_reader :dispatches
175
+
176
+ include WordGate
177
+
178
+ def initialize = @dispatches = []
179
+
180
+ # THE COMMAND ITSELF (ADR 0025, "events and reactions" — command
181
+ # references become first-class), same shape and same reasons
182
+ # as `PolicyBuilder#trigger`'s own header — bare constant live,
183
+ # quoted text only under shadow-parsing (S0a's bridge; frozen
184
+ # era text still writes `dispatch "Banking::Account.Debit"`).
185
+ #
186
+ # RENAMED FROM `dispatch` — item #13's full metaprogrammed
187
+ # dispatch (slice 4), same reasoning as trigger_impl above.
188
+ def dispatch_impl(command_ref, with: nil)
189
+ if command_ref.is_a?(::String) && !MetaValidator.shadow_parsing?
190
+ raise InvalidProcessManager,
191
+ "dispatch #{command_ref.inspect} is quoted text — give the bare command constant " \
192
+ "instead, e.g. dispatch Account::Debit"
193
+ end
194
+
195
+ DispatchSpec.new(
196
+ command_name: Naming.command_ref(command_ref),
197
+ with_spec: (with || {}).to_a
198
+ ).tap do |dispatch|
199
+ dispatch.instance_variable_set(:@projection_declared, !with.nil?)
200
+ @dispatches << dispatch
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,113 @@
1
+ require_relative "word_gate"
2
+ module Hecks
3
+ module Bluebook
4
+ module DSL
5
+ class QueryBuilder
6
+ GRAMMAR_CONTEXT = "Query"
7
+
8
+ include AttributeCollector
9
+ include QuerySpecification::Common::DSL
10
+ include WordGate
11
+
12
+ def initialize(name)
13
+ @name = name
14
+ @wheres = []
15
+ end
16
+
17
+ def description(value) = @description = value
18
+
19
+ # A query parameter naming another aggregate's own identity
20
+ # (Card.Active's own `Board`, filtering to one board's cards) —
21
+ # just a plain attribute typed as a reference,
22
+ # AttributeCollector#attribute already handling an Reference
23
+ # exactly like any other. No "acts on itself" case to
24
+ # distinguish here the way a command's own reference_to has —
25
+ # a query has no root of its own to act on, only parameters.
26
+ # RENAMED FROM `reference_to` — item #13's full metaprogrammed
27
+ # dispatch (slice 4b). Bootstrap-reachable, in
28
+ # GenericDispatch::BOOTSTRAP_CALLS_FALLBACK.
29
+ def reference_to_impl(type, as: nil, optional: false)
30
+ target = Naming.demodulise(type)
31
+ attribute_impl(as || default_reference_name(target), Reference.new(target), optional: optional)
32
+ end
33
+
34
+ def build
35
+ seal_cursor
36
+ Query.new(
37
+ name: @name,
38
+ description: @description,
39
+ attributes: attributes,
40
+ wheres: @wheres,
41
+ order_by: @order_by,
42
+ limit: @limit,
43
+ offset: @offset,
44
+ cursor: @cursor,
45
+ authorization: @authorization,
46
+ null_semantics: @null_semantics,
47
+ inspection: @inspection
48
+ )
49
+ end
50
+
51
+ def self.build(name, owner_attributes: [], &block)
52
+ builder = new(name)
53
+ builder.instance_eval(&block) if block
54
+ # `send`, not a public call — this isn't a bluebook DSL word (no
55
+ # author ever writes `derive_from_owner!` inside a query block),
56
+ # just internal wiring between this class method and the instance
57
+ # it just built. Kept private below so syntax_conformance_spec's
58
+ # own "every word QueryBuilder answers is declared" check doesn't
59
+ # mistake it for one.
60
+ builder.send(:derive_from_owner!, owner_attributes, block) if block
61
+ builder.build
62
+ end
63
+
64
+ private
65
+
66
+ # A block parameter names one of the OWNER's (the aggregate or entity
67
+ # this query is declared on) own already-declared attributes —
68
+ # `query "ForDecision" do |decision| where decision: :decision end`
69
+ # on Submission, whose own `attribute :decision, DecisionRef` already
70
+ # says what `decision` is. Restating `attribute :decision, DecisionRef`
71
+ # a second time inside the query was pure duplication; this derives
72
+ # the same type from the owner instead. Only fills in a name the block
73
+ # body did NOT already declare explicitly (checked AFTER instance_eval
74
+ # runs, so an existing bluebook still spelling it out both ways keeps
75
+ # working unchanged — this only removes the need to, never refuses
76
+ # the choice to). A block parameter matching nothing on the owner is
77
+ # left alone; MetaValidator's own unresolved-attribute check names it,
78
+ # the same way a typo in a hand-written `attribute` call already does.
79
+ def derive_from_owner!(owner_attributes, block)
80
+ block.parameters.each do |kind, param_name|
81
+ next unless %i[req opt].include?(kind)
82
+ next if attributes.any? { |a| a.name == param_name }
83
+
84
+ owner_attr = owner_attributes.find { |a| a.name == param_name }
85
+ next unless owner_attr
86
+
87
+ # `Attribute.new` DIRECTLY, not the public `attribute(...)` DSL
88
+ # entry — `owner_attr.type` is already SPELLED (a demodulised
89
+ # String, `Attribute#spell`'s own doing), not a bareword the
90
+ # bluebook author typed, so it must not run through
91
+ # `AttributeCollector#attribute`'s quoted-type refusal (ADR
92
+ # 0025, "Attributes") — that refusal exists for DSL source
93
+ # text, not for a type already resolved elsewhere and copied.
94
+ attributes << Attribute.new(name: param_name, type: owner_attr.type, optional: owner_attr.optional?)
95
+ end
96
+ end
97
+
98
+ # `cursor` parses, round-trips through the IR, and is read by nothing —
99
+ # no interpreter (Memory, Sqlite, Postgres) ever applies it. Refusing
100
+ # it here, rather than deleting the word, keeps the declared syntax
101
+ # honest (the language still knows the shape) while refusing to let a
102
+ # bluebook author believe cursor-based pagination actually happens.
103
+ def seal_cursor
104
+ return unless @cursor
105
+
106
+ raise Malformed,
107
+ "#{@name} declares cursor, but no interpreter implements cursor " \
108
+ "pagination — use limit/offset instead"
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,233 @@
1
+ require_relative "word_gate"
2
+ module Hecks
3
+ module Bluebook
4
+ module DSL
5
+ class ReadModelBuilder
6
+ GRAMMAR_CONTEXT = "ReadModel"
7
+
8
+ include QuerySpecification::Common::DSL
9
+ include WordGate
10
+
11
+ def initialize(name)
12
+ @name = name
13
+ end
14
+
15
+ def description(value)
16
+ # moved to the language: ProjectionText / purpose, on Projection.Declare
17
+ @description = value
18
+ end
19
+
20
+ # RENAMED FROM `reference_to` — item #13's full metaprogrammed
21
+ # dispatch (slice 4b). Bootstrap-reachable, in
22
+ # GenericDispatch::BOOTSTRAP_CALLS_FALLBACK.
23
+ def reference_to_impl(type, as: nil)
24
+ raise Malformed, "#{@name} already has a projection reference" if @reference_target
25
+
26
+ @reference_target = Naming.demodulise(type)
27
+ @reference_name = (as || Naming.snake(@reference_target)).to_sym
28
+ end
29
+
30
+ # Order-independent. `many:` is decided by comparing the included type
31
+ # against the reference target, so this used to REFUSE an include
32
+ # declared before the reference — a rule guarding an implementation
33
+ # limitation rather than a truth about read models. The includes are
34
+ # collected raw and resolved at build, when the reference is known, so
35
+ # there is no rule left to enforce.
36
+ # RENAMED FROM `include`/`group_by` — item #13's full
37
+ # metaprogrammed dispatch (slice 4c). `include` IS bootstrap-
38
+ # reachable (every core chapter's own `read_model` names which
39
+ # aggregates it includes with it — a first grep dismissed this
40
+ # as `Module#include` noise and was wrong; the cold-boot test
41
+ # after this rename caught it directly), so it's in
42
+ # BOOTSTRAP_CALLS_FALLBACK; `group_by` is not (no core read_model
43
+ # groups). The class-level `include WordGate` this file's own
44
+ # class body uses is `Module#include`, a different receiver,
45
+ # unaffected by renaming this INSTANCE method either way.
46
+ def include_impl(type, as: nil)
47
+ @includes ||= []
48
+ @includes << [Naming.demodulise(type), as]
49
+ end
50
+
51
+ # NAMES which of the eligible head's own fields to nest its rows
52
+ # under — one level per field, the leaf being that row with the
53
+ # named fields removed (they're already spent, as the keys that
54
+ # reached it). The same "exactly one many-side head" rule
55
+ # `seal_query_options` already enforces for where/order_by/etc
56
+ # applies here too (`seal_group_by`) — grouping is a question
57
+ # about ONE collection's own rows, same as those are.
58
+ def group_by_impl(*fields)
59
+ # Hash rows, `{field:}`, not bare symbols — same shape
60
+ # `aggregate_heads` already uses for exactly the reason it
61
+ # does: the language's own self-hosted grammar (`projection
62
+ # .bluebook`'s `GroupByField`) has to have SOMETHING to read a
63
+ # `field:` off of when `Judge` walks this list generically: a
64
+ # bare `Symbol` has no attribute of its own to read.
65
+ @group_by = fields.map { |field| { field: field.to_sym } }
66
+ end
67
+
68
+ # `count` -- a bare row count over the eligible many-side head's
69
+ # own rows (after `where`/`order_by`/`limit`/`offset` apply, the
70
+ # same rows `group_by` itself would nest) -- ANSWERS "how many
71
+ # match", not "which ones". A sibling REDUCTION to `group_by`,
72
+ # not a filter: `seal_aggregation` refuses combining it with
73
+ # `group_by` or with `median`, the same "exactly one many-side
74
+ # head" rule `seal_group_by` already enforces for the same
75
+ # reason -- a bare marker, so `@count` is left unset (nil, not
76
+ # false) rather than defaulted, matching the "ABSENT is not
77
+ # EMPTY" reading `Lifecycle`'s own optional fields already rely
78
+ # on for the Judge's setter dispatch (Behaviour::ReadModel#
79
+ # count?, ReadModelInterpreter#aggregation_target).
80
+ # `count` — item #13's full metaprogrammed dispatch, slice 1
81
+ # (whole-project table-unification survey): the ONLY Keyword row
82
+ # filling `count` — a bare marker, now stored as literal `true`
83
+ # by `GenericDispatch` off that same table fact.
84
+
85
+ # `median(field)` -- the median VALUE of one numeric field
86
+ # across the eligible many-side head's own rows. EVEN COUNT: the
87
+ # average of its two middle values (the standard definition,
88
+ # not "the lower of the two") -- see
89
+ # Runtime::ReadModelInterpreter#median for where that lands and
90
+ # is documented for a caller. `field` must name a numeric
91
+ # attribute (a bare numeric primitive, or a value object
92
+ # carrying one) -- checked once, at read time, by
93
+ # ReadModelInterpreter#aggregation_target, the same place
94
+ # `group_by`'s own field names are checked.
95
+ # `median` — item #13's full metaprogrammed dispatch, slice 1:
96
+ # same shape as `count`, above (a bare, kind-driven coerce-and-
97
+ # assign).
98
+
99
+ # `reference_to` is now OPTIONAL — a read model with no root is a
100
+ # BULK one: every `include`d head reads its own aggregate whole
101
+ # (no FK match against a root that doesn't exist), and dispatch
102
+ # takes no id argument at all. This used to be REQUIRED, on the
103
+ # assumption a read model was always "one root record's own
104
+ # cross-aggregate view" — true of every real corpus report so
105
+ # far, but not a truth about read models themselves: `group_by`'s
106
+ # own real use (nesting an aggregate's OWN whole table by its own
107
+ # field values) has no root to speak of. Still needs to describe
108
+ # SOMETHING — zero includes AND no reference is refused.
109
+ def build
110
+ raise Malformed,
111
+ "#{@name} needs an aggregate-head reference or at least one include" if !@reference_target && Array(@includes).empty?
112
+
113
+ Array(@includes).each do |target, as|
114
+ add_aggregate_head(target, as, many: target != @reference_target)
115
+ end
116
+ seal_query_options
117
+ seal_group_by
118
+ seal_aggregation
119
+ seal_cursor
120
+ ReadModel.new(name: @name, description: @description, reference_name: @reference_name,
121
+ reference_target: @reference_target, aggregate_heads: @aggregate_heads || [],
122
+ wheres: @wheres || [], order_by: @order_by, limit: @limit, offset: @offset,
123
+ cursor: @cursor,
124
+ authorization: @authorization, null_semantics: @null_semantics,
125
+ inspection: @inspection, group_by: @group_by || [],
126
+ count: @count, median_field: @median_field)
127
+ end
128
+
129
+ def self.build(name, &block)
130
+ builder = new(name)
131
+ builder.instance_eval(&block) if block
132
+ builder.build
133
+ end
134
+
135
+ private
136
+
137
+ # where/order_by/limit/offset/authorize's tenant all apply to exactly
138
+ # one collection — the single `include`d aggregate whose head is
139
+ # "many" (the "one" side, the reference target itself, is a single
140
+ # row; ordering, paging, or tenant-scoping one row means nothing). A
141
+ # read model with zero many-heads has nothing for them to filter ;
142
+ # one with several has no way to say WHICH of several unrelated
143
+ # collections a caller meant — so both refuse here rather than
144
+ # silently applying to an arbitrary one. `authorize`'s tenant counts
145
+ # here too — TenantScope enforces it against this same head, so an
146
+ # ambiguous target is exactly as unusable as it is for the others.
147
+ def seal_query_options
148
+ declared = @wheres&.any? || @order_by || @limit || @offset || @authorization&.tenant
149
+ return unless declared
150
+
151
+ many = Array(@aggregate_heads).count { |head| head[:many] }
152
+ return if many == 1
153
+
154
+ raise Malformed,
155
+ "#{@name} declares where/order_by/limit/offset but includes #{many} many-side " \
156
+ "aggregates, not exactly one — these options apply to a single collection; " \
157
+ "name which one by including only it, or drop the options"
158
+ end
159
+
160
+ # Same shape as `seal_query_options`, same reason — `group_by`
161
+ # answers a question about ONE collection's own rows, so zero or
162
+ # several many-side heads leaves it with no unambiguous target.
163
+ def seal_group_by
164
+ return unless @group_by&.any?
165
+
166
+ many = Array(@aggregate_heads).count { |head| head[:many] }
167
+ return if many == 1
168
+
169
+ raise Malformed,
170
+ "#{@name} declares group_by but includes #{many} many-side " \
171
+ "aggregates, not exactly one — group_by nests a single collection's " \
172
+ "own rows; name which one by including only it"
173
+ end
174
+
175
+ # `count`/`median` are the OTHER two reductions a read model may
176
+ # declare over its one eligible collection — same "exactly one
177
+ # many-side head" rule as `seal_group_by`, plus a rule
178
+ # `seal_group_by` doesn't need: a read model reports ONE shape,
179
+ # so `count` and `median` cannot both be declared, and neither
180
+ # may combine with `group_by` (nesting rows and reducing them to
181
+ # a scalar are answers to different questions ; a caller asking
182
+ # "how many, nested by state" is not a shape this read model
183
+ # produces today — a real, deliberate scope limit, not an
184
+ # oversight, the same discipline the rootless model's own
185
+ # "each head reads independently" limit already documents).
186
+ def seal_aggregation
187
+ return unless @count || @median_field
188
+
189
+ if @count && @median_field
190
+ raise Malformed,
191
+ "#{@name} declares both count and median — a read model reports " \
192
+ "one shape; choose one"
193
+ end
194
+ if @group_by&.any?
195
+ raise Malformed,
196
+ "#{@name} declares count/median together with group_by — a read " \
197
+ "model reports one shape; choose one"
198
+ end
199
+
200
+ many = Array(@aggregate_heads).count { |head| head[:many] }
201
+ return if many == 1
202
+
203
+ raise Malformed,
204
+ "#{@name} declares count/median but includes #{many} many-side " \
205
+ "aggregates, not exactly one — count/median reduce a single " \
206
+ "collection's own rows; name which one by including only it"
207
+ end
208
+
209
+ # `cursor` parses, round-trips through the IR, and is read by nothing —
210
+ # no interpreter (Memory, Sqlite, Postgres) ever applies it. Refusing
211
+ # it here, rather than deleting the word, keeps the declared syntax
212
+ # honest (the language still knows the shape) while refusing to let a
213
+ # bluebook author believe cursor-based pagination actually happens.
214
+ def seal_cursor
215
+ return unless @cursor
216
+
217
+ raise Malformed,
218
+ "#{@name} declares cursor, but no interpreter implements cursor " \
219
+ "pagination — use limit/offset instead"
220
+ end
221
+
222
+ def add_aggregate_head(type, name, many:)
223
+ @aggregate_heads ||= []
224
+ target = Naming.demodulise(type)
225
+ output = (name || (many ? Naming.plural(Naming.snake(target)) : Naming.snake(target))).to_sym
226
+ raise Malformed, "#{@name} already projects #{output}" if @aggregate_heads.any? { |head| head[:as] == output }
227
+
228
+ @aggregate_heads << { aggregate: target, as: output, many: many }
229
+ end
230
+ end
231
+ end
232
+ end
233
+ end