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
@@ -23,7 +23,7 @@ module Hecks
23
23
  raise Malformed, "#{@path}: json error: #{e.message}"
24
24
  end
25
25
 
26
- # Temp-file-plus-rename, fsynced before the rename: a reader can
26
+ # Temp-file-plus-rename, fsynced ahead of the `File.rename` call: a reader can
27
27
  # only ever see the last complete snapshot or the one before it,
28
28
  # never a truncated or partial one — `File.binwrite`'s old
29
29
  # truncate-then-write left a window, proportional to the whole
@@ -25,12 +25,18 @@ module Hecks
25
25
 
26
26
  attr_reader :aggregate, :path, :events
27
27
 
28
+ # @param aggregate [Bluebook::Aggregate] the aggregate this store persists
29
+ # @param settings [Hash] adapter settings; `dir:`/`"dir"` (a storage directory, `"data"`
30
+ # by default) and `domain:`/`"domain"` (the saga-persistence scope, defaulting to
31
+ # `aggregate.name`) are read
32
+ # @param root [String, nil] the directory `settings[:dir]` resolves relative to when it
33
+ # is not absolute; defaults to the process's current working directory
28
34
  def initialize(aggregate:, settings: {}, root: nil)
29
35
  @aggregate = aggregate
30
36
  @path = resolve_path(settings, root)
31
37
  @journal_path = "#{@path}.journal"
32
38
  @events = []
33
- # THE OPTIONAL saga-persistence capability's own scoping (§2/§4)
39
+ # The optional saga-persistence capability's own scoping (§2/§4)
34
40
  # — falls back to the aggregate's own name for a directly-
35
41
  # instantiated adapter (specs), same fallback shape Postgres's
36
42
  # own @domain already uses.
@@ -47,6 +53,10 @@ module Hecks
47
53
  FileUtils.mkdir_p(File.dirname(@path))
48
54
  end
49
55
 
56
+ # Reads one record's current projected state.
57
+ #
58
+ # @param id [String, Object] the record's identity, compared as `id.to_s`
59
+ # @return [Runtime::Instance, nil] the stored record, or nil when no record has that id
50
60
  def find(id)
51
61
  record = store[id.to_s]
52
62
  return nil unless record
@@ -54,24 +64,45 @@ module Hecks
54
64
  instance(id.to_s, record)
55
65
  end
56
66
 
67
+ # Lists every record currently projected, sorted by id then reordered as requested.
68
+ #
69
+ # @param order_by [String, Symbol, nil] an attribute name to sort by; nil keeps id order
70
+ # @param direction [Symbol] `:asc` or `:desc`
71
+ # @return [Array<Runtime::Instance>] the stored records; `[]` when there are none
57
72
  def all(order_by: nil, direction: :asc)
58
73
  records = store.sort_by { |id, _| id }.map { |id, record| instance(id, record) }
59
74
  InMemoryOrdering.ordered(records, aggregate: @aggregate, order_by: order_by, direction: direction)
60
75
  end
61
76
 
77
+ # Counts the records currently projected.
78
+ #
79
+ # @return [Integer] number of stored records, not of journal entries
62
80
  def count = store.size
63
81
 
82
+ # Answers a declared query specification against the projected records.
83
+ #
64
84
  # `registry: context[:registry]` — Memory's own `query` already
65
85
  # threads this through; Heki's own never did, which made
66
86
  # `none_in_state?` (Ports::Query::InMemory) unconditionally
67
87
  # return `true` (its own graceful "no registry, no way to look
68
- # the target up" default) for EVERY `none_in_state` where-clause
88
+ # the target up" default) for every `none_in_state` where-clause
69
89
  # against a Heki-backed aggregate — silently excluding nothing,
70
90
  # always, no matter the actual target state.
91
+ #
92
+ # @param specification [QuerySpecification::Common::Options,
93
+ # Bluebook::Behaviour::ReadModel::FilteredOptions] the declared query specification
94
+ # @param args [Hash{Symbol => Object}] bound values for the specification's placeholders
95
+ # @param context [Hash{Symbol => Object}] call context; `:registry` is read and passed
96
+ # through for registry-aware comparisons
97
+ # @return [Array<Runtime::Instance>] the matching records, ordered and paged
71
98
  def query(specification, args = {}, context: {})
72
99
  Ports::Query::InMemory.execute(all, specification, args, registry: context[:registry])
73
100
  end
74
101
 
102
+ # Writes one entry to the durable journal, before any projection of it.
103
+ #
104
+ # @param entry [Persistence::Entry] the save or delete to journal
105
+ # @return [Persistence::Entry] `entry`, unchanged
75
106
  def append(entry)
76
107
  @entry_mirrors = entry.mirrors
77
108
  append_entry(entry.operation, entry.id, entry.state)
@@ -80,18 +111,42 @@ module Hecks
80
111
  @entry_mirrors = nil
81
112
  end
82
113
 
114
+ # Applies one journaled entry to the current-state snapshot.
115
+ #
83
116
  # Reads fresh rather than trusting the memoized `store` — under
84
117
  # `with_lock`, another process may have projected a snapshot since
85
118
  # this one last read it, and mutating *its* stale copy would
86
119
  # overwrite that write on disk rather than layer on top of it.
120
+ #
121
+ # On a delete, though, the record `current.delete` finds is already
122
+ # gone: every caller (`#delete` above, `AppendOnly#delete`) appends
123
+ # before it projects, so the fresh read above has already replayed
124
+ # this very entry off the journal. What the memoized `store` last
125
+ # held — from before this call — is read up front, for the return
126
+ # value only; it plays no part in what gets written.
127
+ #
128
+ # @param entry [Persistence::Entry] the save or delete to materialize
129
+ # @return [Runtime::Instance, nil] on a save, the newly stored record; on a delete, the
130
+ # removed record, or nil when no record had that id
87
131
  def project(entry)
132
+ removed = entry.delete? ? store[entry.id] : nil
88
133
  current = read
89
- entry.save? ? current[entry.id] = entry.state.dup : current.delete(entry.id)
134
+ if entry.save?
135
+ current[entry.id] = Ports::Persistence::StateCodec.encode(@aggregate, entry.state)
136
+ projected = Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: entry.state)
137
+ else
138
+ current.delete(entry.id)
139
+ projected = removed && instance(entry.id, removed)
140
+ end
90
141
  write(current)
91
142
  @store = current
92
- entry
143
+ projected
93
144
  end
94
145
 
146
+ # Journals and projects an instance's state under the file lock.
147
+ #
148
+ # @param instance [Runtime::Instance] the record to persist
149
+ # @return [Runtime::Instance] `instance`, unchanged
95
150
  def save(instance)
96
151
  entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
97
152
  with_lock do
@@ -101,6 +156,11 @@ module Hecks
101
156
  instance
102
157
  end
103
158
 
159
+ # Journals and projects the removal of one record, if it exists, under the file lock.
160
+ #
161
+ # @param id [String, Object] the record's identity, compared as `id.to_s`
162
+ # @return [Boolean] true when a record was found and deleted, false when there was none
163
+ # and nothing was journaled
104
164
  def delete(id)
105
165
  return false unless find(id)
106
166
 
@@ -112,20 +172,55 @@ module Hecks
112
172
  true
113
173
  end
114
174
 
175
+ # Records one emitted event in this adapter's in-memory event log.
176
+ #
177
+ # @param event [Runtime::Event] the event to record
178
+ # @return [Array<Runtime::Event>] the adapter's in-memory event log, including `event`
115
179
  def record_event(event) = @events << event
116
180
 
117
- # ── the OPTIONAL saga-persistence capability (§2) — Heki's own
181
+ # ── the optional saga-persistence capability (§2) — Heki's own
118
182
  # shape (a sibling snapshot+journal file pair, `SagaStore`,
119
183
  # heki/saga_store.rb) rather than a table in a store this adapter
120
184
  # doesn't have.
185
+ #
186
+ # @param process_manager [String, Symbol] the process manager's name, compared as
187
+ # `.to_s`
188
+ # @param correlation [String, Symbol, Object] the instance's correlation value, compared
189
+ # as `.to_s`
190
+ # @param state [String, Symbol] the saga's current state name, compared as `.to_s`
191
+ # @param memory [Hash] the saga's working memory to persist
192
+ # @param completed_compensations [Array] the ledger of completed compensable legs;
193
+ # `[]` when none
194
+ # @return [Hash{String => Hash}] `SagaStore`'s internal records Hash after the write;
195
+ # callers ignore it
121
196
  def save_saga(process_manager:, correlation:, state:, memory:, completed_compensations: [])
122
197
  saga_store.save_saga(@domain, process_manager.to_s, correlation.to_s, state.to_s, memory, completed_compensations)
123
198
  end
124
199
 
200
+ # Removes a finished saga instance's checkpoint; a missing one is not an error.
201
+ #
202
+ # @param process_manager [String, Symbol] the process manager's name, compared as
203
+ # `.to_s`
204
+ # @param correlation [String, Symbol, Object] the instance's correlation value, compared
205
+ # as `.to_s`
206
+ # @return [Hash{String => Hash}] `SagaStore`'s internal records Hash after the delete;
207
+ # callers ignore it
125
208
  def delete_saga(process_manager:, correlation:)
126
209
  saga_store.delete_saga(@domain, process_manager.to_s, correlation.to_s)
127
210
  end
128
211
 
212
+ # Yields every checkpointed saga instance of this domain, for `Registry
213
+ # #rehydrate_sagas!` to restore at boot.
214
+ #
215
+ # @yieldparam process_manager [String] the process manager's name
216
+ # @yieldparam correlation [String] the instance's correlation value
217
+ # @yieldparam state [String] the saga's state name
218
+ # @yieldparam memory [Hash{Symbol => Object}] the saga's memory, Symbol keys at every
219
+ # depth
220
+ # @yieldparam completed_compensations [Array] the completed-compensation ledger, `[]`
221
+ # when none was recorded
222
+ # @return [Enumerator, Hash{String => Hash}] an enumerator over the same five values
223
+ # when no block is given; otherwise `SagaStore`'s internal records Hash
129
224
  def each_saga(&) = saga_store.each_saga(@domain, &)
130
225
 
131
226
  private
@@ -134,11 +229,13 @@ module Hecks
134
229
  @saga_store ||= SagaStore.new(File.dirname(@path))
135
230
  end
136
231
 
232
+ # `record` is the snapshot/journal's own string-keyed JSON — decoded
233
+ # deep through the state codec, never symbolized one level by hand.
137
234
  def instance(id, record)
138
235
  Runtime::Instance.new(
139
236
  aggregate: @aggregate,
140
237
  id: id,
141
- state: record.transform_keys(&:to_sym)
238
+ state: Ports::Persistence::StateCodec.decode(@aggregate, record)
142
239
  )
143
240
  end
144
241
 
@@ -152,10 +249,10 @@ module Hecks
152
249
  end
153
250
 
154
251
  # `dir: :default` — a bare Symbol, the framework's own convention
155
- # for "a DECLARED value that resolves by convention, never a silent
156
- # fallback" — used to crash `File.join` outright
252
+ # for "a declared value that resolves by convention, never a silent
253
+ # fallback" — would otherwise crash `File.join` outright
157
254
  # (`TypeError: no implicit conversion of Symbol into String`):
158
- # `resolve_path` only ever checked for a MISSING `dir` setting,
255
+ # `resolve_path` only ever checked for a missing `dir` setting,
159
256
  # never a Symbol one. Treated the same as no setting at all — falls
160
257
  # back to the existing "data" default, not a new special case.
161
258
  def resolve_path(settings, root)
@@ -1,18 +1,28 @@
1
1
  module Hecks
2
2
  module Adapters
3
- # THE `identity_resolution` PORT, FULFILLED BY THE Identity FRAMEWORK
4
- # BLUEBOOK — same registry, same boot, so this is a dispatch against
3
+ # The `identity_resolution` port, fulfilled by the Identity framework
4
+ # bluebook — same registry, same boot, so this is a dispatch against
5
5
  # records already sitting in the store this adapter is handed, not a
6
6
  # bridge to a second runtime. Same reasoning `GovernanceAuthorization`
7
7
  # gives for itself, one port over.
8
8
  module IdentityRegistry
9
9
  module_function
10
10
 
11
+ # Looks up the id of the identity an authenticated (issuer, subject) pair is linked to,
12
+ # querying the Identity framework bluebook's own `ResolvedBy`.
13
+ #
11
14
  # `nil` for a pair nothing has linked, the first match otherwise —
12
15
  # `ResolvedBy` is a lookup by the exact (issuer, subject) an
13
16
  # authenticated token carries, not a listing, so more than one row
14
17
  # would mean two links share a pair, which `Link`'s own natural
15
18
  # key already prevents by construction.
19
+ #
20
+ # @param registry [Runtime::Registry] the booted registry, queried for the linked
21
+ # identity's id
22
+ # @param issuer [String] the OIDC issuer that authenticated the caller, compared as a
23
+ # String
24
+ # @param subject [String] the OIDC subject the issuer vouches for, compared as a String
25
+ # @return [String, nil] the linked identity's id, or nil if nothing has linked this pair
16
26
  def resolve(registry, issuer:, subject:)
17
27
  rows = Runtime::Dispatcher.new(registry).query(
18
28
  "Identity::ExternalIdentifier.ResolvedBy",
@@ -6,7 +6,7 @@ require_relative "../../runtime/errors"
6
6
 
7
7
  module Hecks
8
8
  module Adapters
9
- # SHARED BY every adapter that holds decoded Ruby records rather than
9
+ # Shared by every adapter that holds decoded Ruby records rather than
10
10
  # running real SQL (Memory, Lambda, Heki) — the identical dotted-path
11
11
  # value-object member-picking Postgres/Sqlite/D1's own
12
12
  # order_expression does (numeric member wins, else the one-field
@@ -16,10 +16,24 @@ module Hecks
16
16
  module InMemoryOrdering
17
17
  module_function
18
18
 
19
- # order_by IS A RUNTIME VALUE (an HTTP query param, in the
19
+ # Orders decoded records by a declared attribute, then by id as the total-order
20
+ # tiebreaker.
21
+ #
22
+ # order_by is a runtime value (an HTTP query param, in the
20
23
  # console's case), not framework-authored bluebook source — see
21
24
  # postgres.rb's own all for the full reasoning. Whitelisted the
22
25
  # identical way before FieldPath.dig ever runs.
26
+ #
27
+ # @param records [Array<Runtime::Instance>] the records to order
28
+ # @param aggregate [Bluebook::Aggregate] the aggregate `records` belong to, checked for
29
+ # `order_by`'s attribute
30
+ # @param order_by [String, Symbol, nil] a dotted attribute path to sort by; nil returns
31
+ # `records` unchanged
32
+ # @param direction [Symbol] `:asc` or `:desc`
33
+ # @return [Array<Runtime::Instance>] `records`, ordered by `order_by` then id; unchanged
34
+ # when `order_by` is nil
35
+ # @raise [Runtime::WiringError] if `order_by` names no attribute of `aggregate` and is
36
+ # not its lifecycle field
23
37
  def ordered(records, aggregate:, order_by:, direction:)
24
38
  return records unless order_by
25
39
 
@@ -36,6 +50,14 @@ module Hecks
36
50
  end
37
51
  end
38
52
 
53
+ # Resolves the dotted path `FieldPath.dig` should read to compare a value object field.
54
+ #
55
+ # @param aggregate [Bluebook::Aggregate] the aggregate `field` belongs to
56
+ # @param field [String, Symbol] a dotted order_by path, such as `"price"` or
57
+ # `"price.cents"`
58
+ # @return [String] `field` unchanged for an already-dotted path, the lifecycle field, or
59
+ # an attribute with no value-object type; otherwise `"<name>.<member>"` naming the
60
+ # attribute's numeric member, its sole member, or the bare `"value"` convention
39
61
  def sortable_path(aggregate, field)
40
62
  name, *path = field.to_s.split(".")
41
63
  return field.to_s unless path.empty?
@@ -46,7 +68,7 @@ module Hecks
46
68
  vo = aggregate.value_object(attribute.type)
47
69
  return field.to_s unless vo
48
70
 
49
- # Numeric member first, then the SOLE attribute whatever it is
71
+ # Numeric member first, then the sole attribute whatever it is
50
72
  # named (single-attribute value objects strictly answer `.value`
51
73
  # — the same generalization `SqlQueryBuilder#query_expression`
52
74
  # makes for the column side, kept in lockstep so Memory and SQL
@@ -0,0 +1,3 @@
1
+ Hecks.adapter "InProcessKeyVault" do
2
+ port "key_vault"
3
+ end
@@ -0,0 +1,53 @@
1
+ require "securerandom"
2
+
3
+ module Hecks
4
+ module Adapters
5
+ # The real `key_vault` fulfillment for a single running process — a
6
+ # table of key material, keyed by an opaque reference never exposed to
7
+ # a bluebook or an event. Cryptoshredding only works if the key
8
+ # genuinely leaves memory when destroyed: `destroy` deletes the Hash
9
+ # entry outright, not merely a flag, so a `fetch` afterward has
10
+ # nothing left to decrypt with.
11
+ #
12
+ # **Not crash-durable** — a process restart loses every key this
13
+ # adapter ever issued, along with every ciphertext under it. A
14
+ # deployment that needs destruction to survive a restart (or to leave
15
+ # a physical-destruction audit trail) backs this port with a real
16
+ # `KMS` or `HSM` instead; this default asks for nothing external to
17
+ # run, the same tradeoff `SecureRandomIdentity` already makes for
18
+ # identity minting.
19
+ module InProcessKeyVault
20
+ module_function
21
+
22
+ # Mints and stores a fresh symmetric key, returning only an opaque handle to it.
23
+ #
24
+ # @param subject_id [String] the data subject the key is being issued for; held
25
+ # alongside the key material for {#destroy}'s own bookkeeping, never returned
26
+ # @return [String] an opaque key reference; the key material itself never leaves this
27
+ # adapter
28
+ def issue(subject_id:)
29
+ key_reference = SecureRandom.uuid
30
+ (@keys ||= {})[key_reference] = { subject_id: subject_id, secret: SecureRandom.hex(32) }
31
+ key_reference
32
+ end
33
+
34
+ # Looks up the live key material behind a reference, for encrypting or decrypting.
35
+ #
36
+ # @param key_reference [String] the opaque reference {#issue} returned
37
+ # @return [String, nil] the key's hex-encoded secret, or nil once {#destroy} has run
38
+ def fetch(key_reference) = (@keys ||= {})[key_reference]&.fetch(:secret)
39
+
40
+ # Irrevocably deletes a key's material, so {#fetch} can never answer for it again.
41
+ #
42
+ # @param key_reference [String] the opaque reference {#issue} returned
43
+ # @return [Boolean] true when a key was held and is now gone; false when this
44
+ # reference was already destroyed, or never issued
45
+ def destroy(key_reference:) = !(@keys ||= {}).delete(key_reference).nil? # rubocop:disable Naming/PredicateMethod
46
+
47
+ # Resets the vault to empty, forgetting every key this process ever issued.
48
+ #
49
+ # @return [void]
50
+ def reset! = @keys = {}
51
+ end
52
+ end
53
+ end
@@ -3,41 +3,94 @@ require "json"
3
3
  module Hecks
4
4
  module Adapters
5
5
  class Lambda
6
- # THE THIN AWS CLIENT — shared by this adapter's own read methods
6
+ # **The thin AWS client** — shared by this adapter's own read methods
7
7
  # (Lambda#find/#all/#count/#query) and Runtime::RemoteDispatcher's
8
8
  # write methods. One Lambda invoke, one JSON round trip, nothing
9
9
  # domain-specific: neither caller needs to know an AWS SDK is
10
10
  # involved at all.
11
11
  #
12
- # FUNCTION NAME IS COMPUTED, NOT PASSED — `"hecks-#{domain}"`,
12
+ # ## Resolving the function name
13
+ #
14
+ # Function name is computed unless it is named — `"hecks-#{domain}"`,
13
15
  # lowercased, matches bin/project_deploy's own `stack_name`
14
16
  # exactly (bin/project_deploy: `stack_name = "hecks-#{domain_name}"`,
15
17
  # `domain_name = File.basename(domain)`). `domain` here is the
16
- # bluebook's OWN declared name (`Embryonaut`, not the directory);
18
+ # bluebook's own declared name (`Embryonaut`, not the directory);
17
19
  # today's real corpus has directory name == declared name
18
20
  # lowercased for every domain that deploys, so `.downcase` alone
19
21
  # reproduces the same string bin/project_deploy computes from the
20
- # directory — this is a real, load-bearing assumption, not a
21
- # coincidence to lean on silently forever if that ever stops
22
- # holding.
22
+ # directory.
23
+ #
24
+ # ## When the assumption breaks
25
+ #
26
+ # That assumption is not always true, and when it breaks nothing
27
+ # about it is recoverable from here. `bin/project_deploy` honours
28
+ # a `.world`'s own `stack_prefix`/`stack_name` — settings that
29
+ # exist precisely so a domain whose AWS identity predates a rename
30
+ # keeps targeting the stack that is actually live rather than
31
+ # standing up a second, empty one beside it. A real one:
32
+ # embryonautfoundersapp deploys as `hecksagain-embryonaut`, and no
33
+ # value of `domain` (or of `DOMAIN_NAME`, the deployed-Lambda
34
+ # override callers already pass) can make this computation produce
35
+ # a name with no dash after "hecks". Every Ruby-side read and
36
+ # dispatch for that app has therefore been invoking a function
37
+ # that does not exist — found live, and visible in the deployed
38
+ # journal having never received a single row.
39
+ #
40
+ # ## Naming it explicitly
41
+ #
42
+ # So the name can be named, in the one place the rest of this
43
+ # deployment is already described: the `.world`'s own
44
+ # `persisted_by("Lambda")`/`dispatched_by("Lambda")` block, beside
45
+ # `region`. Given, it wins outright; absent, the computation above
46
+ # is unchanged, which is every domain whose stack name was never
47
+ # pinned.
23
48
  class Client
24
- def initialize(domain:, region:)
49
+ # @param domain [String, Symbol] the bluebook's own declared domain name; computes
50
+ # the function name when `function` is not given
51
+ # @param region [String] the AWS region to invoke in
52
+ # @param function [String, Symbol, nil] an explicit Lambda function name, from a
53
+ # `.world`'s `persisted_by("Lambda")`/`dispatched_by("Lambda")` block; nil computes
54
+ # `"hecks-#{domain.downcase}"`
55
+ def initialize(domain:, region:, function: nil)
25
56
  require "aws-sdk-lambda"
26
- @function_name = "hecks-#{domain.to_s.downcase}"
57
+ @function_name = function.to_s.empty? ? "hecks-#{domain.to_s.downcase}" : function.to_s
27
58
  @client = Aws::Lambda::Client.new(region: region)
28
59
  end
29
60
 
30
- # THE WHOLE DOMAIN, EVERY TIME — matches dispatch::read's own
61
+ # The function this client actually invokes — read by
62
+ # `Runtime::WiringError` messages and worth asserting on
63
+ # directly, since "which function did we call" is otherwise
64
+ # unanswerable from outside.
65
+ attr_reader :function_name
66
+
67
+ # Reads the whole domain's current state and event log from the remote function.
68
+ #
69
+ # **The whole domain, every time** — matches dispatch::read's own
31
70
  # rehydrate-the-full-journal design (Phase 1, rust/host). No
32
- # caching here, deliberately not even per-request: Lambda#all
33
- # used to memoize this across calls, which silently served
34
- # stale reads for a warm web process's whole lifetime once a
35
- # write happened elsewhere — see Lambda#instances's own comment
36
- # on the real, live bug that caught it.
71
+ # caching here, deliberately: memoizing this across calls once
72
+ # silently served stale reads for a warm web process's whole
73
+ # lifetime after a write happened elsewhere — see Lambda#instances's
74
+ # own comment on the real, live bug that caught it.
75
+ #
76
+ # @return [Hash{String => Object}] the parsed JSON response; the deployed function's
77
+ # own top-level keys (`"instances"`, `"events"`, …)
78
+ # @raise [Runtime::WiringError] if the function reports a `functionError` (see
79
+ # `invoke`)
37
80
  def read
38
81
  invoke({ "read" => true })
39
82
  end
40
83
 
84
+ # Dispatches one command to the remote function and returns its parsed response.
85
+ #
86
+ # @param verb [String] the fully-qualified verb to dispatch
87
+ # @param args [Hash] the command's declared arguments
88
+ # @param role [String, Symbol, nil] the role to dispatch as; omitted from the payload
89
+ # when nil
90
+ # @return [Hash{String => Object}] the parsed JSON response, including a `"refusals"`
91
+ # array and a `"mutations"` array
92
+ # @raise [Runtime::WiringError] if the function reports a `functionError` (see
93
+ # `invoke`)
41
94
  def dispatch(verb, args, role: nil)
42
95
  payload = { "verb" => verb, "args" => args }
43
96
  payload["role"] = role if role