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,632 @@
1
+ require "json"
2
+ require "fileutils"
3
+ require "time"
4
+ require_relative "facade/command_request"
5
+ require_relative "facade/json_door"
6
+ require_relative "naming"
7
+ require_relative "projector"
8
+ require_relative "runtime/errors"
9
+
10
+ module Hecks
11
+ # THE BUS, NOT A DOOR — `docs/hecks-survey-what-we-wish-we-had.md` and
12
+ # `docs/future-features.md` both name the sibling project's own
13
+ # "Storehouse" the single highest-priority gap this repo had: "no
14
+ # per-command tool... the bluebook IS the contract, the [door] just
15
+ # projects it." This module IS that bus, borrowing its name too — the
16
+ # dispatch/query/state/... surface below is a pure function of a
17
+ # `Runtime::Dispatcher` plus plain Ruby arguments, no IO, no protocol
18
+ # awareness. `bin/hecks_mcp_door` is ONE projection of it — MCP over
19
+ # stdio — not the whole thing; a plain CLI door, an HTTP door, a second
20
+ # transport of any shape, would sit beside it on the exact same bus,
21
+ # sharing the same audit log and caller-identity handling, without
22
+ # ever needing to speak MCP. (This module used to BE named `McpDoor`
23
+ # and live under `Facade` — that conflated the bus with its one built
24
+ # transport; this file is the rename, not a rewrite.)
25
+ #
26
+ # catalog — what aggregates a domain declares, and what each can do
27
+ # describe — one aggregate's full command/query/refusal contract
28
+ # validate — is this domain's wiring sound (deep: also model-checks it)
29
+ # domains — every domain directory under a root, discovered not typed
30
+ # dispatch — issue a command (dry_run: preview, steps: batch)
31
+ # query — ask a question
32
+ # state — read what is actually stored, no verb involved
33
+ # events — what HAPPENED, with payloads, to one record — not just its
34
+ # current state; events THIS BUS witnessed, sourced from
35
+ # its own audit log, not a full event-sourcing replay
36
+ # history — the full append-only journal, not just current state
37
+ # behaviors — run a domain's hand-curated `.behaviors` examples
38
+ # follow — tail this bus's own dispatch/query/state audit log
39
+ #
40
+ # `dispatch`/`query`/`state` EACH TAKE A `summary` — the survey's "every
41
+ # audit row carries human intent for free" — and `dispatch`/`query`
42
+ # additionally take an optional `source:` (`SOURCE_TAGS`, the survey's
43
+ # `SourceTag`: who is calling) AND an optional `role:`/`actor_id:` —
44
+ # a real caller identity, bound for the call's duration via `Hecks.
45
+ # as_caller`, checked against a `role`-gated command's own declared
46
+ # role (`CommandRules::Authorization`) rather than merely documented by
47
+ # `describe`. `dispatch` REQUIRES it for any command that declares a
48
+ # role — `require_caller_for_role_gated!` refuses an unbound dispatch
49
+ # against one rather than silently running it unchecked; `query`'s own
50
+ # authorization runs on a separate mechanism (`Runtime::TenantScope`)
51
+ # that `role:` does not gate. This is self-asserted identity, not
52
+ # authentication: the caller states its own `role`/`actor_id`, and this
53
+ # bus checks it consistently once stated — see `bin/hecks_mcp_door`'s
54
+ # header for what that does and does not guarantee. Every call through
55
+ # those three, plus a dry run, is appended to a per-domain JSONL audit
56
+ # log (`record!`) `follow` tails back — a record of what the caller
57
+ # SAID it was, not independently verified identity. `catalog`/
58
+ # `describe`/`validate`/`domains`/`history`/`behaviors`/`events` need
59
+ # neither — they change nothing and commit nothing to any log.
60
+ #
61
+ # A CALLER HANDS IN AN ALREADY-BOOTED `runtime`, the same division of
62
+ # labor `Facade::CliRunner` already keeps against `bin/run`: booting a
63
+ # domain from a path is IO the calling `bin/` script owns, this stays a
64
+ # pure function of a `Runtime::Dispatcher` plus plain Ruby arguments —
65
+ # no different from `CliRunner.call(runtime:, argv:)` itself.
66
+ # `validate` and `domains` are the two exceptions: `validate`'s whole
67
+ # job is to attempt the boot and report whether it survived, so it
68
+ # takes the domain PATH instead and boots it; `domains` has no
69
+ # domain to be handed one of yet, that's what it's answering.
70
+ #
71
+ # NO NEW VOCABULARY otherwise. Every method here composes doors that
72
+ # already exist — `Projector.call(:cli, ...)` for verb/question alias
73
+ # resolution (the identical table `CliRunner` itself resolves against),
74
+ # `Projector.call(:docs, ...)` for `describe`, `Facade::JsonDoor` for
75
+ # the JSON↔Runtime::Value boundary, `Registry#repository` for `state`/
76
+ # `history`, `Registry#verify!` (run by every boot) for `validate`,
77
+ # `Bluebook::ModelCheck` for `validate(deep: true)`, `Hecks::Behaviors`
78
+ # for `behaviors`, `Adapters::Folder#domain?` for `domains`,
79
+ # `Dispatcher#dry_run?` for `dispatch(dry_run: true)`. A second copy of
80
+ # any of these here would be the exact duplication `Facade::JsonDoor`'s
81
+ # own header already warns against.
82
+ module Storehouse
83
+ module_function
84
+
85
+ # THE SAME CLOSED SET `docs/hecks-survey-what-we-wish-we-had.md`'s
86
+ # `SourceTag` names — WHO dispatched, not WHAT. Optional: a caller
87
+ # that omits it gets `source: nil` recorded, honestly, rather than a
88
+ # guessed default.
89
+ SOURCE_TAGS = %w[process-manager operator hook sidequest-agent cascade daemon].freeze
90
+
91
+ # THE BUS'S OWN AUDIT TRAIL — a JSONL file per domain, one line per
92
+ # `dispatch`/`query`/`state`/dry-run call, `follow` tails it back.
93
+ # `tmp/`, not the domain's own directory: this is the BUS's record
94
+ # of what was asked of it, not part of the domain's own persisted
95
+ # state, and `tmp/` is already gitignored for exactly this kind of
96
+ # local, disposable-but-useful-while-it-lasts file.
97
+ LOG_ROOT = File.expand_path("../../tmp/storehouse", __dir__)
98
+
99
+ # THE ROOT EVERY `domain:`/`under:` MUST RESOLVE UNDER — the project
100
+ # directory by default, `HECKS_STOREHOUSE_ROOT` to widen or move it.
101
+ # `Hecks.boot` `Kernel.load`s the `.hecksagon`/`.bluebook`/`.world`
102
+ # files a domain path resolves to, and those are Ruby, not a data
103
+ # format — a caller-supplied path with no confinement at all is an
104
+ # unmarked door out of "a narrower, checked surface" (the README's
105
+ # own pitch for this bus) and into arbitrary code execution from any
106
+ # path on disk. This is a boundary a local convention was standing
107
+ # in for, not new behavior for a caller already confined to the
108
+ # project tree.
109
+ BOOT_ROOT = File.expand_path(ENV["HECKS_STOREHOUSE_ROOT"] || File.expand_path("../..", __dir__))
110
+
111
+ # REFUSED, NOT SILENTLY CLAMPED — a path outside `BOOT_ROOT` is either
112
+ # an honest mistake (a relative path typed against the wrong cwd) or
113
+ # the exact thing this check exists to catch, and both deserve the
114
+ # same clear refusal rather than a silent rewrite to something the
115
+ # caller didn't ask for.
116
+ def confine!(path, label)
117
+ resolved = File.expand_path(path.to_s, BOOT_ROOT)
118
+ return resolved if resolved == BOOT_ROOT || resolved.start_with?("#{BOOT_ROOT}#{File::SEPARATOR}")
119
+
120
+ raise Runtime::TypeMismatch,
121
+ "#{label}: #{path.inspect} resolves outside #{BOOT_ROOT} — this bus only boots domains under its " \
122
+ "own root (HECKS_STOREHOUSE_ROOT to widen it)"
123
+ end
124
+
125
+ # ── shared resolution helpers ────────────────────────────────────
126
+
127
+ # THE ONE BLUEBOOK A DOMAIN DIRECTORY BOOTS. Every `bin/*` script that
128
+ # projects a whole-domain CLI or doc set makes this same assumption
129
+ # (`Facade::CliRunner#call`'s own `bluebook = runtime.registry.
130
+ # bluebooks.values.first`) — one `.hecksagon` names one chapter.
131
+ def bluebook_for(runtime)
132
+ runtime.registry.bluebooks.values.first or
133
+ raise Runtime::NotFound, "this boot loaded no bluebook"
134
+ end
135
+
136
+ def aggregate_ir!(bluebook, name)
137
+ bluebook.aggregate(name) or
138
+ raise Runtime::NotFound, "#{bluebook.name} declares no aggregate named #{name.inspect} — " \
139
+ "known: #{bluebook.aggregates.map(&:hecks_name).sort.join(', ')}"
140
+ end
141
+
142
+ # THE SAME ALIAS TABLE `CliRunner` RESOLVES A TYPED WORD AGAINST — a
143
+ # short name when it's unambiguous, the qualified `Aggregate.Verb`
144
+ # form always. Shared here so `dispatch` and `query` (and their error
145
+ # messages) never drift from what a human typing `bin/run` sees.
146
+ def resolve!(cli, name, asking:)
147
+ pool = asking ? cli[:questions] : cli[:verbs]
148
+ key = cli[:names][asking ? :question : :command][name]
149
+ spec = pool[key]
150
+ return spec if spec
151
+
152
+ known = cli[:names][asking ? :question : :command].keys.sort.join(", ")
153
+ raise Runtime::NotFound, "no such #{asking ? 'query' : 'command'}: #{name.inspect} — known: #{known}"
154
+ end
155
+
156
+ def require_summary!(summary)
157
+ return unless summary.nil? || summary.to_s.strip.empty?
158
+
159
+ raise Runtime::TypeMismatch,
160
+ "a one-line summary: is required on dispatch/query/state — it is what makes an audit row legible later"
161
+ end
162
+
163
+ def valid_source!(source)
164
+ return if source.nil? || SOURCE_TAGS.include?(source.to_s)
165
+
166
+ raise Runtime::TypeMismatch, "source: #{source.inspect} is not one of #{SOURCE_TAGS.join(', ')}"
167
+ end
168
+
169
+ # `actor_id` NAMES WHO, `role` NAMES WHAT THEY HOLD — `Hecks.
170
+ # as_caller` requires the latter always, the former is additive
171
+ # (`Runtime::Caller::Current`'s own shape). An `actor_id` with no
172
+ # `role` would silently do nothing rather than bind a real caller,
173
+ # which is worse than refusing: a caller who thinks they've
174
+ # identified themselves and haven't deserves to be told.
175
+ def valid_caller!(role, actor_id)
176
+ return unless actor_id && role.nil?
177
+
178
+ raise Runtime::TypeMismatch, "actor_id: requires role: too — a caller names WHO through WHICH role they hold"
179
+ end
180
+
181
+ # BOUND FOR THE DURATION OF ONE CALL, THEN GONE — `Hecks.as_caller`
182
+ # is itself a `Thread.current`-scoped `ensure`-guarded block, so
183
+ # nothing here needs its own cleanup. `role: nil` yields unbound —
184
+ # for `query`, exactly as before: `CommandRules::Authorization#
185
+ # refuse_role_mismatch` is OPT-IN on the domain side (`return unless
186
+ # caller`), and query authorization runs on a wholly separate
187
+ # mechanism (`authorize policy, tenant: :field`, checked against an
188
+ # explicit `tenant:` argument — see `Runtime::TenantScope`), so
189
+ # binding a caller around a query has no effect on it TODAY; it is
190
+ # still accepted here, for symmetry and for the audit log, against
191
+ # the day a read model does check `Caller.current`. For `dispatch`,
192
+ # `require_caller_for_role_gated!` (below) now refuses BEFORE this
193
+ # is ever reached when the command declares a role and no caller is
194
+ # bound — so an unbound `dispatch` here means either the command
195
+ # declares no role at all, or a caller-side check let it through.
196
+ def with_caller(role, actor_id, &block)
197
+ return block.call if role.nil?
198
+
199
+ Hecks.as_caller(role: role, actor_id: actor_id, &block)
200
+ end
201
+
202
+ # THE FAIL-OPEN HALF `with_caller` ITSELF CANNOT CLOSE — ADR 0025's
203
+ # Governance RBAC work fixed WHAT a *bound* role is checked against
204
+ # (a live `Governance::RoleAssignment` lookup instead of a bare
205
+ # string match), but changed nothing about a caller who binds no
206
+ # role at all: `refuse_role_mismatch` `return`s immediately when
207
+ # `Caller.current` is nil, so a bus caller who simply omits `role:`
208
+ # sails past a role-gated command unchecked, not denied. That is a
209
+ # property of THIS BUS choosing to dispatch unbound, not of the
210
+ # domain rule — `bin/run`, the human CLI, has no such gap because a
211
+ # human always dispatches through a real `Hecks.as_caller` binding
212
+ # upstream of it. Refusing here, before `with_caller`/`dispatch` are
213
+ # ever reached, makes the bus keep the same promise: a command whose
214
+ # bluebook declares a role is not run through this bus without one.
215
+ def require_caller_for_role_gated!(spec, role)
216
+ return unless spec[:role_gated] && role.nil?
217
+
218
+ raise Runtime::Unauthorized,
219
+ "#{spec[:verb]} requires role: #{spec[:role].inspect} — this command is role-gated and no caller " \
220
+ "(role:/actor_id:) is bound; dispatching it unbound is refused, not silently unchecked"
221
+ end
222
+
223
+ # `dry_run?` (Runtime::Dispatcher) understands only the OLD flat
224
+ # legacy args shape — no to:/with: envelope, `route:` never passed
225
+ # (its own header explains why: built directly against
226
+ # CommandInterpreter/EntityInterpreter's pre-envelope contract,
227
+ # never updated because nothing else needed it to be — a real
228
+ # record of history, not a defect this bus should paper over
229
+ # silently). `Facade::CommandRequest`/`spec[:legacy_receiver]`
230
+ # already know how to NAME that same flat shape for every receiver
231
+ # kind (a bare id under one string key for :aggregate, a
232
+ # {aggregate:, entity:} pair of keys for :entity) — this is the one
233
+ # door back INTO it.
234
+ def flatten_legacy(envelope, receiver, legacy_receiver)
235
+ facts = envelope[:with] || {}
236
+ return facts unless envelope.key?(:to)
237
+
238
+ route = envelope[:to]
239
+ case receiver
240
+ when :aggregate then facts.merge(legacy_receiver.to_sym => route)
241
+ when :entity
242
+ facts.merge(legacy_receiver.fetch(:aggregate).to_sym => route[:aggregate],
243
+ legacy_receiver.fetch(:entity).to_sym => route[:entity])
244
+ else facts
245
+ end
246
+ end
247
+
248
+ # ── the audit log `follow` reads back ───────────────────────────────
249
+
250
+ def log_path(domain_name)
251
+ File.join(LOG_ROOT, "#{domain_name.to_s.gsub(/[^A-Za-z0-9_-]/, '_')}.jsonl")
252
+ end
253
+
254
+ # NEVER FAILS A REAL CALL BECAUSE ITS OWN AUDIT LOG COULDN'T BE
255
+ # WRITTEN — a full disk or a permissions problem is a `follow`
256
+ # feature going dark, not a reason to refuse the dispatch/query/
257
+ # state call that was actually asked for.
258
+ def record!(domain_name, tool:, summary:, source:, outcome:, verb: nil, role: nil, actor_id: nil)
259
+ return unless domain_name
260
+
261
+ entry = { time: Time.now.utc.iso8601, tool: tool, verb: verb, summary: summary, source: source,
262
+ role: role, actor_id: actor_id,
263
+ ok: outcome[:ok], id: outcome[:id], error: outcome[:error], events: outcome[:events] }.compact
264
+ FileUtils.mkdir_p(LOG_ROOT)
265
+ File.open(log_path(domain_name), "a") { |f| f.puts(JSON.generate(entry)) }
266
+ rescue StandardError
267
+ nil
268
+ end
269
+
270
+ # ── the three that drive it ────────────────────────────────────────
271
+
272
+ # `dry_run: true` ANSWERS A DIFFERENT QUESTION than a real dispatch
273
+ # does — "would this succeed", not "here is what happened" — so a
274
+ # domain refusal is the legitimate, complete ANSWER (`ok: true,
275
+ # would_succeed: false`), not a failed call. A malformed request
276
+ # (unknown command, a bad args shape) is still a failed call
277
+ # (`ok: false`) either way — it never reached the domain to be asked.
278
+ def dispatch(runtime:, command:, summary:, args: {}, source: nil, dry_run: false, role: nil, actor_id: nil)
279
+ bluebook = bluebook_for(runtime)
280
+ tool = dry_run ? "dry_run" : "dispatch"
281
+ outcome = perform_dispatch(runtime, bluebook, command, summary, args, source, dry_run, role, actor_id)
282
+
283
+ record!(bluebook.name, tool: tool, verb: outcome[:verb], summary: summary, source: source,
284
+ outcome: outcome, role: role, actor_id: actor_id)
285
+ outcome.except(:verb)
286
+ rescue *refusal_classes => e
287
+ outcome = refused(e, summary: summary)
288
+ record!(bluebook&.name, tool: tool, summary: summary, source: source, outcome: outcome,
289
+ role: role, actor_id: actor_id)
290
+ outcome
291
+ end
292
+
293
+ def perform_dispatch(runtime, bluebook, command, summary, args, source, dry_run, role, actor_id)
294
+ require_summary!(summary)
295
+ valid_source!(source)
296
+ valid_caller!(role, actor_id)
297
+ cli = Projector.call(:cli, bluebook: bluebook, options: { program: "mcp" })
298
+ spec = resolve!(cli, command, asking: false)
299
+ require_caller_for_role_gated!(spec, role)
300
+ envelope = Facade::CommandRequest.normalize(Facade::JsonDoor.deep_symbolize(args),
301
+ receiver: spec[:receiver],
302
+ legacy_receiver: spec[:legacy_receiver])
303
+
304
+ result = with_caller(role, actor_id) do
305
+ dry_run ? dry_run_outcome(runtime, spec, envelope, summary: summary) : real_dispatch(runtime, spec, envelope, summary)
306
+ end
307
+ result.merge(verb: spec[:verb])
308
+ end
309
+
310
+ def real_dispatch(runtime, spec, envelope, summary)
311
+ result = runtime.dispatch(spec[:verb], **envelope)
312
+ ok(summary: summary,
313
+ id: result.id,
314
+ state: result.state.nil? ? nil : Facade::JsonDoor.materialize(result.state),
315
+ events: result.events.map { |event| { name: event.name, payload: Facade::JsonDoor.materialize(event.payload) } })
316
+ end
317
+
318
+ def dry_run_outcome(runtime, spec, envelope, summary:)
319
+ flat = flatten_legacy(envelope, spec[:receiver], spec[:legacy_receiver])
320
+ runtime.dry_run?(spec[:verb], **flat)
321
+ ok(summary: summary, would_succeed: true)
322
+ rescue Runtime::WiringError, *Runtime::DOMAIN_REFUSALS => e
323
+ ok(summary: summary, would_succeed: false, error: e.message)
324
+ end
325
+
326
+ # ONE CALL, MANY STEPS — the survey's own `bin/run <domain> script`
327
+ # shape, so an agent issuing a known SEQUENCE of commands (open an
328
+ # account, then fund it) pays one round trip instead of N. Every step
329
+ # goes through `dispatch` itself — same resolution, same audit log
330
+ # line per step, same summary/source stamped on all of them since
331
+ # the batch is what the caller is declaring intent about, not each
332
+ # individual step. Runs every step regardless of an earlier one
333
+ # refusing — a later step naming a record an earlier step never
334
+ # created will refuse honestly on its own account, which is more
335
+ # informative than silently dropping the rest of the batch.
336
+ def dispatch_batch(runtime:, steps:, summary:, source: nil, role: nil, actor_id: nil)
337
+ require_summary!(summary)
338
+ results = Array(steps).map do |raw|
339
+ step = Facade::JsonDoor.deep_symbolize(raw)
340
+ dispatch(runtime: runtime, command: step[:command], args: step[:args] || {},
341
+ summary: summary, source: source, role: role, actor_id: actor_id)
342
+ end
343
+ { ok: results.all? { |r| r[:ok] }, summary: summary, results: results }
344
+ rescue *refusal_classes => e
345
+ refused(e, summary: summary)
346
+ end
347
+
348
+ def query(runtime:, question:, summary:, args: {}, source: nil, role: nil, actor_id: nil)
349
+ bluebook = bluebook_for(runtime)
350
+ outcome = perform_query(bluebook, runtime, question, summary, args, source, role, actor_id)
351
+
352
+ record!(bluebook.name, tool: "query", verb: outcome[:verb], summary: summary, source: source,
353
+ outcome: outcome, role: role, actor_id: actor_id)
354
+ outcome.except(:verb)
355
+ rescue *refusal_classes => e
356
+ outcome = refused(e, summary: summary)
357
+ record!(bluebook&.name, tool: "query", summary: summary, source: source, outcome: outcome,
358
+ role: role, actor_id: actor_id)
359
+ outcome
360
+ end
361
+
362
+ def perform_query(bluebook, runtime, question, summary, args, source, role, actor_id)
363
+ require_summary!(summary)
364
+ valid_source!(source)
365
+ valid_caller!(role, actor_id)
366
+ cli = Projector.call(:cli, bluebook: bluebook, options: { program: "mcp" })
367
+ spec = resolve!(cli, question, asking: true)
368
+ rows = with_caller(role, actor_id) { runtime.query(spec[:verb], **Facade::JsonDoor.deep_symbolize(args)) }
369
+
370
+ ok(summary: summary, rows: rows.map { |row| Facade::JsonDoor.materialize(row) }).merge(verb: spec[:verb])
371
+ end
372
+
373
+ # WHAT IS ACTUALLY STORED — no verb, no interpretation, the repository
374
+ # itself. `id:` given answers one record (`NotFound` when it names
375
+ # nothing); omitted answers every record the aggregate currently
376
+ # holds. This is the difference `query` can't cover: a query answers a
377
+ # DECLARED question, and an aggregate that never declared "list
378
+ # everything" has no query this could reuse.
379
+ def state(runtime:, aggregate:, summary:, id: nil, source: nil)
380
+ bluebook = bluebook_for(runtime)
381
+ outcome = perform_state(runtime, bluebook, aggregate, summary, id)
382
+
383
+ record!(bluebook.name, tool: "state", summary: summary, source: source, outcome: outcome)
384
+ outcome
385
+ rescue *refusal_classes => e
386
+ outcome = refused(e, summary: summary)
387
+ record!(bluebook&.name, tool: "state", summary: summary, source: source, outcome: outcome)
388
+ outcome
389
+ end
390
+
391
+ def perform_state(runtime, bluebook, aggregate, summary, id)
392
+ require_summary!(summary)
393
+ ir = aggregate_ir!(bluebook, aggregate)
394
+ repository = runtime.registry.repository(bluebook.name, ir)
395
+
396
+ if id
397
+ instance = repository.find(id) or
398
+ raise Runtime::NotFound, "no #{ir.hecks_name} found for id #{id.inspect}"
399
+ ok(summary: summary, record: Facade::JsonDoor.materialize(instance.to_h))
400
+ else
401
+ records = repository.all.map { |instance| Facade::JsonDoor.materialize(instance.to_h) }
402
+ ok(summary: summary, count: records.length, records: records)
403
+ end
404
+ end
405
+
406
+ # ── the four zoom levels ─────────────────────────────────────────
407
+
408
+ # ZOOM LEVEL ZERO — every domain directory a root actually holds,
409
+ # discovered rather than typed from memory. Every other tool takes
410
+ # `domain:` as a directory it assumes the caller already knows; this
411
+ # is how a caller who doesn't finds out. `Adapters::Folder#domain?`
412
+ # is the same predicate `domain_root`/`nearest_domain` already walk
413
+ # up directories checking — a bare `.hecksagon` or one under
414
+ # `bluebook/`, the two real shapes this corpus uses.
415
+ def domains(under: "examples")
416
+ root = confine!(under, "under")
417
+ return ok(under: under, domains: []) unless Dir.exist?(root)
418
+
419
+ folder = Adapters::Folder.new
420
+ found = Dir.children(root).sort.select { |name| folder.domain?(File.join(root, name)) }
421
+
422
+ ok(under: under, domains: found.map { |name| File.join(under, name) })
423
+ end
424
+
425
+ # ZOOM LEVEL ONE — every aggregate this domain declares, and every
426
+ # command/query name each answers to, snake_cased exactly as
427
+ # `dispatch`/`query` want it. Enough to pick a target; `describe` is
428
+ # the next level down for what one of them actually takes.
429
+ def catalog(runtime:)
430
+ bluebook = bluebook_for(runtime)
431
+
432
+ ok(domain: bluebook.name,
433
+ aggregates: bluebook.aggregates.map do |aggregate|
434
+ { name: aggregate.hecks_name,
435
+ commands: aggregate.commands.map { |c| "#{Naming.snake(c.hecks_name)}!" }.sort,
436
+ queries: aggregate.queries.map { |q| Naming.snake(q.hecks_name) }.sort }
437
+ end)
438
+ rescue *refusal_classes => e
439
+ refused(e)
440
+ end
441
+
442
+ # ZOOM LEVEL TWO — the exact same usage document a human gets from
443
+ # `bin/docs <domain> [aggregate]` (`Projector::DocsProjector`, the
444
+ # identical projection `Surface::AggregateDoor#docs` calls one door
445
+ # over): every command's arguments, the states it may be issued
446
+ # from, and every way it can refuse. `aggregate:` omitted answers the
447
+ # whole chapter.
448
+ def describe(runtime:, aggregate: nil)
449
+ bluebook = bluebook_for(runtime)
450
+ options = aggregate ? { aggregate: aggregate_ir!(bluebook, aggregate).hecks_name } : {}
451
+
452
+ ok(domain: bluebook.name, docs: Projector.call(:docs, bluebook: bluebook, options: options))
453
+ rescue *refusal_classes => e
454
+ refused(e)
455
+ end
456
+
457
+ # ZOOM LEVEL THREE — is the wiring sound at all: every bind names a
458
+ # declared aggregate, every adapter satisfies the port it claims, the
459
+ # default adapter is usable. `Registry#verify!` (`runtime/registry/
460
+ # verification.rb`) is the one place this repo already answers that
461
+ # question, and `Runtime::Loader.boot` already calls it as the last
462
+ # step of every boot — so THIS is the one method here that boots for
463
+ # itself rather than taking a `runtime:` already in hand, because a
464
+ # runtime that successfully reached this line already answered the
465
+ # question. Given a domain PATH, not a booted runtime, deliberately:
466
+ # asking "is this valid" about a domain that failed to boot at all
467
+ # has to be askable without a runtime to hand it.
468
+ #
469
+ # `deep: true` GOES PAST WIRING INTO LOGIC — `Bluebook::ModelCheck`,
470
+ # the lightweight-formal-methods leg (dead lifecycle transitions, a
471
+ # saga state no handler chain reaches, a dispatch to nowhere). Opt-in
472
+ # and separate from the base check on purpose: a wiring defect is
473
+ # "this cannot run at all", a model-check finding is "this runs, but
474
+ # part of it can never fire" — two different questions, and the
475
+ # first is far cheaper to ask on every boot.
476
+ #
477
+ # ANY BOOT FAILURE ANSWERS THE QUESTION, not only `WiringError` — a
478
+ # domain path with no `.hecksagon`, a malformed bluebook, is just as
479
+ # much "not valid" as a real wiring mismatch, and this tool exists
480
+ # precisely so none of those ever cross a projection of this bus as
481
+ # a crash.
482
+ def validate(domain:, deep: false)
483
+ runtime = Hecks.boot(confine!(domain, "domain"), install_facade: false)
484
+ result = { ok: true, domain: domain, valid: true }
485
+
486
+ if deep
487
+ require_relative "bluebook/model_check"
488
+ findings = runtime.registry.bluebooks.values.flat_map { |bluebook| Bluebook::ModelCheck.call(bluebook) }
489
+ result[:findings] = findings.map { |f| { kind: f.kind, severity: f.severity, subject: f.subject, message: f.message } }
490
+ end
491
+
492
+ result
493
+ rescue StandardError => e
494
+ { ok: false, domain: domain, valid: false, error: "#{e.class}: #{e.message}" }
495
+ end
496
+
497
+ # ── beyond the zoom levels ──────────────────────────────────────────
498
+
499
+ # THE FULL WRITE HISTORY, not just the current head — `bin/history`'s
500
+ # own logic, unchanged: an append-only-backed aggregate's `entries`,
501
+ # every operation that ever touched it. An aggregate bound to a
502
+ # non-append-only adapter (Memory, Postgres proper) answers an empty
503
+ # list honestly rather than pretending to a history it never kept.
504
+ def history(runtime:)
505
+ bluebook = bluebook_for(runtime)
506
+ entries = bluebook.aggregates.each_with_object({}) do |aggregate, all|
507
+ repository = runtime.registry.repository(bluebook.name, aggregate)
508
+ all[aggregate.storage_name] = journal_entries(repository)
509
+ end
510
+
511
+ ok(domain: bluebook.name, history: entries)
512
+ rescue *refusal_classes => e
513
+ refused(e)
514
+ end
515
+
516
+ def journal_entries(repository)
517
+ return [] unless repository.is_a?(Ports::Persistence::AppendOnly)
518
+
519
+ repository.entries.map { |entry| { operation: entry.operation, id: entry.id, state: Facade::JsonDoor.materialize(entry.state) } }
520
+ end
521
+
522
+ # `.behaviors` FILES, RUN AND REPORTED — hand-curated examples of how
523
+ # to use a domain, in domain vocabulary (`docs/guides/behaviors.md`),
524
+ # the survey's own "honest-refusal", generated-example-suite items.
525
+ # `Hecks::Behaviors` boots each test fresh through `Hecks.boot_files`
526
+ # itself — `target:` names a `.behaviors` file OR a directory to
527
+ # sweep, never a `runtime:`, the one other method here besides
528
+ # `validate` that takes a path instead.
529
+ def behaviors(target:)
530
+ require_relative "behaviors"
531
+ raise Runtime::NotFound, "no such file or directory: #{target.inspect}" unless target && File.exist?(target)
532
+
533
+ if File.directory?(target)
534
+ sweep = Hecks::Behaviors.run_all(target)
535
+ ok(target: target, files: sweep.files.map { |file| behaviors_file(file) }, counts: sweep.summary)
536
+ else
537
+ result = Hecks::Behaviors.run(target)
538
+ ok(target: target, files: [behaviors_file(result)], counts: Hecks::Behaviors.summarize([result]))
539
+ end
540
+ rescue *refusal_classes => e
541
+ refused(e)
542
+ end
543
+
544
+ def behaviors_file(result)
545
+ { path: result.path,
546
+ parse_error: result.parse_error,
547
+ runs: Array(result.runs).map { |run| { description: run.description, status: run.status, message: run.message } } }
548
+ end
549
+
550
+ # A LIVE TAIL WITHOUT A LIVE PROCESS — `bin/hecks_mcp_door` (its
551
+ # transport of MCP-over-stdio) answers one request at a time, no push
552
+ # channel to a client that only ever asks. This is the honest version
553
+ # of the survey's `storehouse follow` for that shape: not a
554
+ # subscription, a durable JSONL log every `dispatch`/`query`/`state`/
555
+ # dry-run call appends to (`record!`), tailed back here. Still real,
556
+ # still cross-process — the log outlives any one door's own process —
557
+ # just pull instead of push.
558
+ def follow(runtime:, limit: 20)
559
+ bluebook = bluebook_for(runtime)
560
+ entries = log_lines(bluebook.name).last([limit.to_i, 1].max)
561
+
562
+ ok(domain: bluebook.name, entries: entries)
563
+ rescue *refusal_classes => e
564
+ refused(e)
565
+ end
566
+
567
+ def log_lines(domain_name)
568
+ path = log_path(domain_name)
569
+ return [] unless File.exist?(path)
570
+
571
+ File.readlines(path).map { |line| JSON.parse(line, symbolize_names: true) }
572
+ end
573
+
574
+ # WHAT ACTUALLY HAPPENED, with payloads — distinct from `state`
575
+ # (what's stored NOW) and `history` (append-only operation
576
+ # SNAPSHOTS, no payload). NOT a domain-wide event-sourcing replay:
577
+ # "one boot per call" means `runtime.events` is always empty except
578
+ # during the very call that populated it, discarded the moment that
579
+ # call returns — there is no cross-call in-memory log to read here.
580
+ # So this reads the SAME durable audit log `follow` already tails
581
+ # (`record!` now stamps a successful dispatch's own announced
582
+ # events onto its log line), reshaped: `follow` answers "what was
583
+ # CALLED, in order, across every tool"; this answers "what HAPPENED
584
+ # to one aggregate/record" — events THIS BUS witnessed, which is
585
+ # every real dispatch ever routed through it, but no more than that.
586
+ # `aggregate:` narrows to one aggregate; `id:` (requires
587
+ # `aggregate:` — an id alone is not unique across aggregates)
588
+ # narrows to one record's own events.
589
+ def events(runtime:, aggregate: nil, id: nil, limit: nil)
590
+ raise Runtime::TypeMismatch, "id: requires aggregate: too — an id alone is not unique across aggregates" if id && !aggregate
591
+
592
+ bluebook = bluebook_for(runtime)
593
+ fqn = aggregate ? "#{bluebook.name}::#{aggregate_ir!(bluebook, aggregate).hecks_name}" : nil
594
+
595
+ found = log_lines(bluebook.name).filter_map do |entry|
596
+ next unless entry[:tool] == "dispatch" && entry[:ok] && entry[:events]
597
+ next if fqn && !entry[:verb].to_s.start_with?("#{fqn}.")
598
+ next if id && entry[:id] != id
599
+
600
+ entry[:events].map { |event| event.merge(time: entry[:time], verb: entry[:verb], id: entry[:id]) }
601
+ end.flatten(1)
602
+
603
+ found = found.last(limit.to_i) if limit
604
+
605
+ ok(domain: bluebook.name, events: found)
606
+ rescue *refusal_classes => e
607
+ refused(e)
608
+ end
609
+
610
+ # ── shared shape ────────────────────────────────────────────────────
611
+
612
+ # `Runtime::WiringError` BELONGS HERE TOO, alongside the true domain
613
+ # refusals — not because it IS one (it's a structural defect, not a
614
+ # rule the caller broke), but because "this domain isn't wired to
615
+ # answer what you're asking" (a `role:`+`actor_id:` caller reaching
616
+ # an authorization port nothing implements, a dry run against a
617
+ # port verb) is exactly the shape this bus promises never crashes
618
+ # through it. `dry_run_outcome` already treats it this way locally;
619
+ # this makes every OTHER caller of `refusal_classes` do the same.
620
+ def refusal_classes = [Runtime::NotFound, Runtime::TypeMismatch, Runtime::WiringError, *Runtime::DOMAIN_REFUSALS]
621
+
622
+ def ok(**fields) = { ok: true }.merge(fields)
623
+
624
+ # AN HONEST REFUSAL, NOT A CRASH — the survey's own item #9: "an
625
+ # explicit, structured refusal a caller can act on" rather than a
626
+ # stack trace an agent has to parse to find the one line that
627
+ # mattered. The domain's own refusal text travels verbatim
628
+ # (`RefusalWording` already renders every one of these to be read),
629
+ # this only wraps it consistently.
630
+ def refused(error, summary: nil) = { ok: false, summary: summary, error: error.message }
631
+ end
632
+ end
@@ -0,0 +1,16 @@
1
+ module Hecks
2
+ # ISOLATED, ON PURPOSE — the gemspec reads this file directly rather
3
+ # than `require_relative "lib/hecks"` (the whole framework),
4
+ # specifically so evaluating the gemspec never triggers ANY of this
5
+ # gem's own dependencies (prism among them) before Bundler has even
6
+ # resolved what to install. A real, live chicken-and-egg bug caught
7
+ # deploying to a Ruby version that doesn't bundle prism for free
8
+ # (AWS Lambda's ruby3.2): the gemspec's own `require_relative
9
+ # "lib/hecks"` pulled in adapters/driven/prism.rb's
10
+ # unconditional `require "prism"`, which failed before Bundler ever
11
+ # read what the gemspec itself declared as a dependency — declaring
12
+ # it there, or even in the consuming Gemfile, never closed the gap,
13
+ # because gemspec evaluation happens before anything Bundler
14
+ # resolves is actually loadable yet.
15
+ VERSION = "1.0.0"
16
+ end