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,183 @@
1
+ require_relative "../../storage_shape"
2
+
3
+ module Hecks
4
+ module Translation
5
+ module Scaffold
6
+ # Diff two eras' storage-shape projections into the edge's
7
+ # declarations — confident rules by unique signature pairing,
8
+ # everything ambiguous left unresolved for a human.
9
+ module Differ
10
+ # Diff two bluebook IRs into the edge's declarations.
11
+ def diff(held_bluebook, current_bluebook)
12
+ held_shapes = projections(held_bluebook)
13
+ current_shapes = projections(current_bluebook)
14
+
15
+ matched = match_aggregates(held_shapes, current_shapes)
16
+ aggregates = current_bluebook.aggregates.filter_map do |aggregate|
17
+ held_name = matched[:pairs][aggregate.name]
18
+ next unless held_name
19
+
20
+ rules = attribute_rules(held_shapes[held_name], current_shapes[aggregate.name])
21
+ was = held_name == aggregate.name ? nil : held_name
22
+ next if rules.empty? && was.nil?
23
+
24
+ ScaffoldedAggregate.new(name: aggregate.name, was: was, rules: rules)
25
+ end
26
+
27
+ { aggregates: aggregates, retired: matched[:retired], unclaimed: matched[:unclaimed] }
28
+ end
29
+
30
+ def projections(bluebook)
31
+ Runtime::StorageShape.project(bluebook)["aggregates"].to_h { |shape| [shape["name"], shape] }
32
+ end
33
+
34
+ # A vanished aggregate whose full shape reappears under exactly one
35
+ # new name was renamed. `retired` is only confident when NOTHING
36
+ # remains it could plausibly have become — a vanished aggregate
37
+ # beside an unmatched new one might be a rename-plus-reshape, and
38
+ # writing `retired` there would be a guess that strands data.
39
+ # Anything ambiguous stays unclaimed, and the coverage gate names
40
+ # it until a human decides.
41
+ def match_aggregates(held_shapes, current_shapes)
42
+ pairs = {}
43
+ current_shapes.each_key { |name| pairs[name] = held_shapes.key?(name) ? name : nil }
44
+
45
+ vanished = held_shapes.keys - current_shapes.keys
46
+ appeared = current_shapes.keys.select { |name| pairs[name].nil? }
47
+ unclaimed = []
48
+ vanished.each do |old_name|
49
+ old_shape = held_shapes[old_name].merge("name" => nil)
50
+ candidates = appeared.select { |name| current_shapes[name].merge("name" => nil) == old_shape }
51
+ if candidates.size == 1
52
+ pairs[candidates.first] = old_name
53
+ appeared -= candidates
54
+ else
55
+ unclaimed << old_name
56
+ end
57
+ end
58
+
59
+ retired = appeared.empty? ? unclaimed : []
60
+ { pairs: pairs, retired: retired, unclaimed: retired.empty? ? unclaimed : [] }
61
+ end
62
+
63
+ # Every changed path in one aggregate, resolved into rules by
64
+ # signature matching over the FULL path set — top-level names and
65
+ # dotted members alike. Unique signature pair: a rename (both
66
+ # top-level) or a move (any dotted end). Same path, same members,
67
+ # new type name: a retype. Anything else: unresolved, carrying its
68
+ # type-compatible candidates (an empty list is the arrow toward
69
+ # compute, or drop).
70
+ def attribute_rules(held_shape, current_shape)
71
+ rules = []
72
+ rules << identity_hint(held_shape, current_shape)
73
+ rules.compact!
74
+ held_attrs = (held_shape["attributes"] || []).to_h { |attribute| [attribute["name"], attribute] }
75
+ current_attrs = (current_shape["attributes"] || []).to_h { |attribute| [attribute["name"], attribute] }
76
+
77
+ # retype pass: same attribute name, same member structure, the
78
+ # TYPE's own name changed
79
+ (held_attrs.keys & current_attrs.keys).each do |name|
80
+ held = held_attrs[name]
81
+ current = current_attrs[name]
82
+ next if held == current
83
+ next unless container?(held) && container?(current)
84
+ next unless held["type"]["members"] == current["type"]["members"] && held["list"] == current["list"]
85
+
86
+ rules << { kind: :retype, from: held["type"]["type"], to: current["type"]["type"] }
87
+ end
88
+ retyped = rules.filter_map { |rule| rule[:from] if rule[:kind] == :retype }
89
+
90
+ vanished = vanished_paths(held_attrs, current_attrs, retyped)
91
+ appeared = appeared_paths(held_attrs, current_attrs, retyped)
92
+
93
+ vanished.each do |path, signature|
94
+ matches = appeared.select { |_, candidate| candidate == signature }.keys
95
+ taken = rules.filter_map { |rule| rule[:to] if %i[rename move].include?(rule[:kind]) }
96
+ matches -= taken
97
+ if matches.size == 1 && vanished.count { |_, other| other == signature } == 1
98
+ target = matches.first
99
+ kind = path.include?(".") || target.include?(".") ? :move : :rename
100
+ rules << { kind: kind, from: path, to: target }
101
+ else
102
+ candidates = matches.empty? ? compatible_candidates(appeared, signature) : matches
103
+ rules << { kind: :unresolved, from: path, candidates: candidates }
104
+ end
105
+ end
106
+ rules
107
+ end
108
+
109
+ # Held paths that need explaining: whole attributes that vanished,
110
+ # and members that vanished (or changed type) inside a kept
111
+ # attribute — mirroring exactly what EraGuard will demand coverage
112
+ # for.
113
+ def vanished_paths(held_attrs, current_attrs, retyped)
114
+ paths = {}
115
+ held_attrs.each do |name, held|
116
+ next if retyped.include?(container?(held) ? held["type"]["type"] : nil)
117
+
118
+ current = current_attrs[name]
119
+ if current.nil?
120
+ paths[name] = held["type"]
121
+ next
122
+ end
123
+ next if held == current
124
+
125
+ held_members = members_of(held)
126
+ current_members = members_of(current)
127
+ held_members.each do |member, signature|
128
+ paths["#{name}.#{member}"] = signature if current_members[member] != signature
129
+ end
130
+ end
131
+ paths
132
+ end
133
+
134
+ def appeared_paths(held_attrs, current_attrs, retyped)
135
+ paths = {}
136
+ current_attrs.each do |name, current|
137
+ next if retyped.include?(container?(current) ? current["type"]["type"] : nil)
138
+
139
+ held = held_attrs[name]
140
+ if held.nil?
141
+ paths[name] = current["type"]
142
+ next
143
+ end
144
+ next if held == current
145
+
146
+ held_members = members_of(held)
147
+ members_of(current).each do |member, signature|
148
+ paths["#{name}.#{member}"] = signature if held_members[member] != signature
149
+ end
150
+ end
151
+ paths
152
+ end
153
+
154
+ # THE ONE THING `attribute_rules` COULD NOT SEE BEFORE — the shape
155
+ # projection already carries `"identity"` (`StorageShape
156
+ # .project_aggregate`), it was simply never read here. An
157
+ # unresolved placeholder, not a guess: `coverage_check.rb`'s own
158
+ # `check_identity_unchanged!` is the real gate this only hints
159
+ # toward, the same "tool proactively guides you" pattern the
160
+ # generic unresolved message already gives unfed fields.
161
+ def identity_hint(held_shape, current_shape)
162
+ return if held_shape["identity"] == current_shape["identity"]
163
+
164
+ { kind: :unresolved, from: :identity, candidates: [] }
165
+ end
166
+
167
+ def compatible_candidates(appeared, signature)
168
+ appeared.select { |_, candidate| scalar_of(candidate) == scalar_of(signature) }.keys
169
+ end
170
+
171
+ def scalar_of(signature) = signature.is_a?(Hash) ? signature["members"] : signature
172
+
173
+ def members_of(attribute)
174
+ return {} unless container?(attribute)
175
+
176
+ attribute["type"]["members"].to_h { |member| [member["name"], member["type"]] }
177
+ end
178
+
179
+ def container?(attribute) = attribute["type"].is_a?(Hash)
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,41 @@
1
+ module Hecks
2
+ module Translation
3
+ module Scaffold
4
+ # Render an edge into .bluebook text — confident rules inline,
5
+ # ambiguities as parse-refusing `unresolved` constructs, never
6
+ # comments, so an unresolved file can only boot into a refusal.
7
+ module Renderer
8
+ def render(edge)
9
+ lines = ["Hecks.data_translation #{edge.domain.inspect}, from: #{edge.from.inspect}, to: #{edge.to.inspect} do"]
10
+ edge.aggregates.each do |aggregate|
11
+ header = " aggregate #{aggregate.name.inspect}"
12
+ header += ", was: #{aggregate.was.inspect}" if aggregate.was
13
+ if aggregate.rules.empty?
14
+ lines << "#{header} do"
15
+ else
16
+ lines << "#{header} do"
17
+ aggregate.rules.each { |rule| lines << " #{render_rule(rule)}" }
18
+ end
19
+ lines << " end"
20
+ end
21
+ edge.retired.each { |name| lines << " retired #{name.inspect}" }
22
+ lines << "end"
23
+ "#{lines.join("\n")}\n"
24
+ end
25
+
26
+ def render_rule(rule)
27
+ case rule[:kind]
28
+ when :rename then "rename :#{rule[:from]}, to: :#{rule[:to]}"
29
+ when :move then "move #{rule[:from].inspect}, to: #{rule[:to].inspect}"
30
+ when :retype then "retype #{rule[:from].inspect}, to: #{rule[:to].inspect}"
31
+ when :unresolved
32
+ candidates = rule[:candidates].map { |candidate| render_path(candidate) }.join(", ")
33
+ "unresolved #{render_path(rule[:from])}, candidates: [#{candidates}]"
34
+ end
35
+ end
36
+
37
+ def render_path(path) = path.to_s.include?(".") ? path.to_s.inspect : ":#{path}"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,28 @@
1
+ require "fileutils"
2
+
3
+ module Hecks
4
+ module Translation
5
+ module Scaffold
6
+ # Put the rendered edge on disk, regenerating in place when a file
7
+ # for the same shape pair already exists.
8
+ module Writer
9
+ # The edge file, regenerated in place when one for the same shape
10
+ # pair already exists (matched textually — an unresolved file
11
+ # cannot be LOADED to ask, that being the whole point of
12
+ # unresolved).
13
+ def write!(directory, edge)
14
+ translations_dir = File.join(directory, "translations")
15
+ FileUtils.mkdir_p(translations_dir)
16
+
17
+ existing = Dir[File.join(translations_dir, "*.bluebook")].find do |path|
18
+ text = File.read(path)
19
+ text.include?("from: #{edge.from.inspect}") && text.include?("to: #{edge.to.inspect}")
20
+ end
21
+ path = existing || File.join(translations_dir, "#{edge.ordinal}-#{edge.label}.bluebook")
22
+ File.write(path, render(edge))
23
+ path
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,30 @@
1
+ require_relative "scaffold/differ"
2
+ require_relative "scaffold/renderer"
3
+ require_relative "scaffold/writer"
4
+
5
+ module Hecks
6
+ module Translation
7
+ # The scaffold writes translations; humans resolve ambiguity. It
8
+ # diffs the held era's storage-shape projection against the current
9
+ # one (scaffold/differ.rb) and WRITES the edge file
10
+ # (scaffold/renderer.rb, scaffold/writer.rb): confident rules inline
11
+ # (unique signature pairs → rename/move, aggregate renames → `was:`,
12
+ # type renames with identical members → retype, aggregates gone
13
+ # without a successor → retired), ambiguities as parse-refusing
14
+ # `unresolved` constructs — never comments, so an unresolved file can
15
+ # only boot into a refusal, never a guess. It NEVER proposes a
16
+ # `compute` (a computation takes domain judgment no mechanical diff
17
+ # can infer) — an empty-candidate `unresolved` is what points the
18
+ # author there. It never proposes a `drop` either: data loss is a
19
+ # decision, not a default. Re-running regenerates the file, matched
20
+ # by shape pair.
21
+ module Scaffold
22
+ Edge = Struct.new(:domain, :from, :to, :ordinal, :label, :aggregates, :retired, keyword_init: true)
23
+ ScaffoldedAggregate = Struct.new(:name, :was, :rules, keyword_init: true)
24
+
25
+ extend Differ
26
+ extend Renderer
27
+ extend Writer
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,11 @@
1
+ module Hecks
2
+ # The era-translation toolchain: scaffold drafts a translation from two
3
+ # eras' shapes, audit judges a written one, reattest re-seals it.
4
+ module Translation
5
+ end
6
+ end
7
+
8
+ require_relative "translation/rule_compiler"
9
+ require_relative "translation/scaffold"
10
+ require_relative "translation/audit"
11
+ require_relative "translation/reattest"
@@ -0,0 +1,48 @@
1
+ require_relative "../plugin"
2
+ require_relative "era/era_check"
3
+ require_relative "era/era_guard"
4
+ require_relative "era/era_tamper"
5
+ require_relative "era/storage_shape"
6
+ require_relative "era/lineage"
7
+ require_relative "era/postgres_era"
8
+ require_relative "era/translation"
9
+
10
+ module Hecks
11
+ module Ports
12
+ module Persistence
13
+ module Plugins
14
+ # ADR 0033 — requiring THIS FILE is installing the plugin. Nothing in
15
+ # Hecks core requires it; an app that binds `PostgresEra`, or wants
16
+ # schema-translation support at all, requires it explicitly — the
17
+ # same shape every adapter-specific spec fixture already uses to
18
+ # load one particular `.adapter` file rather than all of them.
19
+ module Era
20
+ module_function
21
+
22
+ # `Runtime::Loader.run_boot_gates!` asks every loaded persistence
23
+ # plugin to contribute here, generically — it never mentions
24
+ # `EraCheck` or "era" by name. Two gates, both `:pre_verify`:
25
+ #
26
+ # `:era_compute_rules` — unconditional, whenever this plugin is
27
+ # loaded at all. A `compute`/`rekey` rule requires Postgres
28
+ # whatever adapter is actually bound (ADR 0031's own reasoning,
29
+ # unchanged) — this is the rich, adapter-aware version of that
30
+ # check; `Runtime::Loader`'s own structural backstop (plain
31
+ # `Bluebook::Translation` data, no plugin-specific class) only
32
+ # ever fires when NO persistence plugin is loaded at all.
33
+ #
34
+ # `:era_check` — conditional, exactly ADR 0031's own gate,
35
+ # unchanged: registered only when this registry has an aggregate
36
+ # actually bound to a lineage-capable adapter.
37
+ def contribute_boot_gates(registry, gates)
38
+ gates.register(:era_compute_rules, ->(reg, _dir) { Runtime::EraCheck.check_compute_rules_for_registry!(reg) }, phase: :pre_verify)
39
+ gates.register(:era_check, Runtime::EraCheck.method(:check_lineage!), phase: :pre_verify) if
40
+ Runtime::EraCheck.lineage_capable_registry?(registry)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ Hecks::Ports::Persistence.register_plugin(:era, Hecks::Ports::Persistence::Plugins::Era)
@@ -0,0 +1,42 @@
1
+ require_relative "../../runtime/registry"
2
+
3
+ module Hecks
4
+ module Ports
5
+ module Persistence
6
+ # THE OTHER SHAPE AN ADAPTER CAN BE. `AppendOnly` names one shape
7
+ # already — something that stores bytes, locally (Postgres, SQLite,
8
+ # Heki) or over the network (D1), doesn't matter, the point is it
9
+ # does real local interpretation and has real entries to replay.
10
+ # This names the second, different shape: not "storage reached
11
+ # remotely," but the real INTERPRETER itself living behind a call
12
+ # boundary — `append`/`project` don't do partial local work and
13
+ # then fail, they raise unconditionally, because there is no local
14
+ # write-ahead log to have; `entries` is always `[]` for the same
15
+ # reason (`AppendOnly#recover!` replaying zero entries here is a
16
+ # correct no-op, not a lie).
17
+ #
18
+ # `Adapters::Lambda` is the first adapter to `include` this, not
19
+ # the only one meant to — any future adapter for some other
20
+ # remote-runtime target (a different compute platform, a gRPC
21
+ # stub, whatever) gets this shape for free by including one module
22
+ # instead of re-deriving the same raise-on-write/always-empty-
23
+ # entries boilerplate from scratch. And because it's a real,
24
+ # checkable module rather than an adapter's own ad hoc
25
+ # implementation, a caller can ask "is this adapter a remote-
26
+ # runtime delegate?" as a genuine capability check
27
+ # (`registry.adapter_class(name) <= RemoteRuntime`,
28
+ # `Runtime::RemoteDispatcher`'s own use) instead of comparing
29
+ # adapter names by string.
30
+ module RemoteRuntime
31
+ def append(*)
32
+ raise Runtime::WiringError,
33
+ "#{self.class.name} is a remote-runtime delegate — dispatch through " \
34
+ "Runtime::RemoteDispatcher instead of writing through the repository directly"
35
+ end
36
+ alias_method :project, :append
37
+
38
+ def entries = []
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,30 @@
1
+ require_relative "append_only"
2
+
3
+ module Hecks
4
+ module Ports
5
+ module Persistence
6
+ # Turns a declared adapter binding plus its world configuration into a
7
+ # concrete repository. Selection policy stays out of adapter creation.
8
+ module RepositoryFactory
9
+ module_function
10
+
11
+ def build(registry, domain, aggregate, bind, recover: true, settings_verb: VERB)
12
+ registry.check_verb(bind)
13
+ settings = (registry.world(domain)&.for_binding(settings_verb, bind.adapter) || {})
14
+ .reject { |key, _| key.to_sym == :role }
15
+ registry.check_settings(bind, settings)
16
+ # The domain and the resolved era ride along after the
17
+ # declared-settings check: a lineage adapter journals per
18
+ # DOMAIN and writes into its own ERA's partition — neither of
19
+ # which a world's settings carry.
20
+ adapter = registry.adapter_class(bind.adapter)
21
+ .new(aggregate: aggregate,
22
+ settings: settings.merge(domain: domain.to_s, era: registry.resolved_eras[domain.to_s]),
23
+ root: registry.root)
24
+ repository = AppendOnly.new(adapter)
25
+ recover ? repository.recover! : repository
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,4 @@
1
+ Hecks.port "persistence" do
2
+ verb "persisted_by"
3
+ signal :reply
4
+ end
@@ -0,0 +1,39 @@
1
+ module Hecks
2
+ module Ports
3
+ module Persistence
4
+ NAME = "persistence"
5
+ VERB = "persisted_by"
6
+ DEFAULT_ADAPTER = "Memory"
7
+ end
8
+ end
9
+ end
10
+
11
+ require_relative "persistence/binding_policy"
12
+ require_relative "persistence/plugin"
13
+ require_relative "persistence/repository_factory"
14
+ require_relative "persistence/append_only"
15
+ require_relative "persistence/execution"
16
+ require_relative "persistence/remote_runtime"
17
+ require_relative "persistence/null_saga_store"
18
+
19
+ module Hecks
20
+ module Ports
21
+ module Persistence
22
+ module_function
23
+
24
+ # The public persistence port owns only authoritative aggregate heads.
25
+ def repository(registry, domain, aggregate)
26
+ authoritative = BindingPolicy.resolve(registry, domain, aggregate)
27
+ RepositoryFactory.build(registry, domain, aggregate, authoritative)
28
+ end
29
+
30
+ def binds_for(registry, domain, aggregate)
31
+ [BindingPolicy.resolve(registry, domain, aggregate), []]
32
+ end
33
+
34
+ def bind_for(registry, domain, aggregate)
35
+ binds_for(registry, domain, aggregate).first
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,4 @@
1
+ Hecks.port "projection" do
2
+ verb "projected_by"
3
+ signal :effect
4
+ end
@@ -0,0 +1,96 @@
1
+ require_relative "persistence"
2
+ require_relative "persistence/repository_factory"
3
+ require_relative "../runtime/registry"
4
+
5
+ module Hecks
6
+ module Ports
7
+ # Projection is a read-side port. Its stores are rebuildable consumers of
8
+ # authoritative entries and never participate in command decisions.
9
+ module Projection
10
+ NAME = "projection"
11
+ VERB = "projected_by"
12
+
13
+ module_function
14
+
15
+ def binds_for(registry, domain, aggregate)
16
+ registry.hecksagon(domain)&.binds_for(aggregate.hecks_name, VERB) || []
17
+ end
18
+
19
+ def worker(registry, domain, aggregate, policy: :refresh)
20
+ bind = binds_for(registry, domain, aggregate).first
21
+ return unless bind
22
+
23
+ authoritative = Persistence.repository(registry, domain, aggregate)
24
+ target = Persistence::RepositoryFactory.build(registry, domain, aggregate, bind,
25
+ recover: true, settings_verb: VERB)
26
+ Worker.new(authoritative, target, policy: policy)
27
+ end
28
+
29
+ class Worker
30
+ attr_reader :projection
31
+
32
+ # The only two policies anything in this codebase ever passes
33
+ # (`bin/project`, every spec) — there is no third, legitimate
34
+ # "lenient append" policy on record anywhere. Before this, any
35
+ # value OTHER than the exact symbol `:strict` silently fell
36
+ # through the `consistent?` check below and appended onto
37
+ # divergent history without a word — not just a real typo like
38
+ # `:strikt`, but a caller-supplied String `"strict"` too (this
39
+ # duck-typed fine via `policy.to_sym`, but that was luck, not a
40
+ # contract: nothing here declared what a valid policy even was).
41
+ # Refusing loudly at construction, once, for anything outside
42
+ # this list turns a silent no-op into an immediate, named error
43
+ # — the "refuse rather than silently skip" reading of L1, since
44
+ # `:strict` really is meant to be the only enforcing contract.
45
+ VALID_POLICIES = %i[refresh strict].freeze
46
+
47
+ def initialize(authoritative, projection, policy: :refresh)
48
+ @authoritative = authoritative
49
+ @projection = projection
50
+ @policy = policy.to_sym
51
+ unless VALID_POLICIES.include?(@policy)
52
+ raise ArgumentError,
53
+ "unknown projection catch_up! policy #{@policy.inspect} — expected one of " \
54
+ "#{VALID_POLICIES.map(&:inspect).join(' or ')}"
55
+ end
56
+ end
57
+
58
+ # Invoke from a separate process or scheduler. The command-side write
59
+ # path never calls this method.
60
+ def catch_up!
61
+ entries = Queue.new(@authoritative).entries
62
+ present = @projection.entries
63
+ if @policy == :refresh
64
+ @projection.reset!
65
+ present = []
66
+ end
67
+ unless consistent?(entries, present)
68
+ raise Runtime::WiringError, "projection history does not match its authoritative history" if @policy == :strict
69
+ end
70
+ entries.drop(present.length).each { |entry| @projection.append(entry); @projection.project(entry) }
71
+ @projection
72
+ end
73
+
74
+ def checkpoint = @projection.entries.length
75
+
76
+ private
77
+
78
+ def same_entry?(left, right) = left.operation == right.operation && left.id == right.id && left.state == right.state
79
+
80
+ def consistent?(entries, present)
81
+ present.length <= entries.length && entries.first(present.length).zip(present).all? { |left, right|
82
+ same_entry?(left, right)
83
+ }
84
+ end
85
+ end
86
+
87
+ # The authoritative append-only journal is the durable projection queue.
88
+ # It is committed before a worker sees it; projection entries are the
89
+ # worker's durable checkpoint, so delivery is at-least-once and safe to replay.
90
+ class Queue
91
+ def initialize(authoritative) = @authoritative = authoritative
92
+ def entries = @authoritative.entries
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,60 @@
1
+ require_relative "ordering"
2
+ require_relative "../../runtime/value"
3
+ require_relative "../../query_specification/field_path"
4
+ require_relative "../../query_specification/common/comparison"
5
+
6
+ module Hecks
7
+ module Ports
8
+ module Query
9
+ module InMemory
10
+ FieldPath = QuerySpecification::FieldPath
11
+ Comparison = QuerySpecification::Common::Comparison
12
+
13
+ module_function
14
+
15
+ def execute(records, declared, args = {}, registry: nil)
16
+ matched = records.select do |record|
17
+ declared.wheres.all? { |clause|
18
+ holds?(clause, comparable(FieldPath.dig(record, clause.field)), args, registry: registry)
19
+ }
20
+ end
21
+ field = declared.order_by&.field
22
+ matched = Ordering.apply(matched, declared.order_by, declared.null_semantics,
23
+ identity: ->(record) { record.id.to_s }) { |record| comparable(FieldPath.dig(record, field)) }
24
+ # OFFSET FIRST, THEN LIMIT — the order SQL means by `LIMIT n
25
+ # OFFSET m`, which is what `SqlQueryBuilder` emits and therefore
26
+ # what every SQL-backed aggregate already answers. Written the
27
+ # other way round here, and the two engines disagreed on the
28
+ # same declaration: `limit 2, offset 1` over three rows is rows
29
+ # two and three in Postgres and Sqlite, and was row two alone in
30
+ # memory. Silently — a short page reads exactly like a page that
31
+ # ran out of rows.
32
+ #
33
+ # It gets worse the further you page, which is the case nobody
34
+ # writing the first page ever sees: at `limit 10, offset 10`,
35
+ # taking ten and then dropping ten leaves NOTHING, so page two
36
+ # of a memory-backed query came back empty however many rows
37
+ # were really there.
38
+ matched = matched.drop(resolve(declared.offset.value, args).to_i) if declared.offset
39
+ matched = matched.first(resolve(declared.limit.value, args).to_i) if declared.limit
40
+ matched
41
+ end
42
+
43
+ # The comparator table itself lives in
44
+ # QuerySpecification::Common::Comparison — this module and
45
+ # Runtime::QueryInterpreter used to carry a copy each, and the two
46
+ # drifted (see that file's own comment for what it cost). What
47
+ # stays here is how a value is REACHED for this path: a registry
48
+ # arrives as an argument rather than as instance state, and the
49
+ # field is dug through FieldPath before it arrives.
50
+ def holds?(clause, held, args, registry: nil)
51
+ Comparison.holds?(clause.op, held, comparable(resolve(clause.value, args)), registry: registry)
52
+ end
53
+
54
+ def resolve(value, args) = value.is_a?(Symbol) ? args[value] : value
55
+
56
+ def comparable(value) = Comparison.comparable(value)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,41 @@
1
+ require_relative "../../query_specification/common/null_policy"
2
+
3
+ module Hecks
4
+ module Ports
5
+ module Query
6
+ # What order an ask ANSWERS IN — the meaning of the ask, not a property
7
+ # of the store that happens to hold it. Declared here once so an adapter
8
+ # may satisfy it natively but never redefine it : SQLite pushes both
9
+ # tiers into SQL (NullPolicy.sql_order renders `field DIR, id DIR`),
10
+ # while Heki and Memory have no query engine and delegate straight back
11
+ # to InMemory.
12
+ #
13
+ # Two tiers, in this order : the DECLARED order_by when there is one,
14
+ # then IDENTITY, always. The identity tier is what makes an ask total.
15
+ # Without it, an ask with no order_by — or a declared order with tied
16
+ # keys — hands back whatever order the store happened to hold, and
17
+ # store order was quietly standing in for a rule while every
18
+ # hand-written query in the corpus stayed green : not one of them
19
+ # had a tie for store order to decide.
20
+ #
21
+ # An adapter that pushes ordering down MUST push limit down with it.
22
+ # Re-ordering a page the store already cut would be a top-N of the
23
+ # wrong N — the one way this can be got quietly, expensively wrong.
24
+ module Ordering
25
+ module_function
26
+
27
+ def apply(rows, order_by, null_semantics = nil, identity:, &value_of)
28
+ # STABLE, because sort_by is not : two rows whose identity ties would
29
+ # otherwise swap arbitrarily, and a tier meant to REMOVE store-dependence
30
+ # would be adding a coin flip of its own.
31
+ rows = rows.each_with_index.sort_by { |row, index| [identity.call(row), index] }.map(&:first)
32
+ return rows unless order_by
33
+
34
+ QuerySpecification::Common::NullPolicy.order(
35
+ rows, direction: order_by.direction, policy: null_semantics, &value_of
36
+ )
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end