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,306 @@
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
+ # `starts_on Transfer::TransferRequested` — BARE CONSTANT
18
+ # ACCEPTED (ADR 0025, S6 — "events first-class"), resolved
19
+ # through `ConstShim` the same way `on_impl`/`transition_impl`
20
+ # resolve an event reference — but through `Naming.
21
+ # event_name_ref`, NOT `Naming.event_ref` (that method's own
22
+ # header has the full account: `SagaInterpreter` matches
23
+ # `pm.starts_on`/`pm.ends_on` against a BARE `event.name`, never
24
+ # a "." qualified one). A plain String still passes through
25
+ # unchanged, both for `shadow_parse` and for any corpus site a
26
+ # future pass hasn't migrated yet.
27
+ def starts_on_impl(event_ref)
28
+ @starts_on = Naming.event_name_ref(event_ref)
29
+ end
30
+
31
+ # `ends_on` — same reasoning as `starts_on_impl`, above.
32
+ def ends_on_impl(event_ref)
33
+ @ends_on = Naming.event_name_ref(event_ref)
34
+ end
35
+
36
+ # `correlates_by` — item #13's full metaprogrammed dispatch,
37
+ # slice 1 (whole-project table-unification survey): a bare,
38
+ # kind-driven coerce-and-assign with nothing else, executed by
39
+ # `GenericDispatch`.
40
+ #
41
+ # `starts_on`/`ends_on` USED TO be table-driven the same way,
42
+ # until ADR 0025 S6 gave each a second, `kind: "constant"`
43
+ # ArgumentSeed row (`starts_on Transfer::TransferRequested`) —
44
+ # `GenericDispatch::COERCE_BY_KIND` only knows `"text"`/
45
+ # `"symbol"`, and `shape_for` refuses to pick a `single_fill`
46
+ # shape at all once two Argument rows share a `fills:` target
47
+ # (`arguments.size == 1` below), so both words are hand-written
48
+ # again, `calls:`-routed like `transition` already is.
49
+
50
+ # ONE STATE-MACHINE VOCABULARY (S7, ADR 0025 — "events and
51
+ # reactions"): the SAME word `Lifecycle#transition` already
52
+ # carries, one level over — `transition "AccountDebited" =>
53
+ # "awaiting_credit", from: "requested" do ... end` replaces `on
54
+ # "AccountDebited", transition: { "requested" => "awaiting_
55
+ # credit" } do ... end`. Same bare rocket-pair argument shape
56
+ # (not a NAMED `transition:` kwarg wrapping a second Hash), same
57
+ # `from:` — including the array form Lifecycle's own commands
58
+ # could already take and a process manager's own events could
59
+ # not — and the states a procedure runs on are DERIVED from the
60
+ # transitions that name them, the same way `Behaviour::Lifecycle
61
+ # #states` already derives an aggregate's ; `state "x"` lines
62
+ # duplicated exactly what the transition list already said,
63
+ # and could drift from it (`validate!`'s own "undeclared state"
64
+ # check existed only because they could).
65
+ #
66
+ # `starts_on`/`ends_on` are NOT unified into this — verified
67
+ # against the real corpus rather than assumed: Settlement's own
68
+ # `ends_on "TransferSettled"` names an event NONE of its own
69
+ # transitions ever handle (`Transfer.Settle`'s own emission, a
70
+ # full step downstream of the transition that dispatches it),
71
+ # so "the terminal state's own event" is not a fact the
72
+ # transition graph carries — deriving it would either be wrong
73
+ # for this exact corpus member or need a second new word to
74
+ # cover the case, which is not less vocabulary than keeping the
75
+ # one that already says it correctly.
76
+ #
77
+ # EXPANDS IMMEDIATELY, unlike `Lifecycle#transition` (which
78
+ # defers to `Behaviour::Lifecycle#expand`, called at emission
79
+ # time) — `ProcessManager`'s own IR constructor takes `states:`/
80
+ # `handlers:` exactly as it always has, so the runtime
81
+ # (`Behaviour::ProcessManager`, `SagaInterpreter`, saga
82
+ # persistence/rehydration) needs no change at all: what changed
83
+ # is how the DECLARATION reaches that same shape, not the shape
84
+ # a real run ever sees or persists.
85
+ # RENAMED FROM `transition` — item #13's full metaprogrammed
86
+ # dispatch (slice 4c). Not bootstrap-reachable (checked
87
+ # directly — no core/attached chapter declares a ProcessManager
88
+ # of its own).
89
+ def transition_impl(mapping, &block)
90
+ mapping = mapping.dup
91
+ from = mapping.delete(:from)
92
+
93
+ # ALWAYS REQUIRED, unlike `Lifecycle#transition`'s own `from:`
94
+ # — an aggregate's unconstrained transition is admitted from
95
+ # ANY current state (`Behaviour::Lifecycle#applies_from?`
96
+ # returns true for a nil `from`), a reading `SagaInterpreter#
97
+ # advance_saga`'s own admission check does not share: it tests
98
+ # `instance[:state] == handler.from_state` by plain equality,
99
+ # nothing softer. Leaving that check unchanged (this slice's
100
+ # own scope decision — see the class-level comment on why the
101
+ # runtime stays untouched) means an unconstrained PM
102
+ # transition would build cleanly and then match no instance
103
+ # ever, silently — refused here instead, at the one point that
104
+ # can still see the mistake.
105
+ if from.nil?
106
+ raise InvalidProcessManager,
107
+ "#{@name}'s transition #{mapping.inspect} names no from: — a process manager's own " \
108
+ "admission checks a saga instance's CURRENT state exactly, so a transition with no " \
109
+ "from: would match no instance ever, silently"
110
+ end
111
+
112
+ handler = HandlerBuilder.new
113
+ handler.instance_eval(&block) if block
114
+
115
+ mapping.each do |event_type, target|
116
+ # BARE CONSTANT ACCEPTED (ADR 0025, S6 — "events first-
117
+ # class"), `transition Account::AccountDebited => "state"` —
118
+ # `Naming.event_name_ref`, NOT the DOTTED `Naming.event_ref`
119
+ # transform `PolicyBuilder#on_impl` uses (that method's own
120
+ # header has the full account, found live wiring a real
121
+ # migrated corpus site into `bin/model_check` for the first
122
+ # time: `SagaInterpreter#advance_saga` matches `handler.
123
+ # event_type` against a BARE `event.name`, never a "."
124
+ # qualified one — a policy's own cross-aggregate match
125
+ # works differently, splitting the qualifier apart from the
126
+ # name rather than comparing the whole string). Writing the
127
+ # qualifier is still worth it (the same provenance `trigger
128
+ # Account::Debit` gives a reader) — `event_name_ref` keeps
129
+ # only the final segment, so `Account::AccountDebited` and a
130
+ # bare `AccountDebited` store identically. A plain String
131
+ # still passes through unchanged, both for `shadow_parse`
132
+ # and for every corpus site this pass didn't migrate.
133
+ state_transition = StateTransition.new(target: target, from: from)
134
+ expand(Naming.event_name_ref(event_type), state_transition, handler.dispatches).each { |row| @handlers << row }
135
+ end
136
+ end
137
+
138
+ def build
139
+ validate!
140
+
141
+ ProcessManager.new(
142
+ name: @name,
143
+ correlates_by: @correlates_by,
144
+ starts_on: @starts_on,
145
+ ends_on: @ends_on,
146
+ states: derived_states,
147
+ handlers: @handlers
148
+ )
149
+ end
150
+
151
+ def self.build(name, &block)
152
+ builder = new(name)
153
+ builder.instance_eval(&block) if block
154
+ builder.build
155
+ end
156
+
157
+ private
158
+
159
+ # ONE DECLARED TRANSITION IS SEVERAL ROWS when `from` names more
160
+ # than one source state — `Behaviour::Lifecycle#expand`'s own
161
+ # comment, the identical fan-out, one level over: a
162
+ # `ProcessManagerHandler` only ever carries a single `from_state`,
163
+ # so a `from: [...]` transition mints one row per source, each
164
+ # carrying the SAME dispatches.
165
+ def expand(event_type, transition, dispatches)
166
+ sources = transition.from.nil? ? [nil] : Array(transition.from)
167
+
168
+ sources.map do |source|
169
+ ProcessManagerHandler.new(
170
+ event_type: event_type,
171
+ from_state: source.to_s,
172
+ to_state: transition.target,
173
+ dispatches: dispatches
174
+ )
175
+ end
176
+ end
177
+
178
+ # DERIVED, not declared (S7) — every state this procedure ever
179
+ # runs on is already named by some transition's own `from_state`
180
+ # or `to_state`; a state nothing transitions into or out of is
181
+ # not a state this procedure has, the same reading
182
+ # `Behaviour::Lifecycle#states` already gives an aggregate's own
183
+ # field. FIRST-SEEN ORDER, walking declaration order — `begin_
184
+ # saga`'s own `pm.states.first` is what a fresh instance starts
185
+ # in, so the order has to survive the derivation, not just the
186
+ # membership.
187
+ def derived_states
188
+ @handlers.flat_map { |h| [h.from_state, h.to_state] }.reject(&:empty?).uniq
189
+ end
190
+
191
+ def validate!
192
+ raise InvalidProcessManager, "#{@name} declares no correlates_by — " \
193
+ "nothing would tie its events to one instance" unless @correlates_by
194
+
195
+ # THE FIELD, NAMED — never the value object that carries it. A bare
196
+ # `correlates_by :end_to_end` reads whatever the payload holds under
197
+ # that key AS the correlation key, and what a non-scalar key even
198
+ # is stays open (the object itself? its serialised text?).
199
+ # Requiring the dotted spelling —
200
+ # `:"end_to_end.value"` — makes every correlates_by name a scalar
201
+ # by construction, the same discipline `identified_by` already
202
+ # holds a head to. This is a syntactic check, not a type check: it
203
+ # does not know or care whether the field IS a value object, only
204
+ # that the declaration cannot leave that question open.
205
+ raise InvalidProcessManager, "#{@name} correlates_by #{@correlates_by.inspect}, which names a whole " \
206
+ "field rather than one of its scalars — say which one, e.g. " \
207
+ "#{@correlates_by}.value" unless @correlates_by.to_s.include?(".")
208
+
209
+ raise InvalidProcessManager, "#{@name} declares no starts_on — " \
210
+ "nothing would ever begin it" if @starts_on.to_s.empty?
211
+
212
+ raise InvalidProcessManager, "#{@name} declares no transitions — " \
213
+ "it would start and then ignore every event" if @handlers.empty?
214
+ end
215
+
216
+ class HandlerBuilder
217
+ GRAMMAR_CONTEXT = "Handler"
218
+
219
+ attr_reader :dispatches
220
+
221
+ include WordGate
222
+
223
+ def initialize = @dispatches = []
224
+
225
+ # THE COMMAND ITSELF (ADR 0025, "events and reactions" — command
226
+ # references become first-class), same shape and same reasons
227
+ # as `PolicyBuilder#trigger`'s own header — bare constant live,
228
+ # quoted text only under shadow-parsing (S0a's bridge; frozen
229
+ # era text still writes `dispatch "Banking::Account.Debit"`).
230
+ #
231
+ # RENAMED FROM `dispatch` — item #13's full metaprogrammed
232
+ # dispatch (slice 4), same reasoning as trigger_impl above.
233
+ #
234
+ # AN OPTIONAL BLOCK OPENS `compensates` ON THIS DISPATCH
235
+ # SPECIFICALLY — per-dispatch saga compensation, replacing a
236
+ # hand-written list at the saga's own `on :refused` leg. Real,
237
+ # live bug this closes: `examples/banking/bluebook/transfers_
238
+ # and_payments.bluebook`'s own `Settlement` saga wrote a
239
+ # compensating reversal by hand that depended on an event only
240
+ # fired if someone dispatched it manually — "the reversal was
241
+ # written and never armed" (that file's own comment). The
242
+ # runtime now tracks which legs actually completed
243
+ # (`SagaInterpreter`'s own `completed_compensations`) and
244
+ # compensates only those, newest first, instead of trusting an
245
+ # author's static list to be complete and correctly ordered.
246
+ def dispatch_impl(command_ref, with: nil, &block)
247
+ if command_ref.is_a?(::String) && !MetaValidator.shadow_parsing?
248
+ raise InvalidProcessManager,
249
+ "dispatch #{command_ref.inspect} is quoted text — give the bare command constant " \
250
+ "instead, e.g. dispatch Account::Debit"
251
+ end
252
+
253
+ spec = DispatchSpec.new(
254
+ command_name: Naming.command_ref(command_ref),
255
+ with_spec: (with || {}).to_a
256
+ )
257
+ spec.instance_variable_set(:@projection_declared, !with.nil?)
258
+
259
+ if block
260
+ builder = DispatchBuilder.new
261
+ builder.instance_eval(&block)
262
+ # `instance_variable_get`, not a public `compensates_spec`
263
+ # reader — a public one would be a method the grammar
264
+ # never declares, exactly what `spec/syntax_conformance_
265
+ # spec.rb`'s "answers only words the language declares"
266
+ # check exists to catch.
267
+ spec.compensates = builder.instance_variable_get(:@compensates_spec)
268
+ end
269
+
270
+ @dispatches << spec
271
+ spec
272
+ end
273
+
274
+ # THE NESTED SCOPE `dispatch ... do ... end` OPENS — one word
275
+ # only (`compensates`), the compensating half of the dispatch it
276
+ # sits inside. Its own `compensates_impl` builds a SECOND
277
+ # `DispatchSpec`, shape-identical to `HandlerBuilder#dispatch_
278
+ # impl`'s own — a compensation takes the exact same two
279
+ # arguments (a bare command constant, an optional `with:`)
280
+ # because it resolves through the identical scope (current
281
+ # event payload, opening event memory, correlation binding)
282
+ # any saga dispatch already does
283
+ # (`SagaInterpreter#dispatch_args`).
284
+ class DispatchBuilder
285
+ GRAMMAR_CONTEXT = "Dispatch"
286
+
287
+ include WordGate
288
+
289
+ def compensates_impl(command_ref, with: nil)
290
+ if command_ref.is_a?(::String) && !MetaValidator.shadow_parsing?
291
+ raise InvalidProcessManager,
292
+ "compensates #{command_ref.inspect} is quoted text — give the bare command constant " \
293
+ "instead, e.g. compensates Account::Credit"
294
+ end
295
+
296
+ @compensates_spec = DispatchSpec.new(
297
+ command_name: Naming.command_ref(command_ref),
298
+ with_spec: (with || {}).to_a
299
+ )
300
+ end
301
+ end
302
+ end
303
+ end
304
+ end
305
+ end
306
+ 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