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
@@ -27,17 +27,31 @@ module Hecks
27
27
 
28
28
  attr_reader :aggregate, :path
29
29
 
30
+ # Names the optional persistence capabilities `Ports::Persistence::AppendOnly` may rely on.
31
+ #
32
+ # @return [Array<Symbol>] `[:atomic_put]`
30
33
  def persistence_capabilities = [:atomic_put]
31
34
 
35
+ # Opens (creating if absent) the database file and its aggregate, journal, event, saga
36
+ # and outbox tables.
37
+ #
38
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose table this adapter owns
39
+ # @param settings [Hash{Symbol, String => Object}] world settings for the binding:
40
+ # `database` (file path, default `data/<table>.db`) and `domain` (scopes saga rows,
41
+ # default the aggregate's name), each read under a Symbol or a String key
42
+ # @param root [String, nil] directory a relative `database` path resolves against; nil
43
+ # means the process working directory
44
+ # @raise [LoadError] if the `sqlite3` gem is not installed
45
+ # @raise [SQLite3::Exception] if the file cannot be opened or a table cannot be created
32
46
  def initialize(aggregate:, settings: {}, root: nil)
33
- # LAZY, ON PURPOSE — a domain that never wires Sqlite should never
47
+ # **Lazy, on purpose** — a domain that never wires Sqlite should never
34
48
  # need the gem installed. `require "hecks"` alone must not
35
49
  # force a database client library nobody asked for.
36
50
  require "sqlite3"
37
51
 
38
52
  @aggregate = aggregate
39
53
  @path = resolve_path(settings, root)
40
- # THE OPTIONAL saga-persistence capability's own scoping column
54
+ # The optional saga-persistence capability's own scoping column
41
55
  # (§2/§4) — falls back to the aggregate's own name for a
42
56
  # directly-instantiated adapter (specs), same fallback shape
43
57
  # Postgres's own @domain already uses.
@@ -67,18 +81,33 @@ module Hecks
67
81
  create_outbox_table!
68
82
  end
69
83
 
70
- # RE-ENTRANT ON PURPOSE — `atomic_put` opens its own transaction
84
+ # Runs the block inside one SQLite transaction, joining an already-open one.
85
+ #
86
+ # **Re-entrant on purpose** — `atomic_put` opens its own transaction
71
87
  # and `Interpreting#run_dispatch_order` opens one around the whole
72
88
  # save+emit pair; SQLite3 refuses a BEGIN inside a BEGIN, so the
73
89
  # inner call joins the outer one instead. Same shape Postgres uses.
90
+ #
91
+ # @yield the writes to commit together; an exception raised inside rolls the
92
+ # outermost transaction back
93
+ # @return [Object] the block's own result
94
+ # @raise [SQLite3::Exception] if `BEGIN`, a statement inside the block, or `COMMIT` fails
74
95
  def transaction(&)
75
96
  return yield if @db.transaction_active?
76
97
 
77
98
  @db.transaction(&)
78
99
  end
79
100
 
101
+ # Names the aggregate's table; the journal table and outbox rows are keyed off it.
102
+ #
103
+ # @return [String] the aggregate's snake_case storage name, unquoted
80
104
  def table = @aggregate.storage_name
81
105
 
106
+ # Reads the current row for one aggregate identity.
107
+ #
108
+ # @param id [String, Object] the aggregate identity, bound as `id.to_s`
109
+ # @return [Runtime::Instance, nil] the decoded record, or nil when no row has that id
110
+ # @raise [SQLite3::Exception] if the statement fails
82
111
  def find(id)
83
112
  row = @db.get_first_row("SELECT * FROM #{quoted_table} WHERE id = ?", [id.to_s])
84
113
  return nil unless row
@@ -86,9 +115,19 @@ module Hecks
86
115
  Runtime::Instance.new(aggregate: @aggregate, id: row["id"], state: decode(row))
87
116
  end
88
117
 
89
- # order_by IS A RUNTIME VALUE — see postgres.rb's own all for the
118
+ # Lists every stored record, ordered by id unless an ordering attribute is given.
119
+ #
120
+ # order_by is a runtime value — see postgres.rb's own all for the
90
121
  # full reasoning; whitelisted the identical way before it ever
91
122
  # reaches order_expression.
123
+ #
124
+ # @param order_by [String, Symbol, nil] attribute (or dotted value-object path) to sort
125
+ # by; nil orders by id alone
126
+ # @param direction [Symbol, String] `:asc` or `:desc`, case-insensitive; anything else
127
+ # sorts ascending
128
+ # @return [Array<Runtime::Instance>] the decoded records, `[]` when the table is empty
129
+ # @raise [Runtime::WiringError] if `order_by` names no attribute of the aggregate
130
+ # @raise [SQLite3::Exception] if the statement fails
92
131
  def all(order_by: nil, direction: :asc)
93
132
  order_sql = "ORDER BY id"
94
133
  if order_by
@@ -107,21 +146,38 @@ module Hecks
107
146
  end
108
147
  end
109
148
 
149
+ # Counts the rows in the aggregate's table, deleted records excluded.
150
+ #
151
+ # @return [Integer] number of current records
152
+ # @raise [SQLite3::Exception] if the statement fails
110
153
  def count = @db.get_first_value("SELECT COUNT(*) FROM #{quoted_table}").to_i
111
154
 
155
+ # Inserts one journal row, outside any transaction of its own.
156
+ #
157
+ # @param entry [Ports::Persistence::Entry] the save or delete to journal; `state` is
158
+ # encoded through the state codec and `mirrors` stored as JSON, or NULL when nil
159
+ # @return [Ports::Persistence::Entry] the same `entry`
160
+ # @raise [SQLite3::Exception] if the insert fails
112
161
  def append(entry)
113
162
  @db.execute(
114
163
  "INSERT INTO #{quoted_entry_table} (aggregate_id, operation, state, mirrors) VALUES (?, ?, ?, ?)",
115
- # `mirrors` (unlike `state`) is a NULLABLE column — an absent
164
+ # `mirrors` (unlike `state`) is a nullable column — an absent
116
165
  # mirrors hash must bind a real SQL NULL, not the four-character
117
166
  # JSON text `"null"` (`JSON.generate(nil)`), or a future `IS NULL`
118
167
  # check against it would never match. Same guard `postgres_era.rb`
119
168
  # already uses for its own journal's `mirrors` column.
120
- [entry.id, entry.operation, JSON.generate(entry.state), entry.mirrors && JSON.generate(entry.mirrors)]
169
+ [entry.id, entry.operation, JSON.generate(Ports::Persistence::StateCodec.encode(@aggregate, entry.state)),
170
+ entry.mirrors && JSON.generate(entry.mirrors)]
121
171
  )
122
172
  entry
123
173
  end
124
174
 
175
+ # Replaces or deletes the aggregate's row for one journal entry.
176
+ #
177
+ # @param entry [Ports::Persistence::Entry] the save or delete to materialize
178
+ # @return [Runtime::Instance, Array] for a save, a new instance over the entry's state;
179
+ # for a delete, the `DELETE` statement's empty result rows
180
+ # @raise [SQLite3::Exception] if the statement fails
125
181
  def project(entry)
126
182
  return @db.execute("DELETE FROM #{quoted_table} WHERE id = ?", [entry.id]) if entry.delete?
127
183
 
@@ -137,24 +193,41 @@ module Hecks
137
193
  instance
138
194
  end
139
195
 
196
+ # Reads the whole journal back in append order, for `AppendOnly#recover!` to replay.
197
+ #
198
+ # @return [Array<Ports::Persistence::Entry>] every journalled entry, state decoded
199
+ # through the state codec and `mirrors` parsed with String keys (nil when none were
200
+ # stored); a NULL `operation` reads as `"save"`; `[]` when nothing has been appended
201
+ # @raise [SQLite3::Exception] if the statement fails
202
+ # @raise [JSON::ParserError] if a stored `state` or `mirrors` value is not valid JSON
140
203
  def entries
141
204
  @db.execute("SELECT aggregate_id, operation, state, mirrors FROM #{quoted_entry_table} ORDER BY sequence").map do |row|
142
205
  state = JSON.parse(row["state"])
143
206
  Ports::Persistence::Entry.new(
144
207
  operation: row["operation"] || "save",
145
208
  id: row["aggregate_id"],
146
- state: state&.transform_keys(&:to_sym),
209
+ state: Ports::Persistence::StateCodec.decode(@aggregate, state),
147
210
  mirrors: row["mirrors"] && JSON.parse(row["mirrors"])
148
211
  )
149
212
  end
150
213
  end
151
214
 
215
+ # Deletes every row of the aggregate's table and its journal; events, saga rows and
216
+ # outbox rows are left in place.
217
+ #
218
+ # @return [Adapters::Sqlite] self
219
+ # @raise [SQLite3::Exception] if a statement fails
152
220
  def reset!
153
221
  @db.execute("DELETE FROM #{quoted_table}")
154
222
  @db.execute("DELETE FROM #{quoted_entry_table}")
155
223
  self
156
224
  end
157
225
 
226
+ # Journals and replaces an instance's current state in one transaction.
227
+ #
228
+ # @param instance [Runtime::Instance] the instance to store
229
+ # @return [Runtime::Instance] a new instance over a shallow copy of the saved state
230
+ # @raise [SQLite3::Exception] if either statement fails; the transaction is rolled back
158
231
  def save(instance)
159
232
  entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
160
233
  transaction do
@@ -163,9 +236,17 @@ module Hecks
163
236
  end
164
237
  end
165
238
 
239
+ # Stores an entry and reports whether it inserted, replaced or conflicted.
240
+ #
166
241
  # The outcome lookup, journal append and snapshot replacement share one
167
242
  # SQLite transaction. The runtime performs no preliminary find; this
168
243
  # adapter-native operation owns both concurrency and outcome reporting.
244
+ #
245
+ # @param entry [Ports::Persistence::Entry] the save to store
246
+ # @param insert_only [Boolean] when true, an existing row is left untouched
247
+ # @return [Symbol] `:inserted`, `:replaced`, or `:conflicted` when `insert_only` met an
248
+ # existing row and nothing was written
249
+ # @raise [SQLite3::Exception] if a statement fails; the transaction is rolled back
169
250
  def atomic_put(entry, insert_only: false)
170
251
  status = nil
171
252
  transaction do
@@ -181,6 +262,12 @@ module Hecks
181
262
  status
182
263
  end
183
264
 
265
+ # Journals a delete and removes the row, whether or not a row exists. The two
266
+ # statements share a transaction only when the caller has one open.
267
+ #
268
+ # @param id [String, Object] the aggregate identity, journalled as `id.to_s`
269
+ # @return [Boolean] always true
270
+ # @raise [SQLite3::Exception] if either statement fails
184
271
  def delete(id)
185
272
  entry = Ports::Persistence::Entry.new(operation: "delete", id: id.to_s, state: nil)
186
273
  append(entry)
@@ -188,6 +275,11 @@ module Hecks
188
275
  true
189
276
  end
190
277
 
278
+ # Inserts an emitted event into the database's shared `events` table.
279
+ #
280
+ # @param event [Runtime::Event] the emitted event; `payload` is stored as JSON
281
+ # @return [Array] the insert's empty result rows; callers ignore it
282
+ # @raise [SQLite3::Exception] if the insert fails
191
283
  def record_event(event)
192
284
  @db.execute(
193
285
  "INSERT INTO events (name, aggregate, aggregate_id, payload, occurred_at) VALUES (?, ?, ?, ?, ?)",
@@ -195,6 +287,12 @@ module Hecks
195
287
  )
196
288
  end
197
289
 
290
+ # Reads back every recorded event in insertion order — the database file's whole
291
+ # `events` table, not only this aggregate's rows.
292
+ #
293
+ # @return [Array<Runtime::Event>] the stored events, `payload` parsed with Symbol keys
294
+ # and `occurred_at` as stored; `[]` when none are recorded
295
+ # @raise [SQLite3::Exception] if the statement fails
198
296
  def events
199
297
  @db.execute("SELECT * FROM events ORDER BY id").map do |row|
200
298
  Runtime::Event.new(
@@ -207,26 +305,22 @@ module Hecks
207
305
  end
208
306
  end
209
307
 
210
- # ── the OPTIONAL saga-persistence capability (§2) — reuses the
211
- # DDL every SQLite-backed aggregate table already lives beside
212
- # (`create_saga_table!`, `Sqlite::SchemaBuilder`, shared with D1).
213
- # SQLite's `resolve_path` defaults to one `.db` file PER
214
- # AGGREGATE unless a domain shares one `database` setting across
215
- # its aggregates — since saga persistence resolves through
216
- # whichever adapter instance backs the domain's FIRST aggregate
217
- # (`Registry#saga_persistence`), this table ends up living inside
218
- # THAT one aggregate's own file by default. Correct and durable
219
- # either way; a domain that wants an obviously-named saga store
220
- # already gets one by sharing `database` across its aggregates,
221
- # the recommended, common case.
222
- # THE OUTBOX — see `Runtime::Outbox`. Rows land in the SAME
308
+ # Inserts new outbox rows as pending, skipping any whose `delivery_id` already exists.
309
+ #
310
+ # The outbox — see `Runtime::Outbox`. Rows land in the same
223
311
  # database as this aggregate (the only way the enqueue shares the
224
312
  # save's transaction), keyed by the aggregate's storage name so an
225
313
  # adapter instance only ever reads back its own rows even when
226
314
  # several aggregates share one file. `INSERT OR IGNORE` on the
227
- # UNIQUE delivery_id makes a re-enqueue of the same (event,
315
+ # unique delivery_id makes a re-enqueue of the same (event,
228
316
  # consumer) a no-op; `outbox_claim`'s `WHERE status = 'pending'`
229
317
  # is the compare-and-set that lets exactly one relay win a row.
318
+ #
319
+ # @param rows [Array<Runtime::Outbox::Row>] rows to enqueue; each accepted row has its
320
+ # `id` and `status` assigned in place. `row.aggregate` is stored as given
321
+ # @return [Array<Runtime::Outbox::Row>] the rows actually inserted, `[]` when every one
322
+ # was a duplicate
323
+ # @raise [SQLite3::Exception] if an insert fails
230
324
  def outbox_enqueue(rows)
231
325
  rows.filter_map do |row|
232
326
  @db.execute(
@@ -243,6 +337,12 @@ module Hecks
243
337
  end
244
338
  end
245
339
 
340
+ # Claims a pending outbox row with a compare-and-set update, counting the attempt.
341
+ #
342
+ # @param id [Integer] the row id `outbox_enqueue` assigned
343
+ # @return [Boolean] true when the row was pending and is now claimed; false when it is
344
+ # unknown or another claimer got there first
345
+ # @raise [SQLite3::Exception] if the update fails
246
346
  def outbox_claim(id)
247
347
  @db.execute(
248
348
  "UPDATE hecks_outbox SET status = 'claimed', attempts = attempts + 1, claimed_at = ? " \
@@ -252,6 +352,15 @@ module Hecks
252
352
  @db.changes == 1
253
353
  end
254
354
 
355
+ # Records a delivery outcome and its settle time on an outbox row, whatever status it
356
+ # held.
357
+ #
358
+ # @param id [Integer] the row id `outbox_enqueue` assigned
359
+ # @param status [String, Symbol] the new status, one of `Runtime::Outbox::STATUSES`;
360
+ # not validated here
361
+ # @param error [String, nil] the failure description, or nil to store NULL
362
+ # @return [Boolean] true when exactly one row was updated; false when no row has `id`
363
+ # @raise [SQLite3::Exception] if the update fails
255
364
  def outbox_settle(id, status:, error: nil)
256
365
  @db.execute(
257
366
  "UPDATE hecks_outbox SET status = ?, error = ?, settled_at = ? WHERE id = ?",
@@ -260,6 +369,13 @@ module Hecks
260
369
  @db.changes == 1
261
370
  end
262
371
 
372
+ # Lists the outbox rows whose `aggregate` column equals this adapter's `table`, in
373
+ # enqueue order.
374
+ #
375
+ # @param status [String, Symbol, nil] only rows with this status; nil lists every row
376
+ # @return [Array<Runtime::Outbox::Row>] the matching rows, `event` parsed with Symbol
377
+ # keys; `[]` when none match
378
+ # @raise [SQLite3::Exception] if the statement fails
263
379
  def outbox_rows(status: nil)
264
380
  sql = "SELECT * FROM hecks_outbox WHERE aggregate = ?"
265
381
  binds = [table]
@@ -270,6 +386,31 @@ module Hecks
270
386
  @db.execute("#{sql} ORDER BY id", binds).map { |row| outbox_row(row) }
271
387
  end
272
388
 
389
+ # Replaces one saga instance's checkpoint, keyed by domain, process manager and
390
+ # correlation.
391
+ #
392
+ # ── the optional saga-persistence capability (§2) — reuses the
393
+ # DDL every SQLite-backed aggregate table already lives beside
394
+ # (`create_saga_table!`, `Sqlite::SchemaBuilder`, shared with D1).
395
+ # SQLite's `resolve_path` defaults to one `.db` file per
396
+ # aggregate unless a domain shares one `database` setting across
397
+ # its aggregates — since saga persistence resolves through
398
+ # whichever adapter instance backs the domain's first aggregate
399
+ # (`Registry#saga_persistence`), this table ends up living inside
400
+ # that one aggregate's own file by default. Correct and durable
401
+ # either way; a domain that wants an obviously-named saga store
402
+ # already gets one by sharing `database` across its aggregates,
403
+ # the recommended, common case.
404
+ #
405
+ # @param process_manager [String, Symbol] the process manager's name
406
+ # @param correlation [String, Object] the instance's correlation value, stored as
407
+ # `correlation.to_s`
408
+ # @param state [String, Symbol] the saga's current state name
409
+ # @param memory [Hash] the saga's memory; must be JSON-serializable
410
+ # @param completed_compensations [Array] the ledger of completed compensable legs; must
411
+ # be JSON-serializable
412
+ # @return [Array] the statement's empty result rows; callers ignore it
413
+ # @raise [SQLite3::Exception] if the statement fails
273
414
  def save_saga(process_manager:, correlation:, state:, memory:, completed_compensations: [])
274
415
  @db.execute(
275
416
  "INSERT OR REPLACE INTO hecks_saga_instances (domain, process_manager, correlation, state, memory, " \
@@ -279,6 +420,13 @@ module Hecks
279
420
  )
280
421
  end
281
422
 
423
+ # Removes a finished saga instance's checkpoint; a missing row is not an error.
424
+ #
425
+ # @param process_manager [String, Symbol] the process manager's name
426
+ # @param correlation [String, Object] the instance's correlation value, matched as
427
+ # `correlation.to_s`
428
+ # @return [Array] the statement's empty result rows; callers ignore it
429
+ # @raise [SQLite3::Exception] if the statement fails
282
430
  def delete_saga(process_manager:, correlation:)
283
431
  @db.execute(
284
432
  "DELETE FROM hecks_saga_instances WHERE domain = ? AND process_manager = ? AND correlation = ?",
@@ -286,6 +434,18 @@ module Hecks
286
434
  )
287
435
  end
288
436
 
437
+ # Yields every checkpointed saga instance of this adapter's domain, for
438
+ # `Registry#rehydrate_sagas!` to restore at boot.
439
+ #
440
+ # @yieldparam process_manager [String] the process manager's name
441
+ # @yieldparam correlation [String] the instance's correlation value
442
+ # @yieldparam state [String] the saga's state name
443
+ # @yieldparam memory [Hash{Symbol => Object}] the saga's memory, Symbol keys at every depth
444
+ # @yieldparam completed_compensations [Array] the completed-compensation ledger, `[]`
445
+ # when the column is NULL
446
+ # @return [Enumerator, Array<Hash>] an enumerator over the same five values when no
447
+ # block is given; otherwise the raw result rows
448
+ # @raise [SQLite3::Exception] if the statement fails
289
449
  def each_saga
290
450
  return enum_for(:each_saga) unless block_given?
291
451
 
@@ -8,6 +8,9 @@ module Hecks
8
8
  module SystemClock
9
9
  module_function
10
10
 
11
+ # Reads the machine's own current time.
12
+ #
13
+ # @return [Integer] the current time, in Unix epoch seconds
11
14
  def now = Time.now.to_i
12
15
  end
13
16
  end
@@ -0,0 +1,3 @@
1
+ Hecks.adapter "TenantProvisioner" do
2
+ port "TenantProvisioning"
3
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Hecks
6
+ module Adapters
7
+ # THE IMPERATIVE HALF of provisioning a tenant, pulled out of
8
+ # `bin/project_tenant` and behind a real driven port
9
+ # (`Deploy::Tenant.port "TenantProvisioning"`) instead — the same
10
+ # hexagonal reasoning `GithubChecks` already gives for its own
11
+ # port: real, impure, side-effecting work (writing the overlay
12
+ # file) belongs in an adapter, not in a bare script or a command
13
+ # handler. `Deploy::Tenant`'s own `asks "Provision"` operation calls
14
+ # `#provision` and turns whatever it returns into `TenantProvisioned`,
15
+ # or whatever it raises into `ProvisioningRefused` — see
16
+ # `PortOperationInterpreter#ask`'s own "every failure is an answer"
17
+ # comment for why nothing here needs its own rescue.
18
+ #
19
+ # DELIBERATELY DOES NOT BOOT THE TARGET DOMAIN — that step (proving
20
+ # it boots for real, running the tenant_capable? gate) stays in
21
+ # `bin/project_tenant` itself, called AFTER this operation answers,
22
+ # not nested inside it. Booting a whole domain from inside an
23
+ # adapter method that is itself running mid-dispatch (this port
24
+ # operation) is a real, separate concern from writing one file, and
25
+ # keeping the two apart avoids a nested `Hecks.boot` call ever
26
+ # running inside another boot's own dispatch call stack.
27
+ #
28
+ # RETURNS A HASH SHAPED LIKE `Tenancy::Tenant.Register`'S OWN
29
+ # ARGUMENTS, on purpose — `tenancy.hecksagon`'s own `translates`
30
+ # reaction forwards an answered event's payload verbatim, so this
31
+ # adapter's own return value IS the translation from Deploy's
32
+ # vocabulary into Tenancy's, decided here rather than in a mapping
33
+ # layer neither side could see into.
34
+ class TenantProvisioner
35
+ def initialize(aggregate: nil, settings: {}, root: nil); end
36
+
37
+ # `directory:` IS REQUIRED, NOT DERIVED FROM `domain` — a real,
38
+ # confirmed bug found only under CI's own real-Postgres suite: a
39
+ # domain's own DECLARED NAME (`domain`, e.g. "Scratch") is not a
40
+ # filesystem path, and File.expand_path(domain, Dir.pwd) silently
41
+ # resolved to the wrong directory (relative to whatever the
42
+ # RUNNING PROCESS' own cwd happened to be) whenever the CLI's own
43
+ # `--domain=` value didn't happen to equal its own directory
44
+ # argument by coincidence. `bin/project_tenant` passes its own
45
+ # real `domain_directory` local straight through.
46
+
47
+ def provision(slug:, domain:, realm:, schema:, database:, adapter:, directory:)
48
+ domain_directory = File.expand_path(directory[:value] || directory)
49
+
50
+ overlay_path = File.join(domain_directory, "environments", "#{slug[:value] || slug}.world")
51
+ FileUtils.mkdir_p(File.dirname(overlay_path))
52
+ File.write(overlay_path, <<~WORLD)
53
+ Hecks.world "#{domain[:value] || domain}" do
54
+ realm "#{realm[:value] || realm}"
55
+ persisted_by("#{adapter[:value] || adapter}") do
56
+ database "#{database[:value] || database}"
57
+ schema "#{schema[:value] || schema}"
58
+ end
59
+ end
60
+ WORLD
61
+
62
+ { slug: slug, domain: domain, realm: realm, schema: schema }
63
+ end
64
+ end
65
+ end
66
+ end
@@ -14,7 +14,7 @@ end
14
14
  require_relative "driven/memory"
15
15
  require_relative "driven/sqlite"
16
16
  require_relative "driven/postgres"
17
- # `PostgresEra` is NOT `require_relative`d here — ADR 0033 moved it, and
17
+ # `PostgresEra` is not `require_relative`d here — ADR 0033 moved it, and
18
18
  # the rest of the era/lineage/translation subsystem, behind a loadable
19
19
  # persistence plugin (`hecks/ports/persistence/plugins/era`) rather than
20
20
  # requiring every app to carry it whether or not anything ever binds
@@ -22,10 +22,10 @@ require_relative "driven/postgres"
22
22
  # that makes that genuinely load-nothing-until-asked instead of pushing
23
23
  # an explicit `require` onto every one of the ~15 generic `bin/*` tools
24
24
  # that boot an arbitrary checked-in domain (several of which — pizzas,
25
- # compliance, chess, roster — bind PostgresEra): the FIRST real
25
+ # compliance, chess, roster — bind PostgresEra): the first real
26
26
  # `Adapters.const_get("PostgresEra")` (`registry/verification.rb`'s own
27
27
  # adapter resolution, unchanged) transparently `require`s the plugin
28
- # entry point, which defines the constant AND calls `register_plugin`
28
+ # entry point, which defines the constant and calls `register_plugin`
29
29
  # as its own side effect (`plugins/era.rb`'s last line) — the exact
30
30
  # same effect an explicit `require "hecks/ports/persistence/plugins/
31
31
  # era"` has, just deferred to the moment something is actually asked
@@ -40,10 +40,12 @@ require_relative "driven/prism"
40
40
  require_relative "driven/folder"
41
41
  require_relative "driven/d1"
42
42
  require_relative "driven/mock_stripe_adapter"
43
+ require_relative "driven/tenant_provisioner"
43
44
  require_relative "driven/secure_random_identity"
45
+ require_relative "driven/in_process_key_vault"
44
46
  require_relative "driven/system_clock"
45
47
  # `SequentialIdentity` — the deterministic identity_generation test
46
- # double — is NOT required here on purpose. It lives at
48
+ # double — is not required here on purpose. It lives at
47
49
  # spec/fixtures/sequential_identity.{adapter,rb}, loaded explicitly by
48
50
  # whichever spec wants it: this file's `require_relative` list is what
49
51
  # `Folder#load_library`'s `.adapter` glob backs, and `.adapter` DSL
@@ -4,19 +4,19 @@ require "rack"
4
4
 
5
5
  module Hecks
6
6
  module Adapters
7
- # THE DRIVING SIDE — code an OUTSIDE caller reaches IN through,
8
- # rather than code the domain reaches OUT through. Every existing
7
+ # **The driving side** — code an outside caller reaches in through,
8
+ # rather than code the domain reaches out through. Every existing
9
9
  # file under `adapters/driven/` is the latter: a store or reader a
10
- # `persisted_by`/`port` binding resolves TO, called BY this
10
+ # `persisted_by`/`port` binding resolves to, called by this
11
11
  # framework's own runtime. Nothing under this repository has ever
12
- # been the mirror image before — code that receives a request FROM
12
+ # been the mirror image before — code that receives a request from
13
13
  # the outside world and turns it into a dispatch — so this is the
14
14
  # first entry, and the directory itself is new.
15
15
  module Driving
16
- # A GITHUB WEBHOOK RECEIVER, TRANSPORT ONLY — the same split
16
+ # **A GitHub webhook receiver, transport only** — the same split
17
17
  # `Hecks::Adapters::GithubChecks` (qa/adapters/github_checks.rb,
18
- # this class's own PULL-side sibling) already draws for itself:
19
- # THIS file owns proving a request really came from GitHub and
18
+ # this class's own pull-side sibling) already draws for itself:
19
+ # this file owns proving a request really came from GitHub and
20
20
  # unwrapping GitHub's own webhook envelope (`X-GitHub-Event`, the
21
21
  # JSON body, GitHub's own automatic `ping` check) — never which
22
22
  # commands to dispatch about what it finds inside. That is exactly
@@ -26,10 +26,12 @@ module Hecks
26
26
  # in `qa/adapters/github_ci_webhook.rb`, the subclass of this file
27
27
  # that actually knows what a `QualityControl::Clearance` is.
28
28
  #
29
- # A PLAIN RACK APP (`#call(env)`) — no Sinatra, no Rails — the same
29
+ # ## A plain rack app
30
+ #
31
+ # A plain rack app (`#call(env)`) — no Sinatra, no Rails — the same
30
32
  # shape `Hecks::Forms::App` (lib/hecks/forms/app.rb) already
31
33
  # established for the one other HTTP-facing surface this library
32
- # ships. `rack` is a LAZY Gemfile dependency for exactly the reason
34
+ # ships. `rack` is a lazy Gemfile dependency for exactly the reason
33
35
  # that file's own header gives: this file is never required by
34
36
  # `require "hecks"` (nothing under `adapters.rb`'s own eager
35
37
  # `adapters/driven` load names it — see that file's own header),
@@ -37,9 +39,11 @@ module Hecks
37
39
  # `rack` installed, the same "opt in by requiring the file at all"
38
40
  # contract `hecks/forms.rb` already has for `Forms::App`.
39
41
  #
40
- # SUBCLASS RESPONSIBILITY: implement `#handle_event(event, action,
42
+ # ## Subclass responsibility
43
+ #
44
+ # Implement `#handle_event(event, action,
41
45
  # payload)`, returning `[http_status, response_body_hash]`. Called
42
- # ONLY after the signature has verified and the body has parsed as
46
+ # only after the signature has verified and the body has parsed as
43
47
  # JSON — a subclass never has to re-check either. `event` is
44
48
  # GitHub's own `X-GitHub-Event` header value ("check_suite",
45
49
  # "check_run", "pull_request", ...); `action` is the payload's own
@@ -51,7 +55,7 @@ module Hecks
51
55
  # subclass at all; there is nothing domain-specific to decide
52
56
  # about it.
53
57
  class GithubWebhook
54
- # REFUSED, LOUDLY — the same shape a domain refusal already takes
58
+ # **Refused, loudly** — the same shape a domain refusal already takes
55
59
  # everywhere else in this codebase (`Runtime::DOMAIN_REFUSALS`,
56
60
  # `Forms::App`'s own `{error:, message:}` JSON body for a bad
57
61
  # command). A request that cannot prove it came from GitHub gets
@@ -60,31 +64,40 @@ module Hecks
60
64
  # exactly like success in a log nobody re-reads.
61
65
  class InvalidSignature < StandardError; end
62
66
 
63
- # THE BODY DID NOT EVEN PARSE — distinct from a signature refusal:
67
+ # **The body did not even parse** — distinct from a signature refusal:
64
68
  # this body genuinely came from whoever signed it (checked
65
- # FIRST, before parsing ever runs — see `#call`), and simply
69
+ # first, before parsing ever runs — see `#call`), and simply
66
70
  # is not JSON. Still refused, never guessed at.
67
71
  class MalformedPayload < StandardError; end
68
72
 
69
73
  SIGNATURE_HEADER = "HTTP_X_HUB_SIGNATURE_256".freeze
70
74
  EVENT_HEADER = "HTTP_X_GITHUB_EVENT".freeze
71
75
 
72
- # `secret:` HAS NO DEFAULT, ON PURPOSE — the same rule
76
+ # `secret:` has no default, on purpose — the same rule
73
77
  # `GoogleAuthentication`'s own header states for its own
74
78
  # `ENV.fetch`, restated here because the consequence is worse for
75
79
  # a webhook: an unverified signature check is not "half
76
- # configured", it is NO verification at all, silently accepting
80
+ # configured", it is no verification at all, silently accepting
77
81
  # anything claiming to be GitHub. A caller passes the real
78
82
  # secret explicitly — from `ENV.fetch("GITHUB_WEBHOOK_SECRET")`
79
83
  # or wherever it keeps one — rather than this class reaching into
80
84
  # the environment itself and hiding that requirement inside a
81
85
  # default.
86
+ # @param secret [String] the shared webhook secret (e.g. `ENV.fetch
87
+ # ("GITHUB_WEBHOOK_SECRET")`), checked against each request's `X-Hub-Signature-256`
88
+ # @raise [ArgumentError] if `secret` is nil or empty
82
89
  def initialize(secret:)
83
90
  raise ArgumentError, "no webhook secret configured" if secret.to_s.empty?
84
91
 
85
92
  @secret = secret
86
93
  end
87
94
 
95
+ # The rack entry point: verifies, parses, and routes one webhook request.
96
+ #
97
+ # @param env [Hash] the rack environment
98
+ # @return [Array(Integer, Hash, Array<String>)] the rack response triple — status,
99
+ # headers, and a one-element body Array holding the JSON-encoded response
100
+ # @raise [NotImplementedError] if the subclass has not implemented `#handle_event`
88
101
  def call(env)
89
102
  request = Rack::Request.new(env)
90
103
  return respond(405, error: "MethodNotAllowed", message: "POST only") unless request.post?
@@ -107,8 +120,8 @@ module Hecks
107
120
 
108
121
  private
109
122
 
110
- # CONSTANT-TIME COMPARE, NOT `==`. A byte-by-byte `==` returns
111
- # the moment it finds the first mismatching byte, so how LONG
123
+ # Constant-time compare, not `==`. A byte-by-byte `==` returns
124
+ # the moment it finds the first mismatching byte, so how long
112
125
  # that took leaks how many leading bytes of a forged signature
113
126
  # were already right to anyone timing the response — GitHub's
114
127
  # own webhook documentation calls this out by name and recommends