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,306 @@
1
+ require "json"
2
+ require "fileutils"
3
+
4
+ require_relative "sql_query_builder"
5
+ require_relative "sqlite/schema_builder"
6
+ require_relative "sqlite/codec"
7
+ require_relative "../../ports/persistence/append_only"
8
+ require_relative "../../query_specification/common/null_policy"
9
+ require_relative "../../query_specification/common/order_by"
10
+ require_relative "../../runtime/errors"
11
+ require_relative "../../runtime/event"
12
+ require_relative "../../runtime/instance"
13
+
14
+ module Hecks
15
+ module Adapters
16
+ # The SQLite store: one table per aggregate head, an append-only entry
17
+ # table beside it. The DDL lives in sqlite/schema_builder.rb, the
18
+ # column codec in sqlite/codec.rb, and the query compilation is the
19
+ # shared SqlQueryBuilder — this file supplies only SQLite's dialect.
20
+ class Sqlite
21
+ include SqlQueryBuilder
22
+ include SchemaBuilder
23
+ include Codec
24
+
25
+ SQL_TYPES = { "Integer" => "INTEGER", "Float" => "REAL" }.freeze
26
+
27
+ attr_reader :aggregate, :path
28
+
29
+ def persistence_capabilities = [:atomic_put]
30
+
31
+ def initialize(aggregate:, settings: {}, root: nil)
32
+ # LAZY, ON PURPOSE — a domain that never wires Sqlite should never
33
+ # need the gem installed. `require "hecks"` alone must not
34
+ # force a database client library nobody asked for.
35
+ require "sqlite3"
36
+
37
+ @aggregate = aggregate
38
+ @path = resolve_path(settings, root)
39
+ # THE OPTIONAL saga-persistence capability's own scoping column
40
+ # (§2/§4) — falls back to the aggregate's own name for a
41
+ # directly-instantiated adapter (specs), same fallback shape
42
+ # Postgres's own @domain already uses.
43
+ @domain = (
44
+ if settings.key?(:domain)
45
+ settings[:domain]
46
+ elsif settings.key?("domain")
47
+ settings["domain"]
48
+ else
49
+ aggregate.name
50
+ end
51
+ ).to_s
52
+
53
+ FileUtils.mkdir_p(File.dirname(@path))
54
+ @db = SQLite3::Database.new(@path)
55
+ @db.results_as_hash = true
56
+ # The append is the recovery commit point. Keep SQLite's fsync policy
57
+ # explicit instead of inheriting a process-wide pragma choice.
58
+ @db.execute("PRAGMA synchronous = FULL")
59
+
60
+ create_aggregate_table!
61
+ create_entry_table!
62
+ ensure_entry_operation_column!
63
+ ensure_entry_mirrors_column!
64
+ create_event_table!
65
+ create_saga_table!
66
+ end
67
+
68
+ def table = @aggregate.storage_name
69
+
70
+ def find(id)
71
+ row = @db.get_first_row("SELECT * FROM #{quoted_table} WHERE id = ?", [id.to_s])
72
+ return nil unless row
73
+
74
+ Runtime::Instance.new(aggregate: @aggregate, id: row["id"], state: decode(row))
75
+ end
76
+
77
+ # order_by IS A RUNTIME VALUE — see postgres.rb's own all for the
78
+ # full reasoning; whitelisted the identical way before it ever
79
+ # reaches order_expression.
80
+ def all(order_by: nil, direction: :asc)
81
+ order_sql = "ORDER BY id"
82
+ if order_by
83
+ name = order_by.to_s.split(".").first
84
+ raise Runtime::WiringError, "#{@aggregate.name} has no attribute #{order_by.inspect} to order by" unless @aggregate.lifecycle&.field.to_s == name || @aggregate.attribute(name)
85
+
86
+ spec = QuerySpecification::Common::OrderBy.new(field: order_by, direction: direction)
87
+ order_sql = "ORDER BY #{order_clause(spec, nil)}"
88
+ end
89
+
90
+ @db.execute("SELECT * FROM #{quoted_table} #{order_sql}").map do |row|
91
+ Runtime::Instance.new(aggregate: @aggregate, id: row["id"], state: decode(row))
92
+ end
93
+ end
94
+
95
+ def count = @db.get_first_value("SELECT COUNT(*) FROM #{quoted_table}").to_i
96
+
97
+ def append(entry)
98
+ @db.execute(
99
+ "INSERT INTO #{quoted_entry_table} (aggregate_id, operation, state, mirrors) VALUES (?, ?, ?, ?)",
100
+ # `mirrors` (unlike `state`) is a NULLABLE column — an absent
101
+ # mirrors hash must bind a real SQL NULL, not the four-character
102
+ # JSON text `"null"` (`JSON.generate(nil)`), or a future `IS NULL`
103
+ # check against it would never match. Same guard `postgres_era.rb`
104
+ # already uses for its own journal's `mirrors` column.
105
+ [entry.id, entry.operation, JSON.generate(entry.state), entry.mirrors && JSON.generate(entry.mirrors)]
106
+ )
107
+ entry
108
+ end
109
+
110
+ def project(entry)
111
+ return @db.execute("DELETE FROM #{quoted_table} WHERE id = ?", [entry.id]) if entry.delete?
112
+
113
+ instance = Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: entry.state)
114
+ columns = (["id"] + persisted_fields.map { |field| field[:name].to_s }).map { |c| quote_ident(c) }
115
+ values = [instance.id.to_s] + persisted_fields.map { |field| encode_field(field, instance[field[:name]]) }
116
+ slots = Array.new(columns.size, "?").join(", ")
117
+
118
+ @db.execute(
119
+ "INSERT OR REPLACE INTO #{quoted_table} (#{columns.join(', ')}) VALUES (#{slots})",
120
+ values
121
+ )
122
+ instance
123
+ end
124
+
125
+ def entries
126
+ @db.execute("SELECT aggregate_id, operation, state, mirrors FROM #{quoted_entry_table} ORDER BY sequence").map do |row|
127
+ state = JSON.parse(row["state"])
128
+ Ports::Persistence::Entry.new(
129
+ operation: row["operation"] || "save",
130
+ id: row["aggregate_id"],
131
+ state: state&.transform_keys(&:to_sym),
132
+ mirrors: row["mirrors"] && JSON.parse(row["mirrors"])
133
+ )
134
+ end
135
+ end
136
+
137
+ def reset!
138
+ @db.execute("DELETE FROM #{quoted_table}")
139
+ @db.execute("DELETE FROM #{quoted_entry_table}")
140
+ self
141
+ end
142
+
143
+ def save(instance)
144
+ entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
145
+ append(entry)
146
+ project(entry)
147
+ end
148
+
149
+ # The outcome lookup, journal append and snapshot replacement share one
150
+ # SQLite transaction. The runtime performs no preliminary find; this
151
+ # adapter-native operation owns both concurrency and outcome reporting.
152
+ def atomic_put(entry, insert_only: false)
153
+ status = nil
154
+ @db.transaction do
155
+ exists = !@db.get_first_value("SELECT 1 FROM #{quoted_table} WHERE id = ?", [entry.id.to_s]).nil?
156
+ if insert_only && exists
157
+ status = :conflicted
158
+ next
159
+ end
160
+ status = exists ? :replaced : :inserted
161
+ append(entry)
162
+ project(entry)
163
+ end
164
+ status
165
+ end
166
+
167
+ def delete(id)
168
+ entry = Ports::Persistence::Entry.new(operation: "delete", id: id.to_s, state: nil)
169
+ append(entry)
170
+ project(entry)
171
+ true
172
+ end
173
+
174
+ def record_event(event)
175
+ @db.execute(
176
+ "INSERT INTO events (name, aggregate, aggregate_id, payload, occurred_at) VALUES (?, ?, ?, ?, ?)",
177
+ [event.name, event.aggregate, event.id.to_s, JSON.generate(event.payload), event.occurred_at]
178
+ )
179
+ end
180
+
181
+ def events
182
+ @db.execute("SELECT * FROM events ORDER BY id").map do |row|
183
+ Runtime::Event.new(
184
+ name: row["name"],
185
+ aggregate: row["aggregate"],
186
+ id: row["aggregate_id"],
187
+ payload: JSON.parse(row["payload"], symbolize_names: true),
188
+ occurred_at: row["occurred_at"]
189
+ )
190
+ end
191
+ end
192
+
193
+ # ── the OPTIONAL saga-persistence capability (§2) — reuses the
194
+ # DDL every SQLite-backed aggregate table already lives beside
195
+ # (`create_saga_table!`, `Sqlite::SchemaBuilder`, shared with D1).
196
+ # SQLite's `resolve_path` defaults to one `.db` file PER
197
+ # AGGREGATE unless a domain shares one `database` setting across
198
+ # its aggregates — since saga persistence resolves through
199
+ # whichever adapter instance backs the domain's FIRST aggregate
200
+ # (`Registry#saga_persistence`), this table ends up living inside
201
+ # THAT one aggregate's own file by default. Correct and durable
202
+ # either way; a domain that wants an obviously-named saga store
203
+ # already gets one by sharing `database` across its aggregates,
204
+ # the recommended, common case.
205
+ def save_saga(process_manager:, correlation:, state:, memory:, completed_compensations: [])
206
+ @db.execute(
207
+ "INSERT OR REPLACE INTO hecks_saga_instances (domain, process_manager, correlation, state, memory, completed_compensations) " \
208
+ "VALUES (?, ?, ?, ?, ?, ?)",
209
+ [@domain, process_manager.to_s, correlation.to_s, state.to_s, JSON.generate(memory), JSON.generate(completed_compensations)]
210
+ )
211
+ end
212
+
213
+ def delete_saga(process_manager:, correlation:)
214
+ @db.execute(
215
+ "DELETE FROM hecks_saga_instances WHERE domain = ? AND process_manager = ? AND correlation = ?",
216
+ [@domain, process_manager.to_s, correlation.to_s]
217
+ )
218
+ end
219
+
220
+ def each_saga
221
+ return enum_for(:each_saga) unless block_given?
222
+
223
+ @db.execute(
224
+ "SELECT process_manager, correlation, state, memory, completed_compensations FROM hecks_saga_instances WHERE domain = ?",
225
+ [@domain]
226
+ ).each do |row|
227
+ yield row["process_manager"], row["correlation"], row["state"],
228
+ JSON.parse(row["memory"], symbolize_names: true),
229
+ JSON.parse(row["completed_compensations"] || "[]", symbolize_names: true)
230
+ end
231
+ end
232
+
233
+ private
234
+
235
+ # ── SqlQueryBuilder's dialect hooks ─────────────────────────────
236
+
237
+ def select_list = "*"
238
+ def from_relation = quoted_table
239
+ def dialect_name = "SQLite"
240
+ def empty_in_clause = "0"
241
+
242
+ def placeholder(binds, value)
243
+ binds << value
244
+ "?"
245
+ end
246
+
247
+ def contains_clause(expression, placeholder)
248
+ "instr(#{expression}, #{placeholder}) > 0"
249
+ end
250
+
251
+ def list_contains_clause(column, member, placeholder)
252
+ target = member.empty? ? "json_each.value" : "json_extract(json_each.value, '$.#{member}')"
253
+ "EXISTS (SELECT 1 FROM json_each(#{quote_ident(column)}) WHERE #{target} = #{placeholder})"
254
+ end
255
+
256
+ def plain_column(name) = quote_ident(name)
257
+
258
+ def nested_expression(name, path, member)
259
+ json_path = path.empty? ? "$.#{member || 'value'}" : "$.#{path.join('.')}"
260
+ "json_extract(#{quote_ident(name)}, '#{json_path}')"
261
+ end
262
+
263
+ # SQLite has no bare OFFSET — LIMIT -1 is its own documented
264
+ # unbounded spelling, exactly for this case.
265
+ def unbounded_limit = " LIMIT -1"
266
+
267
+ def order_clause(order_by, policy)
268
+ QuerySpecification::Common::NullPolicy.sql_order(query_expression(order_by.field), order_by.direction, policy)
269
+ end
270
+
271
+ def execute_query(sql, binds)
272
+ @db.execute(sql, binds).map { |row| Runtime::Instance.new(aggregate: @aggregate, id: row["id"], state: decode(row)) }
273
+ end
274
+
275
+ # ── the rest of the dialect ─────────────────────────────────────
276
+
277
+ def quote_ident(name)
278
+ %("#{name.to_s.gsub('"', '""')}")
279
+ end
280
+
281
+ def quoted_table = quote_ident(table)
282
+ def entry_table = "#{table}_entries"
283
+ def quoted_entry_table = quote_ident(entry_table)
284
+
285
+ def resolve_path(settings, root)
286
+ declared =
287
+ if settings.key?(:database)
288
+ settings[:database]
289
+ elsif settings.key?("database")
290
+ settings["database"]
291
+ else
292
+ "data/#{table}.db"
293
+ end
294
+ return declared if declared.start_with?("/")
295
+
296
+ File.join(root || Dir.pwd, declared)
297
+ end
298
+ end
299
+
300
+ # Port-specific bindings share SQLite's storage mechanics but advertise
301
+ # only one operational contract each.
302
+ class SqlitePersistence < Sqlite; end
303
+ end
304
+ end
305
+
306
+ require_relative "sqlite/projection"
@@ -0,0 +1,3 @@
1
+ Hecks.adapter "SystemClock" do
2
+ port "clock"
3
+ end
@@ -0,0 +1,14 @@
1
+ module Hecks
2
+ module Adapters
3
+ # The real `clock` fulfillment — the machine's own time, in seconds.
4
+ #
5
+ # Whole seconds on purpose. The sublanguage does integer arithmetic, a
6
+ # staleness window is measured in minutes, and sub-second precision in a
7
+ # durable ledger is precision nobody reads and every diff has to ignore.
8
+ module SystemClock
9
+ module_function
10
+
11
+ def now = Time.now.to_i
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,54 @@
1
+ module Hecks
2
+ module Adapters
3
+ # The driven side — every store or reader an adapter declaration can
4
+ # bind. A small adapter is one file beside this one; sqlite, postgres
5
+ # and heki keep a directory for their supporting cast. Each `.adapter`
6
+ # file alongside is the DSL declaration the grammar bootstrap and the
7
+ # Folder adapter load as data.
8
+ end
9
+ end
10
+
11
+ require_relative "driven/memory"
12
+ require_relative "driven/sqlite"
13
+ require_relative "driven/postgres"
14
+ # `PostgresEra` is NOT `require_relative`d here — ADR 0033 moved it, and
15
+ # the rest of the era/lineage/translation subsystem, behind a loadable
16
+ # persistence plugin (`hecks/ports/persistence/plugins/era`) rather than
17
+ # requiring every app to carry it whether or not anything ever binds
18
+ # `persisted_by "PostgresEra"`. `Module#autoload` is the one-line seam
19
+ # that makes that genuinely load-nothing-until-asked instead of pushing
20
+ # an explicit `require` onto every one of the ~15 generic `bin/*` tools
21
+ # that boot an arbitrary checked-in domain (several of which — pizzas,
22
+ # compliance, chess, roster — bind PostgresEra): the FIRST real
23
+ # `Adapters.const_get("PostgresEra")` (`registry/verification.rb`'s own
24
+ # adapter resolution, unchanged) transparently `require`s the plugin
25
+ # entry point, which defines the constant AND calls `register_plugin`
26
+ # as its own side effect (`plugins/era.rb`'s last line) — the exact
27
+ # same effect an explicit `require "hecks/ports/persistence/plugins/
28
+ # era"` has, just deferred to the moment something is actually asked
29
+ # for `PostgresEra` rather than sprinkled across every caller that
30
+ # might. A domain that never binds it never triggers the autoload, so
31
+ # never loads a line of era code — unchanged from ADR 0033's own goal.
32
+ Hecks::Adapters.autoload(:PostgresEra, "hecks/ports/persistence/plugins/era")
33
+ require_relative "driven/lambda"
34
+ require_relative "driven/heki"
35
+ require_relative "driven/prism"
36
+ require_relative "driven/folder"
37
+ require_relative "driven/d1"
38
+ require_relative "driven/mock_stripe_adapter"
39
+ require_relative "driven/secure_random_identity"
40
+ require_relative "driven/system_clock"
41
+ # `SequentialIdentity` — the deterministic identity_generation test
42
+ # double — is NOT required here on purpose. It lives at
43
+ # spec/fixtures/sequential_identity.{adapter,rb}, loaded explicitly by
44
+ # whichever spec wants it: this file's `require_relative` list is what
45
+ # `Folder#load_library`'s `.adapter` glob backs, and `.adapter` DSL
46
+ # declarations register into every registry any real `Hecks.boot` ever
47
+ # builds. Two adapters answering `identity_generation` unconditionally
48
+ # would make the port permanently ambiguous — `Ports::IdentityGeneration
49
+ # .adapter` refuses to choose between more than one — for every
50
+ # consumer, forever, not just specs that want the deterministic one.
51
+ require_relative "driven/governance_authorization"
52
+ require_relative "driven/identity_registry"
53
+ require_relative "driven/google_authentication"
54
+ require_relative "driven/claude_code"
@@ -0,0 +1,6 @@
1
+ module Hecks
2
+ module Adapters
3
+ end
4
+ end
5
+
6
+ require_relative "adapters/driven"
@@ -0,0 +1,125 @@
1
+ require_relative "ir"
2
+
3
+ # The authoring surface for `Hecks.behaviors "Name" do ... end` — one
4
+ # `test "description" do ... end` block per case, `tests`/`setup`/`input`/
5
+ # `expect` inside. `instance_eval`-based, the same shape every other
6
+ # hecks DSL builder uses (`WorldBuilder`, `HecksagonBuilder`).
7
+ module Hecks
8
+ module Behaviors
9
+ # Raised at `Hecks.behaviors` build time — a missing `vision` or
10
+ # `loads` line names the fix directly rather than failing later, deep
11
+ # inside a boot, over a suite that was never going to be scoped.
12
+ class Malformed < StandardError; end
13
+
14
+ class TestCaseBuilder
15
+ def initialize(description)
16
+ @description = description
17
+ @tests_command = nil
18
+ @on_aggregate = nil
19
+ @kind = :command
20
+ @setups = []
21
+ @input = {}
22
+ @expect = {}
23
+ end
24
+
25
+ def tests(command, on: nil, kind: :command)
26
+ @tests_command = command
27
+ @on_aggregate = on
28
+ @kind = kind
29
+ end
30
+
31
+ def setup(command, **kwargs)
32
+ @setups << TestSetup.new(command: command, args: kwargs)
33
+ end
34
+
35
+ def input(**kwargs) = @input.merge!(kwargs)
36
+ def expect(**kwargs) = @expect.merge!(kwargs)
37
+
38
+ def build
39
+ unless @tests_command
40
+ raise Malformed, "test #{@description.inspect} never calls `tests` — " \
41
+ "say which command or query this example exercises"
42
+ end
43
+
44
+ validate_expect!
45
+
46
+ TestCase.new(description: @description, tests_command: @tests_command,
47
+ on_aggregate: @on_aggregate, kind: @kind,
48
+ setups: @setups, input: @input, expect: @expect)
49
+ end
50
+
51
+ private
52
+
53
+ # Closes the silent-pass paths a free-form `expect(**kwargs)` merge
54
+ # otherwise leaves open: a test with no `expect` at all asserts
55
+ # nothing and passes whenever dispatch doesn't raise; `count:` on a
56
+ # command and `emits:` on a query are each read by neither runner
57
+ # (Expectations#run_command/#run_query), so they're accepted here
58
+ # and then silently ignored at run time. Checking both at BUILD
59
+ # time, not in the runners, makes them errors on the file that
60
+ # wrote them rather than green checks nobody questions.
61
+ def validate_expect!
62
+ if @expect.empty?
63
+ raise Malformed, "test #{@description.inspect} has no `expect` — say what this " \
64
+ "example proves (ok:, refused:, emits:, count:, or a field name)"
65
+ end
66
+
67
+ if @kind == :command && @expect.key?(:count)
68
+ raise Malformed, "test #{@description.inspect}: `expect count:` only applies to " \
69
+ "queries — a command has no row count to check"
70
+ end
71
+
72
+ if @kind == :query && @expect.key?(:emits)
73
+ raise Malformed, "test #{@description.inspect}: `expect emits:` only applies to " \
74
+ "commands — a query never dispatches, so it never emits"
75
+ end
76
+ end
77
+ end
78
+
79
+ class BehaviorsBuilder
80
+ def initialize(name, source_path:)
81
+ @name = name
82
+ @source_path = source_path
83
+ @source_dir = File.dirname(source_path)
84
+ @vision = nil
85
+ @loads = nil
86
+ @tests = []
87
+ end
88
+
89
+ def vision(text) = @vision = text
90
+
91
+ # Relative to THIS `.behaviors` file, never to the filesystem's cwd
92
+ # or a same-stem convention — scope is a fact this file declares,
93
+ # not one a runner infers.
94
+ def loads(*paths)
95
+ @loads = paths.map { |path| File.expand_path(path, @source_dir) }
96
+ end
97
+
98
+ def test(description, &block)
99
+ builder = TestCaseBuilder.new(description)
100
+ builder.instance_eval(&block) if block
101
+ @tests << builder.build
102
+ end
103
+
104
+ def build
105
+ unless @vision
106
+ raise Malformed, "#{@source_path}: no `vision \"...\"` — say in one line " \
107
+ "what this suite is examples of"
108
+ end
109
+ unless @loads
110
+ raise Malformed, "#{@source_path}: no `loads \"...\"` — a behaviors file " \
111
+ "must declare exactly which files to boot"
112
+ end
113
+
114
+ BehaviorsSuite.new(name: @name, vision: @vision, loads: @loads,
115
+ tests: @tests, path: @source_path)
116
+ end
117
+
118
+ def self.build(name, source_path:, &block)
119
+ builder = new(name, source_path: source_path)
120
+ builder.instance_eval(&block) if block
121
+ builder.build
122
+ end
123
+ end
124
+ end
125
+ end