hecks 0.2.0 → 0.3.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 (408) 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 +403 -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 +62 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +51 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +38 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +148 -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 +131 -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 +80 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +168 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +391 -0
  31. data/lib/hecks/adapters/driven/postgres_era/lineage/era_store.rb +171 -0
  32. data/lib/hecks/adapters/driven/postgres_era/lineage/field_cache.rb +190 -0
  33. data/lib/hecks/adapters/driven/postgres_era/lineage/head_compiler.rb +429 -0
  34. data/lib/hecks/adapters/driven/postgres_era/lineage/mint_transaction.rb +166 -0
  35. data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +286 -0
  36. data/lib/hecks/adapters/driven/postgres_era/lineage/resumable_backfill.rb +168 -0
  37. data/lib/hecks/adapters/driven/postgres_era/lineage/tail_merge.rb +163 -0
  38. data/lib/hecks/adapters/driven/postgres_era/lineage/transform_installer.rb +114 -0
  39. data/lib/hecks/adapters/driven/postgres_era/lineage.rb +137 -0
  40. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/coverage_check.rb +89 -0
  41. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +82 -0
  42. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  43. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/minter.rb +132 -0
  44. data/lib/hecks/adapters/driven/postgres_era/lineage_manager.rb +74 -0
  45. data/lib/hecks/adapters/driven/postgres_era.adapter +6 -0
  46. data/lib/hecks/adapters/driven/postgres_era.rb +671 -0
  47. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  48. data/lib/hecks/adapters/driven/prism.rb +80 -0
  49. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  50. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  51. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  52. data/lib/hecks/adapters/driven/sqlite/codec.rb +63 -0
  53. data/lib/hecks/adapters/driven/sqlite/projection.rb +105 -0
  54. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +180 -0
  55. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  56. data/lib/hecks/adapters/driven/sqlite.rb +285 -0
  57. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  58. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  59. data/lib/hecks/adapters/driven.rb +36 -0
  60. data/lib/hecks/adapters.rb +6 -0
  61. data/lib/hecks/behaviors/dsl.rb +96 -0
  62. data/lib/hecks/behaviors/expectations.rb +280 -0
  63. data/lib/hecks/behaviors/ir.rb +31 -0
  64. data/lib/hecks/behaviors/rspec.rb +42 -0
  65. data/lib/hecks/behaviors/runner.rb +96 -0
  66. data/lib/hecks/behaviors.rb +25 -0
  67. data/lib/hecks/bluebook/aggregate.rb +108 -0
  68. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  69. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  70. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  71. data/lib/hecks/bluebook/assembly/contracts.rb +409 -0
  72. data/lib/hecks/bluebook/assembly/marks.rb +227 -0
  73. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  74. data/lib/hecks/bluebook/assembly.rb +78 -0
  75. data/lib/hecks/bluebook/attribute.rb +96 -0
  76. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  77. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  78. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  79. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  80. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  81. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  82. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  83. data/lib/hecks/bluebook/behaviour/lifecycle.rb +51 -0
  84. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  85. data/lib/hecks/bluebook/behaviour/process_manager.rb +38 -0
  86. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  87. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  88. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  89. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  90. data/lib/hecks/bluebook/chapter.rb +68 -0
  91. data/lib/hecks/bluebook/command.rb +124 -0
  92. data/lib/hecks/bluebook/domain_port.rb +102 -0
  93. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  94. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +906 -0
  95. data/lib/hecks/bluebook/dsl/attribute_collector.rb +327 -0
  96. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  97. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1018 -0
  98. data/lib/hecks/bluebook/dsl/command_builder.rb +626 -0
  99. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  100. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  101. data/lib/hecks/bluebook/dsl/entity_builder.rb +305 -0
  102. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  103. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  104. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  105. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  106. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  107. data/lib/hecks/bluebook/dsl/policy_builder.rb +120 -0
  108. data/lib/hecks/bluebook/dsl/port_builder.rb +30 -0
  109. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  110. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +207 -0
  111. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  112. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  113. data/lib/hecks/bluebook/dsl/rule_reference.rb +172 -0
  114. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  115. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  116. data/lib/hecks/bluebook/dsl/word_gate.rb +221 -0
  117. data/lib/hecks/bluebook/dsl/world_builder.rb +77 -0
  118. data/lib/hecks/bluebook/dsl.rb +45 -0
  119. data/lib/hecks/bluebook/entity.rb +103 -0
  120. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -0
  121. data/lib/hecks/bluebook/expression/evaluator.rb +261 -0
  122. data/lib/hecks/bluebook/expression/projection.json +170 -0
  123. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  124. data/lib/hecks/bluebook/expression/resolver.rb +630 -0
  125. data/lib/hecks/bluebook/expression.rb +13 -0
  126. data/lib/hecks/bluebook/hexagon.rb +60 -0
  127. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  128. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  129. data/lib/hecks/bluebook/meta_validator/judge.rb +559 -0
  130. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  131. data/lib/hecks/bluebook/meta_validator/port_judge.rb +47 -0
  132. data/lib/hecks/bluebook/meta_validator/readings.rb +350 -0
  133. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +312 -0
  134. data/lib/hecks/bluebook/meta_validator/shapes.rb +246 -0
  135. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  136. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  137. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  138. data/lib/hecks/bluebook/meta_validator.rb +452 -0
  139. data/lib/hecks/bluebook/model_check.rb +318 -0
  140. data/lib/hecks/bluebook/pattern_subset.rb +120 -0
  141. data/lib/hecks/bluebook/policy.rb +41 -0
  142. data/lib/hecks/bluebook/process_manager.rb +86 -0
  143. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  144. data/lib/hecks/bluebook/project_loader.rb +40 -0
  145. data/lib/hecks/bluebook/project_register.rb +82 -0
  146. data/lib/hecks/bluebook/query.rb +61 -0
  147. data/lib/hecks/bluebook/read_model.rb +109 -0
  148. data/lib/hecks/bluebook/reference.rb +74 -0
  149. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  150. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  151. data/lib/hecks/bluebook/translation.rb +92 -0
  152. data/lib/hecks/bluebook/value_object.rb +58 -0
  153. data/lib/hecks/bluebook.rb +74 -0
  154. data/lib/hecks/codemod.rb +342 -0
  155. data/lib/hecks/construct.rb +71 -0
  156. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  157. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  158. data/lib/hecks/deploy/oidc.json +18 -0
  159. data/lib/hecks/doc/reference.rb +389 -0
  160. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  161. data/lib/hecks/facade/cli_door.rb +116 -0
  162. data/lib/hecks/facade/cli_runner.rb +190 -0
  163. data/lib/hecks/facade/command_request.rb +105 -0
  164. data/lib/hecks/facade/handle.rb +173 -0
  165. data/lib/hecks/facade/json_door.rb +154 -0
  166. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  167. data/lib/hecks/facade/surface/chapter.rb +107 -0
  168. data/lib/hecks/facade/surface.rb +48 -0
  169. data/lib/hecks/facade.rb +44 -0
  170. data/lib/hecks/forms/app.rb +300 -0
  171. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  172. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  173. data/lib/hecks/forms/field_renderer.rb +170 -0
  174. data/lib/hecks/forms/field_shape.rb +232 -0
  175. data/lib/hecks/forms/html.rb +53 -0
  176. data/lib/hecks/forms/index_renderer.rb +35 -0
  177. data/lib/hecks/forms/page.rb +157 -0
  178. data/lib/hecks/forms/params.rb +132 -0
  179. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  180. data/lib/hecks/forms/record_renderer.rb +115 -0
  181. data/lib/hecks/forms/record_table.rb +63 -0
  182. data/lib/hecks/forms/reference_options.rb +30 -0
  183. data/lib/hecks/forms/value_object_shape.rb +46 -0
  184. data/lib/hecks/forms.rb +54 -0
  185. data/lib/hecks/fqn.rb +94 -0
  186. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  187. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  188. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  189. data/lib/hecks/framework/bluebook/governance.bluebook +130 -0
  190. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  191. data/lib/hecks/framework/oidc.json +39 -0
  192. data/lib/hecks/framework.rb +90 -0
  193. data/lib/hecks/freezer.rb +67 -0
  194. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  195. data/lib/hecks/fuzzing/isolated_boot.rb +92 -0
  196. data/lib/hecks/fuzzing/properties.rb +1146 -0
  197. data/lib/hecks/fuzzing/replay.rb +635 -0
  198. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  199. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +54 -0
  200. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  201. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +185 -0
  202. data/lib/hecks/fuzzing/sequence_generator.rb +130 -0
  203. data/lib/hecks/fuzzing/value_generator.rb +177 -0
  204. data/lib/hecks/fuzzing.rb +12 -0
  205. data/lib/hecks/grammar/evolve.rb +291 -0
  206. data/lib/hecks/grammar/expression.bluebook +417 -0
  207. data/lib/hecks/grammar/expression_operators.json +1152 -0
  208. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  209. data/lib/hecks/grammar/oidc.json +69 -0
  210. data/lib/hecks/grammar/translation.bluebook +203 -0
  211. data/lib/hecks/grammar.rb +178 -0
  212. data/lib/hecks/ir.rb +126 -0
  213. data/lib/hecks/language/adapter.bluebook +116 -0
  214. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  215. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  216. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  217. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  218. data/lib/hecks/language/bluebook/command.bluebook +437 -0
  219. data/lib/hecks/language/bluebook/entity.bluebook +316 -0
  220. data/lib/hecks/language/bluebook/policy.bluebook +172 -0
  221. data/lib/hecks/language/bluebook/process_manager.bluebook +264 -0
  222. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  223. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  224. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  225. data/lib/hecks/language/bluebook/syntax.bluebook +447 -0
  226. data/lib/hecks/language/bluebook/vocabulary.bluebook +364 -0
  227. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  228. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  229. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  230. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  231. data/lib/hecks/language/oidc.json +313 -0
  232. data/lib/hecks/language/port.bluebook +92 -0
  233. data/lib/hecks/language/translation/translation.bluebook +110 -0
  234. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  235. data/lib/hecks/language/world/wiring.bluebook +62 -0
  236. data/lib/hecks/language/world/world.bluebook +84 -0
  237. data/lib/hecks/literal.rb +125 -0
  238. data/lib/hecks/naming.rb +121 -0
  239. data/lib/hecks/ports/access_control.port +4 -0
  240. data/lib/hecks/ports/access_control.rb +62 -0
  241. data/lib/hecks/ports/agent/answers.rb +104 -0
  242. data/lib/hecks/ports/agent.port +4 -0
  243. data/lib/hecks/ports/agent.rb +167 -0
  244. data/lib/hecks/ports/authentication.port +4 -0
  245. data/lib/hecks/ports/authentication.rb +50 -0
  246. data/lib/hecks/ports/authorization.port +4 -0
  247. data/lib/hecks/ports/authorization.rb +59 -0
  248. data/lib/hecks/ports/clock.port +4 -0
  249. data/lib/hecks/ports/clock.rb +62 -0
  250. data/lib/hecks/ports/extraction.port +4 -0
  251. data/lib/hecks/ports/extraction.rb +37 -0
  252. data/lib/hecks/ports/identity_assignment.port +4 -0
  253. data/lib/hecks/ports/identity_assignment.rb +45 -0
  254. data/lib/hecks/ports/identity_generation.port +4 -0
  255. data/lib/hecks/ports/identity_generation.rb +49 -0
  256. data/lib/hecks/ports/identity_resolution.port +4 -0
  257. data/lib/hecks/ports/identity_resolution.rb +40 -0
  258. data/lib/hecks/ports/loading.port +4 -0
  259. data/lib/hecks/ports/loading.rb +13 -0
  260. data/lib/hecks/ports/persistence/append_only.rb +102 -0
  261. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  262. data/lib/hecks/ports/persistence/execution.rb +18 -0
  263. data/lib/hecks/ports/persistence/lineage.rb +277 -0
  264. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  265. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  266. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  267. data/lib/hecks/ports/persistence.port +4 -0
  268. data/lib/hecks/ports/persistence.rb +39 -0
  269. data/lib/hecks/ports/projection.port +4 -0
  270. data/lib/hecks/ports/projection.rb +76 -0
  271. data/lib/hecks/ports/query/in_memory.rb +60 -0
  272. data/lib/hecks/ports/query/ordering.rb +41 -0
  273. data/lib/hecks/ports/query.rb +36 -0
  274. data/lib/hecks/ports.rb +27 -0
  275. data/lib/hecks/projections/diagrams.rb +413 -0
  276. data/lib/hecks/projections/ir.rb +18 -0
  277. data/lib/hecks/projections/model/deviations.rb +98 -0
  278. data/lib/hecks/projections/model.rb +145 -0
  279. data/lib/hecks/projections/oidc.rb +110 -0
  280. data/lib/hecks/projections/parser_table.rb +159 -0
  281. data/lib/hecks/projections/reference.rb +38 -0
  282. data/lib/hecks/projections/shape.rb +31 -0
  283. data/lib/hecks/projections/statements.rb +110 -0
  284. data/lib/hecks/projections/vocabulary.rb +100 -0
  285. data/lib/hecks/projections.rb +32 -0
  286. data/lib/hecks/projector/cli_projector.rb +437 -0
  287. data/lib/hecks/projector/docs_projector.rb +321 -0
  288. data/lib/hecks/projector/exporter.rb +141 -0
  289. data/lib/hecks/projector/ir_projector.rb +18 -0
  290. data/lib/hecks/projector/narrate_projector.rb +243 -0
  291. data/lib/hecks/projector/target.rb +97 -0
  292. data/lib/hecks/projector.rb +186 -0
  293. data/lib/hecks/query_ir.rb +392 -0
  294. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  295. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  296. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  297. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  298. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  299. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  300. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  301. data/lib/hecks/query_specification/common/null_policy.rb +80 -0
  302. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  303. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  304. data/lib/hecks/query_specification/common/options.rb +32 -0
  305. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  306. data/lib/hecks/query_specification/common/specification.rb +10 -0
  307. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  308. data/lib/hecks/query_specification/field_path.rb +89 -0
  309. data/lib/hecks/query_specification/hop_path.rb +130 -0
  310. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  311. data/lib/hecks/query_specification.rb +14 -0
  312. data/lib/hecks/rendering.rb +48 -0
  313. data/lib/hecks/router/namespace_installer.rb +157 -0
  314. data/lib/hecks/router.rb +70 -0
  315. data/lib/hecks/runtime/caller.rb +50 -0
  316. data/lib/hecks/runtime/capability_graph.rb +44 -0
  317. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +124 -0
  318. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +248 -0
  319. data/lib/hecks/runtime/command_interpreter.rb +413 -0
  320. data/lib/hecks/runtime/command_rules/admissibility.rb +307 -0
  321. data/lib/hecks/runtime/command_rules/arithmetic.rb +254 -0
  322. data/lib/hecks/runtime/command_rules/authorization.rb +64 -0
  323. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  324. data/lib/hecks/runtime/command_rules/references.rb +181 -0
  325. data/lib/hecks/runtime/command_rules.rb +28 -0
  326. data/lib/hecks/runtime/dependency_planning.rb +231 -0
  327. data/lib/hecks/runtime/dispatcher.rb +286 -0
  328. data/lib/hecks/runtime/entity_element.rb +253 -0
  329. data/lib/hecks/runtime/entity_interpreter.rb +228 -0
  330. data/lib/hecks/runtime/era_check.rb +136 -0
  331. data/lib/hecks/runtime/era_guard/shape_diff.rb +128 -0
  332. data/lib/hecks/runtime/era_guard.rb +193 -0
  333. data/lib/hecks/runtime/era_tamper.rb +61 -0
  334. data/lib/hecks/runtime/errors.rb +100 -0
  335. data/lib/hecks/runtime/event.rb +51 -0
  336. data/lib/hecks/runtime/identity.rb +128 -0
  337. data/lib/hecks/runtime/instance.rb +120 -0
  338. data/lib/hecks/runtime/interpreting.rb +69 -0
  339. data/lib/hecks/runtime/loader.rb +143 -0
  340. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  341. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  342. data/lib/hecks/runtime/query_interpreter.rb +254 -0
  343. data/lib/hecks/runtime/reaction_invocation.rb +245 -0
  344. data/lib/hecks/runtime/read_model_interpreter.rb +266 -0
  345. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  346. data/lib/hecks/runtime/reference_hop.rb +99 -0
  347. data/lib/hecks/runtime/refusal_wording.rb +124 -0
  348. data/lib/hecks/runtime/registry/saga_persistence.rb +70 -0
  349. data/lib/hecks/runtime/registry/verification.rb +158 -0
  350. data/lib/hecks/runtime/registry.rb +218 -0
  351. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  352. data/lib/hecks/runtime/routing.rb +96 -0
  353. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  354. data/lib/hecks/runtime/saga_interpreter.rb +301 -0
  355. data/lib/hecks/runtime/storage_shape.rb +120 -0
  356. data/lib/hecks/runtime/tenant_check.rb +84 -0
  357. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  358. data/lib/hecks/runtime/value/admission.rb +112 -0
  359. data/lib/hecks/runtime/value/coercion.rb +500 -0
  360. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  361. data/lib/hecks/runtime/value.rb +125 -0
  362. data/lib/hecks/runtime.rb +95 -0
  363. data/lib/hecks/translation/audit/approval_digest.rb +31 -0
  364. data/lib/hecks/translation/audit/layer_one.rb +37 -0
  365. data/lib/hecks/translation/audit/layer_two.rb +77 -0
  366. data/lib/hecks/translation/audit/unfed_report.rb +42 -0
  367. data/lib/hecks/translation/audit.rb +70 -0
  368. data/lib/hecks/translation/reattest.rb +72 -0
  369. data/lib/hecks/translation/rule_compiler.rb +120 -0
  370. data/lib/hecks/translation/scaffold/differ.rb +183 -0
  371. data/lib/hecks/translation/scaffold/renderer.rb +41 -0
  372. data/lib/hecks/translation/scaffold/writer.rb +28 -0
  373. data/lib/hecks/translation/scaffold.rb +30 -0
  374. data/lib/hecks/translation.rb +11 -0
  375. data/lib/hecks/version.rb +16 -0
  376. data/lib/hecks/vocabulary.rb +213 -0
  377. data/lib/hecks.rb +118 -6
  378. metadata +401 -222
  379. data/bin/hecks +0 -7
  380. data/bin/hecks-package +0 -65
  381. data/bin/hecks_console +0 -12
  382. data/bin/hecks_serverless +0 -6
  383. data/lib/cli/build.rb +0 -14
  384. data/lib/cli/command_runner.rb +0 -28
  385. data/lib/cli/console.rb +0 -10
  386. data/lib/cli/generate.rb +0 -37
  387. data/lib/cli/hecks-cli.rb +0 -27
  388. data/lib/cli/test.rb +0 -57
  389. data/lib/console/commands.rb +0 -8
  390. data/lib/console/hecks-console.rb +0 -1
  391. data/lib/packager/README.md +0 -0
  392. data/lib/packager/app_runner.rb +0 -21
  393. data/lib/packager/args.rb +0 -26
  394. data/lib/packager/compatibility/fixnum.rb +0 -6
  395. data/lib/packager/hecks.rb +0 -39
  396. data/lib/packager/query_runner.rb +0 -21
  397. data/lib/packager/resources/Dockerfile +0 -11
  398. data/lib/packager/resources/app_binary +0 -7
  399. data/lib/packager/resources/bundle_config +0 -3
  400. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  401. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  402. data/lib/serverless/Domain +0 -32
  403. data/lib/serverless/cli.rb +0 -75
  404. data/lib/serverless/resources/command_name.js +0 -5
  405. data/lib/serverless/resources/environment.js +0 -7
  406. data/lib/serverless/resources/handler.js.tt +0 -28
  407. data/lib/serverless/resources/run_binary.js +0 -22
  408. 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,62 @@
1
+ module Hecks
2
+ module Adapters
3
+ # THE `authorization` PORT, FULFILLED BY GOVERNANCE — same registry,
4
+ # same boot, so "ask Governance" is a dispatch against records
5
+ # already sitting in the store this adapter is handed, not a bridge
6
+ # to a second runtime. `Runtime::Dispatcher.new(registry)` is cheap
7
+ # to build fresh per call (`Registry#capability_graph`'s own
8
+ # neighbor, `#repository`, does the same kind of on-demand build) —
9
+ # nothing here holds one across calls, so there is no boot-order
10
+ # dependency on WHEN Governance's bluebook loads relative to this
11
+ # adapter, only that it has by the time `holds_role?` is called.
12
+ #
13
+ # AN ACTIVE ASSIGNMENT, not merely a historical one : `RoleAssignment`
14
+ # answers with every assignment an actor has ever held
15
+ # (`AssignmentsForActor`'s own description — "currently or
16
+ # historically") and leaves `ends_at` for the caller to read, the
17
+ # same deferral `Governance::RoleTransition.Allowed` makes for the
18
+ # same reason. This is that caller.
19
+ module GovernanceAuthorization
20
+ module_function
21
+
22
+ def holds_role?(registry, actor_id:, role:)
23
+ rows = Runtime::Dispatcher.new(registry).query(
24
+ "Governance::RoleAssignment.AssignmentsForActor",
25
+ actor_id: { value: actor_id.to_s }
26
+ )
27
+
28
+ rows.any? { |row| row[:role_name][:value] == role.to_s && row[:ends_at].nil? }
29
+ end
30
+
31
+ # THE OTHER HALF — may role X act as role Y. `RoleTransition.Allowed`
32
+ # is identified by the exact pair, so at most one row ever comes
33
+ # back ; still read as `.any?` rather than trusting that structurally,
34
+ # the same defensiveness `holds_role?` already has to have anyway
35
+ # since `AssignmentsForActor` can return several.
36
+ def authorized_as?(registry, from_role:, to_role:)
37
+ rows = Runtime::Dispatcher.new(registry).query(
38
+ "Governance::RoleTransition.Allowed",
39
+ from_role: { value: from_role.to_s }, to_role: { value: to_role.to_s }
40
+ )
41
+
42
+ rows.any? { |row| row[:ends_at].nil? }
43
+ end
44
+
45
+ # THE ROLE ITSELF, not just a yes/no about one — the same
46
+ # `AssignmentsForActor` query `holds_role?` runs, just returning
47
+ # the live (non-revoked) row's `role_name` instead of comparing it
48
+ # against a caller-supplied guess. `nil` for no live assignment at
49
+ # all — the caller's own fallback (an aggregate's own role field,
50
+ # a default) is domain-specific and does not belong here.
51
+ def live_role_for(registry, actor_id:)
52
+ rows = Runtime::Dispatcher.new(registry).query(
53
+ "Governance::RoleAssignment.AssignmentsForActor",
54
+ actor_id: { value: actor_id.to_s }
55
+ )
56
+
57
+ live = rows.find { |row| row[:ends_at].nil? }
58
+ live && live[:role_name][:value]
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,51 @@
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
+ File.open(@journal_path, "ab") do |journal|
42
+ journal.write(JSON.generate(operation: operation, id: id.to_s, state: state, mirrors: @entry_mirrors))
43
+ journal.write("\n")
44
+ journal.flush
45
+ journal.fsync
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,38 @@
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
+ def write(records)
27
+ sorted = records.sort_by { |id, _| id }.to_h
28
+ json = JSON.generate(sorted)
29
+
30
+ File.binwrite(
31
+ @path,
32
+ MAGIC + [sorted.size].pack("N") + Zlib::Deflate.deflate(json, Zlib::BEST_COMPRESSION)
33
+ )
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,4 @@
1
+ Hecks.adapter "Heki" do
2
+ port "persistence"
3
+ field :dir
4
+ end
@@ -0,0 +1,148 @@
1
+ require "json"
2
+ require "fileutils"
3
+ require_relative "heki/snapshot"
4
+ require_relative "heki/journal"
5
+ require_relative "heki/saga_store"
6
+ require_relative "../../ports/persistence/append_only"
7
+ require_relative "../../ports/query/in_memory"
8
+ require_relative "in_memory_ordering"
9
+ require_relative "../../runtime/instance"
10
+
11
+ module Hecks
12
+ module Adapters
13
+ # The file-backed store: a compressed snapshot (heki/snapshot.rb)
14
+ # plus an append-only journal beside it (heki/journal.rb). What stays
15
+ # here is the repository surface — find/all/save/delete, and the
16
+ # entry append/project pair the persistence port drives.
17
+ class Heki
18
+ include Snapshot
19
+ include Journal
20
+
21
+ MAGIC = "HEKI"
22
+ HEADER_BYTES = 8
23
+
24
+ class Malformed < StandardError; end
25
+
26
+ attr_reader :aggregate, :path
27
+
28
+ def initialize(aggregate:, settings: {}, root: nil)
29
+ @aggregate = aggregate
30
+ @path = resolve_path(settings, root)
31
+ @journal_path = "#{@path}.journal"
32
+ @events = []
33
+ # THE OPTIONAL saga-persistence capability's own scoping (§2/§4)
34
+ # — falls back to the aggregate's own name for a directly-
35
+ # instantiated adapter (specs), same fallback shape Postgres's
36
+ # own @domain already uses.
37
+ @domain = (settings[:domain] || settings["domain"] || aggregate.name).to_s
38
+
39
+ FileUtils.mkdir_p(File.dirname(@path))
40
+ end
41
+
42
+ def find(id)
43
+ record = store[id.to_s]
44
+ return nil unless record
45
+
46
+ instance(id.to_s, record)
47
+ end
48
+
49
+ def all(order_by: nil, direction: :asc)
50
+ records = store.sort_by { |id, _| id }.map { |id, record| instance(id, record) }
51
+ InMemoryOrdering.ordered(records, aggregate: @aggregate, order_by: order_by, direction: direction)
52
+ end
53
+
54
+ def count = store.size
55
+
56
+ # `registry: context[:registry]` — Memory's own `query` already
57
+ # threads this through; Heki's own never did, which made
58
+ # `none_in_state?` (Ports::Query::InMemory) unconditionally
59
+ # return `true` (its own graceful "no registry, no way to look
60
+ # the target up" default) for EVERY `none_in_state` where-clause
61
+ # against a Heki-backed aggregate — silently excluding nothing,
62
+ # always, no matter the actual target state.
63
+ def query(specification, args = {}, context: {})
64
+ Ports::Query::InMemory.execute(all, specification, args, registry: context[:registry])
65
+ end
66
+
67
+ def append(entry)
68
+ @entry_mirrors = entry.mirrors
69
+ append_entry(entry.operation, entry.id, entry.state)
70
+ entry
71
+ ensure
72
+ @entry_mirrors = nil
73
+ end
74
+
75
+ def project(entry)
76
+ current = store
77
+ entry.save? ? current[entry.id] = entry.state.dup : current.delete(entry.id)
78
+ write(current)
79
+ @store = current
80
+ entry
81
+ end
82
+
83
+ def save(instance)
84
+ entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
85
+ append(entry)
86
+ project(entry)
87
+ instance
88
+ end
89
+
90
+ def delete(id)
91
+ return false unless find(id)
92
+
93
+ entry = Ports::Persistence::Entry.new(operation: "delete", id: id.to_s, state: nil)
94
+ append(entry)
95
+ project(entry)
96
+ true
97
+ end
98
+
99
+ def record_event(event) = @events << event
100
+
101
+ def events = @events
102
+
103
+ # ── the OPTIONAL saga-persistence capability (§2) — Heki's own
104
+ # shape (a sibling snapshot+journal file pair, `SagaStore`,
105
+ # heki/saga_store.rb) rather than a table in a store this adapter
106
+ # doesn't have.
107
+ def save_saga(process_manager:, correlation:, state:, memory:)
108
+ saga_store.save_saga(@domain, process_manager.to_s, correlation.to_s, state.to_s, memory)
109
+ end
110
+
111
+ def delete_saga(process_manager:, correlation:)
112
+ saga_store.delete_saga(@domain, process_manager.to_s, correlation.to_s)
113
+ end
114
+
115
+ def each_saga(&block) = saga_store.each_saga(@domain, &block)
116
+
117
+ private
118
+
119
+ def saga_store
120
+ @saga_store ||= SagaStore.new(File.dirname(@path))
121
+ end
122
+
123
+ def instance(id, record)
124
+ Runtime::Instance.new(
125
+ aggregate: @aggregate,
126
+ id: id,
127
+ state: record.transform_keys(&:to_sym)
128
+ )
129
+ end
130
+
131
+ def store
132
+ @store ||= read
133
+ end
134
+
135
+ def read
136
+ snapshot = read_snapshot
137
+ replay_journal(snapshot)
138
+ end
139
+
140
+ def resolve_path(settings, root)
141
+ declared = settings[:dir] || settings["dir"] || "data"
142
+ dir = declared.start_with?("/") ? declared : File.join(root || Dir.pwd, declared)
143
+
144
+ File.join(dir, "#{@aggregate.storage_name}.heki")
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,3 @@
1
+ Hecks.adapter "IdentityRegistry" do
2
+ port "identity_resolution"
3
+ end
@@ -0,0 +1,26 @@
1
+ module Hecks
2
+ module Adapters
3
+ # THE `identity_resolution` PORT, FULFILLED BY THE Identity FRAMEWORK
4
+ # BLUEBOOK — same registry, same boot, so this is a dispatch against
5
+ # records already sitting in the store this adapter is handed, not a
6
+ # bridge to a second runtime. Same reasoning `GovernanceAuthorization`
7
+ # gives for itself, one port over.
8
+ module IdentityRegistry
9
+ module_function
10
+
11
+ # `nil` for a pair nothing has linked, the first match otherwise —
12
+ # `ResolvedBy` is a lookup by the exact (issuer, subject) an
13
+ # authenticated token carries, not a listing, so more than one row
14
+ # would mean two links share a pair, which `Link`'s own natural
15
+ # key already prevents by construction.
16
+ def resolve(registry, issuer:, subject:)
17
+ rows = Runtime::Dispatcher.new(registry).query(
18
+ "Identity::ExternalIdentifier.ResolvedBy",
19
+ issuer: { value: issuer.to_s }, subject: { value: subject.to_s }
20
+ )
21
+
22
+ rows.first&.fetch(:identity)
23
+ end
24
+ end
25
+ end
26
+ end