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,341 @@
1
+ require_relative "../rendering"
2
+ require_relative "errors"
3
+ require_relative "value"
4
+ require_relative "refusal_wording"
5
+ require_relative "tenant_scope"
6
+ require_relative "../ports/query/in_memory"
7
+ require_relative "../query_specification/field_path"
8
+
9
+ module Hecks
10
+ module Runtime
11
+ class ReadModelInterpreter
12
+ def initialize(registry) = @registry = registry
13
+
14
+ def call(domain, model, args)
15
+ project(domain, model, args)
16
+ end
17
+
18
+ private
19
+
20
+ def project(domain, model, args)
21
+ bluebook = @registry.bluebook(domain)
22
+ rootless = model.reference_target.nil?
23
+ # BEFORE the adapter early-return below, so the SQLite path inherits it.
24
+ # Without this a stale caller passing a wrapped reference gets a
25
+ # path-dependent answer — an adapter could quietly open the wrapped
26
+ # reference while the in-process path reads it whole and finds nothing.
27
+ # Refused up front, identically on every path. Nothing to refuse for
28
+ # a rootless model — there's no reference argument to have offered
29
+ # wrong at all.
30
+ refuse_object_reference(model, args) unless rootless
31
+ reference_id = reference(args.fetch(model.reference_name)) unless rootless
32
+ # Computed off the ORIGINAL model, before TenantScope wraps it — the
33
+ # "which head do options apply to" question is about what the
34
+ # bluebook author declared, not about the synthetic tenant clause
35
+ # the wrapper adds underneath.
36
+ eligible = model.filtered_head_name
37
+ model = TenantScope.apply(model, args)
38
+ # A ROOTLESS, `group_by`-declared, or `count`/`median`-declared
39
+ # model skips the SQLite native escape hatch entirely (there is
40
+ # no root aggregate to look up a repository FOR when rootless,
41
+ # and `query_read_model` knows nothing about grouping or
42
+ # reducing) — always runs the in-process loop below instead.
43
+ # Correct everywhere ; not SQL-pushed-down for a SQLite-backed
44
+ # aggregate yet, a real, named limit, not a silent one — the
45
+ # same one `group_by` already accepted, extended here rather
46
+ # than narrowed.
47
+ unless rootless || model.group_by.any? || model.count? || model.median_field
48
+ repository = @registry.read_repository(domain, bluebook.aggregate(model.reference_target))
49
+ return repository.query_read_model(domain, model, args, bluebook) if repository.respond_to?(:query_read_model) && repository.adapter.respond_to?(:query_read_model)
50
+ end
51
+
52
+ # ROOT FIRST, ALWAYS — regardless of `include` order in the
53
+ # bluebook. `read_model_builder.rb`'s own `include` is
54
+ # documented "Order-independent" (the `:many` flag is resolved
55
+ # at build time, once `@reference_target` is known), but that
56
+ # promise was never kept HERE: this loop used to run heads in
57
+ # their literal declared order and match each "many" head
58
+ # against whatever was ALREADY in `projected` — empty, the
59
+ # very first time through, if a many-side head happened to be
60
+ # declared before the root. A real, live bug (not a guess):
61
+ # `include Promotion` before `include Item` on a read model
62
+ # whose root IS Item silently returned an empty array for
63
+ # Promotion — no error, just a wrong, too-small answer — while
64
+ # the reverse order worked purely by accident. `partition`,
65
+ # not `sort_by`: Ruby's `sort_by` is not guaranteed stable,
66
+ # and correctness here must not depend on it being so by
67
+ # chance on the current MRI build.
68
+ root_heads, other_heads = model.aggregate_heads.partition { |head| head[:aggregate] == model.reference_target }
69
+ projected = []
70
+ rows_by_as = {}
71
+ (root_heads + order_other_heads(bluebook, root_heads, other_heads)).each do |head|
72
+ rows = if head[:aggregate] == model.reference_target
73
+ [fetch(bluebook, domain, head[:aggregate], reference_id)]
74
+ elsif rootless
75
+ # No root to FK-match against — a rootless model reads
76
+ # each of its own heads WHOLE, independently. (Multiple
77
+ # heads on one rootless model aren't cross-joined
78
+ # against each other either — each is its own bulk
79
+ # read. A real, deliberate scope limit for now.)
80
+ records(bluebook, domain, head[:aggregate])
81
+ else
82
+ matching(records(bluebook, domain, head[:aggregate])) do |record|
83
+ projected.any? do |source|
84
+ reference_fields(bluebook.aggregate(head[:aggregate]), source[:aggregate]).any? do |field|
85
+ source[:rows].any? { |parent| reference(record[field]) == parent.id }
86
+ end
87
+ end
88
+ end
89
+ end
90
+ rows = Ports::Query::InMemory.execute(rows, model, args) if head[:as] == eligible
91
+ projected << { aggregate: head[:aggregate], rows: rows }
92
+ rows_by_as[head[:as]] = head[:many] ? rows : rows.first
93
+ end
94
+ # Declared order preserved in the OUTPUT — only the
95
+ # computation above needed reordering, not what a caller sees
96
+ # back.
97
+ heads = model.aggregate_heads.each_with_object({}) { |head, report| report[head[:as]] = rows_by_as[head[:as]] }
98
+ grouped_head = group_by_target(model, bluebook)
99
+ reduced_head = aggregation_target(model, bluebook)
100
+ [heads.each_with_object({}) do |(as, value), out|
101
+ out[as] = if grouped_head && as == grouped_head[:as]
102
+ nest(value.map { |record| Value.materialize_unwrapped(row(record)) }, model.group_by_fields)
103
+ elsif reduced_head && as == reduced_head[:as] && model.count?
104
+ value.length
105
+ elsif reduced_head && as == reduced_head[:as]
106
+ median(value, model.median_field)
107
+ elsif value.is_a?(Array)
108
+ value.map { |record| Value.materialize(row(record)) }
109
+ else
110
+ Value.materialize(row(value))
111
+ end
112
+ end]
113
+ end
114
+
115
+ # THE ROOT-FIRST FIX'S OWN FIX — root-first alone only reaches one
116
+ # level: it guarantees the root is in `projected` before any other
117
+ # head is matched, but a CHAIN of non-root heads (a head that
118
+ # references another non-root head, not the root) is still
119
+ # matched against whatever declaration order happened to put in
120
+ # `projected` so far. `include Coupon` before `include Promotion`
121
+ # on a read model rooted at Item, where Coupon references
122
+ # Promotion (which references Item), silently returned an empty
123
+ # `coupons` array — Coupon's match ran while `projected` held only
124
+ # Item, one level short of what it needed.
125
+ #
126
+ # Fixed the same way root-first was: not by asking bluebook authors
127
+ # to declare `include` in dependency order (the same promise
128
+ # `read_model_builder.rb` already makes and this file is the one
129
+ # place obligated to keep), but by topologically sorting the
130
+ # non-root heads on their OWN declared reference fields before
131
+ # this method's runtime matching ever runs — Kahn's algorithm,
132
+ # picking ready heads in DECLARED order at each step so declaring
133
+ # order still governs whenever there is no dependency to break a
134
+ # tie. This generalizes root-first (a chain of length 1) to a
135
+ # chain of any depth, and to a head depending on more than one
136
+ # other head at once (not just a straight chain).
137
+ #
138
+ # A cycle among non-root heads (A references B which references A)
139
+ # has no valid topological order at all — falls back to the
140
+ # remaining heads' declared order rather than looping forever, the
141
+ # same "whichever runs first finds nothing" behaviour this method
142
+ # had for every non-root head before root-first existed.
143
+ def order_other_heads(bluebook, root_heads, other_heads)
144
+ resolved = root_heads.map { |head| head[:aggregate] }
145
+ remaining = other_heads.dup
146
+ ordered = []
147
+ until remaining.empty?
148
+ ready, blocked = remaining.partition do |head|
149
+ depends_on(bluebook, head, other_heads).all? { |target| resolved.include?(target) }
150
+ end
151
+ if ready.empty?
152
+ ordered.concat(remaining)
153
+ break
154
+ end
155
+ ordered.concat(ready)
156
+ resolved.concat(ready.map { |head| head[:aggregate] })
157
+ remaining = blocked
158
+ end
159
+ ordered
160
+ end
161
+
162
+ # Which OTHER declared (non-root) heads a head's own aggregate
163
+ # holds a reference field toward — the same relationship this
164
+ # file's runtime matching checks record-by-record, asked here
165
+ # statically, once, to order heads before any record is read.
166
+ #
167
+ # `head[:aggregate]` names whatever `include` was given — and
168
+ # `include` accepts a nested ENTITY (Member, nested under
169
+ # ValueObject ; Handler and Dispatch, nested under ProcessManager
170
+ # — bluebook.bluebook's own `WholeBluebook` read model includes
171
+ # all three) just as readily as a top-level aggregate.
172
+ # `bluebook.aggregate` only ever finds the latter
173
+ # (Behaviour::Chapter#aggregate searches `@aggregates`, which
174
+ # holds no entities), so it returns nil for an entity-headed
175
+ # include — a real case, not a malformed one. `records`, below,
176
+ # already treats that nil as "no rows of its own to fetch" ;
177
+ # a head with no rows of its own has nothing to check for a
178
+ # reference field either, so it depends on nothing here, the
179
+ # same as it always silently read empty before this file's
180
+ # topological sort existed.
181
+ def depends_on(bluebook, head, other_heads)
182
+ aggregate = bluebook.aggregate(head[:aggregate])
183
+ return [] unless aggregate
184
+
185
+ other_heads.reject { |other| other[:aggregate] == head[:aggregate] }
186
+ .select { |other| reference_fields(aggregate, other[:aggregate]).any? }
187
+ .map { |other| other[:aggregate] }
188
+ end
189
+
190
+ # `group_by`'s own declared fields, checked against the ONE
191
+ # many-side head they apply to (`seal_group_by` already refuses
192
+ # zero or several) — resolved here, once, rather than re-derived
193
+ # per row. Raises loudly on a typo'd field name rather than
194
+ # silently grouping every row into one bucket keyed `nil`.
195
+ def group_by_target(model, bluebook)
196
+ return nil unless model.group_by.any?
197
+
198
+ target = model.aggregate_heads.find { |head| head[:many] }
199
+ aggregate = bluebook.aggregate(target[:aggregate])
200
+ model.group_by_fields.each do |field|
201
+ next if aggregate.attribute(field)
202
+ # THE LIFECYCLE FIELD IS A FIELD, and refusing it here was a drift
203
+ # between two halves of the same language: `where(status: "logged")`
204
+ # has always been legal on the same aggregate, because a lifecycle
205
+ # state is stored on the record like anything else — it is simply
206
+ # declared by `lifecycle :status` rather than by `attribute`.
207
+ #
208
+ # It is also the grouping anybody actually wants. "How are we doing"
209
+ # over a bug ledger IS the count per status, and a report that could
210
+ # group by every field EXCEPT that one could not answer the question
211
+ # reports exist for.
212
+ next if aggregate.lifecycle && aggregate.lifecycle.field.to_sym == field.to_sym
213
+
214
+ raise ArgumentError,
215
+ "#{model.name}'s group_by names #{field.inspect}, but #{target[:aggregate]} " \
216
+ "declares no such attribute (it declares #{aggregate.attributes.map(&:name).join(', ')})"
217
+ end
218
+ target
219
+ end
220
+
221
+ # One level of nesting per field, in `group_by`'s own declared
222
+ # order — the leaf is the row with every grouped field removed
223
+ # (already spent, as the keys that reached it). ASSUMES the full
224
+ # `group_by` path uniquely identifies one row (true for grouping by
225
+ # an aggregate's own full identity, ConsoleSettings' own real use)
226
+ # — `leaves.first` silently keeps only the first row when several
227
+ # share the same full key path. A real, deliberate scope limit:
228
+ # true multi-row-per-leaf grouping would change the leaf shape
229
+ # from "one row" to "an array of rows", which no caller needs yet.
230
+ def nest(rows, fields)
231
+ field, *rest = fields
232
+ rows.group_by { |row| row[field] }.transform_values do |group|
233
+ # Strip ONLY the field just grouped by, not the whole remaining
234
+ # list — `rest`'s own fields have to survive into the recursive
235
+ # call below, or the NEXT level groups by a key that's already
236
+ # gone (found by trying it: a two-field group_by's own second
237
+ # level came back keyed `nil` for every group, every time).
238
+ stripped = group.map { |row| row.reject { |key, _| key == field } }
239
+ rest.empty? ? stripped.first : nest(stripped, rest)
240
+ end
241
+ end
242
+
243
+ # `count`/`median`'s own declared target — the SAME single
244
+ # many-side head `group_by_target` resolves, for the same reason
245
+ # (`seal_aggregation` already refuses zero or several many-side
246
+ # heads, and refuses count/median declared alongside group_by, so
247
+ # there is exactly one to name whenever this is asked at all).
248
+ # Raises loudly on a `median` field that doesn't exist, or exists
249
+ # but isn't numeric, rather than silently comparing garbage or
250
+ # averaging strings.
251
+ def aggregation_target(model, bluebook)
252
+ return nil unless model.count? || model.median_field
253
+
254
+ target = model.aggregate_heads.find { |head| head[:many] }
255
+ return target unless model.median_field
256
+
257
+ aggregate = bluebook.aggregate(target[:aggregate])
258
+ attribute = aggregate.attribute(model.median_field)
259
+ unless attribute
260
+ raise ArgumentError,
261
+ "#{model.name}'s median names #{model.median_field.inspect}, but #{target[:aggregate]} " \
262
+ "declares no such attribute (it declares #{aggregate.attributes.map(&:name).join(', ')})"
263
+ end
264
+ unless QuerySpecification::FieldPath.numeric?(attribute, []) { |type| aggregate.value_object(type) }
265
+ raise ArgumentError,
266
+ "#{model.name}'s median names #{model.median_field.inspect} on #{target[:aggregate]}, " \
267
+ "which is not numeric — median needs a numeric field (a bare number, or a " \
268
+ "value object carrying one)"
269
+ end
270
+ target
271
+ end
272
+
273
+ # THE STANDARD DEFINITION. An ODD count's median is its one true
274
+ # middle value, sorted ; an EVEN count's median is the AVERAGE of
275
+ # its two middle values — the common convention (as opposed to,
276
+ # say, always taking the lower of the two), and the one this
277
+ # session's own task named explicitly as the deliberate choice.
278
+ # An EMPTY collection has no median: nil, not zero, so a caller
279
+ # cannot mistake "nothing to average" for "the values averaged to
280
+ # zero". Reuses `Ports::Query::InMemory`'s own field reading
281
+ # (`FieldPath.dig` + `comparable`) — the same unwrap `where`/
282
+ # `order_by` already give a value-object-carrying field, so
283
+ # `median :daily_limit` on a `DailyLimit{cents}` field reads the
284
+ # same scalar an `order_by :daily_limit` comparison already would.
285
+ def median(rows, field)
286
+ values = rows.map { |record| Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(row(record), field)) }
287
+ .compact.sort
288
+ return nil if values.empty?
289
+
290
+ middle = values.length / 2
291
+ values.length.odd? ? values[middle] : (values[middle - 1] + values[middle]) / 2.0
292
+ end
293
+
294
+ def fetch(bluebook, domain, aggregate_name, id)
295
+ @registry.read_repository(domain, bluebook.aggregate(aggregate_name)).find(id) ||
296
+ raise(NotFound, RefusalWording.render("NotFound", "read_model_reference_missing",
297
+ aggregate: aggregate_name, offered: Rendering.describe(id)))
298
+ end
299
+
300
+ def records(bluebook, domain, aggregate_name)
301
+ aggregate = bluebook.aggregate(aggregate_name)
302
+ aggregate ? @registry.read_repository(domain, aggregate).all : []
303
+ end
304
+
305
+ # A stored reference holds the target's id inside the REFERENCE
306
+ # ATTRIBUTE's own declared shape, so reading it is reading that shape —
307
+ # a different thing from the identity unwrap that was removed. An
308
+ # IDENTITY is declared as a path and followed (Runtime::Identity) ; a
309
+ # reference has no path of its own, and `Value.scalar` refuses a
310
+ # composite rather than guessing which field was meant.
311
+ #
312
+ # Storing the scalar itself would remove this reading altogether. That
313
+ # is a change to how references are STORED, not to how identities are
314
+ # declared, so it is not made here.
315
+ # An ask names itself where a command would name itself, and says the
316
+ # same thing about the same shape. `Value.refuse_object_reference` is
317
+ # not reused because it speaks of a COMMAND and its attribute ; a read
318
+ # model has a query name and one declared reference.
319
+ def refuse_object_reference(model, args)
320
+ offered = args.fetch(model.reference_name, nil)
321
+ return unless offered.is_a?(Hash) || offered.is_a?(Value)
322
+
323
+ raise TypeMismatch,
324
+ RefusalWording.render("TypeMismatch", "read_model_object_reference",
325
+ query: model.query_name, field: model.reference_name)
326
+ end
327
+
328
+ # A reference is the id, in the argument and in the stored row alike.
329
+ def reference(value) = value.to_s
330
+
331
+ def reference_fields(aggregate, target)
332
+ aggregate.attributes
333
+ .select { |attribute| attribute.reference? && attribute.type.target_name == target.to_s }
334
+ .map(&:name)
335
+ end
336
+
337
+ def matching(records) = records.select { |record| yield(record) }.sort_by(&:id)
338
+ def row(record) = record.to_h
339
+ end
340
+ end
341
+ end
@@ -0,0 +1,74 @@
1
+ module Hecks
2
+ module Runtime
3
+ # THE OUT-OF-BAND HALF OF `projects` (S12, ADR 0025 — "Consistency
4
+ # across aggregate boundaries"). A projected field is never written
5
+ # by the command that reads it — nothing at dispatch time takes a
6
+ # live cross-aggregate read the way `CommandRules::References
7
+ # #dereference` still does — so this is the one place a projected
8
+ # field's value actually gets copied over: walk every record of the
9
+ # OWNING aggregate, resolve each of its own `projected_fields`
10
+ # through the reference it names, and `save` the local copy.
11
+ #
12
+ # EXPLICIT AND CALLABLE, NOT AUTOMATIC — no on-boot detection of a
13
+ # freshly-declared `projects` with no held-era precedent, no
14
+ # generated `Policy#for_each` reaction keeping it live in real
15
+ # time as the target changes. Both are real extensions this same
16
+ # ADR section describes; both are deliberately deferred. This is
17
+ # the mechanism a caller reaches for by hand — `bin/rebuild
18
+ # Domain::Aggregate`, a scheduled job, whatever the deployment
19
+ # needs — proven to work end to end before either automatic
20
+ # trigger is built on top of it.
21
+ #
22
+ # NEEDS NO NEW ADAPTER CAPABILITY. `find`/`all`/`save` are the same
23
+ # three primitives every real adapter already answers identically
24
+ # (`Ports::Persistence::AppendOnly#save` — append, then project,
25
+ # the same for Memory/Postgres/SQLite/D1/Heki) — confirmed by
26
+ # direct reading before this was built, not assumed.
27
+ module RebuildSweep
28
+ module_function
29
+
30
+ # One aggregate's own projected fields, refreshed across every
31
+ # record it holds. Returns how many records actually changed —
32
+ # `save` only runs when a projected value would differ from what
33
+ # is already stored, so re-running a sweep with nothing having
34
+ # moved on the target side touches the append log not at all.
35
+ def call(registry, domain, aggregate)
36
+ return 0 if aggregate.projected_fields.empty?
37
+
38
+ repository = registry.repository(domain, aggregate)
39
+ repository.all.count { |record| refresh(registry, domain, aggregate, record, repository) }
40
+ end
41
+
42
+ def refresh(registry, domain, aggregate, record, repository)
43
+ changed = false
44
+
45
+ aggregate.projected_fields.each do |field|
46
+ value = remote_value(registry, domain, aggregate, record, field)
47
+ next if value.nil?
48
+ next if record.key?(field.name) && record[field.name] == value
49
+
50
+ record[field.name] = value
51
+ changed = true
52
+ end
53
+
54
+ repository.save(record) if changed
55
+ changed
56
+ end
57
+
58
+ # `nil` when the reference itself does not resolve in this
59
+ # chapter (a cross-domain target left "unfollowed" the same way
60
+ # References#dereference already leaves one) or when the record
61
+ # names no target at all — an optional reference nobody set.
62
+ def remote_value(registry, domain, aggregate, record, field)
63
+ target = aggregate.attribute(field.reference)&.type&.resolve
64
+ return nil unless target
65
+
66
+ id = record[field.reference]
67
+ return nil if id.nil?
68
+
69
+ remote = registry.repository(domain, target).find(id.to_s)
70
+ remote&.[](field.remote_field)
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,99 @@
1
+ require "delegate"
2
+ require_relative "../query_specification/common/where_clause"
3
+ require_relative "../query_specification/common/options"
4
+ require_relative "../query_specification/hop_path"
5
+ require_relative "../ports/query"
6
+ require_relative "registry"
7
+
8
+ module Hecks
9
+ module Runtime
10
+ # `TenantScope`'s sibling — a hop folds to a synthetic `in`
11
+ # where-clause the exact same way a tenant boundary folds to a
12
+ # synthetic `eq` one, so every engine that already reads `.wheres`
13
+ # answers a hop for free, with no per-engine code and no way for
14
+ # one engine to forget it.
15
+ #
16
+ # A hop's own filtering never happens here — RESOLVING one hop
17
+ # means running one ordinary, adapter-agnostic query against the
18
+ # hop's TARGET aggregate (through the same Ports::Query boundary
19
+ # any other query goes through), and folding the ids it answers
20
+ # back in as a local membership check. A multi-hop chain resolves
21
+ # from the FAR END inward: `fold` only ever peels off the head hop
22
+ # (QuerySpecification::HopPath.next_hop, the one-step primitive),
23
+ # and hands everything still left in the tail to a recursive
24
+ # `apply` call — so hop 2, hop 3, and so on each get resolved by
25
+ # their own ordinary query against their own target, one recursion
26
+ # level at a time, never by this module trying to see the whole
27
+ # chain at once. It terminates because each recursion is over a
28
+ # strictly shorter dotted string, and
29
+ # BluebookBuilder#validate_query_hops! already refused any chain
30
+ # deep enough to matter before this ever runs.
31
+ module ReferenceHop
32
+ module_function
33
+
34
+ def apply(declared, args, registry:, domain:, aggregate:)
35
+ hopped, local = declared.wheres.partition { |clause| QuerySpecification::HopPath.hop_head?(clause.field, aggregate.attributes) }
36
+ return declared if hopped.empty?
37
+
38
+ folded = hopped.map { |clause| fold(clause, args, registry: registry, domain: domain, aggregate: aggregate) }
39
+ Folded.new(declared, local + folded)
40
+ end
41
+
42
+ def fold(clause, args, registry:, domain:, aggregate:)
43
+ step = QuerySpecification::HopPath.next_hop(clause.field, aggregate.attributes)
44
+ hop, rest = step
45
+
46
+ # A guarantee BluebookBuilder#validate_query_hops! already gave
47
+ # this exact clause before the domain ever booted. Held anyway:
48
+ # an unresolvable target folding silently to "matches
49
+ # everything" is precisely the failure shape this whole feature
50
+ # exists to close, and a hop that can no longer resolve (a
51
+ # target aggregate unloaded after boot, say) is a real fact
52
+ # worth a real error, not a query that quietly stops filtering.
53
+ unless hop&.target
54
+ raise WiringError,
55
+ "#{clause.field} hops through a reference this domain cannot resolve " \
56
+ "right now — the chapter seal already checked this once; something " \
57
+ "changed between then and this dispatch"
58
+ end
59
+
60
+ inner = QuerySpecification::Common::WhereClause.new(field: rest, op: clause.op, value: clause.value)
61
+ ids = matching_ids(domain, hop.target, [inner], args, registry: registry)
62
+
63
+ QuerySpecification::Common::WhereClause.new(field: hop.attribute.name, op: "in", value: ids)
64
+ end
65
+
66
+ # Every id the inner clause admits on the hop's TARGET — one
67
+ # whole, ordinary query against the target's own repository,
68
+ # through the very same Ports::Query boundary the outer ask
69
+ # uses, so a hop is answered by whatever engine the target
70
+ # aggregate is actually bound to (which may not be the engine
71
+ # the OUTER aggregate is bound to at all) rather than by a
72
+ # second reading of the comparators.
73
+ def matching_ids(domain, target, wheres, args, registry:)
74
+ spec = apply(QuerySpecification::Common::Options.new(wheres: wheres), args,
75
+ registry: registry, domain: domain, aggregate: target)
76
+ repository = registry.repository(domain, target)
77
+ rows = Ports::Query.execute(repository, spec, args, context: { domain: domain, aggregate: target }) ||
78
+ Ports::Query::InMemory.execute(repository.all, spec, args)
79
+
80
+ rows.map { |row| row.id.to_s }.uniq
81
+ end
82
+
83
+ # Never returned to a caller that might call an IR-level method
84
+ # (`to_h`, …) whose OWN internal `wheres` read would resolve
85
+ # against the original object, not this override — the exact
86
+ # caution TenantScope::Scoped's own comment gives, for the exact
87
+ # same reason: SimpleDelegator only intercepts calls made
88
+ # directly on the wrapper.
89
+ class Folded < SimpleDelegator
90
+ def initialize(declared, wheres)
91
+ super(declared)
92
+ @wheres = wheres
93
+ end
94
+
95
+ def wheres = @wheres
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,126 @@
1
+ module Hecks
2
+ module Runtime
3
+ # Every DomainRefusal wording that is not already data — `given`/
4
+ # `ensures`/a declared `invariant` already carry their own description,
5
+ # read at dispatch time off the command or value object that declared
6
+ # them. These are different in kind: LANGUAGE-LEVEL refusals, the same
7
+ # wording for every domain, not authored per-bluebook.
8
+ #
9
+ # Declared the same way in Vocabulary::RefusalTemplate
10
+ # (language/bluebook/vocabulary.bluebook) — spec/refusal_wording_
11
+ # conformance_spec holds this table equal to the language, both
12
+ # directions. Hand-typed rather than read live off the meta-domain at
13
+ # every dispatch, the same reason Expression::Evaluator::
14
+ # SIGN_TEST_OPERATORS is hand-typed beside Vocabulary::SignTest.
15
+ module RefusalWording
16
+ TEMPLATES = {
17
+ %w[NotFound creating_no_identity] =>
18
+ "{command} creates a {aggregate} — pass {identity}:",
19
+ %w[AlreadyExists creating_duplicate] =>
20
+ "{command} creates a {aggregate} that already exists — {identity} {offered}",
21
+ %w[AlreadyExists entity_duplicate] =>
22
+ "a {entity} already exists on {aggregate} — {identity} {offered}",
23
+ %w[NotFound acting_no_identity] =>
24
+ "{command} acts on an existing {aggregate} — pass {identity}:",
25
+ %w[NotFound record_missing] =>
26
+ "no {aggregate} with {identity} {offered}",
27
+ %w[NotFound entity_parent_no_identity] =>
28
+ "{command} acts on a {aggregate}'s {entity} — pass {identity}:",
29
+ %w[UnknownVerb entity_unknown] =>
30
+ "{aggregate} has no entity {entity}",
31
+ %w[NotFound entity_element_no_identity] =>
32
+ "{command} acts on one {entity} — pass {identity}:",
33
+ %w[NotFound entity_element_missing] =>
34
+ "no {entity} with {identity} {wants} on {aggregate} {parent_id}",
35
+ %w[NotFound reference_target_missing] =>
36
+ "no {target} with {heads} {key}",
37
+ %w[NotFound read_model_reference_missing] =>
38
+ "no {aggregate} with reference {offered}",
39
+ %w[TypeMismatch read_model_object_reference] =>
40
+ "{query} refused — a reference is an id, and {field} arrived as an object",
41
+ %w[UnknownVerb no_query] =>
42
+ "{aggregate} has no query {query}",
43
+ %w[UnknownVerb entity_query_missing] =>
44
+ "{entity} has no query {query}",
45
+ %w[UnknownVerb entity_holds_no_list] =>
46
+ "{aggregate} holds no list of {entity}",
47
+ %w[UnknownVerb entity_no_command] =>
48
+ "{entity} has no command {command}",
49
+ %w[UnknownVerb aggregate_no_command] =>
50
+ "{aggregate} has no command {command}",
51
+ %w[UnknownVerb port_no_operation] =>
52
+ "{port} has no operation {operation}",
53
+ %w[UnknownVerb no_domain] =>
54
+ "no domain {domain} loaded (verb {verb})",
55
+ %w[UnknownVerb no_read_model] =>
56
+ "{domain} has no read model {query}",
57
+ %w[UnknownVerb not_fully_qualified] =>
58
+ "{verb} is not a fully-qualified verb (Domain::Aggregate.Command)",
59
+ %w[UnknownVerb no_aggregate] =>
60
+ "{domain} has no aggregate {aggregate}",
61
+ %w[LifecycleRefused transition_blocked] =>
62
+ "{command} refused — {field} is {current}, and {command} moves it only from {allowed}",
63
+ %w[TypeMismatch value_object_shape] =>
64
+ "{name} is a {type} — pass its fields as an object, not {offered}",
65
+ %w[TypeMismatch reference_as_object] =>
66
+ "{command} refused — a reference is an id, and {attribute} arrived as an object{known_by}",
67
+ %w[TypeMismatch multi_field_scalar] =>
68
+ "{type} has multiple fields and cannot stand in for a scalar",
69
+ %w[TypeMismatch composite_identity] =>
70
+ "{type} is a composite identity — an identity must have exactly one field",
71
+ %w[TypeMismatch numeric_field] =>
72
+ "{type}.{field} expects {expected}, got {offered}",
73
+ %w[TypeMismatch non_finite_field] =>
74
+ "{type}.{field} must be a finite number, got {offered}",
75
+ %w[TypeMismatch pattern_mismatch] =>
76
+ "{type}.{field} must match {pattern}, got {offered}",
77
+ %w[TypeMismatch arithmetic_amount] =>
78
+ "{op} of {target} needs an Integer, got {offered}",
79
+ %w[TypeMismatch arithmetic_current] =>
80
+ "{op} of {target} needs an Integer {target}, got {offered}",
81
+ %w[TypeMismatch arithmetic_shared_field] =>
82
+ "{op} of {target} needs a value object with one shared Integer field",
83
+ %w[UnknownArgument unknown_args] =>
84
+ "{command} does not declare {unknown} — it takes {declared}",
85
+ %w[AbsentArgument absent_args] =>
86
+ "{command} was not given {absent} — it takes {declared}",
87
+ %w[InvariantViolation closed_set_member] =>
88
+ "{type} admits {admitted} — got {offered}",
89
+ %w[InvariantViolation value_object_invariant] =>
90
+ "{name} invariant violated — {description} (given {offered})",
91
+ %w[InvariantViolation admits_declared_set] =>
92
+ "{name} admits {admits} — {admitted} — got {offered}",
93
+ %w[InvariantViolation undeclared_set] =>
94
+ "{name} admits {admits}, which this chapter does not declare — a closed set is named " \
95
+ "Aggregate::SetName, and it must be one the bluebook actually holds",
96
+ %w[Unauthorized tenant_required] =>
97
+ "{query} declares authorize with tenant: {field} — pass {field}: to name which " \
98
+ "{field} this ask is scoped to",
99
+ %w[Unauthorized role_mismatch] =>
100
+ "{command} refused — role: {role}, and the caller stated {caller_role}",
101
+ %w[AttributeAbsent absent_read] =>
102
+ "{aggregate} {field} is absent on this record — declared, not optional, and " \
103
+ "added since it was written. Backfill it in a translation (backfill :{field}, " \
104
+ "default: ...), or declare it optional: true",
105
+ %w[ProjectionAbsent absent_read] =>
106
+ "{aggregate} {field} is not yet projected on this record — declared via " \
107
+ "projects :{field}, but no rebuild sweep has populated it. Run the sweep, " \
108
+ "or read {reference}.{remote_field} directly if this rule cannot wait"
109
+ }.freeze
110
+
111
+ module_function
112
+
113
+ # Plain text substitution, never expression syntax — a template is
114
+ # read, not evaluated. `render` computes the placeholder VALUES via
115
+ # whatever the call site already had (a joined list, a rendered
116
+ # identity reading, …) and this only replaces the markers.
117
+ def render(refusal, site, **values)
118
+ template = TEMPLATES.fetch([refusal, site]) do
119
+ raise KeyError, "no refusal template for #{refusal}/#{site} — declare it in " \
120
+ "Vocabulary::RefusalTemplate first"
121
+ end
122
+ values.reduce(template) { |text, (key, value)| text.gsub("{#{key}}", value.to_s) }
123
+ end
124
+ end
125
+ end
126
+ end