hecks 1.2.0 → 1.4.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 (358) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +7 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +10 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
  7. data/lib/hecks/adapters/driven/heki/journal.rb +60 -2
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
  9. data/lib/hecks/adapters/driven/heki.rb +13 -7
  10. data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
  12. data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
  13. data/lib/hecks/adapters/driven/lambda.rb +39 -33
  14. data/lib/hecks/adapters/driven/local_storage.rb +17 -10
  15. data/lib/hecks/adapters/driven/memory.rb +205 -9
  16. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
  17. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  18. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  19. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  20. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  21. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  22. data/lib/hecks/adapters/driven/postgres_era.adapter +5 -0
  23. data/lib/hecks/adapters/driven/prism.rb +4 -4
  24. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  25. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  26. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  27. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  28. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  29. data/lib/hecks/adapters/driven.rb +4 -4
  30. data/lib/hecks/adapters/driving/github_webhook.rb +145 -0
  31. data/lib/hecks/behaviors/dsl.rb +2 -2
  32. data/lib/hecks/behaviors/expectations.rb +51 -23
  33. data/lib/hecks/behaviors/ir.rb +1 -1
  34. data/lib/hecks/behaviors/rspec.rb +1 -1
  35. data/lib/hecks/behaviors/runner.rb +2 -2
  36. data/lib/hecks/behaviors.rb +1 -1
  37. data/lib/hecks/bluebook/aggregate.rb +13 -13
  38. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
  39. data/lib/hecks/bluebook/assembly/build.rb +1 -1
  40. data/lib/hecks/bluebook/assembly/contract.rb +39 -16
  41. data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
  42. data/lib/hecks/bluebook/assembly/marks.rb +18 -18
  43. data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
  44. data/lib/hecks/bluebook/assembly.rb +9 -9
  45. data/lib/hecks/bluebook/attribute.rb +9 -9
  46. data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
  47. data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
  48. data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
  49. data/lib/hecks/bluebook/behaviour/command.rb +23 -23
  50. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -3
  51. data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
  52. data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
  53. data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
  54. data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
  55. data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
  56. data/lib/hecks/bluebook/behaviour/query.rb +1 -1
  57. data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
  58. data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
  59. data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
  60. data/lib/hecks/bluebook/capabilities.rb +27 -0
  61. data/lib/hecks/bluebook/chapter.rb +28 -9
  62. data/lib/hecks/bluebook/command.rb +12 -12
  63. data/lib/hecks/bluebook/domain_port.rb +9 -9
  64. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  65. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  66. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
  67. data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
  68. data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
  69. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
  70. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
  71. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  72. data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
  73. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  74. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  75. data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
  76. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  77. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
  78. data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
  79. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  80. data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
  81. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  82. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  83. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
  84. data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
  85. data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
  86. data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
  87. data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
  88. data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
  89. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  90. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  91. data/lib/hecks/bluebook/entity.rb +11 -11
  92. data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
  93. data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
  94. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
  95. data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
  96. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
  97. data/lib/hecks/bluebook/expression/resolver.rb +60 -62
  98. data/lib/hecks/bluebook/hexagon.rb +1 -1
  99. data/lib/hecks/bluebook/lifecycle.rb +1 -1
  100. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
  101. data/lib/hecks/bluebook/meta_validator/judge.rb +122 -86
  102. data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
  103. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
  104. data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
  105. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
  106. data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
  107. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
  108. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
  109. data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
  110. data/lib/hecks/bluebook/meta_validator.rb +70 -70
  111. data/lib/hecks/bluebook/model_check.rb +301 -84
  112. data/lib/hecks/bluebook/pattern_subset.rb +9 -9
  113. data/lib/hecks/bluebook/policy.rb +15 -13
  114. data/lib/hecks/bluebook/process_manager.rb +14 -14
  115. data/lib/hecks/bluebook/project_register.rb +6 -6
  116. data/lib/hecks/bluebook/query.rb +4 -4
  117. data/lib/hecks/bluebook/read_model.rb +14 -14
  118. data/lib/hecks/bluebook/reference.rb +8 -8
  119. data/lib/hecks/bluebook/smoke_test.rb +19 -19
  120. data/lib/hecks/bluebook/synthesizer.rb +12 -12
  121. data/lib/hecks/bluebook/translation.rb +4 -4
  122. data/lib/hecks/bluebook/value_object.rb +6 -6
  123. data/lib/hecks/bluebook.rb +2 -2
  124. data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
  125. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
  126. data/lib/hecks/codemod.rb +36 -35
  127. data/lib/hecks/construct.rb +6 -6
  128. data/lib/hecks/corpus.rb +317 -0
  129. data/lib/hecks/deprecation.rb +95 -0
  130. data/lib/hecks/doc/reference.rb +19 -19
  131. data/lib/hecks/embryonaut_bluebook.rb +11 -11
  132. data/lib/hecks/facade/cli_door.rb +69 -10
  133. data/lib/hecks/facade/cli_runner.rb +105 -24
  134. data/lib/hecks/facade/command_request.rb +23 -0
  135. data/lib/hecks/facade/handle.rb +79 -32
  136. data/lib/hecks/facade/json_door.rb +106 -25
  137. data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
  138. data/lib/hecks/facade/surface/chapter.rb +26 -17
  139. data/lib/hecks/facade/surface.rb +16 -3
  140. data/lib/hecks/facade.rb +15 -4
  141. data/lib/hecks/forms/app.rb +46 -30
  142. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  143. data/lib/hecks/forms/field_renderer.rb +142 -6
  144. data/lib/hecks/forms/field_shape.rb +183 -20
  145. data/lib/hecks/forms/html.rb +51 -7
  146. data/lib/hecks/forms/index_renderer.rb +14 -2
  147. data/lib/hecks/forms/params.rb +120 -23
  148. data/lib/hecks/forms/port_argument.rb +2 -2
  149. data/lib/hecks/forms/query_form_renderer.rb +2 -2
  150. data/lib/hecks/forms/record_renderer.rb +2 -2
  151. data/lib/hecks/forms/record_table.rb +1 -1
  152. data/lib/hecks/forms/value_object_shape.rb +3 -3
  153. data/lib/hecks/forms.rb +24 -4
  154. data/lib/hecks/fqn.rb +1 -1
  155. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  156. data/lib/hecks/framework.rb +48 -17
  157. data/lib/hecks/freezer.rb +11 -11
  158. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
  159. data/lib/hecks/fuzzing/combination_miner.rb +119 -0
  160. data/lib/hecks/fuzzing/concurrent_dispatch.rb +361 -0
  161. data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
  162. data/lib/hecks/fuzzing/differential.rb +158 -0
  163. data/lib/hecks/fuzzing/domain_generator.rb +694 -0
  164. data/lib/hecks/fuzzing/era_boundary.rb +124 -0
  165. data/lib/hecks/fuzzing/form_census.rb +199 -0
  166. data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
  167. data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
  168. data/lib/hecks/fuzzing/isolated_boot.rb +226 -38
  169. data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
  170. data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
  171. data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
  172. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
  173. data/lib/hecks/fuzzing/properties/guards.rb +129 -26
  174. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
  175. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
  176. data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
  177. data/lib/hecks/fuzzing/properties/querying.rb +17 -17
  178. data/lib/hecks/fuzzing/properties.rb +84 -39
  179. data/lib/hecks/fuzzing/qa_settings.rb +152 -0
  180. data/lib/hecks/fuzzing/replay.rb +224 -114
  181. data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
  182. data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
  183. data/lib/hecks/fuzzing/self_consistency.rb +676 -0
  184. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
  189. data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
  190. data/lib/hecks/fuzzing/shrinker.rb +197 -0
  191. data/lib/hecks/fuzzing/structural_skips.rb +39 -0
  192. data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
  193. data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
  194. data/lib/hecks/fuzzing/value_generator.rb +66 -14
  195. data/lib/hecks/fuzzing.rb +11 -0
  196. data/lib/hecks/grammar/evolve.rb +10 -10
  197. data/lib/hecks/grammar.rb +7 -7
  198. data/lib/hecks/ir.rb +13 -13
  199. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  200. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  201. data/lib/hecks/language/bluebook/vocabulary.bluebook +365 -15
  202. data/lib/hecks/language/oidc.json +5 -0
  203. data/lib/hecks/literal.rb +9 -9
  204. data/lib/hecks/naming.rb +89 -21
  205. data/lib/hecks/ports/access_control.rb +58 -2
  206. data/lib/hecks/ports/agent/answers.rb +83 -6
  207. data/lib/hecks/ports/agent.rb +119 -35
  208. data/lib/hecks/ports/authentication.rb +44 -4
  209. data/lib/hecks/ports/authorization.rb +53 -11
  210. data/lib/hecks/ports/clock.rb +42 -23
  211. data/lib/hecks/ports/extraction.rb +16 -0
  212. data/lib/hecks/ports/identity_assignment.rb +24 -2
  213. data/lib/hecks/ports/identity_generation.rb +17 -3
  214. data/lib/hecks/ports/identity_resolution.rb +18 -1
  215. data/lib/hecks/ports/loading.rb +4 -0
  216. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  217. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  218. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  219. data/lib/hecks/ports/persistence/execution.rb +4 -0
  220. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  221. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  222. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  223. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  224. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  225. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  226. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  227. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  228. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  229. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  230. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  231. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +160 -58
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +44 -6
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +338 -83
  242. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  243. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  244. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  245. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
  246. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  247. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  248. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  249. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
  250. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
  251. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
  252. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  253. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  254. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  255. data/lib/hecks/ports/persistence/repository_factory.rb +29 -6
  256. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  257. data/lib/hecks/ports/persistence.rb +36 -1
  258. data/lib/hecks/ports/projection.rb +61 -7
  259. data/lib/hecks/ports/query/in_memory.rb +3 -3
  260. data/lib/hecks/ports/query/ordering.rb +6 -6
  261. data/lib/hecks/ports/query.rb +35 -0
  262. data/lib/hecks/projections/bootstrap_table.rb +112 -0
  263. data/lib/hecks/projections/diagrams.rb +75 -75
  264. data/lib/hecks/projections/glossary/html.rb +250 -0
  265. data/lib/hecks/projections/glossary/markdown.rb +105 -0
  266. data/lib/hecks/projections/glossary/mermaid.rb +110 -0
  267. data/lib/hecks/projections/glossary/page.css +271 -0
  268. data/lib/hecks/projections/glossary/page.js +72 -0
  269. data/lib/hecks/projections/glossary/sections.rb +17 -0
  270. data/lib/hecks/projections/glossary/sentences.rb +205 -0
  271. data/lib/hecks/projections/glossary.rb +214 -286
  272. data/lib/hecks/projections/ir.rb +1 -1
  273. data/lib/hecks/projections/model/deviations.rb +18 -17
  274. data/lib/hecks/projections/model.rb +25 -21
  275. data/lib/hecks/projections/oidc.rb +7 -7
  276. data/lib/hecks/projections/parser_table.rb +5 -5
  277. data/lib/hecks/projections/reference.rb +3 -3
  278. data/lib/hecks/projections/rust_vocabulary.rb +443 -0
  279. data/lib/hecks/projections/shape.rb +2 -2
  280. data/lib/hecks/projections/statements.rb +11 -11
  281. data/lib/hecks/projections/vocabulary.rb +9 -9
  282. data/lib/hecks/projections.rb +5 -3
  283. data/lib/hecks/projector/cli_projector.rb +29 -29
  284. data/lib/hecks/projector/docs_projector.rb +13 -13
  285. data/lib/hecks/projector/exporter.rb +42 -21
  286. data/lib/hecks/projector/ir_projector.rb +1 -1
  287. data/lib/hecks/projector/narrate_projector.rb +15 -22
  288. data/lib/hecks/projector/target.rb +13 -13
  289. data/lib/hecks/projector.rb +15 -15
  290. data/lib/hecks/query_ir.rb +47 -47
  291. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  292. data/lib/hecks/query_specification/common/comparison.rb +132 -24
  293. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  294. data/lib/hecks/query_specification/common/null_policy.rb +57 -13
  295. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  296. data/lib/hecks/query_specification/common/options.rb +25 -0
  297. data/lib/hecks/query_specification/field_path.rb +69 -15
  298. data/lib/hecks/query_specification/hop_path.rb +57 -20
  299. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  300. data/lib/hecks/rendering.rb +3 -3
  301. data/lib/hecks/router/namespace_installer.rb +3 -3
  302. data/lib/hecks/router.rb +1 -1
  303. data/lib/hecks/runtime/aggregate_lock.rb +11 -11
  304. data/lib/hecks/runtime/boot_gates.rb +3 -3
  305. data/lib/hecks/runtime/caller.rb +8 -8
  306. data/lib/hecks/runtime/capability_graph.rb +2 -2
  307. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
  308. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +64 -67
  309. data/lib/hecks/runtime/command_interpreter.rb +159 -102
  310. data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
  311. data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
  312. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  313. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  314. data/lib/hecks/runtime/command_rules/references.rb +94 -19
  315. data/lib/hecks/runtime/command_rules.rb +3 -0
  316. data/lib/hecks/runtime/dependency_planning.rb +11 -11
  317. data/lib/hecks/runtime/dispatcher.rb +283 -90
  318. data/lib/hecks/runtime/entity_element.rb +282 -46
  319. data/lib/hecks/runtime/entity_interpreter.rb +160 -69
  320. data/lib/hecks/runtime/errors.rb +19 -19
  321. data/lib/hecks/runtime/event.rb +6 -6
  322. data/lib/hecks/runtime/identity.rb +22 -22
  323. data/lib/hecks/runtime/instance.rb +39 -14
  324. data/lib/hecks/runtime/interpreting.rb +12 -12
  325. data/lib/hecks/runtime/invocation.rb +276 -0
  326. data/lib/hecks/runtime/loader.rb +14 -14
  327. data/lib/hecks/runtime/outbox.rb +23 -23
  328. data/lib/hecks/runtime/policy_interpreter.rb +54 -54
  329. data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
  330. data/lib/hecks/runtime/query_interpreter.rb +111 -56
  331. data/lib/hecks/runtime/reaction_invocation.rb +76 -9
  332. data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
  333. data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
  334. data/lib/hecks/runtime/reference_hop.rb +6 -6
  335. data/lib/hecks/runtime/refusal_wording.rb +92 -112
  336. data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
  337. data/lib/hecks/runtime/registry/verification.rb +36 -26
  338. data/lib/hecks/runtime/registry.rb +56 -27
  339. data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
  340. data/lib/hecks/runtime/routing.rb +10 -88
  341. data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
  342. data/lib/hecks/runtime/saga_interpreter.rb +93 -56
  343. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  344. data/lib/hecks/runtime/tenant_check.rb +9 -9
  345. data/lib/hecks/runtime/tenant_scope.rb +5 -5
  346. data/lib/hecks/runtime/value/admission.rb +75 -30
  347. data/lib/hecks/runtime/value/coercion.rb +379 -226
  348. data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
  349. data/lib/hecks/runtime/value.rb +28 -23
  350. data/lib/hecks/runtime.rb +7 -7
  351. data/lib/hecks/storehouse.rb +64 -64
  352. data/lib/hecks/version.rb +3 -3
  353. data/lib/hecks/vocabulary.rb +207 -5
  354. data/lib/hecks.rb +13 -11
  355. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
  356. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
  357. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
  358. metadata +43 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d66cdbdb775a92f3cfcf3f626637cffd6fb81915d7edbcccfe21e1b7c980606
4
- data.tar.gz: a43786075ea299d26d6d1f23075cf2176fdad15b7efa235234c4a6e321ec15f3
3
+ metadata.gz: badbf552a5137cc39f791c4a398fe7d33423ff6d15c82cb018d1ef7c464af754
4
+ data.tar.gz: 5bf8d933f52178d2870d19dd0e33b03cacd060b848b54340c4940edbe94e008e
5
5
  SHA512:
6
- metadata.gz: ac3883e9e6ebeef723718dfa39785d2dcdd0e57e613f6ca21a9519040eb11278f22eedbe597965d1b28e3dfa08b98ac2d0a989686cd7b8aec0d8d97dd476acec
7
- data.tar.gz: f610074205ca1a39121169ef02109445b60ca66bf517c1a8351e8c761a1863fccfe0f9b41075fa00cc8225c017c82eefdb47c38566469fe378d0879aec115860
6
+ metadata.gz: 9327ee7366dacb9dcc90fc155999e5ff055efefc8e840ff4ddb08a7042adfcedbe5396c7547713562bebe1db618017b6851421f48d0ac300ab4339cf674a4cca
7
+ data.tar.gz: 97e1aa8115377269a23b29609e88370f86cfaec8dee9f79889232755dea7a55921d7e3db2fa6c3af26ca4f2afaca56a034fd7a6b5b8413ad00e45aef9c24c0d9
@@ -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,7 +40,7 @@ module Hecks
40
40
 
41
41
  module_function
42
42
 
43
- # THE NEXT BEST QUESTION. `state` is whatever
43
+ # **The next best question**. `state` is whatever
44
44
  # `Interview::Session#declaration`/`#gaps` produced — passed
45
45
  # through as JSON, not reformatted, so this adapter never
46
46
  # re-derives what the session already knows.
@@ -53,7 +53,7 @@ module Hecks
53
53
  )
54
54
  end
55
55
 
56
- # PROSE -> PROPOSED DECLARATIONS.
56
+ # Prose -> proposed declarations.
57
57
  def interpret(prose:, state:)
58
58
  call(
59
59
  system: SYSTEM_PREFIX + "Given the domain model so far and a sentence the human just said, " \
@@ -66,7 +66,7 @@ module Hecks
66
66
  )
67
67
  end
68
68
 
69
- # WHAT IS WRONG WITH THIS AS A MODEL — closed to the same kind
69
+ # What is wrong with this as a model — closed to the same kind
70
70
  # vocabulary `Ports::Agent::CRITIQUE_KINDS` declares, spelled out
71
71
  # here too since the system prompt is the only place the model
72
72
  # itself ever sees that list.
@@ -82,7 +82,7 @@ module Hecks
82
82
  )
83
83
  end
84
84
 
85
- # VOCABULARY HELP. Named `suggest_name`, not `name` — see
85
+ # **Vocabulary help**. Named `suggest_name`, not `name` — see
86
86
  # `Ports::Agent#suggest_name`'s own comment for why `name` is
87
87
  # never a safe module-function name here.
88
88
  def suggest_name(meaning:, kind:, near:)
@@ -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
 
@@ -31,7 +31,7 @@ module Hecks
31
31
  load_each(File.join(root, ADAPTERS), %w[*.adapter */*.adapter */*/*.adapter])
32
32
  end
33
33
 
34
- # CHAPTERS FIRST, JUDGED ONCE, THEN EVERYTHING THAT READS THEM.
34
+ # Chapters first, judged once, then everything that reads them.
35
35
  # A chapter may be split across files (the language's own grammar is
36
36
  # nine), and judging file one before files two-through-nine exist
37
37
  # refuses references that are perfectly well declared a file later —
@@ -39,8 +39,8 @@ module Hecks
39
39
  # already places every `*.bluebook` ahead of hecksagons and worlds,
40
40
  # so the window ends at the last chapter pattern rather than at a
41
41
  # hand-written list this would otherwise have to keep in step.
42
- # `environment:` — ONE MORE PAIR OF FILES, LOADED LAST, NOT A GLOB.
43
- # RECOVERED, not new — see Runtime::Loader.boot's own comment for
42
+ # `environment:` — one more pair of files, loaded last, not a glob.
43
+ # Recovered, not new — see Runtime::Loader.boot's own comment for
44
44
  # the provenance. A caller passing `Hecks.boot(path, environment:
45
45
  # "production")` gets exactly `environments/production.hecksagon`
46
46
  # and `environments/production.world` loaded, whichever exist (a
@@ -53,7 +53,7 @@ module Hecks
53
53
  # globs (all non-recursive, none named `environments/*`), so this
54
54
  # is the only thing that ever reaches them. Loaded as genuine
55
55
  # `Hecks.hecksagon "SameDomain" do ... end` / `Hecks.world
56
- # "SameDomain" do ... end` blocks — MERGED into the base file's
56
+ # "SameDomain" do ... end` blocks — merged into the base file's
57
57
  # own hecksagon/world (Registry#add_hecksagon / #add_world,
58
58
  # concatenate/override rather than replace), so an overlay can
59
59
  # rebind or add settings for anything the base file declared
@@ -73,7 +73,7 @@ module Hecks
73
73
  load_each(directory, [File.join("environments", "#{environment}.world")])
74
74
  end
75
75
 
76
- # EVERY BLUEBOOK IN A FOLDER IS ONE DECLARATION SET. Individual files
76
+ # Every bluebook in a folder is one declaration set. Individual files
77
77
  # remain organized in the domain expert's language; the folder is the
78
78
  # unit callers load. Builders group declarations by the chapter name in
79
79
  # each file, so a folder may hold more than one chapter without a catalog.
@@ -84,7 +84,7 @@ module Hecks
84
84
  Bluebook::MetaValidator.judge_deferred!(Hecks.current_registry)
85
85
  end
86
86
 
87
- # THE EXPLICIT-FILE SIBLING OF `load_domain` — for a caller that names
87
+ # The explicit-file sibling of `load_domain` — for a caller that names
88
88
  # its own exact files rather than a directory to glob (`Loader.boot_files`,
89
89
  # behind `Hecks.boot_files`). No `Dir.glob`, no copying: every path here
90
90
  # is a real file on disk, wherever it actually lives, loaded in place —
@@ -93,7 +93,7 @@ module Hecks
93
93
  # tmpdir (see Loader.boot_files's own header for why that pattern is a
94
94
  # hazard, not a convenience).
95
95
  #
96
- # ORDERED BY CATEGORY, NOT BY THE CALLER'S OWN LIST ORDER — same four
96
+ # Ordered by category, not by the caller's own list order — same four
97
97
  # groups `Vocabulary.fetch("LoadOrder")` walks a directory in
98
98
  # (bluebook chapters, translations, hecksagons, worlds), because a
99
99
  # hecksagon can reference a bluebook's own constants and must not load
@@ -138,11 +138,11 @@ module Hecks
138
138
  raise Errno::ENOENT, "no such domain directory: #{path}"
139
139
  end
140
140
 
141
- # THE DOMAIN YOU ARE STANDING IN. Walks up from `from` — the way git
141
+ # **The domain you are standing in**. Walks up from `from` — the way git
142
142
  # finds `.git` — and answers the nearest directory a boot would accept,
143
143
  # or nil if there is not one above you.
144
144
  #
145
- # MARKED BY A `.hecksagon`, NOT BY A `.bluebook`. Chapters are
145
+ # Marked by a `.hecksagon`, not by a `.bluebook`. Chapters are
146
146
  # everywhere: era translations, the language's own self-hosted grammar,
147
147
  # and `spec/fixtures`, which holds a dozen unrelated ones in a single
148
148
  # directory. A `.hecksagon` is the file that says "this is a domain, and
@@ -152,7 +152,7 @@ module Hecks
152
152
  # Both layouts, because `bluebook_directory` above accepts both: a
153
153
  # domain directory holding a `bluebook/` subdirectory (every example in
154
154
  # this corpus), or one holding the files directly.
155
- # NORMALISED TO THE OUTER DIRECTORY. Standing in `examples/banking/bluebook`,
155
+ # Normalised to the outer directory. Standing in `examples/banking/bluebook`,
156
156
  # the `.hecksagon` is right there, so a plain walk stops on the
157
157
  # `bluebook/` directory itself. Both boot identically — `bluebook_directory`
158
158
  # accepts either and `Loader.boot` takes `File.dirname` of what it gets,
@@ -4,26 +4,26 @@ require_relative "../../ports/authentication"
4
4
 
5
5
  module Hecks
6
6
  module Adapters
7
- # GOOGLE'S OWN OIDC HANDSHAKE — the `authentication` port's one real
7
+ # **Google's own OIDC handshake** — the `authentication` port's one real
8
8
  # implementation today, moved here from being hand-rolled per-app
9
9
  # (an embryonaut_console `google_auth.rb` used to do exactly this;
10
10
  # any hecks-based app gets Google sign-in for free now, the
11
11
  # same "one adapter, reusable everywhere" value every other adapter
12
12
  # in this directory already has).
13
13
  #
14
- # `oauth2` does ONLY the authorization-code exchange (no Rack
14
+ # `oauth2` does only the authorization-code exchange (no Rack
15
15
  # middleware, no Omniauth strategy indirection) ; `google-id-token`
16
- # does ONLY ID-token verification (signature checked against
16
+ # does only ID-token verification (signature checked against
17
17
  # Google's real, rotating JWKS — real, maintained code, never
18
18
  # hand-rolled here). Neither library decides what a verified
19
- # (issuer, subject) MEANS — that's `Ports::IdentityResolution`'s
19
+ # (issuer, subject) means — that's `Ports::IdentityResolution`'s
20
20
  # job, called by whoever consumes this port's `verify`.
21
21
  #
22
- # LAZY REQUIRES, same reasoning `Postgres.connect_for` already
22
+ # Lazy requires, same reasoning `Postgres.connect_for` already
23
23
  # holds itself to for `pg`: a domain that never binds
24
24
  # `authentication` to this adapter should never need these gems
25
- # installed. This FILE loads in every boot (driven.rb's own
26
- # unconditional require_relative list) ; the GEMS load only where
25
+ # installed. This file loads in every boot (driven.rb's own
26
+ # unconditional require_relative list) ; the gems load only where
27
27
  # a real handshake actually happens.
28
28
  #
29
29
  # `GOOGLE_CLIENT_ID`/`GOOGLE_CLIENT_SECRET`/`GOOGLE_REDIRECT_URI` —
@@ -68,7 +68,7 @@ module Hecks
68
68
  # itself. `email_verified` rides along because a caller granting
69
69
  # access off this email needs to know Google actually checked it.
70
70
  def verify(code:, state:, expected_state:)
71
- # BOTH GEMS, BEFORE ANYTHING ELSE — not staggered further down
71
+ # **Both gems, before anything else** — not staggered further down
72
72
  # this method: the rescue clause below names GoogleIDToken
73
73
  # ::ValidationError, and Ruby resolves that constant reference
74
74
  # at the moment an exception is being matched, not at parse