hecks 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (408) hide show
  1. checksums.yaml +5 -5
  2. data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
  3. data/lib/hecks/adapters/driven/claude_code.rb +127 -0
  4. data/lib/hecks/adapters/driven/d1.adapter +12 -0
  5. data/lib/hecks/adapters/driven/d1.rb +403 -0
  6. data/lib/hecks/adapters/driven/folder.adapter +3 -0
  7. data/lib/hecks/adapters/driven/folder.rb +199 -0
  8. data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
  9. data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
  10. data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
  11. data/lib/hecks/adapters/driven/governance_authorization.rb +62 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +51 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +38 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +148 -0
  17. data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
  18. data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
  19. data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
  20. data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
  21. data/lib/hecks/adapters/driven/lambda.adapter +4 -0
  22. data/lib/hecks/adapters/driven/lambda.rb +131 -0
  23. data/lib/hecks/adapters/driven/memory.adapter +3 -0
  24. data/lib/hecks/adapters/driven/memory.rb +103 -0
  25. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
  26. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
  27. data/lib/hecks/adapters/driven/postgres/codec.rb +80 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +168 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +391 -0
  31. data/lib/hecks/adapters/driven/postgres_era/lineage/era_store.rb +171 -0
  32. data/lib/hecks/adapters/driven/postgres_era/lineage/field_cache.rb +190 -0
  33. data/lib/hecks/adapters/driven/postgres_era/lineage/head_compiler.rb +429 -0
  34. data/lib/hecks/adapters/driven/postgres_era/lineage/mint_transaction.rb +166 -0
  35. data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +286 -0
  36. data/lib/hecks/adapters/driven/postgres_era/lineage/resumable_backfill.rb +168 -0
  37. data/lib/hecks/adapters/driven/postgres_era/lineage/tail_merge.rb +163 -0
  38. data/lib/hecks/adapters/driven/postgres_era/lineage/transform_installer.rb +114 -0
  39. data/lib/hecks/adapters/driven/postgres_era/lineage.rb +137 -0
  40. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/coverage_check.rb +89 -0
  41. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +82 -0
  42. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  43. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/minter.rb +132 -0
  44. data/lib/hecks/adapters/driven/postgres_era/lineage_manager.rb +74 -0
  45. data/lib/hecks/adapters/driven/postgres_era.adapter +6 -0
  46. data/lib/hecks/adapters/driven/postgres_era.rb +671 -0
  47. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  48. data/lib/hecks/adapters/driven/prism.rb +80 -0
  49. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  50. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  51. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  52. data/lib/hecks/adapters/driven/sqlite/codec.rb +63 -0
  53. data/lib/hecks/adapters/driven/sqlite/projection.rb +105 -0
  54. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +180 -0
  55. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  56. data/lib/hecks/adapters/driven/sqlite.rb +285 -0
  57. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  58. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  59. data/lib/hecks/adapters/driven.rb +36 -0
  60. data/lib/hecks/adapters.rb +6 -0
  61. data/lib/hecks/behaviors/dsl.rb +96 -0
  62. data/lib/hecks/behaviors/expectations.rb +280 -0
  63. data/lib/hecks/behaviors/ir.rb +31 -0
  64. data/lib/hecks/behaviors/rspec.rb +42 -0
  65. data/lib/hecks/behaviors/runner.rb +96 -0
  66. data/lib/hecks/behaviors.rb +25 -0
  67. data/lib/hecks/bluebook/aggregate.rb +108 -0
  68. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  69. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  70. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  71. data/lib/hecks/bluebook/assembly/contracts.rb +409 -0
  72. data/lib/hecks/bluebook/assembly/marks.rb +227 -0
  73. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  74. data/lib/hecks/bluebook/assembly.rb +78 -0
  75. data/lib/hecks/bluebook/attribute.rb +96 -0
  76. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  77. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  78. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  79. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  80. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  81. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  82. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  83. data/lib/hecks/bluebook/behaviour/lifecycle.rb +51 -0
  84. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  85. data/lib/hecks/bluebook/behaviour/process_manager.rb +38 -0
  86. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  87. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  88. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  89. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  90. data/lib/hecks/bluebook/chapter.rb +68 -0
  91. data/lib/hecks/bluebook/command.rb +124 -0
  92. data/lib/hecks/bluebook/domain_port.rb +102 -0
  93. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  94. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +906 -0
  95. data/lib/hecks/bluebook/dsl/attribute_collector.rb +327 -0
  96. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  97. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1018 -0
  98. data/lib/hecks/bluebook/dsl/command_builder.rb +626 -0
  99. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  100. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  101. data/lib/hecks/bluebook/dsl/entity_builder.rb +305 -0
  102. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  103. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  104. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  105. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  106. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  107. data/lib/hecks/bluebook/dsl/policy_builder.rb +120 -0
  108. data/lib/hecks/bluebook/dsl/port_builder.rb +30 -0
  109. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  110. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +207 -0
  111. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  112. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  113. data/lib/hecks/bluebook/dsl/rule_reference.rb +172 -0
  114. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  115. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  116. data/lib/hecks/bluebook/dsl/word_gate.rb +221 -0
  117. data/lib/hecks/bluebook/dsl/world_builder.rb +77 -0
  118. data/lib/hecks/bluebook/dsl.rb +45 -0
  119. data/lib/hecks/bluebook/entity.rb +103 -0
  120. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -0
  121. data/lib/hecks/bluebook/expression/evaluator.rb +261 -0
  122. data/lib/hecks/bluebook/expression/projection.json +170 -0
  123. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  124. data/lib/hecks/bluebook/expression/resolver.rb +630 -0
  125. data/lib/hecks/bluebook/expression.rb +13 -0
  126. data/lib/hecks/bluebook/hexagon.rb +60 -0
  127. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  128. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  129. data/lib/hecks/bluebook/meta_validator/judge.rb +559 -0
  130. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  131. data/lib/hecks/bluebook/meta_validator/port_judge.rb +47 -0
  132. data/lib/hecks/bluebook/meta_validator/readings.rb +350 -0
  133. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +312 -0
  134. data/lib/hecks/bluebook/meta_validator/shapes.rb +246 -0
  135. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  136. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  137. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  138. data/lib/hecks/bluebook/meta_validator.rb +452 -0
  139. data/lib/hecks/bluebook/model_check.rb +318 -0
  140. data/lib/hecks/bluebook/pattern_subset.rb +120 -0
  141. data/lib/hecks/bluebook/policy.rb +41 -0
  142. data/lib/hecks/bluebook/process_manager.rb +86 -0
  143. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  144. data/lib/hecks/bluebook/project_loader.rb +40 -0
  145. data/lib/hecks/bluebook/project_register.rb +82 -0
  146. data/lib/hecks/bluebook/query.rb +61 -0
  147. data/lib/hecks/bluebook/read_model.rb +109 -0
  148. data/lib/hecks/bluebook/reference.rb +74 -0
  149. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  150. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  151. data/lib/hecks/bluebook/translation.rb +92 -0
  152. data/lib/hecks/bluebook/value_object.rb +58 -0
  153. data/lib/hecks/bluebook.rb +74 -0
  154. data/lib/hecks/codemod.rb +342 -0
  155. data/lib/hecks/construct.rb +71 -0
  156. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  157. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  158. data/lib/hecks/deploy/oidc.json +18 -0
  159. data/lib/hecks/doc/reference.rb +389 -0
  160. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  161. data/lib/hecks/facade/cli_door.rb +116 -0
  162. data/lib/hecks/facade/cli_runner.rb +190 -0
  163. data/lib/hecks/facade/command_request.rb +105 -0
  164. data/lib/hecks/facade/handle.rb +173 -0
  165. data/lib/hecks/facade/json_door.rb +154 -0
  166. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  167. data/lib/hecks/facade/surface/chapter.rb +107 -0
  168. data/lib/hecks/facade/surface.rb +48 -0
  169. data/lib/hecks/facade.rb +44 -0
  170. data/lib/hecks/forms/app.rb +300 -0
  171. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  172. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  173. data/lib/hecks/forms/field_renderer.rb +170 -0
  174. data/lib/hecks/forms/field_shape.rb +232 -0
  175. data/lib/hecks/forms/html.rb +53 -0
  176. data/lib/hecks/forms/index_renderer.rb +35 -0
  177. data/lib/hecks/forms/page.rb +157 -0
  178. data/lib/hecks/forms/params.rb +132 -0
  179. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  180. data/lib/hecks/forms/record_renderer.rb +115 -0
  181. data/lib/hecks/forms/record_table.rb +63 -0
  182. data/lib/hecks/forms/reference_options.rb +30 -0
  183. data/lib/hecks/forms/value_object_shape.rb +46 -0
  184. data/lib/hecks/forms.rb +54 -0
  185. data/lib/hecks/fqn.rb +94 -0
  186. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  187. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  188. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  189. data/lib/hecks/framework/bluebook/governance.bluebook +130 -0
  190. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  191. data/lib/hecks/framework/oidc.json +39 -0
  192. data/lib/hecks/framework.rb +90 -0
  193. data/lib/hecks/freezer.rb +67 -0
  194. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  195. data/lib/hecks/fuzzing/isolated_boot.rb +92 -0
  196. data/lib/hecks/fuzzing/properties.rb +1146 -0
  197. data/lib/hecks/fuzzing/replay.rb +635 -0
  198. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  199. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +54 -0
  200. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  201. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +185 -0
  202. data/lib/hecks/fuzzing/sequence_generator.rb +130 -0
  203. data/lib/hecks/fuzzing/value_generator.rb +177 -0
  204. data/lib/hecks/fuzzing.rb +12 -0
  205. data/lib/hecks/grammar/evolve.rb +291 -0
  206. data/lib/hecks/grammar/expression.bluebook +417 -0
  207. data/lib/hecks/grammar/expression_operators.json +1152 -0
  208. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  209. data/lib/hecks/grammar/oidc.json +69 -0
  210. data/lib/hecks/grammar/translation.bluebook +203 -0
  211. data/lib/hecks/grammar.rb +178 -0
  212. data/lib/hecks/ir.rb +126 -0
  213. data/lib/hecks/language/adapter.bluebook +116 -0
  214. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  215. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  216. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  217. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  218. data/lib/hecks/language/bluebook/command.bluebook +437 -0
  219. data/lib/hecks/language/bluebook/entity.bluebook +316 -0
  220. data/lib/hecks/language/bluebook/policy.bluebook +172 -0
  221. data/lib/hecks/language/bluebook/process_manager.bluebook +264 -0
  222. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  223. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  224. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  225. data/lib/hecks/language/bluebook/syntax.bluebook +447 -0
  226. data/lib/hecks/language/bluebook/vocabulary.bluebook +364 -0
  227. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  228. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  229. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  230. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  231. data/lib/hecks/language/oidc.json +313 -0
  232. data/lib/hecks/language/port.bluebook +92 -0
  233. data/lib/hecks/language/translation/translation.bluebook +110 -0
  234. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  235. data/lib/hecks/language/world/wiring.bluebook +62 -0
  236. data/lib/hecks/language/world/world.bluebook +84 -0
  237. data/lib/hecks/literal.rb +125 -0
  238. data/lib/hecks/naming.rb +121 -0
  239. data/lib/hecks/ports/access_control.port +4 -0
  240. data/lib/hecks/ports/access_control.rb +62 -0
  241. data/lib/hecks/ports/agent/answers.rb +104 -0
  242. data/lib/hecks/ports/agent.port +4 -0
  243. data/lib/hecks/ports/agent.rb +167 -0
  244. data/lib/hecks/ports/authentication.port +4 -0
  245. data/lib/hecks/ports/authentication.rb +50 -0
  246. data/lib/hecks/ports/authorization.port +4 -0
  247. data/lib/hecks/ports/authorization.rb +59 -0
  248. data/lib/hecks/ports/clock.port +4 -0
  249. data/lib/hecks/ports/clock.rb +62 -0
  250. data/lib/hecks/ports/extraction.port +4 -0
  251. data/lib/hecks/ports/extraction.rb +37 -0
  252. data/lib/hecks/ports/identity_assignment.port +4 -0
  253. data/lib/hecks/ports/identity_assignment.rb +45 -0
  254. data/lib/hecks/ports/identity_generation.port +4 -0
  255. data/lib/hecks/ports/identity_generation.rb +49 -0
  256. data/lib/hecks/ports/identity_resolution.port +4 -0
  257. data/lib/hecks/ports/identity_resolution.rb +40 -0
  258. data/lib/hecks/ports/loading.port +4 -0
  259. data/lib/hecks/ports/loading.rb +13 -0
  260. data/lib/hecks/ports/persistence/append_only.rb +102 -0
  261. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  262. data/lib/hecks/ports/persistence/execution.rb +18 -0
  263. data/lib/hecks/ports/persistence/lineage.rb +277 -0
  264. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  265. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  266. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  267. data/lib/hecks/ports/persistence.port +4 -0
  268. data/lib/hecks/ports/persistence.rb +39 -0
  269. data/lib/hecks/ports/projection.port +4 -0
  270. data/lib/hecks/ports/projection.rb +76 -0
  271. data/lib/hecks/ports/query/in_memory.rb +60 -0
  272. data/lib/hecks/ports/query/ordering.rb +41 -0
  273. data/lib/hecks/ports/query.rb +36 -0
  274. data/lib/hecks/ports.rb +27 -0
  275. data/lib/hecks/projections/diagrams.rb +413 -0
  276. data/lib/hecks/projections/ir.rb +18 -0
  277. data/lib/hecks/projections/model/deviations.rb +98 -0
  278. data/lib/hecks/projections/model.rb +145 -0
  279. data/lib/hecks/projections/oidc.rb +110 -0
  280. data/lib/hecks/projections/parser_table.rb +159 -0
  281. data/lib/hecks/projections/reference.rb +38 -0
  282. data/lib/hecks/projections/shape.rb +31 -0
  283. data/lib/hecks/projections/statements.rb +110 -0
  284. data/lib/hecks/projections/vocabulary.rb +100 -0
  285. data/lib/hecks/projections.rb +32 -0
  286. data/lib/hecks/projector/cli_projector.rb +437 -0
  287. data/lib/hecks/projector/docs_projector.rb +321 -0
  288. data/lib/hecks/projector/exporter.rb +141 -0
  289. data/lib/hecks/projector/ir_projector.rb +18 -0
  290. data/lib/hecks/projector/narrate_projector.rb +243 -0
  291. data/lib/hecks/projector/target.rb +97 -0
  292. data/lib/hecks/projector.rb +186 -0
  293. data/lib/hecks/query_ir.rb +392 -0
  294. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  295. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  296. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  297. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  298. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  299. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  300. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  301. data/lib/hecks/query_specification/common/null_policy.rb +80 -0
  302. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  303. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  304. data/lib/hecks/query_specification/common/options.rb +32 -0
  305. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  306. data/lib/hecks/query_specification/common/specification.rb +10 -0
  307. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  308. data/lib/hecks/query_specification/field_path.rb +89 -0
  309. data/lib/hecks/query_specification/hop_path.rb +130 -0
  310. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  311. data/lib/hecks/query_specification.rb +14 -0
  312. data/lib/hecks/rendering.rb +48 -0
  313. data/lib/hecks/router/namespace_installer.rb +157 -0
  314. data/lib/hecks/router.rb +70 -0
  315. data/lib/hecks/runtime/caller.rb +50 -0
  316. data/lib/hecks/runtime/capability_graph.rb +44 -0
  317. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +124 -0
  318. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +248 -0
  319. data/lib/hecks/runtime/command_interpreter.rb +413 -0
  320. data/lib/hecks/runtime/command_rules/admissibility.rb +307 -0
  321. data/lib/hecks/runtime/command_rules/arithmetic.rb +254 -0
  322. data/lib/hecks/runtime/command_rules/authorization.rb +64 -0
  323. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  324. data/lib/hecks/runtime/command_rules/references.rb +181 -0
  325. data/lib/hecks/runtime/command_rules.rb +28 -0
  326. data/lib/hecks/runtime/dependency_planning.rb +231 -0
  327. data/lib/hecks/runtime/dispatcher.rb +286 -0
  328. data/lib/hecks/runtime/entity_element.rb +253 -0
  329. data/lib/hecks/runtime/entity_interpreter.rb +228 -0
  330. data/lib/hecks/runtime/era_check.rb +136 -0
  331. data/lib/hecks/runtime/era_guard/shape_diff.rb +128 -0
  332. data/lib/hecks/runtime/era_guard.rb +193 -0
  333. data/lib/hecks/runtime/era_tamper.rb +61 -0
  334. data/lib/hecks/runtime/errors.rb +100 -0
  335. data/lib/hecks/runtime/event.rb +51 -0
  336. data/lib/hecks/runtime/identity.rb +128 -0
  337. data/lib/hecks/runtime/instance.rb +120 -0
  338. data/lib/hecks/runtime/interpreting.rb +69 -0
  339. data/lib/hecks/runtime/loader.rb +143 -0
  340. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  341. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  342. data/lib/hecks/runtime/query_interpreter.rb +254 -0
  343. data/lib/hecks/runtime/reaction_invocation.rb +245 -0
  344. data/lib/hecks/runtime/read_model_interpreter.rb +266 -0
  345. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  346. data/lib/hecks/runtime/reference_hop.rb +99 -0
  347. data/lib/hecks/runtime/refusal_wording.rb +124 -0
  348. data/lib/hecks/runtime/registry/saga_persistence.rb +70 -0
  349. data/lib/hecks/runtime/registry/verification.rb +158 -0
  350. data/lib/hecks/runtime/registry.rb +218 -0
  351. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  352. data/lib/hecks/runtime/routing.rb +96 -0
  353. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  354. data/lib/hecks/runtime/saga_interpreter.rb +301 -0
  355. data/lib/hecks/runtime/storage_shape.rb +120 -0
  356. data/lib/hecks/runtime/tenant_check.rb +84 -0
  357. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  358. data/lib/hecks/runtime/value/admission.rb +112 -0
  359. data/lib/hecks/runtime/value/coercion.rb +500 -0
  360. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  361. data/lib/hecks/runtime/value.rb +125 -0
  362. data/lib/hecks/runtime.rb +95 -0
  363. data/lib/hecks/translation/audit/approval_digest.rb +31 -0
  364. data/lib/hecks/translation/audit/layer_one.rb +37 -0
  365. data/lib/hecks/translation/audit/layer_two.rb +77 -0
  366. data/lib/hecks/translation/audit/unfed_report.rb +42 -0
  367. data/lib/hecks/translation/audit.rb +70 -0
  368. data/lib/hecks/translation/reattest.rb +72 -0
  369. data/lib/hecks/translation/rule_compiler.rb +120 -0
  370. data/lib/hecks/translation/scaffold/differ.rb +183 -0
  371. data/lib/hecks/translation/scaffold/renderer.rb +41 -0
  372. data/lib/hecks/translation/scaffold/writer.rb +28 -0
  373. data/lib/hecks/translation/scaffold.rb +30 -0
  374. data/lib/hecks/translation.rb +11 -0
  375. data/lib/hecks/version.rb +16 -0
  376. data/lib/hecks/vocabulary.rb +213 -0
  377. data/lib/hecks.rb +118 -6
  378. metadata +401 -222
  379. data/bin/hecks +0 -7
  380. data/bin/hecks-package +0 -65
  381. data/bin/hecks_console +0 -12
  382. data/bin/hecks_serverless +0 -6
  383. data/lib/cli/build.rb +0 -14
  384. data/lib/cli/command_runner.rb +0 -28
  385. data/lib/cli/console.rb +0 -10
  386. data/lib/cli/generate.rb +0 -37
  387. data/lib/cli/hecks-cli.rb +0 -27
  388. data/lib/cli/test.rb +0 -57
  389. data/lib/console/commands.rb +0 -8
  390. data/lib/console/hecks-console.rb +0 -1
  391. data/lib/packager/README.md +0 -0
  392. data/lib/packager/app_runner.rb +0 -21
  393. data/lib/packager/args.rb +0 -26
  394. data/lib/packager/compatibility/fixnum.rb +0 -6
  395. data/lib/packager/hecks.rb +0 -39
  396. data/lib/packager/query_runner.rb +0 -21
  397. data/lib/packager/resources/Dockerfile +0 -11
  398. data/lib/packager/resources/app_binary +0 -7
  399. data/lib/packager/resources/bundle_config +0 -3
  400. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  401. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  402. data/lib/serverless/Domain +0 -32
  403. data/lib/serverless/cli.rb +0 -75
  404. data/lib/serverless/resources/command_name.js +0 -5
  405. data/lib/serverless/resources/environment.js +0 -7
  406. data/lib/serverless/resources/handler.js.tt +0 -28
  407. data/lib/serverless/resources/run_binary.js +0 -22
  408. data/lib/serverless/resources/serverless.yml +0 -20
@@ -0,0 +1,210 @@
1
+ require_relative "interpreting"
2
+ require_relative "command_interpreter/argument_gate"
3
+ require_relative "event"
4
+ require_relative "value"
5
+ require_relative "../naming"
6
+
7
+ module Hecks
8
+ module Runtime
9
+ # The dispatch pipeline for a port operation — called by an adapter living
10
+ # outside the bluebook entirely, never by the domain itself. Deliberately
11
+ # a trimmed CommandInterpreter: the same payload gate and coercion
12
+ # (ArgumentGate, Interpreting#normalize_args), but no `given`, no
13
+ # `mutations`, no lifecycle, no save. A port operation does not act on an
14
+ # aggregate instance — it translates an external call into an event in
15
+ # this domain's own vocabulary, and whatever mutation follows happens
16
+ # wherever a `policy` reacts to that event, exactly as it would for any
17
+ # command-emitted one.
18
+ class PortOperationInterpreter
19
+ include Interpreting
20
+ include CommandInterpreter::ArgumentGate
21
+
22
+ Context = Struct.new(:domain, :aggregate, :operation, :args, :route, :instance, :result)
23
+
24
+ DISPATCH_ORDER = %i[
25
+ refuse_unknown_arguments refuse_absent_arguments normalize_args resolve_references resolve_route emit
26
+ ].freeze
27
+
28
+ def initialize(registry, rules:)
29
+ @registry = registry
30
+ @rules = rules
31
+ end
32
+
33
+ def call(domain, aggregate, operation, args, route:)
34
+ ctx = Context.new(domain, aggregate, operation, args)
35
+ ctx.route = route
36
+ run_dispatch_order(DISPATCH_ORDER, ctx)
37
+ ctx.result
38
+ end
39
+
40
+ private
41
+
42
+ def step_refuse_unknown_arguments(ctx)
43
+ step(:refuse_unknown_arguments) { refuse_unknown_arguments(ctx.domain, ctx.aggregate, ctx.operation, ctx.args) }
44
+ end
45
+
46
+ def step_refuse_absent_arguments(ctx)
47
+ step(:refuse_absent_arguments) { refuse_absent_arguments(ctx.operation, ctx.args, aggregate: ctx.aggregate) }
48
+ end
49
+
50
+ def step_normalize_args(ctx)
51
+ ctx.args = step(:normalize_args) { normalize_args(ctx.aggregate, ctx.operation, ctx.args) }
52
+ end
53
+
54
+ def step_resolve_references(ctx)
55
+ step(:resolve_references) { @rules.resolve_references(ctx.domain, ctx.operation, ctx.args) }
56
+ end
57
+
58
+ def step_resolve_route(ctx)
59
+ ctx.instance = step(:resolve_route) do
60
+ @registry.repository(ctx.domain, ctx.aggregate).find(ctx.route.aggregate) ||
61
+ raise(NotFound, "#{ctx.aggregate.hecks_name} #{ctx.route.aggregate.inspect} does not exist")
62
+ end
63
+ end
64
+
65
+ def step_emit(ctx)
66
+ ctx.result = step(:emit) { ctx.operation.outbound? ? ask(ctx) : emit(ctx) }
67
+ end
68
+
69
+ # THE DOMAIN CALLING OUT, AND BOTH ENDINGS RECORDED.
70
+ #
71
+ # The adapter is found the same way every other port's is — by name,
72
+ # across whatever adapters this boot loaded — so an `asks` is bound by
73
+ # an adapter declaring `port "IssueTracker"` and nothing new to learn.
74
+ #
75
+ # EVERY FAILURE IS AN ANSWER. A raise from the far side of a boundary is
76
+ # not an exception in this domain's terms, it is the outside saying no,
77
+ # and the chapter already named the word for that. So the rescue is
78
+ # deliberately wide: a timeout, a bad credential, an adapter that does
79
+ # not exist, a nil where a number was wanted — all of them become the
80
+ # `refuses` event, carrying what was said. A policy reacts to it, a
81
+ # retry counter reads it, and nothing has to catch anything.
82
+ #
83
+ # AN ASK IS HANDED THE RECORD IT IS ABOUT.
84
+ #
85
+ # An INBOUND operation deliberately cannot read state — it is the
86
+ # anti-corruption boundary, translating a fact from outside, and letting
87
+ # it read the aggregate would make it a second place rules live. That
88
+ # rule was written for that direction and does not survive the crossing.
89
+ #
90
+ # An outbound one almost always needs the record. `asks "File"` names
91
+ # `reference_to Ticket` and the adapter needs the ticket's repository,
92
+ # title and body — which are ON the ticket, and which the policy that
93
+ # triggered this cannot supply because a command's event payload is its
94
+ # ARGUMENTS, not its state. Without this, every ask would have to have
95
+ # its data re-passed through the command that fired it, so the same text
96
+ # would live in two places and could differ.
97
+ #
98
+ # ARGUMENTS WIN over state, because an argument is what THIS call said
99
+ # and state is what the record happens to hold.
100
+ def ask(ctx)
101
+ payload = held_state(ctx).merge(materialise(ctx.args))
102
+ answer = adapter_for(ctx).public_send(Naming.snake(ctx.operation.hecks_name), **payload)
103
+ announce(ctx, ctx.operation.answers, ctx.args.merge(spread(answer)))
104
+ rescue StandardError => e
105
+ announce(ctx, ctx.operation.refuses, ctx.args.merge(refusal: { value: "#{e.class}: #{e.message}" }))
106
+ end
107
+
108
+ # THE ANSWER IS SPREAD, NOT NESTED — and that is what makes the loop
109
+ # close. A policy re-enters its target with the event payload VERBATIM;
110
+ # it cannot reach inside a key. So an answer tucked under `answered:`
111
+ # can be read by a human and by nothing else, and the command that
112
+ # should record the issue number never gets one.
113
+ #
114
+ # Spread, the adapter's own keys ARE the arguments of whatever command
115
+ # reacts to the answering event. Which is a real contract on the adapter
116
+ # — it must return what that command takes, in the shape the runtime
117
+ # coerces (`{ number: { value: 43 } }`, not `43`) — and naming it here
118
+ # is cheaper than a mapping layer nobody could see into.
119
+ #
120
+ # A non-Hash answer keeps the old shape: a port that returns a URL
121
+ # string has nothing to spread, and `answered:` is the honest word for
122
+ # a single unnamed value.
123
+ def spread(answer)
124
+ return { answered: answer } unless answer.is_a?(Hash)
125
+
126
+ answer.to_h { |key, value| [key.to_sym, deep_symbolize(value)] }
127
+ end
128
+
129
+ def deep_symbolize(value)
130
+ case value
131
+ when Hash then value.to_h { |k, v| [k.to_sym, deep_symbolize(v)] }
132
+ when Array then value.map { |element| deep_symbolize(element) }
133
+ else value
134
+ end
135
+ end
136
+
137
+ # THE RECORD, IF THERE IS ONE. A record that does not exist yet is not
138
+ # an error here — the ask still goes, carrying only its arguments, and
139
+ # whatever the adapter makes of that is its own business. Refusing
140
+ # would put a second existence check behind the one `resolve_references`
141
+ # already performed.
142
+ def held_state(ctx)
143
+ ctx.instance ? Value.materialize(ctx.instance.state) : {}
144
+ rescue StandardError
145
+ {}
146
+ end
147
+
148
+ # THE PORT THIS OPERATION BELONGS TO, found by asking the aggregate
149
+ # rather than threading it through the call — the dispatcher already
150
+ # resolved it once to get here, and a second parameter carried purely so
151
+ # this method can read it would be a parameter every OTHER step ignores.
152
+ def port_name_for(ctx)
153
+ owning = ctx.aggregate.ports.find { |port| port.operations.any? { |op| op.equal?(ctx.operation) } }
154
+ owning&.name or raise WiringError,
155
+ "#{ctx.operation.hecks_name} belongs to no port on #{ctx.aggregate.hecks_name}"
156
+ end
157
+
158
+ def adapter_for(ctx)
159
+ name = port_name_for(ctx)
160
+ implementations = @registry.adapters.values.select { |adapter| adapter.port == name }
161
+
162
+ case implementations.size
163
+ when 1 then Adapters.const_get(implementations.first.name).new
164
+ when 0 then raise WiringError, "no adapter implements the #{name} port — nothing can answer #{ctx.operation.hecks_name}"
165
+ else raise WiringError,
166
+ "#{implementations.size} adapters implement the #{name} port " \
167
+ "(#{implementations.map(&:name).sort.join(', ')}) — the runtime will not choose for you"
168
+ end
169
+ end
170
+
171
+ # A Value never crosses the boundary — an adapter is somebody else's
172
+ # code and should be handed plain data, the same reasoning `JsonDoor`
173
+ # gives for materialising before it hands anything to an HTTP caller.
174
+ def materialise(args) = Value.materialize(args)
175
+
176
+ def announce(ctx, event_name, payload)
177
+ event = Event.new(
178
+ name: event_name,
179
+ aggregate: "#{ctx.domain}::#{ctx.aggregate.hecks_name}",
180
+ id: ctx.route.aggregate,
181
+ payload: payload,
182
+ occurred_at: Time.now.utc.iso8601
183
+ )
184
+ @registry.event_log << event
185
+ [event]
186
+ end
187
+
188
+ # THE ONE PLACE THIS DIFFERS FROM CommandRules::Emission — there is no
189
+ # mutated instance to read an id off, because nothing was hydrated or
190
+ # saved. The record this event is ABOUT is named by whichever attribute
191
+ # is a reference to the owning aggregate (PortOperationBuilder#build
192
+ # already refused to build an operation with none), so its coerced
193
+ # value — already a plain id, never an object, per
194
+ # Value::Coercion#refuse_object_reference — is what stamps the event.
195
+ def emit(ctx)
196
+ ctx.operation.emits.map do |event_name|
197
+ event = Event.new(
198
+ name: event_name,
199
+ aggregate: "#{ctx.domain}::#{ctx.aggregate.hecks_name}",
200
+ id: ctx.route.aggregate,
201
+ payload: ctx.args,
202
+ occurred_at: Time.now.utc.iso8601
203
+ )
204
+ @registry.event_log << event.emit!
205
+ event
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,254 @@
1
+ require_relative "../naming"
2
+ require_relative "../ports/query"
3
+ require_relative "../ports/query/ordering"
4
+ require_relative "../query_specification/field_path"
5
+ require_relative "../query_specification/common/comparison"
6
+ require_relative "errors"
7
+ require_relative "reference_hop"
8
+ require_relative "refusal_wording"
9
+ require_relative "tenant_scope"
10
+ require_relative "value"
11
+
12
+ module Hecks
13
+ module Runtime
14
+ class QueryInterpreter
15
+ attr_reader :registry
16
+
17
+ def initialize(registry)
18
+ @registry = registry
19
+ end
20
+
21
+ def call(domain, aggregate, query_name, args)
22
+ return entity_rows(domain, aggregate, query_name, args) if query_name.include?(".")
23
+
24
+ declared = declared_query(aggregate, query_name)
25
+ args = normalize_args(aggregate, declared, args)
26
+ declared = TenantScope.apply(declared, args)
27
+ # AFTER TenantScope, so its synthetic clause is already present
28
+ # in `.wheres` and rides through as an ordinary LOCAL clause on
29
+ # the OUTER query. It does not reach the hop's own inner
30
+ # sub-query against the TARGET aggregate — a hop's target may
31
+ # not even declare the same tenant boundary, and propagating one
32
+ # aggregate's tenant scope onto an unrelated aggregate's own
33
+ # query is a real design question of its own, not answered here.
34
+ declared = ReferenceHop.apply(declared, args, registry: @registry, domain: domain, aggregate: aggregate)
35
+
36
+ repository = @registry.repository(domain, aggregate)
37
+ # `registry:` is threaded through so a `none_in_state` where-clause
38
+ # can look its target aggregate up — see
39
+ # QuerySpecification::Common::Comparison#none_in_state? for the
40
+ # comparator itself, and for why an ordinary aggregate-level
41
+ # Memory query needs the registry passed explicitly rather than
42
+ # closing over an instance variable.
43
+ if (native = Ports::Query.execute(repository, declared, args,
44
+ context: { domain: domain, aggregate: aggregate, registry: @registry }))
45
+ records = native
46
+ # `record.state.merge(id: record.id)` — id LAST, not first. See
47
+ # Instance#to_h's own comment: an aggregate free to declare its
48
+ # own attribute literally named `id` has that attribute's own
49
+ # wrapped value sitting in `record.state[:id]` already; merging
50
+ # it OVER a `{id:}.merge(state)` used to let it silently
51
+ # clobber the correct bare identity this row is supposed to
52
+ # carry.
53
+ # A QUERY ROW IS AN ANSWER, NOT A HANDLE. Mutating one edits
54
+ # nobody's state and silently disagrees with the store.
55
+ return Freezer.deep(records.map { |record| record.state.merge(id: record.id) })
56
+ end
57
+
58
+ Freezer.deep(interpret(repository.all, declared, args, domain: domain))
59
+ end
60
+
61
+ # The REFERENCE answer — this interpreter's own evaluation, never an
62
+ # adapter's native hook. The fuzzer's query oracle replays every
63
+ # generated ask through both paths and treats a difference as a
64
+ # finding: the differential gate the retired cross-runtime harness
65
+ # should always have been, aimed where the divergence actually
66
+ # lives — between the engines inside this one runtime.
67
+ #
68
+ # A hop clause is answered here by its OWN, deliberately naive
69
+ # walk (reference_where_holds?) — never Runtime::ReferenceHop's
70
+ # partition/fold/IN-clause. Sharing that algorithm would have made
71
+ # this oracle blind to exactly the code the hop feature adds: every
72
+ # PHASE of a shared fold would still get diffed against the native
73
+ # adapters, but the fold itself — the empty candidate set, a
74
+ # duplicate id, a dangling reference, a chain's inside-out
75
+ # resolution order — would only ever be compared against itself.
76
+ def reference_call(domain, aggregate, query_name, args)
77
+ return entity_rows(domain, aggregate, query_name, args) if query_name.include?(".")
78
+
79
+ declared = declared_query(aggregate, query_name)
80
+ args = normalize_args(aggregate, declared, args)
81
+ declared = TenantScope.apply(declared, args)
82
+ reference_interpret(@registry.repository(domain, aggregate).all, declared, args,
83
+ domain: domain, shape: aggregate)
84
+ end
85
+
86
+ private
87
+
88
+ def declared_query(aggregate, query_name)
89
+ aggregate.query(query_name) ||
90
+ raise(UnknownVerb, RefusalWording.render("UnknownVerb", "no_query",
91
+ aggregate: aggregate.hecks_name, query: query_name.inspect))
92
+ end
93
+
94
+ def interpret(records, declared, args, domain: nil)
95
+ matched = records.select { |r| declared.wheres.all? { |w| where_holds?(w, r, args, domain: domain) } }
96
+ ordered = ordered(matched, declared.order_by, declared.null_semantics)
97
+ # OFFSET FIRST, THEN LIMIT — the order SQL means by `LIMIT n
98
+ # OFFSET m`, and the order Ports::Query::InMemory#execute already
99
+ # applies (see that file's own comment). This interpreter used to
100
+ # never read declared.offset at all — offset silently vanished for
101
+ # any query answered here, not just come out reversed.
102
+ skipped = declared.offset ? ordered.drop(resolve_query_value(declared.offset.value, args).to_i) : ordered
103
+ capped = declared.limit ? skipped.first(resolve_query_value(declared.limit.value, args).to_i) : skipped
104
+
105
+ # id LAST — see the native-path comment above; same clobbering
106
+ # risk for the in-memory reference interpreter's own rows.
107
+ capped.map { |r| r.state.merge(id: r.id) }
108
+ end
109
+
110
+ # `interpret`'s own twin, for reference_call alone — same
111
+ # select/order/limit shape, but a clause that hops through a
112
+ # reference is answered by reference_where_holds? instead of the
113
+ # plain FieldPath.dig(record, field) `where_holds?` uses (which
114
+ # has no concept of a reference at all — it would just read the
115
+ # raw id straight off the record and compare THAT).
116
+ def reference_interpret(records, declared, args, domain:, shape:)
117
+ matched = records.select { |r|
118
+ declared.wheres.all? { |w|
119
+ reference_where_holds?(w, r, args, domain: domain, shape: shape)
120
+ }
121
+ }
122
+ ordered = ordered(matched, declared.order_by, declared.null_semantics)
123
+ # OFFSET FIRST, THEN LIMIT — same fix, same reasoning, as
124
+ # #interpret's own rows above.
125
+ skipped = declared.offset ? ordered.drop(resolve_query_value(declared.offset.value, args).to_i) : ordered
126
+ capped = declared.limit ? skipped.first(resolve_query_value(declared.limit.value, args).to_i) : skipped
127
+
128
+ # id LAST — same reasoning, same fix, as interpret's own rows.
129
+ capped.map { |r| r.state.merge(id: r.id) }
130
+ end
131
+
132
+ # THE NAIVE READING OF A HOP: not a fold, not an id set — for
133
+ # each candidate row, walk the reference by hand and dig the
134
+ # field out of whatever it actually points at. A nil reference,
135
+ # or one that resolves to nothing (a dangling id), makes the
136
+ # WHOLE clause false outright, whatever the comparator — "points
137
+ # at a client that is not active" is false for a proposal with no
138
+ # client at all, the same way it is false for one whose client
139
+ # really is active; falling through to holds?(clause, nil, args)
140
+ # instead would answer `ne` wrong (nil != "active" is true).
141
+ def reference_where_holds?(clause, record, args, domain:, shape:)
142
+ step = QuerySpecification::HopPath.next_hop(clause.field, shape.attributes)
143
+ return where_holds?(clause, record, args) unless step
144
+
145
+ hop, rest = step
146
+ inner = QuerySpecification::Common::WhereClause.new(field: rest, op: clause.op, value: clause.value)
147
+ held = record[hop.attribute.name]
148
+ reference_ids = hop.attribute.list? ? Array(held) : [held]
149
+
150
+ reference_ids.compact.any? do |reference_id|
151
+ target_record = @registry.repository(domain, hop.target).find(reference_id)
152
+ next false unless target_record
153
+
154
+ reference_where_holds?(inner, target_record, args, domain: domain, shape: hop.target)
155
+ end
156
+ end
157
+
158
+ def entity_rows(domain, aggregate, dotted, args)
159
+ entity_name, query_name = Naming.split_dotted(dotted)
160
+ entity = aggregate.entities.find { |piece| piece.hecks_name == entity_name } ||
161
+ raise(UnknownVerb, RefusalWording.render("UnknownVerb", "entity_unknown",
162
+ aggregate: aggregate.hecks_name, entity: entity_name.inspect))
163
+ declared = entity.query(query_name) ||
164
+ raise(UnknownVerb, RefusalWording.render("UnknownVerb", "entity_query_missing",
165
+ entity: entity_name, query: query_name.inspect))
166
+ declared = TenantScope.apply(declared, args)
167
+ list_attr = aggregate.attributes.find { |a| a.list? && a.type.to_s == entity_name } ||
168
+ raise(UnknownVerb, RefusalWording.render("UnknownVerb", "entity_holds_no_list",
169
+ aggregate: aggregate.hecks_name, entity: entity_name))
170
+
171
+ parent_key = Naming.reference_key(aggregate.hecks_name)
172
+ rows = @registry.repository(domain, aggregate).all.flat_map do |record|
173
+ Array(record[list_attr.name])
174
+ .select { |el| declared.wheres.all? { |w| element_where_holds?(w, el, args) } }
175
+ .map { |el| { parent_key => record.id }.merge(el) }
176
+ end
177
+
178
+ ordered = ordered_elements(rows, declared.order_by, declared.null_semantics,
179
+ parent_key, entity.identity_heads)
180
+ declared.limit ? ordered.first(resolve_query_value(declared.limit.value, args).to_i) : ordered
181
+ end
182
+
183
+ def element_where_holds?(clause, element, args)
184
+ holds?(clause, element[clause.field.to_sym], args)
185
+ end
186
+
187
+ # A row's own key, however the store spells it. A sub-list row is a plain hash
188
+ # merged from stored state, so its keys arrive as strings from one adapter and
189
+ # symbols from another — and reading only one spelling gave every row the SAME
190
+ # identity, which is a tie, which is the exact nondeterminism this tier exists
191
+ # to remove. It rides `comparable` for the same reason a where-clause does : an
192
+ # identity is a value object, and `to_s` on one is an OBJECT ADDRESS — a sort key
193
+ # that differs run to run, which is worse than the store order it replaced.
194
+ def cell(row, key) = row[key.to_sym] || row[key.to_s]
195
+
196
+ # A sub-list row is identified by its PARENT and then its own key : two
197
+ # entities under different parents can share a sequence, so the parent has
198
+ # to lead or the tie is not broken at all.
199
+ #
200
+ # EVERY KEY THE PIECE IS KNOWN BY, in declaration order, for the same
201
+ # reason the parent leads: a part that ties is a part that breaks no tie.
202
+ # This took `identified_by`, which is the SINGLE head and is nil the
203
+ # moment an identity has two parts — and `cell(row, nil)` calls
204
+ # `nil.to_sym`, so a query against a composite piece did not sort wrongly,
205
+ # it raised. A piece known by one key sorts exactly as it did.
206
+ def ordered_elements(rows, order_by, null_semantics, parent_key, entity_keys)
207
+ field = order_by&.field
208
+ Ports::Query::Ordering.apply(
209
+ rows, order_by, null_semantics,
210
+ identity: lambda { |row|
211
+ [row[parent_key].to_s, *Array(entity_keys).map { |key| comparable(cell(row, key)) }]
212
+ }
213
+ ) { |row| comparable(QuerySpecification::FieldPath.dig(row, field)) }
214
+ end
215
+
216
+ def where_holds?(clause, record, args, domain: nil)
217
+ holds?(clause, QuerySpecification::FieldPath.dig(record, clause.field), args, record: record, domain: domain)
218
+ end
219
+
220
+ # The comparator table itself lives in
221
+ # QuerySpecification::Common::Comparison. This method and
222
+ # Ports::Query::InMemory#holds? used to carry a copy each and the
223
+ # two drifted — `none_in_state` reached only one of them, and
224
+ # `comparable` disagreed about value objects with two numeric
225
+ # members. What stays here is how a value is REACHED for this
226
+ # path: the registry is instance state rather than an argument.
227
+ def holds?(clause, held, args, record: nil, domain: nil)
228
+ QuerySpecification::Common::Comparison.holds?(
229
+ clause.op, comparable(held), comparable(resolve_query_value(clause.value, args)), registry: @registry
230
+ )
231
+ end
232
+
233
+ def resolve_query_value(value, args)
234
+ value.is_a?(Symbol) ? args[value] : value
235
+ end
236
+
237
+ def normalize_args(aggregate, declared, args)
238
+ declared.attributes.each_with_object(args.dup) do |attribute, normalized|
239
+ next unless normalized.key?(attribute.name)
240
+
241
+ normalized[attribute.name] = Value.for_attribute(aggregate, attribute, normalized[attribute.name])
242
+ end
243
+ end
244
+
245
+ def comparable(value) = QuerySpecification::Common::Comparison.comparable(value)
246
+
247
+ def ordered(records, order_by, null_semantics = nil)
248
+ field = order_by&.field
249
+ Ports::Query::Ordering.apply(records, order_by, null_semantics,
250
+ identity: ->(record) { record.id.to_s }) { |record| comparable(record[field]) }
251
+ end
252
+ end
253
+ end
254
+ end