hecks 1.3.0 → 1.5.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 (373) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +72 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +83 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
  7. data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
  9. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  10. data/lib/hecks/adapters/driven/heki.rb +106 -9
  11. data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
  12. data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
  13. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  15. data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
  16. data/lib/hecks/adapters/driven/lambda.rb +82 -35
  17. data/lib/hecks/adapters/driven/local_storage.rb +83 -10
  18. data/lib/hecks/adapters/driven/memory.rb +205 -9
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  24. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  25. data/lib/hecks/adapters/driven/prism.rb +50 -11
  26. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  31. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  32. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  33. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  34. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  35. data/lib/hecks/adapters/driven.rb +6 -4
  36. data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
  37. data/lib/hecks/behaviors/dsl.rb +60 -2
  38. data/lib/hecks/behaviors/expectations.rb +190 -29
  39. data/lib/hecks/behaviors/ir.rb +12 -1
  40. data/lib/hecks/behaviors/rspec.rb +9 -1
  41. data/lib/hecks/behaviors/runner.rb +21 -2
  42. data/lib/hecks/behaviors.rb +9 -1
  43. data/lib/hecks/bluebook/aggregate.rb +43 -13
  44. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
  45. data/lib/hecks/bluebook/assembly/build.rb +26 -5
  46. data/lib/hecks/bluebook/assembly/contract.rb +98 -23
  47. data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
  48. data/lib/hecks/bluebook/assembly/marks.rb +159 -30
  49. data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
  50. data/lib/hecks/bluebook/assembly.rb +32 -14
  51. data/lib/hecks/bluebook/attribute.rb +26 -12
  52. data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
  53. data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
  54. data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
  55. data/lib/hecks/bluebook/behaviour/command.rb +55 -25
  56. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
  57. data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
  58. data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
  59. data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
  60. data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
  61. data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
  62. data/lib/hecks/bluebook/behaviour/query.rb +6 -1
  63. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
  64. data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
  65. data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
  66. data/lib/hecks/bluebook/capabilities.rb +27 -0
  67. data/lib/hecks/bluebook/chapter.rb +51 -9
  68. data/lib/hecks/bluebook/command.rb +62 -17
  69. data/lib/hecks/bluebook/domain_port.rb +34 -9
  70. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  71. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  72. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
  73. data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
  74. data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
  75. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
  76. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
  77. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  78. data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
  79. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  80. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  81. data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
  82. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  83. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
  84. data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
  85. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  86. data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
  87. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  88. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  89. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
  90. data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
  91. data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
  92. data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
  93. data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
  94. data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
  95. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  96. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  97. data/lib/hecks/bluebook/entity.rb +40 -11
  98. data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
  99. data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
  100. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
  101. data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
  102. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
  103. data/lib/hecks/bluebook/expression/resolver.rb +369 -128
  104. data/lib/hecks/bluebook/hexagon.rb +35 -1
  105. data/lib/hecks/bluebook/lifecycle.rb +12 -1
  106. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
  107. data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
  108. data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
  109. data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
  110. data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
  111. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
  112. data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
  113. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
  114. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
  115. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
  116. data/lib/hecks/bluebook/meta_validator.rb +235 -139
  117. data/lib/hecks/bluebook/model_check.rb +434 -104
  118. data/lib/hecks/bluebook/pattern_subset.rb +32 -10
  119. data/lib/hecks/bluebook/policy.rb +15 -13
  120. data/lib/hecks/bluebook/process_manager.rb +27 -14
  121. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  122. data/lib/hecks/bluebook/project_loader.rb +40 -0
  123. data/lib/hecks/bluebook/project_register.rb +50 -6
  124. data/lib/hecks/bluebook/query.rb +31 -4
  125. data/lib/hecks/bluebook/read_model.rb +35 -15
  126. data/lib/hecks/bluebook/reference.rb +26 -13
  127. data/lib/hecks/bluebook/smoke_test.rb +46 -23
  128. data/lib/hecks/bluebook/synthesizer.rb +46 -12
  129. data/lib/hecks/bluebook/translation.rb +34 -5
  130. data/lib/hecks/bluebook/value_object.rb +29 -11
  131. data/lib/hecks/bluebook.rb +5 -6
  132. data/lib/hecks/codemod.rb +138 -50
  133. data/lib/hecks/construct.rb +21 -7
  134. data/lib/hecks/corpus.rb +438 -0
  135. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  136. data/lib/hecks/doc/reference.rb +200 -31
  137. data/lib/hecks/embryonaut_bluebook.rb +38 -15
  138. data/lib/hecks/facade/cli_door.rb +69 -10
  139. data/lib/hecks/facade/cli_runner.rb +105 -24
  140. data/lib/hecks/facade/command_request.rb +23 -0
  141. data/lib/hecks/facade/handle.rb +155 -35
  142. data/lib/hecks/facade/json_door.rb +106 -25
  143. data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
  144. data/lib/hecks/facade/surface/chapter.rb +26 -17
  145. data/lib/hecks/facade/surface.rb +16 -3
  146. data/lib/hecks/facade.rb +15 -4
  147. data/lib/hecks/forms/app.rb +46 -30
  148. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  149. data/lib/hecks/forms/field_renderer.rb +142 -6
  150. data/lib/hecks/forms/field_shape.rb +183 -19
  151. data/lib/hecks/forms/html.rb +51 -7
  152. data/lib/hecks/forms/index_renderer.rb +14 -2
  153. data/lib/hecks/forms/page.rb +14 -0
  154. data/lib/hecks/forms/params.rb +120 -23
  155. data/lib/hecks/forms/port_argument.rb +14 -2
  156. data/lib/hecks/forms/query_form_renderer.rb +65 -2
  157. data/lib/hecks/forms/record_renderer.rb +60 -2
  158. data/lib/hecks/forms/record_table.rb +28 -1
  159. data/lib/hecks/forms/reference_options.rb +24 -0
  160. data/lib/hecks/forms/value_object_shape.rb +13 -3
  161. data/lib/hecks/forms.rb +24 -4
  162. data/lib/hecks/fqn.rb +59 -1
  163. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  164. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  165. data/lib/hecks/framework/oidc.json +15 -0
  166. data/lib/hecks/framework.rb +79 -25
  167. data/lib/hecks/freezer.rb +27 -11
  168. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
  169. data/lib/hecks/fuzzing/combination_miner.rb +178 -0
  170. data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
  171. data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
  172. data/lib/hecks/fuzzing/differential.rb +192 -0
  173. data/lib/hecks/fuzzing/domain_generator.rb +871 -0
  174. data/lib/hecks/fuzzing/era_boundary.rb +68 -19
  175. data/lib/hecks/fuzzing/form_census.rb +121 -20
  176. data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
  177. data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
  178. data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
  179. data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
  180. data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
  181. data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
  182. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
  183. data/lib/hecks/fuzzing/properties/guards.rb +86 -42
  184. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
  185. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
  186. data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
  187. data/lib/hecks/fuzzing/properties/querying.rb +82 -28
  188. data/lib/hecks/fuzzing/properties.rb +84 -51
  189. data/lib/hecks/fuzzing/qa_settings.rb +164 -0
  190. data/lib/hecks/fuzzing/replay.rb +241 -133
  191. data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
  192. data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
  193. data/lib/hecks/fuzzing/self_consistency.rb +315 -137
  194. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
  195. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  196. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  197. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  198. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
  199. data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
  200. data/lib/hecks/fuzzing/shrinker.rb +309 -0
  201. data/lib/hecks/fuzzing/structural_skips.rb +37 -130
  202. data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
  203. data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
  204. data/lib/hecks/fuzzing/value_generator.rb +110 -22
  205. data/lib/hecks/fuzzing.rb +5 -0
  206. data/lib/hecks/grammar/evolve.rb +188 -12
  207. data/lib/hecks/grammar.rb +53 -7
  208. data/lib/hecks/ir.rb +51 -20
  209. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  210. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  211. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  212. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  213. data/lib/hecks/language/oidc.json +5 -0
  214. data/lib/hecks/literal.rb +41 -9
  215. data/lib/hecks/naming.rb +112 -31
  216. data/lib/hecks/ports/access_control.rb +53 -2
  217. data/lib/hecks/ports/agent/answers.rb +83 -6
  218. data/lib/hecks/ports/agent.rb +119 -35
  219. data/lib/hecks/ports/authentication.rb +44 -4
  220. data/lib/hecks/ports/authorization.rb +50 -11
  221. data/lib/hecks/ports/clock.rb +42 -23
  222. data/lib/hecks/ports/extraction.rb +16 -0
  223. data/lib/hecks/ports/identity_assignment.rb +23 -2
  224. data/lib/hecks/ports/identity_generation.rb +17 -3
  225. data/lib/hecks/ports/identity_resolution.rb +17 -1
  226. data/lib/hecks/ports/key_vault.port +6 -0
  227. data/lib/hecks/ports/key_vault.rb +58 -0
  228. data/lib/hecks/ports/loading.rb +4 -0
  229. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  230. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  231. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  232. data/lib/hecks/ports/persistence/execution.rb +4 -0
  233. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  234. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  235. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  236. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  237. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  238. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  239. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  250. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  251. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  252. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
  253. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  254. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  255. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  256. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  257. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  258. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
  259. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  260. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  261. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  262. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
  263. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
  264. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  265. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
  266. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  267. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  268. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  269. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  270. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  271. data/lib/hecks/ports/persistence.rb +36 -1
  272. data/lib/hecks/ports/projection.rb +61 -7
  273. data/lib/hecks/ports/query/in_memory.rb +41 -8
  274. data/lib/hecks/ports/query/ordering.rb +21 -6
  275. data/lib/hecks/ports/query.rb +35 -0
  276. data/lib/hecks/ports.rb +1 -0
  277. data/lib/hecks/projections/bootstrap_table.rb +147 -0
  278. data/lib/hecks/projections/diagrams.rb +315 -79
  279. data/lib/hecks/projections/glossary/html.rb +91 -3
  280. data/lib/hecks/projections/glossary/markdown.rb +50 -3
  281. data/lib/hecks/projections/glossary/mermaid.rb +49 -1
  282. data/lib/hecks/projections/glossary/sections.rb +1 -1
  283. data/lib/hecks/projections/glossary/sentences.rb +110 -5
  284. data/lib/hecks/projections/glossary.rb +173 -31
  285. data/lib/hecks/projections/ir.rb +1 -1
  286. data/lib/hecks/projections/model/deviations.rb +62 -17
  287. data/lib/hecks/projections/model.rb +75 -21
  288. data/lib/hecks/projections/oidc.rb +45 -8
  289. data/lib/hecks/projections/parser_table.rb +53 -12
  290. data/lib/hecks/projections/reference.rb +15 -3
  291. data/lib/hecks/projections/rust_vocabulary.rb +646 -0
  292. data/lib/hecks/projections/shape.rb +10 -2
  293. data/lib/hecks/projections/statements.rb +69 -22
  294. data/lib/hecks/projections/vocabulary.rb +26 -9
  295. data/lib/hecks/projections.rb +5 -3
  296. data/lib/hecks/projector/cli_projector.rb +245 -37
  297. data/lib/hecks/projector/docs_projector.rb +154 -28
  298. data/lib/hecks/projector/exporter.rb +104 -29
  299. data/lib/hecks/projector/ir_projector.rb +7 -1
  300. data/lib/hecks/projector/narrate_projector.rb +144 -23
  301. data/lib/hecks/projector/target.rb +42 -18
  302. data/lib/hecks/projector.rb +86 -18
  303. data/lib/hecks/query_ir.rb +94 -47
  304. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  305. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  306. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  307. data/lib/hecks/query_specification/common/null_policy.rb +61 -15
  308. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  309. data/lib/hecks/query_specification/common/options.rb +25 -0
  310. data/lib/hecks/query_specification/field_path.rb +69 -15
  311. data/lib/hecks/query_specification/hop_path.rb +57 -20
  312. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  313. data/lib/hecks/rendering.rb +9 -3
  314. data/lib/hecks/router/namespace_installer.rb +16 -3
  315. data/lib/hecks/router.rb +56 -1
  316. data/lib/hecks/runtime/aggregate_lock.rb +20 -11
  317. data/lib/hecks/runtime/boot_gates.rb +21 -3
  318. data/lib/hecks/runtime/caller.rb +40 -8
  319. data/lib/hecks/runtime/capability_graph.rb +13 -2
  320. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
  321. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
  322. data/lib/hecks/runtime/command_interpreter.rb +195 -113
  323. data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
  324. data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
  325. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  326. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  327. data/lib/hecks/runtime/command_rules/references.rb +145 -55
  328. data/lib/hecks/runtime/command_rules.rb +3 -0
  329. data/lib/hecks/runtime/dependency_planning.rb +56 -11
  330. data/lib/hecks/runtime/dispatcher.rb +263 -92
  331. data/lib/hecks/runtime/entity_element.rb +304 -67
  332. data/lib/hecks/runtime/entity_interpreter.rb +149 -88
  333. data/lib/hecks/runtime/errors.rb +37 -23
  334. data/lib/hecks/runtime/event.rb +15 -10
  335. data/lib/hecks/runtime/identity.rb +91 -23
  336. data/lib/hecks/runtime/instance.rb +103 -18
  337. data/lib/hecks/runtime/interpreting.rb +24 -16
  338. data/lib/hecks/runtime/invocation.rb +358 -0
  339. data/lib/hecks/runtime/loader.rb +105 -19
  340. data/lib/hecks/runtime/outbox.rb +164 -26
  341. data/lib/hecks/runtime/policy_interpreter.rb +73 -60
  342. data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
  343. data/lib/hecks/runtime/query_interpreter.rb +93 -74
  344. data/lib/hecks/runtime/reaction_invocation.rb +73 -28
  345. data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
  346. data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
  347. data/lib/hecks/runtime/reference_hop.rb +48 -6
  348. data/lib/hecks/runtime/refusal_wording.rb +142 -115
  349. data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
  350. data/lib/hecks/runtime/registry/verification.rb +153 -28
  351. data/lib/hecks/runtime/registry.rb +202 -32
  352. data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
  353. data/lib/hecks/runtime/routing.rb +36 -154
  354. data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
  355. data/lib/hecks/runtime/saga_interpreter.rb +90 -76
  356. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  357. data/lib/hecks/runtime/tenant_check.rb +33 -13
  358. data/lib/hecks/runtime/tenant_scope.rb +23 -5
  359. data/lib/hecks/runtime/value/admission.rb +75 -30
  360. data/lib/hecks/runtime/value/coercion.rb +555 -142
  361. data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
  362. data/lib/hecks/runtime/value.rb +71 -21
  363. data/lib/hecks/runtime.rb +39 -7
  364. data/lib/hecks/storehouse.rb +368 -72
  365. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  366. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  367. data/lib/hecks/version.rb +3 -3
  368. data/lib/hecks/vocabulary.rb +205 -4
  369. data/lib/hecks.rb +91 -11
  370. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
  371. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
  372. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
  373. metadata +28 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b85a8382bb201f9ab54417b3535d8fe1ede57c842f627e32a2ff75d7f2dd6618
4
- data.tar.gz: e4852fd6b22145bec8a215e4c46fc6347527f7bd05811ff7da780e74dc24fe1d
3
+ metadata.gz: 830e92d9660dc86cc7de1af49d85fd7ce5ab611e07341dd11f4633274b920fda
4
+ data.tar.gz: 1c70b3c2994fd520a604ebd8c8cc8a53e7531e3cb5ba1dd59c6fd751b9b9dd99
5
5
  SHA512:
6
- metadata.gz: a538ddbe90fed2ccfa8c066f00777699b0eadade3c6523e6fbf5f62c19385ab7ace8e2c8de6fabafffce46abbefd36642401f6e5f23696127445378e95d98aad
7
- data.tar.gz: e22574943d4246998ba9121c0c84d7b39ec944416066ad881f144f166a09deb5c1a1fbb8b6c8378084b89856ed9a4e5bfd9c66a3cd6d07fa9a9a21eee77d63fc
6
+ metadata.gz: 3aeb0240903bed58fbd5684c23ee2069e9b3021aef07082d11e26ffa5052cebccf42d78e3aff243ba192183460056113e0510c7667306d1ed2cde3048ad118c0
7
+ data.tar.gz: e104c2252f0dbbb1ba5ea97a0f6f56f890cab76e12ff86f246605003738228b94062e317f16db1ebfb376753536d8309cedff86e00275781497232de67b17e1e
@@ -6,16 +6,16 @@ require_relative "../../ports/agent"
6
6
 
7
7
  module Hecks
8
8
  module Adapters
9
- # THE REAL `agent` FULFILLMENT — shells out to the `claude` CLI
9
+ # **The real `agent` fulfillment** — shells out to the `claude` CLI
10
10
  # itself, `claude -p --output-format json`, one process per call.
11
11
  # `Ports::Agent`'s own scripted double (`spec/fixtures/scripted_
12
12
  # agent.{adapter,rb}`) is the deterministic sibling every spec binds
13
13
  # instead, the same relationship `SecureRandomIdentity` already has
14
14
  # to `SequentialIdentity`.
15
15
  #
16
- # THIS FILE OWNS TRANSPORT ONLY — spawning the process, unwrapping
16
+ # **This file owns transport only** — spawning the process, unwrapping
17
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
18
+ # model's raw text, and parsing that text as JSON. It hands back a
19
19
  # plain Hash. Whether that Hash has the keys a caller asked for, and
20
20
  # whether its values are within the closed vocabularies this port
21
21
  # recognizes (a critique's `kind`, a proposal's `verb` pattern) is
@@ -40,10 +40,22 @@ module Hecks
40
40
 
41
41
  module_function
42
42
 
43
- # THE NEXT BEST QUESTION. `state` is whatever
43
+ # Asks the model for the next best interview question, as a raw parsed-JSON reply.
44
+ #
45
+ # **The next best question**. `state` is whatever
44
46
  # `Interview::Session#declaration`/`#gaps` produced — passed
45
47
  # through as JSON, not reformatted, so this adapter never
46
48
  # re-derives what the session already knows.
49
+ #
50
+ # @param state [Hash] the interview's whole current picture (declaration plus gaps),
51
+ # JSON-able
52
+ # @param asked [Array<Object>] JSON-able record of the questions already asked
53
+ # @return [Object] the parsed JSON reply, expected to be a Hash holding a `"questions"`
54
+ # array; shape validated downstream by `Ports::Agent::Answers.questions`
55
+ # @raise [Ports::Agent::Unavailable] if the `claude` binary is missing, the subprocess
56
+ # fails, or the call times out
57
+ # @raise [Ports::Agent::ValidationError] if the CLI's own envelope has no `"result"`, or
58
+ # the result text is not valid JSON
47
59
  def ask(state:, asked:)
48
60
  call(
49
61
  system: SYSTEM_PREFIX + "Given the domain model so far, ask the single best next " \
@@ -53,7 +65,18 @@ module Hecks
53
65
  )
54
66
  end
55
67
 
56
- # PROSE -> PROPOSED DECLARATIONS.
68
+ # Turns a human's sentence into proposed declarations, as a raw parsed-JSON reply.
69
+ #
70
+ # Prose -> proposed declarations.
71
+ #
72
+ # @param prose [String] a human's plain-English sentence
73
+ # @param state [Hash] the interview's whole current picture, JSON-able
74
+ # @return [Object] the parsed JSON reply, expected to be a Hash holding a `"proposals"`
75
+ # array; shape validated downstream by `Ports::Agent::Answers.proposals`
76
+ # @raise [Ports::Agent::Unavailable] if the `claude` binary is missing, the subprocess
77
+ # fails, or the call times out
78
+ # @raise [Ports::Agent::ValidationError] if the CLI's own envelope has no `"result"`, or
79
+ # the result text is not valid JSON
57
80
  def interpret(prose:, state:)
58
81
  call(
59
82
  system: SYSTEM_PREFIX + "Given the domain model so far and a sentence the human just said, " \
@@ -66,10 +89,22 @@ module Hecks
66
89
  )
67
90
  end
68
91
 
69
- # WHAT IS WRONG WITH THIS AS A MODEL — closed to the same kind
92
+ # Asks the model to judge a declared model on taste, as a raw parsed-JSON reply.
93
+ #
94
+ # What is wrong with this as a model — closed to the same kind
70
95
  # vocabulary `Ports::Agent::CRITIQUE_KINDS` declares, spelled out
71
96
  # here too since the system prompt is the only place the model
72
97
  # itself ever sees that list.
98
+ #
99
+ # @param declared [Hash] the chapter as declared so far, JSON-able
100
+ # @param refusals [Array<Object>] JSON-able refusals the language itself already raised
101
+ # @param findings [Array<Object>] JSON-able mechanical findings already found
102
+ # @return [Object] the parsed JSON reply, expected to be a Hash holding a `"findings"`
103
+ # array; shape validated downstream by `Ports::Agent::Answers.findings`
104
+ # @raise [Ports::Agent::Unavailable] if the `claude` binary is missing, the subprocess
105
+ # fails, or the call times out
106
+ # @raise [Ports::Agent::ValidationError] if the CLI's own envelope has no `"result"`, or
107
+ # the result text is not valid JSON
73
108
  def critique(declared:, refusals:, findings:)
74
109
  kinds = Ports::Agent::CRITIQUE_KINDS.join(", ")
75
110
  call(
@@ -82,9 +117,22 @@ module Hecks
82
117
  )
83
118
  end
84
119
 
85
- # VOCABULARY HELP. Named `suggest_name`, not `name` — see
120
+ # Suggests a name for a construct, as a raw parsed-JSON reply.
121
+ #
122
+ # **Vocabulary help**. Named `suggest_name`, not `name` — see
86
123
  # `Ports::Agent#suggest_name`'s own comment for why `name` is
87
124
  # never a safe module-function name here.
125
+ #
126
+ # @param meaning [String] what the new name needs to mean
127
+ # @param kind [String] the kind of construct being named, such as `"event"`
128
+ # @param near [Array<String>] names already in use nearby, which a suggestion must not
129
+ # collide with
130
+ # @return [Object] the parsed JSON reply, expected to be a Hash holding a `"names"`
131
+ # array; shape validated downstream by `Ports::Agent::Answers.suggestions`
132
+ # @raise [Ports::Agent::Unavailable] if the `claude` binary is missing, the subprocess
133
+ # fails, or the call times out
134
+ # @raise [Ports::Agent::ValidationError] if the CLI's own envelope has no `"result"`, or
135
+ # the result text is not valid JSON
88
136
  def suggest_name(meaning:, kind:, near:)
89
137
  call(
90
138
  system: SYSTEM_PREFIX + "Suggest a name for a #{kind} meaning \"#{meaning}\", distinct from " \
@@ -96,6 +144,16 @@ module Hecks
96
144
 
97
145
  # ── transport ───────────────────────────────────────────────────
98
146
 
147
+ # Runs `claude` as a subprocess with `payload` on stdin and returns its parsed JSON reply.
148
+ #
149
+ # @param system [String] the system-prompt text appended via `--append-system-prompt`
150
+ # @param payload [Hash] the JSON-able payload written to the subprocess's stdin
151
+ # @return [Object] the parsed JSON value nested under the CLI envelope's `"result"` key
152
+ # (see `unwrap`)
153
+ # @raise [Ports::Agent::Unavailable] if the `claude` binary is missing, the subprocess
154
+ # exits non-zero, or the call does not finish within `TIMEOUT_SECONDS`
155
+ # @raise [Ports::Agent::ValidationError] if the envelope has no `"result"`, or the
156
+ # result text is not valid JSON (see `unwrap`)
99
157
  def call(system:, payload:)
100
158
  stdout, status = Timeout.timeout(TIMEOUT_SECONDS) do
101
159
  Open3.capture2(
@@ -113,6 +171,13 @@ module Hecks
113
171
  raise Ports::Agent::Unavailable, "claude is not on PATH: #{e.message}"
114
172
  end
115
173
 
174
+ # Unwraps the CLI's own JSON envelope down to the model's parsed reply.
175
+ #
176
+ # @param stdout [String] the raw stdout of the `claude` CLI invocation, the outer
177
+ # `{"result": "..."}` envelope
178
+ # @return [Object] the JSON value parsed out of the envelope's `"result"` string
179
+ # @raise [Ports::Agent::ValidationError] if the envelope has no `"result"` key, or
180
+ # `result` is not valid JSON
116
181
  def unwrap(stdout)
117
182
  envelope = JSON.parse(stdout)
118
183
  result = envelope["result"]
@@ -16,15 +16,15 @@ require_relative "../../runtime/instance"
16
16
  module Hecks
17
17
  module Adapters
18
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
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
21
  # and the column encode/decode) and SqlQueryBuilder's dialect hooks
22
22
  # are copied near-verbatim from sqlite.rb — the only real difference
23
23
  # is the transport (D1::Connection, an HTTP call per query, vs a
24
24
  # persistent local sqlite3 handle). See sqlite.rb's own header
25
25
  # comment: "this file supplies only SQLite's dialect" — true here too.
26
26
  class D1
27
- # THE TRANSPORT — mirrors just the slice of SQLite3::Database's own
27
+ # **The transport** — mirrors just the slice of SQLite3::Database's own
28
28
  # interface (execute/get_first_row/get_first_value, rows as
29
29
  # column-name-keyed hashes) that Sqlite::SchemaBuilder, Sqlite::Codec,
30
30
  # and this file's own methods already assume. One stateless HTTP call
@@ -33,20 +33,41 @@ module Hecks
33
33
  class Connection
34
34
  ENDPOINT = "https://api.cloudflare.com/client/v4".freeze
35
35
 
36
+ # @param account_id [String] the Cloudflare account that owns the database
37
+ # @param database_id [String] the D1 database's id
38
+ # @param api_token [String] a Cloudflare API token, sent as a bearer token on every
39
+ # request
36
40
  def initialize(account_id:, database_id:, api_token:)
37
41
  @uri = URI("#{ENDPOINT}/accounts/#{account_id}/d1/database/#{database_id}/query")
38
42
  @api_token = api_token
39
43
  end
40
44
 
45
+ # Runs one statement in its own HTTP request and returns its rows.
46
+ #
47
+ # @param sql [String] the statement, with `?` placeholders
48
+ # @param binds [Array<Object>] one value per placeholder, in order
49
+ # @return [Array<Hash{String => Object}>] the result rows keyed by column name; `[]`
50
+ # for a statement that returns none
51
+ # @raise [Runtime::WiringError] if D1 reports the query failed or answers with a body
52
+ # that is not JSON
41
53
  def execute(sql, binds = [])
42
54
  response_results({ sql: sql, params: binds }).first.fetch("results", [])
43
55
  end
44
56
 
57
+ # Runs several statements as one transaction in a single HTTP request.
58
+ #
45
59
  # D1 batches are SQL transactions: statements execute in order and a
46
60
  # failure rolls the entire sequence back. Keep the tuple-shaped local
47
61
  # seam small so adapter code and focused fakes do not need to know the
48
62
  # REST request envelope.
49
63
  # https://developers.cloudflare.com/d1/worker-api/d1-database/#batch
64
+ #
65
+ # @param statements [Array<Array(String, Array)>] `[sql, binds]` pairs in execution
66
+ # order; nil binds mean none
67
+ # @return [Array<Array<Hash{String => Object}>>] each statement's result rows, in
68
+ # the order given
69
+ # @raise [Runtime::WiringError] if D1 reports the request or any one statement failed,
70
+ # or answers with a body that is not JSON
50
71
  def batch(statements)
51
72
  payload = {
52
73
  batch: statements.map do |sql, binds|
@@ -83,10 +104,10 @@ module Hecks
83
104
  raise Runtime::WiringError, "D1 query failed: non-JSON response (HTTP #{response.code}): #{response.body}"
84
105
  end
85
106
 
86
- # `messages` is the whole-response :errors fallback, tried LAST —
107
+ # `messages` is the whole-response :errors fallback, tried last —
87
108
  # a per-statement `failed["error"]`/`failed["message"]` (checked
88
109
  # by key presence, not truthiness, so an explicit `nil` still
89
- # counts as "the key was there") is always more specific to WHICH
110
+ # counts as "the key was there") is always more specific to which
90
111
  # statement failed, when either is present.
91
112
  def failed_statement_detail(failed, messages)
92
113
  detail =
@@ -102,10 +123,26 @@ module Hecks
102
123
 
103
124
  public
104
125
 
126
+ # Runs one statement and keeps only its first row, as `SQLite3::Database` does.
127
+ #
128
+ # @param sql [String] the statement, with `?` placeholders
129
+ # @param binds [Array<Object>] one value per placeholder, in order
130
+ # @return [Hash{String => Object}, nil] the first row keyed by column name, or nil when
131
+ # the statement returned none
132
+ # @raise [Runtime::WiringError] if D1 reports the query failed or answers with a body
133
+ # that is not JSON
105
134
  def get_first_row(sql, binds = [])
106
135
  execute(sql, binds).first
107
136
  end
108
137
 
138
+ # Runs one statement and keeps only the first column of its first row.
139
+ #
140
+ # @param sql [String] the statement, with `?` placeholders
141
+ # @param binds [Array<Object>] one value per placeholder, in order
142
+ # @return [Object, nil] the value as D1's JSON carries it, or nil when the statement
143
+ # returned no row
144
+ # @raise [Runtime::WiringError] if D1 reports the query failed or answers with a body
145
+ # that is not JSON
109
146
  def get_first_value(sql, binds = [])
110
147
  get_first_row(sql, binds)&.values&.first
111
148
  end
@@ -119,8 +156,22 @@ module Hecks
119
156
 
120
157
  attr_reader :aggregate
121
158
 
159
+ # Names the optional persistence capabilities `Ports::Persistence::AppendOnly` may rely on.
160
+ #
161
+ # @return [Array<Symbol>] `[:atomic_put]`
122
162
  def persistence_capabilities = [:atomic_put]
123
163
 
164
+ # Checks the credentials are declared and creates the aggregate, journal, event and
165
+ # saga tables if absent, one HTTP request per statement.
166
+ #
167
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose table this adapter owns
168
+ # @param settings [Hash{Symbol, String => Object}] world settings for the binding:
169
+ # `account_id`, `database_id` and `api_token` (all required) and `domain` (scopes saga
170
+ # rows, default the aggregate's name), each read under a Symbol or a String key
171
+ # @param root [String, nil] project root directory; accepted for the shared adapter
172
+ # constructor shape and ignored
173
+ # @raise [Runtime::WiringError] if a required setting is missing or empty, or D1 rejects
174
+ # a table-creation statement
124
175
  def initialize(aggregate:, settings: {}, root: nil)
125
176
  @aggregate = aggregate
126
177
 
@@ -132,7 +183,7 @@ module Hecks
132
183
  end
133
184
 
134
185
  @db = Connection.new(account_id: account_id, database_id: database_id, api_token: api_token)
135
- # THE OPTIONAL saga-persistence capability's own scoping column
186
+ # The optional saga-persistence capability's own scoping column
136
187
  # (§2/§4) — D1's domain isolation is by whole-database identity
137
188
  # (one D1 database per domain in practice), so unlike Sqlite's
138
189
  # own per-aggregate-file default, there's no "which file does
@@ -159,8 +210,16 @@ module Hecks
159
210
  create_saga_table!
160
211
  end
161
212
 
213
+ # Names the aggregate's table; the journal table is keyed off it.
214
+ #
215
+ # @return [String] the aggregate's snake_case storage name, unquoted
162
216
  def table = @aggregate.storage_name
163
217
 
218
+ # Reads the current row for one aggregate identity.
219
+ #
220
+ # @param id [String, Object] the aggregate identity, bound as `id.to_s`
221
+ # @return [Runtime::Instance, nil] the decoded record, or nil when no row has that id
222
+ # @raise [Runtime::WiringError] if D1 rejects the statement
164
223
  def find(id)
165
224
  row = @db.get_first_row("SELECT * FROM #{quoted_table} WHERE id = ?", [id.to_s])
166
225
  return nil unless row
@@ -168,9 +227,19 @@ module Hecks
168
227
  Runtime::Instance.new(aggregate: @aggregate, id: row["id"], state: decode(row))
169
228
  end
170
229
 
171
- # order_by IS A RUNTIME VALUE — see postgres.rb's own all for the
230
+ # Lists every stored record, ordered by id unless an ordering attribute is given.
231
+ #
232
+ # order_by is a runtime value — see postgres.rb's own all for the
172
233
  # full reasoning; whitelisted the identical way, same order_clause
173
234
  # Sqlite's own all reuses (D1 speaks the identical dialect).
235
+ #
236
+ # @param order_by [String, Symbol, nil] attribute (or dotted value-object path) to sort
237
+ # by, with id as the tie-break; nil orders by id alone
238
+ # @param direction [Symbol, String] `:asc` or `:desc`, case-insensitive; anything else
239
+ # sorts ascending
240
+ # @return [Array<Runtime::Instance>] the decoded records, `[]` when the table is empty
241
+ # @raise [Runtime::WiringError] if `order_by` names no attribute of the aggregate, or D1
242
+ # rejects the statement
174
243
  def all(order_by: nil, direction: :asc)
175
244
  order_sql = "ORDER BY id"
176
245
  if order_by
@@ -189,21 +258,37 @@ module Hecks
189
258
  end
190
259
  end
191
260
 
261
+ # Counts the rows in the aggregate's table, deleted records excluded.
262
+ #
263
+ # @return [Integer] number of current records
264
+ # @raise [Runtime::WiringError] if D1 rejects the statement
192
265
  def count = @db.get_first_value("SELECT COUNT(*) FROM #{quoted_table}").to_i
193
266
 
267
+ # Inserts one journal row in its own HTTP request.
268
+ #
269
+ # @param entry [Ports::Persistence::Entry] the save or delete to journal; `state` is
270
+ # encoded through the state codec and `mirrors` stored as JSON, or NULL when nil
271
+ # @return [Ports::Persistence::Entry] the same `entry`
272
+ # @raise [Runtime::WiringError] if D1 rejects the insert
194
273
  def append(entry)
195
274
  @db.execute(
196
275
  "INSERT INTO #{quoted_entry_table} (aggregate_id, operation, state, mirrors) VALUES (?, ?, ?, ?)",
197
- # `mirrors` (unlike `state`) is a NULLABLE column — an absent
276
+ # `mirrors` (unlike `state`) is a nullable column — an absent
198
277
  # mirrors hash must bind a real SQL NULL, not the four-character
199
278
  # JSON text `"null"` (`JSON.generate(nil)`), or a future `IS NULL`
200
279
  # check against it would never match. Same guard `postgres_era.rb`
201
280
  # already uses for its own journal's `mirrors` column.
202
- [entry.id, entry.operation, JSON.generate(entry.state), entry.mirrors && JSON.generate(entry.mirrors)]
281
+ [entry.id, entry.operation, state_json(entry.state), entry.mirrors && JSON.generate(entry.mirrors)]
203
282
  )
204
283
  entry
205
284
  end
206
285
 
286
+ # Replaces or deletes the aggregate's row for one journal entry.
287
+ #
288
+ # @param entry [Ports::Persistence::Entry] the save or delete to materialize
289
+ # @return [Runtime::Instance, Array] for a save, a new instance over the entry's state;
290
+ # for a delete, the `DELETE` statement's empty result rows
291
+ # @raise [Runtime::WiringError] if D1 rejects the statement
207
292
  def project(entry)
208
293
  return @db.execute("DELETE FROM #{quoted_table} WHERE id = ?", [entry.id]) if entry.delete?
209
294
 
@@ -219,59 +304,89 @@ module Hecks
219
304
  instance
220
305
  end
221
306
 
307
+ # Reads the whole journal back in append order, for `AppendOnly#recover!` to replay.
308
+ #
309
+ # @return [Array<Ports::Persistence::Entry>] every journalled entry, state decoded
310
+ # through the state codec and `mirrors` parsed with String keys (nil when none were
311
+ # stored); a NULL `operation` reads as `"save"`; `[]` when nothing has been appended
312
+ # @raise [Runtime::WiringError] if D1 rejects the statement
313
+ # @raise [JSON::ParserError] if a stored `state` or `mirrors` value is not valid JSON
222
314
  def entries
223
315
  @db.execute("SELECT aggregate_id, operation, state, mirrors FROM #{quoted_entry_table} ORDER BY sequence").map do |row|
224
316
  state = JSON.parse(row["state"])
225
317
  Ports::Persistence::Entry.new(
226
318
  operation: row["operation"] || "save",
227
319
  id: row["aggregate_id"],
228
- state: state&.transform_keys(&:to_sym),
320
+ state: Ports::Persistence::StateCodec.decode(@aggregate, state),
229
321
  mirrors: row["mirrors"] && JSON.parse(row["mirrors"])
230
322
  )
231
323
  end
232
324
  end
233
325
 
326
+ # Deletes every row of the aggregate's table and its journal; events and saga rows
327
+ # are left in place.
328
+ #
329
+ # @return [Adapters::D1] self
330
+ # @raise [Runtime::WiringError] if D1 rejects a statement
234
331
  def reset!
235
332
  @db.execute("DELETE FROM #{quoted_table}")
236
333
  @db.execute("DELETE FROM #{quoted_entry_table}")
237
334
  self
238
335
  end
239
336
 
337
+ # Journals and then replaces an instance's current state, as two separate HTTP
338
+ # requests rather than one batch.
339
+ #
340
+ # @param instance [Runtime::Instance] the instance to store
341
+ # @return [Runtime::Instance] a new instance over a shallow copy of the saved state
342
+ # @raise [Runtime::WiringError] if D1 rejects either statement; a journal row already
343
+ # inserted stays
240
344
  def save(instance)
241
345
  entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
242
346
  append(entry)
243
347
  project(entry)
244
348
  end
245
349
 
350
+ # Stores an entry and reports whether it inserted, replaced or conflicted, all in one
351
+ # batched HTTP request.
352
+ #
246
353
  # Classification, durable journal append and current-state projection
247
354
  # are one D1 batch transaction and therefore one HTTP request. The first
248
355
  # statement supplies the outcome from database state inside that same
249
356
  # transaction; the runtime performs no preliminary find.
250
357
  #
251
- # `insert_only:` used to spend a SEPARATE, EARLIER round trip finding
252
- # out whether the row existed before ever building the batch — a real
358
+ # Under `insert_only:`, spending a separate, earlier round trip finding
359
+ # out whether the row exists before building the batch would be a real
253
360
  # TOCTOU gap (two concurrent creates at the same identity could both
254
361
  # pass that check before either wrote). D1's batch has no conditional
255
- # BRANCH of its own, true, but it does not need one: a batch's own
362
+ # branch of its own, true, but it does not need one: a batch's own
256
363
  # statements already execute in order, atomically, as one transaction
257
364
  # (Connection#batch's own comment) — the exact guarantee the single-
258
365
  # connection adapters' own `@db.transaction do ... end` gets locally.
259
- # So the existence check moves INSIDE the batch as its own first
366
+ # So the existence check sits inside the batch as its own first
260
367
  # statement, and the two writes are individually gated with `WHERE NOT
261
368
  # EXISTS (...)` against that same table, evaluated in the same
262
369
  # transaction — a row that already existed makes both writes into
263
370
  # real, zero-row no-ops rather than skipping them from the Ruby side,
264
- # matching Sqlite#atomic_put's `next` (skip append AND project both,
371
+ # matching Sqlite#atomic_put's `next` (skip append and project both,
265
372
  # together) with no second HTTP call and no gap for another writer to
266
373
  # land in between the check and the write.
267
- # Three SQL statements, built here and batched together as ONE
268
- # transaction below — see the comment above on the real TOCTOU gap
269
- # this exact shape closes (the existence check moved INSIDE the
374
+ #
375
+ # Three SQL statements, built here and batched together as one
376
+ # transaction below — see the paragraph above on the real TOCTOU gap
377
+ # this exact shape closes (the existence check inside the
270
378
  # batch, not run as a separate earlier round trip). Splitting the
271
379
  # per-statement builders out would still need columns/values/slots/
272
380
  # not_exists/quoted_table threaded into each, and would separate
273
- # three pieces of ONE atomic batch across methods with no single
381
+ # three pieces of one atomic batch across methods with no single
274
382
  # place left to see that they are, together, the fix.
383
+ #
384
+ # @param entry [Ports::Persistence::Entry] the save to store
385
+ # @param insert_only [Boolean] when true, an existing row turns both writes into
386
+ # zero-row no-ops
387
+ # @return [Symbol] `:inserted`, `:replaced`, or `:conflicted` when `insert_only` met an
388
+ # existing row and nothing was written
389
+ # @raise [Runtime::WiringError] if D1 rejects the batch; the whole batch is rolled back
275
390
  # rubocop:disable-next Metrics/AbcSize
276
391
  # rubocop:disable-next Metrics/MethodLength
277
392
  def atomic_put(entry, insert_only: false)
@@ -290,7 +405,7 @@ module Hecks
290
405
  "THEN 'replaced' ELSE 'inserted' END AS status"
291
406
  end
292
407
 
293
- # `mirrors` is NULLABLE (unlike `state`) — see `append`'s own comment.
408
+ # `mirrors` is nullable (unlike `state`) — see `append`'s own comment.
294
409
  encoded_mirrors = entry.mirrors && JSON.generate(entry.mirrors)
295
410
 
296
411
  entry_sql, entry_binds =
@@ -298,12 +413,12 @@ module Hecks
298
413
  [
299
414
  "INSERT INTO #{quoted_entry_table} (aggregate_id, operation, state, mirrors) " \
300
415
  "SELECT ?, ?, ?, ? #{not_exists}",
301
- [entry.id, entry.operation, JSON.generate(entry.state), encoded_mirrors, entry.id.to_s]
416
+ [entry.id, entry.operation, state_json(entry.state), encoded_mirrors, entry.id.to_s]
302
417
  ]
303
418
  else
304
419
  [
305
420
  "INSERT INTO #{quoted_entry_table} (aggregate_id, operation, state, mirrors) VALUES (?, ?, ?, ?)",
306
- [entry.id, entry.operation, JSON.generate(entry.state), encoded_mirrors]
421
+ [entry.id, entry.operation, state_json(entry.state), encoded_mirrors]
307
422
  ]
308
423
  end
309
424
 
@@ -329,6 +444,12 @@ module Hecks
329
444
  results.fetch(0).fetch(0).fetch("status").to_sym
330
445
  end
331
446
 
447
+ # Journals a delete and then removes the row, whether or not a row exists, as two
448
+ # separate HTTP requests.
449
+ #
450
+ # @param id [String, Object] the aggregate identity, journalled as `id.to_s`
451
+ # @return [Boolean] always true
452
+ # @raise [Runtime::WiringError] if D1 rejects either statement
332
453
  def delete(id)
333
454
  entry = Ports::Persistence::Entry.new(operation: "delete", id: id.to_s, state: nil)
334
455
  append(entry)
@@ -336,6 +457,11 @@ module Hecks
336
457
  true
337
458
  end
338
459
 
460
+ # Inserts an emitted event into the database's shared `events` table.
461
+ #
462
+ # @param event [Runtime::Event] the emitted event; `payload` is stored as JSON
463
+ # @return [Array] the insert's empty result rows; callers ignore it
464
+ # @raise [Runtime::WiringError] if D1 rejects the insert
339
465
  def record_event(event)
340
466
  @db.execute(
341
467
  "INSERT INTO events (name, aggregate, aggregate_id, payload, occurred_at) VALUES (?, ?, ?, ?, ?)",
@@ -343,6 +469,12 @@ module Hecks
343
469
  )
344
470
  end
345
471
 
472
+ # Reads back every recorded event in insertion order — the database's whole `events`
473
+ # table, not only this aggregate's rows.
474
+ #
475
+ # @return [Array<Runtime::Event>] the stored events, `payload` parsed with Symbol keys
476
+ # and `occurred_at` as stored; `[]` when none are recorded
477
+ # @raise [Runtime::WiringError] if D1 rejects the statement
346
478
  def events
347
479
  @db.execute("SELECT * FROM events ORDER BY id").map do |row|
348
480
  Runtime::Event.new(
@@ -355,10 +487,23 @@ module Hecks
355
487
  end
356
488
  end
357
489
 
358
- # ── the OPTIONAL saga-persistence capability (§2) — reuses the DDL
490
+ # Replaces one saga instance's checkpoint, keyed by domain, process manager and
491
+ # correlation.
492
+ #
493
+ # ── the optional saga-persistence capability (§2) — reuses the DDL
359
494
  # `Sqlite::SchemaBuilder` already shares with Sqlite (`d1.rb`'s own
360
495
  # file header). Same `?`-placeholder shape every other write here
361
496
  # already uses through `Connection#execute`.
497
+ #
498
+ # @param process_manager [String, Symbol] the process manager's name
499
+ # @param correlation [String, Object] the instance's correlation value, stored as
500
+ # `correlation.to_s`
501
+ # @param state [String, Symbol] the saga's current state name
502
+ # @param memory [Hash] the saga's memory; must be JSON-serializable
503
+ # @param completed_compensations [Array] the ledger of completed compensable legs; must
504
+ # be JSON-serializable
505
+ # @return [Array] the statement's empty result rows; callers ignore it
506
+ # @raise [Runtime::WiringError] if D1 rejects the statement
362
507
  def save_saga(process_manager:, correlation:, state:, memory:, completed_compensations: [])
363
508
  @db.execute(
364
509
  "INSERT OR REPLACE INTO hecks_saga_instances (domain, process_manager, correlation, state, memory, " \
@@ -368,6 +513,13 @@ module Hecks
368
513
  )
369
514
  end
370
515
 
516
+ # Removes a finished saga instance's checkpoint; a missing row is not an error.
517
+ #
518
+ # @param process_manager [String, Symbol] the process manager's name
519
+ # @param correlation [String, Object] the instance's correlation value, matched as
520
+ # `correlation.to_s`
521
+ # @return [Array] the statement's empty result rows; callers ignore it
522
+ # @raise [Runtime::WiringError] if D1 rejects the statement
371
523
  def delete_saga(process_manager:, correlation:)
372
524
  @db.execute(
373
525
  "DELETE FROM hecks_saga_instances WHERE domain = ? AND process_manager = ? AND correlation = ?",
@@ -375,6 +527,18 @@ module Hecks
375
527
  )
376
528
  end
377
529
 
530
+ # Yields every checkpointed saga instance of this adapter's domain, for
531
+ # `Registry#rehydrate_sagas!` to restore at boot.
532
+ #
533
+ # @yieldparam process_manager [String] the process manager's name
534
+ # @yieldparam correlation [String] the instance's correlation value
535
+ # @yieldparam state [String] the saga's state name
536
+ # @yieldparam memory [Hash{Symbol => Object}] the saga's memory, Symbol keys at every depth
537
+ # @yieldparam completed_compensations [Array] the completed-compensation ledger, `[]`
538
+ # when the column is NULL
539
+ # @return [Enumerator, Array<Hash>] an enumerator over the same five values when no
540
+ # block is given; otherwise the raw result rows
541
+ # @raise [Runtime::WiringError] if D1 rejects the statement
378
542
  def each_saga
379
543
  return enum_for(:each_saga) unless block_given?
380
544