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
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: c40a3c6e1879b5df4a299ae07488cffe80fc0c9ee22905044b876b3887c2cfc3
4
+ data.tar.gz: 983121c63439d9f7d79c77e0dabcf34e1625df7067368c27438ff64f0065291e
5
5
  SHA512:
6
- metadata.gz: 7f100beec30a025476d12fd3061cdddbf24a1111f8123805c890a4d654b611f59c674364c3fa820be9895cc9a4be4f235b86ad18574fc76c04d14e94e8c08eed
7
- data.tar.gz: d2ee8bb2c264169866082e0b176a616ad8bb0fc02bd8c8468a5088faa6ca8acd4bfb70bfabe915f32c817343fc22355f4e2e8128ad165a09880dc95d91ad1b04
6
+ metadata.gz: 17ffccfc67272cb90216229ebb41e6fdf4199b6ca6f4e1f5ecee7964191190c2e0f562b2c1fd0c0f7d304f3da95ab3521a3ca13fdcc162935fc99c9a33aca866
7
+ data.tar.gz: 9cb2f68d53120a1e7a7ab59e91387661151243d51927f7de690986597c3e2932f09b7680cabb0933e1b82683dffd29b9581167f05799864c56e3ddfa1b502970
@@ -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,403 @@
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 = failed["error"] || failed["message"] || messages
85
+ raise Runtime::WiringError, "D1 query failed: #{detail.to_s.empty? ? 'a batched statement failed' : detail}"
86
+ end
87
+
88
+ results
89
+ end
90
+
91
+ public
92
+
93
+ def get_first_row(sql, binds = [])
94
+ execute(sql, binds).first
95
+ end
96
+
97
+ def get_first_value(sql, binds = [])
98
+ get_first_row(sql, binds)&.values&.first
99
+ end
100
+ end
101
+
102
+ include SqlQueryBuilder
103
+ include Sqlite::SchemaBuilder
104
+ include Sqlite::Codec
105
+
106
+ SQL_TYPES = { "Integer" => "INTEGER", "Float" => "REAL" }.freeze
107
+
108
+ attr_reader :aggregate
109
+
110
+ def persistence_capabilities = [:atomic_put]
111
+
112
+ def initialize(aggregate:, settings: {}, root: nil)
113
+ @aggregate = aggregate
114
+
115
+ account_id = settings[:account_id] || settings["account_id"]
116
+ database_id = settings[:database_id] || settings["database_id"]
117
+ api_token = settings[:api_token] || settings["api_token"]
118
+ { "account_id" => account_id, "database_id" => database_id, "api_token" => api_token }.each do |name, value|
119
+ raise Runtime::WiringError, "D1 needs a #{name.inspect} in its world settings" if value.to_s.empty?
120
+ end
121
+
122
+ @db = Connection.new(account_id: account_id, database_id: database_id, api_token: api_token)
123
+ # THE OPTIONAL saga-persistence capability's own scoping column
124
+ # (§2/§4) — D1's domain isolation is by whole-database identity
125
+ # (one D1 database per domain in practice), so unlike Sqlite's
126
+ # own per-aggregate-file default, there's no "which file does
127
+ # this end up in" ambiguity here: every aggregate on D1 within a
128
+ # domain already shares one database.
129
+ @domain = (settings[:domain] || settings["domain"] || aggregate.name).to_s
130
+
131
+ # No PRAGMA synchronous here, unlike Sqlite — D1 is a managed
132
+ # durable service; there is no local fsync policy for a caller to
133
+ # tune, and the query endpoint has no PRAGMA write-access to it.
134
+ create_aggregate_table!
135
+ create_entry_table!
136
+ ensure_entry_operation_column!
137
+ ensure_entry_mirrors_column!
138
+ create_event_table!
139
+ create_saga_table!
140
+ end
141
+
142
+ def table = @aggregate.storage_name
143
+
144
+ def find(id)
145
+ row = @db.get_first_row("SELECT * FROM #{quoted_table} WHERE id = ?", [id.to_s])
146
+ return nil unless row
147
+
148
+ Runtime::Instance.new(aggregate: @aggregate, id: row["id"], state: decode(row))
149
+ end
150
+
151
+ # order_by IS A RUNTIME VALUE — see postgres.rb's own all for the
152
+ # full reasoning; whitelisted the identical way, same order_clause
153
+ # Sqlite's own all reuses (D1 speaks the identical dialect).
154
+ def all(order_by: nil, direction: :asc)
155
+ order_sql = "ORDER BY id"
156
+ if order_by
157
+ name = order_by.to_s.split(".").first
158
+ raise Runtime::WiringError, "#{@aggregate.name} has no attribute #{order_by.inspect} to order by" unless @aggregate.lifecycle&.field.to_s == name || @aggregate.attribute(name)
159
+
160
+ spec = QuerySpecification::Common::OrderBy.new(field: order_by, direction: direction)
161
+ order_sql = "ORDER BY #{order_clause(spec, nil)}"
162
+ end
163
+
164
+ @db.execute("SELECT * FROM #{quoted_table} #{order_sql}").map do |row|
165
+ Runtime::Instance.new(aggregate: @aggregate, id: row["id"], state: decode(row))
166
+ end
167
+ end
168
+
169
+ def count = @db.get_first_value("SELECT COUNT(*) FROM #{quoted_table}").to_i
170
+
171
+ def append(entry)
172
+ @db.execute(
173
+ "INSERT INTO #{quoted_entry_table} (aggregate_id, operation, state, mirrors) VALUES (?, ?, ?, ?)",
174
+ [entry.id, entry.operation, JSON.generate(entry.state), JSON.generate(entry.mirrors)]
175
+ )
176
+ entry
177
+ end
178
+
179
+ def project(entry)
180
+ return @db.execute("DELETE FROM #{quoted_table} WHERE id = ?", [entry.id]) if entry.delete?
181
+
182
+ instance = Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: entry.state)
183
+ columns = (["id"] + persisted_fields.map { |field| field[:name].to_s }).map { |c| quote_ident(c) }
184
+ values = [instance.id.to_s] + persisted_fields.map { |field| encode_field(field, instance[field[:name]]) }
185
+ slots = Array.new(columns.size, "?").join(", ")
186
+
187
+ @db.execute(
188
+ "INSERT OR REPLACE INTO #{quoted_table} (#{columns.join(', ')}) VALUES (#{slots})",
189
+ values
190
+ )
191
+ instance
192
+ end
193
+
194
+ def entries
195
+ @db.execute("SELECT aggregate_id, operation, state, mirrors FROM #{quoted_entry_table} ORDER BY sequence").map do |row|
196
+ state = JSON.parse(row["state"])
197
+ Ports::Persistence::Entry.new(
198
+ operation: row["operation"] || "save",
199
+ id: row["aggregate_id"],
200
+ state: state&.transform_keys(&:to_sym),
201
+ mirrors: row["mirrors"] && JSON.parse(row["mirrors"])
202
+ )
203
+ end
204
+ end
205
+
206
+ def reset!
207
+ @db.execute("DELETE FROM #{quoted_table}")
208
+ @db.execute("DELETE FROM #{quoted_entry_table}")
209
+ self
210
+ end
211
+
212
+ def save(instance)
213
+ entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
214
+ append(entry)
215
+ project(entry)
216
+ end
217
+
218
+ # Classification, durable journal append and current-state projection
219
+ # are one D1 batch transaction and therefore one HTTP request. The first
220
+ # statement supplies the outcome from database state inside that same
221
+ # transaction; the runtime performs no preliminary find.
222
+ #
223
+ # `insert_only:` used to spend a SEPARATE, EARLIER round trip finding
224
+ # out whether the row existed before ever building the batch — a real
225
+ # TOCTOU gap (two concurrent creates at the same identity could both
226
+ # pass that check before either wrote). D1's batch has no conditional
227
+ # BRANCH of its own, true, but it does not need one: a batch's own
228
+ # statements already execute in order, atomically, as one transaction
229
+ # (Connection#batch's own comment) — the exact guarantee the single-
230
+ # connection adapters' own `@db.transaction do ... end` gets locally.
231
+ # So the existence check moves INSIDE the batch as its own first
232
+ # statement, and the two writes are individually gated with `WHERE NOT
233
+ # EXISTS (...)` against that same table, evaluated in the same
234
+ # transaction — a row that already existed makes both writes into
235
+ # real, zero-row no-ops rather than skipping them from the Ruby side,
236
+ # matching Sqlite#atomic_put's `next` (skip append AND project both,
237
+ # together) with no second HTTP call and no gap for another writer to
238
+ # land in between the check and the write.
239
+ def atomic_put(entry, insert_only: false)
240
+ instance = Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: entry.state)
241
+ columns = (["id"] + persisted_fields.map { |field| field[:name].to_s }).map { |column| quote_ident(column) }
242
+ values = [instance.id.to_s] + persisted_fields.map { |field| encode_field(field, instance[field[:name]]) }
243
+ slots = Array.new(columns.size, "?").join(", ")
244
+ not_exists = "WHERE NOT EXISTS (SELECT 1 FROM #{quoted_table} WHERE id = ?)"
245
+
246
+ status_sql =
247
+ if insert_only
248
+ "SELECT CASE WHEN EXISTS (SELECT 1 FROM #{quoted_table} WHERE id = ?) " \
249
+ "THEN 'conflicted' ELSE 'inserted' END AS status"
250
+ else
251
+ "SELECT CASE WHEN EXISTS (SELECT 1 FROM #{quoted_table} WHERE id = ?) " \
252
+ "THEN 'replaced' ELSE 'inserted' END AS status"
253
+ end
254
+
255
+ entry_sql, entry_binds =
256
+ if insert_only
257
+ [
258
+ "INSERT INTO #{quoted_entry_table} (aggregate_id, operation, state, mirrors) " \
259
+ "SELECT ?, ?, ?, ? #{not_exists}",
260
+ [entry.id, entry.operation, JSON.generate(entry.state), JSON.generate(entry.mirrors), entry.id.to_s]
261
+ ]
262
+ else
263
+ [
264
+ "INSERT INTO #{quoted_entry_table} (aggregate_id, operation, state, mirrors) VALUES (?, ?, ?, ?)",
265
+ [entry.id, entry.operation, JSON.generate(entry.state), JSON.generate(entry.mirrors)]
266
+ ]
267
+ end
268
+
269
+ aggregate_sql, aggregate_binds =
270
+ if insert_only
271
+ [
272
+ "INSERT INTO #{quoted_table} (#{columns.join(', ')}) SELECT #{slots} #{not_exists}",
273
+ values + [entry.id.to_s]
274
+ ]
275
+ else
276
+ [
277
+ "INSERT OR REPLACE INTO #{quoted_table} (#{columns.join(', ')}) VALUES (#{slots})",
278
+ values
279
+ ]
280
+ end
281
+
282
+ results = @db.batch([
283
+ [status_sql, [entry.id.to_s]],
284
+ [entry_sql, entry_binds],
285
+ [aggregate_sql, aggregate_binds]
286
+ ])
287
+
288
+ results.fetch(0).fetch(0).fetch("status").to_sym
289
+ end
290
+
291
+ def delete(id)
292
+ entry = Ports::Persistence::Entry.new(operation: "delete", id: id.to_s, state: nil)
293
+ append(entry)
294
+ project(entry)
295
+ true
296
+ end
297
+
298
+ def record_event(event)
299
+ @db.execute(
300
+ "INSERT INTO events (name, aggregate, aggregate_id, payload, occurred_at) VALUES (?, ?, ?, ?, ?)",
301
+ [event.name, event.aggregate, event.id.to_s, JSON.generate(event.payload), event.occurred_at]
302
+ )
303
+ end
304
+
305
+ def events
306
+ @db.execute("SELECT * FROM events ORDER BY id").map do |row|
307
+ Runtime::Event.new(
308
+ name: row["name"],
309
+ aggregate: row["aggregate"],
310
+ id: row["aggregate_id"],
311
+ payload: JSON.parse(row["payload"], symbolize_names: true),
312
+ occurred_at: row["occurred_at"]
313
+ )
314
+ end
315
+ end
316
+
317
+ # ── the OPTIONAL saga-persistence capability (§2) — reuses the DDL
318
+ # `Sqlite::SchemaBuilder` already shares with Sqlite (`d1.rb`'s own
319
+ # file header). Same `?`-placeholder shape every other write here
320
+ # already uses through `Connection#execute`.
321
+ def save_saga(process_manager:, correlation:, state:, memory:)
322
+ @db.execute(
323
+ "INSERT OR REPLACE INTO hecks_saga_instances (domain, process_manager, correlation, state, memory) " \
324
+ "VALUES (?, ?, ?, ?, ?)",
325
+ [@domain, process_manager.to_s, correlation.to_s, state.to_s, JSON.generate(memory)]
326
+ )
327
+ end
328
+
329
+ def delete_saga(process_manager:, correlation:)
330
+ @db.execute(
331
+ "DELETE FROM hecks_saga_instances WHERE domain = ? AND process_manager = ? AND correlation = ?",
332
+ [@domain, process_manager.to_s, correlation.to_s]
333
+ )
334
+ end
335
+
336
+ def each_saga
337
+ return enum_for(:each_saga) unless block_given?
338
+
339
+ @db.execute(
340
+ "SELECT process_manager, correlation, state, memory FROM hecks_saga_instances WHERE domain = ?",
341
+ [@domain]
342
+ ).each do |row|
343
+ yield row["process_manager"], row["correlation"], row["state"],
344
+ JSON.parse(row["memory"], symbolize_names: true)
345
+ end
346
+ end
347
+
348
+ private
349
+
350
+ # ── SqlQueryBuilder's dialect hooks — identical to Sqlite's, since
351
+ # D1 speaks the same dialect (copied, not shared by module include,
352
+ # because Sqlite's own copies are private instance methods on a
353
+ # different class — see the file header) ───────────────────────
354
+ def select_list = "*"
355
+ def from_relation = quoted_table
356
+ def dialect_name = "D1"
357
+ def empty_in_clause = "0"
358
+
359
+ def placeholder(binds, value)
360
+ binds << value
361
+ "?"
362
+ end
363
+
364
+ def contains_clause(expression, placeholder)
365
+ "instr(#{expression}, #{placeholder}) > 0"
366
+ end
367
+
368
+ def list_contains_clause(column, member, placeholder)
369
+ target = member.empty? ? "json_each.value" : "json_extract(json_each.value, '$.#{member}')"
370
+ "EXISTS (SELECT 1 FROM json_each(#{quote_ident(column)}) WHERE #{target} = #{placeholder})"
371
+ end
372
+
373
+ def plain_column(name) = quote_ident(name)
374
+
375
+ def nested_expression(name, path, member)
376
+ json_path = path.empty? ? "$.#{member || 'value'}" : "$.#{path.join('.')}"
377
+ "json_extract(#{quote_ident(name)}, '#{json_path}')"
378
+ end
379
+
380
+ # SQLite (and D1, the same engine) has no bare OFFSET — LIMIT -1 is
381
+ # its own documented unbounded spelling, exactly for this case.
382
+ def unbounded_limit = " LIMIT -1"
383
+
384
+ def order_clause(order_by, policy)
385
+ QuerySpecification::Common::NullPolicy.sql_order(query_expression(order_by.field), order_by.direction, policy)
386
+ end
387
+
388
+ def execute_query(sql, binds)
389
+ @db.execute(sql, binds).map { |row| Runtime::Instance.new(aggregate: @aggregate, id: row["id"], state: decode(row)) }
390
+ end
391
+
392
+ # ── the rest of the dialect ─────────────────────────────────────
393
+
394
+ def quote_ident(name)
395
+ %("#{name.to_s.gsub('"', '""')}")
396
+ end
397
+
398
+ def quoted_table = quote_ident(table)
399
+ def entry_table = "#{table}_entries"
400
+ def quoted_entry_table = quote_ident(entry_table)
401
+ end
402
+ end
403
+ end
@@ -0,0 +1,3 @@
1
+ Hecks.adapter "Folder" do
2
+ port "loading"
3
+ end