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,80 @@
1
+ require "prism"
2
+ require_relative "../../bluebook/expression/canonical_form"
3
+
4
+ module Hecks
5
+ module Adapters
6
+ class NotExtractable < StandardError; end
7
+
8
+ module Prism
9
+ TREES = {}
10
+
11
+ module_function
12
+
13
+ def canonical(block)
14
+ body = body_source(block)
15
+ body && canonicalise(body)
16
+ end
17
+
18
+ def body_source(block)
19
+ file, line = block.source_location
20
+ return nil unless file && File.readable?(file)
21
+
22
+ node = block_node_at(file, line)
23
+ node&.body&.slice
24
+ end
25
+
26
+ def block_node_at(file, line)
27
+ found = nil
28
+ walk(tree_for(file)) do |node|
29
+ next unless node.is_a?(::Prism::BlockNode)
30
+ next unless node.location.start_line == line
31
+
32
+ found ||= node
33
+ end
34
+ found
35
+ end
36
+
37
+ def tree_for(file)
38
+ # ::Prism.parse_file(file) reads the file itself, at the C
39
+ # extension level — bypassing Ruby's own File/IO layer entirely.
40
+ # That's invisible to anything that virtualizes the filesystem at
41
+ # the Ruby level instead of the OS level (e.g. tebako's memfs,
42
+ # used to press a hecks-based app into a single executable —
43
+ # see domain/README.md's "Deploying" section in lifeadelics for
44
+ # why that matters). ::Prism.parse(File.read(file)) parses the
45
+ # exact same bytes, just read through Ruby's File.read first,
46
+ # which those tools do intercept.
47
+ TREES[file] ||= ::Prism.parse(File.read(file)).value
48
+ end
49
+
50
+ # `TREES` caches for the life of the PROCESS, keyed by path, with
51
+ # no staleness check — correct for every ordinary caller (a file
52
+ # loads once per process: one `bin/ir` run, one rspec worker,
53
+ # never edited out from under it), but WRONG for anything that
54
+ # legitimately reloads an edited file in-process: a stale cached
55
+ # tree reports a `given`/`ensures` block at its OLD line number,
56
+ # which no longer matches the freshly re-executed file's own
57
+ # `block.source_location` — surfacing as "did not survive
58
+ # extraction" on a perfectly valid file. Found for real building
59
+ # `Hecks::Codemod` (lib/hecks/codemod.rb), which used to
60
+ # reach into `TREES.clear` directly — a private implementation
61
+ # detail poked from outside. `forget`/`forget_all` are the real
62
+ # API so nothing else that reloads an edited file in-process has
63
+ # to know `TREES` exists at all.
64
+ def forget(file) = TREES.delete(file)
65
+
66
+ def forget_all = TREES.clear
67
+
68
+ def walk(node, &visit)
69
+ return unless node.is_a?(::Prism::Node)
70
+
71
+ visit.call(node)
72
+ node.compact_child_nodes.each { |child| walk(child, &visit) }
73
+ end
74
+
75
+ def canonicalise(source)
76
+ Bluebook::Expression::CanonicalForm.apply(source)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,3 @@
1
+ Hecks.adapter "SecureRandomIdentity" do
2
+ port "identity_generation"
3
+ end
@@ -0,0 +1,14 @@
1
+ require "securerandom"
2
+
3
+ module Hecks
4
+ module Adapters
5
+ # The real `identity_generation` fulfillment — an actual random
6
+ # UUID, for an actual deployment. `SequentialIdentity` is the
7
+ # deterministic sibling specs/fuzzing bind instead.
8
+ module SecureRandomIdentity
9
+ module_function
10
+
11
+ def uuid = SecureRandom.uuid
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,221 @@
1
+ require_relative "../../forms/value_object_shape"
2
+ require_relative "../../query_specification/common/null_policy"
3
+ require_relative "../../query_specification/field_path"
4
+ require_relative "../../runtime/errors"
5
+ require_relative "../../runtime/value"
6
+
7
+ module Hecks
8
+ module Adapters
9
+ # The one SQL query compilation, shared by the SQLite and Postgres
10
+ # adapters — each used to carry its own copy of this walk, near-line-
11
+ # identical, and the copies could only drift. Every declared operator
12
+ # compiles fully into SQL or the query refuses loudly; the null-policy
13
+ # predicate, the comma-separated `in` convention, and the identity
14
+ # ORDER BY fallback are spelled once, here.
15
+ #
16
+ # What a dialect actually differs on is narrow, and each adapter
17
+ # supplies exactly those hooks:
18
+ #
19
+ # placeholder(binds, value) — "?" or "$N", recording the bind
20
+ # contains_clause(expr, ph) — instr() or position(), for a SCALAR field
21
+ # list_contains_clause(name, member, ph) — real element containment,
22
+ # for a `list_of` field (json_each /
23
+ # jsonb_array_elements)
24
+ # empty_in_clause — "0" or "FALSE"
25
+ # comparable_expression(e, v) — ::numeric cast, where the dialect casts
26
+ # plain_column(name) — a real column or a jsonb path
27
+ # nested_expression(name, path, member) — json_extract or state #>>
28
+ # order_clause(order_by, policy)
29
+ # execute_query(sql, binds) — run it, return instances
30
+ # dialect_name — for the refusal message
31
+ module SqlQueryBuilder
32
+ COMPARATORS = {
33
+ "eq" => "=", "ne" => "<>", "gt" => ">", "gte" => ">=", "lt" => "<", "lte" => "<="
34
+ }.freeze
35
+
36
+ def query(declared, args = {}, context: {})
37
+ sql = +"SELECT #{select_list} FROM #{from_relation}"
38
+ clauses = []
39
+ binds = []
40
+ declared.wheres.each do |clause|
41
+ value = query_value(clause.value, args)
42
+ expression = query_expression(clause.field, value: value)
43
+ if (null_predicate = QuerySpecification::Common::NullPolicy.sql_predicate(expression, clause.op, value))
44
+ clauses << null_predicate.first
45
+ next
46
+ end
47
+ clauses << where_clause(clause.op.to_s, expression, value, binds, field: clause.field)
48
+ end
49
+ sql << " WHERE #{clauses.join(' AND ')}" unless clauses.empty?
50
+ sql << if declared.order_by
51
+ " ORDER BY #{order_clause(declared.order_by, declared.null_semantics)}"
52
+ else
53
+ " ORDER BY id"
54
+ end
55
+ sql << " LIMIT #{placeholder(binds, query_value(declared.limit.value, args).to_i)}" if declared.limit
56
+ # An offset with no declared limit — SQLite refuses a bare OFFSET
57
+ # outright (LIMIT -1 is its unbounded idiom) while Postgres accepts
58
+ # one, so the dialect supplies its own unbounded spelling. Found by
59
+ # the adapter-agreement gate on its first run, not by either
60
+ # adapter's own spec: each was self-consistent, they just disagreed.
61
+ sql << unbounded_limit if !declared.limit && declared.offset
62
+ sql << " OFFSET #{placeholder(binds, query_value(declared.offset.value, args).to_i)}" if declared.offset
63
+ execute_query(sql, binds)
64
+ end
65
+
66
+ private
67
+
68
+ def where_clause(op, expression, value, binds, field: nil)
69
+ case op
70
+ when "eq", "ne", "gt", "gte", "lt", "lte"
71
+ "#{comparable_expression(expression, value)} #{COMPARATORS.fetch(op)} #{placeholder(binds, value)}"
72
+ when "contains"
73
+ member = field && list_member(field)
74
+ if member
75
+ list_contains_clause(field.to_s, member, placeholder(binds, value.to_s))
76
+ else
77
+ contains_clause(expression, placeholder(binds, value.to_s))
78
+ end
79
+ when "in"
80
+ members = in_members(value)
81
+ return empty_in_clause if members.empty?
82
+
83
+ # BOTH SIDES AS TEXT. `in` is a textual reading everywhere else
84
+ # — Ports::Query::InMemory#holds? compares `held.to_s` against
85
+ # stringified members, and `in_members` above stringifies its
86
+ # own — so a numeric field was the one shape where the engines
87
+ # could not agree: `in_members` binds '100', while the column
88
+ # expression yields the number 100, and `100 IN ('100','300')`
89
+ # is false in SQLite (a json_extract result carries no column
90
+ # affinity to coerce it) where Memory matched. Casting the
91
+ # expression rather than typing the members keeps one rule for
92
+ # every field: no type inference, and `in` means the same thing
93
+ # on a String, an Integer, and a value-object member.
94
+ #
95
+ # `eq`/`gt`/`lt` never had this problem because they bind the
96
+ # value's OWN type and route through `comparable_expression`,
97
+ # which Postgres overrides to cast numerics.
98
+ "CAST(#{expression} AS TEXT) IN (#{members.map { |member| placeholder(binds, member) }.join(', ')})"
99
+ else
100
+ raise ArgumentError, "#{dialect_name} query adapter does not support #{op.inspect}"
101
+ end
102
+ end
103
+
104
+ # A REAL ARRAY ALREADY SAYS WHERE ITS MEMBERS END. Splitting one on
105
+ # commas re-reads a boundary it already drew — and an id is a
106
+ # domain value (Naming::IDENTITY_JOIN joins a composite identity's
107
+ # own parts, and the parts it joins are whatever an identity path
108
+ # pulled out of real data), so a name carrying a comma would
109
+ # silently become two members matching the wrong rows, or nothing.
110
+ # This branch used to call `value.to_s` unconditionally, which
111
+ # meant only a comma-joined STRING ever worked here, while
112
+ # Ports::Query::InMemory and QueryInterpreter's own `members`
113
+ # already handled a real Array — three implementations of `in`,
114
+ # two readings of it. Narrower than those two on one point,
115
+ # deliberately: they unwrap a value-object/Hash element to its
116
+ # own scalar first (`comparable`) before stringifying, a rule
117
+ # this doesn't reproduce — every real caller of `in` today, and
118
+ # the cross-aggregate hop this exists for, passes plain scalar
119
+ # ids, never a value-object element.
120
+ def in_members(value)
121
+ return value.map(&:to_s).reject(&:empty?) if value.is_a?(Array)
122
+
123
+ value.to_s.split(",").map(&:strip).reject(&:empty?)
124
+ end
125
+
126
+ # Every declared field compiles into an expression over the stored
127
+ # shape — or the query never runs. The member-picking rules are the
128
+ # same in both dialects: a value-object field compares through the
129
+ # member the bound value (or the declared types) say is numeric,
130
+ # falling back to the one-field convention `value`.
131
+ #
132
+ # A REFERENCE IS AN ID, stored as a bare scalar (never wrapped —
133
+ # `Runtime::Value.refuse_object_reference` guarantees it), so it
134
+ # takes the SAME plain-column path as any other non-value-object
135
+ # attribute. Excluding it into the value-object member-picking logic
136
+ # compiles a nested read against a row that has no nested key — a
137
+ # path that can never match. Measured, not assumed: a `where` on a
138
+ # has_one/belongs_to/reference_to field returned zero rows against
139
+ # real data until that exclusion was removed.
140
+ def query_expression(field, value: nil)
141
+ name, *path = field.to_s.split(".")
142
+ attribute = @aggregate.attribute(name)
143
+
144
+ return plain_column(name) if path.empty? && @aggregate.lifecycle&.field.to_s == name
145
+ return plain_column(name) if path.empty? && attribute && !value_object?(attribute)
146
+
147
+ member = if path.empty? && value
148
+ hash = value.is_a?(Runtime::Value) ? value.to_h : value
149
+ numeric = hash.is_a?(Hash) && hash.find { |_key, item| item.is_a?(Numeric) }
150
+ numeric ? numeric.first : nil
151
+ end
152
+ member ||= if path.empty? && attribute && value_object?(attribute)
153
+ object = @aggregate.value_object(attribute.type)
154
+ Forms::ValueObjectShape.numeric_member(object)&.name
155
+ end
156
+ nested_expression(name, path, member)
157
+ end
158
+
159
+ # A NUMERIC MEMBER WINS if the value object has one (Price, Money —
160
+ # what every ordered comparison in the corpus until now compared),
161
+ # otherwise the SAME single-field fallback `query_expression`'s own
162
+ # `nested_expression(name, path, member || "value")` already
163
+ # makes for the COLUMN side — a plain `attribute :value, String`
164
+ # value object (IdentityId, RoleName, ...) has no numeric member at
165
+ # all, and returning nil there silently turned an equality
166
+ # comparison into `IS NULL`, matching nothing. Both sides of one
167
+ # comparison have to agree on which field they mean, or they
168
+ # compare two different things and call it a where clause.
169
+ def query_value(value, args)
170
+ value = args[value] if value.is_a?(Symbol)
171
+ hash = value.is_a?(Runtime::Value) ? value.to_h : value
172
+ if hash.is_a?(Hash)
173
+ numeric = hash.find { |_key, item| item.is_a?(Numeric) }
174
+ return numeric.last if numeric
175
+ return hash[:value] if hash.key?(:value)
176
+ return hash.values.first if hash.size == 1
177
+ end
178
+
179
+ Runtime::Value.scalar(value)
180
+ rescue Runtime::TypeMismatch
181
+ value.is_a?(Hash) && value.size == 1 ? value.values.first : value
182
+ end
183
+
184
+ def value_object?(attr)
185
+ !attr.list? && !@aggregate.value_object(attr.type).nil?
186
+ end
187
+
188
+ # `contains` on a `list_of` field means real ELEMENT membership, not
189
+ # a substring search over the column's raw JSON text — the reading
190
+ # QueryInterpreter#holds? and Ports::Query::InMemory already give a
191
+ # real Array, and the SQL side used to disagree with it (a substring
192
+ # match over `[{"value":"not_high_risk"}]` falsely matches
193
+ # "high_risk"). Returns nil for a non-list field (the caller falls
194
+ # back to `contains_clause`, unchanged), "" for a list of bare
195
+ # scalars (no member to walk into), or the one field name a
196
+ # single-field value-object element carries. A list of a
197
+ # MULTI-field value object has no one scalar to compare against and
198
+ # is refused rather than guessed.
199
+ def list_member(field)
200
+ return nil if field.to_s.include?(".")
201
+
202
+ attribute = @aggregate.attribute(field.to_s)
203
+ return nil unless attribute&.list?
204
+
205
+ object = @aggregate.value_object(attribute.type)
206
+ return "" unless object
207
+ return object.attributes.first.name.to_s if object.attributes.size == 1
208
+
209
+ raise ArgumentError,
210
+ "#{dialect_name} query adapter cannot compile contains on #{field} — " \
211
+ "#{attribute.type} carries more than one field, so no single scalar to compare"
212
+ end
213
+
214
+ # The dialect that casts nothing overrides nothing.
215
+ def comparable_expression(expression, _value) = expression
216
+
217
+ # The dialect that accepts a bare OFFSET overrides nothing.
218
+ def unbounded_limit = ""
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,63 @@
1
+ require_relative "../../../runtime/value"
2
+
3
+ module Hecks
4
+ module Adapters
5
+ class Sqlite
6
+ # How a state crosses the column boundary: which fields persist,
7
+ # and how each one encodes into (and decodes out of) its column.
8
+ module Codec
9
+ private
10
+
11
+ def persisted_fields
12
+ fields = @aggregate.attributes.reject { |attribute| attribute.name == :id }.map do |attribute|
13
+ { name: attribute.name, attribute: attribute, sql_type: sql_type(attribute) }
14
+ end
15
+ lifecycle = @aggregate.lifecycle
16
+ fields << { name: lifecycle.field, attribute: nil, sql_type: "TEXT" } if lifecycle && !fields.any? { |field| field[:name] == lifecycle.field }
17
+ fields
18
+ end
19
+
20
+ def encode(attr, value)
21
+ # NEVER SET IS NOT EMPTY. `then_set ... append:` starts a list at `[]`
22
+ # the moment the first element lands, but a list attribute nothing has
23
+ # ever appended to — the shape every OTHER aggregate in the corpus
24
+ # happened not to carry, until one declared a list-typed head
25
+ # attribute that no creating command populates by default — has to
26
+ # stay NULL to answer the same as Memory does. Forcing it to `[]`
27
+ # here is how a persistence topology invented data no dispatch wrote.
28
+ return (value.nil? ? nil : JSON.generate(value)) if attr.list?
29
+ return JSON.generate(value) if value.is_a?(Hash) || value.is_a?(Runtime::Value)
30
+
31
+ value
32
+ end
33
+
34
+ def encode_field(field, value)
35
+ return value unless field[:attribute]
36
+
37
+ encode(field[:attribute], value)
38
+ end
39
+
40
+ def decode(row)
41
+ persisted_fields.each_with_object({}) do |field, state|
42
+ attr = field[:attribute]
43
+ unless attr
44
+ state[field[:name]] = row[field[:name].to_s]
45
+ next
46
+ end
47
+ raw = row[attr.name.to_s]
48
+ state[attr.name] =
49
+ if attr.list? || value_object?(attr)
50
+ raw ? JSON.parse(raw, symbolize_names: true) : nil
51
+ else
52
+ # A REFERENCE lands here now, with the ordinary scalars. It holds
53
+ # the id of a head, which is text — `JSON.parse("acct-1")` raises,
54
+ # so reading it as JSON was only ever survivable while the column
55
+ # held an object.
56
+ raw
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,105 @@
1
+ require_relative "../../../runtime/instance"
2
+ require_relative "../../../runtime/value"
3
+ require_relative "../../../ports/query/in_memory"
4
+
5
+ # The subclass needs its parent — and sqlite.rb requires this file at
6
+ # its BOTTOM, after class Sqlite is defined, so the require cycle this
7
+ # creates resolves correctly from either entry point.
8
+ require_relative "../sqlite"
9
+
10
+ module Hecks
11
+ module Adapters
12
+ # Rebuilds a read store from the authoritative journal, entry by entry.
13
+ # A value object is written as its JSON object and a reference as the
14
+ # bare id it holds — the same shapes the command path writes, so there
15
+ # is no second representation to accept here any more.
16
+ class SqliteProjection < Sqlite
17
+ def project(entry)
18
+ return @db.execute("DELETE FROM #{quoted_table} WHERE id = ?", [entry.id]) if entry.delete?
19
+
20
+ columns = (["id"] + persisted_fields.map { |field| field[:name].to_s }).map { |column| quote_ident(column) }
21
+ values = [entry.id.to_s] + persisted_fields.map { |field| encode_field(field, entry.state[field[:name]]) }
22
+ @db.execute(
23
+ "INSERT OR REPLACE INTO #{quoted_table} (#{columns.join(', ')}) VALUES (#{Array.new(columns.size,
24
+ '?').join(', ')})", values
25
+ )
26
+ entry
27
+ end
28
+
29
+ # Execute a declared read model against the projected aggregate-head
30
+ # tables. The report shape is assembled from SQL-selected rows, rather
31
+ # than scanning repositories and matching references in Ruby.
32
+ def query_read_model(_domain, model, args, bluebook = nil)
33
+ raise ArgumentError, "projection query needs its domain bluebook" unless bluebook
34
+
35
+ # The REFERENCE's own shape, read the same way ReadModelInterpreter
36
+ # reads it — not the identity unwrap, which is gone : an identity is
37
+ # declared as a path and followed.
38
+ reference_id = args.fetch(model.reference_name).to_s
39
+ eligible = model.filtered_head_name
40
+ reports = {}
41
+ model.aggregate_heads.each do |head|
42
+ aggregate = bluebook.aggregate(head[:aggregate])
43
+ rows = if head[:aggregate] == model.reference_target
44
+ [select_projected(aggregate, reference_id)].compact
45
+ else
46
+ select_related(aggregate, model.reference_target, reference_id)
47
+ end
48
+ rows = Ports::Query::InMemory.execute(rows, model, args) if head[:as] == eligible
49
+ reports[head[:as]] = if head[:many]
50
+ rows.map { |row| Runtime::Value.materialize(row.to_h) }
51
+ else
52
+ rows.first && Runtime::Value.materialize(rows.first.to_h)
53
+ end
54
+ end
55
+ [reports]
56
+ end
57
+
58
+ private
59
+
60
+ def select_projected(aggregate, id)
61
+ row = @db.get_first_row("SELECT * FROM #{quote_ident(aggregate.storage_name)} WHERE id = ?", [id.to_s])
62
+ row && projected_instance(aggregate, row)
63
+ end
64
+
65
+ def select_related(aggregate, target, id)
66
+ references = aggregate.attributes.select do |attribute|
67
+ attribute.reference? && attribute.type.target_name == target.to_s
68
+ end
69
+ return [] if references.empty?
70
+
71
+ # A REFERENCE COLUMN HOLDS THE ID, so it compares as itself. The
72
+ # `json_extract(col,'$.value') = ? OR col = ?` this replaced was
73
+ # reading both shapes because both existed — one written by the
74
+ # command path, one by older journals. There is one shape now.
75
+ clauses = references.map { |attribute| "#{quote_ident(attribute.name)} = ?" }
76
+ bind = references.map { id.to_s }
77
+ @db.execute("SELECT * FROM #{quote_ident(aggregate.storage_name)} WHERE #{clauses.join(' OR ')} ORDER BY id", bind)
78
+ .map { |row| projected_instance(aggregate, row) }
79
+ end
80
+
81
+ def projected_instance(aggregate, row)
82
+ Runtime::Instance.new(aggregate: aggregate, id: row["id"], state: decode_for(aggregate, row))
83
+ end
84
+
85
+ def decode_for(aggregate, row)
86
+ fields = aggregate.attributes.map { |attribute| [attribute.name, attribute] }
87
+ if (lifecycle = aggregate.lifecycle) && !fields.any? { |name, _| name == lifecycle.field }
88
+ fields << [lifecycle.field, nil]
89
+ end
90
+ fields.each_with_object({}) do |(name, attribute), state|
91
+ raw = row[name.to_s]
92
+ state[name] =
93
+ if attribute.nil?
94
+ raw
95
+ elsif attribute.list? || !aggregate.value_object(attribute.type).nil?
96
+ raw ? JSON.parse(raw, symbolize_names: true) : nil
97
+ else
98
+ # A reference is a scalar id — see Codec#decode.
99
+ raw
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,180 @@
1
+ module Hecks
2
+ module Adapters
3
+ class Sqlite
4
+ # The DDL: one table per aggregate head, an append-only entry table
5
+ # beside it, the shared events table, and the two ALTERs that let an
6
+ # older database grow the columns newer code writes.
7
+ module SchemaBuilder
8
+ private
9
+
10
+ def create_aggregate_table!
11
+ columns = persisted_fields.map { |field| "#{quote_ident(field[:name])} #{field[:sql_type]}" }
12
+ @db.execute(
13
+ "CREATE TABLE IF NOT EXISTS #{quoted_table} (id TEXT PRIMARY KEY#{columns.empty? ? '' : ', '}#{columns.join(', ')})"
14
+ )
15
+ # RIGHT HERE, NOT AS A SEPARATE STEP IN `Sqlite#initialize` —
16
+ # `create_aggregate_table!` is the one piece of DDL `D1`
17
+ # already calls verbatim through this same shared module
18
+ # (`d1.rb`'s own header: "reuses Sqlite::SchemaBuilder and
19
+ # Sqlite::Codec UNCHANGED"). Folding index creation in here,
20
+ # right after the table it indexes exists, is what makes D1's
21
+ # own automatic indexing a real, running thing rather than an
22
+ # aspiration that only Sqlite ever calls — no D1-specific
23
+ # wiring needed, because D1 never has to know this exists.
24
+ ensure_indexes!
25
+ end
26
+
27
+ def create_event_table!
28
+ @db.execute(<<~SQL)
29
+ CREATE TABLE IF NOT EXISTS events (
30
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
31
+ name TEXT NOT NULL,
32
+ aggregate TEXT NOT NULL,
33
+ aggregate_id TEXT NOT NULL,
34
+ payload TEXT,
35
+ occurred_at TEXT
36
+ )
37
+ SQL
38
+ end
39
+
40
+ def create_entry_table!
41
+ @db.execute(<<~SQL)
42
+ CREATE TABLE IF NOT EXISTS #{quoted_entry_table} (
43
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT,
44
+ aggregate_id TEXT NOT NULL,
45
+ operation TEXT NOT NULL DEFAULT 'save',
46
+ state TEXT NOT NULL,
47
+ mirrors TEXT
48
+ )
49
+ SQL
50
+ end
51
+
52
+ def ensure_entry_operation_column!
53
+ columns = @db.execute("PRAGMA table_info(#{quoted_entry_table})").map { |row| row["name"] }
54
+ return if columns.include?("operation")
55
+
56
+ @db.execute("ALTER TABLE #{quoted_entry_table} ADD COLUMN operation TEXT NOT NULL DEFAULT 'save'")
57
+ end
58
+
59
+ def ensure_entry_mirrors_column!
60
+ columns = @db.execute("PRAGMA table_info(#{quoted_entry_table})").map { |row| row["name"] }
61
+ return if columns.include?("mirrors")
62
+
63
+ @db.execute("ALTER TABLE #{quoted_entry_table} ADD COLUMN mirrors TEXT")
64
+ end
65
+
66
+ # THE OPTIONAL saga-persistence capability's own table (§2/§4) —
67
+ # shared here so `D1`, which `include`s this module verbatim for
68
+ # its own `events`-table DDL (`d1.rb`), gets this for free too.
69
+ # `domain` stays an explicit column even though SQLite has no
70
+ # schema/namespace concept the way Postgres does — matches
71
+ # `hecks_saga_instances`' own Postgres shape (§3) and covers the
72
+ # (uncommon but real) case of a domain explicitly sharing one
73
+ # `database` file/D1 database with another.
74
+ def create_saga_table!
75
+ @db.execute(<<~SQL)
76
+ CREATE TABLE IF NOT EXISTS hecks_saga_instances (
77
+ domain TEXT NOT NULL,
78
+ process_manager TEXT NOT NULL,
79
+ correlation TEXT NOT NULL,
80
+ state TEXT NOT NULL,
81
+ memory TEXT NOT NULL,
82
+ PRIMARY KEY (domain, process_manager, correlation)
83
+ )
84
+ SQL
85
+ end
86
+
87
+ def sql_type(attr)
88
+ return "TEXT" if attr.list?
89
+
90
+ SQL_TYPES.fetch(attr.type, "TEXT")
91
+ end
92
+
93
+ # ── automatic indexing (plan principle 3: derived from existing
94
+ # declared `where`/`order_by` IR, no new DSL keyword — this runs
95
+ # unconditionally on every boot, `CREATE INDEX IF NOT EXISTS`,
96
+ # the same self-healing idiom `postgres_era.rb`'s own
97
+ # `ensure_head_snapshot!`/`ensure_first_head!` already use) ────
98
+
99
+ # Every field a declared query ever filters or sorts on, across
100
+ # the aggregate's OWN queries and every entity's own queries —
101
+ # `query_surfaces` in `dsl/aggregate_builder.rb` walks the
102
+ # identical pair. AN ENTITY'S QUERY STILL RUNS AGAINST THIS
103
+ # TABLE: it compiles through `query_expression`, which is
104
+ # `@aggregate`-scoped, not entity-scoped, so an entity's
105
+ # declared `where`/`order_by` is indexed here too, same as any
106
+ # other declared query — not skipped as "some other table's
107
+ # concern."
108
+ def ensure_indexes!
109
+ declared_query_fields.each { |field| ensure_index_for_field!(field) }
110
+ end
111
+
112
+ def declared_query_fields
113
+ queries = @aggregate.queries + @aggregate.entities.flat_map(&:queries)
114
+ queries.flat_map { |query| query.wheres.map(&:field) + [query.order_by&.field] }
115
+ .compact.map(&:to_s).uniq
116
+ end
117
+
118
+ # One field name resolves to exactly one of three outcomes:
119
+ #
120
+ # - a plain scalar attribute (or the lifecycle field itself)
121
+ # — a real btree index on the column, using `plain_column`'s
122
+ # own `quote_ident(name)` phrasing (reached by calling
123
+ # `query_expression` itself, not a second copy of its
124
+ # plain-vs-nested decision);
125
+ # - a non-list value-object attribute, referenced bare or
126
+ # through a member path (`field` or `field.member`) — an
127
+ # EXPRESSION index over `query_expression(field)`'s own
128
+ # `json_extract(...)` text. Reusing the query compiler's own
129
+ # expression, not re-deriving the string a second way, is
130
+ # the whole point: a textual mismatch between the index and
131
+ # what a real query compiles to is invisible to SQLite's
132
+ # planner, and two independent copies of this logic can only
133
+ # drift apart over time;
134
+ # - a list-typed attribute — NO index. SQLite's `contains`
135
+ # compiles to `EXISTS (SELECT 1 FROM json_each(col) WHERE
136
+ # ...)` (`list_contains_clause`, in `sql_query_builder.rb`)
137
+ # — an element-membership scan a plain index on the raw
138
+ # column (or even an expression index on it) does nothing to
139
+ # speed, since neither indexes the elements individually.
140
+ # SQLite has no inverted/array index short of FTS5/R-tree,
141
+ # and neither fits a `list_of` scalar or value-object field
142
+ # — well beyond this plan's scope. Left unindexed
143
+ # deliberately, not a silent gap.
144
+ #
145
+ # A field naming neither a real attribute nor the lifecycle
146
+ # field can't happen through the DSL today — `seal_query_field`
147
+ # (`dsl/aggregate_builder.rb`) already refuses it at PARSE time.
148
+ # This runs at adapter BOOT, not parse time, so it skips rather
149
+ # than crashes ugly if that invariant is ever violated.
150
+ def ensure_index_for_field!(field)
151
+ name, * = field.to_s.split(".")
152
+ attribute = @aggregate.attribute(name)
153
+ lifecycle_field = @aggregate.lifecycle&.field.to_s == name
154
+
155
+ return if !lifecycle_field && attribute.nil?
156
+ return if attribute&.list?
157
+
158
+ expression = query_expression(field)
159
+ @db.execute(
160
+ "CREATE INDEX IF NOT EXISTS #{quote_ident(index_name(field))} ON #{quoted_table}(#{expression})"
161
+ )
162
+ end
163
+
164
+ # `idx_<table>_<sanitized field>` — TABLE-PREFIXED because
165
+ # SQLite index names are GLOBAL to the database, not scoped per
166
+ # table the way a column name is; two different aggregates each
167
+ # indexing a field called "name" would collide without it. The
168
+ # field itself is sanitized (a dotted path's "." in particular)
169
+ # to stay a valid identifier while remaining visibly tied to
170
+ # what it indexes — collision-free across every field/path this
171
+ # aggregate declares, since `declared_query_fields` already
172
+ # de-duplicates the field strings themselves.
173
+ def index_name(field)
174
+ sanitized = field.to_s.gsub(/[^a-zA-Z0-9_]/, "_")
175
+ "idx_#{table}_#{sanitized}"
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end