hecks 0.2.0 → 1.0.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 (419) hide show
  1. checksums.yaml +5 -5
  2. data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
  3. data/lib/hecks/adapters/driven/claude_code.rb +127 -0
  4. data/lib/hecks/adapters/driven/d1.adapter +12 -0
  5. data/lib/hecks/adapters/driven/d1.rb +427 -0
  6. data/lib/hecks/adapters/driven/folder.adapter +3 -0
  7. data/lib/hecks/adapters/driven/folder.rb +199 -0
  8. data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
  9. data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
  10. data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
  11. data/lib/hecks/adapters/driven/governance_authorization.rb +101 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +56 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +99 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +65 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +179 -0
  17. data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
  18. data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
  19. data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
  20. data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
  21. data/lib/hecks/adapters/driven/lambda.adapter +4 -0
  22. data/lib/hecks/adapters/driven/lambda.rb +145 -0
  23. data/lib/hecks/adapters/driven/memory.adapter +3 -0
  24. data/lib/hecks/adapters/driven/memory.rb +103 -0
  25. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
  26. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
  27. data/lib/hecks/adapters/driven/postgres/codec.rb +88 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +207 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +438 -0
  31. data/lib/hecks/adapters/driven/postgres_era.adapter +17 -0
  32. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  33. data/lib/hecks/adapters/driven/prism.rb +80 -0
  34. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  35. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  36. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  37. data/lib/hecks/adapters/driven/sqlite/codec.rb +80 -0
  38. data/lib/hecks/adapters/driven/sqlite/projection.rb +172 -0
  39. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +192 -0
  40. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  41. data/lib/hecks/adapters/driven/sqlite.rb +306 -0
  42. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  43. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  44. data/lib/hecks/adapters/driven.rb +54 -0
  45. data/lib/hecks/adapters.rb +6 -0
  46. data/lib/hecks/behaviors/dsl.rb +125 -0
  47. data/lib/hecks/behaviors/expectations.rb +340 -0
  48. data/lib/hecks/behaviors/ir.rb +31 -0
  49. data/lib/hecks/behaviors/rspec.rb +42 -0
  50. data/lib/hecks/behaviors/runner.rb +96 -0
  51. data/lib/hecks/behaviors.rb +25 -0
  52. data/lib/hecks/bluebook/aggregate.rb +108 -0
  53. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  54. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  55. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  56. data/lib/hecks/bluebook/assembly/contracts.rb +438 -0
  57. data/lib/hecks/bluebook/assembly/marks.rb +228 -0
  58. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  59. data/lib/hecks/bluebook/assembly.rb +91 -0
  60. data/lib/hecks/bluebook/attribute.rb +96 -0
  61. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  62. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  63. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  64. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  65. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  66. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  67. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  68. data/lib/hecks/bluebook/behaviour/lifecycle.rb +68 -0
  69. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  70. data/lib/hecks/bluebook/behaviour/process_manager.rb +51 -0
  71. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  72. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  73. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  74. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  75. data/lib/hecks/bluebook/chapter.rb +78 -0
  76. data/lib/hecks/bluebook/command.rb +124 -0
  77. data/lib/hecks/bluebook/domain_port.rb +102 -0
  78. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  79. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +1018 -0
  80. data/lib/hecks/bluebook/dsl/attribute_collector.rb +348 -0
  81. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  82. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1087 -0
  83. data/lib/hecks/bluebook/dsl/command_builder.rb +714 -0
  84. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  85. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  86. data/lib/hecks/bluebook/dsl/entity_builder.rb +430 -0
  87. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  88. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  89. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  90. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  91. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  92. data/lib/hecks/bluebook/dsl/policy_builder.rb +135 -0
  93. data/lib/hecks/bluebook/dsl/port_builder.rb +39 -0
  94. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  95. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +306 -0
  96. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  97. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  98. data/lib/hecks/bluebook/dsl/rule_reference.rb +173 -0
  99. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  100. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  101. data/lib/hecks/bluebook/dsl/word_gate.rb +228 -0
  102. data/lib/hecks/bluebook/dsl/world_builder.rb +117 -0
  103. data/lib/hecks/bluebook/dsl.rb +45 -0
  104. data/lib/hecks/bluebook/entity.rb +103 -0
  105. data/lib/hecks/bluebook/expression/canonical_form.rb +123 -0
  106. data/lib/hecks/bluebook/expression/evaluator.rb +304 -0
  107. data/lib/hecks/bluebook/expression/projection.json +218 -0
  108. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  109. data/lib/hecks/bluebook/expression/resolver.rb +781 -0
  110. data/lib/hecks/bluebook/expression.rb +13 -0
  111. data/lib/hecks/bluebook/hexagon.rb +60 -0
  112. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  113. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  114. data/lib/hecks/bluebook/meta_validator/judge.rb +621 -0
  115. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  116. data/lib/hecks/bluebook/meta_validator/port_judge.rb +51 -0
  117. data/lib/hecks/bluebook/meta_validator/readings.rb +360 -0
  118. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +351 -0
  119. data/lib/hecks/bluebook/meta_validator/shapes.rb +266 -0
  120. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  121. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  122. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  123. data/lib/hecks/bluebook/meta_validator.rb +542 -0
  124. data/lib/hecks/bluebook/model_check.rb +445 -0
  125. data/lib/hecks/bluebook/pattern_subset.rb +184 -0
  126. data/lib/hecks/bluebook/policy.rb +41 -0
  127. data/lib/hecks/bluebook/process_manager.rb +128 -0
  128. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  129. data/lib/hecks/bluebook/project_loader.rb +40 -0
  130. data/lib/hecks/bluebook/project_register.rb +82 -0
  131. data/lib/hecks/bluebook/query.rb +61 -0
  132. data/lib/hecks/bluebook/read_model.rb +109 -0
  133. data/lib/hecks/bluebook/reference.rb +74 -0
  134. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  135. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  136. data/lib/hecks/bluebook/translation.rb +92 -0
  137. data/lib/hecks/bluebook/value_object.rb +66 -0
  138. data/lib/hecks/bluebook.rb +74 -0
  139. data/lib/hecks/codemod.rb +342 -0
  140. data/lib/hecks/construct.rb +71 -0
  141. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  142. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  143. data/lib/hecks/deploy/oidc.json +18 -0
  144. data/lib/hecks/doc/reference.rb +410 -0
  145. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  146. data/lib/hecks/facade/cli_door.rb +119 -0
  147. data/lib/hecks/facade/cli_runner.rb +190 -0
  148. data/lib/hecks/facade/command_request.rb +105 -0
  149. data/lib/hecks/facade/handle.rb +173 -0
  150. data/lib/hecks/facade/json_door.rb +166 -0
  151. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  152. data/lib/hecks/facade/surface/chapter.rb +107 -0
  153. data/lib/hecks/facade/surface.rb +48 -0
  154. data/lib/hecks/facade.rb +44 -0
  155. data/lib/hecks/forms/app.rb +341 -0
  156. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  157. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  158. data/lib/hecks/forms/field_renderer.rb +177 -0
  159. data/lib/hecks/forms/field_shape.rb +232 -0
  160. data/lib/hecks/forms/html.rb +84 -0
  161. data/lib/hecks/forms/index_renderer.rb +35 -0
  162. data/lib/hecks/forms/page.rb +157 -0
  163. data/lib/hecks/forms/params.rb +161 -0
  164. data/lib/hecks/forms/port_argument.rb +46 -0
  165. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  166. data/lib/hecks/forms/record_renderer.rb +119 -0
  167. data/lib/hecks/forms/record_table.rb +68 -0
  168. data/lib/hecks/forms/reference_options.rb +30 -0
  169. data/lib/hecks/forms/value_object_shape.rb +46 -0
  170. data/lib/hecks/forms.rb +54 -0
  171. data/lib/hecks/fqn.rb +94 -0
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  175. data/lib/hecks/framework/bluebook/governance.bluebook +145 -0
  176. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  177. data/lib/hecks/framework/oidc.json +39 -0
  178. data/lib/hecks/framework.rb +90 -0
  179. data/lib/hecks/freezer.rb +67 -0
  180. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
  181. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  182. data/lib/hecks/fuzzing/isolated_boot.rb +286 -0
  183. data/lib/hecks/fuzzing/properties.rb +1192 -0
  184. data/lib/hecks/fuzzing/replay.rb +668 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +80 -0
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +250 -0
  189. data/lib/hecks/fuzzing/sequence_generator.rb +131 -0
  190. data/lib/hecks/fuzzing/value_generator.rb +200 -0
  191. data/lib/hecks/fuzzing.rb +12 -0
  192. data/lib/hecks/grammar/evolve.rb +324 -0
  193. data/lib/hecks/grammar/expression.bluebook +420 -0
  194. data/lib/hecks/grammar/expression_operators.json +1648 -0
  195. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  196. data/lib/hecks/grammar/oidc.json +69 -0
  197. data/lib/hecks/grammar/translation.bluebook +203 -0
  198. data/lib/hecks/grammar.rb +178 -0
  199. data/lib/hecks/ir.rb +126 -0
  200. data/lib/hecks/language/adapter.bluebook +116 -0
  201. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  202. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  203. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  204. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  205. data/lib/hecks/language/bluebook/command.bluebook +471 -0
  206. data/lib/hecks/language/bluebook/entity.bluebook +392 -0
  207. data/lib/hecks/language/bluebook/policy.bluebook +189 -0
  208. data/lib/hecks/language/bluebook/process_manager.bluebook +381 -0
  209. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  210. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  211. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  212. data/lib/hecks/language/bluebook/syntax.bluebook +448 -0
  213. data/lib/hecks/language/bluebook/vocabulary.bluebook +379 -0
  214. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  215. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  216. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  217. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  218. data/lib/hecks/language/oidc.json +333 -0
  219. data/lib/hecks/language/port.bluebook +120 -0
  220. data/lib/hecks/language/translation/translation.bluebook +110 -0
  221. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  222. data/lib/hecks/language/world/wiring.bluebook +62 -0
  223. data/lib/hecks/language/world/world.bluebook +84 -0
  224. data/lib/hecks/literal.rb +125 -0
  225. data/lib/hecks/naming.rb +174 -0
  226. data/lib/hecks/ports/access_control.port +9 -0
  227. data/lib/hecks/ports/access_control.rb +62 -0
  228. data/lib/hecks/ports/agent/answers.rb +104 -0
  229. data/lib/hecks/ports/agent.port +8 -0
  230. data/lib/hecks/ports/agent.rb +167 -0
  231. data/lib/hecks/ports/authentication.port +6 -0
  232. data/lib/hecks/ports/authentication.rb +50 -0
  233. data/lib/hecks/ports/authorization.port +7 -0
  234. data/lib/hecks/ports/authorization.rb +62 -0
  235. data/lib/hecks/ports/clock.port +5 -0
  236. data/lib/hecks/ports/clock.rb +62 -0
  237. data/lib/hecks/ports/extraction.port +5 -0
  238. data/lib/hecks/ports/extraction.rb +37 -0
  239. data/lib/hecks/ports/identity_assignment.port +5 -0
  240. data/lib/hecks/ports/identity_assignment.rb +45 -0
  241. data/lib/hecks/ports/identity_generation.port +5 -0
  242. data/lib/hecks/ports/identity_generation.rb +49 -0
  243. data/lib/hecks/ports/identity_resolution.port +5 -0
  244. data/lib/hecks/ports/identity_resolution.rb +40 -0
  245. data/lib/hecks/ports/loading.port +4 -0
  246. data/lib/hecks/ports/loading.rb +13 -0
  247. data/lib/hecks/ports/persistence/append_only.rb +138 -0
  248. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  249. data/lib/hecks/ports/persistence/execution.rb +23 -0
  250. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  251. data/lib/hecks/ports/persistence/plugin.rb +54 -0
  252. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +169 -0
  253. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +128 -0
  254. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +161 -0
  255. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +61 -0
  256. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +304 -0
  257. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +171 -0
  258. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +190 -0
  259. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +478 -0
  260. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +166 -0
  261. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +314 -0
  262. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +168 -0
  263. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +170 -0
  264. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +134 -0
  265. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +137 -0
  266. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +89 -0
  267. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +85 -0
  268. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  269. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +132 -0
  270. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +74 -0
  271. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +758 -0
  272. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +120 -0
  273. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +31 -0
  274. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +46 -0
  275. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +102 -0
  276. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +46 -0
  277. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +70 -0
  278. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +72 -0
  279. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +120 -0
  280. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +183 -0
  281. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +41 -0
  282. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +28 -0
  283. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +30 -0
  284. data/lib/hecks/ports/persistence/plugins/era/translation.rb +11 -0
  285. data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
  286. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  287. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  288. data/lib/hecks/ports/persistence.port +4 -0
  289. data/lib/hecks/ports/persistence.rb +39 -0
  290. data/lib/hecks/ports/projection.port +4 -0
  291. data/lib/hecks/ports/projection.rb +96 -0
  292. data/lib/hecks/ports/query/in_memory.rb +60 -0
  293. data/lib/hecks/ports/query/ordering.rb +41 -0
  294. data/lib/hecks/ports/query.rb +36 -0
  295. data/lib/hecks/ports.rb +27 -0
  296. data/lib/hecks/projections/diagrams.rb +642 -0
  297. data/lib/hecks/projections/ir.rb +18 -0
  298. data/lib/hecks/projections/model/deviations.rb +98 -0
  299. data/lib/hecks/projections/model.rb +145 -0
  300. data/lib/hecks/projections/oidc.rb +110 -0
  301. data/lib/hecks/projections/parser_table.rb +159 -0
  302. data/lib/hecks/projections/reference.rb +38 -0
  303. data/lib/hecks/projections/shape.rb +44 -0
  304. data/lib/hecks/projections/statements.rb +110 -0
  305. data/lib/hecks/projections/vocabulary.rb +100 -0
  306. data/lib/hecks/projections.rb +32 -0
  307. data/lib/hecks/projector/cli_projector.rb +446 -0
  308. data/lib/hecks/projector/docs_projector.rb +321 -0
  309. data/lib/hecks/projector/exporter.rb +158 -0
  310. data/lib/hecks/projector/ir_projector.rb +18 -0
  311. data/lib/hecks/projector/narrate_projector.rb +243 -0
  312. data/lib/hecks/projector/target.rb +97 -0
  313. data/lib/hecks/projector.rb +186 -0
  314. data/lib/hecks/query_ir.rb +411 -0
  315. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  316. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  317. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  318. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  319. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  320. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  321. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  322. data/lib/hecks/query_specification/common/null_policy.rb +92 -0
  323. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  324. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  325. data/lib/hecks/query_specification/common/options.rb +32 -0
  326. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  327. data/lib/hecks/query_specification/common/specification.rb +10 -0
  328. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  329. data/lib/hecks/query_specification/field_path.rb +107 -0
  330. data/lib/hecks/query_specification/hop_path.rb +132 -0
  331. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  332. data/lib/hecks/query_specification.rb +14 -0
  333. data/lib/hecks/rendering.rb +48 -0
  334. data/lib/hecks/router/namespace_installer.rb +157 -0
  335. data/lib/hecks/router.rb +70 -0
  336. data/lib/hecks/runtime/aggregate_lock.rb +45 -0
  337. data/lib/hecks/runtime/boot_gates.rb +41 -0
  338. data/lib/hecks/runtime/caller.rb +66 -0
  339. data/lib/hecks/runtime/capability_graph.rb +44 -0
  340. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +135 -0
  341. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +260 -0
  342. data/lib/hecks/runtime/command_interpreter.rb +497 -0
  343. data/lib/hecks/runtime/command_rules/admissibility.rb +357 -0
  344. data/lib/hecks/runtime/command_rules/arithmetic.rb +260 -0
  345. data/lib/hecks/runtime/command_rules/authorization.rb +65 -0
  346. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  347. data/lib/hecks/runtime/command_rules/references.rb +189 -0
  348. data/lib/hecks/runtime/command_rules.rb +28 -0
  349. data/lib/hecks/runtime/dependency_planning.rb +245 -0
  350. data/lib/hecks/runtime/dispatcher.rb +301 -0
  351. data/lib/hecks/runtime/entity_element.rb +253 -0
  352. data/lib/hecks/runtime/entity_interpreter.rb +299 -0
  353. data/lib/hecks/runtime/errors.rb +122 -0
  354. data/lib/hecks/runtime/event.rb +51 -0
  355. data/lib/hecks/runtime/identity.rb +156 -0
  356. data/lib/hecks/runtime/instance.rb +175 -0
  357. data/lib/hecks/runtime/interpreting.rb +90 -0
  358. data/lib/hecks/runtime/loader.rb +184 -0
  359. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  360. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  361. data/lib/hecks/runtime/query_interpreter.rb +286 -0
  362. data/lib/hecks/runtime/reaction_invocation.rb +253 -0
  363. data/lib/hecks/runtime/read_model_interpreter.rb +341 -0
  364. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  365. data/lib/hecks/runtime/reference_hop.rb +99 -0
  366. data/lib/hecks/runtime/refusal_wording.rb +126 -0
  367. data/lib/hecks/runtime/registry/saga_persistence.rb +142 -0
  368. data/lib/hecks/runtime/registry/verification.rb +246 -0
  369. data/lib/hecks/runtime/registry.rb +279 -0
  370. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  371. data/lib/hecks/runtime/routing.rb +96 -0
  372. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  373. data/lib/hecks/runtime/saga_interpreter.rb +503 -0
  374. data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
  375. data/lib/hecks/runtime/tenant_check.rb +84 -0
  376. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  377. data/lib/hecks/runtime/value/admission.rb +130 -0
  378. data/lib/hecks/runtime/value/coercion.rb +565 -0
  379. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  380. data/lib/hecks/runtime/value.rb +125 -0
  381. data/lib/hecks/runtime.rb +107 -0
  382. data/lib/hecks/storehouse.rb +632 -0
  383. data/lib/hecks/version.rb +16 -0
  384. data/lib/hecks/vocabulary.rb +218 -0
  385. data/lib/hecks.rb +123 -6
  386. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
  387. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
  388. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
  389. metadata +412 -222
  390. data/bin/hecks +0 -7
  391. data/bin/hecks-package +0 -65
  392. data/bin/hecks_console +0 -12
  393. data/bin/hecks_serverless +0 -6
  394. data/lib/cli/build.rb +0 -14
  395. data/lib/cli/command_runner.rb +0 -28
  396. data/lib/cli/console.rb +0 -10
  397. data/lib/cli/generate.rb +0 -37
  398. data/lib/cli/hecks-cli.rb +0 -27
  399. data/lib/cli/test.rb +0 -57
  400. data/lib/console/commands.rb +0 -8
  401. data/lib/console/hecks-console.rb +0 -1
  402. data/lib/packager/README.md +0 -0
  403. data/lib/packager/app_runner.rb +0 -21
  404. data/lib/packager/args.rb +0 -26
  405. data/lib/packager/compatibility/fixnum.rb +0 -6
  406. data/lib/packager/hecks.rb +0 -39
  407. data/lib/packager/query_runner.rb +0 -21
  408. data/lib/packager/resources/Dockerfile +0 -11
  409. data/lib/packager/resources/app_binary +0 -7
  410. data/lib/packager/resources/bundle_config +0 -3
  411. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  412. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  413. data/lib/serverless/Domain +0 -32
  414. data/lib/serverless/cli.rb +0 -75
  415. data/lib/serverless/resources/command_name.js +0 -5
  416. data/lib/serverless/resources/environment.js +0 -7
  417. data/lib/serverless/resources/handler.js.tt +0 -28
  418. data/lib/serverless/resources/run_binary.js +0 -22
  419. data/lib/serverless/resources/serverless.yml +0 -20
@@ -0,0 +1,199 @@
1
+ require_relative "../../vocabulary"
2
+
3
+ module Hecks
4
+ module Adapters
5
+ class Folder
6
+ DOMAIN_ORDER = Hecks::Vocabulary.fetch("LoadOrder")
7
+ PORTS = "ports"
8
+ ADAPTERS = "adapters"
9
+
10
+ def initialize(settings: {}, root: nil)
11
+ @settings = settings
12
+ @root = root
13
+ end
14
+
15
+ def load_library
16
+ load_each(library(PORTS), %w[*.port])
17
+ load_each(library(ADAPTERS), %w[*/*.adapter */*/*.adapter])
18
+ end
19
+
20
+ def load_project(root)
21
+ return unless root
22
+
23
+ load_each(File.join(root, PORTS), %w[*.port */*.port])
24
+ load_each(File.join(root, ADAPTERS), %w[*.adapter */*.adapter */*/*.adapter])
25
+ end
26
+
27
+ # CHAPTERS FIRST, JUDGED ONCE, THEN EVERYTHING THAT READS THEM.
28
+ # A chapter may be split across files (the language's own grammar is
29
+ # nine), and judging file one before files two-through-nine exist
30
+ # refuses references that are perfectly well declared a file later —
31
+ # see MetaValidator.defer for the whole reasoning. DOMAIN_ORDER
32
+ # already places every `*.bluebook` ahead of hecksagons and worlds,
33
+ # so the window ends at the last chapter pattern rather than at a
34
+ # hand-written list this would otherwise have to keep in step.
35
+ # `environment:` — ONE MORE PAIR OF FILES, LOADED LAST, NOT A GLOB.
36
+ # RECOVERED, not new — see Runtime::Loader.boot's own comment for
37
+ # the provenance. A caller passing `Hecks.boot(path, environment:
38
+ # "production")` gets exactly `environments/production.hecksagon`
39
+ # and `environments/production.world` loaded, whichever exist (a
40
+ # missing one is a silent no-op — not every environment overrides
41
+ # both; see docs/implemented/guides/wiring.md's "Swapping wiring per
42
+ # environment" for the motivating case: swapping a driven adapter
43
+ # — e.g. a real payment gateway for a mock one, or a hosting
44
+ # layer's tenancy settings — without an `if`/`else` anywhere in
45
+ # the domain's own wiring). Never matched by `DOMAIN_ORDER`'s own
46
+ # globs (all non-recursive, none named `environments/*`), so this
47
+ # is the only thing that ever reaches them. Loaded as genuine
48
+ # `Hecks.hecksagon "SameDomain" do ... end` / `Hecks.world
49
+ # "SameDomain" do ... end` blocks — MERGED into the base file's
50
+ # own hecksagon/world (Registry#add_hecksagon / #add_world,
51
+ # concatenate/override rather than replace), so an overlay can
52
+ # rebind or add settings for anything the base file declared
53
+ # without needing to know what else the base file said.
54
+ def load_domain(directory, environment: nil)
55
+ boundary = DOMAIN_ORDER.rindex { |pattern| pattern.end_with?(".bluebook") }
56
+ if boundary
57
+ load_bluebooks(directory, DOMAIN_ORDER[0..boundary])
58
+ load_each(directory, DOMAIN_ORDER[(boundary + 1)..])
59
+ else
60
+ load_each(directory, DOMAIN_ORDER)
61
+ end
62
+
63
+ return unless environment
64
+
65
+ load_each(directory, [File.join("environments", "#{environment}.hecksagon")])
66
+ load_each(directory, [File.join("environments", "#{environment}.world")])
67
+ end
68
+
69
+ # EVERY BLUEBOOK IN A FOLDER IS ONE DECLARATION SET. Individual files
70
+ # remain organized in the domain expert's language; the folder is the
71
+ # unit callers load. Builders group declarations by the chapter name in
72
+ # each file, so a folder may hold more than one chapter without a catalog.
73
+ # Sorting makes source order deterministic while the deferred window keeps
74
+ # cross-file references from being judged against a partial chapter.
75
+ def load_bluebooks(directory, patterns = ["*.bluebook"])
76
+ Bluebook::MetaValidator.defer { load_each(directory, patterns) }
77
+ Bluebook::MetaValidator.judge_deferred!(Hecks.current_registry)
78
+ end
79
+
80
+ # THE EXPLICIT-FILE SIBLING OF `load_domain` — for a caller that names
81
+ # its own exact files rather than a directory to glob (`Loader.boot_files`,
82
+ # behind `Hecks.boot_files`). No `Dir.glob`, no copying: every path here
83
+ # is a real file on disk, wherever it actually lives, loaded in place —
84
+ # a `.behaviors` file's `loads` scopes a boot this way specifically so a
85
+ # per-test boot never has to fake isolation by staging bluebooks into a
86
+ # tmpdir (see Loader.boot_files's own header for why that pattern is a
87
+ # hazard, not a convenience).
88
+ #
89
+ # ORDERED BY CATEGORY, NOT BY THE CALLER'S OWN LIST ORDER — same four
90
+ # groups `Vocabulary.fetch("LoadOrder")` walks a directory in
91
+ # (bluebook chapters, translations, hecksagons, worlds), because a
92
+ # hecksagon can reference a bluebook's own constants and must not load
93
+ # first regardless of which order a caller happened to write `loads
94
+ # "x.hecksagon", "x.bluebook"` in. Bluebook chapters are judged as one
95
+ # deferred group exactly like `load_domain` does, for the identical
96
+ # forward-reference reason (MetaValidator.defer's own header).
97
+ def load_selected(files, environment: nil)
98
+ bluebooks, rest = files.partition { |f| f.end_with?(".bluebook") }
99
+
100
+ if bluebooks.any?
101
+ Bluebook::MetaValidator.defer { bluebooks.sort.each { |f| Kernel.load(f) } }
102
+ Bluebook::MetaValidator.judge_deferred!(Hecks.current_registry)
103
+ end
104
+
105
+ %w[.hecksagon .world].each do |ext|
106
+ rest.select { |f| f.end_with?(ext) }.sort.each { |f| Kernel.load(f) }
107
+ end
108
+
109
+ return unless environment
110
+
111
+ directory = File.dirname(files.first)
112
+ load_each(directory, [File.join("environments", "#{environment}.hecksagon")])
113
+ load_each(directory, [File.join("environments", "#{environment}.world")])
114
+ end
115
+
116
+ def load_each(directory, patterns)
117
+ return unless File.directory?(directory)
118
+
119
+ patterns.each do |pattern|
120
+ Dir[File.join(directory, pattern)].sort.each { |file| Kernel.load(file) }
121
+ end
122
+ end
123
+
124
+ def bluebook_directory(path)
125
+ expanded = File.expand_path(path)
126
+ nested = File.join(expanded, "bluebook")
127
+
128
+ return nested if File.directory?(nested)
129
+ return expanded if File.directory?(expanded)
130
+
131
+ raise Errno::ENOENT, "no such domain directory: #{path}"
132
+ end
133
+
134
+ # THE DOMAIN YOU ARE STANDING IN. Walks up from `from` — the way git
135
+ # finds `.git` — and answers the nearest directory a boot would accept,
136
+ # or nil if there is not one above you.
137
+ #
138
+ # MARKED BY A `.hecksagon`, NOT BY A `.bluebook`. Chapters are
139
+ # everywhere: era translations, the language's own self-hosted grammar,
140
+ # and `spec/fixtures`, which holds a dozen unrelated ones in a single
141
+ # directory. A `.hecksagon` is the file that says "this is a domain, and
142
+ # here is how its aggregates are stored", which is exactly the claim a
143
+ # caller is relying on when they omit the path.
144
+ #
145
+ # Both layouts, because `bluebook_directory` above accepts both: a
146
+ # domain directory holding a `bluebook/` subdirectory (every example in
147
+ # this corpus), or one holding the files directly.
148
+ # NORMALISED TO THE OUTER DIRECTORY. Standing in `examples/banking/bluebook`,
149
+ # the `.hecksagon` is right there, so a plain walk stops on the
150
+ # `bluebook/` directory itself. Both boot identically — `bluebook_directory`
151
+ # accepts either and `Loader.boot` takes `File.dirname` of what it gets,
152
+ # so the registry root comes out the same — but `examples/banking` is the
153
+ # directory a person names, and the one a `.world`'s `dir "data"` reads
154
+ # as relative to.
155
+ def domain_root(from = Dir.pwd)
156
+ found = nearest_domain(File.expand_path(from))
157
+ return nil unless found
158
+
159
+ parent = File.dirname(found)
160
+ File.basename(found) == "bluebook" && domain?(parent) ? parent : found
161
+ end
162
+
163
+ def nearest_domain(current)
164
+ loop do
165
+ return current if domain?(current)
166
+
167
+ parent = File.dirname(current)
168
+ return nil if parent == current
169
+
170
+ current = parent
171
+ end
172
+ end
173
+
174
+ def domain?(directory)
175
+ !Dir[File.join(directory, "*.hecksagon")].empty? ||
176
+ !Dir[File.join(directory, "bluebook", "*.hecksagon")].empty?
177
+ end
178
+
179
+ def shared_root(given, directory)
180
+ return File.expand_path(given) if given
181
+
182
+ current = directory
183
+ loop do
184
+ return current if File.directory?(File.join(current, PORTS)) ||
185
+ File.directory?(File.join(current, ADAPTERS))
186
+
187
+ parent = File.dirname(current)
188
+ return nil if parent == current
189
+
190
+ current = parent
191
+ end
192
+ end
193
+
194
+ def library(folder)
195
+ File.expand_path("../../#{folder}", __dir__)
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,3 @@
1
+ Hecks.adapter "GoogleAuthentication" do
2
+ port "authentication"
3
+ end
@@ -0,0 +1,100 @@
1
+ require "securerandom"
2
+
3
+ require_relative "../../ports/authentication"
4
+
5
+ module Hecks
6
+ module Adapters
7
+ # GOOGLE'S OWN OIDC HANDSHAKE — the `authentication` port's one real
8
+ # implementation today, moved here from being hand-rolled per-app
9
+ # (an embryonaut_console `google_auth.rb` used to do exactly this;
10
+ # any hecks-based app gets Google sign-in for free now, the
11
+ # same "one adapter, reusable everywhere" value every other adapter
12
+ # in this directory already has).
13
+ #
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`.
21
+ #
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.
32
+ module GoogleAuthentication
33
+ ISSUER = "https://accounts.google.com"
34
+
35
+ module_function
36
+
37
+ def client
38
+ require "oauth2"
39
+ OAuth2::Client.new(
40
+ ENV.fetch("GOOGLE_CLIENT_ID"), ENV.fetch("GOOGLE_CLIENT_SECRET"),
41
+ site: "https://oauth2.googleapis.com",
42
+ authorize_url: "https://accounts.google.com/o/oauth2/v2/auth",
43
+ token_url: "/token"
44
+ )
45
+ end
46
+
47
+ # The URL to send a browser to, carrying a fresh CSRF `state` the
48
+ # caller is responsible for stashing (a session, typically) and
49
+ # checking again in `verify`.
50
+ def authorization_url
51
+ state = SecureRandom.hex(24)
52
+ url = client.auth_code.authorize_url(
53
+ redirect_uri: ENV.fetch("GOOGLE_REDIRECT_URI"),
54
+ scope: "openid email profile",
55
+ state: state
56
+ )
57
+ [url, state]
58
+ end
59
+
60
+ # Exchanges `code` for tokens, verifies the returned ID token's
61
+ # signature and claims against Google's own JWKS, and confirms
62
+ # `state` matches what `authorization_url` handed out — a
63
+ # mismatch (or any failure in the exchange/verification itself)
64
+ # is a Ports::Authentication::ValidationError, never a
65
+ # silently-empty result. Returns
66
+ # {issuer:, subject:, email:, email_verified:} on success — never
67
+ # the raw token, never anything a caller would need to re-verify
68
+ # itself. `email_verified` rides along because a caller granting
69
+ # access off this email needs to know Google actually checked it.
70
+ def verify(code:, state:, expected_state:)
71
+ # BOTH GEMS, BEFORE ANYTHING ELSE — not staggered further down
72
+ # this method: the rescue clause below names GoogleIDToken
73
+ # ::ValidationError, and Ruby resolves that constant reference
74
+ # at the moment an exception is being matched, not at parse
75
+ # time. An OAuth2::Error raised before a later `require
76
+ # "google-id-token"` line ran would make the rescue clause
77
+ # itself blow up on an undefined constant, masking the real
78
+ # error. Requiring both up front means whichever one raises
79
+ # first, the constant is already there to catch it.
80
+ require "oauth2"
81
+ require "google-id-token"
82
+
83
+ unless state && expected_state && state == expected_state
84
+ raise Ports::Authentication::ValidationError, "state mismatch"
85
+ end
86
+
87
+ token = client.auth_code.get_token(code, redirect_uri: ENV.fetch("GOOGLE_REDIRECT_URI"))
88
+ id_token = token.params["id_token"] or raise Ports::Authentication::ValidationError, "no id_token in the response"
89
+
90
+ payload = GoogleIDToken::Validator.new.check(id_token, ENV.fetch("GOOGLE_CLIENT_ID"))
91
+ {
92
+ issuer: payload.fetch("iss"), subject: payload.fetch("sub"),
93
+ email: payload["email"], email_verified: [true, "true"].include?(payload["email_verified"])
94
+ }
95
+ rescue OAuth2::Error, GoogleIDToken::ValidationError => e
96
+ raise Ports::Authentication::ValidationError, e.message
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,3 @@
1
+ Hecks.adapter "GovernanceAuthorization" do
2
+ port "authorization"
3
+ end
@@ -0,0 +1,101 @@
1
+ require "time"
2
+
3
+ module Hecks
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 historical one : `RoleAssignment`
16
+ # answers with every assignment an actor has ever held
17
+ # (`AssignmentsForActor`'s own description — "currently or
18
+ # historically") and leaves `ends_at` for the caller to read, the
19
+ # same deferral `Governance::RoleTransition.Allowed` makes for the
20
+ # same reason. This is that caller.
21
+ module GovernanceAuthorization
22
+ module_function
23
+
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.
32
+ def holds_role?(registry, actor_id:, role:, as_of: nil, scope: nil)
33
+ rows = Runtime::Dispatcher.new(registry).query(
34
+ "Governance::RoleAssignment.AssignmentsForActor",
35
+ actor_id: { value: actor_id.to_s }
36
+ )
37
+
38
+ rows.any? do |row|
39
+ row[:role_name][:value] == role.to_s &&
40
+ row[:ends_at].nil? &&
41
+ in_scope?(row, scope) &&
42
+ started?(row, as_of)
43
+ end
44
+ end
45
+
46
+ # `scope` UNCHECKED WHEN NOT STATED, same as every other opt-in
47
+ # field here — a caller that never says which scope it is acting
48
+ # in gets the pre-scope behavior: any live assignment for the role
49
+ # authorizes, everywhere. A caller that does state one only
50
+ # authorizes against an assignment granted for THAT scope.
51
+ def in_scope?(row, scope)
52
+ scope.nil? || row[:scope][:value] == scope.to_s
53
+ end
54
+
55
+ # `starts_at` IS A FREE-TEXT STRING in the bluebook (`Timestamp`'s
56
+ # only invariant is "present", not any particular format) — parsed
57
+ # here with `Time.parse` rather than compared lexically, since
58
+ # nothing guarantees every caller writes it zero-padded ISO 8601.
59
+ # FAILS CLOSED : a `starts_at` that does not parse is treated as
60
+ # not-yet-started rather than silently ignored, the same direction
61
+ # every other check in this method already fails.
62
+ def started?(row, as_of)
63
+ return true if as_of.nil?
64
+
65
+ Time.parse(row[:starts_at][:value].to_s).to_i <= as_of
66
+ rescue ArgumentError, TypeError
67
+ false
68
+ end
69
+
70
+ # THE OTHER HALF — may role X act as role Y. `RoleTransition.Allowed`
71
+ # is identified by the exact pair, so at most one row ever comes
72
+ # back ; still read as `.any?` rather than trusting that structurally,
73
+ # the same defensiveness `holds_role?` already has to have anyway
74
+ # since `AssignmentsForActor` can return several.
75
+ def authorized_as?(registry, from_role:, to_role:)
76
+ rows = Runtime::Dispatcher.new(registry).query(
77
+ "Governance::RoleTransition.Allowed",
78
+ from_role: { value: from_role.to_s }, to_role: { value: to_role.to_s }
79
+ )
80
+
81
+ rows.any? { |row| row[:ends_at].nil? }
82
+ end
83
+
84
+ # THE ROLE ITSELF, not just a yes/no about one — the same
85
+ # `AssignmentsForActor` query `holds_role?` runs, just returning
86
+ # the live (non-revoked) row's `role_name` instead of comparing it
87
+ # against a caller-supplied guess. `nil` for no live assignment at
88
+ # all — the caller's own fallback (an aggregate's own role field,
89
+ # a default) is domain-specific and does not belong here.
90
+ def live_role_for(registry, actor_id:)
91
+ rows = Runtime::Dispatcher.new(registry).query(
92
+ "Governance::RoleAssignment.AssignmentsForActor",
93
+ actor_id: { value: actor_id.to_s }
94
+ )
95
+
96
+ live = rows.find { |row| row[:ends_at].nil? }
97
+ live && live[:role_name][:value]
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,56 @@
1
+ require_relative "../../../ports/persistence/append_only"
2
+
3
+ module Hecks
4
+ module Adapters
5
+ class Heki
6
+ # The append-only journal beside the snapshot: one JSON line per
7
+ # entry, fsynced on append, replayed over the snapshot on read.
8
+ module Journal
9
+ def entries
10
+ return [] unless File.exist?(@journal_path)
11
+
12
+ File.readlines(@journal_path, chomp: true).reject(&:empty?).map do |line|
13
+ value = JSON.parse(line)
14
+ state = value["state"]&.transform_keys(&:to_sym)
15
+ Ports::Persistence::Entry.new(operation: value.fetch("operation"), id: value.fetch("id"), state: state,
16
+ mirrors: value["mirrors"])
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def replay_journal(records)
23
+ return records unless File.exist?(@journal_path)
24
+
25
+ File.foreach(@journal_path) do |line|
26
+ entry = JSON.parse(line)
27
+ id = entry.fetch("id")
28
+
29
+ case entry.fetch("operation")
30
+ when "save" then records[id] = entry.fetch("state")
31
+ when "delete" then records.delete(id)
32
+ else raise Malformed, "#{@journal_path}: unknown journal operation #{entry.fetch("operation").inspect}"
33
+ end
34
+ end
35
+ records
36
+ rescue JSON::ParserError => e
37
+ raise Malformed, "#{@journal_path}: json error: #{e.message}"
38
+ end
39
+
40
+ def append_entry(operation, id, state)
41
+ line = "#{JSON.generate(operation: operation, id: id.to_s, state: state, mirrors: @entry_mirrors)}\n"
42
+
43
+ # One write, not JSON-then-newline as two: two concurrent
44
+ # appends can only interleave *between* writes, never inside
45
+ # one, so this line can't come out split by another process's
46
+ # line landing in the middle of it.
47
+ File.open(@journal_path, "ab") do |journal|
48
+ journal.write(line)
49
+ journal.flush
50
+ journal.fsync
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,99 @@
1
+ require_relative "snapshot"
2
+ require_relative "journal"
3
+
4
+ module Hecks
5
+ module Adapters
6
+ class Heki
7
+ # THE OPTIONAL saga-persistence capability (§2), Heki's own shape —
8
+ # a SIBLING snapshot+journal file pair, built the exact same way an
9
+ # aggregate's own persistence already is: `Snapshot`/`Journal`
10
+ # (heki/snapshot.rb, heki/journal.rb) operate generically on
11
+ # `@path`/`@journal_path`/`@entry_mirrors` and never touch
12
+ # `@aggregate`, so this reuses them unchanged rather than
13
+ # re-deriving the same binary framing and crash-recovery replay.
14
+ #
15
+ # Reserved file name (`hecks_saga_instances.heki`, matching the
16
+ # `hecks_`-prefix convention every other new saga table in this
17
+ # work uses) avoids colliding with any real aggregate's own
18
+ # `storage_name`. Lives in the SAME directory an aggregate's own
19
+ # `.heki` file would (`File.dirname(@path)`, `Heki`'s own call
20
+ # below) — which, since Heki's `resolve_path` has no per-domain
21
+ # component at all, is typically shared across EVERY domain
22
+ # booted from the same `root`. `domain` is therefore carried
23
+ # inside each record and filtered on read, the same reason
24
+ # Postgres's own `hecks_saga_instances` keeps an explicit `domain`
25
+ # column under schema isolation (§3).
26
+ #
27
+ # ONE flat records hash, keyed by a composite string (Heki's own
28
+ # snapshot format is id-keyed, not tuple-keyed) — never exposed
29
+ # outside this class; `each_saga` yields the five real fields a
30
+ # caller actually wants, not the internal key shape.
31
+ #
32
+ # Locked the same way an aggregate's own store is: `with_lock`
33
+ # (`Snapshot`, shared) serializes each save/delete's read-modify-
34
+ # write against `@path`'s own lock file — a saga gets exactly the
35
+ # durability and concurrency-safety this adapter already gives its
36
+ # aggregates, no better, no worse.
37
+ class SagaStore
38
+ include Snapshot
39
+ include Journal
40
+
41
+ def initialize(dir)
42
+ @path = File.join(dir, "hecks_saga_instances.heki")
43
+ @journal_path = "#{@path}.journal"
44
+ @entry_mirrors = nil
45
+ end
46
+
47
+ def save_saga(domain, process_manager, correlation, state, memory, completed_compensations = [])
48
+ key = key_for(domain, process_manager, correlation)
49
+ record = { "domain" => domain, "process_manager" => process_manager,
50
+ "correlation" => correlation, "state" => state, "memory" => memory,
51
+ "completed_compensations" => completed_compensations }
52
+
53
+ with_lock do
54
+ append_entry("save", key, record)
55
+ current = replay_journal(read_snapshot)
56
+ current[key] = record
57
+ write(current)
58
+ @store = current
59
+ end
60
+ end
61
+
62
+ def delete_saga(domain, process_manager, correlation)
63
+ key = key_for(domain, process_manager, correlation)
64
+
65
+ with_lock do
66
+ append_entry("delete", key, nil)
67
+ current = replay_journal(read_snapshot)
68
+ current.delete(key)
69
+ write(current)
70
+ @store = current
71
+ end
72
+ end
73
+
74
+ def each_saga(domain)
75
+ return enum_for(:each_saga, domain) unless block_given?
76
+
77
+ store.each_value do |record|
78
+ next unless record["domain"] == domain
79
+
80
+ yield record["process_manager"], record["correlation"], record["state"],
81
+ (record["memory"] || {}).transform_keys(&:to_sym),
82
+ record["completed_compensations"] || []
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def store
89
+ @store ||= replay_journal(read_snapshot)
90
+ end
91
+
92
+ # A plain space-joined key, not anything fancier — a correlation value is real
93
+ # business data (an order id, a customer reference) and nothing
94
+ # here should assume it never contains a space.
95
+ def key_for(domain, process_manager, correlation) = [domain, process_manager, correlation].join(" ")
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,65 @@
1
+ require "zlib"
2
+
3
+ module Hecks
4
+ module Adapters
5
+ class Heki
6
+ # The binary snapshot codec: magic-header framing around
7
+ # deflate-compressed, id-sorted JSON. Reading refuses loudly on a
8
+ # short file, a bad magic, or a body neither zlib nor JSON will own.
9
+ module Snapshot
10
+ private
11
+
12
+ def read_snapshot
13
+ return {} unless File.exist?(@path)
14
+
15
+ data = File.binread(@path)
16
+ raise Malformed, "#{@path}: too short" if data.bytesize < HEADER_BYTES
17
+ raise Malformed, "#{@path}: bad magic" unless data[0, 4] == MAGIC
18
+
19
+ JSON.parse(Zlib::Inflate.inflate(data[HEADER_BYTES..]))
20
+ rescue Zlib::DataError => e
21
+ raise Malformed, "#{@path}: zlib error: #{e.message}"
22
+ rescue JSON::ParserError => e
23
+ raise Malformed, "#{@path}: json error: #{e.message}"
24
+ end
25
+
26
+ # Temp-file-plus-rename, fsynced before the rename: 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.
31
+ def write(records)
32
+ sorted = records.sort_by { |id, _| id }.to_h
33
+ json = JSON.generate(sorted)
34
+ body = MAGIC + [sorted.size].pack("N") + Zlib::Deflate.deflate(json, Zlib::BEST_COMPRESSION)
35
+ tmp = "#{@path}.tmp.#{Process.pid}.#{object_id}"
36
+
37
+ File.open(tmp, "wb") do |file|
38
+ file.write(body)
39
+ file.flush
40
+ file.fsync
41
+ end
42
+ File.rename(tmp, @path)
43
+ end
44
+
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.
52
+ def with_lock
53
+ File.open(lock_path, File::CREAT | File::RDWR, 0o644) do |lock|
54
+ lock.flock(File::LOCK_EX)
55
+ yield
56
+ ensure
57
+ lock.flock(File::LOCK_UN)
58
+ end
59
+ end
60
+
61
+ def lock_path = "#{@path}.lock"
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,4 @@
1
+ Hecks.adapter "Heki" do
2
+ port "persistence"
3
+ field :dir
4
+ end