hecks 2.7.0 → 2.9.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 (463) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +11 -0
  3. data/lib/hecks/adapters/driven/claude_code.rb +4 -104
  4. data/lib/hecks/adapters/driven/d1.rb +44 -237
  5. data/lib/hecks/adapters/driven/folder.rb +17 -76
  6. data/lib/hecks/adapters/driven/google_authentication.rb +15 -49
  7. data/lib/hecks/adapters/driven/governance_authorization.rb +23 -70
  8. data/lib/hecks/adapters/driven/heki/journal.rb +8 -41
  9. data/lib/hecks/adapters/driven/heki/saga_store.rb +12 -54
  10. data/lib/hecks/adapters/driven/heki/snapshot.rb +4 -12
  11. data/lib/hecks/adapters/driven/heki.rb +13 -42
  12. data/lib/hecks/adapters/driven/identity_registry.rb +3 -14
  13. data/lib/hecks/adapters/driven/in_memory_ordering.rb +9 -26
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +2 -14
  15. data/lib/hecks/adapters/driven/lambda/client.rb +10 -51
  16. data/lib/hecks/adapters/driven/lambda.rb +10 -84
  17. data/lib/hecks/adapters/driven/local_storage.rb +5 -40
  18. data/lib/hecks/adapters/driven/memory.rb +18 -99
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +5 -21
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +6 -28
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +3 -12
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +4 -30
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +34 -108
  24. data/lib/hecks/adapters/driven/postgres.rb +145 -134
  25. data/lib/hecks/adapters/driven/postgres_era.adapter +4 -15
  26. data/lib/hecks/adapters/driven/prism.rb +7 -30
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +21 -121
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +8 -40
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +26 -82
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +40 -84
  31. data/lib/hecks/adapters/driven/sqlite.rb +128 -65
  32. data/lib/hecks/adapters/driven/tenant_provisioner.rb +15 -37
  33. data/lib/hecks/adapters/driven.rb +5 -36
  34. data/lib/hecks/adapters/driving/github_webhook.rb +8 -76
  35. data/lib/hecks/behaviors/dsl.rb +7 -25
  36. data/lib/hecks/behaviors/expectations.rb +32 -142
  37. data/lib/hecks/behaviors/ir.rb +7 -20
  38. data/lib/hecks/behaviors/rspec.rb +6 -17
  39. data/lib/hecks/behaviors/runner.rb +10 -34
  40. data/lib/hecks/behaviors.rb +4 -8
  41. data/lib/hecks/bluebook/aggregate.rb +9 -75
  42. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +9 -38
  43. data/lib/hecks/bluebook/assembly/build.rb +4 -15
  44. data/lib/hecks/bluebook/assembly/contract.rb +15 -136
  45. data/lib/hecks/bluebook/assembly/contracts.rb +54 -174
  46. data/lib/hecks/bluebook/assembly/marks.rb +31 -223
  47. data/lib/hecks/bluebook/assembly/specializer.rb +6 -52
  48. data/lib/hecks/bluebook/assembly.rb +6 -45
  49. data/lib/hecks/bluebook/attribute.rb +12 -61
  50. data/lib/hecks/bluebook/behaviour/aggregate.rb +10 -39
  51. data/lib/hecks/bluebook/behaviour/attribute.rb +3 -14
  52. data/lib/hecks/bluebook/behaviour/chapter.rb +17 -42
  53. data/lib/hecks/bluebook/behaviour/command.rb +14 -66
  54. data/lib/hecks/bluebook/behaviour/domain_port.rb +4 -20
  55. data/lib/hecks/bluebook/behaviour/entity.rb +7 -27
  56. data/lib/hecks/bluebook/behaviour/hexagon.rb +23 -41
  57. data/lib/hecks/bluebook/behaviour/lifecycle.rb +15 -40
  58. data/lib/hecks/bluebook/behaviour/policy.rb +23 -52
  59. data/lib/hecks/bluebook/behaviour/process_manager.rb +30 -57
  60. data/lib/hecks/bluebook/behaviour/read_model.rb +38 -26
  61. data/lib/hecks/bluebook/behaviour/traits.rb +27 -64
  62. data/lib/hecks/bluebook/behaviour/value_object.rb +5 -24
  63. data/lib/hecks/bluebook/capabilities.rb +17 -49
  64. data/lib/hecks/bluebook/chapter.rb +10 -48
  65. data/lib/hecks/bluebook/command.rb +23 -81
  66. data/lib/hecks/bluebook/domain_port.rb +14 -63
  67. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +50 -229
  68. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +50 -377
  69. data/lib/hecks/bluebook/dsl/attribute_collector.rb +33 -204
  70. data/lib/hecks/bluebook/dsl/binding_proxy.rb +17 -65
  71. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +86 -708
  72. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +56 -170
  73. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
  74. data/lib/hecks/bluebook/dsl/command_builder.rb +74 -546
  75. data/lib/hecks/bluebook/dsl/const_shim.rb +8 -41
  76. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +12 -87
  77. data/lib/hecks/bluebook/dsl/entity_builder.rb +44 -330
  78. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +20 -269
  79. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +26 -189
  80. data/lib/hecks/bluebook/dsl/identity_declaration.rb +13 -88
  81. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +6 -20
  82. data/lib/hecks/bluebook/dsl/policy_builder.rb +16 -88
  83. data/lib/hecks/bluebook/dsl/port_builder.rb +2 -6
  84. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +22 -96
  85. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +31 -184
  86. data/lib/hecks/bluebook/dsl/query_builder.rb +11 -42
  87. data/lib/hecks/bluebook/dsl/read_model_builder.rb +75 -201
  88. data/lib/hecks/bluebook/dsl/rule_reference.rb +22 -163
  89. data/lib/hecks/bluebook/dsl/translation_builder.rb +28 -114
  90. data/lib/hecks/bluebook/dsl/value_object_builder.rb +10 -124
  91. data/lib/hecks/bluebook/dsl/word_gate.rb +16 -162
  92. data/lib/hecks/bluebook/dsl/world_builder.rb +19 -106
  93. data/lib/hecks/bluebook/entity.rb +13 -71
  94. data/lib/hecks/bluebook/expression/ast_json.rb +24 -190
  95. data/lib/hecks/bluebook/expression/ast_reader.rb +12 -54
  96. data/lib/hecks/bluebook/expression/canonical_form.rb +11 -88
  97. data/lib/hecks/bluebook/expression/evaluator.rb +47 -295
  98. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +30 -147
  99. data/lib/hecks/bluebook/expression/resolver.rb +37 -660
  100. data/lib/hecks/bluebook/hexagon.rb +5 -28
  101. data/lib/hecks/bluebook/lifecycle.rb +5 -16
  102. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -7
  103. data/lib/hecks/bluebook/meta_validator/judge.rb +111 -361
  104. data/lib/hecks/bluebook/meta_validator/plan.rb +33 -162
  105. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -7
  106. data/lib/hecks/bluebook/meta_validator/readings.rb +56 -342
  107. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -176
  108. data/lib/hecks/bluebook/meta_validator/shapes.rb +54 -268
  109. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +47 -308
  110. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +7 -43
  111. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -18
  112. data/lib/hecks/bluebook/meta_validator.rb +79 -349
  113. data/lib/hecks/bluebook/model_check/client_profile.rb +10 -91
  114. data/lib/hecks/bluebook/model_check.rb +102 -600
  115. data/lib/hecks/bluebook/pattern_subset.rb +11 -69
  116. data/lib/hecks/bluebook/process_manager.rb +13 -83
  117. data/lib/hecks/bluebook/project_loader.rb +3 -39
  118. data/lib/hecks/bluebook/project_register.rb +12 -60
  119. data/lib/hecks/bluebook/query.rb +9 -48
  120. data/lib/hecks/bluebook/read_model.rb +57 -81
  121. data/lib/hecks/bluebook/reference.rb +7 -41
  122. data/lib/hecks/bluebook/smoke_test.rb +8 -98
  123. data/lib/hecks/bluebook/synthesizer.rb +13 -62
  124. data/lib/hecks/bluebook/translation.rb +26 -62
  125. data/lib/hecks/bluebook/value_object.rb +11 -36
  126. data/lib/hecks/bluebook.rb +5 -22
  127. data/lib/hecks/cache_dir.rb +84 -0
  128. data/lib/hecks/cli/document.rb +86 -0
  129. data/lib/hecks/cli/ir.rb +45 -0
  130. data/lib/hecks/cli/mcp.rb +31 -0
  131. data/lib/hecks/cli/mcp_door.rb +366 -0
  132. data/lib/hecks/cli/model_check.rb +203 -0
  133. data/lib/hecks/cli/project_cli.rb +90 -0
  134. data/lib/hecks/cli/project_diagrams.rb +36 -0
  135. data/lib/hecks/cli/run.rb +173 -0
  136. data/lib/hecks/cli/smoke_test.rb +45 -0
  137. data/lib/hecks/cli/stores.rb +53 -0
  138. data/lib/hecks/cli.rb +160 -0
  139. data/lib/hecks/construct.rb +8 -34
  140. data/lib/hecks/deploy/bluebook/deploy.bluebook +33 -142
  141. data/lib/hecks/deploy/bluebook/deploy.hecksagon +2 -9
  142. data/lib/hecks/embryonaut_bluebook/lock.rb +3 -35
  143. data/lib/hecks/embryonaut_bluebook/shape.rb +1 -17
  144. data/lib/hecks/embryonaut_bluebook/vendor.rb +8 -53
  145. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +4 -19
  146. data/lib/hecks/embryonaut_bluebook.rb +4 -71
  147. data/lib/hecks/facade/cli_door.rb +24 -106
  148. data/lib/hecks/facade/cli_runner.rb +28 -172
  149. data/lib/hecks/facade/command_request.rb +13 -33
  150. data/lib/hecks/facade/handle.rb +34 -121
  151. data/lib/hecks/facade/json_door.rb +31 -146
  152. data/lib/hecks/facade/surface/aggregate_door.rb +39 -104
  153. data/lib/hecks/facade/surface/chapter.rb +10 -54
  154. data/lib/hecks/facade/surface.rb +8 -23
  155. data/lib/hecks/facade.rb +10 -25
  156. data/lib/hecks/forms/app.rb +15 -70
  157. data/lib/hecks/forms/command_form_renderer.rb +10 -29
  158. data/lib/hecks/forms/field_renderer.rb +17 -156
  159. data/lib/hecks/forms/field_shape.rb +30 -222
  160. data/lib/hecks/forms/html.rb +12 -47
  161. data/lib/hecks/forms/index_renderer.rb +1 -4
  162. data/lib/hecks/forms/page.rb +3 -9
  163. data/lib/hecks/forms/params.rb +14 -60
  164. data/lib/hecks/forms/port_argument.rb +2 -19
  165. data/lib/hecks/forms/query_form_renderer.rb +4 -79
  166. data/lib/hecks/forms/record_renderer.rb +8 -75
  167. data/lib/hecks/forms/record_table.rb +7 -50
  168. data/lib/hecks/forms/reference_options.rb +3 -30
  169. data/lib/hecks/forms/value_object_shape.rb +8 -35
  170. data/lib/hecks/forms.rb +5 -33
  171. data/lib/hecks/fqn.rb +5 -12
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +16 -65
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +47 -134
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +5 -14
  175. data/lib/hecks/framework/bluebook/governance.bluebook +13 -34
  176. data/lib/hecks/framework/bluebook/identity.bluebook +11 -26
  177. data/lib/hecks/framework/bluebook/privacy.bluebook +8 -33
  178. data/lib/hecks/framework.rb +11 -80
  179. data/lib/hecks/freezer.rb +5 -39
  180. data/lib/hecks/grammar/expression.bluebook +15 -40
  181. data/lib/hecks/grammar/translation.bluebook +3 -6
  182. data/lib/hecks/grammar.rb +16 -41
  183. data/lib/hecks/ir.rb +14 -83
  184. data/lib/hecks/language/adapter.bluebook +2 -7
  185. data/lib/hecks/language/bluebook/aggregate.bluebook +56 -221
  186. data/lib/hecks/language/bluebook/attaches/paging.bluebook +6 -15
  187. data/lib/hecks/language/bluebook/bluebook.bluebook +15 -83
  188. data/lib/hecks/language/bluebook/bluebook.hecksagon +3 -15
  189. data/lib/hecks/language/bluebook/command.bluebook +27 -144
  190. data/lib/hecks/language/bluebook/entity.bluebook +24 -97
  191. data/lib/hecks/language/bluebook/policy.bluebook +16 -67
  192. data/lib/hecks/language/bluebook/process_manager.bluebook +28 -151
  193. data/lib/hecks/language/bluebook/projection.bluebook +126 -60
  194. data/lib/hecks/language/bluebook/query.bluebook +12 -43
  195. data/lib/hecks/language/bluebook/shape.bluebook +23 -73
  196. data/lib/hecks/language/bluebook/syntax.bluebook +51 -262
  197. data/lib/hecks/language/bluebook/vocabulary.bluebook +98 -197
  198. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +3 -10
  199. data/lib/hecks/language/hecksagon/domain_port.bluebook +4 -19
  200. data/lib/hecks/language/hecksagon/hecksagon.bluebook +6 -25
  201. data/lib/hecks/language/hecksagon/port_operation.bluebook +3 -14
  202. data/lib/hecks/language/oidc.json +35 -0
  203. data/lib/hecks/language/port.bluebook +4 -17
  204. data/lib/hecks/language/translation/translation.bluebook +3 -9
  205. data/lib/hecks/language/translation/translation_aggregate.bluebook +5 -24
  206. data/lib/hecks/language/world/wiring.bluebook +1 -3
  207. data/lib/hecks/language/world/world.bluebook +5 -7
  208. data/lib/hecks/literal.rb +8 -50
  209. data/lib/hecks/mcp_door_scope.rb +95 -0
  210. data/lib/hecks/mcp_stdio_guard.rb +4 -41
  211. data/lib/hecks/naming.rb +29 -137
  212. data/lib/hecks/ports/access_control.rb +7 -23
  213. data/lib/hecks/ports/agent/answers.rb +5 -23
  214. data/lib/hecks/ports/agent.rb +47 -172
  215. data/lib/hecks/ports/authentication.rb +12 -39
  216. data/lib/hecks/ports/authorization.rb +16 -43
  217. data/lib/hecks/ports/clock.rb +5 -45
  218. data/lib/hecks/ports/extraction.rb +7 -12
  219. data/lib/hecks/ports/identity_assignment.rb +8 -29
  220. data/lib/hecks/ports/identity_generation.rb +6 -29
  221. data/lib/hecks/ports/identity_resolution.rb +2 -10
  222. data/lib/hecks/ports/key_vault.rb +9 -45
  223. data/lib/hecks/ports/persistence/append_only.rb +62 -65
  224. data/lib/hecks/ports/persistence/binding_policy.rb +4 -14
  225. data/lib/hecks/ports/persistence/codec_boundary.rb +20 -80
  226. data/lib/hecks/ports/persistence/null_saga_store.rb +4 -20
  227. data/lib/hecks/ports/persistence/plugin.rb +13 -42
  228. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +38 -141
  229. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +15 -115
  230. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +16 -84
  231. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +7 -26
  232. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -22
  233. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +55 -181
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/events.rb +77 -0
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +12 -122
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +28 -128
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +73 -474
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +31 -124
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +45 -219
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +13 -108
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +18 -75
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +9 -49
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +6 -123
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +23 -55
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +14 -72
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +7 -16
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +14 -115
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +13 -45
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +127 -554
  250. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +13 -63
  251. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +2 -17
  252. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +3 -14
  253. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +13 -58
  254. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -17
  255. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +9 -26
  256. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +9 -25
  257. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +22 -122
  258. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +34 -123
  259. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +5 -12
  260. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +3 -9
  261. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -14
  262. data/lib/hecks/ports/persistence/plugins/era.rb +4 -23
  263. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +7 -23
  264. data/lib/hecks/ports/persistence/postgres_dump.rb +4 -35
  265. data/lib/hecks/ports/persistence/remote_runtime.rb +2 -24
  266. data/lib/hecks/ports/persistence/repository_factory.rb +8 -27
  267. data/lib/hecks/ports/persistence/state_codec.rb +23 -103
  268. data/lib/hecks/ports/persistence.rb +7 -14
  269. data/lib/hecks/ports/projection.rb +42 -84
  270. data/lib/hecks/ports/query/in_memory.rb +10 -58
  271. data/lib/hecks/ports/query/ordering.rb +11 -32
  272. data/lib/hecks/ports/query.rb +14 -37
  273. data/lib/hecks/ports.rb +2 -8
  274. data/lib/hecks/projections/bootstrap_table.rb +11 -43
  275. data/lib/hecks/projections/deploy/fargate/assembly.rb +2 -15
  276. data/lib/hecks/projections/deploy/fargate/cdn.rb +1 -48
  277. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +2 -11
  278. data/lib/hecks/projections/deploy/fargate/check.rb +5 -14
  279. data/lib/hecks/projections/deploy/fargate/containers.rb +3 -78
  280. data/lib/hecks/projections/deploy/fargate/extras.rb +3 -44
  281. data/lib/hecks/projections/deploy/fargate/monitoring.rb +0 -44
  282. data/lib/hecks/projections/deploy/fargate/settings.rb +2 -54
  283. data/lib/hecks/projections/deploy/fargate/yaml.rb +13 -58
  284. data/lib/hecks/projections/deploy/fargate.rb +35 -202
  285. data/lib/hecks/projections/deploy/lambda.rb +151 -679
  286. data/lib/hecks/projections/deploy/preview/access_yaml.rb +1 -33
  287. data/lib/hecks/projections/deploy/preview/containers.rb +6 -111
  288. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +4 -56
  289. data/lib/hecks/projections/deploy/preview/script.rb +2 -14
  290. data/lib/hecks/projections/deploy/preview/settings.rb +3 -33
  291. data/lib/hecks/projections/deploy/preview/task_yaml.rb +2 -38
  292. data/lib/hecks/projections/deploy/preview/template.rb +2 -58
  293. data/lib/hecks/projections/deploy/preview.rb +2 -67
  294. data/lib/hecks/projections/deploy/scripts/settings.rb +3 -25
  295. data/lib/hecks/projections/deploy/scripts.rb +4 -57
  296. data/lib/hecks/projections/deploy/shared.rb +47 -146
  297. data/lib/hecks/projections/deploy/smoke.rb +4 -61
  298. data/lib/hecks/projections/deploy/template_diff/comparer.rb +3 -25
  299. data/lib/hecks/projections/deploy/template_diff/loader.rb +2 -11
  300. data/lib/hecks/projections/deploy/template_diff.rb +5 -57
  301. data/lib/hecks/projections/diagrams.rb +20 -582
  302. data/lib/hecks/projections/glossary/html.rb +14 -130
  303. data/lib/hecks/projections/glossary/markdown.rb +8 -69
  304. data/lib/hecks/projections/glossary/mermaid.rb +9 -66
  305. data/lib/hecks/projections/glossary/sensitivity.rb +5 -34
  306. data/lib/hecks/projections/glossary/sentences.rb +14 -159
  307. data/lib/hecks/projections/glossary.rb +30 -222
  308. data/lib/hecks/projections/ir.rb +3 -10
  309. data/lib/hecks/projections/model/deviations.rb +15 -42
  310. data/lib/hecks/projections/model.rb +12 -51
  311. data/lib/hecks/projections/oidc.rb +12 -56
  312. data/lib/hecks/projections/parser_table.rb +9 -28
  313. data/lib/hecks/projections/reference.rb +9 -16
  314. data/lib/hecks/projections/rust_vocabulary.rb +12 -250
  315. data/lib/hecks/projections/shape.rb +3 -29
  316. data/lib/hecks/projections/statements.rb +7 -86
  317. data/lib/hecks/projections/vocabulary.rb +6 -22
  318. data/lib/hecks/projections.rb +2 -16
  319. data/lib/hecks/projector/cli_projector.rb +60 -375
  320. data/lib/hecks/projector/docs_projector.rb +10 -201
  321. data/lib/hecks/projector/exporter.rb +46 -138
  322. data/lib/hecks/projector/ir_projector.rb +2 -9
  323. data/lib/hecks/projector/narrate_projector.rb +15 -189
  324. data/lib/hecks/projector/target.rb +13 -76
  325. data/lib/hecks/projector.rb +30 -121
  326. data/lib/hecks/query_specification/common/comparators.rb +5 -26
  327. data/lib/hecks/query_specification/common/comparison.rb +35 -143
  328. data/lib/hecks/query_specification/common/dsl.rb +8 -24
  329. data/lib/hecks/query_specification/common/null_policy.rb +20 -70
  330. data/lib/hecks/query_specification/common/options.rb +2 -6
  331. data/lib/hecks/query_specification/common/where_clause.rb +2 -10
  332. data/lib/hecks/query_specification/field_path.rb +37 -77
  333. data/lib/hecks/query_specification/hop_path.rb +12 -76
  334. data/lib/hecks/release/gem_pin.rb +2 -26
  335. data/lib/hecks/release/runner/ci_publisher.rb +93 -0
  336. data/lib/hecks/release/runner/commands.rb +66 -0
  337. data/lib/hecks/release/runner/console.rb +52 -0
  338. data/lib/hecks/release/runner/gem_publisher.rb +43 -0
  339. data/lib/hecks/release/runner/git.rb +48 -0
  340. data/lib/hecks/release/runner/npm_publisher.rb +75 -0
  341. data/lib/hecks/release/runner/preflight.rb +104 -0
  342. data/lib/hecks/release/runner/published.rb +55 -0
  343. data/lib/hecks/release/runner/tagger.rb +80 -0
  344. data/lib/hecks/release/runner.rb +215 -0
  345. data/lib/hecks/rendering.rb +4 -26
  346. data/lib/hecks/router/namespace_installer.rb +12 -36
  347. data/lib/hecks/router.rb +4 -8
  348. data/lib/hecks/runtime/aggregate_lock.rb +8 -33
  349. data/lib/hecks/runtime/boot_gates.rb +3 -18
  350. data/lib/hecks/runtime/caller.rb +19 -59
  351. data/lib/hecks/runtime/capability_graph.rb +5 -21
  352. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +16 -78
  353. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +23 -160
  354. data/lib/hecks/runtime/command_interpreter.rb +125 -307
  355. data/lib/hecks/runtime/command_rules/admissibility.rb +95 -366
  356. data/lib/hecks/runtime/command_rules/arithmetic.rb +66 -196
  357. data/lib/hecks/runtime/command_rules/authorization.rb +13 -45
  358. data/lib/hecks/runtime/command_rules/emission.rb +8 -21
  359. data/lib/hecks/runtime/command_rules/references.rb +23 -202
  360. data/lib/hecks/runtime/dependency_planning.rb +30 -102
  361. data/lib/hecks/runtime/dispatcher.rb +82 -241
  362. data/lib/hecks/runtime/entity_element.rb +40 -462
  363. data/lib/hecks/runtime/entity_interpreter.rb +65 -220
  364. data/lib/hecks/runtime/errors.rb +32 -115
  365. data/lib/hecks/runtime/event.rb +3 -25
  366. data/lib/hecks/runtime/identity.rb +18 -154
  367. data/lib/hecks/runtime/instance.rb +18 -150
  368. data/lib/hecks/runtime/interpreting.rb +11 -70
  369. data/lib/hecks/runtime/invocation.rb +28 -164
  370. data/lib/hecks/runtime/loader.rb +48 -186
  371. data/lib/hecks/runtime/outbox.rb +38 -261
  372. data/lib/hecks/runtime/policy_interpreter.rb +38 -200
  373. data/lib/hecks/runtime/port_operation_interpreter.rb +26 -87
  374. data/lib/hecks/runtime/query_interpreter.rb +45 -187
  375. data/lib/hecks/runtime/reaction_invocation.rb +36 -138
  376. data/lib/hecks/runtime/read_model_interpreter.rb +139 -208
  377. data/lib/hecks/runtime/rebuild_sweep.rb +8 -54
  378. data/lib/hecks/runtime/reference_hop.rb +8 -81
  379. data/lib/hecks/runtime/refusal_wording.rb +8 -89
  380. data/lib/hecks/runtime/registry/saga_persistence.rb +10 -102
  381. data/lib/hecks/runtime/registry/verification.rb +52 -225
  382. data/lib/hecks/runtime/registry/world_defaults.rb +4 -53
  383. data/lib/hecks/runtime/registry.rb +103 -244
  384. data/lib/hecks/runtime/remote_dispatcher.rb +37 -152
  385. data/lib/hecks/runtime/routing.rb +4 -15
  386. data/lib/hecks/runtime/saga_interpreter/correlation.rb +14 -61
  387. data/lib/hecks/runtime/saga_interpreter.rb +79 -263
  388. data/lib/hecks/runtime/saga_pending_dispatch.rb +2 -43
  389. data/lib/hecks/runtime/tenant_check.rb +6 -68
  390. data/lib/hecks/runtime/tenant_scope.rb +3 -28
  391. data/lib/hecks/runtime/value/admission.rb +16 -67
  392. data/lib/hecks/runtime/value/coercion.rb +155 -641
  393. data/lib/hecks/runtime/value/entity_list_coercion.rb +26 -219
  394. data/lib/hecks/runtime/value.rb +12 -84
  395. data/lib/hecks/runtime.rb +17 -65
  396. data/lib/hecks/storehouse.rb +146 -567
  397. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +8 -32
  398. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +6 -23
  399. data/lib/hecks/vendoring/git_environment.rb +2 -8
  400. data/lib/hecks/vendoring/git_source.rb +4 -10
  401. data/lib/hecks/vendoring.rb +7 -46
  402. data/lib/hecks/version.rb +3 -14
  403. data/lib/hecks/vocabulary.rb +5 -0
  404. data/lib/hecks.rb +32 -82
  405. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -46
  406. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +11 -80
  407. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +10 -99
  408. metadata +30 -59
  409. data/lib/hecks/bench/cli.rb +0 -74
  410. data/lib/hecks/bench/environment.rb +0 -100
  411. data/lib/hecks/bench/postgres_probe.rb +0 -41
  412. data/lib/hecks/bench/report.rb +0 -116
  413. data/lib/hecks/bench/ruby_runner.rb +0 -151
  414. data/lib/hecks/bench/run.rb +0 -55
  415. data/lib/hecks/bench/rust_runner.rb +0 -165
  416. data/lib/hecks/bench/stats.rb +0 -75
  417. data/lib/hecks/bench/suite.rb +0 -172
  418. data/lib/hecks/bench/workload.rb +0 -137
  419. data/lib/hecks/bench.rb +0 -28
  420. data/lib/hecks/codemod.rb +0 -443
  421. data/lib/hecks/corpus.rb +0 -479
  422. data/lib/hecks/doc/reference.rb +0 -585
  423. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
  424. data/lib/hecks/fuzzing/combination_miner.rb +0 -178
  425. data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
  426. data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
  427. data/lib/hecks/fuzzing/differential.rb +0 -192
  428. data/lib/hecks/fuzzing/domain_generator.rb +0 -871
  429. data/lib/hecks/fuzzing/era_boundary.rb +0 -154
  430. data/lib/hecks/fuzzing/form_census.rb +0 -285
  431. data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
  432. data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
  433. data/lib/hecks/fuzzing/isolated_boot.rb +0 -658
  434. data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
  435. data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
  436. data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
  437. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
  438. data/lib/hecks/fuzzing/properties/guards.rb +0 -398
  439. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
  440. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
  441. data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
  442. data/lib/hecks/fuzzing/properties/querying.rb +0 -260
  443. data/lib/hecks/fuzzing/properties.rb +0 -267
  444. data/lib/hecks/fuzzing/qa_settings.rb +0 -164
  445. data/lib/hecks/fuzzing/replay.rb +0 -930
  446. data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
  447. data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
  448. data/lib/hecks/fuzzing/self_consistency.rb +0 -825
  449. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
  450. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
  451. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
  452. data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
  453. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
  454. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
  455. data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
  456. data/lib/hecks/fuzzing/shrinker.rb +0 -309
  457. data/lib/hecks/fuzzing/structural_skips.rb +0 -53
  458. data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
  459. data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
  460. data/lib/hecks/fuzzing/value_generator.rb +0 -340
  461. data/lib/hecks/fuzzing.rb +0 -23
  462. data/lib/hecks/grammar/evolve.rb +0 -524
  463. data/lib/hecks/query_ir.rb +0 -482
@@ -1,6 +1,7 @@
1
1
  require "json"
2
2
  require "fileutils"
3
3
  require "time"
4
+ require_relative "cache_dir"
4
5
  require_relative "facade/command_request"
5
6
  require_relative "facade/json_door"
6
7
  require_relative "naming"
@@ -8,127 +9,28 @@ require_relative "projector"
8
9
  require_relative "runtime/errors"
9
10
 
10
11
  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 bus is deliberately not
23
- # `Facade::McpDoor` — folding it into `Facade` under that name would
24
- # conflate the bus with its one built transport.)
25
- #
26
- # ## The surface
27
- #
28
- # catalog — what aggregates a domain declares, and what each can do
29
- # describe — one aggregate's full command/query/refusal contract
30
- # validate — is this domain's wiring sound (deep: also model-checks it)
31
- # domains — every domain directory under a root, discovered not typed
32
- # dispatch — issue a command (dry_run: preview, steps: batch)
33
- # query — ask a question
34
- # state — read what is actually stored, no verb involved
35
- # events — what happened, with payloads, to one record — not just its
36
- # current state; events this bus witnessed, sourced from
37
- # its own audit log, not a full event-sourcing replay
38
- # history — the full append-only journal, not just current state
39
- # behaviors — run a domain's hand-curated `.behaviors` examples
40
- # follow — tail this bus's own dispatch/query/state audit log
41
- #
42
- # ## Auditing and caller identity
43
- #
44
- # `dispatch`/`query`/`state` each take a `summary` — the survey's "every
45
- # audit row carries human intent for free" — and `dispatch`/`query`
46
- # additionally take an optional `source:` (`SOURCE_TAGS`, the survey's
47
- # `SourceTag`: who is calling) and an optional `role:`/`actor_id:` —
48
- # a real caller identity, bound for the call's duration via `Hecks.
49
- # as_caller`, checked against a `role`-gated command's own declared
50
- # role (`CommandRules::Authorization`) rather than merely documented by
51
- # `describe`. `dispatch` requires it for any command that declares a
52
- # role — `require_caller_for_role_gated!` refuses an unbound dispatch
53
- # against one rather than silently running it unchecked; `query`'s own
54
- # authorization runs on a separate mechanism (`Runtime::TenantScope`)
55
- # that `role:` does not gate. This is self-asserted identity, not
56
- # authentication: the caller states its own `role`/`actor_id`, and this
57
- # bus checks it consistently once stated — see `bin/hecks_mcp_door`'s
58
- # header for what that does and does not guarantee. Every call through
59
- # those three, plus a dry run, is appended to a per-domain JSONL audit
60
- # log (`record!`) `follow` tails back — a record of what the caller
61
- # said it was, not independently verified identity. `catalog`/
62
- # `describe`/`validate`/`domains`/`history`/`behaviors`/`events` need
63
- # neither — they change nothing and commit nothing to any log.
64
- #
65
- # ## Booting is the caller's job
66
- #
67
- # A caller hands in an already-booted `runtime`, the same division of
68
- # labor `Facade::CliRunner` already keeps against `bin/run`: booting a
69
- # domain from a path is IO the calling `bin/` script owns, this stays a
70
- # pure function of a `Runtime::Dispatcher` plus plain Ruby arguments —
71
- # no different from `CliRunner.call(runtime:, argv:)` itself.
72
- # `validate` and `domains` are the two exceptions: `validate`'s whole
73
- # job is to attempt the boot and report whether it survived, so it
74
- # takes the domain path instead and boots it; `domains` has no
75
- # domain to be handed one of yet, that's what it's answering.
76
- #
77
- # ## Built from existing doors
78
- #
79
- # No new vocabulary otherwise. Every method here composes doors that
80
- # already exist — `Projector.call(:cli, ...)` for verb/question alias
81
- # resolution (the identical table `CliRunner` itself resolves against),
82
- # `Projector.call(:docs, ...)` for `describe`, `Facade::JsonDoor` for
83
- # the JSON↔Runtime::Value boundary, `Registry#repository` for `state`/
84
- # `history`, `Registry#verify!` (run by every boot) for `validate`,
85
- # `Bluebook::ModelCheck` for `validate(deep: true)`, `Hecks::Behaviors`
86
- # for `behaviors`, `Adapters::Folder#domain?` for `domains`,
87
- # `Dispatcher#dry_run?` for `dispatch(dry_run: true)`. A second copy of
88
- # any of these here would be the exact duplication `Facade::JsonDoor`'s
89
- # own header already warns against.
12
+ # Dispatch/query/state and introspection over one booted domain, pure
13
+ # beyond its own JSONL audit log — booting stays the caller's job.
90
14
  module Storehouse
91
15
  module_function
92
16
 
93
- # The same closed set `docs/hecks-survey-what-we-wish-we-had.md`'s
94
- # `SourceTag` names — who dispatched, not what. Optional: a caller
95
- # that omits it gets `source: nil` recorded, honestly, rather than a
96
- # guessed default.
17
+ # Optional on dispatch/query; omitted, source is recorded as nil rather
18
+ # than guessed.
97
19
  SOURCE_TAGS = %w[process-manager operator hook sidequest-agent cascade daemon].freeze
98
20
 
99
- # **The bus's own audit trail** — a JSONL file per domain, one line per
100
- # `dispatch`/`query`/`state`/dry-run call, `follow` tails it back.
101
- # `tmp/`, not the domain's own directory: this is the bus's record
102
- # of what was asked of it, not part of the domain's own persisted
103
- # state, and `tmp/` is already gitignored for exactly this kind of
104
- # local, disposable-but-useful-while-it-lasts file.
105
- LOG_ROOT = File.expand_path("../../tmp/storehouse", __dir__)
106
-
107
- # The root every `domain:`/`under:` must resolve under — the project
108
- # directory by default, `HECKS_STOREHOUSE_ROOT` to widen or move it.
109
- # `Hecks.boot` `Kernel.load`s the `.hecksagon`/`.bluebook`/`.world`
110
- # files a domain path resolves to, and those are Ruby, not a data
111
- # format — a caller-supplied path with no confinement at all is an
112
- # unmarked door out of "a narrower, checked surface" (the README's
113
- # own pitch for this bus) and into arbitrary code execution from any
114
- # path on disk. This is a boundary a local convention was standing
115
- # in for, not new behavior for a caller already confined to the
116
- # project tree.
21
+ # Kept under Hecks::CacheDir, not the domain directory (part of the domain's
22
+ # own state) or the gem directory (read-only once installed).
23
+ # :nodoc:
24
+ def log_root = Hecks::CacheDir.path("storehouse")
25
+
26
+ # The root every domain path resolves under; HECKS_STOREHOUSE_ROOT widens it.
27
+ # Hecks.boot Kernel.loads Ruby files here, so an unconfined path is
28
+ # arbitrary code execution, not just a wrong-directory mistake.
117
29
  BOOT_ROOT = File.expand_path(ENV["HECKS_STOREHOUSE_ROOT"] || File.expand_path("../..", __dir__))
118
30
 
119
- # Resolves `path` against `BOOT_ROOT` and refuses one that escapes it.
120
- #
121
- # **Refused, not silently clamped** — a path outside `BOOT_ROOT` is either
122
- # an honest mistake (a relative path typed against the wrong cwd) or
123
- # the exact thing this check exists to catch, and both deserve the
124
- # same clear refusal rather than a silent rewrite to something the
125
- # caller didn't ask for.
126
- #
127
- # @param path [String, #to_s] the caller-supplied path to confine
128
- # @param label [String] the argument's own name, for the refusal message
129
- # (e.g. `"domain"`, `"under"`)
130
- # @return [String] `path` resolved to an absolute path under `BOOT_ROOT`
131
- # @raise [Runtime::TypeMismatch] if the resolved path is outside `BOOT_ROOT`
31
+ # Refuses a path outside BOOT_ROOT rather than silently clamping it — a
32
+ # stray relative path and a deliberate escape both deserve a clear refusal.
33
+ # :nodoc:
132
34
  def confine!(path, label)
133
35
  resolved = File.expand_path(path.to_s, BOOT_ROOT)
134
36
  return resolved if resolved == BOOT_ROOT || resolved.start_with?("#{BOOT_ROOT}#{File::SEPARATOR}")
@@ -138,45 +40,24 @@ module Hecks
138
40
  "own root (HECKS_STOREHOUSE_ROOT to widen it)"
139
41
  end
140
42
 
141
- # ── shared resolution helpers ────────────────────────────────────
142
-
143
- # The one bluebook a domain directory boots. Every `bin/*` script that
144
- # projects a whole-domain CLI or doc set makes this same assumption
145
- # (`Facade::CliRunner#call`'s own `bluebook = runtime.registry.
146
- # bluebooks.values.first`) — one `.hecksagon` names one chapter.
147
- # The one bluebook this runtime booted.
148
- #
149
- # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
150
- # @return [Bluebook::Chapter] the runtime's one loaded bluebook
151
- # @raise [Runtime::NotFound] if the runtime's boot loaded no bluebook
43
+ # The one bluebook this runtime booted; every whole-domain door assumes
44
+ # the same one-bluebook-per-boot shape.
45
+ # :nodoc:
152
46
  def bluebook_for(runtime)
153
47
  runtime.registry.bluebooks.values.first or
154
48
  raise Runtime::NotFound, "this boot loaded no bluebook"
155
49
  end
156
50
 
157
- # Finds one aggregate by name on a bluebook, or refuses.
158
- #
159
- # @param bluebook [Bluebook::Chapter] the chapter to search
160
- # @param name [String, Symbol, #to_s] the aggregate's declared name
161
- # @return [Bluebook::Aggregate] the matching aggregate
162
- # @raise [Runtime::NotFound] if `bluebook` declares no aggregate named `name`
51
+ # :nodoc:
163
52
  def aggregate_ir!(bluebook, name)
164
53
  bluebook.aggregate(name) or
165
54
  raise Runtime::NotFound, "#{bluebook.name} declares no aggregate named #{name.inspect} — " \
166
55
  "known: #{bluebook.aggregates.map(&:hecks_name).sort.join(', ')}"
167
56
  end
168
57
 
169
- # The same alias table `CliRunner` resolves a typed word against — a
170
- # short name when it's unambiguous, the qualified `Aggregate.Verb`
171
- # form always. Shared here so `dispatch` and `query` (and their error
172
- # messages) never drift from what a human typing `bin/run` sees.
173
- #
174
- # @param cli [Hash{Symbol => Object}] a `Projector.call(:cli, ...)` result
175
- # @param name [String, Symbol, #to_s] the command or query name, as given by the caller
176
- # @param asking [Boolean] true to resolve a query (against `cli[:questions]`),
177
- # false to resolve a command (against `cli[:verbs]`)
178
- # @return [Hash{Symbol => Object}] the resolved verb/question spec
179
- # @raise [Runtime::NotFound] if `name` matches no known command or query
58
+ # The same alias table CliRunner resolves against, kept here so dispatch
59
+ # and query never drift from what a human typing bin/run sees.
60
+ # :nodoc:
180
61
  def resolve!(cli, name, asking:)
181
62
  pool = asking ? cli[:questions] : cli[:verbs]
182
63
  key = cli[:names][asking ? :question : :command][name]
@@ -187,11 +68,8 @@ module Hecks
187
68
  raise Runtime::NotFound, "no such #{asking ? 'query' : 'command'}: #{name.inspect} — known: #{known}"
188
69
  end
189
70
 
190
- # Refuses a call with no summary.
191
- #
192
- # @param summary [String, nil] the caller-supplied one-line summary
193
- # @return [void]
194
- # @raise [Runtime::TypeMismatch] if `summary` is nil or blank
71
+ # Required on dispatch/query/state: what makes an audit row legible later.
72
+ # :nodoc:
195
73
  def require_summary!(summary)
196
74
  return unless summary.nil? || summary.to_s.strip.empty?
197
75
 
@@ -199,80 +77,34 @@ module Hecks
199
77
  "a one-line summary: is required on dispatch/query/state — it is what makes an audit row legible later"
200
78
  end
201
79
 
202
- # Refuses a `source:` that is not one of `SOURCE_TAGS`.
203
- #
204
- # @param source [String, Symbol, nil] the caller-supplied source tag
205
- # @return [void]
206
- # @raise [Runtime::TypeMismatch] if `source` is present and not in `SOURCE_TAGS`
80
+ # :nodoc:
207
81
  def valid_source!(source)
208
82
  return if source.nil? || SOURCE_TAGS.include?(source.to_s)
209
83
 
210
84
  raise Runtime::TypeMismatch, "source: #{source.inspect} is not one of #{SOURCE_TAGS.join(', ')}"
211
85
  end
212
86
 
213
- # `actor_id` names who, `role` names what they hold — `Hecks.
214
- # as_caller` requires the latter always, the former is additive
215
- # (`Runtime::Caller::Current`'s own shape). An `actor_id` with no
216
- # `role` would silently do nothing rather than bind a real caller,
217
- # which is worse than refusing: a caller who thinks they've
218
- # identified themselves and haven't deserves to be told.
219
- #
220
- # @param role [String, Symbol, nil] the caller's self-asserted role
221
- # @param actor_id [String, nil] the caller's self-asserted identity
222
- # @return [void]
223
- # @raise [Runtime::TypeMismatch] if `actor_id` is given without `role`
87
+ # actor_id without role would silently bind nothing rather than a real
88
+ # caller — refusing here beats a caller thinking it identified itself.
89
+ # :nodoc:
224
90
  def valid_caller!(role, actor_id)
225
91
  return unless actor_id && role.nil?
226
92
 
227
93
  raise Runtime::TypeMismatch, "actor_id: requires role: too — a caller names WHO through WHICH role they hold"
228
94
  end
229
95
 
230
- # Bound for the duration of one call, then gone — `Hecks.as_caller`
231
- # is itself a `Thread.current`-scoped `ensure`-guarded block, so
232
- # nothing here needs its own cleanup. `role: nil` yields unbound —
233
- # for `query`, exactly as before: `CommandRules::Authorization#
234
- # refuse_role_mismatch` is opt-in on the domain side (`return unless
235
- # caller`), and query authorization runs on a wholly separate
236
- # mechanism (`authorize policy, tenant: :field`, checked against an
237
- # explicit `tenant:` argument — see `Runtime::TenantScope`), so
238
- # binding a caller around a query has no effect on it today; it is
239
- # still accepted here, for symmetry and for the audit log, against
240
- # the day a read model does check `Caller.current`. For `dispatch`,
241
- # `require_caller_for_role_gated!` (below) now refuses before this
242
- # is ever reached when the command declares a role and no caller is
243
- # bound — so an unbound `dispatch` here means either the command
244
- # declares no role at all, or a caller-side check let it through.
245
- #
246
- # @param role [String, Symbol, nil] the caller's self-asserted role; nil runs
247
- # `block` unbound
248
- # @param actor_id [String, nil] the caller's self-asserted identity
249
- # @yield the dispatch/query to run, bound to the caller for its duration
250
- # @return [Object] the block's own return value
96
+ # Binds role/actor_id for the block's duration via Hecks.as_caller; role: nil
97
+ # runs the block unbound (query's own authorization does not depend on it).
98
+ # :nodoc:
251
99
  def with_caller(role, actor_id, &block)
252
100
  return block.call if role.nil?
253
101
 
254
102
  Hecks.as_caller(role: role, actor_id: actor_id, &block)
255
103
  end
256
104
 
257
- # The fail-open half `with_caller` itself cannot close — ADR 0025's
258
- # Governance RBAC work fixed what a *bound* role is checked against
259
- # (a live `Governance::RoleAssignment` lookup instead of a bare
260
- # string match), but changed nothing about a caller who binds no
261
- # role at all: `refuse_role_mismatch` `return`s immediately when
262
- # `Caller.current` is nil, so a bus caller who simply omits `role:`
263
- # sails past a role-gated command unchecked, not denied. That is a
264
- # property of this bus choosing to dispatch unbound, not of the
265
- # domain rule — `bin/run`, the human CLI, has no such gap because a
266
- # human always dispatches through a real `Hecks.as_caller` binding
267
- # upstream of it. Refusing here, before `with_caller`/`dispatch` are
268
- # ever reached, makes the bus keep the same promise: a command whose
269
- # bluebook declares a role is not run through this bus without one.
270
- #
271
- # @param spec [Hash{Symbol => Object}] a resolved command spec (from `resolve!`),
272
- # read for `:role_gated`, `:role` and `:verb`
273
- # @param role [String, Symbol, nil] the caller's self-asserted role
274
- # @return [void]
275
- # @raise [Runtime::Unauthorized] if `spec` is role-gated and `role` is nil
105
+ # A caller who omits role: would otherwise reach a role-gated command
106
+ # unchecked (refuse_role_mismatch no-ops with no bound caller) — refuse here instead.
107
+ # :nodoc:
276
108
  def require_caller_for_role_gated!(spec, role)
277
109
  return unless spec[:role_gated] && role.nil?
278
110
 
@@ -281,26 +113,9 @@ module Hecks
281
113
  "(role:/actor_id:) is bound; dispatching it unbound is refused, not silently unchecked"
282
114
  end
283
115
 
284
- # `dry_run?` (Runtime::Dispatcher) understands only the old flat
285
- # legacy args shape — no to:/with: envelope, `route:` never passed
286
- # (its own header explains why: built directly against
287
- # CommandInterpreter/EntityInterpreter's pre-envelope contract,
288
- # never updated because nothing else needed it to be — a real
289
- # record of history, not a defect this bus should paper over
290
- # silently). `Facade::CommandRequest`/`spec[:legacy_receiver]`
291
- # already know how to name that same flat shape for every receiver
292
- # kind (a bare id under one string key for :aggregate, a
293
- # {aggregate:, entity:} pair of keys for :entity) — this is the one
294
- # door back into it.
295
- #
296
- # @param envelope [Hash{Symbol => Object}] a normalized command request, with an
297
- # optional `:to` route and `:with` facts
298
- # @param receiver [Symbol, nil] `:aggregate`, `:entity`, or nil for anything else
299
- # @param legacy_receiver [Symbol, String, Hash{Symbol => Symbol, String}, nil] where
300
- # the route belongs in the flat shape: a single key for `:aggregate`, an
301
- # `{aggregate:, entity:}` pair of keys for `:entity`
302
- # @return [Hash{Symbol => Object}] the facts, with the route merged in under its
303
- # legacy key(s) when `envelope` carries one
116
+ # dry_run? (Runtime::Dispatcher) only understands the pre-envelope flat
117
+ # args shape; this is the one door back into it from dispatch's own envelope.
118
+ # :nodoc:
304
119
  def flatten_legacy(envelope, receiver, legacy_receiver)
305
120
  facts = envelope[:with] || {}
306
121
  return facts unless envelope.key?(:to)
@@ -315,69 +130,38 @@ module Hecks
315
130
  end
316
131
  end
317
132
 
318
- # ── the audit log `follow` reads back ───────────────────────────────
319
-
320
- # The audit log file one domain's calls are appended to.
321
- #
322
- # @param domain_name [String, Symbol, #to_s] the domain name
323
- # @return [String] the absolute path to that domain's JSONL log file, under
324
- # `LOG_ROOT`
133
+ # :nodoc:
325
134
  def log_path(domain_name)
326
- File.join(LOG_ROOT, "#{domain_name.to_s.gsub(/[^A-Za-z0-9_-]/, '_')}.jsonl")
135
+ File.join(log_root, "#{domain_name.to_s.gsub(/[^A-Za-z0-9_-]/, '_')}.jsonl")
327
136
  end
328
137
 
329
- # Appends one call's outcome to its domain's audit log.
330
- #
331
- # Never fails a real call because its own audit log couldn't be
332
- # written — a full disk or a permissions problem is a `follow`
333
- # feature going dark, not a reason to refuse the dispatch/query/
334
- # state call that was actually asked for.
335
- #
336
- # @param domain_name [String, Symbol, nil] the domain to log against; nil skips
337
- # logging entirely (a call that never resolved a bluebook)
338
- # @param tool [String] which tool logged this: `"dispatch"`, `"dry_run"`,
339
- # `"query"`, or `"state"`
340
- # @param summary [String, nil] the caller's one-line summary
341
- # @param source [String, Symbol, nil] the caller's source tag
342
- # @param outcome [Hash{Symbol => Object}] the call's own result, read for `:ok`,
343
- # `:id`, `:error` and `:events`
344
- # @param verb [String, nil] the resolved verb dispatched or queried, when known
345
- # @param role [String, Symbol, nil] the caller's self-asserted role
346
- # @param actor_id [String, nil] the caller's self-asserted identity
347
- # @return [void]
138
+ # Never fails a real call because its own audit log couldn't be written —
139
+ # a full disk silences follow, not the dispatch/query/state call itself.
140
+ # :nodoc:
348
141
  def record!(domain_name, tool:, summary:, source:, outcome:, verb: nil, role: nil, actor_id: nil)
349
142
  return unless domain_name
350
143
 
351
144
  entry = { time: Time.now.utc.iso8601, tool: tool, verb: verb, summary: summary, source: source,
352
145
  role: role, actor_id: actor_id,
353
146
  ok: outcome[:ok], id: outcome[:id], error: outcome[:error], events: outcome[:events] }.compact
354
- FileUtils.mkdir_p(LOG_ROOT)
147
+ FileUtils.mkdir_p(log_root)
355
148
  File.open(log_path(domain_name), "a") { |f| f.puts(JSON.generate(entry)) }
356
149
  rescue StandardError
357
150
  nil
358
151
  end
359
152
 
360
- # ── the three that drive it ────────────────────────────────────────
361
-
362
- # `dry_run: true` answers a different question than a real dispatch
363
- # does — "would this succeed", not "here is what happened" — so a
364
- # domain refusal is the legitimate, complete answer (`ok: true,
365
- # would_succeed: false`), not a failed call. A malformed request
366
- # (unknown command, a bad args shape) is still a failed call
367
- # (`ok: false`) either way — it never reached the domain to be asked.
153
+ # Issues a command, or previews it with dry_run: true.
368
154
  #
369
- # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
155
+ # @param runtime [Runtime::Registry] the booted domain to dispatch against
370
156
  # @param command [String, Symbol] the command name, bare or qualified
371
- # @param summary [String] a one-line human summary of the call, for the audit log
372
- # @param args [Hash] the command's arguments
373
- # @param source [String, Symbol, nil] a `SOURCE_TAGS` tag naming who is calling
374
- # @param dry_run [Boolean] true to check whether the command would succeed,
375
- # without actually dispatching it
376
- # @param role [String, Symbol, nil] the caller's self-asserted role
377
- # @param actor_id [String, nil] the caller's self-asserted identity
378
- # @return [Hash{Symbol => Object}] `:ok` plus, on success, `:id`/`:state`/`:events`
379
- # (a real dispatch) or `:would_succeed`/`:error` (a dry run); on refusal, the
380
- # shape `refused` returns
157
+ # @param summary [String] a one-line audit summary; required
158
+ # @param args [Hash] the command's arguments, JSON-shaped
159
+ # @param source [String, Symbol, nil] a SOURCE_TAGS tag naming who is calling
160
+ # @param dry_run [Boolean] true previews; a false would_succeed is not a failure
161
+ # @param role [String, Symbol, nil] the caller's bound role, or nil to run unbound
162
+ # @param actor_id [String, nil] the caller's identity; requires role:
163
+ # @return [Hash] :ok plus :id/:state/:events (real) or :would_succeed/:error
164
+ # (dry run); or the refused shape
381
165
  def dispatch(runtime:, command:, summary:, args: {}, source: nil, dry_run: false, role: nil, actor_id: nil)
382
166
  bluebook = bluebook_for(runtime)
383
167
  tool = dry_run ? "dry_run" : "dispatch"
@@ -393,24 +177,7 @@ module Hecks
393
177
  outcome
394
178
  end
395
179
 
396
- # Validates and dispatches one command, real or dry-run.
397
- #
398
- # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
399
- # @param bluebook [Bluebook::Chapter] the domain's one loaded chapter
400
- # @param command [String, Symbol] the command name, bare or qualified
401
- # @param summary [String, nil] a one-line human summary of the call
402
- # @param args [Hash] the command's arguments, JSON-shaped
403
- # @param source [String, Symbol, nil] a `SOURCE_TAGS` tag naming who is calling
404
- # @param dry_run [Boolean] true to check whether the command would succeed,
405
- # without actually dispatching it
406
- # @param role [String, Symbol, nil] the caller's self-asserted role
407
- # @param actor_id [String, nil] the caller's self-asserted identity
408
- # @return [Hash{Symbol => Object}] the outcome from `real_dispatch`/`dry_run_outcome`,
409
- # merged with `:verb`
410
- # @raise [Runtime::TypeMismatch] if `summary`, `source`, or `actor_id` without
411
- # `role` is invalid
412
- # @raise [Runtime::NotFound] if `command` names no known command
413
- # @raise [Runtime::Unauthorized] if `command` is role-gated and `role` is nil
180
+ # :nodoc:
414
181
  def perform_dispatch(runtime, bluebook, command, summary, args, source, dry_run, role, actor_id)
415
182
  require_summary!(summary)
416
183
  valid_source!(source)
@@ -428,14 +195,7 @@ module Hecks
428
195
  result.merge(verb: spec[:verb])
429
196
  end
430
197
 
431
- # Dispatches one command for real and shapes its result for the bus.
432
- #
433
- # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
434
- # @param spec [Hash{Symbol => Object}] the resolved command spec (from `resolve!`)
435
- # @param envelope [Hash{Symbol => Object}] the normalized `to:`/`with:` request
436
- # @param summary [String, nil] a one-line human summary of the call
437
- # @return [Hash{Symbol => Object}] `:ok`, `:summary`, `:id`, `:state` (JSON-safe,
438
- # nil for a port operation) and `:events` (name/payload pairs)
198
+ # :nodoc:
439
199
  def real_dispatch(runtime, spec, envelope, summary)
440
200
  result = runtime.dispatch_flat(spec[:verb], envelope)
441
201
  ok(summary: summary,
@@ -444,14 +204,7 @@ module Hecks
444
204
  events: result.events.map { |event| { name: event.name, payload: Facade::JsonDoor.materialize(event.payload) } })
445
205
  end
446
206
 
447
- # Checks whether one command would succeed, without dispatching it for real.
448
- #
449
- # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
450
- # @param spec [Hash{Symbol => Object}] the resolved command spec (from `resolve!`)
451
- # @param envelope [Hash{Symbol => Object}] the normalized `to:`/`with:` request
452
- # @param summary [String, nil] a one-line human summary of the call
453
- # @return [Hash{Symbol => Object}] `:ok`, `:summary`, `:would_succeed`, and
454
- # `:error` (the refusal message) when it would not succeed
207
+ # :nodoc:
455
208
  def dry_run_outcome(runtime, spec, envelope, summary:)
456
209
  flat = flatten_legacy(envelope, spec[:receiver], spec[:legacy_receiver])
457
210
  runtime.dry_run?(spec[:verb], **flat)
@@ -460,28 +213,18 @@ module Hecks
460
213
  ok(summary: summary, would_succeed: false, error: e.message)
461
214
  end
462
215
 
463
- # Dispatches a whole sequence of commands as one call.
464
- #
465
- # **One call, many steps** — the survey's own `bin/run <domain> script`
466
- # shape, so an agent issuing a known sequence of commands (open an
467
- # account, then fund it) pays one round trip instead of N. Every step
468
- # goes through `dispatch` itself — same resolution, same audit log
469
- # line per step, same summary/source stamped on all of them since
470
- # the batch is what the caller is declaring intent about, not each
471
- # individual step. Runs every step regardless of an earlier one
472
- # refusing — a later step naming a record an earlier step never
473
- # created will refuse honestly on its own account, which is more
474
- # informative than silently dropping the rest of the batch.
216
+ # Dispatches a sequence of commands as one call, through dispatch itself —
217
+ # same audit log line per step. Runs every step even after an earlier one
218
+ # refuses, since a later step naming a since-refused record will refuse
219
+ # honestly on its own account.
475
220
  #
476
- # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
477
- # @param steps [Array<Hash>] each step's `command`/`args`, JSON-shaped
478
- # @param summary [String] a one-line human summary of the whole batch
479
- # @param source [String, Symbol, nil] a `SOURCE_TAGS` tag naming who is calling
480
- # @param role [String, Symbol, nil] the caller's self-asserted role
481
- # @param actor_id [String, nil] the caller's self-asserted identity
482
- # @return [Hash{Symbol => Object}] `:ok` (true only if every step's own `:ok` was
483
- # true), `:summary` and `:results` (each step's own `dispatch` outcome); on a
484
- # refusal outside any one step, the shape `refused` returns
221
+ # @param runtime [Runtime::Registry] the booted domain to dispatch against
222
+ # @param steps [Array<Hash>] each step's command/args, JSON-shaped
223
+ # @param summary [String] a one-line audit summary; required
224
+ # @param source [String, Symbol, nil] a SOURCE_TAGS tag naming who is calling
225
+ # @param role [String, Symbol, nil] the caller's bound role, or nil to run unbound
226
+ # @param actor_id [String, nil] the caller's identity; requires role:
227
+ # @return [Hash] :ok (true only if every step's own :ok was true), :results
485
228
  def dispatch_batch(runtime:, steps:, summary:, source: nil, role: nil, actor_id: nil)
486
229
  require_summary!(summary)
487
230
  results = Array(steps).map do |raw|
@@ -496,15 +239,14 @@ module Hecks
496
239
 
497
240
  # Answers one declared query.
498
241
  #
499
- # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
242
+ # @param runtime [Runtime::Registry] the booted domain to query
500
243
  # @param question [String, Symbol] the query name, bare or qualified
501
- # @param summary [String] a one-line human summary of the call, for the audit log
502
- # @param args [Hash] the query's arguments
503
- # @param source [String, Symbol, nil] a `SOURCE_TAGS` tag naming who is calling
504
- # @param role [String, Symbol, nil] the caller's self-asserted role
505
- # @param actor_id [String, nil] the caller's self-asserted identity
506
- # @return [Hash{Symbol => Object}] `:ok` and `:rows` (each JSON-safe) on success,
507
- # or the shape `refused` returns
244
+ # @param summary [String] a one-line audit summary; required
245
+ # @param args [Hash] the query's arguments, JSON-shaped
246
+ # @param source [String, Symbol, nil] a SOURCE_TAGS tag naming who is calling
247
+ # @param role [String, Symbol, nil] the caller's bound role, or nil to run unbound
248
+ # @param actor_id [String, nil] the caller's identity; requires role:
249
+ # @return [Hash] :ok and :rows (each JSON-safe); or the refused shape
508
250
  def query(runtime:, question:, summary:, args: {}, source: nil, role: nil, actor_id: nil)
509
251
  bluebook = bluebook_for(runtime)
510
252
  outcome = perform_query(bluebook, runtime, question, summary, args, source, role, actor_id)
@@ -519,20 +261,7 @@ module Hecks
519
261
  outcome
520
262
  end
521
263
 
522
- # Validates and answers one query.
523
- #
524
- # @param bluebook [Bluebook::Chapter] the domain's one loaded chapter
525
- # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
526
- # @param question [String, Symbol] the query name, bare or qualified
527
- # @param summary [String, nil] a one-line human summary of the call
528
- # @param args [Hash] the query's arguments, JSON-shaped
529
- # @param source [String, Symbol, nil] a `SOURCE_TAGS` tag naming who is calling
530
- # @param role [String, Symbol, nil] the caller's self-asserted role
531
- # @param actor_id [String, nil] the caller's self-asserted identity
532
- # @return [Hash{Symbol => Object}] `:ok`, `:rows` (each JSON-safe) and `:verb`
533
- # @raise [Runtime::TypeMismatch] if `summary`, `source`, or `actor_id` without
534
- # `role` is invalid
535
- # @raise [Runtime::NotFound] if `question` names no known query
264
+ # :nodoc:
536
265
  def perform_query(bluebook, runtime, question, summary, args, source, role, actor_id)
537
266
  require_summary!(summary)
538
267
  valid_source!(source)
@@ -544,22 +273,15 @@ module Hecks
544
273
  ok(summary: summary, rows: rows.map { |row| Facade::JsonDoor.materialize(row) }).merge(verb: spec[:verb])
545
274
  end
546
275
 
547
- # Reads one aggregate's stored records directly, bypassing any declared query.
276
+ # Reads one aggregate's stored records directly, bypassing any declared
277
+ # query — id: answers one record, omitted answers every record.
548
278
  #
549
- # **What is actually stored** — no verb, no interpretation, the repository
550
- # itself. `id:` given answers one record (`NotFound` when it names
551
- # nothing); omitted answers every record the aggregate currently
552
- # holds. This is the difference `query` can't cover: a query answers a
553
- # declared question, and an aggregate that never declared "list
554
- # everything" has no query this could reuse.
555
- #
556
- # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
279
+ # @param runtime [Runtime::Registry] the booted domain to read
557
280
  # @param aggregate [String, Symbol] the aggregate's declared name
558
- # @param summary [String] a one-line human summary of the call, for the audit log
281
+ # @param summary [String] a one-line audit summary; required
559
282
  # @param id [String, Object, nil] one record's identity, or nil for every record
560
- # @param source [String, Symbol, nil] a `SOURCE_TAGS` tag naming who is calling
561
- # @return [Hash{Symbol => Object}] `:ok` and, with `id:`, `:record`; without it,
562
- # `:count` and `:records` (each JSON-safe); or the shape `refused` returns
283
+ # @param source [String, Symbol, nil] a SOURCE_TAGS tag naming who is calling
284
+ # @return [Hash] :ok and, with id:, :record; without it, :count/:records; or refused
563
285
  def state(runtime:, aggregate:, summary:, id: nil, source: nil)
564
286
  bluebook = bluebook_for(runtime)
565
287
  outcome = perform_state(runtime, bluebook, aggregate, summary, id)
@@ -572,18 +294,7 @@ module Hecks
572
294
  outcome
573
295
  end
574
296
 
575
- # Validates and reads one aggregate's stored record(s).
576
- #
577
- # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
578
- # @param bluebook [Bluebook::Chapter] the domain's one loaded chapter
579
- # @param aggregate [String, Symbol] the aggregate's declared name
580
- # @param summary [String, nil] a one-line human summary of the call
581
- # @param id [String, Object, nil] one record's identity, or nil for every record
582
- # @return [Hash{Symbol => Object}] `:ok` and, with `id`, `:record`; without it,
583
- # `:count` and `:records` (each JSON-safe)
584
- # @raise [Runtime::TypeMismatch] if `summary` is invalid
585
- # @raise [Runtime::NotFound] if `aggregate` names no known aggregate, or `id`
586
- # names no record of it
297
+ # :nodoc:
587
298
  def perform_state(runtime, bluebook, aggregate, summary, id)
588
299
  require_summary!(summary)
589
300
  ir = aggregate_ir!(bluebook, aggregate)
@@ -599,23 +310,12 @@ module Hecks
599
310
  end
600
311
  end
601
312
 
602
- # ── the four zoom levels ─────────────────────────────────────────
603
-
604
- # Lists every domain directory under a root.
605
- #
606
- # **Zoom level zero** — every domain directory a root actually holds,
607
- # discovered rather than typed from memory. Every other tool takes
608
- # `domain:` as a directory it assumes the caller already knows; this
609
- # is how a caller who doesn't finds out. `Adapters::Folder#domain?`
610
- # is the same predicate `domain_root`/`nearest_domain` already walk
611
- # up directories checking — a bare `.hecksagon` or one under
612
- # `bluebook/`, the two real shapes this corpus uses.
313
+ # Lists every domain directory found under a root (discovered, not typed).
613
314
  #
614
- # @param under [String] the directory to search, relative to `BOOT_ROOT`
615
- # @return [Hash{Symbol => Object}] `:ok`, `:under`, and `:domains` (each found
616
- # domain's path, relative to `BOOT_ROOT`, sorted); `:domains` is `[]` when
617
- # `under` does not exist
618
- # @raise [Runtime::TypeMismatch] if `under` resolves outside `BOOT_ROOT`
315
+ # @param under [String] the directory to search, relative to BOOT_ROOT
316
+ # @return [Hash] :ok, :under, :domains (each path relative to BOOT_ROOT,
317
+ # sorted); :domains is [] when under does not exist
318
+ # @raise [Runtime::TypeMismatch] if under resolves outside BOOT_ROOT
619
319
  def domains(under: "examples")
620
320
  root = confine!(under, "under")
621
321
  return ok(under: under, domains: []) unless Dir.exist?(root)
@@ -626,17 +326,12 @@ module Hecks
626
326
  ok(under: under, domains: found.map { |name| File.join(under, name) })
627
327
  end
628
328
 
629
- # Lists a domain's aggregates and their command/query names.
329
+ # Lists a domain's aggregates and their command/query names, snake_cased
330
+ # exactly as dispatch/query want them.
630
331
  #
631
- # **Zoom level one** — every aggregate this domain declares, and every
632
- # command/query name each answers to, snake_cased exactly as
633
- # `dispatch`/`query` want it. Enough to pick a target; `describe` is
634
- # the next level down for what one of them actually takes.
635
- #
636
- # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
637
- # @return [Hash{Symbol => Object}] `:ok`, `:domain`, and `:aggregates` — an Array
638
- # of `{name:, commands:, queries:}` Hashes, commands and queries sorted,
639
- # command names suffixed `!`; or the shape `refused` returns
332
+ # @param runtime [Runtime::Registry] the booted domain to describe
333
+ # @return [Hash] :ok, :domain, :aggregates (each {name:, commands:, queries:},
334
+ # command names suffixed !); or the refused shape
640
335
  def catalog(runtime:)
641
336
  bluebook = bluebook_for(runtime)
642
337
 
@@ -650,21 +345,14 @@ module Hecks
650
345
  refused(e)
651
346
  end
652
347
 
653
- # Answers one aggregate's (or the whole chapter's) full usage documentation.
654
- #
655
- # **Zoom level two** — the exact same usage document a human gets from
656
- # `bin/docs <domain> [aggregate]` (`Projector::DocsProjector`, the
657
- # identical projection `Surface::AggregateDoor#docs` calls one door
658
- # over): every command's arguments, the states it may be issued
659
- # from, and every way it can refuse. `aggregate:` omitted answers the
660
- # whole chapter.
348
+ # One aggregate's (or the whole chapter's) full usage documentation — the
349
+ # same document bin/docs renders for a human.
661
350
  #
662
- # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
351
+ # @param runtime [Runtime::Registry] the booted domain to describe
663
352
  # @param aggregate [String, Symbol, nil] one aggregate's name, or nil for the
664
353
  # whole chapter
665
- # @return [Hash{Symbol => Object}] `:ok`, `:domain` and `:docs` (the rendered
666
- # documentation); or the shape `refused` returns
667
- # @raise [Runtime::NotFound] if `aggregate` names no known aggregate
354
+ # @return [Hash] :ok, :domain, :docs; or the refused shape
355
+ # @raise [Runtime::NotFound] if aggregate names no known aggregate
668
356
  def describe(runtime:, aggregate: nil)
669
357
  bluebook = bluebook_for(runtime)
670
358
  options = aggregate ? { aggregate: aggregate_ir!(bluebook, aggregate).hecks_name } : {}
@@ -674,39 +362,16 @@ module Hecks
674
362
  refused(e)
675
363
  end
676
364
 
677
- # Boots a domain and reports whether its wiring (and, optionally, its logic) is sound.
678
- #
679
- # **Zoom level three** — is the wiring sound at all: every bind names a
680
- # declared aggregate, every adapter satisfies the port it claims, the
681
- # default adapter is usable. `Registry#verify!` (`runtime/registry/
682
- # verification.rb`) is the one place this repo already answers that
683
- # question, and `Runtime::Loader.boot` already calls it as the last
684
- # step of every boot — so this is the one method here that boots for
685
- # itself rather than taking a `runtime:` already in hand, because a
686
- # runtime that successfully reached this line already answered the
687
- # question. Given a domain path, not a booted runtime, deliberately:
688
- # asking "is this valid" about a domain that failed to boot at all
689
- # has to be askable without a runtime to hand it.
365
+ # Boots a domain and reports whether its wiring is sound; deep: true also
366
+ # runs Bluebook::ModelCheck past wiring, into logic. Boots for itself,
367
+ # unlike every other tool here, since a domain that failed to boot has
368
+ # no runtime to hand in — any boot failure answers the question, not just
369
+ # WiringError.
690
370
  #
691
- # `deep: true` goes past wiring into logic — `Bluebook::ModelCheck`,
692
- # the lightweight-formal-methods leg (dead lifecycle transitions, a
693
- # saga state no handler chain reaches, a dispatch to nowhere). Opt-in
694
- # and separate from the base check on purpose: a wiring defect is
695
- # "this cannot run at all", a model-check finding is "this runs, but
696
- # part of it can never fire" — two different questions, and the
697
- # first is far cheaper to ask on every boot.
698
- #
699
- # Any boot failure answers the question, not only `WiringError` — a
700
- # domain path with no `.hecksagon`, a malformed bluebook, is just as
701
- # much "not valid" as a real wiring mismatch, and this tool exists
702
- # precisely so none of those ever cross a projection of this bus as
703
- # a crash.
704
- #
705
- # @param domain [String] the domain's directory path, relative to `BOOT_ROOT`
706
- # @param deep [Boolean] also run `Bluebook::ModelCheck` past wiring, into logic
707
- # @return [Hash{Symbol => Object}] `{ok: true, domain:, valid: true}`, plus
708
- # `:findings` (each `{kind:, severity:, subject:, message:}`) when `deep`; or
709
- # `{ok: false, domain:, valid: false, error:}` if the boot itself failed
371
+ # @param domain [String] the domain's directory path, relative to BOOT_ROOT
372
+ # @param deep [Boolean] also run Bluebook::ModelCheck past wiring, into logic
373
+ # @return [Hash] {ok: true, domain:, valid: true} plus :findings when deep;
374
+ # {ok: false, domain:, valid: false, error:} if the boot itself failed
710
375
  def validate(domain:, deep: false)
711
376
  runtime = Hecks.boot(confine!(domain, "domain"), install_facade: false)
712
377
  result = { ok: true, domain: domain, valid: true }
@@ -722,22 +387,12 @@ module Hecks
722
387
  { ok: false, domain: domain, valid: false, error: "#{e.class}: #{e.message}" }
723
388
  end
724
389
 
725
- # ── beyond the zoom levels ──────────────────────────────────────────
726
-
727
- # The full write history, not just the current head — `bin/history`'s
728
- # own logic, unchanged: every operation that ever touched the
729
- # aggregate, read off its repository's own `entries`. `Registry#repository`
730
- # always hands back a `Ports::Persistence::AppendOnly`-wrapped adapter,
731
- # never the bare adapter, so this never needs to guard against one —
732
- # what varies by adapter is only how much the wrapped `entries` itself
733
- # actually holds: a fresh Memory boot honestly has none yet, and a
734
- # durable adapter (Heki, Postgres, SQLite, D1) has whatever it
735
- # journaled.
390
+ # The full write history, not just the current head — every operation that
391
+ # ever touched each aggregate, off its repository's own append-only entries.
736
392
  #
737
- # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
738
- # @return [Hash{Symbol => Object}] `:ok`, `:domain` and `:history` (each
739
- # aggregate's storage name mapped to its `journal_entries`); or the shape
740
- # `refused` returns
393
+ # @param runtime [Runtime::Registry] the booted domain to read
394
+ # @return [Hash] :ok, :domain, :history (each aggregate's storage name
395
+ # mapped to its journal_entries); or the refused shape
741
396
  def history(runtime:)
742
397
  bluebook = bluebook_for(runtime)
743
398
  entries = bluebook.aggregates.each_with_object({}) do |aggregate, all|
@@ -750,30 +405,17 @@ module Hecks
750
405
  refused(e)
751
406
  end
752
407
 
753
- # One aggregate's full append-only write history, JSON-safe.
754
- #
755
- # @param repository [Ports::Persistence::AppendOnly] the aggregate's repository, as
756
- # `Registry#repository` always returns it
757
- # @return [Array<Hash>] each journal entry's `:operation`, `:id` and `:state`
758
- # (materialized); `[]` when the adapter has journaled nothing yet
408
+ # :nodoc:
759
409
  def journal_entries(repository)
760
410
  repository.entries.map { |entry| { operation: entry.operation, id: entry.id, state: Facade::JsonDoor.materialize(entry.state) } }
761
411
  end
762
412
 
763
- # `.behaviors` files, run and reported — hand-curated examples of how
764
- # to use a domain, in domain vocabulary (`docs/guides/behaviors.md`),
765
- # the survey's own "honest-refusal", generated-example-suite items.
766
- # `Hecks::Behaviors` boots each test fresh through `Hecks.boot_files`
767
- # itself — `target:` names a `.behaviors` file or a directory to
768
- # sweep, never a `runtime:`, the one other method here besides
769
- # `validate` that takes a path instead.
413
+ # Runs a domain's hand-curated .behaviors examples and reports the results.
770
414
  #
771
- # @param target [String] a `.behaviors` file's path, or a directory to sweep
772
- # for every `.behaviors` file under it
773
- # @return [Hash{Symbol => Object}] `:ok`, `:target`, `:files` (each file's own
774
- # `behaviors_file` shape) and `:counts` (`Behaviors.summarize`'s tally); or
775
- # the shape `refused` returns
776
- # @raise [Runtime::NotFound] if `target` is nil or names no real file or directory
415
+ # @param target [String] a .behaviors file's path, or a directory to sweep
416
+ # @return [Hash] :ok, :target, :files (each file's behaviors_file shape),
417
+ # :counts; or the refused shape
418
+ # @raise [Runtime::NotFound] if target is nil or names no real file or directory
777
419
  def behaviors(target:)
778
420
  require_relative "behaviors"
779
421
  raise Runtime::NotFound, "no such file or directory: #{target.inspect}" unless target && File.exist?(target)
@@ -789,31 +431,20 @@ module Hecks
789
431
  refused(e)
790
432
  end
791
433
 
792
- # Shapes one `.behaviors` file's own result for the bus.
793
- #
794
- # @param result [Behaviors::FileResult] one file's run result
795
- # @return [Hash{Symbol => Object}] `:path`, `:parse_error`, and `:runs` (each
796
- # test's `description`/`status`/`message`)
434
+ # :nodoc:
797
435
  def behaviors_file(result)
798
436
  { path: result.path,
799
437
  parse_error: result.parse_error,
800
438
  runs: Array(result.runs).map { |run| { description: run.description, status: run.status, message: run.message } } }
801
439
  end
802
440
 
803
- # A live tail without a live process — `bin/hecks_mcp_door` (its
804
- # transport of MCP-over-stdio) answers one request at a time, no push
805
- # channel to a client that only ever asks. This is the honest version
806
- # of the survey's `storehouse follow` for that shape: not a
807
- # subscription, a durable JSONL log every `dispatch`/`query`/`state`/
808
- # dry-run call appends to (`record!`), tailed back here. Still real,
809
- # still cross-process — the log outlives any one door's own process —
810
- # just pull instead of push.
441
+ # Tails this bus's own dispatch/query/state audit log — a pull-based
442
+ # substitute for a push subscription, since a stdio door answers one
443
+ # request at a time with no channel to push through.
811
444
  #
812
- # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
813
- # @param limit [Integer, #to_i] how many of the most recent log entries to return;
814
- # clamped to at least 1
815
- # @return [Hash{Symbol => Object}] `:ok`, `:domain` and `:entries` (each logged
816
- # call's own JSON entry); or the shape `refused` returns
445
+ # @param runtime [Runtime::Registry] the booted domain to tail
446
+ # @param limit [Integer, #to_i] how many recent log entries to return
447
+ # @return [Hash] :ok, :domain, :entries; or the refused shape
817
448
  def follow(runtime:, limit: 20)
818
449
  bluebook = bluebook_for(runtime)
819
450
  entries = log_lines(bluebook.name).last([limit.to_i, 1].max)
@@ -823,11 +454,7 @@ module Hecks
823
454
  refused(e)
824
455
  end
825
456
 
826
- # Reads back one domain's whole audit log.
827
- #
828
- # @param domain_name [String, Symbol, #to_s] the domain name
829
- # @return [Array<Hash>] every logged entry, parsed with Symbol keys, in log
830
- # order; `[]` if the domain has no log file yet
457
+ # :nodoc:
831
458
  def log_lines(domain_name)
832
459
  path = log_path(domain_name)
833
460
  return [] unless File.exist?(path)
@@ -835,32 +462,18 @@ module Hecks
835
462
  File.readlines(path).map { |line| JSON.parse(line, symbolize_names: true) }
836
463
  end
837
464
 
838
- # What actually happened, with payloads — distinct from `state`
839
- # (what's stored now) and `history` (append-only operation
840
- # snapshots, no payload). Not a domain-wide event-sourcing replay:
841
- # "one boot per call" means `runtime.events` is always empty except
842
- # during the very call that populated it, discarded the moment that
843
- # call returns — there is no cross-call in-memory log to read here.
844
- # So this reads the same durable audit log `follow` already tails
845
- # (`record!` now stamps a successful dispatch's own announced
846
- # events onto its log line), reshaped: `follow` answers "what was
847
- # called, in order, across every tool"; this answers "what happened
848
- # to one aggregate/record" — events this bus witnessed, which is
849
- # every real dispatch ever routed through it, but no more than that.
850
- # `aggregate:` narrows to one aggregate; `id:` (requires
851
- # `aggregate:` — an id alone is not unique across aggregates)
852
- # narrows to one record's own events.
465
+ # What actually happened, with payloads, to one aggregate or record — read
466
+ # off this bus's own audit log (a dispatch's announced events), not a full
467
+ # event-sourcing replay. aggregate: narrows the search; id: (requires
468
+ # aggregate:) narrows further to one record.
853
469
  #
854
- # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
855
- # @param aggregate [String, Symbol, nil] narrows to one aggregate's own events
856
- # @param id [String, Object, nil] narrows to one record's own events; requires
857
- # `aggregate`
858
- # @param limit [Integer, #to_i, nil] how many of the most recent matching events
859
- # to return; nil returns every one found
860
- # @return [Hash{Symbol => Object}] `:ok`, `:domain` and `:events` (each with
861
- # `:name`, `:payload`, `:time`, `:verb` and `:id`); or the shape `refused` returns
862
- # @raise [Runtime::TypeMismatch] if `id` is given without `aggregate`
863
- # @raise [Runtime::NotFound] if `aggregate` names no known aggregate
470
+ # @param runtime [Runtime::Registry] the booted domain to search
471
+ # @param aggregate [String, Symbol, nil] narrows the search to one aggregate
472
+ # @param id [String, Object, nil] narrows further to one record; requires aggregate
473
+ # @param limit [Integer, #to_i, nil] how many recent matching events to
474
+ # return; nil returns every one found
475
+ # @return [Hash] :ok, :domain, :events; or the refused shape
476
+ # @raise [Runtime::TypeMismatch] if id is given without aggregate
864
477
  def events(runtime:, aggregate: nil, id: nil, limit: nil)
865
478
  raise Runtime::TypeMismatch, "id: requires aggregate: too — an id alone is not unique across aggregates" if id && !aggregate
866
479
 
@@ -875,20 +488,7 @@ module Hecks
875
488
  refused(e)
876
489
  end
877
490
 
878
- # One log line's own contribution to `events`, above — nil unless it
879
- # was a successful dispatch carrying events, matching `fqn` (when an
880
- # aggregate narrowed the search) and `id` (when a record did). A
881
- # self-contained per-entry check with nothing to share with its
882
- # neighbors, extracted only to keep `events` itself to the query's
883
- # own shape: build the filter, apply the limit, wrap the result.
884
- #
885
- # @param entry [Hash{Symbol => Object}] one logged audit entry
886
- # @param fqn [String, nil] narrows to a dispatch whose verb starts with this
887
- # aggregate FQN; nil matches any aggregate
888
- # @param id [String, Object, nil] narrows to a dispatch whose settled record has
889
- # this id; nil matches any record
890
- # @return [Array<Hash>, nil] `entry`'s own events (each with `:time`, `:verb` and
891
- # `:id` merged in), or nil if `entry` isn't a matching successful dispatch
491
+ # :nodoc:
892
492
  def entry_events(entry, fqn, id)
893
493
  return unless entry[:tool] == "dispatch" && entry[:ok] && entry[:events]
894
494
  return if fqn && !entry[:verb].to_s.start_with?("#{fqn}.")
@@ -897,39 +497,18 @@ module Hecks
897
497
  entry[:events].map { |event| event.merge(time: entry[:time], verb: entry[:verb], id: entry[:id]) }
898
498
  end
899
499
 
900
- # ── shared shape ────────────────────────────────────────────────────
901
-
902
- # `Runtime::WiringError` belongs here too, alongside the true domain
903
- # refusals — not because it is one (it's a structural defect, not a
904
- # rule the caller broke), but because "this domain isn't wired to
905
- # answer what you're asking" (a `role:`+`actor_id:` caller reaching
906
- # an authorization port nothing implements, a dry run against a
907
- # port verb) is exactly the shape this bus promises never crashes
908
- # through it. `dry_run_outcome` already treats it this way locally;
909
- # this makes every other caller of `refusal_classes` do the same.
910
- #
911
- # @return [Array<Class>] every exception class this bus catches and reshapes
912
- # into an honest refusal rather than letting crash through
500
+ # Runtime::WiringError is included alongside the true domain refusals: a
501
+ # bus caller sees an honest refusal here too, not a crash, even though a
502
+ # wiring defect isn't a rule the caller broke.
503
+ # :nodoc:
913
504
  def refusal_classes = [Runtime::NotFound, Runtime::TypeMismatch, Runtime::WiringError, *Runtime::DOMAIN_REFUSALS]
914
505
 
915
- # Builds a successful outcome Hash.
916
- #
917
- # @param fields [Hash{Symbol => Object}] fields to merge in alongside `ok: true`
918
- # @return [Hash{Symbol => Object}] `{ok: true}` merged with `fields`
506
+ # :nodoc:
919
507
  def ok(**fields) = { ok: true }.merge(fields)
920
508
 
921
- # Builds a refused outcome Hash from a caught exception.
922
- #
923
- # **An honest refusal, not a crash** — the survey's own item #9: "an
924
- # explicit, structured refusal a caller can act on" rather than a
925
- # stack trace an agent has to parse to find the one line that
926
- # mattered. The domain's own refusal text travels verbatim
927
- # (`RefusalWording` already renders every one of these to be read),
928
- # this only wraps it consistently.
929
- #
930
- # @param error [StandardError] the caught exception, a member of `refusal_classes`
931
- # @param summary [String, nil] the caller's one-line summary
932
- # @return [Hash{Symbol => Object}] `{ok: false, summary:, error:}`
509
+ # The domain's own refusal text travels verbatim; this only wraps it
510
+ # consistently, rather than letting a stack trace reach the caller.
511
+ # :nodoc:
933
512
  def refused(error, summary: nil) = { ok: false, summary: summary, error: error.message }
934
513
  end
935
514
  end