hecks 2.8.0 → 2.9.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 (404) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +4 -6
  3. data/lib/hecks/adapters/driven/claude_code.rb +4 -104
  4. data/lib/hecks/adapters/driven/d1.rb +44 -237
  5. data/lib/hecks/adapters/driven/folder.rb +17 -76
  6. data/lib/hecks/adapters/driven/google_authentication.rb +15 -49
  7. data/lib/hecks/adapters/driven/governance_authorization.rb +23 -70
  8. data/lib/hecks/adapters/driven/heki/journal.rb +8 -41
  9. data/lib/hecks/adapters/driven/heki/saga_store.rb +12 -54
  10. data/lib/hecks/adapters/driven/heki/snapshot.rb +4 -12
  11. data/lib/hecks/adapters/driven/heki.rb +13 -42
  12. data/lib/hecks/adapters/driven/identity_registry.rb +3 -14
  13. data/lib/hecks/adapters/driven/in_memory_ordering.rb +9 -26
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +2 -14
  15. data/lib/hecks/adapters/driven/lambda/client.rb +10 -51
  16. data/lib/hecks/adapters/driven/lambda.rb +10 -84
  17. data/lib/hecks/adapters/driven/local_storage.rb +5 -40
  18. data/lib/hecks/adapters/driven/memory.rb +17 -98
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +5 -21
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +6 -28
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +3 -12
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +4 -30
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +34 -108
  24. data/lib/hecks/adapters/driven/postgres.rb +145 -134
  25. data/lib/hecks/adapters/driven/postgres_era.adapter +4 -15
  26. data/lib/hecks/adapters/driven/prism.rb +7 -30
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +21 -121
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +8 -40
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +26 -82
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +40 -84
  31. data/lib/hecks/adapters/driven/sqlite.rb +128 -65
  32. data/lib/hecks/adapters/driven/tenant_provisioner.rb +5 -48
  33. data/lib/hecks/adapters/driven.rb +5 -36
  34. data/lib/hecks/adapters/driving/github_webhook.rb +8 -76
  35. data/lib/hecks/behaviors/dsl.rb +7 -25
  36. data/lib/hecks/behaviors/expectations.rb +31 -141
  37. data/lib/hecks/behaviors/ir.rb +7 -20
  38. data/lib/hecks/behaviors/rspec.rb +6 -17
  39. data/lib/hecks/behaviors/runner.rb +10 -34
  40. data/lib/hecks/behaviors.rb +4 -8
  41. data/lib/hecks/bluebook/aggregate.rb +9 -75
  42. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +9 -38
  43. data/lib/hecks/bluebook/assembly/build.rb +4 -15
  44. data/lib/hecks/bluebook/assembly/contract.rb +15 -136
  45. data/lib/hecks/bluebook/assembly/contracts.rb +54 -174
  46. data/lib/hecks/bluebook/assembly/marks.rb +31 -223
  47. data/lib/hecks/bluebook/assembly/specializer.rb +6 -52
  48. data/lib/hecks/bluebook/assembly.rb +6 -45
  49. data/lib/hecks/bluebook/attribute.rb +12 -61
  50. data/lib/hecks/bluebook/behaviour/aggregate.rb +10 -39
  51. data/lib/hecks/bluebook/behaviour/attribute.rb +3 -14
  52. data/lib/hecks/bluebook/behaviour/chapter.rb +14 -42
  53. data/lib/hecks/bluebook/behaviour/command.rb +14 -66
  54. data/lib/hecks/bluebook/behaviour/domain_port.rb +4 -20
  55. data/lib/hecks/bluebook/behaviour/entity.rb +7 -27
  56. data/lib/hecks/bluebook/behaviour/hexagon.rb +23 -41
  57. data/lib/hecks/bluebook/behaviour/lifecycle.rb +15 -40
  58. data/lib/hecks/bluebook/behaviour/policy.rb +23 -52
  59. data/lib/hecks/bluebook/behaviour/process_manager.rb +30 -57
  60. data/lib/hecks/bluebook/behaviour/read_model.rb +25 -31
  61. data/lib/hecks/bluebook/behaviour/traits.rb +27 -64
  62. data/lib/hecks/bluebook/behaviour/value_object.rb +5 -24
  63. data/lib/hecks/bluebook/capabilities.rb +17 -49
  64. data/lib/hecks/bluebook/chapter.rb +10 -48
  65. data/lib/hecks/bluebook/command.rb +23 -81
  66. data/lib/hecks/bluebook/domain_port.rb +14 -63
  67. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +50 -229
  68. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +50 -377
  69. data/lib/hecks/bluebook/dsl/attribute_collector.rb +33 -204
  70. data/lib/hecks/bluebook/dsl/binding_proxy.rb +16 -69
  71. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +86 -708
  72. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +56 -170
  73. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
  74. data/lib/hecks/bluebook/dsl/command_builder.rb +74 -546
  75. data/lib/hecks/bluebook/dsl/const_shim.rb +8 -41
  76. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +12 -87
  77. data/lib/hecks/bluebook/dsl/entity_builder.rb +44 -330
  78. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +20 -269
  79. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +26 -194
  80. data/lib/hecks/bluebook/dsl/identity_declaration.rb +13 -88
  81. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +6 -20
  82. data/lib/hecks/bluebook/dsl/policy_builder.rb +16 -88
  83. data/lib/hecks/bluebook/dsl/port_builder.rb +2 -6
  84. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +22 -96
  85. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +31 -184
  86. data/lib/hecks/bluebook/dsl/query_builder.rb +11 -42
  87. data/lib/hecks/bluebook/dsl/read_model_builder.rb +75 -201
  88. data/lib/hecks/bluebook/dsl/rule_reference.rb +22 -163
  89. data/lib/hecks/bluebook/dsl/translation_builder.rb +28 -114
  90. data/lib/hecks/bluebook/dsl/value_object_builder.rb +10 -124
  91. data/lib/hecks/bluebook/dsl/word_gate.rb +16 -162
  92. data/lib/hecks/bluebook/dsl/world_builder.rb +19 -111
  93. data/lib/hecks/bluebook/entity.rb +13 -71
  94. data/lib/hecks/bluebook/expression/ast_json.rb +24 -190
  95. data/lib/hecks/bluebook/expression/ast_reader.rb +12 -54
  96. data/lib/hecks/bluebook/expression/canonical_form.rb +11 -88
  97. data/lib/hecks/bluebook/expression/evaluator.rb +47 -295
  98. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +30 -147
  99. data/lib/hecks/bluebook/expression/resolver.rb +37 -660
  100. data/lib/hecks/bluebook/hexagon.rb +5 -28
  101. data/lib/hecks/bluebook/lifecycle.rb +5 -16
  102. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -7
  103. data/lib/hecks/bluebook/meta_validator/judge.rb +111 -361
  104. data/lib/hecks/bluebook/meta_validator/plan.rb +33 -162
  105. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -7
  106. data/lib/hecks/bluebook/meta_validator/readings.rb +56 -342
  107. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -176
  108. data/lib/hecks/bluebook/meta_validator/shapes.rb +53 -267
  109. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +43 -305
  110. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +7 -43
  111. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -18
  112. data/lib/hecks/bluebook/meta_validator.rb +79 -349
  113. data/lib/hecks/bluebook/model_check/client_profile.rb +6 -21
  114. data/lib/hecks/bluebook/model_check.rb +100 -597
  115. data/lib/hecks/bluebook/pattern_subset.rb +11 -69
  116. data/lib/hecks/bluebook/process_manager.rb +13 -83
  117. data/lib/hecks/bluebook/project_loader.rb +3 -39
  118. data/lib/hecks/bluebook/project_register.rb +12 -60
  119. data/lib/hecks/bluebook/query.rb +9 -48
  120. data/lib/hecks/bluebook/read_model.rb +57 -81
  121. data/lib/hecks/bluebook/reference.rb +7 -41
  122. data/lib/hecks/bluebook/smoke_test.rb +8 -98
  123. data/lib/hecks/bluebook/synthesizer.rb +13 -62
  124. data/lib/hecks/bluebook/translation.rb +26 -62
  125. data/lib/hecks/bluebook/value_object.rb +11 -36
  126. data/lib/hecks/bluebook.rb +5 -22
  127. data/lib/hecks/cache_dir.rb +4 -23
  128. data/lib/hecks/cli/document.rb +3 -21
  129. data/lib/hecks/cli/ir.rb +3 -5
  130. data/lib/hecks/cli/mcp.rb +2 -3
  131. data/lib/hecks/cli/mcp_door.rb +19 -114
  132. data/lib/hecks/cli/model_check.rb +18 -85
  133. data/lib/hecks/cli/project_cli.rb +7 -40
  134. data/lib/hecks/cli/project_diagrams.rb +0 -7
  135. data/lib/hecks/cli/run.rb +12 -58
  136. data/lib/hecks/cli/smoke_test.rb +2 -16
  137. data/lib/hecks/cli/stores.rb +1 -2
  138. data/lib/hecks/cli.rb +18 -24
  139. data/lib/hecks/construct.rb +8 -34
  140. data/lib/hecks/deploy/bluebook/deploy.bluebook +33 -142
  141. data/lib/hecks/deploy/bluebook/deploy.hecksagon +2 -9
  142. data/lib/hecks/embryonaut_bluebook/lock.rb +3 -35
  143. data/lib/hecks/embryonaut_bluebook/shape.rb +1 -17
  144. data/lib/hecks/embryonaut_bluebook/vendor.rb +8 -53
  145. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +4 -19
  146. data/lib/hecks/embryonaut_bluebook.rb +4 -71
  147. data/lib/hecks/facade/cli_door.rb +24 -106
  148. data/lib/hecks/facade/cli_runner.rb +28 -172
  149. data/lib/hecks/facade/command_request.rb +13 -33
  150. data/lib/hecks/facade/handle.rb +34 -121
  151. data/lib/hecks/facade/json_door.rb +31 -146
  152. data/lib/hecks/facade/surface/aggregate_door.rb +39 -104
  153. data/lib/hecks/facade/surface/chapter.rb +10 -54
  154. data/lib/hecks/facade/surface.rb +8 -23
  155. data/lib/hecks/facade.rb +10 -25
  156. data/lib/hecks/forms/app.rb +15 -70
  157. data/lib/hecks/forms/command_form_renderer.rb +10 -29
  158. data/lib/hecks/forms/field_renderer.rb +17 -156
  159. data/lib/hecks/forms/field_shape.rb +30 -222
  160. data/lib/hecks/forms/html.rb +12 -47
  161. data/lib/hecks/forms/index_renderer.rb +1 -4
  162. data/lib/hecks/forms/page.rb +3 -9
  163. data/lib/hecks/forms/params.rb +14 -60
  164. data/lib/hecks/forms/port_argument.rb +2 -19
  165. data/lib/hecks/forms/query_form_renderer.rb +4 -79
  166. data/lib/hecks/forms/record_renderer.rb +8 -75
  167. data/lib/hecks/forms/record_table.rb +7 -50
  168. data/lib/hecks/forms/reference_options.rb +3 -30
  169. data/lib/hecks/forms/value_object_shape.rb +8 -35
  170. data/lib/hecks/forms.rb +5 -33
  171. data/lib/hecks/fqn.rb +5 -12
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +16 -65
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +47 -134
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +5 -14
  175. data/lib/hecks/framework/bluebook/governance.bluebook +13 -34
  176. data/lib/hecks/framework/bluebook/identity.bluebook +11 -26
  177. data/lib/hecks/framework/bluebook/privacy.bluebook +8 -33
  178. data/lib/hecks/framework.rb +11 -80
  179. data/lib/hecks/freezer.rb +5 -39
  180. data/lib/hecks/grammar/expression.bluebook +15 -40
  181. data/lib/hecks/grammar/translation.bluebook +3 -6
  182. data/lib/hecks/grammar.rb +16 -41
  183. data/lib/hecks/ir.rb +14 -83
  184. data/lib/hecks/language/adapter.bluebook +2 -7
  185. data/lib/hecks/language/bluebook/aggregate.bluebook +56 -221
  186. data/lib/hecks/language/bluebook/attaches/paging.bluebook +6 -15
  187. data/lib/hecks/language/bluebook/bluebook.bluebook +15 -83
  188. data/lib/hecks/language/bluebook/bluebook.hecksagon +3 -15
  189. data/lib/hecks/language/bluebook/command.bluebook +27 -144
  190. data/lib/hecks/language/bluebook/entity.bluebook +24 -97
  191. data/lib/hecks/language/bluebook/policy.bluebook +16 -67
  192. data/lib/hecks/language/bluebook/process_manager.bluebook +28 -151
  193. data/lib/hecks/language/bluebook/projection.bluebook +126 -60
  194. data/lib/hecks/language/bluebook/query.bluebook +12 -43
  195. data/lib/hecks/language/bluebook/shape.bluebook +23 -73
  196. data/lib/hecks/language/bluebook/syntax.bluebook +51 -262
  197. data/lib/hecks/language/bluebook/vocabulary.bluebook +82 -197
  198. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +3 -10
  199. data/lib/hecks/language/hecksagon/domain_port.bluebook +4 -19
  200. data/lib/hecks/language/hecksagon/hecksagon.bluebook +6 -25
  201. data/lib/hecks/language/hecksagon/port_operation.bluebook +3 -14
  202. data/lib/hecks/language/oidc.json +35 -0
  203. data/lib/hecks/language/port.bluebook +4 -17
  204. data/lib/hecks/language/translation/translation.bluebook +3 -9
  205. data/lib/hecks/language/translation/translation_aggregate.bluebook +5 -24
  206. data/lib/hecks/language/world/wiring.bluebook +1 -3
  207. data/lib/hecks/language/world/world.bluebook +5 -7
  208. data/lib/hecks/literal.rb +8 -50
  209. data/lib/hecks/mcp_door_scope.rb +9 -90
  210. data/lib/hecks/mcp_stdio_guard.rb +4 -41
  211. data/lib/hecks/naming.rb +29 -137
  212. data/lib/hecks/ports/access_control.rb +7 -23
  213. data/lib/hecks/ports/agent/answers.rb +5 -23
  214. data/lib/hecks/ports/agent.rb +47 -172
  215. data/lib/hecks/ports/authentication.rb +12 -39
  216. data/lib/hecks/ports/authorization.rb +16 -43
  217. data/lib/hecks/ports/clock.rb +5 -45
  218. data/lib/hecks/ports/extraction.rb +7 -12
  219. data/lib/hecks/ports/identity_assignment.rb +8 -29
  220. data/lib/hecks/ports/identity_generation.rb +6 -29
  221. data/lib/hecks/ports/identity_resolution.rb +2 -10
  222. data/lib/hecks/ports/key_vault.rb +9 -45
  223. data/lib/hecks/ports/persistence/append_only.rb +61 -64
  224. data/lib/hecks/ports/persistence/binding_policy.rb +4 -14
  225. data/lib/hecks/ports/persistence/codec_boundary.rb +20 -80
  226. data/lib/hecks/ports/persistence/null_saga_store.rb +4 -20
  227. data/lib/hecks/ports/persistence/plugin.rb +13 -42
  228. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +38 -141
  229. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +15 -115
  230. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +16 -84
  231. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +6 -25
  232. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -22
  233. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +55 -181
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/events.rb +77 -0
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +12 -122
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +28 -128
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +73 -474
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +31 -124
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +45 -219
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +13 -108
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +18 -75
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +9 -49
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +6 -123
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +23 -55
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +14 -72
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +7 -16
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +14 -115
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +13 -45
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +127 -554
  250. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +13 -63
  251. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +2 -17
  252. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +3 -14
  253. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +13 -58
  254. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -17
  255. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +9 -26
  256. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +9 -25
  257. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -124
  258. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +34 -123
  259. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +5 -12
  260. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +3 -9
  261. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -14
  262. data/lib/hecks/ports/persistence/plugins/era.rb +4 -23
  263. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +7 -23
  264. data/lib/hecks/ports/persistence/postgres_dump.rb +4 -35
  265. data/lib/hecks/ports/persistence/remote_runtime.rb +2 -24
  266. data/lib/hecks/ports/persistence/repository_factory.rb +8 -27
  267. data/lib/hecks/ports/persistence/state_codec.rb +23 -103
  268. data/lib/hecks/ports/persistence.rb +7 -14
  269. data/lib/hecks/ports/projection.rb +42 -84
  270. data/lib/hecks/ports/query/in_memory.rb +10 -58
  271. data/lib/hecks/ports/query/ordering.rb +11 -32
  272. data/lib/hecks/ports/query.rb +14 -37
  273. data/lib/hecks/ports.rb +2 -8
  274. data/lib/hecks/projections/bootstrap_table.rb +11 -43
  275. data/lib/hecks/projections/deploy/fargate/assembly.rb +2 -15
  276. data/lib/hecks/projections/deploy/fargate/cdn.rb +1 -48
  277. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +2 -11
  278. data/lib/hecks/projections/deploy/fargate/check.rb +5 -14
  279. data/lib/hecks/projections/deploy/fargate/containers.rb +3 -78
  280. data/lib/hecks/projections/deploy/fargate/extras.rb +3 -44
  281. data/lib/hecks/projections/deploy/fargate/monitoring.rb +0 -44
  282. data/lib/hecks/projections/deploy/fargate/settings.rb +2 -54
  283. data/lib/hecks/projections/deploy/fargate/yaml.rb +13 -58
  284. data/lib/hecks/projections/deploy/fargate.rb +35 -202
  285. data/lib/hecks/projections/deploy/lambda.rb +150 -678
  286. data/lib/hecks/projections/deploy/preview/access_yaml.rb +1 -33
  287. data/lib/hecks/projections/deploy/preview/containers.rb +6 -111
  288. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +4 -56
  289. data/lib/hecks/projections/deploy/preview/script.rb +2 -14
  290. data/lib/hecks/projections/deploy/preview/settings.rb +3 -33
  291. data/lib/hecks/projections/deploy/preview/task_yaml.rb +2 -38
  292. data/lib/hecks/projections/deploy/preview/template.rb +2 -58
  293. data/lib/hecks/projections/deploy/preview.rb +2 -67
  294. data/lib/hecks/projections/deploy/scripts/settings.rb +3 -25
  295. data/lib/hecks/projections/deploy/scripts.rb +4 -57
  296. data/lib/hecks/projections/deploy/shared.rb +47 -146
  297. data/lib/hecks/projections/deploy/smoke.rb +4 -61
  298. data/lib/hecks/projections/deploy/template_diff/comparer.rb +3 -25
  299. data/lib/hecks/projections/deploy/template_diff/loader.rb +2 -11
  300. data/lib/hecks/projections/deploy/template_diff.rb +5 -57
  301. data/lib/hecks/projections/diagrams.rb +20 -582
  302. data/lib/hecks/projections/glossary/html.rb +14 -130
  303. data/lib/hecks/projections/glossary/markdown.rb +8 -69
  304. data/lib/hecks/projections/glossary/mermaid.rb +9 -66
  305. data/lib/hecks/projections/glossary/sensitivity.rb +5 -34
  306. data/lib/hecks/projections/glossary/sentences.rb +14 -159
  307. data/lib/hecks/projections/glossary.rb +30 -222
  308. data/lib/hecks/projections/ir.rb +3 -10
  309. data/lib/hecks/projections/model/deviations.rb +15 -42
  310. data/lib/hecks/projections/model.rb +12 -51
  311. data/lib/hecks/projections/oidc.rb +12 -56
  312. data/lib/hecks/projections/parser_table.rb +9 -28
  313. data/lib/hecks/projections/reference.rb +2 -15
  314. data/lib/hecks/projections/rust_vocabulary.rb +12 -250
  315. data/lib/hecks/projections/shape.rb +3 -29
  316. data/lib/hecks/projections/statements.rb +7 -86
  317. data/lib/hecks/projections/vocabulary.rb +6 -22
  318. data/lib/hecks/projections.rb +2 -16
  319. data/lib/hecks/projector/cli_projector.rb +60 -375
  320. data/lib/hecks/projector/docs_projector.rb +10 -201
  321. data/lib/hecks/projector/exporter.rb +42 -134
  322. data/lib/hecks/projector/ir_projector.rb +2 -9
  323. data/lib/hecks/projector/narrate_projector.rb +15 -189
  324. data/lib/hecks/projector/target.rb +13 -76
  325. data/lib/hecks/projector.rb +30 -121
  326. data/lib/hecks/query_specification/common/comparators.rb +5 -26
  327. data/lib/hecks/query_specification/common/comparison.rb +35 -143
  328. data/lib/hecks/query_specification/common/dsl.rb +8 -24
  329. data/lib/hecks/query_specification/common/null_policy.rb +20 -70
  330. data/lib/hecks/query_specification/common/options.rb +2 -6
  331. data/lib/hecks/query_specification/common/where_clause.rb +2 -10
  332. data/lib/hecks/query_specification/field_path.rb +37 -77
  333. data/lib/hecks/query_specification/hop_path.rb +12 -76
  334. data/lib/hecks/release/gem_pin.rb +2 -26
  335. data/lib/hecks/release/runner/ci_publisher.rb +3 -6
  336. data/lib/hecks/release/runner/commands.rb +2 -4
  337. data/lib/hecks/release/runner/gem_publisher.rb +2 -3
  338. data/lib/hecks/release/runner/npm_publisher.rb +6 -20
  339. data/lib/hecks/release/runner/preflight.rb +4 -19
  340. data/lib/hecks/release/runner/tagger.rb +2 -9
  341. data/lib/hecks/release/runner.rb +3 -32
  342. data/lib/hecks/rendering.rb +4 -26
  343. data/lib/hecks/router/namespace_installer.rb +12 -36
  344. data/lib/hecks/router.rb +4 -8
  345. data/lib/hecks/runtime/aggregate_lock.rb +8 -33
  346. data/lib/hecks/runtime/boot_gates.rb +3 -18
  347. data/lib/hecks/runtime/caller.rb +19 -59
  348. data/lib/hecks/runtime/capability_graph.rb +5 -21
  349. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +16 -78
  350. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +23 -160
  351. data/lib/hecks/runtime/command_interpreter.rb +124 -306
  352. data/lib/hecks/runtime/command_rules/admissibility.rb +95 -366
  353. data/lib/hecks/runtime/command_rules/arithmetic.rb +66 -196
  354. data/lib/hecks/runtime/command_rules/authorization.rb +13 -45
  355. data/lib/hecks/runtime/command_rules/emission.rb +8 -21
  356. data/lib/hecks/runtime/command_rules/references.rb +23 -202
  357. data/lib/hecks/runtime/dependency_planning.rb +30 -102
  358. data/lib/hecks/runtime/dispatcher.rb +82 -241
  359. data/lib/hecks/runtime/entity_element.rb +40 -462
  360. data/lib/hecks/runtime/entity_interpreter.rb +64 -219
  361. data/lib/hecks/runtime/errors.rb +32 -115
  362. data/lib/hecks/runtime/event.rb +3 -25
  363. data/lib/hecks/runtime/identity.rb +18 -154
  364. data/lib/hecks/runtime/instance.rb +18 -150
  365. data/lib/hecks/runtime/interpreting.rb +11 -70
  366. data/lib/hecks/runtime/invocation.rb +28 -164
  367. data/lib/hecks/runtime/loader.rb +48 -186
  368. data/lib/hecks/runtime/outbox.rb +38 -261
  369. data/lib/hecks/runtime/policy_interpreter.rb +38 -200
  370. data/lib/hecks/runtime/port_operation_interpreter.rb +26 -87
  371. data/lib/hecks/runtime/query_interpreter.rb +45 -187
  372. data/lib/hecks/runtime/reaction_invocation.rb +36 -138
  373. data/lib/hecks/runtime/read_model_interpreter.rb +117 -208
  374. data/lib/hecks/runtime/rebuild_sweep.rb +8 -54
  375. data/lib/hecks/runtime/reference_hop.rb +8 -81
  376. data/lib/hecks/runtime/refusal_wording.rb +8 -89
  377. data/lib/hecks/runtime/registry/saga_persistence.rb +10 -102
  378. data/lib/hecks/runtime/registry/verification.rb +52 -225
  379. data/lib/hecks/runtime/registry/world_defaults.rb +4 -53
  380. data/lib/hecks/runtime/registry.rb +99 -244
  381. data/lib/hecks/runtime/remote_dispatcher.rb +37 -152
  382. data/lib/hecks/runtime/routing.rb +4 -15
  383. data/lib/hecks/runtime/saga_interpreter/correlation.rb +14 -61
  384. data/lib/hecks/runtime/saga_interpreter.rb +79 -263
  385. data/lib/hecks/runtime/saga_pending_dispatch.rb +2 -43
  386. data/lib/hecks/runtime/tenant_check.rb +6 -68
  387. data/lib/hecks/runtime/tenant_scope.rb +3 -28
  388. data/lib/hecks/runtime/value/admission.rb +16 -67
  389. data/lib/hecks/runtime/value/coercion.rb +155 -641
  390. data/lib/hecks/runtime/value/entity_list_coercion.rb +26 -219
  391. data/lib/hecks/runtime/value.rb +12 -84
  392. data/lib/hecks/runtime.rb +17 -65
  393. data/lib/hecks/storehouse.rb +141 -568
  394. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +8 -32
  395. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +6 -23
  396. data/lib/hecks/vendoring/git_environment.rb +2 -8
  397. data/lib/hecks/vendoring/git_source.rb +4 -10
  398. data/lib/hecks/vendoring.rb +7 -46
  399. data/lib/hecks/version.rb +3 -14
  400. data/lib/hecks.rb +26 -81
  401. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -46
  402. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +11 -80
  403. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +10 -99
  404. metadata +3 -2
@@ -2,13 +2,8 @@ require_relative "../../vocabulary"
2
2
 
3
3
  module Hecks
4
4
  module Adapters
5
- # The filesystem-backed loader — glob-and-`Kernel.load` a domain's own
6
- # `.port`/`.adapter`/`.bluebook`/`.hecksagon`/`.world`/environment-overlay
7
- # files off disk, in the order `Vocabulary.fetch("LoadOrder")` requires,
8
- # and locate a domain's own root directory by walking up from any file
9
- # inside it looking for a `.hecksagon`. What `Hecks.boot(path)` uses when
10
- # a caller names a directory rather than an explicit file list (see
11
- # `Loader.boot_files` for that other path).
5
+ # Loads a domain's files off disk in `Vocabulary.fetch("LoadOrder")` order and finds
6
+ # a domain's root directory. Used by `Hecks.boot(path)` when given a directory.
12
7
  class Folder
13
8
  DOMAIN_ORDER = Hecks::Vocabulary.fetch("LoadOrder")
14
9
  PORTS = "ports".freeze
@@ -41,34 +36,12 @@ module Hecks
41
36
  load_each(File.join(root, ADAPTERS), %w[*.adapter */*.adapter */*/*.adapter])
42
37
  end
43
38
 
44
- # Chapters first, judged once, then everything that reads them.
45
- # A chapter may be split across files (the language's own grammar is
46
- # nine), and judging file one before files two-through-nine exist
47
- # refuses references that are perfectly well declared a file later —
48
- # see MetaValidator.defer for the whole reasoning. DOMAIN_ORDER
49
- # already places every `*.bluebook` ahead of hecksagons and worlds,
50
- # so the window ends at the last chapter pattern rather than at a
51
- # hand-written list this would otherwise have to keep in step.
39
+ # Loads bluebook chapters first, judged once as a deferred group, then everything
40
+ # else in `DOMAIN_ORDER`. Deferring keeps a chapter split across files from being
41
+ # judged before its later files exist (see MetaValidator.defer).
52
42
  #
53
- # `environment:` — one more pair of files, loaded last, not a glob.
54
- # Recovered, not new — see Runtime::Loader.boot's own comment for
55
- # the provenance. A caller passing `Hecks.boot(path, environment:
56
- # "production")` gets exactly `environments/production.hecksagon`
57
- # and `environments/production.world` loaded, whichever exist (a
58
- # missing one is a silent no-op — not every environment overrides
59
- # both; see docs/implemented/guides/wiring.md's "Swapping wiring per
60
- # environment" for the motivating case: swapping a driven adapter
61
- # — e.g. a real payment gateway for a mock one, or a hosting
62
- # layer's tenancy settings — without an `if`/`else` anywhere in
63
- # the domain's own wiring). Never matched by `DOMAIN_ORDER`'s own
64
- # globs (all non-recursive, none named `environments/*`), so this
65
- # is the only thing that ever reaches them. Loaded as genuine
66
- # `Hecks.hecksagon "SameDomain" do ... end` / `Hecks.world
67
- # "SameDomain" do ... end` blocks — merged into the base file's
68
- # own hecksagon/world (Registry#add_hecksagon / #add_world,
69
- # concatenate/override rather than replace), so an overlay can
70
- # rebind or add settings for anything the base file declared
71
- # without needing to know what else the base file said.
43
+ # An `environment:` overlay (`environments/<name>.hecksagon` and `.world`) loads
44
+ # last and merges into the base hecksagon/world; a missing overlay file is skipped.
72
45
  #
73
46
  # @param directory [String] the domain's bluebook directory to load, as
74
47
  # `bluebook_directory` resolves it
@@ -90,12 +63,8 @@ module Hecks
90
63
  load_each(directory, [File.join("environments", "#{environment}.world")])
91
64
  end
92
65
 
93
- # Every bluebook in a folder is one declaration set. Individual files
94
- # remain organized in the domain expert's language; the folder is the
95
- # unit callers load. Builders group declarations by the chapter name in
96
- # each file, so a folder may hold more than one chapter without a catalog.
97
- # Sorting makes source order deterministic while the deferred window keeps
98
- # cross-file references from being judged against a partial chapter.
66
+ # Loads every bluebook in a folder as one declaration set, judged after all are loaded
67
+ # so cross-file references never meet a partial chapter.
99
68
  #
100
69
  # @param directory [String] the directory to glob for bluebook chapter files
101
70
  # @param patterns [Array<String>] glob patterns, relative to `directory`, selecting the
@@ -106,23 +75,11 @@ module Hecks
106
75
  Bluebook::MetaValidator.judge_deferred!(Hecks.current_registry)
107
76
  end
108
77
 
109
- # The explicit-file sibling of `load_domain` — for a caller that names
110
- # its own exact files rather than a directory to glob (`Loader.boot_files`,
111
- # behind `Hecks.boot_files`). No `Dir.glob`, no copying: every path here
112
- # is a real file on disk, wherever it actually lives, loaded in place —
113
- # a `.behaviors` file's `loads` scopes a boot this way specifically so a
114
- # per-test boot never has to fake isolation by staging bluebooks into a
115
- # tmpdir (see Loader.boot_files's own header for why that pattern is a
116
- # hazard, not a convenience).
78
+ # Loads exactly the named files in place, without globbing (see `Loader.boot_files`).
79
+ #
80
+ # Files load by category, not in the caller's order: bluebooks (judged as one deferred
81
+ # group), then hecksagons, then worlds, since a hecksagon may reference bluebook constants.
117
82
  #
118
- # Ordered by category, not by the caller's own list order — same four
119
- # groups `Vocabulary.fetch("LoadOrder")` walks a directory in
120
- # (bluebook chapters, translations, hecksagons, worlds), because a
121
- # hecksagon can reference a bluebook's own constants and must not load
122
- # first regardless of which order a caller happened to write `loads
123
- # "x.hecksagon", "x.bluebook"` in. Bluebook chapters are judged as one
124
- # deferred group exactly like `load_domain` does, for the identical
125
- # forward-reference reason (MetaValidator.defer's own header).
126
83
  # @param files [Array<String>] file paths to load, relative to `bluebook_directory`
127
84
  # @param environment [String, nil] the environment name whose overlay, if present,
128
85
  # loads after the selected files
@@ -181,27 +138,11 @@ module Hecks
181
138
 
182
139
  # Finds the nearest domain directory at or above `from`, or nil if there is not one.
183
140
  #
184
- # **The domain you are standing in**. Walks up from `from` — the way git
185
- # finds `.git` — and answers the nearest directory a boot would accept,
186
- # or nil if there is not one above you.
187
- #
188
- # Marked by a `.hecksagon`, not by a `.bluebook`. Chapters are
189
- # everywhere: era translations, the language's own self-hosted grammar,
190
- # and `spec/fixtures`, which holds a dozen unrelated ones in a single
191
- # directory. A `.hecksagon` is the file that says "this is a domain, and
192
- # here is how its aggregates are stored", which is exactly the claim a
193
- # caller is relying on when they omit the path.
141
+ # Walks up from `from`, like git finding `.git`. A domain is marked by a `.hecksagon`,
142
+ # not a `.bluebook`, since chapters also appear in translations and fixtures.
194
143
  #
195
- # Both layouts, because `bluebook_directory` above accepts both: a
196
- # domain directory holding a `bluebook/` subdirectory (every example in
197
- # this corpus), or one holding the files directly.
198
- # Normalised to the outer directory. Standing in `examples/banking/bluebook`,
199
- # the `.hecksagon` is right there, so a plain walk stops on the
200
- # `bluebook/` directory itself. Both boot identically — `bluebook_directory`
201
- # accepts either and `Loader.boot` takes `File.dirname` of what it gets,
202
- # so the registry root comes out the same — but `examples/banking` is the
203
- # directory a person names, and the one a `.world`'s `dir "data"` reads
204
- # as relative to.
144
+ # A `bluebook/` subdirectory result is normalised to its parent, the directory
145
+ # a `.world`'s `dir "data"` is relative to.
205
146
  #
206
147
  # @param from [String] the directory to walk up from; defaults to the process's current
207
148
  # working directory
@@ -4,31 +4,14 @@ require_relative "../../ports/authentication"
4
4
 
5
5
  module Hecks
6
6
  module Adapters
7
- # **Google's own OIDC handshake** — the `authentication` port's one real
8
- # implementation today. Consolidates what was hand-rolled per-app
9
- # (a console app's `google_auth.rb` did this on its own) into
10
- # one adapter, so any hecks-based app gets Google sign-in for free, the
11
- # same "one adapter, reusable everywhere" value every other adapter
12
- # in this directory already has.
7
+ # Google's OIDC handshake — the `authentication` port's adapter for Google
8
+ # sign-in, via `oauth2` (code exchange) and `google-id-token` (verification).
13
9
  #
14
- # `oauth2` does only the authorization-code exchange (no Rack
15
- # middleware, no Omniauth strategy indirection) ; `google-id-token`
16
- # does only ID-token verification (signature checked against
17
- # Google's real, rotating JWKS — real, maintained code, never
18
- # hand-rolled here). Neither library decides what a verified
19
- # (issuer, subject) means — that's `Ports::IdentityResolution`'s
20
- # job, called by whoever consumes this port's `verify`.
10
+ # `oauth2`/`google-id-token` load lazily, inside the methods that use
11
+ # them, so a domain that never binds this adapter never needs them installed.
21
12
  #
22
- # Lazy requires, same reasoning `Postgres.connect_for` already
23
- # holds itself to for `pg`: a domain that never binds
24
- # `authentication` to this adapter should never need these gems
25
- # installed. This file loads in every boot (driven.rb's own
26
- # unconditional require_relative list) ; the gems load only where
27
- # a real handshake actually happens.
28
- #
29
- # `GOOGLE_CLIENT_ID`/`GOOGLE_CLIENT_SECRET`/`GOOGLE_REDIRECT_URI` —
30
- # no defaults, on purpose: a login mechanism silently
31
- # half-configured is worse than one that refuses to boot at all.
13
+ # `GOOGLE_CLIENT_ID`/`GOOGLE_CLIENT_SECRET`/`GOOGLE_REDIRECT_URI` have no
14
+ # defaults, on purpose: a half-configured login should refuse to boot.
32
15
  module GoogleAuthentication
33
16
  ISSUER = "https://accounts.google.com".freeze
34
17
 
@@ -48,9 +31,8 @@ module Hecks
48
31
  )
49
32
  end
50
33
 
51
- # The URL to send a browser to, carrying a fresh CSRF `state` the
52
- # caller is responsible for stashing (a session, typically) and
53
- # checking again in `verify`.
34
+ # The URL to send a browser to, carrying a fresh CSRF `state` the caller
35
+ # must stash (typically in a session) and check again in `verify`.
54
36
  #
55
37
  # @return [Array(String, String)] the URL to send the browser to, and the fresh CSRF
56
38
  # `state` embedded in it
@@ -66,38 +48,22 @@ module Hecks
66
48
  [url, state]
67
49
  end
68
50
 
69
- # Exchanges `code` for tokens, verifies the returned ID token's
70
- # signature and claims against Google's own JWKS, and confirms
71
- # `state` matches what `authorization_url` handed out — a
72
- # mismatch (or any failure in the exchange/verification itself)
73
- # is a Ports::Authentication::ValidationError, never a
74
- # silently-empty result. Returns
75
- # {issuer:, subject:, email:, email_verified:} on success — never
76
- # the raw token, never anything a caller would need to re-verify
77
- # itself. `email_verified` rides along because a caller granting
78
- # access off this email needs to know Google actually checked it.
79
- #
51
+ # Exchanges `code` for tokens and verifies the ID token against Google's
52
+ # JWKS; `email_verified` tells a caller if Google actually checked the email.
80
53
  # @param code [String] the authorization code the provider sent back
81
54
  # @param state [String, nil] the state parameter the provider returned; nil is refused
82
55
  # @param expected_state [String, nil] the state `authorization_url` handed out before
83
56
  # the redirect; nil is refused
84
57
  # @return [Hash{Symbol => Object}] `issuer:` and `subject:` (String), `email:` (String,
85
58
  # or nil if the token carries none), `email_verified:` (Boolean)
86
- # @raise [Ports::Authentication::ValidationError] if either state is nil or the two
87
- # differ, the code exchange fails, the response has no ID token, or the ID token does
88
- # not verify
59
+ # @raise [Ports::Authentication::ValidationError] if the state check, code exchange, or
60
+ # ID token verification fails
89
61
  # @raise [KeyError] if `GOOGLE_CLIENT_ID` or `GOOGLE_REDIRECT_URI` is unset, or a
90
62
  # verified token lacks an `iss` or `sub` claim
91
63
  def verify(code:, state:, expected_state:)
92
- # **Both gems, before anything else** — not staggered further down
93
- # this method: the rescue clause below names GoogleIDToken
94
- # ::ValidationError, and Ruby resolves that constant reference
95
- # at the moment an exception is being matched, not at parse
96
- # time. An OAuth2::Error raised before a later `require
97
- # "google-id-token"` line ran would make the rescue clause
98
- # itself blow up on an undefined constant, masking the real
99
- # error. Requiring both up front means whichever one raises
100
- # first, the constant is already there to catch it.
64
+ # Both requires happen up front: the rescue below matches
65
+ # GoogleIDToken::ValidationError, resolved only when caught, so
66
+ # requiring it lazily after an OAuth2::Error would mask the real failure.
101
67
  require "oauth2"
102
68
  require "google-id-token"
103
69
 
@@ -2,45 +2,23 @@ require "time"
2
2
 
3
3
  module Hecks
4
4
  module Adapters
5
- # **The `authorization` port, fulfilled by governance** — same registry,
6
- # same boot, so "ask Governance" is a dispatch against records
7
- # already sitting in the store this adapter is handed, not a bridge
8
- # to a second runtime. `Runtime::Dispatcher.new(registry)` is cheap
9
- # to build fresh per call (`Registry#capability_graph`'s own
10
- # neighbor, `#repository`, does the same kind of on-demand build) —
11
- # nothing here holds one across calls, so there is no boot-order
12
- # dependency on when Governance's bluebook loads relative to this
13
- # adapter, only that it has by the time `holds_role?` is called.
14
- #
15
- # An active assignment, not merely a past one — `RoleAssignment`
16
- # answers with every assignment an actor has ever held, current or
17
- # past (`AssignmentsForActor`'s own description covers both), and
18
- # leaves `ends_at` for the caller to read, the same deferral
19
- # `Governance::RoleTransition.Allowed` makes for the same reason.
20
- # This is that caller.
5
+ # The `authorization` port, answered by dispatching queries against Governance's records
6
+ # in the same registry. Only assignments with no `ends_at` count as live.
21
7
  module GovernanceAuthorization
22
8
  module_function
23
9
 
24
- # `as_of` and `scope` are both optional, same opt-in shape
25
- # `refuse_role_mismatch` already gives `actor_id` itself — an
26
- # unbound `as_of` skips the `starts_at` check and an unbound
27
- # `scope` skips the `scope` check, exactly the behavior before
28
- # either existed. Neither is fetched here: `as_of` arrives already
29
- # resolved from `Ports::Clock.now`, called by the caller at the
30
- # door, never by this adapter — see `Ports::Clock`'s own header
31
- # for why the dispatch path must not consult the clock itself.
10
+ # Reports whether `actor_id` holds a live grant of `role`.
32
11
  #
33
- # @param registry [Runtime::Registry] the booted registry, to resolve the authorization
34
- # provider's verb
35
- # @param actor_id [String] the actor whose grants are checked, compared as a String
36
- # @param role [String, Symbol] the role name to look for, compared as a String
37
- # @param as_of [Integer, nil] Unix epoch seconds; a grant whose `starts_at` is later, or
38
- # does not parse as a time, does not count. nil skips the `starts_at` check
12
+ # `as_of` and `scope` are opt-in: nil skips the matching check. `as_of` comes from the
13
+ # caller, since the dispatch path must not read the clock (see `Ports::Clock`).
14
+ #
15
+ # @param registry [Runtime::Registry] the booted registry
16
+ # @param actor_id [String] the actor whose grants are checked
17
+ # @param role [String, Symbol] the role name to look for
18
+ # @param as_of [Integer, nil] Unix epoch seconds; a later (or unparseable) `starts_at` fails
39
19
  # @param scope [String, nil] the scope the caller acts in; nil skips the scope check
40
- # @return [Boolean] true if at least one live grant of `role` to `actor_id` passes the
41
- # `as_of` and `scope` checks
42
- # @raise [Runtime::WiringError] if the loaded chapters providing `"authorization"` are
43
- # not exactly one (see `provided_verb`)
20
+ # @return [Boolean] true if a live grant of `role` to `actor_id` passes both checks
21
+ # @raise [Runtime::WiringError] unless exactly one chapter provides `"authorization"`
44
22
  def holds_role?(registry, actor_id:, role:, as_of: nil, scope: nil)
45
23
  rows = Runtime::Dispatcher.new(registry).query(
46
24
  provided_verb(registry, :assignments),
@@ -55,11 +33,7 @@ module Hecks
55
33
  end
56
34
  end
57
35
 
58
- # `scope` unchecked when not stated, same as every other opt-in
59
- # field here — a caller that never says which scope it is acting
60
- # in gets the pre-scope behavior: any live assignment for the role
61
- # authorizes, everywhere. A caller that does state one only
62
- # authorizes against an assignment granted for that scope.
36
+ # Without a `scope`, any live assignment for the role authorizes everywhere.
63
37
  #
64
38
  # @param row [Hash{Symbol => Object}] one `RoleAssignment` row as
65
39
  # `Runtime::Dispatcher#query` returns it; `:scope` holds a `{value: String}` Hash
@@ -69,13 +43,8 @@ module Hecks
69
43
  scope.nil? || row[:scope][:value] == scope.to_s
70
44
  end
71
45
 
72
- # `starts_at` is a free-text string in the bluebook (`Timestamp`'s
73
- # only invariant is "present", not any particular format) — parsed
74
- # here with `Time.parse` rather than compared lexically, since
75
- # nothing guarantees every caller writes it zero-padded ISO 8601.
76
- # Fails closed : a `starts_at` that does not parse is treated as
77
- # not-yet-started rather than silently ignored, the same direction
78
- # every other check in this method already fails.
46
+ # `starts_at` is free text, so it is parsed with `Time.parse` rather than compared
47
+ # lexically. Fails closed: an unparseable value counts as not yet started.
79
48
  #
80
49
  # @param row [Hash{Symbol => Object}] one `RoleAssignment` row as
81
50
  # `Runtime::Dispatcher#query` returns it; `:starts_at` holds a `{value: String}` Hash
@@ -92,21 +61,13 @@ module Hecks
92
61
 
93
62
  # Answers whether one role may act as another.
94
63
  #
95
- # **The other half** — may role X act as role Y. `RoleTransition.Allowed`
96
- # is identified by the exact pair, so at most one row ever comes
97
- # back ; still read as `.any?` rather than trusting that structurally,
98
- # the same defensiveness `holds_role?` already has to have anyway
99
- # since `AssignmentsForActor` can return several.
64
+ # Read with `.any?` rather than trusting that the exact-pair lookup returns one row.
100
65
  #
101
- # @param registry [Runtime::Registry] the booted registry, to resolve the authorization
102
- # provider's verb
66
+ # @param registry [Runtime::Registry] the booted registry
103
67
  # @param from_role [String, Symbol] the role the caller holds, compared as a String
104
- # @param to_role [String, Symbol] the role the caller wants to act as, compared as a
105
- # String
106
- # @return [Boolean] true if a live (not ended) allowance lets `from_role` act as
107
- # `to_role`
108
- # @raise [Runtime::WiringError] if the loaded chapters providing `"authorization"` are
109
- # not exactly one (see `provided_verb`)
68
+ # @param to_role [String, Symbol] the role the caller wants to act as, compared as a String
69
+ # @return [Boolean] true if a live allowance lets `from_role` act as `to_role`
70
+ # @raise [Runtime::WiringError] unless exactly one chapter provides `"authorization"`
110
71
  def authorized_as?(registry, from_role:, to_role:)
111
72
  rows = Runtime::Dispatcher.new(registry).query(
112
73
  provided_verb(registry, :transitions),
@@ -116,12 +77,7 @@ module Hecks
116
77
  rows.any? { |row| row[:ends_at].nil? }
117
78
  end
118
79
 
119
- # The role itself, not just a yes/no about one — the same
120
- # `AssignmentsForActor` query `holds_role?` runs, just returning
121
- # the live (non-revoked) row's `role_name` instead of comparing it
122
- # against a caller-supplied guess. `nil` for no live assignment at
123
- # all — the caller's own fallback (an aggregate's own role field,
124
- # a default) is domain-specific and does not belong here.
80
+ # Returns the actor's live role name, or nil; any fallback is the caller's.
125
81
  #
126
82
  # @param registry [Runtime::Registry] the booted registry, to resolve the authorization
127
83
  # provider's verb
@@ -140,11 +96,8 @@ module Hecks
140
96
  live && live[:role_name][:value]
141
97
  end
142
98
 
143
- # The verb, read from the provider's own declaration — `provides
144
- # "authorization", assignments: ..., transitions: ...` on whichever
145
- # loaded chapter declares it (Governance's, in every boot today).
146
- # Exactly one provider, the same "the runtime will not choose for
147
- # you" rule `Ports::Authorization.adapter` applies to adapters.
99
+ # Reads a verb from the one chapter that `provides "authorization"`; the runtime will
100
+ # not choose between several (as in `Ports::Authorization.adapter`).
148
101
  #
149
102
  # @param registry [Runtime::Registry] the booted registry to search for the
150
103
  # `"authorization"` provider
@@ -22,35 +22,13 @@ module Hecks
22
22
  end
23
23
  end
24
24
 
25
- # An explicit, opt-in maintenance operation — never run
26
- # automatically after an ordinary save/delete. Heki's journal is
27
- # not a disposable write-ahead log: it is this adapter's own
28
- # answer to `entries`, and `entries` is a real port contract
29
- # (`Ports::Persistence::AppendOnly`'s own required-methods list)
30
- # read in full, forever, by `Ports::Projection::Worker#catch_up!`
31
- # and `Registry#projection_current?` to catch a projection up to
32
- # its authoritative source, and by `bin/history` to show "every
33
- # journal entry a domain's append-only adapters hold" — the same
34
- # contract Postgres/Sqlite/D1 uphold by way of a journal table
35
- # that is never pruned. A real example (`examples/banking`,
36
- # `persisted_by("Heki")` + `projected_by("SqliteProjection")`)
37
- # depends on this today. Compacting throws that full history away
38
- # for whatever happened before the call — correct only for an
39
- # aggregate nothing ever projects from; callers (`bin/
40
- # heki_compact`) are responsible for confirming that first.
25
+ # Folds the journal into the snapshot and truncates it. Opt-in; never run after a save.
41
26
  #
42
- # Crash-safety ordering: `write` below is the exact same
43
- # temp-file, fsync, atomic-rename sequence `save`/`delete`
44
- # already use — the snapshot it produces is confirmed durably on
45
- # disk before this method ever touches the journal. Only once
46
- # that succeeds does `truncate_journal!` run. A crash between the
47
- # two leaves old (now fully redundant) journal lines in place;
48
- # replaying them again over the fresh snapshot on the next boot
49
- # is idempotent — the same value gets set again, never a wrong
50
- # one — so nothing is lost, just a little wasted replay work
51
- # once. A crash before `write` completes leaves the journal
52
- # fully intact and the prior snapshot untouched, exactly today's
53
- # existing crash-recovery guarantee.
27
+ # This discards the history `entries` returns, which projections and `bin/history`
28
+ # read in full, so use it only on an aggregate nothing projects from.
29
+ #
30
+ # The snapshot is written durably before the journal is truncated; a crash between
31
+ # the two leaves redundant lines whose replay is idempotent.
54
32
  #
55
33
  # @return [void]
56
34
  # @raise [Malformed] if the snapshot or journal file is corrupt
@@ -68,15 +46,7 @@ module Hecks
68
46
  def truncate_journal!
69
47
  return unless File.exist?(@journal_path)
70
48
 
71
- # A single `truncate(0)` syscall on an already-open file
72
- # descriptor — the file's length changes atomically at the
73
- # filesystem level, so there is no "half truncated" state to
74
- # observe even under a crash mid-call. `fsync` below makes
75
- # that change durable before this method returns; without it
76
- # a crash could still leave the old (harmless-to-replay)
77
- # content on disk after a normal return, which is fine per
78
- # the crash-safety note above, but the durable case is the
79
- # one actually worth returning success for.
49
+ # `truncate(0)` is atomic; the fsync makes it durable before returning.
80
50
  File.open(@journal_path, "r+b") do |file|
81
51
  file.truncate(0)
82
52
  file.flush
@@ -106,10 +76,7 @@ module Hecks
106
76
  encoded = Ports::Persistence::StateCodec.encode(@aggregate, state)
107
77
  line = "#{JSON.generate(operation: operation, id: id.to_s, state: encoded, mirrors: @entry_mirrors)}\n"
108
78
 
109
- # One write, not JSON-then-newline as two: two concurrent
110
- # appends can only interleave *between* writes, never inside
111
- # one, so this line can't come out split by another process's
112
- # line landing in the middle of it.
79
+ # One write, so a concurrent append cannot land in the middle of this line.
113
80
  File.open(@journal_path, "ab") do |journal|
114
81
  journal.write(line)
115
82
  journal.flush
@@ -4,46 +4,11 @@ require_relative "journal"
4
4
  module Hecks
5
5
  module Adapters
6
6
  class Heki
7
- # The optional saga-persistence capability (§2)'s Heki-specific store — a sibling
8
- # snapshot+journal file pair, keyed by (domain, process manager, correlation).
7
+ # Saga checkpoints in a sibling snapshot+journal file pair, keyed by
8
+ # (domain, process manager, correlation).
9
9
  #
10
- # ## Why a sibling file pair, not a table
11
- #
12
- # Built the exact same way an aggregate's own persistence already
13
- # is: `Snapshot`/`Journal` (heki/snapshot.rb, heki/journal.rb)
14
- # operate generically on `@path`/`@journal_path`/`@entry_mirrors`
15
- # and never touch `@aggregate`, so this reuses them unchanged
16
- # rather than re-deriving the same binary framing and
17
- # crash-recovery replay.
18
- #
19
- # ## Where the file lives
20
- #
21
- # Reserved file name (`hecks_saga_instances.heki`, matching the
22
- # `hecks_`-prefix convention every other new saga table in this
23
- # work uses) avoids colliding with any real aggregate's own
24
- # `storage_name`. Lives in the same directory an aggregate's own
25
- # `.heki` file would (`File.dirname(@path)`, `Heki`'s own call
26
- # below) — which, since Heki's `resolve_path` has no per-domain
27
- # component at all, is typically shared across every domain
28
- # booted from the same `root`. `domain` is therefore carried
29
- # inside each record and filtered on read, the same reason
30
- # Postgres's own `hecks_saga_instances` keeps an explicit `domain`
31
- # column under schema isolation (§3).
32
- #
33
- # ## Record shape
34
- #
35
- # One flat records hash, keyed by a composite string (Heki's own
36
- # snapshot format is id-keyed, not tuple-keyed) — never exposed
37
- # outside this class; `each_saga` yields the five real fields a
38
- # caller actually wants, not the internal key shape.
39
- #
40
- # ## Locking
41
- #
42
- # Locked the same way an aggregate's own store is: `with_lock`
43
- # (`Snapshot`, shared) serializes each save/delete's read-modify-
44
- # write against `@path`'s own lock file — a saga gets exactly the
45
- # durability and concurrency-safety this adapter already gives its
46
- # aggregates, no better, no worse.
10
+ # The file is shared by every domain booted from the same directory, so `domain`
11
+ # is stored in each record and filtered on read.
47
12
  class SagaStore
48
13
  include Snapshot
49
14
  include Journal
@@ -59,16 +24,13 @@ module Hecks
59
24
  # Upserts one saga instance's checkpoint, keyed by domain, process manager and
60
25
  # correlation, under the file lock.
61
26
  #
62
- # @param domain [String] the owning domain, carried in the record and filtered on
63
- # read
27
+ # @param domain [String] the owning domain, carried in the record and filtered on read
64
28
  # @param process_manager [String] the process manager's name
65
29
  # @param correlation [String] the instance's correlation value
66
30
  # @param state [String] the saga's current state name
67
31
  # @param memory [Hash] the saga's working memory to persist
68
- # @param completed_compensations [Array] the ledger of completed compensable legs;
69
- # `[]` when none
70
- # @return [Hash{String => Hash}] the store's full internal records Hash after the
71
- # write; callers ignore it
32
+ # @param completed_compensations [Array] completed compensable legs; `[]` when none
33
+ # @return [Hash{String => Hash}] the full records Hash after the write; callers ignore it
72
34
  def save_saga(domain, process_manager, correlation, state, memory, completed_compensations = [])
73
35
  key = key_for(domain, process_manager, correlation)
74
36
  record = { "domain" => domain, "process_manager" => process_manager,
@@ -104,8 +66,7 @@ module Hecks
104
66
  end
105
67
  end
106
68
 
107
- # Yields every checkpointed saga instance of `domain`, for `Registry
108
- # #rehydrate_sagas!` to restore at boot.
69
+ # Yields every checkpointed saga instance of `domain`, for boot-time rehydration.
109
70
  #
110
71
  # @param domain [String] the owning domain to filter records to
111
72
  # @yieldparam process_manager [String] the process manager's name
@@ -113,10 +74,8 @@ module Hecks
113
74
  # @yieldparam state [String] the saga's state name
114
75
  # @yieldparam memory [Hash{Symbol => Object}] the saga's memory, Symbol keys at every
115
76
  # depth
116
- # @yieldparam completed_compensations [Array] the completed-compensation ledger, `[]`
117
- # when the record carries none
118
- # @return [Enumerator, Hash{String => Hash}] an enumerator over the same five values
119
- # when no block is given; otherwise the store's full internal records Hash
77
+ # @yieldparam completed_compensations [Array] the completed-compensation ledger
78
+ # @return [Enumerator, Hash{String => Hash}] an enumerator when no block is given
120
79
  def each_saga(domain)
121
80
  return enum_for(:each_saga, domain) unless block_given?
122
81
 
@@ -135,9 +94,8 @@ module Hecks
135
94
  @store ||= replay_journal(read_snapshot)
136
95
  end
137
96
 
138
- # A plain space-joined key, not anything fancier — a correlation value is real
139
- # business data (an order id, a customer reference) and nothing
140
- # here should assume it never contains a space.
97
+ # Space-joined; the key is opaque and never parsed back, so spaces in a
98
+ # correlation value are harmless.
141
99
  def key_for(domain, process_manager, correlation) = [domain, process_manager, correlation].join(" ")
142
100
  end
143
101
  end
@@ -23,11 +23,7 @@ module Hecks
23
23
  raise Malformed, "#{@path}: json error: #{e.message}"
24
24
  end
25
25
 
26
- # Temp-file-plus-rename, fsynced ahead of the `File.rename` call: a reader can
27
- # only ever see the last complete snapshot or the one before it,
28
- # never a truncated or partial one — `File.binwrite`'s old
29
- # truncate-then-write left a window, proportional to the whole
30
- # dataset, where the file on disk was neither.
26
+ # Temp file, fsync, then rename, so a reader never sees a partial snapshot.
31
27
  def write(records)
32
28
  sorted = records.sort_by { |id, _| id }.to_h
33
29
  json = JSON.generate(sorted)
@@ -42,13 +38,9 @@ module Hecks
42
38
  File.rename(tmp, @path)
43
39
  end
44
40
 
45
- # Serializes the read-modify-write each save/delete does against
46
- # the snapshot (and the journal append that precedes it) across
47
- # processes — `flock` is per-process advisory, so every writer
48
- # has to go through this to matter, which `with_lock`'s only two
49
- # callers (`Heki#save`/`#delete`, `SagaStore#save_saga`/
50
- # `#delete_saga`) both do. The lock file is separate from
51
- # `@path` so a held lock never blocks `write`'s own rename.
41
+ # Serializes each save/delete's read-modify-write across processes; `flock` is
42
+ # advisory, so every writer must use it. The lock file is separate from `@path`
43
+ # so a held lock never blocks `write`'s rename.
52
44
  def with_lock
53
45
  File.open(lock_path, File::CREAT | File::RDWR, 0o644) do |lock|
54
46
  lock.flock(File::LOCK_EX)