hecks 0.2.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. checksums.yaml +5 -5
  2. data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
  3. data/lib/hecks/adapters/driven/claude_code.rb +127 -0
  4. data/lib/hecks/adapters/driven/d1.adapter +12 -0
  5. data/lib/hecks/adapters/driven/d1.rb +427 -0
  6. data/lib/hecks/adapters/driven/folder.adapter +3 -0
  7. data/lib/hecks/adapters/driven/folder.rb +199 -0
  8. data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
  9. data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
  10. data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
  11. data/lib/hecks/adapters/driven/governance_authorization.rb +101 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +56 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +99 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +65 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +179 -0
  17. data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
  18. data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
  19. data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
  20. data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
  21. data/lib/hecks/adapters/driven/lambda.adapter +4 -0
  22. data/lib/hecks/adapters/driven/lambda.rb +145 -0
  23. data/lib/hecks/adapters/driven/memory.adapter +3 -0
  24. data/lib/hecks/adapters/driven/memory.rb +103 -0
  25. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
  26. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
  27. data/lib/hecks/adapters/driven/postgres/codec.rb +88 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +207 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +438 -0
  31. data/lib/hecks/adapters/driven/postgres_era.adapter +17 -0
  32. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  33. data/lib/hecks/adapters/driven/prism.rb +80 -0
  34. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  35. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  36. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  37. data/lib/hecks/adapters/driven/sqlite/codec.rb +80 -0
  38. data/lib/hecks/adapters/driven/sqlite/projection.rb +172 -0
  39. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +192 -0
  40. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  41. data/lib/hecks/adapters/driven/sqlite.rb +306 -0
  42. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  43. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  44. data/lib/hecks/adapters/driven.rb +54 -0
  45. data/lib/hecks/adapters.rb +6 -0
  46. data/lib/hecks/behaviors/dsl.rb +125 -0
  47. data/lib/hecks/behaviors/expectations.rb +340 -0
  48. data/lib/hecks/behaviors/ir.rb +31 -0
  49. data/lib/hecks/behaviors/rspec.rb +42 -0
  50. data/lib/hecks/behaviors/runner.rb +96 -0
  51. data/lib/hecks/behaviors.rb +25 -0
  52. data/lib/hecks/bluebook/aggregate.rb +108 -0
  53. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  54. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  55. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  56. data/lib/hecks/bluebook/assembly/contracts.rb +438 -0
  57. data/lib/hecks/bluebook/assembly/marks.rb +228 -0
  58. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  59. data/lib/hecks/bluebook/assembly.rb +91 -0
  60. data/lib/hecks/bluebook/attribute.rb +96 -0
  61. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  62. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  63. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  64. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  65. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  66. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  67. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  68. data/lib/hecks/bluebook/behaviour/lifecycle.rb +68 -0
  69. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  70. data/lib/hecks/bluebook/behaviour/process_manager.rb +51 -0
  71. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  72. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  73. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  74. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  75. data/lib/hecks/bluebook/chapter.rb +78 -0
  76. data/lib/hecks/bluebook/command.rb +124 -0
  77. data/lib/hecks/bluebook/domain_port.rb +102 -0
  78. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  79. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +1018 -0
  80. data/lib/hecks/bluebook/dsl/attribute_collector.rb +348 -0
  81. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  82. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1087 -0
  83. data/lib/hecks/bluebook/dsl/command_builder.rb +714 -0
  84. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  85. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  86. data/lib/hecks/bluebook/dsl/entity_builder.rb +430 -0
  87. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  88. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  89. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  90. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  91. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  92. data/lib/hecks/bluebook/dsl/policy_builder.rb +135 -0
  93. data/lib/hecks/bluebook/dsl/port_builder.rb +39 -0
  94. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  95. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +306 -0
  96. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  97. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  98. data/lib/hecks/bluebook/dsl/rule_reference.rb +173 -0
  99. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  100. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  101. data/lib/hecks/bluebook/dsl/word_gate.rb +228 -0
  102. data/lib/hecks/bluebook/dsl/world_builder.rb +117 -0
  103. data/lib/hecks/bluebook/dsl.rb +45 -0
  104. data/lib/hecks/bluebook/entity.rb +103 -0
  105. data/lib/hecks/bluebook/expression/canonical_form.rb +123 -0
  106. data/lib/hecks/bluebook/expression/evaluator.rb +304 -0
  107. data/lib/hecks/bluebook/expression/projection.json +218 -0
  108. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  109. data/lib/hecks/bluebook/expression/resolver.rb +781 -0
  110. data/lib/hecks/bluebook/expression.rb +13 -0
  111. data/lib/hecks/bluebook/hexagon.rb +60 -0
  112. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  113. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  114. data/lib/hecks/bluebook/meta_validator/judge.rb +621 -0
  115. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  116. data/lib/hecks/bluebook/meta_validator/port_judge.rb +51 -0
  117. data/lib/hecks/bluebook/meta_validator/readings.rb +360 -0
  118. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +351 -0
  119. data/lib/hecks/bluebook/meta_validator/shapes.rb +266 -0
  120. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  121. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  122. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  123. data/lib/hecks/bluebook/meta_validator.rb +542 -0
  124. data/lib/hecks/bluebook/model_check.rb +445 -0
  125. data/lib/hecks/bluebook/pattern_subset.rb +184 -0
  126. data/lib/hecks/bluebook/policy.rb +41 -0
  127. data/lib/hecks/bluebook/process_manager.rb +128 -0
  128. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  129. data/lib/hecks/bluebook/project_loader.rb +40 -0
  130. data/lib/hecks/bluebook/project_register.rb +82 -0
  131. data/lib/hecks/bluebook/query.rb +61 -0
  132. data/lib/hecks/bluebook/read_model.rb +109 -0
  133. data/lib/hecks/bluebook/reference.rb +74 -0
  134. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  135. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  136. data/lib/hecks/bluebook/translation.rb +92 -0
  137. data/lib/hecks/bluebook/value_object.rb +66 -0
  138. data/lib/hecks/bluebook.rb +74 -0
  139. data/lib/hecks/codemod.rb +342 -0
  140. data/lib/hecks/construct.rb +71 -0
  141. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  142. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  143. data/lib/hecks/deploy/oidc.json +18 -0
  144. data/lib/hecks/doc/reference.rb +410 -0
  145. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  146. data/lib/hecks/facade/cli_door.rb +119 -0
  147. data/lib/hecks/facade/cli_runner.rb +190 -0
  148. data/lib/hecks/facade/command_request.rb +105 -0
  149. data/lib/hecks/facade/handle.rb +173 -0
  150. data/lib/hecks/facade/json_door.rb +166 -0
  151. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  152. data/lib/hecks/facade/surface/chapter.rb +107 -0
  153. data/lib/hecks/facade/surface.rb +48 -0
  154. data/lib/hecks/facade.rb +44 -0
  155. data/lib/hecks/forms/app.rb +341 -0
  156. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  157. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  158. data/lib/hecks/forms/field_renderer.rb +177 -0
  159. data/lib/hecks/forms/field_shape.rb +232 -0
  160. data/lib/hecks/forms/html.rb +84 -0
  161. data/lib/hecks/forms/index_renderer.rb +35 -0
  162. data/lib/hecks/forms/page.rb +157 -0
  163. data/lib/hecks/forms/params.rb +161 -0
  164. data/lib/hecks/forms/port_argument.rb +46 -0
  165. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  166. data/lib/hecks/forms/record_renderer.rb +119 -0
  167. data/lib/hecks/forms/record_table.rb +68 -0
  168. data/lib/hecks/forms/reference_options.rb +30 -0
  169. data/lib/hecks/forms/value_object_shape.rb +46 -0
  170. data/lib/hecks/forms.rb +54 -0
  171. data/lib/hecks/fqn.rb +94 -0
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  175. data/lib/hecks/framework/bluebook/governance.bluebook +145 -0
  176. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  177. data/lib/hecks/framework/oidc.json +39 -0
  178. data/lib/hecks/framework.rb +90 -0
  179. data/lib/hecks/freezer.rb +67 -0
  180. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
  181. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  182. data/lib/hecks/fuzzing/isolated_boot.rb +286 -0
  183. data/lib/hecks/fuzzing/properties.rb +1192 -0
  184. data/lib/hecks/fuzzing/replay.rb +668 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +80 -0
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +250 -0
  189. data/lib/hecks/fuzzing/sequence_generator.rb +131 -0
  190. data/lib/hecks/fuzzing/value_generator.rb +200 -0
  191. data/lib/hecks/fuzzing.rb +12 -0
  192. data/lib/hecks/grammar/evolve.rb +324 -0
  193. data/lib/hecks/grammar/expression.bluebook +420 -0
  194. data/lib/hecks/grammar/expression_operators.json +1648 -0
  195. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  196. data/lib/hecks/grammar/oidc.json +69 -0
  197. data/lib/hecks/grammar/translation.bluebook +203 -0
  198. data/lib/hecks/grammar.rb +178 -0
  199. data/lib/hecks/ir.rb +126 -0
  200. data/lib/hecks/language/adapter.bluebook +116 -0
  201. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  202. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  203. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  204. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  205. data/lib/hecks/language/bluebook/command.bluebook +471 -0
  206. data/lib/hecks/language/bluebook/entity.bluebook +392 -0
  207. data/lib/hecks/language/bluebook/policy.bluebook +189 -0
  208. data/lib/hecks/language/bluebook/process_manager.bluebook +381 -0
  209. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  210. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  211. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  212. data/lib/hecks/language/bluebook/syntax.bluebook +448 -0
  213. data/lib/hecks/language/bluebook/vocabulary.bluebook +379 -0
  214. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  215. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  216. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  217. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  218. data/lib/hecks/language/oidc.json +333 -0
  219. data/lib/hecks/language/port.bluebook +120 -0
  220. data/lib/hecks/language/translation/translation.bluebook +110 -0
  221. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  222. data/lib/hecks/language/world/wiring.bluebook +62 -0
  223. data/lib/hecks/language/world/world.bluebook +84 -0
  224. data/lib/hecks/literal.rb +125 -0
  225. data/lib/hecks/naming.rb +174 -0
  226. data/lib/hecks/ports/access_control.port +9 -0
  227. data/lib/hecks/ports/access_control.rb +62 -0
  228. data/lib/hecks/ports/agent/answers.rb +104 -0
  229. data/lib/hecks/ports/agent.port +8 -0
  230. data/lib/hecks/ports/agent.rb +167 -0
  231. data/lib/hecks/ports/authentication.port +6 -0
  232. data/lib/hecks/ports/authentication.rb +50 -0
  233. data/lib/hecks/ports/authorization.port +7 -0
  234. data/lib/hecks/ports/authorization.rb +62 -0
  235. data/lib/hecks/ports/clock.port +5 -0
  236. data/lib/hecks/ports/clock.rb +62 -0
  237. data/lib/hecks/ports/extraction.port +5 -0
  238. data/lib/hecks/ports/extraction.rb +37 -0
  239. data/lib/hecks/ports/identity_assignment.port +5 -0
  240. data/lib/hecks/ports/identity_assignment.rb +45 -0
  241. data/lib/hecks/ports/identity_generation.port +5 -0
  242. data/lib/hecks/ports/identity_generation.rb +49 -0
  243. data/lib/hecks/ports/identity_resolution.port +5 -0
  244. data/lib/hecks/ports/identity_resolution.rb +40 -0
  245. data/lib/hecks/ports/loading.port +4 -0
  246. data/lib/hecks/ports/loading.rb +13 -0
  247. data/lib/hecks/ports/persistence/append_only.rb +138 -0
  248. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  249. data/lib/hecks/ports/persistence/execution.rb +23 -0
  250. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  251. data/lib/hecks/ports/persistence/plugin.rb +54 -0
  252. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +169 -0
  253. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +128 -0
  254. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +161 -0
  255. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +61 -0
  256. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +304 -0
  257. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +171 -0
  258. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +190 -0
  259. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +478 -0
  260. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +166 -0
  261. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +314 -0
  262. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +168 -0
  263. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +170 -0
  264. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +134 -0
  265. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +137 -0
  266. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +89 -0
  267. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +85 -0
  268. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  269. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +132 -0
  270. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +74 -0
  271. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +758 -0
  272. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +120 -0
  273. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +31 -0
  274. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +46 -0
  275. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +102 -0
  276. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +46 -0
  277. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +70 -0
  278. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +72 -0
  279. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +120 -0
  280. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +183 -0
  281. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +41 -0
  282. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +28 -0
  283. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +30 -0
  284. data/lib/hecks/ports/persistence/plugins/era/translation.rb +11 -0
  285. data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
  286. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  287. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  288. data/lib/hecks/ports/persistence.port +4 -0
  289. data/lib/hecks/ports/persistence.rb +39 -0
  290. data/lib/hecks/ports/projection.port +4 -0
  291. data/lib/hecks/ports/projection.rb +96 -0
  292. data/lib/hecks/ports/query/in_memory.rb +60 -0
  293. data/lib/hecks/ports/query/ordering.rb +41 -0
  294. data/lib/hecks/ports/query.rb +36 -0
  295. data/lib/hecks/ports.rb +27 -0
  296. data/lib/hecks/projections/diagrams.rb +642 -0
  297. data/lib/hecks/projections/ir.rb +18 -0
  298. data/lib/hecks/projections/model/deviations.rb +98 -0
  299. data/lib/hecks/projections/model.rb +145 -0
  300. data/lib/hecks/projections/oidc.rb +110 -0
  301. data/lib/hecks/projections/parser_table.rb +159 -0
  302. data/lib/hecks/projections/reference.rb +38 -0
  303. data/lib/hecks/projections/shape.rb +44 -0
  304. data/lib/hecks/projections/statements.rb +110 -0
  305. data/lib/hecks/projections/vocabulary.rb +100 -0
  306. data/lib/hecks/projections.rb +32 -0
  307. data/lib/hecks/projector/cli_projector.rb +446 -0
  308. data/lib/hecks/projector/docs_projector.rb +321 -0
  309. data/lib/hecks/projector/exporter.rb +158 -0
  310. data/lib/hecks/projector/ir_projector.rb +18 -0
  311. data/lib/hecks/projector/narrate_projector.rb +243 -0
  312. data/lib/hecks/projector/target.rb +97 -0
  313. data/lib/hecks/projector.rb +186 -0
  314. data/lib/hecks/query_ir.rb +411 -0
  315. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  316. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  317. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  318. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  319. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  320. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  321. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  322. data/lib/hecks/query_specification/common/null_policy.rb +92 -0
  323. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  324. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  325. data/lib/hecks/query_specification/common/options.rb +32 -0
  326. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  327. data/lib/hecks/query_specification/common/specification.rb +10 -0
  328. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  329. data/lib/hecks/query_specification/field_path.rb +107 -0
  330. data/lib/hecks/query_specification/hop_path.rb +132 -0
  331. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  332. data/lib/hecks/query_specification.rb +14 -0
  333. data/lib/hecks/rendering.rb +48 -0
  334. data/lib/hecks/router/namespace_installer.rb +157 -0
  335. data/lib/hecks/router.rb +70 -0
  336. data/lib/hecks/runtime/aggregate_lock.rb +45 -0
  337. data/lib/hecks/runtime/boot_gates.rb +41 -0
  338. data/lib/hecks/runtime/caller.rb +66 -0
  339. data/lib/hecks/runtime/capability_graph.rb +44 -0
  340. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +135 -0
  341. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +260 -0
  342. data/lib/hecks/runtime/command_interpreter.rb +497 -0
  343. data/lib/hecks/runtime/command_rules/admissibility.rb +357 -0
  344. data/lib/hecks/runtime/command_rules/arithmetic.rb +260 -0
  345. data/lib/hecks/runtime/command_rules/authorization.rb +65 -0
  346. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  347. data/lib/hecks/runtime/command_rules/references.rb +189 -0
  348. data/lib/hecks/runtime/command_rules.rb +28 -0
  349. data/lib/hecks/runtime/dependency_planning.rb +245 -0
  350. data/lib/hecks/runtime/dispatcher.rb +301 -0
  351. data/lib/hecks/runtime/entity_element.rb +253 -0
  352. data/lib/hecks/runtime/entity_interpreter.rb +299 -0
  353. data/lib/hecks/runtime/errors.rb +122 -0
  354. data/lib/hecks/runtime/event.rb +51 -0
  355. data/lib/hecks/runtime/identity.rb +156 -0
  356. data/lib/hecks/runtime/instance.rb +175 -0
  357. data/lib/hecks/runtime/interpreting.rb +90 -0
  358. data/lib/hecks/runtime/loader.rb +184 -0
  359. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  360. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  361. data/lib/hecks/runtime/query_interpreter.rb +286 -0
  362. data/lib/hecks/runtime/reaction_invocation.rb +253 -0
  363. data/lib/hecks/runtime/read_model_interpreter.rb +341 -0
  364. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  365. data/lib/hecks/runtime/reference_hop.rb +99 -0
  366. data/lib/hecks/runtime/refusal_wording.rb +126 -0
  367. data/lib/hecks/runtime/registry/saga_persistence.rb +142 -0
  368. data/lib/hecks/runtime/registry/verification.rb +246 -0
  369. data/lib/hecks/runtime/registry.rb +279 -0
  370. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  371. data/lib/hecks/runtime/routing.rb +96 -0
  372. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  373. data/lib/hecks/runtime/saga_interpreter.rb +503 -0
  374. data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
  375. data/lib/hecks/runtime/tenant_check.rb +84 -0
  376. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  377. data/lib/hecks/runtime/value/admission.rb +130 -0
  378. data/lib/hecks/runtime/value/coercion.rb +565 -0
  379. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  380. data/lib/hecks/runtime/value.rb +125 -0
  381. data/lib/hecks/runtime.rb +107 -0
  382. data/lib/hecks/storehouse.rb +632 -0
  383. data/lib/hecks/version.rb +16 -0
  384. data/lib/hecks/vocabulary.rb +218 -0
  385. data/lib/hecks.rb +123 -6
  386. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
  387. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
  388. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
  389. metadata +412 -222
  390. data/bin/hecks +0 -7
  391. data/bin/hecks-package +0 -65
  392. data/bin/hecks_console +0 -12
  393. data/bin/hecks_serverless +0 -6
  394. data/lib/cli/build.rb +0 -14
  395. data/lib/cli/command_runner.rb +0 -28
  396. data/lib/cli/console.rb +0 -10
  397. data/lib/cli/generate.rb +0 -37
  398. data/lib/cli/hecks-cli.rb +0 -27
  399. data/lib/cli/test.rb +0 -57
  400. data/lib/console/commands.rb +0 -8
  401. data/lib/console/hecks-console.rb +0 -1
  402. data/lib/packager/README.md +0 -0
  403. data/lib/packager/app_runner.rb +0 -21
  404. data/lib/packager/args.rb +0 -26
  405. data/lib/packager/compatibility/fixnum.rb +0 -6
  406. data/lib/packager/hecks.rb +0 -39
  407. data/lib/packager/query_runner.rb +0 -21
  408. data/lib/packager/resources/Dockerfile +0 -11
  409. data/lib/packager/resources/app_binary +0 -7
  410. data/lib/packager/resources/bundle_config +0 -3
  411. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  412. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  413. data/lib/serverless/Domain +0 -32
  414. data/lib/serverless/cli.rb +0 -75
  415. data/lib/serverless/resources/command_name.js +0 -5
  416. data/lib/serverless/resources/environment.js +0 -7
  417. data/lib/serverless/resources/handler.js.tt +0 -28
  418. data/lib/serverless/resources/run_binary.js +0 -22
  419. data/lib/serverless/resources/serverless.yml +0 -20
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 798ee36082afdda202f54dfb446727e79c38f99a
4
- data.tar.gz: 120419fa4fcd0b8f0dc2401fea4482c37e5924e1
2
+ SHA256:
3
+ metadata.gz: 9d056aad218f4fe26394319c5ce486211ac9790022de8159ba23f050a634f5df
4
+ data.tar.gz: d6fa0c2f693152be740b0c5f658008a6ba71ae9bd80e175e8da78b142f00f133
5
5
  SHA512:
6
- metadata.gz: 7f100beec30a025476d12fd3061cdddbf24a1111f8123805c890a4d654b611f59c674364c3fa820be9895cc9a4be4f235b86ad18574fc76c04d14e94e8c08eed
7
- data.tar.gz: d2ee8bb2c264169866082e0b176a616ad8bb0fc02bd8c8468a5088faa6ca8acd4bfb70bfabe915f32c817343fc22355f4e2e8128ad165a09880dc95d91ad1b04
6
+ metadata.gz: 4ae526f5a34b194a9b3d23751fae0bb6bdee1bcd8011653904d2b324bf4478ee7b4049d96f0b2a74d005a810ac7023d6efa5a8d0639fb079ed8691da93a5f217
7
+ data.tar.gz: c2b1a7be12ebfbd52ad5cb13ddd1aa7693a7d903d1402c580baad40414a7f3318d7263379658debc502492986956f97b5377681ac93cf5cb36e058c3e1ccfb46
@@ -0,0 +1,3 @@
1
+ Hecks.adapter "ClaudeCode" do
2
+ port "agent"
3
+ end
@@ -0,0 +1,127 @@
1
+ require "open3"
2
+ require "json"
3
+ require "timeout"
4
+
5
+ require_relative "../../ports/agent"
6
+
7
+ module Hecks
8
+ module Adapters
9
+ # THE REAL `agent` FULFILLMENT — shells out to the `claude` CLI
10
+ # itself, `claude -p --output-format json`, one process per call.
11
+ # `Ports::Agent`'s own scripted double (`spec/fixtures/scripted_
12
+ # agent.{adapter,rb}`) is the deterministic sibling every spec binds
13
+ # instead, the same relationship `SecureRandomIdentity` already has
14
+ # to `SequentialIdentity`.
15
+ #
16
+ # THIS FILE OWNS TRANSPORT ONLY — spawning the process, unwrapping
17
+ # the CLI's own JSON envelope (`{"result": "..."}`) down to the
18
+ # model's raw text, and parsing THAT text as JSON. It hands back a
19
+ # plain Hash. Whether that Hash has the keys a caller asked for, and
20
+ # whether its values are within the closed vocabularies this port
21
+ # recognizes (a critique's `kind`, a proposal's `verb` pattern) is
22
+ # `Ports::Agent::Answers`' job, not this file's — see `ports/agent.rb`'s
23
+ # own header for why that split is load-bearing.
24
+ #
25
+ # `Open3.capture2` with an explicit argv array, never a shell string
26
+ # — the same shape every other subprocess call in this repo already
27
+ # uses (`bin/rust_conformance`), and the reason `claude` being
28
+ # shell-aliased to `claude --dangerously-skip-permissions` in a
29
+ # human's own terminal never reaches this code at all: an argv spawn
30
+ # invokes the real binary directly, no shell, no alias.
31
+ #
32
+ # `--allowedTools ""` — this adapter asks a question and reads back
33
+ # text; it never wants the model reaching for a tool mid-answer.
34
+ module ClaudeCode
35
+ TIMEOUT_SECONDS = 120
36
+
37
+ SYSTEM_PREFIX = "You are assisting a domain-modeling interview for the hecks " \
38
+ "event-sourced framework. Reply with EXACTLY ONE JSON object, no prose " \
39
+ "before or after it, no markdown code fence. "
40
+
41
+ module_function
42
+
43
+ # THE NEXT BEST QUESTION. `state` is whatever
44
+ # `Interview::Session#declaration`/`#gaps` produced — passed
45
+ # through as JSON, not reformatted, so this adapter never
46
+ # re-derives what the session already knows.
47
+ def ask(state:, asked:)
48
+ call(
49
+ system: SYSTEM_PREFIX + "Given the domain model so far, ask the single best next " \
50
+ 'discovery question. Reply as {"questions": [{"text": "...", "because": "..."}]} ' \
51
+ "with exactly one entry. Never repeat a question already asked.",
52
+ payload: { state: state, already_asked: asked }
53
+ )
54
+ end
55
+
56
+ # PROSE -> PROPOSED DECLARATIONS.
57
+ def interpret(prose:, state:)
58
+ call(
59
+ system: SYSTEM_PREFIX + "Given the domain model so far and a sentence the human just said, " \
60
+ "propose zero or more declarations that capture what the sentence names as domain " \
61
+ "fact. A verb must be fully qualified as Chapter::Aggregate.Command. Reply as " \
62
+ '{"proposals": [{"verb": "...", "rationale": "...", "arguments": ' \
63
+ '[{"name": "...", "field": "...", "value": "..."}]}]}. If the sentence carries no ' \
64
+ 'declaration (a question back, a clarification), reply {"proposals": []}.',
65
+ payload: { state: state, prose: prose }
66
+ )
67
+ end
68
+
69
+ # WHAT IS WRONG WITH THIS AS A MODEL — closed to the same kind
70
+ # vocabulary `Ports::Agent::CRITIQUE_KINDS` declares, spelled out
71
+ # here too since the system prompt is the only place the model
72
+ # itself ever sees that list.
73
+ def critique(declared:, refusals:, findings:)
74
+ kinds = Ports::Agent::CRITIQUE_KINDS.join(", ")
75
+ call(
76
+ system: SYSTEM_PREFIX + "Given a domain declaration, the refusals it already triggered, and " \
77
+ "the mechanical findings already reported, judge it on TASTE — do not restate what is " \
78
+ "already known. Only use one of these kinds: #{kinds}. Only use severity error or " \
79
+ 'warning. Reply as {"findings": [{"kind": "...", "severity": "...", "subject": "...", ' \
80
+ '"message": "..."}]}. Reply {"findings": []} if there is nothing worth saying.',
81
+ payload: { declared: declared, refusals: refusals, findings: findings }
82
+ )
83
+ end
84
+
85
+ # VOCABULARY HELP. Named `suggest_name`, not `name` — see
86
+ # `Ports::Agent#suggest_name`'s own comment for why `name` is
87
+ # never a safe module-function name here.
88
+ def suggest_name(meaning:, kind:, near:)
89
+ call(
90
+ system: SYSTEM_PREFIX + "Suggest a name for a #{kind} meaning \"#{meaning}\", distinct from " \
91
+ 'the names already in use nearby. Reply as {"names": [{"name": "...", "because": ' \
92
+ '"...", "rejected": ["...", "..."]}]} with exactly one entry.',
93
+ payload: { meaning: meaning, kind: kind, near: near }
94
+ )
95
+ end
96
+
97
+ # ── transport ───────────────────────────────────────────────────
98
+
99
+ def call(system:, payload:)
100
+ stdout, status = Timeout.timeout(TIMEOUT_SECONDS) {
101
+ Open3.capture2(
102
+ "claude", "-p", "--output-format", "json",
103
+ "--append-system-prompt", system, "--allowedTools", "",
104
+ stdin_data: JSON.generate(payload)
105
+ )
106
+ }
107
+ raise Ports::Agent::Unavailable, "claude exited #{status.exitstatus}: #{stdout}" unless status.success?
108
+
109
+ unwrap(stdout)
110
+ rescue Timeout::Error
111
+ raise Ports::Agent::Unavailable, "claude did not answer within #{TIMEOUT_SECONDS}s"
112
+ rescue Errno::ENOENT => e
113
+ raise Ports::Agent::Unavailable, "claude is not on PATH: #{e.message}"
114
+ end
115
+
116
+ def unwrap(stdout)
117
+ envelope = JSON.parse(stdout)
118
+ result = envelope["result"]
119
+ raise Ports::Agent::ValidationError, "no \"result\" in the claude envelope: #{stdout}" unless result
120
+
121
+ JSON.parse(result)
122
+ rescue JSON::ParserError => e
123
+ raise Ports::Agent::ValidationError, "claude's reply was not JSON: #{e.message}\n#{stdout}"
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,12 @@
1
+ Hecks.adapter "D1" do
2
+ port "persistence"
3
+ field :account_id
4
+ field :database_id
5
+ secret :api_token
6
+ end
7
+
8
+ # No "D1Projection" declared yet, unlike Sqlite/SqliteProjection —
9
+ # read-model projection support (query_read_model, mirroring
10
+ # sqlite/projection.rb) is real future work, not started here. Declaring
11
+ # the adapter without the class behind it would just be a WiringError trap
12
+ # for whoever binds projected_by("D1Projection") first.
@@ -0,0 +1,427 @@
1
+ require "json"
2
+ require "net/http"
3
+ require "uri"
4
+
5
+ require_relative "sql_query_builder"
6
+ require_relative "sqlite/schema_builder"
7
+ require_relative "sqlite/codec"
8
+ require_relative "sqlite" # for Sqlite::SchemaBuilder/Sqlite::Codec — see the reuse note below
9
+ require_relative "../../ports/persistence/append_only"
10
+ require_relative "../../query_specification/common/null_policy"
11
+ require_relative "../../query_specification/common/order_by"
12
+ require_relative "../../runtime/errors"
13
+ require_relative "../../runtime/event"
14
+ require_relative "../../runtime/instance"
15
+
16
+ module Hecks
17
+ module Adapters
18
+ # Cloudflare D1 — SQLite, managed, reached over its REST API rather
19
+ # than a local file. D1 IS SQLite, dialect and all, so this file
20
+ # reuses Sqlite::SchemaBuilder and Sqlite::Codec UNCHANGED (the DDL
21
+ # and the column encode/decode) and SqlQueryBuilder's dialect hooks
22
+ # are copied near-verbatim from sqlite.rb — the only real difference
23
+ # is the transport (D1::Connection, an HTTP call per query, vs a
24
+ # persistent local sqlite3 handle). See sqlite.rb's own header
25
+ # comment: "this file supplies only SQLite's dialect" — true here too.
26
+ class D1
27
+ # THE TRANSPORT — mirrors just the slice of SQLite3::Database's own
28
+ # interface (execute/get_first_row/get_first_value, rows as
29
+ # column-name-keyed hashes) that Sqlite::SchemaBuilder, Sqlite::Codec,
30
+ # and this file's own methods already assume. One stateless HTTP call
31
+ # per execute, not a persistent connection — D1 has no connection to
32
+ # hold open.
33
+ class Connection
34
+ ENDPOINT = "https://api.cloudflare.com/client/v4"
35
+
36
+ def initialize(account_id:, database_id:, api_token:)
37
+ @uri = URI("#{ENDPOINT}/accounts/#{account_id}/d1/database/#{database_id}/query")
38
+ @api_token = api_token
39
+ end
40
+
41
+ def execute(sql, binds = [])
42
+ response_results({ sql: sql, params: binds }).first.fetch("results", [])
43
+ end
44
+
45
+ # D1 batches are SQL transactions: statements execute in order and a
46
+ # failure rolls the entire sequence back. Keep the tuple-shaped local
47
+ # seam small so adapter code and focused fakes do not need to know the
48
+ # REST request envelope.
49
+ # https://developers.cloudflare.com/d1/worker-api/d1-database/#batch
50
+ def batch(statements)
51
+ payload = {
52
+ batch: statements.map do |sql, binds|
53
+ { sql: sql, params: binds || [] }
54
+ end
55
+ }
56
+ response_results(payload).map { |result| result.fetch("results", []) }
57
+ end
58
+
59
+ private
60
+
61
+ def response_results(payload)
62
+ request = Net::HTTP::Post.new(@uri)
63
+ request["Authorization"] = "Bearer #{@api_token}"
64
+ request["Content-Type"] = "application/json"
65
+ request.body = JSON.generate(payload)
66
+
67
+ response = Net::HTTP.start(@uri.host, @uri.port, use_ssl: true) { |http| http.request(request) }
68
+ body =
69
+ begin
70
+ JSON.parse(response.body)
71
+ rescue JSON::ParserError
72
+ raise Runtime::WiringError, "D1 query failed: non-JSON response (HTTP #{response.code}): #{response.body}"
73
+ end
74
+
75
+ unless body["success"]
76
+ messages = (body["errors"] || []).map { |error| error["message"] }.join("; ")
77
+ raise Runtime::WiringError, "D1 query failed: #{messages.empty? ? response.body : messages}"
78
+ end
79
+
80
+ results = body.fetch("result")
81
+ failed = results.find { |result| result["success"] == false }
82
+ if failed
83
+ messages = (body["errors"] || []).map { |error| error["message"] }.join("; ")
84
+ detail =
85
+ if failed.key?("error")
86
+ failed["error"]
87
+ elsif failed.key?("message")
88
+ failed["message"]
89
+ else
90
+ messages
91
+ end
92
+ raise Runtime::WiringError, "D1 query failed: #{detail.to_s.empty? ? 'a batched statement failed' : detail}"
93
+ end
94
+
95
+ results
96
+ end
97
+
98
+ public
99
+
100
+ def get_first_row(sql, binds = [])
101
+ execute(sql, binds).first
102
+ end
103
+
104
+ def get_first_value(sql, binds = [])
105
+ get_first_row(sql, binds)&.values&.first
106
+ end
107
+ end
108
+
109
+ include SqlQueryBuilder
110
+ include Sqlite::SchemaBuilder
111
+ include Sqlite::Codec
112
+
113
+ SQL_TYPES = { "Integer" => "INTEGER", "Float" => "REAL" }.freeze
114
+
115
+ attr_reader :aggregate
116
+
117
+ def persistence_capabilities = [:atomic_put]
118
+
119
+ def initialize(aggregate:, settings: {}, root: nil)
120
+ @aggregate = aggregate
121
+
122
+ account_id = settings.key?(:account_id) ? settings[:account_id] : settings["account_id"]
123
+ database_id = settings.key?(:database_id) ? settings[:database_id] : settings["database_id"]
124
+ api_token = settings.key?(:api_token) ? settings[:api_token] : settings["api_token"]
125
+ { "account_id" => account_id, "database_id" => database_id, "api_token" => api_token }.each do |name, value|
126
+ raise Runtime::WiringError, "D1 needs a #{name.inspect} in its world settings" if value.to_s.empty?
127
+ end
128
+
129
+ @db = Connection.new(account_id: account_id, database_id: database_id, api_token: api_token)
130
+ # THE OPTIONAL saga-persistence capability's own scoping column
131
+ # (§2/§4) — D1's domain isolation is by whole-database identity
132
+ # (one D1 database per domain in practice), so unlike Sqlite's
133
+ # own per-aggregate-file default, there's no "which file does
134
+ # this end up in" ambiguity here: every aggregate on D1 within a
135
+ # domain already shares one database.
136
+ @domain = (
137
+ if settings.key?(:domain)
138
+ settings[:domain]
139
+ elsif settings.key?("domain")
140
+ settings["domain"]
141
+ else
142
+ aggregate.name
143
+ end
144
+ ).to_s
145
+
146
+ # No PRAGMA synchronous here, unlike Sqlite — D1 is a managed
147
+ # durable service; there is no local fsync policy for a caller to
148
+ # tune, and the query endpoint has no PRAGMA write-access to it.
149
+ create_aggregate_table!
150
+ create_entry_table!
151
+ ensure_entry_operation_column!
152
+ ensure_entry_mirrors_column!
153
+ create_event_table!
154
+ create_saga_table!
155
+ end
156
+
157
+ def table = @aggregate.storage_name
158
+
159
+ def find(id)
160
+ row = @db.get_first_row("SELECT * FROM #{quoted_table} WHERE id = ?", [id.to_s])
161
+ return nil unless row
162
+
163
+ Runtime::Instance.new(aggregate: @aggregate, id: row["id"], state: decode(row))
164
+ end
165
+
166
+ # order_by IS A RUNTIME VALUE — see postgres.rb's own all for the
167
+ # full reasoning; whitelisted the identical way, same order_clause
168
+ # Sqlite's own all reuses (D1 speaks the identical dialect).
169
+ def all(order_by: nil, direction: :asc)
170
+ order_sql = "ORDER BY id"
171
+ if order_by
172
+ name = order_by.to_s.split(".").first
173
+ raise Runtime::WiringError, "#{@aggregate.name} has no attribute #{order_by.inspect} to order by" unless @aggregate.lifecycle&.field.to_s == name || @aggregate.attribute(name)
174
+
175
+ spec = QuerySpecification::Common::OrderBy.new(field: order_by, direction: direction)
176
+ order_sql = "ORDER BY #{order_clause(spec, nil)}"
177
+ end
178
+
179
+ @db.execute("SELECT * FROM #{quoted_table} #{order_sql}").map do |row|
180
+ Runtime::Instance.new(aggregate: @aggregate, id: row["id"], state: decode(row))
181
+ end
182
+ end
183
+
184
+ def count = @db.get_first_value("SELECT COUNT(*) FROM #{quoted_table}").to_i
185
+
186
+ def append(entry)
187
+ @db.execute(
188
+ "INSERT INTO #{quoted_entry_table} (aggregate_id, operation, state, mirrors) VALUES (?, ?, ?, ?)",
189
+ # `mirrors` (unlike `state`) is a NULLABLE column — an absent
190
+ # mirrors hash must bind a real SQL NULL, not the four-character
191
+ # JSON text `"null"` (`JSON.generate(nil)`), or a future `IS NULL`
192
+ # check against it would never match. Same guard `postgres_era.rb`
193
+ # already uses for its own journal's `mirrors` column.
194
+ [entry.id, entry.operation, JSON.generate(entry.state), entry.mirrors && JSON.generate(entry.mirrors)]
195
+ )
196
+ entry
197
+ end
198
+
199
+ def project(entry)
200
+ return @db.execute("DELETE FROM #{quoted_table} WHERE id = ?", [entry.id]) if entry.delete?
201
+
202
+ instance = Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: entry.state)
203
+ columns = (["id"] + persisted_fields.map { |field| field[:name].to_s }).map { |c| quote_ident(c) }
204
+ values = [instance.id.to_s] + persisted_fields.map { |field| encode_field(field, instance[field[:name]]) }
205
+ slots = Array.new(columns.size, "?").join(", ")
206
+
207
+ @db.execute(
208
+ "INSERT OR REPLACE INTO #{quoted_table} (#{columns.join(', ')}) VALUES (#{slots})",
209
+ values
210
+ )
211
+ instance
212
+ end
213
+
214
+ def entries
215
+ @db.execute("SELECT aggregate_id, operation, state, mirrors FROM #{quoted_entry_table} ORDER BY sequence").map do |row|
216
+ state = JSON.parse(row["state"])
217
+ Ports::Persistence::Entry.new(
218
+ operation: row["operation"] || "save",
219
+ id: row["aggregate_id"],
220
+ state: state&.transform_keys(&:to_sym),
221
+ mirrors: row["mirrors"] && JSON.parse(row["mirrors"])
222
+ )
223
+ end
224
+ end
225
+
226
+ def reset!
227
+ @db.execute("DELETE FROM #{quoted_table}")
228
+ @db.execute("DELETE FROM #{quoted_entry_table}")
229
+ self
230
+ end
231
+
232
+ def save(instance)
233
+ entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
234
+ append(entry)
235
+ project(entry)
236
+ end
237
+
238
+ # Classification, durable journal append and current-state projection
239
+ # are one D1 batch transaction and therefore one HTTP request. The first
240
+ # statement supplies the outcome from database state inside that same
241
+ # transaction; the runtime performs no preliminary find.
242
+ #
243
+ # `insert_only:` used to spend a SEPARATE, EARLIER round trip finding
244
+ # out whether the row existed before ever building the batch — a real
245
+ # TOCTOU gap (two concurrent creates at the same identity could both
246
+ # pass that check before either wrote). D1's batch has no conditional
247
+ # BRANCH of its own, true, but it does not need one: a batch's own
248
+ # statements already execute in order, atomically, as one transaction
249
+ # (Connection#batch's own comment) — the exact guarantee the single-
250
+ # connection adapters' own `@db.transaction do ... end` gets locally.
251
+ # So the existence check moves INSIDE the batch as its own first
252
+ # statement, and the two writes are individually gated with `WHERE NOT
253
+ # EXISTS (...)` against that same table, evaluated in the same
254
+ # transaction — a row that already existed makes both writes into
255
+ # real, zero-row no-ops rather than skipping them from the Ruby side,
256
+ # matching Sqlite#atomic_put's `next` (skip append AND project both,
257
+ # together) with no second HTTP call and no gap for another writer to
258
+ # land in between the check and the write.
259
+ def atomic_put(entry, insert_only: false)
260
+ instance = Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: entry.state)
261
+ columns = (["id"] + persisted_fields.map { |field| field[:name].to_s }).map { |column| quote_ident(column) }
262
+ values = [instance.id.to_s] + persisted_fields.map { |field| encode_field(field, instance[field[:name]]) }
263
+ slots = Array.new(columns.size, "?").join(", ")
264
+ not_exists = "WHERE NOT EXISTS (SELECT 1 FROM #{quoted_table} WHERE id = ?)"
265
+
266
+ status_sql =
267
+ if insert_only
268
+ "SELECT CASE WHEN EXISTS (SELECT 1 FROM #{quoted_table} WHERE id = ?) " \
269
+ "THEN 'conflicted' ELSE 'inserted' END AS status"
270
+ else
271
+ "SELECT CASE WHEN EXISTS (SELECT 1 FROM #{quoted_table} WHERE id = ?) " \
272
+ "THEN 'replaced' ELSE 'inserted' END AS status"
273
+ end
274
+
275
+ # `mirrors` is NULLABLE (unlike `state`) — see `append`'s own comment.
276
+ encoded_mirrors = entry.mirrors && JSON.generate(entry.mirrors)
277
+
278
+ entry_sql, entry_binds =
279
+ if insert_only
280
+ [
281
+ "INSERT INTO #{quoted_entry_table} (aggregate_id, operation, state, mirrors) " \
282
+ "SELECT ?, ?, ?, ? #{not_exists}",
283
+ [entry.id, entry.operation, JSON.generate(entry.state), encoded_mirrors, entry.id.to_s]
284
+ ]
285
+ else
286
+ [
287
+ "INSERT INTO #{quoted_entry_table} (aggregate_id, operation, state, mirrors) VALUES (?, ?, ?, ?)",
288
+ [entry.id, entry.operation, JSON.generate(entry.state), encoded_mirrors]
289
+ ]
290
+ end
291
+
292
+ aggregate_sql, aggregate_binds =
293
+ if insert_only
294
+ [
295
+ "INSERT INTO #{quoted_table} (#{columns.join(', ')}) SELECT #{slots} #{not_exists}",
296
+ values + [entry.id.to_s]
297
+ ]
298
+ else
299
+ [
300
+ "INSERT OR REPLACE INTO #{quoted_table} (#{columns.join(', ')}) VALUES (#{slots})",
301
+ values
302
+ ]
303
+ end
304
+
305
+ results = @db.batch([
306
+ [status_sql, [entry.id.to_s]],
307
+ [entry_sql, entry_binds],
308
+ [aggregate_sql, aggregate_binds]
309
+ ])
310
+
311
+ results.fetch(0).fetch(0).fetch("status").to_sym
312
+ end
313
+
314
+ def delete(id)
315
+ entry = Ports::Persistence::Entry.new(operation: "delete", id: id.to_s, state: nil)
316
+ append(entry)
317
+ project(entry)
318
+ true
319
+ end
320
+
321
+ def record_event(event)
322
+ @db.execute(
323
+ "INSERT INTO events (name, aggregate, aggregate_id, payload, occurred_at) VALUES (?, ?, ?, ?, ?)",
324
+ [event.name, event.aggregate, event.id.to_s, JSON.generate(event.payload), event.occurred_at]
325
+ )
326
+ end
327
+
328
+ def events
329
+ @db.execute("SELECT * FROM events ORDER BY id").map do |row|
330
+ Runtime::Event.new(
331
+ name: row["name"],
332
+ aggregate: row["aggregate"],
333
+ id: row["aggregate_id"],
334
+ payload: JSON.parse(row["payload"], symbolize_names: true),
335
+ occurred_at: row["occurred_at"]
336
+ )
337
+ end
338
+ end
339
+
340
+ # ── the OPTIONAL saga-persistence capability (§2) — reuses the DDL
341
+ # `Sqlite::SchemaBuilder` already shares with Sqlite (`d1.rb`'s own
342
+ # file header). Same `?`-placeholder shape every other write here
343
+ # already uses through `Connection#execute`.
344
+ def save_saga(process_manager:, correlation:, state:, memory:, completed_compensations: [])
345
+ @db.execute(
346
+ "INSERT OR REPLACE INTO hecks_saga_instances (domain, process_manager, correlation, state, memory, completed_compensations) " \
347
+ "VALUES (?, ?, ?, ?, ?, ?)",
348
+ [@domain, process_manager.to_s, correlation.to_s, state.to_s, JSON.generate(memory), JSON.generate(completed_compensations)]
349
+ )
350
+ end
351
+
352
+ def delete_saga(process_manager:, correlation:)
353
+ @db.execute(
354
+ "DELETE FROM hecks_saga_instances WHERE domain = ? AND process_manager = ? AND correlation = ?",
355
+ [@domain, process_manager.to_s, correlation.to_s]
356
+ )
357
+ end
358
+
359
+ def each_saga
360
+ return enum_for(:each_saga) unless block_given?
361
+
362
+ @db.execute(
363
+ "SELECT process_manager, correlation, state, memory, completed_compensations FROM hecks_saga_instances WHERE domain = ?",
364
+ [@domain]
365
+ ).each do |row|
366
+ yield row["process_manager"], row["correlation"], row["state"],
367
+ JSON.parse(row["memory"], symbolize_names: true),
368
+ JSON.parse(row["completed_compensations"] || "[]", symbolize_names: true)
369
+ end
370
+ end
371
+
372
+ private
373
+
374
+ # ── SqlQueryBuilder's dialect hooks — identical to Sqlite's, since
375
+ # D1 speaks the same dialect (copied, not shared by module include,
376
+ # because Sqlite's own copies are private instance methods on a
377
+ # different class — see the file header) ───────────────────────
378
+ def select_list = "*"
379
+ def from_relation = quoted_table
380
+ def dialect_name = "D1"
381
+ def empty_in_clause = "0"
382
+
383
+ def placeholder(binds, value)
384
+ binds << value
385
+ "?"
386
+ end
387
+
388
+ def contains_clause(expression, placeholder)
389
+ "instr(#{expression}, #{placeholder}) > 0"
390
+ end
391
+
392
+ def list_contains_clause(column, member, placeholder)
393
+ target = member.empty? ? "json_each.value" : "json_extract(json_each.value, '$.#{member}')"
394
+ "EXISTS (SELECT 1 FROM json_each(#{quote_ident(column)}) WHERE #{target} = #{placeholder})"
395
+ end
396
+
397
+ def plain_column(name) = quote_ident(name)
398
+
399
+ def nested_expression(name, path, member)
400
+ json_path = path.empty? ? "$.#{member || 'value'}" : "$.#{path.join('.')}"
401
+ "json_extract(#{quote_ident(name)}, '#{json_path}')"
402
+ end
403
+
404
+ # SQLite (and D1, the same engine) has no bare OFFSET — LIMIT -1 is
405
+ # its own documented unbounded spelling, exactly for this case.
406
+ def unbounded_limit = " LIMIT -1"
407
+
408
+ def order_clause(order_by, policy)
409
+ QuerySpecification::Common::NullPolicy.sql_order(query_expression(order_by.field), order_by.direction, policy)
410
+ end
411
+
412
+ def execute_query(sql, binds)
413
+ @db.execute(sql, binds).map { |row| Runtime::Instance.new(aggregate: @aggregate, id: row["id"], state: decode(row)) }
414
+ end
415
+
416
+ # ── the rest of the dialect ─────────────────────────────────────
417
+
418
+ def quote_ident(name)
419
+ %("#{name.to_s.gsub('"', '""')}")
420
+ end
421
+
422
+ def quoted_table = quote_ident(table)
423
+ def entry_table = "#{table}_entries"
424
+ def quoted_entry_table = quote_ident(entry_table)
425
+ end
426
+ end
427
+ end
@@ -0,0 +1,3 @@
1
+ Hecks.adapter "Folder" do
2
+ port "loading"
3
+ end